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, EXPIRATION or BILLING_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 Authorization header 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_version": "1.0", "event": { "id", "type", "app_user_id", "product_id", "environment", ... } }.

Event (event.type)Description
TESTSent by Send test event in the dashboard
INITIAL_PURCHASENew subscription purchased
RENEWALSubscription renewed, or a lapsed user resubscribed
CANCELLATIONCanceled or refunded (see cancel_reason); access continues until EXPIRATION
EXPIRATIONSubscription expired: revoke access (see expiration_reason)
BILLING_ISSUECharge failed; not yet expired
PRODUCT_CHANGESubscriber 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_ENTITLEMENT_GRANT, VIRTUAL_CURRENCY_TRANSACTION, EXPERIMENT_ENROLLMENT, PURCHASE_REDEEMED, PRICE_INCREASE_CONSENT_REQUIRED, PRICE_INCREASE_CONSENT_APPROVED. 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 and event_timestamp_ms).
  • Respond within 60 s (RevenueCat disconnects after that); store the event, return 200, then process asynchronously.
  • RevenueCat recommends calling GET /subscribers/{app_user_id} after any webhook and syncing from that, rather than per-event logic.

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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Oct 28, 2026

View on GitHub →