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

PropertyValue
MethodPOST
Path/api/chat
Content typeapplication/json
Responsetext/event-stream
AuthNone on the public demo endpoint — rate limited by IP
Max duration30 seconds per request
IdempotencyNone — every request is a fresh inference call

Header Specification

HeaderRequirement
Content-TypeRequired. Must be application/json; any other body encoding fails JSON parsing and returns 400.
AcceptOptional. text/event-stream is recommended for streaming clients; the response is an event stream regardless.
AuthorizationNot 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-ForSet 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

The public demo endpoint is intentionally unauthenticated so the terminal works with no signup. It is protected by request validation and per-IP rate limits instead. Token-based tenant auth is part of the private deployment path, not of this endpoint — treat any bearer token you send here as ignored.

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
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" }]
    }
  ]
}
TypeScript — request schema
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.

Shell — minimal streaming request
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.

LimitValueBehaviour past the limit
messages per request100Rejected — 400
parts per message32Rejected — 400
chars per part4,000Rejected — 400
chars per request50,000Rejected — 400
context messages20Trimmed to the most recent 20
context chars12,000Oldest turns dropped until it fits
requests per minute10 per IPRejected — 429
requests per day100 per IPRejected — 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 typePayloadMeaning
startResponse opened
start-stepModel step began
text-startidA text part opened; id identifies it
text-deltaid, deltaAppend delta to the part with that id
text-endidThat text part is complete
finish-stepModel step complete
finishfinishReasonResponse complete, e.g. "stop"
errorerrorTextFailure raised mid-stream after 200 OK
[DONE]Stream terminator — not JSON
Response — captured stream
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.

React — minimal client
'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.

StatusBodyCause
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

A streaming endpoint can fail after it has already returned 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.

Deployed request path
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

The design below is the target topology for a production tenant. It is not what serves this public demo, which runs the request path in Live Architecture with no semantic cache layer.
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-out

Two 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

The deployment model offered for enterprise engagements. Availability and scope are agreed per contract — talk to hi@bayar.dev before designing against it.
Private cloud — 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 only

The 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.