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 Recovery API response doesn't tell you when the next retry will run or how collection ends — webhooks do. Recover sends four events across an invoice's retry lifecycle so your system can follow progress and record the final outcome. This guide covers what each event means, the order they arrive in, how to tie them back to an invoice, and how to handle delivery securely.
Note Set your endpoint URL and choose which events to receive in the onboarding flow — see Getting started.

The four events

Recover fires these four events as an invoice moves through its retry schedule:

EventWhen it firesHow often
new_dunning_eventAn invoice enters or reaches a milestone in the dunning retry schedule.Once per configured schedule milestone
successful_paymentA retry transaction is created and collected by the gateway.Once per invoice
failed_paymentA retry transaction is created and declined by the gateway.Can fire multiple times per invoice, depending on the retry window
closed_invoiceA past-due invoice reaches a final state — either paid or the retry window is exhausted.Once per invoice
Tip Treat 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_eventsuccessful_paymentclosed_invoice (state: collected/paid)

Exhausted retry window

new_dunning_eventfailed_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

Important Treat webhooks as triggers, not the source of truth: verify they're genuine, ignore repeats, and confirm state through an API query before acting.
ConcernHow to handle it
AuthenticityVerify 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 deliveriesRecurly 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 eventsNever 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 cardEvents delivered
Successfulnew_dunning_event, successful_payment, closed_invoice
Decliningnew_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


Did this page help you?