Developer documentationGet API keys

Webhooks

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.

The payload

{
  "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
    }
  }
}

Headers

HeaderMeaning
ronda-signaturet=<unix>,v1=<hmac> — HMAC-SHA256 of `${t}.${body}`
ronda-event-typeThe event type, for routing without parsing
ronda-delivery-idUnique per delivery attempt — use it to deduplicate
ronda-modetest or live

Verifying a signature

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.

Retries

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.

Idempotency on your side

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).

Endpoint requirements

  • HTTPS, on a publicly resolvable address.
  • Private, loopback, link-local and cloud-metadata ranges are refused — both when you save the URL and again immediately before every delivery, so DNS cannot be re-pointed at an internal service afterwards.
  • Redirects are not followed.

Event catalogue

Payments

EventWhen it fires
payment.createdA payment intent was recorded, before the bank was called
payment.succeededMoney was taken. The one to act on.
payment.failedThe bank declined. Check failure_code.
payment.requires_actionThe issuer forced a 3-D Secure step-up on a recurring charge
payment.refundedFully refunded
payment.partially_refundedPartly refunded
payment.settledThe bank settled the funds to the merchant

Subscriptions

EventWhen it fires
subscription.createdA subscription exists; it may be trialing
subscription.activatedFirst charge succeeded
subscription.trial_will_endThree days before a trial ends
subscription.updatedQuantity, period or cancellation flag changed
subscription.plan_changedPlan changed; carries the proration amounts
subscription.past_dueA renewal failed and dunning has started
subscription.charge_failedA dunning retry also failed
subscription.recoveredDunning worked — the subscription is active again
subscription.pausedPaused by the merchant
subscription.resumedResumed
subscription.canceledCanceled, by request or after exhausting dunning

Customers, cards and checkout

EventWhen it fires
customer.createdA customer record was created
customer.updatedCustomer details changed
customer.deletedPersonal data cleared, cards deactivated
payment_method.attachedA card was saved (the token, never the number)
payment_method.detachedA card was removed and deactivated at the bank
checkout.session.completedA checkout finished successfully
checkout.session.expiredA checkout was abandoned and expired

Money and account

EventWhen it fires
refund.createdA refund was recorded
refund.succeededThe bank accepted the refund
refund.failedThe bank rejected the refund
dispute.openedA chargeback was raised
dispute.closedA chargeback was won or lost
product.createdA product was created
product.updatedA product changed
product.archivedA product and its prices were archived
plan.createdA price was created
plan.updatedA price changed
plan.archivedA price was archived
coupon.createdA coupon was created
coupon.archivedA coupon was archived
discount.createdA coupon was applied to a subscription
discount.removedA discount was removed
payment_link.createdA payment link was created
payment_link.updatedA payment link changed
invoice.finalizedAn invoice to your customer was issued
invoice.paidAn invoice was paid
invoice.voidedAn invoice was voided
invoice.uncollectibleAn invoice went unpaid past its grace period
invoice.createdYour monthly Ronda invoice was issued
invoice.payment_failedYour Ronda invoice could not be collected
merchant.suspendedLive processing was suspended
merchant.reinstatedLive processing was restored