PictureFlow API

Plug PictureFlow into your pipeline. Keep the tools you already built.

ShotGrid, ftrack, in-house trackers, publish tools and editorial scripts can read your shows and bids, link them to their own ids and hear about every change. If you’d rather not write the glue yourself, we’ll help you hook in.

API keys

Make a key in Settings. It sees only what you can see, can be limited to one show, and can be revoked any time.

Available now

Read and write

Read shows, shots, tasks, notes, people, bids and topsheets. Add and update shots (500 at a time), tasks, hours and notes, and publish versions for review straight from your publish tool.

Available now

Change alerts

Signed webhooks when a status changes, a note lands or a shot is added, plus a change feed for studios behind a firewall. Link your ShotGrid or in-house ids so nothing duplicates.

Available now

ShotGrid link

Connect a show to a ShotGrid project with your admin’s script key. Import shots and tasks, push awarded bids and statuses back.

Coming soon

Quick start

Three steps. About five minutes.

  1. 1. Make a key. In Settings › API keys, name it after the tool that will use it and pick what it can read. Copy it into your pipeline’s secrets store.
  2. 2. Check it works. Call /me. You get back who the key belongs to and what it may do.
  3. 3. Sync. Page through shots or tasks with updated_since and keep the newest updated_at for next time.
Shell
curl https://www.pictureflow.pro/api/v1/me \
  -H "Authorization: Bearer $PICTUREFLOW_KEY"
Python: incremental shot sync
import os, requests

API = "https://www.pictureflow.pro/api/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['PICTUREFLOW_KEY']}"

def shots(show_id, updated_since=None):
    """Every shot that changed since the last sync, one page at a time."""
    params = {"limit": 500}
    if updated_since:
        params["updated_since"] = updated_since
    while True:
        page = session.get(f"{API}/shows/{show_id}/shots", params=params).json()
        yield from page["data"]
        if not page["has_more"]:
            break
        params["cursor"] = page["next_cursor"]

for show in session.get(f"{API}/shows").json()["data"]:
    for shot in shots(show["id"]):
        print(show["code"], shot["code"], shot["status"], shot.get("bid_hours"))

How it works

The rules, in one place.

Keys act as you
Send Authorization: Bearer pf_live_…. A key sees exactly what its owner sees on the website: a vendor’s key gets that vendor’s shots, a client’s key gets client-facing shots and client notes. Keys can be limited to one show.
Permissions
  • tracker:read Shows, episodes, shots, tasks, notes, people and activity.
  • budgets:read Projects, versions, topsheets and VFX shot bids.
  • finance:read Day rates, vendor costs and contract value, for people who already see Finance.
  • tracker:write Add and update shots, tasks, notes and episodes, log hours, and link them to ShotGrid or in-house ids. Needs edit access to the show.
  • webhooks:manage Add, change and test webhooks, and read the change feed.
Pages
Lists return data, has_more and next_cursor. Pass the cursor back as ?cursor=. Up to 500 per page.
Limits
600 requests a minute per key. Every response carries RateLimit-Remaining; over the limit you get a 429 with Retry-After.
Errors
Always JSON: {"error": {"type", "code", "message", "request_id"}}. Types: invalid_request 400, authentication 401, plan_required 402, permission 403, not_found 404, conflict 409, rate_limited 429.
Format
snake_case JSON. Dates are YYYY-MM-DD, times are ISO 8601 in UTC, money is in the show or project currency. The full description is at openapi.json for Postman and code generators.

Writing back

Your tools change shows the same way people do.

Adding and changing shots, tasks, notes and episodes follows the website’s rules: the same permissions, status rules, notifications and activity, shown as “Sam Lee via ShotGrid sync”. Keys need the tracker:write permission and edit access to the show. During the 14-day trial keys are read-only; writing back comes with a plan.

Python: send shots, log hours
import uuid

def push_shots(show_id, sg_shots):
    """Send ShotGrid shots to PictureFlow: matched by ShotGrid id, then by code."""
    body = {
        "source": "shotgrid",
        "shots": [
            {"external_id": s["id"], "code": s["code"], "description": s["description"],
             "episode_code": s["episode"], "frames": s["frames"]}
            for s in sg_shots[:500]
        ],
    }
    r = session.post(f"{API}/shows/{show_id}/shots/batch", json=body,
                     headers={"Idempotency-Key": str(uuid.uuid4())})
    for item in r.json()["data"]:
        if not item["ok"]:
            print(item.get("code"), item["error"]["message"])

def log_hours(task_id, hours):
    """Time tracking: add one session's hours to a task."""
    session.patch(f"{API}/tasks/{task_id}", json={"add_hours": hours})
  • Safe to retry. Send an Idempotency-Key header (any unique string) with a POST or PATCH. If the connection drops and you send it again, you get the first answer back instead of a second shot.
  • Matched by your ids. Put external_ids on anything you create, or match a batch by external_id, and later syncs update instead of duplicating.
  • Nothing is deleted. The API adds and changes. To drop a shot, set its status to Omitted, the same as the website asks.

Publishing versions

Publish from your pipeline, review in PictureFlow.

When an artist publishes, your publish tool sends the version straight to the shot’s Review page: v001, v002 and on, with the shot’s lead told it’s ready. Approving it can finish the task, the same as on the website.

Python: send a version from a publish hook
from pictureflow import PictureFlow   # pictureflow.py, linked below

pf = PictureFlow()   # reads PICTUREFLOW_KEY

def on_publish(shot_id, step, version_path, review_mp4, still_jpg, sg_version_id):
    """Call from your publish hook: the original stays on your storage."""
    v = pf.publish_version(
        shot_id=shot_id,
        step_id=step,                                  # e.g. "comp"
        source_url=f"https://media.mystudio.example/{version_path}",
        review_file=review_mp4,                        # optional, hosted for review
        thumbnail_file=still_jpg,                      # optional
        comment="Sent from publish",
        external_ids={"shotgrid": sg_version_id},
    )
    print("Up for review:", v["shot_code"], v["name"], v["url"])
  • Your files stay yours. Point source_url at the original on your own server or storage. It costs nothing and shows as “Watch” in the Tracker.
  • Optional review copy. Upload a small H.264 or still for playback in PictureFlow. It counts against your render storage and is cleared after 30 days unless it’s approved or kept. Connected your own storage in Tracker Settings? Uploads go straight there instead.
  • A ready-made helper. pictureflow.py uploads, retries safely and sends the version in one call. It only needs requests.

Change alerts

Hear about changes the moment they happen.

Add a webhook in Settings › API keys, or through the API. PictureFlow POSTs each event to your https URL, signed with your secret. Answer with any 2xx within 8 seconds; otherwise we retry after 1 minute, 5 minutes, 30 minutes, 2 hours, 8 hours and 24 hours.

What arrives
POST /pictureflow  HTTP/1.1
Content-Type: application/json
PictureFlow-Event: shot.status_changed
PictureFlow-Delivery: cm3del01
PictureFlow-Signature: t=1793460652,v1=5f2b…

{
  "id": "cm3evt01",
  "type": "shot.status_changed",
  "api_version": "2026-11-01",
  "created_at": "2026-11-05T09:12:40.000Z",
  "show": { "id": "cm3show0001", "name": "Northbound", "code": "NB" },
  "actor": { "name": "Sam Lee", "source": "ui" },
  "data": {
    "object": { "object": "shot", "code": "NB101_010", "status": "crv",
                "external_ids": { "shotgrid": "1234" }, … },
    "previous": { "status": "int" }
  }
}
Python: check the signature
import hashlib, hmac, time

def verify(body: bytes, header: str, secret: str) -> bool:
    """Check a PictureFlow-Signature header: t=<unix>,v1=<hex HMAC-SHA256 of "t.body">."""
    parts = dict(p.split("=", 1) for p in header.split(","))
    t = parts.get("t", "0")
    if abs(time.time() - int(t)) > 300:  # older than 5 minutes: replayed
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))
  • Ignore your own changes. actor.source says where a change came from (ui, api, bid_sync), so a sync can skip echoes of what it just wrote.
  • Behind a firewall? Poll GET /events?after= with the last seq you read instead. Events are kept 30 days.
  • Team only, no money. Alerts go to people on a show’s team and never include rates, costs or contract values.

Reference

Every endpoint.

Base URL https://www.pictureflow.pro/api/v1

GET/me

Check a key

Who the key belongs to, its permissions and limits. Any key can call it.

Example response
{
  "object": "key_owner",
  "user": {
    "id": "cm3user01",
    "name": "Sam Lee",
    "email": "sam@example.com",
    "company": "Northbound Pictures"
  },
  "plan": {
    "state": "subscribed"
  },
  "key": {
    "id": "cm3key01",
    "name": "ShotGrid sync",
    "prefix": "pf_live_Ab3dE9xK",
    "scopes": [
      "tracker:read"
    ],
    "show_ids": [],
    "project_ids": []
  },
  "api_version": "2026-11-01",
  "rate_limit_per_minute": 600
}
GET/showstracker:read

List shows

Tracker shows the key's owner runs or was invited to. Add archived=true to include archived shows.

archivedquerytrue to include archived shows.
Example response
{
  "object": "list",
  "data": [
    {
      "object": "show",
      "id": "cm3show0001",
      "name": "Northbound",
      "code": "NB",
      "client": "Harbor Light",
      "status": "active",
      "archived": false,
      "start_date": "2026-11-01",
      "delivery_date": "2027-03-12",
      "hours_per_day": 10,
      "currency": "USD",
      "your_role": "owner",
      "steps": [
        {
          "id": "comp",
          "name": "Compositing",
          "short": "CP",
          "group": "2d",
          "color": "#5e5ce6"
        }
      ],
      "source_bid": null,
      "url": "https://tracker.pictureflow.pro/shows/cm3show0001"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
GET/shows/{showId}tracker:read

Get a show

One show with its pipeline steps. Day rates and contract value are included only when the key has finance:read and its owner sees Finance.

showIdpathThe show's id, from GET /shows.
Example response
{
  "object": "show",
  "id": "cm3show0001",
  "name": "Northbound",
  "code": "NB",
  "…": "…"
}
GET/shows/{showId}/episodestracker:read

List episodes

showIdpathThe show's id, from GET /shows.
Example response
{
  "object": "list",
  "data": [
    {
      "object": "episode",
      "id": "cm3ep0001",
      "code": "NB101",
      "name": "Pilot",
      "turnover": "2026-11-03",
      "temp_date": null,
      "picture_final": "2027-01-15",
      "final_delivery": "2027-02-01",
      "air_date": null,
      "sort": 0
    }
  ],
  "has_more": false,
  "next_cursor": null
}
GET/shows/{showId}/shotstracker:read

List shots and assets

Sorted oldest change first, so a sync can page through and keep the last updated_at for next time. Clients see client-facing shots only, without people or hours; vendors see their own shots.

showIdpathThe show's id, from GET /shows.
statusqueryOne or more status codes, comma separated.wtg · ip · int · crv · fin · dlv · hld · omt
kindqueryshot or asset.shot · asset
episode_idqueryOnly this episode.
codequeryOne or more shot codes, comma separated.
vendorqueryOnly shots awarded to this vendor.
external_sourcequeryWith external_id: find items by their id in another system, like shotgrid.
external_idqueryOne or more outside ids, comma separated.
updated_sincequeryOnly items changed at or after this ISO timestamp. Use it for incremental syncs.
limitqueryItems per page, 1–500. Default 100.
cursorquerynext_cursor from the previous page.
Example response
{
  "object": "list",
  "data": [
    {
      "object": "shot",
      "id": "cm3shot0001",
      "show_id": "cm3show0001",
      "kind": "shot",
      "code": "NB101_010",
      "sequence": "NB101_010",
      "episode_id": "cm3ep0001",
      "episode_code": "NB101",
      "description": "Sky replacement through the windshield, add snow",
      "status": "ip",
      "complexity": "medium",
      "frames": 96,
      "tags": [
        "sky"
      ],
      "due_date": "2026-12-04",
      "delivered_at": null,
      "thumbnail_url": "https://www.pictureflow.pro/api/v1/shots/cm3shot0001/thumbnail",
      "open_notes": 1,
      "cut_from_bid": false,
      "sort": 1,
      "assumptions": "Clean plate provided",
      "lead": "Alex Rivera",
      "vendor": null,
      "bid_hours": 62,
      "logged_hours": 18.5,
      "external_ids": {
        "shotgrid": "1234"
      },
      "url": "https://tracker.pictureflow.pro/shows/cm3show0001/shots?shot=cm3shot0001",
      "created_at": "2026-11-02T17:04:11.000Z",
      "updated_at": "2026-11-05T09:12:40.000Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
GET/shots/{shotId}tracker:read

Get a shot

shotIdpathThe shot's id.
Example response
{
  "object": "shot",
  "id": "cm3shot0001",
  "show_id": "cm3show0001",
  "kind": "shot",
  "code": "NB101_010",
  "sequence": "NB101_010",
  "episode_id": "cm3ep0001",
  "episode_code": "NB101",
  "description": "Sky replacement through the windshield, add snow",
  "status": "ip",
  "complexity": "medium",
  "frames": 96,
  "tags": [
    "sky"
  ],
  "due_date": "2026-12-04",
  "delivered_at": null,
  "thumbnail_url": "https://www.pictureflow.pro/api/v1/shots/cm3shot0001/thumbnail",
  "open_notes": 1,
  "cut_from_bid": false,
  "sort": 1,
  "assumptions": "Clean plate provided",
  "lead": "Alex Rivera",
  "vendor": null,
  "bid_hours": 62,
  "logged_hours": 18.5,
  "external_ids": {
    "shotgrid": "1234"
  },
  "url": "https://tracker.pictureflow.pro/shows/cm3show0001/shots?shot=cm3shot0001",
  "created_at": "2026-11-02T17:04:11.000Z",
  "updated_at": "2026-11-05T09:12:40.000Z"
}
GET/shots/{shotId}/thumbnailtracker:read

Download a shot's thumbnail

Returns the image itself (JPEG, PNG or WebP), or redirects to it.

shotIdpathThe shot's id.
Example response
(image bytes)
GET/shows/{showId}/taskstracker:read

List tasks

One task per shot and pipeline step. step_id matches the show's steps.

showIdpathThe show's id, from GET /shows.
statusqueryOne or more status codes, comma separated.wtg · ip · int · crv · fin · dlv · hld · omt
shot_idqueryOnly this shot's tasks.
step_idqueryOnly this pipeline step.
assigneequeryOnly tasks assigned to this name.
external_sourcequeryWith external_id: find items by their id in another system, like shotgrid.
external_idqueryOne or more outside ids, comma separated.
updated_sincequeryOnly items changed at or after this ISO timestamp. Use it for incremental syncs.
limitqueryItems per page, 1–500. Default 100.
cursorquerynext_cursor from the previous page.
Example response
{
  "object": "list",
  "data": [
    {
      "object": "task",
      "id": "cm3task0001",
      "show_id": "cm3show0001",
      "shot_id": "cm3shot0001",
      "shot_code": "NB101_010",
      "step_id": "comp",
      "step_name": "Compositing",
      "status": "ip",
      "start_date": "2026-11-10",
      "due_date": "2026-11-21",
      "assignee": "Alex Rivera",
      "bid_hours": 30,
      "logged_hours": 12,
      "external_ids": {},
      "updated_at": "2026-11-05T09:12:40.000Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
GET/tasks/{taskId}tracker:read

Get a task

taskIdpathThe task's id.
Example response
{
  "object": "task",
  "id": "cm3task0001",
  "…": "…"
}
GET/shows/{showId}/notestracker:read

List notes

Oldest first. Clients only see client notes.

showIdpathThe show's id, from GET /shows.
shot_idqueryOnly this shot's notes.
kindqueryclient or internal.client · internal
statusqueryopen or addressed.open · addressed
created_sincequeryOnly items created at or after this ISO timestamp.
limitqueryItems per page, 1–500. Default 100.
cursorquerynext_cursor from the previous page.
Example response
{
  "object": "list",
  "data": [
    {
      "object": "note",
      "id": "cm3note0001",
      "show_id": "cm3show0001",
      "shot_id": "cm3shot0001",
      "shot_code": "NB101_010",
      "author_name": "Jordan Park",
      "kind": "client",
      "body": "Warm the sky slightly on the last 20 frames.",
      "status": "open",
      "created_at": "2026-11-06T18:30:00.000Z",
      "resolved_at": null
    }
  ],
  "has_more": false,
  "next_cursor": null
}
GET/shows/{showId}/memberstracker:read

List the team

People on the show and their roles. Not available to client or vendor keys. Day rates need finance:read.

showIdpathThe show's id, from GET /shows.
Example response
{
  "object": "list",
  "data": [
    {
      "object": "member",
      "id": "cm3mem0001",
      "name": "Alex Rivera",
      "email": "alex@example.com",
      "role": "artist",
      "access": "edit",
      "step_id": "comp",
      "signed_in": true
    }
  ],
  "has_more": false,
  "next_cursor": null
}
GET/shows/{showId}/activitytracker:read

List activity

What changed on the show, oldest first. Not available to client or vendor keys.

showIdpathThe show's id, from GET /shows.
shot_idqueryOnly this shot's history.
created_sincequeryOnly items created at or after this ISO timestamp.
limitqueryItems per page, 1–500. Default 100.
cursorquerynext_cursor from the previous page.
Example response
{
  "object": "list",
  "data": [
    {
      "object": "activity",
      "id": "cm3act0001",
      "show_id": "cm3show0001",
      "shot_id": "cm3shot0001",
      "user_name": "Sam Lee",
      "text": "NB101_010 status → In progress",
      "created_at": "2026-11-05T09:12:40.000Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
GET/notes/{noteId}tracker:read

Get a note

noteIdpathThe note's id.
Example response
{
  "object": "note",
  "id": "cm3note0001",
  "…": "…"
}
GET/episodes/{episodeId}tracker:read

Get an episode

episodeIdpathThe episode's id.
Example response
{
  "object": "episode",
  "id": "cm3ep0001",
  "…": "…"
}
POST/shows/{showId}/shotstracker:write

Add a shot

Adds a shot or asset with its first tasks (by step_id from GET /shows/{showId}; without steps it gets a Compositing task). Any shot field can be set at the same time, and external_ids links it to your system. Needs to be on the show's team with edit access. Send an Idempotency-Key header so a retry can't add it twice.

showIdpathThe show's id, from GET /shows.
Example body
{
  "code": "NB101_020",
  "episode_code": "NB101",
  "description": "Wide of the harbor, add fog",
  "status": "wtg",
  "frames": 120,
  "due_date": "2026-12-11",
  "steps": [
    {
      "step_id": "roto",
      "bid_hours": 8
    },
    {
      "step_id": "comp",
      "bid_hours": 24
    }
  ],
  "external_ids": {
    "shotgrid": "1240"
  }
}
Example response
{
  "object": "shot",
  "id": "cm3shot0002",
  "show_id": "cm3show0001",
  "kind": "shot",
  "code": "NB101_020",
  "sequence": "NB101_010",
  "episode_id": "cm3ep0001",
  "episode_code": "NB101",
  "description": "Sky replacement through the windshield, add snow",
  "status": "wtg",
  "complexity": "medium",
  "frames": 96,
  "tags": [
    "sky"
  ],
  "due_date": "2026-12-04",
  "delivered_at": null,
  "thumbnail_url": "https://www.pictureflow.pro/api/v1/shots/cm3shot0001/thumbnail",
  "open_notes": 1,
  "cut_from_bid": false,
  "sort": 1,
  "assumptions": "Clean plate provided",
  "lead": "Alex Rivera",
  "vendor": null,
  "bid_hours": 62,
  "logged_hours": 18.5,
  "external_ids": {
    "shotgrid": "1240"
  },
  "url": "https://tracker.pictureflow.pro/shows/cm3show0001/shots?shot=cm3shot0001",
  "created_at": "2026-11-02T17:04:11.000Z",
  "updated_at": "2026-11-05T09:12:40.000Z"
}
PATCH/shots/{shotId}tracker:write

Change a shot

Send only the fields that change. A status change runs the show's status rules and sends change alerts, the same as on the website. vendor_cost needs finance:read and a producer. Vendors can change status, due date, lead and description on their own shots.

shotIdpathThe shot's id.
Example body
{
  "status": "ip",
  "lead": "Alex Rivera",
  "due_date": "2026-12-04"
}
Example response
{
  "object": "shot",
  "id": "cm3shot0001",
  "show_id": "cm3show0001",
  "kind": "shot",
  "code": "NB101_010",
  "sequence": "NB101_010",
  "episode_id": "cm3ep0001",
  "episode_code": "NB101",
  "description": "Sky replacement through the windshield, add snow",
  "status": "ip",
  "complexity": "medium",
  "frames": 96,
  "tags": [
    "sky"
  ],
  "due_date": "2026-12-04",
  "delivered_at": null,
  "thumbnail_url": "https://www.pictureflow.pro/api/v1/shots/cm3shot0001/thumbnail",
  "open_notes": 1,
  "cut_from_bid": false,
  "sort": 1,
  "assumptions": "Clean plate provided",
  "lead": "Alex Rivera",
  "vendor": null,
  "bid_hours": 62,
  "logged_hours": 18.5,
  "external_ids": {
    "shotgrid": "1234"
  },
  "url": "https://tracker.pictureflow.pro/shows/cm3show0001/shots?shot=cm3shot0001",
  "created_at": "2026-11-02T17:04:11.000Z",
  "updated_at": "2026-11-05T09:12:40.000Z"
}
POST/shows/{showId}/shots/batchtracker:write

Send many shots at once

Up to 500 shots per request. Each is matched first by external_id (with source), then by code. Matched shots are updated, and a different code renames them; the rest are added, unless create_missing is false. Each shot's result is reported separately, so one bad shot doesn't stop the others. Fields are the same as Add a shot. Updates are quick; adding hundreds of new shots takes longer, so a first import is smoothest in requests of 100 to 200.

showIdpathThe show's id, from GET /shows.
Example body
{
  "source": "shotgrid",
  "shots": [
    {
      "external_id": 1234,
      "code": "NB101_010",
      "status": "ip",
      "frames": 96
    },
    {
      "external_id": 1240,
      "code": "NB101_020",
      "episode_code": "NB101",
      "description": "Wide of the harbor, add fog"
    }
  ]
}
Example response
{
  "object": "batch_result",
  "created": 1,
  "updated": 1,
  "failed": 0,
  "data": [
    {
      "index": 0,
      "ok": true,
      "action": "updated",
      "id": "cm3shot0001",
      "code": "NB101_010"
    },
    {
      "index": 1,
      "ok": true,
      "action": "created",
      "id": "cm3shot0002",
      "code": "NB101_020"
    }
  ]
}
POST/shots/{shotId}/taskstracker:write

Add a task

Adds the task for one pipeline step, or updates it if the shot already has one (then the answer is 200, not 201). Takes the same fields as Change a task.

shotIdpathThe shot's id.
Example body
{
  "step_id": "fx",
  "assignee": "Morgan Diaz",
  "bid_hours": 16,
  "due_date": "2026-12-01"
}
Example response
{
  "object": "task",
  "id": "cm3task0002",
  "shot_id": "cm3shot0001",
  "step_id": "fx",
  "step_name": "FX",
  "status": "wtg",
  "assignee": "Morgan Diaz",
  "bid_hours": 16,
  "logged_hours": 0,
  "…": "…"
}
PATCH/tasks/{taskId}tracker:write

Change a task or log hours

Status, assignee, dates and hours. add_hours adds to the hours already logged, for time tracking tools that send each session (use logged_hours to set the total instead). A new assignee is told, and status changes run the show's status rules.

taskIdpathThe task's id.
Example body
{
  "status": "int",
  "add_hours": 2.5
}
Example response
{
  "object": "task",
  "id": "cm3task0001",
  "status": "int",
  "logged_hours": 14.5,
  "…": "…"
}
POST/shows/{showId}/notestracker:write

Leave a note

An internal or client note on a shot (by shot_id or shot_code), or a reply with parent_id. @mentions and the show's people are told, the same as on the website. Notes from a key show the key's name, like "Sam Lee via Review tool".

showIdpathThe show's id, from GET /shows.
Example body
{
  "shot_code": "NB101_010",
  "kind": "internal",
  "body": "v012 is up for review. @Jordan Park can you take a look?",
  "external_ids": {
    "shotgrid": "77120"
  }
}
Example response
{
  "object": "note",
  "id": "cm3note0002",
  "shot_code": "NB101_010",
  "author_name": "Sam Lee via Review tool",
  "kind": "internal",
  "status": "open",
  "…": "…"
}
PATCH/notes/{noteId}tracker:write

Change a note

Mark it addressed or open again, or change its text. The team can change any note; others only their own.

noteIdpathThe note's id.
Example body
{
  "status": "addressed"
}
Example response
{
  "object": "note",
  "id": "cm3note0001",
  "status": "addressed",
  "…": "…"
}
POST/shows/{showId}/episodestracker:write

Add an episode

Team only. Dates are YYYY-MM-DD.

showIdpathThe show's id, from GET /shows.
Example body
{
  "code": "NB102",
  "name": "The Crossing",
  "turnover": "2026-12-01",
  "final_delivery": "2027-02-20",
  "external_ids": {
    "shotgrid": "89"
  }
}
Example response
{
  "object": "episode",
  "id": "cm3ep0002",
  "code": "NB102",
  "name": "The Crossing",
  "…": "…"
}
PATCH/episodes/{episodeId}tracker:write

Change an episode

episodeIdpathThe episode's id.
Example body
{
  "picture_final": "2027-01-22"
}
Example response
{
  "object": "episode",
  "id": "cm3ep0001",
  "picture_final": "2027-01-22",
  "…": "…"
}
POST/shows/{showId}/uploadstracker:write

Upload a review copy or thumbnail

Step one of hosting a file. Say what you're sending; you get back an upload id and an address. PUT the file's bytes to upload.url with exactly the headers in upload.headers (no API key), then pass the id as review_upload_id or thumbnail_upload_id when you send the version. Review copies (MP4, MOV, WebM or an image) count against the account's render storage, are cleared after 30 days unless approved or kept, and can be up to 250 MB; thumbnails (JPEG, PNG, WebP) up to 10 MB. If the show's owner connected their own storage in Tracker Settings, the address points at their bucket instead: files up to 5 GB, nothing counted against our storage. The address works for an hour. Too big, or no storage left: 413 with code file_too_large or storage_full; send a source_url instead.

showIdpathThe show's id, from GET /shows.
Example body
{
  "kind": "review",
  "content_type": "video/mp4",
  "size": 18432000,
  "filename": "NB101_010_v012.mp4"
}
Example response
{
  "object": "upload",
  "id": "up_eyJzIjoi…",
  "kind": "review",
  "content_type": "video/mp4",
  "size": 18432000,
  "expires_at": "2026-11-06T18:02:11.000Z",
  "upload": {
    "url": "https://blob.vercel-storage.com/?pathname=…",
    "method": "PUT",
    "headers": {
      "authorization": "Bearer vercel_blob_client_…",
      "x-api-version": "9",
      "x-content-type": "video/mp4",
      "x-add-random-suffix": "0"
    }
  }
}
POST/shots/{shotId}/versionstracker:write

Send a version for review

Adds the shot's next version (v001, v002…) to the Review page and tells the shot's lead, the same as sending one on the website. Point source_url at the original on your own storage (free, shown as "Watch"), add a hosted review copy and thumbnail from POST /shows/{showId}/uploads, or both. Pick the task with task_id or step_id. Vendors can send versions on their own shots. keep (hold the review copy past 30 days) is for the show's team.

shotIdpathThe shot's id.
Example body
{
  "step_id": "comp",
  "source_url": "https://media.northbound.example/NB101_010/comp/v012/NB101_010_comp_v012.mov",
  "review_upload_id": "up_eyJzIjoi…",
  "thumbnail_upload_id": "up_eyJrIjoi…",
  "comment": "Sky warmed on the last 20 frames",
  "external_ids": {
    "shotgrid": "45210"
  }
}
Example response
{
  "object": "shot_version",
  "id": "cm3sv0012",
  "show_id": "cm3show0001",
  "shot_id": "cm3shot0001",
  "shot_code": "NB101_010",
  "task_id": "cm3task0001",
  "number": 12,
  "name": "v012",
  "status": "pending",
  "comment": "Sky warmed on the last 20 frames",
  "source_url": "https://media.northbound.example/NB101_010/comp/v012/NB101_010_comp_v012.mov",
  "review": {
    "url": "https://abc123.public.blob.vercel-storage.com/tracker/cm3show0001/api/NB101_010_v012.mp4",
    "content_type": "video/mp4",
    "size": 18432000,
    "removed_at": null
  },
  "thumbnail_url": "https://abc123.public.blob.vercel-storage.com/tracker/cm3show0001/api/NB101_010_v012.jpg",
  "keep": false,
  "submitted_by": "Sam Lee via Publish tool",
  "external_ids": {
    "shotgrid": "45210"
  },
  "url": "https://tracker.pictureflow.pro/shows/cm3show0001/shots?shot=cm3shot0001",
  "created_at": "2026-11-06T17:02:11.000Z",
  "reviewed_at": null
}
GET/shots/{shotId}/versionstracker:read

List a shot's versions

Newest first. Not available to client keys: clients review through review links.

shotIdpathThe shot's id.
Example response
{
  "object": "list",
  "data": [
    {
      "object": "shot_version",
      "id": "cm3sv0012",
      "show_id": "cm3show0001",
      "shot_id": "cm3shot0001",
      "shot_code": "NB101_010",
      "task_id": "cm3task0001",
      "number": 12,
      "name": "v012",
      "status": "pending",
      "comment": "Sky warmed on the last 20 frames",
      "source_url": "https://media.northbound.example/NB101_010/comp/v012/NB101_010_comp_v012.mov",
      "review": {
        "url": "https://abc123.public.blob.vercel-storage.com/tracker/cm3show0001/api/NB101_010_v012.mp4",
        "content_type": "video/mp4",
        "size": 18432000,
        "removed_at": null
      },
      "thumbnail_url": "https://abc123.public.blob.vercel-storage.com/tracker/cm3show0001/api/NB101_010_v012.jpg",
      "keep": false,
      "submitted_by": "Sam Lee via Publish tool",
      "external_ids": {
        "shotgrid": "45210"
      },
      "url": "https://tracker.pictureflow.pro/shows/cm3show0001/shots?shot=cm3shot0001",
      "created_at": "2026-11-06T17:02:11.000Z",
      "reviewed_at": null
    }
  ],
  "has_more": false,
  "next_cursor": null
}
GET/shows/{showId}/versionstracker:read

List a show's versions

Oldest first, paged. Vendors see their own shots' versions.

showIdpathThe show's id, from GET /shows.
shot_idqueryOnly this shot's versions.
statusquerypending, notes or approved, comma separated.pending · notes · approved
created_sincequeryOnly items created at or after this ISO timestamp.
external_sourcequeryWith external_id: find items by their id in another system, like shotgrid.
external_idqueryOne or more outside ids, comma separated.
limitqueryItems per page, 1–500. Default 100.
cursorquerynext_cursor from the previous page.
Example response
{
  "object": "list",
  "data": [
    {
      "object": "shot_version",
      "id": "cm3sv0012",
      "show_id": "cm3show0001",
      "shot_id": "cm3shot0001",
      "shot_code": "NB101_010",
      "task_id": "cm3task0001",
      "number": 12,
      "name": "v012",
      "status": "pending",
      "comment": "Sky warmed on the last 20 frames",
      "source_url": "https://media.northbound.example/NB101_010/comp/v012/NB101_010_comp_v012.mov",
      "review": {
        "url": "https://abc123.public.blob.vercel-storage.com/tracker/cm3show0001/api/NB101_010_v012.mp4",
        "content_type": "video/mp4",
        "size": 18432000,
        "removed_at": null
      },
      "thumbnail_url": "https://abc123.public.blob.vercel-storage.com/tracker/cm3show0001/api/NB101_010_v012.jpg",
      "keep": false,
      "submitted_by": "Sam Lee via Publish tool",
      "external_ids": {
        "shotgrid": "45210"
      },
      "url": "https://tracker.pictureflow.pro/shows/cm3show0001/shots?shot=cm3shot0001",
      "created_at": "2026-11-06T17:02:11.000Z",
      "reviewed_at": null
    }
  ],
  "has_more": false,
  "next_cursor": null
}
GET/shot_versions/{versionId}tracker:read

Get a version

versionIdpathThe version's id (from a shot_version, not a budget version).
Example response
{
  "object": "shot_version",
  "id": "cm3sv0012",
  "show_id": "cm3show0001",
  "shot_id": "cm3shot0001",
  "shot_code": "NB101_010",
  "task_id": "cm3task0001",
  "number": 12,
  "name": "v012",
  "status": "pending",
  "comment": "Sky warmed on the last 20 frames",
  "source_url": "https://media.northbound.example/NB101_010/comp/v012/NB101_010_comp_v012.mov",
  "review": {
    "url": "https://abc123.public.blob.vercel-storage.com/tracker/cm3show0001/api/NB101_010_v012.mp4",
    "content_type": "video/mp4",
    "size": 18432000,
    "removed_at": null
  },
  "thumbnail_url": "https://abc123.public.blob.vercel-storage.com/tracker/cm3show0001/api/NB101_010_v012.jpg",
  "keep": false,
  "submitted_by": "Sam Lee via Publish tool",
  "external_ids": {
    "shotgrid": "45210"
  },
  "url": "https://tracker.pictureflow.pro/shows/cm3show0001/shots?shot=cm3shot0001",
  "created_at": "2026-11-06T17:02:11.000Z",
  "reviewed_at": null
}
PATCH/shot_versions/{versionId}tracker:write

Review or change a version

status (pending, notes, approved) and keep are for the show's team; approving can finish the task when the show's status rule is on, the same as on the website. Anyone who can work on the shot can change comment and source_url.

versionIdpathThe version's id (from a shot_version, not a budget version).
Example body
{
  "status": "approved"
}
Example response
{
  "object": "shot_version",
  "id": "cm3sv0012",
  "show_id": "cm3show0001",
  "shot_id": "cm3shot0001",
  "shot_code": "NB101_010",
  "task_id": "cm3task0001",
  "number": 12,
  "name": "v012",
  "status": "approved",
  "comment": "Sky warmed on the last 20 frames",
  "source_url": "https://media.northbound.example/NB101_010/comp/v012/NB101_010_comp_v012.mov",
  "review": {
    "url": "https://abc123.public.blob.vercel-storage.com/tracker/cm3show0001/api/NB101_010_v012.mp4",
    "content_type": "video/mp4",
    "size": 18432000,
    "removed_at": null
  },
  "thumbnail_url": "https://abc123.public.blob.vercel-storage.com/tracker/cm3show0001/api/NB101_010_v012.jpg",
  "keep": false,
  "submitted_by": "Sam Lee via Publish tool",
  "external_ids": {
    "shotgrid": "45210"
  },
  "url": "https://tracker.pictureflow.pro/shows/cm3show0001/shots?shot=cm3shot0001",
  "created_at": "2026-11-06T17:02:11.000Z",
  "reviewed_at": "2026-11-07T10:15:00.000Z"
}
GET/shot_versions/{versionId}/reviewtracker:read

Download the review copy

Returns the file, or redirects to it.

versionIdpathThe version's id (from a shot_version, not a budget version).
Example response
(video or image bytes)
GET/shot_versions/{versionId}/thumbnailtracker:read

Download the thumbnail

Returns the image, or redirects to it.

versionIdpathThe version's id (from a shot_version, not a budget version).
Example response
(image bytes)
GET/projectsbudgets:read

List projects

Budgets and bids from every PictureFlow app that the key's owner owns or was invited to.

archivedquerytrue to include archived projects.
Example response
{
  "object": "list",
  "data": [
    {
      "object": "project",
      "id": "cm3proj01",
      "name": "Northbound VFX bid",
      "client": "Harbor Light",
      "kind": "vfx",
      "type": "shot_bid",
      "app": "vfx",
      "currency": "USD",
      "archived": false,
      "your_role": "owner",
      "section_limited": false,
      "url": "https://vfx.pictureflow.pro/app/p/cm3proj01"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
GET/projects/{projectId}budgets:read

Get a project

The project with its versions and their totals.

projectIdpathThe project's id.
Example response
{
  "object": "project",
  "id": "cm3proj01",
  "…": "…",
  "versions": [
    {
      "object": "version",
      "id": "cm3ver01",
      "name": "Bid v2",
      "locked": false,
      "currency": "USD",
      "total": 412500
    }
  ]
}
GET/projects/{projectId}/versionsbudgets:read

List versions

projectIdpathThe project's id.
Example response
{
  "object": "list",
  "data": [
    {
      "object": "version",
      "id": "cm3ver01",
      "name": "Bid v2",
      "locked": false,
      "currency": "USD",
      "total": 412500
    }
  ],
  "has_more": false,
  "next_cursor": null
}
GET/versions/{versionId}/topsheetbudgets:read

Get a topsheet

Section and account totals, fringes, fees, discounts and actuals, calculated the same way as the app. Includes the locked-in bid beside each total when there is one.

versionIdpathThe version's id.
Example response
{
  "object": "topsheet",
  "version_id": "cm3ver01",
  "version_name": "Bid v2",
  "currency": "USD",
  "sections": [
    {
      "id": "s1",
      "code": "A",
      "name": "Pre-production",
      "kind": "btl",
      "subtotal": 18200,
      "fringe": 3100,
      "total": 21300,
      "actual": 0,
      "bid": 21300,
      "accounts": [
        {
          "id": "a1",
          "code": "1",
          "name": "Producer",
          "total": 6500,
          "actual": 0,
          "bid": 6500
        }
      ]
    }
  ],
  "sections_total": 371000,
  "fees": [
    {
      "name": "Production fee",
      "percent": 10,
      "base": 371000,
      "amount": 37100
    }
  ],
  "grand_total": 412500,
  "actual_total": 0
}
GET/versions/{versionId}/shotsbudgets:read

Get a VFX shot bid

Every shot and asset in a VFX bid with days per step and cost. Use it to create shots and tasks in your own tracker.

versionIdpathThe version's id.
Example response
{
  "object": "shot_bid",
  "version_id": "cm3ver01",
  "currency": "USD",
  "group_by": "episode",
  "hours_per_day": 10,
  "steps": [
    {
      "id": "comp",
      "name": "Compositing",
      "day_rate": 650,
      "days": 42,
      "cost": 27300
    }
  ],
  "shots": [
    {
      "object": "bid_shot",
      "id": "b1",
      "code": "NB101_010",
      "description": "Sky replacement",
      "status": "awarded",
      "complexity": "medium",
      "frames": 96,
      "days": {
        "comp": 3,
        "roto": 2
      },
      "total_days": 5,
      "cost": 3050
    }
  ],
  "assets": [],
  "counts": {
    "bid": 0,
    "awarded": 10,
    "omit": 0
  },
  "total": 61200
}
GET/webhookswebhooks:manage

List webhooks

Webhooks belonging to the key's owner.

Example response
{
  "object": "list",
  "data": [
    {
      "object": "webhook",
      "id": "cm3hook01",
      "url": "https://pipeline.example.com/pictureflow",
      "description": "Slack #vfx-status",
      "show_id": null,
      "events": [
        "shot.status_changed",
        "note.created"
      ],
      "active": true,
      "disabled_reason": null
    }
  ],
  "has_more": false,
  "next_cursor": null
}
POST/webhookswebhooks:manage

Add a webhook

PictureFlow will POST matching events to this https URL. events takes exact types, patterns like shot.* or * for everything. Leave show_id out for every show you're on the team of. The signing secret is only returned here, so store it.

Example body
{
  "url": "https://pipeline.example.com/pictureflow",
  "events": [
    "shot.status_changed",
    "task.status_changed",
    "note.created"
  ],
  "show_id": "cm3show0001",
  "description": "Slack #vfx-status"
}
Example response
{
  "object": "webhook",
  "id": "cm3hook01",
  "url": "https://pipeline.example.com/pictureflow",
  "description": "Slack #vfx-status",
  "show_id": null,
  "events": [
    "shot.status_changed",
    "note.created"
  ],
  "active": true,
  "disabled_reason": null,
  "secret": "whsec_…"
}
GET/webhooks/{webhookId}webhooks:manage

Get a webhook

webhookIdpathThe webhook's id.
Example response
{
  "object": "webhook",
  "id": "cm3hook01",
  "url": "https://pipeline.example.com/pictureflow",
  "description": "Slack #vfx-status",
  "show_id": null,
  "events": [
    "shot.status_changed",
    "note.created"
  ],
  "active": true,
  "disabled_reason": null
}
PATCH/webhooks/{webhookId}webhooks:manage

Change a webhook

Change its URL, events or description, or turn it off and on. Turning it back on clears a switch-off after repeated failures.

webhookIdpathThe webhook's id.
Example body
{
  "events": [
    "*"
  ],
  "active": true
}
Example response
{
  "object": "webhook",
  "id": "cm3hook01",
  "url": "https://pipeline.example.com/pictureflow",
  "description": "Slack #vfx-status",
  "show_id": null,
  "events": [
    "shot.status_changed",
    "note.created"
  ],
  "active": true,
  "disabled_reason": null
}
DELETE/webhooks/{webhookId}webhooks:manage

Delete a webhook

webhookIdpathThe webhook's id.
Example response
{
  "object": "webhook",
  "id": "cm3hook01",
  "deleted": true
}
POST/webhooks/{webhookId}/rotate_secretwebhooks:manage

Get a new signing secret

The old secret stops working straight away.

webhookIdpathThe webhook's id.
Example response
{
  "object": "webhook",
  "id": "cm3hook01",
  "url": "https://pipeline.example.com/pictureflow",
  "description": "Slack #vfx-status",
  "show_id": null,
  "events": [
    "shot.status_changed",
    "note.created"
  ],
  "active": true,
  "disabled_reason": null,
  "secret": "whsec_…"
}
POST/webhooks/{webhookId}/testwebhooks:manage

Send a test

Sends a ping event now and tells you what your server answered.

webhookIdpathThe webhook's id.
Example response
{
  "object": "webhook_delivery",
  "id": "cm3del01",
  "event_type": "ping",
  "status": "ok",
  "attempts": 1,
  "response_status": 200,
  "error": null
}
GET/webhooks/{webhookId}/deliverieswebhooks:manage

List deliveries

Recent deliveries, newest first: sent, retrying (with when) or failed.

webhookIdpathThe webhook's id.
limitqueryItems per page, 1–500. Default 100.
Example response
{
  "object": "list",
  "data": [
    {
      "object": "webhook_delivery",
      "id": "cm3del01",
      "event_id": "cm3evt01",
      "event_type": "shot.status_changed",
      "status": "failed",
      "attempts": 2,
      "response_status": 503,
      "error": "Your server answered 503.",
      "next_attempt_at": "2026-11-05T09:42:00.000Z"
    }
  ]
}
POST/webhooks/{webhookId}/deliveries/{deliveryId}/retrywebhooks:manage

Send a delivery again

webhookIdpathThe webhook's id.
deliveryIdpathThe delivery's id.
Example response
{
  "object": "webhook_delivery",
  "id": "cm3del01",
  "status": "ok"
}
GET/eventswebhooks:manage

Read the change feed

Every event on the shows you're on the team of, oldest first, kept 30 days. For studios that can't receive webhooks through their firewall: poll with after set to the last seq you read.

afterqueryThe seq of the last event you read.
show_idqueryOnly this show.
typesqueryOnly these event types, comma separated.
limitqueryItems per page, 1–500. Default 100.
Example response
{
  "object": "list",
  "data": [
    {
      "object": "event",
      "id": "cm3evt01",
      "seq": 1042,
      "type": "shot.status_changed",
      "show_id": "cm3show0001",
      "actor": {
        "name": "Sam Lee",
        "source": "ui"
      },
      "data": {
        "object": {
          "object": "shot",
          "code": "NB101_010",
          "status": "crv"
        },
        "previous": {
          "status": "int"
        }
      },
      "created_at": "2026-11-05T09:12:40.000Z"
    }
  ],
  "has_more": false,
  "last_seq": 1042
}

Already have a pipeline? We’ll help you hook in.

Whether you run ShotGrid, ftrack or tools you built yourselves, tell us how your studio publishes, reviews and delivers. We’ll help connect PictureFlow to it, in whichever direction you want the data to flow.