Throughline Docs

REST API v1

Throughline's external API exposes a production's graph — scenes, assets, schedule, approvals, deliveries, people, activity — to tools that live around the production rather than inside the app: pipeline scripts, reporting dashboards, delivery trackers, and similar integrations.

It is a read-mostly, write-carefully surface. The read side mirrors what crew see in the app, scoped to one production. The write side is deliberately narrow: create an asset, attach a note, decide an approval — because nothing in Throughline moves without an accountable person (or a key acting on one's behalf) behind it.

Most of what's below is one resource, one endpoint — the ordinary REST shape. A newer part of the surface — bundles, turnovers, media and purchase orders — is built the other way: one call that answers a whole question, assembled server-side and clamped to whatever the calling key may see. Both shapes share the same keys, the same scopes, and the same MCP door.

Base URL

https://throughline-backend-idkf.onrender.com/api/v1

This host is provisional for the pilot. All requests and JSON bodies use Content-Type: application/json.

Authentication

Every request carries a Throughline access key as a bearer token:

Authorization: Bearer tl_your_access_key_here

A key is minted by a production's admin, from inside Throughline, for a specific person or integration's use — one key, one label, one production. The full token is shown once, at creation; after that only a recognizable prefix is kept. There is no cross-production surface: a key minted on one production sees that production and nothing else.

Each key carries one or more scopes:

ScopeGrants
readEvery GET endpoint below except /v1/purchase-orders
writeEvery POST endpoint below except /v1/turnovers/{id}/submissions
submit:turnoverPOST /v1/turnovers/{id}/submissions — submitting work back on a delivery
read:moneyGET /v1/purchase-orders — a vendor's own purchase orders and cost-report line

A key can hold any combination. read/write are the default when a key is minted with no scopes named; submit:turnover and read:money are never on by default — they're for the engagement-bound shape below and have to be asked for explicitly.

Two shapes of key

Production-wide is the ordinary shape, and everything above this point describes it: it reads and writes across the whole production, scoped only by read/write.

Engagement-bound is the other shape — minted for one vendor organization's own automation, bound to that vendor's engagement on this production. It:

  • reaches only the bundles, turnovers, media and purchase-order surface below — every endpoint above this point refuses it outright with a 403, regardless of which scopes it holds;
  • on that surface, is clamped to exactly what its engagement has been granted — the identical reach a signed-in person seated on that same engagement would get, computed the same way;
  • is what a vendor's own pipeline uses instead of a person's login. There is no other way for an outside organization's automation to reach Throughline at all.

A production-wide key also works on the bundle/turnover/media/purchase-order surface — unclamped, the same production-scoped read it already has everywhere else. The engagement clamp only ever narrows; it is never a wider door than the key already has.

Auth errors

StatusMeaning
401Missing or malformed Authorization header, or an unknown/revoked key
403The key is valid but lacks the scope the endpoint needs
403An engagement-bound key called an endpoint outside the bundle/turnover/media/purchase-order surface

Revoking a key from Settings takes effect immediately.

Read endpoints

All of the following require the read scope.

GET /production

The production the key belongs to, with headline counts.

curl -s https://throughline-backend-idkf.onrender.com/api/v1/production \
  -H "Authorization: Bearer $TL_KEY"
{
  "id": 1,
  "title": "Example Production",
  "logline": "One line describing the show.",
  "status": "active",
  "phase": "production",
  "accent": "sea-glass",
  "format": "single",
  "stats": {
    "departments": 12,
    "open_approvals": 3,
    "deliveries_in_flight": 2,
    "scenes": 44,
    "nodes": 210
  }
}

An episodic production also carries "format": "episodic" and a season_number. accent is a key into a small curated palette, never a raw color — never document one you didn't see returned.

GET /departments

Every department on the production.

curl -s https://throughline-backend-idkf.onrender.com/api/v1/departments \
  -H "Authorization: Bearer $TL_KEY"
[
  {
    "id": 7,
    "slug": "props",
    "name": "Props",
    "cluster": "art",
    "phases": ["prep", "production"],
    "mission": "Every object an actor touches.",
    "key_roles": ["Props Master", "Props Assistant"],
    "privacy": "open",
    "unveiled": true
  }
]

GET /departments/{slug}

One department, with its people, its registry, and an asset count.

curl -s https://throughline-backend-idkf.onrender.com/api/v1/departments/props \
  -H "Authorization: Bearer $TL_KEY"
{
  "id": 7,
  "slug": "props",
  "name": "Props",
  "cluster": "art",
  "registry": {},
  "people": [
    { "id": 14, "name": "<person name>", "role": "Props Master", "is_hod": true }
  ],
  "assets": 38
}

GET /scenes

The current script breakdown, in script order.

curl -s https://throughline-backend-idkf.onrender.com/api/v1/scenes \
  -H "Authorization: Bearer $TL_KEY"
[
  {
    "node_id": 41,
    "number": "12",
    "slugline": "EXT. LIGHTHOUSE GALLERY - NIGHT",
    "synopsis": "One line describing the scene.",
    "status": "active",
    "revision_color": "Blue",
    "elements": { "props": ["Brass compass"], "cast": ["KEEPER"] }
  }
]

On an episodic production, every scene also carries episode_id and episode_code (e.g. "S2E7"); a single-output production's response omits both keys entirely rather than sending them as null.

GET /assets

Non-scene graph nodes — props, locations, characters, shots, and so on. Every optional query parameter narrows the list:

ParameterMeaning
departmentDepartment slug, e.g. props
kindNode kind, e.g. asset, character, location
statusOne of planned, active, blocked, done
curl -s "https://throughline-backend-idkf.onrender.com/api/v1/assets?department=props&status=active" \
  -H "Authorization: Bearer $TL_KEY"
[
  {
    "node_id": 112,
    "label": "Brass compass",
    "kind": "asset",
    "status": "active",
    "department": "props",
    "versions": 3,
    "updated_at": "2026-07-09T17:42:11Z"
  }
]

Rows are logistical — labels, statuses, version counts — never media.

GET /assets/{node_id}

One asset in full. Released versions carry media; anything not yet released comes back as a locked stub, the same rule the app applies to anyone outside the owning department.

curl -s https://throughline-backend-idkf.onrender.com/api/v1/assets/112 \
  -H "Authorization: Bearer $TL_KEY"

GET /schedule

Shoot days with their scenes.

curl -s https://throughline-backend-idkf.onrender.com/api/v1/schedule \
  -H "Authorization: Bearer $TL_KEY"
[
  {
    "id": 3,
    "day_number": 3,
    "date": "2026-07-10",
    "general_call": "06:30",
    "status": "scheduled",
    "scenes": [
      { "node_id": 41, "number": "12", "slugline": "EXT. LIGHTHOUSE GALLERY - NIGHT" }
    ]
  }
]

episode_id is a repeatable query parameter (?episode_id=1&episode_id=2) on episodic productions. It marks matching strips rather than dropping anything from the response, and has no effect on a single-output production.

GET /deliveries

Every delivery — inter-department and external — with its gating approval, if it has one.

curl -s https://throughline-backend-idkf.onrender.com/api/v1/deliveries \
  -H "Authorization: Bearer $TL_KEY"

GET /approvals

Approval chains. Filter with ?status=pending|approved|changes_requested|rejected.

curl -s "https://throughline-backend-idkf.onrender.com/api/v1/approvals?status=pending" \
  -H "Authorization: Bearer $TL_KEY"

GET /script-changes

Detected script-draft differences awaiting a decision. Defaults to ?status=pending; also accepts approved, dismissed, or all. On an episodic production, ?episode_id= scopes to one episode and ?all_episodes=true widens to every episode at once.

GET /activity

The production activity feed, newest first. ?limit= defaults to 50, capped at 200.

GET /people

The crew directory — name, role, department, and HOD/admin flags. No contact details.

GET /jobs

Recent generation jobs (image/video renders). ?limit= as for /activity.

Write endpoints

All of the following require the write scope.

POST /assets

Create a new graph node.

curl -s -X POST https://throughline-backend-idkf.onrender.com/api/v1/assets \
  -H "Authorization: Bearer $TL_KEY" \
  -H "Content-Type: application/json" \
  -d '{"label": "Hero lantern", "kind": "asset", "department": "props", "status": "active"}'

label is required. kind defaults to "asset"; scenes cannot be created here — they come from script breakdown. status must be one of planned, active, blocked, done.

POST /assets/{node_id}/notes

Attach a note to an asset. It lands on the asset's newest version if it has one, or as an activity entry if it doesn't — either way it's on the record.

curl -s -X POST https://throughline-backend-idkf.onrender.com/api/v1/assets/112/notes \
  -H "Authorization: Bearer $TL_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "Approved for hero use.", "author": "Editorial pipeline"}'

text is required. author is free text attributed to your integration if omitted.

POST /approvals/{approval_id}/decide

Advance the current step of an approval chain.

curl -s -X POST https://throughline-backend-idkf.onrender.com/api/v1/approvals/12/decide \
  -H "Authorization: Bearer $TL_KEY" \
  -H "Content-Type: application/json" \
  -d '{"decision": "approved", "decided_by": "Dana Whitfield", "note": "Signed off via external review tool."}'

decision must be "approved" or "changes". decided_by names the person making the call, and a write-scoped key is not a signature — if decided_by isn't the person whose step it actually is, the request is refused rather than silently recorded under someone else's name. Decision-mode approvals refuse an anonymous decision outright; multi-step chains permit omitting it.

One round trip, clamped to the caller

Seven endpoints, in four families — bundles, turnovers, media and purchase orders — built for the tools that don't want a resource each: they want one call that answers a whole question, assembled on the server and clamped to whatever the calling key may see. A production-wide key gets the unclamped answer it already gets everywhere else; an engagement-bound key gets exactly its own engagement's slice, and nothing here is a wider door than the resource-by-resource endpoints above already open.

GET /bundles/scene/{node_id}

Everything about one scene in a single call: its script state, its linked assets and their released versions, its shoot days, its approvals, its department notes. Requires read.

curl -s https://throughline-backend-idkf.onrender.com/api/v1/bundles/scene/41 \
  -H "Authorization: Bearer $TL_KEY"
{
  "version": 1,
  "scene": { "node_id": 41, "number": "12", "slugline": "EXT. LIGHTHOUSE GALLERY - NIGHT",
             "status": "active", "revision_color": "Blue", "elements": {} },
  "assets": [
    { "node_id": 112, "label": "Brass compass", "kind": "asset", "status": "active",
      "released_versions": [ { "id": 88, "version": 3, "variant": "A", "released": true } ] }
  ],
  "shoot_days": [ { "id": 3, "day_number": 3, "date": "2026-07-10", "status": "scheduled", "general_call": "06:30" } ],
  "approvals": [ { "id": 12, "title": "Compass hero build", "status": "pending", "mode": "chain", "due": "2026-07-12" } ],
  "department_notes": [ { "id": 210, "summary": "Approved for hero use.", "created_at": "2026-07-09T18:03:27Z" } ],
  "etag": "\"3f9c1a…\""
}

assets carries released versions only — the same rule every other read on this API follows. The response carries an ETag; send it back as If-None-Match on your next request and get a bare 304 when nothing has changed, rather than re-fetching the whole document.

GET /bundles/character/{node_id}

A character's costumes, plus the ingredient files that fed them — the relationship a pipeline can't reconstruct itself from a resource list. Requires read.

curl -s https://throughline-backend-idkf.onrender.com/api/v1/bundles/character/57 \
  -H "Authorization: Bearer $TL_KEY"
{
  "version": 1,
  "character": { "node_id": 57, "label": "Mara", "status": "active" },
  "costumes": [
    { "node_id": 88, "label": "Storm slicker", "status": "approved",
      "released_versions": [ { "id": 201, "version": 1, "variant": "A", "released": true } ],
      "ingredients": [ { "id": 40, "kind": "swatch", "label": "Oilskin swatch" } ] }
  ],
  "etag": "\"a01e2f…\""
}

GET /turnovers

The deliveries this key's engagement holds a grant against — read or submit, either counts. Requires read; empty (never an error) for a production-wide key or an ungranted engagement.

curl -s https://throughline-backend-idkf.onrender.com/api/v1/turnovers \
  -H "Authorization: Bearer $TL_VENDOR_KEY"
[
  { "id": 71, "title": "Costume turnover — batch 1", "status": "sent",
    "due": "2026-07-11", "external_to": "Meridian FX", "spec": [],
    "sent_at": "2026-07-01T09:00:00Z", "can_submit": true }
]

can_submit is whether this engagement specifically holds a submit grant on that delivery — a read-only grant lists the delivery here but answers false.

POST /turnovers/{id}/submissions

Work coming back. Requires submit:turnover and a live submit grant on this one delivery — either alone is refused. Multipart, not JSON:

curl -s -X POST https://throughline-backend-idkf.onrender.com/api/v1/turnovers/71/submissions \
  -H "Authorization: Bearer $TL_VENDOR_KEY" \
  -F "title=Batch 1 comps" \
  -F "note=Two shots, first pass" \
  -F "files=@shot_010.mov"
{
  "id": 14,
  "delivery_id": 71,
  "status": "submitted",
  "title": "Batch 1 comps",
  "note": "Two shots, first pass",
  "media": [ { "id": 501, "label": "shot_010.mov", "kind": "submission" } ],
  "created_at": "2026-07-02T14:20:00Z"
}

Every uploaded file becomes its own media object, below — never a raw path handed back. Nothing in the request names where a file lands on disk.

GET /media/{id} and GET /media/{id}/content

A piece of media, read by id rather than by guessing a path. /media/{id} returns its metadata; /media/{id}/content streams the bytes. Requires read.

curl -s https://throughline-backend-idkf.onrender.com/api/v1/media/501/content \
  -H "Authorization: Bearer $TL_VENDOR_KEY" -o shot_010.mov

Visible to a production-wide key unconditionally. For an engagement-bound key, work submitted on a turnover is visible only to the engagement that uploaded it — never through the delivery it was submitted against, even to another engagement holding a read grant on that same delivery. Vendors in film and TV are frequently competitors; isolation between them is the safe default, not an oversight.

GET /purchase-orders

A vendor's own purchase orders and its own cost-report line — nothing else money-shaped. Requires read:money and a live grant on the engagement. A key without the scope is refused with a 403, the same as any other missing scope; a key that holds the scope but has no engagement, or an engagement with no money grant, answers with an empty result rather than an error.

curl -s https://throughline-backend-idkf.onrender.com/api/v1/purchase-orders \
  -H "Authorization: Bearer $TL_VENDOR_KEY"
{
  "purchase_orders": [
    { "id": 90, "department_id": 12, "account": "1100", "vendor": "Meridian FX",
      "description": "VFX turnover batch 1", "amount": 40000.0, "status": "open", "date": "2026-06-01" }
  ],
  "line": [ { "account": "1100", "budget": 100000.0 } ],
  "week_of": "2026-06-01"
}

Never a production's whole topsheet, and never another vendor's rows.

Not yet in v1

The full budget topsheet across every department, and the assistant's chat interface, are part of the in-app surface today and are not yet exposed to external API keys — a vendor's own purchase orders above are the one slice of money data that is. If your integration needs the rest, talk to your Throughline contact.

MCP

Most of this page is also reachable over MCP — the same keys, the same scopes, the same clamped answers, a different transport. Fifteen tools cover the read surface, both bundles, turnovers and turnover submissions. What has no tool yet: /script-changes, /people, /jobs, the two /media reads, and the three write endpoints. Use REST for those.

Versioning

The path prefix is /api/v1. Changes within it are additive only — new endpoints, new optional fields, new query parameters. Nothing is renamed or removed inside v1; a breaking change would ship as /api/v2.

Timestamps are UTC ISO-8601. IDs are integers, stable within a production. Unknown fields may appear in any response at any time — parse leniently.