Concepts
Async & webhooks
Every generation runs in the background. Find out when it's done by polling, or via a signed webhook.
The flow
POST /generations→ a job withstatus: "processing".- Poll
GET /generations/:iduntilcompleted/failed— or pass awebhook_urland get notified.
Polling
Simple, no setup required. Check the job every few seconds:
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:
| Header | Value |
|---|---|
X-Caranguejo-Event | generation.completed | generation.failed |
X-Caranguejo-Signature | sha256=<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:
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 3× with backoff on network errors / 5xx / 429. Respond
2xx quickly and process idempotently — the same delivery can arrive more than once.