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
curl https://bothub-api.bookab.info/v1/images/generations \
-H "Authorization: Bearer $BOTHUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-image-2.5",
"prompt": "an orange tabby cat in an astronaut helmet, oil painting"
}'
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".
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/..." }
}
}
Pass n:
{ "model": "gpt-image-2.5", "prompt": "...", "n": 3 }
The model's own maximum still applies; requests above it are clamped.
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.
images.generate() cannot parse it. Use plain
HTTP requests for async mode.status values: pending, running, done, failed.
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.
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.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 / failedGET /v1/videos/{id}/content — download the file once it completesTasks and their artifacts are kept for 24 hours and then cleaned up. Download anything you need to keep.
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.