Webhooks
Receive a POST notification when a generation finishes, verified with HMAC-SHA256 and retried automatically.
Pass a webhook_url when you submit a generation, and Clipia will POST the result to that URL once it completes — no polling required. Every delivery is signed with HMAC-SHA256 (the X-Clipia-Signature header), and failed deliveries are retried up to 6 times with exponential backoff.
Payload
On success:
{
"request_id": "764cabcf-b745-4b3e-ae38-1200304cf45b",
"status": "OK",
"payload": {
"model": "nano-banana-2",
"output": { "images": [{ "url": "https://media.clipia.ai/works/result.png" }] },
"cost": 12
}
}On error:
{
"request_id": "764cabcf-b745-4b3e-ae38-1200304cf45b",
"status": "ERROR",
"error": { "code": "GENERATION_FAILED", "message": "..." }
}Payload fields
Prop
Type
Signature verification
Each delivery carries three headers:
X-Clipia-Webhook-Id: 1f2e3d4c-5b6a-7890-abcd-ef0123456789
X-Clipia-Timestamp: 1717243200
X-Clipia-Signature: t=1717243200,v1=5257a869e7...The signature is HMAC_SHA256(secret, "{timestamp}.{raw_body}"), where secret is the webhook signing secret from your developer console. Compute it over the raw request body (before JSON parsing) and compare with a constant-time function.
import crypto from "node:crypto";
const secret = process.env.CLIPIA_WEBHOOK_SECRET;
function verify(req) {
const sig = req.headers["x-clipia-signature"]; // "t=...,v1=..."
const parts = Object.fromEntries(sig.split(",").map(p => p.split("=")));
const signed = `${parts.t}.${req.rawBody}`;
const expected = crypto.createHmac("sha256", secret).update(signed).digest("hex");
const ok = crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300; // 5-min window
return ok && fresh;
}import hashlib
import hmac
import os
import time
SECRET = os.environ["CLIPIA_WEBHOOK_SECRET"].encode()
def verify(headers: dict, raw_body: bytes) -> bool:
sig = headers["X-Clipia-Signature"] # "t=...,v1=..."
parts = dict(p.split("=", 1) for p in sig.split(","))
signed = f"{parts['t']}.".encode() + raw_body
expected = hmac.new(SECRET, signed, hashlib.sha256).hexdigest()
ok = hmac.compare_digest(parts["v1"], expected)
fresh = abs(time.time() - int(parts["t"])) < 300 # 5-minute window
return ok and freshCheck freshness
Reject a delivery if X-Clipia-Timestamp differs from the current time by more than 5 minutes. This protects against replay of old requests.
Delivery and retries
- Respond with a
2xxstatus within 10 seconds. - On a non-
2xxresponse or timeout, delivery is retried with exponential backoff, up to 6 attempts. - Delivery history is visible in the developer console, which is handy for debugging.
Handle deliveries idempotently
The same delivery may arrive more than once. Deduplicate by request_id (or X-Clipia-Webhook-Id) before running any side effects.