Developer documentation

One API, every modality.

Full reference for the AIport public API — text completions, image generation, image-to-image, and video. Async jobs are polled through a single endpoint. Billed in IDR; no monthly fee, no expiry.

↓ Download for LLM contextJump to QuickstartSee pricing →

Introduction

AIport is a unified AI API gateway for Indonesian developers — one API key, one IDR balance, every modality.

Why AIport.

  • Top up in IDR. No FX markup, no monthly fee, no expiry. Each call deducts exactly what it costs from your balance.
  • One key, every modality. Text, image, image-to-image, and video generation behind a single Authorization: Bearer header.
  • Provider routing & auto-refund. When a provider errors mid-job, credits are refunded automatically.
  • OpenAI-compatible text endpoint. POST /v1/chat/completions is a drop-in for the OpenAI SDK, including stream: true.

Base URL

https://aiport.id

All endpoints are JSON unless noted. Async modalities (image, img2img, video) return a job_id you poll via GET /v1/jobs/{job_id}.

Authentication

Every /v1/* call requires a Bearer token. Create one in your dashboard at /dashboard/api-keys.

Send your API key in the Authorization header:

Authorization: Bearer aip_live_...

Keys are scoped to your account and bill against your IDR balance. Rotate compromised keys immediately by deleting and re-creating from the dashboard. Never embed an API key in client-side code — proxy through your own backend.

Quickstart

Every endpoint is plain HTTP + JSON. The only thing that varies is whether the response is synchronous or asynchronous.

Sync vs async — read this first.

Every AIport endpoint is a normal HTTP request: POST (or GET for jobs), Authorization: Bearer header, JSON body. The difference is only in how the result comes back:

  • Synchronous — /v1/chat/completions (alias /v1/text/completions). The response body *is* the result, unless you opt into stream: true. Typical latency: 1–10 s.
  • Asynchronous — /v1/image/generate, /v1/image/img2img, all /v1/video/*. The POST returns immediately with { "job_id": "...", "status": "pending" }. You then poll GET /v1/jobs/{job_id} every few seconds until status becomes done (read result_url) or failed. Typical latency: 30–120 s for video, 5–30 s for image.

Async exists because images and videos can take a minute or more — too long to hold open a single HTTP request reliably.

1. Synchronous: text completion

The text endpoint is OpenAI-compatible — point the OpenAI SDK at https://aiport.id/v1, or call it directly:

curl -X POST https://aiport.id/v1/chat/completions \
  -H "Authorization: Bearer $AIPORT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"deepseek-v4-flash","messages":[{"role":"user","content":"Hello"}]}'

Response (200) — final answer is already inside choices[0].message.content:

{ "choices": [ { "message": { "role": "assistant", "content": "Hi!" } } ], "usage": { ... } }

Add "stream": true to receive the answer incrementally as Server-Sent Events (OpenAI chat.completion.chunk objects, ending with data: [DONE]). See the Text completions section for the chunk shape.

2. Asynchronous: image generation — step by step

Step 2a — POST the request. Same shape as text, just a different URL:

curl -X POST https://aiport.id/v1/image/generate \
  -H "Authorization: Bearer $AIPORT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-image-2","prompt":"Sunset over Mount Bromo","resolution":"2k"}'

Response (202) — note: no image yet, only a job id to poll:

{ "job_id": "job_01HX...", "status": "pending" }

Step 2b — GET the job until it's done:

curl https://aiport.id/v1/jobs/job_01HX... \
  -H "Authorization: Bearer $AIPORT_API_KEY"

Response (200) — when finished, status is done and result_url points at the image. Its host can vary between jobs and isn't guaranteed to be under aiport.id — don't hardcode a domain allowlist against it:

{ "job_id": "job_01HX...", "status": "done", "result_url": "https://result.example/..." }

3. Same thing, scripted with a poll loop (optional)

The block below is just the two-step flow wrapped in a shell loop — useful for trying things in a terminal:

# Submit
JOB=$(curl -s -X POST https://aiport.id/v1/image/generate \
  -H "Authorization: Bearer $AIPORT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-image-2","prompt":"Sunset over Mount Bromo","resolution":"2k"}' | jq -r .job_id)

# Poll every 3s
while true; do
  STATUS=$(curl -s https://aiport.id/v1/jobs/$JOB \
    -H "Authorization: Bearer $AIPORT_API_KEY")
  echo "$STATUS"
  case "$(echo $STATUS | jq -r .status)" in
    done|failed) break ;;
  esac
  sleep 3
done

In production, replace the shell loop with a job-polling routine in your language of choice (or a webhook on your side if you don't want to poll).

Text completions

Chat-style generation with an OpenAI-compatible request/response shape. POST /v1/chat/completions (alias: /v1/text/completions). Synchronous by default, or set stream: true for Server-Sent Events. GET /v1/models lists the available text models for OpenAI-compatible clients.

POST/v1/chat/completions

Chat-style text generation. OpenAI-compatible request and response shapes — a drop-in for the OpenAI SDK (set base_url to https://aiport.id/v1). The legacy path /v1/text/completions is a backward-compatible alias.

Auth: Authorization: Bearer <api_key>
Requestapplication/json
messages
ChatMessage[]required
Conversation history. Each message: { role: 'system' | 'developer' | 'user' | 'assistant' | 'tool', content }. content is a string or an array of multipart content parts; assistant messages with tool_calls may have null content, and tool messages need a string content plus tool_call_id. 'developer' is treated as 'system'. No fixed character limit — input is bounded by the selected model's context window and billed per token.
model
stringdefault deepseek-v4-flash
Text model id. See the Models section for the full list of available text models.
max_tokens
integer
Upper bound on generated tokens. Capped by the model's own max_output_tokens.
temperature
number
Sampling temperature, range 0–2. Claude models other than Haiku 4.5 (Sonnet 5 / 5.5, Opus 5 / 5.5, Fable 5.1) don't support custom sampling, so on those models this field is ignored and the model's default is used.
reasoning_effort
string
Only honored by models that advertise reasoning support. "none" disables reasoning on OpenAI GPT models and is ignored by other providers. GPT reasoning models only accept tools with "none" — when a request carries tools and omits reasoning_effort, "none" is sent automatically.
Allowed: none low medium high
stream
booleandefault false
When true, the response is streamed as Server-Sent Events of OpenAI chat.completion.chunk objects, terminated by a data: [DONE] line. See the Streaming example below.
tools
Tool[]
Functions the model may call, in the OpenAI shape: { type: 'function', function: { name, description, parameters } } where parameters is a JSON Schema object. When the model decides to call one, the response message has tool_calls and finish_reason: 'tool_calls'. See the Tool calling section.
tool_choice
string | object
Controls whether the model calls a tool. 'auto' (default when tools are present) lets the model decide, 'none' forces a text reply, 'required' forces some tool call, or pass { type: 'function', function: { name } } to force a specific one. Claude Sonnet 5.5, Opus 5.5, and Fable 5.1 don't support forced tool calls: on these models 'required' and a named function are sent as 'auto' plus a system instruction to call the tool, so a call is strongly encouraged but not guaranteed.
Allowed: auto none required
parallel_tool_calls
booleandefault true
Whether the model may emit multiple tool calls in a single turn. Forwarded to upstreams that support it; ignored otherwise.
Example
curl -X POST https://aiport.id/v1/chat/completions \
  -H "Authorization: Bearer $AIPORT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "deepseek-v4-flash",
    "messages": [
      {"role": "user", "content": "Write a haiku about Jakarta traffic."}
    ]
  }'
Response200
{
  "id": "chatcmpl_...",
  "object": "chat.completion",
  "created": 1716480000,
  "model": "deepseek-v4-flash",
  "choices": [
    {
      "index": 0,
      "message": {"role": "assistant", "content": "..."},
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 12,
    "completion_tokens": 38,
    "total_tokens": 50,
    "prompt_tokens_details": {"cached_tokens": 0},
    "completion_tokens_details": {"reasoning_tokens": 0}
  }
}

Synchronous unless stream is true. prompt_tokens_details.cached_tokens reflects prompt-cache hits (billed at a reduced input rate when the upstream reports them, currently DeepSeek); completion_tokens_details.reasoning_tokens counts reasoning output. Standard OpenAI params (top_p, stop, presence_penalty, frequency_penalty, seed, …) are forwarded verbatim to OpenAI-compatible upstreams; unknown params are dropped.

Streamingstream: true

With stream: true the response is text/event-stream. Each event is a chat.completion.chunk: the first carries delta.role, the rest carry delta.content, then a chunk with finish_reason: stop, a final usage-only chunk (choices: []), and the data: [DONE] terminator. If the upstream fails mid-stream the connection ends with an in-band error event — there is no provider switch once the first byte is sent, and a stream that already emitted output is not refunded.

data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1716480000,"model":"deepseek-v4-flash","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}

data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1716480000,"model":"deepseek-v4-flash","choices":[{"index":0,"delta":{"content":"Hi"},"finish_reason":null}]}

data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1716480000,"model":"deepseek-v4-flash","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}

data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1716480000,"model":"deepseek-v4-flash","choices":[],"usage":{"prompt_tokens":12,"completion_tokens":38,"total_tokens":50,"prompt_tokens_details":{"cached_tokens":0},"completion_tokens_details":{"reasoning_tokens":0}}}

data: [DONE]
Errors
  • 400 invalid_request_errormessages missing, content too long, invalid temperature, or reasoning_effort on a non-reasoning model.
  • 400 upstream_invalid_requestThe upstream model rejected the request's parameters (e.g. tools with a non-"none" reasoning_effort on a GPT reasoning model). The message explains why; not retryable as-is. Refunded.
  • 401 unauthorizedMissing or invalid API key.
  • 404 MODEL_NOT_FOUNDRequested model id does not exist or is disabled.
  • 429 insufficient_quotaIDR balance cannot cover the request — top up to continue.
  • 502 provider_errorUpstream AI provider failed. Refunded unless a stream already emitted output. A stream that fails before its first chunk returns a normal JSON error with the matching HTTP status instead of an SSE event.
  • 503 provider_unavailableEvery upstream provider for the model is down or failing. Refunded; safe to retry later.
GET/v1/models

List the text/chat models available to your key, in the OpenAI models shape. OpenAI-compatible clients call this to verify the base URL and populate model pickers. GET /v1/models/{model} retrieves a single model.

Auth: Authorization: Bearer <api_key>
Request—
Example
curl https://aiport.id/v1/models \
  -H "Authorization: Bearer $AIPORT_API_KEY"
Response200
{
  "object": "list",
  "data": [
    {
      "id": "deepseek-v4-flash",
      "object": "model",
      "created": 1716480000,
      "owned_by": "aiport"
    }
  ]
}

Only enabled text/chat models are returned — the same ids you can pass as model to /v1/chat/completions. Image and video models are not listed here (those modalities are async and not OpenAI-compatible). created is a Unix timestamp in seconds. GET /v1/models/{model} returns a single model object, or 404 model_not_found if the id is unknown, disabled, or not a text model.

Errors
  • 401 unauthorizedMissing or invalid API key.
  • 404 model_not_foundGET /v1/models/{model}: id does not exist, is disabled, or is not a text model.

Tool calling

Let a model call your functions. Works on /v1/chat/completions with the standard OpenAI tools / tool_choice fields — pass tool definitions, run the call yourself, feed the result back. Supported across OpenAI-compatible, DeepSeek, Gemini, and Claude models.

Tool calling is a round-trip you drive. You describe the functions the model is allowed to call; the model replies with the function name and JSON arguments; you execute it and send the result back; the model uses that result to answer.

1. Send a request with tools.

curl -X POST https://aiport.id/v1/chat/completions \
  -H "Authorization: Bearer $AIPORT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "deepseek-v4-flash",
    "messages": [
      {"role": "user", "content": "What is the weather in Jakarta?"}
    ],
    "tools": [
      {
        "type": "function",
        "function": {
          "name": "get_weather",
          "description": "Get the current weather for a city",
          "parameters": {
            "type": "object",
            "properties": {
              "city": {"type": "string", "description": "City name"}
            },
            "required": ["city"]
          }
        }
      }
    ]
  }'

2. The model asks to call the tool. Instead of content, the assistant message carries tool_calls and finish_reason is tool_calls. arguments is a JSON string you must parse:

{
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": null,
        "tool_calls": [
          {
            "id": "call_abc123",
            "type": "function",
            "function": {
              "name": "get_weather",
              "arguments": "{\"city\":\"Jakarta\"}"
            }
          }
        ]
      },
      "finish_reason": "tool_calls"
    }
  ]
}

3. Run the function and reply with a tool message. Append the assistant's tool_calls message verbatim, then one role: "tool" message per call, echoing its tool_call_id. Keep sending tools on this follow-up request too — resubmit it on every turn of the conversation, exactly as the OpenAI SDK does, or some models reject the replayed tool history:

curl -X POST https://aiport.id/v1/chat/completions \
  -H "Authorization: Bearer $AIPORT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "deepseek-v4-flash",
    "messages": [
      {"role": "user", "content": "What is the weather in Jakarta?"},
      {"role": "assistant", "content": null, "tool_calls": [
        {"id": "call_abc123", "type": "function",
         "function": {"name": "get_weather", "arguments": "{\"city\":\"Jakarta\"}"}}
      ]},
      {"role": "tool", "tool_call_id": "call_abc123", "content": "{\"temp_c\":31,\"sky\":\"partly cloudy\"}"}
    ],
    "tools": [
      {"type": "function", "function": {"name": "get_weather",
        "description": "Get the current weather for a city",
        "parameters": {"type": "object",
          "properties": {"city": {"type": "string", "description": "City name"}},
          "required": ["city"]}}}
    ]
  }'

The model now replies with normal content (finish_reason: "stop"):

{ "choices": [ { "message": { "role": "assistant", "content": "It's 31°C and partly cloudy in Jakarta." }, "finish_reason": "stop" } ] }

Forcing or disabling calls. Use tool_choice: "auto" (default when tools is set) lets the model decide, "required" forces some tool call, "none" forces a plain-text reply, and { "type": "function", "function": { "name": "get_weather" } } forces one specific function. Set parallel_tool_calls: false to limit the model to one call per turn.

Claude models. Claude Sonnet 5.5, Opus 5.5, and Fable 5.1 don't support forced tool calls. On these models "required" and a named function are sent as "auto" plus a system instruction telling the model to call the tool. The model almost always follows it, but check finish_reason rather than assuming a tool_calls reply. Other Claude models (Haiku 4.5, Sonnet 5, Opus 5) honor forced calls normally.

Streaming. With stream: true, tool calls arrive incrementally on delta.tool_calls (each fragment carries an index; function.arguments is concatenated across chunks). Accumulate by index until the terminating chunk with finish_reason: "tool_calls".

Round-trip integrity. Send each assistant tool_calls message back exactly as you received it — including the id on every call — and pair it with a matching tool_call_id in your tool reply. Some models (e.g. Gemini reasoning models) attach opaque state to the call id that must survive the round-trip, so don't rewrite or drop it.

Image generation

Text-to-image. Async — returns a job_id; poll /v1/jobs/{job_id} for the result. Pricing varies by resolution tier on supporting models.

POST/v1/image/generate

Text-to-image. Returns an async job_id — poll GET /v1/jobs/{job_id} until status is done.

Auth: Authorization: Bearer <api_key>
Requestapplication/json
prompt
stringrequired
Description of the image to generate.
model
stringdefault gpt-image-2
Image model id. Capability differs per model — some support resolution tiers, some accept aspect_ratio, some accept neither.
aspect_ratio
string
Output aspect ratio. Ignored by models that don't support it. Note: this field is named `aspect_ratio` on image endpoints and `ratio` on video endpoints — same meaning, different name per endpoint. `4:5` (Instagram feed) is not supported by any model; generate `3:4` and crop instead.
Allowed: 1:1 3:2 2:3 4:3 3:4 16:9 9:16
resolution
string
Output resolution tier. Determines pricing for models that support it; ignored otherwise.
Allowed: 1k 2k 4k
Example
curl -X POST https://aiport.id/v1/image/generate \
  -H "Authorization: Bearer $AIPORT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-image-2",
    "prompt": "A traditional Balinese temple at golden hour, cinematic",
    "aspect_ratio": "16:9",
    "resolution": "2k"
  }'
Response202
{
  "job_id": "job_01HX...",
  "status": "pending"
}

Poll GET /v1/jobs/{job_id} every 2–5 seconds until status is 'done' (then read result_url) or 'failed'. Image generation typically takes 10–60 seconds at 1k, 30–120 seconds at 2k, and up to 180 seconds at 4k. Jobs that exceed 10 minutes are auto-failed and credits are refunded.

Errors
  • 400 invalid_requestMissing prompt, invalid aspect_ratio, or invalid resolution.
  • 400 invalid_requestUnrecognized field in the request body (e.g. a typo or deprecated field name).
  • 401 unauthorizedMissing or invalid API key.
  • 402 insufficient_balanceIDR balance cannot cover the request.
  • 404 MODEL_NOT_FOUNDRequested model id does not exist or is disabled.

Image-to-image

Edit or transform an existing image. Upload reference images first (or pass any HTTPS URL), then call /v1/image/img2img.

POST/v1/image/upload

Upload a reference image to be used by /v1/image/img2img. Returns a hosted URL — pass that URL (not an id) in the img2img call's images field.

Auth: Authorization: Bearer <api_key>
Requestmultipart/form-data
file
filerequired
Image file. Max 30 MB. JPEG, PNG, or WEBP.
Example
curl -X POST https://aiport.id/v1/image/upload \
  -H "Authorization: Bearer $AIPORT_API_KEY" \
  -F "file=@./reference.jpg;type=image/jpeg"
Response200
{
  "url": "https://cdn.aiport.id/uploads/...",
  "type": "image"
}

No upload_id — the returned url is what you pass to img2img or other endpoints that accept a reference image URL. The server also detects the file type from its content, so a plain -F "file=@./reference.jpg" (no ;type=) works too — the explicit ;type= above is only needed if your client sends application/octet-stream for this extension (some curl builds do this for .webp).

Errors
  • 400File content does not match a declared, specific Content-Type.
  • 401 unauthorizedMissing or invalid API key.
  • 413File exceeds the per-modality size limit.
  • 415Unsupported or undetectable file type (not JPEG, PNG, or WebP).
POST/v1/image/img2img

Image-to-image editing. Provide reference image URL(s) (from /v1/image/upload or any HTTPS URL) plus a prompt. Returns an async job_id.

Auth: Authorization: Bearer <api_key>
Requestapplication/json
prompt
stringrequired
Edit/transform instruction.
images
string[]required
One or more HTTPS image URLs. Use /v1/image/upload to get hosted URLs for local files.
model
string
Image-to-image model id.
aspect_ratio
string
Output aspect ratio. Ignored by models that don't support it. Note: this field is named `aspect_ratio` on image endpoints and `ratio` on video endpoints — same meaning, different name per endpoint. `4:5` (Instagram feed) is not supported by any model; generate `3:4` and crop instead.
Allowed: 1:1 3:2 2:3 4:3 3:4 16:9 9:16
resolution
string
Output resolution tier.
Allowed: 1k 2k 4k
Example
curl -X POST https://aiport.id/v1/image/img2img \
  -H "Authorization: Bearer $AIPORT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Make it look like a Studio Ghibli scene",
    "images": ["https://cdn.aiport.id/uploads/.../ref.jpg"],
    "resolution": "2k"
  }'
Response202
{ "job_id": "job_01HX...", "status": "pending" }

Poll GET /v1/jobs/{job_id} every 2–5 seconds. Image-to-image typically takes 10–60 seconds at 1k and 30–180 seconds at higher resolutions. Jobs that exceed 10 minutes are auto-failed and credits are refunded.

Errors
  • 400Missing prompt, invalid image URL, or unsupported model parameter.
  • 400 invalid_requestUnrecognized field in the request body (e.g. a typo or deprecated field name).
  • 401 unauthorizedMissing or invalid API key.
  • 402 insufficient_balanceIDR balance cannot cover the request.

Video generation

Cinematic video generation, billed per generated second by output resolution. Three flavors: text-to-video, image-to-video, and multimodal (multiple references).

POST/v1/video/generate

Text-to-video. Async — returns job_id, then poll /v1/jobs/{job_id} until done. Billing is per generated second by output resolution.

Auth: Authorization: Bearer <api_key>
Requestapplication/json
prompt
stringrequired
Description of the video to generate.
model
stringdefault seedance-2.0-fast
Video model id.
resolution
string
Output resolution. Determines per-second price.
Allowed: 480p 720p 1080p 2k 4k
duration
integer
Length in seconds (4–15).
Allowed: 4 5 6 7 8 9 10 11 12 13 14 15
ratio
string
Aspect ratio. 'adaptive' lets the model pick. Note: this field is named `aspect_ratio` on image endpoints and `ratio` on video endpoints — same meaning, different name per endpoint. `4:5` (Instagram feed) is not supported by any model; generate `3:4` and crop instead.
Allowed: adaptive 16:9 4:3 1:1 3:4 9:16 21:9
generate_audio
boolean
Synthesize audio alongside video. Model-dependent.
web_search
boolean
Allow the model to ground on web search results. Model-dependent.
return_last_frame
boolean
Also return the final frame as an image URL.
seed
integer
Deterministic seed. -1 means random.
Example
curl -X POST https://aiport.id/v1/video/generate \
  -H "Authorization: Bearer $AIPORT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "seedance-2.0-fast",
    "prompt": "A drone shot flying over rice terraces at sunrise",
    "resolution": "1080p",
    "duration": 6,
    "ratio": "16:9"
  }'
Response202
{ "job_id": "job_01HX...", "status": "pending" }

Video jobs typically take 30–120 seconds. Poll every 3–5 seconds.

Errors
  • 400Invalid resolution, duration, ratio, or missing prompt.
  • 400 invalid_requestUnrecognized field in the request body (e.g. a typo or deprecated field name).
  • 401 unauthorizedMissing or invalid API key.
  • 402 insufficient_balanceIDR balance cannot cover the request.
POST/v1/video/img2video

Image-to-video. Provide a first frame (and optional last frame) URL; the model interpolates motion. Async job. Two request shapes depending on model: standard models (default) use the fields below. Sora 2 (`img2video-sora-2`) uses a different shape instead — `image_url` (required, https) in place of `first_frame_url`/`last_frame_url`, no `resolution` or `conversion_slots`, and `duration` restricted to 4, 8, or 12 seconds at flat per-tier pricing (not per-second). See /pricing for which tier applies to each model.

Auth: Authorization: Bearer <api_key>
Requestapplication/json
prompt
stringrequired
Motion / scene description.
first_frame_url
stringrequired
HTTPS URL of the starting frame. img2video-sora-2 uses `image_url` instead of this field.
last_frame_url
string
Optional HTTPS URL of the ending frame. Not supported by img2video-sora-2.
model
stringdefault seedance-2.0-fast-img2video
Image-to-video model id.
resolution
string
Output resolution. Not used by img2video-sora-2 (no resolution parameter).
Allowed: 480p 720p 1080p 2k 4k
duration
integer
Length in seconds (4–15). img2video-sora-2 only accepts 4, 8, or 12.
Allowed: 4 5 6 7 8 9 10 11 12 13 14 15
real_person_mode
boolean
Hint to the model that the reference depicts a real person — improves identity preservation.
conversion_slots
string[]
Which slots may be transcoded/normalized by the platform. Default: ['all']. Not supported by img2video-sora-2.
Allowed: all firstFrameUrl lastFrameUrl
Example
curl -X POST https://aiport.id/v1/video/img2video \
  -H "Authorization: Bearer $AIPORT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "seedance-2.0-fast-img2video",
    "prompt": "Camera slowly pushes in, leaves drift across frame",
    "first_frame_url": "https://cdn.aiport.id/uploads/.../start.jpg",
    "resolution": "720p",
    "duration": 5
  }'
Response202
{ "job_id": "job_01HX...", "status": "pending" }
Errors
  • 400Missing first_frame_url, invalid duration/resolution.
  • 400 invalid_requestUnrecognized field in the request body (e.g. a typo or deprecated field name).
  • 401 unauthorizedMissing or invalid API key.
  • 402 insufficient_balanceIDR balance cannot cover the request.
POST/v1/video/upload

Upload a reference video to be used by /v1/video/multimodal. Returns a hosted URL with its duration already measured — pass that URL in video_urls and you won't need video_durations for it.

Auth: Authorization: Bearer <api_key>
Requestmultipart/form-data
file
filerequired
Video file. Max 50 MB. MP4 or MOV.
Example
curl -X POST https://aiport.id/v1/video/upload \
  -H "Authorization: Bearer $AIPORT_API_KEY" \
  -F "file=@./scene.mp4;type=video/mp4"
Response200
{
  "url": "https://cdn.aiport.id/uploads/...",
  "type": "video",
  "duration_seconds": 8
}

duration_seconds is measured server-side and stored against this url — pass the url in video_urls on /v1/video/multimodal without also needing video_durations for it. The server also detects the file type from its content, so a plain -F "file=@./scene.mp4" (no ;type=) works too — the explicit ;type= above is only needed if your client sends application/octet-stream for this extension (curl's own MIME table on most systems has no entry for .mp4/.mov, so it does this by default). If upload fails with "Only MP4 and MOV are supported" for a file you know is a real MP4, this is almost always the cause.

Errors
  • 400File content does not match a declared, specific Content-Type.
  • 401 unauthorizedMissing or invalid API key.
  • 413File exceeds the per-modality size limit.
  • 415Unsupported or undetectable file type (not MP4 or MOV).
  • 422 unparseable_videoCould not read the video's duration. Re-encode with faststart and try again.
  • 400 invalid_reference_videoVideo dimensions are outside the reference-video bounds: total pixels must be 409,600–2,086,876 (roughly 480p to 1080p), each side 300–6000px.
POST/v1/audio/upload

Upload a reference audio clip to be used by /v1/video/multimodal. Returns a hosted URL — pass that URL in audio_urls.

Auth: Authorization: Bearer <api_key>
Requestmultipart/form-data
file
filerequired
Audio file. Max 50 MB. MP3, WAV, or M4A.
Example
curl -X POST https://aiport.id/v1/audio/upload \
  -H "Authorization: Bearer $AIPORT_API_KEY" \
  -F "file=@./voiceover.mp3;type=audio/mpeg"
Response200
{
  "url": "https://cdn.aiport.id/uploads/...",
  "type": "audio"
}

No upload_id — the returned url is what you pass in audio_urls on /v1/video/multimodal. The server also detects the file type from its content, so a plain -F "file=@./voiceover.mp3" (no ;type=) works too — the explicit ;type= above is only needed if your client sends application/octet-stream for this extension (some curl builds do this for .mp3/.wav/.m4a).

Errors
  • 400File content does not match a declared, specific Content-Type.
  • 401 unauthorizedMissing or invalid API key.
  • 413File exceeds the per-modality size limit.
  • 415Unsupported or undetectable file type (not MP3, WAV, or M4A).
POST/v1/video/multimodal

Multimodal video generation — combine reference images, videos, and audio with a text prompt. Async job; per-resolution per-second billing with a min-billable-seconds floor (see /pricing). • seedance-2.0-fast-multimodal (default): image, video, and audio references — 480p/720p/1080p/2k/4k, 4–15s, at least one reference required. • minimax-h3-multimodal: image and audio references only (no video) — 480p/720p/1080p only, 1080p capped at 10s, references optional. • Deprecated: earlier docs used images/videos/audios for these fields. The server never reads those names — use image_urls/video_urls/audio_urls instead, or the request is now rejected with 400 invalid_request.

Auth: Authorization: Bearer <api_key>
Requestapplication/json
prompt
stringrequired
Scene / motion description.
model
stringdefault seedance-2.0-fast-multimodal
Multimodal video model id.
image_urls
string[]
Up to 9 reference image URLs (max 30 MB each).
video_urls
string[]
Up to 3 reference video URLs (max 50 MB each). Not accepted by minimax-h3-multimodal (image/audio references only). Each URL needs a known duration. Recommended: upload via POST /v1/video/upload first — it measures and stores the duration for you, so this URL won't need a video_durations entry. Otherwise pass the matching entry in video_durations yourself. Reference video constraints: MP4 or MOV, 2–15s, 24–60fps, total pixels 409,600–2,086,876 (roughly 480p to 1080p — a low-res video like 320x240 falls below this), each side 300–6000px. POST /v1/video/upload enforces these and returns 400 invalid_reference_video with the specific reason. A video_urls entry from an external HTTPS URL is not pre-checked — if it's out of range, the job fails at the provider with a generic "Provider failed" error and credits are auto-refunded.
video_durations
number[]
Duration in seconds for each URL in video_urls, same order/index (video_durations[0] is the duration of video_urls[0], etc). Only needed for URLs not uploaded via POST /v1/video/upload (that endpoint records the duration automatically). Omitting it for an unrecognized URL fails with 400 unknown_reference_video.
audio_urls
string[]
Up to 3 reference audio URLs (max 50 MB each). Use POST /v1/audio/upload to get a hosted URL for a local file.
resolution
string
Output resolution. minimax-h3-multimodal: only 480p/720p/1080p (no 2k/4k).
Allowed: 480p 720p 1080p 2k 4k
duration
integer
Requested length in seconds. minimax-h3-multimodal: 1080p output capped at 10s.
Allowed: 4 5 6 7 8 9 10 11 12 13 14 15
ratio
string
Aspect ratio. Allowed values depend on model: • seedance-2.0-fast-multimodal: adaptive, 16:9, 4:3, 1:1, 3:4, 9:16, 21:9 ('adaptive' lets the model pick). • minimax-h3-multimodal: 16:9, 9:16 only. Note: this field is named `aspect_ratio` on image endpoints and `ratio` on video endpoints — same meaning, different name per endpoint. `4:5` (Instagram feed) is not supported by any model; generate `3:4` and crop instead.
conversion_slots
string[]
Per-asset transcoding hints, e.g. ['image1', 'video2']. Default: ['all'].
Allowed: all image1 image2 image3 image4 image5 image6 image7 image8 image9 video1 video2 video3
Example
curl -X POST https://aiport.id/v1/video/multimodal \
  -H "Authorization: Bearer $AIPORT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "seedance-2.0-fast-multimodal",
    "prompt": "Subject from image1 walking through scene from video1",
    "image_urls": ["https://cdn.aiport.id/uploads/.../subject.jpg"],
    "video_urls": ["https://cdn.aiport.id/uploads/.../scene.mp4"],
    "resolution": "720p",
    "duration": 6,
    "ratio": "16:9"
  }'
Response202
{ "job_id": "job_01HX...", "status": "pending" }

Billing when a reference asset is supplied: • seedance-2.0-fast-multimodal: 'with reference' tier + per-duration min-billable floor — see /pricing. • minimax-h3-multimodal: flat per-resolution rate regardless of references — see /pricing.

Errors
  • 400Too many reference assets, invalid URL, or invalid resolution/duration/ratio.
  • 400 invalid_requestUnrecognized field in the request body — e.g. images/videos/audios instead of image_urls/video_urls/audio_urls.
  • 400 unknown_reference_videoA video_urls entry has no known duration — its matching video_durations entry is missing and the URL wasn't submitted with a duration before.
  • 401 unauthorizedMissing or invalid API key.
  • 402 insufficient_balanceIDR balance cannot cover the request.

Polling jobs

Async modalities (image, img2img, video) return a job_id. Poll for status here.

GET/v1/jobs/{job_id}

Poll an async job (image, img2img, or video). Returns the current status and — when done — the result URL.

Auth: Authorization: Bearer <api_key>
Request—
job_id
stringrequired
Path parameter. The job_id returned from a generate call.
Example
curl https://aiport.id/v1/jobs/job_01HX... \
  -H "Authorization: Bearer $AIPORT_API_KEY"
Response200
{
  "job_id": "job_01HX...",
  "status": "done",
  "result_url": "https://result.example/...",
  "error_message": null,
  "created_at": 1716480000000
}

status is one of: 'pending', 'processing', 'done', 'failed'. On 'failed', check error_message for details (e.g. 'Provider failed. Please try again.' or 'Job timed out after N minutes'). Credits are auto-refunded on failure. result_url's host can vary between jobs and is not guaranteed to be under the aiport.id domain — don't hardcode a domain allowlist against it, just fetch whatever URL is returned. created_at is a Unix timestamp in milliseconds.

Errors
  • 401 unauthorizedMissing or invalid API key.
  • 404Job does not exist or does not belong to this API key's owner.

Errors

All errors return an OpenAI-style JSON body of the form { "error": { "message": "...", "type": "...", "param": null, "code": "..." } } with a meaningful HTTP status.

StatusMeaning
400Invalid request: missing or malformed parameters.
401Missing or invalid Authorization header.
402Insufficient IDR balance on async modalities (image, img2img, video) — top up at /dashboard/topup.
404Resource not found (unknown model id, unknown job_id, or job belongs to another user).
413Upload exceeded the per-modality size limit.
429insufficient_quota: IDR balance cannot cover an OpenAI-compatible text request — top up. Also returned when rate-limited — back off and retry.
5xxUpstream provider failure. Credits for failed async jobs are auto-refunded.

Available models

Live catalog. See /pricing for IDR rates per tier.

Text · 24
Model idDisplay name
deepseek-v4-flashDeepSeek V4 Flash (Routing to v4.1)
gemini-3.1-flash-liteGemini 3.1 Flash Lite
claude-sonnet-5Claude Sonnet 5
claude-opus-5Claude Opus 5
kimi-k3Kimi K3
kimi-k3-fastKimi K3 Fast
gemini-3.7-flashGemini 3.7 Flash
gemini-3.5-flash-liteGemini 3.5 Flash Lite
qwen-3.8-27bQwen3.8 27B
glm-5.3GLM 5.3
claude-fable-5-1Claude Fable 5.1
gemini-3.8-flashGemini 3.8 flash
gpt-6-astraGPT 6 Astra
deepseek-flashDeepseek V4.1 Flash
claude-opus-5-5Claude opus 5.5
gpt-6-solGpt 6 sol
gpt-6-lunaGpt 6 luna
claude-sonnet-5-5Claude Sonnet 5.5
deepseek-v4-proDeepSeek V4 Pro
claude-haiku-4-5Claude Haiku 4.5
minimax-m3MiniMax M3
glm-5.2GLM 5.2
glm-5.2-fastGLM 5.2 Fast
kimi-k2.7-codeKimi K2.7 Code
Image · 6
Model idDisplay name
gpt-image-2GPT Image 2
gpt-image-2.5-flareGpt image 2.5 flare
gpt-image-2.5-sunburstGpt image 2.5 sunburst
nano-banana2-flashNano Banana 2 Flash
z-image-turboZ-Image Turbo
wan-2.7-text2imgWAN 2.7
Image-to-image · 6
Model idDisplay name
gpt-image-2-img2imgGPT Image 2
gpt-image-2.5-flare-img2imgGpt image 2.5 flare
gpt-image-2.5-sunburst-img2imgGpt image 2.5 sunburst
nano-banana2-img2imgNano Banana 2
z-image-turbo-img2imgZ-Image Turbo
wan-2.7-img2imgWAN 2.7
Video · 6
Model idDisplay name
seedance-2.0-fastSeedance 2.0 Fast
seedance-2.0-fast-img2videoSeedance 2.0 Fast (img2video)
seedance-2.0-fast-multimodalSeedance 2.0 Fast (multimodal)
txt2video-sora-2Sora 2
img2video-sora-2Sora 2 (Image to Video)
minimax-h3-multimodalMinimax H3 (Multimodal)
Want this as LLM context? Download aiport-docs.md and paste it into your coding assistant — every endpoint, parameter, and the live model list in a single markdown file.

Get an API key in two minutes.

Sign up, top up any amount in IDR, and call text · image · video from the same key.

Create free account →See pricing