REST API Reference
chat-web is the FastAPI entry point for the Chat app. It listens on
http://localhost:8080, serves Swagger UI at /docs, and ships a small
HTML/JS client at /.
Observability wiring¶
Set environment variables before startup to enable shared observability wiring:
flowchart LR
REQ["HTTP request / SSE"]
APP["chat-web create_app()"]
OBS["bootstrap_from_env('concierge-chat')"]
LC["FoundryChatbotResponder -> chat_model.stream(..., config=trace_config(...))"]
F["Foundry tracing UI"]
M["MLflow UI :5000"]
REQ --> APP --> OBS --> LC
LC --> F
LC --> M
Pages and ports¶
| URL | What it is |
|---|---|
| http://localhost:8080/ | Unified chat UI — text chat + realtime voice (Japanese labels) |
| http://localhost:8080/accessible | Minimal deafblind accessibility UI (whole-screen call toggle + text-only dialogue region) |
| http://localhost:8080/accessible/config | Runtime config for /accessible ({"realtime": bool, "tts_rate": number, "transcription": bool}) |
| http://localhost:8080/ttsplayground/ | Voice-button playground with preset phrases, free text, and browser text-to-speech |
| http://localhost:8080/realtime | Legacy path; returns 301 redirect to / |
| http://localhost:8080/capabilities | Feature-flag JSON ({"realtime": bool}) consumed by the UI to show / hide the call button |
| http://localhost:8080/docs | Swagger UI (interactive REST docs) |
| http://localhost:8080/openapi.json | OpenAPI schema |
| http://localhost:8080/healthz | Liveness probe ({"status":"ok"}) |
The TTS playground is available by opening its URL directly; the chat UI does
not include a navigation link to it. /ttsplayground redirects to the canonical
trailing-slash URL so its relative config and audio paths resolve correctly.
Authentication¶
Every endpoint that touches a conversation requires the caller to identify
themselves via the X-User-Id header. The value must be a UUID; FastAPI
returns 422 otherwise. The bundled web client generates and stores one for
you; from a script, generate one once and reuse it:
Endpoints at a glance¶
| Method | Path | Description |
|---|---|---|
| POST | /conversations |
Create conversation |
| GET | /conversations |
List conversations (?mine=true filters to your own) |
| GET | /conversations/{conversation_id} |
Get conversation |
| DELETE | /conversations/{conversation_id} |
Delete conversation |
| POST | /conversations/{conversation_id}/participants |
Join conversation |
| POST | /conversations/{conversation_id}/messages |
Post a user message (no bot reply; agent reply is a separate request) |
| GET | /conversations/{conversation_id}/messages |
List messages |
| POST | /conversations/{conversation_id}/agent-replies |
Stream an AI agent reply over Server-Sent Events |
| WS | /conversations/{conversation_id}/realtime |
Realtime voice session (WebSocket proxy to Foundry) |
| GET | /healthz |
Health check |
| GET | /capabilities |
{"realtime": bool} — true when AZURE_AI_PROJECT_ENDPOINT_REALTIME is configured |
| GET | / |
Unified HTML front-end (text + realtime voice) |
| GET | /accessible |
Minimal deafblind accessibility front-end |
| GET | /accessible/config |
Runtime config for /accessible ({"realtime": bool, "tts_rate": number}) |
| GET | /ttsplayground |
Redirect to /ttsplayground/ with status 307 |
| GET | /ttsplayground/ |
Voice-button TTS playground front-end |
| GET | /ttsplayground/config |
Built-in phrases and browser speech settings used by the playground |
| GET | /realtime |
301 redirect to / (kept for backward compatibility) |
End-to-end curl walkthrough¶
This is a copy-pasteable script that exercises every read/write endpoint once.
export USER_ID=$(python -c 'import uuid; print(uuid.uuid4())')
# Create
CONV_ID=$(curl -s -X POST http://localhost:8080/conversations \
-H "X-User-Id: ${USER_ID}" -H 'content-type: application/json' \
-d '{"title":"general","display_name":"alice"}' \
| python -c 'import json,sys; print(json.load(sys.stdin)["id"])')
echo "CONV_ID=$CONV_ID"
# List
curl -s -H "X-User-Id: ${USER_ID}" http://localhost:8080/conversations
# Get
curl -s -H "X-User-Id: ${USER_ID}" \
"http://localhost:8080/conversations/${CONV_ID}"
# Post a message
curl -s -X POST "http://localhost:8080/conversations/${CONV_ID}/messages" \
-H "X-User-Id: ${USER_ID}" -H 'content-type: application/json' \
-d '{"content":"hello","display_name":"alice"}'
# List messages
curl -s -H "X-User-Id: ${USER_ID}" \
"http://localhost:8080/conversations/${CONV_ID}/messages"
# Delete
curl -s -o /dev/null -w "HTTP %{http_code}\n" -X DELETE \
-H "X-User-Id: ${USER_ID}" \
"http://localhost:8080/conversations/${CONV_ID}"
# → HTTP 204
Sample ConversationResponse:
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"title": "general",
"participants": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa7",
"kind": "USER",
"display_name": "alice"
}
],
"created_at": "2026-05-14T06:03:22.642785Z",
"updated_at": "2026-05-14T06:03:22.642785Z"
}
Sample MessageResponse:
{
"id": "ac815590-189b-42c4-92a0-a9a9874e87c0",
"conversation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"sender": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa7",
"kind": "USER",
"display_name": "alice"
},
"role": "USER",
"content": "hello",
"created_at": "2026-05-14T06:03:23.000000Z"
}
Status codes¶
| Status | When |
|---|---|
200 OK |
Successful GETs |
201 Created |
Successful POSTs that create resources |
204 No Content |
Successful DELETE /conversations/{id} |
404 Not Found |
conversation_id does not exist |
503 Service Unavailable |
AZURE_AI_PROJECT_ENDPOINT is not configured when calling /agent-replies |
422 Unprocessable Entity |
Bad request body or non-UUID X-User-Id |
AI agent reply endpoint¶
POST /conversations/{conversation_id}/messages is intentionally agent-free:
it only persists the caller's message. AI replies live behind a dedicated
endpoint so client apps can opt-in explicitly. See
AI chatbot replies (optional) for
the setup steps.
To route this endpoint through the shared agents runtime, set:
Streaming reply via POST .../agent-replies¶
The endpoint returns a Server-Sent Events stream so the client can
render the reply incrementally and never has to poll. The response uses
Content-Type: text/event-stream and emits the following events:
| Event | Data | Notes |
|---|---|---|
delta |
{"content": "<chunk>"} |
Emitted once per partial token. Concatenate content values in order. |
complete |
MessageResponse JSON |
Final event with the persisted AGENT message. |
error |
{"detail": "<message>"} |
Emitted instead of complete if generation fails mid-stream. |
Synchronous validation runs before the stream starts, so unknown conversation IDs and missing configuration are surfaced via the regular JSON error response.
| Status | When | Body |
|---|---|---|
200 OK |
Stream opened — events follow | text/event-stream |
404 Not Found |
conversation_id does not exist |
{"detail": "..."} |
422 Unprocessable Entity |
image_url is not an inline data:image/*;base64,… URL (or exceeds the size cap) |
{"detail": "..."} |
503 Service Unavailable |
AZURE_AI_PROJECT_ENDPOINT is not configured |
{"detail": "..."} |
Optional image input (request body)¶
The endpoint accepts an optional JSON body to attach a camera-captured image to the turn. The body is omitted by most callers; when present it carries a single field:
| Field | Type | Notes |
|---|---|---|
image_url |
string | Inline data:image/*;base64,… URL. Request-scoped and never persisted (ephemeral). Remote http(s) URLs are rejected (422) so the model never fetches attacker-controlled URLs server-side; capped at ~12 MB. |
The image is threaded to the selected agent as payload.image_url. Vision
capable agents (langgraph) ground the reply in the image; echo acknowledges
receipt; the default foundry responder and the other agents currently ignore
it (the contract is in place for incremental rollout). The web UI reuses the
same camera capture overlay as the realtime voice call.
curl -N -s -X POST "http://localhost:8080/conversations/${CONV_ID}/agent-replies?agent_type=langgraph" \
-H "X-User-Id: ${USER_ID}" \
-H "Content-Type: application/json" \
-d '{"image_url": "data:image/jpeg;base64,/9j/4AAQSk..."}'
curl -N -s -X POST "http://localhost:8080/conversations/${CONV_ID}/agent-replies" \
-H "X-User-Id: ${USER_ID}"
# event: delta
# data: {"content": "Hello"}
#
# event: delta
# data: {"content": "! How can I help?"}
#
# event: complete
# data: {"id": "...", "role": "AGENT", ...}
The complete event payload matches the MessageResponse schema:
{
"id": "f1c1e4cb-...",
"conversation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"sender": {
"id": "00000000-0000-0000-0000-000000000001",
"kind": "AGENT",
"display_name": "Concierge AI"
},
"role": "AGENT",
"content": "Hello! How can I help?",
"created_at": "2026-05-14T06:03:23.000000Z"
}
Realtime voice WebSocket¶
The realtime voice feature streams microphone audio to Microsoft Foundry's
GPT Realtime API through a server-side WebSocket proxy and persists the
resulting user / AI transcripts as regular Message objects. See the
Realtime voice (optional) section of
the overview for setup, the .env reference, and UI walkthrough.
This section documents the wire protocol only.
Endpoint¶
WS /conversations/{conversation_id}/realtime
?user_id=<uuid>
[&display_name=<string>]
[&mode=accessible]
The user_id query parameter is the same UUID as the X-User-Id header
used by the REST endpoints. WebSocket frames must not include the header
because browsers cannot attach custom headers to a WebSocket handshake.
mode=accessible selects the deafblind session used by the /accessible UI:
it applies CHAT_REALTIME_ACCESSIBLE_SYSTEM_PROMPT (slow, simple-concept
instructions) and adds the hands-free capture_image camera tool. See
Accessibility mode.
Server → Client events¶
type |
Payload | Notes |
|---|---|---|
concierge.session.ready |
{"conversation_id": "..."} |
First message after accept |
oai-event |
{"payload": <Foundry event JSON>} |
Transparent relay of all Foundry events (response.output_audio.delta, response.output_audio_transcript.delta, etc.) |
concierge.message.persisted |
{"message": <MessageResponse>} |
USER or AGENT transcript saved |
concierge.camera.capture |
{"prompt": "..."?} |
mode=accessible only. Sent when the model calls capture_image; the client takes a photo automatically and replies with concierge.image.input (auto_describe: true). |
concierge.error |
{"detail": "..."} |
Unhandled server error |
Client → Server events¶
type |
Payload | Notes |
|---|---|---|
oai-event |
{"payload": <Foundry event JSON>} |
Forwarded to Foundry (typically input_audio_buffer.append with base64 PCM16) |
concierge.image.input |
{"image_url": "data:image/jpeg;base64,…", "prompt": "<optional>", "auto_describe": <optional bool>} |
Injects a camera image into the live session. The server builds a conversation.item.create (input_image) item so the model can ground its next turn in the image. By default no response is triggered — the user's next spoken turn drives the reply. When auto_describe is true (the hands-free capture_image flow) the server also sends response.create so the model describes the photo immediately. |
Image input is server-mediated
The browser sends a concierge.image.input control frame rather than the
raw Foundry conversation.item.create event. The server owns the Foundry
wire format and validates the payload: image_url must be an inline
data:image/*;base64,… URL (remote http(s) URLs are rejected so the
model never fetches attacker-controlled URLs) and is capped at ~12 MB.
Invalid payloads return a concierge.error and are not forwarded. This
single seam is also where future enhancements (persisting the image,
moderation, server-side downscaling) would live.
Tool calling is handled server-side
When the model requests a tool the relay processes the
response.output_item.done (function_call) event itself: it runs the
tool and sends conversation.item.create (function_call_output) plus
response.create back to Foundry. The browser only sees the relayed
oai-event frames and the final spoken/transcribed answer — it never has
to implement the function-calling round-trip. See
Tool calling (function calling) for how
to register new tools.
Close codes¶
| Code | Meaning |
|---|---|
4400 |
user_id is missing or not a valid UUID |
4404 |
conversation_id does not exist |
4503 |
AZURE_AI_PROJECT_ENDPOINT_REALTIME is not configured |
1000 |
Normal client-initiated close |
Capability probe¶
Clients should call GET /capabilities before showing realtime UI and only
open the WebSocket when the response is {"realtime": true}. The flag is
true whenever create_realtime_responder() succeeds, which currently
means AZURE_AI_PROJECT_ENDPOINT_REALTIME is non-empty.
CLI status check¶
For a non-interactive sanity check that the realtime endpoint is wired
correctly without opening a WebSocket, use
chat-cli realtime status.