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).
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:
curl --fail-with-body --silent --show-error \
"$TANGARA_API_URL/profiles" \
-H "Authorization: Bearer $TANGARA_API_KEY" \
-o profiles.json
jq . profiles.jsonBuild the request once. Its UUID identifies this particular generation request:
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:
{
"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.
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:
{
"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:
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:
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:
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:
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.jsonresult 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:
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.mp4The 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:
wss://tangara.app/api/v1/events
Authorization: Bearer <YOUR_API_KEY>The server sends this notification immediately after connection and whenever your tasks change:
{"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:
{
"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.