Introduction
OttoPort is a unified gateway that routes LLM, image, and video generation through a single OpenAI-compatible surface. Every request is authenticated with one key and billed against one credit balance — no per-provider accounts to juggle.
- Chat is synchronous. Responses stream token-by-token, exactly like the OpenAI API.
- Image & video wait on the queue. The gateway holds the request until the provider finishes and returns the result URL.
- One catalog. Discover every model, its provider, and its price at
GET /v1/models.
Quickstart
Install the OpenAI SDK, set base_url to the OttoPort gateway, and use your op- key as the API key. Your first chat request is three lines away.
pip install openai # or: npm install openaicurl https://ottoport.ai/api/v1/chat/completions \
-H "Authorization: Bearer $OTTOPORT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-5",
"messages": [{ "role": "user", "content": "Hello!" }]
}'The same client works for images and video — switch the model id and call images.generate or the videos endpoint. See the sections below.
Authentication
Every request must include an Authorization header with your secret key. Keys are prefixed with op- and are created in API Keys. Keep them server-side — never ship a key in client code.
Authorization: Bearer op-xxxxxxxxxxxxxxxxxxxxxxxxOTTOPORT_API_KEY.Chat completions
/v1/chat/completionsOpenAI-compatible chat across GPT, Claude, and Gemini. Streaming, tool use, and function calling work unchanged — OttoPort translates between the OpenAI schema and each provider.
| Parameter | Type | Description |
|---|---|---|
modelrequired | string | Any chat model id, e.g. claude-sonnet-5. |
messagesrequired | array | Conversation as role/content objects. |
streamoptional | boolean | Stream tokens as server-sent events. Defaults to false. |
temperatureoptional | number | Sampling temperature, 0–2. |
max_tokensoptional | integer | Cap on generated tokens. |
top_poptional | number | Nucleus sampling cutoff. |
stopoptional | string | string[] | Sequence(s) that halt generation. |
curl https://ottoport.ai/api/v1/chat/completions \
-H "Authorization: Bearer $OTTOPORT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.4",
"messages": [
{ "role": "system", "content": "You are concise." },
{ "role": "user", "content": "Explain MCP in one line." }
],
"temperature": 0.7,
"stream": false
}'Response
{
"id": "chatcmpl-...",
"object": "chat.completion",
"model": "gpt-5.4",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "..." },
"finish_reason": "stop"
}
],
"usage": { "prompt_tokens": 24, "completion_tokens": 18, "total_tokens": 42 }
}Image generation
/v1/images/generationsGenerate or edit images with GPT Image and Nano Banana models. Pass image_url to run image-to-image edits. The gateway waits for the provider and returns hosted URLs.
The same endpoint serves layer decomposition. qwen-image-layered and seedream-5.0-layers take one image_url and return one data entry per RGBA layer, bottom of the stack first, each with a layer object — its index, and on Seedream its name, z-order and bounding box. The prompt is an optional caption there, and you are billed per layer returned.
| Parameter | Type | Description |
|---|---|---|
modelrequired | string | Image model id, e.g. nano-banana-pro. |
promptrequired | string | Text description of the image. On a layer-decomposition model, an optional caption of the input that steers the split. |
sizeoptional | string | e.g. 1024x1024 or 16:9. |
qualityoptional | string | How much compute the model spends, on models that list qualities — the GPT Image family and ideogram-4.5. low, medium (the default), high, plus xhigh and max on gpt-image-2.5. Priced by tier: max costs sixteen times medium. |
noptional | integer | Number of images to return. |
seedoptional | integer | Seed for reproducible output. |
image_urloptional | string | Source image for edits / image-to-image. Required on a layer-decomposition model. |
num_layersoptional | integer | Layer models only: how many RGBA layers to split into — 2 to 10 on qwen-image-layered. seedream-5.0-layers picks its own count. |
asyncoptional | boolean | Answer with a job instead of waiting for the image. Implied by webhook_url. Poll it at GET /v1/images/generations/:id, which carries status and progress. |
webhook_urloptional | string | HTTPS endpoint to notify when the job finishes, with webhook_secret. See Webhooks. |
horizontal_angleoptional | number | Camera models only (qwen-image-angles): orbit around the subject, 0–360°. With vertical_angle (-30–90°) and zoom (0–10); the prompt becomes optional. |
curl https://ottoport.ai/api/v1/images/generations \
-H "Authorization: Bearer $OTTOPORT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "nano-banana-pro",
"prompt": "a chrome torus on a black background, studio light",
"size": "1024x1024",
"n": 1
}'Response
{
"created": 1736200000,
"model": "nano-banana-pro",
"data": [{ "url": "https://cdn.ottoport.ai/images/9f2a...png", "expires_at": "2026-01-14T00:00:00.000Z" }],
"usage": { "cost": 0.24 }
}Video generation
/v1/videos/generationsGenerate video with Veo, Kling, and Seedance. Video takes minutes, so the request does not wait: it places a hold on your balance for the quoted price, queues the job and answers 202 with a job whose status is queued. Follow it by polling GET /v1/videos/generations/:id or, better, by registering a webhook. A finished job is completed with the result URLs, or failed with an error and the hold released. Files hosted by OttoPort carry expires_at; download them within three days.
The same endpoint serves depth estimation. video-depth-anything takes one video_url and nothing else — no prompt — and returns a grayscale depth video of the same length and size. It is billed per second of the source, which the gateway measures.
Every submission needs an Idempotency-Key header, a unique string of up to 255 characters such as a UUID. The key is tied to its request body, so reusing it with a different body returns 409 idempotency_key_reused.
| Parameter | Type | Description |
|---|---|---|
modelrequired | string | Video model id, e.g. kling-3.0. |
promptrequired | string | Text description of the shot. Not taken by a depth model. |
image_urloptional | string | First frame for image-to-video. |
durationoptional | integer | Clip length in seconds. |
aspect_ratiooptional | string | e.g. 16:9, 9:16. |
seedoptional | integer | Seed for reproducible output. |
last_frame_urloptional | string | Closing frame, with image_url, on models that list first-last-frame. |
reference_image_urlsoptional | string[] | Subjects the shot carries through, on models that list reference-to-video. |
reference_video_urlsoptional | string[] | Reference videos — motion, a performance, a scene — on models whose capabilities list referenceMedia, within its counts and seconds. Billed per second of reference video where the upstream charges for it; measured by the gateway. |
reference_audio_urlsoptional | string[] | Reference audio beside reference images or videos, on the same models. Checked, not billed. |
keyframesoptional | object[] | Images pinned in time, [{ image_url, time? }] with time in seconds, on models that list keyframes-to-video. Untimed keyframes are spread evenly. |
video_urloptional | string | A video to work from, on models that list video-edit or video-extend. The only field a depth model takes. An edit or a depth estimate is billed per second of this video, which the gateway measures; at most 60 seconds. |
video_taskoptional | string | edit (default) restyles video_url; extend continues it for duration more seconds. |
draftoptional | boolean | On seedance-2.5: render a 480p preview instead, billed at the draft rate. Text, image and reference-image requests; leave resolution out. The job's id is its draft_id. |
draft_idoptional | string | On seedance-2.5: the id of a finished draft job of yours, to render that shot at 1080p. Send only model and this (and a webhook) — prompt, inputs, duration and ratio come from the draft, which is billed on its length. Drafts can be completed for seven days. |
webhook_urloptional | string | HTTPS endpoint to notify when the job finishes. See Webhooks. |
webhook_secretoptional | string | Required with webhook_url. Your own secret; deliveries are signed with it. Never returned by the API. |
curl https://ottoport.ai/api/v1/videos/generations \
-H "Authorization: Bearer $OTTOPORT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"model": "kling-3.0",
"prompt": "cinematic product reveal, slow dolly, black background",
"duration": 5,
"aspect_ratio": "16:9",
"webhook_url": "https://example.com/hooks/ottoport",
"webhook_secret": "'"$OTTOPORT_WEBHOOK_SECRET"'"
}'
# Without a webhook, poll until status is completed or failed:
curl https://ottoport.ai/api/v1/videos/generations/$JOB_ID \
-H "Authorization: Bearer $OTTOPORT_API_KEY"Response
// 202 Accepted
{
"id": "7c1e9a52-...",
"job_id": "7c1e9a52-...",
"model": "kling-3.0",
"status": "queued",
"created": 1736200000,
"updated": "2026-01-11T00:00:00.000Z"
}
// GET /v1/videos/generations/7c1e9a52-... once finished
{
"id": "7c1e9a52-...",
"status": "completed",
"data": [{ "url": "https://cdn.ottoport.ai/videos/7c1e....mp4", "expires_at": "2026-01-14T00:00:00.000Z" }],
"webhook": {
"url": "https://example.com/hooks/ottoport",
"deliveries": [{ "event": "video.completed", "status": "delivered", "attempts": 1, "max_attempts": 5, "response_status": 200 }]
}
}Webhooks
Pass webhook_url and webhook_secret when you submit a video, or an image with async, and OttoPort POSTs the finished job to that URL instead of making you poll. Webhooks are registered per job; there is nothing to set up in the dashboard.
| Parameter | Type | Description |
|---|---|---|
webhook_urloptional | string | An absolute https:// URL on the default port, with no username or password in it. The host must resolve to a public address; private, loopback and link-local targets are rejected with unsafe_webhook_url. |
webhook_secretoptional | string | A random string you generate and keep, e.g. openssl rand -hex 32. Use one per endpoint. OttoPort stores it only to sign deliveries. |
Events
| Parameter | Type | Description |
|---|---|---|
video.completedoptional | event | The job finished. data.data holds the output URLs. |
video.failedoptional | event | The job failed. data.error says why, and nothing was charged. |
image.completedoptional | event | An image job finished — the same shape, from async image generation. |
image.failedoptional | event | An image job failed; nothing was charged. |
speech.completed / music.completedoptional | event | An audio job finished, from async speech or music generation. Poll it at GET /v1/audio/speech/:id or GET /v1/audio/music/:id. |
speech.failed / music.failedoptional | event | An audio job failed; nothing was charged. |
Each delivery is a JSON POST whose body is { event, data }, where data is the same job object GET /v1/videos/generations/:id returns.
POST /hooks/ottoport HTTP/1.1
content-type: application/json
user-agent: OttoPort-Webhooks/1.0
x-ottoport-event: video.completed
x-ottoport-delivery: 3f0b8d4e-...
x-ottoport-signature: t=1736200123,v1=5d2c0b...
{
"event": "video.completed",
"data": {
"id": "7c1e9a52-...",
"job_id": "7c1e9a52-...",
"model": "kling-3.0",
"status": "completed",
"created": 1736200000,
"updated": "2026-01-11T00:02:03.000Z",
"data": [{ "url": "https://cdn.ottoport.ai/videos/7c1e....mp4", "expires_at": "2026-01-14T00:02:03.000Z" }]
}
}Verify the signature
x-ottoport-signature is t=<unix seconds>,v1=<hex>, where the hex is HMAC-SHA256 of `${t}.${raw body}` keyed with your webhook_secret. Compute it over the raw request body, since re-serialized JSON will not match. Compare in constant time, and reject timestamps more than five minutes old to stop replays.
# Recompute the signature by hand for a saved delivery body:
TS=1736200123
printf '%s.%s' "$TS" "$(cat body.json)" \
| openssl dgst -sha256 -hmac "$OTTOPORT_WEBHOOK_SECRET" -hex
# Compare the digest with v1= in x-ottoport-signature.Delivery and retries
A delivery succeeds when your endpoint answers 2xx within 10 seconds. Any other status, a timeout, a connection error or a redirect counts as a failed attempt; redirects are not followed, so point webhook_url at the final address. Failed deliveries are retried with increasing delays, 1, 5, 30 and 120 minutes, for 5 attempts over about two and a half hours. Answer quickly and do slow work in the background.
Deliveries are at least once, so the same event can arrive twice. x-ottoport-delivery stays the same across automatic retries of one delivery; key idempotency on the job id in data.id. The job's webhook field in GET /v1/videos/generations/:id shows every attempt and your endpoint's response.
Redeliver
/v1/videos/generations/:id/webhookSends a finished job's webhook again right away and resets its attempt count, so this also works after all 5 attempts have failed. Redelivery gets a new x-ottoport-delivery id. You can do the same from the Webhooks page in the dashboard, which lists every delivery by workspace.
curl -X POST https://ottoport.ai/api/v1/videos/generations/$JOB_ID/webhook \
-H "Authorization: Bearer $OTTOPORT_API_KEY"Collections
Generated files are temporary: their URLs stop working at expires_at, a few days after they are made. A collection keeps them for good. Collections live inside the project of the API key that creates them, so each project — and each team — has its own. Saved files stay until you delete them.
Collections unlock once the account has topped up $10 in total (refunds count against it). Before that, every collection route that would store something answers 403 collections_locked with how much is left to go; listing and deleting always work. Saved files count toward the account's storage allowance, which grows with what it has paid in: 5 GB from $10, 50 GB from $100, 200 GB from $500 and 1 TB from $2,000. Past it, saving answers 413 storage_quota_exceeded, naming the next tier. GET /v1/collections reports the tier, usage and the next tier up.
Save while you generate
Add collection to any image, video, speech or music request. Each output then carries an asset_id and an asset_url that does not expire, alongside the usual temporary url. A locked account or an unknown collection is refused before anything is generated or charged. If one file cannot be saved, the generation still succeeds and that output carries asset_error instead; save it from its url while it lasts.
| Parameter | Type | Description |
|---|---|---|
collectionoptional | string | object | A collection id, or { "external_id": "…" } to address it by your own id. Add "create": true and a name to create it on first use. |
curl https://ottoport.ai/api/v1/images/generations \
-H "Authorization: Bearer $OTTOPORT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-image-2",
"prompt": "spring campaign hero, pastel, studio light",
"collection": { "external_id": "campaign-spring", "name": "Spring campaign", "create": true }
}'
# Each output carries the kept copy:
# "data": [{
# "url": "https://cdn.ottoport.ai/images/…", ← expires at expires_at
# "expires_at": "2026-10-15T09:12:00.000Z",
# "asset_id": "5b1c…",
# "asset_url": "https://cdn.ottoport.ai/assets/…" ← kept until deleted
# }]Endpoints
| Parameter | Type | Description |
|---|---|---|
GET /v1/collectionsoptional | list | The key's project's collections, with access: whether saving is unlocked, what has been paid in, and storage used. |
POST /v1/collectionsoptional | create | { name, description?, external_id?, metadata? }. external_id is unique within the project. |
GET · PATCH · DELETE /v1/collections/:idoptional | one | Read, rename, or delete a collection together with every file in it. |
GET /v1/assetsoptional | list | Saved files, newest first. Filter with collection_id and kind (image, video, audio); page with limit and offset. |
POST /v1/assetsoptional | save | { url, collection, prompt?, model?, metadata? } keeps a file that was already generated or uploaded. url must be an OttoPort URL that has not expired. |
GET · PATCH · DELETE /v1/assets/:idoptional | one | PATCH { collection } moves it to another collection of the project; { metadata } replaces its metadata. DELETE removes the file. |
POST /v1/uploadsoptional | upload | { content_type, size } returns an upload_url to PUT the file to and the url it will be read from. That URL works as any model input for a day; save it with POST /v1/assets to keep it. Uploading needs no unlock. |
# 1. Ask where to put the file
curl https://ottoport.ai/api/v1/uploads -H "Authorization: Bearer $OTTOPORT_API_KEY" \
-H "Content-Type: application/json" -d '{ "content_type": "image/png", "size": 482113 }'
# → { "upload_url": "https://…", "url": "https://cdn.ottoport.ai/uploads/….png", "headers": { "content-type": "image/png" } }
# 2. PUT the bytes there
curl -X PUT "$UPLOAD_URL" -H "content-type: image/png" --data-binary @face.png
# 3. Keep it (optional — "url" already works as a model input for a day)
curl https://ottoport.ai/api/v1/assets -H "Authorization: Bearer $OTTOPORT_API_KEY" \
-H "Content-Type: application/json" -d '{ "url": "https://cdn.ottoport.ai/uploads/….png", "collection": "<collection id>" }'The same collections are in the dashboard under Collections, and in the MCP server as ottoport_list_collections, ottoport_create_collection, ottoport_list_assets, ottoport_save_asset, ottoport_upload_file and ottoport_delete_asset, plus a collection argument on every generate tool.
List models
/v1/modelsReturns the catalog in OpenAI list format, enriched with modality, provider, and pricing. Filter by modality with the modality query parameter.
| Parameter | Type | Description |
|---|---|---|
modalityoptional | string | Filter to chat, image, or video. |
curl "https://ottoport.ai/api/v1/models?modality=video" \
-H "Authorization: Bearer $OTTOPORT_API_KEY"Response
{
"object": "list",
"data": [
{
"id": "kling-3.0",
"object": "model",
"owned_by": "kuaishou",
"modality": "video",
"display_name": "Kling 3.0",
"description": "...",
"pricing": { "perSecond": 0.084 }
}
]
}Model catalog
Every model available today, straight from the gateway registry. Chat is priced per 1M tokens (input / output); image per image; video per second.
| Model ID | Modality | Provider | Pricing |
|---|---|---|---|
gpt-6-astra | chat | openai | $11.08 / $55.39 per 1M |
gpt-6-astra-pro | chat | openai | $11.08 / $55.39 per 1M |
gpt-6.1-sol | chat | openai | $2.22 / $11.08 per 1M |
gpt-6.1-sol-pro | chat | openai | $2.22 / $11.08 per 1M |
gpt-6-sol | chat | openai | $2.22 / $11.08 per 1M |
gpt-6-sol-pro | chat | openai | $2.22 / $11.08 per 1M |
gpt-6-luna | chat | openai | $0.111 / $0.554 per 1M |
gpt-6-luna-pro | chat | openai | $0.111 / $0.554 per 1M |
gpt-5.6-sol | chat | openai | $2.22 / $11.08 per 1M |
gpt-5.6-terra | chat | openai | $2.22 / $13.29 per 1M |
gpt-5.6-luna | chat | openai | $0.222 / $1.33 per 1M |
gpt-5.5 | chat | openai | $5.54 / $33.23 per 1M |
gpt-5.2 | chat | openai | $1.94 / $15.51 per 1M |
gpt-4o | chat | openai | $2.77 / $11.08 per 1M |
sonar-deep-research | chat | perplexity | $2.22 / $46.53 per 1M |
gpt-5.4 | chat | openai | $2.77 / $16.62 per 1M |
gpt-5.4-mini | chat | openai | $0.831 / $4.98 per 1M |
gpt-5.4-nano | chat | openai | $0.222 / $1.38 per 1M |
claude-fable-5.1 | chat | anthropic | $11.08 / $55.39 per 1M |
claude-opus-5.5 | chat | anthropic | $4.43 / $22.15 per 1M |
claude-opus-5 | chat | anthropic | $5.54 / $27.69 per 1M |
claude-fable-5 | chat | anthropic | $11.08 / $55.39 per 1M |
claude-opus-4.8 | chat | anthropic | $5.54 / $27.69 per 1M |
claude-sonnet-5.5 | chat | anthropic | $2.22 / $11.08 per 1M |
claude-sonnet-5 | chat | anthropic | $2.22 / $11.08 per 1M |
claude-haiku-5.5 | chat | anthropic | $0.111 / $0.554 per 1M |
claude-haiku-4.5 | chat | anthropic | $1.11 / $5.54 per 1M |
gemini-3.8-flash | chat | $0.831 / $4.15 per 1M | |
gemini-3.7-flash | chat | $0.831 / $4.15 per 1M | |
gemini-3.5-flash | chat | $1.66 / $9.97 per 1M | |
gemini-3.1-pro-preview | chat | $2.22 / $13.29 per 1M | |
video-understanding | chat | ottoport | $2.22 / $13.29 per 1M |
gemini-3.1-flash-lite | chat | $0.277 / $1.66 per 1M | |
gemini-3.5-flash-lite | chat | $0.332 / $2.77 per 1M | |
gemini-2.5-flash | chat | $0.332 / $2.77 per 1M | |
kimi-k3 | chat | moonshotai | $3.32 / $16.62 per 1M |
kimi-k2.7-code | chat | moonshotai | $0.756 / $3.68 per 1M |
kimi-k2.6 | chat | moonshotai | $0.693 / $3.58 per 1M |
kimi-k2.5 | chat | moonshotai | $0.394 / $2.13 per 1M |
kimi-k2-thinking | chat | moonshotai | $0.63 / $2.63 per 1M |
kimi-k2-0905 | chat | moonshotai | $0.63 / $2.63 per 1M |
kimi-k2 | chat | moonshotai | $0.598 / $2.42 per 1M |
glm-5.3 | chat | z-ai | $1.22 / $3.82 per 1M |
glm-5.3-flash | chat | z-ai | $0.166 / $0.554 per 1M |
glm-5.2 | chat | z-ai | $0.775 / $2.44 per 1M |
deepseek-v4.1-flash | chat | deepseek | $0.155 / $0.498 per 1M |
deepseek-v4-pro-0813 | chat | deepseek | $1.25 / $3.73 per 1M |
deepseek-v4-pro | chat | deepseek | $1.06 / $2.13 per 1M |
deepseek-v4-flash | chat | deepseek | $0.101 / $0.202 per 1M |
grok-4.7 | chat | x-ai | $2.22 / $6.65 per 1M |
grok-4.6 | chat | x-ai | $2.22 / $6.65 per 1M |
grok-4.5 | chat | x-ai | $2.22 / $6.65 per 1M |
qwen3.8-max | chat | qwen | $2.22 / $6.65 per 1M |
qwen3.8-flash | chat | qwen | $0.166 / $0.521 per 1M |
jev-1.13 | decision | typesafe | $0.047 per 1M input |
gpt-image-2.5 | image | openai | $0.026 / image |
gpt-image-2.5-flare | image | openai | $0.026 / image |
gpt-image-2 | image | openai | $0.07 / image |
gpt-image-1.5 | image | openai | $0.044 / image |
gpt-image-1-mini | image | openai | $0.022 / image |
nano-banana-2.1 | image | $0.088 / image | |
nano-banana-2 | image | $0.088 / image | |
nano-banana-pro | image | $0.165 / image | |
nano-banana-lite | image | $0.044 / image | |
seedream-4.7 | image | bytedance | $0.033 / image |
seedream-4.7-sequential | image | bytedance | $0.033 / image |
qwen-image-3.0 | image | qwen | $0.033 / image |
qwen-image-3.0-pro | image | qwen | $0.044 / image |
wan-2.7-pro-image | image | alibaba | $0.083 / image |
flux-2-pro | image | black-forest-labs | $0.033 / image |
ideogram-4.5 | image | ideogram | $0.066 / image |
qwen-image-angles | image | qwen | $0.04 / image |
qwen-image-layered | image | qwen | $0.028 / layer |
seedream-5.0-layers | image | bytedance | $0.037 / layer |
midjourney-v7 | image | midjourney | $0.088 / image |
midjourney-v8 | image | midjourney | $0.088 / image |
midjourney-v8.1 | image | midjourney | $0.095 / image |
midjourney-v8.2 | image | midjourney | $0.088 / image |
seedance-2.5 | video | bytedance | $0.35 / sec |
seedance-2.0 | video | bytedance | $0.227 / sec |
seedance-2.0-fast | video | bytedance | $0.11 / sec |
seedance-2.0-mini | video | bytedance | $0.043 / sec |
seedance-1.5-pro | video | bytedance | $0.063 / sec |
seedance-1.5-pro-fast | video | bytedance | $0.05 / sec |
minimax-h3 | video | minimax | $0.163 / sec |
minimax-h3-max | video | minimax | $0.178 / sec |
flux-3 | video | black-forest-labs | $0.213 / sec |
kling-3.0 | video | kuaishou | $0.105 / sec |
kling-3.0-turbo | video | kuaishou | $0.119 / sec |
kling-3.0-pro | video | kuaishou | $0.179 / sec |
kling-3.0-4k | video | kuaishou | $0.446 / sec |
kling-o1 | video | kuaishou | $0.119 / sec |
kling-o3 | video | kuaishou | $0.105 / sec |
kling-o3-pro | video | kuaishou | $0.14 / sec |
kling-o3-4k | video | kuaishou | $0.525 / sec |
wan3.0-t2v | video | alibaba | $0.2 / sec |
wan3.0-i2v | video | alibaba | $0.2 / sec |
wan3.0-prime-t2v | video | alibaba | $0.315 / sec |
wan3.0-prime-i2v | video | alibaba | $0.315 / sec |
gemini-omni-1.1-flash | video | $0.138 / sec | |
gemini-omni-flash | video | $0.156 / sec | |
veo-3.1 | video | $0.528 / sec | |
veo-3.1-fast | video | $0.132 / sec | |
veo-3.1-lite | video | $0.066 / sec | |
hailuo-2.3 | video | minimax | $0.07 / sec |
hailuo-2.3-fast | video | minimax | $0.048 / sec |
hailuo-02 | video | minimax | $0.07 / sec |
wan2.7-t2v | video | alibaba | $0.125 / sec |
wan2.7-i2v | video | alibaba | $0.125 / sec |
wan2.6-t2v | video | alibaba | $0.088 / sec |
wan2.6-i2v | video | alibaba | $0.088 / sec |
wan2.6-i2v-flash | video | alibaba | $0.031 / sec |
wan2.5-t2v | video | alibaba | $0.063 / sec |
wan2.5-i2v | video | alibaba | $0.063 / sec |
video-depth-anything | video | bytedance | $0.063 / sec |
kling-avatar-2-standard | video | kuaishou | $0.07 / sec |
kling-avatar-2-pro | video | kuaishou | $0.144 / sec |
omnihuman-1.5 | video | bytedance | $0.2 / sec |
sync-lipsync-2 | video | sync | $0.063 / sec |
sync-lipsync-2-pro | video | sync | $0.104 / sec |
kling-lipsync | video | kuaishou | $0.018 / sec |
kling-2.6-motion-control | video | kuaishou | $0.088 / sec |
kling-2.6-pro-motion-control | video | kuaishou | $0.14 / sec |
kling-3.0-motion-control | video | kuaishou | $0.158 / sec |
kling-3.0-pro-motion-control | video | kuaishou | $0.21 / sec |
seedream-5.0 | image | bytedance | $0.05 / image |
seedream-5.0-lite | image | bytedance | $0.039 / image |
gpt-4o-mini-tts | tts | openai | — |
eleven-v3 | tts | elevenlabs | — |
eleven-turbo-2.5 | tts | elevenlabs | — |
gemini-2.5-flash-tts | tts | — | |
seed-audio-1.0 | tts | bytedance | — |
speech-2.8-turbo | tts | minimax | — |
speech-2.8-hd | tts | minimax | — |
gpt-4o-mini-transcribe | stt | openai | $0.0000607 / sec |
gpt-4o-transcribe | stt | openai | $0.000121 / sec |
qwen3-asr-flash | stt | qwen | $0.0000425 / sec |
qwen3-embedding-8b | embedding | qwen | $0.011 per 1M input |
bge-m3 | embedding | baai | $0.011 per 1M input |
text-embedding-3-small | embedding | openai | $0.022 per 1M input |
text-embedding-3-large | embedding | openai | $0.144 per 1M input |
voyage-4 | embedding | voyageai | $0.066 per 1M input |
voyage-4-lite | embedding | voyageai | $0.022 per 1M input |
suno-v6 | music | suno | — |
suno-v6-mini | music | suno | — |
suno-v6-wild | music | suno | — |
lyria-2 | music | — | |
seed-music | music | bytedance | — |
Errors
Errors use standard HTTP status codes and an OpenAI-style error object with a stable code. The type mirrors code for SDK compatibility.
{
"error": {
"message": "`model` and `messages` are required",
"type": "invalid_request",
"code": "invalid_request"
}
}| Status | Code | When |
|---|---|---|
| 400 | invalid_request | Missing or malformed body (e.g. no model or messages). |
| 401 | missing_api_key | No Authorization: Bearer header was sent. |
| 401 | invalid_api_key | Key is malformed or not recognized. |
| 402 | insufficient_credits | The account's credit balance is exhausted. |
| 404 | model_not_found | The requested model id is not in the catalog. |
| 429 | rate_limited | Too many requests — back off and retry. |
| 502 | upstream_error | The upstream provider failed or timed out. |
Rate limits
Limits scale with your spend tier. When you exceed them the gateway returns 429 rate_limited — back off with exponential retry and honor any Retry-After header. Need higher throughput or dedicated capacity? Talk to us.
SDKs & tools
The gateway is the product; every other surface is a thin client over it. Use the one that fits your workflow — they all share the same model ids and one key.
OpenAI SDKs
Point base_url at the gateway from the Python or Node OpenAI SDK.
Quickstart →MCP & CLI
Drive OttoPort from agents, terminals, and CI with the MCP server and CLI.
MCP & CLI →Playground
Try any model interactively before wiring it into your app.
Open playground →API Keys
Create, rotate, and revoke your op- keys.
Manage keys →