@@ -0,0 +1,515 @@
|
||||
# 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) |
|
||||
|
||||
---
|
||||
|
||||
## 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`
|
||||
|
||||
```json
|
||||
{
|
||||
"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`
|
||||
|
||||
```json
|
||||
{
|
||||
"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`
|
||||
|
||||
```json
|
||||
{
|
||||
"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`
|
||||
|
||||
```json
|
||||
{
|
||||
"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`
|
||||
|
||||
```json
|
||||
{
|
||||
"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
|
||||
|
||||
```http
|
||||
GET /api/ext/shots/clxxxxxxxxxxxxxx
|
||||
```
|
||||
|
||||
### By Shot Code
|
||||
|
||||
```http
|
||||
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`
|
||||
|
||||
```json
|
||||
{
|
||||
"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:
|
||||
|
||||
```json
|
||||
{ "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
|
||||
|
||||
```http
|
||||
PATCH /api/ext/shots/clxxxxxxxxxxxxxx
|
||||
Authorization: Bearer your-api-key
|
||||
Content-Type: application/json
|
||||
|
||||
{ "shotVersion": "v004" }
|
||||
```
|
||||
|
||||
### Response `200 OK`
|
||||
|
||||
```json
|
||||
{ "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 |
|
||||
|
||||
```json
|
||||
{ "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" }` |
|
||||
@@ -0,0 +1,60 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { db } from "@/lib/db";
|
||||
|
||||
// ── Auth ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
function isAuthorized(req: NextRequest): boolean {
|
||||
const apiKey = process.env.API_SECRET_KEY;
|
||||
if (!apiKey) return false;
|
||||
const authHeader = req.headers.get("authorization") ?? "";
|
||||
if (authHeader.startsWith("Bearer ")) return authHeader.slice(7) === apiKey;
|
||||
return (req.headers.get("x-api-key") ?? "") === apiKey;
|
||||
}
|
||||
|
||||
// ── GET /api/ext/projects ─────────────────────────────────────────────────────
|
||||
//
|
||||
// Returns all projects with their showIds. Useful for pipeline tools that need
|
||||
// to enumerate available projects and resolve a showId → projectCode mapping.
|
||||
//
|
||||
// Query params (all optional):
|
||||
// status Filter by project status: ACTIVE (default) | ARCHIVED | ALL
|
||||
//
|
||||
// Example:
|
||||
// GET /api/ext/projects
|
||||
// GET /api/ext/projects?status=ALL
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
if (!isAuthorized(req)) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(req.url);
|
||||
const statusParam = searchParams.get("status")?.toUpperCase() ?? "ACTIVE";
|
||||
|
||||
const whereStatus =
|
||||
statusParam === "ALL"
|
||||
? undefined
|
||||
: statusParam === "ARCHIVED"
|
||||
? { status: "ARCHIVED" as const }
|
||||
: { status: "ACTIVE" as const };
|
||||
|
||||
const projects = await db.project.findMany({
|
||||
where: whereStatus,
|
||||
orderBy: [{ showId: "asc" }, { code: "asc" }],
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
code: true,
|
||||
showId: true,
|
||||
projectType: true,
|
||||
status: true,
|
||||
startDate: true,
|
||||
dueDate: true,
|
||||
_count: {
|
||||
select: { shots: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ projects, total: projects.length });
|
||||
}
|
||||
Reference in New Issue
Block a user