aethexDocs

Quickstart

Talk to your first voice agent in under 2 minutes.

Before you start

You'll need Python 3.10+ and a modern browser with a microphone (for step 3). The quickstart scripts import requests and flask, so install both into a virtual environment first — pick your OS below. When you run the scripts in the next steps, set the environment variables inline on macOS and Linux (AETHEX_API_KEY=ae_live_... python3 create_agent.py); on Windows, set them on their own line first with $env:AETHEX_API_KEY="ae_live_..." (PowerShell) or set AETHEX_API_KEY=ae_live_... (cmd), then run py create_agent.py. Keep this terminal open for steps 2–3; if you open a new one, re-activate with source .venv/bin/activate (macOS/Linux) or .venv\Scripts\Activate.ps1 (Windows) before running any script.

bash
# Check you have Python 3.10+
python3 --version

# Create an isolated environment, then install the two
# libraries the quickstart scripts import.
python3 -m venv .venv
source .venv/bin/activate
pip install requests flask

1. Get your API key

Sign up at developers.aethexai.com, then subscribe to a plan or buy credits from the billing dashboard: there is no free tier, and calls need credits. Create a key and copy it. Keys look like ae_live_....

2. Create your agent

The script below picks an English voice from your catalog, creates an agent that uses it, and prints the new agent ID. Save it, run it, and grab the ID from the output for step 3.

create_agent.py
python
# Run with:
#   AETHEX_API_KEY=ae_live_... python3 create_agent.py

import os
import requests

API_KEY = os.environ["AETHEX_API_KEY"]
BASE_URL = "https://api.aethexai.com/api/v1"
headers = {"X-API-Key": API_KEY, "Content-Type": "application/json"}

# Pick the first non-cloned English voice from the global catalog.
voices_resp = requests.get(f"{BASE_URL}/voices?language=english", headers=headers)
voices_resp.raise_for_status()
voices = [v for v in voices_resp.json() if not v.get("is_cloned")]
if not voices:
    raise SystemExit("No voices found. Check your API key and try again.")
voice_id = voices[0]["id"]

SYSTEM_PROMPT = """You are Maya, a customer service agent for First Capital Bank. You help
existing customers with general account questions: branch hours, fees,
how to dispute a charge, how to enroll in mobile banking, and what
documents you need for common requests.

Speak in short, clear sentences. Be warm but professional, not chatty.
When the caller's request is ambiguous, restate it before answering to
confirm you understood. If the caller sounds frustrated, slow down and
acknowledge it before moving on.

When you do not know an answer from the bank's knowledge base, say so
and offer to transfer to a human agent. Never invent a fee, policy, or
branch hour. Never ask for a full account number, social security
number, or PIN; those go through the secure online portal. Never
discuss another customer's account.

Tools you can use:
- end_call: invoke after the customer confirms they have what they
  need, or after a clear goodbye on either side.
- transfer_to_number: invoke for account-specific requests (fraud
  claim, lost card, large transfer, dispute escalation) or whenever
  the customer asks for a human.
"""

agent_resp = requests.post(f"{BASE_URL}/agents", headers=headers, json={
    "name": "First Capital Support",
    "system_prompt": SYSTEM_PROMPT,
    "first_message": "Hi, thanks for calling First Capital Bank. This is Maya. How can I help today?",
    "voice_id": voice_id,
    "language": "english",
})
agent_resp.raise_for_status()
agent = agent_resp.json()

print(f"Agent created: {agent['id']}")

3. Talk to your agent from a browser

Now that you have an agent_id, the fastest way to hear it talk is to open a WebRTC session from a browser. Two files: a tiny Flask server that proxies the signaling calls (so your API key never reaches the browser) and a single-page HTML client that connects through it.Save both files next to your create_agent.py, set AGENT_ID to the ID you just printed, run python3 server.py, and open http://localhost:5000 in a browser. Allow mic access when prompted.

Heads up
macOS: Port 5000 is used by AirPlay Receiver, which answers localhost:5000 with 403 Forbidden even though server.py says it started. If that happens, turn it off under System Settings → General → AirDrop & Handoff → AirPlay Receiver, or change port=5000 in server.py and the SERVER URL in client.html to 5001.
server.py
python
# Run with:
#   AETHEX_API_KEY=ae_live_... AGENT_ID=<from step 2> python3 server.py
#
# The browser cannot hold the API key, so the server proxies the
# two WebRTC signaling calls (/connect, /offer) to Aethex
# on its behalf.

import os
import requests
from flask import Flask, jsonify, request, send_from_directory

API_KEY = os.environ["AETHEX_API_KEY"]
AGENT_ID = os.environ["AGENT_ID"]
BASE_URL = "https://api.aethexai.com/api/v1"
HEADERS = {"X-API-Key": API_KEY, "Content-Type": "application/json"}

app = Flask(__name__)

@app.after_request
def add_cors(response):
    response.headers["Access-Control-Allow-Origin"] = "http://localhost:5000"
    response.headers["Access-Control-Allow-Methods"] = "GET, POST, PATCH, OPTIONS"
    response.headers["Access-Control-Allow-Headers"] = "Content-Type"
    return response

@app.route("/")
def index():
    return send_from_directory(os.path.dirname(__file__), "client.html")

@app.post("/session")
def start_session():
    r = requests.post(
        f"{BASE_URL}/conversation/connect",
        json={"agent_id": AGENT_ID},
        headers=HEADERS,
    )
    r.raise_for_status()
    return jsonify(r.json())  # {session_id, ice_config}

@app.post("/session/<sid>/offer")
def proxy_offer(sid):
    r = requests.post(
        f"{BASE_URL}/conversation/{sid}/offer",
        json=request.get_json(),
        headers=HEADERS,
    )
    return jsonify(r.json()), r.status_code

if __name__ == "__main__":
    app.run(port=5000)

The browser client

The HTML page is served by server.py at /. It hits /session to start a call, opens an RTCPeerConnection, sends an SDP offer, and plays the agent's audio when it arrives. Total: ~40 lines of vanilla JS, no build step.

Tip
This is the minimum viable flow. The WebRTC calls concept page walks through the production version: ICE trickle, reconnection, audio-level visualization, and the secure server-side variant where the browser never sees the agent ID.
client.html
html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Aethex AI – Quickstart</title>
  <style>
    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
      background: #f5f5f7;
      color: #1d1d1f;
      display: flex;
      align-items: center;
      justify-content: center;
      min-height: 100vh;
    }
    .card {
      background: #fff;
      border-radius: 16px;
      box-shadow: 0 2px 12px rgba(0,0,0,.08);
      padding: 2.5rem 2rem;
      width: 380px;
      text-align: center;
    }
    h1 { font-size: 1.25rem; font-weight: 600; margin-bottom: .25rem; }
    .subtitle { font-size: .85rem; color: #86868b; margin-bottom: 1.5rem; }

    .badge {
      font-size: .75rem;
      font-weight: 600;
      text-transform: uppercase;
      letter-spacing: .04em;
      padding: .25rem .75rem;
      border-radius: 999px;
      margin-bottom: 1.5rem;
    }
    .state-idle, .state-ended { background: #e8e8ed; color: #6e6e73; }
    .state-connecting         { background: #fff3cd; color: #856404; }
    .state-connected          { background: #d4edda; color: #155724; }
    .state-error              { background: #f8d7da; color: #721c24; }

    .btn {
      font-size: .95rem;
      font-weight: 500;
      border: none;
      border-radius: 10px;
      padding: .7rem 1.6rem;
      cursor: pointer;
    }
    .btn:disabled { opacity: .4; cursor: default; }
    .btn-start { background: #0071e3; color: #fff; }
    .btn-end   { background: #ff3b30; color: #fff; }

    .error-msg { margin-top: 1rem; font-size: .85rem; color: #ff3b30; }
  </style>
</head>
<body>
  <div class="card">
    <h1>Aethex Voice Agent</h1>
    <p class="subtitle">Quickstart demo</p>

    <div><span id="badge" class="badge state-idle">Idle</span></div>

    <button id="startBtn" class="btn btn-start">Start call</button>
    <button id="endBtn" class="btn btn-end" disabled>End call</button>

    <audio id="out" autoplay playsinline></audio>
    <p id="error" class="error-msg"></p>
  </div>

  <script>
    const SERVER = "http://localhost:5000"
    const badge = document.getElementById("badge")
    const startBtn = document.getElementById("startBtn")
    const endBtn = document.getElementById("endBtn")
    const audioEl = document.getElementById("out")
    const errorEl = document.getElementById("error")

    let pc = null
    let micStream = null
    let pingTimer = null

    function setState(name, label) {
      badge.className = "badge state-" + name
      badge.textContent = label || name
    }

    function stopLivenessPing() {
      if (pingTimer) {
        clearInterval(pingTimer)
        pingTimer = null
      }
    }

    function endCall(reason) {
      stopLivenessPing()
      if (micStream) { micStream.getTracks().forEach(t => t.stop()); micStream = null }
      if (pc) { pc.close(); pc = null }
      audioEl.srcObject = null
      endBtn.disabled = true
      startBtn.disabled = false
      if (reason !== "error") setState("ended", "Call ended")
    }

    startBtn.onclick = async () => {
      startBtn.disabled = true
      errorEl.textContent = ""

      try {
        setState("connecting", "Connecting...")

        const res = await fetch(`${SERVER}/session`, { method: "POST" })
        if (!res.ok) throw new Error(`Session failed (${res.status})`)
        const { session_id, ice_config } = await res.json()

        pc = new RTCPeerConnection(ice_config)
        const appChannel = pc.createDataChannel("aethex-app", { ordered: true })
        appChannel.onopen = () => {
          const sendPing = () => {
            if (appChannel.readyState === "open") appChannel.send(`ping:${Date.now()}`)
          }
          sendPing()
          pingTimer = setInterval(sendPing, 1000)
        }
        appChannel.onclose = stopLivenessPing
        appChannel.onerror = stopLivenessPing

        pc.onconnectionstatechange = () => {
          if (!pc) return
          if (pc.connectionState === "connected") setState("connected", "Connected")
          if (["failed", "closed"].includes(pc.connectionState)) endCall("ended")
        }

        pc.ontrack = (ev) => { audioEl.srcObject = ev.streams[0] }

        micStream = await navigator.mediaDevices.getUserMedia({ audio: true })
        micStream.getTracks().forEach(t => pc.addTrack(t, micStream))

        const offer = await pc.createOffer()
        await pc.setLocalDescription(offer)

        if (pc.iceGatheringState !== "complete") {
          const localPc = pc
          await new Promise((resolve, reject) => {
            const timeout = setTimeout(() => {
              localPc.onicegatheringstatechange = null
              reject(new Error("ICE gathering timed out"))
            }, 10000)
            localPc.onicegatheringstatechange = () => {
              if (localPc.iceGatheringState === "complete") {
                clearTimeout(timeout)
                localPc.onicegatheringstatechange = null
                resolve()
              }
            }
          })
        }

        const { sdp, type } = pc.localDescription
        const answerRes = await fetch(`${SERVER}/session/${session_id}/offer`, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ sdp, type }),
        })
        if (!answerRes.ok) throw new Error(`Offer failed (${answerRes.status})`)
        const answer = await answerRes.json()
        await pc.setRemoteDescription({ sdp: answer.sdp, type: "answer" })

        endBtn.disabled = false
      } catch (err) {
        errorEl.textContent = err.message
        setState("error", "Error")
        endCall("error")
      }
    }

    endBtn.onclick = () => endCall("ended")
  </script>
</body>
</html>

Next steps

Now that you have a working agent, here are the most common things to add next.

  1. Dial real phone numbers

    Bring a number one of three ways: connect a managed Twilio number, bring your own carrier/PBX over SIP, or connect Telnyx and we provision the trunk for you. Twilio and SIP numbers register in one step; Telnyx numbers are registered during provisioning. Trigger outbound calls from any registered line — BYO SIP lines additionally need an outbound trunk from /sip-trunks/onboard; a number attached to an inbound-only trunk cannot dial out. Walkthrough: Phone Numbers.

    python
    # After registering a number on the dashboard:
    call_resp = requests.post(f"{BASE_URL}/calls/trigger", headers=headers, json={
        "agent_id": agent["id"],
        "to_number": "+14155551234",   # the phone you want to dial
        "from_number": "+18885551234", # your registered Aethex number
        "dynamic_variables": {         # overrides agent defaults; required only for placeholders with no default
            "customer_name": "Alice",
        },
    })
    call_resp.raise_for_status()
    print(f"Call started: {call_resp.json()['id']}")
  2. Pick a different voice

    Browse GET /voices to see the catalog, then set voice_id on your agent. Voices vary by language and style.

  3. Add tools and knowledge

    Wire function-calling tools so the agent can look up data or trigger actions, and upload knowledge-base documents (PDF/DOCX/TXT/MD) for retrieval-augmented answers.

  4. Stream voice into your own app

    Use the WebRTC API directly to embed the agent in a browser or mobile app you control, with full audio in/out.