Documentation
API & Architecture Reference
The streaming endpoint behind the terminal on this site, documented exactly as deployed — request schema, SSE event protocol, headers, limits, and error contract — followed by the reference architecture for edge caching and private-cloud deployment.
Last updated: August 27, 2026 · API version: v1
Overview
bayar.dev exposes a single public endpoint. It is stateless: the client holds the conversation and replays the full history on every turn, and the server keeps nothing between requests. Responses are streamed token by token over server-sent events.
Everything under Endpoint through Errors describes behaviour that is live right now and can be verified with curl. The two architecture sections at the end are explicitly labelled where they describe a target design rather than a deployed system.
Endpoint
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/chat |
| Content type | application/json |
| Response | text/event-stream |
| Auth | None on the public demo endpoint — rate limited by IP |
| Max duration | 30 seconds per request |
| Idempotency | None — every request is a fresh inference call |
Header Specification
| Header | Requirement |
|---|---|
| Content-Type | Required. Must be application/json; any other body encoding fails JSON parsing and returns 400. |
| Accept | Optional. text/event-stream is recommended for streaming clients; the response is an event stream regardless. |
| Authorization | Not used by the public endpoint. Private deployments enforce Bearer <token> at the gateway — see VPC & BYOK. Sending it against the public endpoint is ignored, never logged, and never forwarded upstream. |
| X-Forwarded-For | Set by the platform edge, not by callers. First entry is the rate-limit identity; CF-Connecting-IP and X-Real-IP are the fallbacks. |
On bearer tokens
Request Body
A single messages array, ordered oldest to newest. The server derives the model context from it and discards it after the response completes.
POST /api/chat HTTP/1.1
Host: bayar.dev
Content-Type: application/json
{
"messages": [
{
"id": "msg_1",
"role": "user",
"parts": [{ "type": "text", "text": "benchmark --latency" }]
},
{
"id": "msg_2",
"role": "assistant",
"parts": [{ "type": "text", "text": "[demo] P95 <85ms ..." }]
},
{
"id": "msg_3",
"role": "user",
"parts": [{ "type": "text", "text": "architecture --stack" }]
}
]
}type ChatRequest = {
messages: {
// Optional client-side id, echoed nowhere. Max 128 chars.
id?: string;
// 'system' is rejected: instructions cannot be injected by a client.
role: 'user' | 'assistant';
// 1-32 parts per message. Parts whose type is not 'text' are
// discarded server-side rather than rejected, so SDK-emitted
// parts such as 'step-start' are safe to send back verbatim.
parts: {
type: string; // max 64 chars
text?: string; // max 4,000 chars
}[];
}[]; // 1-100 messages per request
};Unknown top-level keys are stripped rather than rejected. Parts with a type other than text are dropped before the request reaches the model, which is what makes it safe to echo an SDK-generated assistant message straight back.
curl -N -X POST https://bayar.dev/api/chat \
-H 'Content-Type: application/json' \
-d '{
"messages": [
{
"role": "user",
"parts": [{ "type": "text", "text": "architecture --stack" }]
}
]
}'Limits
Two different mechanisms apply. Payload caps reject abuse outright. Context budgets trim a long-but-legitimate conversation instead of failing it, so a session that runs long loses its oldest turns rather than breaking.
| Limit | Value | Behaviour past the limit |
|---|---|---|
| messages per request | 100 | Rejected — 400 |
| parts per message | 32 | Rejected — 400 |
| chars per part | 4,000 | Rejected — 400 |
| chars per request | 50,000 | Rejected — 400 |
| context messages | 20 | Trimmed to the most recent 20 |
| context chars | 12,000 | Oldest turns dropped until it fits |
| requests per minute | 10 per IP | Rejected — 429 |
| requests per day | 100 per IP | Rejected — 429 |
A 429 carries Retry-After in seconds alongside X-RateLimit-Limit and X-RateLimit-Remaining. Both windows are consumed on every request, so tripping the per-minute limit does not shield the daily budget. The limit check runs before the body is read, which keeps a flood cheap to reject.
SSE Streaming Schema
The response is a UI message stream: one JSON object per data: line, terminated by data: [DONE]. Each object has a type discriminant.
| Event type | Payload | Meaning |
|---|---|---|
| start | — | Response opened |
| start-step | — | Model step began |
| text-start | id | A text part opened; id identifies it |
| text-delta | id, delta | Append delta to the part with that id |
| text-end | id | That text part is complete |
| finish-step | — | Model step complete |
| finish | finishReason | Response complete, e.g. "stop" |
| error | errorText | Failure raised mid-stream after 200 OK |
| [DONE] | — | Stream terminator — not JSON |
HTTP/1.1 200 OK
content-type: text/event-stream
cache-control: no-cache
x-accel-buffering: no
x-vercel-ai-ui-message-stream: v1
transfer-encoding: chunked
data: {"type":"start"}
data: {"type":"start-step"}
data: {"type":"text-start","id":"msg_0b19..."}
data: {"type":"text-delta","id":"msg_0b19...","delta":"P95"}
data: {"type":"text-delta","id":"msg_0b19...","delta":" <85ms"}
data: {"type":"text-end","id":"msg_0b19..."}
data: {"type":"finish-step"}
data: {"type":"finish","finishReason":"stop"}
data: [DONE]text-delta events are additive: concatenate deltas in arrival order, keyed by id. A stream may contain more than one text part, so do not assume a single buffer. x-accel-buffering: no is set to stop intermediate proxies from buffering the stream into one chunk.
Client Usage
The reference client is the AI SDK's useChat, which handles history accumulation, incremental parsing, and cancellation. This is what the terminal on the home page runs.
'use client';
import { useChat } from '@ai-sdk/react';
export function Terminal() {
// Defaults to POST /api/chat and accumulates the full history client-side.
const { messages, sendMessage, status, error, stop } = useChat();
const streaming = status === 'streaming' || status === 'submitted';
return (
<>
{messages.map((m) => (
<p key={m.id}>
{m.parts
.filter((p) => p.type === 'text')
.map((p) => p.text)
.join('')}
</p>
))}
<button onClick={() => sendMessage({ text: 'benchmark --latency' })}>
run
</button>
{/* Aborts the fetch and closes the stream mid-response. */}
{streaming && <button onClick={stop}>stop</button>}
{/* 4xx bodies arrive as JSON in error.message. */}
{error && <p role="alert">{error.message}</p>}
</>
);
}Error Contract
Errors are returned as JSON with an error string. Messages are deliberately generic: upstream provider identity, hostnames, and stack traces are logged server-side and never serialised to the client.
| Status | Body | Cause |
|---|---|---|
| 400 | "Malformed JSON body." | Body was not valid JSON |
| 400 | "Invalid request body." | Schema violation — bad role, empty array, or a payload cap exceeded |
| 400 | "No text content in request." | Valid shape, but no text part survived filtering |
| 400 | "Conversation too long." | Total characters over 50,000 |
| 429 | "Rate limit exceeded. Try again in Ns." | Per-minute or per-day window exhausted |
| 500 | "Internal Server Error" | Request failed before the stream opened |
| 200 + error event | "Upstream model request failed." | Failure after headers were sent — arrives as an in-stream error event |
Handling the split failure model
200 OK. Clients must handle both a non-2xx JSON response and an error event mid-stream — treating only the status code as the success signal will silently swallow upstream failures.Live Architecture
What actually serves this site today. Every stage below is in the request path of the terminal above.
browser (React client, useChat)
|
| POST /api/chat · full message history · application/json
v
Next.js route handler (App Router · maxDuration 30s)
|
+-- 1. rate limit per-identity fixed window · 10/min · 100/day
| checked BEFORE the body is read
+-- 2. validate schema check · role allowlist · per-part size caps
+-- 3. normalise non-text parts dropped
| context trimmed to 20 messages / 12,000 chars
+-- 4. stream Azure AI Foundry · OpenAI-compatible /openai/v1
|
v
SSE response to browser
upstream detail is logged server-side and masked on the wire,
so provider hostnames and stack traces never reach the client.Rate-limit counters are held in a shared store when one is configured, and in process memory otherwise. In-memory counters are per-instance: a platform running N instances effectively allows N times the limit, so a shared store is required wherever the limit is load-bearing.
Edge Caching
Reference architecture
+--------------------------------------+
request ----> edge PoP | 1. static assets (immutable, CDN) |
| 2. semantic cache (normalised key) |
+------------------+-------------------+
hit | | miss
v v
replayed stream origin route handler
|
v
model provider
|
write-through <-------+
key = hash(tenant, model, normalised prompt embedding bucket)
scope = per-tenant namespace; never shared across tenants
ttl = short by default; invalidated on system-prompt change
bypass = requests marked non-deterministic, or tenant opt-outTwo tiers with different correctness requirements. Static assets are content-hashed and cached immutably at the PoP. Inference responses use a semantic cache keyed on a normalised prompt within a per-tenant namespace — never a global one, since cross-tenant reuse would leak prompt content between customers.
Cache entries are scoped to the model and the system prompt, so a prompt change invalidates rather than silently serving stale answers. Requests marked non-deterministic bypass the cache entirely, and tenants can disable it per environment.
Private VPC & BYOK Deployment
Reference architecture
customer cloud account (Azure or AWS)
|
+-- private subnet
| +-- bayar.dev gateway container image, customer-operated
| +-- private endpoint ----> customer's own model deployment
|
+-- Key Vault / KMS customer-managed keys (BYOK)
| +-- encryption at rest and in transit under customer keys
| +-- key rotation and revocation stay with the customer
|
+-- observability logs and metrics stay in-account
|
+-- egress no data path to a bayar.dev control
plane; licence and image pulls onlyThe gateway runs as a container inside the customer's own account and subnet, reaching the model deployment over a private endpoint rather than the public internet. Encryption keys stay in the customer's Key Vault or KMS, so rotation and revocation are customer-controlled — that is the substance of BYOK, as opposed to a provider-held key labelled as one.
In this topology, prompt and completion data never crosses into infrastructure operated by bayar.dev. Tenant authentication is enforced at the gateway with the bearer token described under Headers. Data handling for the public endpoint is documented separately in the privacy policy.