Documentation
Webhooks
Receive events, verify their signature, and rely on retries and replay.
Events
VEYRA sends an HTTPS POST with a JSON event to each endpoint subscribed to that event type.
{
"id": "evt_...",
"type": "payment_intent.succeeded",
"created": 1760000000,
"livemode": false,
"api_version": "2026-10-01",
"data": {
"object": { "id": "pi_...", "object": "payment_intent", "amount": 12000, "currency": "EUR", "status": "succeeded" }
}
}Event types
payment_intent.createdpayment_intent.requires_actionpayment_intent.processingpayment_intent.succeededpayment_intent.payment_failedpayment_intent.canceledcheckout.session.completedcheckout.session.expiredcharge.refundeddispute.createddispute.closedpayout.createdpayout.paidpayout.failedpayout.returnedbank_account.verifiedbank_account.verification_failedSignature
Each request carries a Veyra-Signature header with a timestamp and an HMAC-SHA256 signature computed with the endpoint's secret (whsec_…):
Veyra-Signature: t=1760000000,v1=5f2b8c...e9
signed_payload = "<t>.<raw request body>"
v1 = hex( HMAC-SHA256( whsec_secret, signed_payload ) )- Compute the HMAC over the raw body bytes, before any JSON parsing.
- Compare with a constant-time function.
- Reject events whose timestamp is more than 5 minutes away from your clock, to prevent replay attacks.
Verifying in your language
import { createHmac, timingSafeEqual } from "node:crypto";
const TOLERANCE_SECONDS = 5 * 60;
export function verifyVeyraSignature(rawBody: string, header: string, secret: string): boolean {
// header: "t=1760000000,v1=5f2b...e9"
const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=") as [string, string]));
const t = Number(parts.t);
if (!parts.v1 || !Number.isInteger(t)) return false;
if (Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDS) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(parts.v1, "hex");
return a.length === b.length && timingSafeEqual(a, b);
}
// Express: use the RAW body — not the parsed JSON.
app.post("/webhooks/veyra", express.raw({ type: "application/json" }), (req, res) => {
const ok = verifyVeyraSignature(req.body.toString("utf8"), req.get("Veyra-Signature") ?? "", process.env.VEYRA_WEBHOOK_SECRET!);
if (!ok) return res.sendStatus(400);
const event = JSON.parse(req.body.toString("utf8"));
// Handle event.type idempotently (events can be delivered more than once).
res.sendStatus(200);
});Delivery and retries
Respond with any 2xx status within 10 seconds. Anything else — or no response — counts as a failure, and VEYRA retries with exponential backoff for about three days:
| Attempt | Approximate delay after the previous one |
|---|---|
| 1 | immediately |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 6 hours |
| 7 | 12 hours |
| 8–10 | 24 hours each |
At-least-once delivery
evt_ IDs and make your handler idempotent; re-fetch the object if you need its latest state.Replay
Every event is kept and can be resent from Developers → Events in the dashboard — useful after fixing a bug in your handler. You can also rotate an endpoint's secret there; during rotation both secrets are valid for a short overlap.

