Skip to main content

Verifying Deliveries

Request headers​

Every webhook request is a POST with Content-Type: application/json, User-Agent: RynoPay-Webhooks/1.0, and these headers:

HeaderValue
Ryno-EventThe event name, one of the event names.
Ryno-Event-IdThe event. The same on every retry and every replay — this is your dedupe key.
Ryno-DeliveryThis delivery (one per event for your endpoint); a replay is a new delivery of the same event.
Ryno-Attempt1, 2, … for this delivery.
Ryno-TimestampUnix seconds when this attempt was signed.
Ryno-Signaturet=<timestamp>,v1=<hex>[,v1=<hex>] — see below.
Ryno-Replaytrue on a replay you or RynoPay triggered; absent otherwise.

Signature​

hex = HMAC-SHA256(secret, "{t}.{body}")

hex is lower-case hex over the timestamp from the header, a dot, and the exact bytes of the request body. Recompute it, compare in constant time with any v1 value present, and reject deliveries whose t is more than five minutes old.

During the 24 hours after a secret rotation there are two v1 values, newest first, one per live secret; accept the delivery if either matches.

Example (Node.js)​

const crypto = require('crypto');

// rawBody: the exact bytes of the request body, before any JSON parsing.
function verifyRynoSignature(rawBody, signatureHeader, secret) {
const parts = signatureHeader.split(',').map((p) => p.trim());
const t = parts.find((p) => p.startsWith('t='))?.slice(2);
const signatures = parts.filter((p) => p.startsWith('v1=')).map((p) => p.slice(3));
if (!t || signatures.length === 0) return false;

// Reject deliveries whose t is more than five minutes old.
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;

const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.`)
.update(rawBody)
.digest('hex');

// Accept if any v1 value matches (two are present during a rotation).
const expectedBytes = Buffer.from(expected, 'hex');
return signatures.some((sig) => {
const actual = Buffer.from(sig, 'hex');
return actual.length === expectedBytes.length && crypto.timingSafeEqual(actual, expectedBytes);
});
}

The body​

{
"id": "0f8c2b1e-...",
"type": "customer.onboarding.approved",
"specVersion": "1.0",
"eventVersion": 1,
"occurredAt": "2026-09-20T10:15:00.0000000+00:00",
"correlation": { "kind": "customer", "id": "b3f1..." },
"data": { }
}
FieldNotes
idThe event, equal to Ryno-Event-Id. The same on every retry and replay — treat a repeated id as a duplicate and ignore it. That is the safe way to make your handler idempotent.
typeThe event name, matching Ryno-Event. Ignore a type you do not recognise rather than failing: every partner receives every event, so new ones start arriving without a subscription step.
specVersionVersion of the envelope. Fields will only be added within 1.0.
eventVersionVersion of data for this type. A breaking payload change ships as a new version; you receive the latest.
occurredAtWhen the change happened, not when this attempt was made — a retry a day later still reports the original time. Deliveries are not ordered; use it to discard an update you have already superseded.
correlationWhat the event is about: kind is customer for every business onboarding event and id the customer's id, so you can route without parsing data.
dataThe event's payload. Its shape depends on type; see Events and payloads.

Acknowledging​

Return any 2xx within 10 seconds once you have stored the event, and do your own processing afterwards: a 2xx that arrives later than that counts as a failure and the delivery is retried, so you would see it again. Do not follow the delivery with a lookup unless you need to; the payload is complete.

If your endpoint does not acknowledge in time, the delivery is retried — see Retries and endpoint health.