# SURAD — integration reference for AI coding agents API base: https://app.surad-ai.com (HTTPS only, JSON bodies unless noted). Human docs (Arabic): https://surad-ai.com/developers Live OpenAPI: https://app.surad-ai.com/api/v1/developer-kit/v1/openapi.json (root /openapi.json is blocked by design) ## Concepts - Workspace: a tenant's space. Has agents, API keys, allowed origins, conversations. - Agent: built and published in SURAD Studio. A published revision fixes: brain mode, instructions, model, knowledge, voice profile, pronunciation rules, reply format (`text` | `voice` | `text_and_voice`) and reply length (`short` | `balanced` | `detailed`). Changes reach users only after the owner publishes. - Brain mode ("who writes replies", Studio → agent → reply format): - `SURAD_MANAGED`: SURAD writes replies with the agent's instructions/knowledge and speaks them. - `EXTERNAL_AGENT`: your system writes replies; SURAD only speaks them (TTS) and listens (STT). - Two credentials, never mixed: - API key `sk_…`: server-side only. Created in Studio → Integration. Scopes: `sessions.create`, `sessions.stream`, `speech.transcribe`, `speech.synthesize`, `agents.read` (agents + conversation history), `agents.manage`, `tools.ack`, `usage.read`. Default 120 requests/minute. - Session token: short-lived, bound to one origin, the only thing a browser/app holds. - Allowed origins: exact origins registered in Studio → Integration (e.g. `https://www.example.com`; `https://example.com` is a different origin). Every client request sends `Origin`; mismatches get `403 ORIGIN_DENIED`. ## Getting access No self-signup today: SURAD provisions your organization, workspace and an admin user (temporary password; change it in Studio → Settings). Then in Studio: build and publish an agent; Studio → Integration: create an API key and register your origins. ## The agent (Studio, or API with a user session / `agents.manage`) - Lifecycle: draft revision → configure → test in Studio → publish (new sessions use it) → edits after testing open a new revision automatically → rollback restores the previous one. GET /api/v1/workspaces/{workspace_id}/agents/{agent_id}/releases POST /api/v1/workspaces/{workspace_id}/agents/{agent_id}/releases/publish body `{"agent_version_id": "…", "expected_epoch": }` → `{"release": {"published_version_id", "previous_version_id", "epoch", "published_at", …}}` POST /api/v1/workspaces/{workspace_id}/agents/{agent_id}/releases/rollback body `{"expected_epoch": }` - Brain mode, reply format and length, one-line embed opt-in: fields of the channel config, applied on publish. GET|PUT /api/v1/workspaces/{workspace_id}/agents/{agent_id}/orchestration (PUT body `{"agent_version_id", "config"}`) GET /api/v1/workspaces/{workspace_id}/agents/{agent_id}/orchestration/default/{mode} (start from this when switching `SURAD_MANAGED` ↔ `EXTERNAL_AGENT`) config fields: `orchestration_mode`, `response_output` (`text`|`voice`|`text_and_voice`), `reply_length` (`short`|`balanced`|`detailed`), `public_embed` (bool), `voice_profile_id`. - Managed brain: PATCH /api/v1/workspaces/{workspace_id}/agents/{agent_id}/versions/{agent_version_id}/runtime-profile (`instructions` ≤ 8000, `model_deployment_id`, `temperature` 0–2, `max_output_tokens` 16–4096). Default model: Gemini 2.5 Flash run by the platform (no customer key). Grounded on knowledge bound to the revision; says it does not know instead of inventing. - Voice: GET /api/v1/workspaces/{workspace_id}/speech/experience/catalog (voices: `library_id`, `voice_id`, `gender_presentation`, `selectable`, `experimental`, `availability`); GET|PUT /api/v1/workspaces/{workspace_id}/agents/{agent_id}/voice-experience (`library_id`, `speaking_speed_level` −2..2, `tone`, …). Defaults for everyone: `vp-msa-female` (default), `vp-msa-male`. The chosen voice is the voice that speaks. - Pronunciation: GET|POST /api/v1/workspaces/{workspace_id}/speech/pronunciation-rules body `{"written": "SURAD", "spoken": "صُرَد", "confirm": true, "agent_id"?: "…"}` (written ≤ 80, spoken ≤ 160). - Discovery for a named agent: GET /api/v1/workspaces/{workspace_id}/agents/{agent_id}/capabilities ## Path A — one-line widget (no backend) Prerequisites (all three): agent published; "Allow one-line embed" enabled on the agent (reply format section) and published; site origin registered. ```html ``` Attributes: `data-embed` (public signed agent reference from Studio → Integration; cannot be guessed or forged and grants nothing by itself), `data-title`, `data-greeting`, `data-color` (#hex), `data-position` (`left` default | `right`), `data-voice="off"` (hide the microphone), `data-token-url` (instead of `data-embed`: a same-origin path on your server that answers POST with `{"token": "…"}` minted as in Path B step 1). Behavior: Shadow-DOM UI, RTL. Text chat; push-to-talk voice (tap to talk, tap to send, 30 s cap; 16 kHz mono WAV upload); reply shown and played in the agent's voice; talking stops playback. Session tokens (600 s) are minted per visitor and renewed automatically. Limits: 20 sessions per visitor per 10 minutes, 600 per agent per hour. Microphone needs HTTPS and user permission. Public mint endpoint used by the widget (you normally don't call it yourself): POST /api/v1/widget/embed/token body `{"embed_id": "emb_…"}`, header `Origin` → `{"token": "…", "expires_at": }`. ## Path B — SURAD agent from your server/app 1) Mint a session token (server, API key): POST /api/v1/workspaces/{workspace_id}/widget/session-tokens Headers: `Authorization: Bearer sk_…`, `Content-Type: application/json` Body: `agent_id` (uuid, required), `origin` (registered origin, required), `ttl_seconds` (60–3600, default 900), `agent_version_id` (optional pin; default = published revision) Response: `{"token": "…", "claims": {…}}` 2) Open a session (client): POST /api/v1/widget/sessions Headers: `Authorization: Bearer `, `Origin: ` Body: `mode` (`text` default | `voice`), `conversation_id` (resume an existing conversation with a fresh token), `external_visitor_id` (your id for the visitor) Response: `conversation.conversation_id` A token opens one session only (`409 TOKEN_REPLAYED`); afterwards it authorizes that session's requests until expiry (`401 TOKEN_EXPIRED` → mint a new token and reopen with the same `conversation_id`). 3) Text turn: POST /api/v1/widget/conversations/{conversation_id}/turns Body: `text` (required), `mode` (`text` | `voice`), `idempotency_key` (new UUID per request and per retry; unique across the workspace) Response (main fields): `reply.display`, `reply.spoken`, `audio` (when the agent's reply format includes voice: `content_base64` WAV, `media_type`, `voice`), `identity.voice_profile_id`, `turn_id`, `llm_invoked`, `tts_invoked`. 4) Voice message (STT + reply in one call): POST /api/v1/widget/conversations/{conversation_id}/voice/utterance multipart/form-data, field `file` (WAV mono 16 kHz PCM16 recommended, ≤ 4 MB) Response: same as a turn plus `transcript.text`. 5) Live events (optional), SSE: GET /api/v1/widget/conversations/{conversation_id}/events (headers: Authorization, Origin) Frames: `event: ` + `data: `; details in `data.payload`. Types include: session.ready, transcript.final, agent.understanding, agent.responding, audio.first, audio.chunk, audio.completed, tool.requested, session.error, turn.completed. Browsers' EventSource cannot send Authorization — read it with streaming fetch or relay through your server. 6) Control: POST /api/v1/widget/conversations/{conversation_id}/cancel — stop the reply in progress (barge-in); stale audio is never delivered afterwards. POST /api/v1/widget/conversations/{conversation_id}/close — end the session (history is kept). 7) Conversation history (API key with `agents.read`, or Studio → Conversations): GET /api/v1/workspaces/{workspace_id}/conversations?agent_id=&before=&limit= (newest first, limit ≤ 100, `next_before` for paging) GET /api/v1/workspaces/{workspace_id}/conversations/{conversation_id} Turns carry `role`, `content`, `io_mode`. Empty user content means SURAD never received the visitor's words (external brain sends ready replies only) or a voice message whose text was not stored. Audio recordings are never stored. ## Path C — voice for your own brain (agent in EXTERNAL_AGENT mode) Tokens and sessions as in Path B. Then: Speak a ready reply: POST /api/v1/widget/conversations/{conversation_id}/turns Body: `capability: "tts_only"`, `ready_text` (final Arabic text, required), `idempotency_key`, optional `client_request_id` (echoed back), optional `synthesis_transport: "grpc_stream"`. Response: complete WAV in `audio.content_base64`, plus `identity.executed_profile_id`, `identity.voice`, `identity.transport`. Streaming (`synthesis_transport: "grpc_stream"`, enabled per workspace by SURAD; otherwise ignored): open the SSE events stream first; then `audio.first` (payload: encoding LINEAR16, sample_rate_hz 24000, channels 1, container pcm), `audio.chunk` (payload.seq, payload.content_base64 raw PCM16 — play in seq order immediately), `audio.completed`. The POST still returns the complete audio at the end — do not play both. Streaming applies to Gemini-based voice profiles; other voices return the complete WAV. Measured server-side on a Gemini voice with ~5 s replies: first chunk ≈ 0.6 s median vs ≈ 3.7 s for the complete audio; network, your server and the browser add to that. Transcribe user speech: POST /api/v1/widget/conversations/{conversation_id}/voice/utterance (as Path B step 4); use `transcript.text`. Latency practice: send the whole reply in one TTS request (not one per sentence); open the session early; relay audio chunks without buffering or re-encoding; log t0 (request), audio.first, playback start, audio.completed with identity.request_id. Streaming WebSockets for server relays (enabled per workspace; accept platform-approved origins; auth = Authorization bearer session token + Origin): - WS /api/v1/widget/conversations/{conversation_id}/stt-stream — send `{"type":"start","stream_id":"…"}`, binary frames (4-byte LE seq + PCM16 mono 16 kHz), `{"type":"end"}` (also `cancel`, `ping`); receive stream_opened, speech_started, transcript_interim, speech_ended, transcript_final, no_speech, stream_error. Max 60 s per stream. - WS /api/v1/widget/conversations/{conversation_id}/tts-stream — send `{"type":"synthesize","stream_id":"…","text":"…","idempotency_key":"…"}` (≤ 4000 chars; also `cancel`, `ping`); receive binary frames (4-byte LE seq + PCM16 mono 24 kHz) as produced, plus tts_started, audio_config, tts_completed, tts_cancelled, tts_error. ## Verifying the voice on every turn `audio.executed_voice` (the voice that actually spoke) is present on every spoken turn. The agent's voice profile is `identity.voice_profile_id` on SURAD-managed turns and `identity.executed_profile_id` on `tts_only` turns. Log them with your measurements. ## Usage and limits GET /api/v1/workspaces/{workspace_id}/developer/usage (API key with `usage.read`) → `request_count`, `used_units`, `quota_units`, `remaining_units`, `budget_cents`, `speech_jobs`, `usage_cap_mode` (default `MONITOR_ONLY`: recorded, not blocked), `overage_policy`, `quota_state`. Limits: session token 60–3600 s (default 900; embed tokens 600 s); API key default 120 requests/minute; voice upload 4 MB; widget recording 30 s; conversation list ≤ 100 per page. ## Retry policy Retry only retryable failures (RATE_LIMITED, AUDIO_PRODUCTION_FAILURE, network errors) with short backoff. A key identifies one execution across the workspace. Repeating a key never synthesizes again and costs nothing. If the earlier request completed in the same conversation and generation within the last 10 minutes, the replay returns its original audio (`idempotent_replay: true`, `tts_invoked: false`). Otherwise (it failed, is still in progress, the 10 minutes passed, or the key belongs to another conversation) the replay has `audio: null` and `requires_new_idempotency_key: true`: send a new key. So: after a network drop, retry with the same key; after a recorded failure, retry with a new key. A provider failure to speak a reply is not an HTTP error: the turn returns 200 with `audio: null`, `failed: true`, `error_code` and the full text — retry once with a new key. On 401 TOKEN_EXPIRED, or 404 CONVERSATION_NOT_FOUND (live sessions are held in the API process and end on a SURAD restart), mint a new token and reopen with the same `conversation_id`; history is kept. Do not retry other 4xx unchanged. The reply text is always returned; if audio still fails after one retry, show the text. ## Errors Shape: `{"error": {"code": "...", "message": "...", "request_id": "..."}}`; `X-Request-ID` response header. Session-path messages are Arabic and user-displayable. | code | http | retry | meaning | |---|---|---|---| | AUTH_FAILED | 401 | no | missing/invalid token or embed id | | TOKEN_EXPIRED | 401 | no | mint a new token, reopen with same conversation_id | | ORIGIN_DENIED | 403 | no | origin not registered or not the token's origin | | TENANT_MISMATCH | 403 | no | session belongs to another workspace | | TOKEN_REPLAYED | 409 | no | token already opened a session | | IDEMPOTENCY_CONFLICT | 409 | no | key already used; send a new key | | STALE_GENERATION | 409 | no | stale reply discarded after cancel/barge-in | | CONVERSATION_NOT_FOUND | 404 | no | session gone (incl. after a SURAD restart): new token, reopen with the same conversation_id | | NO_PUBLISHED_AGENT | 404 | no | agent not published, or embed not enabled on the published revision | | VALIDATION_FAILED | 422 | no | bad request body | | AUDIO_EMPTY | 400 | no | empty/invalid audio file | | RATE_LIMITED | 429 | yes | back off and retry | | BUDGET_EXHAUSTED | 429 | no | quota used up | | AUDIO_PRODUCTION_FAILURE | 502 | yes | agent voice settings could not be loaded; retry once with a new key, then fall back to text | | INTERNAL | 500 | no | send request_id to support | API-key errors: AUTHENTICATION_FAILED 401, API_KEY_REVOKED 401, SCOPE_DENIED 403, INVALID_SCOPES 422, QUOTA_EXCEEDED 429 (not raised in MONITOR_ONLY), BUDGET_EXCEEDED 429 (BLOCK plans only), WORKSPACE_QUOTA_EXCEEDED 422 (speech storage). ## Execution identity and stability Every spoken turn reports what actually ran: `identity.voice_profile_id` or `identity.executed_profile_id`, `audio.voice`, `identity.transport`. The voice is the agent's configured voice; SURAD never silently substitutes another voice.