Unirail

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:

HeaderValue
webhook-idA unique id for the delivery. Dedupe on it.
webhook-timestampUnix seconds when it was signed.
webhook-signatureSpace-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.

On this page