Durango API
Programmatic access to every chat, image-generation, and video-generation model in Durango. Requests authenticate with a personal API key and consume the same usage credits as the app — there is no separate billing.
Durango API documentation
https://durango.shAll endpoints accept and return JSON, except image/video downloads (raw bytes) and streaming chat (Server-Sent Events). CORS is enabled on every /api/v1/* endpoint, so the API can be called from browsers as well as servers.
/api/v1/openapi.jsonOpenAPI 3.1 specification (no auth)Authentication
Create an API key in Settings → API Keys (or at /settings/api-keys). Keys look like dgo_… and are shown once at creation time — store them securely. Revoking a key disables it immediately.
Send the key on every request, either way works:
Authorization: Bearer dgo_your_key_here
# or
X-API-Key: dgo_your_key_hereA key acts on behalf of the account that created it: every request spends that account's credits, and generated images/videos land in that account's gallery. Missing, invalid, or revoked keys return 401 with an error body (see Errors).
Credits & billing
API usage draws from the same monthly credit balance as the Durango app and appears in the same usage history.
pricing in the model list). The exact charge is returned on every non-streaming response as usage.credits_used.required_tier of pro or ultra); calling them from a lower tier returns 403. Image and video generation require Pro or Ultra.402 insufficient_credits until your monthly reset or an upgrade.List models
Lists every model callable through the API, with capabilities, required tier, and pricing.
/api/v1/models— List available modelstypestringchat, image, or video.curl "https://durango.sh/api/v1/models?type=chat" \
-H "Authorization: Bearer $DURANGO_API_KEY"{
"object": "list",
"data": [
{
"id": "anthropic/claude-sonnet-4.5",
"object": "model",
"type": "chat",
"name": "Claude Sonnet 4.5",
"provider": "Anthropic",
"category": "favorites",
"capabilities": { "vision": true, "reasoning": true, "toolUse": true, "imageGen": false },
"required_tier": "free",
"context_length": 200000,
"pricing": { "prompt": "0.000003", "completion": "0.000015" }
}
]
}idstringmodel in chat / image / video requests.typestringchat models go to /api/v1/chat/completions; image models go to /api/v1/images/generations; video models go to /api/v1/videos/generations.required_tierstringfree, pro, or ultra — the minimum subscription needed to call the model.pricingobject | nullprompt = input, completion = output), when known. Multiply by 1,000,000 for $/1M tokens.capabilities.visionbooleanvideo_capabilitiesobject | nullChat completions
OpenAI-compatible chat completion. Only models present in the Durango catalog are accepted (404 model_not_found otherwise).
/api/v1/chat/completions— Create a chat completionmodelstringrequiredGET /api/v1/models?type=chat.messagesarrayrequiredstreambooleantrue to receive an SSE stream. Default false.temperaturenumbermax_tokensintegertop_pnumberfrequency_penalty / presence_penaltynumberstopstring | string[]seedintegerresponse_formatobject{"type": "json_object"} for JSON mode.toolsarraytool_choiceanyEach message is { "role": ..., "content": ... } with roles system, developer, user, assistant, or tool. content is a string — or, for multimodal input on vision-capable models, an array of parts. image_url.url accepts HTTPS URLs or data URLs. Assistant messages with tool_calls and role: "tool" results are supported for function-calling loops.
{
"role": "user",
"content": [
{ "type": "text", "text": "What is in this image?" },
{ "type": "image_url", "image_url": { "url": "data:image/png;base64,iVBORw0..." } }
]
}curl https://durango.sh/api/v1/chat/completions \
-H "Authorization: Bearer $DURANGO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "anthropic/claude-sonnet-4.5",
"messages": [
{ "role": "system", "content": "You are a concise assistant." },
{ "role": "user", "content": "Name three uses for a paperclip." }
],
"max_tokens": 200
}'{
"id": "gen-abc123",
"object": "chat.completion",
"created": 1750000000,
"model": "anthropic/claude-sonnet-4.5",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "1. Holding papers..." },
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 28,
"completion_tokens": 47,
"total_tokens": 75,
"credits_used": 0.0789
}
}The response is the OpenAI chat.completion shape, extended with usage.credits_used — the exact number of Durango credits charged for the request.
Streaming
Set "stream": true. The response is text/event-stream: a sequence of data: {json} lines, each an OpenAI chat.completion.chunk with incremental choices[0].delta.content, ending with data: [DONE]. A final chunk carries usage; credits are deducted when the stream completes.
curl -N https://durango.sh/api/v1/chat/completions \
-H "Authorization: Bearer $DURANGO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "openai/gpt-5.2", "stream": true, "messages": [{"role": "user", "content": "Write a haiku about rivers."}]}'data: {"id":"gen-x","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"Sil"}}]}
data: {"id":"gen-x","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"ver water"}}]}
data: {"id":"gen-x","object":"chat.completion.chunk","choices":[],"usage":{"prompt_tokens":14,"completion_tokens":19}}
data: [DONE]OpenAI SDKs
Because the endpoint is OpenAI-compatible, official OpenAI SDKs work by overriding the base URL.
from openai import OpenAI
client = OpenAI(
base_url="https://durango.sh/api/v1",
api_key="dgo_your_key_here",
)
completion = client.chat.completions.create(
model="anthropic/claude-sonnet-4.5",
messages=[{"role": "user", "content": "Hello!"}],
)
print(completion.choices[0].message.content)import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://durango.sh/api/v1",
apiKey: process.env.DURANGO_API_KEY,
});
const completion = await client.chat.completions.create({
model: "google/gemini-3-pro-preview",
messages: [{ role: "user", content: "Hello!" }],
stream: true,
});
for await (const chunk of completion) {
process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}Image generation
Generates an image with one model, saves it to your Durango gallery, and returns it. The call is synchronous: it responds when generation finishes, typically 10–60 seconds, so use a generous HTTP timeout (120s+ recommended). Requires a Pro or Ultra subscription (403 pro_required otherwise).
/api/v1/images/generations— Generate an imagemodelstringrequiredGET /api/v1/models?type=image.promptstringrequiredaspect_ratiostring1:1 (default), 16:9, 9:16, 4:3, 3:4, …image_sizestring1K (default) or 2K.response_formatstringurl (default) or b64_json to additionally inline base64 image bytes.source_imagesstring[]privatebooleantrue keeps the image out of public surfaces. Default false.curl https://durango.sh/api/v1/images/generations \
-H "Authorization: Bearer $DURANGO_API_KEY" \
-H "Content-Type: application/json" \
--max-time 180 \
-d '{
"model": "google/gemini-3-pro-image-preview",
"prompt": "A watercolor painting of a lighthouse at dawn",
"aspect_ratio": "16:9",
"response_format": "b64_json"
}'{
"id": "run_abc123",
"created": 1750000000,
"model": "google/gemini-3-pro-image-preview",
"data": [
{
"id": "img_xyz789",
"url": "https://durango.sh/api/v1/images/img_xyz789",
"b64_json": "iVBORw0KGgoAAAANS...",
"content_type": "image/png"
}
],
"text": null,
"usage": { "credits_used": 12 }
}data[].url requires the same API-key authentication to download — it is not a public link. Use response_format: "b64_json" if you want the bytes inline.text carries any accompanying text the model produced (some models narrate their generations).502 generation_failed with the upstream message; no credits are charged for failed generations.Image download
Returns the raw image bytes for any image generated by your account.
/api/v1/images/{imageId}— Download a generated imagecurl https://durango.sh/api/v1/images/img_xyz789 \
-H "Authorization: Bearer $DURANGO_API_KEY" \
-o lighthouse.pngVideo generation
Starts an asynchronous video generation run, saves completed videos to your Durango gallery, and returns a poll URL. Requires a Pro or Ultra subscription (403 pro_required otherwise).
/api/v1/videos/generations— Start a video generationmodelstringrequiredGET /api/v1/models?type=video.model_idsstring[]promptstringrequiredaspect_ratiostring16:9 (default), 9:16, 1:1, etc. depending on model support.resolutionstring720p (default), 1080p, 4K, etc. depending on model support.durationnumbergenerate_audiobooleansource_imagesstring[]source_image_idsstring[]privatebooleantrue keeps the video out of public surfaces. Default false.curl https://durango.sh/api/v1/videos/generations \
-H "Authorization: Bearer $DURANGO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "google/veo-3.1-fast",
"prompt": "A slow dolly shot through a rainy neon market",
"aspect_ratio": "16:9",
"resolution": "720p",
"duration": 5,
"private": true
}'{
"id": "run_abc123",
"object": "video.generation",
"created": 1750000000,
"status": "running",
"model": "google/veo-3.1-fast",
"models": ["google/veo-3.1-fast"],
"poll_url": "https://durango.sh/api/v1/videos/generations/run_abc123",
"data": []
}/api/v1/videos/generations/{runId}— Poll video status{
"id": "run_abc123",
"object": "video.generation",
"status": "completed",
"data": [
{
"id": "vid_xyz789",
"url": "https://durango.sh/api/v1/videos/vid_xyz789",
"model": "google/veo-3.1-fast",
"content_type": "video/mp4"
}
],
"usage": { "credits_used": 50 }
}Video download
Returns the raw video bytes for any video in your library — generated or edited.
/api/v1/videos/{videoId}— Download a videocurl https://durango.sh/api/v1/videos/vid_xyz789 \
-H "Authorization: Bearer $DURANGO_API_KEY" \
-o market.mp4The route streams from object storage and supports HTTP Range requests, so players can seek and clients can resume. A satisfied range answers 206 Partial Content with Content-Range; an impossible one answers 416. Every response carries Accept-Ranges: bytes. The same applies to /api/v1/videos/uploads/{id}/content.
curl https://durango.sh/api/v1/videos/vid_xyz789 \
-H "Authorization: Bearer $DURANGO_API_KEY" \
-H "Range: bytes=0-1048575" \
-o first-mb.partVideo library
Lists the videos in your library, newest first.
/api/v1/videos— List your videoslimitquery{
"object": "list",
"data": [
{
"id": "vid_xyz789",
"url": "/api/v1/videos/vid_xyz789",
"duration_seconds": 44.7,
"width": 1080,
"height": 1920,
"content_type": "video/mp4",
"byte_size": 9876543,
"prompt": "Create a 45s highlights reel",
"model": "durango/video-edit",
"kind": "edit",
"created_at": 1750000000000
}
]
}kind is generation for model generations and edit for outputs of the video editor.
Video uploads
Bring your own footage into Durango so it can be edited. Upload a file directly, or hand Durango a URL — including HLS and DASH manifests, which are normalized to MP4 on ingest.
/api/v1/videos/uploads— Upload a videourlstring.m3u8 / .mpd manifest.namestringfilefile partContent-Type: multipart/form-data with the video in the file field.# From a URL
curl https://durango.sh/api/v1/videos/uploads \
-H "Authorization: Bearer $DURANGO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/roll1.mp4", "name": "roll1.mp4" }'
# From a local file
curl https://durango.sh/api/v1/videos/uploads \
-H "Authorization: Bearer $DURANGO_API_KEY" \
-F "file=@roll1.mp4"{
"id": "upl_abc123",
"object": "video.upload",
"name": "roll1.mp4",
"content_type": "video/mp4",
"byte_size": 12345678,
"duration_seconds": 182.4,
"width": 1920,
"height": 1080,
"created_at": 1750000000000,
"url": "/api/v1/videos/uploads/upl_abc123/content"
}/api/v1/videos/uploads— List uploads (?limit=, default 50)/api/v1/videos/uploads/{id}— Get one upload/api/v1/videos/uploads/{id}/content— Download the raw bytes/api/v1/videos/uploads/{id}— Delete the upload and its stored object413 upload_too_large) and must contain a readable video stream (400 invalid_video otherwise). Duration and dimensions are probed at ingest.400 url_not_allowed. For an HLS manifest the same check applies to every segment, init segment and key URI inside the playlist; master playlists ingest their highest-bandwidth variant.429 rate_limit_exceeded, honour Retry-After). The name field is normalized on the way in: control characters are stripped and it is capped at 200 characters.409 upload_in_use — wait for the edit to settle first.Video editing
Starts an asynchronous edit over your videos. Describe the result and Durango watches the footage, plans the cuts with a chat model, and renders an H.264/AAC MP4. Requires a Pro or Ultra subscription (403 pro_required otherwise).
How planning works. A prompt-only edit runs three stages before anything renders. Probe reads duration, resolution, audio, and samples scene changes and loudness. Vision samples frames across each source (every 2 seconds on short footage, widening to at most ~8 seconds on long sources, with the source timestamp burned into each frame) and shows them to a vision model together with your prompt, which returns timestamped, labelled events scored 1–10 for how well each matches what you asked for — this is what lets the planner find a slow submission or a quiet reaction, which motion and loudness alone cannot see. It is the slow stage, and it is what progress 0.2–0.35 reports. Plan then builds the timeline from the highest-scoring relevant events, pads each one (~1–2s before, ~2–3s after), keeps them chronological, and respects output.max_duration.
Vision works to a fixed budget, so a long or many-source edit costs what a short one does: only the first 30 minutes of each source is looked at, the frame budget is shared across all sources in proportion to their duration, and the stage stops when its wall-clock budget runs out — sources it did not reach are planned from motion and loudness instead. If the vision model keeps refusing the request (it does not accept images, or the provider fails repeatedly) the pass is abandoned rather than retried chunk by chunk, and the reason appears in analysis.warnings.
The detected events come back on the edit object as analysis, so you can see what the planner was working from. If the planner model fails, the plan is built directly from the top-scored events; if the vision stage fails too, planning falls back to the scene-change/loudness heuristic. Either way the edit still renders, and plan.planner tells you which path ran. Vision tokens are included in the per-second edit price — there is no separate charge.
/api/v1/videos/edits— Start a video editsource_video_idsstring[]requiredpromptstringoutput.aspect_ratiostringoriginal (default), 9:16, 16:9, 1:1output.resolutionstringoriginal (default), 720p, 1080poutput.max_durationnumberoutput.formatstringmp4 — the only value in v1.operationsarraymodelstring400 invalid_planner_model otherwise).vision_modelstringcapabilities.vision: true — an unknown id is 404 model_not_found, one that does not accept image input is 400 invalid_vision_model.webhook_urlstringcurl https://durango.sh/api/v1/videos/edits \
-H "Authorization: Bearer $DURANGO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source_video_ids": ["upl_abc123"],
"prompt": "Create a 45s highlights reel of the best action moments",
"output": { "aspect_ratio": "9:16", "resolution": "1080p", "max_duration": 45 }
}'{
"id": "edt_abc123",
"object": "video.edit",
"status": "queued",
"created_at": 1750000000000,
"poll_url": "https://durango.sh/api/v1/videos/edits/edt_abc123"
}/api/v1/videos/edits/{id}— Poll edit statusstatus moves queued → planning → rendering → completed / failed, with progress between 0 and 1. Results are stored as normal videos, so they appear in GET /api/v1/videos and download from GET /api/v1/videos/{id}.
{
"id": "edt_abc123",
"object": "video.edit",
"status": "completed",
"progress": 1,
"prompt": "Create a 45s highlights reel of the best action moments",
"source_video_ids": ["upl_abc123"],
"output": { "aspect_ratio": "9:16", "resolution": "1080p", "max_duration": 45, "format": "mp4" },
"plan": { "operations": [{ "op": "clip", "source_index": 0, "start": 12.5, "end": 19 }], "planner": "ai" },
"analysis": {
"vision": { "model": "google/gemini-3-flash-preview", "frames": 96, "chunks_annotated": 3, "chunks_total": 3, "prompt_tokens": 41200, "completion_tokens": 980 },
"sources": [
{
"source_index": 0,
"duration_seconds": 192.4,
"width": 1920,
"height": 1080,
"event_count": 7,
"events": [
{ "start": 14, "end": 19, "label": "armbar submission", "significance": 9, "description": "Locks in an armbar and the opponent taps." }
]
}
]
},
"data": [
{
"id": "vid_xyz789",
"url": "/api/v1/videos/vid_xyz789",
"duration_seconds": 44.7,
"width": 1080,
"height": 1920,
"content_type": "video/mp4",
"byte_size": 9876543
}
],
"error": null,
"credits_used": 135,
"created_at": 1750000000000,
"updated_at": 1750000060000,
"completed_at": 1750000060000
}/api/v1/videos/edits— List edits (?limit=, default 20)Editing costs 2 credits per second of rendered output (minimum 5, ×1.5 at 1080p). Credits are reserved when the job is accepted — against the exact plan length when you send operations, against output.max_duration (default 60s) otherwise — and settled against the real output length, with the unused part returned. A failed edit is refunded in full; an edit interrupted by a server restart is reaped and refunded within ~30 minutes of its last progress update, and any edit still unfinished after about two hours is cancelled and refunded regardless.
An account may hold 10 edits in progress at once; one more returns 429 too_many_active_edits. The limit is a deployment setting (DURANGO_MAX_ACTIVE_EDITS) — ask your Durango operator to raise it if one key fronts many end-users. Renders are queued server-side, so an accepted job can sit at status: "queued" before it starts. When webhook_url is set, Durango POSTs the terminal edit object once — delivery is best-effort and not retried, so keep polling as the source of truth.
Edit operations
The plan IR the AI planner emits — and that you can send yourself for a deterministic edit.
cliprequiredreframemax 1speedmax 1audiomax 1keep (default) or mute.textmax 8top, center, or bottom.Clip bounds are clamped to the real source durations and the timeline is trimmed to output.max_duration (or the 60-second default cap on a prompt-only edit), so a render can never run past the length credits were reserved against. Malformed operations return 400 invalid_operations with a message naming the offending index.
Command line
The durango CLI wraps this API, including the full upload → edit → download flow.
npm install -g durango
durango login # or: export DURANGO_API_KEY=dgo_...
durango whoami # identity, tier, credit balanceEvery command accepts --json (exactly one JSON object on stdout, human text suppressed), --base-url, and --api-key, which makes the CLI safe to drive from scripts and coding agents. durango help --json prints a machine-readable schema of every command and flag.
# Bring footage in (local file or a URL / HLS / DASH manifest)
durango video upload ./roll1.mp4
durango video upload --url https://example.com/master.m3u8 --name roll1.mp4
durango uploads list
durango uploads delete <id>
# Edit it — uploads the local file first, waits for the render, saves the result
durango video edit \
--source-file ./roll1.mp4 \
--prompt "45s highlights reel of the best action moments" \
--aspect-ratio 9:16 --resolution 1080p --max-duration 45 \
--out highlights.mp4
# Or against ids you already have, without blocking
durango video edit --source <id> --prompt "..." --no-wait
durango video edits status <id>
durango video edits list
durango video listCredit balance
Returns the authenticated account's remaining credits, monthly limit, subscription tier, and period end.
/api/v1/credits— Get credit balancecurl https://durango.sh/api/v1/credits \
-H "Authorization: Bearer $DURANGO_API_KEY"{
"object": "credits",
"balance": 412.37,
"limit": 500,
"tier": "pro",
"period_end": 1751328000000
}Errors
All errors use a single envelope.
{
"error": {
"message": "Human-readable explanation.",
"type": "invalid_request_error",
"code": "model_not_found"
}
}missing_api_keyinvalid_api_keyinsufficient_creditspremium_model_requiredpro_requiredmodel_required, invalid_messages, …invalid_operationsinvalid_planner_modelinvalid_vision_modelinvalid_webhook_urlurl_not_allowed, url_not_video, invalid_videomodel_not_foundimage_not_foundvideo_generation_not_found, video_not_foundupload_not_found, video_edit_not_found, source_video_not_foundupload_in_useupload_too_largerange_not_satisfiablerate_limit_exceededtoo_many_active_editsupstream_errorupstream_unreachable, generation_failedFor AI agents
Pointers for autonomous agents working against this API.
GET /api/v1/openapi.json (schema) → GET /api/v1/models (what you can call) → GET /api/v1/credits (budget).Content-Type: application/json on POSTs.stream: false unless you need incremental output; the non-streaming response includes exact credits_used.poll_url, and download data[].url after completion.POST /api/v1/videos/edits → poll poll_url every 3–5 seconds → download data[0].url. To edit footage you did not generate, upload it first via POST /api/v1/videos/uploads and pass the returned id in source_video_ids.operations instead of prompt when you already know the cuts you want — it is deterministic, skips the planner, and returns the same result shape.output.max_duration explicitly on prompt-only edits: without it the output is capped at 60 seconds and credits are reserved against that cap.402 as terminal until the human upgrades or credits reset; treat 502 as retryable. Treat 429 as backoff-and-retry — too_many_active_edits clears as your other edits finish, rate_limit_exceeded after Retry-After.