Skip to content
Try chatGet API key

How Bayle works

This page explains what happens between your request reaching https://api.bayle.ai and the response coming back. You don’t need it to make your first call (that’s the Quickstart). It’s for when you’re building something that has to behave well under timeouts, failures and large payloads, or when you want to know where your data goes.

Your code / SDK / tool chat.bayle.ai (web chat)
│ browser ──► chat server
│ │
│ HTTPS, Authorization: Bearer <key> │ key from an HttpOnly cookie
▼ ▼
┌──────────────────────────────────────────────────────────────┐
│ Bayle API (api.bayle.ai) │
│ CORS ─► request ID ─► request log ─► panic recovery │
│ ─► language ─► auth ─► body limit ─► handler │
└──────────────────────────────────────────────────────────────┘
│
▼
Model infrastructure
(runs the model you named in `model`)

The API is stateless. It has no database, cache or session store of its own, so nothing about one request carries over to the next.

Every request goes through the same stages, in this order.

Stage What it does
CORS Every response allows any origin. A browser preflight (OPTIONS) is answered with 204 straight away and goes no further.
Request ID Each request gets an ID used to correlate its log line.
Request log One line per request, written when the response finishes: method, path, status, duration, request ID. Nothing else.
Panic recovery An unexpected internal failure becomes a normal error envelope rather than a dropped connection.
Language Reads Accept-Language, picks English or Brazilian Portuguese, and sets Content-Language on the response before anything else is written.
Authentication Checks the key, unless the endpoint is public.
Body limit Caps the request body at 10 MiB.
Handler Validates the shape, calls the model, and writes the response.

Authentication runs before the body limit, deliberately. A request without a valid key is turned away after its headers are read, before any of its body is.

The API serves exactly eight operations, at the root of https://api.bayle.ai.

Endpoint Key needed
GET /models No
POST /video/quote No
POST /chat/completions Yes
POST /embeddings Yes
POST /images/generations Yes
POST /video/queue Yes
POST /video/retrieve Yes
POST /video/complete Yes

Authentication is on by default. Only the two endpoints marked “No” are exempt, so anything added later is protected unless it is deliberately opened.

Send your key as a bearer token:

Authorization: Bearer <your Bayle key>

Every way this can go wrong returns the same answer: a missing header, a scheme other than Bearer, a malformed key, a key that is not one of Bayle’s, an empty value, or the header sent twice. All of them get 401 with code invalid_api_key and the same message. The response never tells you which check failed, so it gives nothing away to someone guessing keys.

A key that is valid but out of balance is a different case. It authenticates, and the request then fails with 402 and code usd_spend_limit_exceeded. Adding balance keeps the same key. See Authentication.

Bayle validates the parts of the body it needs and passes the rest through. A field it doesn’t name is sent to the model as you sent it, so a parameter your model supports reaches it even if these docs don’t mention it yet.

  • On /chat/completions, the only field Bayle acts on is stream, which decides between a JSON response and an event stream.
  • Bayle adds no system prompt of its own. The model sees the messages you send.
  • A body that isn’t valid JSON, or doesn’t match the operation’s shape, is rejected with 400 before the model is called, so it costs nothing.

Set "stream": true on a chat completion to get Server-Sent Events.

HTTP/1.1 200 OK
Content-Type: text/event-stream; charset=utf-8
Cache-Control: no-cache
X-Accel-Buffering: no
Content-Language: en
data: {"id":"...","choices":[{"delta":{"content":"Hel"}}]}
data: {"id":"...","choices":[{"delta":{"content":"lo"}}]}
data: [DONE]

Events are sent to you line by line and flushed as they arrive, so you see tokens as the model produces them. A healthy stream always ends with data: [DONE].

If a stream breaks after it has started, the status code can’t change any more: the 200 was already sent. So Bayle reports the failure inside the stream instead. You get one final event carrying the usual error envelope, then [DONE]:

data: {"error":{"message":"...","type":"api_error","param":null,"code":null}}
data: [DONE]

This happens when the model’s stream ends without its closing marker, when the connection to the model breaks, or when a single event exceeds 4 MiB.

A stream has no deadline of Bayle’s own. It can run for up to an hour, after which the hosting platform closes the connection. That cut arrives without an error event, so treat a connection that closes without [DONE] as a failure too.

Bayle stops waiting for the model after a fixed time per call, and answers 504 with code request_timeout.

Call Deadline
GET /models 30 s
POST /chat/completions (not streaming) 30 s
POST /chat/completions (streaming) None of Bayle’s; the connection is closed after one hour
POST /embeddings 30 s
POST /images/generations 4 min
POST /video/quote, /queue, /retrieve, /complete 30 s each

A long answer that won’t fit in 30 seconds should be streamed. Video gets around the limit by design: generation runs in the background, and each call is a short one.

Some calls are retried for you, out of sight, and some never are. The dividing line is whether a second attempt could charge you twice.

Retried by Bayle Never retried
GET /models POST /chat/completions
POST /embeddings POST /images/generations
POST /video/quote POST /video/queue
POST /video/retrieve
POST /video/complete

The left column is retried up to three times, with a short backoff, on a dropped connection, a 429 or a 5xx. They are reads, quotes or cleanup, so repeating one is harmless.

The right column generates billed output. If a connection drops after the model has started, the work may already be done and paid for, and a silent retry could pay for it again. So Bayle hands the failure to you and lets you decide. When you retry one yourself, the Errors page says which codes are worth it.

Every error uses OpenAI’s envelope:

{
"error": {
"message": "Your Bayle API key is missing or could not be verified.",
"type": "authentication_error",
"param": null,
"code": "invalid_api_key"
}
}
  • Branch on error.code. message is for people and changes with the language.
  • Every message is Bayle’s own text, from one catalogue, in English or Brazilian Portuguese. A failure Bayle doesn’t recognise becomes a generic api_error with a null code.
  • The status code says what kind of failure it was: 400 for a bad request, 401 for the key, 402 for balance, 429 for rate limits, 503 when a model is offline or at capacity, 504 for a timeout.
  • A failure to reach the model at all is 502 with type api_error and a null code.

The full list is on Errors.

Send Accept-Language. A tag starting with pt, in any case, selects Brazilian Portuguese; anything else, or no header, selects English. Every response, success or error, carries Content-Language saying which one was used. Only error messages are translated; what the model writes is up to your prompt. See Response language.

Limit Value What happens past it
Request body 10 MiB 400, before the model is called
Response from /models, /chat/completions, /embeddings, /video/quote, /video/queue, /video/complete 8 MiB The body is cut off at the cap
Response from /images/generations 32 MiB 502
Response from /video/retrieve 128 MiB 502
A single streamed event 4 MiB The stream ends with an error event

The image and video caps are checked before anything is sent, so an oversized result is refused with 502 rather than delivered broken. A response with no declared size that runs past its cap is cut off instead. In practice none of these caps is close to what a normal response needs.

Images come back inline as base64, which is why their cap is larger. For large inputs, such as an image for a vision prompt, keep the encoded request under 10 MiB.

Video generation takes too long for one request, so it’s a job you start and then poll.

POST /video/quote what it will cost (no key, free)
│
POST /video/queue start the job ──► queue_id (billed)
│ └─ 409 needs_consent: resend the same body with `consents`
▼
POST /video/retrieve every ~10 s
├─ JSON: still processing ─► wait, ask again
└─ video/mp4 bytes, or a download URL ─► done
│
POST /video/complete release the stored media

/video/retrieve returns either a JSON status or the finished video itself under the same 200, so read Content-Type before you read the body.

Bayle keeps no record of your queue_id. It isn’t stored anywhere on Bayle’s side, so you are the only one holding it: lose it and the job can’t be found again. Calling /video/complete twice is answered on its merits rather than from a record. The Video guide has the full flow and the consent step.

chat.bayle.ai is a client of this same API, built so your key never reaches the browser.

Browser Chat server Bayle API
─────── ─────────── ─────────
POST /api/chat ──────────► same-origin check
(cookie sent by the read key from cookie
browser, unreadable add Authorization: Bearer ─────► /chat/completions
to page scripts) add Accept-Language
◄──────── stream the reply back ◄────────
  • The page never calls api.bayle.ai directly. Every call goes to the chat’s own server routes.
  • Your key lives in an HttpOnly, Secure, SameSite=Strict cookie. Page scripts can’t read it; the browser only sends it back to the chat’s own server.
  • Chat history, generated media and settings are kept in your browser’s IndexedDB, never on the server. The key is never stored there.
Place What What it never holds
Bayle API Nothing between requests Prompts, replies, media, keys
API logs Method, path, status, duration, request ID; failure reasons Headers, bodies, prompts, keys
Hosting access logs URL, status, latency, client IP, user agent Bodies
Account records Your key’s balance and tier, indexed by a one-way hash of the key The key itself
Your browser (web chat) Chats, media, settings in IndexedDB; key in a cookie The key in IndexedDB
Model infrastructure Depends on the model’s privacy level See Privacy

GET /models lists the catalog. Filter it with ?type=: text, code, embedding, image, video, and others. Each entry carries an id to pass back as model, a type, and a model_spec object with the model’s capabilities, limits and privacy level.

Only text, embedding, image and video models have endpoints here. The catalog lists other types too, but there is nothing to call them with.

Bayle’s prices are in Brazilian reais. The Models tab shows each model’s price per 100K tokens for text, per million for embeddings, per image or per video, in reais converted from dollars at a live USD/BRL rate in your browser, or in US dollars if you switch the currency in the header. Spending draws down your key’s balance; when it runs out, requests fail with 402 until you add more.

The catalog changes as models are added and retired. Look identifiers up rather than hard-coding a list, and treat them as opaque strings. See Models.