Skip to content

Webhooks

Slicer API webhooks — events about uploads, montage, export and publishing, the HMAC-SHA256 signature and verifying it in Node.js and Python, retries.

On this page

A webhook is an address on your server that Slicer calls on its own when something happens: an upload is ready, a montage is finished, a video is published. It's faster and cheaper than polling: no need to ask for the status every 5 seconds, and deliveries don't count against your rate limit.

Add a webhook

Requires the webhooks scope. The address must be a public https URL; up to 5 webhooks per account.

Terminal
curl https://api.slicer.media/v1/webhooks \
  -H "Authorization: Bearer $SLICER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/slicer/webhook","events":["project.succeeded","project.failed","export.succeeded"],"description":"Team chat notifications"}'
Response
{
  "ok": true,
  "data": {
    "webhook": {
      "id": "5f8e2a10-6c3b-4d7e-9f21-0a1b2c3d4e5f",
      "url": "https://example.com/slicer/webhook",
      "events": ["project.succeeded", "project.failed", "export.succeeded"],
      "description": "Team chat notifications",
      "enabled": true,
      "disabledReason": null,
      "failures": 0,
      "lastDeliveryAt": null,
      "createdAt": "2026-10-16T09:30:00.000Z"
    },
    "secret": "whsec_…"
  }
}

secret is the signing key. It's shown once: store it in an environment variable on your server, for example SLICER_WEBHOOK_SECRET. You can also add webhooks in the app: Profile → Developers → Webhooks.

Events

Event When it arrives data
upload.readyan upload was checked and is ready for a montage (including URL imports)the upload
upload.failedthe file was rejectedthe upload with the reason in error
project.succeededa montage, rebuild or retry finishedthe project
project.faileda run failed; the token reservation was returnedthe project with error
project.cancelleda run was stoppedthe project
project.version_readyvideo versions are waiting for a choice — arrives after project.succeeded, only if a montage produced several versions. A montage now produces one, so this event doesn't come: wait for project.succeededthe project with versionChoice
export.succeededexport is done, the file is updatedthe project
export.failedexport failed, the previous video is keptthe project with error
publication.publishedthe video was published to a social networkthe publication with url and projectId
publication.failedpublishing failedthe publication with errorCode and projectId
pinga webhook check (POST /v1/webhooks/{id}/test) — sent immediately, without retrieswebhookId and message

The list of events may grow (Versioning): skip unknown types and still answer 2xx.

Request format

Slicer sends a POST with a JSON body and these headers:

Request to your server
POST /slicer/webhook HTTP/1.1
Content-Type: application/json
User-Agent: Slicer-Webhooks/1.0 (+https://docs.slicer.media/api/webhooks/)
Slicer-Signature: t=1792152060,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Slicer-Event: project.succeeded
Slicer-Delivery: 48213

{
  "id": "evt_9a3c1f0e7b2d4c58a1e6f03b7d9c2e41",
  "type": "project.succeeded",
  "createdAt": "2026-10-16T09:41:00.000Z",
  "data": { "id": 318, "title": "A reel in 3 minutes", "status": "succeeded", "appUrl": "https://app.slicer.media/projects/318" }
}
  • id — a unique event id (evt_…); use it to drop duplicates.
  • type — the event type; it's also in the Slicer-Event header.
  • data — an object in the same shape the API returns (GET /v1/projects/{id}, GET /v1/uploads/{id} or a publication).
  • Slicer-Delivery — the delivery number: it makes the delivery easy to find in GET /v1/webhooks/{id}/deliveries.

Verifying the signature

The signature proves the request came from Slicer and wasn't modified on the way. The Slicer-Signature header contains the send time t (Unix seconds) and the signature v1 — a hex HMAC-SHA256 of the string <t>.<request body> keyed with secret.

How to verify:

  1. Take the raw request body — the bytes as they arrived, before JSON parsing. After parsing and re-serializing, the signature won't match.
  2. Compute HMAC-SHA256 over t, a dot and the body with your secret.
  3. Compare it with v1 in constant time (timingSafeEqual, hmac.compare_digest), not with a plain ==.
  4. Reject the request if t differs from the current time by more than 5 minutes — this protects against replaying an intercepted request.
JavaScript
Python
webhook.mjs
// Node.js 20+: npm install express
import { createHmac, timingSafeEqual } from 'node:crypto';
import express from 'express';

const TOLERANCE_SEC = 5 * 60;

/** true if the signature is valid and the request is no older than 5 minutes. */
export function verifySlicerSignature(rawBody, header, secret, nowMs = Date.now()) {
  if (typeof header !== 'string') return false;
  const pairs = header.split(',').map((part) => part.trim().split('='));
  const timestamp = pairs.find(([key]) => key === 't')?.[1];
  const signatures = pairs.filter(([key]) => key === 'v1').map(([, value]) => value);
  if (!timestamp || !/^\d+$/.test(timestamp) || signatures.length === 0) return false;
  if (Math.abs(nowMs / 1000 - Number(timestamp)) > TOLERANCE_SEC) return false;

  const expected = createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest();
  return signatures.some((value) => {
    const received = Buffer.from(value, 'hex');
    return received.length === expected.length && timingSafeEqual(received, expected);
  });
}

const app = express();
const seen = new Set(); // in production — a database or Redis

// express.raw: the body arrives as a Buffer, exactly as Slicer signed it
app.post('/slicer/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  if (!verifySlicerSignature(req.body, req.get('Slicer-Signature'), process.env.SLICER_WEBHOOK_SECRET)) {
    return res.status(400).send('invalid signature');
  }
  const event = JSON.parse(req.body.toString('utf8'));
  res.sendStatus(204); // answer fast, process afterwards

  if (seen.has(event.id)) return;
  seen.add(event.id);
  setImmediate(() => {
    if (event.type === 'project.succeeded') console.log(`Video ready: ${event.data.title} — ${event.data.appUrl}`);
  });
});

app.listen(3000);

Delivery and retries

  • Success — a 2xx response within 10 seconds. Answer right away and do long work after responding or in a queue.
  • Retries — on any other response, a timeout or a connection error: after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours — up to 7 attempts in total.
  • Redirects are not followed. A 3xx response counts as a failed delivery — use the final address.
  • Order isn't guaranteed, and the same event may arrive twice (for example, if your response got lost). Drop duplicates by id, and when in doubt take the current state from the API.
  • Disabling — after 50 failed delivery attempts in a row the webhook is turned off (enabled: false, the reason is in disabledReason), and your account email gets a message. Fix the server and turn the webhook back on: PATCH /v1/webhooks/{id} with {"enabled": true}.
  • The address is checked when the webhook is created and before every delivery: if it stops pointing to a public address, nothing is delivered.

Testing and the delivery log

Terminal
# send a test ping event
curl -X POST https://api.slicer.media/v1/webhooks/$WEBHOOK_ID/test -H "Authorization: Bearer $SLICER_TOKEN"

# the last 100 deliveries: status, response code, attempts
curl https://api.slicer.media/v1/webhooks/$WEBHOOK_ID/deliveries -H "Authorization: Bearer $SLICER_TOKEN"

Change the address, events or description with PATCH /v1/webhooks/{id}, delete with DELETE. All endpoints are in the reference.

What's next