Files
vfxreview/EXT_API_REFERENCE.md
twotalesanimation feb1c7b3e9 feat(render-pipeline): validation, QC and delivery builder
Completes RenderPipeline2 phases 3-5. Additive throughout: with
validation.enabled=false the pipeline behaves exactly as before.

Validation (§8) runs on the worker straight after a render, while it
still holds the job and the files are local. Everything is read from the
EXR header — one 64 KB read per sampled frame, no pixel decode — so the
engine needs no oiiotool. The render job deliberately stays open through
the stage, so a worker that dies there is reaped and retried instead of
stranding the export in VALIDATING.

Timecode is the primary check and is anchored on the plate, never the
API: measured on the live show, Shot.timecodeStart is the editorial
in-point and sits one handle-length (8f) ahead of the plate, so checking
against it would pass the defective exports and fail the correct ones.
Reference precedence is manifest timing plate -> plate found on disk ->
API value, and the API rank is WARN-only. The panel now resolves plate
provenance by walking the comp's own footage layers, so the worker never
guesses which element carries the comp's timing.

Verified against real studio renders via the new --validate mode: it
catches both defect classes seen in shipped work — the +8 handle offset
and frame-derived timecode (00:00:41:17 = frame 1001 at 24fps).

QC (§10) is one service behind two front doors, API-key for the panel
and session for the web. PASS means ready to upload and nothing else;
FAIL sets the comp task to CHANGES. The panel gains a QC section that
imports the real EXR sequence at full res rather than the H.264.

Delivery (§11) rides the existing queue as a DELIVERY_BUILD job: hard
links with a copy fallback, verification by re-hashing delivered bytes
against the validation-time digest, manifest.json and the CSV report.
fsutil hardlink list is unavailable over SMB, so link identity is never
queried — reading the bytes back covers links and copies alike.

Also fixes ApiClient.ProgressAsync throwing on transport failure, which
would have aborted work in flight during a brief server outage; progress
reporting is best-effort by contract.

Removes the stale RenderWorker/ rule from the root .gitignore. The
worker's source is tracked like the rest of the repo and its own
.gitignore already covers bin/, obj/ and publish/; the rule silently
excluded new files while the existing 20 stayed tracked.

Not yet applied: migration 20260813000000 was hand-written because the
dev database was unavailable — run prisma migrate deploy and verify
before relying on it. None of this has run against a live server.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 13:49:51 +02:00

15 KiB

VFXReview External API Reference

Base URL: https://review.twotalesvfx.com

All requests require an Authorization header. Two formats are accepted:

Authorization: Bearer <API_SECRET_KEY>
X-Api-Key: <API_SECRET_KEY>

Endpoints Overview

Method Path Description
GET /api/ext/projects List all projects with showIds
GET /api/ext/shots/lookup Look up a shot by code — primary pipeline endpoint
GET /api/ext/projects/{projectCode}/episodes List episodes for a project
GET /api/ext/projects/{projectCode}/shots List shots for a project by code
GET /api/ext/shots List shots by database project ID
GET /api/ext/shots/{shotId} Get a single shot by DB id or shot code
POST /api/ext/shots Create a new shot
PATCH /api/ext/shots/{shotId} Update shot fields (e.g. increment version)

Render pipeline (RenderPipeline2)

# Method Path Caller Description
E1 POST /api/ext/exports panel Queue Export — creates Export + RenderJob
E2 GET /api/ext/exports/latest panel Latest export + live status for the panel header
E3 GET /api/ext/exports/{exportId} panel/worker Export detail incl. validations and QC
E4 POST /api/ext/exports/{exportId}/qc panel QC Pass / QC Fail
E5 GET /api/ext/qc/queue panel Exports in READY_FOR_QC
E6 POST /api/ext/workers/register worker Register machine, receive tuning
E7 POST /api/ext/workers/{machineId}/heartbeat worker Heartbeat + cancel channel
E8 POST /api/ext/render/jobs/claim worker Atomically claim the next job
E9 PATCH /api/ext/render/jobs/{jobId}/progress worker Progress/ETA; renews the lease
E10 POST /api/ext/render/jobs/{jobId}/fail worker Report failure
E11 POST /api/ext/render/jobs/{jobId}/complete-render worker aerender finished → VALIDATING
E12 POST /api/ext/render/jobs/{jobId}/validation worker Report all validation results
E13 POST /api/ext/render/jobs/{jobId}/finalize worker Preview artifacts done → READY_FOR_QC
E14 POST /api/ext/exports/{exportId}/retry panel Retry a failed export
E20 POST /api/ext/render/jobs/{jobId}/artifact-presign worker Presigned upload URL
— POST /api/ext/render/jobs/{jobId}/delivery worker Report a DELIVERY_BUILD result

Session-authenticated twins for the web UI live under /api/render/* and /api/deliveries/* — same service layer, no API key.


Shot Status Values

Value Description
WAITING Not yet started
IN_PROGRESS Currently being worked on
IN_REVIEW Submitted for review
REVISIONS Changes requested
COMPLETE Approved and complete

Shot Priority Values

Value Description
LOW Low priority
NORMAL Default
HIGH High priority
URGENT Urgent

Shot Code Format

{showId}_{episode}_{scene}_{shotNumber}

e.g.  UNG_106_010_020
      ^^^  ^^^  ^^^  ^^^
      |    |    |    shot number (zero-padded)
      |    |    scene
      |    episode
      show ID

1. List Projects

GET /api/ext/projects

Returns all projects with their showId, code, and metadata. Useful for populating project selectors in pipeline tools or resolving a showId → projectCode mapping.

Query Parameters

Parameter Required Default Description
status No ACTIVE ACTIVE, ARCHIVED, or ALL

Response 200 OK

{
  "projects": [
    {
      "id": "cmp6l5mzq0001ua0gz07bk72f",
      "name": "Ungo Season 1",
      "code": "UNG_S1",
      "showId": "UNG",
      "projectType": "EPISODIC",
      "status": "ACTIVE",
      "startDate": "2026-01-01T00:00:00.000Z",
      "dueDate": "2026-12-01T00:00:00.000Z",
      "_count": { "shots": 312 }
    }
  ],
  "total": 1
}

Project Type Values

Value Description
STANDARD Non-episodic project
EPISODIC Series with episode numbers

Project Status Values

Value Description
ACTIVE Currently in production
ARCHIVED Completed or archived

2. Look Up Shot by Code

Primary endpoint used by pipeline tools (After Effects, Nuke, etc.)

GET /api/ext/shots/lookup?shotCode={shotCode}&projectCode={projectCode}

Query Parameters

Parameter Required Description
shotCode Yes Human-readable shot code, e.g. UNG_106_010_020
projectCode Recommended Project code, e.g. UNG_S1 — disambiguates if same shot code appears in multiple projects

Response 200 OK

{
  "shot": {
    "id": "clxxxxxxxxxxxxxx",
    "shotCode": "UNG_106_010_020",
    "scene": "010",
    "episode": "106",
    "sequence": "010",
    "shotNumber": 20,
    "description": "Hero establishing wide",
    "notes": "Keep sky practical",
    "status": "IN_PROGRESS",
    "priority": "NORMAL",
    "frameStart": 1001,
    "frameEnd": 1120,
    "fps": 24,
    "dueDate": "2026-08-01T00:00:00.000Z",
    "thumbnailUrl": "/api/files/thumb_abc123",
    "createdAt": "2026-06-01T10:00:00.000Z",
    "updatedAt": "2026-07-15T14:30:00.000Z",

    "shotVersion": "v003",
    "exrOutput": "UNG_106_010_020_cmp_TT_v003",

    "sourceClip": "A106_C020_0601AB",
    "timecodeStart": "01:00:10:00",
    "timecodeEnd": "01:00:15:00",
    "clipDuration": "00:00:05:00",
    "seqTimecodeStart": "00:59:49:12",
    "seqTimecodeEnd": "00:59:55:00",

    "project": {
      "id": "cmp6l5mzq0001ua0gz07bk72f",
      "name": "Ungo Season 1",
      "code": "UNG_S1",
      "showId": "UNG"
    },
    "artist": {
      "id": "clxxxxxxxxxxxxxx",
      "name": "Jane Smith",
      "email": "jane@studio.com"
    },
    "tasks": [
      {
        "id": "clxxxxxxxxxxxxxx",
        "title": "Comp",
        "type": "COMP",
        "status": "IN_PROGRESS"
      }
    ],
    "versions": [
      {
        "id": "clxxxxxxxxxxxxxx",
        "versionNumber": 3,
        "approvalStatus": "PENDING",
        "reviewStatus": "PENDING",
        "fileUrl": "/api/files/vid_abc123",
        "thumbnailUrl": "/api/files/thumb_abc123",
        "createdAt": "2026-07-15T14:30:00.000Z"
      }
    ]
  }
}

Pipeline-Specific Fields

Field Type Description
shotVersion string Current version string, e.g. v003. Used for output naming.
exrOutput string Base filename for EXR renders, e.g. UNG_106_010_020_cmp_TT_v003
seqTimecodeStart string Shot in-point in the locked picture sequence (HH:MM:SS:FF)
seqTimecodeEnd string Shot out-point in the locked picture sequence (HH:MM:SS:FF)
sourceClip string Original camera roll/clip name from the EDL
timecodeStart string Source clip timecode in (HH:MM:SS:FF)
timecodeEnd string Source clip timecode out (HH:MM:SS:FF)
clipDuration string Clip duration (HH:MM:SS:FF)

3. List Episodes

GET /api/ext/projects/{projectCode}/episodes

Path Parameters

Parameter Description
projectCode Human-readable project code, e.g. UNG_S1

Query Parameters

Parameter Required Description
shotNames No Pass 1 to include full per-episode shot list (default off)

Response 200 OK

{
  "project": {
    "id": "cmp6l5mzq0001ua0gz07bk72f",
    "name": "Ungo Season 1",
    "code": "UNG_S1",
    "showId": "UNG"
  },
  "episodes": [
    {
      "episode": "106",
      "shotCount": 42,
      "sequences": ["010", "020", "030"]
    }
  ]
}

When shotNames=1, each episode entry also includes a shots array with: id, shotCode, episode, sequence, status, exrOutput, seqTimecodeStart, seqTimecodeEnd, sourceClip, timecodeStart, timecodeEnd, clipDuration.


4. List Shots by Project Code

GET /api/ext/projects/{projectCode}/shots

Path Parameters

Parameter Description
projectCode Human-readable project code, e.g. UNG_S1

Query Parameters

Parameter Required Default Description
episode No — Filter by episode, e.g. 106
sequence No — Filter by sequence, e.g. 010
status No — Filter by shot status
page No 1 Page number
limit No 200 Results per page (max 500)

Response 200 OK

{
  "project": { "id": "...", "name": "...", "code": "UNG_S1", "showId": "UNG" },
  "pagination": { "page": 1, "limit": 200, "total": 42, "pages": 1 },
  "shots": [
    {
      "id": "clxxxxxxxxxxxxxx",
      "shotCode": "UNG_106_010_020",
      "scene": "010",
      "episode": "106",
      "sequence": "010",
      "shotNumber": 20,
      "description": "Hero establishing wide",
      "status": "IN_PROGRESS",
      "priority": "NORMAL",
      "frameStart": 1001,
      "frameEnd": 1120,
      "fps": 24,
      "dueDate": "2026-08-01T00:00:00.000Z",
      "thumbnailUrl": null,
      "exrOutput": "UNG_106_010_020_cmp_TT_v003",
      "seqTimecodeStart": "00:59:49:12",
      "seqTimecodeEnd": "00:59:55:00",
      "sourceClip": "A106_C020_0601AB",
      "timecodeStart": "01:00:10:00",
      "timecodeEnd": "01:00:15:00",
      "clipDuration": "00:00:05:00",
      "artist": { "id": "...", "name": "Jane Smith", "email": "jane@studio.com" },
      "_count": { "versions": 3, "tasks": 2 }
    }
  ]
}

Note: shotVersion is not included in this response. Use the lookup endpoint to get it.


5. List Shots by Project ID

GET /api/ext/shots?projectId={projectId}

Legacy endpoint. Prefer /api/ext/projects/{projectCode}/shots for new integrations.

Query Parameters

Parameter Required Description
projectId Yes Database CUID of the project
episode No Filter by episode
status No Filter by shot status
shotCode No Return only the shot matching this exact code

Response 200 OK

{
  "shots": [ /* array of shot objects */ ],
  "total": 42
}

Shot objects include: id, shotCode, scene, episode, shotNumber, description, status, priority, frameStart, frameEnd, fps, dueDate, createdAt, updatedAt, artist, _count.

Note: Pipeline-specific fields (exrOutput, shotVersion, seqTimecodes, etc.) are not included. Use the lookup endpoint.


6. Get Single Shot

GET /api/ext/shots/{shotId}

By Database ID

GET /api/ext/shots/clxxxxxxxxxxxxxx

By Shot Code

GET /api/ext/shots/UNG_106_010_020?byCode=1&projectId=cmp6l5mzq0001ua0gz07bk72f
Query Param Description
byCode Set to 1 to treat the path segment as a shot code
projectId Required when byCode=1 to scope the lookup

Response 200 OK

Returns the same full shot object as the lookup endpoint, including tasks and latest version. Pipeline-specific fields (exrOutput, shotVersion, seqTimecodeStart, seqTimecodeEnd, sourceClip, etc.) are included.


7. Create Shot

POST /api/ext/shots
Content-Type: application/json

Accepts either application/json or multipart/form-data (when uploading a thumbnail file).

Request Body

Field Type Required Description
projectId string (CUID) Yes Database project ID
scene string Yes Scene identifier, alphanumeric + underscore, e.g. 010
episode string No Episode number. Required for EPISODIC projects
description string No Shot description
artistId string (CUID) No Assign to an artist by their DB id
priority string No LOW, NORMAL, HIGH, or URGENT (default NORMAL)
fps number No Frame rate (default 24)
frameStart integer No First frame number
frameEnd integer No Last frame number
dueDate string (ISO 8601) No Due date
thumbnailUrl string (URL) No Pre-hosted thumbnail URL
thumbnail File No Thumbnail image upload (multipart only, max 50 MB)
shotGroupName string No Shot group name — created automatically if it doesn't exist
shotCode string No Explicit shot code — bypasses auto-generation, must be unique in the project

Response 201 Created

{
  "shot": {
    "id": "clxxxxxxxxxxxxxx",
    "shotCode": "UNG_106_010_0010",
    "scene": "010",
    "episode": "106",
    "shotNumber": 10,
    "description": null,
    "status": "WAITING",
    "priority": "NORMAL",
    "fps": 24,
    "frameStart": null,
    "frameEnd": null,
    "dueDate": null,
    "thumbnailUrl": null,
    "projectId": "cmp6l5mzq0001ua0gz07bk72f",
    "artistId": null,
    "shotGroupId": null,
    "createdAt": "2026-07-21T09:00:00.000Z"
  }
}

Error 409 Conflict

Returned when an explicit shotCode already exists in the project:

{ "error": "A shot with that code already exists in this project", "shotCode": "UNG_106_010_020" }

8. Update Shot

PATCH /api/ext/shots/{shotId}
Content-Type: application/json

Used by pipeline tools to write back metadata after a render/upload.

Path Parameters

Parameter Description
shotId Database ID of the shot (from the lookup response shot.id)

Request Body

Field Type Required Validation Description
shotVersion string Yes* Must match v###, e.g. v004 Sets the current version string

*At least one field must be provided; currently only shotVersion is supported.

Example Request

PATCH /api/ext/shots/clxxxxxxxxxxxxxx
Authorization: Bearer your-api-key
Content-Type: application/json

{ "shotVersion": "v004" }

Response 200 OK

{ "success": true, "shot": { "id": "clxxxxxxxxxxxxxx", "shotVersion": "v004" } }

Error Responses

Status Meaning
400 Missing required parameter
401 Missing or invalid API key
403 Forbidden (session-auth routes only)
404 Resource not found
409 Conflict (duplicate shot code)
413 Payload too large (thumbnail > 50 MB)
422 Validation error
500 Internal server error
{ "error": "shotCode query param is required" }
{ "error": "Validation error", "details": [ ... ] }

After Effects Connector — Endpoints Used

For reference, the endpoints called by VFXReviewConnector.jsx:

Action Endpoint
Populate Episode dropdown GET /api/ext/projects/{PROJECT_CODE}/episodes
Populate Shot dropdown GET /api/ext/projects/{PROJECT_CODE}/shots?episode={value}
All shot operations (overlay, queue, preview, delivery) GET /api/ext/shots/lookup?shotCode={code}&projectCode={PROJECT_CODE}
Increment Shot Version PATCH /api/ext/shots/{shot.id} with { "shotVersion": "v00X" }
Queue Export POST /api/ext/exports (E1)
Refresh Status / poll GET /api/ext/exports/latest (E2)
Retry Export POST /api/ext/exports/{id}/retry (E14)
Regenerate Previews POST /api/ext/exports/{id}/regenerate-preview
Load QC Queue GET /api/ext/qc/queue?projectCode={PROJECT_CODE} (E5)
QC Pass / QC Fail POST /api/ext/exports/{id}/qc (E4)