Guide to Recurly Webhooks: Features and Best Practices
Recurly webhooks notify your application about subscription and billing activity: a subscription is created, renewed, or canceled, a payment succeeds or fails, an invoice is issued. If you're building on Recurly, webhooks are how you react to billing events without polling.
This guide covers how Recurly webhooks work, the events you'll handle, how to verify the recurly-signature, its layered auth, and the best practices for production.
What are Recurly webhooks?
Recurly webhooks are HTTP POSTs. For JSON endpoints, Recurly signs each delivery with a recurly-signature header: a Unix timestamp (in milliseconds) followed by one or more comma-separated HMAC-SHA256 signatures. The signature is a hex HMAC-SHA256 over <timestamp> + "." + raw_body, keyed with the endpoint's secret. During a 24-hour key rotation the header carries multiple signatures, and any one matching is valid. Legacy XML payloads are not signed, so choose JSON to get signatures. Recurly can also layer HTTP Basic Auth and an IP allowlist on top.
Recurly webhook features
| Feature | Details |
|---|---|
| Configuration | Admin UI: Integrations > Webhooks (up to 10 notification types per endpoint) |
| Signature header | recurly-signature (JSON only): <ms-timestamp>,<sig>[,<sig>...] |
| Signature scheme | Hex HMAC-SHA256 over <timestamp>.<raw_body>, keyed with the endpoint secret |
| Key rotation | 24h overlap; multiple signatures, any match is valid |
| Additional layers | HTTP Basic Auth + IP allowlist |
| XML | Legacy XML payloads are unsigned; secure with Basic Auth + IP allowlist |
| SDK | None for verification (the recurly SDK is an API client); verify manually |
Common events
Recurly notification types are named *_notification. A representative set:
| Event | Fires when |
|---|---|
new_subscription_notification | A subscription is created |
updated_subscription_notification / canceled_subscription_notification | A subscription changes or cancels |
renewed_subscription_notification / expired_subscription_notification | A subscription renews or expires |
successful_payment_notification / failed_payment_notification | A payment succeeds or fails |
successful_refund_notification / void_payment_notification | A refund or void occurs |
new_invoice_notification / past_due_invoice_notification | An invoice is issued or goes past due |
new_account_notification / billing_info_updated_notification | An account or billing info changes |
In the classic JSON format the notification type is the single top-level key wrapping the related objects, so route via Object.keys(notification)[0]. Note that one real-world event can produce several notifications (a new paid subscription emits both new_subscription_notification and successful_payment_notification).
Setting up Recurly webhooks
Recurly webhooks are configured in the Admin UI (not the API): Integrations > Webhooks > Add Endpoint. Choose the JSON payload format (recommended, signed) rather than legacy XML, set an HTTPS URL, and select up to 10 notification types. Recurly generates a secret key on the Webhook Endpoints page, store it as RECURLY_WEBHOOK_SECRET. Optionally set HTTP Basic Auth credentials (RECURLY_WEBHOOK_USER / RECURLY_WEBHOOK_PASSWORD) and restrict to Recurly's published IP ranges.
Securing Recurly webhooks
Split the recurly-signature on commas: the first element is the timestamp, the rest are signatures. Compute a hex HMAC-SHA256 over <timestamp> + "." + raw_body with your secret, and accept if it matches any signature in the header (constant-time). Verify against the raw body before parsing. If you use Basic Auth, check it too.
const crypto = require("crypto");
const SECRET = process.env.RECURLY_WEBHOOK_SECRET;
function verify(rawBody, signatureHeader) {
if (!signatureHeader) return false;
const [timestamp, ...signatures] = signatureHeader.split(",");
if (!timestamp || signatures.length === 0) return false;
const expected = crypto
.createHmac("sha256", SECRET)
.update(`${timestamp}.`)
.update(rawBody)
.digest("hex");
const expectedBuf = Buffer.from(expected);
// Multiple signatures appear during 24h key rotation; any match is valid.
return signatures.some((sig) => {
const sigBuf = Buffer.from(sig.trim());
return sigBuf.length === expectedBuf.length && crypto.timingSafeEqual(sigBuf, expectedBuf);
});
}
app.post("/webhooks/recurly", express.raw({ type: "*/*" }), (req, res) => {
if (!verify(req.body, req.headers["recurly-signature"])) return res.sendStatus(401);
res.sendStatus(200); // acknowledge fast
const notification = JSON.parse(req.body.toString("utf8"));
processQueue.add(notification); // route on Object.keys(notification)[0], async
});
The same check in Python:
import hashlib
import hmac
import os
SECRET = os.environ["RECURLY_WEBHOOK_SECRET"].encode()
def verify(raw_body: bytes, header: str) -> bool:
if not header:
return False
parts = header.split(",")
timestamp, signatures = parts[0], parts[1:]
if not timestamp or not signatures:
return False
message = timestamp.encode() + b"." + raw_body
expected = hmac.new(SECRET, message, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, sig.strip()) for sig in signatures)
Recurly webhook limitations and pain points
JSON is signed, XML is not
The Problem: Only JSON endpoints get a recurly-signature. Legacy XML endpoints have no signature at all, so there's nothing to verify cryptographically.
Why It Happens: Recurly added signing for JSON; XML predates it.
Workarounds:
- Choose JSON to get signatures; for XML, rely on HTTP Basic Auth and the IP allowlist.
How Hookdeck Can Help: Hookdeck can verify JSON signatures and apply access controls at the edge, so verification isn't tied to payload format.
The timestamp is in milliseconds and part of the signed string
The Problem: The signed message is <timestamp>.<raw_body> with the timestamp in milliseconds. Stripping it, reformatting it, or assuming seconds breaks verification.
Why It Happens: Recurly signs the exact timestamp string in milliseconds.
Workarounds:
- Use the timestamp exactly as sent, and treat it as milliseconds for any freshness check.
How Hookdeck Can Help: Hookdeck reconstructs and verifies the signed message at the edge.
Multiple signatures during rotation
The Problem: When you regenerate the secret, the old key stays valid for 24 hours and the header carries multiple signatures. Checking only the first causes intermittent failures right after rotating.
Why It Happens: Recurly overlaps old and new keys for 24 hours.
Workarounds:
- Match against all signatures and accept if any is valid.
How Hookdeck Can Help: Hookdeck handles rotation at the edge.
Out-of-order delivery and duplicates
The Problem: Recurly may deliver notifications out of order and more than once, and a single event can emit several notifications.
Why It Happens: At-least-once delivery with up to 10 retries.
Workarounds:
- Handle idempotently, and confirm state via the Recurly API before acting on ambiguous sequences.
How Hookdeck Can Help: Hookdeck deduplicates and durably queues events at the edge. See our guide to webhook idempotency.
Best practices
Use JSON and verify the signature
Choose JSON endpoints, compute the hex HMAC-SHA256 over <timestamp>.<raw_body>, and match any signature in constant time.
Layer Basic Auth and an IP allowlist
Add HTTP Basic Auth and restrict to Recurly's IP ranges as defense in depth alongside the signature.
Handle rotation, ordering, and duplicates
Accept any matching signature, reconcile via the API, and make handlers idempotent.
Acknowledge fast, process asynchronously
Return 200 quickly and defer work to a queue. See why to process webhooks asynchronously.
Make Recurly webhooks production-ready
Hookdeck verifies recurly-signature, deduplicates, and durably queues every billing event
Conclusion
Recurly signs JSON webhooks with a recurly-signature header: a millisecond timestamp plus one or more hex HMAC-SHA256 signatures over <timestamp>.<raw_body>, with any matching during 24-hour key rotation. Legacy XML is unsigned. Choose JSON, verify over the raw body, layer Basic Auth and an IP allowlist, route on the top-level notification key, and handle out-of-order duplicates idempotently.
Hookdeck verifies the signature, deduplicates, and durably queues every event at the edge, so your app only ever processes verified, unique events.
Get started with Hookdeck for free and handle Recurly webhooks reliably in minutes.