Your signing secret
Each API key has its ownwebhookSecret, issued with the key and looking like
whsec_ followed by 64 hex characters. It signs every delivery to the webhooks
on that key — both ramp updates and payment
settlements.
You will find it on the key in the dashboard, and in the response to
PATCH /pub/v2/webhook.
Keys issued before signing existed did not have a secret. One is created for
those automatically the first time the key is read or used, so if a key shows no
secret, open it in the dashboard once and it will have one.
What arrives with a delivery
Two headers:
The signature covers the timestamp and the body joined by a single dot:
1788224623 carrying {"id":"pay_1","amount":25.5}
is a digest over exactly this string:
v1= prefix names the scheme. If we ever introduce a second one, both will
be sent together for a transition period, so match on the prefix rather than
assuming the whole header is a digest.
Verifying a delivery
1
Read the raw body
Capture the request body as the exact bytes that arrived, before any JSON
parsing.
2
Check the timestamp is recent
Reject anything older than about five minutes. This is what stops a captured
delivery being replayed at you later.
3
Recompute the digest
HMAC-SHA256 over
{timestamp}.{raw body} using your webhookSecret, hex
encoded.4
Compare in constant time
Use your language’s timing-safe comparison, not
==.Verify against the raw body
The most common reason a signature will not verify is parsing the body first, then re-serialising it to check. Those are not the same bytes: key order, whitespace, and how unicode is escaped all differ between our serialiser and yours, and any of them changes the digest. Capture the raw body and verify that, then parse. In Express this meansexpress.raw({ type: 'application/json' }) on the webhook route specifically,
or express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }) if the
route also needs a parsed body. Most frameworks have an equivalent — in NestJS,
rawBody: true on NestFactory.create and then req.rawBody.
Why the timestamp is signed
The timestamp sits inside the signed string rather than merely beside it, which means it cannot be edited in transit. That is what makes it usable as a replay window: someone who captures a valid delivery cannot move its timestamp forward without invalidating the signature, so once your five minutes are up the capture is worthless. This matters for retries. A failed delivery is retried with a fresh signature and a fresh timestamp, never a replay of the original, so a retry that arrives half an hour later still verifies against a five-minute window.Redirects are not followed
Deliveries are posted to your URL and no further. If your endpoint answers with a redirect, the delivery is not followed to the new location and counts as failed — following it would hand a valid signature to a host you never configured. Point the webhook at its final URL directly. Watch for this if your URL redirectshttp to https, or a bare domain to
www. Configure the destination itself.
Deliveries that arrive unsigned
Ramp orders opened outside a business have no API key behind them and so no secret to sign with. If you create orders that way, those deliveries arrive without the signature headers. Everything authenticated with an API key — every payment settlement, and every ramp order opened withx-api-key — is signed. If you only use the v2 API, treat
a missing signature as a failed verification.