Skip to main content

Event Types

Every event payload includes a type field that identifies what occurred. The current event catalog:
Subscribe only to the event types your integration needs. Unnecessary subscriptions increase processing overhead and expose more surface area to errors.

Delivery Model

At-least-once delivery

Every event is guaranteed to be delivered — but may arrive more than once. Your handler must be idempotent. Use the event id field to deduplicate.

Automatic retries

If your endpoint does not return a 2xx status within the timeout window, delivery is retried with exponential backoff across multiple attempts before the event is marked failed.
Ordering is not guaranteed. An order.ticketed event may arrive before its corresponding order.created event under retry conditions. Always design handlers to tolerate out-of-order delivery.

Request Format

Every webhook is a POST request with the following structure:
POST /your-webhook-endpoint HTTP/1.1
Content-Type: application/json
X-Signature: t=1714000000,v1=a3f...9c2
X-Delivery-ID: del_01HXZ9K3BVMQ7GFNEW4ARTY5C8

{
  "id": "evt_01HXZ9K3BVMQ7GFNEW4ARTY5C8",
  "type": "order.created",
  "created_at": "2024-04-25T10:00:00Z",
  "data": {
    "order_id": "ord_99XABCDE",
    "amount": 12000,
    "currency": "usd"
  }
}
FieldDescription
idUnique event identifier — use this for deduplication
typeThe event type from the catalog above
created_atISO 8601 timestamp of when the event was generated
dataEvent-specific payload — shape varies by type

Delivery Flow

Respond with 200 OK immediately after verification — before any business logic runs. If processing takes longer than the timeout window, the delivery is considered failed and retried.

Signature Verification

Every webhook request includes an X-Signature header. Verifying it proves the request originated from the API and was not tampered with in transit.

Signature format

X-Signature: t=<timestamp>,v1=<hmac>
  • t — Unix timestamp of when the request was sent
  • v1 — HMAC-SHA256 of the signed payload using your webhook secret

Signed payload construction

The string that is signed is formed by concatenating the timestamp, a literal period, and the raw request body:
<timestamp>.<raw_body>

Verification implementation

import { createHmac, timingSafeEqual } from "crypto";

function verifyWebhookSignature(
  rawBody: string,
  signatureHeader: string,
  secret: string,
  toleranceSeconds = 300
): boolean {
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((p) => p.split("="))
  );

  const timestamp = parseInt(parts["t"], 10);
  const receivedHmac = parts["v1"];

  // Reject stale requests
  const age = Math.floor(Date.now() / 1000) - timestamp;
  if (age > toleranceSeconds) {
    throw new Error(`Webhook timestamp too old: ${age}s`);
  }

  // Recompute expected HMAC
  const signedPayload = `${timestamp}.${rawBody}`;
  const expected = createHmac("sha256", secret)
    .update(signedPayload)
    .digest("hex");

  // Compare in constant time to prevent timing attacks
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(receivedHmac, "hex");

  if (a.length !== b.length) return false;
  return timingSafeEqual(a, b);
}
Never use string equality (===) to compare HMACs. Use timingSafeEqual to prevent timing attacks that could allow an attacker to forge valid signatures.

Handling Duplicates

Because delivery is at-least-once, your handler will receive the same event more than once. Guard against this by tracking processed event IDs:
async function handleWebhook(event: WebhookEvent) {
  const alreadyProcessed = await db.processedEvents.exists(event.id);
  if (alreadyProcessed) {
    console.log(`Skipping duplicate event: ${event.id}`);
    return; // Return 200 — do not reprocess
  }

  await db.processedEvents.create({ id: event.id, receivedAt: new Date() });
  await processEvent(event); // Your business logic
}
Store processed event IDs with a TTL that matches the maximum retry window of the delivery system (typically 72 hours). There is no need to retain them indefinitely.

Retry Schedule

When delivery fails, the system retries with exponential backoff:
AttemptDelayCumulative time
1 (initial)0s
230s30s
35 min~5 min
430 min~35 min
52 hours~2h 35 min
6 (final)5 hours~7h 35 min
After the final attempt, the event is marked as failed and no further delivery is attempted. You can manually replay failed events from the dashboard or via the API.

Implementation Checklist

1

Register your endpoint

Provide a publicly accessible HTTPS URL. HTTP endpoints are rejected.
2

Store your webhook secret securely

Retrieve your signing secret from the dashboard and store it in an environment variable — never hardcode it.
3

Verify the signature on every request

Reject any request that fails HMAC verification before touching the payload.
4

Respond 200 before processing

Acknowledge receipt immediately. Offload business logic to a background queue.
5

Deduplicate using event ID

Check the event id against your store before processing. Return 200 for duplicates without reprocessing.
6

Handle out-of-order events

Use created_at timestamps and idempotent state transitions to safely handle events that arrive out of sequence.

Best Practices

Verify every signature

Treat any request that fails signature verification as hostile. Log and discard it immediately.

Respond fast, process async

Acknowledge within the timeout window. Push processing to a queue — never block the HTTP response on business logic.

Idempotent handlers

Design every handler to produce the same outcome when called multiple times with the same event.

Common Mistakes

MistakeConsequenceFix
Trusting unverified payloadsAttackers can forge events and trigger unintended actionsAlways verify HMAC before reading data
Blocking on business logic before respondingDelivery timeout triggers unnecessary retriesReturn 200 first, process in a background job
Not handling duplicate eventsDouble-processing causes data corruption, duplicate chargesDeduplicate on event.id with a processed-events store
Using string equality for HMAC comparisonVulnerable to timing attacks that allow signature forgeryUse timingSafeEqual or equivalent
Ignoring the t timestampReplay attacks can resend valid old payloadsReject requests where t is older than 5 minutes
Returning non-2xx for already-seen eventsThe system interprets it as a failure and retries indefinitelyAlways return 200 for duplicates