# 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).

```javascript
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/](https://github.com/hookdeck/webhook-skills/tree/main/skills/revenuecat-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/revenuecat-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/revenuecat-webhooks/examples/fastapi/).

## Common Event Types

Body shape: `{ "api_version": "1.0", "event": { "id", "type", "app_user_id", "product_id", "environment", ... } }`.

| 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_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](https://github.com/hookdeck/webhook-skills/blob/main/skills/revenuecat-webhooks/references/overview.md) and RevenueCat's [Event types and fields](https://www.revenuecat.com/docs/integrations/webhooks/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

```bash
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

```bash
# 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](https://github.com/hookdeck/webhook-skills/blob/main/skills/revenuecat-webhooks/references/overview.md) - Event types, payload fields, delivery and retry behaviour
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/revenuecat-webhooks/references/setup.md) - Dashboard configuration, Authorization header, enabling HMAC signing
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/revenuecat-webhooks/references/verification.md) - Signature and Authorization checks, timestamp tolerance, gotchas