API Documentation
Base URL: https://apicafe.ai/api/v1
Generating media takes time, so the API is asynchronous: you submit a job, receive a task id right away, and poll for the result. Video and image endpoints work the same way.
Quickstart
1. Create an API key on the API Keys page of your dashboard. The key is shown once, so store it safely.
2. Submit a job:
curl -X POST https://apicafe.ai/api/v1/videos \
-H "Authorization: Bearer sk-..." \
-H "Content-Type: application/json" \
-d '{
"model": "seedance-2.5",
"prompt": "A red paper boat drifts across a calm pond at sunrise",
"duration": 5,
"resolution": "720p"
}'
The response is 202 Accepted with a task:
{
"id": "0b1f9c0e-6c7a-4d52-9d0e-2f6a1d8b7c11",
"object": "video.task",
"model": "seedance-2.5",
"status": "processing",
"video_url": null,
"error": null,
"credits_charged": 263,
"created_at": "2026-11-09T08:30:00.000Z",
"completed_at": null
}
3. Poll until it finishes:
curl https://apicafe.ai/api/v1/videos/0b1f9c0e-6c7a-4d52-9d0e-2f6a1d8b7c11 \
-H "Authorization: Bearer sk-..."
When status is succeeded, video_url holds the finished video.
Authentication
Send your key in the Authorization header on every request:
Authorization: Bearer sk-...
Keep keys out of client-side code. A revoked key stops working immediately.
When you create a key you can add guard rails: a credit limit (lifetime spend cap), an expiry date, a model allow-list and an IP allow-list (single addresses or IPv4 ranges such as 203.0.113.0/24). Use them to hand a key to an app or a teammate with a bounded blast radius.
Create a video
POST /videos
| Field | Type | Required | Description |
|---|---|---|---|
| model | string | yes | A video model id, for example seedance-2.5. See List models. |
| prompt | string | yes | What to generate. 1 to 4000 characters. |
| duration | integer | no | Length in seconds. Each model accepts its own range or set of values. Defaults to the model's default. |
| resolution | string | no | For example 720p. Defaults to the model's default. |
| aspect_ratio | string | no | For example 16:9. Defaults to the model's default. |
| audio | boolean | no | Generate sound. Only on models that support it (such as Veo 3.1); it changes the price. |
| negative_prompt | string | no | What to avoid. Up to 2000 characters. |
| image_urls | string array | no | Public URLs of reference images, up to the model's limit. Models with one slot use the first image as the start frame. |
Credits for the job are reserved when you submit it. The exact amount is returned as credits_charged.
Create an image
POST /images
| Field | Type | Required | Description |
|---|---|---|---|
| model | string | yes | An image model id, for example nano-banana-2. |
| prompt | string | yes | What to generate. 1 to 4000 characters. |
| resolution | string | no | For example 1K, 2K. Defaults to the model's default. |
| aspect_ratio | string | no | For example 1:1, 16:9. Defaults to the model's default. |
| image_urls | string array | no | Public URLs of images to edit or use as references, up to the model's limit. |
curl -X POST https://apicafe.ai/api/v1/images \
-H "Authorization: Bearer sk-..." \
-H "Content-Type: application/json" \
-d '{"model": "nano-banana-2", "prompt": "A red apple on a white table, photorealistic"}'
Some image models answer instantly: the task you get back may already be succeeded. Always read status instead of assuming.
OpenAI-compatible image endpoint
Already using the OpenAI images API? Point your client at this base URL and use your API key; nothing else changes.
POST /images/generations
from openai import OpenAI
client = OpenAI(base_url="https://apicafe.ai/api/v1", api_key="sk-...")
result = client.images.generate(
model="nano-banana-2",
prompt="A red apple on a white table, photorealistic",
size="1024x1024",
)
print(result.data[0].url)
| Field | Notes |
|---|---|
| model | An image model id from List models. |
| prompt | Required. |
| size | Optional. Mapped to the nearest aspect ratio and resolution the model supports (1024x1024 is square at the lowest tier, 1792x1024 landscape at 2K, and so on). Omit it or use auto for the model default. |
| n | Only 1 is supported. |
| response_format | Only url is supported. |
The call waits for the image (up to about 50 seconds) and answers with {"created": ..., "data": [{"url": "..."}], "id": "...", "credits_charged": ...}. If a generation outlasts the wait you get 504 generation_timeout with the task id in the message: fetch it with GET /images/{id}. The credits stay reserved and are refunded automatically if it fails.
Retrieve a task
GET /videos/{id} and GET /images/{id}
Return the same task object. You can only retrieve your own tasks, and a video id will not resolve on the image endpoint (or the other way round).
| Field | Description |
|---|---|
| id | Task id. |
| object | video.task or image.task. |
| model | The model that was used. |
| status | processing, succeeded, or failed. |
| video_url | Video tasks only. URL of the finished video, or null until succeeded. |
| image_url | Image tasks only. URL of the finished image, or null until succeeded. |
| error | A short reason when status is failed, otherwise null. |
| credits_charged | Credits reserved for this job. Refunded automatically if the job fails. |
| created_at | When the task was created (ISO 8601). |
| completed_at | When the task finished, or null. |
Finished files are copied to our storage. Download and keep your own copy of anything you need long-term.
List models
GET /models (no authentication required). Add ?type=video or ?type=image to filter.
Returns the available models with their endpoint, limits and a pricing list (credits_per_unit for each resolution, and for each audio setting where it matters). unit says what one price covers: a generated second (video) or an image. This list is always current.
Pricing and credits
- 1 credit = $0.01.
- Video is priced per generated second, images per image. The price depends on the model, the resolution and, for some models, whether sound is on.
- Credits are reserved when you submit a job and refunded automatically if it fails, is interrupted, or times out.
- A video's credit cost never exceeds credits_per_unit multiplied by duration. It can be slightly lower because of rounding.
Rate limits
Each API key can make 20 requests per 10 seconds. Beyond that you receive 429 rate_limited with a Retry-After header. Polling counts toward the limit, so poll every few seconds rather than in a tight loop.
Errors
Errors use one envelope:
{
"error": {
"code": "insufficient_credits",
"message": "This request costs 263 credits. Top up your balance and retry."
}
}
| HTTP | Code | Meaning |
|---|---|---|
| 400 | invalid_json | The body is not valid JSON. |
| 400 | invalid_request | A field is missing, has the wrong type, or is not supported by the model. |
| 400 | invalid_duration | duration is outside what the model accepts. |
| 400 | invalid_resolution | resolution is not supported by the model. |
| 400 | invalid_aspect_ratio | aspect_ratio is not supported by the model. |
| 400 | content_policy | The prompt was refused by the content policy. You are not charged. |
| 400 | too_many_images | More reference images than the model accepts. |
| 401 | missing_api_key | No Authorization header. |
| 401 | invalid_api_key | The key is unknown or revoked. |
| 401 | api_key_expired | The key has passed its expiry date. |
| 402 | insufficient_credits | Your balance is too low for this job. |
| 402 | key_limit_exceeded | The key's own credit limit would be exceeded. |
| 403 | account_suspended | The account is suspended. |
| 403 | ip_not_allowed | The key is restricted to other IP addresses. |
| 403 | model_not_allowed | The key is not allowed to use this model. |
| 404 | model_not_found | Unknown or retired model, or a model of the other media type. |
| 404 | task_not_found | No such task. |
| 429 | rate_limited | Too many requests. |
| 502 | upstream_error | The generation service was unavailable. You were not charged. |
| 500 | internal_error | Something went wrong on our side. Retry. |
Examples
Python
import time
import requests
API = "https://apicafe.ai/api/v1"
HEADERS = {"Authorization": "Bearer sk-..."}
task = requests.post(
f"{API}/videos",
headers=HEADERS,
json={"model": "seedance-2.5", "prompt": "A red paper boat on a pond"},
).json()
while task["status"] == "processing":
time.sleep(5)
task = requests.get(f"{API}/videos/{task['id']}", headers=HEADERS).json()
print(task["status"], task.get("video_url") or task["error"])
JavaScript
const API = "https://apicafe.ai/api/v1";
const headers = {
Authorization: "Bearer sk-...",
"Content-Type": "application/json",
};
let task = await (
await fetch(`${API}/images`, {
method: "POST",
headers,
body: JSON.stringify({
model: "nano-banana-2",
prompt: "A red apple on a white table",
}),
})
).json();
while (task.status === "processing") {
await new Promise((resolve) => setTimeout(resolve, 3000));
task = await (await fetch(`${API}/images/${task.id}`, { headers })).json();
}
console.log(task.status, task.image_url ?? task.error);