Documentation

Webhooks

Partners can configure one HTTPS webhook endpoint in the dashboard. RoamWorks sends signed events for completed and processing orders and for redemptions. The delivery log is visible in the dashboard.

Events

EventWhenData
order.completedThe purchase is complete and every child order is fulfilled.purchaseId, reference, orderIds, amountMinor, currency, status, pendingOrderIds
order.processingThe purchase is paid but at least one unit awaits upstream fulfilment.Same shape as order.completed.
esim.redeemedA redemption batch was created.batchId, purchaseId, orderIds, itemIds, format

No webhook payload contains LPA text, QR data, a secret, card data or customer personal data. Use the purchase or batch id to fetch authorised data through the API when needed.

Partner webhook management is currently dashboard-only. A management API is Planned.

Envelope

JSONCode
{ "id": "99999999-9999-4999-8999-999999999999", "type": "order.completed", "createdAt": "2026-11-04T03:00:00.000Z", "data": { "purchaseId": "33333333-3333-4333-8333-333333333333", "reference": "booking-123", "orderIds": ["44444444-4444-4444-8444-444444444444"], "amountMinor": 1618, "currency": "USD", "status": "completed", "pendingOrderIds": [] } }

Signature

Every delivery carries:

Code
x-roamworks-signature: t=1793251200,v1=5f2c9d...lowercase-hex... x-roamworks-event: order.completed content-type: application/json user-agent: RoamWorks-Webhooks/1.0

The signed content is:

Code
<t>.<raw request body>

v1 is the lowercase hex HMAC-SHA256 of that string using your webhook signing secret. The secret is shown once when you save or rotate the endpoint in the dashboard. Store it in a secret manager.

Verify

Verify against the raw bytes before parsing JSON. Reject a timestamp older than five minutes. Compare digests in constant time.

TypeScriptCode
import { createHmac, timingSafeEqual } from "node:crypto"; export function verifyRoamWorksWebhook(input: { rawBody: string; header: string | null; secret: string; nowSeconds?: number; }) { if (!input.header) return false; const parts = Object.fromEntries( input.header.split(",").map((part) => { const index = part.indexOf("="); return [part.slice(0, index).trim(), part.slice(index + 1).trim()]; }), ); const timestamp = Number(parts.t); const given = parts.v1; if (!Number.isInteger(timestamp) || !given) return false; const now = input.nowSeconds ?? Math.floor(Date.now() / 1000); if (Math.abs(now - timestamp) > 300) return false; const expected = createHmac("sha256", input.secret) .update(timestamp + "." + input.rawBody) .digest("hex"); const a = Buffer.from(expected, "hex"); const b = Buffer.from(given, "hex"); return a.length === b.length && timingSafeEqual(a, b); }

Reject the delivery with a non-2xx response when verification fails. Do not parse and re-serialize the body before verification; whitespace matters.

Delivery and retries

  • Request timeout: 10 seconds.
  • Success: any 2xx response.
  • Retry: connection errors, timeouts, non-2xx responses and redirects (redirects are not followed).
  • Attempts: up to 10.
  • Delay after attempt 1 to 9: 5 seconds, 15 seconds, 60 seconds, 5 minutes, 15 minutes, 30 minutes, 1 hour, 2 hours, 4 hours.
  • After the tenth failed attempt the event is marked dead in the dashboard.

Deliveries are at least once. Deduplicate on the envelope id. A duplicate delivery can arrive after a timeout even if your handler processed the first one.

Endpoint requirements

  • Public HTTPS URL. HTTP, loopback, link-local, private and local-only addresses are rejected.
  • No embedded username or password in the URL.
  • A valid public certificate. The API does not disable certificate validation.
  • Return a 2xx quickly, then process asynchronously. A slow handler increases retry pressure.
  • Use a dedicated endpoint path. Do not expose the signing secret in a query string.

Local testing

Use a tunnel with a public HTTPS URL. The API resolves the hostname and rejects private addresses, so a localhost URL will not work. Verify the signature with the same raw body your framework received.

Stripe funding webhook

POST /v1/webhooks/stripe is a separate server-to-server receiver for Stripe. It is not a partner-configured endpoint and partners do not call it. It returns 501 when Stripe funding is not enabled and 400 for an invalid Stripe signature.