Events and webhooks
One event stream for every provider, delivered as Standard Webhooks and replayable from the API.
Providers report progress in different ways: webhooks with their own signatures, status endpoints you have to poll, or nothing at all until a deadline passes. Unirail handles all of that and turns it into one stream of events (evt_…) for each environment.
{
"id": "evt_4Hq9sLm2Xa",
"object": "event",
"type": "payment_intent.succeeded",
"mode": "test",
"createdAt": "2026-10-08T10:00:00.000Z",
"data": { "object": { "id": "pi_…", "object": "payment_intent", "status": "succeeded", "…": "…" } }
}data.object is a snapshot of the object when the event happened. Fetch the object again when you need its current state.
Event types
- link_session.completed
- link_session.failed
- account.updated
- account.reauth_required
- payment_intent.requires_action
- payment_intent.processing
- payment_intent.succeeded
- payment_intent.failed
- payment_intent.cancelled
- leg.updated
- connection.degraded
Webhook endpoints
Add endpoints per environment in the dashboard under Webhooks, optionally limited to some event types. Each endpoint has a signing secret (whsec_…) shown when you create it; you can rotate it and send a test delivery from the same page.
Verifying deliveries
Deliveries follow the Standard Webhooks scheme:
| Header | Value |
|---|---|
webhook-id | A unique id for the delivery. Dedupe on it. |
webhook-timestamp | Unix seconds when it was signed. |
webhook-signature | Space-separated v1,<base64> entries: HMAC-SHA256 with your secret over ${id}.${timestamp}.${body}. |
Verify against the raw request body, exactly as received:
import { webhooks } from "@unirail/sdk";
const event = await webhooks.verify({
body: await request.text(),
headers: request.headers,
secret: process.env.UNIRAIL_WEBHOOK_SECRET!,
});verify throws a WebhookVerificationError when a header is missing, no signature matches, or the timestamp is more than five minutes from now (toleranceSeconds changes that). While you rotate a secret, pass both: secret: [newSecret, oldSecret].
Any Standard Webhooks library can verify deliveries too, so you don't need the SDK in the service that receives them.
To test your handler, sign a payload the way Unirail does:
const secret = webhooks.generateSecret();
const body = JSON.stringify(event);
const headers = await webhooks.sign({ id: "msg_test_1", body, secret });Replaying what you missed
Webhooks can be late or lost on your side. The API keeps the stream, so walk it with a cursor to catch up, for example from a reconciliation job:
let after = await cursor.load(); // the last evt_… you processed
for (;;) {
const page = await unirail.events.list({ after, limit: 100 });
for (const event of page.data) {
await handle(event);
after = event.id;
}
await cursor.save(after);
if (!page.hasMore) break;
}Handle events idempotently by event.id, since the same event can reach you both by webhook and by replay.