Agent skill
RevenueCat Webhooks Skill
Receive and verify RevenueCat webhooks. Use when setting up RevenueCat webhook handlers, verifying the opt-in X-RevenueCat-Webhook-Signature HMAC (t=<timestamp>,v1=<hex> over "<t>.<raw body>") or the static Authorization header, debugging signature verification, or handling in-app subscription events like INITIAL_PURCHASE, RENEWAL, CANCELLATION, EXPIRATION, BILLING_ISSUE, PRODUCT_CHANGE or TEST.
Install this skill
npx skills add hookdeck/webhook-skills --skill revenuecat-webhooks
When to Use This Skill
- How do I receive RevenueCat webhooks?
- How do I verify RevenueCat webhook signatures (
X-RevenueCat-Webhook-Signature)? - How do I check the RevenueCat webhook Authorization header?
- Why is my RevenueCat webhook signature verification failing?
- Handling subscription events like
INITIAL_PURCHASE,RENEWAL,CANCELLATION,EXPIRATIONorBILLING_ISSUE
Verification (core)
Caveat: HMAC signing is opt-in. It's only sent if you toggle HMAC webhook signing on the integration (the secret is shown once, at creation or rotation). Otherwise RevenueCat sends only the static
Authorizationheader value you configured in the dashboard, and you must compare it to the expected value in constant time.
With signing on, every delivery carries:
X-RevenueCat-Webhook-Signature: t=<unix_timestamp>,v1=<hmac_sha256_hex>
v1 is HMAC-SHA256, hex encoded, over "<t>.<raw body>", keyed with the signing secret as a UTF-8 string. Verify the raw body bytes before parsing JSON, compare in constant time, and reject t more than 300 s from now (RevenueCat re-signs every retry, so t is always fresh).
const crypto = require('crypto');
// Signature (when REVENUECAT_WEBHOOK_SECRET is set)
const { t, v1 } = Object.fromEntries(
(req.get('x-revenuecat-webhook-signature') || '').split(',').map((p) => {
const i = p.indexOf('=');
return [p.slice(0, i).trim(), p.slice(i + 1).trim()];
})
);
const expected = crypto.createHmac('sha256', process.env.REVENUECAT_WEBHOOK_SECRET)
.update(`${t}.`).update(rawBody).digest('hex');
const signatureOk = /^[0-9]+$/.test(t || '') && v1?.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected)) &&
Math.abs(Date.now() / 1000 - Number(t)) <= 300;
// Authorization header (when REVENUECAT_WEBHOOK_AUTH is set): hash so lengths match
const h = (s) => crypto.createHash('sha256').update(s).digest();
const auth = req.get('authorization');
const authOk = !!auth && crypto.timingSafeEqual(h(auth), h(process.env.REVENUECAT_WEBHOOK_AUTH));
Return 401 on failure. On success return exactly 200: RevenueCat treats any other status as a failure and retries (up to 5 times over ~80 minutes).
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Common Event Types
Body shape: { "api_.
Event (event.type) | Description |
|---|---|
TEST | Sent by Send test event in the dashboard |
INITIAL_PURCHASE | New subscription purchased |
RENEWAL | Subscription renewed, or a lapsed user resubscribed |
CANCELLATION | Canceled or refunded (see cancel_reason); access continues until EXPIRATION |
EXPIRATION | Subscription expired: revoke access (see expiration_reason) |
BILLING_ISSUE | Charge failed; not yet expired |
PRODUCT_CHANGE | Subscriber changed product (new_product_id); may not be in effect yet |
Also: UNCANCELLATION, NON_RENEWING_PURCHASE, SUBSCRIPTION_PAUSED, SUBSCRIPTION_EXTENDED, REFUND_REVERSED, INVOICE_ISSUANCE, TRANSFER, TEMPORARY_, VIRTUAL_, EXPERIMENT_ENROLLMENT, PURCHASE_REDEEMED, PRICE_, PRICE_. New types can appear without an api_version change, so return 200 for unknown ones.
For the full event reference, see references/overview.md and RevenueCat's Event types and fields.
Delivery and Idempotency
- At least once delivery: deduplicate on
event.id(retries reuse it andevent_timestamp_ms). - Respond within 60 s (RevenueCat disconnects after that); store the event, return
200, then process asynchronously. - RevenueCat recommends calling
GET /subscribers/{app_after any webhook and syncing from that, rather than per-event logic.user_ id}
Environment Variables
REVENUECAT_WEBHOOK_SECRET=your_revenuecat_signing_secret # HMAC signing secret (if signing is enabled)
REVENUECAT_WEBHOOK_AUTH="Bearer your_authorization_header_value" # Exact Authorization value (if set)
Set at least one; the examples enforce every check that's configured.
Local Development
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 revenuecat --path /webhooks/revenuecat
Use the printed URL as the integration's webhook URL, then click Send test event.
Reference Materials
- references/overview.md - Event types, payload fields, delivery and retry behaviour
- references/setup.md - Dashboard configuration, Authorization header, enabling HMAC signing
- references/verification.md - Signature and Authorization checks, timestamp tolerance, gotchas