Quick start
Make your first API request in minutes. You'll need an API key and can use any HTTP client or OpenAI-compatible SDK.
/v1/chat/completionsCreates a chat completion response from one of onomeo's models.
Runnable versions of everything on this page, one file each in Python, Node and curl, are on GitHub.
Authentication
Use your API key in the Authorization header. Keep your key secure and never share it in client-side code.
Authorization: Bearer YOUR_API_KEYYour balance lives on your account, so a lost key is recoverable — sign in again and create a new one on the My account page. Still, treat the key itself as a secret: do not commit it, do not share it. A key seen in use from several places at once gets disabled.
Leaked keys
When one of your keys appears in a public GitHub repository, GitHub notifies us within seconds and the key is disabled straight away. An email then goes to the address on your account, naming the key and where it was found.
A disabled key stops working immediately: any request still carrying it is answered with 401 unauthorized. Your account, your balance and your other keys are unaffected.
What to do next
- Create a replacement key in the console and put it into your application.
- Remove the old key from your code. Deleting the line is not enough: the key remains in the repository history, so rewrite the history, or treat that key as permanently public.
- Keep keys out of source code altogether. Read them from an environment variable or a secrets manager, and list the file holding them in .gitignore.
If you notice a leak yourself
Disable the key yourself in the console — it takes effect at once — then create a replacement. If you suspect someone else has used it, contact us and we will go through that key's request history with you.
Chat completions
Send a list of messages to a model and receive a generated response. This is the core endpoint for conversational AI. Request and response bodies follow the OpenAI shape.
Response
{
"model": "MODEL_ID",
"choices": [{
"message": { "role": "assistant",
"content": "..." }
}],
"usage": { "total_tokens": 150 }
}The number under usage is what this call used. On Claude, GPT and other big models the charge to your balance is worked out from it; on free models nothing is charged, and calls are counted by the length of what you send (see Billing and limits for how). It counts visible text only: what you sent plus what the model replied. Reasoning the model does before answering is not charged. On the big models, prompt_tokens and completion_tokens are the plain counts and total_tokens is the amount actually charged: returned text at the model's multiplier, sent text at a share of that, and the part of the sent text the provider billed as cached at a smaller share again (the shares are under Billing and limits).
Answers returned through the API to accounts without a balance end with the line "— via onomeo.com". Accounts with a balance do not get it. Tool-call answers, structured-output answers and answers that consist of a single JSON object or array are never changed. The line is not counted in usage, and calls made on your own provider key are returned as they are.
Parameters
The following parameters are commonly used with /v1/chat/completions.
| Parameter | Type | Description |
|---|---|---|
model | string | The exact model ID from the current Models page. |
messages | array | A list of message objects with role and content. |
temperature | number | Controls randomness. Use 0.0 for deterministic output. |
max_tokens | integer | Optional. Very small values are raised to a floor, so the answer does not come back empty. |
stream | boolean | Whether to stream the response (default: false). |
import requests response = requests.post( "/v1/chat/completions", headers={"Authorization": "Bearer YOUR_API_KEY"}, json={"model": "MODEL_ID", "messages": [ {"role": "user", "content": "Hello!"} ]} ) print(response.json())
Streaming
Set stream to true and you get server-sent events, same format as the OpenAI API. Every official client handles this out of the box.
from openai import OpenAI client = OpenAI(api_key="YOUR_API_KEY", base_url="/v1") stream = client.chat.completions.create( model="MODEL_ID", messages=[{"role": "user", "content": "Hello!"}], stream=True, ) for chunk in stream: print(chunk.choices[0].delta.content or "", end="")
Billing works the same way on a stream: the final chunk carries the usage count, computed from the text you sent and the text you received; on big models the charge is worked out from it, and free models are not charged.
Images
Models that read images take pictures inside a user message: put the text and each image as parts of content, each image as a base64 data URL.
{
"model": "MODEL_ID",
"messages": [{
"role": "user",
"content": [
{ "type": "text", "text": "What is in this picture?" },
{ "type": "image_url",
"image_url": { "url": "data:image/jpeg;base64,/9j/4AAQ..." } }
]
}]
}Models that read images right now: Loading the list…
On big models each image costs a fixed amount on every request that carries it, on top of the text; free models charge nothing for images. A request may carry at most 8 images; each must be PNG, JPEG, WebP or GIF and at most 3 MB. An image sent to a model that cannot read it is refused with model_no_vision, and nothing is charged. Images are passed to the model provider for the answer and are not stored on this site.
Other endpoints
The same key works on the endpoints below, so a tool built for another provider's request format can be pointed at this site as well. Each is called with POST, and the key goes in the Authorization header as Bearer unless a row says otherwise.
| Endpoint | What it does |
|---|---|
/ | Anthropic format, for Claude Code and other tools that use only that format. The key may also go in the x-api-key header. It takes this site's paid models (for accounts that have topped up) and models on your own keys, not the free models. |
/ | OpenAI Responses format, used by Codex. It takes the same models as chat completions. |
/ | Google Gemini format, for Gemini CLI and other tools that use only that format: generateContent, streamGenerateContent (add alt=sse for server-sent events) and countTokens, with the model name in the path; GET /v1beta/models lists the models. The key may also go in the x-goog-api-key header or the key query parameter. |
/ | The older text completion format. The prompt is passed to the model as one user message and the answer comes back as text. One prompt per request; echo and logprobs are not supported. |
/ | Turns text into vectors for search and knowledge bases. Models: loading. Up to 2,048 inputs per request; only gemini-embedding-001 accepts dimensions; encoding_format may be float or base64. |
/ | Orders documents by how well they match a query, with the request fields used by Jina and Cohere: model, query, documents, top_n and return_documents. Models: loading. |
/ | Turns speech into text. Send the audio as a multipart form field named file, at most 25 MB; response_format may be json, text or verbose_json. Models: loading. |
/ | Draws pictures. Free models: loading; their pictures count toward the daily picture limit. n is the number of pictures, size is one of the sizes the model draws (such as 1024x1024), and response_format may be url or b64_json. The pictures are saved in your library, under Mine on the Works page. |
/ | Makes a short video with sound from one sentence, charged to the balance by the second. See Videos below. |
Videos
POST /v1/videos makes a short video with sound from one sentence. The request body is JSON with these fields:
| Field | Description |
|---|---|
model | A video model from the table at the end of this section. |
prompt | One sentence describing the video. |
seconds | The length in seconds, one of those the model offers. |
size | Optional. Width by height, such as "1280x720" for landscape or "720x1280" for portrait. |
resolution | Optional, in place of size: one of the model's resolutions, such as "480p" or "720p". |
aspect_ratio | Optional, with resolution: "16:9" or "9:16". |
first_frame | Optional, on models that take it: the picture the video starts on, as a data:image/…;base64 string. |
last_frame | Optional, on models that take it, and only with first_frame: the picture the video ends on, in the same form. |
reference_images | Optional, on models that take them: an array of pictures whose people, objects and style the video follows, in the same form. Not together with first_frame or last_frame. |
curl "/v1/videos" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "MODEL_ID", "prompt": "A lighthouse on a cliff at dusk, waves breaking below", "seconds": 5, "size": "1280x720" }'
The reply comes at once. It carries the video's id, status in_progress and progress 0:
{
"id": "video_abc123",
"object": "video",
"model": "MODEL_ID",
"status": "in_progress",
"progress": 0,
"created_at": 1760500000,
"seconds": 5,
"size": "1280x720"
}Ask GET /v1/videos/{id} until status is completed, when the reply also carries url and completed_at, or failed, when it carries error with code and message. A video usually takes one to two minutes. GET /v1/videos/{id}/content downloads the MP4 file.
curl "/v1/videos/video_abc123" \ -H "Authorization: Bearer YOUR_API_KEY" curl -L "/v1/videos/video_abc123/content" \ -H "Authorization: Bearer YOUR_API_KEY" \ -o video.mp4
A video is charged to the balance by the second, at the price of its resolution, only once it has been made; a video that fails is not charged. The models and their prices right now:
| Model | Lengths | Price per second | Pictures it takes |
|---|---|---|---|
| Loading the list… | |||
Models
These are the models you can call right now. Free models are not charged and are limited by number of calls. For the others the cost column is measured, not promised: a 30-day average of what one answer cost, in dollars, so a long answer costs more and a short one less.
Models are served through independent third-party providers but use the same request shape, so switching models only changes one string in your code. The list can change without notice: read it from the models endpoint at runtime rather than hard-coding names.
| Model | Rough cost per reply |
|---|---|
| Loading the list… | |
The same list, with who made each model, is on the Models page. To list them from code, call the models endpoint with your key:
curl "/v1/models" \ -H "Authorization: Bearer YOUR_API_KEY"
Billing and limits
The balance is counted in US dollars and comes only from top-ups: the amount you pay is your balance, and it never expires. Claude, GPT and other big models are charged to it by usage, worked out from the visible text sent and received; the DeepSeek coding line is charged at what DeepSeek billed for the call. Free models are not charged; they are limited by number of calls, and a longer message counts as several calls. Length is counted over everything the request sends, the earlier conversation, the persona and file text included: each Chinese character counts 1 and every 4 other characters (letters, digits, spaces, punctuation) count 1, and each 50,000 counted, or part of it, is one call. The first time a single API request to a charged model would cost a large amount, the site does not run it: it answers with an ordinary reply stating the estimated cost, and nothing is charged. Sending the same request again runs it as usual. Requests of that size are not announced again; the notice comes once more only when a single request would cost ten times as much.
Each model's price is on the Pricing page, where you can also top up.
| Limit | Value |
|---|---|
| Loading the limits… | |
Check your balance any time from the My account page, or from code. The reply gives balance in the site's internal unit; divide it by money.unitsPerUsd from GET /api/info to get dollars:
curl "/api/me" \ -H "Authorization: Bearer YOUR_API_KEY"
Errors
Every failure comes back as JSON with a stable code field. Match on the code, not on the message text, which is written for humans and may change. Amounts in an error body are in the site's internal unit; where a body carries need or balance, it also carries needUsd or balanceUsd, the same amount in dollars. When sending the same request again would fail the same way (a bad key, an insufficient balance, a used-up quota), the response also carries the x-should-retry: false header; only 408, 409, 429 and 5xx are worth retrying. Tools that resend every refusal (Codex, omp, or any streaming request sent again unchanged within seconds of a refusal) receive the refusal as an ordinary reply that states the reason, so that they stop.
{
"error": {
"code": "not_enough_credit",
"balance": 6000,
"balanceUsd": 0.01,
"need": 15000,
"needUsd": 0.025,
"message": "Insufficient balance: this request needs about $0.025 and your balance is $0.01. Top up, or switch to a free model."
}
}| Code | HTTP status | What it means |
|---|---|---|
bad_key | 401 | The key is invalid, or no longer exists. |
not_enough_credit | 402 | Balance too low for this request; it only happens on paid models, such as Claude and GPT. The body carries balance and need (what the request needs), with balanceUsd and needUsd beside them in dollars, and, when a way out can be worked out, ways: that free models are not charged (freeOk; it describes the billing and does not mean free-model calls are left right now), and what a new conversation with only the last message would need (freshNeed). |
premium_daily_cap | 402 | This account has reached its daily limit on Claude, GPT and other big models; nothing is deducted. The body carries spent (used today), cap, need (this request's estimate) and buyer (whether the account has topped up). |
coding_buyers_only | 402 | The DeepSeek coding line is open to accounts that have topped up. Nothing is deducted. The body carries buyUrl, where to top up. |
premium_buyers_only | 402 | Claude, GPT and other big models are open to accounts that have topped up. Nothing is deducted. The body carries buyUrl, where to top up. The free trial in the web chat does not extend to API calls. |
key_expired | 401 | This key is past its expiry date. Extend it on the My account page or use another key. |
key_limit_reached | 402 | This key has reached its own spending cap; the account balance is untouched. The body carries the cap, the amount used and this request's estimate. |
model_not_allowed | 400 | That model is not on the list. The body carries the allowed names. |
model_paused | 400 | That model is switched off for now and returns to the list once it is back on. The body carries the names available right now. The big models the site pays for are switched off together when the balance the site keeps with their provider runs out; the body then also carries held: true. |
model_no_vision | 400 | The request carries images and the model cannot read them. Pick a model that reads images, or leave the images out. |
too_many_images | 400 | The request carries more images than allowed (8). |
bad_image | 400 | An image is not a PNG, JPEG, WebP or GIF data URL of at most 3 MB. |
request_too_large | 413 | The request body is larger than 8 MB. |
ip_limit | 429 | Too many new accounts created from this network today. Returned at sign-in. |
too_fast | 429 | Too many requests from this account within a minute. The body carries retryAfter in seconds; the Retry-After header carries the same value. |
account_quota | 403 | This account has used its free-model calls for the current window, or has fewer left than this message needs (a longer message counts as several calls; see Billing and limits for how length is counted). Every key on the account shares the limit; Claude, GPT and other big models do not count toward it. The body carries retryAfter (seconds to wait; the Retry-After header carries the same value) and max (the window's limit); when the message needs more than one call, it also carries need (the calls this message counts as) and left (the calls left in this window). If need is above max, waiting does not help: the error message says the message is too long, and the conversation should be shortened or a new one started. While top-ups are open, an account that has not topped up also gets codingModels (the model names of the DeepSeek coding line, which is charged to the balance and not counted toward this limit) and buyUrl (where to top up). |
network_quota | 403 | This network address has used its free-model calls for the current window, or has fewer left than this message needs; Claude, GPT and other big models do not count toward it. The body carries retryAfter (seconds to wait; the Retry-After header carries the same value) and max (the window's limit); when the message needs more than one call, it also carries need (the calls this message counts as) and left (the calls left in this window). If need is above max, waiting does not help: the error message says the message is too long, and the conversation should be shortened or a new one started. While top-ups are open, an account that has not topped up also gets codingModels (the model names of the DeepSeek coding line, which is charged to the balance and not counted toward this limit) and buyUrl (where to top up). |
site_busy | 429 / 403 | The free-model calls shared by the whole site are used up for the current window, or fewer are left than this message needs; Claude, GPT and other big models are not affected. The body carries retryAfter (seconds to wait; the Retry-After header carries the same value); the site-wide limit and the calls left are not disclosed. If the message is larger than the whole window allows, the status is 403 and waiting does not help: the error message says the message is too long, and the conversation should be shortened or a new one started. While top-ups are open, an account that has not topped up also gets codingModels (the model names of the DeepSeek coding line, which is charged to the balance and not counted toward this limit) and buyUrl (where to top up). |
premium_busy | 429 | Other answers on Claude, GPT and the other big models are still running and this request did not fit in, even after waiting about 10 seconds. The day's budget is not used up. Nothing is deducted. The body carries retryAfter in seconds; the Retry-After header carries the same value. |
upstream_failed | 503 | The model provider did not answer. Nothing is deducted when this happens. |
think_overrun | 503 | The model spent its whole output limit thinking and wrote no answer. For free models whose thinking can be turned off, the site first asks once more with thinking off; this error comes back only when that did not answer either. Nothing is deducted and the call is given back. Raise max_tokens, split the task into smaller parts, or pick another model. In a streamed request this error arrives before [DONE]. |
content_filtered | 400 | The model provider's content filter blocked this request, so there is no answer. Nothing is deducted and the call is given back. Rephrase the request, or pick another model. In a streamed request this error arrives before [DONE]. |
content_blocked | 422 | The last user message does not comply with the site's content policy (onomeo.com/content-policy), so it was not sent to any model. Nothing is deducted and no call is counted. |
upstream_unconfigured | 503 | The service is not wired to a provider right now. |
premium_budget_exhausted | 403 | The site-wide budget for Claude, GPT and other big models opens in four rounds a day (00:00, 06:00, 12:00 and 18:00 UTC), whatever a round leaves carrying over to the next. Returned while the current round is used up, until the next opens; once the whole day's budget is used up, until 00:00 UTC. The body carries resetAt (the reset time, ISO 8601) and retryAfter (seconds until then); the Retry-After header carries the same value. Nothing is deducted, and the free models still answer. The rounds do not apply to accounts that have topped up: they get this error only once the whole day's budget is used up. In the first three rounds, an account that has not topped up gets this error once 70% of the round is used up, and the body then also carries reserved: true and buyUrl, where to top up. |
coding_line_out | 403 | The DeepSeek coding line is paused: the site's balance with DeepSeek is running low, or the day's spending limit for the line is reached. Nothing is deducted. It comes back once the site has added to its balance with DeepSeek, or the next day (UTC); pick another model meanwhile. |
draw_daily_limit | 429 | Returned by the drawing page's own request (POST /api/draws), not by /v1. The account has used its free pictures for today, or fewer are left than this request asks for; nothing is drawn. The body carries today (used and max, in pictures) and resetsAt (the next 00:00 UTC, ISO 8601). Pictures drawn on your own provider key, and pictures on a model that is charged per picture, do not count. |
FAQ
Can I use this in production?
No. This is a free beta with no uptime promise. Use it to try things, prototype, or run something that can fail without consequences.
What happens if I lose the key?
Nothing is lost. Sign in again and create a new key on the My account page: it is on the same account, with the same balance. Delete the old key from the My account page if you think someone else has it.
Can I create several keys?
Yes. Each account can hold up to 10 keys. They draw on the same balance and share the same call limits, so more keys do not mean more calls.
Do you keep my prompts?
For requests to the API, onomeo keeps an excerpt of each request and answer (the first 1,000 characters of the system prompt and the first 4,000 of the last user message and of the answer; the latest 100 per account, for at most 30 days), which the operator can read in the administration area; the amount used is also recorded, for billing and for counting calls. Conversations in the web chat are saved with your account (up to 50, and you can delete them). Either way, messages are forwarded to third-party model providers, and some of them, particularly on free tiers, may retain submitted content or use it to improve their services. onomeo does not use your content for training.
Is there image generation, video, audio or embeddings?
Yes. Pictures come through POST /v1/images/generations, and embeddings, rerank and speech-to-text through endpoints of their own (see Other endpoints); videos come through POST /v1/videos (see Videos). Text-to-speech is not offered, and function calling is not guaranteed on every model.
Why is my answer empty?
Almost always a max_tokens set too low. These models think before they answer, and the thinking counts against the same budget. Leaving the field out entirely is safest.
Roo Code
OpenCode
Aider
Zed
VS Code
Pi
Cherry Studio
Qwen Code
Jan