Developers
API and webhooks
Do you have an API or integrations? Yes: API keys, webhooks, Slack, Discord, Premiere.
A REST API with workspace API keys, signed webhooks for notes, replies, versions and approvals, Slack and Discord channels, the Premiere Pro panel, and an MCP server for Claude.
Sign in with an API key
A workspace owner or admin makes a key in Workspace settings → API keys (paid plans). It is shown once. Send it on every call:
Authorization: Bearer cr_live_…The API lives at https://cutroom-review.fly.dev. Every answer is JSON; an error carries a machine-readable code: { "code": "NOTE_NOT_FOUND", "error": "That note was deleted." }. A key acts as the person who made it, in their workspaces, and can be revoked from the same page.
curl https://cutroom-review.fly.dev/api/versions/VERSION_ID/notes \
-H "Authorization: Bearer cr_live_…" \
-H "Content-Type: application/json" \
-d '{"t": 42.0, "text": "Music dips too hard here"}'REST endpoints
| Call | What it does |
|---|---|
GET /api/me | Who the key belongs to, and their workspaces (ids, names, plan state). |
GET /api/workspaces/{workspace_id}/projects | The workspace's projects. |
GET /api/projects/{project_id} | A project's files, each with its versions (ids, numbers, approval, status) and note counts. |
GET /api/versions/{version_id}/review | One version: its media, approval and every note with its replies, timecode, author and status. |
POST /api/versions/{version_id}/notes | Leave a note at a time, or on the whole cut.{ "t": 42.5, "text": "Music dips too hard here" }
// or the whole cut: { "general": true, "text": "Great pacing overall" } |
POST /api/notes/{note_id}/replies | Reply in a note's thread.{ "text": "Fixed in V3" } |
POST /api/notes/{note_id}/resolve | Mark a note done (or open again).{ "resolved": true } |
POST /api/versions/{version_id}/approval | Set the approval.{ "status": "approved" } // or "needs_changes", "in_review" |
GET /api/versions/{version_id}/markers?format=csv | The notes as markers: resolve-edl, premiere-csv, fcpxml, avid-txt or csv. |
Timecodes are seconds from the start of the cut (t, and t2 for a range). Uploads and the full list are on the API reference.
Webhooks
Add an endpoint in Apps & integrations → Webhooks (owners and admins, paid plans): an https address, the events, and a project or every project. You get a signing secret once. Send test sends a sample event shaped like a real note.
note.created | Someone leaves a note (team-only notes are never sent). |
note.resolved | A note is marked done. |
reply.created | Someone replies in a note's thread. |
version.created | An upload finishes and starts processing. |
version.ready | A version has processed and plays in the review. |
approval.changed | Approved, Needs changes, or back to In review. |
share_link.viewed | Someone opens a share link (once per person every 30 minutes). |
Every event carries what a no-code tool needs, so there is no second call: the workspace, project, file and version with links, and for notes the words, the author, the timecode and whether it is about the whole cut. Team-only notes and their replies are never sent.
{
"id": "evt_Q2x9…",
"type": "note.created",
"created_at": "2026-10-27T14:03:11.204Z",
"data": {
"workspace": { "id": "…", "name": "Northlight Studio" },
"project": { "id": "…", "name": "Brand film", "url": "https://post-prod.io/p/…" },
"asset": { "id": "…", "name": "Brand film_v3.mp4" },
"version": { "id": "…", "number": 3, "status": "ready", "approval": "in_review", "url": "https://post-prod.io/r/…" },
"actor": { "name": "Maya Chen" },
"note": {
"id": "…", "short_id": "k3Pz9QaB", "kind": "note",
"text": "Cut to the product shot 2 frames earlier",
"status": "open", "whole_cut": false,
"timecode": "00:01:12", "at_seconds": 72.4, "to_seconds": null,
"author": { "name": "Maya Chen", "kind": "guest" },
"resolved_by": null, "created_at": "2026-10-27T14:03:10.981Z",
"url": "https://post-prod.io/r/…#n-k3Pz9QaB"
}
}
}X-PostProd-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>" with the endpoint's secret> |
X-PostProd-Timestamp | The same unix seconds as t. Refuse anything more than 5 minutes old (a replay). |
X-PostProd-Event | The event, e.g. note.created. A test from Send test is ping. |
X-PostProd-Delivery | The event's id (evt_…). The same on every retry: use it to drop duplicates. |
X-PostProd-Webhook-Id | Which of your endpoints this is. |
Answer with any 2xx within 10 seconds. Anything else is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours (6 tries). Redirects are not followed. After 3 events in a row fail every try, or at once on 410 Gone, the endpoint turns itself off and the workspace owner gets an email; turn it back on in Apps & integrations → Webhooks. Each endpoint keeps a log of its last 50 deliveries, with Send again.
Check the signature
HMAC-SHA256 of the timestamp, a dot, and the raw body, with your endpoint's secret, compared in constant time. Refuse anything older than 5 minutes.
Node.js
import crypto from "node:crypto";
import express from "express";
export function verifyPostProd(rawBody, header, secret, toleranceS = 300) {
const parts = Object.fromEntries(String(header).split(",").map((p) => p.split("=", 2)));
const t = Number(parts.t);
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceS) return false; // too old: a replay
const want = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const got = String(parts.v1 || "");
return got.length === want.length && crypto.timingSafeEqual(Buffer.from(want), Buffer.from(got));
}
const app = express();
// the raw bytes, exactly as sent: never JSON.parse and re-serialise before checking
app.post("/postprod", express.raw({ type: "application/json" }), (req, res) => {
const raw = req.body.toString("utf8");
if (!verifyPostProd(raw, req.get("X-PostProd-Signature"), process.env.POSTPROD_WEBHOOK_SECRET)) return res.sendStatus(400);
const event = JSON.parse(raw);
// drop duplicates by event.id (X-PostProd-Delivery), then act on event.type
res.sendStatus(204);
});Python
import hashlib, hmac, time
def verify_postprod(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
try:
t = int(parts.get("t", ""))
except ValueError:
return False
if abs(time.time() - t) > tolerance: # too old: a replay
return False
want = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(want, parts.get("v1", ""))
# Flask: verify_postprod(request.get_data(), request.headers["X-PostProd-Signature"], SECRET)Make
- In Make, create a scenario and add the Webhooks app → Custom webhook. Press Add, name it, and copy the address it gives you.
- In post-prod.io: Apps & integrations → Webhooks → paste the address, tick the events, and press Add webhook.
- Back in Make, press Run once. Then in post-prod.io press Send test: Make learns every field from it (note text, timecode, author, links).
- Add the next module (Trello, Google Sheets, Slack, Notion…) and map the fields. Turn the scenario on.
Make answers every delivery with “Accepted”. A Make webhook with no scenario attached for 5 days answers 410 Gone, and post-prod.io turns that endpoint off and emails the owner.
Zapier
- Webhooks by Zapier needs a paid Zapier plan. Make a Zap with the trigger Webhooks by Zapier → Catch Hook, and copy its URL.
- In post-prod.io: Apps & integrations → Webhooks → paste it, tick the events, and press Add webhook.
- Press Send test here, then Test trigger in Zapier. The note's fields appear under data → note.
- Add your action and map the fields (for one part of the event only, set Pick off a child key to data).
Make and Zapier cannot check the signature without a code step. Treat the hook's address as a secret, and use your own server (below) where you need the check.
Slack, Discord, Premiere, MCP
Post notes, replies, approvals and new versions to a Slack or Discord channel, put every note on your Premiere Pro sequence as a marker with the panel, or connect Claude and other assistants over MCP with an API key. All of it is in Integrations.
Something missing? Email contact@post-prod.io.
Get started
Every feature. $10 per admin a month ($8 billed yearly). Unlimited guests. Cancel any time.