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

FieldTypeRequiredDescription
modelstringyesA video model id, for example seedance-2.5. See List models.
promptstringyesWhat to generate. 1 to 4000 characters.
durationintegernoLength in seconds. Each model accepts its own range or set of values. Defaults to the model's default.
resolutionstringnoFor example 720p. Defaults to the model's default.
aspect_ratiostringnoFor example 16:9. Defaults to the model's default.
audiobooleannoGenerate sound. Only on models that support it (such as Veo 3.1); it changes the price.
negative_promptstringnoWhat to avoid. Up to 2000 characters.
image_urlsstring arraynoPublic 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

FieldTypeRequiredDescription
modelstringyesAn image model id, for example nano-banana-2.
promptstringyesWhat to generate. 1 to 4000 characters.
resolutionstringnoFor example 1K, 2K. Defaults to the model's default.
aspect_ratiostringnoFor example 1:1, 16:9. Defaults to the model's default.
image_urlsstring arraynoPublic 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)
FieldNotes
modelAn image model id from List models.
promptRequired.
sizeOptional. 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.
nOnly 1 is supported.
response_formatOnly 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).

FieldDescription
idTask id.
objectvideo.task or image.task.
modelThe model that was used.
statusprocessing, succeeded, or failed.
video_urlVideo tasks only. URL of the finished video, or null until succeeded.
image_urlImage tasks only. URL of the finished image, or null until succeeded.
errorA short reason when status is failed, otherwise null.
credits_chargedCredits reserved for this job. Refunded automatically if the job fails.
created_atWhen the task was created (ISO 8601).
completed_atWhen 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

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."
  }
}
HTTPCodeMeaning
400invalid_jsonThe body is not valid JSON.
400invalid_requestA field is missing, has the wrong type, or is not supported by the model.
400invalid_durationduration is outside what the model accepts.
400invalid_resolutionresolution is not supported by the model.
400invalid_aspect_ratioaspect_ratio is not supported by the model.
400content_policyThe prompt was refused by the content policy. You are not charged.
400too_many_imagesMore reference images than the model accepts.
401missing_api_keyNo Authorization header.
401invalid_api_keyThe key is unknown or revoked.
401api_key_expiredThe key has passed its expiry date.
402insufficient_creditsYour balance is too low for this job.
402key_limit_exceededThe key's own credit limit would be exceeded.
403account_suspendedThe account is suspended.
403ip_not_allowedThe key is restricted to other IP addresses.
403model_not_allowedThe key is not allowed to use this model.
404model_not_foundUnknown or retired model, or a model of the other media type.
404task_not_foundNo such task.
429rate_limitedToo many requests.
502upstream_errorThe generation service was unavailable. You were not charged.
500internal_errorSomething 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);