Skip to content
API Reference

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": "..." }
}

The output field takes one of three shapes depending on the model type: images: [{ url }], video: { url }, or audio: { url } for speech synthesis.

The webhook payload is slimmer than the REST response

Two differences that trip integrations up most often:

  1. original_url (the full-quality original) is not included in the webhook — fetch GET /v1/requests/:id when you need it.
  2. For multi-image results, only the first URL is delivered; the full list lives at the same request_id.

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 fresh

Check 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 2xx status within 10 seconds.
  • On a non-2xx response 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.