Webhooks
Track Recurly Recover retry progress and confirm each invoice's final outcome with the four webhook events, and handle delivery securely and idempotently.
The four events
Recover fires these four events as an invoice moves through its retry schedule:
| Event | When it fires | How often |
new_dunning_event | An invoice enters or reaches a milestone in the dunning retry schedule. | Once per configured schedule milestone |
successful_payment | A retry transaction is created and collected by the gateway. | Once per invoice |
failed_payment | A retry transaction is created and declined by the gateway. | Can fire multiple times per invoice, depending on the retry window |
closed_invoice | A past-due invoice reaches a final state — either paid or the retry window is exhausted. | Once per invoice |
closed_invoice as the authoritative final outcome. Its state value reflects either collected/paid or failed/unpaid, and confirms that no further collection attempts will occur.Event sequences
Every invoice follows one of two paths. A new_dunning_event marks the schedule milestone, a payment event reports the outcome of that attempt, and closed_invoice records the final state.
Successful recovery
new_dunning_event → successful_payment → closed_invoice (state: collected/paid)
Exhausted retry window
new_dunning_event → failed_payment (one or more) → closed_invoice (state: failed/unpaid)
Correlate events with an invoice
Every event carries an invoice_id — a shared identifier that links all related invoices and their transactions across object types. Key your local records on invoice_id to match incoming events back to the invoice you submitted.
Handle webhooks safely
| Concern | How to handle it |
| Authenticity | Verify the recurly-signature header (HMAC-SHA256) on every JSON webhook using your endpoint's secret key. You can optionally add HTTP Basic Auth and IP allowlisting. |
| Duplicate deliveries | Recurly resends on delivery failure, so expect repeats. Dedupe on the recurly-notification-id header, which stays identical across retries of the same notification, and reply with a 2XX within 5 seconds so Recurly doesn't retry unnecessarily. |
| Out-of-order events | Never act on the payload alone. Use the event as a signal to call the API, compare the result against your local record, and update only when the API confirms a change. |
Verify the signature
Compute an HMAC-SHA256 of the raw request body with your endpoint's secret key and compare it, in constant time, to the recurly-signature header.
const crypto = require("crypto");
function verifyRecurlySignature(rawBody, signatureHeader, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signatureHeader)
);
}Test in the sandbox
Use Stripe test cards configured to trigger declines and successes. Testing both in your sandbox delivers the corresponding events to your configured endpoint.
| Stripe test card | Events delivered |
| Successful | new_dunning_event, successful_payment, closed_invoice |
| Declining | new_dunning_event, failed_payment — and closed_invoice once a shortened retry window reaches its final milestone and the invoice moves to a failed state |
What's next
- Recovery API reference — the complete endpoint and field schema
- Submit invoices via the Recovery API — submit an invoice, read the response, and stop retries
- Getting started — configure your endpoint and select events
Updated 15 days ago