ClippedDocs

API reference

Updates and webhooks

Two ways to hear what happened. Poll: GET /api/v1/clips with a cursor for clips, GET /api/v1/streamers for who is live — the simplest thing, and what an agent that can't receive HTTP does. Or have Clipped POST to a URL of yours as it happens: a webhook.

A webhook starts from the moment it is made; nothing from before is sent. It can be limited to some events and some channels. Its URL must be public https, and redirects are not followed.

Delivery is at least once, in order. An endpoint that does not answer 2xx within ten seconds is retried, backing off from 15 seconds to an hour, with what came after it held back so order is kept. Twenty failures in a row switch the webhook off; PATCH { "active": true } switches it back on from where it stopped. Each event has an id — if you see one twice, it is the same event.

Every request is signed. Clipped-Signature is t=<unix seconds>,v1=<hex>, where the hex is HMAC-SHA256 of <t>.<raw body> keyed with the webhook's secret. The secret is in the answer that makes the webhook, and is not shown again. Check the signature against the raw body, before parsing it, and refuse a t more than five minutes old.

Endpoints

GET/api/v1/webhooks

Your webhooks, and each one's last delivery.

POST/api/v1/webhooks

Make one.

{ url, events?, channels? }. Up to five.

PATCH/api/v1/webhooks/{id}

Change one, or switch it back on.

{ url?, events?, channels?, active? }.

POST/api/v1/webhooks/{id}/test

Send it a ping now.

Answers with how the URL responded.

DELETE/api/v1/webhooks/{id}

Delete one.

What is sent

Events

clip.readyclip

A new clip was found. data is the clip, as GET /api/v1/clips gives it; if its cut is uncut, fetching video cuts it. Its thumbnail is there when it could be made within a few seconds, else null: GET /api/v1/clips/{id}/thumbnail has it later.

clip.published{ clip, destination, url }

A clip went up somewhere — posted by hand, through the API, or on its own.

streamer.live{ channel, sessionId, title, game, startedAt }

A streamer you watch went live and is being recorded.

streamer.offline{ channel, sessionId }

That recording ended.

ping{ webhook, message }

Only from the test endpoint.

Examples

What arrivesJSON
POST /clipped HTTP/1.1
Content-Type: application/json
Clipped-Event: clip.ready
Clipped-Delivery: evt_5b1f…
Clipped-Signature: t=1791918000,v1=9c2e…

{
  "id": "evt_5b1f…",
  "type": "clip.ready",
  "createdAt": "2026-10-13T20:00:00.000Z",
  "data": {
    "id": "jynxzi-41234567:m12",
    "channel": "jynxzi",
    "title": "Jynxzi clutches a 1v4 on Villa",
    "score": 88,
    "seconds": 34.2,
    "cut": "ready",
    "video": "https://app.clipped.so/api/v1/clips/jynxzi-41234567%3Am12/video",
    "thumbnail": "https://cdn.clipped.so/thumbs/jynxzi-41234567/m12-5d0c….webp",
    "published": null
  }
}
Checking the signature (Node)JavaScript
import { createHmac, timingSafeEqual } from "node:crypto"

export function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")))
  const t = Number(parts.t)
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex")
  return expected.length === parts.v1?.length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
}

Use cases