API docs
Base URL https://api.keelrouter.com. One key for every model; each model page lists its prices and parameters.
Authentication
Create a key in the console and send it on every request as Authorization: Bearer YOUR_KEY. x-api-key works too. Keys start with sk_ and are shown once.
Language models
POST /v1/chat/completions takes the OpenAI chat format, including stream: true. Claude models also answer on POST /v1/messages in the Anthropic format, so the official SDKs work with only the base URL and key changed.
curl https://api.keelrouter.com/v1/chat/completions \
-H "Authorization: Bearer $KEELROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "gpt-6.1-sol", "messages": [{"role": "user", "content": "Hello"}]}'Every response has an x-request-id header; non-streaming responses also carry x-keelrouter-cost in USD. Before a call we hold an estimate from your balance (from the request size and max_tokens) and settle at the real token count when it ends.
Images and video
Image and video models are async. POST /v1/images/generations or POST /v1/video/generations with model and the model's parameters returns a task. The price is fixed when the task starts, held from your balance, and released if the task fails.
{ "code": 200, "msg": "success", "data": { "taskId": "…", "state": "pending" } }Poll the same path with ?task_id= every 5 to 10 seconds. state is pending, processing, completed or failed. Completed tasks carry resultUrls; files are deleted after 30 days.
{
"code": 200, "msg": "success",
"data": {
"taskId": "…", "model": "seedance-2.0", "state": "completed",
"resultUrls": ["https://…/results/…/0.mp4"],
"createTime": 1792800000000, "completeTime": 1792800090000, "costTime": 90000,
"credits": 0.51
}
}Bad parameters are rejected right away with HTTP 400 and nothing is held.
Callbacks
Pass callback_url (https) when you create a task. When it ends we POST the same body as a poll response, retrying up to 3 times and again for an hour if your endpoint is down.
Each callback is signed: X-KeelRouter-Signature: t=<seconds>,v1=<hex>, where v1 is HMAC-SHA256 of t + "." + body keyed with the SHA-256 hex of the API key that created the task. Checking it is optional.
const { createHash, createHmac, timingSafeEqual } = require('crypto');
function verify(body, header, apiKey) {
const { t, v1 } = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const key = createHash('sha256').update(apiKey).digest('hex');
const mac = createHmac('sha256', key).update(`${t}.${body}`).digest('hex');
return timingSafeEqual(Buffer.from(mac), Buffer.from(v1));
}Records and balance
GET /v1/records/{id} returns the exact amount charged for a request (the x-request-id). GET /v1/balance returns your available, held and total balance in USD. GET /v1/models lists every model and its endpoint, no key needed.
Errors
Errors use { "code": <status>, "msg": "…" }: 400 bad request, 401 missing or invalid key, 402 not enough balance, 404 unknown model or task, 429 rate limited, 502 or 503 upstream trouble. Failed async tasks report a failCode:
| failCode | Meaning | Retry |
|---|---|---|
| CONTENT_MODERATION | The model refused the content. | no |
| INVALID_INPUT | A parameter or input file was not accepted. | no |
| INSUFFICIENT_BALANCE | Not enough balance. | no |
| UPSTREAM_BUSY | Every route was busy or rate-limited. | yes |
| UPSTREAM_FAILED | The generation failed upstream. | yes |
| TIMEOUT | The task ran past its time limit. | yes |
| INTERNAL_ERROR | Something failed on our side. | yes |
Failed and refused calls are never charged.