Concepts

Async & webhooks

Every generation runs in the background. Find out when it's done by polling, or via a signed webhook.

The flow

  1. POST /generations → a job with status: "processing".
  2. Poll GET /generations/:id until completed / failedor pass a webhook_url and get notified.

Polling

Simple, no setup required. Check the job every few seconds:

bash
ID=9f0c1a7e-…
# repita até status = completed | failed
curl -s https://caranguejo.art/api/v1/dev/generations/$ID \
  -H "Authorization: Bearer $CK" | jq '.data.status'

Signed webhooks

Include webhook_url (public, https) when creating the job to receive a POST once it reaches a terminal state. Events: generation.completed and generation.failed. Delivery headers:

HeaderValue
X-Caranguejo-Eventgeneration.completed | generation.failed
X-Caranguejo-Signaturesha256=<hex> — HMAC-SHA256 of the raw body

The body is a WebhookEvent with event, created_at, and data (the Generation itself).

Verifying the signature

Compute the HMAC-SHA256 of the raw body (not the re-serialized JSON) using your whsec_… secret (Account → Apps & API), and compare it in constant time:

node
import crypto from "node:crypto";

function verify(rawBody, signature, secret) {
  const expected = "sha256=" +
    crypto.createHmac("sha256", secret).update(rawBody, "utf8").digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

// Express: use o corpo BRUTO, não o já parseado
app.post("/webhooks/caranguejo", express.raw({ type: "*/*" }), (req, res) => {
  const sig = req.header("X-Caranguejo-Signature");
  if (!verify(req.body, sig, process.env.WEBHOOK_SECRET)) return res.sendStatus(401);
  const evt = JSON.parse(req.body.toString("utf8"));
  // evt.event, evt.data.output.assets[…]
  res.sendStatus(200);
});
We redeliver up to with backoff on network errors / 5xx / 429. Respond 2xx quickly and process idempotently — the same delivery can arrive more than once.