Kie.ai sells one key across roughly a hundred image, video, music and chat models. The API is straightforward once you know three things that the getting-started page does not make obvious: where the OpenAI-compatible base URL actually lives, that media generation is task-based rather than synchronous, and that the HTTP status code is not where the errors are.
That last one will cost you an afternoon if nobody tells you. So here it is first.
The status code lies
Send an unauthenticated request and you get HTTP 200, with the real result inside the JSON body:
POST https://api.kie.ai/api/v1/jobs/createTask
→ HTTP 200
{"code":401,"msg":"Unauthorized – Authentication failed..."}
The same holds on the credit endpoint and on the Anthropic-compatible path. Kie’s own sample code branches on response.status === 401 and 402, which cannot fire, because the transport status is 200 in every one of those cases.
Branch on body.code, not on the HTTP status. Unmatched routes are the exception and do return a genuine 404.
The documented code enum:
| Code | Meaning |
|---|---|
| 200 | Success |
| 401 | Unauthorized |
| 402 | Insufficient quota |
| 422 | Validation error |
| 429 | Rate limited |
| 433 | Subkey usage exceeded |
| 455 | Service unavailable, maintenance |
| 501 | Generation failed |
| 505 | Feature disabled |
Note that 501 means your generation failed, not that the server is broken. Handle it separately from 500.
Base URL and auth
The base is https://api.kie.ai, and authentication is an ordinary bearer token. Keys look like sk-kie-... and come from the account settings page.
Authorization: Bearer YOUR_API_KEY
File uploads live on a different host, https://kieai.redpandaai.co, with URL, stream and base64 endpoints. Uploads are free, and the files auto-delete after 24 hours, so treat that host as a staging area rather than storage.
Where the OpenAI-compatible endpoint actually is
This is the thing people search for and rarely find, because the answer is not what the question assumes. There is no single global OpenAI-compatible base URL. Compatibility is exposed per model, as a path prefix on api.kie.ai.
| Wire format | Path |
|---|---|
| OpenAI chat completions | /gpt-5-2/v1/chat/completions |
| OpenAI chat completions | /gemini-3-8-flash-openai/v1/chat/completions |
| OpenAI responses API | /codex/v1/responses |
| OpenAI responses API | /grok/v1/responses |
| Anthropic messages | /claude/v1/messages |
| Native Google | /gemini/v1/models/gemini-3-8-flash |
So an OpenAI SDK points at https://api.kie.ai/<model-slug>/v1 rather than at the bare host. Note that one documented slug contains a literal dot, gemini-3.1-pro, so do not normalise your slugs.
For Claude Code the setup is documented explicitly, and the docs specifically tell you not to append the path:
ANTHROPIC_BASE_URL=https://api.kie.ai/claude
ANTHROPIC_API_KEY="Bearer <your-key>"
ANTHROPIC_AUTH_TOKEN also works and takes the key without the Bearer prefix.
There is no /v1/models endpoint. A request to it returns a hard 404. Any OpenAI-compatible client that lists models on startup will break, and that failure looks like a broken key rather than a missing route, which is a miserable hour of debugging. Hardcode your model list.
Media generation is task-based
Chat is request and response. Everything that makes an image, video or track is a job you submit and poll.
POST https://api.kie.ai/api/v1/jobs/createTask
{ "model": "google/nano-banana-2", "input": { ... } }
→ {"code":200,"msg":"success","data":{"taskId":"dc1928bfcbc77..."}}
Then poll:
GET https://api.kie.ai/api/v1/jobs/recordInfo?taskId=<taskId>
The response carries state, which moves through waiting, queuing, generating, and then either success or fail, alongside failCode, failMsg, costTime and completeTime.
One detail worth knowing before you write the parser: resultJson is a JSON-encoded string, not an object. You parse it a second time to reach resultUrls. The original param field is the same. It is a small thing that produces a confusing type error if you assume otherwise.
Kie states the point plainly in its own docs: a 200 response to createTask means the task was created, not that it finished.
If you would rather not poll, createTask accepts a callBackUrl. Webhooks are signed with HMAC-SHA256, delivered with X-Webhook-Timestamp and X-Webhook-Signature headers, where the signature is computed over the task ID and timestamp joined by a dot.
The order I would build it in
Getting a first request working takes about ten minutes if you go in the right order, and considerably longer if you do not.
Start with the credit endpoint rather than a generation, because it is cheap, fast and tells you immediately whether your key is live. Remember to read code from the body rather than trusting the 200. If you see 401 there, the key is wrong or the Bearer prefix is missing.
Then submit one cheap image job and poll it by hand before you write any polling loop. You want to see the state transitions with your own eyes, and you want to hit the double-encoded resultJson once interactively rather than inside a parser you are also debugging.
Only after both of those work should you wire in webhooks. Signature verification is easy to get subtly wrong, and debugging it against a generation pipeline you have not yet proven is two problems at once.
The failure mode worth rehearsing deliberately is a fail state. Submit something your model will reject, watch failCode and failMsg populate, and make sure your code treats that as a normal outcome. Generation failures are routine at volume, and a pipeline that only handles success will stall on its first refusal.
Rate limits, and what is missing from them
The documented limit is 20 new generation requests per 10 seconds, applied per account, which Kie also describes as 120 per minute. Rejected requests do not enter the queue. Roughly 100 concurrent running tasks is described as typical, with no hard concurrency cap. Individual keys can additionally carry hourly, daily and total usage caps plus IP allowlisting, which is genuinely useful if you are handing a key to something automated.
What is absent is the part that makes a rate limit workable. I found no X-RateLimit-Limit, no X-RateLimit-Remaining, no X-RateLimit-Reset and no Retry-After documented anywhere, and none in observed responses. Combined with a 429 that arrives inside a 200, backing off intelligently means tracking your own request count against their published window rather than reading anything off the response.
Build that counter before you need it.
No official SDK
There is a Kie-AI organisation on GitHub with zero public repositories, nothing on PyPI, and no SDK referenced anywhere in the documentation. The packages you will find on npm are third-party wrappers maintained by individuals.
That is not disqualifying, because the REST surface is small enough to wrap in an afternoon. It does mean the integration is yours to maintain, and that a client library you adopt is one person’s side project.
Media is also not stored indefinitely. Generated files are retained for 14 days, logs and metadata for two months, and the signed download URLs expire after 20 minutes. Whatever you generate, move it to your own storage in the same job that created it.
Running it from a coding agent
One thing that makes this API more interesting than most media APIs is that Kie documents the Claude Code path directly. Point ANTHROPIC_BASE_URL at the Claude-compatible prefix and your agent’s model calls route through Kie instead of Anthropic, on the same key that reaches the image and video models.
That is convenient and it changes what you are depending on. Your agent’s reasoning and its media generation now share one vendor, one balance and one rate limit, and a problem at that vendor stops both at once. Whether that trade is worth the discount is a judgement about your own tolerance, not something an article can settle.
There is a second route worth knowing about. A community MCP server wraps the media models as agent tools, so your agent can generate an image mid-task rather than you calling the API yourself. Kie ships no official MCP server, and the community one carries the usual single-maintainer caveat.
If you take that path, set its enabled-tools list before you ship. An MCP server injects every tool’s schema into context on every turn, not just the turn you call it, and a catalogue this size is expensive to keep loaded when you are refactoring a router rather than making a video.
If you do run agents this way, the operational questions arrive quickly. Which agent is holding which key, what happened on the last run, and which outputs came from which task. Sharkly works on that layer, around the tools doing the execution: model usage keeps running on whatever subscriptions and API keys you configure in those tools, and the tasks, results and review stay in one place rather than spread across terminals.



