Fittins AI Logo
API docs menu

Reference

Webhooks

Register an https URL and we send an event when a generation finishes, so you don't have to poll. Each event is signed, so you can check it came from us.

EndpointWhat it does
POST /v1/webhook_endpointsCreate one: url, and events (default: all of generation.succeeded, generation.failed). The answer includes the signing secret, shown once. Up to 5 per account.
GET /v1/webhook_endpointsList endpoints with their last delivery.
DELETE /v1/webhook_endpoints/{id}Stop sending and delete the endpoint.
POST /v1/webhook_endpoints/{id}/rotate_secretA new secret, shown once. For 24 hours events are signed with both, so you can switch without missing any.
curl
curl https://api.fittins.ai/v1/webhook_endpoints \
  -H "Authorization: Bearer $FITTINS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "url": "https://yourstore.com/hooks/fittins",
  "events": [
    "generation.succeeded",
    "generation.failed"
  ]
}'

The event

HTTP
POST https://yourstore.com/hooks/fittins
Fittins-Signature: t=1793905382,v1=5b1e0f7c…
Content-Type: application/json

{
  "id": "evt_9Lw3Kd2PqR7sT1vX4yZ8",
  "object": "event",
  "type": "generation.succeeded",
  "created_at": "2026-11-08T17:03:02.000Z",
  "livemode": true,
  "data": { "object": { /* the full generation */ } }
}

Checking the signature

The Fittins-Signature header has a timestamp t and one or two v1 signatures. Each v1 is the HMAC-SHA256, in hex, of the timestamp, a dot and the raw request body, using your secret.

import crypto from "node:crypto";

// rawBody: the request body exactly as received (a string)
export function verifyFittins(rawBody, header, secret) {
  const parts = header.split(",").map((p) => p.split("="));
  const t = parts.find(([k]) => k === "t")?.[1];
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
  return fresh && parts.some(([k, v]) => k === "v1" && v.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(v), Buffer.from(expected)));
}

The JavaScript and Python libraries include this check. See Libraries.

Delivery

  • Answer with any 2xx within 10 seconds. Do slow work after answering.
  • A failed delivery is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours.
  • An event can arrive more than once. Use its id to ignore repeats.
  • An endpoint that only fails for 3 days is paused and the account owner is emailed. To start again, delete it and create a new one.
  • Test keys have their own endpoints and only send test events.

A notice for one generation

Instead of an endpoint for every event, a create request can carry webhook_url: an https address we call once when that generation ends, whether it succeeded, failed or was canceled. It needs no endpoint and works with test keys too.

HTTP
POST https://yourstore.com/fittins/done/7Hq2
Content-Type: application/json

{
  "id": "evt_5Wm2Lq8RzK4c",
  "object": "event",
  "type": "generation.succeeded",
  "created_at": "2026-11-10T17:03:02.000Z",
  "livemode": true,
  "data": { "object": { "id": "gen_7Hq2Mz9TkQ4x1bVnR8sL", "object": "generation", "type": "model", "status": "succeeded" } }
}
  • It carries only the generation's id, type and status, never the result, and it isn't signed. Read the result with GET /v1/generations/{id} and your key.
  • It is retried at the same times as endpoint events, and checked against the same rules for public addresses.
  • Endpoint events keep coming as before; the notice doesn't replace them.
Questions? Email support@fittins.ai with the X-Request-Id of the request.Next: Usage →