ClippedDocs

Use case

Be told the moment a clip is ready

Have Clipped POST to your server when a clip is found, when one is posted, and when a streamer goes live or offline.

Instead of asking every few seconds, give Clipped a URL and it calls you. Events arrive in order, at least once, each signed with a secret only you and Clipped hold.

  1. 1Make the webhook

    The URL must be public https. events and channels narrow what is sent; leave them out for everything. The answer carries the webhook's secret, once: keep it.

    A webhook starts from the moment it is made. Nothing from before is sent.

    Shell
    curl -X POST https://app.clipped.so/api/v1/webhooks -H "Authorization: Bearer $KEY" \
      -H "Content-Type: application/json" \
      -d '{ "url": "https://example.com/clipped", "events": ["clip.ready", "clip.published"] }'
  2. 2Check the signature, then answer fast

    Clipped-Signature is t=<unix seconds>,v1=<hex>: HMAC-SHA256 of <t>.<raw body> with the secret. Check it against the raw body, before parsing, and refuse a t more than five minutes old.

    Answer 2xx within ten seconds and do the work afterwards. Anything else is retried, from 15 seconds up to an hour apart, and later events wait behind it so order is kept.

    A receiver (Node)JavaScript
    import { createHmac, timingSafeEqual } from "node:crypto"
    import { createServer } from "node:http"
    
    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))
    }
    
    createServer(async (req, res) => {
      let raw = ""
      for await (const chunk of req) raw += chunk
      if (!verify(raw, req.headers["clipped-signature"] ?? "", process.env.CLIPPED_WEBHOOK_SECRET)) {
        res.writeHead(401).end()
        return
      }
      res.writeHead(204).end() // answer first
    
      const event = JSON.parse(raw)
      if (event.type === "clip.ready") console.log("new clip", event.data.id, event.data.score, event.data.thumbnail)
      if (event.type === "clip.published") console.log("posted", event.data.clip.id, "to", event.data.destination, event.data.url)
    }).listen(8080)
  3. 3Send yourself a test

    A ping to the webhook now, and the answer says how your URL responded. GET /api/v1/webhooks shows each webhook's last delivery, which is where to look when events stop.

    Shell
    curl -X POST https://app.clipped.so/api/v1/webhooks/$WEBHOOK_ID/test -H "Authorization: Bearer $KEY"

Worth knowing

  • clip.ready is sent once, when the clip is found, which is often before its thumbnail exists. Clipped tries to make one first; when it can't in a few seconds thumbnail is null, and GET /api/v1/clips/{id}/thumbnail has it later.
  • Every event has an id. Seeing one twice means a retry of the same event: ignore the second.
  • Twenty failures in a row switch a webhook off. PATCH { "active": true } switches it back on from where it stopped.
  • An account can have five webhooks.