# Recurly Webhooks

## When to Use This Skill

* How do I receive Recurly webhooks?
* How do I verify the Recurly `recurly-signature` header?
* How do I secure a Recurly webhook endpoint with HTTP Basic Auth?
* How do I handle `new_subscription_notification` or `successful_payment_notification`?
* Why is my Recurly webhook signature verification failing?

## How Recurly Secures Webhooks

Recurly webhooks are secured with up to three layers. Use all that apply:

1. Signature (JSON payloads only). Recurly signs JSON notifications and sends
  the signature in a `recurly-signature` header. This is the primary integrity
  check — always verify it. (Legacy XML payloads are not signed.)
2. HTTP Basic Auth. Configure a username/password on the endpoint in Recurly;
  Recurly sends them in the `Authorization` header on every request.
3. IP allowlist. Accept requests only from Recurly's published IP ranges.

Prefer the JSON payload format so you get signature verification. The
official Recurly SDK (`recurly`) is an API client and does not ship a webhook
verification helper, so verify the signature manually with HMAC-SHA256.

## Verification (core)

The `recurly-signature` header is a Unix timestamp (ms) followed by one or more
hex HMAC-SHA256 signatures, comma-separated:

```
recurly-signature: 1659641851000,a8c8524a0cdd99e3...,cafe677a2e6fa177...

```

Signed message is ``${timestamp}.${rawBody}`` (the raw, unparsed body).
Multiple signatures appear during a 24h key rotation; the notification is valid
if any one matches. Use a constant-time compare.

```javascript
const crypto = require('crypto');

function verifyRecurlySignature(rawBody, header, secret) {
  if (!header || !secret) return false;
  const [timestamp, ...signatures] = header.split(',');
  if (!timestamp || signatures.length === 0) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.`)
    .update(rawBody)                 // Buffer/string of the raw request body
    .digest('hex');

  const expBuf = Buffer.from(expected);
  return signatures.some((sig) => {
    const sigBuf = Buffer.from(sig.trim());
    return sigBuf.length === expBuf.length && crypto.timingSafeEqual(sigBuf, expBuf);
  });
}

```

> For complete handlers with Basic Auth, event dispatch, and tests, see:
> 
> * [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/recurly-webhooks/examples/express/)
> * [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/recurly-webhooks/examples/nextjs/)
> * [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/recurly-webhooks/examples/fastapi/)

## Parsing the Notification

Classic Recurly JSON webhooks use the notification type as the single
top-level key, wrapping the related objects:

```json
{
  "new_subscription_notification": {
    "account": { "account_code": "1", "email": "verena@example.com" },
    "subscription": { "uuid": "8435b96eb70e5640a0eaf82d0e0d6d", "state": "active" }
  }
}

```

Read the notification type from the top-level key, then dispatch:

```javascript
const notification = JSON.parse(rawBody);
const type = Object.keys(notification)[0];      // e.g. "new_subscription_notification"
const data = notification[type];                 // { account, subscription | transaction, ... }

```

> Confirm state via the API before acting. Webhooks can arrive out of order.
> For critical flows, fetch the referenced object with the `recurly` SDK to
> confirm its current state (e.g. `client.getSubscription('uuid-' + data.subscription.uuid)`).

## Common Notification Types

| Notification | Triggered When |
| --- | --- |
| `new_subscription_notification` | A subscription is created |
| `updated_subscription_notification` | A subscription is upgraded, downgraded, or changed |
| `canceled_subscription_notification` | A subscription is canceled |
| `expired_subscription_notification` | A subscription expires |
| `renewed_subscription_notification` | A subscription renews for a new term |
| `successful_payment_notification` | A payment succeeds |
| `failed_payment_notification` | A payment is declined |
| `new_account_notification` | A new account is created |

> For the full list, see [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/recurly-webhooks/references/overview.md) and
> [Recurly's Webhooks reference](https://docs.recurly.com/recurly-subscriptions/docs/overview-webhooks).

## Environment Variables

```bash
RECURLY_WEBHOOK_SECRET=xxxxx        # Endpoint secret key (Webhook Endpoints page) — signs JSON
RECURLY_WEBHOOK_USER=xxxxx          # Optional: HTTP Basic Auth username configured in Recurly
RECURLY_WEBHOOK_PASSWORD=xxxxx      # Optional: HTTP Basic Auth password configured in Recurly
RECURLY_API_KEY=xxxxx               # Optional: API key to confirm object state via the SDK

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 recurly --path /webhooks/recurly

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/recurly-webhooks/references/overview.md) - What Recurly webhooks are, notification types, payload structure
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/recurly-webhooks/references/setup.md) - Configure endpoints, get the secret key, Basic Auth, IP allowlist
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/recurly-webhooks/references/verification.md) - Signature verification details and gotchas