$npx -y skills add hookdeck/webhook-skills --skill recurly-webhooksReceive and verify Recurly webhooks. Use when setting up Recurly webhook handlers, verifying the recurly-signature header (HMAC-SHA256), securing the endpoint with HTTP Basic Auth, or handling subscription and billing events like new_subscription_notification, updated_subscriptio
| 1 | # Recurly Webhooks |
| 2 | |
| 3 | ## When to Use This Skill |
| 4 | |
| 5 | - How do I receive Recurly webhooks? |
| 6 | - How do I verify the Recurly `recurly-signature` header? |
| 7 | - How do I secure a Recurly webhook endpoint with HTTP Basic Auth? |
| 8 | - How do I handle `new_subscription_notification` or `successful_payment_notification`? |
| 9 | - Why is my Recurly webhook signature verification failing? |
| 10 | |
| 11 | ## How Recurly Secures Webhooks |
| 12 | |
| 13 | Recurly webhooks are secured with up to three layers. Use all that apply: |
| 14 | |
| 15 | 1. **Signature (JSON payloads only).** Recurly signs JSON notifications and sends |
| 16 | the signature in a `recurly-signature` header. This is the primary integrity |
| 17 | check — always verify it. (Legacy **XML** payloads are **not** signed.) |
| 18 | 2. **HTTP Basic Auth.** Configure a username/password on the endpoint in Recurly; |
| 19 | Recurly sends them in the `Authorization` header on every request. |
| 20 | 3. **IP allowlist.** Accept requests only from Recurly's published IP ranges. |
| 21 | |
| 22 | Prefer the **JSON** payload format so you get signature verification. The |
| 23 | official Recurly SDK (`recurly`) is an API client and does **not** ship a webhook |
| 24 | verification helper, so verify the signature manually with HMAC-SHA256. |
| 25 | |
| 26 | ## Verification (core) |
| 27 | |
| 28 | The `recurly-signature` header is a Unix timestamp (ms) followed by one or more |
| 29 | **hex** HMAC-SHA256 signatures, comma-separated: |
| 30 | |
| 31 | ``` |
| 32 | recurly-signature: 1659641851000,a8c8524a0cdd99e3...,cafe677a2e6fa177... |
| 33 | ``` |
| 34 | |
| 35 | Signed message is `` `${timestamp}.${rawBody}` `` (the **raw**, unparsed body). |
| 36 | Multiple signatures appear during a 24h key rotation; the notification is valid |
| 37 | if **any one** matches. Use a constant-time compare. |
| 38 | |
| 39 | ```javascript |
| 40 | const crypto = require('crypto'); |
| 41 | |
| 42 | function verifyRecurlySignature(rawBody, header, secret) { |
| 43 | if (!header || !secret) return false; |
| 44 | const [timestamp, ...signatures] = header.split(','); |
| 45 | if (!timestamp || signatures.length === 0) return false; |
| 46 | |
| 47 | const expected = crypto |
| 48 | .createHmac('sha256', secret) |
| 49 | .update(`${timestamp}.`) |
| 50 | .update(rawBody) // Buffer/string of the raw request body |
| 51 | .digest('hex'); |
| 52 | |
| 53 | const expBuf = Buffer.from(expected); |
| 54 | return signatures.some((sig) => { |
| 55 | const sigBuf = Buffer.from(sig.trim()); |
| 56 | return sigBuf.length === expBuf.length && crypto.timingSafeEqual(sigBuf, expBuf); |
| 57 | }); |
| 58 | } |
| 59 | ``` |
| 60 | |
| 61 | > **For complete handlers with Basic Auth, event dispatch, and tests**, see: |
| 62 | > - [examples/express/](examples/express/) |
| 63 | > - [examples/nextjs/](examples/nextjs/) |
| 64 | > - [examples/fastapi/](examples/fastapi/) |
| 65 | |
| 66 | ## Parsing the Notification |
| 67 | |
| 68 | Classic Recurly JSON webhooks use the **notification type as the single |
| 69 | top-level key**, wrapping the related objects: |
| 70 | |
| 71 | ```json |
| 72 | { |
| 73 | "new_subscription_notification": { |
| 74 | "account": { "account_code": "1", "email": "verena@example.com" }, |
| 75 | "subscription": { "uuid": "8435b96eb70e5640a0eaf82d0e0d6d", "state": "active" } |
| 76 | } |
| 77 | } |
| 78 | ``` |
| 79 | |
| 80 | Read the notification type from the top-level key, then dispatch: |
| 81 | |
| 82 | ```javascript |
| 83 | const notification = JSON.parse(rawBody); |
| 84 | const type = Object.keys(notification)[0]; // e.g. "new_subscription_notification" |
| 85 | const data = notification[type]; // { account, subscription | transaction, ... } |
| 86 | ``` |
| 87 | |
| 88 | > **Confirm state via the API before acting.** Webhooks can arrive out of order. |
| 89 | > For critical flows, fetch the referenced object with the `recurly` SDK to |
| 90 | > confirm its current state (e.g. `client.getSubscription('uuid-' + data.subscription.uuid)`). |
| 91 | |
| 92 | ## Common Notification Types |
| 93 | |
| 94 | | Notification | Triggered When | |
| 95 | |--------------|----------------| |
| 96 | | `new_subscription_notification` | A subscription is created | |
| 97 | | `updated_subscription_notification` | A subscription is upgraded, downgraded, or changed | |
| 98 | | `canceled_subscription_notification` | A subscription is canceled | |
| 99 | | `expired_subscription_notification` | A subscription expires | |
| 100 | | `renewed_subscription_notification` | A subscription renews for a new term | |
| 101 | | `successful_payment_notification` | A payment succeeds | |
| 102 | | `failed_payment_notification` | A payment is declined | |
| 103 | | `new_account_notification` | A new account is created | |
| 104 | |
| 105 | > **For the full list**, see [references/overview.md](references/overview.md) and |
| 106 | > [Recurly's Webhooks reference](https://docs.recurly.com/recurly-subscriptions/docs/overview-webhooks). |
| 107 | |
| 108 | ## Environment Variables |
| 109 | |
| 110 | ```bash |
| 111 | RECURLY_WEBHOOK_SECRET=xxxxx # Endpoint secret key (Webhook Endpoints page) — signs JSON |
| 112 | RECURLY_WEBHOOK_USER=xxxxx # Optional: HTTP Basic Auth username configured |