What does the RevenueDot webhook event EXPIRATION mean?
The RevenueDot webhook event EXPIRATION means a customer's access has ended.
Quick facts#
| Event | EXPIRATION |
| Where | RevenueDot webhook (event.type), RevenueCat's webhook format |
| Sent to | Every enabled webhook whose filters match |
| Fields that are specific to it | expiration_reason |
| What it means | Access ended: the period ran out, billing retry gave up or the subscription paused. |
When it is sent#
- The paid period, and any grace period, passed with no renewal.
expiration_reasonsays why:UNSUBSCRIBE,BILLING_ERROR,DEVELOPER_INITIATED,PRICE_INCREASE,CUSTOMER_SUPPORT,UNKNOWNorSUBSCRIPTION_PAUSED.- It is recorded once per period by a scheduled job that runs every minute, so it can arrive up to a minute after the expiry time.
What your server should do#
- Remove the entitlements of the subscription.
- Use
expiration_reasonto choose the win-back message: a billing error needs a payment prompt. - Keep the customer's data.
- Deduplicate on
event.id: deliveries can repeat, because anything but HTTP 200 is retried after 5, 10, 20, 40 and 80 minutes.
Example#
JavaScript
import { createHmac, timingSafeEqual } from "node:crypto";
// X-RevenueCat-Webhook-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">
function verify(rawBody, header, secret, toleranceSeconds = 300) {
const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header ?? "");
if (!m || Math.abs(Date.now() / 1000 - Number(m[1])) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret).update(`${m[1]}.${rawBody}`).digest();
return timingSafeEqual(expected, Buffer.from(m[2], "hex"));
}
// Sign a sample delivery the way RevenueDot does, then handle it.
const secret = "whsec_test_secret";
const rawBody = JSON.stringify({ api_version: "1.0", event: {"id":"66339910-3BFF-49F4-B873-D1283D673DE2","type":"EXPIRATION","app_user_id":"user_1","product_id":"pro_monthly","expiration_reason":"BILLING_ERROR"} });
const t = Math.floor(Date.now() / 1000);
const header = `t=${t},v1=${createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex")}`;
const seen = new Set(); // use a database table in production
function handle(rawBody, header) {
if (!verify(rawBody, header, secret)) return "401 bad signature";
const { event } = JSON.parse(rawBody);
if (seen.has(event.id)) return "200 duplicate";
seen.add(event.id);
if (event.type !== "EXPIRATION") return "200 ignored";
return event.expiration_reason === "BILLING_ERROR" ? "remove access, ask for a new payment method" : "remove access, start win-back";
}
console.log(handle(rawBody, header));Run-checked: npm run check:snippets runs this snippet with Node and compares its output with ask for a new payment method (checked 2026-10-03).
How RevenueDot produces it#
A scheduled job (services/tick.ts) records EXPIRATION for every subscription whose access, grace period included, has ended, and claims each one once so replicas do not send it twice. The reason comes from the stored state: paused, then a billing issue, then the stored cancel reason, else UNSUBSCRIBE.
Related#
- What does the RevenueDot webhook event CANCELLATION mean?
- What does the RevenueDot webhook event BILLING_ISSUE mean?
- What does the RevenueDot webhook event SUBSCRIPTION_PAUSED mean?
- EXPIRATION fields and a recorded example payload
- Webhooks guide: set up, verify and test
Source#
- RevenueDot docs: which webhook events RevenueDot sends
- RevenueCat: webhook event types and fields (the format RevenueDot follows)
- RevenueDot core: events.ts (the event types and the rules that derive them)
- RevenueDot server: services/tick.ts (the scheduled EXPIRATION)
- RevenueDot server: services/events.ts (how an event is recorded and delivered)
Checked: 2026-10-03