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

# Telnyx Voice

> Capture Telnyx Voice STT and TTS activity in Agnost

Telnyx Voice uses Agnost's voice webhook service. No Agnost SDK import is required.

## Data Sources

| Telnyx data             | Who has it                          | Agnost capture                           |
| ----------------------- | ----------------------------------- | ---------------------------------------- |
| In-call transcription   | Telnyx webhook `call.transcription` | Forward the webhook                      |
| REST/WebSocket STT text | Your app receives the transcript    | Post `kind: "stt"`                       |
| TTS / speak text        | Your app sends the text to Telnyx   | Post `kind: "tts"` after Telnyx succeeds |

## Minimal Server Integration

Keep Telnyx pointed at your app. Add one small forwarder for final STT and one for TTS:

```text theme={null}
POST https://<agnost-voice-host>/api/voice/v1/events/telnyx
X-Org-Id: <agnost-org-id>
```

Forward final transcription events:

```json theme={null}
{
  "kind": "stt",
  "data": {
    "event_type": "call.transcription",
    "payload": {
      "call_session_id": "call-session-id",
      "from": "+15551234567",
      "transcription_data": {
        "is_final": true,
        "transcript": "hello from Telnyx"
      }
    }
  }
}
```

Record TTS where your app already calls Telnyx speak/playback:

```json theme={null}
{
  "kind": "tts",
  "text": "Sure, I can help with billing.",
  "session_id": "call-session-id",
  "user_id": "caller-id",
  "mode": "in_call_playback"
}
```

## Copy-Paste Middleware

Use one server-side helper. STT can run inside the incoming Telnyx webhook handler.
TTS must run beside the existing Telnyx speak/playback call because that is where your backend has the bot text.

```ts theme={null}
const agnostVoiceUrl = process.env.AGNOST_VOICE_URL;
const agnostOrgId = process.env.AGNOST_ORG_ID;

async function sendAgnostTelnyx(body: Record<string, unknown>) {
  await fetch(`${agnostVoiceUrl}/api/voice/v1/events/telnyx`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Org-Id": agnostOrgId!,
    },
    body: JSON.stringify(body),
  });
}
```

Forward final STT from your Telnyx webhook:

```ts theme={null}
app.post("/webhooks/telnyx", async (req, res) => {
  const payload = req.body;
  const transcript = payload?.data?.payload?.transcription_data?.transcript;
  const isFinal = payload?.data?.payload?.transcription_data?.is_final !== false;
  const sessionId = payload?.data?.payload?.call_session_id;

  if (payload?.data?.event_type === "call.transcription" && isFinal && transcript) {
    await sendAgnostTelnyx({
      kind: "stt",
      session_id: sessionId,
      event_id: payload.data.id,
      user_id: payload.data.payload.from,
      agent_name: "telnyx_voice",
      text: transcript,
    });
  }

  res.sendStatus(200);
});
```

Forward TTS where your product already speaks to the caller:

```ts theme={null}
const replyText = await agentReply(userText);

await telnyx.calls.actions.speak(callControlId, { payload: replyText, voice: "female" });
await sendAgnostTelnyx({
  kind: "tts",
  session_id: callSessionId,
  event_id: crypto.randomUUID(),
  user_id: callerId,
  agent_name: "telnyx_voice",
  text: replyText,
});
```

## Telnyx SDK Users

Using the Telnyx Node or Python SDK does not replace the STT webhook. Keep `webhook_url`
configured on the call so final `call.transcription` events still reach your backend.
Then wrap the SDK speak call to record TTS.

Node.js:

```ts theme={null}
import Telnyx from "telnyx";
import crypto from "node:crypto";

const telnyx = new Telnyx({ apiKey: process.env.TELNYX_API_KEY });

async function speakWithAgnost({
  callControlId,
  callSessionId,
  callerId,
  text,
}: {
  callControlId: string;
  callSessionId: string;
  callerId: string;
  text: string;
}) {
  const eventId = crypto.randomUUID();

  await telnyx.calls.actions.speak(callControlId, {
    payload: text,
    voice: "female",
    command_id: eventId,
  });

  await sendAgnostTelnyx({
    kind: "tts",
    session_id: callSessionId,
    event_id: eventId,
    user_id: callerId,
    agent_name: "telnyx_voice",
    text,
  });
}
```

Python:

```python theme={null}
import os
import uuid
import json
import urllib.request

from telnyx import Telnyx

telnyx = Telnyx(api_key=os.environ.get("TELNYX_API_KEY"))


def send_agnost_telnyx(body: dict) -> None:
    request = urllib.request.Request(
        f"{os.environ['AGNOST_VOICE_URL']}/api/voice/v1/events/telnyx",
        data=json.dumps(body).encode("utf-8"),
        headers={
            "Content-Type": "application/json",
            "X-Org-Id": os.environ["AGNOST_ORG_ID"],
        },
        method="POST",
    )
    with urllib.request.urlopen(request, timeout=5):
        pass


def speak_with_agnost(call_control_id: str, call_session_id: str, caller_id: str, text: str) -> None:
    event_id = str(uuid.uuid4())

    telnyx.calls.actions.speak(
        call_control_id=call_control_id,
        payload=text,
        voice="female",
        command_id=event_id,
    )

    send_agnost_telnyx({
        "kind": "tts",
        "session_id": call_session_id,
        "event_id": event_id,
        "user_id": caller_id,
        "agent_name": "telnyx_voice",
        "text": text,
    })
```

Use the same `session_id` for STT and TTS events from the same call/session.
It can be the Telnyx `conversation_id`, `call_session_id`, or `call_control_id`; Agnost turns non-UUID Telnyx IDs into stable dashboard UUIDs and keeps the original ID in metadata.
Pass a stable `event_id` when your middleware can retry the same STT/TTS event.

Agnost's voice service stores final STT in memory for up to 10 minutes until the next TTS event for the same `session_id`.
That TTS write becomes one dashboard event with user speech in `args` and bot speech in `result`.
If a TTS event arrives without a queued STT, or after that queue expires, Agnost records it as output-only.

## Troubleshooting

* **Only bot messages appear**: TTS is arriving before final STT, more than 10 minutes after STT, or with a different `session_id`.
* **No user input appears**: you are forwarding partial STT only. Agnost ignores STT where `final` or `is_final` is `false`.
* **TTS appears in a separate conversation**: pass the same Telnyx conversation or call ID as `session_id`.
* **The accepted `session_id` looks different from the Telnyx ID**: use the returned `conversation_id` in dashboard/debug links. The original Telnyx ID is stored as metadata.
* **No events appear**: confirm `X-Org-Id` is set and your server can reach the Agnost voice service.
* **Missing bot output**: you are forwarding STT only. Post the TTS text after the Telnyx speak/playback request succeeds.

## Telnyx References

* [Telnyx Voice API webhooks](https://developers.telnyx.com/docs/voice/programmable-voice/voice-api-webhooks)
* [Telnyx TTS overview](https://developers.telnyx.com/docs/voice/tts/overview)
* [Telnyx STT models](https://developers.telnyx.com/docs/voice/stt/models)
* [Telnyx Node.js SDK](https://developers.telnyx.com/development/sdk/node/index)
* [Telnyx Python SDK](https://developers.telnyx.com/development/sdk/python)
