iFragment

Webhooks

Order and account events, HMAC-SHA256 signature, retries.

A webhook is a POST to your Project's URL on every event. The endpoint and the signing secret are configured in the dashboard; the secret is shown only once.

Events

typeWhen
order.succeededthe order was fulfilled
order.failedthe order was not fulfilled, the balance hold was released
order.needs_reviewthe outcome is unknown, support is checking the order
supplier_account.health_changedthe health of your Supplier Account changed (OWN mode)

Format

POST /ifragment/webhook HTTP/1.1
Content-Type: application/json
X-IFragment-Signature: t=1790000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
{
  "id": "0199a1b2-7c3d-7e4f-8a9b-0c1d2e3f4a5b",
  "type": "order.succeeded",
  "createdAt": "2026-09-26T10:15:30.000Z",
  "data": {
    "id": "0199a1b2-6f00-7abc-9def-112233445566",
    "externalId": "1042",
    "product": "STARS",
    "quantity": 500,
    "months": null,
    "username": "durov",
    "recipientName": "Pavel Durov",
    "status": "SUCCEEDED",
    "price": "8.25",
    "currency": "USD",
    "failure": null,
    "createdAt": "2026-09-26T10:15:02.000Z",
    "sentAt": "2026-09-26T10:15:05.000Z",
    "completedAt": "2026-09-26T10:15:30.000Z"
  }
}

For supplier_account.health_changed, data contains the account:

{
  "accountId": "0199a1b2-1111-7abc-9def-112233445566",
  "label": "Main wallet",
  "previousHealth": "HEALTHY",
  "health": "DEGRADED",
  "reasons": ["LOW_BALANCE"],
  "changedAt": "2026-09-26T10:20:00.000Z"
}
  • id is the event id, and it stays the same across redeliveries. Store processed ids and skip duplicates: delivery is "at least once".
  • data holds the order object (as in GET /v1/orders/{id}) or the account for supplier_account.health_changed. For order.failed, the reason is in data.failure.code.
  • Events may arrive out of order; rely on data.status, and when in doubt, fetch the order through the API.

Response

Reply with any 2xx within 10 seconds; the response body is ignored. Do long-running work after replying (a queue, a background job).

Retries

On a non-2xx response, a timeout or a connection error, delivery is retried with exponential backoff: 12 attempts over about 24 hours in total. After that the delivery is marked FAILED; you can retry it manually from the dashboard.

Verifying the signature

X-IFragment-Signature: t=<unix>,v1=<hex>, where v1 = hex(HMAC-SHA256(secret, "<t>.<rawBody>")):

  1. split the header into t and v1;
  2. compute HMAC-SHA256 over the string t + "." + raw request body, that is, the bytes as received, before JSON parsing;
  3. compare it with v1 in constant time;
  4. reject the request if t differs from the current time by more than 5 minutes (replay protection).
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyIFragmentSignature(rawBody: string, header: string, secret: string, toleranceSec = 300): boolean {
  const parts = Object.fromEntries(header.split(',').map((part) => part.split('=', 2) as [string, string]));
  const timestamp = Number(parts.t);
  if (!parts.v1 || !Number.isInteger(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > toleranceSec) {
    return false;
  }
  const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest();
  const received = Buffer.from(parts.v1, 'hex');
  return received.length === expected.length && timingSafeEqual(received, expected);
}

// Express: take the raw body, not the parsed JSON.
app.post('/ifragment/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const raw = req.body.toString('utf8');
  if (!verifyIFragmentSignature(raw, req.get('X-IFragment-Signature') ?? '', process.env.IFRAGMENT_WEBHOOK_SECRET!)) {
    return res.sendStatus(401);
  }
  const event = JSON.parse(raw);
  res.sendStatus(200);
  queue.add(event); // process after replying
});

A common mistake is computing the signature over re-serialized JSON (JSON.stringify(req.body)). Field order and whitespace may differ, and the signature will not match. Always use the raw body.

On this page