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: Bearerheader. - Provider routing & auto-refund. When a provider errors mid-job, credits are refunded automatically.
- OpenAI-compatible text endpoint.
POST /v1/chat/completionsis a drop-in for the OpenAI SDK, includingstream: 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 intostream: 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 pollGET /v1/jobs/{job_id}every few seconds untilstatusbecomesdone(readresult_url) orfailed. 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:
{ "job_id": "job_01HX...", "status": "done", "result_url": "https://uploads.aiport.id/results/..." }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
doneIn 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.
/v1/chat/completionsChat-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.
low medium highauto none requiredcurl -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."}
]
}'{
"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.
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]400 invalid_request_errormessages missing, content too long, invalid temperature, or reasoning_effort on a non-reasoning model.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. Non-streamed calls are refunded; a stream that already emitted output is not.
/v1/modelsList 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.
curl https://aiport.id/v1/models \ -H "Authorization: Bearer $AIPORT_API_KEY"
{
"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.
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, and Gemini 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.
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.
/v1/image/generateText-to-image. Returns an async job_id — poll GET /v1/jobs/{job_id} until status is done.
3:2 1:1 2:3 5:4 4:5 16:9 9:16 21:9 3:4 4:3 9:211k 2k 4kcurl -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"
}'{
"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.
400 invalid_requestMissing prompt, invalid aspect_ratio, or invalid resolution.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.
/v1/image/uploadUpload a reference image to be used by /v1/image/img2img. Returns an upload_id you reference in the img2img call.
curl -X POST https://aiport.id/v1/image/upload \ -H "Authorization: Bearer $AIPORT_API_KEY" \ -F "file=@./reference.jpg"
{
"upload_id": "upl_01HX...",
"url": "https://uploads.aiport.id/uploads/..."
}400Missing file or unsupported MIME type.401 unauthorizedMissing or invalid API key.413File exceeds the per-modality size limit.
/v1/image/img2imgImage-to-image editing. Provide reference image URL(s) (from /v1/image/upload or any HTTPS URL) plus a prompt. Returns an async job_id.
3:2 1:1 2:3 5:4 4:5 16:9 9:16 21:9 3:4 4:3 9:211k 2k 4kcurl -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://uploads.aiport.id/uploads/.../ref.jpg"],
"resolution": "2k"
}'{ "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.
400Missing prompt, invalid image URL, or unsupported model parameter.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).
/v1/video/generateText-to-video. Async — returns job_id, then poll /v1/jobs/{job_id} until done. Billing is per generated second by output resolution.
480p 720p 1080p 2k 4k4 5 6 7 8 9 10 11 12 13 14 15adaptive 16:9 4:3 1:1 3:4 9:16 21:9curl -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"
}'{ "job_id": "job_01HX...", "status": "pending" }Video jobs typically take 30–120 seconds. Poll every 3–5 seconds.
400Invalid resolution, duration, ratio, or missing prompt.401 unauthorizedMissing or invalid API key.402 insufficient_balanceIDR balance cannot cover the request.
/v1/video/img2videoImage-to-video. Provide a first frame (and optional last frame) URL; the model interpolates motion. Async job.
480p 720p 1080p 2k 4k4 5 6 7 8 9 10 11 12 13 14 15all firstFrameUrl lastFrameUrlcurl -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://uploads.aiport.id/uploads/.../start.jpg",
"resolution": "720p",
"duration": 5
}'{ "job_id": "job_01HX...", "status": "pending" }400Missing first_frame_url, invalid duration/resolution.401 unauthorizedMissing or invalid API key.402 insufficient_balanceIDR balance cannot cover the request.
/v1/video/multimodalMultimodal video generation — combine up to 9 reference images, 3 reference videos, and 3 reference audio tracks with a text prompt. Per-resolution per-second billing with a min-billable-seconds floor (see /pricing).
480p 720p 1080p 2k 4k4 5 6 7 8 9 10 11 12 13 14 15all image1 image2 image3 image4 image5 image6 image7 image8 image9 video1 video2 video3curl -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",
"images": ["https://uploads.aiport.id/uploads/.../subject.jpg"],
"videos": ["https://uploads.aiport.id/uploads/.../scene.mp4"],
"resolution": "720p",
"duration": 6
}'{ "job_id": "job_01HX...", "status": "pending" }When any reference asset is supplied, billing uses the 'with reference' tier and a per-duration min-billable floor — see /pricing for the exact table.
400Too many reference assets, invalid URL, or invalid resolution/duration.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.
/v1/jobs/{job_id}Poll an async job (image, img2img, or video). Returns the current status and — when done — the result URL.
curl https://aiport.id/v1/jobs/job_01HX... \ -H "Authorization: Bearer $AIPORT_API_KEY"
{
"job_id": "job_01HX...",
"status": "done",
"result_url": "https://uploads.aiport.id/results/...",
"error_message": null,
"created_at": "2026-05-23T08:00:00Z"
}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.
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.
| Status | Meaning |
|---|---|
| 400 | Invalid request: missing or malformed parameters. |
| 401 | Missing or invalid Authorization header. |
| 402 | Insufficient IDR balance on async modalities (image, img2img, video) — top up at /dashboard/topup. |
| 404 | Resource not found (unknown model id, unknown job_id, or job belongs to another user). |
| 413 | Upload exceeded the per-modality size limit. |
| 429 | insufficient_quota: IDR balance cannot cover an OpenAI-compatible text request — top up. Also returned when rate-limited — back off and retry. |
| 5xx | Upstream provider failure. Credits for failed async jobs are auto-refunded. |
Available models
Live catalog. See /pricing for IDR rates per tier.
| Model id | Display name |
|---|---|
deepseek-v4-flash | DeepSeek V4 Flash |
gpt-5.4-mini | GPT 5.4 mini |
gpt-5.4 | GPT 5.4 |
gpt-5.5 | GPT 5.5 |
gemini-3.1-flash-lite | Gemini 3.1 Flash Lite |
claude-opus-4-8 | Claude Opus 4.8 |
deepseek-v4-pro | DeepSeek V4 Pro |
claude-sonnet-4-6 | Claude Sonnet 4.6 |
claude-haiku-4-5 | Claude Haiku 4.5 |
gemini-3.5-flash | Gemini 3.5 Flash |
minimax-m3 | MiniMax M3 |
glm-5.2 | GLM 5.2 |
glm-5.2-fast | GLM 5.2 Fast |
kimi-k2.6 | Kimi K2.6 |
kimi-k2.6-fast | Kimi K2.6 Fast |
kimi-k2.7-code | Kimi K2.7 Code |
| Model id | Display name |
|---|---|
gpt-image-2 | GPT Image 2 |
nano-banana2-flash | Nano Banana 2 Flash |
grok-image | Grok Image |
z-image-turbo | Z-Image Turbo |
wan-2.7-text2img | WAN 2.7 |
| Model id | Display name |
|---|---|
gpt-image-2-img2img | GPT Image 2 |
nano-banana2-img2img | Nano Banana 2 |
grok-image-img2img | Grok Image |
z-image-turbo-img2img | Z-Image Turbo |
wan-2.7-img2img | WAN 2.7 |
| Model id | Display name |
|---|---|
seedance-2.0-fast | Seedance 2.0 Fast |
seedance-2.0-fast-img2video | Seedance 2.0 Fast (img2video) |
seedance-2.0-fast-multimodal | Seedance 2.0 Fast (multimodal) |