crxpaydocs

Webhooks

Server-side events for subscription lifecycle, coupons, and install tracking.

Your backend can subscribe to the same lifecycle events the SDK fires on-device. Useful when your extension needs to keep a server-side source of truth — license servers, team-plan seat management, or ops dashboards.

Setup

  1. Dashboard → your extension → Outgoing webhooks
  2. Add an endpoint URL (HTTPS)
  3. Copy the signing secret — you'll need it to verify each request

crxpay retries failed deliveries with exponential backoff (up to 24h). Webhook handlers should be idempotent — use the event id field as your dedup key.

Verification

Every webhook request carries an X-CrxPay-Signature header: t=<unix-ts>,v1=<hmac-sha256>. Verify using the helper from @crxpay/sdk/server:

import { verifyWebhook } from '@crxpay/sdk/server';

export async function POST(req: Request) {
  const event = await verifyWebhook(req, process.env.CRXPAY_SECRET!);
  // event is { id, type, data, timestamp } — already validated
  await handle(event);
  return Response.json({ ok: true });
}

verifyWebhook throws if the signature is invalid or the timestamp is older than 5 minutes. Return 2xx to ack; any non-2xx triggers a retry.

Event types

Subscription lifecycle

TypeFires when
subscription.createdNew subscription (checkout completed or trial started)
subscription.updatedPlan change, seat change, metadata update
subscription.cancelledExplicit cancel (end of period or immediate)
subscription.expiredStripe deleted the sub (trial ended without conversion, or final cancel after period)
subscription.paused / subscription.resumedStripe pause collection toggled
subscription.trial_endingFires 3 days before trial_end — prompt to add a card

Dunning

TypeFires when
subscription.payment_failedAn invoice failed to charge
subscription.grace_startedGrace window opened — user still entitled until data.graceUntil
subscription.payment_recoveredA retry succeeded and the invoice was paid
subscription.recoveredGrace cleared, subscription back to active

Coupons

TypeFires when
coupon.redeemedFirst time this customer used a given coupon (idempotent per customer+sub)

Customer lifecycle

TypeFires when
customer.installedExtension installed (fires once per install)
customer.identifiedCrxPay.identify(...) attached an email / externalUserId
customer.uninstalledChrome hit the uninstall URL

Event payload shape

{
  "id": "evt_01HXYZ…",
  "type": "subscription.created",
  "timestamp": 1708300000000,
  "data": {
    "customerId": "cus_crxpay_…",
    "subscriptionId": "sub_crxpay_…",
    "processorSubscriptionId": "sub_1PabcStripe",
    "mode": "live",
    "status": "active",
    "priceId": "price_…",
    "currentPeriodEnd": "2026-05-21T00:00:00.000Z"
  }
}

data shape varies by event type. All events carry customerId so your handler can join to your own user table.

Retries & idempotency

  • Non-2xx responses (including timeouts over 10s) are retried
  • Retry schedule: 1m, 5m, 15m, 1h, 6h, 24h (then dead-letter)
  • The same event id is delivered on every retry — store it and skip duplicates
  • Events are not strictly ordered across types; don't assume subscription.created arrives before the first coupon.redeemed

Was this page helpful?

Your feedback shapes what we document next.