Webhooks
Get an HTTPS request on your server when Sirv AI Studio adds an asset or saves an AI result, and verify that the request came from Studio.
A webhook sends an HTTPS POST to your server when something happens in Studio, so your system can react without polling the API. For example, you can copy every new AI result into your own storage, or start a job when an image arrives.
Who can use this
- Plan: webhook deliveries need Growth (100,000 a month) or Enterprise (custom). You create and delete subscriptions through the API, and creating or deleting one with an API key needs full API access, which comes with Enterprise. See Who can use this.
- Permissions: creating and deleting subscriptions needs permission to connect and disconnect integrations. Owners and Admins have it by default.
- Scope: a subscription belongs to the person who created it, in one workspace. It gets events for assets that this person adds. Assets that teammates add do not trigger it.
There is no webhook page in Studio settings. Manage subscriptions with the API calls below.
Events
| Event | Studio sends it when |
|---|---|
asset.created |
You upload an image through an AI tool, the batch tool or a workflow source, and Studio adds it to the library. |
asset.processed |
An AI tool saves its main result to the library as a new asset. |
Studio sends only these two events. Uploads through the library Upload dialog and assets imported through the API do not send asset.created.
Payload
Every delivery has the same envelope. data depends on the event.
{
"id": "6f1d0c7e-2f4b-4c1e-9a55-3b0f8f6f2a10",
"event": "asset.processed",
"created_at": "2026-10-31T09:12:44.512Z",
"data": {
"asset_id": "b8a1e2c4-5d6f-4a7b-8c9d-0e1f2a3b4c5d",
"url": "https://example.com/results/handbag-black.png",
"operation": "remove-background",
"parent_asset_id": "1c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
"source": "processed",
"review_status": "pending_review"
}
}
| Field | Meaning |
|---|---|
id |
The delivery ID. It stays the same when Studio retries the delivery. |
event |
asset.created or asset.processed. |
created_at |
When Studio sent this attempt, in ISO 8601 UTC. |
data |
The event's fields, listed below. |
asset.created data:
| Field | Meaning |
|---|---|
asset_id |
The new asset's ID. |
url |
The file's URL. |
filename |
The file name. |
source |
Always upload. |
mime_type, file_size, width, height |
The file type, size in bytes and pixel size. Any of them can be missing. |
asset.processed data:
| Field | Meaning |
|---|---|
asset_id |
The new asset's ID. |
url |
The result file's URL. |
operation |
The AI tool that made it, such as remove-background or upscale. |
parent_asset_id |
The asset the tool started from, or null. |
source |
Always processed. |
review_status |
approved, or pending_review when the result is waiting for someone to review it. Do not publish a pending_review result. |
Each request also has these headers:
| Header | Value |
|---|---|
Content-Type |
application/json |
User-Agent |
SirvAIStudio-Webhook/1.0 |
X-Webhook-Event |
The event name. |
X-Webhook-ID |
The delivery ID, the same as id in the body. |
X-Webhook-Timestamp |
Unix time in seconds when Studio signed this attempt. Only sent when the subscription has a secret. |
X-Webhook-Timestamp-Signature |
sha256= and the HMAC-SHA256 of the timestamp, a . and the raw body. Only sent when the subscription has a secret. |
X-Webhook-Signature |
sha256= and the HMAC-SHA256 of the raw body alone. Kept for older receivers. Only sent when the subscription has a secret. |
Verify the signature
Give every subscription a secret, then check X-Webhook-Timestamp-Signature before you trust a request:
- Read the raw request body. Do not parse and re-serialise the JSON first.
- Refuse the request if the timestamp is missing or more than 5 minutes from your server's clock.
- Compute the HMAC-SHA256 of the timestamp, a
.and the raw body, with your secret as the key. - Compare it with the hex digest after
sha256=, using a constant-time comparison.
This Node.js server uses only built-in modules:
import { createHmac, timingSafeEqual } from "node:crypto";
import { createServer } from "node:http";
const SECRET = process.env.SIRV_STUDIO_WEBHOOK_SECRET;
const TOLERANCE_SECONDS = 300;
function isValid(rawBody, timestamp, signature) {
if (!/^\d+$/.test(timestamp ?? "")) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_SECONDS) return false;
const match = /^sha256=([0-9a-f]{64})$/.exec(signature ?? "");
if (!match) return false;
const expected = createHmac("sha256", SECRET).update(`${timestamp}.`).update(rawBody).digest();
return timingSafeEqual(expected, Buffer.from(match[1], "hex"));
}
createServer((req, res) => {
const chunks = [];
req.on("data", (chunk) => chunks.push(chunk));
req.on("end", () => {
const rawBody = Buffer.concat(chunks);
const timestamp = req.headers["x-webhook-timestamp"];
const signature = req.headers["x-webhook-timestamp-signature"];
if (!isValid(rawBody, timestamp, signature)) {
res.writeHead(401).end();
return;
}
const delivery = JSON.parse(rawBody.toString("utf8"));
// Store delivery.id and skip any delivery you have already handled.
console.log(delivery.event, delivery.data.asset_id);
res.writeHead(200).end();
});
}).listen(3000);
Do not fall back to X-Webhook-Signature when the timestamp check fails. It does not cover the timestamp, so it cannot stop an old request from being replayed.
Retries and failures
- Studio counts any
2xxresponse as delivered. Reply within 30 seconds, and do slow work after you reply. - If your server returns another status, times out or cannot be reached, Studio retries the delivery up to 5 more times, waiting longer each time.
- A delivery can arrive more than once. Use
idto skip duplicates. - If 10 deliveries in a row fail after all their retries, Studio disables the subscription. A successful delivery resets the count.
- When your workspace reaches its monthly delivery limit, Studio skips deliveries until the next month. It does not send them later.
Manage subscriptions
All three calls use your API key and work in the key's workspace.
Create a subscription
Send the event, an https:// URL that is reachable from the internet and a secret of at least 16 characters:
curl -X POST https://www.sirv.studio/api/webhooks \
-H "Authorization: Bearer $SIRV_STUDIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/hooks/sirv-studio",
"event": "asset.processed",
"secret": "'"$SIRV_STUDIO_WEBHOOK_SECRET"'"
}'
{
"id": "9e3b7a52-1c4d-4f8e-a6b0-2d7c5e9f1a34",
"url": "https://example.com/hooks/sirv-studio",
"event": "asset.processed",
"hasSecret": true
}
To get both events, create one subscription for each. You can have up to 10 active subscriptions for each event. Studio does not show the secret again, so keep a copy. To generate a strong secret, run openssl rand -hex 32.
List your subscriptions
curl https://www.sirv.studio/api/webhooks \
-H "Authorization: Bearer $SIRV_STUDIO_API_KEY"
The response lists your subscriptions in this workspace, newest first. Each one has id, url, event, hasSecret, disabled, failureCount and createdAt.
Delete a subscription
curl -X DELETE https://www.sirv.studio/api/webhooks/9e3b7a52-1c4d-4f8e-a6b0-2d7c5e9f1a34 \
-H "Authorization: Bearer $SIRV_STUDIO_API_KEY"
Studio returns {"deleted": true}. A disabled subscription stays disabled. To start receiving events again, delete it and create a new one.