Firsty

Search...

Search...

Core concepts

Webhooks

Subscribe to activation and usage events.

Webhooks push lifecycle events to your backend so you don't have to poll.

Setting up an endpoint

Add an endpoint yourself, on the Webhooks page of the partner area or with the webhook management API (experimental). Name the event types it should receive. There's no wildcard, so list each one. A client can have up to five endpoints.

POST /management/webhooks
requests.post(
    "https://connect.test.firsty.app/api/v3/management/webhooks",
    headers={
      "Authorization": "Bearer YOUR_ACCESS_TOKEN",
      "Content-Type": "application/json"
    },
    json={
      "url": "https://partner.example.com/firsty/webhooks",
      "eventTypes": [
        "package.created",
        "package.usage.threshold"
      ],
      "description": "Production"
    }
)

The response carries the endpoint's signing secret, and it's the only time you see it. Keep it with your other secrets: you need it to verify deliveries. Lost it? Rotating the secret issues a new one. Get in touch if you'd rather Firsty set one up for you.

Events

The webhooks reference lists every event and its payload, and marks the experimental ones.

The ones most integrations care about first: esim.enabled (the user genuinely has working data) and package.usage.threshold (time to offer a top-up).

Verifying signatures

Deliveries are signed following the Standard Webhooks spec: an HMAC-SHA256 over {webhook-id}.{webhook-timestamp}.{raw-request-body}, sent in the webhook-signature header as v1,{base64}. Verify before trusting the payload. An unverified webhook endpoint is an unauthenticated write endpoint.

The simplest route is a Standard Webhooks library, such as standardwebhooks on npm or com.standardwebhooks:standardwebhooks on Maven Central. To verify by hand: the secret is whsec_ followed by base64, and the HMAC key is the decoded bytes after that prefix.

Verify a delivery
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(headers: Headers, rawBody: string, secret: string): boolean {
  const id = headers.get("webhook-id");
  const timestamp = headers.get("webhook-timestamp");
  const header = headers.get("webhook-signature"); // "v1,{base64}"
  if (!id || !timestamp || !header) return false;

  // Reject malformed or stale timestamps to blunt replay attacks.
  if (!/^\d{1,10}$/.test(timestamp)) return false;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = `v1,${createHmac("sha256", key)
    .update(`${id}.${timestamp}.${rawBody}`)
    .digest("base64")}`;
  const a = Buffer.from(expected);

  // The header can carry several space-separated signatures; any match passes.
  return header.split(" ").some((candidate) => {
    const b = Buffer.from(candidate);
    return a.length === b.length && timingSafeEqual(a, b);
  });
}

Use the raw request body. Parsing to JSON and re-serialising changes key order and whitespace, and the signature will never match.

Rotating the signing secret takes effect immediately, so update your verifier straight after rotating. Deliveries it rejects in the meantime are retried within the retry window below.

Delivery

Respond 2xx within 30 seconds and do the real work asynchronously. Anything else, a timeout or a connection failure is retried, except 410 Gone. Delivery is attempted up to ten times over roughly 16 hours, backing off from 30 seconds to 8 hours. The deliveries log shows each delivery of the last 7 days and its outcome.

To debug a delivery, open the endpoint on the Webhooks page. It lists the last 7 days of deliveries, with the payload sent and your endpoint's answer. Over the API, that's GET /management/webhooks/{webhookEndpointReference}/deliveries.

Delivery is at-least-once and ordering is best-effort, so the same event can arrive twice, and package.activated can land before esim.enabled. Make the handler idempotent by keying it on the webhook-id header, and treat each event on its own rather than assuming a sequence.