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

# Quickstart: Web Call

> Add a live AI voice button to your website in under 15 minutes

This guide walks you from zero to a working "Talk to AI" button on your website using Truedy's WebRTC API and the Ultravox browser SDK. By the end, a visitor clicks the button, the browser requests microphone access, and they are immediately in a live two-way voice conversation with your agent.

## Prerequisites

* A Truedy account with an active agent
* Your Truedy API key (Settings → API Keys)
* Node.js 18+ or Python 3.9+ for the server component
* A modern browser (Chrome, Edge, Firefox, or Safari 15.4+)

<Warning>
  Your Truedy API key must never appear in client-side code. All requests to `api.truedy.ai` must be made from your server. The browser only receives a short-lived `joinUrl`.
</Warning>

***

<Steps>
  <Step title="Install the Ultravox client SDK">
    The Ultravox SDK handles WebRTC negotiation and the audio session in the browser.

    ```bash theme={null}
    npm install ultravox-client
    ```

    If you are not using a bundler, you can load it from a CDN:

    ```html theme={null}
    <script type="module">
      import { UltravoxSession } from "https://esm.sh/ultravox-client";
    </script>
    ```
  </Step>

  <Step title="Create a server endpoint that issues a joinUrl">
    Your server calls the Truedy API to start a WebRTC session and returns the single-use `joinUrl` to the browser. The browser never sees your API key.

    <CodeGroup>
      ```javascript Node.js (Express) theme={null}
      import express from "express";

      const app = express();

      app.post("/api/start-call", async (req, res) => {
        try {
          const response = await fetch(
            "https://api.truedy.ai/api/public/v1/webrtc/call",
            {
              method: "POST",
              headers: {
                Authorization: `Bearer ${process.env.TRUEDY_API_KEY}`,
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                agent_id: process.env.TRUEDY_AGENT_ID,
              }),
            }
          );

          if (!response.ok) {
            const error = await response.json();
            return res.status(502).json({ error: error.message ?? "Failed to start call" });
          }

          const { join_url } = await response.json();

          // join_url is single-use — send it directly, do not cache it
          res.json({ joinUrl: join_url });
        } catch (err) {
          console.error("start-call error:", err);
          res.status(500).json({ error: "Internal server error" });
        }
      });

      app.listen(3000, () => console.log("Server running on http://localhost:3000"));
      ```

      ```python Python (Flask) theme={null}
      import os
      import requests
      from flask import Flask, jsonify

      app = Flask(__name__)

      @app.route("/api/start-call", methods=["POST"])
      def start_call():
          try:
              resp = requests.post(
                  "https://api.truedy.ai/api/public/v1/webrtc/call",
                  headers={"Authorization": f"Bearer {os.environ['TRUEDY_API_KEY']}"},
                  json={"agent_id": os.environ["TRUEDY_AGENT_ID"]},
                  timeout=10,
              )
              resp.raise_for_status()
              join_url = resp.json()["join_url"]
              return jsonify({"joinUrl": join_url})
          except requests.HTTPError as e:
              return jsonify({"error": str(e)}), 502
          except Exception as e:
              return jsonify({"error": "Internal server error"}), 500

      if __name__ == "__main__":
          app.run(port=3000)
      ```
    </CodeGroup>

    Set the required environment variables before starting your server:

    ```bash theme={null}
    export TRUEDY_API_KEY="YOUR_API_KEY"
    export TRUEDY_AGENT_ID="YOUR_AGENT_ID"
    ```
  </Step>

  <Step title="Build the browser page">
    Create a minimal `index.html`. This is a complete, self-contained example — no build step required.

    ```html index.html theme={null}
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <title>Talk to AI</title>
      <style>
        body {
          font-family: system-ui, sans-serif;
          display: flex;
          flex-direction: column;
          align-items: center;
          justify-content: center;
          min-height: 100vh;
          margin: 0;
          background: #f9fafb;
          gap: 1rem;
        }
        #talk-btn {
          padding: 0.75rem 2rem;
          font-size: 1rem;
          border-radius: 9999px;
          border: none;
          background: #6366f1;
          color: white;
          cursor: pointer;
          transition: background 0.2s;
        }
        #talk-btn:hover  { background: #4f46e5; }
        #talk-btn:disabled { background: #a5b4fc; cursor: not-allowed; }
        #status { color: #6b7280; font-size: 0.9rem; }
      </style>
    </head>
    <body>
      <button id="talk-btn">Talk to AI</button>
      <p id="status">Click to start</p>

      <script type="module">
        import { UltravoxSession } from "https://esm.sh/ultravox-client";

        const btn = document.getElementById("talk-btn");
        const status = document.getElementById("status");
        let session = null;

        btn.addEventListener("click", async () => {
          if (session) {
            // End the active call
            await session.leaveCall();
            session = null;
            btn.textContent = "Talk to AI";
            status.textContent = "Call ended";
            return;
          }

          btn.disabled = true;
          status.textContent = "Connecting...";

          try {
            // 1. Fetch a single-use joinUrl from your server
            const res = await fetch("/api/start-call", { method: "POST" });
            if (!res.ok) throw new Error("Could not start call");
            const { joinUrl } = await res.json();

            // 2. Join the call — the SDK requests microphone access automatically
            session = new UltravoxSession();

            session.addEventListener("status", (e) => {
              status.textContent = e.state;
            });

            await session.joinCall(joinUrl);

            btn.disabled = false;
            btn.textContent = "End call";
          } catch (err) {
            console.error(err);
            status.textContent = "Connection failed — please try again";
            btn.disabled = false;
          }
        });
      </script>
    </body>
    </html>
    ```
  </Step>

  <Step title="Test it">
    1. Start your server (`node index.js` or `flask run`)
    2. Open `http://localhost:3000` in your browser
    3. Click **Talk to AI**
    4. Accept the microphone permission prompt
    5. Speak — your agent will respond in real time

    <Tip>
      Open your browser's developer console to see SDK status events and any errors. The `status` event cycles through `connecting → connected → active` on a successful call.
    </Tip>
  </Step>
</Steps>

***

## Key concepts

### The joinUrl is single-use

Every call to `POST /webrtc/call` produces a unique `joinUrl` that can only be joined once. Never cache or reuse it. If a user refreshes the page, your frontend must request a new `joinUrl` from your server.

### API key stays on the server

Your Truedy API key authenticates you to `api.truedy.ai`. It must only ever exist in server-side code or environment variables — never in HTML, JavaScript files served to the browser, or version control.

### Microphone permissions

The Ultravox SDK calls `getUserMedia` internally when `joinCall` is invoked. The browser will display a native permission prompt the first time. If the user denies microphone access, `joinCall` throws an error — handle it gracefully in your UI.

***

## Next steps

<Columns>
  <Card title="Custom UI and live captions" icon="subtitles" href="/guides/webrtc-custom-widget">
    Subscribe to transcript events for real-time captions and build a fully branded call UI.
  </Card>

  <Card title="Template variables" icon="brackets-curly" href="/guides/agents-calls-overview">
    Pass dynamic data into your agent's prompt at call time using template variables.
  </Card>
</Columns>

<Columns>
  <Card title="Widget builder" icon="puzzle-piece" href="/guides/webrtc-custom-widget">
    Use Truedy's no-code widget builder to embed a pre-built voice button without writing frontend code.
  </Card>

  <Card title="Production security checklist" icon="shield-check" href="/guides/securing-webhooks">
    Review authentication, rate limiting, and webhook signature verification before going live.
  </Card>
</Columns>
