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
- Dashboard → your extension → Outgoing webhooks
- Add an endpoint URL (HTTPS)
- 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
| Type | Fires when |
|---|---|
subscription.created | New subscription (checkout completed or trial started) |
subscription.updated | Plan change, seat change, metadata update |
subscription.cancelled | Explicit cancel (end of period or immediate) |
subscription.expired | Stripe deleted the sub (trial ended without conversion, or final cancel after period) |
subscription.paused / subscription.resumed | Stripe pause collection toggled |
subscription.trial_ending | Fires 3 days before trial_end — prompt to add a card |
Dunning
| Type | Fires when |
|---|---|
subscription.payment_failed | An invoice failed to charge |
subscription.grace_started | Grace window opened — user still entitled until data.graceUntil |
subscription.payment_recovered | A retry succeeded and the invoice was paid |
subscription.recovered | Grace cleared, subscription back to active |
Coupons
| Type | Fires when |
|---|---|
coupon.redeemed | First time this customer used a given coupon (idempotent per customer+sub) |
Customer lifecycle
| Type | Fires when |
|---|---|
customer.installed | Extension installed (fires once per install) |
customer.identified | CrxPay.identify(...) attached an email / externalUserId |
customer.uninstalled | Chrome 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
idis delivered on every retry — store it and skip duplicates - Events are not strictly ordered across types; don't assume
subscription.createdarrives before the firstcoupon.redeemed
Was this page helpful?
Your feedback shapes what we document next.