API & webhooks

API access and webhooks.

Use the read-only API, MCP server, and webhooks to connect your tools to a workspace. These features require Studio.

Getting a token Link to Getting a token#

  1. As a workspace owner or admin, open Workspace › Developers.
  2. Enter a token name that identifies the tool.
  3. Select Create token.
  4. Copy the token immediately. Screencake shows the complete token only once.

The Developers page shows only the first characters of previous tokens. To stop access with a token immediately, select Revoke.

Anyone with the token can read every production and script in its workspace. The audit log records token creation and revocation.

The API requires Studio. If a workspace leaves this plan, API requests return 402. Its tokens work again if the workspace has the plan again.

Authentication Link to Authentication#

Include the token in the Authorization header of each request. The base URL is https://screencake.com/api/v1.

curl -H "Authorization: Bearer sck_…" https://screencake.com/api/v1/workspace

Requests and responses use JSON. The content endpoint returns a file. URLs and bodies use the same 26-character public IDs as app links. Responses include Cache-Control: no-store.

Endpoints Link to Endpoints#

The API is read-only. Each endpoint below uses GET.

An OpenAPI 3.1 description of the endpoints is at https://screencake.com/api/v1/openapi.json. It does not require a token. The API catalog at https://screencake.com/.well-known/api-catalog (RFC 9727) links to the description, this page, and the health check at https://screencake.com/up.

Production endpoints include schedules, call sheets, day out of days, crew, cast, breakdowns, budgets, locations, tasks, shots, and time cards. These responses do not include email addresses and phone numbers. A token gives workspace access without a personal role for contact permissions.

Production data comes from the script document. For an encrypted script, these endpoints return 409 encrypted_script. Each production endpoint also returns an ETag header.

GET /workspace Link to GET /workspace#

The workspace and its members.

{
  "id": "01J8ZQ3K4X0000000000000000",
  "name": "Lantern Pictures",
  "created_at": "2026-01-14T09:12:00Z",
  "members": [
    { "id": "01J8…", "name": "Ada Wren", "email": "ada@lantern.example", "role": "owner" }
  ]
}

GET /projects Link to GET /projects#

Every production in the workspace.

{
  "projects": [
    { "id": "01J8…", "title": "Night Bus", "description": null, "created_at": "2026-02-03T18:40:11Z" }
  ]
}

GET /projects/:id Link to GET /projects/:id#

One production and its scripts. The list includes archived scripts. The archived_at field identifies them.

{
  "id": "01J8…",
  "title": "Night Bus",
  "description": null,
  "created_at": "2026-02-03T18:40:11Z",
  "scripts": [ { …script row… } ]
}

GET /projects/:id/scripts Link to GET /projects/:id/scripts#

The scripts in one production, including archived scripts.

{ "scripts": [ { …script row… } ] }

GET /scripts/:id Link to GET /scripts/:id#

One script record with an ETag header.

{
  "id": "01J8…",
  "title": "Night Bus",
  "project_id": "01J8…",
  "kind": "screenplay",
  "template": false,
  "page_format": "letter",
  "revision": "white",
  "encrypted": false,
  "archived_at": null,
  "content_version": 1757930118,
  "created_at": "2026-02-03T18:41:02Z",
  "updated_at": "2026-09-15T10:35:18Z",
  "metadata": {
    "logline": "A bus, at night.",
    "format": "Feature",
    "revision": "white",
    "title_page": { "title": "NIGHT BUS", "author": "Ada Wren" },
    "season_number": null,
    "episode_number": null
  }
}

GET /scripts/:id/content Link to GET /scripts/:id/content#

The script as an exported file. Use ?format=fountain, fdx, pdf, or docx. The default format is fountain. PDF options are revised=1, marks=1, comments=1, title=0, and stamp=Name.

INT. BUS - NIGHT

The bus idles.

GET /scripts/:id/versions Link to GET /scripts/:id/versions#

Saved versions, with the newest first. The automatic field is true for hourly captures.

{
  "versions": [
    { "id": "01J9…", "label": "Draft one", "automatic": false, "wordCount": 18240, "createdAt": "2026-09-12T16:02:44Z" }
  ]
}

GET /scripts/:id/schedule Link to GET /scripts/:id/schedule#

Each shoot day and its assigned scene ordinals. The response also includes the production calendar: preparation, shooting, post-production, milestones, and off periods.

{
  "days": [
    {
      "id": "01JA…",
      "title": "Day 1",
      "date": "2026-03-02",
      "unit": "2nd unit",
      "location": "Bus depot",
      "location_key": "01JB…",
      "first_call_at": "2026-03-02T07:00:00Z",
      "scene_ordinals": [12, 13, 4],
      "parking": null, "hospital": null, "weather": null, "safety": null, "notes": null,
      "created_at": "2026-02-10T09:00:00Z",
      "updated_at": "2026-02-28T17:04:00Z"
    }
  ],
  "events": [
    { "id": "01JC…", "kind": "prep", "title": "Prep week", "starts_on": "2026-02-23",
      "ends_on": "2026-02-27", "start_time": null, "notes": null,
      "created_at": "2026-02-01T11:00:00Z", "updated_at": "2026-02-01T11:00:00Z" }
  ]
}

GET /scripts/:id/call_sheets/:day_id Link to GET /scripts/:id/call_sheets/:day_id#

The current call sheet for one day. It includes scenes in shooting order, cast, breakdown tags, shots, crew, and call times. It does not include phone numbers.

{
  "day": {
    "id": "01JA…", "title": "Day 1", "date": "2026-03-02",
    "location": "Bus depot — 1 Depot Road",
    "location_coords": { "lat": 51.5072, "lng": -0.1276 },
    "unit": "2nd unit", "first_call_at": "2026-03-02T07:00:00Z",
    "parking": "Yard", "hospital": "St Thomas'", "weather": "Clear, high 9°C",
    "safety": null, "notes": null,
    "department_notes": [ { "department": "camera", "note": "Second body on the 35." } ],
    "scene_ordinals": [12, 13]
  },
  "events": [ { "id": "01JD…", "label": "Lunch", "starts_at": "2026-03-02T13:00:00Z", "ends_at": "2026-03-02T13:30:00Z" } ],
  "scenes": [ { "number": "12", "uid": "01JE…", "heading": "INT. BUS - NIGHT", "omitted": false,
    "int_ext": "INT", "location": "BUS", "time_of_day": "NIGHT", "eighths": 6 } ],
  "cast": [ { "character": "WIM", "actor": "Iris Vale", "scene_ordinals": [12] } ],
  "breakdown": [ { "category": "prop", "label": "Torch", "scene_heading": "INT. BUS - NIGHT" } ],
  "shots": [ { "scene_ordinal": 12, "scene_heading": "INT. BUS - NIGHT", "shot_type": "WIDE",
    "description": "The bus idles", "movement": "static", "angle": "eye", "lens": "35mm",
    "duration_min": 2, "setup": "A", "done": false } ],
  "crew": [ { "id": "01JF…", "name": "Ada Wren", "department": "camera", "position": "DP",
    "call_at": "2026-03-02T06:30:00Z", "note": null } ]
}

GET /scripts/:id/dood Link to GET /scripts/:id/dood#

The day out of days lists characters, work days, and daily codes. SW means start-work, W means work, and WF means work-finish. SWF identifies one-day work. H means hold. Characters with the most work days are first.

{
  "days": [ { "id": "01JA…", "title": "Day 1", "date": "2026-03-02" } ],
  "rows": [
    { "character": "WIM", "work_days": 2, "hold_days": 1, "total_days": 3,
      "days": [ { "day_id": "01JA…", "code": "SW" }, { "day_id": "01JB…", "code": "H" },
        { "day_id": "01JC…", "code": "WF" } ] }
  ],
  "unavailable": [ { "character": "WIM", "from": "2026-03-03", "to": "2026-03-03" } ]
}

GET /scripts/:id/crew Link to GET /scripts/:id/crew#

The crew list in call-sheet order. The response does not include email addresses, phone numbers, or rates.

{
  "crew": [
    { "id": "01JF…", "name": "Ada Wren", "department": "camera", "position": "DP",
      "call_offset_min": -30, "created_at": "2026-02-10T09:00:00Z", "updated_at": "2026-02-10T09:00:00Z" }
  ]
}

GET /scripts/:id/cast Link to GET /scripts/:id/cast#

The cast list includes each character, actor, agency, and agent name. It does not include contact details.

{
  "cast": [
    { "id": "01JG…", "character": "Wim", "character_key": "WIM", "actor_name": "Iris Vale",
      "agency": "Talent Co", "agent_name": "Nell Roe", "position": 1,
      "created_at": "2026-02-11T14:00:00Z", "updated_at": "2026-02-20T10:12:00Z" }
  ]
}

GET /scripts/:id/breakdown Link to GET /scripts/:id/breakdown#

Every breakdown tag in the script, ordered by category and label. The key field contains the document tag ID. Each row carries the item's element profile: description, notes, status (needed, sourced, on_hand, wrapped, or null), quantity, source, cost_cents in the budget currency (the currency field of GET /scripts/:id/budget), and the owning crew member. These fields are null or empty when the item has no profile.

{
  "breakdown": [
    { "key": "prop:torch", "category": "prop", "label": "Torch", "character": null,
      "set_name": "Night 1", "scene_heading": "INT. BUS - NIGHT",
      "description": "Brass hand torch, working", "notes": "", "status": "sourced",
      "quantity": 2, "source": "Prop house", "cost_cents": 12500,
      "owner_key": "01JH…", "owner_name": "Ada Props",
      "created_at": "2026-02-12T08:30:00Z", "updated_at": "2026-02-12T08:30:00Z" }
  ]
}

GET /scripts/:id/budget Link to GET /scripts/:id/budget#

The priced budget: settings, scenarios, accounts with their totals, every line in the view with its price, and the grand total. Money fields are integer hundredths of the currency. Use ?scenario= with a key from scenarios to price that scenario; the default is the base budget, and an unknown key returns 404. The lock is null until the budget is locked. A script with no budget returns the empty default budget.

{
  "currency": "USD", "days_per_week": 5, "fx": { "EUR": 1.08 },
  "lock": { "at": "2026-03-01T09:00:00Z", "by_name": "Ada Wren" },
  "scenarios": [ { "key": "01JK…", "name": "Six-day weeks" } ],
  "accounts": [
    { "key": "01JL…", "number": "2200", "name": "Set design", "group": "production",
      "totals": { "total_cents": 1250000, "actual_cents": 0, "committed_cents": 0,
        "etc_cents": 1250000, "efc_cents": 1250000, "variance_cents": 0, "locked_cents": 1250000 } }
  ],
  "lines": [
    { "key": "01JM…", "account": "01JL…", "description": "Production designer", "unit": "week",
      "qty": 5, "rate_cents": 250000, "pct_of": "total", "currency": null,
      "fringe_pct": 0, "fringe_key": null, "actual_cents": null, "committed_cents": null,
      "etc_cents": null, "locked_cents": 1250000, "link": null,
      "rate_link": { "type": "crew_day_rate", "crew_key": "01JF…", "crew_name": "Nell Roe" },
      "scenario": null, "note": "",
      "price": { "base_cents": 1250000, "fringe_cents": 0, "total_cents": 1250000, "reported": false,
        "actual_cents": null, "committed_cents": null, "etc_cents": 1250000, "efc_cents": 1250000,
        "variance_cents": 0, "locked_cents": 1250000 } }
  ],
  "grand": { "total_cents": 1250000, "actual_cents": 0, "committed_cents": 0,
    "etc_cents": 1250000, "efc_cents": 1250000, "variance_cents": 0, "locked_cents": 1250000 }
}

GET /scripts/:id/locations Link to GET /scripts/:id/locations#

Production locations include addresses, coordinates, parking, the nearest hospital, tags, and the script sets at each location. The response does not include keyholder contact details.

{
  "locations": [
    { "id": "01JB…", "name": "Bus depot", "address": "1 Depot Road", "lat": 51.5072, "lng": -0.1276,
      "notes": null, "parking": "Yard", "hospital": "St Thomas'", "tags": ["night"], "sets": ["BUS"],
      "created_at": "2026-02-09T12:00:00Z", "updated_at": "2026-02-09T12:00:00Z" }
  ]
}

GET /scripts/:id/tasks Link to GET /scripts/:id/tasks#

Production tasks include the assignee name. They do not include the assignee email address.

{
  "tasks": [
    { "id": "01JH…", "title": "Book the bus", "status": "open", "assignee_name": "Ada Wren",
      "due_on": "2026-03-01", "done_at": null, "scene_ordinal": 12, "shoot_day_key": "01JA…",
      "created_at": "2026-02-14T16:00:00Z", "updated_at": "2026-02-14T16:00:00Z" }
  ]
}

GET /scripts/:id/shots Link to GET /scripts/:id/shots#

The shot list in shooting order, from SHOT elements in the script.

{
  "shots": [
    { "key": "01JI…", "scene_ordinal": 12, "scene_heading": "INT. BUS - NIGHT", "shot_type": "WIDE",
      "description": "The bus idles", "movement": "static", "angle": "eye", "lens": "35mm",
      "duration_min": 2, "setup": "A", "done": false, "position": 0,
      "created_at": "2026-02-15T10:00:00Z", "updated_at": "2026-02-15T10:00:00Z" }
  ]
}

GET /scripts/:id/time_cards Link to GET /scripts/:id/time_cards#

Only approved time cards. The computed field contains the agreement calculations: hours, pay bands, night hours, meal penalty, day multiplier, and estimated pay in cents.

{
  "time_cards": [
    { "id": "01JJ…", "shoot_day_id": "01JA…", "crew_member_key": "01JF…", "name": "Ada Wren",
      "started_at": "2026-03-02T07:00:00Z", "meal_started_at": "2026-03-02T13:00:00Z",
      "meal_ended_at": "2026-03-02T13:30:00Z", "ended_at": "2026-03-02T20:00:00Z",
      "note": null, "status": "approved", "reviewed_at": "2026-03-03T09:00:00Z",
      "computed": { "hours": 12.5, "straight": 8.0, "overtime": 4.0, "double_time": 0.5,
        "night_hours": 0.0, "meal_penalty_cents": 0, "day_multiplier_pct": 100,
        "estimate_cents": 61250 },
      "created_at": "2026-03-02T20:05:00Z", "updated_at": "2026-03-03T09:00:00Z" }
  ]
}

GET /audit Link to GET /audit#

The workspace audit log, with the newest first and 50 records per page. To get the next page, use nextBefore as ?before=. At the end, nextBefore is null. Use ?group=security to filter by an event group listed under Webhooks.

{
  "events": [
    {
      "id": "01J9…",
      "action": "organization.mfa_required",
      "actor": "Ada Wren",
      "details": {},
      "createdAt": "2026-09-14T08:00:12Z"
    }
  ],
  "nextBefore": "01J9…"
}

ETags Link to ETags#

The script record and production endpoints return an ETag header. Its value changes when the script record, content, or records for that endpoint change.

To check for changes, send the previous value in If-None-Match. If the response is unchanged, the server returns 304 Not Modified without a body.

curl -H "Authorization: Bearer sck_…" -H 'If-None-Match: W/"…"' https://screencake.com/api/v1/scripts/01J8…

Rate limit Link to Rate limit#

The maximum is 600 requests per minute for each token. More requests return 429 with { "error": "rate_limited" } until the rate-limit window ends. Each token has its own limit.

Errors Link to Errors#

An error response contains a JSON error code:

  • 401 unauthorized — The token is missing, unknown, or revoked.
  • 402 upgrade_required — The workspace does not have Studio. The token works again if the workspace has that plan again.
  • 404 not_found — The workspace has no record with this ID.
  • 409 encrypted_script — The request asks for encrypted script content or production data. The server cannot decrypt it.
  • 422 unknown_format — The requested format is not fountain, fdx, pdf, or docx.
  • 429 rate_limited — The token exceeded 600 requests in one minute.

Listings include encrypted scripts with "encrypted": true. The API cannot return their content or production data.

MCP server Link to MCP server#

@screencake/mcp gives API access through a Model Context Protocol server. It uses stdio to connect an assistant to production data. For example, an assistant can identify scenes without shots or cast on a call sheet.

The server requires Node 20 or later. Set SCREENCAKE_API_TOKEN to a workspace token. To use another deployment, set SCREENCAKE_API_URL. Its default value is https://screencake.com/api/v1.

{
  "mcpServers": {
    "screencake": {
      "command": "npx",
      "args": ["-y", "@screencake/mcp"],
      "env": { "SCREENCAKE_API_TOKEN": "sck_…" }
    }
  }
}

The server has these tools:

  • list_projects — Every production in the workspace.
  • list_scripts — One production's scripts.
  • get_script — The script record, or script text with format=fountain|fdx.
  • get_schedule — Shoot days with their scene ordinals, and the production calendar.
  • get_call_sheet — One day's sheet: scenes, cast, breakdown, shots, crew grid.
  • get_dood — The day-out-of-days codes and the cast off periods.
  • get_crew — The crew list in call-sheet grid order.
  • get_cast — Character, actor, agency, agent name.
  • get_breakdown — Every breakdown tag, by category.
  • get_locations — Address, coordinates, parking, hospital, tags, sets.
  • get_tasks — Title, status, assignee, due date, scene, shoot day.
  • get_shots — The shot list: type, movement, angle, lens, setup, duration.
  • get_time_cards — Approved cards with their computed hours and estimate.
  • list_audit — The workspace audit log in pages.

Each tool uses GET and cannot change workspace data. Each token gives access to one workspace. The maximum is 600 requests per minute. Production responses do not include contact details.

API errors, such as not_found, upgrade_required, and encrypted_script, cause a failed tool call.

An agent can read /auth.md on the site root. The file tells the agent that a person creates the token on the Developers page and how a request sends it. Screencake does not publish OAuth metadata or an agent registration endpoint.

The skill screencake-api gives an agent the same instructions in Agent Skills format. The index at https://screencake.com/.well-known/agent-skills/index.json lists it with a SHA-256 digest. The AI catalog at https://screencake.com/.well-known/ai-catalog.json lists the OpenAPI description and the skill. Screencake does not publish an MCP server card. The server has no remote endpoint.

Webhooks Link to Webhooks#

Webhooks send workspace audit events to an HTTPS endpoint that you control. Add the endpoint under Workspace › Developers. Use a URL that resolves to a public address. Select the events to receive.

Copy the signing secret when you create the endpoint. Screencake shows the secret only once.

Events Link to Events#

These event groups match the audit page filters:

  • access.session.signed_in, script.opened, script.exported, membership.role_changed, membership.removed, membership.auto_joined, invite.sent, invite.revoked, invite.accepted
  • sharing.share_link.created, share_link.download_changed, share_link.revoked, share_link.viewed, share_link.downloaded, file.sent, announcement.sent
  • security.organization.mfa_required, organization.mfa_unrequired, organization.cloud_ai_forbidden, organization.cloud_ai_allowed, organization.link_passcode_required, organization.link_passcode_unrequired, organization.link_downloads_forbidden, organization.link_downloads_allowed, organization.link_max_age_set, organization.link_max_age_cleared, organization.encryption_required, organization.encryption_unrequired, organization.session_max_age_set, organization.session_max_age_cleared, script.encrypted, script.decrypted, sso.connected, sso.updated, sso.disconnected, sso.domain_verified, sso.enforced, sso.unenforced
  • scripts.organization.renamed, organization.exported, script.created, script.renamed, script.archived, script.unarchived, file.uploaded, file.removed, budget.locked, budget.unlocked
  • developers.api_token.created, api_token.revoked, webhook.created, webhook.updated, webhook.removed

Only Studio records sign-ins, script opens, exports, and guest views. Other events are available on every plan. Webhook delivery requires Studio.

Delivery Link to Delivery#

Each delivery uses POST with a JSON body and these headers:

  • X-Screencake-Event: the event name.
  • X-Screencake-Delivery: the event ID, which stays the same on retries.
  • X-Screencake-Signature: the signature and timestamp.

Use the event ID to prevent duplicate processing.

{
  "id": "01J9ABCD…",
  "type": "script.renamed",
  "created_at": "2026-09-15T10:35:18Z",
  "organization": { "id": "01J8…", "name": "Lantern Pictures" },
  "actor": { "id": "01J8…", "name": "Ada Wren" },
  "subject": { "type": "script", "id": "01J8…", "title": "Night Bus" },
  "details": { "script": "Night Bus", "from": "Untitled Script" }
}

For system or guest actions, actor is null. If an event has no single subject, subject is null. If the subject was deleted, its id is null. The event details remain available.

Verifying the signature Link to Verifying the signature#

The signature header has the form t=<unix seconds>,v1=<hex>. The v1 value is an HMAC-SHA256 signature of <t>.<raw body>, calculated with your signing secret.

  1. Calculate the signature from the received body bytes. Do not serialize the JSON again.
  2. Compare signatures with a constant-time comparison.
  3. Reject timestamps outside your permitted interval.
import { createHmac, timingSafeEqual } from 'node:crypto'

// rawBody: the request body as received — bytes, not re-serialised JSON.
export function verify(header, rawBody, secret, tolerance = 300) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
  const expected = createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex')
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) <= tolerance
  return fresh && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
}
require "openssl"

# raw_body: the request body as received — bytes, not re-serialised JSON.
def verify(header, raw_body, secret, tolerance: 300)
  parts = header.split(",").to_h { |pair| pair.split("=", 2) }
  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{parts["t"]}.#{raw_body}")
  fresh = (Time.now.to_i - parts["t"].to_i).abs <= tolerance
  fresh && OpenSSL.secure_compare(expected, parts["v1"])
end

Retries and failures Link to Retries and failures#

Return a 2xx response within ten seconds.

For a timeout, 5xx, or 429, Screencake makes up to 5 delivery attempts in total. The delay increases between attempts. Other responses count as failures without retries.

After 10 deliveries fail one after the other, Screencake turns off the endpoint. The Developers page shows its last error and failure count. To turn on the endpoint and reset the count, turn on Active. Events that occur while the endpoint is off are not sent later.