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.
The pieces
Section titled “The pieces” 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.
The request path
Section titled “The request path”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.
Public and authenticated endpoints
Section titled “Public and authenticated endpoints”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.
Authentication
Section titled “Authentication”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.
Request bodies
Section titled “Request bodies”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 isstream, 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
400before the model is called, so it costs nothing.
Streaming
Section titled “Streaming”Set "stream": true on a chat completion to get Server-Sent Events.
HTTP/1.1 200 OKContent-Type: text/event-stream; charset=utf-8Cache-Control: no-cacheX-Accel-Buffering: noContent-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.
Timeouts
Section titled “Timeouts”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.
Retries
Section titled “Retries”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.
Errors
Section titled “Errors”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.messageis 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_errorwith anullcode. - The status code says what kind of failure it was:
400for a bad request,401for the key,402for balance,429for rate limits,503when a model is offline or at capacity,504for a timeout. - A failure to reach the model at all is
502with typeapi_errorand anullcode.
The full list is on Errors.
Response language
Section titled “Response language”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.
Size limits
Section titled “Size limits”| 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 jobs
Section titled “Video jobs”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.
The web chat
Section titled “The web chat”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.aidirectly. Every call goes to the chat’s own server routes. - Your key lives in an
HttpOnly,Secure,SameSite=Strictcookie. 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.
Where data is kept
Section titled “Where data is kept”| 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 |
Models and pricing
Section titled “Models and pricing”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.