↗ tangara.docs Open studio
TANGARA API v1

Generate videos with Tangara

Turn a prompt and optional reference images into an MP4. Upload images once, reuse their IDs, and follow generation until the finished video is ready to download.

Base URL: https://tangara.app/api/v1 · Authentication: Bearer API key

Generation is asynchronous: creating a task returns its ID immediately. A compatible GPU worker processes it when available. A queued task is saved even if you disconnect.

Get your API key

Sign in at tangara.app, open your account menu and select API key. Copy the key into an environment variable on your server or computer. The examples below use Bash, curl, jq and Python 3 (only to generate UUIDs).

bash
export TANGARA_API_URL='https://tangara.app/api/v1'
export TANGARA_API_KEY='tgapi_REPLACE_WITH_YOUR_KEY'

curl --fail-with-body --silent --show-error \
  "$TANGARA_API_URL/me" \
  -H "Authorization: Bearer $TANGARA_API_KEY"

Your key grants access to your own images, videos and balance. It has the same active-task limit as the website. It is not a GPU worker or administrator key. Keep it on the server side; do not put it in browser JavaScript, URLs or Git. No session cookie, login request or Origin header is needed for this API.

New accounts receive a key immediately. Existing accounts receive one when opening API key. Regenerating it invalidates the previous key and closes its WebSockets. Password reset also rotates the key. Fetch the new key from your account afterwards.

Quick start

This example creates a five-second video from text, without sound. No image is required. First check the currently supported profiles:

bash
curl --fail-with-body --silent --show-error \
  "$TANGARA_API_URL/profiles" \
  -H "Authorization: Bearer $TANGARA_API_KEY" \
  -o profiles.json

jq . profiles.json

Build the request once. Its UUID identifies this particular generation request:

bash
jq -n \
  --arg request_id "$(python3 -c 'import uuid; print(uuid.uuid4())')" \
  --arg profile_id "$(jq -r '.default_profile_id' profiles.json)" \
  '{
    client_request_id: $request_id,
    prompt: "A slow camera move through a sunlit forest, leaves moving gently in the breeze.",
    profile_id: $profile_id,
    duration_seconds: 5,
    audio: false,
    seed: 42
  }' > request.json

curl --fail-with-body --silent --show-error \
  -X POST "$TANGARA_API_URL/tasks" \
  -H "Authorization: Bearer $TANGARA_API_KEY" \
  -H 'Content-Type: application/json' \
  --data-binary @request.json \
  -o created-task.json

TASK_ID=$(jq -er '.id' created-task.json)
printf 'Task: %s\n' "$TASK_ID"

A new task returns 201 Created. Retrying the same request returns 200 OK with the same task. If the POST times out, resend request.json unchanged; do not regenerate its UUID. See Retries and limits.

A shortened example response:

json
{
  "id": "11111111-1111-4111-8111-111111111111",
  "client_request_id": "22222222-2222-4222-8222-222222222222",
  "result": "in_progress",
  "stage": "queued",
  "progress_percent": null,
  "attempt_count": 0,
  "result_asset_id": null,
  "error_message": null,
  "retry_at": null
}

Continue with Track generation and Download the MP4.

Upload and reuse images

Images are independent of tasks. Upload each image using multipart field file; the response is an asset object with its id. Files are stored in your private library immediately, even if you have not created a video yet. Originals are preserved.

bash
curl --fail-with-body --silent --show-error \
  -X POST "$TANGARA_API_URL/assets" \
  -H "Authorization: Bearer $TANGARA_API_KEY" \
  -F '[email protected]' \
  -o start-image.json

FIRST_IMAGE_ID=$(jq -er '.id' start-image.json)

Example response, 201 Created:

json
{
  "id": "33333333-3333-4333-8333-333333333333",
  "kind": "image",
  "mime_type": "image/png",
  "size_bytes": 248320,
  "width": 1344,
  "height": 768
}

For an end frame, upload a second file the same way:

bash
curl --fail-with-body --silent --show-error \
  -X POST "$TANGARA_API_URL/assets" \
  -H "Authorization: Bearer $TANGARA_API_KEY" \
  -F '[email protected]' \
  -o end-image.json

LAST_IMAGE_ID=$(jq -er '.id' end-image.json)

Accepted inputs: PNG, JPEG or WebP, up to 20 MiB each, 40 megapixels, and at most 16,384 pixels per side. Send file bytes, not base64 or a remote URL. Let curl set the multipart Content-Type and boundary automatically.

Reuse an existing image by selecting its ID from your library:

bash
curl --fail-with-body --silent --show-error \
  "$TANGARA_API_URL/assets?offset=0" \
  -H "Authorization: Bearer $TANGARA_API_KEY" | jq .

The response contains images and has_more, with up to 24 images per page. Increase offset by 24 while has_more is true. The same image ID can be used in multiple videos. Image uploads do not currently have an idempotency key: repeating an upload creates another asset, so keep its returned ID.

Create a video with images

Create a new request that references the previously uploaded IDs:

bash
jq -n \
  --arg request_id "$(python3 -c 'import uuid; print(uuid.uuid4())')" \
  --arg first_image_id "$FIRST_IMAGE_ID" \
  --arg last_image_id "$LAST_IMAGE_ID" \
  --arg profile_id "$(jq -r '.default_profile_id' profiles.json)" \
  '{
    client_request_id: $request_id,
    prompt: "A smooth cinematic transition from the first scene to the final scene.",
    first_image_id: $first_image_id,
    last_image_id: $last_image_id,
    profile_id: $profile_id,
    duration_seconds: 10,
    audio: false,
    seed: 42
  }' > image-request.json

curl --fail-with-body --silent --show-error \
  -X POST "$TANGARA_API_URL/tasks" \
  -H "Authorization: Bearer $TANGARA_API_KEY" \
  -H 'Content-Type: application/json' \
  --data-binary @image-request.json \
  -o created-task.json

TASK_ID=$(jq -er '.id' created-task.json)

For a starting image only, omit last_image_id or set it to null. For text-only generation, omit both image fields. An end frame requires a start frame. Only IDs of your own uploaded images are accepted; upload files before creating the task.

Generation settings

Field Required Meaning
client_request_id Yes A UUID for this request; keep it unchanged for retries
prompt Yes 1–4000 characters, after trimming surrounding whitespace
profile_id Yes An ID returned by GET /profiles
first_image_id No Your starting image UUID, or null
last_image_id No Your ending image UUID; requires first_image_id
duration_seconds No Integer from 1 to 20; default 5
audio No true to include audio (default); false for a silent MP4
seed No Integer from 0 to 2147483647; default 0

Use /profiles as the source of supported sizes and max_duration_seconds. Do not send width, height, fps or model directly: unknown JSON fields are rejected. The current profiles are:

Profile ID Resolution
ltx-2.5-512p-v2 896 × 512
ltx-2.5-768p-v2 1344 × 768 (default)
ltx-2.5-1088p-v2 1920 × 1088
ltx-2.5-1472p-v2 2560 × 1472
ltx-2.5-2176p-v2 3840 × 2176

Current profiles use LTX-2.5 at 24 fps. Availability of a profile does not guarantee an online GPU capable of serving it. A task waits for a compatible worker. The response's generation_config records the settings accepted for that task. With audio: false, the delivered MP4 has no audio track; this does not promise less GPU work.

Seeds and reproducibility

seed controls the initial randomness used by the model. Reusing it helps compare generations, but the seed alone does not identify a video. The prompt, input images, resolution, duration, model weights and inference settings also matter.

Tangara passes the requested seed to LTX. We do not currently guarantee identical frames or identical MP4 bytes between runs, even with the same inputs. GPU kernels, hardware and library versions can affect reproducibility; see the PyTorch reproducibility guide.

To request a new variation, choose another seed and a new client_request_id. To retry an interrupted API request, keep the same client_request_id and request body. That returns the existing task instead of running the model again. To obtain exactly the same completed video, download the original task's result.

Track generation

Read the task by its ID:

bash
curl --fail-with-body --silent --show-error \
  "$TANGARA_API_URL/tasks/$TASK_ID" \
  -H "Authorization: Bearer $TANGARA_API_KEY" \
  -o task.json

jq '{id, result, stage, progress_percent, attempt_count, retry_at, error_message, result_asset_id}' task.json

result is the authoritative completion state:

Result Meaning
in_progress Queued, generating, waiting to retry or uploading
succeeded Ready; result_asset_id identifies the downloadable MP4
failed Final failure; inspect error_message

stage gives more detail: queued, preparing, generating, uploading, completed or failed. progress_percent may be null, meaning unknown. During generation it describes completed model steps, not elapsed time. Even 100% is not a ready video: decoding and upload can still be running. Wait for result == "succeeded" before downloading.

If a generation attempt is interrupted, the same task can return to queued with retry_at set and a higher attempt_count. Keep tracking the same task. An error_message on an in_progress task can describe a previous attempt; it does not by itself mean final failure.

Poll every 3–5 seconds, or use WebSocket updates. If you set a client-side timeout, save TASK_ID and resume checking it later. Disconnecting or stopping polling does not cancel the task. When no compatible GPU is online, the task remains queued; the API does not start a GPU server automatically.

To resume without a saved task ID, GET /tasks?offset=0 returns your newest tasks, active_count, max_active_tasks, has_more and generation_paused. Task pages contain up to 50 entries. Images and tasks are shared with your website account.

Download the MP4

After result becomes succeeded, read result_asset_id from the task response:

bash
RESULT_ASSET_ID=$(jq -er 'select(.result == "succeeded") | .result_asset_id // empty' task.json)

curl --fail --show-error --location \
  --proto '=https' --proto-redir '=https' \
  "$TANGARA_API_URL/assets/$RESULT_ASSET_ID?download=1" \
  -H "Authorization: Bearer $TANGARA_API_KEY" \
  --output video.mp4

The API responds with 307 Temporary Redirect to a short-lived signed R2 URL. Use an up-to-date curl: it strips Authorization when following a redirect to a different host. Do not use --location-trusted. In another HTTP client, explicitly ensure the Tangara key is not forwarded to R2. R2 receives only its signed URL.

Video bytes are downloaded directly from storage. If the temporary URL expires, request /assets/{id} again to receive a fresh one. Keep the permanent asset ID, not the signed URL. ?download=1 adds a download filename; omit it for viewing.

WebSocket updates

Connect from a server or native client to:

text
wss://tangara.app/api/v1/events
Authorization: Bearer <YOUR_API_KEY>

The server sends this notification immediately after connection and whenever your tasks change:

json
{"type":"tasks_changed"}

It is an invalidation notification, not a task snapshot. On each notification, fetch your task with GET /tasks/{id}, or refresh the task list. Multiple changes may be combined into one notification. Reconnect with backoff and re-fetch state after every reconnect so an interrupted connection does not lose progress. Use a WebSocket library that automatically responds to protocol ping frames. The server checks authorization during updates and periodically while connected.

The connection is bound to the key's owner; you do not send a user ID. A revoked key closes the connection. Standard browser WebSocket cannot set an Authorization header: the Tangara website uses its HttpOnly session cookie at /api/events. Never put a personal API key in the WebSocket URL or public frontend code.

Retries and limits

Create one UUID per intended video and persist it together with the exact request body before the first POST. After a timeout or transient server failure, repeat that body with that UUID. This also works if the task was created but its response was lost, or if the account's active-task limit has since been reached.

Changing the prompt, images or settings with an already-used UUID returns 409. A new intended video needs a new UUID. Keep optional fields unchanged on retries: omitting audio and explicitly sending audio: true are different request bodies.

The website and API share an active-task limit. GET /me provides max_active_tasks; GET /tasks also provides active_count. A 429 task_limit means wait for an active task to finish before creating another one. Do not create replacement tasks because generation is slow or an attempt is retrying.

There is currently no public task cancellation, task editing or asset deletion endpoint. Uploading an image again creates a separate asset.

Errors

API errors use JSON with a machine-readable code and readable error:

json
{
  "code": "task_limit",
  "error": "Your active generation limit has been reached"
}
HTTP status Typical reason What to do
400 Invalid prompt, settings, image reference or file Correct the request; check field names and ownership
401 Missing, malformed or revoked API key Copy the current key from your account
404 Missing resource or another user's resource Check the ID and account
409 Same request UUID with different parameters Retry the original body or use a new UUID for a new video
413 Image too large Use a file within the upload limits
415 JSON Content-Type missing Send Content-Type: application/json for JSON bodies
429 Active-task limit reached Wait for an existing task to finish
500, 503 Transient service/storage failure Retry with backoff; preserve the task request UUID

Errors from a proxy or expired storage URL may not use the API's JSON format. If curl fails, inspect its HTTP status and error body instead of assuming that an empty output file is a successful upload, task or video.

Endpoint reference

All paths below are relative to https://tangara.app/api/v1 and require your key.

Method Path Purpose
GET /me Your account and task limit
GET /profiles Supported generation profiles
POST /assets Upload one original image
GET /assets Your image library
GET /assets/{id} Retrieve an image or video
POST /tasks Create a video task
GET /tasks Your tasks and queue counts
GET /tasks/{id} Task state and result
GET /events WebSocket invalidations
GET /billing Your balance and transaction history
POST /billing/checkout Create a local test checkout
GET /billing/checkout/{id} Read its state
POST /billing/checkout/{id}/complete Simulate successful checkout
POST /billing/checkout/{id}/cancel Cancel the test checkout

Payments and generation charges are currently disabled. Checkout is a local test flow, not a real Stripe payment, and does not change your balance.