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:

  1. Read the raw request body. Do not parse and re-serialise the JSON first.
  2. Refuse the request if the timestamp is missing or more than 5 minutes from your server's clock.
  3. Compute the HMAC-SHA256 of the timestamp, a . and the raw body, with your secret as the key.
  4. 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 2xx response 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 id to 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.