# Voice Agents > A voice agent that answers phone calls, checks your availability, and books meetings for you -- 24/7. Powered by ElevenLabs Conversational AI and Twilio Voice. > Source: https://linktime.io/docs/ai/voice > Last updated: February 2026 ## Overview Voice Agents add a conversational AI assistant to your LinkTime account. The assistant can: - Answer inbound phone calls and greet callers by name - List your available meeting types - Check real-time availability on your calendar - Book meetings, create contacts, and send confirmations - Make outbound calls for reminders, follow-ups, and confirmations (via Workflows) - Work via a browser-based web widget (free tier) or a dedicated US phone number (Pro+) ## Multi-Agent As of v2.31.0, you can create **multiple voice agents**, each with its own personality, phone number, and configuration. This is ideal for businesses that need separate agents for different locations, departments, or languages. ### Agent limits by plan | Plan | Max Agents | |------|------------| | Free | 1 | | Pro | 2 | | Business | 20 | **Shared minutes pool:** Your monthly voice minutes are shared across all agents. If you have 60 minutes on the Pro plan, those 60 minutes are consumed by calls to any of your agents. **Per-agent configuration:** Each agent can have its own name, voice, greeting, business context, language, linked event types, and active hours schedule. This lets you tailor each agent to a specific role or audience. ## Active Hours Set business hours for each agent so it only answers calls during the times you choose. Outside of active hours, callers hear an after-hours response instead. ### How it works - **7-day schedule:** Configure start and end times for each day of the week (e.g., Mon-Fri 9 AM - 6 PM, Sat 10 AM - 2 PM, Sun off). - **Timezone-aware:** Active hours are evaluated in the agent's configured timezone, so your schedule works correctly regardless of the caller's location. - **After-hours behavior:** Choose what happens outside active hours: play a voicemail greeting, deliver a custom message, or redirect the caller to your online booking page. > **Default (24/7):** If no active hours are configured, the agent operates around the clock. You can enable active hours at any time from the agent settings page. ## Linked Event Types Control which of your event types each agent is allowed to book. This gives you fine-grained control over what each agent offers to callers. - **Default:** all event types. When no specific event types are linked, the agent can book any of your active, non-secret event types. - **Restricted:** link specific event types to limit what the agent offers. For example, a "Sales" agent might only book "Discovery Call" and "Demo", while a "Support" agent only books "Technical Consultation". > **Multi-location tip:** Combine linked event types with multi-agent to create location-specific booking flows. Each location's agent only shows the event types available at that site. ## Use Cases Here are three common patterns for multi-agent setups. ### Multi-Location Business A clinic, salon, or service business with multiple physical locations. Each location gets its own agent with a dedicated phone number. - **Agent "Downtown Office"** answers calls for +1 (212) 555-0100, books only downtown event types, open Mon-Fri 8 AM - 6 PM. - **Agent "Midtown Office"** answers calls for +1 (212) 555-0200, books only midtown event types, open Mon-Sat 9 AM - 7 PM. ### Sales + Support Split Route inbound calls to the right AI agent depending on the caller's intent. - **Agent "Sales"** qualifies leads, books "Discovery Call" and "Product Demo" event types. - **Agent "Support"** answers technical questions and books "Support Session" and "Troubleshooting Call" event types. ### Multilingual Service Serve callers in their preferred language with agents configured for different languages, all sharing the same pool of event types. - **Agent "English"** (language: en) -- primary number, answers in English. - **Agent "Espanol"** (language: es) -- separate number, answers in Spanish. - Both agents can book the same event types. Minutes are deducted from the same shared pool. ## Architecture LinkTime uses [ElevenLabs Conversational AI](https://elevenlabs.io/docs/conversational-ai/overview) for speech-to-text, LLM reasoning, and text-to-speech. [Twilio](https://www.twilio.com) provides phone number provisioning and telephony. ElevenLabs natively bridges the two -- no custom TwiML or WebSocket code needed. ### Inbound Call Flow ``` Caller dials your Twilio number -> Twilio routes to ElevenLabs (auto-configured) -> ElevenLabs calls /api/voice/webhook/personalization for greeting + context -> Agent converses with caller, calling tool webhooks as needed: -> /api/voice/tools/event-types (list meeting types) -> /api/voice/tools/available-slots (check calendar) -> /api/voice/tools/create-booking (book meeting) -> Call ends -> ElevenLabs calls /api/voice/webhook/call-status with transcript + duration ``` ### Outbound Call Flow ``` Workflow trigger (e.g., 1h before meeting) -> LinkTime calls ElevenLabs outbound API -> Twilio dials the callee -> Agent converses (reminder, follow-up, etc.) -> Post-call webhook updates CallLog ``` ### Web Widget Flow (free tier) ``` Visitor clicks "Talk to AI Assistant" on your booking page -> Browser mic -> WebRTC -> ElevenLabs -> Same agent, same tools, same booking flow -> Zero Twilio cost (no phone number needed) ``` ## Plan Limits | Plan | Monthly Minutes | Phone Number | Web Widget | |------|----------------|--------------|------------| | Free | 5 minutes | No | Yes | | Pro | 60 minutes | Yes (dedicated number) | Yes | | Organization | Unlimited | Yes (shared org number) | Yes | > **How minute enforcement works:** Before each inbound call, the personalization webhook checks remaining minutes. If the user has <1 minute left, the agent greets the caller with an "unavailable" message and directs them to the online booking URL. Calls are never cut off mid-conversation -- small overages are allowed. After each call, if the user exceeds their limit, the agent is auto-deactivated until the next billing period. ## International Phone Provisioning (v2.45.0) Phone numbers can be provisioned in 14 countries. When creating a voice agent or adding a phone number, choose your preferred country from the country selector in the settings UI. Prices are approximate monthly costs charged by Twilio. | Country | Dial Code | Approx. Cost | SMS Inbound | |---------|-----------|-------------|-------------| | United States | +1 | ~$1/mo | Yes | | Canada | +1 | ~$1/mo | Yes | | United Kingdom | +44 | ~$1/mo | Yes | | France | +33 | ~$1/mo | Yes | | Germany | +49 | ~$1/mo | Yes | | Spain | +34 | ~$1/mo | Yes | | Netherlands | +31 | ~$1/mo | Yes | | Italy | +39 | ~$1/mo | Yes | | Australia | +61 | ~$2/mo | Yes | | Brazil | +55 | ~$3/mo | Yes | | Mexico | +52 | ~$2/mo | Yes | | Japan | +81 | ~$5/mo | Yes | | Singapore | +65 | ~$3/mo | Yes | | UAE | +971 | ~$5/mo | Yes | > **Country data source:** The full list with ISO codes, flags, and SMS capabilities is defined in `src/lib/countries.ts` (`PHONE_COUNTRIES` array). The `getCountryFlag()` helper maps E.164 phone numbers to country flag emojis for display. ### How country selection works 1. In the Voice Agents settings or Phone Numbers dashboard, a country dropdown shows all 14 supported countries with flag emojis. 2. When provisioning, LinkTime searches for a local number in the selected country first, then falls back to a toll-free number if no local numbers are available. 3. The provisioned number is automatically imported into ElevenLabs and bound to your agent. ## Setup Guide 1. **Navigate to Voice Agents** - Go to Dashboard -> Voice Agents in your LinkTime dashboard. 2. **Choose a template** - This triggers a multi-step provisioning process: (1) creates an ElevenLabs conversational agent with your booking tools, (2) purchases a phone number from Twilio in your selected country (Pro+ only), (3) imports the number into ElevenLabs so it auto-answers calls. Takes about 5 seconds. 3. **Your agent is live** - You'll see your agent status (Active), phone number, and usage bar. Callers can now call the number and the AI will answer, check your calendar, and book meetings. 4. **Customize (optional)** - Go to Voice Agents -> Settings to change the agent name, custom greeting, voice, business context, and language. ## Agent API (CRUD) Manage your voice agents programmatically. All endpoints require Clerk session authentication. > **v2.31.0 -- Multi-agent endpoints:** The new plural endpoints (`/api/voice/agents`) support creating and managing multiple agents. The legacy singular endpoint (`/api/voice/agent`) continues to work for backwards compatibility and operates on your first (default) agent. ### GET /api/voice/agents Returns all voice agents for the current user, along with shared usage stats and plan info. **Response Example:** ```json { "agents": [ { "id": "cm_abc...", "agentName": "Sales Assistant", "voiceId": "21m00Tcm4TlvDq8ikWAM", "greeting": null, "businessContext": "We sell enterprise SaaS.", "language": "en", "phoneNumber": "+19143593794", "isActive": true, "elevenlabsAgentId": "abc123...", "createdAt": "2026-02-08T15:00:00.000Z" }, { "id": "cm_def...", "agentName": "Soporte Tecnico", "language": "es", "phoneNumber": "+19143593800", "isActive": true, "elevenlabsAgentId": "def456...", "createdAt": "2026-02-10T10:00:00.000Z" } ], "usage": { "minutesUsed": 12, "minuteLimit": 60, "remaining": 48 }, "plan": { "name": "PRO", "hasPhoneNumber": true, "maxMinutes": 60, "maxAgents": 2 } } ``` ### POST /api/voice/agents Create a new voice agent. Provisions an ElevenLabs conversational agent and (for Pro+ users) a dedicated Twilio phone number. Returns `403` if the user has reached their plan's agent limit. **Request Body:** | Field | Type | Default | Description | |-------|------|---------|-------------| | `agentName` | string | "Scheduling Assistant" | Name used in greetings (1-100 chars) | | `greeting` | string? | Auto-generated | Custom first message (max 500 chars) | | `businessContext` | string? | null | Describe your business so the agent can answer questions (max 5000 chars) | | `voiceId` | string? | Rachel (default) | ElevenLabs voice ID | | `language` | string? | "en" | Agent language code | **Example (curl):** ```bash curl -X POST https://linktime.io/api/voice/agents \ -H "Content-Type: application/json" \ -H "Cookie: __session=" \ -d '{ "agentName": "Sarah", "greeting": "Hi! I'\''m Sarah, Emmanuel'\''s scheduling assistant.", "businessContext": "I run a design consultancy specializing in UX.", "language": "en" }' ``` **Example (JavaScript):** ```javascript const response = await fetch("/api/voice/agents", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ agentName: "Sarah", greeting: "Hi! I'm Sarah, Emmanuel's scheduling assistant.", businessContext: "I run a design consultancy specializing in UX.", language: "en", }), }); const { agent } = await response.json(); console.log(agent.phoneNumber); // "+19143593794" ``` > **Provisioning rollback:** If any step fails (agent creation, number purchase, or ElevenLabs import), all previous steps are rolled back automatically. You won't be charged for a Twilio number if the ElevenLabs import fails. ### GET / PATCH / DELETE /api/voice/agents/[id] Read, update, or delete a specific agent by its ID. - **GET** -- Returns the agent's full configuration, including linked event types and active hours. - **PATCH** -- Update agent settings (name, voice, greeting, businessContext, language). Changes sync to ElevenLabs automatically. - **DELETE** -- Deactivate the agent. Its phone number enters a **30-day hold period** before release. **Example (update agent):** ```bash curl -X PATCH https://linktime.io/api/voice/agents/cm_abc... \ -H "Content-Type: application/json" \ -H "Cookie: __session=" \ -d '{ "agentName": "Alex", "businessContext": "We are a law firm specializing in IP law." }' ``` ### PUT /api/voice/agents/[id]/event-types Link specific event types to this agent. Send an array of event type IDs. Send an empty array to reset to "all event types" (the default). **Request Body:** ```json { "eventTypeIds": ["cm_evt1...", "cm_evt2..."] } ``` ### PATCH /api/voice/agents/[id]/active-hours Configure the agent's active hours schedule. Pass `enabled: false` to disable active hours (agent operates 24/7). **Request Body:** ```json { "enabled": true, "timezone": "America/New_York", "afterHoursBehavior": "voicemail", "schedule": [ { "day": "monday", "start": "09:00", "end": "18:00" }, { "day": "tuesday", "start": "09:00", "end": "18:00" }, { "day": "wednesday", "start": "09:00", "end": "18:00" }, { "day": "thursday", "start": "09:00", "end": "18:00" }, { "day": "friday", "start": "09:00", "end": "17:00" }, { "day": "saturday", "start": "10:00", "end": "14:00" } ] } ``` ### POST / DELETE /api/voice/agents/[id]/phone Manage phone numbers for a specific agent. - **POST** -- Provision a new Twilio phone number and bind it to this agent (Pro+ plan required). - **DELETE** -- Release the agent's phone number. The number enters a 30-day hold before being recycled. ## Usage API ### GET /api/voice/usage Returns voice calling usage for the current billing period. Requires Clerk session auth. **Response Example:** ```json { "minutesUsed": 12, "minuteLimit": 60, "remaining": 48, "callsThisMonth": 8, "avgDurationSeconds": 90, "topOutcomes": [ { "outcome": "booking_created", "count": 5 }, { "outcome": "info_provided", "count": 2 }, { "outcome": "no_action", "count": 1 } ] } ``` ## Tool Webhooks (mid-call) These endpoints are called by the ElevenLabs agent **during a live conversation** to access your calendar and booking system. They are authenticated with a shared secret in the `X-Voice-Secret` header. > **Note:** You don't call these endpoints directly. They are configured automatically when your agent is created and are invoked by ElevenLabs during phone/web conversations. ### POST /api/voice/tools/event-types **Tool:** list_event_types Returns the user's active, non-secret event types. For organization agents, returns all members' event types with member names. **Request:** ```json { "user_id": "usr_abc123" } ``` **Response:** ```json { "event_types": [ { "slug": "30-min", "name": "30 Minute Meeting", "duration_minutes": 30, "description": "Quick chat" }, { "slug": "consultation", "name": "Consultation", "duration_minutes": 60, "description": "In-depth session" } ] } ``` ### POST /api/voice/tools/available-slots **Tool:** get_available_slots Checks real-time calendar availability. Wraps the core availability engine -- accounts for existing bookings, calendar events, availability rules, and overrides. **Request:** ```json { "user_id": "usr_abc123", "event_type_slug": "30-min", "date": "2026-02-10", "timezone": "America/New_York" } ``` **Response:** ```json { "slots": [ { "time": "9:00 AM", "start_time": "2026-02-10T14:00:00Z" }, { "time": "10:30 AM", "start_time": "2026-02-10T15:30:00Z" }, { "time": "2:00 PM", "start_time": "2026-02-10T19:00:00Z" } ], "date": "2026-02-10" } ``` ### POST /api/voice/tools/create-booking **Tool:** create_booking Creates a confirmed booking, creates/updates the contact, and returns a confirmation message the agent speaks to the caller. **Request:** ```json { "user_id": "usr_abc123", "event_type_slug": "30-min", "invitee_name": "John Doe", "invitee_email": "john@example.com", "start_time": "2026-02-10T14:00:00Z", "timezone": "America/New_York", "notes": "Wants to discuss Q3 plan", "invitee_phone": "+14155551234" } ``` **Response:** ```json { "success": true, "booking": { "id": "cm...", "event_name": "30 Minute Meeting", "date": "Monday, February 10, 2026", "time": "9:00 AM EST", "confirmation_message": "Your 30 Minute Meeting with Emmanuel is booked for Monday, February 10 at 9:00 AM EST. A confirmation email is on its way to john@example.com." } } ``` ### POST /api/voice/tools/booking-details **Tool:** get_booking_details Retrieves booking details for reminder and follow-up calls. Used by outbound workflow calls. **Request:** ```json { "booking_id": "cm..." } ``` **Response:** ```json { "booking": { "id": "cm...", "event_name": "30 Minute Meeting", "invitee_name": "John Doe", "invitee_email": "john@example.com", "date": "Monday, February 10, 2026", "time": "9:00 AM EST", "status": "CONFIRMED", "meeting_link": "https://meet.google.com/...", "notes": "Wants to discuss Q3 plan" } } ``` ### Error responses All tool endpoints return user-friendly error messages that the agent can speak naturally to the caller: ```json { "error": true, "message": "I'm having trouble accessing the calendar right now. Can I take your number and have someone call you back?" } ``` ## Lifecycle Webhooks These endpoints handle the start and end of each call. They are called by ElevenLabs automatically. ### POST /api/voice/webhook/personalization Called when an inbound call arrives. Looks up the voice agent by the called phone number, checks plan limits, creates a CallLog entry, and returns dynamic variables for the agent greeting. **ElevenLabs sends:** ```json { "from": "+14155551234", "to": "+19143593794", "conversation_id": "conv_abc...", "call_sid": "CA..." } ``` **LinkTime responds (normal):** ```json { "type": "conversation_initiation_client_data", "dynamic_variables": { "host_name": "Emmanuel", "business_context": "I run a design consultancy", "booking_url": "https://linktime.io/emmanuel", "host_email": "emmanuel@example.com", "host_timezone": "America/New_York", "agent_name": "Scheduling Assistant" }, "conversation_config_override": { "agent": { "first_message": "Hi! I'm Emmanuel's scheduling assistant. How can I help you today?" } } } ``` **LinkTime responds (over minute limit):** ```json { "type": "conversation_initiation_client_data", "conversation_config_override": { "agent": { "first_message": "I'm sorry, Emmanuel's scheduling assistant is currently unavailable. Please visit linktime.io/emmanuel to book a meeting online.", "prompt": { "prompt": "Apologize that you're unavailable and direct the caller to the booking URL. End the call politely." } } } } ``` ### POST /api/voice/webhook/call-status Called when a call ends. Authenticated via HMAC-SHA256 signature in the `elevenlabs-signature` header. **Payload from ElevenLabs:** ```json { "conversation_id": "conv_abc...", "status": "completed", "transcript": "Agent: Hi! I'm Emmanuel's assistant...\nCaller: I'd like to book a meeting...", "duration_secs": 142, "tool_calls": [ { "name": "list_event_types", "result": "..." }, { "name": "get_available_slots", "result": "..." }, { "name": "create_booking", "result": "..." } ] } ``` **What happens:** Updates the CallLog with duration, transcript, and cost estimate ($0.10/min). Determines the call outcome from tool calls (`booking_created`, `info_provided`, `no_action`). If the user is now over their monthly limit, the agent is auto-deactivated. ## Web Widget (free tier) Free-tier users (and all users) get a browser-based voice widget on their public booking page. It uses [@elevenlabs/react](https://www.npmjs.com/package/@elevenlabs/react) with WebRTC -- no phone number or Twilio needed. ### How it works 1. When your voice agent is active, a "Talk to AI Assistant" button appears on your booking page (`linktime.io/your-username`). 2. The visitor clicks the button -> browser requests microphone permission. 3. A WebRTC connection is established directly with ElevenLabs. The AI agent greets the visitor and can check availability, book meetings, etc. 4. The visitor clicks "End Call" when done. A CallLog is created via the post-call webhook. ### Embedding on external sites The widget is built into your LinkTime booking page automatically. To add voice to an external site, use the [Embed Widget](https://linktime.io/docs/developers/embed) which includes the voice button when your agent is active. ```html ``` ## Workflow Integration (outbound calls) The `AI_CALL` workflow action lets you trigger outbound voice agent calls based on booking events. This requires an active voice agent with a phone number (Pro+ plan). ### Setting up a workflow 1. Go to Dashboard -> Workflows and create a new workflow. 2. Choose a trigger (e.g., "Before Meeting" -> 1 hour). 3. Add a "Voice Call" action. 4. Configure: who to call (invitee, host, or custom number) and call type (reminder, confirmation, follow-up, or custom). ### Call types | Type | Description | Example trigger | |------|-------------|-----------------| | `reminder` | Reminds the invitee of their upcoming meeting, offers to confirm or reschedule | 1 hour before meeting | | `confirmation` | Confirms the booking was made, reads back the details | Immediately after booking | | `follow_up` | Follows up after a meeting, offers to schedule another | 1 day after meeting | | `custom` | Custom message you define | Any trigger | ### AI_CALL action configuration ```json // Workflow action configuration (internal) { "type": "AI_CALL", "config": { "to": "invitee", // "invitee" | "host" | "custom" "customPhone": null, // Required when to = "custom" "callType": "reminder", // "reminder" | "follow_up" | "confirmation" | "custom" "customMessage": null // Optional message for the agent } } ``` > **Safety checks:** Before each outbound call, LinkTime checks: (1) the target phone hasn't opted out via STOP keyword, (2) the user hasn't exceeded their monthly minute limit, (3) the user has an active voice agent with a phone number. If any check fails, the call is skipped and logged. ## Security ### Tool webhook authentication All 4 tool endpoints verify the `X-Voice-Secret` header against `ELEVENLABS_WEBHOOK_SECRET`. This secret is configured in the ElevenLabs agent as a "Secret" and sent with every tool call. ### Post-call webhook signature The `/api/voice/webhook/call-status` endpoint verifies HMAC-SHA256 signatures using `ELEVENLABS_SIGNING_SECRET` and `crypto.timingSafeEqual` to prevent timing attacks. ### User isolation Tool webhooks include `user_id` or `organization_id` in the request body (set when the agent is created). Handlers only return that user's data -- a user's agent can never access another user's calendar or bookings. ### Outbound call restrictions Outbound calls can only be made to numbers from confirmed bookings or explicitly provided in workflow configurations. The system checks the `SmsOptOut` table before every outbound call -- if someone texts STOP to opt out of SMS, they're also excluded from AI calls. ### TCPA compliance The AI agent discloses it's an AI assistant in its greeting. Opt-out is checked before every outbound call. Inbound calls are always answered (caller initiated). The 30-day number hold ensures callers don't reach a disconnected number immediately after an agent is deactivated. ## Troubleshooting ### "Failed to create AI agent" Check that the `ELEVENLABS_API_KEY` environment variable is set and valid. The ElevenLabs account must have Conversational AI access (Creator plan or higher). ### "Failed to provision phone number" Verify Twilio credentials (`TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN`). The Twilio account must be upgraded from trial to paid for buying numbers. Check that phone number availability exists for the requested country. ### Agent doesn't answer calls Verify the agent is active in the dashboard. Check if the user has exceeded their monthly minute limit (the agent auto-deactivates). Ensure the phone number is still assigned -- after deactivation, there's a 30-day hold before release. ### Web widget shows "Microphone access required" The browser must grant microphone permission for the WebRTC voice connection. On iOS Safari, microphone access must be allowed for the domain. Check browser settings -> Site permissions -> Microphone. ### Agent says "having trouble with the scheduling system" This means a tool webhook failed. Check server logs for errors on the `/api/voice/tools/*` routes. Common causes: calendar not connected (Google/Outlook token expired), database connection issue, or the `ELEVENLABS_WEBHOOK_SECRET` doesn't match. ### Outbound workflow calls are skipped Check: (1) the user has an active voice agent, (2) the user hasn't exceeded their minute limit, (3) the target phone number hasn't opted out (STOP), (4) the invitee has a phone number on the booking. All skip reasons are logged in the workflow execution log. ## Environment Variables | Variable | Required | Description | |----------|----------|-------------| | `ELEVENLABS_API_KEY` | Yes | ElevenLabs API key ([get one here](https://elevenlabs.io/app/settings/api-keys)) | | `ELEVENLABS_WEBHOOK_SECRET` | Yes | Shared secret for tool webhook auth (set in ElevenLabs agent config) | | `ELEVENLABS_SIGNING_SECRET` | Yes | HMAC secret for post-call webhook signature verification | | `TWILIO_ACCOUNT_SID` | For phone | Twilio Account SID ([Twilio Console](https://console.twilio.com)) | | `TWILIO_AUTH_TOKEN` | For phone | Twilio Auth Token |