Skip to content

Anuva Web API Doc

Last refreshed: 2026-05-22

Purpose

This document tells the Anuva Python Server exactly how to integrate with the Anuva Web App worker API that is implemented in this repository.

It covers:

  1. What the Python Server should call
  2. How requests must be authenticated
  3. What each route expects and returns
  4. How uploads work
  5. What environment and schema setup is required
  6. The full setup sequence for both the Web App and the Python Server side

This document is implementation-oriented. It describes the currently implemented Web App contract under /api/worker/v1.

Scope

This API is for the external render worker only.

The Python Server should use it for:

  1. Discovering queued render jobs
  2. Claiming a job
  3. Downloading the render package
  4. Sending progress and heartbeats
  5. Uploading the final MP4 through signed S3 upload
  6. Completing, failing, or cancelling the job

The Python Server should not use it for:

  1. Creating presentations
  2. Generating scenes
  3. Editing presentation content
  4. Reading arbitrary Payload collections

Base URL And Auth

Base namespace:

/api/worker/v1

Authentication:

Authorization: Bearer <ANUVA_WORKER_API_TOKEN>
Accept: application/json
Content-Type: application/json

If the token is missing or wrong, the API returns:

{
  "error": "unauthorized_worker_request",
  "message": "Worker bearer token is invalid"
}

If the server is missing ANUVA_WORKER_API_TOKEN, the API returns:

{
  "error": "worker_auth_not_configured",
  "message": "ANUVA_WORKER_API_TOKEN is not configured"
}

Setup Steps

1. Set up the Anuva Web App

  1. Populate .env from .env.example.
  2. Set the normal app variables:
  3. DATABASE_URI
  4. PAYLOAD_SECRET
  5. NEXT_PUBLIC_SERVER_URL
  6. Set the storage variables required by worker asset download and upload signing:
  7. S3_BUCKET
  8. S3_ENDPOINT
  9. S3_REGION
  10. S3_ACCESS_KEY_ID
  11. S3_SECRET_ACCESS_KEY
  12. Set the worker auth variable:
  13. ANUVA_WORKER_API_TOKEN
  14. Apply database schema changes before expecting the worker API or admin UI to work correctly.
  15. Start the app with pnpm dev or the equivalent production process.

2. Apply database migrations correctly

The worker API depends on these Payload collections existing in Postgres:

  1. video_generation_jobs
  2. render_workers
  3. render_worker_reports

Important environment note:

  • If DATABASE_URI uses a Supabase pooler host, PAYLOAD_DB_PUSH=1 is ignored.
  • In that case you must run explicit migrations against a direct Postgres connection.

Recommended sequence:

pnpm payload migrate

If your environment points at a pooler, switch DATABASE_URI to a direct Postgres connection first, run the migration, then switch back if needed.

3. Confirm worker API readiness

Before connecting the Python Server, confirm:

  1. The Web App is running on the expected base URL
  2. ANUVA_WORKER_API_TOKEN is present in the Web App environment
  3. The worker routes respond with auth failures instead of internal server errors when called without a token
  4. video_generation_jobs rows exist for test render jobs
  5. Asset URLs and S3 signing work in the current environment

4. Configure the Python Server

Point the Python Server's configurable endpoint map to the production namespace:

GET    /api/worker/v1/jobs/next
GET    /api/worker/v1/jobs/available
GET    /api/worker/v1/jobs/{jobId}
POST   /api/worker/v1/jobs/{jobId}/claim
GET    /api/worker/v1/jobs/{jobId}/package
PATCH  /api/worker/v1/jobs/{jobId}/status
POST   /api/worker/v1/workers/{workerId}/heartbeat
POST   /api/worker/v1/jobs/{jobId}/uploads/signed
POST   /api/worker/v1/jobs/{jobId}/uploads/confirm
POST   /api/worker/v1/jobs/{jobId}/complete
POST   /api/worker/v1/jobs/{jobId}/fail
POST   /api/worker/v1/jobs/{jobId}/cancel

The Python Server should also be configured with:

  1. The Web App base URL
  2. The same bearer token value as ANUVA_WORKER_API_TOKEN
  3. A stable workerId
  4. Local workspace directories for package rewriting and Unity handoff

5. Verify the full worker flow

Recommended smoke test:

  1. Queue one video_generation_jobs document in queued
  2. Call GET /jobs/available
  3. Claim the job
  4. Fetch the package
  5. Send one heartbeat
  6. Send inProgress status
  7. Request a signed upload URL
  8. Upload an MP4 to S3
  9. Confirm the upload
  10. Complete the job
  11. Confirm the presentation now points to the final video

End-to-end Flow

sequenceDiagram
  participant Py as Python Server
  participant Web as Anuva Web App
  participant S3 as S3 Storage
  participant Unity as Unity Renderer

  Py->>Web: GET /api/worker/v1/jobs/available
  Py->>Web: POST /api/worker/v1/jobs/{jobId}/claim
  Py->>Web: GET /api/worker/v1/jobs/{jobId}/package
  Py->>Web: POST /api/worker/v1/workers/{workerId}/heartbeat
  Py->>Unity: render.start with local package URLs
  Unity-->>Py: render.progress
  Py->>Web: PATCH /api/worker/v1/jobs/{jobId}/status
  Unity-->>Py: render.completed
  Py->>Web: POST /api/worker/v1/jobs/{jobId}/uploads/signed
  Py->>S3: PUT rendered MP4
  Py->>Web: POST /api/worker/v1/jobs/{jobId}/uploads/confirm
  Py->>Web: POST /api/worker/v1/jobs/{jobId}/complete

Implemented Route Catalog

GET /api/worker/v1/jobs/next

Returns the next queued render job or null.

Supported query params:

  1. workerId optional
  2. jobType optional: preview | finalRender
  3. priority optional: standard | priority | fastest
  4. workspaceId optional
  5. limit optional, though this route always returns at most one job

Example response:

{
  "jobId": "job_001",
  "workspaceId": "workspace_001",
  "presentationId": "presentation_001",
  "compilationId": "compilation_001",
  "jobType": "finalRender",
  "priority": "standard",
  "status": "queued",
  "progress": 0,
  "message": null,
  "workerId": null,
  "claimedAt": null,
  "updatedAt": "2026-05-22T10:00:00.000Z",
  "errorCode": null,
  "errorMessage": null,
  "resultMediaId": null
}

GET /api/worker/v1/jobs/available

Returns a safe list of queued jobs for manual or hybrid claim flows.

Supported query params:

  1. workerId optional
  2. jobType optional
  3. priority optional
  4. workspaceId optional
  5. limit optional, max 50

Example response:

[
  {
    "jobId": "job_001",
    "workspaceId": "workspace_001",
    "presentationId": "presentation_001",
    "compilationId": "compilation_001",
    "jobType": "finalRender",
    "priority": "standard",
    "status": "queued",
    "title": "Q2 Product Launch",
    "createdAt": "2026-05-22T09:55:00.000Z",
    "updatedAt": "2026-05-22T09:55:00.000Z",
    "estimatedDurationSeconds": 75,
    "assetCount": 14
  }
]

GET /api/worker/v1/jobs/{jobId}

Returns one RenderJobSummary.

Use this for operator refresh or manual claim screens.

POST /api/worker/v1/jobs/{jobId}/claim

Claims a queued job atomically.

Request body:

{
  "workerId": "unity-worker-01"
}

Success response:

{
  "jobId": "job_001",
  "workspaceId": "workspace_001",
  "presentationId": "presentation_001",
  "compilationId": "compilation_001",
  "jobType": "finalRender",
  "priority": "standard",
  "status": "claimed",
  "progress": 0,
  "message": "claimed by worker",
  "workerId": "unity-worker-01",
  "claimedAt": "2026-05-22T10:00:00.000Z",
  "updatedAt": "2026-05-22T10:00:00.000Z",
  "errorCode": null,
  "errorMessage": null,
  "resultMediaId": null
}

Conflict response when another worker already claimed it:

{
  "error": "job_already_claimed",
  "message": "Job was already claimed by another worker"
}

GET /api/worker/v1/jobs/{jobId}/package

Returns the full RenderJobPackage.

Important rule:

  • This route returns 409 if the job is still queued.
  • The Python Server should claim first, then request the package.

Example response shape:

{
  "jobId": "job_001",
  "workspaceId": "workspace_001",
  "presentationId": "presentation_001",
  "compilationId": "compilation_001",
  "jobType": "finalRender",
  "priority": "standard",
  "config": {},
  "assets": [
    {
      "assetId": "voice-001",
      "kind": "audio",
      "url": "https://storage.example.com/...",
      "filename": "voice-001.mp3",
      "mimeType": "audio/mpeg",
      "checksum": "sha256...",
      "required": true
    }
  ],
  "render": {
    "resolution": "1080p",
    "frameRate": 30,
    "aspectRatio": "16:9",
    "format": "mp4",
    "watermark": false
  }
}

Important package details:

  1. config is the persisted PresentationConfigPublic snapshot from video_generation_jobs.configSnapshot.publicConfig.
  2. assets[*].filename is always a safe basename, not a nested path.
  3. Asset URLs are generated dynamically when the package is requested.
  4. Asset URLs should be treated as temporary signed download URLs.

PATCH /api/worker/v1/jobs/{jobId}/status

Updates a non-terminal or non-final status.

Request body:

{
  "status": "inProgress",
  "progress": 0.42,
  "message": "rendering scene 5 of 12"
}

Important status rules:

  1. API progress is 0.0..1.0
  2. Stored DB progress is 0..100
  3. completed is not allowed here
  4. Use /complete to finalize the render

Supported worker-visible statuses:

  1. queued
  2. claimed
  3. inProgress
  4. uploading
  5. uploadFailed
  6. completed
  7. failed
  8. cancelled

Valid transition rules:

  1. queued -> queued | claimed | cancelled
  2. claimed -> claimed | inProgress | failed | cancelled
  3. inProgress -> inProgress | uploading | uploadFailed | failed | cancelled
  4. uploading -> uploading | completed | uploadFailed | failed | cancelled
  5. uploadFailed -> uploadFailed | uploading | failed | cancelled
  6. terminal states remain terminal

POST /api/worker/v1/workers/{workerId}/heartbeat

Stores the latest worker heartbeat in render_workers.

Request body:

{
  "status": "ready",
  "activeJobId": "job_001",
  "details": {
    "mode": "auto",
    "unityReady": true,
    "unityConnected": true
  }
}

Response:

{
  "status": "ok"
}

POST /api/worker/v1/jobs/{jobId}/uploads/signed

Creates a signed upload target for the final rendered MP4.

Request body:

{
  "filename": "presentation-final.mp4",
  "contentType": "video/mp4",
  "fileSizeBytes": 12345678,
  "metadata": {
    "workerId": "unity-worker-01"
  }
}

Response:

{
  "uploadUrl": "https://storage.example.com/...",
  "headers": {
    "Content-Type": "video/mp4"
  },
  "storageKey": "render-results/job_001/presentation-final.mp4",
  "uploadId": "uuid-value",
  "publicUrl": "https://app.example.com/api/render-assets/render-results/job_001/presentation-final.mp4"
}

Current implementation note:

  • V1 supports signed upload flow only.
  • There is no direct multipart upload endpoint in the implemented Web App route set.

POST /api/worker/v1/jobs/{jobId}/uploads/confirm

Confirms that the upload succeeded and creates or reuses a media_assets record.

Request body:

{
  "upload": {
    "uploadUrl": "https://storage.example.com/...",
    "headers": {
      "Content-Type": "video/mp4"
    },
    "storageKey": "render-results/job_001/presentation-final.mp4",
    "uploadId": "uuid-value"
  },
  "metadata": {
    "workerId": "unity-worker-01",
    "fileSizeBytes": 12345678
  }
}

Response:

{
  "mediaId": "media_001",
  "url": "https://app.example.com/api/render-assets/render-results/job_001/presentation-final.mp4",
  "filename": "presentation-final.mp4",
  "mimeType": "video/mp4",
  "fileSizeBytes": 12345678,
  "metadata": {
    "jobId": "job_001",
    "workerId": "unity-worker-01"
  }
}

Side effect:

  • The job is moved to uploading and resultMedia is set.

POST /api/worker/v1/jobs/{jobId}/complete

Marks the render job completed and updates the owning presentation's final video pointer.

Request body:

{
  "resultMediaId": "media_001"
}

Response shape:

{
  "job": {
    "jobId": "job_001",
    "status": "completed",
    "progress": 1
  },
  "finalVideo": {
    "mediaId": "media_001",
    "url": "https://app.example.com/api/render-assets/render-results/job_001/presentation-final.mp4",
    "filename": "presentation-final.mp4",
    "mimeType": "video/mp4",
    "fileSizeBytes": 12345678,
    "metadata": {
      "jobId": "job_001",
      "workerId": "unity-worker-01"
    }
  }
}

POST /api/worker/v1/jobs/{jobId}/fail

Marks the render job failed and writes a diagnostic row to render_worker_reports.

Request body:

{
  "errorCode": "UNITY_RENDER_FAILED",
  "errorMessage": "Unity render returned non-zero exit status",
  "diagnostics": {
    "workerId": "unity-worker-01",
    "unityLogPath": "C:\\\\logs\\\\unity.log"
  }
}

POST /api/worker/v1/jobs/{jobId}/cancel

Marks a non-terminal render job cancelled.

Request body:

{
  "reason": "operator cancelled render"
}

Data Contract Notes

Render job persistence

The worker API is backed by Payload collections:

  1. video_generation_jobs
  2. render_workers
  3. render_worker_reports

Important fields in video_generation_jobs:

  1. jobType: preview | finalRender
  2. priority: standard | priority | fastest
  3. status: queued | claimed | inProgress | uploading | uploadFailed | completed | failed | cancelled
  4. progress: stored as 0..100
  5. workerId
  6. claimedAt
  7. statusMessage
  8. errorCode
  9. errorMessage
  10. resultMedia
  11. configSnapshot.publicConfig

Package config

The Python Server should treat:

RenderJobPackage.config = PresentationConfigPublic

That object is the renderer-facing contract. The Python Server should serve it locally to Unity after rewriting any asset URLs it needs to local HTTP/HTTPS URLs.

Asset rules

The package assets list is a normalized manifest built from the config.

Rules:

  1. Every asset has a stable assetId
  2. Every asset has a safe basename in filename
  3. URLs are remote signed URLs from the Web App storage layer
  4. The worker may download and rewrite them into a deterministic local workspace
  5. Required assets must exist or package generation fails with 422

Error Model

Validation errors return:

{
  "error": "invalid_request",
  "message": "..."
}

Domain errors return:

{
  "error": "<machine_code>",
  "message": "<human message>"
}

Common error codes:

  1. job_not_found
  2. job_already_claimed
  3. job_not_claimed
  4. invalid_status_transition
  5. use_complete_endpoint
  6. job_missing_presentation
  7. invalid_render_job_package
  8. unauthorized_worker_request
  9. worker_auth_not_configured
  10. worker_api_failed

Current Limitations

The Python Server should account for these current implementation details:

  1. Signed upload flow is the only supported upload path in V1.
  2. GET /jobs/next returns the oldest queued job by createdAt; it does not claim automatically.
  3. Asset URLs are signed at package-fetch time and should be treated as temporary.
  4. The Web App currently assumes S3 path-style object URLs for asset download and render-result upload.
  5. Schema changes require explicit migrations in pooler-backed Supabase environments.

Use this exact order:

  1. GET /jobs/available or GET /jobs/next
  2. POST /jobs/{jobId}/claim
  3. GET /jobs/{jobId}/package
  4. POST /workers/{workerId}/heartbeat
  5. PATCH /jobs/{jobId}/status to inProgress
  6. POST /jobs/{jobId}/uploads/signed
  7. Upload the MP4 to S3 using the returned uploadUrl and headers
  8. POST /jobs/{jobId}/uploads/confirm
  9. POST /jobs/{jobId}/complete

If rendering fails:

  1. POST /jobs/{jobId}/fail

If the operator stops the job:

  1. POST /jobs/{jobId}/cancel

If upload fails after rendering:

  1. PATCH /jobs/{jobId}/status with uploadFailed
  2. Retry signed upload and confirm
  3. Or fail the job if the upload cannot be recovered

File Map

These are the relevant implementation files in this repo:

  1. docs/AnuvaPythonServer_SystemDescription.md
  2. src/app/api/worker/v1/**
  3. src/lib/render-worker/contracts.ts
  4. src/lib/render-worker/service.ts
  5. src/lib/render-worker/auth.ts
  6. src/lib/render-worker/http.ts
  7. src/lib/render-worker/storage.ts
  8. docs/core/WorkersAndJobs.md
  9. docs/state/EnvAndServices.md