Developer API

Images and video

Synchronous image generation, async tasks, editing and video.

Images: get the picture back directly

POST /v1/images/generations matches OpenAI's endpoint:

resp = client.images.generate(
    model="gpt-image-2.5",
    prompt="an orange tabby cat in an astronaut helmet, oil painting",
)
image_base64 = resp.data[0].b64_json

Response:

{
  "created": 1789000000,
  "model": "gpt-image-2.5",
  "data": [
    {
      "b64_json": "iVBORw0KGgoAAAANS...",
      "url": "https://bothub-api.bookab.info/v1/tasks/<task>/artifacts/<artifact>",
      "revised_prompt": "An orange tabby cat wearing an astronaut helmet, oil painting"
    }
  ]
}

Both forms are returned by default: b64_json carries the bytes, url points at the same image (downloading it also requires your API key). Ask for just one with "response_format": "b64_json" or "url".

How long the request stays open

Generating an image takes time, and this HTTP request stays open until the image is ready — usually tens of seconds, up to 5 minutes by default.

So raise your client timeout. The OpenAI SDK default is often too short for image work:

client = OpenAI(
    base_url="https://bothub-api.bookab.info/v1",
    api_key="bhk_live_xxxxxxxx",
    timeout=600.0,   # seconds
)

If the wait exceeds the limit you get 504 with IMAGE_GENERATION_TIMEOUT — the task is not cancelled, it is still running. The detail carries the task id so you can collect the result:

{
  "error": {
    "message": "Image generation did not finish within 300s. The task is still running — poll GET /v1/tasks/<task> to collect the result.",
    "type": "invalid_request_error",
    "code": "image_generation_timeout",
    "detail": { "taskId": "...", "statusUrl": "/v1/tasks/..." }
  }
}
The opposite is also true: disconnecting mid-wait cancels the task, so the upstream provider stops burning your balance on a result nobody is waiting for. Don't "probe" with a short timeout — every attempt would be thrown away.

Several images at once

Pass n:

{ "model": "gpt-image-2.5", "prompt": "...", "n": 3 }

The model's own maximum still applies; requests above it are clamped.

Images: async mode

To avoid holding a request open, switch to async with "async": true or the header X-BotHub-Async: true:

curl https://bothub-api.bookab.info/v1/images/generations \
  -H "Authorization: Bearer $BOTHUB_API_KEY" \
  -H "Content-Type: application/json" \
  -H "X-BotHub-Async: true" \
  -d '{"model": "gpt-image-2.5", "prompt": "an orange cat"}'

You get 202 immediately:

{
  "id": "7f2c...",
  "kind": "image",
  "status": "pending",
  "model": "gpt-image-2.5",
  "status_url": "/v1/tasks/7f2c...",
  "result_urls": [],
  "thumbnail_urls": [],
  "error_code": "",
  "error_message": ""
}

Poll GET /v1/tasks/{id} until status is done (or failed), then download from result_urls with your API key.

This shape is BotHub's own, not OpenAI's — the SDK's images.generate() cannot parse it. Use plain HTTP requests for async mode.

status values: pending, running, done, failed.

Image editing

Editing goes through the native task API, POST /v1/tasks:

curl https://bothub-api.bookab.info/v1/tasks \
  -H "Authorization: Bearer $BOTHUB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "image",
    "operation": "edit",
    "model": "gpt-image-2.5",
    "input": {
      "prompt": "replace the background with a starry sky",
      "image_urls": ["https://example.com/cat.png"]
    }
  }'

The response is the same 202 task body; poll it the same way.

Every entry in image_urls must be something the upstream model can fetch: a public URL or a data: URI. A local path (/Users/me/cat.png) is rejected with an explanation — it is never silently dropped.

Video generation

POST /v1/videos. Video takes minutes, so it is async only:

curl https://bothub-api.bookab.info/v1/videos \
  -H "Authorization: Bearer $BOTHUB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "<video model id>",
    "prompt": "a cat running through snow, cinematic camera",
    "seconds": 5
  }'

Returns 202:

{
  "id": "9a1b...",
  "object": "video",
  "status": "queued",
  "status_url": "/v1/videos/9a1b...",
  "result_url": null,
  "error": null
}
  • GET /v1/videos/{id} — status goes queued → in_progress → completed / failed
  • GET /v1/videos/{id}/content — download the file once it completes

Retention

Tasks and their artifacts are kept for 24 hours and then cleaned up. Download anything you need to keep.

Idempotency

POST /v1/images/generations, /v1/videos and /v1/tasks accept an Idempotency-Key header. Resubmitting the same key returns the same task — nothing is generated or charged twice. Worth adding on flaky networks.

Note that reusing a key with a different request body returns 409 — change the key when you change the request.