> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sayna.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# REST API surface

> Health checks, catalog discovery, TTS synthesis, LiveKit room controls, and SIP utilities.

Sayna exposes a compact set of HTTP endpoints that reuse the same providers and caches that power the streaming stack. Use the interactive playground inside the [API reference](/api-reference/introduction) for request-by-request schemas and try-it-out support.

If you need tenant-scoped room management (listing rooms, inspecting participants, or moderating LiveKit sessions), see the [Livekit Room Management guide](/guides/livekit-room-management).

<Note>
  Authentication is optional. Enable it through the [Authentication guide](/guides/authentication) if you need shared secrets or delegated JWT validation.
</Note>

<Tip>
  Want idiomatic clients? Install one of the [Sayna SDKs](/sdks/js) to reuse the snippets shown below.
</Tip>

<Note>
  SDK examples assume you have already instantiated a `client` (see the SDK guides for setup) and are showing only the method call relevant to each endpoint.
</Note>

## `GET /` – health check

* **Purpose**: returns `{ "status": "ok" }` when the server and dependencies are alive.
* **Status codes**: always `200 OK` when the Axum router is reachable.
* **Usage**: liveness/readiness probes and smoke tests after deployments.

<Tabs>
  <Tab title="curl">
    ```bash theme={null}
    curl https://api.sayna.ai/
    # => {"status":"ok"}
    ```
  </Tab>

  <Tab title="Node SDK">
    ```ts theme={null}
    const health = await client.health();
    console.log(health.status);
    ```
  </Tab>

  <Tab title="Python SDK">
    ```python theme={null}
    from sayna_client import SaynaClient, STTConfig, TTSConfig

    client = SaynaClient(
        url="https://api.sayna.ai",
        stt_config=STTConfig(provider="deepgram", model="nova-2"),
        tts_config=TTSConfig(provider="elevenlabs", voice_id="21m00Tcm4TlvDq8ikWAM"),
    )

    health = await client.health()
    print(health.status)
    ```
  </Tab>
</Tabs>

## `GET /voices` – provider catalog

| Detail         | Description                                                            |
| -------------- | ---------------------------------------------------------------------- |
| Request body   | none                                                                   |
| Authentication | Required when `AUTH_REQUIRED=true`                                     |
| Success        | `200 OK` with provider metadata and voice options                      |
| Failure        | `401 Unauthorized` when auth fails, `500` for upstream provider errors |

Response schema mirrors each provider's capabilities, including languages, sample rates, and optional tags. Use the payload to drive voice pickers inside your product.

<Tabs>
  <Tab title="curl">
    ```bash theme={null}
    curl https://api.sayna.ai/voices | jq '.voices[0]'
    ```
  </Tab>

  <Tab title="Node SDK">
    ```ts theme={null}
    const voices = await client.getVoices();
    console.log(voices.deepgram?.[0]);
    ```
  </Tab>

  <Tab title="Python SDK">
    ```python theme={null}
    voices = await client.get_voices()
    print(next(iter(voices.values()))[0])
    ```
  </Tab>
</Tabs>

## `POST /speak` – one-shot synthesis

| Field                      | Type    | Required | Description                                                                                                         |
| -------------------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------- |
| `text`                     | string  | Yes      | Text to convert into audio                                                                                          |
| `tts_config`               | object  | Yes      | TTS configuration (same schema as the WebSocket `tts_config`)                                                       |
| `tts_config.provider`      | string  | Yes      | Provider slug (e.g., `deepgram`, `elevenlabs`, `cartesia`, `azure`, `google`)                                       |
| `tts_config.model`         | string  | Yes      | Model name for the TTS provider                                                                                     |
| `tts_config.voice_id`      | string  | No       | Voice from `/voices`; provider default when omitted                                                                 |
| `tts_config.audio_format`  | string  | No       | Audio format preference (e.g., `linear16`, `mp3`)                                                                   |
| `tts_config.sample_rate`   | integer | No       | Sample rate in Hz                                                                                                   |
| `tts_config.speaking_rate` | number  | No       | Speaking rate (0.25–4.0, default 1.0)                                                                               |
| `tts_config.auth`          | object  | No       | Per-request provider credential override (see [Provider auth overrides](/guides/websocket#provider-auth-overrides)) |

* **Request**: JSON body plus optional auth via `Authorization` header or `api_key` query parameter.
* **Response**: `200 OK` with raw `audio/pcm` bytes plus `x-audio-format` and `x-sample-rate` response headers.
* **Errors**: `400` when `text` is empty, `500` for provider failures or missing credentials.

<Tabs>
  <Tab title="curl">
    ```bash theme={null}
    curl -X POST https://api.sayna.ai/speak \
      -H "Content-Type: application/json" \
      -d '{
        "text": "Welcome to Sayna",
        "tts_config": {
          "provider": "deepgram",
          "model": "aura-asteria-en"
        }
      }' \
      --output audio.pcm
    ```

    ```bash theme={null}
    # With per-request provider credentials
    curl -X POST https://api.sayna.ai/speak \
      -H "Content-Type: application/json" \
      -d '{
        "text": "Hello from Sayna",
        "tts_config": {
          "provider": "elevenlabs",
          "model": "eleven_flash_v2_5",
          "voice_id": "voice_id_here",
          "auth": {
            "api_key": "your-elevenlabs-key"
          }
        }
      }' \
      --output audio.pcm
    ```
  </Tab>

  <Tab title="Node SDK">
    ```ts theme={null}
    const audioBuffer = await client.speakRest("Welcome to Sayna", {
      provider: "deepgram",
      model: "aura-asteria-en",
      voice_id: "aura-asteria-en",
    });

    console.log(`Received ${audioBuffer.byteLength} bytes`);
    ```

    ```ts theme={null}
    // With per-request provider credentials
    const audioBuffer = await client.speakRest("Welcome to Sayna", {
      provider: "elevenlabs",
      model: "eleven_flash_v2_5",
      voice_id: "voice_id_here",
      auth: { api_key: "your-elevenlabs-key" },
    });
    ```
  </Tab>

  <Tab title="Python SDK">
    ```python theme={null}
    audio_bytes, headers = await client.speak_rest(
        "Welcome to Sayna",
        TTSConfig(provider="deepgram", voice_id="aura-asteria-en"),
    )

    print(len(audio_bytes), "bytes of", headers.get("Content-Type"))
    ```

    ```python theme={null}
    # With per-request provider credentials
    audio_bytes, headers = await client.speak_rest(
        "Welcome to Sayna",
        TTSConfig(
            provider="elevenlabs",
            model="eleven_flash_v2_5",
            voice_id="voice_id_here",
            auth={"api_key": "your-elevenlabs-key"},
        ),
    )
    ```
  </Tab>
</Tabs>

## `POST /livekit/token` – participant tokens

| Field                  | Type   | Description                            |
| ---------------------- | ------ | -------------------------------------- |
| `room_name`            | string | Room to join or create                 |
| `participant_name`     | string | Display name shown inside LiveKit      |
| `participant_identity` | string | Stable identity string for permissions |

Returns:

| Field                  | Type   | Description                            |
| ---------------------- | ------ | -------------------------------------- |
| `token`                | string | Signed LiveKit JWT for the participant |
| `room_name`            | string | Echoes the requested room              |
| `participant_identity` | string | Echo of the requested identity         |
| `livekit_url`          | string | Client-facing LiveKit URL from config  |

When authentication is enabled, this endpoint creates the room if it doesn't exist and sets `metadata.auth_id` for tenant isolation.

* **Errors**: `400` when any field is empty, `403` when the room exists with a different tenant's `auth_id`, `500` when LiveKit credentials are misconfigured.
* **Typical flow**: once a WebSocket session advertises LiveKit settings in the `config` message, call this REST endpoint from your control plane to mint attendee tokens.

<Tabs>
  <Tab title="curl">
    ```bash theme={null}
    curl -X POST https://api.sayna.ai/livekit/token \
      -H "Content-Type: application/json" \
      -d '{
            "room_name": "support-42",
            "participant_name": "alex",
            "participant_identity": "alex-support"
          }'
    ```
  </Tab>

  <Tab title="Node SDK">
    ```ts theme={null}
    const tokenInfo = await client.getLiveKitToken(
      "support-42",
      "alex",
      "alex-support"
    );

    console.log(tokenInfo.token, tokenInfo.livekit_url);
    ```
  </Tab>

  <Tab title="Python SDK">
    ```python theme={null}
    token = await client.get_livekit_token("support-42", "alex", "alex-support")
    print(token.token, token.livekit_url)
    ```
  </Tab>
</Tabs>

## `GET /recording/{stream_id}` – download session audio

| Detail         | Description                                                                                                          |
| -------------- | -------------------------------------------------------------------------------------------------------------------- |
| Path param     | `stream_id` from the WebSocket `ready` message (or the value you provided). Empty values, `..`, or `/` are rejected. |
| Authentication | Required when `AUTH_REQUIRED=true`                                                                                   |
| Success        | `200 OK` with `audio/ogg` body and `Content-Disposition: attachment; filename="{stream_id}.ogg"`                     |
| Failure        | `400` invalid `stream_id`, `404` recording missing, `503` storage not configured or unavailable                      |

Recording files live at `{recording_s3_prefix}/{stream_id}/audio.ogg` when `livekit.enable_recording=true` and storage credentials are set.

<Tabs>
  <Tab title="curl">
    ```bash theme={null}
    # Download and save as <stream_id>.ogg
    curl -OJ https://api.sayna.ai/recording/support-call-789
    ```
  </Tab>

  <Tab title="Node SDK">
    ```ts theme={null}
    import { promises as fs } from "fs";

    const streamId = client.streamId; // from ready()
    const audio = await client.getRecording(streamId!);
    await fs.writeFile(`${streamId}.ogg`, Buffer.from(audio));
    ```
  </Tab>

  <Tab title="Python SDK">
    ```python theme={null}
    stream_id = client.received_stream_id
    audio_bytes, headers = await client.get_recording(stream_id)
    with open(f"{stream_id}.ogg", "wb") as f:
        f.write(audio_bytes)
    ```
  </Tab>
</Tabs>

## `POST /sip/call` – initiate SIP call

| Field                  | Type   | Required | Description                                                      |
| ---------------------- | ------ | -------- | ---------------------------------------------------------------- |
| `room_name`            | string | Yes      | The LiveKit room to connect the call to                          |
| `participant_name`     | string | Yes      | Display name for the SIP participant in the room                 |
| `participant_identity` | string | Yes      | Unique identity for the SIP participant                          |
| `from_phone_number`    | string | Yes      | Caller ID phone number (must be configured in your SIP provider) |
| `to_phone_number`      | string | Yes      | Destination phone number to dial                                 |
| `sip`                  | object | No       | Per-request SIP configuration overrides (see below)              |

The optional `sip` object allows overriding global SIP configuration on a per-request basis. This is useful when you need to use different SIP providers or credentials for specific calls.

| Field                  | Type         | Description                                                        |
| ---------------------- | ------------ | ------------------------------------------------------------------ |
| `sip.outbound_address` | string\|null | SIP server address override. Format: `hostname` or `hostname:port` |
| `sip.auth_username`    | string\|null | SIP authentication username override                               |
| `sip.auth_password`    | string\|null | SIP authentication password override                               |

* **Request**: JSON body plus optional auth via `Authorization` header or `api_key` query parameter.
* **Response**: `200 OK` with call status, room name, participant identity, participant ID, and SIP call ID.
* **Errors**: `400` when phone number is invalid or required fields are empty, `404` when room exists with a different tenant's `auth_id`, `500` when LiveKit is not configured, outbound address is missing, or call fails.

Phone numbers support international format (`+1234567890`), national format (`07123456789`), or internal extensions (`1234`).

See the [API reference](/api-reference/endpoint/sip-call) for detailed schema information.

<Tabs>
  <Tab title="curl">
    ```bash theme={null}
    # Basic call (uses global SIP configuration)
    curl -X POST https://api.sayna.ai/sip/call \
      -H "Content-Type: application/json" \
      -d '{
            "room_name": "call-room-123",
            "participant_name": "John Doe",
            "participant_identity": "caller-456",
            "from_phone_number": "+15105550123",
            "to_phone_number": "+15551234567"
          }'
    ```

    ```bash theme={null}
    # With per-request SIP configuration overrides
    curl -X POST https://api.sayna.ai/sip/call \
      -H "Content-Type: application/json" \
      -d '{
            "room_name": "call-room-123",
            "participant_name": "John Doe",
            "participant_identity": "caller-456",
            "from_phone_number": "+15105550123",
            "to_phone_number": "+15551234567",
            "sip": {
              "outbound_address": "sip.provider.com:5060",
              "auth_username": "user123",
              "auth_password": "secret456"
            }
          }'
    ```
  </Tab>

  <Tab title="Node SDK">
    <Note>
      The `sipCall` method is available in SDK version 0.3.0 and later.
    </Note>

    ```ts theme={null}
    const result = await client.sipCall({
      roomName: "call-room-123",
      participantName: "John Doe",
      participantIdentity: "caller-456",
      fromPhoneNumber: "+15105550123",
      toPhoneNumber: "+15551234567",
    });

    console.log(result.sipCallId, result.participantId);
    ```

    ```ts theme={null}
    // With per-request SIP configuration overrides
    const result = await client.sipCall({
      roomName: "call-room-123",
      participantName: "John Doe",
      participantIdentity: "caller-456",
      fromPhoneNumber: "+15105550123",
      toPhoneNumber: "+15551234567",
      sip: {
        outboundAddress: "sip.provider.com:5060",
        authUsername: "user123",
        authPassword: "secret456",
      },
    });
    ```
  </Tab>

  <Tab title="Python SDK">
    <Note>
      The `sip_call` method is available in SDK version 0.3.0 and later.
    </Note>

    ```python theme={null}
    result = await client.sip_call(
        room_name="call-room-123",
        participant_name="John Doe",
        participant_identity="caller-456",
        from_phone_number="+15105550123",
        to_phone_number="+15551234567",
    )

    print(result.sip_call_id, result.participant_id)
    ```

    ```python theme={null}
    # With per-request SIP configuration overrides
    result = await client.sip_call(
        room_name="call-room-123",
        participant_name="John Doe",
        participant_identity="caller-456",
        from_phone_number="+15105550123",
        to_phone_number="+15551234567",
        sip={
            "outbound_address": "sip.provider.com:5060",
            "auth_username": "user123",
            "auth_password": "secret456",
        },
    )
    ```
  </Tab>
</Tabs>

## Cache-aware behavior

* `/speak` reuses cached synthesis when both the `text` and `tts_config` hash match a previous request and caching is enabled.
* Cache assets live under `CACHE_PATH`; mount a persistent volume in production if you want to avoid cold starts.
* Clear server caches when rotating provider credentials or when you change voice defaults that affect TTS hashes.
