Skip to content

API

Receive webhooks when a job ends

Have Snipri post each job to your server when it ends: set the endpoint, read the event, check its signature, answer in time and replace the secret.

On this page

Rather than read a job until it ends, your server can be told. When a job made through the API completes or fails, Snipri posts it to an address you choose, with its clips and the links to download them.

Set the endpoint

#
  1. Open the account menu and choose API.
  2. Under Webhook, type the address your server receives at in Endpoint URL.
  3. Press Save.

The address has to meet these rules:

  • It starts with https:// and uses the standard port, 443.
  • It is at a public address, with a full domain name.
  • It carries no user name or password, and is at most 2048 characters long.
  • Redirects are not followed, so save the address that answers.

Saving an address makes the account's signing secret, if it has none yet. To stop the deliveries, clear the field and press Remove. Under the field, the screen says how the last delivery went: the status your server answered with, or that it could not be reached.

A job can also name an address of its own as webhookUrl in the request that makes it, and its events go there in place of the account's. Set clip options in an API request lists the field.

The events

#
EventWhen it is sent
job.completedA job made through the API finished, with its clips, or with none when nothing was worth clipping.
job.failedA job made through the API stopped before it gave clips.

Only jobs made with a key are posted. A job started in the Studio is not, even on an account with an endpoint.

What a delivery carries

#

Each event is a POST with a JSON body. Its data is the job as reading it would give, with its clips and fresh links to them, so there is nothing to fetch before you can use it.

Body
{
  "type": "job.completed",
  "timestamp": "2026-11-02T18:24:12.000Z",
  "data": {
    "id": "cmhj2b9xk0003l708s4f1z9qa",
    "status": "completed",
    "name": "episode-12",
    "origin": "api",
    "source": {
      "type": "url",
      "url": "https://example.com/episode-12.mp4"
    },
    "durationSec": 3542.5,
    "videos": 1,
    "error": null,
    "createdAt": "2026-11-02T18:00:00.000Z",
    "completedAt": "2026-11-02T18:24:10.000Z",
    "clips": [
      {
        "id": "cmhj2kq4e0007l708u2m3c8vd",
        "index": 0,
        "part": 0,
        "title": "Launch week, honestly",
        "hook": "Nobody tells you this about launch week",
        "reason": "A clear question and a complete answer, and the last line lands without anything said before it.",
        "scores": {
          "hook": 8,
          "payoff": 8,
          "standalone": 9,
          "arc": 7,
          "quotability": 7
        },
        "startSec": 312.4,
        "endSec": 349.9,
        "durationSec": 37.5,
        "videoUrl": "https://...",
        "thumbnailUrl": "https://...",
        "urlsExpireAt": "2026-11-02T19:24:12.000Z"
      }
    ]
  }
}
HeaderWhat it carries
webhook-idThe event's id, the same on every attempt to deliver it. Use it to drop a repeat.
webhook-timestampWhen this attempt was signed, in seconds since 1 January 1970, UTC.
webhook-signaturev1, a comma and the signature in base64. While a replaced secret still signs, there are two, with a space between them.
content-typeapplication/json
user-agentSnipri-Webhooks/1.0 (+https://www.snipri.com)

Check the signature

#

Every delivery is signed in the Standard Webhooks format, so a library made for it can check one with your signing secret. To find the secret, press Show secret under Signing secret on the API screen, and keep it on your server as you keep a key. To check a delivery yourself:

  1. Take the body exactly as it arrived, before any parsing. Parsed and written out again, it is no longer the text that was signed.
  2. Join the webhook-id header, a dot, the webhook-timestamp header, another dot and the body.
  3. Sign that with HMAC-SHA256, keyed with the part of the secret after whsec_, decoded from base64.
  4. Compare the result, in base64, with each entry of the webhook-signature header after its v1 and comma, in constant time. One match is enough.
  5. Refuse a delivery whose timestamp is more than a few minutes from your own clock, so an old one sent again cannot pass.
Check a delivery in Node.js
import { createHmac, timingSafeEqual } from "node:crypto";

// secret: the signing secret from the API screen, whsec_...
// headers: the request's headers, with lowercase names
// body: the request's raw body, exactly as it arrived
export function verifySnipriWebhook(secret, headers, body, toleranceSeconds = 300) {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  if (!id || !timestamp) return false;
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!(age <= toleranceSeconds)) return false;
  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${body}`).digest();
  return String(headers["webhook-signature"] ?? "").split(" ").some((entry) => {
    const [version, signature] = entry.split(",");
    if (version !== "v1" || !signature) return false;
    const given = Buffer.from(signature, "base64");
    return given.length === expected.length && timingSafeEqual(given, expected);
  });
}
Use it in an Express route
const { SNIPRI_WEBHOOK_SECRET } = process.env;

app.post("/snipri-webhook", express.raw({ type: "application/json" }), (req, res) => {
  const body = req.body.toString("utf8");
  if (!verifySnipriWebhook(SNIPRI_WEBHOOK_SECRET, req.headers, body)) {
    return res.sendStatus(400);
  }
  const event = JSON.parse(body);
  if (event.type === "job.completed") {
    // event.data is the job, with its clips
  }
  res.sendStatus(204);
});

Answer in time

#

Answer with any 2xx status within 10 seconds. Any other answer, or none in that time, is tried again up to 3 more times. Each attempt is signed afresh and carries the job as it is then, with fresh clip links.

Answer first and do the work after: a server that downloads the clips before it answers can run out of time and be sent the same event again. The webhook-id is the same on every attempt, so keep the ids you have handled and drop a repeat.

Note: Now and then a job that went quiet is failed, and job.failed is sent, before its clips arrive after all. Then job.completed follows for the same job. Treat job.completed as the last word on a job.

Replace the secret

#
  1. Under Signing secret, press Replace secret, then Replace it now to confirm.
  2. Copy the new secret from the screen and put it on your server.

For 24 hours after the secret is replaced, every delivery is signed with the new secret and the old one, two entries in webhook-signature, so your server can switch over whenever it suits. Replacing it again in that time drops the oldest.

The screen shows until when the previous secret keeps signing.