Attaching a webhook#
Pass webhook_url in the body of POST /v1/renders — or at either level of a batch (row-level wins):
shell
curl -X POST https://api.anjin.design/v1/renders \
-H "Authorization: Bearer $ANJIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"template": "acme_banner_1200x630",
"modifications": [{"name": "headline", "text": "It shipped"}],
"webhook_url": "https://example.com/hooks/anjin"
}'The payload#
When the render completes, we POST JSON to your URL:
completion delivery
{
"id": "6f1c…",
"status": "completed",
"url": "https://…signed…", // ~1h, like output_url
"asset_url": "https://…permanent…",
"width": 1200,
"height": 630,
"credits_charged": 1,
"metadata": { "warnings": [] }
}credits_charged reflects scale — a scale-4 render says 16. Store asset_url, not url.
Verifying the signature#
Each delivery carries:
X-Anjin-Signature: sha256=<hex of HMAC-SHA256(secret, raw body)>
Fetch your signing secret once from the API (keep it server-side, like your API key):
GET/v1/webhook-secret
Verify with a constant-time comparison against the raw request body:
node
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody, signatureHeader, secret) {
const expected = "sha256=" +
createHmac("sha256", secret).update(rawBody, "utf8").digest("hex");
const a = Buffer.from(signatureHeader);
const b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
}Delivery rules#
| Rule | Notes | |
|---|---|---|
target | public URL | Private, loopback and link-local addresses are rejected — the endpoint must be reachable from the public internet. |
when | on completion | Delivered when the render completes. Failed renders are refunded rather than delivered; poll GET /v1/renders/:id if you need failure states pushed into your system. |
unsigned | reject | Treat a delivery without a valid signature as untrusted and drop it. |
