Ronda POSTs a signed JSON payload to your endpoint every time something happens. Deliveries are written to an outbox in the same database transaction as the change that caused them, so an event can never be lost because a request failed.
{
"id": "evt_9f3a2c…",
"object": "event",
"type": "payment.succeeded",
"mode": "live",
"created": 1786998900,
"data": {
"object": {
"id": "pay_a348cb…",
"object": "payment",
"amount": 12900,
"currency": "AMD",
"status": "succeeded",
"customer": "cus_1b4c…",
"subscription": "sub_5c1e…",
"attempt": 1,
"is_recovery": false
}
}
}| Header | Meaning |
|---|---|
ronda-signature | t=<unix>,v1=<hmac> — HMAC-SHA256 of `${t}.${body}` |
ronda-event-type | The event type, for routing without parsing |
ronda-delivery-id | Unique per delivery attempt — use it to deduplicate |
ronda-mode | test or live |
The timestamp is inside the signed material, which is what makes replay detection possible: a captured request cannot be re-sent later, because its t falls outside your tolerance and changing t invalidates the HMAC. Reject anything older than five minutes.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(",").map(kv => kv.split("=")));
const age = Math.abs(Math.floor(Date.now() / 1000) - Number(parts.t));
if (!Number.isFinite(Number(parts.t)) || age > toleranceSeconds) return false;
const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1 ?? "");
return a.length === b.length && timingSafeEqual(a, b);
}Verify against the raw request body. Parsing and re-serialising the JSON changes the bytes and the signature will not match.
Any response outside 2xx is a failure. Ronda retries up to eight times with backoff — 10 seconds, 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours — then stops. An endpoint that fails twenty times in a row is disabled and shown as such in the dashboard, where you can also inspect and replay any delivery.
Reply quickly and do the work afterwards. If your handler takes longer than 8 seconds, Ronda treats the delivery as failed and will send it again.
A retry means your endpoint can see the same event twice. Deduplicate on the event id (stable across retries) rather than ronda-delivery-id (which changes per attempt).
| Event | When it fires |
|---|---|
payment.created | A payment intent was recorded, before the bank was called |
payment.succeeded | Money was taken. The one to act on. |
payment.failed | The bank declined. Check failure_code. |
payment.requires_action | The issuer forced a 3-D Secure step-up on a recurring charge |
payment.refunded | Fully refunded |
payment.partially_refunded | Partly refunded |
payment.settled | The bank settled the funds to the merchant |
| Event | When it fires |
|---|---|
subscription.created | A subscription exists; it may be trialing |
subscription.activated | First charge succeeded |
subscription.trial_will_end | Three days before a trial ends |
subscription.updated | Quantity, period or cancellation flag changed |
subscription.plan_changed | Plan changed; carries the proration amounts |
subscription.past_due | A renewal failed and dunning has started |
subscription.charge_failed | A dunning retry also failed |
subscription.recovered | Dunning worked — the subscription is active again |
subscription.paused | Paused by the merchant |
subscription.resumed | Resumed |
subscription.canceled | Canceled, by request or after exhausting dunning |
| Event | When it fires |
|---|---|
customer.created | A customer record was created |
customer.updated | Customer details changed |
customer.deleted | Personal data cleared, cards deactivated |
payment_method.attached | A card was saved (the token, never the number) |
payment_method.detached | A card was removed and deactivated at the bank |
checkout.session.completed | A checkout finished successfully |
checkout.session.expired | A checkout was abandoned and expired |
| Event | When it fires |
|---|---|
refund.created | A refund was recorded |
refund.succeeded | The bank accepted the refund |
refund.failed | The bank rejected the refund |
dispute.opened | A chargeback was raised |
dispute.closed | A chargeback was won or lost |
product.created | A product was created |
product.updated | A product changed |
product.archived | A product and its prices were archived |
plan.created | A price was created |
plan.updated | A price changed |
plan.archived | A price was archived |
coupon.created | A coupon was created |
coupon.archived | A coupon was archived |
discount.created | A coupon was applied to a subscription |
discount.removed | A discount was removed |
payment_link.created | A payment link was created |
payment_link.updated | A payment link changed |
invoice.finalized | An invoice to your customer was issued |
invoice.paid | An invoice was paid |
invoice.voided | An invoice was voided |
invoice.uncollectible | An invoice went unpaid past its grace period |
invoice.created | Your monthly Ronda invoice was issued |
invoice.payment_failed | Your Ronda invoice could not be collected |
merchant.suspended | Live processing was suspended |
merchant.reinstated | Live processing was restored |