# Durango API

The Durango API gives programmatic access to every chat, image-generation, and video-generation model available in the Durango app. Requests authenticate with a personal API key and consume the same usage credits as the web app — there is no separate billing.

- **Base URL**: `https://durango.sh`
- **Machine-readable spec (OpenAPI 3.1)**: `GET https://durango.sh/api/v1/openapi.json`
- **This document as plain markdown (for AI agents)**: `GET https://durango.sh/docs/llms.txt`
- **Create / manage API keys**: [https://durango.sh/settings/api-keys](https://durango.sh/settings/api-keys)

All endpoints accept and return JSON (except image/video downloads, which return raw bytes, and streaming chat, which returns Server-Sent Events). CORS is enabled on all `/api/v1/*` endpoints, so they can be called from browsers as well as servers.

## Endpoints at a glance

| Method | Path | Purpose |
|---|---|---|
| POST | `/api/v1/chat/completions` | Chat completion (OpenAI-compatible, optional SSE streaming) |
| POST | `/api/v1/images/generations` | Generate an image (synchronous) |
| GET | `/api/v1/images/{imageId}` | Download a generated image (raw bytes) |
| POST | `/api/v1/videos/generations` | Start a video generation (asynchronous) |
| GET | `/api/v1/videos/generations/{runId}` | Poll video generation status |
| GET | `/api/v1/videos/{videoId}` | Download a generated video (raw bytes) |
| GET | `/api/v1/models` | List available chat + image + video models |
| GET | `/api/v1/credits` | Current credit balance, tier, and reset date |
| GET | `/api/v1/openapi.json` | OpenAPI 3.1 specification (no auth required) |

---

## Authentication

Create an API key at [Settings → API Keys](https://durango.sh/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_here
```

A 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/revoked keys return `401` with an error body (see [Errors](#errors)).

## Credits & billing

- **1 credit = $0.01 USD.** Balances are tracked fractionally (a cheap chat message might cost 0.02 credits).
- Credits are shared with the Durango app — API usage draws from the same monthly balance and appears in the same usage history.
- Monthly allowance by subscription tier: **Free** 10 credits, **Pro** 500 credits, **Ultra** 5,000 credits. Credits reset at the start of each billing period.
- **Chat cost** = input tokens × model input price + output tokens × model output price (model prices come from the catalog; see `pricing` in `GET /api/v1/models`). The exact charge is returned on every non-streaming response as `usage.credits_used`.
- **Image cost** is token-based when the model reports usage, with per-model fallback pricing otherwise (roughly 3–18 credits per image depending on the model). The charge is returned as `usage.credits_used`.
- **Video cost** is reserved when the async run starts, then finalized from provider usage or per-model fallback pricing when the run completes. Poll `/api/v1/videos/generations/{runId}` for final `usage.credits_used`.
- When your balance reaches 0, requests fail with `402 insufficient_credits` until your monthly reset or an upgrade.
- Some models require a minimum subscription tier (`required_tier` of `pro` or `ultra` in the model list). Calling them from a lower tier returns `403`.
- Image and video generation require a **Pro** or **Ultra** subscription.

Check your balance any time:

```bash
curl https://durango.sh/api/v1/credits \
  -H "Authorization: Bearer $DURANGO_API_KEY"
```

```json
{
  "object": "credits",
  "balance": 412.37,
  "limit": 500,
  "tier": "pro",
  "period_end": 1751328000000
}
```

---

## List models

`GET /api/v1/models` — lists every model callable through the API. Optional query parameter `type=chat`, `type=image`, or `type=video`.

```bash
curl "https://durango.sh/api/v1/models?type=chat" \
  -H "Authorization: Bearer $DURANGO_API_KEY"
```

Response:

```json
{
  "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" }
    }
  ]
}
```

Field notes:

- `id` — pass this as `model` in chat/image/video requests.
- `type` — `chat` models go to `/api/v1/chat/completions`; `image` models go to `/api/v1/images/generations`; `video` models go to `/api/v1/videos/generations`.
- `required_tier` — `free`, `pro`, or `ultra`: the minimum subscription needed to call the model.
- `pricing` — USD per token (`prompt` = input, `completion` = output), when known. Multiply by 1,000,000 for $/1M tokens.
- `capabilities.vision` — whether the model accepts image inputs in chat messages.
- `video_capabilities` — supported durations, resolutions, aspect ratios, and frame-image inputs for video models.

---

## Chat completions

`POST /api/v1/chat/completions` — OpenAI-compatible chat completion. Only models present in the Durango catalog are accepted (`404 model_not_found` otherwise).

### Request body

| Field | Type | Required | Description |
|---|---|---|---|
| `model` | string | yes | Model id from `GET /api/v1/models?type=chat` |
| `messages` | array | yes | Conversation messages, OpenAI format (see below) |
| `stream` | boolean | no | `true` to receive an SSE stream (default `false`) |
| `temperature` | number | no | Sampling temperature |
| `max_tokens` | integer | no | Cap on generated tokens |
| `top_p` | number | no | Nucleus sampling |
| `frequency_penalty` / `presence_penalty` | number | no | Repetition controls |
| `stop` | string \| string[] | no | Stop sequences |
| `seed` | integer | no | Deterministic sampling on supporting models |
| `response_format` | object | no | e.g. `{"type": "json_object"}` for JSON mode |
| `tools` | array | no | OpenAI-style function tool definitions, forwarded to the model |
| `tool_choice` | any | no | Tool selection directive |

### Messages

Each message is `{ "role": ..., "content": ... }`. Roles: `system`, `developer`, `user`, `assistant`, `tool`. `content` is a string, or — for multimodal input on vision-capable models — an array of parts:

```json
{
  "role": "user",
  "content": [
    { "type": "text", "text": "What is in this image?" },
    { "type": "image_url", "image_url": { "url": "data:image/png;base64,iVBORw0..." } }
  ]
}
```

`image_url.url` accepts HTTPS URLs or data URLs. Assistant messages with `tool_calls` and `role: "tool"` results are supported for function-calling loops.

### Non-streaming example

```bash
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
  }'
```

Response (OpenAI `chat.completion` shape, plus `usage.credits_used`):

```json
{
  "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
  }
}
```

### 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.

```bash
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]
```

### Using OpenAI client libraries

Because the endpoint is OpenAI-compatible, official OpenAI SDKs work by overriding the base URL:

```python
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)
```

```javascript
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

`POST /api/v1/images/generations` — generates one 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).

### Request body

| Field | Type | Required | Description |
|---|---|---|---|
| `model` | string | yes | Model id from `GET /api/v1/models?type=image` |
| `prompt` | string | yes | Description of the image to generate |
| `aspect_ratio` | string | no | `"1:1"` (default), `"16:9"`, `"9:16"`, `"4:3"`, `"3:4"`, ... |
| `image_size` | string | no | `"1K"` (default) or `"2K"` |
| `response_format` | string | no | `"url"` (default) or `"b64_json"` to inline base64 image bytes |
| `source_images` | string[] | no | Up to 4 reference images for image-to-image editing; each an HTTPS URL or data URL, max 10 MB |
| `private` | boolean | no | `true` to keep the image out of public surfaces (default `false`) |

### Example

```bash
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"
  }'
```

Response:

```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 }
}
```

Notes:

- `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).
- Failures (e.g. content-policy refusals, model errors) return `502 generation_failed` with the upstream message; no credits are charged for failed generations.

### Downloading images

`GET /api/v1/images/{imageId}` returns the raw image bytes (`Content-Type: image/png` etc.) for any image generated by your account:

```bash
curl https://durango.sh/api/v1/images/img_xyz789 \
  -H "Authorization: Bearer $DURANGO_API_KEY" \
  -o lighthouse.png
```

---

## Video generation

`POST /api/v1/videos/generations` — starts an asynchronous video generation run. The initial response is `202` with a `poll_url`; poll until `status` is `completed` or `failed`.

Requires a **Pro** or **Ultra** subscription (`403 pro_required` otherwise).

### Request body

| Field | Type | Required | Description |
|---|---|---|---|
| `model` | string | yes | Model id from `GET /api/v1/models?type=video` |
| `model_ids` | string[] | no | Optional multi-model run; `model` is included with these ids |
| `prompt` | string | yes | Description of the video to generate |
| `aspect_ratio` | string | no | `"16:9"` (default), `"9:16"`, `"1:1"`, etc. depending on model support |
| `resolution` | string | no | `"720p"` (default), `"1080p"`, `"4K"`, etc. depending on model support |
| `duration` | number | no | Seconds, 1–15, depending on model support |
| `generate_audio` | boolean | no | Ask for audio when the selected model supports it |
| `source_images` | string[] | no | Up to 2 reference images; HTTPS URL, data URL, or authenticated `/api/v1/images/{id}` URL from the same account |
| `source_image_ids` | string[] | no | Up to 2 generated image ids from the same Durango account |
| `private` | boolean | no | `true` to keep the video out of public surfaces (default `false`) |

### Start a run

```bash
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
  }'
```

Response:

```json
{
  "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": []
}
```

### Poll the run

```bash
curl https://durango.sh/api/v1/videos/generations/run_abc123 \
  -H "Authorization: Bearer $DURANGO_API_KEY"
```

Completed response:

```json
{
  "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 }
}
```

### Downloading videos

`GET /api/v1/videos/{videoId}` returns raw video bytes for any video generated by your account:

```bash
curl https://durango.sh/api/v1/videos/vid_xyz789 \
  -H "Authorization: Bearer $DURANGO_API_KEY" \
  -o market.mp4
```

---

## Errors

All errors use a single envelope:

```json
{
  "error": {
    "message": "Human-readable explanation.",
    "type": "invalid_request_error",
    "code": "model_not_found"
  }
}
```

| HTTP status | `code` | Meaning |
|---|---|---|
| 401 | `missing_api_key` | No key in `Authorization` / `X-API-Key` header |
| 401 | `invalid_api_key` | Key is malformed, unknown, or revoked |
| 402 | `insufficient_credits` | Credit balance is 0 — upgrade or wait for monthly reset |
| 403 | `premium_model_required` | Model needs a Pro/Ultra subscription |
| 403 | `pro_required` | Image generation needs a Pro/Ultra subscription |
| 400 | `model_required`, `prompt_required`, `invalid_messages`, ... | Malformed request (message explains exactly what) |
| 404 | `model_not_found` | Model id is not in the catalog — check `GET /api/v1/models` |
| 404 | `image_not_found` | Image id unknown or owned by another account |
| 404 | `video_generation_not_found`, `video_not_found` | Video run/id unknown or owned by another account |
| 429 / 4xx | `upstream_error` | The upstream model provider rejected the request |
| 502 | `upstream_unreachable`, `generation_failed` | Upstream failure — safe to retry |
| 500 | `server_error` and others | Unexpected server error |

## Notes for AI agents

- Discover capabilities in this order: `GET /api/v1/openapi.json` (schema), `GET /api/v1/models` (what you can call), `GET /api/v1/credits` (budget).
- Always send `Content-Type: application/json` on POSTs.
- Prefer `stream: false` unless you need incremental output; the non-streaming response includes exact `credits_used`.
- Image generation is slow (10–60s) — set client timeouts to at least 120 seconds and do not retry while a request is in flight (each retry costs credits if it succeeds).
- Video generation is asynchronous — start the run once, poll `poll_url`, and download `data[].url` after completion.
- Treat `402` as terminal until the human upgrades or credits reset; treat `502` as retryable.
