# Access Control
Source: https://docs.retellai.com/accounts/access-control
Manage your Retell workspace with role-based access control: invite members, assign Admin, Developer, Analyst, or Viewer roles, and control permissions.
To keep your workspace safe, we have an RBAC (Role-Based Access Control) system.
### System Roles
#### Admin
Full control over workspace resources and members. Complete access to all features including billing, user management, and workspace settings.
Can:
* Invite, remove, and change roles for members
* View and manage billing: usage, invoices, payment methods, subscriptions, and customer portal
* Create, edit, and delete agents, conversation flows, Retell LLMs, knowledge bases, voices, and folders
* Configure developer settings
* Access all data, including raw transcripts and recordings
* Manage telephony settings
* Update or delete the workspace
Cannot:
* None — Admins have full permissions
#### Developer
Full functional access to build and test agents, view raw data, manage analytics and developer settings. Cannot manage billing or organization users.
Can:
* Build and edit agents, flows, LLMs, KBs, voices
* Test and simulate; manage test cases and playground
* View raw logs, transcripts, recordings, and analytics
* Create exports; run calls and batch calls
* Manage API/public keys and webhooks; adjust concurrency/CPS
Cannot:
* Manage billing (usage, invoices, payment methods, subscriptions)
* Invite, remove, or change member roles
* Update or delete the workspace
#### Member
Read-only access to agents, testing artifacts, scrubbed history, and analytics. Cannot make changes or view sensitive data.
Can:
* View agents and configurations (read‑only)
* View tests, playground threads, and analytics
* View scrubbed history; batch calls and phone resources
* List workspace members
Cannot:
* Create, edit, or delete resources
* Start calls/chats, run simulations, or change playground
* Access API/public keys, webhooks, or raw transcripts/recordings
* Manage billing, settings, or team members
### Invite User
Now under the user management of your workspace, you (admin) can invite a user with a specific role.
### Change User Role
You can change the role of active users in the workspace.
### Remove User
You can also remove a user from your workspace.
# Manage your Retell AI account
Source: https://docs.retellai.com/accounts/account
Check account status, reset your password, change your email address, switch to Google SSO, stop charges, and delete your Retell AI account.
To keep your information safe, we use **Auth0** as our login system.
## Account Status
Your account could have the following statuses:
* **Active**: Your account is active and all services are available.
* **Verification Needed**: Your account is on hold for further verification due to high-volume calls. Please contact us via the Customer Support Portal or [support@retellai.com](mailto:support@retellai.com). Raise a ticket with your company's name, use case, and verification that you represent the company.
* **Invoice Past Due**: Your account has a past due invoice and is at risk of automatic shutdown in 7 days. Please make a payment to keep your service active.
* **Invoice Overdue**: Your account has an overdue balance, and your service has been temporarily deactivated. Please make a payment to restore your service.
## Reset your password
If you want to change or reset your password:
1. Go to the login page.
2. Click the **"Don’t remember your password?"** link.
3. Enter your email address and follow the instructions sent to your inbox.
For better security, we recommend using **Sign in with Google**. This provides an extra layer of protection and reduces the need to manage separate passwords.
### Important Note About SSO and Password-Based Sign-In
Please note that **Google SSO** and **email-password sign-in** are treated as **separate accounts** in our system. They are not automatically linked.
If you currently use one sign-in method (e.g., **email-password sign-in**) and wish to switch to the other (e.g., **Google SSO**), follow these steps:
1. Log in to your account using your current method (either Google SSO or email-password sign-in).
2. Click your profile photo at the bottom left of the dashboard.
3. Navigate to **"Workspace"** and send an invite to your email address.
4. Log out of your current account.
5. Open the invite email and use the **alternative sign-in method** to create a new account (e.g., sign in with Google if you currently use email, or sign up with email and password if you currently use Google SSO).
This will create a separate account with the new sign-in method while maintaining access to your existing workspace.
## Change your email address
You can change the email on your account after confirming the new address. Retell doesn't switch your email right away — it emails a verification link to the new address, and the change takes effect only once you open that link.
Enter your new email address in your account settings. Retell sends a verification link to that address.
Open the verification link sent to the new email. Your account email updates only after you confirm — until then, your current email stays active.
While a change is pending, you can cancel it any time before confirming. Canceling discards the new address and leaves your current email and its verified status unchanged.
Email changes are available only for accounts that sign in with an email and password. If you sign in with Google SSO or another social login, your email is managed by that provider — update it there, or see [switching sign-in methods](#important-note-about-sso-and-password-based-sign-in). SSO users should contact their administrator.
## Cancel or stop charges
Retell uses **post-usage billing** — there is no recurring subscription or fixed monthly fee on the pay-as-you-go plan. You are only charged for usage you actually incur (call minutes, phone numbers, knowledge bases, and other line items shown on the Billing page).
To stop accruing charges without deleting your account, follow [Stop charges or close your account](/accounts/billing#stop-charges-or-close-your-account) on the Billing page. That guide covers stopping calls, releasing phone numbers, deleting extra knowledge bases, and settling any outstanding invoices.
If you are on a custom or enterprise plan and need help confirming no recurring charges remain, contact [Retell support](https://support.retellai.com/).
## Delete your account
Deleting your account permanently removes it along with all associated data. If you're the last member of any workspace, those workspaces are deleted too. This can't be undone.
Before proceeding:
* Ensure you have downloaded any important data you wish to keep.
* Settle any outstanding invoices on the [Billing page](/accounts/billing).
* Stop any ongoing usage — see [Stop charges or close your account](/accounts/billing#stop-charges-or-close-your-account) for the full checklist (release phone numbers, delete extra knowledge bases, stop calls).
* Remove any connected third-party integrations.
Account deletion is permanent and cannot be undone. All associated data, including billing history and user preferences, will be permanently removed.
To also erase your personal data from the third-party services Retell uses — such as its CRM, support, and identity-verification providers — see [requesting erasure of your personal data](/general/compliance#how-do-i-request-erasure-of-my-personal-data).
# Add payment methods
Source: https://docs.retellai.com/accounts/add-payment
Add a Stripe payment method to your Retell workspace to buy credits, keep calling after the free trial, purchase phone numbers, and avoid service interruptions.
Adding a payment method is required before you can purchase phone numbers or use Retell services beyond the free trial. We use Stripe to securely process all payments.
Your payment method on file is used for:
* **Credit-based accounts** — purchasing credits, auto-recharge top-ups, and the end-of-month invoice for subscription items (phone numbers, knowledge bases, CPS, and concurrency).
* **Legacy monthly-billing accounts** — the end-of-period charge for your usage and recurring items.
See [Billing overview](/accounts/billing) to check which billing version your account uses.
Payment methods are scoped **per workspace**. Adding a card in one workspace does not carry it over to another workspace, even under the same login — each workspace has its own billing setup, invoices, and usage totals. If you create a second workspace, switch into it and repeat the steps below before starting calls there. See [Create and manage Retell workspaces](/accounts/workspace) for how to switch workspaces.
## Adding Your Payment Method
Go to the Billing tab in your dashboard
Click "Change payment methods" to open payment settings
In the Stripe portal, click "Add payment method" and enter your payment details. Your payment information is securely handled by Stripe.
# API Key Overview
Source: https://docs.retellai.com/accounts/api-keys-overview
How API keys authenticate your Retell REST API requests, SDK integrations, and webhook endpoints, plus best practices for storing and rotating keys safely.
API keys are used to authenticate your requests to:
* REST API endpoints
* SDK integrations
* CLI commands
* Webhook endpoints
Each workspace can have multiple API keys, all sharing the same permission level.
### REST API Authentication
To authenticate your REST API requests, include your API key in the request headers:
```http theme={"dark"}
Authorization: Bearer YOUR_API_KEY
```
Try out authentication in our [API Playground](/api-references/create-phone-call) to see it in action.
### Webhook API Key
For enhanced security, we automatically designate one of your API keys for [webhook authentication](/features/secure-webhook). This designated webhook API key:
* Is used to sign and verify webhook requests
* Cannot be deleted
* Ensures your webhook endpoints only receive legitimate requests
Keep your API keys secure and never share them in public repositories or client-side code.
# Retell AI billing: credits, auto recharge, and invoices
Source: https://docs.retellai.com/accounts/billing
How Retell billing works: prepaid credits and auto recharge on credit-based accounts, legacy end-of-month billing, usage tracking, and invoices.
The Billing tab in your dashboard is where you manage payments, buy credits, track usage, and download invoices. Retell has two billing versions, and this page covers both.
## Which billing version am I on?
Check the Billing tab to see which version applies to your workspace:
* **Credit-based billing** — your Billing page shows a **Credits Balance** with **Buy credits** and **Auto recharge** buttons. You pay for usage upfront with prepaid credits. This applies to newer accounts.
* **Monthly billing (legacy)** — your Billing page shows only monthly invoices, with no credit balance. Usage is billed at the end of each period. This applies to accounts created before credit-based billing was introduced.
Billing is scoped **per workspace**, not per account. Every workspace, including new ones you create later, has its own payment method, credit balance, invoices, and usage totals. Adding a card in one workspace does not carry it over to another workspace under the same login: switch into each new workspace and add a payment method there before running calls. See [Create and manage Retell workspaces](/accounts/workspace) for how to switch workspaces.
Choose your billing version below.
## How credit-based billing works
Credit-based accounts pay for usage with a prepaid credit balance:
* **Usage costs** — everything metered, such as per-minute call costs (voice, LLM, telephony) — are deducted from your credit balance in real time.
* **Subscription items** — phone numbers, knowledge bases beyond the free tier, calls per second (CPS), and purchased [concurrency](/deploy/concurrency) — are not paid with credits. They are billed to your payment method on file at the end of each billing cycle.
The billing cycle runs from the 1st of the month to the end of the month. Subscription items purchased mid-month are prorated for the portion of the month they were active.
New accounts start with **\$10 in free trial credits** so you can test Retell before adding a payment method. The trial credit is granted once per email address. If you previously had a Retell account with the same email (including a deleted one), signing up again won't grant the credit a second time.
When your credit balance reaches zero, new calls are blocked until you buy credits or auto recharge tops up your balance. Set up auto recharge to avoid interruptions.
## Buy credits
Go to the **Billing** tab in your dashboard and click **Buy credits**.
Enter the amount and click **Purchase**. The charge goes to your payment method on file, processed securely through Stripe (see [Add payment methods](/accounts/add-payment)).
Credits never expire, but they are non-refundable once purchased.
## Set up auto recharge
Auto recharge buys credits automatically when your balance runs low, so calls are never blocked:
Go to the **Billing** tab, click **Auto recharge**, and toggle **Auto Recharge** on.
Set **When credits drop below** (the balance that triggers a recharge) and **Bring credits back to** (the balance restored on each recharge), then click **Save**.
Each recharge is charged to your payment method on file. If a recharge payment fails, follow [Handle failed payments](/accounts/fail-payment).
## Subscription items and invoices
At the end of each calendar month, you receive an invoice for subscription items:
* Phone numbers [purchased through Retell](/deploy/purchase-number)
* Knowledge bases beyond the free tier
* Calls per second (CPS) upgrades
* Purchased [concurrency](/deploy/concurrency)
These are charged to your payment method on file and prorated if purchased mid-cycle. Download invoices from the Billing page by clicking the "Invoice" button next to the respective period.
## Stop charges or close your account
To stop accruing charges on a credit-based account:
1. **Turn off auto recharge** so your balance is not topped up automatically.
2. **Release any phone numbers you own.** Numbers are billed monthly until released. Remove the number from the Phone Numbers page, or call the [Delete Phone Number API](/api-references/delete-phone-number).
3. **Delete knowledge bases beyond the free tier** and remove any CPS or concurrency upgrades. These are billed monthly until removed.
4. **Settle any outstanding invoices** on the Billing page.
Remaining credits are non-refundable, so spend down your balance before closing your account. To close your account entirely, follow [Delete your account](/accounts/account#delete-your-account) after completing the steps above.
## Billing overview
The Billing tab allows you to manage payments, track expenses, and download invoices:
1. Payment management: We use Stripe for secure and reliable payment processing. Update your payment methods by clicking the "Change payment methods" button.
2. Billing history: Review your monthly expenses, including cost breakdowns by category (e.g., Voice Infra, LLM).
3. Invoices: Download invoices by clicking the "Invoice" button next to the respective period.
4. Current charges: View ongoing costs for the current billing period, including itemized amounts and usage details.
Legacy accounts use **post-usage billing**, not a prepaid credit balance. You are charged at the end of each billing period based on actual usage on the payment method you have on file. There is no manual top-up or auto recharge to configure. Make sure a valid payment method is added (see [Add payment methods](/accounts/add-payment)) to avoid service interruptions. If a charge fails, follow [Handle failed payments](/accounts/fail-payment).
## Stop charges or close your account
Because legacy accounts use post-usage billing, there is no recurring subscription to cancel and no fixed monthly fee on the pay-as-you-go plan. You are only charged for usage you actually incur (call minutes, phone numbers, knowledge bases, and other line items shown on the Billing page).
To stop accruing charges:
1. **Stop running calls.** Once no calls are made, no usage charges accrue for the next billing period.
2. **Release any phone numbers you own.** Numbers purchased through Retell are billed monthly until released. Remove the number from the Phone Numbers page in the dashboard, or call the [Delete Phone Number API](/api-references/delete-phone-number).
3. **Delete knowledge bases beyond the free tier.** Additional knowledge bases are billed monthly until deleted.
4. **Settle any outstanding invoices** on the Billing page.
If you also want to close your account entirely, follow [Delete your account](/accounts/account#delete-your-account) after completing the steps above. If you are on a custom/enterprise plan or need help confirming there are no remaining recurring charges, contact [Retell support](https://support.retellai.com/).
## View usage breakdown
The Usage tab on the Billing page provides a breakdown of your workspace's activity and costs, regardless of billing version:
1. Total cost: Your total expenses for the selected billing period.
2. Call minutes: The total number of call minutes used.
3. Average cost per minute: The average cost for each minute of calls.
4. Daily or weekly call costs, making it easy to identify high-cost periods and track spending trends over time.
5. Cost by provider: A breakdown of expenses across voice infra, large language models (LLMs), telephony services, and concurrency usage.
What each category covers:
* **Voice infra**: Retell's conversation voice engine. This is the flat per-minute platform cost of running the real-time call, separate from LLM and telephony costs.
* **LLM**: the per-minute cost of the language model your agent uses, which varies by model.
* **Telephony**: per-minute call rates and monthly phone number fees for Retell-provided numbers. Custom telephony (SIP trunking) carries no Retell telephony charge.
* **Concurrency**: monthly fees for purchased concurrency beyond the 20 free concurrent calls.
See the [Retell pricing page](https://www.retellai.com/pricing) for current rates for each component.
Certain call characteristics can adjust the billed duration; see [Exceptions to per-minute pricing](/accounts/billing-exceptions).
# Exceptions to Our Per-Minute Pricing
Source: https://docs.retellai.com/accounts/billing-exceptions
Billing adjustments that may affect Retell call costs: 10-second minimum for dynamic opening messages and extra charges for long prompt token counts.
## Overview
While we generally bill based on actual call duration, certain call characteristics may result in adjusted billing to ensure fair pricing for our services.
## Rule 1: Minimum Duration for Dynamic Opening Messages
**When it applies:** Calls shorter than 10 seconds that use dynamic opening messages when AI speaks first
**Billing adjustment:** Minimum charge of 10 seconds
**Example:**
* Call duration: 6 seconds
* Dynamic opening messages: Enabled
* Billed duration: 10 seconds (4 seconds additional charge)
**Why:** Dynamic opening messages require processing time regardless of call length, so we ensure a minimum charge to cover these costs.
## Rule 2: LLM Price Scaling for > 4,000 Token Prompt Length
**When it applies:** Agents that use more than 4,000 LLM tokens in their prompts
**Billing adjustment:** Duration is scaled proportionally based on token usage
**What's included in token calculation:**
* global prompt
* functions (tool descriptions)
* state / node prompt
* transcript between agent and user
* tool call history and results
* retrieved [knowledge base](/build/knowledge-base) content
[Flex mode](/build/conversation-flow/flex-mode) is a common trigger for this rule.
It compiles all node prompts, transitions, and tool descriptions into a single LLM
context, which can push the token count well above 4,000.
**Price calculation:**
* Scaling Factor = Prompt LLM Tokens ÷ 4,000
* Billed Duration = Original Duration × Scaling Factor (rounded up)
**Example:**
* Call duration: 60 seconds
* LLM tokens used: 4,800
* Scaling factor: 4,800 ÷ 4,000 = 1.2
* Billed duration: 72 seconds (12 seconds additional charge)
**Why:** Larger LLM prompt lengths incur greater costs due to token-based pricing from our underlying model providers, so we scale the billing accordingly to reflect these increased expenses.
# Data Retention Policy
Source: https://docs.retellai.com/accounts/data-retention
Configure per-agent data retention to automatically delete call and chat data — transcripts, recordings, and logs — after a set period for compliance.
# Data Retention Policy
Retell allows you to configure a data retention period per agent. After the retention period expires, call and chat data associated with that agent is automatically and permanently deleted.
By default, data is kept indefinitely (no automatic deletion).
## How It Works
* Data retention is configured **per agent** under Security & Fallback Settings
* Expired data is automatically deleted on a daily basis
* Deletion is **permanent and irreversible** — deleted data cannot be recovered
* Applies to both voice calls and chats
## How to Configure
1. Navigate to your agent
2. Open **Security & Fallback Settings**
3. Under **Data Storage Settings**, select your preferred data storage mode
4. Use the **Retention** dropdown to set how long data is kept before automatic deletion
The retention period applies regardless of which data storage mode you select (Everything, Everything except PII, or Basic Attributes Only).
## What Gets Deleted
When the retention period expires, the following data is permanently removed:
* **Call recordings** (audio files)
* **Transcripts**
* **Call and chat logs**
* **Knowledge base retrieval logs**
* **Dynamic variables and metadata**
While some basic metadata is retained internally, the call or chat is effectively deleted and will no longer appear in session history or API responses.
Deletion is irreversible. Make sure to export any data you need before the retention period expires. You can use [webhook events](/features/webhook-overview) to capture call data in real time, or use the [Get Call](/api-references/get-call) / [Get Chat](/api-references/get-chat) API to retrieve data before it expires.
## Available Retention Periods
| Option | Duration |
| ------------ | ------------------------------- |
| Keep forever | No automatic deletion (default) |
| 1 day | 24 hours after call/chat starts |
| 3 days | |
| 7 days | |
| 30 days | 1 month |
| 60 days | 2 months |
| 90 days | 3 months |
| 180 days | 6 months |
| 365 days | 1 year |
| 730 days | 2 years |
## API Configuration
You can set the retention period via the API when creating or updating an agent:
```json theme={"dark"}
{
"data_storage_retention_days": 90
}
```
* **Field**: `data_storage_retention_days`
* **Type**: integer (1–730) or `null`
* **Default**: `null` (keep forever)
See the [Update Agent](/api-references/update-agent) or [Create Agent](/api-references/create-agent) API reference for details.
## Related
* [Data Storage Settings](/accounts/privacy-disable) — control what types of data are stored
* [Secure URLs](/accounts/signed-secure-url) — configure URL expiration for recordings and logs
# Handle failed payments
Source: https://docs.retellai.com/accounts/fail-payment
Resolve failed Retell payments: contact your bank to allowlist transactions, request written confirmation, and update your payment method on Stripe.
If your payment fails, follow these steps to resolve the issue:
1. Contact your bank to ensure the transaction isn't being blocked
2. Request to allowlist transactions from Retell
3. Return to the Retell Dashboard and retry the payment
1. Obtain written confirmation from your bank that Retell has been allowlisted
* Statement should be on bank letterhead
* Should explicitly confirm that transactions from Retell are now approved
2. Email the statement to [support@retellai.com](mailto:support@retellai.com)
3. Include your Retell account details in the email
1. Consider adding a different payment method
2. Go to the "Billing" tab
3. Click "Change payment methods"
4. Add a new card or payment method
If you continue experiencing issues, please contact our support team via the [Customer Support Portal](https://support.retellai.com/) or at [support@retellai.com](mailto:support@retellai.com) for further assistance.
# KYC Verification
Source: https://docs.retellai.com/accounts/kyc
Complete KYC verification — automatic, Persona-based, or manual review — to unlock outbound calling, phone number purchases, and SMS on your Retell account.
Before you can make outbound calls with Retell, you’ll need to complete KYC (Know Your Customer) verification. Depending on your account information, there are a few ways to pass KYC.
### How to Pass KYC
#### Automatic verification
We may automatically verify your account based on the information you provided during registration. If this applies, your KYC will be approved without any additional steps.
#### Verification via Persona
If automatic verification is not possible, you will be asked to complete KYC through Persona using your government-issued ID.
You can go to “Phone Numbers”, click on any of your numbers, and you’ll see the interface where you can start the KYC process.
We currently support verification in 83 countries.
If your country is not listed, [contact our support team](/general/support) with your company name, use case, and proof you represent the company. We will review your case based on risk and business needs and may enable verification for your country.
#### Manual review
If neither automatic nor Persona verification applies, your KYC goes to manual review, which takes longer than the other paths. If your verification is delayed, [reach out to support](/general/support) with your workspace id, company name, use case, and proof you represent the company so we can prioritize it.
### ID Verification Restrictions
Each person can verify only one account. Our system detects duplicate identities, so even using a different government ID may be flagged as a duplicate. If your previous account was verified and later deleted, [contact our support team](/general/support) to request a manual review.
# Manage API keys and permission scopes
Source: https://docs.retellai.com/accounts/manage-api-keys
Create, delete, and rotate Retell API keys, set a webhook signing key, and restrict permissions with read or edit scopes for Build, Monitor, and Deploy.
The "API Keys" section belongs to System "Settings". The "API Keys" section allows you to manage your authentication credentials for accessing the API. Here's what you can do:
1. **Create a new API key**
* Click the "Add" button
* Give your key a descriptive name to identify its purpose
2. **Delete an existing API key**
* Locate the key you want to remove
* Click the delete (trash) icon
* Confirm the deletion when prompted
3. **Set a webhook API key**
* Select an existing API key
* Click "Set as Webhook Key" to designate it for webhook authentication
* Only one key can be set as the webhook key at a time
4. **Restrict an API key's permissions**
* Enable "Restrict permissions" when creating or editing a key
* Grant each permission group **No Access**, **Read**, or **Edit**
* See [Restrict API key permissions](#restrict-api-key-permissions) for details
Keep your API keys secure and never share them publicly. If a key is compromised, delete it immediately and create a new one.
## Restrict API key permissions
By default, an API key has **full access** to every API endpoint that supports API key authentication. To limit what a key can do, enable **Restrict permissions** when you create or edit the key, then choose an access level for each permission group. Scoped keys are useful when you share a key with a third party or want to limit it to a single integration.
Each group offers up to three access levels:
* **No Access** — the key cannot call any endpoint in this group.
* **Read** — the key can call read-only endpoints in this group (for example, listing or fetching resources).
* **Edit** — the key can call both read and write endpoints in this group. Selecting **Edit** also grants Read access.
Some groups are action-only and have no separate **Read** level — their Read column shows a dash (–). For those groups you can only choose **No Access** or **Edit**.
### Permission groups
| Group | Permission | Access levels | Grants access to |
| ----------- | ---------- | ----------------------- | ------------------------------------------------------------------------------------------------------ |
| **Build** | Agent | No Access · Read · Edit | Agents and chat agents, conversation flows, Retell LLMs, knowledge bases, voices, and folders |
| **Build** | Testing | No Access · Read · Edit | Test cases and results, batch test jobs, playground threads and completions, and web-call testing |
| **Monitor** | History | No Access · Read · Edit | Call and chat history, transcripts, recordings, and call metadata |
| **Monitor** | Export | No Access · Edit | Creating and managing export requests for history data |
| **Deploy** | Call | No Access · Edit | Creating and managing web, phone, and batch calls, chat sessions, and live-call controls |
| **Deploy** | Phone | No Access · Read · Edit | Phone numbers, A2P campaigns, business profiles, branded call and phone verification, and SMS webhooks |
Grant the narrowest access a key needs. For example, a key that only pulls call history needs just **History → Read**, while a key that places outbound calls needs **Call → Edit**.
Restrictions apply only to API requests made with that key, and you can change them anytime by editing the key. A key created without restrictions keeps full access.
# Data Storage Settings
Source: https://docs.retellai.com/accounts/privacy-disable
Control how Retell stores sensitive call data — recordings, transcripts, dynamic variables, caller IDs, KB logs — with per-agent privacy settings.
# Data Storage Privacy Settings
By default, we store potentially sensitive data related to your calls, including:
* Call logs
* Transcriptions
* Call recordings
* Caller ID for inbound call
* Callee ID for outbound call
* Knowledge base retrieved contents logs
* Dynamic variables
* Metadata
## How to Manage Data Storage
You can opt out of sensitive data storage at any time:
1. Navigate to your agent
2. Under **Security & Fallback Settings → Data Storage Settings**, select:
* **Everything** — store transcripts, recordings, and logs
* **Everything except PII** — store content, excluding PII when possible
* **Basic Attributes Only** — store only metadata (no transcripts/recordings/logs)
You can also configure a **data retention period** to automatically delete stored data after a set number of days. See [Data Retention Policy](/accounts/data-retention) for details.
## What Happens When You Change Storage Settings
When you opt out:
* You will continue to receive [webhook events](/features/webhook-overview), where you can access the transcript, call recording, and other sensitive data in it
* The call recording link will expire after 10 minutes upon receiving the webhook
* **Everything**: All artifacts (transcripts, recordings, logs) are stored
* **Everything except PII**: Artifacts are stored with PII removed according to the categories you select in your `pii_config`. See [PII scrubbing](#pii-scrubbing) below for exactly which fields are affected.
* **Basic Attributes Only**: No transcripts/recordings/logs are stored; if you query the call with the get call API later, you will not get these fields
## PII scrubbing
When you choose "Everything except PII", you can configure which personally identifiable information (PII) is removed after the call completes. Scrubbing is applied across the **transcript, recording, public logs, and structured fields on the call record itself** (dynamic variables, metadata, call analysis, tool call arguments and results).
PII configuration is defined on the agent. Per-call storage behavior can still be limited via `data_storage_setting` inherited onto the call.
### Content categories
When any of these categories are selected, occurrences are detected post-call and replaced with `[category number]` placeholders (e.g. `[email 1]`, `[person name 2]`):
* `person_name`
* `address`
* `email`
* `ssn`
* `passport`
* `driver_license`
* `credit_card`
* `bank_account`
* `password`
* `pin`
* `medical_id`
* `date_of_birth`
* `customer_account_number`
These placeholders are written into the scrubbed copies of:
* **Transcript** — user and agent utterances
* **Recording** — audio is replaced with a beep over the PII intervals (surfaced as `scrubbed_recording_url`)
* **Public logs** — log file content
* **Dynamic variables** — `retell_llm_dynamic_variables`, collected dynamic variables, and override dynamic variables (surfaced as their `scrubbed_*` counterparts)
* **Metadata** — `metadata` (surfaced as `scrubbed_metadata`)
* **Call analysis** — `call_analysis.call_summary` and `call_analysis.custom_analysis_data` (surfaced as `scrubbed_call_analysis`)
* **Tool calls** — arguments and results
* **DTMF digits** — replaced with `[PII INFO]` whenever any PII category is enabled, to avoid exposing touch-tone passwords or PINs
* **SMS message text** (for chat agents)
The raw originals are deleted under "Everything except PII" — only the scrubbed versions remain.
### `phone_number` (directional)
`phone_number` behaves differently from the content categories. Selecting it redacts the **customer's** phone number from the call record itself, not just the transcript:
* **Inbound calls**: `from_number` is removed
* **Outbound calls**: `to_number` is removed
* **SMS chats**: `user_number` is removed
The opposite-direction number (your Retell number) is preserved. The field is removed entirely from the call object — there is no placeholder. If you need to keep the customer's number visible for downstream systems, do not include `phone_number` in your categories.
### What is always preserved
Regardless of the categories you configure, these fields are never altered or removed by PII scrubbing:
* **Identifiers**: `call_id`, `agent_id`
* **Timing**: `start_timestamp`, `end_timestamp`, `duration_ms`
* **Outcome**: `call_status`, `disconnection_reason`, `call_successful`, `user_sentiment`, `in_voicemail`
* **Operational**: `direction`, `transfer_destination`, `call_latency`, `cost_metadata`, `call_cost`, `custom_attributes`
* **Tool call records** — the name, timing, and success of each tool call (their *arguments and results* are still scrubbed when content categories are selected)
To remove these fields as well, use the **Basic Attributes Only** storage setting or configure a [data retention period](/accounts/data-retention).
## Announce a recording disclaimer
Retell does not have a dedicated setting for playing a "this call may be recorded" announcement, but you can have the agent say it as its first utterance. Recording, when enabled, runs for the entire call, so the disclaimer itself is captured in the recording.
* **Single or multi-prompt agents** — put the disclaimer text in the agent's **Begin Message** (for example, `"This call may be recorded for quality assurance."`) so it is spoken before any user turn.
* **Conversation flow agents** — add a conversation node at the start of the flow with the disclaimer as its static text, enable **Skip Response** so the agent moves on without waiting for a user reply, and enable **Block Interruptions** so the user can't cut it off. See [Conversation Node](/build/conversation-flow/conversation-node).
If you don't want the disclaimer stored, switch that agent's storage to **Basic Attributes Only** — no recording is retained, but the agent still speaks the announcement live.
# Public keys
Source: https://docs.retellai.com/accounts/public-keys
Use Retell AI public keys for web calls and the website widget: authenticate browser requests, restrict allowed domains, and configure reCAPTCHA protection.
Public keys authenticate the Retell website widget and browser web calls. You can include public keys in frontend code; keep [API keys](/accounts/api-keys-overview) on your server.
Use public keys for:
* Embedding the [website widget](/deploy/chat-widget)
* Starting [web calls](/deploy/web-call) with `RetellClient.createWebCall()`
## Create or edit a public key
1. Open **API Keys → Public Keys** in the Retell dashboard.
2. Add a public key or select an existing one to edit.
3. Add the domains that can use it, such as `example.com` or `app.example.com`. Add `localhost` for local development.
4. Save the key and copy its value into your widget configuration or Web SDK client.
Only allow domains you control, and remove domains you no longer use.
## Google reCAPTCHA v3 protection (optional)
Enable Google reCAPTCHA v3 to help limit automated abuse. When enabled, creating a web call or starting a widget conversation requires a valid reCAPTCHA token.
To enable reCAPTCHA:
1. Edit the public key and enable **Abuse Prevention (Google reCAPTCHA)**.
2. Add your reCAPTCHA **secret key** from [Google's reCAPTCHA console](https://www.google.com/recaptcha).
3. Set the **Score Threshold**. The dashboard defaults to 0.5 (as of September 2026). Requests below the threshold are rejected; higher thresholds can also reject more legitimate users.
4. Save your changes.
Your frontend uses the reCAPTCHA **site key** to obtain tokens; keep the secret key in the dashboard. Follow [Google's reCAPTCHA v3 guide](https://developers.google.com/recaptcha/docs/v3) to obtain a fresh token when the user starts a call. For the Web SDK, pass it as `recaptchaToken` to `createWebCall()`. For the website widget, configure its [reCAPTCHA site key](/deploy/chat-widget#recaptcha-protection).
# Opt in to secure URL
Source: https://docs.retellai.com/accounts/signed-secure-url
Opt in to secure URLs so Retell-generated call recording and log links automatically expire after 24 hours, preventing unauthorized access if a link leaks.
# Secure URL
By default, the URLs we generate for call recordings and logs do not expire, allowing you to easily share the links with other people.
However, if security is a concern and you want to prevent unauthorized access in case the URL is leaked, you can opt in for secure URLs.
Secure URLs automatically expire 24 hours after they are generated, providing an additional layer of security.
## How to Opt In
You can opt in to secure URLs at any time:
1. Navigate to your agent
2. Toggle the "Opt In Secure URL" switch
## What Happens When You Opt In
* Every time you request the URLs of your call's recording and log, we will generate a URL with a signature that will expire 24 hours after it is generated.
* Accessing the resource using the URL after 24 hours will be denied.
* Files created before secure URLs were enabled will still be generated without signatures.
## What Happens When You Opt Out After Opt In
* Files created while secure URLs were enabled will continue to generate signed URLs with 24-hour expiration whenever you request their URLs, even after you opt out.
* Only files created after you opt out will generate non-expiring URLs.
# Create and manage Retell workspaces
Source: https://docs.retellai.com/accounts/workspace
Create and manage Retell workspaces, find your workspace ID, invite teammates to collaborate, switch between workspaces, and safely delete a workspace.
Learn how to create and manage workspaces, and collaborate with team members.
## Default Workspace
When you first create an account, a default workspace is automatically created for you. This workspace serves as your primary environment for managing projects and collaborating with team members.
## Find your workspace ID / org ID
Sometimes we might ask for your workspace ID / org ID when debugging issues related to your account. You can find it in your workspace settings page.
Open **Settings** from the sidebar:
Then open **Workspace** → **General**. Your workspace ID appears under **Workspace ID**:
## Creating Additional Workspaces
To create a new workspace:
1. Click the workspace selector in the top left corner of the dashboard
2. Select **Add another workspace**
3. Enter your workspace name
4. Click **Save**
## Managing Team Members
### Inviting Members
To invite team members to your workspace:
1. Open **Settings** → **Workspace** → **Users**
2. Click **Invite a member**
3. Enter their email address and select a role
4. Send the invitation
If a team member does not receive the invitation email, you can resend it by inviting them again with the same email address.
### Invitation Process
* Invited members receive an email with a link to join the workspace
* They can create a new account or use an existing one
* Once accepted, they have immediate access
## Leave a Workspace
To leave a workspace:
1. Open **Settings** → **Workspace** → **Users**
2. Click **Leave Workspace**
3. Confirm your action
**Important considerations before leaving:**
* If you are the only member, leaving permanently deletes the workspace and all its data, and a final invoice is generated for any outstanding usage — the same as [deleting the workspace](#delete-workspace). Your account stays active and you keep access to any other workspaces.
* If other members remain but you are the workspace's last admin, you can't leave until another member has the Admin role. Assign Admin to someone else first, or delete the workspace.
* You can rejoin a workspace if another member invites you back.
## Delete Workspace
Only a workspace **Admin** can delete a workspace. In **Settings** → **Workspace** → **General**, open the options menu (⋯) and select **Delete**, then type the workspace name to confirm. Before deletion, please note:
* All workspace data will be permanently deleted and cannot be recovered
* A final invoice will be generated and charged for any usage up to the deletion date
* All team members will lose access to the workspace
* Any active API keys will be invalidated
You can't delete your only workspace on its own. If this is the only workspace on your account, deletion is blocked — to remove it, [delete your account](/accounts/account#delete-your-account) from account settings instead.
If there is no payment method on file when you delete the workspace, the final invoice is still generated for any outstanding usage and remains due. Add a valid payment method before deleting (see [Add payment methods](/accounts/add-payment)) so the final charge can settle automatically. If a charge fails afterward, follow [Handle failed payments](/accounts/fail-payment) or contact Retell support to resolve the balance.
# Agent workflow
Source: https://docs.retellai.com/agent/agent-workflow
Run pre-call and post-call functions on a Retell AI agent: look up the caller before the agent speaks, then log the outcome and create follow-ups.
An agent's **Workflow** page is where you run functions outside the conversation: before the agent speaks, so it starts already knowing who it's talking to, and after the session ends, so your systems are updated without anyone touching them. Voice agents call these **pre-call** and **post-call functions**; chat agents get the same two slots, named **pre-chat** and **post-chat functions**. A function can be a tool from a [connected provider](/integrations/overview), an HTTP request to your own endpoint, a code snippet, or an SMS.
Each slot is a dependency graph rather than a flat list. Functions with no dependency all start at once, and a function that depends on another waits for its output.
Everything below works the same on both channels except where noted.
A video walkthrough of the Workflow page, covering pre-call and post-call functions:
## When to use it
Reach for a pre-call or pre-chat function when the agent needs a fact it can't ask for, or shouldn't have to:
* **Greet an identified person by name.** Look the caller's number, or the chat's contact details, up in your CRM before the first word, instead of asking "can I take your name?"
* **Load account state.** Pull their open tickets, last order, or policy status so the agent answers the first question without a lookup mid-conversation.
* **Decide how to open.** Fetch a flag (overdue balance, VIP tier, do-not-call) and branch the greeting on it.
Reach for a post-call or post-chat function when the work happens after the conversation, not in it:
* **Log the outcome.** Write the summary and disposition back to the CRM timeline.
* **Create the follow-up.** Open a task, a ticket, or a callback when the conversation agreed to one.
* **Notify.** Text the caller a confirmation, or push the result to your own endpoint.
Keep a tool in the conversation itself when the agent needs the result *while* it's talking, like checking calendar availability for a time the caller just named. See [integration tools for prompt agents](/build/single-multi-prompt/integration-tools) and [integration tools in conversation flow](/build/conversation-flow/integration-tools) for that surface.
Workflow functions are configured on the dashboard only (as of August 2026). They are not part of the public API, so you can't set them through `create-agent` or `update-agent`.
## What you can run
Both slots take the same four kinds of function, from the same **+** menu:
| Function | What it does | Available in |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- |
| **Integration tool** | A tool from a [connected provider](/integrations/overview) — HubSpot, Salesforce, Dynamics 365, GoHighLevel, Zoho CRM, Zendesk, Calendly, Cal.com | Both slots, both channels |
| **Custom function** | An HTTP request to your own endpoint. The request body carries the session object — under `call` on a voice agent, `chat` on a chat agent | Both slots, both channels |
| **Code** | JavaScript run in Retell's sandbox — reshape a value, compute a date, call a small API | Both slots, both channels |
| **SMS** | Send a text message, with the body written by the LLM from the transcript | Post-call only, voice agents only |
Saving an SMS function in the pre-call slot, or on a chat agent, is rejected: the session is over before an SMS can make sense, and a chat agent has no number to send from.
## Add functions on the Workflow page
In the agent editor, select **Workflow** in the left rail. The canvas lays the whole session out end to end: **Dial in/out**, **Pre-call functions**, **Call started**, the agent, **Call ended**, **Post-call data extraction**, and **Post-call functions**. A chat agent shows the same flow with **Any trigger**, **Pre-chat functions**, **Chat started**, **Chat ended**, **Post-chat data extraction**, and **Post-chat functions**.
Click **+ Add** on the opening or closing functions node and pick **SMS**, **Code**, **Custom function**, or a tool from one of your connected providers. **Add integration** connects a new provider without leaving the page.
Give it a **Name**. This is how other functions refer to it, so it must be unique within that slot. Then set its inputs and outputs in the **Function fields** card. Every input is either a literal you set now, which may be a `{{variable}}` reference, or a description the LLM fills in; every output you keep is what the agent sees and, optionally, a dynamic variable. On a pre-call function the inputs start on a literal rather than a description, since there's no conversation to infer from yet, but you can switch any of them. [Configure a tool's inputs and outputs](/integrations/overview#configure-a-tools-inputs-and-outputs) covers the modes and the test run in full.
Hover a function's row and click its **+**, then choose **Add sequential function** to run the new one *after* it, or **Add parallel function** to run it *alongside* it. Use the node's own **+ Add** instead to add a function that starts immediately, independent of the rest.
## Run functions in parallel or in sequence
Retell runs the graph in batches: every function whose dependencies have finished runs concurrently, then the next batch, until the graph is done. Total time is the longest chain, not the sum of every function.
* **Parallel.** A function with no dependency starts as soon as the phase begins, so two independent lookups cost as long as the slower one. **Add parallel function** starts the new function at the same point as the row you clicked, and anything that was waiting on that row now waits for both.
* **Sequential.** **Add sequential function** makes the new function depend on every function in the row's card, so it starts only once they've all succeeded. That's what lets it use their output.
A chain can be at most 4 functions deep, and each slot holds at most 15 functions (as of August 2026). Saving is rejected if a function depends on itself, on a name that doesn't exist, or on a cycle.
## Pass a response from one function to the next
Chaining works through [dynamic variables](/build/dynamic-variables). On the function's **Output** tab, give a response field a name in the **Dynamic variable** column; downstream functions then reference it as `{{that_name}}` in any input, and the agent's prompt can use it too.
Variables become available as the graph runs, so a function only ever sees output from functions that finished before it. The dashboard enforces the same rule as you type: when you pick a variable, it offers only the outputs of the functions this one waits on, not the ones running beside it or after it.
Two more things to know about how the data reaches the agent:
* **The result itself lands in the agent's context**, not just the variables you named. Every tool call is woven into the transcript the LLM reads, with its arguments and its response; what a workflow function changes is only *where* — a pre-conversation call is prepended, so the agent has it from its first turn. Responses are capped at 30,000 characters for an integration tool and 15,000 for a custom function or code tool (as of August 2026). Use **Select fields** on the **Output** tab to send only the fields that matter, rather than letting a large provider payload crowd the prompt.
* **Dynamic variables are extracted from the raw response**, before that field selection. Narrowing what the agent sees never breaks a variable you mapped.
If a `{{variable}}` in an input has no value — the function it came from didn't run, or returned nothing for that field — the input is left out. If that input was required, the function is skipped rather than sent with a gap in it.
### Variables a post-conversation function can use
Post-call and post-chat functions run after [Post Call Extraction](/features/post-call-analysis-overview), so they can read its results on top of everything available during the session:
| Voice | Chat | What it holds |
| ----------------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------- |
| `{{call_summary}}` | `{{chat_summary}}` | The analysis summary |
| `{{call_successful}}` | `{{chat_successful}}` | Whether the analysis judged the session successful |
| `{{user_sentiment}}` | `{{user_sentiment}}` | The user's sentiment |
| One per custom analysis field | One per custom analysis field | Named after the field, from your [Post Call Extraction](/features/post-call-analysis-overview) setup |
| `{{disconnection_reason}}` | `{{disconnection_reason}}` | Why the session ended — see [call disconnect reasons](/reliability/debug-call-disconnect) |
| `{{call_status}}` | `{{chat_status}}` | The session's final status |
These are available to post-conversation functions only; nothing earlier can read them, because the analysis hasn't run yet.
## Gate a post-conversation function on a condition
Every post-call and post-chat function can carry an **only when** gate, so you don't write a task on a call that never connected. In the function's config, set the match to **All** or **Any**, then add rows:
* **Variables condition** — compare a `{{variable}}` against a value with `=`, `≠`, `>`, `<`, `≥`, `≤`, contains, not contains, exists, or not exists. Picking exists or not exists drops the value field, since there's nothing to compare against.
* **Disconnection reason** — pick one or more reasons the session must have ended with.
* **Call status** — require a specific final status.
A function with no rows always runs. A gated-off function counts as not having run, so anything depending on it is skipped too. That's usually what you want: gate the lookup, and the write that follows it drops with it.
## Examples
### Greet an identified caller and load their tickets
A support line answers with the caller's name and their open ticket already in context.
**Pre-call functions:**
1. `find_user` — Zendesk **Search User**, with `phone_number` pinned to `{{user_number}}`. On the **Output** tab, map the user's `id` to `zendesk_user_id` and `name` to `caller_name`.
2. `list_tickets` — Zendesk **List User Requested Tickets**, added as a *sequential* function under `find_user`, with `user_id` set to `{{zendesk_user_id}}`.
The prompt then opens with `Greet {{caller_name}} by name.` and the agent already has the ticket list in context, so "what's the status of my ticket?" is answered on the first turn instead of the third.
The same pair works on a [chat agent](/build/create-chat-agent) as pre-chat functions. Swap `{{user_number}}` for whichever [dynamic variable](/build/dynamic-variables) your chat is started with, such as an email address passed in when the widget opens.
### Log the session and open a follow-up task
A sales agent writes every conversation back to HubSpot, and creates a task only when there's something to follow up on.
**Pre-call function:**
* `search_contact` — HubSpot **Search Contact** on `{{user_number}}`, mapping the record ID to `hubspot_contact_id`.
**Post-call functions:**
1. `log_hubspot_call` — HubSpot **Log Call Activity** on `{{hubspot_contact_id}}`, with the body set to `{{call_summary}}`. Gated on **All**: `{{call_summary}}` exists, so a call that never got far enough to analyze writes nothing.
2. `create_followup` — HubSpot **Create Task**, added as a *parallel* function beside `log_hubspot_call` so both writes go out at once. Gated on **All**: `{{call_status}}` = `ended` **and** `{{followup_needed}}` = `true`, where `followup_needed` is a custom [Post Call Extraction](/features/post-call-analysis-overview) field. Its subject is a description the LLM fills in from the transcript.
The session is logged every time it completes; the task appears only on the conversations that earned one.
### Confirm a booking by text
A clinic texts the caller after the call, but only when a booking actually happened.
**Post-call functions:**
1. `get_booking` — Cal.com **Get Booking**, with the UID set to `{{booking_uid}}`, a variable an in-conversation **Book Appointment** tool mapped earlier in the call. Gated on **All**: `{{booking_uid}}` exists.
2. `send_confirmation` — an **SMS** function, sequential under `get_booking`, whose content the LLM writes from the transcript and the booking response.
No booking means `{{booking_uid}}` never gets set, the gate fails, and both functions drop, so no text goes out. On a chat agent, replace the SMS with a **Custom function** that posts to your own notification endpoint.
## Timing and limits
A call waits for its pre-call graph before the conversation begins, up to a budget that depends on the call:
* **Inbound and web calls wait 1 minute.**
* **Outbound calls wait 5 minutes, before Retell dials.** The extra room is there because nothing is ringing yet, so nobody is sitting on the line while a CRM lookup runs.
Exceeding the budget doesn't cancel anything. The remaining functions **keep running in the background**, and what they return still reaches the agent: each call and its response are added to the transcript the LLM reads, and the variables they map stay available to everything downstream, post-call functions included. What a late arrival misses is the initial prompt and the agent's opening message, which are already built by the time it lands. So the budget is the deadline for anything the agent's first words depend on, not for the data itself.
Post-call functions are capped at **5 minutes** total. That one is a hard stop: whatever hasn't finished is cancelled so teardown can complete.
| | Pre-conversation | Post-conversation |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| When it runs | Before the agent's first message; for outbound calls, before Retell dials | After Post Call Extraction completes |
| Time budget (voice) | 1 minute inbound and web, 5 minutes outbound | 5 minutes |
| Time budget (chat) | None; the chat waits for the graph | None |
| Past the budget | The session starts; remaining functions finish in the background and still reach the transcript, but too late for the initial prompt and opening message | Remaining functions are cancelled |
| Max functions | 15 | 15 |
| Max chain depth | 4 | 4 |
Figures are as of August 2026.
## When a function fails
A failure stays inside the function it happened in. A function that errors, times out, is missing a required input, or is gated off is marked unsuccessful, and:
* **Everything waiting on it is skipped**, and everything waiting on those. A lookup that fails takes down the write that needed its ID, rather than sending a request with a blank field.
* **Independent branches carry on.** A failed CRM lookup doesn't stop the calendar check running beside it.
* **The session continues.** A pre-conversation failure still connects the call or opens the chat, just without those variables, so write the prompt to read correctly when a lookup came back empty. A post-conversation failure still completes teardown.
## See what ran
Open the call in [call and chat history](/features/session-history) and read the transcript. Pre-call function calls sit under a **Before call** divider at the top, the conversation follows **Call started**, and post-call ones sit under **Call ended** at the bottom. Each entry shows the arguments sent and the response returned, so you can tell a bad input apart from a provider error. During a live call, the same entries collapse under **Before conversation** in the live transcript.
Chat sessions run their pre-chat and post-chat functions the same way.
## FAQ
No. Every function in the slot runs whenever its dependencies are met. Unlike a tool in the conversation, there is no LLM decision about *whether* to call it. The LLM is only involved in filling inputs you described rather than pinned, and post-conversation functions add an **only when** gate you control.
From the agent's prompt and the dynamic variables available at that point, including outputs from earlier functions in the graph. There's no conversation yet, so an input that can only come from the user belongs on an in-conversation tool instead. Post-conversation functions also see the full transcript.
Yes, by as long as the graph takes, up to 1 minute on an inbound or web call, and a chat waits for its pre-chat graph with no budget at all. Outbound calls absorb their 5-minute budget before dialing, so nobody hears the delay. Keep the chain shallow: two functions running in parallel cost one function's latency, while two chained cost both. If a lookup is slow and not needed for the opening line, run it as an in-conversation tool instead.
Yes. Pre-conversation outputs stay in the session's dynamic variables for the whole session, so a post-conversation function can reference them directly. That's how the HubSpot example above reuses `{{hubspot_contact_id}}` without looking the contact up twice.
Saving is rejected. Names are the handle other functions depend on, so they have to be unique within a slot. The same name may appear once in the pre-conversation slot and once in the post-conversation slot.
# Set language for your agent
Source: https://docs.retellai.com/agent/language
Set a single language for a Retell agent to control speech recognition, voice pronunciation, and the language the agent uses to respond during calls.
You can set a specific language for an agent. The language setting affects three things during a call:
* **Speech recognition** — which language the agent transcribes the caller from.
* **Voice pronunciation** — the language the voice uses to pronounce words and shape its accent.
* **Agent text** — the agent is automatically instructed to respond in the configured language. You do **not** need to add a "respond in X" instruction to your prompt.
The voice you select is still the primary determinant of accent — the language setting refines pronunciation rules and ensures the right speech recognition model is used.
## Pick a single language (recommended)
A single-language agent is the most accurate setup: the agent transcribes in one language only, the voice speaks with that language's pronunciation, and the agent always responds in that language — no language detection involved.
If no language is selected, the agent defaults to **English (US)**.
The chosen voice must support the chosen language. The dashboard hides unsupported combinations from the picker, with a tooltip explaining which voice or voice model is blocking it.
## Need more than one language?
For agents that serve callers in different languages, see the multilingual guide.
If you already know the caller's language at call time (for example, from CRM context or the dialed number), prefer overriding the language per call via the [inbound call webhook](/features/inbound-call-webhook). This gives you single-language accuracy on each call without limiting which customers the agent can serve.
Speech recognition often confuses closely related languages — for example **Cantonese vs. Mandarin Chinese**, or Cantonese mixed with English. If your callers speak these, keep the agent single-language and route each caller to the right agent (or set the language per call) instead of relying on auto-detect. See [Closely related languages are hard to distinguish](/agent/multilingual#closely-related-languages-are-hard-to-distinguish) for details.
## Supported languages
The language you pick must be supported by both your chosen **voice provider** (for pronunciation) and at least one **speech recognition provider** (for transcription). The combinations change as providers add coverage, so the dashboard is the source of truth.
On any agent page, open the language picker — the dropdown shows the live set of supported languages and updates as you change the voice provider, voice (and pinned voice model), and ASR provider. The same picker works in both single-language and multiselect modes. Unsupported combinations are greyed out, with a tooltip explaining which selection is blocking each one.
For a full per-language breakdown of which voice and speech recognition providers cover each language, see [Language support by provider](/build/language-support). Still picking your stack? See the [TTS provider comparison](/build/tts-provider-comparison) and [speech recognition providers](/build/asr-providers) for provider-level coverage and trade-offs.
For locale variants (for example, British English vs. US English, European Spanish vs. Latin American Spanish), the dashboard's language picker exposes the specific dialect codes when supported.
# Configure a multilingual agent
Source: https://docs.retellai.com/agent/multilingual
Configure a Retell multilingual agent: pick which languages it supports, understand ASR and TTS accuracy trade-offs, and when single-language is a better fit.
Use a multilingual agent when callers may speak in different languages and you can't determine which language ahead of time. If you can determine the language at the start of a call (for example, from CRM context or the dialed number), you'll get better accuracy by keeping the agent single-language and overriding the language per call via the [inbound call webhook](/features/inbound-call-webhook).
## Granular language selection
In the dashboard, switch the language selector to **Multiselect** and pick the exact set of languages the agent should support — for example, English (US) + Spanish (Spain).
What happens at call time:
* **Speech recognition** — the agent figures out which of the selected languages the caller is speaking and transcribes accordingly.
* **Voice pronunciation** — the agent detects the language of each response and uses the matching pronunciation. If detection fails, it falls back to the first language you selected. Not all voice providers handle accents.
* **Agent text** — the agent is allowed to respond in any of the selected languages and chooses based on what the caller speaks (and any instructions you give in the prompt).
## Accuracy trade-offs
Selecting multiple **variants of the same base language** (for example, `en-US` and `en-GB`) does **not** trigger the multilingual speech-recognition pipeline. The agent stays on the single-language path for that base, with no accuracy penalty.
Crossing language families (for example, `en-US` and `es-ES`) routes speech recognition to the multilingual pipeline, which is less accurate per language than single-language models. Pick the smallest set of languages you actually need.
From most to least accurate:
1. **Single language** — best accuracy. Use this whenever you can.
2. **Multiple variants of the same base language** (e.g., `en-US` + `en-GB`) — same accuracy as single language for that base.
3. **Multiple languages across families** (e.g., `en-US` + `es-ES`) — multilingual pipeline; some accuracy loss per language.
4. **Legacy Multilingual setting** — static list of supported languages, see below.
### Closely related languages are hard to distinguish
Speech recognition struggles most on languages that share vocabulary, phonemes, or a writing system — for example **Cantonese and Mandarin Chinese**, or Cantonese in a mix with English. In practice, callers routinely get transcribed as the wrong Chinese variant, which then makes the agent respond in the wrong language.
For these combinations:
* **Prefer single-language agents.** If you know the caller's language ahead of time — from CRM data, the dialed number, or a language-selection IVR step — spin up one agent per language and route callers to the right one, or override the language per call via the [inbound call webhook](/features/inbound-call-webhook).
* **Don't rely on auto-detect to separate close variants.** A multilingual agent that includes both Cantonese and Mandarin will mix them up frequently no matter which voice or ASR provider you pick.
* **English mixed with a Chinese variant is a separate constraint.** Not every ASR provider covers Cantonese, and even fewer cover Cantonese together with Mandarin or English in the multilingual pipeline — check the picker in the dashboard for currently supported combinations, and see [Language support by provider](/build/language-support) for a per-language breakdown.
## Legacy Multilingual setting
Older agents may still have the generic **Multilingual** setting selected. It is preserved so existing agents keep working, but the dashboard now flags it as a legacy setting:
> "Multilingual" is a legacy setting. Pick specific languages to update.
The legacy Multilingual setting covers these ten languages only: English (US), Spanish (ES), French (FR), German (DE), Hindi (IN), Russian (RU), Portuguese (PT), Japanese (JP), Italian (IT), Dutch (NL).
For new agents, pick the specific languages you need instead — narrower sets are more accurate.
## Picks the dashboard won't allow
Some combinations are blocked at selection time:
* **Voice doesn't support a language.** Each voice (and pinned voice model) only supports a subset of languages. Unsupported combinations are greyed out in the language picker, with a tooltip explaining which voice or voice model is blocking it. Pick a different voice to enable that language.
* **No speech-recognition provider covers the combination.** Some combinations of languages have no single speech-recognition provider that covers all of them together. The dashboard greys those languages out with the reason — either drop a language or split the use case across multiple agents.
# Set up versioning and tags for agents
Source: https://docs.retellai.com/agent/version
Use Retell agent versions and environment tags to lock in production configurations, track change history, and roll back drafts without breaking live calls.
Versioning lets you update an agent while keeping other versions unchanged for production use.
It has two main purposes:
1. **Lock in configuration**: published versions cannot be changed, and you can attach specific versions to phone numbers or environment tags to lock in the agent configuration.
2. **Version control & history**: you can create multiple draft versions from any past version, track your version history, and publish any draft when it's ready.
## How version numbers work
Version numbers start at **V0** and go up by one each time you create a new version. The dashboard labels tell you whether a version is live or still in progress.
| UI | Meaning |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `V0`, `V1`, `V2`, … | **Published** versions, immutable |
| `V3 (draft)`, `V4 (draft)`, … | **Draft** versions — unpublished copies that reserve the next version number. The **(draft)** suffix means you can still edit and publish them. |
**Example flow**
1. You publish your first agent → **V0** appears under **Published**.
2. You create a new draft from V0 → the UI shows **V1 (draft)** under **Draft**.
3. You publish that draft → **V1** moves to **Published** and is no longer editable.
4. You create another draft from V1 → **V2 (draft)** appears under **Draft**.
Multiple drafts can exist at the same time (for example **V2 (draft)** and **V3 (draft)**). Each version also shows which version it was branched from.
Published versions cannot be changed. Only versions labeled with **(draft)** can be edited.
## How to manage versions
Click the version button in the upper right corner of the agent page to open the versions panel.
The panel shows two sections:
* **Draft**: unpublished versions that can still be edited.
* **Published**
Versions with attached environment tags (e.g. `prod`, `staging`) are shown inline on the version entry.
### Create a new draft
Click the **+** button in the versions panel to create a new draft from the currently selected version. The draft will show which version it was branched from.
### Publish a draft
You can publish any draft version. Select the draft in the versions panel, then click the **Publish** button in the upper right corner.
After clicking **Publish**, a modal appears where you can add an optional version name and description. You can also check **Auto Create a New Draft** to automatically create a new draft version after publishing.
If a draft has no changes compared to the version it was branched from, the **Publish** button is disabled. Hovering over it shows **No changes to publish since V*N***, where V*N* is the base version. Make an edit to the draft to enable publishing again.
To attach phone numbers to a version, use the **Phone Numbers** tab in the dashboard.
### Delete a version
To delete a draft or published version, open the versions panel, select the version, and use the delete option. Published versions with active phone numbers or environment tags attached should have those removed before deleting.
## Environment Tags
Environment tags store environment-specific configs. Apply a tag (e.g. `prod`, `staging`) to a version to instantly load the right settings — and move the tag to a different version to deploy without manually rerouting phone numbers, dynamic variables, etc.
A version can have multiple tags attached. You can also filter [alert rules](/features/alerting-overview#filters) by environment tag to watch a metric only for the versions running in a given environment.
### How environment tags work
Each agent comes with `prod` and `staging` tags by default. You can create up to **10 tags total** per agent (including the defaults).
Tags serve two purposes:
1. **Labels** — tags appear directly on version entries in the panel, so you can see at a glance which versions are running in which environments (e.g. which version is `prod`, which is `staging`).
2. **Environment-specific config** — each tag carries its own set of dynamic variable values. When a tag is active on a version, those values are automatically injected, so the same agent behaves correctly across environments without duplicating configuration.
To deploy a new version, move the tag from the old version to the new one. Phone numbers, webhooks, and anything else pointing to that tag switch over instantly — no manual rerouting needed.
### Configure tags
Click the **Environment** button in the agent header to apply a tag to the current version, or select **Configure Tags** to manage your tags.
In the **Configure Environment Tags** modal you can:
* Create a new tag by clicking **+ Add** in the left sidebar and giving it a name.
* Define **environment dynamic variables** — key/value pairs that are injected into the agent when that tag is active. This lets the same agent behave differently across environments without duplicating configuration.
**Tag name requirements**
* Up to 20 characters
* May contain lowercase letters, numbers, `-`, or `_`
* Must start with a lowercase letter
* Cannot be `latest`
* Cannot match the pattern `v` followed by numbers only (e.g. `v3`, `v12`)
### Applying tags to a version
To attach a tag to a version, select the version in the versions panel, then click the **Environment** button and choose the tag. The tag label will appear on the version entry in the panel.
## How to use versions and tags in the API
You can pass a version reference in API calls such as `get_agent`, `get_retell_llm`, `create_web_call`, and `create_phone_number`.
A version reference can be:
* A version number, such as `2`
* `latest`
* An environment tag, such as `prod` or `staging`
When you pass an environment tag, Retell uses the version currently assigned to
that tag and applies the tag's dynamic variables. If the tag exists but is not
assigned to a version, Retell uses the latest version.
If you do not pass a version reference, the Retell SDK defaults to the latest
version of the agent.
```typescript Node theme={"dark"}
// Gets the version with version number 2.
const agent = await client.agent.retrieve("agent_id", { version: 2 });
// Gets the version assigned to the prod tag.
const prodAgent = await client.agent.retrieve("agent_id", { version: "prod" });
// Starts a web call with the version assigned to the staging tag.
const webCall = await client.call.createWebCall({
agent_id: "agent_id",
agent_version: "staging",
});
// Gets the latest version of the agent.
const latestAgent = await client.agent.retrieve("agent_id");
```
```python Python theme={"dark"}
# Gets the version with version number 2.
agent = client.agent.retrieve("agent_id", version=2)
# Gets the version assigned to the prod tag.
prod_agent = client.agent.retrieve("agent_id", version="prod")
# Starts a web call with the version assigned to the staging tag.
web_call = client.call.create_web_call(
agent_id="agent_id",
agent_version="staging",
)
# Gets the latest version of the agent.
latest_agent = client.agent.retrieve("agent_id")
```
# Compare agent versions
Source: https://docs.retellai.com/agent/version-comparison
Compare any two versions of a Retell agent side by side to review prompt, voice, tool, and flow changes before publishing or auditing past modifications.
You can compare any two versions of an agent to see exactly what changed between them. This is useful for reviewing changes before publishing, auditing past modifications, or understanding the differences between versions.
## How to Access Version Comparison
There are two ways to open the version comparison modal:
1. **From Version History**: Click the clock icon to open version history, hover over a version, and click the "Compare versions" button. This will compare the selected version against the current draft.
2. **From Publish Modal**: When publishing a new version, click the "Compare" button in the publish modal to see what's changing between the last published version and your current draft.
## Comparison Modes
The version comparison feature offers two different view modes to help you understand changes:
### Standard View (JSON Diff)
The standard view shows a traditional diff view with the full JSON configuration of both versions side by side.
**Features:**
* **Split view**: Shows the two versions in separate columns for easy comparison
* **Syntax highlighting**: JSON syntax is color-coded for readability
* **Show parent fields**: Toggle this option to see the parent JSON structure containing changes, making it easier to understand the context of modifications
### Semantic Diff
The semantic diff view provides a more human-readable summary of changes, organizing them by field.
**Features:**
* **Change summary**: Shows a count of additions, removals, and modifications at the top
* **Categorized by change type**: Changes are categorized as added (+), removed (−), or changed (\~)
* **Field paths**: Each change shows the full path to the modified field (e.g., `response_engine.prompt`)
* **Expandable details**: Click on complex values to expand and see the full content
* **Word-level diffs**: For long text changes, highlights the specific words that were added or removed
Use **Standard View** when you need to see the complete configuration context or verify exact JSON structure. Use **Semantic Diff** when you want a quick, scannable summary of what changed.
## What Gets Compared
When comparing versions, the modal shows differences across all components of your agent:
* **Agent configuration**: Basic agent settings and metadata
* **Response Engine**: For single/multi prompt agents, shows Retell LLM changes. For conversation flow agents, shows flow configuration changes
* **Related entities**: Any linked knowledge bases, functions, or other configurations
# Address metric issues
Source: https://docs.retellai.com/ai-qa/address-metric-issues
Step-by-step fixes for common AI QA metric issues — hallucinations, knowledge base mistakes, overlapping speech, latency, sentiment, and tool errors.
## Overview
When AI QA analyzes your calls, it flags specific metrics that didn't meet expectations. This page provides actionable guidance for addressing each type of metric issue to improve your agent's performance. For detailed definitions of each metric, see [AI QA metrics](/ai-qa/terminologies).
## AI Accuracy
### High Agent Hallucination Rate
When the agent generates incorrect or fabricated information not supported by the conversation context or knowledge base.
**How to fix:** Use the call QA sheet to see whether each instance is Fabrication, Contradiction, or Confusion, then apply the right fix:
* **Fabrication** (inventing facts): Add the correct information to your knowledge base or system prompt so the agent has it instead of guessing
* **Contradiction** (conflicting with provided info): Simplify or clarify conflicting instructions in your system prompt
* **Confusion** (misunderstanding user intent): Break complex instructions into simpler steps, or use conversation flow nodes with focused prompts
### Low KB (Knowledge Base) Recall
When relevant knowledge base chunks are not being retrieved when they should be.
**How to fix:**
* Reduce the KB retrieval threshold and increase the number of chunks to reduce false negatives (missed relevant chunks)
* Adjust these in your agent's [Knowledge Base](/build/knowledge-base) configuration; make small changes and monitor impact in later QA runs
## Response-engine issues
### High Node Transition Inaccuracy
When node transitions are inaccurate, the agent is moving to the wrong conversation state. This problem is specific to conversation flow agents, because only that engine type uses nodes and transitions between them.
**How to fix:**
* Clarify the transition conditions in your conversation flow node prompts
* Add examples that demonstrate the correct transition behavior for edge cases
* Keep transition prompts unambiguous and avoid overlapping conditions between nodes; see the [conversation flow debug guide](/build/conversation-flow/debug-guide) for a step-by-step walkthrough
### High Tool Call Inaccuracy
When the agent calls the wrong tools, misses required tool calls, or passes incorrect arguments. This problem is specific to single-prompt and multi-prompt agents, because only those engine types decide freely when and which tools to call.
**How to fix:**
* In your agent prompt, spell out when to call which tools (and when not to)
* In your [tool definitions](/build/add-function-calling), use clear names and descriptions and add examples for parameters so the agent can choose and invoke tools correctly
Tool Call Inaccuracy measures whether the agent made the right **decision** about which tools to call and with what arguments. For issues with tool **execution** failing (e.g., endpoint errors), see [Custom Tool Failures](#custom-tool-failures) below.
## Speech Quality
### Poor Agent Naturalness
When the agent sounds unnatural — issues like mispronunciation, robotic pacing, or audio artifacts.
**How to fix:**
* **Change voice**: Custom-cloned voices tend to have more naturalness issues; switching to a platform voice often improves stability
* **Adjust voice temperature**: Change the temperature setting to affect vocal expressiveness
* **Switch voice provider**: Different providers have different strengths (e.g., naturalness vs. specific languages or accents)
### Poor Agent Sentiment
When the agent's responses carry negative or inappropriate emotional tone.
**How to fix:**
* Adjust your system prompt with explicit tone guidelines (e.g., "respond warmly and helpfully")
* If using a conversation flow, check whether any node prompts produce overly terse or cold responses
* Reword dismissive phrases (e.g., "I can't help with that" → "Let me find another way to help")
## Transcription Quality
### High Word Error Rate (WER)
When the speech-to-text transcription has a high error rate, causing the agent to misunderstand what the user said.
**How to fix:**
* **Switch STT provider**: Choose a higher-accuracy speech-to-text provider for your use case
* **Check language settings**: Ensure the language setting matches the actual spoken language — mismatched settings significantly increase WER
* **Add custom vocabulary**: If your STT provider supports it, add frequently used names, technical terms, or domain-specific words as boosted keywords
* **Use Mistranscribed Entities feedback**: Review the mistranscribed entities surfaced by AI QA and add those specific terms as [boosted keywords](/reliability/wrong-transcript) in your STT configuration
* **Reduce background noise**: If the call environment is too noisy, try turning on background removal to improve transcription accuracy
## User Experience
### High User Negative Sentiment
When multiple user utterances show negative sentiment, the agent may not be handling the conversation well.
**How to fix:**
* Adjust your agent's system prompt to encourage more empathetic, friendly responses
* Add instructions for handling frustrated users (e.g., acknowledge concerns before offering solutions)
### High Overlapping Speech Count
Frequent overlapping speech may indicate latency or responsiveness issues. The fix depends on whether latency is also a problem:
| Scenario | How to fix |
| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **High latency (e2e p50 > 2.5s)** | Fix latency first. Choose faster models and lower-latency voice providers. When the agent takes too long to respond, users are more likely to talk over it. |
| **Normal latency** | Increase the agent's response wait time or interruption sensitivity. The agent may be starting to speak too quickly or not detecting when the user wants to continue. |
## Tool Execution
### Custom Tool Failures
When custom tool calls fail during a call.
**How to fix:**
* Check your tool endpoint logs for the failing call to see the specific error
* Ensure endpoints handle edge cases and return appropriate error responses
* Verify that tool response formats match the expected schema
* Add timeout handling and retry logic where appropriate; see [Function calling](/build/add-function-calling) for endpoint setup details
This metric measures whether your tool endpoints executed successfully. For issues with the agent choosing the wrong tools to call, see [High Tool Call Inaccuracy](#high-tool-call-inaccuracy) above.
### Transfer Call Issues
When transfer calls fail.
**How to fix:**
* Check the error log for the specific error — it usually indicates the cause
* **Telephony issues** (e.g. connection or configuration): Change the relevant settings or contact your telephony provider to resolve
* **No one picking up**: Review staffing levels during peak times; verify transfer destination numbers and that someone is available to receive the call
* **Human detection not working**: If you use [Warm Transfer](/build/conversation-flow/call-transfer-node) and the system fails to detect when a human has answered, try switching to [Agentic Warm Transfer](/build/conversation-flow/call-transfer-node), which uses a transfer agent to converse with the transfer target before bridging.
## Performance
### High Latency
When end-to-end latency is too high (e.g., p50 exceeds 2.5 seconds).
**How to fix:**
* Use the [latency breakdown](/reliability/troubleshoot-latency) in the call dashboard to find the bottleneck (LLM inference, TTS, network, etc.)
* If LLM inference is the bottleneck, switch to a faster model
* If TTS is the bottleneck, choose a lower-latency voice provider
* If tool calls are slow, optimize tool endpoints or reduce response size
## Custom Evaluation
### Failed Custom Evaluation Criteria
When one or more AI Evaluated Conditions fail.
**How to fix:**
* Use the failure reason in the call QA sheet to identify the gap, then update your agent's system prompt or knowledge base as needed
* If the failure was actually correct behavior (e.g., intentional by design), use [calibration](/ai-qa/terminologies#calibration) to override the evaluation for that call
Not all failures are actionable — some may be caused by external factors (e.g., the user hanging up early) or intentional design decisions (e.g., transferring when a user requests a human). Focus on failures where the agent's behavior or configuration can be improved.
**Calibration best practices:**
* Use calibration to correct edge cases where the automatic evaluation doesn't match your judgment
* If you find yourself calibrating many calls the same way, update your resolution criteria or metric thresholds instead — this is more efficient and applies to all future evaluations
* Add notes when calibrating to document why the override was needed, which helps your team maintain consistency
## Interpreting Your Results
When interpreting metrics, consider them in context:
* Compare metrics across similar cohorts or time periods
* Look for trends rather than focusing on individual data points
* Use multiple metrics together to get a complete picture of call quality
# Define a QA cohort
Source: https://docs.retellai.com/ai-qa/create-cohort
Define a Retell AI QA cohort: pick agents, date range, and call filters, then set a sampling rate to control evaluation volume and cost.
A cohort defines which calls AI QA evaluates and how many of them to sample. Configure it in the Create QA flow, then move on to [resolution criteria](/ai-qa/define-resolution-criteria).
Give the cohort a unique name that indicates its purpose or filters, so it's easy to find on the AI QA dashboard — for example, `High-value customers Q4` or `Support calls - week 1`.
Choose which calls to include based on the following filters:
* **Agents** — Select one or more agents whose calls you want to analyze. Use this to focus on a single agent or compare performance across several.
* **Date range** — The **start date** is required. The **end date** is optional; leave it blank to create a dynamic cohort that keeps adding new matching calls as they occur.
* **Call duration** — Include or exclude calls by length. Filter out very short calls (for example, under 30 seconds) that carry little signal, or focus on longer calls that need more analysis.
* **Disconnection reason** — Filter by [disconnection reason](/reliability/debug-call-disconnect).
* **Post Call Extraction** — Add custom filters based on your [Post Call Extraction](/features/post-call-analysis-overview) results.
Sampling controls how many of the filtered calls are actually analyzed, so you can manage volume and cost.
* **Percentage** — The share of matching calls to include. Setting 50% analyzes half of all calls that match your filters.
* **Weekly max** — A cap on how many calls are analyzed per week. Setting it to 100 keeps the cohort under 100 calls a week even if the percentage would allow more.
The weekly max is a ceiling. If the percentage yields fewer calls than the max, the percentage applies; if it yields more, the max applies.
Once your filters and sampling are set, click **Next** to move on to [resolution criteria](/ai-qa/define-resolution-criteria).
# Define resolution criteria
Source: https://docs.retellai.com/ai-qa/define-resolution-criteria
Define resolution criteria for an AI QA cohort using AI-evaluated conditions and performance metric thresholds to decide whether each call counts as successful.
Resolution criteria decide whether each call in your cohort counts as successful. This is the second step of the Create QA flow, after you [define the cohort](/ai-qa/create-cohort).
## What determines a successful call?
You define this using two kinds of criteria: **AI-evaluated conditions** and **performance metrics**. By default, a call counts as successful only when it meets every condition and every metric. Enable weighted scoring to change how these criteria combine.
An AI-evaluated condition is a qualitative check the AI runs against each call's transcript and context.
* **Name** — A short identifier, such as `Call resolved`, `Customer satisfied`, or `Issue escalated properly`.
* **Prompt description** — The prompt the AI uses to judge whether the condition is met, for example `AI agent was able to resolve the user's query`.
Write prompts that are specific about what success looks like, include context about the call type, and use clear, unambiguous language. Click **+ Add** to add more conditions — each is evaluated independently.
A performance metric is a quantitative threshold a call must meet. Select a metric and set its threshold:
* **Latency** — End-to-end delay between the user speaking and the agent responding.
* **User sentiment** — The caller's emotional state, inferred from speech content, tone, and pitch.
* **Agent sentiment** — The emotional tone of the agent's speech.
* **Overlapping speech** — Count of times the user and agent spoke at once.
* **Transcription** — Word error rate (WER) and count of mistranscribed entities.
* **Agent hallucination** — How often the agent hallucinated.
* **Tool call inaccuracy** — Rate at which the agent invoked the wrong tools.
* **Node transition inaccuracy** — Rate of incorrect node transitions.
* **Agent naturalness** — How human-like the agent sounded across pronunciation, intonation, pacing, and turn-taking.
For full definitions, see [AI QA metrics](/ai-qa/terminologies). Click **+ Add** to add more metrics.
Both AI-evaluated conditions and performance metrics are evaluated. Without weighted scoring, a call is successful only if it meets every condition and every metric.
By default, every condition and metric is required equally, so a call passes only if it meets all of them. Turn on **Weighted scoring** to prioritize some criteria over others instead.
When enabled, assign a weight to each condition and metric, then set the success threshold the weighted total must reach.
Use weighted scoring when some criteria matter more than others. For example, weight `Call resolved` higher than `Customer satisfied` if resolution is your primary goal.
Click **Save and Run QA** to start analysis. Once it finishes, review the output in [View QA results](/ai-qa/view-qa-results).
If saving fails, check that every required field is filled and every threshold is set, then resolve any validation messages before retrying.
# Access AI QA
Source: https://docs.retellai.com/ai-qa/get-started
Get started with Retell AI QA: open the AI QA tab from the sidebar and click Create QA to launch your first automated call quality evaluation.
To get started, click the **AI Quality Assurance** tab in the left sidebar. This opens the AI QA dashboard, where you can view existing cohorts or create new ones.
To start a new evaluation, click the **Create QA** button at the top right of the AI QA page.
This opens the Create QA flow. Continue with [Define a QA cohort](/ai-qa/create-cohort) to choose which calls to evaluate.
# Automatically evaluate call quality with AI QA
Source: https://docs.retellai.com/ai-qa/overview
AI QA scores Retell calls on hallucination, knowledge base accuracy, latency, sentiment, and tool usage to surface quality trends and issues.
AI QA automatically evaluates a sampled set of your calls against rules and metrics you configure. It surfaces high-level trends — average score, resolution rate, latency — and call-level diagnostics like hallucinations, knowledge base accuracy, overlapping speech, sentiment, and tool usage, each backed by transcript evidence.
## Use AI QA to
* Track call quality and resolution over time
* Identify failure patterns and their root causes
* Review individual calls with transcript-level evidence
For example, a health clinic runs AI QA on its appointment-booking agent to catch when the agent gives wrong hours or mishears a caller's name, then uses the [top questions](/ai-qa/view-qa-results#top-questions-from-users) view to see which caller intents resolve least often.
## How it works
Group the calls you want to evaluate by agent, date range, and other filters, and set how many to sample. See [Define a QA cohort](/ai-qa/create-cohort).
Set the AI conditions and metric thresholds that decide whether a call counts as successful. See [Define resolution criteria](/ai-qa/define-resolution-criteria).
Read aggregate trends, drill into individual calls, and act on flagged issues. See [View QA results](/ai-qa/view-qa-results) and [Address metric issues](/ai-qa/address-metric-issues).
New here? Start with [Access AI QA](/ai-qa/get-started). For definitions of every metric and term, see [AI QA metrics](/ai-qa/terminologies).
## Pricing
AI QA is free for the first 100 minutes of analyzed call time per workspace. After that, it's priced at \$0.10 per minute of analyzed call time.
# AI QA metrics
Source: https://docs.retellai.com/ai-qa/terminologies
Reference for every AI QA metric and term — average latency, hallucination rate, KB accuracy, overlapping speech, sentiment — and how each one is evaluated.
## Overview
This page explains every metric and term used in AI QA so you can interpret your call analysis results. For each metric, you’ll see what it measures and how it’s evaluated. When a metric fails your criteria, use [Address metric issues](/ai-qa/address-metric-issues) to find step-by-step guidance on how to fix it.
## Performance Metrics
### Latency
**Average Latency**: Measures the end-to-end delay between a user speaking and the Voice AI beginning its spoken response. Lower latency indicates more responsive interactions.
**Latency P50**: The 50th percentile (median) of latency measurements. This metric shows the typical response time, with half of all responses being faster and half being slower.
Latency is measured in seconds (s). Lower values indicate better performance.
### Sentiment Analysis
**User Sentiment**: Represents the emotional state of the caller as inferred from speech content, tone, and pitch. Sentiment can be positive, negative, or neutral.
* **User Positive Sentiment Rate**: Percentage of user interactions with positive sentiment
* **User Negative Sentiment Rate**: Percentage of user interactions with negative sentiment
* **Negative Sentiment Rate**: Overall rate of negative sentiment detected in the conversation
**Agent Sentiment**: Represents the emotional tone expressed by the Voice AI during speech output. This metric helps ensure your agent maintains an appropriate tone throughout conversations.
* **Agent Positive Sentiment Rate**: Percentage of agent responses with positive sentiment
* **Agent Natural Tonality Rate**: Measures how natural and human-like the agent's tone sounds
### Transcription Metrics
**WER (Word Error Rate)**: Measures the accuracy of speech-to-text transcription by calculating the percentage of words that were incorrectly transcribed. Lower WER indicates better transcription accuracy.
WER is calculated as: (Substitutions + Insertions + Deletions) / Total Words in Reference × 100%
The reference is produced by re-transcribing the user's audio using a process that listens to the audio and uses the original STT transcript as reference. This usually yields a much more accurate transcript than the initial STT output, though the reference can still contain errors.
WER measures how much the original STT transcript diverges from this reference.
**Mistranscribed Entities**: Count of specific entities (names, dates, numbers, etc.) that were incorrectly transcribed during the call. Only critical factual errors that change meaning are counted.
### Call Quality Metrics
**Overlapping Speech**: Count of times the user and agent spoke at the same time during the conversation. Higher counts may indicate the agent is speaking too long or not responding appropriately.
**Avg. Overlapping Speech**: Average number of overlapping speech instances per call across the cohort.
**Agent Naturalness**: Measures how human-like the agent sounded, including pronunciation, intonation, pacing, turn-taking behavior, and the absence of robotic patterns. Higher values indicate more natural-sounding speech.
Agent Naturalness is evaluated using **both the audio and the transcript**. The evaluation looks for issues that would affect how understandable or natural the agent sounds, such as:
* Robotic glitches, distortion, or unexpected volume changes
* Mispronunciation or word substitution that differs from what was intended
* Slurring, mumbling, or dropped sounds
* Speech speed or intonation that sounds unnatural or alters meaning
Only clear issues that would confuse a listener are flagged; minor robotic tone or technical phrasing is not counted against the metric.
**Natural Tonality Rate** = (utterances without naturalness issues / total agent utterances) × 100%
**Natural Tonality Rate**: Percentage of agent speech that sounds natural and human-like in tone and delivery.
### AI Accuracy Metrics
**LLM Hallucination Rate**: Measures how often the Large Language Model (LLM) generated incorrect or fabricated information that wasn't supported by the conversation context or knowledge base.
**Agent Hallucination**: Measures how often the agent hallucinated during conversations. This is a critical metric for ensuring factual accuracy.
Hallucination is evaluated **per agent turn** — each response is checked against the agent's instructions, knowledge base, and conversation history. The rate is the percentage of agent turns that contain a hallucination.
**Types of hallucination:**
* **Fabrication**: Invents factual claims not supported by the context (e.g., making up a case number)
* **Contradiction**: States something that conflicts with what was provided or said earlier
* **Confusion**: Misunderstands instructions and gives factually irrelevant or logically wrong information
Severity can be **major** (critical false information that could cause a failed resolution) or **minor** (non-critical discrepancies). The evaluation focuses on factual accuracy only, not style or tone.
High hallucination rates indicate the agent may be providing incorrect information to users, which can damage trust and lead to poor outcomes.
### Knowledge Base Metrics
**KB (Knowledge Base) Recall**: Measures how effectively the agent retrieved and used relevant information from the knowledge base. Higher recall indicates better knowledge base utilization.
KB Recall is evaluated per retrieval turn. For each turn where the knowledge base is queried, the system determines which retrieved chunks were truly relevant to the user's question.
* A **"Hit"** (full recall) means all relevant chunks were retrieved for that turn
* A **"Miss"** means one or more relevant chunks were missed
**KB Recall Rate** = (turns with full recall / total retrieval turns) × 100%
Relevance is judged strictly: a chunk counts as relevant only if it provides specific, actionable information or instructions that apply to what the user was asking, not general or marketing-style content.
### Tool and Function Metrics
**Tool Call Accuracy**: Measures the rate at which the agent correctly invoked tools or functions. Higher accuracy means the agent is using the right tools at the right time.
**Tool Call Inaccuracy**: Measures the rate at which the agent invoked incorrect tools. This is the inverse of Tool Call Accuracy.
**Custom Tool Success Rate**: Percentage of custom tool calls that completed successfully.
**Avg Custom Tool Latency**: Average time taken for custom tools to execute and return results.
### Conversation Flow Metrics
**Transition Accuracy**: Measures the accuracy of transitions between conversation nodes or states. Higher accuracy indicates the agent is following the intended conversation flow correctly.
**Node Transition Inaccuracy**: Measures incorrect node transitions in conversation flows. This metric helps identify when the agent moves to the wrong conversation state.
### Call Resolution Metrics
**Call Resolution Rate**: Percentage of calls that were successfully resolved according to your defined resolution criteria.
**Average Score**: Overall quality score for calls in the cohort, calculated based on your resolution criteria and weighted scoring configuration.
**Calls Analyzed**: Total number of calls that have been analyzed in the cohort.
### Transfer Metrics
**Transfer Success Rate**: Percentage of calls that were successfully transferred to another agent or system.
**Transfer Wait Time**: Average time users wait before a transfer is completed.
## Call-Level Data
### Call Identification
**Call ID**: Unique identifier for each individual call in the system.
**Call Start Time**: Timestamp indicating when a call began.
**Call Length**: Total duration of the call, typically measured in seconds or minutes.
### Evaluation Status
**Eval**: Evaluation status or score for individual calls, indicating whether the call met the defined resolution criteria.
## Statistical Terms
### Percentiles
**P50 (50th Percentile)**: The median value, where half of all measurements are above and half are below. Also known as the median.
Percentiles help understand the distribution of metrics. P50 shows typical performance, while P95 or P99 show worst-case scenarios.
## Cohort Terms
### Cohort
A **Cohort** is a filtered set of calls that share common characteristics (agents, date range, call duration, etc.) and are analyzed together using the same resolution criteria.
### Sampling
**Sampling Percentage**: The percentage of calls matching your filters that will be included in the cohort for analysis.
**Weekly Max**: Maximum number of calls that can be analyzed per week, regardless of the sampling percentage.
## Resolution Criteria Terms
### AI Evaluated Condition
Custom criteria evaluated by AI based on call transcripts and context. These are qualitative assessments (e.g., "Call resolved", "Customer satisfied") rather than quantitative metrics.
### Performance Metric
Quantitative thresholds that calls must meet, such as latency below 2 seconds or sentiment above 80%.
### Weighted Scoring
A scoring system that assigns different weights to various resolution criteria, allowing you to prioritize certain conditions or metrics over others.
### Calibration
**Calibrate to Success / Calibrate to Failure**: Manual overrides that let you adjust automatic metric evaluations for a specific call.
* **Calibrate to Success**: Mark a failed metric as passed — the failure no longer counts against the call's score
* **Calibrate to Failure**: Mark a passed metric as failed — the pass no longer contributes to the call's score
**Important:** Calibration updates the **per-call score only**. It does **not** change the underlying metric criteria or scoring logic for future calls. Each calibration applies to the specific call you are reviewing.
Some metrics may show "N/A" when there isn't sufficient data or when the metric doesn't apply to a particular call type (e.g., transfer metrics for non-transfer calls).
# View QA results
Source: https://docs.retellai.com/ai-qa/view-qa-results
Read your Retell AI QA results: review aggregate metrics and trends, drill into individual calls, inspect transcripts with QA diagnostics, and calibrate scores.
After you [create a cohort](/ai-qa/create-cohort) and run analysis, the QA results give you two views: **Call QA Overview** for high-level trends and **Detailed Calls** for individual call analysis. From either, you can drill into a single call to review its diagnostics, errors, and transcript.
The dashboard shows summary metrics across all analyzed calls, trend charts over time, individual call records with scores, and call-level QA sheets with transcript analysis. For a definition of any metric named below, see [AI QA metrics](/ai-qa/terminologies).
## Call QA Overview tab
The **Call QA Overview** tab gives a high-level view of the cohort's performance through summary metrics and trend charts.
### Summary metrics
The top section shows key metrics in a grid:
* **Calls Analyzed** — Total calls analyzed in the cohort
* **Average Score** — Overall quality score based on your resolution criteria
* **Call Resolution Rate** — Percentage of calls successfully resolved
* **Transfer Success Rate** — Percentage of calls transferred successfully to another agent or system
* **Transfer Wait Time** — Average time users wait before a transfer completes
* **Average Latency** — Mean response time across all calls
* **LLM Hallucination Rate** — Percentage of calls with AI-generated inaccuracies
* **KB (Knowledge Base) Recall** — How effectively the knowledge base was retrieved
* **Negative Sentiment Rate** — Percentage of interactions with negative sentiment
* **WER (Word Error Rate)** — Transcription accuracy
* **Avg. Overlapping Speech** — Average overlapping-speech instances per call
* **Tool Call Accuracy** — Rate of correct tool invocations
* **Transition Accuracy** — Accuracy of conversation flow transitions
* **Agent Natural Tonality Rate** — Percentage of natural-sounding agent speech
* **Agent Positive Sentiment Rate** — Percentage of agent responses with positive sentiment
* **Avg Custom Tool Latency** — Average time custom tools take to return
* **Custom Tool Success Rate** — Percentage of custom tool calls that completed successfully
### Top questions from users
A table shows the questions callers asked most often, alongside each one's resolution rate.
AI QA groups similar questions into one row — for example, "What are your office hours?" and "What time do you open?" are counted together.
## Detailed Calls tab
The **Detailed Calls** tab is a table of every analyzed call, with sortable columns and per-call metrics.
### Calls table
Each row shows: Call ID, Evaluation Result, Call Start Time, Call Length, LLM Hallucination Rate, KB Recall, Transition Accuracy, User Positive Sentiment Rate, Latency P50, Overlapping Speech Count, WER, Tool Call Accuracy, and Natural Tonality Rate.
### Sorting and filtering
* **Sort** — Click any column header to sort by that metric.
* **Filter** — Use the Filter button to apply date ranges, score thresholds, and more.
Use the ellipsis menu (⋯) in the Action column to rerun QA for a call or delete a call from QA.
## Call-level QA sheet
Click any row in the Detailed Calls table to open its **Call QA Sheet**, which shows the full diagnostics for that call.
### QA result overview
The sheet shows:
* **Overall Score** — Pass/fail status with a numerical score
* **Passed metrics** — Metrics that met their thresholds (green checkmarks)
* **Failed metrics** — Metrics that missed their thresholds (orange warning triangles)
For step-by-step fixes when a metric fails, see [Address metric issues](/ai-qa/address-metric-issues).
### Calibrate a call
You can override an individual call's evaluation by [calibrating](/ai-qa/terminologies#calibration) it:
* **Mark a passed metric as failed** if it should have failed
* **Mark a failed metric as passed** if it should have passed
You can also add custom notes to any call QA. Calibration changes the per-call score only; it doesn't change the criteria for future calls.
### Transcript and errors
The sheet also gives you the full call transcript, specific transcription errors with corrections, and highlights marking where errors occurred.
Low scores or high failure rates can point to systemic issues in your agent configuration, prompts, or knowledge base. Review several failed calls to find the pattern before making changes.
# Add Knowledge Base Sources
Source: https://docs.retellai.com/api-references/add-knowledge-base-sources
openapi-final post /add-knowledge-base-sources/{knowledge_base_id}
Add sources to a knowledge base
```javascript Javascript theme={"dark"}
import Retell from 'retell-sdk';
import fs from 'fs';
const client = new Retell({
apiKey: 'YOUR_RETELL_API_KEY',
});
async function main() {
const knowledgeBaseResponse = await client.knowledgeBase.addSources(
"knowledge_base_xxxxxxxxxxxx",
{
knowledge_base_texts: [
{
title: "Sample Question",
text: "Hello, how are you?",
},
],
knowledge_base_urls: [
"https://www.retellai.com",
"https://docs.retellai.com",
],
knowledge_base_files: [
fs.createReadStream("./sample.txt"),
],
}
);
console.log(knowledgeBaseResponse.knowledge_base_id);
}
main();
```
```Python Python theme={"dark"}
from retell import Retell
client = Retell(
api_key="YOUR_RETELL_API_KEY",
)
file = open("./sample.txt", "rb")
knowledge_base_response = client.knowledge_base.add_sources(
knowledge_base_id="knowledge_base_xxxxxxxxxxxx",
knowledge_base_texts=[
{
"title": "Sample Question",
"text": "Hello, how are you?",
},
],
knowledge_base_urls=[
"https://www.retellai.com",
"https://docs.retellai.com",
],
knowledge_base_files=[file],
)
print(knowledge_base_response.knowledge_base_id)
file.close()
```
```bash cURL theme={"dark"}
curl --request POST \
--url https://api.retellai.com/add-knowledge-base-sources/knowledge_base_xxxxxxxxxxxx \
--header 'Authorization: Bearer YOUR_RETELL_API_KEY' \
--form 'knowledge_base_texts=[{"title": "Sample Question", "text": "Hello, how are you?"}]' \
--form 'knowledge_base_urls=["https://www.retellai.com", "https://docs.retellai.com"]' \
--form 'knowledge_base_files=@./sample.txt'
```
# Add Voice
Source: https://docs.retellai.com/api-references/add-voice
openapi-final post /add-community-voice
Add a community voice to the voice library
# Agent Playground Completion
Source: https://docs.retellai.com/api-references/agent-playground-completion
openapi-final post /agent-playground-completion/{agent_id}
Stateless playground completion. Send the full conversation history (same shape as chat completion messages) and receive only the newly generated messages. Nothing is persisted server-side — the caller manages conversation state.
# Backfill Contact Analysis Data
Source: https://docs.retellai.com/api-references/backfill-contact-analysis-data
openapi-final post /backfill-contact-analysis-data
# Clone Voice
Source: https://docs.retellai.com/api-references/clone-voice
openapi-final post /clone-voice
Clone a voice from audio files
# Create Voice Agent
Source: https://docs.retellai.com/api-references/create-agent
openapi-final post /create-agent
Create a new agent
# Create Draft Agent Version
Source: https://docs.retellai.com/api-references/create-agent-version
openapi-final post /create-agent-version/{agent_id}
Create a new draft agent version from a base version.
# Create App
Source: https://docs.retellai.com/api-references/create-app
openapi-final post /create-app
Create an App: the connection to one external system (a CRM, calendar, support desk, and so on), holding its credentials and settings. Providers with caller-managed credentials accept auth_config. Providers using the OAuth callback must omit auth_config and be authorized through connect-app. Credentials are stored encrypted and never returned. Up to 20 apps per provider.
# Create Batch Call
Source: https://docs.retellai.com/api-references/create-batch-call
openapi-final post /create-batch-call
Create a batch call
# Create Batch Test
Source: https://docs.retellai.com/api-references/create-batch-test
openapi-final post /create-batch-test
Create a batch test to run multiple test cases
# Create Chat
Source: https://docs.retellai.com/api-references/create-chat
openapi-final post /create-chat
Create a chat session
# Create Chat Agent
Source: https://docs.retellai.com/api-references/create-chat-agent
openapi-final post /create-chat-agent
Create a new chat agent
# Create Draft Chat Agent Version
Source: https://docs.retellai.com/api-references/create-chat-agent-version
openapi-final post /create-agent-version/{agent_id}
Create a new draft agent version from a base version.
# Create Chat Completion
Source: https://docs.retellai.com/api-references/create-chat-completion
openapi-final post /create-chat-completion
Create a chat completion message
# Create Contact
Source: https://docs.retellai.com/api-references/create-contact
openapi-final post /create-contact
Create a new contact.
# Create Conversation Flow
Source: https://docs.retellai.com/api-references/create-conversation-flow
openapi-final post /create-conversation-flow
Create a new Conversation Flow that can be attached to an agent. This is used to generate response output for the agent.
# Create Conversation Flow Subflow
Source: https://docs.retellai.com/api-references/create-conversation-flow-component
openapi-final post /create-conversation-flow-component
Create a new shared conversation flow component
# Create Knowledge Base
Source: https://docs.retellai.com/api-references/create-knowledge-base
openapi-final post /create-knowledge-base
Create a new knowledge base
```javascript Javascript theme={"dark"}
import Retell from 'retell-sdk';
const client = new Retell({
apiKey: 'YOUR_RETELL_API_KEY',
});
async function main() {
const knowledgeBaseResponse = await client.knowledgeBase.create({
knowledge_base_name: "Sample KB",
knowledge_base_texts: [
{
text: "Hello, how are you?",
title: "Sample Question",
},
],
knowledge_base_urls: [
"https://www.retellai.com",
"https://docs.retellai.com",
],
knowledge_base_files: [
fs.createReadStream("../sample.txt"),
],
});
console.log(knowledgeBaseResponse.knowledge_base_id);
}
main();
```
```Python Python theme={"dark"}
from retell import Retell
client = Retell(
api_key="YOUR_RETELL_API_KEY",
)
file = open("./test_data.txt", "rb")
knowledge_base_response = client.knowledge_base.create(
knowledge_base_name="Sample KB",
knowledge_base_texts=[
{
"text": "Hello, how are you?",
"title": "Sample Question",
},
],
knowledge_base_urls=[
"https://www.retellai.com",
"https://docs.retellai.com",
],
knowledge_base_files=[file],
)
print(knowledge_base_response.knowledge_base_id)
file.close()
```
```bash cURL theme={"dark"}
curl --request POST \
--url https://api.retellai.com/create-knowledge-base \
--header 'Authorization: Bearer ' \
--form 'knowledge_base_name=Sample KB' \
--form 'knowledge_base_texts=[{"title": "Sample Question", "text": "Hello, how are you?"}]' \
--form 'knowledge_base_urls=["https://www.retellai.com", "https://docs.retellai.com"]' \
--form 'knowledge_base_files=@./test_data.txt'
```
# Create Phone Call
Source: https://docs.retellai.com/api-references/create-phone-call
openapi-final post /v2/create-phone-call
Create a new outbound phone call
# Create Phone Number
Source: https://docs.retellai.com/api-references/create-phone-number
openapi-final post /create-phone-number
Buy a new phone number & Bind agents
# Create Retell LLM
Source: https://docs.retellai.com/api-references/create-retell-llm
openapi-final post /create-retell-llm
Create a new Retell LLM Response Engine that can be attached to an agent. This is used to generate response output for the agent.
# Create Outbound SMS
Source: https://docs.retellai.com/api-references/create-sms-chat
openapi-final post /create-sms-chat
Start an outbound SMS chat conversation with a phone number using the specified agent. The agent must be configured for chat mode. The initial SMS message will be automatically generated and sent based on the agent's configuration.
# Create Test Case Definition
Source: https://docs.retellai.com/api-references/create-test-case-definition
openapi-final post /create-test-case-definition
Create a new test case definition
# Create Web Call
Source: https://docs.retellai.com/api-references/create-web-call
openapi-final post /v3/create-web-call
Create a new web call and return browser connection details.
# Delete Agent
Source: https://docs.retellai.com/api-references/delete-agent
openapi-final delete /delete-agent/{agent_id}
Delete an existing agent
# Delete Agent Version
Source: https://docs.retellai.com/api-references/delete-agent-version
openapi-final delete /delete-agent-version/{agent_id}
Delete a specific agent version.
# Delete App
Source: https://docs.retellai.com/api-references/delete-app
openapi-final delete /delete-app/{app_id}
Delete an App. Fails when agents or knowledge bases still reference it, unless force_delete is set. If a CRM config is linked to this App, the link is cleared.
# Delete Call
Source: https://docs.retellai.com/api-references/delete-call
openapi-final delete /v2/delete-call/{call_id}
Delete a specific call and its associated data
# Delete Chat
Source: https://docs.retellai.com/api-references/delete-chat
openapi-final delete /delete-chat/{chat_id}
Delete an existing chat
# Delete Chat Agent
Source: https://docs.retellai.com/api-references/delete-chat-agent
openapi-final delete /delete-chat-agent/{agent_id}
Delete an existing chat agent
# Delete Chat Agent Version
Source: https://docs.retellai.com/api-references/delete-chat-agent-version
openapi-final delete /delete-agent-version/{agent_id}
Delete a specific agent version.
# Delete Contact
Source: https://docs.retellai.com/api-references/delete-contact
openapi-final delete /delete-contact/{contact_id}
Delete a contact. A contact linked to a record in a connected CRM cannot be deleted while two-way sync is active — unlink the CRM app first, otherwise the next sync would recreate it.
# Delete Conversation Flow
Source: https://docs.retellai.com/api-references/delete-conversation-flow
openapi-final delete /delete-conversation-flow/{conversation_flow_id}
Delete a conversation flow and all its versions
# Delete Conversation Flow Subflow
Source: https://docs.retellai.com/api-references/delete-conversation-flow-component
openapi-final delete /delete-conversation-flow-component/{conversation_flow_component_id}
Delete a shared conversation flow component. When deleting a shared component, creates local copies for all linked conversation flows.
# Delete Knowledge Base
Source: https://docs.retellai.com/api-references/delete-knowledge-base
openapi-final delete /delete-knowledge-base/{knowledge_base_id}
Delete an existing knowledge base
# Delete Knowledge Base Source
Source: https://docs.retellai.com/api-references/delete-knowledge-base-source
openapi-final delete /delete-knowledge-base-source/{knowledge_base_id}/source/{source_id}
Delete an existing source from knowledge base
# Delete Phone Number
Source: https://docs.retellai.com/api-references/delete-phone-number
openapi-final delete /delete-phone-number/{phone_number}
Delete an existing phone number
# Delete Retell LLM
Source: https://docs.retellai.com/api-references/delete-retell-llm
openapi-final delete /delete-retell-llm/{llm_id}
Delete an existing Retell LLM Response Engine
# Delete Test Case Definition
Source: https://docs.retellai.com/api-references/delete-test-case-definition
openapi-final delete /delete-test-case-definition/{test_case_definition_id}
Delete a test case definition
# End Chat
Source: https://docs.retellai.com/api-references/end-chat
openapi-final patch /end-chat/{chat_id}
End an ongoing chat
# Get Voice Agent
Source: https://docs.retellai.com/api-references/get-agent
openapi-final get /get-agent/{agent_id}
Retrieve details of a specific agent
# Get App
Source: https://docs.retellai.com/api-references/get-app
openapi-final get /get-app/{app_id}
Get an App by id.
# Get Backfill Job Status
Source: https://docs.retellai.com/api-references/get-backfill-contact-job-status
openapi-final get /get-backfill-contact-job-status
Get the status of the contact analysis data backfill job.
# Get Batch Test
Source: https://docs.retellai.com/api-references/get-batch-test
openapi-final get /get-batch-test/{test_case_batch_job_id}
Get a batch test job by ID
# Get Call
Source: https://docs.retellai.com/api-references/get-call
openapi-final get /v2/get-call/{call_id}
Retrieve details of a specific call
# Get Chat
Source: https://docs.retellai.com/api-references/get-chat
openapi-final get /get-chat/{chat_id}
Retrieve details of a specific chat
# Get Chat Agent
Source: https://docs.retellai.com/api-references/get-chat-agent
openapi-final get /get-chat-agent/{agent_id}
Retrieve details of a specific chat agent
# Get Concurrency
Source: https://docs.retellai.com/api-references/get-concurrency
openapi-final get /get-concurrency
Get the current concurrency and concurrency limit of the org
# Get Contact
Source: https://docs.retellai.com/api-references/get-contact
openapi-final get /get-contact/{contact_id}
Retrieve a contact by ID.
# Get Contact by Phone
Source: https://docs.retellai.com/api-references/get-contact-by-phone
openapi-final get /get-contact-by-phone/{phone_number}
Retrieve a contact by phone number. At most one contact exists per phone number in an organization.
# Get Conversation Flow
Source: https://docs.retellai.com/api-references/get-conversation-flow
openapi-final get /get-conversation-flow/{conversation_flow_id}
Retrieve details of a specific Conversation Flow
# Get Conversation Flow Subflow
Source: https://docs.retellai.com/api-references/get-conversation-flow-component
openapi-final get /get-conversation-flow-component/{conversation_flow_component_id}
Get a shared conversation flow component
# Get CRM Config
Source: https://docs.retellai.com/api-references/get-crm-config
openapi-final get /get-crm-config
Get the organization's CRM configuration: which CRM app is linked, the custom contact fields defined for it, and how post-call analysis data is written back to contacts. Returns an empty configuration when nothing has been set up yet.
# Get CRM Schema
Source: https://docs.retellai.com/api-references/get-crm-schema
openapi-final get /get-crm-schema
Get the contact schema of the connected CRM: the fields available on its contact object, which are the values that sync mappings can reference.
# Get Knowledge Base
Source: https://docs.retellai.com/api-references/get-knowledge-base
openapi-final get /get-knowledge-base/{knowledge_base_id}
Retrieve details of a specific knowledge base
# Get MCP Tools
Source: https://docs.retellai.com/api-references/get-mcp-tools
openapi-final get /get-mcp-tools/{agent_id}
Get MCP tools for a specific agent
# Get Phone Number
Source: https://docs.retellai.com/api-references/get-phone-number
openapi-final get /get-phone-number/{phone_number}
Retrieve details of a specific phone number
# Get Retell LLM
Source: https://docs.retellai.com/api-references/get-retell-llm
openapi-final get /get-retell-llm/{llm_id}
Retrieve details of a specific Retell LLM Response Engine
# Get Sync Job Status
Source: https://docs.retellai.com/api-references/get-sync-job-status
openapi-final get /get-sync-job-status
Get the status of the organization's contact sync, whether it was started manually or by the schedule. Returns status `idle` when no sync is running.
# Get Test Case Definition
Source: https://docs.retellai.com/api-references/get-test-case-definition
openapi-final get /get-test-case-definition/{test_case_definition_id}
Get a test case definition by ID
# Get Test Run
Source: https://docs.retellai.com/api-references/get-test-run
openapi-final get /get-test-run/{test_case_job_id}
Get a test case job (test run) by ID
# Get Voice
Source: https://docs.retellai.com/api-references/get-voice
openapi-final get /get-voice/{voice_id}
Retrieve details of a specific voice
# Import Phone Number
Source: https://docs.retellai.com/api-references/import-phone-number
openapi-final post /import-phone-number
Import a phone number from custom telephony & Bind agents
**SIP trunk fields are flat on the request body.** Pass `termination_uri`, `sip_trunk_auth_username`, `sip_trunk_auth_password`, and `transport` as top-level properties — not nested inside a `sip_outbound_trunk_config` object.
The response returns those same fields grouped under a `sip_outbound_trunk_config` object (see [Get Phone Number](/api-references/get-phone-number)), which is why the shapes look different between request and response.
# List Agent Versions
Source: https://docs.retellai.com/api-references/list-agent-versions
openapi-final get /list-agent-versions/{agent_id}
List stored versions of a voice or chat agent with pagination.
The response contains version summaries for either a voice or chat agent. To retrieve a version's full configuration, use [Get Agent](/api-references/get-agent) or [Get Chat Agent](/api-references/get-chat-agent) with that version.
The response is paginated. Read version summaries from `items`, then pass the returned `pagination_key` while `has_more` is `true`.
# List Voice Agents
Source: https://docs.retellai.com/api-references/list-agents
openapi-final post /v2/list-agents
List unique agents with pagination.
Use `filter_criteria.channel` with `value: "voice"` to list only voice agents.
The response is paginated. Read agents from `items`, then pass the returned `pagination_key` on the next request while `has_more` is `true`.
```javascript Javascript theme={"dark"}
import Retell from 'retell-sdk';
const client = new Retell({
apiKey: 'YOUR_RETELL_API_KEY',
});
const agents = await client.agent.list({
filter_criteria: {
channel: {
op: 'eq',
value: 'voice',
},
},
});
console.log(agents.items);
```
```Python Python theme={"dark"}
from retell import Retell
client = Retell(api_key="YOUR_RETELL_API_KEY")
agents = client.agent.list(
filter_criteria={
"channel": {
"op": "eq",
"value": "voice",
},
},
)
print(agents.items)
```
```bash cURL theme={"dark"}
curl --request POST \
--url 'https://api.retellai.com/v2/list-agents?limit=50' \
--header 'Authorization: Bearer YOUR_RETELL_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"filter_criteria": {
"channel": {
"op": "eq",
"value": "voice"
}
}
}'
```
# List App Usages
Source: https://docs.retellai.com/api-references/list-app-usages
openapi-final get /list-app-usages/{app_id}
List the agents and knowledge bases referencing an App, most recently configured first by default.
# List Apps
Source: https://docs.retellai.com/api-references/list-apps
openapi-final get /list-apps
List Apps in the organization (paginated).
# List Batch Tests
Source: https://docs.retellai.com/api-references/list-batch-tests
openapi-final get /v2/list-batch-tests
List batch test jobs with pagination
# List Calls
Source: https://docs.retellai.com/api-references/list-calls
openapi-final post /v3/list-calls
List calls with unified cursor pagination response.
`POST /v3/list-calls` is the current list-calls endpoint. It supersedes the legacy [`GET /list-calls`](/api-references/list-calls_deprecated) — there is no `v2/list-calls`. To keep responses lean, the v3 payload omits `transcript`, `transcript_object`, `transcript_with_tool_calls`, and `recording_url`; call [`GET /v1/get-call/{call_id}`](/api-references/get-call) with a specific `call_id` to fetch those fields.
# List Chat Agents
Source: https://docs.retellai.com/api-references/list-chat-agents
openapi-final post /v2/list-agents
List unique agents with pagination.
Use `filter_criteria.channel` with `value: "chat"` to list only chat agents.
The response is paginated. Read agents from `items`, then pass the returned `pagination_key` on the next request while `has_more` is `true`.
```javascript Javascript theme={"dark"}
import Retell from 'retell-sdk';
const client = new Retell({
apiKey: 'YOUR_RETELL_API_KEY',
});
const agents = await client.agent.list({
filter_criteria: {
channel: {
op: 'eq',
value: 'chat',
},
},
});
console.log(agents.items);
```
```Python Python theme={"dark"}
from retell import Retell
client = Retell(api_key="YOUR_RETELL_API_KEY")
agents = client.agent.list(
filter_criteria={
"channel": {
"op": "eq",
"value": "chat",
},
},
)
print(agents.items)
```
```bash cURL theme={"dark"}
curl --request POST \
--url 'https://api.retellai.com/v2/list-agents?limit=50' \
--header 'Authorization: Bearer YOUR_RETELL_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"filter_criteria": {
"channel": {
"op": "eq",
"value": "chat"
}
}
}'
```
# List Chats
Source: https://docs.retellai.com/api-references/list-chats
openapi-final post /v3/list-chats
List chats with unified cursor pagination response.
# List Contact Conversations
Source: https://docs.retellai.com/api-references/list-contact-conversations
openapi-final get /list-contact-conversations/{contact_id}
List a contact's conversations (inbound calls, outbound calls, and chats) merged into a single timeline, most recent first. Results are matched by the contact's phone number. Use the returned `pagination_key` to fetch the next page.
# List Contacts
Source: https://docs.retellai.com/api-references/list-contacts
openapi-final post /list-contacts
List contacts, newest conversation first by default, with the total count of matches alongside the page. Page through results with `pagination_key`; `skip` is available for offset-style paging but is slower on large contact sets and can repeat or miss rows as contacts are updated.
# List Conversation Flow Subflows
Source: https://docs.retellai.com/api-references/list-conversation-flow-components
openapi-final get /v2/list-conversation-flow-components
List shared conversation flow components with pagination
# List Conversation Flows
Source: https://docs.retellai.com/api-references/list-conversation-flows
openapi-final get /v2/list-conversation-flows
List conversation flows with pagination
# List Export Requests
Source: https://docs.retellai.com/api-references/list-export-requests
openapi-final get /v2/list-export-requests
List export requests with pagination
# List Knowledge Bases
Source: https://docs.retellai.com/api-references/list-knowledge-bases
openapi-final get /list-knowledge-bases
List all knowledge bases
# List Phone Numbers
Source: https://docs.retellai.com/api-references/list-phone-numbers
openapi-final get /v2/list-phone-numbers
List phone numbers with pagination
# List Retell LLMs
Source: https://docs.retellai.com/api-references/list-retell-llms
openapi-final get /v2/list-retell-llms
List Retell LLM Response Engines with pagination
# List Test Case Definitions
Source: https://docs.retellai.com/api-references/list-test-case-definitions
openapi-final get /v2/list-test-case-definitions
List test case definitions with pagination
# List Test Runs
Source: https://docs.retellai.com/api-references/list-test-runs
openapi-final get /v2/list-test-runs/{test_case_batch_job_id}
List test case jobs (test runs) for a batch test job with pagination
# List Voices
Source: https://docs.retellai.com/api-references/list-voices
openapi-final get /list-voices
List all voices available to the user
# LLM WebSocket
Source: https://docs.retellai.com/api-references/llm-websocket
LLM WebSocket protocol for streaming requests from Retell to your custom LLM server and returning responses, tool calls, and DTMF actions during a live call.
We recommend [single prompt](/build/prompt) or [conversation flow](/build/conversation-flow/overview) for most agents. They get new features first, and come with built-in tool sets and latency tuning.
This page is the field-by-field protocol spec. For step-by-step instructions, start with the [custom LLM overview](/integrate-llm/overview), then [set up your server](/integrate-llm/setup-websocket-server), [connect your LLM](/integrate-llm/integrate-llm), and [add function calling](/integrate-llm/integrate-function-calling). If a call isn't working, see [troubleshooting](/integrate-llm/troubleshooting).
## Overview
This socket shall connect directly to your server, where you would get live transcript and
other relevant inputs from us, and provide responses back to us using your custom LLM.
In short, this WebSocket controls what the agent says, and controls actions like ending the call.
Retell AI will initiate this WebSocket when starting the call,
and your server should get prepared to handle it.
## Endpoint
WebSocket Endpoint: `{your-server-websocket-endpoint}/{call_id}`
### Path Parameters
Unique call id to identify the call.
## Protocol
Retell and your server would conform to the following protocol sending the WebSocket messages to communicate.
All message event types are "text", where the `data` attribute of message event is
a JSON object stringified.
### Event Flow
The connection would start by your server sending an optional [config event](/api-references/llm-websocket#config-event) to Retell,
and a [response event](/api-references/llm-websocket#response-event)
that serves as the begin message for agent to speak.
Set content to empty string if you want the agent to wait for user to start the conversation.
Retell would send back a [call details event](/api-references/llm-websocket#call-details-event) if that's enabled in config.
Retell and your server would periodically send [ping pong events](/api-references/llm-websocket#ping-pong-event)
to keep the connection alive if configured in config.
As the call goes, Retell would send over live transcript and other updates in
[update only events](/api-references/llm-websocket#update-only-event), and determine when it is appropriate to
ask for responses / reminders in
[response and reminder required events](/api-references/llm-websocket#response-and-reminder-required-events). Your server will be
sending back [response events](/api-references/llm-websocket#response-event) accordingly.
Not all your responses would get spoken out, because the user might
continue to speak even when Retell thought it would be agent's turn.
If at a certain point, you want agent to jump in the conversation and speak something immediately, you can send an
[agent interrupt event](/api-references/llm-websocket#agent-interrupt-event).
### Retell -> Your Server Event Spec
There will be a couple of events that Retell can send to your server in this websocket.
To differentiate between them, check the `interaction_type` field:
* `ping_pong`: (optional) to check for disconnection and keep the connection alive
* `update_only`: (required) to send real-time updates about the call like live transcript
* `response_required`: (required) ask for response content from your server
* `reminder_required`: (required) ask for reminder content from your server
#### Ping Pong Event
When you set `auto_reconnect` to true in the [config event](/api-references/llm-websocket#config-event),
Retell will send ping\_pong events to your server
every 2s to keep the connection alive.
Differentiate what this event is.
Available options: `ping_pong`
Timestamp (milliseconds since epoch) of when Retell sent this event.
You can use this to calculate the time taken for the round trip.
#### Call Details Event
When you set `call_details` to true in the [config event](/api-references/llm-websocket#config-event),
Retell will send call details events to your server right away so that you can save the time of retrieving call detail from
[Get Call API](/api-references/get-call).
Differentiate what this event is.
Available options: `call_details`
Contains the response from [Register Call API](/api-references/register-call).
#### Update Only Event
Retell would send an event when the transcript updates -- either the user speaks, or the agent speaks.
Retell also sends this event when turntaking happens.
Event type. This event is simply an update containing the latest transcript or turntaking information, no response required.
Available options: `update_only`
Complete live transcript collected in the call so far. Presented in the form of a list of
utterances.
See `transcript_object` field of [Get Call API Response](/api-references/get-call) for the detailed schema
for the object in the list.
Transcript of the call weaved with tool call invocation and results. Populated when `transcript_with_tool_calls` field is set to
true in the [config event](/api-references/llm-websocket#config-event).
It precisely captures when (at what utterance, which word) the tool was invoked and what was the result
if the tool calls were sent timely in this LLM websocket. See
[tool call invocation event](/api-references/llm-websocket#tool-call-invocation-event)
and [tool call result event](/api-references/llm-websocket#tool-call-result-event) for more information on how to send
tool call invocations and results.
See `transcript_with_tool_calls` field of [Get Call API Response](/api-references/get-call) for the detailed schema
for the object in the list.
Indicates change of speaker (turn taking). This field will be present when speaker changes to user (user turn), or right before
agent is about to speak (agent turn). This field can be helpful in determining when to call functions in the call.
Available options: `agent_turn`, `user_turn`
#### Response and Reminder Required Events
Retell would continuously assess if it's a good time for agent to speak, and would
ask for content for response / reminder when appropriate, but not all
responses Retell asks for would get spoken out (as user might continue to speak).
Determines what we need from your server.
* `response_required`: Require a response from your server for the current live transcript.
* `reminder_required`: User has not spoken for a while, a reminder is needed from your server.
Available options: `response_required`, `reminder_required`
This unique auto-incrementing id is used to track the response Retell needs, and used to identify the
responses streamed from your server, as you can
send multiple events to stream back responses, and we need an id to group them.
When a new response is needed, a new event with response id will
be sent, and all previous responses will be discarded.
Complete live transcript collected in the call so far. Presented in the form of a list of
utterances.
See `transcript_object` field of [Get Call API Response](/api-references/get-call) for the detailed schema
for the object in the list.
Transcript of the call weaved with tool call invocation and results. Populated when `transcript_with_tool_calls` field is set to
true in the [config event](/api-references/llm-websocket#config-event).
It precisely captures when (at what utterance, which word) the tool was invoked and what was the result
if the tool calls were sent timely in this LLM websocket. See
[tool call invocation event](/api-references/llm-websocket#tool-call-invocation-event)
and [tool call result event](/api-references/llm-websocket#tool-call-result-event) for more information on how to send
tool call invocations and results.
See `transcript_with_tool_calls` field of [Get Call API Response](/api-references/get-call) for the detailed schema
for the object in the list.
### Your Server -> Retell Event Spec
There will be a couple of events that your server can send to Retell in this websocket.
To differentiate between them, set the `response_type` field:
* `config`: (optional) the initial config for configuring reconnection, whether to send call details etc.
* `ping_pong`: (optional) to check for disconnection and keep the connection alive
* `response`: (required) to send back responses to the user when requested
* `agent_interrupt`: (optional) to jump in conversation and speak content in it immediately, interrupts both
agent and user.
* `tool_call_invocation`: (optional) to bookkeep and weave tool call invocations and results in the transcript.
* `tool_call_result`: (optional) to bookkeep and weave tool call invocations and results in the transcript.
* `metadata`: (optional) to pass some data from the server where the LLM is running to the frontend during a web call.
#### Config Event
You can send a config at connection open to configure reconnection, whether to send call details, etc.
Differentiate what this event is.
Available options: `config`
Configuration object to control whether to auto reconnect, and whether Retell sends a call detail over.
If set to true, Retell will send ping pong events to your server, and would expect ping pong events back
from your server every 2s. Once there's 5s without ping pong event, Retell would close the current connection
and restart a new connection to your server for up to 2 times.
If set to true, Retell will send call details over to your server right away. See
[call details event](/api-references/llm-websocket#call-details-event) for more information.
If set to true, Retell will populate an additional field in the [update only events](/api-references/llm-websocket#update-only-event),
[response and reminder required events](/api-references/llm-websocket#response-and-reminder-required-events).
This additional field will contain
transcript of the call weaved with tool call invocation and results.
You need to send tool call invocations and results to us in websocket so that we can construct it. See
[tool call invocation event](/api-references/llm-websocket#tool-call-invocation-event)
and [tool call result event](/api-references/llm-websocket#tool-call-result-event) for more information on how to send
tool call invocations and results.
#### Update Agent Event
You can send agent update events at any time of the call to update some of the agent configurations.
We might add more configuration to this event in the future.
This can be useful when you want to modify the agent behavior during the call, like when
you want to use reminders as a way to let agent continue speaking, or you wish to make agent
less responsive when user is looking up information.
Differentiate what this event is.
Available options: `update_agent`
Set what agent configuration you want to update. All fields are optional.
Controls how responsive the agent is. Value ranging from \[0,1].
Lower value means less responsive agent (wait more, respond slower),
while higher value means faster exchanges (respond when it can).
Controls how sensitive the agent is to user interruptions. Value
ranging from \[0,1]. Lower value means it will take longer / more
words for user to interrupt agent, while higher value means it's
easier for user to interrupt agent.
If set (in milliseconds), will trigger a reminder to the agent to
speak if the user has been silent for the specified duration after
some agent speech. Must be a positive number.
If set, controls how many times agent would remind user when user is
unresponsive. Must be a non-negative integer. Set to 0 to disable agent from
reminding.
#### Ping Pong Event
When you set `auto_reconnect` to true in the [config event](/api-references/llm-websocket#config-event),
you need to send ping\_pong events to signal that your server
is still alive. Retell would expect a ping\_pong event back every 2s, and would close the connection if there's no ping\_pong
event for 5s.
Differentiate what this event is.
Available options: `ping_pong`
Timestamp (milliseconds since epoch) of when your server sent this event.
Retell would use this to calculate the time taken for the round trip.
#### Response Event
Your server needs to respond to [response and reminder required events](/api-references/llm-websocket#response-and-reminder-required-events)
so that agent can speak in time.
You can stream the response or send it in one go, although streaming is
recommended for lower latency. When a newer event that requires response / reminder is received, you can
stop responding to the previous response / reminder required events, as it would not get used. You still need to respond to
previous response / reminder required event if newer update\_only events are received.
Differentiate what this event is.
Available options: `response`
Indicates which requested response this is answering.
Partial or full response content.
Whether the content is complete. When streaming responses back, only the last event of the response
should have this field set to true.
If set to true, agent would not get interrupted by user for content in this event. Useful for conveying important information.
If set to true, Retell would end the call after content associated with this id is fully spoken. If agent was interrupted
during speaking, the end call signal would get discarded.
If set, Retell would transfer the call to the number specified here after content associated with this id is fully spoken.
Only applicable to Retell numbers or imported numbers. If your voice agent is using custom telephony via the dial to
SIP endpoint route, you need to write your own call transfer logic.
If set to true, the transferee will see the original caller's number instead of the Retell number when the call is transferred.
If set, Retell would press the input digit or digits to send DTMF tones after content associated with this id is fully spoken
(although you probably do not want the voice agent to speak anything when pressing digits).
#### Agent Interrupt Event
If at a certain point, you want to jump in the conversation and speak something immediately, you can send this event.
It will stop the agent speech if the agent is speaking, or it will interrupt the user if the user is speaking.
Differentiate what this event is.
Available options: `agent_interrupt`
Used to group the interrupt events. This is a unique id maintained in your server. If interrupt events with the same
ids are received, the content would get spoken in order of the interrupt events received. If interrupt events with
different ids are received, the previous interrupt events are discarded.
Partial or full response content.
Whether the content is complete. When streaming responses back, only the last event of the response
should have this field set to true.
If set to true, agent would not get interrupted by user for content in this event.
This is recommended to set to true here because without this setting,
if user is talking, and agent interrupts here, the two parties would
speak at the same time, and agent would get interrupted quickly.
If set to true, and if this response is used for agent to speak,
we would end the call after content is fully spoken.
If set, we will transfer the call to the number specified here after content is fully spoken.
Only applicable to numbers purchased through Retell.
For call transfer with your own Twilio account, you can trigger it from your server directly.
If set, Retell would press the input digit or digits to send DTMF tones after content associated with this id is fully spoken
(although you probably do not want the voice agent to speak anything when pressing digits).
#### Tool Call Invocation Event
If you send the tool call invocation and results in the websocket,
Retell would populate the `transcript_with_tool_calls` field in the [Get Call API Response](/api-references/get-call)
after call ends. We would weave the transcript and precisely capture when (at what utterance, which word)
the tool was invoked and what was the result.
If you also set `transcript_with_tool_calls` to true in the [config event](/api-references/llm-websocket#config-event),
Retell would populate the `transcript_with_tool_calls`
field in the [update only event](/api-references/llm-websocket#update-only-event) with the tool call invocations and results.
This is helpful when you do not wish to maintain a copy of transcript and function calls locally during the call session
and wish to have Retell manage it for you.
Differentiate what this event is.
Available options: `tool_call_invocation`
Tool call id, globally unique.
Name of the function in this tool call.
Arguments for this tool call, it's a stringified JSON object.
#### Tool Call Result Event
If you send the tool call invocation and results in the websocket,
Retell would populate the `transcript_with_tool_calls` field in the [Get Call API Response](/api-references/get-call)
after call ends. We would weave the transcript and precisely capture when (at what utterance, which word)
the tool was invoked and what was the result.
If you also set `transcript_with_tool_calls` to true in the [config event](/api-references/llm-websocket#config-event),
Retell would populate the `transcript_with_tool_calls`
field in the [update only event](/api-references/llm-websocket#update-only-event) with the tool call invocations and results.
This is helpful when you do not wish to maintain a copy of transcript and function calls locally during the call session
and wish to have Retell manage it for you.
Differentiate what this event is.
Available options: `tool_call_result`
Tool call id, globally unique.
Result of the tool call, can be a string, a stringified json, etc.
Whether the tool call succeeded. Recorded against the tool call in the transcript.
Omitting it means the outcome was not reported, not that the tool call failed, so
set it explicitly if you want to tell the two apart after the call.
#### Metadata Event
Sometimes you may wish to pass some data from the server where the LLM is running to the frontend during a web call,
for animation purposes or other stuff. It can be challenging to make sure the frontend of the call
can connect to the server where the LLM is running. In this case, you can send metadata events to Retell and Retell will
forward it to the frontend.
See [frontend metadata event](/api-references/audio-websocket#metadata-event) for the forwarded event.
Differentiate what this event is.
Available options: `metadata`
You can put anything here that can be json serialized.
## Sample Events
### Retell -> Your Server Sample Events
```json Ping Pong theme={"dark"}
{
"interaction_type": "ping_pong",
"timestamp": 1703302407333
}
```
```json Call Details theme={"dark"}
{
"interaction_type": "call_details",
"call": {
"call_type": "phone_call",
"from_number": "+12137771234",
"to_number": "+12137771235",
"direction": "inbound",
"call_id": "Jabr9TXYYJHfvl6Syypi88rdAHYHmcq6",
"agent_id": "oBeDLoLOeuAbiuaMFXRtDOLriTJ5tSxD",
"call_status": "registered",
"metadata": {},
"retell_llm_dynamic_variables": {
"customer_name": "John Doe"
},
"opt_out_sensitive_data_storage": true
}
}
```
```json Response Required theme={"dark"}
{
"interaction_type": "response_required",
"timestamp": 3,
"transcript": [
{
"role": "agent",
"content": "Hey how can I help you?",
"words": [Array]
},
{
"role": "user",
"content": "Hey. How are you?",
"words": [Array]
}
]
}
```
```json Update Only theme={"dark"}
{
"interaction_type": "update_only",
"transcript": [
{
"role": "agent",
"content": "Hey how can I help you?",
"words": [Array]
},
{
"role": "user",
"content": "Hey. How are you?",
"words": [
{
"word": "Hey.",
"start": 4.375,
"end": 4.615
},
{
"word": "How",
"start": 4.615,
"end": 4.855
},
{
"word": "are",
"start": 4.855,
"end": 5.030156
},
{
"word": "you?",
"start": 5.030156,
"end": 5.2053127
}
]
}
],
"turntaking": "agent_turn"
}
```
```json Reminder Required theme={"dark"}
{
"interaction_type": "reminder_required",
"transcript": [
{
"role": "agent",
"content": "Hey how can I help you?",
"words": [Array]
},
{
"role": "user",
"content": "Hey. How are you?",
"words": [Array]
},
{
"role": "agent",
"content": "I'm doing fine. How can I help you?",
"words": [Array]
}
]
}
```
### Your Server -> Retell Sample Events
```json Config theme={"dark"}
{
"response_type": "config",
"config": {
"auto_reconnect": true,
"call_details": true
}
}
```
```json Agent Update theme={"dark"}
{
"response_type": "update_agent",
"agent_config": {
"responsiveness": 0.5,
"interruption_sensitivity": 0.5,
"reminder_trigger_ms": 5000,
"reminder_max_count": 3
}
}
```
```json Ping Pong theme={"dark"}
{
"response_type": "ping_pong",
"timestamp": 1703302407333
}
```
```json Response theme={"dark"}
{
"response_type": "response",
"response_id": 3,
"content": "I'm doing great, ",
"content_complete": false
}
{
"response_type": "response",
"response_id": 3,
"content": "thank you.",
"content_complete": true
}
// Later on, ending the call
{
"response_type": "response",
"response_id": 10,
"content": "Goodbye.",
"content_complete": true,
"end_call": true
}
```
```json Agent Interrupt theme={"dark"}
{
"response_type": "agent_interrupt",
"interrupt_id": 1,
"content": "Please stop right there, do not",
"content_complete": false,
"no_interruption_allowed": true
}
{
"response_type": "agent_interrupt",
"interrupt_id": 1,
"content": " click on that button yet!",
"content_complete": true,
"no_interruption_allowed": true
}
```
```json Tool Call Invocation theme={"dark"}
{
"response_type": "tool_call_invocation",
"tool_call_id": "some_id_here",
"name": "book_appointment",
"arguments": "{\"date\": \"2022-01-01\", \"time\": \"10:00\"}"
}
```
```json Tool Call Result theme={"dark"}
{
"response_type": "tool_call_result",
"tool_call_id": "some_id_here",
"content": "Appointment booked successfully."
}
```
```json Metadata theme={"dark"}
{
"response_type": "metadata",
"metadata": {
"avatar_emotion": "Angry",
"user_id": "1234"
}
}
```
# Monitor Call WebSocket
Source: https://docs.retellai.com/api-references/monitor-call-websocket
Stream a live call transcript, tool calls, and node transitions from the Retell AI monitor call WebSocket using an API key or public key.
This is the stream behind [Live Monitoring](/features/live-monitoring) in the dashboard. For a call that has already ended, use [Get Call](/api-references/get-call) instead: it returns the final transcript with word timestamps and, when PII scrubbing is on, the scrubbed version.
## Overview
The monitor call WebSocket streams a live call's transcript while the call is in progress. Receive agent and user turns, tool calls, and node transitions as JSON events, starting with a snapshot of the conversation so far and ending when the call ends. Authenticate with an API key on your server or a public key in the browser.
Your application opens this connection and Retell sends events on it. Your application doesn't send anything back.
Use it when you need the conversation while it's still happening:
* **Show live transcripts in your own tools.** Build a supervisor console, embed the transcript in your CRM, or mirror the call in an internal dashboard.
* **React mid-call.** Page a human when the flow reaches an escalation node, alert on a failed tool call, or flag a keyword before the call ends.
* **Audit in real time.** Watch sensitive conversations as they happen instead of only in review.
For example, a dental clinic streams every appointment call into its own operations console. When the agent reaches the "Escalate to staff" node, the clinic's server posts the last few turns to the front-desk channel, and a receptionist picks up the call from the dashboard with [Take Over](/features/live-monitoring#take-over).
## Endpoint
WebSocket endpoint: `wss://api.retellai.com/v2/monitor-call/{call_id}`
### Path parameters
Id of the call to monitor. The call must belong to your workspace and be in progress (`call_status` is `ongoing`).
### Authentication
For a server connection, use `Bearer YOUR_RETELL_API_KEY`, the same as for REST requests. A key with restricted permissions needs **Call → Edit**. Keys without restrictions work as is. See [Manage API keys](/accounts/manage-api-keys).
Browser connections can use a [public key](/accounts/public-keys) from an allowed domain. Browsers cannot set the `Authorization` header on a WebSocket handshake, so pass `["bearer", "public_key_YOUR_PUBLIC_KEY"]` as the WebSocket subprotocol list. The SDK does this automatically when you [enable live transcripts for a web call](/deploy/web-call#enable-live-transcripts).
Keep API keys on your server. If your integration uses an API key, relay events through your own authenticated backend to display them in a browser. A custom SDK `fetch` only handles REST requests; it does not proxy this WebSocket.
### Connection limits
* **The call must be `ongoing`.** A registered call that hasn't connected yet, or a call that has ended, is rejected with close code 4004. Connect when you receive the `call_started` [webhook](/features/webhook-overview), or once Get Call reports `ongoing`.
* **Up to 5 connections per call** (as of September 2026). Dashboard viewers count toward the same limit. A sixth connection is rejected with close code 4008 and the reason `max watchers reached`.
## Protocol
Retell sends events to your application over this WebSocket. All messages are text frames whose `data` is a stringified JSON object. Check the `type` field to tell events apart.
The stream carries text only. Transcript items have the text and a start time, not per-word timing, so fetch the call with Get Call after it ends for word timestamps. During an [agentic warm transfer](/build/conversation-flow/call-transfer-node), the transfer agent's conversation with the transfer target isn't streamed; it appears in the Get Call transcript as `transfer_target` utterances after the call. The text is always the raw transcript: [PII scrubbing](/accounts/privacy-disable) runs after a call ends, so no scrubbed version exists mid-call. Treat the stream as sensitive data. To hear live audio, use [Live Listen](/features/live-monitoring#live-listen) in the dashboard.
### Event flow
Once Retell validates your key and the call, it admits the connection and sends one [transcript snapshot event](#transcript-snapshot-event) with everything said so far, so a connection that joins mid-call starts complete.
As the call goes on, each change arrives as a [transcript updated event](#transcript-updated-event): a new user or agent turn, more text on the current turn, a tool call and its result, a node transition, a keypad press, an SMS, or injected context.
When the call ends, Retell sends a [call ended event](#call-ended-event) and closes the connection with code 1000 and the reason `call_ended`.
Every [transcript item](#transcript-item-spec) has a stable `id`. When an id arrives again, the new item replaces the old one: as a speaker keeps talking, their current turn is re-sent with the full text so far. Keep items in a map keyed by `id` and sort by `time_sec` for display, and you have the live transcript. The snapshot can arrive after the first updates, so apply the same replace-by-id logic to both.
If your connection drops while the call is still ongoing, reconnect. The new connection gets a fresh snapshot, and replace-by-id handles any overlap. If the call's server disappears without sending `call_ended` (for example, an infrastructure failure), Retell closes the connection within about a minute with code 1000 and the reason `call_ended`, possibly without a call ended event first.
### Retell -> your server event spec
Retell sends three kinds of events over this WebSocket. To differentiate between them, check the `type` field:
* `transcript_snapshot`: sent once after the connection is admitted, with the whole transcript so far
* `transcript_updated`: sent whenever the transcript changes
* `call_ended`: sent once when the call ends, right before Retell closes the connection
#### Transcript snapshot event
Sent once, shortly after the connection is admitted. Retell requests the snapshot from the call's server after your connection opens, so it can arrive after the first transcript updated events.
Differentiate what this event is.
Available options: `transcript_snapshot`
Every [transcript item](#transcript-item-spec) in the call so far, sorted by `time_sec`. Empty if nothing has been said yet.
Calls made by the agent's pre-session tools before the conversation started, as `tool_call_invocation` and `tool_call_result` items. Empty when the agent has no pre-session tools.
#### Transcript updated event
Sent whenever the transcript changes. Each event carries one changed item. The exception is a pre-session update, where `transcripts` is empty and `pre_session_transcripts` carries the full pre-session list.
Differentiate what this event is.
Available options: `transcript_updated`
The [transcript item](#transcript-item-spec) that changed, as a one-element array. An item whose id you have already seen replaces the earlier version. Empty when the event only carries `pre_session_transcripts`.
Present only when pre-session tool results arrive after you connect. Contains every pre-session item so far, in the same shape as the snapshot.
#### Call ended event
The last message before Retell closes the connection with code 1000.
Differentiate what this event is.
Available options: `call_ended`
When the call ended, in milliseconds since the Unix epoch.
Why the call ended, such as `user_hangup` or `agent_hangup`. Same values as `disconnection_reason` in the [Get Call](/api-references/get-call) response.
### Transcript item spec
Each item in `transcripts` and `pre_session_transcripts` carries the fields below, plus the fields for its `role`. The `id` is the role followed by a per-role counter, such as `agent_2` or `tool_call_invocation_0`.
Stable id for this item. A later item with the same id replaces this one.
When the item started, in seconds from the start of the call. For `user` and `agent` items this is the start of the first word, or `0` before any word timing exists. Sort by this field for display and keep arrival order for ties.
What kind of item this is.
Available options: `user`, `agent`, `tool_call_invocation`, `tool_call_result`, `node_transition`, `dtmf`, `sms`, `injected`
#### User and agent items
A spoken turn.
The text of the turn so far. It grows while the speaker continues, and each change re-sends the item. When the caller's speech couldn't be recognized, `content` is `(unintelligible audio)`.
#### Tool call invocation item
The agent called a tool. Match it to its result with `tool_call_id`.
Unique id of the tool call.
Name of the tool.
Arguments passed to the tool, as a stringified JSON object.
Kind of tool.
Available options: `custom`, `code`, `mcp`, `integration_app`, `end_call`, `transfer_call`, `bridge_transfer`, `cancel_transfer`, `agent_swap`, `press_digit`, `send_sms`, `extract_dynamic_variable`, `adjust_voice_speed`, `book_appointment_cal`, `check_availability_cal`
#### Tool call result item
The tool the agent called returned.
Id of the invocation this result belongs to.
What the tool returned, often a stringified JSON object.
Whether the tool call succeeded. Absent when the outcome wasn't recorded, which doesn't mean it failed.
#### Node transition item
A conversation flow agent moved to another node.
Id of the node the agent left.
Name of the node the agent left.
Id of the node the agent entered.
Name of the node the agent entered.
How the node was reached: `normal` for a regular edge, `global` for a global node, `global_go_back` when returning from a global node, or `interrupt_go_back` when returning after a user interruption.
Available options: `normal`, `global`, `global_go_back`, `interrupt_go_back`
#### DTMF item
The caller pressed a key on their phone keypad.
The key pressed: a single character such as `1`, `*`, or `#`.
#### SMS item
An SMS the caller sent during the call, for example while the agent was leaving a voicemail. Not part of the spoken conversation.
Text of the message.
MMS attachments. Display only.
Signed URL of the attachment.
Short description of the attachment, when available.
#### Injected item
Context your server added mid-call with [Update Live Call](/api-references/update-live-call). Not spoken by either party.
The injected text.
### Close codes
Retell closes the connection with a standard WebSocket close frame. The reason text says why.
| Code | Meaning |
| ------ | --------------------------------------------------------------------------------------------------------------------------------- |
| `1000` | The call ended. Reason `call_ended`. |
| `4000` | The request was malformed. |
| `4001` | The API key is missing or invalid. |
| `4003` | The key lacks the **Call → Edit** permission. |
| `4004` | The call doesn't exist in your workspace or isn't `ongoing`. Reason `Call not live` for a call that exists but isn't in progress. |
| `4008` | The call already has 5 connections. Reason `max watchers reached`. |
| `1011` | Internal error on Retell's side. Reconnect with backoff. |
## Sample events
### Retell -> your server sample events
```json Transcript Snapshot theme={"dark"}
{
"type": "transcript_snapshot",
"transcripts": [
{
"id": "agent_0",
"role": "agent",
"content": "Hi, this is Ava from Lakeside Dental. Am I speaking with Jordan?",
"time_sec": 0.9
},
{
"id": "user_0",
"role": "user",
"content": "Yes, that's me.",
"time_sec": 5.4
}
],
"pre_session_transcripts": []
}
```
```json Transcript Updated theme={"dark"}
{
"type": "transcript_updated",
"transcripts": [
{
"id": "agent_1",
"role": "agent",
"content": "Sure, I can move your cleaning. What day works best?",
"time_sec": 8.2
}
]
}
```
```json Call Ended theme={"dark"}
{
"type": "call_ended",
"event_timestamp": 1757000000000,
"disconnection_reason": "user_hangup"
}
```
### Transcript item sample events
Each of these appears inside the `transcripts` array of a snapshot or update. Pre-session tool calls use the same shapes inside `pre_session_transcripts`.
```json Spoken Turn theme={"dark"}
// Sent while the caller is still talking
{
"id": "user_1",
"role": "user",
"content": "Can we do Thursday",
"time_sec": 12.7
}
// Re-sent with the same id and the full text once the caller finishes
{
"id": "user_1",
"role": "user",
"content": "Can we do Thursday afternoon?",
"time_sec": 12.7
}
```
```json Tool Call Invocation theme={"dark"}
{
"id": "tool_call_invocation_0",
"role": "tool_call_invocation",
"time_sec": 15.3,
"tool_call_id": "call_8f2e1c",
"name": "reschedule_appointment",
"arguments": "{\"patient_id\": \"p_4821\", \"new_time\": \"2026-09-10T14:00:00-07:00\"}",
"type": "custom"
}
```
```json Tool Call Result theme={"dark"}
{
"id": "tool_call_result_0",
"role": "tool_call_result",
"time_sec": 16.1,
"tool_call_id": "call_8f2e1c",
"content": "{\"status\": \"confirmed\", \"time\": \"2026-09-10T14:00:00-07:00\"}",
"successful": true
}
```
```json Node Transition theme={"dark"}
{
"id": "node_transition_2",
"role": "node_transition",
"time_sec": 17.0,
"former_node_id": "node_reschedule",
"former_node_name": "Reschedule appointment",
"new_node_id": "node_confirm",
"new_node_name": "Confirm details",
"transition_type": "normal"
}
```
```json DTMF theme={"dark"}
{
"id": "dtmf_0",
"role": "dtmf",
"time_sec": 21.4,
"digit": "1"
}
```
```json SMS theme={"dark"}
{
"id": "sms_0",
"role": "sms",
"time_sec": 24.8,
"content": "Running late, call me back in 5"
}
```
```json Injected theme={"dark"}
{
"id": "injected_0",
"role": "injected",
"time_sec": 30.2,
"content": "The patient's insurance was verified this morning."
}
```
## FAQ
No. The stream is transcript only. Live audio is available in the dashboard through [Live Listen](/features/live-monitoring#live-listen), and the recording is available from Get Call after the call ends.
The call hasn't connected yet. Monitoring requires a `call_status` of `ongoing`, and a call stays `registered` until the callee answers or the web client joins. Wait for the `call_started` webhook, or poll Get Call, then connect.
That's how updates work. A user or agent turn is re-sent with more text as the speaker continues, and the snapshot can overlap with updates that arrived first. Replace items by `id`.
No. Retell sends every event and closes the connection when the call ends. There is no config, ping, or acknowledgement to send back, unlike the [LLM WebSocket](/api-references/llm-websocket).
Phone calls and web calls, yes. Chats aren't calls and can't be monitored with this WebSocket.
No. Spoken turns carry the text so far and a start time, without a finalization flag or end time. For an audio visualization in a web call, use the SDK's [audio snapshots](/deploy/web-call#control-audio-during-the-call).
Yes, with a public key from an allowed domain, using the subprotocol authentication described above. For a web call, the SDK handles this when you enable `transcript: true`. If you use an API key instead, keep it on your backend and relay the events to the browser.
# Retell AI API Reference
Source: https://docs.retellai.com/api-references/overview
Reference for the Retell AI API: authentication, base URL, SDKs, and endpoints for building and managing voice and chat agents programmatically.
Use this section to build and manage Retell AI voice and chat agents programmatically.
## What you can do
* Create, update, and manage calls and chats
* Configure voice agents and chat agents
* Manage phone numbers, voices, and knowledge bases
* Run tests and retrieve test run results
* Integrate custom telephony and custom LLM workflows
## Base URL
All REST API requests use the same base URL:
```
https://api.retellai.com
```
Append the endpoint path from the reference (for example, `POST https://api.retellai.com/v2/list-calls`). The audio WebSocket uses a separate host — see [Audio WebSocket](/api-references/audio-websocket).
## Authentication
Every request must include your API key as a bearer token in the `Authorization` header:
```
Authorization: Bearer YOUR_API_KEY
```
Create and manage keys from the **API Keys** tab in the [dashboard](https://dashboard.retellai.com). See [Manage API Keys](/accounts/manage-api-keys) for details.
## SDKs
Official SDKs handle the base URL and authentication for you:
* [Node.js / TypeScript SDK](https://www.npmjs.com/package/retell-sdk)
* [Python SDK](https://pypi.org/project/retell-sdk/)
See [SDKs](/get-started/sdk) for installation, initialization, and usage examples, and [SDK versioning](/get-started/sdk#versioning) for how the TypeScript and Python versions line up.
## Getting started
If you are new to the API:
1. Create your API key in the dashboard.
2. Make your first request with the `Create phone call` endpoint.
3. Use the grouped endpoint sections in the sidebar to expand your integration.
# Publish Agent
Source: https://docs.retellai.com/api-references/publish-agent
openapi-final post /publish-agent-version/{agent_id}
Publish an existing draft version in place.
# Publish Chat Agent
Source: https://docs.retellai.com/api-references/publish-chat-agent
openapi-final post /publish-agent-version/{agent_id}
Publish an existing draft version in place.
# Register Phone Call
Source: https://docs.retellai.com/api-references/register-phone-call
openapi-final post /v2/register-phone-call
Register a new phone call for custom telephony
# Rerun Call Analysis
Source: https://docs.retellai.com/api-references/rerun-call-analysis
openapi-final put /rerun-call-analysis/{call_id}
Rerun post-call analysis for a specific call. This operation incurs charges.
# Rerun Chat Analysis
Source: https://docs.retellai.com/api-references/rerun-chat-analysis
openapi-final put /rerun-chat-analysis/{chat_id}
Rerun post-chat analysis for a specific chat. This operation incurs charges.
# Run Sync Job
Source: https://docs.retellai.com/api-references/run-sync-job
openapi-final post /run-sync-job
Start a contact sync with the linked CRM app immediately, instead of waiting for the scheduled sync. One sync runs per organization at a time: starting another while one is in flight is rejected. Poll get-sync-job-status for progress.
# Search Voice
Source: https://docs.retellai.com/api-references/search-voice
openapi-final post /search-community-voice
Search for community voices from voice providers
# Stop Call
Source: https://docs.retellai.com/api-references/stop-call
openapi-final post /v2/stop-call/{call_id}
Stop an ongoing call.
# Test App Auth
Source: https://docs.retellai.com/api-references/test-app-auth
openapi-final post /test-app-auth/{app_id}
Probe the App's stored credentials by making a minimal authenticated call to the provider. Returns success=true on a successful round-trip, and records the outcome on the App's connection_status either way.
# Update Voice Agent
Source: https://docs.retellai.com/api-references/update-agent
openapi-final patch /update-agent/{agent_id}
Update an existing agent's latest draft version
# Update App
Source: https://docs.retellai.com/api-references/update-app
openapi-final patch /update-app/{app_id}
Partially update an App. Omitted fields remain unchanged. Updating auth_config or tenant metadata invalidates the cached provider token immediately. Providers using the OAuth callback reject auth_config and must be reauthorized through connect-app.
# Update Call
Source: https://docs.retellai.com/api-references/update-call
openapi-final patch /v2/update-call/{call_id}
Update metadata and sensitive data storage settings for an existing call.
# Update Chat
Source: https://docs.retellai.com/api-references/update-chat
openapi-final patch /update-chat/{chat_id}
Update metadata and sensitive data storage settings for an existing chat.
# Update Chat Agent
Source: https://docs.retellai.com/api-references/update-chat-agent
openapi-final patch /update-chat-agent/{agent_id}
Update an existing chat agent
# Update Contact
Source: https://docs.retellai.com/api-references/update-contact
openapi-final patch /update-contact/{contact_id}
Update an existing contact.
# Update Conversation Flow
Source: https://docs.retellai.com/api-references/update-conversation-flow
openapi-final patch /update-conversation-flow/{conversation_flow_id}
Update an existing conversation flow
# Update Conversation Flow Subflow
Source: https://docs.retellai.com/api-references/update-conversation-flow-component
openapi-final patch /update-conversation-flow-component/{conversation_flow_component_id}
Update an existing shared conversation flow component
# Update CRM Config
Source: https://docs.retellai.com/api-references/update-crm-config
openapi-final post /update-crm-config
Update the organization's CRM configuration. Omitted fields stay as they are; a field that is sent replaces its stored value in full.
# Update Live Call
Source: https://docs.retellai.com/api-references/update-live-call
openapi-final patch /v2/update-live-call/{call_id}
Update an ongoing call at runtime. Supports overriding dynamic variables, metadata, and the data storage setting on the running call, and controlling the live agent (inject context, trigger a response). These overrides take effect immediately on the live call; metadata and data storage setting changes are also persisted to the call record. To update a call that is no longer ongoing, use /v2/update-call/{call_id}.
# Update Phone Number
Source: https://docs.retellai.com/api-references/update-phone-number
openapi-final patch /update-phone-number/{phone_number}
Update agent bound to a purchased phone number
# Update Retell LLM
Source: https://docs.retellai.com/api-references/update-retell-llm
openapi-final patch /update-retell-llm/{llm_id}
Update an existing Retell LLM Response Engine
# Update Test Case Definition
Source: https://docs.retellai.com/api-references/update-test-case-definition
openapi-final put /update-test-case-definition/{test_case_definition_id}
Update a test case definition
# Add pause or read slowly
Source: https://docs.retellai.com/build/add-pause
Control speech pacing in Retell agents by adding spaced dashes for short pauses and longer Read Slowly markup for phone numbers, addresses, and confirmations.
Although you can adjust the general speed of the audio by changing the voice speed, you might want to
slow down the agent's speech only at certain points (like reading phone numbers).
You can do this by prompting the LLM and generating text with `-` in between (note, the space around `-` is important):
```json theme={"dark"}
The number is 2 - 1 - 3 - 4
```
Note: The spaces around the dash (`-`) are important for proper pausing behavior.
### How to add long pauses
Sometimes you might want to add longer pauses to the conversation. You can do this by adding multiple `-` in between the words.
Important: The spaces around the dash (`-`) are important for proper pausing behavior:
```json theme={"dark"}
The number is 2 - - - - 1 - - - - 3
// Notice the double spaces between the dashes
```
# Add custom pronunciation
Source: https://docs.retellai.com/build/add-pronunciation
Control how Retell AI agents pronounce words with IPA, CMU, Mandarin Pinyin, or Cantonese Jyutping based on the selected voice provider and model.
You can control how a Retell agent pronounces specific words or phrases. Choose IPA, CMU, Mandarin Pinyin, or Cantonese Jyutping when the selected voice model supports it.
To use the feature, you set a pronunciation dictionary on the agent. The dictionary is a list of entries, and each entry has:
* **Word or phrase** to be annotated. For example, `actually`.
* **Phonetic alphabet** supported by the selected [voice](#voice-support).
* **Phoneme** with the pronunciation in the selected alphabet.
For example:
| Alphabet | API value | Word | Phoneme |
| ------------------ | ---------- | ---------- | -------------------- |
| IPA | `ipa` | `actually` | `æktʃuəli` |
| CMU (ARPAbet) | `cmu` | `actually` | `AE K CH UW AH L IY` |
| Mandarin Pinyin | `pinyin` | `燕少飞` | `yan4 shao3 fei1` |
| Cantonese Jyutping | `jyutping` | `你好` | `nei5 hou2` |
Use tone numbers `1–5` for Pinyin and `1–6` for Jyutping. Separate syllables with spaces.
## Voice support
Which phonetic alphabets you can use depends on the selected voice provider and its effective voice model. The dashboard shows the supported alphabets under **Pronunciation** in the agent's speech settings. See [supported languages by provider](/build/language-support#model-level-restrictions) for voice-model language restrictions.
| Voice | Supported alphabets |
| ------------------------------------------------------------------------- | ------------------------- |
| Retell Platform voices | IPA |
| Cartesia voices | IPA |
| Inworld voices | IPA |
| MiniMax `speech-2.8-turbo` | IPA, Pinyin, and Jyutping |
| MiniMax `speech-02-turbo` | IPA and Pinyin |
| ElevenLabs `eleven_flash_v2` (English only) | IPA and CMU |
| ElevenLabs `eleven_flash_v2_5`, `eleven_multilingual_v2`, and `eleven_v3` | Not supported |
| OpenAI and Fish Audio voices | Not supported |
If an ElevenLabs voice uses **Auto**, an English-only agent resolves to Flash v2 and supports IPA and CMU. Agents that use other languages resolve to a newer ElevenLabs model that does not support pronunciation dictionaries. The dashboard displays the resulting support.
If the selected voice or model doesn't support an entry's alphabet, Retell preserves the entry but doesn't apply it.
## How many entries can I add?
The pronunciation dictionary is a list on the agent — you can add multiple entries and there is no fixed cap enforced by the API. In practice:
* Add one entry per word or short phrase you want to override. Each entry needs its own `word`, `alphabet`, and `phoneme`.
* Keep the dictionary focused on words the model actually mispronounces (uncommon proper nouns, brand names, technical terms, foreign words). Overriding common words can produce unnatural speech.
* Use a unique `word` value for each entry. The API rejects updates that contain duplicate `word` values.
* Very large dictionaries can push the agent payload close to request size limits when calling `update-agent`. If you hit that, split entries across agents or trim rarely used words.
## Manage the dictionary via the API
There is no dedicated pronunciation endpoint. The dictionary lives on the agent object, so manage it with the [Update Agent](/api-references/update-agent) endpoint through the `pronunciation_dictionary` field:
* Send an array of entries, each with `word`, `alphabet` (`ipa`, `cmu`, `pinyin`, or `jyutping`), and `phoneme`.
* The array you send replaces the existing dictionary, so include all entries you want to keep.
* Set `pronunciation_dictionary` to `null` to remove the dictionary from the agent.
```json theme={"dark"}
{
"pronunciation_dictionary": [
{ "word": "actually", "alphabet": "ipa", "phoneme": "ˈæktʃuəli" }
]
}
```
## Finding the phonetic pronunciation
You can search online to find the phonetic pronunciation of a word, or use tools like:
* [IPA pronouncing dictionary tool](https://tophonetics.com/)
* [CMU pronouncing dictionary tool](http://www.speech.cs.cmu.edu/cgi-bin/cmudict/)
# Agent Handbook
Source: https://docs.retellai.com/build/agent-handbook
Turn on Retell Agent Handbook presets to add best-practice prompts for personality, accuracy, and safety with one toggle — no manual prompt writing required.
The Agent Handbook is a collection of ready-to-use prompt presets that improve how your agent communicates. Each preset encodes a specific best practice — toggle it on and the behavior is added automatically, no prompt writing needed.
New agents are created with the **Default Tone (Professional)** and **AI Disclosure When Asked** presets enabled by default.
## How It Works
The Agent Handbook organizes presets into three categories:
* **Personality & Tone** — Shape how the agent sounds and feels in conversation
* **Accuracy & Format** — Improve how the agent handles names, numbers, and data (voice agents only)
* **Trust & Safety** — Control transparency and scope of responses
Toggle any preset on or off from the Agent Handbook panel. Each preset adds a small number of tokens to every interaction — estimated token counts are shown on hover.
To open the Agent Handbook, click the **Agent Handbook** button in the prompt section of your agent settings.
## Presets
### Personality & Tone
Sets your agent's baseline speaking style. Choose one of two tones:
* **Professional** *(\~480 tokens · Voice and Chat · default)* — clear and polite, like a courteous representative. Follows an **Acknowledge → Statement → Next Step** structure, limits filler acknowledgments, and avoids robotic phrases like "Certainly!" or "Absolutely!".
* **Professional + Conversational** *(\~910 tokens · Voice only)* — casual and natural, with no robotic assistant vibes: short turns, one question at a time, real recommendations instead of "it depends", and numbers and times spoken the way people say them. Best for sales, front desk, scheduling, and customer success. See [Conversational Mode](/build/conversational-mode) for side-by-side examples.
**Professional sounds like:** *"I understand this is frustrating — let me look into that for you."*
**Professional + Conversational sounds like:** *"Morning's pretty open — nine or ten thirty. Afternoon works too if that's easier."*
**Available for:** Voice agents only | **Default:** Disabled
Adds occasional filler words like "um", "uh", and "you know" to make the agent sound more human and conversational. Fillers are used sparingly — roughly once every 2–3 sentences.
**When to use:** Great for sales, customer success, or casual conversations. Avoid for formal or regulated contexts (medical, legal).
**Example:** *"So yeah, let me just pull that up for you real quick."*
**Available for:** Voice and Chat agents | **Default:** Disabled
Guides the agent to use empathetic language when the situation calls for it — acknowledging concerns, making callers feel heard, and reassuring them before moving to a solution.
**When to use:** Useful for support agents, complaint handling, or any scenario where callers may be frustrated or emotional.
**Example:** *"I'm sorry you're dealing with this. Let's get it sorted."*
### Accuracy & Format
All presets in this category are available for voice agents only. They are automatically disabled for chat agents.
**Default:** Disabled
The agent repeats back names, phone numbers, and other critical details for confirmation. For uncommon names, it spells them out letter by letter.
**When to use:** Enable for appointment booking, data collection, or any workflow where accurate information capture is critical.
**Example:** *"Just to confirm, your first name is Ryan, last name is James — is that correct?"*
**Default:** Disabled
When spelling is needed, the agent uses the NATO phonetic alphabet (A as in Alfa, B as in Bravo, etc.) with natural pauses for clarity.
**When to use:** Useful for confirming email addresses, reference numbers, account IDs, or names over the phone.
**Example:** *"That's B as in Bravo, 7, K as in Kilo, 2 — correct?"*
**Default:** Disabled
Formats numbers, dates, money, phone numbers, addresses, and emails into natural spoken form. For example, "\$758.08" becomes "seven fifty-eight dollars and eight cents" and phone numbers are read digit by digit with pauses.
**When to use:** Enable when your agent frequently reads back structured data like prices, dates, or contact information. Note the higher token cost (\~910 tokens).
This preset tells the LLM how to format its text output for natural speech. For converting text to spoken form at the audio level, enable the `speech_normalization` option in `handbook_config`. Both can be used together.
**Example:** *"Your total is seventy dollars and eighty-four cents."*
**Default:** Disabled
Handles common speech recognition variations of names gracefully. If a caller confirms their name but the transcription is slightly different (e.g., "Brandon" vs. "Brendon"), the agent treats it as a match and continues naturally.
**When to use:** Enable when your agent looks up names in a database or CRM and needs tolerance for transcription variations.
**Example:** Agent asks "Are you Brandon?" and the caller says "Yes, this is Brendon" — the agent continues without flagging the difference.
### Trust & Safety
**Available for:** Voice and Chat agents | **Default:** Enabled
When someone asks if they're speaking to a human or an AI, the agent clearly acknowledges that it's a virtual assistant.
**When to use:** Recommended for transparency and compliance. Only disable if your use case has specific reasons not to disclose.
**Example:** *"Yes — I'm an AI assistant here to help."*
**Available for:** Voice and Chat agents | **Default:** Disabled
Restricts the agent to only answer questions based on information available in its prompt and knowledge base. If it doesn't know, it says so instead of guessing.
**When to use:** Enable for any agent where factual accuracy is critical, such as healthcare, finance, or legal use cases.
**Example:** *"I don't have that information, but I can connect you to someone who can help."*
## Token Cost Summary
| Preset | Est. Tokens | Voice | Chat | Default |
| -------------------------------------------- | ----------- | ----- | ---- | ------- |
| Default Tone — Professional | \~480 | ✓ | ✓ | On |
| Default Tone — Professional + Conversational | \~910 | ✓ | — | Off |
| Natural Filler Words | \~100 | ✓ | — | Off |
| High Empathy | \~70 | ✓ | ✓ | Off |
| Echo Verification | \~190 | ✓ | — | Off |
| NATO Phonetic Alphabet | \~190 | ✓ | — | Off |
| Speech Normalization | \~910 | ✓ | — | Off |
| Smart Matching | \~110 | ✓ | — | Off |
| AI Disclosure When Asked | \~30 | ✓ | ✓ | On |
| Scope Boundaries | \~60 | ✓ | ✓ | Off |
Token counts are approximate. The token cost of all enabled presets is added to every interaction.
## FAQ
No — presets are fixed best-practice prompts. For custom instructions, write them directly in your agent's prompt. See the [Prompt Engineering Guide](/build/prompt-engineering-guide) for tips.
Handbook presets work alongside your custom prompt. If your prompt gives instructions that overlap with a preset (e.g., you wrote your own empathy guidelines), you may want to disable the overlapping preset to avoid inconsistent behavior.
Five presets — Natural Filler Words, Echo Verification, NATO Phonetic Alphabet, Speech Normalization, and Smart Matching — are designed specifically for voice interactions and are not applicable to chat agents.
# Speech recognition providers
Source: https://docs.retellai.com/build/asr-providers
An overview of Retell's speech recognition providers — Azure, Deepgram, and Soniox — and how Retell auto-routes based on your agent's configured languages.
Retell automatically picks a speech recognition provider based on the languages your agent is configured for. You don't need to choose one manually, but it helps to understand what each provider offers.
These observations are based on our internal testing and routing rules. Results may vary depending on the specific languages, audio conditions, and call patterns.
## Provider overview
### Azure
* **Best for:** Broad single-language coverage, including many rare and less common languages.
* **Language support:** Wide range of individual languages; single-language only.
* **Latency:** \~530ms
### Deepgram
* **Best for:** Common languages with low latency.
* **Language support:** Code-switching across 10 languages: English, Spanish, French, German, Hindi, Russian, Portuguese, Japanese, Italian, Dutch.
* **Latency:** \~300ms
### Soniox
* **Best for:** Multilingual agents that need any-to-any code-switching across a wide set of languages.
* **Language support:** 60+ languages with a single model; same coverage in both single- and multi-language modes.
* **Latency:** \~490ms
## How Retell picks a provider
Retell routes to a provider based on what your agent is optimized for:
* **Optimize for latency** → Retell picks the fastest provider that still performs well for your languages.
* **Optimize for accuracy** → Retell picks the best-performing provider for your languages.
* **Multilingual** → the languages you select also influence which providers are available.
If no single provider can cover all of your selected languages together, the dashboard prevents you from selecting that combination. See [Configure a multilingual agent](/agent/multilingual) for details.
For which languages each provider transcribes, see [Language support by provider](/build/language-support).
# Call transfer node in conversation flow
Source: https://docs.retellai.com/build/conversation-flow/call-transfer-node
Use a call transfer node to hand a Retell phone call off to a different number. Supports Retell and imported numbers, with optional pre-transfer messages.
This node only works during phone calls instead of web calls. It's available for Retell numbers and imported numbers.
Call transfer node is used to transfer the call to another number. The agent will not speak when it's in this node. If you want the agent to say things like `Let me transfer you right away` before performing the actual transfer, you can do so by putting a conversation node (with `skip response` turned on) before this node.
## When Can Transition Happen
Transition happens when transfer fails. There's already a pre-populated edge for this. Feel free to connect that to a node to handle transfer failure.
## Configure Transfer
Set transfer number to be either:
* a number in e.164 format, or a SIP URI in the format of `sip:username@domain` (e.g. `sip:user@retellai.com`).
* [dynamic variable](/build/dynamic-variables) that gets substituted at runtime.
* (Optional) if your transfer destination is not in e.164 format then you can choose to keep the input as is by choosing raw format. This only applies when you are using custom telephony and does not apply when you are using Retell Telephony. This can be useful when you want to transfer to internal pseudo numbers.
Set the transfer number extension if needed. Extension must be 0-9, '\*', '#' (e.g. 123#).
The SIP URI target does not carry inline auth credentials. `sip:user:password@domain` is not supported, and the node has no separate username or password field for the transfer destination. If the destination requires authentication, use custom SIP headers (for example, an `X-` header your SIP endpoint parses), or terminate the transfer at an endpoint you control that then authenticates onward.
Choose between cold transfer, warm transfer, or agentic warm transfer:
* **Cold transfer**: The call is transferred to a destination number and that's it.
* **Warm transfer**: After the call is transferred to the destination number, the AI agent can attempt to detect if the other side is human, leave private messages that are not heard by the user, do a three-way introduction, etc. This is a direct warm transfer flow (not agentic warm transfer). More details below.
* **Agentic warm transfer**: A transfer agent has a two-way conversation with the transfer target and then decides to either bridge the original caller or cancel the transfer.
Use this slider to set how long the destination should ring for this transfer.
* The value you set here applies only to this transfer.
* If you do not set it, we use your agent-level ring duration setting.
* This works for cold transfer, warm transfer, and agentic warm transfer.
You can configure which caller ID shows up to the transfer destination:
1. **Retell Agent's number**: The transfer destination will see the Retell agent's number.
2. **User's Number**: The transfer destination will see the number of the user. Please note that the telephony provider must support caller ID override for this feature to work.
* For warm transfer, it's using SIP DIAL, and we are setting `from` and `P-Asserted-Identity` headers to the user's number.
* For cold transfer, it's using SIP REFER, and it's up to the telephony provider to support caller ID override for SIP REFER.
* Retell Twilio numbers support showing user's number on both warm and cold transfer, Retell Telnyx numbers only support this when using SIP REFER via cold transfer.
* If caller ID override is not supported, the transfer would fail.
For cold transfer, you can configure the following settings:
* **Cold transfer modes**: You can choose `SIP INVITE` or `SIP REFER`.
* **What SIP is**: SIP (Session Initiation Protocol) is the signaling protocol used to set up and route VoIP calls.
* **SIP INVITE**: This is the default transfer method. It establishes or updates the active call path, then bridges the transfer. You can choose which caller ID to use.
* **SIP REFER**: This asks an endpoint to start a separate call to a third party for transfer handoff. Use this only if your telephony provider supports SIP REFER. Caller ID behavior depends on provider support and configuration.
* **Caller ID behavior**: `show transferee as caller` only applies when cold transfer mode is `SIP INVITE`.
For warm transfer (non-agentic), you can configure the following settings:
* **On-hold music**: The audio played to the caller while they are on hold. The default is a standard ringtone.
* **Navigate IVR**: Provide a prompt to help you navigate if the transfer target is an IVR system.
* **Enable human detection**: When enabled, the agent will check if a human is present after the transfer target answers. The original caller will only be connected once a human is detected.
* **Auto-greet**: If enabled, the agent will immediately say “Hello” when the transfer target picks up. This encourages a response, increasing the likelihood of detecting a human.
* **Agent detection timeout**: The maximum amount of time the AI agent will wait to determine whether the transfer target is a human. The caller is connected only if human detection succeeds within this timeframe. Otherwise, the transfer is marked as failed. The default timeout is 30 seconds.
* **Whisper message (optional)**: A message spoken privately to the transfer target before connecting them to the original caller.
* **Three-way message (optional)**: A message spoken to both the transfer target and the original caller once the connection is established.
In an agentic warm transfer, a second AI agent — called a **transfer agent** — answers the handoff, talks with the transfer target, and then decides whether to connect (bridge) your caller or cancel. For this transfer type you can configure:
* **On-hold music**: What the original caller hears while the transfer agent is working.
* **Two-way conversation agent**: The transfer agent (and version) that talks to the transfer target and decides whether to bridge or cancel. See **Choose or create a transfer agent** in the next step.
* **Wait time for agent answer**: How long to give the transfer agent to make a decision.
* **Action on timeout**: What happens if that wait time runs out — **Cancel transfer** (treat it as a failed transfer) or **Bridge the transfer** (connect the caller anyway).
* **Three-way ring tone**: While the transfer agent is handling the handoff, the original caller hears the selected ring tone/on-hold audio until the call is bridged or canceled.
* **Three-way message (optional)**: A message shared with both parties when the call is bridged. You can write it as a **Prompt** (instructions the agent turns into a sentence) or a **Static Sentence** (the exact words to say).
A **transfer agent** is a separate AI agent whose only job is to receive agentic warm transfers — think of it as an AI teammate who answers the handoff, speaks with the transfer target, and decides whether to connect your caller. Transfer agents are kept separate from your normal agents, so they won't clutter your main agent list.
**1. Open the picker.** Click the **Two-way conversation agent** selector (it reads "Transfer screening agent" until one is chosen) to open the **Select transfer agent** window.
**2. Pick an existing one.** Use the search box to find a transfer agent, click it to see a quick preview on the right, then click **Confirm selection**.
**3. Or create a new one.** Click **Create new**, choose the type — **Single Prompt** (simplest: one set of instructions) or **Conversation Flow** (a step-by-step visual flow) — then click **Create**. The new transfer agent opens so you can set it up right away, either in a tab inside the builder or in a new browser tab depending on your agent type.
**4. Manage a transfer agent.** Hover a selected agent and open its **⋯** menu to **Rename** it, choose a specific **Version** to lock to (otherwise it always uses the latest), or **Delete** it. Deleting removes all versions and also removes it from any transfer that was using it.
Add custom SIP headers for outbound calls. Custom SIP headers (usually prefixed with `X-`) let you pass session-specific data, such as user IDs or campaign codes, between VoIP endpoints.
These headers are forwarded to your SIP provider on SIP INVITE and are useful for custom routing and tagging.
Custom SIP headers are preserved only when transferring the call directly to a SIP endpoint. They may be stripped if you are transferring the call to a PSTN number.All header names must start with `X-` or must be `User-To-User` (case insensitive)
**Custom on-hold music (API only):** The dashboard picker offers preset tracks — Ringtone, Relaxing sound, Uplifting beats, or None. To play your own audio during a warm or agentic warm transfer, upload it with the `create-asset` endpoint, then set `on_hold_music` to `custom` and `custom_on_hold_music_asset_id` to the returned asset ID. Accepted formats are MP3, WAV, WebM, OGG, M4A, AAC, and FLAC, up to 10 MB and 210 seconds; Retell normalizes the file on upload. The endpoint accepts an API key, so you can upload from your backend.
**Billing after a transfer:** while the agent is on the line, including the hold and briefing phases of a warm or agentic warm transfer, the call bills at the normal per-minute rate. Once the caller is connected to the destination and the agent drops off, the AI agent fee stops. Only the telephony fee continues for the remainder of the transferred call.
## Rest of Node Settings
* **Talk While Waiting**: when enabled, a text input box will show up where you can write instructions for the agent to follow to generate an utterance like `Let me check that for you.` to say while the function is being executed. You can choose between `Prompt` and `Static Sentence`.
* **Global Node**: read more at [Global Node](/build/conversation-flow/global-node)
* **LLM**: choose a different model for this particular node. Will be used for function argument generation, and potentially talk while waiting message generation.
# Code node: run JavaScript in a conversation flow
Source: https://docs.retellai.com/build/conversation-flow/code-node
Add a Code node to a Retell conversation flow to run JavaScript, read dynamic variables and metadata, call HTTP APIs, and store results.
A Code node runs JavaScript when a conversation flow enters the node. Use it
for calculations, data formatting, and lightweight HTTP lookups that don't
require your own server. The code can read dynamic variables and call metadata,
then store fields from its return value for later nodes.
For example, a Code node can normalize an appointment timestamp, return an
`appointment_label`, and let the flow route based on whether the timestamp was
valid.
If you use a single- or multi-prompt agent, see [Code Tool](/build/single-multi-prompt/code-tool).
## When to use a Code node
| | Code node | Custom function |
| --------------------- | --------------------------------------------------- | --------------------------------------------------- |
| **Runs** | JavaScript in Retell's sandbox | An HTTP request to your server |
| **Trigger** | Automatically when the flow enters the node | Automatically when the flow enters a function node |
| **Inputs** | Dynamic variables and call or chat metadata | Parameters, dynamic variables, and request data |
| **Best for** | Formatting, calculations, and low-risk HTTP lookups | Authenticated integrations and production workflows |
| **Maximum code size** | 20,000 characters | Not applicable |
Use a Code node only for logic that can run without secrets. Dynamic variables
and metadata are stored in plaintext with the call or chat record. For
authentication, secret management, internal systems, or production writes, use
a [custom function](/build/conversation-flow/custom-function) hosted on your
backend.
## Configure a Code node
Select **Code** from the action nodes in the left sidebar.
Select the node, then select **Open** under **Code Configuration**.
Read values from `dv` or `metadata` and return a JSON-serializable value.
Objects are the easiest return type to map into response variables.
```javascript theme={"dark"}
const start = new Date(dv.appointment_start);
if (Number.isNaN(start.getTime())) {
return { valid: false, error: "Invalid appointment_start" };
}
return {
valid: true,
appointment_label: start.toISOString()
};
```
Under **Store Fields as Variables**, map a dynamic-variable name to a path
in the returned value. For the example above, map `appointment_is_valid` to
`valid` and `appointment_label` to `appointment_label`.
See [Store response variables](#store-response-variables) for nested and
array paths.
Set the timeout in the code editor. In the node settings, choose whether the
agent talks or plays a typing sound while the code runs and whether the flow
waits for the result before transitioning.
Use **Dynamic Variables** in the editor to add test values, then select
**Run Code**. The result and any `console.log()` output appear below the
editor.
Test values stay in the dashboard and don't change the live agent. The
dashboard test doesn't populate `metadata`, so metadata-dependent code must
handle an empty object during testing.
## JavaScript environment
The code runs inside a QuickJS sandbox. Top-level `await` is supported.
### `dv`: dynamic variables
Read [dynamic variables](/build/dynamic-variables) as properties of `dv`. All
values in `dv` are strings, including values stored by earlier nodes.
```javascript theme={"dark"}
const customerName = dv.customer_name;
const orderTotal = Number(dv.order_total);
const callerNumber = dv.user_number;
```
Use `dv.user_number`, not `{{user_number}}`, inside JavaScript. Runtime values
such as `call_id`, `chat_id`, `session_type`, `direction`, `user_number`, and
`agent_number` are available when they apply to the current session.
Dynamic variables are available through `dv` inside JavaScript. `{{...}}`
template substitution isn't available inside code.
### `metadata`: call or chat metadata
Read metadata passed when you create the [phone call](/api-references/create-phone-call)
or [chat](/api-references/create-chat).
```javascript theme={"dark"}
const customerId = metadata.customer_id;
const priority = metadata.priority_level;
```
Metadata values can use JSON types. Unlike values in `dv`, they aren't limited
to strings.
### `fetch(url, options)`: HTTP requests
`fetch()` supports HTTP and HTTPS URLs, request methods, string-valued headers,
and request bodies. The returned response supports `status`, `ok`,
`statusText`, `url`, `headers.get()`, `text()`, and `json()`.
```javascript theme={"dark"}
const response = await fetch("https://api.zippopotam.us/us/90210");
if (!response.ok) {
return { found: false, status: response.status };
}
const location = await response.json();
return {
found: true,
city: location.places[0]["place name"],
state: location.places[0]["state abbreviation"]
};
```
The sandbox blocks non-HTTP protocols and selected local, private, and platform
addresses. It doesn't provide the complete browser Fetch API.
Code node `fetch()` requests originate from `35.166.138.221`. If your public
API restricts inbound traffic by IP address, allowlist `35.166.138.221/32`.
### `console.log()`: test output
Use `console.log()` while testing. Logs appear in **Run Code** results. They
aren't added to the live code result or shown as Code node logs in call logs.
```javascript theme={"dark"}
console.log("Customer:", dv.customer_name);
```
### Runtime limits
| Limit | Value |
| ------------------------ | ----------------------------------- |
| Code size | 20,000 characters |
| Timeout | 5–60 seconds; 30 seconds by default |
| Result sent to the agent | 15,000 characters by default |
| Automatic retries | None |
The sandbox provides common JavaScript built-ins such as `Array`, `Date`,
`JSON`, `Map`, `Math`, `Promise`, `RegExp`, and `Set`. Node.js modules, browser
DOM APIs, `require`, `import`, and `Intl` aren't available.
## Store response variables
Response variables copy fields from the full return value into dynamic
variables. For this return value:
```javascript theme={"dark"}
return {
status: "confirmed",
order: {
id: "ord_123",
items: [{ name: "Replacement filter" }]
}
};
```
configure these mappings:
| Dynamic-variable name | Path to value | Stored value |
| --------------------- | --------------------- | -------------------- |
| `order_status` | `status` | `confirmed` |
| `order_id` | `order.id` | `ord_123` |
| `first_item_name` | `order.items[0].name` | `Replacement filter` |
Stored values are strings. Objects and arrays are stored as JSON strings. If a
path doesn't exist or resolves to `null`, Retell skips that variable without
failing the execution.
Response-variable extraction uses the full return value, even when the result
sent to the agent is capped at 15,000 characters.
## Control transition timing
**Wait for Result** is on by default.
* When it is on, Retell waits for the code to finish before evaluating the
node's transition conditions. The return value and stored response variables
are available to those conditions and the next node.
* When it is off, the flow can transition while the code is still running. The
result and stored variables might not be available to the next node.
If **Talk While Waiting** is on, the agent delivers the configured prompt or
static sentence before transitioning. If the caller speaks while the code is
running, the agent can still respond to that turn. Turning **Talk While
Waiting** off suppresses the node's entry message; it doesn't block responses
to caller interruptions.
Use [transition conditions](/build/conversation-flow/transition-condition) that
check stored result fields before the agent confirms an action succeeded.
## Node settings
| Setting | Default | Behavior |
| ------------------------ | ------------ | ----------------------------------------------------------------------------------------------- |
| **Timeout** | 30 seconds | Stops execution after 5–60 seconds. |
| **Talk While Waiting** | Off | Speaks one generated prompt or static sentence when the node starts. |
| **Play typing sound** | Off | Plays a typing sound while Retell waits for the result. Applies when **Wait for Result** is on. |
| **Wait for Result** | On | Waits for the result before transitioning. |
| **Global Node** | Off | Lets other nodes jump to this node without a direct edge. |
| **LLM** | Flow default | Optionally uses a different model for this node's generated speech and transitions. |
| **Fine-tuning Examples** | None | Adds transition examples for this node. |
## Use Code node safely
* Don't put API keys, credentials, or sensitive tokens in code, `dv`, or
`metadata`.
* Use `fetch()` for low-risk HTTP reads. Put authenticated requests and
state-changing operations behind a custom function on your backend.
* For critical routing, provide a safe else path. A timeout, thrown error, or
sandbox failure doesn't retry automatically and doesn't extract response
variables.
## FAQ
No. The sandbox doesn't provide `require`, `import`, Node.js modules, browser DOM APIs, or `Intl`. Use the available JavaScript built-ins and `fetch()`.
Runtime values are available as properties of `dv`, including `dv.call_id`, `dv.user_number`, and `dv.agent_number` when they apply. Use `dv.name` syntax to access them. `{{...}}` template substitution isn't available inside JavaScript.
Retell marks the execution as failed, doesn't extract response variables, and doesn't retry automatically. Use `try/catch` for expected errors and return a structured fallback such as `{ "ok": false, "error": "lookup failed" }`. For critical routing, configure a safe else path.
The setting suppresses the node's entry message. If the caller speaks while the code is running, that user turn can still trigger an agent response before the code result is available. Ground any success confirmation in a stored result field instead of conversation context alone.
Yes. Top-level `await` is supported, including for `fetch()` calls.
Retell sends up to 15,000 characters to the agent by default. Response-variable extraction uses the full return value before that result is capped.
# Reusable subflows for conversation flow agents
Source: https://docs.retellai.com/build/conversation-flow/components
Package parts of a conversation flow into reusable Retell subflows, so you can build complex agents once and share consistent logic across many flows.
## Conversation flow subflows
Make complex agents easier to build, reuse, and maintain by packaging parts of your conversation into subflows. A subflow is a mini flow (a group of nodes) that you can reuse across agents and flows.
Subflows were previously called **Components**. The feature is the same; only the name changed.
### Why use subflows?
* Reuse: Build once, drop into many agents and flows.
* Consistency: Keep behavior uniform across use cases (e.g., identity check).
* Clean canvas: Hide detailed logic inside a focused subflow.
* Faster iteration: Update a shared subflow to improve every agent that uses it.
### Where to find it
In the dashboard, open your agent's builder:
* Left sidebar → **Subflows** tab
* Two sections:
* **Library Subflows**: Account-level, shared across agents.
* **Agent Subflows**: Local to the current agent.
**Switching between flows:** Use the flow selector at the top of the builder (it shows **Main flow** by default) to move between your main agent, any open subflows, and transfer agents. Each one opens in its own editor tab, and you can return to **Main flow** at any time.
## Create a subflow
You can create either a library (shared) subflow or an agent-level (local) one. Both open in a dedicated editor tab.
1. In the **Subflows** tab, click the **+** button next to **Library Subflows** or **Agent Subflows**.
2. You'll start with a "Begin" node, a basic conversation node, and an "Exit Subflow" end node.
3. Add nodes and connect edges to form your subflow.
4. Set a start node by connecting the Begin tag to the first node.
5. Link the "Exit Subflow" node correctly so you won't get stuck inside the subflow.
6. Switch back to the main agent using the flow selector (labeled **Main flow**) at the top of the builder.
7. Rename the subflow by clicking the "..." to the right of its name.
Notes:
* Subflows cannot contain other subflows; you add regular nodes inside a subflow.
* Available node types match your agent's channel (voice vs chat).
## Add a subflow to your flow
* From the **Subflows** tab, click a subflow. A single subflow node appears on your canvas.
* Connect into the subflow: link any node to the subflow node.
* Connect out of the subflow: select the subflow node and connect its outgoing edge to where the conversation should continue.
* To edit what happens inside, open the subflow's editor tab and modify its internal nodes.
* You can rename the subflow node, which is also reflected in the subflow.
Tip: End nodes inside the subflow hand control back to the main flow. Back on the main canvas, make sure the subflow node's outgoing edge points to the next step.
## Shared vs local subflows
Every subflow is either shared across your account or local to a single agent. That choice decides who your next edit reaches.
| | Library Subflow (shared) | Agent Subflow (local) |
| ----------------------- | ------------------------------------------------------------------------------------------ | ----------------------------------------- |
| Where it's stored | Its own account-level record | Inside the agent's flow |
| Which agents can use it | Any agent in the account | Only the agent it was created in |
| Who an edit reaches | Every agent that uses it, published versions included | That agent only |
| Best for | Steps that should stay identical everywhere, like an identity check or a compliance script | Logic that only makes sense for one agent |
### Convert between shared and local
Select the subflow node, open its **Subflow Settings** panel, then set **Subflow Access** to **Agent Only Subflow** or **Shared Library Subflow**.
Going from shared to local, which is the same action as turning off **Sync Updates**, copies the subflow into this agent and stops it from receiving library updates.
### What to watch for
* **Deleting a library subflow** turns every linked instance into a local copy and stops sync updates, so agents that used it keep working.
* **Published agents can reference shared subflows.** The behavior of a [published agent](/agent/version) can still change if it uses a subflow that gets updated. To prevent this, convert to a local subflow before publishing.
## Testing
* To test a subflow under the Test Subflow panel, add the subflow to the main conversation flow first so it initializes properly.
* The global prompt of the main conversation flow applies to all subflow nodes implicitly.
* To test a subflow alone, make it a shared subflow and create a new empty agent with only one subflow node.
## Best practices
* Keep subflows focused: One clear job (e.g., "Collect Shipping Address").
* Name clearly: Use action + outcome (e.g., "Verify Identity").
* Design clean entry/exit: Always set a start node; include an end node to exit cleanly.
* Reuse variables: Use dynamic variables to pass captured data back to the main flow.
* Test in context: Open the Test panel to simulate end-to-end behavior after inserting the subflow.
## FAQ
* How do I update a shared subflow used by many agents?
* Under any agent, navigate to the subflow's edit page. When you edit it, changes apply everywhere it's used.
* Can I stop changes from affecting an agent?
* Yes. In that agent, turn off **Sync Updates** to convert its reference to a local copy.
* What happens if I delete a library subflow?
* Agents keep a local copy; they stop syncing with the deleted library item.
* Can I move a local subflow into the library?
* Yes. In **Subflow Access**, switch it to **Shared Library Subflow** to create a shared version and update references.
* Can subflows include tools/functions?
* Yes. Subflows can include function nodes and use your configured tools. Tools behave the same as in the main flow.
* The tools need to be defined within the subflow and won't be visible outside at the agent level.
* What if I did not link the Begin node in a subflow?
* It transitions to the next node based on the subflow node edges.
* What if I did not link the Exit Subflow node properly?
* It stays stuck inside the subflow and cannot transition out.
# Conversation node
Source: https://docs.retellai.com/build/conversation-flow/conversation-node
Use Retell conversation nodes for multi-turn dialogue without tool calls — the default building block for capturing info and guiding callers through a flow.
The conversation node is the most common node type in a conversation flow. It holds a spoken conversation with the user — no tool calling.
If the agent should also call tools during the same dialogue, use a [subagent node](/build/conversation-flow/subagent-node). If a tool must always run at a fixed point in the flow, use a [function node](/build/conversation-flow/function-node).
Please note that the agent can have a multi-turn conversation inside a single node, so you don't necessarily need to create a new conversation node for every sentence the agent needs to say. It's recommended to split the node when there's a logic split, or the instruction gets too long.
For example, an appointment-booking agent can use one conversation node to collect the caller's preferred date and time — asking follow-up questions across several turns — with transition conditions for "user provided a date and time" and "user asked to speak to a human."
## Write the instruction
Pick how the agent generates what to say in this node:
* **Prompt**: Write instructions and the LLM generates responses dynamically. Use this for anything that depends on what the user says.
* **Static Sentence**: The agent says your exact sentence first. If the conversation stays in the node after that, it continues dynamically based on the static sentence. Use this when the wording must be exact, like disclaimers or greetings.
## When transitions happen
* After the user finishes speaking, following the [evaluation order](/build/conversation-flow/transition-condition#evaluation-order).
* When **Skip Response** is enabled: as soon as the agent finishes speaking.
## Node settings
* **Skip Response**: The node gets a single edge and transitions through it when the agent finishes speaking, without waiting for a reply. Useful for lines that need no response, like a disclaimer before a transfer.
* **Knowledge Base**: Attach node-level knowledge bases to combine topic-specific knowledge with the agent-level knowledge base. Read more at [knowledge base](/build/knowledge-base).
* **Global Node**: Make this node reachable from anywhere in the flow when its condition is met. Read more at [global node](/build/conversation-flow/global-node).
* **LLM**: Choose a different model for this node only. It's used for response generation — for example, a cheaper model for simple routing and a stronger one for complex steps.
* **Speech overrides**: Override the agent-level speech settings for this node only — interruption sensitivity (0–1), response wait time (0–5.5s), voice speed (0.5–2), and whether keypad presses can interrupt the agent (DTMF interruption).
* **Fine-tuning examples**: Add example transcripts to improve this node's responses and transition decisions. Read more at [finetune examples](/build/conversation-flow/finetune-examples).
# Custom function in conversation flow
Source: https://docs.retellai.com/build/conversation-flow/custom-function
Add a custom function to a Retell conversation flow to call your external API mid-call. Configure method, URL, headers, parameters, and responses.
A custom function lets your conversation flow agent call your own API during a live call, then use the response to keep the conversation going. Use it to look up an order, check an account, create a ticket, or trigger any action in your backend that the agent can't do on its own.
## When to use a custom function
Reach for a custom function whenever the agent needs live data from your systems, or has to take an action there, in the middle of a call:
* **Look up real-time data** — order status, appointment slots, account details, shipment tracking.
* **Take an action in your backend** — create a support ticket, update a record, send a confirmation.
* **Branch on your own logic** — call an endpoint that decides where the flow goes next.
Custom functions run inside a [function node](/build/conversation-flow/function-node). If the logic is self-contained and doesn't need your server, a [code node](/build/conversation-flow/code-node) runs it in a sandbox with no endpoint to host, and for common CRM, helpdesk, and calendar actions, [integration tools](/build/conversation-flow/integration-tools) call the provider directly.
### Example: order status lookup
A retail support agent needs to tell callers where their order is. You add a `get_order_status` custom function pointed at `https://api.yourstore.com/orders`. When a caller gives their order number, the agent calls the function with that number, your API returns the status and delivery date, and the flow branches to read it back or to escalate if the order can't be found.
## Create a custom function
When the function is called, Retell sends a request (POST, GET, PUT, PATCH, or DELETE) to your URL with the function name and arguments. You can add headers and query parameters, and extract values from the response.
Give the function a unique **name** using letters and underscores (for example `get_order_status`), and a clear **description**. The agent reads the description to decide when to call the function, so be specific about what it does and when to use it.
For example:
* Name: `get_user_details`
* Description: `Get user details based on name and age`
Select the method Retell uses to call your endpoint: **GET**, **POST**, **PUT**, **PATCH**, or **DELETE**. It defaults to POST.
Retell sends a JSON body only for POST, PUT, and PATCH. GET and DELETE carry data as query parameters only.
Enter the URL Retell calls to run the function. It must be a valid, publicly reachable URL. For security, Retell blocks requests to localhost, private IP ranges, and cloud metadata addresses, so to test against a local server, expose it first with a tunneling service like ngrok.
Set how long Retell waits for your endpoint to respond, in milliseconds. It accepts `1000` (1 second) to `600000` (10 minutes) and defaults to `120000` (2 minutes). If your endpoint doesn't respond in time, the request fails and the agent receives the error message (for example, a timeout error) to act on. By default Retell doesn't retry a custom function, but you can turn on automatic retries with `max_retry` — see [Retries](#retries).
Define custom headers to include with the request. Header values can be static or include [dynamic variables](/build/dynamic-variables), such as an auth token you pass in as `{{token}}`.
Define query parameters as key/value pairs that Retell appends to the endpoint URL. Values are applied directly and can include [dynamic variables](/build/dynamic-variables). Query parameters are never filled in by the LLM — for LLM-supplied values, use the request body parameters below.
For POST, PATCH, and PUT requests, define the parameters the agent sends in the request body, using either JSON schema or the form editor. As with query parameters, a property with a `description` is filled in by the LLM, while a property with a `const` value is applied directly.
**Payload: args only**
When **Payload: args only** is on, the request body is just the function's arguments at the top level, with no wrapper. When it's off, the body follows the [request spec](#request-and-response-spec) below (`name`, `call`, and `args`).
Turn this on when your endpoint expects a flat JSON body that matches your parameter object exactly, with no outer wrapper.
Example parameter schema:
```json theme={"dark"}
{
"type": "object",
"required": ["order_id"],
"properties": {
"name": {
"type": "object",
"properties": {
"first_name": {
"type": "string",
"description": "User first name"
},
"last_name": {
"type": "string",
"const": "{{last_name}}"
}
}
},
"order_id": {
"type": "number",
"const": 1234
}
}
}
```
Example form editor:
Extract values from the JSON response and save them as [dynamic variables](/build/dynamic-variables) to use later in the conversation.
Point each variable at a field in the response using dot notation, with array indexing where needed — for example `user.name` or `data.items[0].id`. This works only when your endpoint returns a JSON object.
For example, from this response you could extract the user's name and reference it later as `{{user_name}}`:
```json theme={"dark"}
{
"user": {
"name": "John Doe",
"age": 26
}
}
```
### Troubleshooting
If the function won't save, the parameters are usually invalid. The most common mistake is leaving `"type": "object"` off the top level of the JSON schema. Click one of the built-in examples and adjust from there.
## Request and response spec
When the function is called, Retell sends a request to your endpoint with the following spec.
**Request**
* Headers
* `X-Retell-Signature`: an HMAC-SHA256 signature of the request body, used to verify the request came from Retell. See [Verify the request is from Retell](#verify-the-request-is-from-retell) below.
* `Content-Type: application/json` (for POST, PUT, and PATCH).
* Body (JSON, for POST, PUT, and PATCH only)
* `name`: the name of the custom function.
* `call`: the call object, for context about the call. It includes the transcript up to the moment the request is sent. See [Get Call](/api-references/get-call) for the full object.
* `args`: the function's arguments, as a JSON object.
When **Payload: args only** is on, the body is just the arguments object — no `name`, `call`, or `args` wrapper. Parse the parameters from the top level, and run signature verification against that same body string.
```json theme={"dark"}
{
"name": "analyze_transcript",
"args": {
"analysis_type": "sentiment"
},
"call": {
"call_type": "web_call",
"access_token": "eyJhbGciOiJIUzI1NiJ9.eyJ2aWRlbyI6eyJyb29tSm9p",
"call_id": "Jabr9TXYYJHfvl6Syypi88rdAHYHmcq6",
"agent_id": "oBeDLoLOeuAbiuaMFXRtDOLriTJ5tSxD",
"agent_version": 1,
"agent_name": "My Agent",
"call_status": "ongoing",
"metadata": {
"internal_customer_id": "cust_12345"
},
"retell_llm_dynamic_variables": {
"customer_name": "John Doe"
},
"custom_sip_headers": {
"X-Custom-Header": "Custom Value"
},
"data_storage_setting": "everything",
"opt_in_signed_url": true,
"start_timestamp": 1703302407333,
"transcript": "Agent: Hi John, thanks for calling! How can I help you today?\nUser: Hi, I'd like to check the status of my recent order.\nAgent: Sure, I'd be happy to help with that. Could you provide me your order number?\nUser: Yes, it's 78542.\nAgent: Let me look that up for you.\n",
"transcript_object": [
{
"role": "agent",
"content": "Hi John, thanks for calling! How can I help you today?",
"words": [
{ "word": "Hi", "start": 0.5, "end": 0.7 },
{ "word": "John,", "start": 0.8, "end": 1.1 },
{ "word": "thanks", "start": 1.2, "end": 1.5 },
{ "word": "for", "start": 1.5, "end": 1.6 },
{ "word": "calling!", "start": 1.7, "end": 2.1 },
{ "word": "How", "start": 2.2, "end": 2.4 },
{ "word": "can", "start": 2.4, "end": 2.5 },
{ "word": "I", "start": 2.5, "end": 2.6 },
{ "word": "help", "start": 2.6, "end": 2.8 },
{ "word": "you", "start": 2.8, "end": 2.9 },
{ "word": "today?", "start": 2.9, "end": 3.3 }
]
},
{
"role": "user",
"content": "Hi, I'd like to check the status of my recent order.",
"words": [
{ "word": "Hi,", "start": 4.0, "end": 4.3 },
{ "word": "I'd", "start": 4.4, "end": 4.6 },
{ "word": "like", "start": 4.6, "end": 4.8 },
{ "word": "to", "start": 4.8, "end": 4.9 },
{ "word": "check", "start": 4.9, "end": 5.2 },
{ "word": "the", "start": 5.2, "end": 5.3 },
{ "word": "status", "start": 5.3, "end": 5.7 },
{ "word": "of", "start": 5.7, "end": 5.8 },
{ "word": "my", "start": 5.8, "end": 5.9 },
{ "word": "recent", "start": 5.9, "end": 6.2 },
{ "word": "order.", "start": 6.2, "end": 6.6 }
]
},
{
"role": "agent",
"content": "Sure, I'd be happy to help with that. Could you provide me your order number?",
"words": [
{ "word": "Sure,", "start": 7.0, "end": 7.4 },
{ "word": "Could", "start": 9.0, "end": 9.2 },
{ "word": "you", "start": 9.2, "end": 9.3 },
{ "word": "provide", "start": 9.3, "end": 9.6 },
{ "word": "your", "start": 9.7, "end": 9.9 },
{ "word": "order", "start": 9.9, "end": 10.2 },
{ "word": "number?", "start": 10.2, "end": 10.6 }
]
},
{
"role": "user",
"content": "Yes, it's 78542.",
"words": [
{ "word": "Yes,", "start": 11.5, "end": 11.8 },
{ "word": "it's", "start": 11.9, "end": 12.1 },
{ "word": "78542.", "start": 12.2, "end": 12.9 }
]
},
{
"role": "agent",
"content": "Let me look that up for you.",
"words": [
{ "word": "Let", "start": 13.5, "end": 13.7 },
{ "word": "me", "start": 13.7, "end": 13.8 },
{ "word": "look", "start": 13.8, "end": 14.0 },
{ "word": "that", "start": 14.0, "end": 14.2 },
{ "word": "up", "start": 14.2, "end": 14.3 },
{ "word": "for", "start": 14.3, "end": 14.5 },
{ "word": "you.", "start": 14.5, "end": 14.8 }
]
}
],
"transcript_with_tool_calls": [
{
"role": "user",
"content": "Yes, it's 78542.",
"words": [
{ "word": "Yes,", "start": 11.5, "end": 11.8 }
]
},
{
"role": "tool_call_invocation",
"tool_call_id": "tool_call_abc123",
"name": "analyze_transcript",
"arguments": "{\"analysis_type\": \"sentiment\"}"
}
],
"latency": {
"e2e": {
"p50": 650,
"p90": 900,
"p95": 1100,
"p99": 1500,
"max": 1600,
"min": 400,
"num": 3,
"values": [400, 650, 1600]
}
}
}
}
```
The request times out after the timeout you set, or 2 minutes by default. By default a custom function isn't retried — if the request fails or times out, the agent receives the error message (such as the HTTP status or a timeout error) and continues based on the flow. To retry failed requests automatically, set `max_retry` (see [Retries](#retries)).
### Retries
Set `max_retry` on the function to retry automatically after a failed attempt. It takes a value from 0 to 5 and defaults to 0 (no retry). Retries fire on any failure — network errors, timeouts, and 4xx or 5xx responses — with exponential backoff plus jitter between attempts. The backoff delay isn't configurable.
The timeout applies per attempt, not as a budget across all attempts, so an attempt that times out is still retried. In the worst case the total time is your timeout times (`max_retry` + 1), plus the backoff between attempts. Only the final attempt's result reaches the agent.
Each retry repeats the request, so your endpoint may process it more than once. Set `max_retry` above 0 only if your endpoint is idempotent.
**Response**
Return a status code between 200 and 299 to signal success. The response body can be a string, buffer, JSON object, or blob — all are converted to a string before being sent to the agent's LLM. Only a JSON object response can populate response variables.
The function result is capped at 15,000 characters by default to avoid overloading the LLM's context window. Contact support if you need a higher limit.
## Verify the request is from Retell
To confirm a request came from Retell, verify the `X-Retell-Signature` header against the raw request body using your Retell API key. For GET and DELETE requests the body is empty, so pass an empty string to the verify function.
```javascript Node.js theme={"dark"}
import { Retell } from "retell-sdk";
import express from "express";
const app = express();
// Use the raw body for signature verification, not JSON.stringify(req.body).
app.use(express.raw({ type: "application/json" }));
app.post("/check-weather", async (req, res) => {
const rawBody = req.body.toString("utf-8");
const signature = req.headers["x-retell-signature"];
if (
typeof signature !== "string" ||
!(await Retell.verify(rawBody, process.env.RETELL_API_KEY, signature))
) {
console.error("Invalid signature");
return res.status(401).json({ message: "Unauthorized" });
}
const content = JSON.parse(rawBody);
if (content.args.city === "New York") {
return res.json("25f and sunny");
}
return res.json("20f and cloudy");
});
```
```python Python theme={"dark"}
import os
import json
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from retell import Retell
app = FastAPI()
retell = Retell(api_key=os.environ["RETELL_API_KEY"])
@app.post("/check-weather")
async def check_weather(request: Request):
raw_body = (await request.body()).decode("utf-8")
valid_signature = retell.verify(
raw_body,
api_key=os.environ["RETELL_API_KEY"],
signature=request.headers.get("X-Retell-Signature"),
)
if not valid_signature:
return JSONResponse(status_code=401, content={"message": "Unauthorized"})
content = json.loads(raw_body)
if content["args"]["city"] == "New York":
return JSONResponse(status_code=200, content={"result": "25f and sunny"})
return JSONResponse(status_code=200, content={"result": "20f and cloudy"})
```
You can also restrict your server to Retell's outbound IP address: `100.20.5.228`.
## FAQ
The agent decides when to call a function from its name, description, and the flow around the function node. Make the description specific about what the function does and when to use it, and make sure the flow reaches the node under the right conditions. Vague descriptions are the most common reason a function never fires.
Unless **Payload: args only** is on, the request body includes a `call` object with details like the call ID, agent ID, metadata, dynamic variables, and the transcript up to the moment the function is called. Use it for context — for example, to look up the caller by their `metadata` or a dynamic variable.
Return it in the response body. Everything you return is converted to a string and handed to the agent's LLM, so the flow can speak or branch on it right away. To reuse a specific value later in the call, map it to a dynamic variable with the response variables setting.
Only if you ask it to. By default (`max_retry` = 0) a custom function isn't retried — if the request fails or times out, the agent receives the error message (such as the HTTP status or a timeout error) and continues based on the flow. Set `max_retry` up to 5 to retry on failure with exponential backoff; because each retry repeats the request, only do this if your endpoint is idempotent. See [Retries](#retries).
The agent handles it like any interruption: it turns to answer the user, and any waiting or follow-up message for that call is skipped. The request to your endpoint is **not** cancelled, though — it runs to completion (up to the timeout), so any action it takes still happens and any response variables it returns are still saved. The result stays in the transcript, so the agent can still bring it up on a later turn; it just won't read it back right at that moment. Because the request still reaches your endpoint, make side-effecting actions like creating a ticket or charging a card idempotent so a repeat call is safe.
Verify against the raw request body, not a re-serialized version. `JSON.stringify(req.body)` can reorder keys or change whitespace, which breaks the signature. Read the raw body (for example, with `express.raw`) and pass that exact string to the verify function.
No. Retell blocks requests to localhost, private IP ranges, and cloud metadata addresses to prevent server-side request forgery. Your endpoint must be publicly reachable — to test a local server, expose it with a tunneling service like ngrok.
Yes. The function result is capped at 15,000 characters by default before it reaches the LLM. Return only what the agent needs, and contact support if you need a higher limit.
# Debug guide
Source: https://docs.retellai.com/build/conversation-flow/debug-guide
Diagnose and fix Retell conversation flow agent issues — wrong responses, missed transitions, and prompt problems — with a step-by-step guide.
Conversation flow is a powerful and flexible tool, which means that there's a lot of action items one can take when the agent's performance is not meeting your expectation. This guide is designed to help you identify the root cause of the issue, and provide actionable steps to improve the agent's responses and transitions.
This guide only covers the response part of the agent, if you have issues with agent audio, like pronunciation, please refer to other guides.
## Step 1: Identify the issue
When the agent is not responding as expected, there can be several reasons:
* The agent is not following instructions within a node
* Node transitions are not working as expected
* The actual conversation does not match the flow graph (e.g., users deviate from expected steps)
## Step 2: Fix the issue
Note that these issues are not mutually exclusive - you may need to implement multiple solutions to fully resolve the problem.
### Issue: Agent is not following instructions within a node
#### Split the node into multiple nodes
For example, if a node contains instructions to collect customer name, phone number, and address, the agent might inconsistently ask for only some of this information:
You can improve consistency by splitting this into three separate nodes:
#### Change the node model
If the instructions are concise but the agent struggles to follow them, try using a more capable LLM model for this node.
#### Add conversation finetune examples
To achieve a specific response style, add conversation finetune examples. Learn more in our [Finetune Examples](/build/conversation-flow/finetune-examples) guide.
#### Adjust the LLM temperature
If the agent's responses are inconsistent, try adjusting the LLM temperature:
### Issue: Node transitions are not working as expected
If the agent isn't transitioning to the expected node, try these solutions:
* Review your transition conditions: Ensure they precisely match your intended triggers. Consider prompt engineering or breaking down complex conditions into multiple simpler ones.
* Add transition finetune examples: Provide examples to help the model understand your expectations. See our [Finetune Examples](/build/conversation-flow/finetune-examples) guide.
* Remove numbered labels from prompts: Items like "PRIORITY 1" or "Step 2" in your global prompt or node instructions can be confused with transition options and trigger a transition whose condition was never met. Use letters ("PRIORITY A") or descriptive names instead — see the [transition conditions FAQ](/build/conversation-flow/transition-condition#faq).
To handle missing transition scenarios:
* Add more nodes to cover edge cases, particularly global nodes for handling unexpected situations. Learn more about [Global Nodes](/build/conversation-flow/global-node).
* Make transition conditions more flexible and general.
### Issue: Actual conversation does not match the flow graph
When users deviate from the defined flow:
* Add key steps as global nodes to allow users to skip or jump between nodes. This is particularly useful for inbound support cases without a rigid call structure. See our [Global Node](/build/conversation-flow/global-node) guide.
* Make node instructions more flexible and let the model handle the details naturally.
# End node
Source: https://docs.retellai.com/build/conversation-flow/end-node
Use an end node in a Retell conversation flow to terminate the call cleanly, optionally with a closing message generated from a prompt or static text.
The end node ends the call. It's a terminal node — it has no outgoing edges, and the call ends the moment the agent enters it. You can add as many end nodes as you need, one for each way a call can finish.
By default the agent hangs up immediately, which callers experience as abrupt. Enable **Speak During Execution** so the agent says a closing line first.
For example, an appointment-booking flow can end with a static closing line — "You're all set for Tuesday at 2 PM. Goodbye!" — after the booking succeeds, and use a second end node with a different message on the cancellation path.
## Node settings
* **Speak During Execution**: When enabled, a text box appears where you define the closing message the agent speaks before hanging up. Choose **Prompt** to let the LLM generate a farewell from your instruction, or **Static Sentence** for an exact line like `Goodbye, have a nice day`.
* **Global Node**: Make the end node reachable from anywhere in the flow when its condition is met — for example `User wants to end the call or says goodbye` — so you don't have to wire an edge to it from every node. Read more at [global node](/build/conversation-flow/global-node).
## End node vs End Call tool
Use an end node when the call should end at a fixed point in the flow. If instead the agent should be able to end the call mid-dialogue whenever it judges the conversation complete, attach an [End Call tool](/build/single-multi-prompt/end-call) to a [subagent node](/build/conversation-flow/subagent-node).
# Extract Dynamic Variable Node
Source: https://docs.retellai.com/build/conversation-flow/extract-dv-node
Add an extract dynamic variable node to a Retell conversation flow to pull values from the dialogue and store them as text, number, boolean, or enum variables.
Extract dynamic variable node is used to extract information from the conversation and store it as a dynamic variable. It's not intended for having a conversation with the user.
## Add a variable
To create a variable, fill in the following details:
* **Variable Name** – A short name to reference this variable.
* **Description** – A brief explanation of what this value should be.
* **Variable Type** – Choose from `Text`, `Number`, `Enum`, or `Boolean`.
* **Enum Options** - Options to choose from. Only when type is enum.
***
## Variable Types
You can create variables of the following types:
* **Text** - Any word or sentence. Examples: `"headache"`, `"John Smith"`
* **Number** - A numeric value. Examples: `42`, `98.6`
* **Enum** - A value from a predefined list. Examples: `"Yes"`, `"No"`, `"Maybe"`
* **Boolean** - True or false.
## Node Settings
* **Global Node**: read more at [Global Node](/build/conversation-flow/global-node)
* **LLM**: choose a different model for this particular node. Will be used for function argument generation, and potentially speak during execution message generation.
* **Fine-tuning Examples**: Can finetune transition. Read more at [Finetune Examples](/build/conversation-flow/finetune-examples)
# Finetune Examples
Source: https://docs.retellai.com/build/conversation-flow/finetune-examples
Add finetune examples to Retell conversation, subagent, and function nodes to correct unexpected responses or transitions with concrete transcript snippets.
When agent response or transition is not meeting your expectation, you might want to supply some examples to finetune the behavior. You can do so by adding `finetune examples`.
Here are the nodes that support finetune examples:
* Conversation Node: support finetune examples for response and transition
* Subagent Node: support finetune examples for response and transition
* Function Node: support finetune examples for transition
When configuring the finetune example, you will provide a transcript as the context. You can select `user`, `agent`, `function` as the role of the transcript. When selecting `function` as the role, you can fill out both the invocation and result of the function. Refer to the History tab of the dashboard to see examples of transcripts.
## Finetune Examples for Conversation
Supplying a transcript as the context is everything you need to do. It's not necessary to provide the entire call transcript, you can simply provide the relevant part. Please note that at least one `agent` response is required, as this is finetuning the agent's response.
## Finetune Examples for Transition
Here you need to provide both a transcript as context, and the transition result. If you cannot distinguish between the different nodes available as transition target, you can try to rename your nodes to make it easier to distinguish.
[Global nodes](/build/conversation-flow/global-node#global-node-examples) also take example conversations, but they follow a different rule: because a global node is only evaluated after a user turn, each example should end with a user utterance.
# Flex Mode: dynamic conversation flow execution
Source: https://docs.retellai.com/build/conversation-flow/flex-mode
Flex Mode compiles a Retell conversation flow into a single structured prompt at runtime so the agent can handle varied user behavior while keeping flow logic.
Flex Mode combines the best of both worlds:
* Conversation Flow: clear, visual business logic that’s easy to manage.
* Single Prompt Agent: flexible, natural handling of varied user behavior.
You design your conversation flow as usual (nodes, edges, tools). At
runtime, Flex Mode compiles that flow into one structured prompt made of Tasks
and available Tools. The agent then navigates Tasks dynamically while still
following your global prompt.
## Cost Impact
Flex mode can significantly increase your LLM costs. Because all node instructions,
transitions, and tool descriptions are compiled into a single prompt, the total token
count is much higher than in rigid mode (where only the active node's prompt is sent
to the LLM). When the combined prompt exceeds 4,000 tokens, the
[token scaling billing rule](/accounts/billing-exceptions#rule-2-llm-price-scaling-for--4000-token-prompt-length)
applies, which can multiply your costs several times over.
To control costs, consider using **rigid mode** or **breaking your flow into smaller
subflows** so that fewer nodes are compiled into a single prompt. If you do use flex
mode, keep node instructions concise to minimize token usage.
## When To Use
* You want the clarity of a flowchart (business steps) but need the freedom of a
single prompt:
* You can easily switch context from different tasks e.g. every node would
become a global node
* The agent could move on to the proper task if the user completed multiple tasks at the
same time.
* After switching the context to another flow, the agent could resume the previous
task without repeating the already completed steps.
## How It Works
You can enable the 'Flex Mode' either at the subflow level or the agent level.
When enabled at agent level, all the nodes get converted to a single flex
node. It will stay on the flex node and behave like a single prompt agent until
it reaches the 'End Call'.
When enabled on a subflow, only that subflow’s nodes are converted into a
single prompt; the rest stays as standard conversation flow.
## Tool Call / Function
There are some differences in how flex mode (single prompt) and traditional
conversation flow handle the tool call/function.
* **Speak During Execution** The execution message part will still work the same.
* **Speak After Execution** There is no 'Speak After Execution' setting in Flex
Mode. The agent will always speak after function execution.
* **Wait For Result** There is no 'waitForResult' setting in Flex Mode. Agent
will always wait for the function to complete (similar to Single Prompt
agent).
## Knowledge Base
Node-level knowledge base will be ignored in Flex Mode. You will need to configure the knowledge base at agent level.
## Best Practices & Known Issues
* Write the node instruction in a concise manner so that LLM could better focus
on the task.
* Only use Prompt edge, avoid using Equation edge as LLM is really bad at
interpreting equation conditions. You might see very weird behaviors.
* Be explicit on transitions: write crisp, observable conditions.
* If you use flex mode for more than 20 nodes, performance might degrade and
agent might have higher hallucination risk. We recommend splitting into
smaller subflows.
* LLM might not always follow the static text instruction.
# Call a function from a conversation flow node
Source: https://docs.retellai.com/build/conversation-flow/function-node
Run a pre-built function, integration tool, or custom function from a Retell conversation flow function node; it fires on entry and transitions on the result.
Function node is used to call a function, whether it's a pre-built function, an integration tool, or a custom function. It's not intended for having a conversation with the user, but the agent can still talk while in this node if needed.
The function associated with this node will be called when entering this node.
## Add a function
Here you need to add the function first, and then select it inside the node. This way if you delete the node, you don't need to re-create the function again.
For specific instructions on different types of functions:
* [Custom Function](/build/conversation-flow/custom-function)
* [Integration tools](/build/conversation-flow/integration-tools) from connected providers like HubSpot, Salesforce, Zendesk, and Cal.com
To check availability or book appointments, use the [Cal.com](/integrations/cal-com-functions) or [Calendly](/integrations/calendly-functions) integration tools. The built-in Cal.com functions are no longer offered in the node — see the [deprecation notice](/deprecation-notice/2026/10-31_legacy_calcom_tools) for what happens to flows that still have them.
## When can transition happen
* if `wait for result` is turned off
* if `talk while waiting` is turned on, the agent will transition once done talking
* if `talk while waiting` is turned off, the agent will transition immediately after function gets invoked, which is right upon entering the node
* if the user interrupts the agent, the transition can also happen once the user is done speaking
* if `wait for result` is turned on
* if `talk while waiting` is turned on, the agent will transition once function result is ready and agent is done talking
* if `talk while waiting` is turned off, the agent will transition once function result is ready
* if the user interrupts the agent, the transition can also happen once function result is ready and the user is done speaking
Given that the function node takes function result into consideration for transition timing, you can write your transition condition to be based on the function result.
## Node settings
* **Talk While Waiting**: when enabled, a text input box will show up where you can write instructions for the agent to follow to generate an utterance like `Let me check that for you.` to say while the function is being executed. You can choose between `Prompt` and `Static Sentence`.
* **Wait for Result**: when enabled, the agent will wait for the function to finish executing before attempting to transition to any other node. This guarantees that when you reach the next node, the result is already ready to be used.
* **Global Node**: read more at [Global Node](/build/conversation-flow/global-node)
* **LLM**: choose a different model for this particular node. Will be used for function argument generation, and potentially speak during execution message generation.
* **Fine-tuning Examples**: Can finetune transition. Read more at [Finetune Examples](/build/conversation-flow/finetune-examples)
## How to tell user the result
Since the function node is not intended for having a conversation with the user, you will need to attach a conversation node to the function node to tell the user the result. You can create different conversation nodes for different function results, so that it can engage the user in different ways when function result varies.
# Global Node
Source: https://docs.retellai.com/build/conversation-flow/global-node
Global nodes in a Retell conversation flow can be reached from anywhere in the agent — ideal for handling universal intents like callbacks and human handoff.
Global nodes can be transitioned to from anywhere in the conversation flow, making them ideal for handling universal scenarios like user objections (e.g. `I want to talk to a human / I need to call back later`). Toggle on `Global Node` in the node settings to enable this.
## Configure Global Node
Set a condition for when the global node should be transitioned to. In the example above, the condition is `When user indicates this is not a good time to continue` — so whenever the user says something like `I need to call back later`, the agent transitions to this node.
Since a global node can be transitioned to from anywhere, it does not need to be connected to the rest of the graph.
## Global Node Examples
Add example conversations to help the AI better understand when to jump to this global node. Click `+ Add` to create examples, then set the type to `Jump to global node` or `Not jump to global node` to demonstrate scenarios where the global node should or should not be activated.
Global nodes are only evaluated after a user turn, so each example should end with a user utterance. New examples default to an agent turn followed by a user turn for this reason. See [Finetune Examples](/build/conversation-flow/finetune-examples) for how transcript examples work.
## Go Back to Previous Node
Enable **Go back to previous node** to let the conversation return to where it left off after the global node is handled. Once enabled, a **Go Back Condition** section appears on the node where you define when the agent should navigate back. In the example above, the condition is `User changed their mind and wants to continue the call`.
Go back conditions support both prompt-based and equation-based conditions. You can add multiple conditions and reorder them by dragging.
## Prevent Immediate Re-Trigger
Enable **Prevent Immediate Re-Trigger** to pause the global node for a specified number of node steps after it has been triggered. Set the number of **Node steps** (defaults to 3) during which the global node will not be activated again. This prevents the conversation from looping when the user's phrasing keeps matching the global node condition.
# Configure global settings
Source: https://docs.retellai.com/build/conversation-flow/global-setting
Configure global settings for a Retell conversation flow agent — voice, language, LLM, denoising, and other agent-level options on the empty canvas.
## Agent Global Settings
Click on the empty canvas and click settings to access the global settings. Here's where you set a lot of agent-level settings.
1. Open the voice selection dropdown menu:
2. Listen to the available voice samples and select the voice you want to use for the agent:
**Custom Voices**: You can also add voices from the ElevenLabs community or clone voices by clicking "Add custom voice". Learn more in our [voice configuration guide](/build/voice).
3. You can also adjust a couple of voice settings:
* voice temperature to make the voice more variant or stable.
* voice speed to make the agent speak faster or slower.
* voice volume to make the agent speak louder or quieter.
* voice model (if applicable): when using certain voice providers, you can choose between different models. Check out the dashboard for detailed nuances of each model.
Pick the language(s) the agent will understand and speak. This affects speech recognition, voice pronunciation, and the language the agent responds in — you do not need to add a "respond in X" instruction to your prompt.
To support multiple languages, switch the selector to **Multiselect** and pick the specific languages you want; for best accuracy, prefer a single language when possible. See [Set language for your agent](/agent/language) and [Configure a multilingual agent](/agent/multilingual) for details.
Select the model you want to use for the agent. The dropdown includes the standard language models plus a **Speech to speech** group (such as `gpt-realtime`), a single model that takes audio in and returns audio directly instead of running a separate text step. Optionally you can tune the LLM temperature to make answers more variant or more stable.
We recommend starting with GPT-4.1, which offers an optimal balance of:
* Response quality
* Latency
* Cost-effectiveness
With a standard model you can override the model on individual [nodes](/build/conversation-flow/node). Speech to speech runs one model across the whole flow, so per-node model overrides are hidden while it's selected. Switching a flow to or from Speech to speech also moves the agent to a compatible voice automatically, since Speech to speech needs a realtime-capable voice.
Here's where you specify the agent's persona, identity, guardrails, etc. This set of text will be available in every node, and will influence all response generation.
Here's where you can supply contexts to the agent via documents, URLs, or texts. Read more at [Knowledge Base Guide](/build/knowledge-base).
Here are a lot of options that allow you to finetune how your agent interacts with the user.
* Background sound: select a background sound that plays throughout the whole call to mimic an environment like a call center, making the conversation more humanlike and engaging.
* Response Wait time: how long the agent deliberately waits after the caller stops speaking before it responds, from no added wait (the default) up to 5.5 seconds, shown in milliseconds below one second and in seconds above it. This is a minimum wait: the agent holds off longer when it detects the caller hasn't finished their thought. Raise it for callers who speak slowly or pause mid-sentence, but note the full wait is added to every turn, so a higher value makes the agent feel slower. In the API this is the `responsiveness` field, from 0 to 1 with a default of 1: a value of 1 adds no wait, 0.9 adds 1 second, and each further 0.1 lower adds 0.5 seconds, up to 5.5 seconds at 0 (values between 0.9 and 1 taper between 0 and 1 second). Check "Dynamically adjust based on user input" to let the agent tune its wait to the caller's pace during the call. Individual nodes can override this setting in their [speech overrides](/build/conversation-flow/conversation-node).
* Interruption Sensitivity: how fast the agent gets interrupted by user interruptions. Set it lower if you want the agent to be more resilient to background speech or user interruptions.
* Backchanneling: Set up how often and what words the agent uses to acknowledge users.
* Boosted Keywords: Provides some biases towards certain words, making it easier to get recognized. Common ones are brand names, people's names, etc.
* Speech Normalization: convert entities like date, currency, numbers into plain words, which can help prevent issues where audio generated was not pronouncing those right.
* Reminder frequency: how often the agent will remind the user when the user is inactive.
* Pronunciation: [set a pronunciation guide](/build/add-pronunciation) for specific words.
Here are a couple of settings that are more call operation related.
* Voicemail related settings: set up voicemail detection and what to do when voicemail is detected. See more at [Handle Voicemail](/build/handle-voicemail).
* End call on silence: set up so that if the user is inactive for a certain amount of time, the call will be ended.
* Call duration: set up the maximum duration of the call.
* Pause before speaking: For the beginning of the call, if the agent speaks first, it will wait for the configured duration before speaking, useful to handle scenarios when the user is still picking up the phone.
Probably set up later; read more at [Post Call Extraction Guide](/features/post-call-analysis-overview).
Here's where you can set up whether to opt out of sensitive data storage, and configure webhook settings for receiving call related events.
## Configure Who Speaks First
Click on `begin` icon, and you can select who speaks first in the call.
# Integration tools in conversation flow
Source: https://docs.retellai.com/build/conversation-flow/integration-tools
Call HubSpot, Salesforce, Zendesk, or Cal.com tools from a Retell AI conversation flow function node, and branch transitions on what comes back.
In a conversation flow, integration tools run through a [function node](/build/conversation-flow/function-node): the node fires the tool on entry, and the flow transitions on the result. Retell handles the provider's API and auth, so there's no server of your own in between. For single- and multi-prompt agents, add the same tools from the Functions section instead; see [integration tools for prompt agents](/build/single-multi-prompt/integration-tools).
See the [integrations overview](/integrations/overview) for the providers Retell connects to, what each one's tools do, and how to connect them. [Subagent nodes](/build/conversation-flow/subagent-node) can use integration tools too, alongside their other tool types. Integration tools can also run outside the conversation, before it starts or after it ends; see [agent workflow](/agent/agent-workflow).
## Add an integration tool to a flow
Connect it once on the dashboard's **Integrations** page — a workspace-level step covered in the [integrations overview](/integrations/overview#connect-a-provider).
In the flow, add a function; connected providers' tools appear in the menu grouped by provider. You can also pick **Add integration** in the menu to connect a new provider without leaving the flow.
Pick the tool in the node, as with any function. Configuration works the same as for [prompt agents](/build/single-multi-prompt/integration-tools): inputs are fixed values or filled from the conversation, response fields map to [dynamic variables](/build/dynamic-variables), and you can test the tool with a live request before the first call.
## Branch on the result
The function node considers the tool's result for transition timing (see [when transitions happen](/build/conversation-flow/function-node#when-can-transition-happen)), so your [transition conditions](/build/conversation-flow/transition-condition) can branch on what came back: one edge for "contact found", another for "no match", each leading to a different conversation node.
Each tool call times out after 3 to 14 seconds, depending on the provider and tool (as of August 2026). On timeout or error the flow still transitions, so give errors their own edge rather than letting them fall through a success path.
# Logic Split Node
Source: https://docs.retellai.com/build/conversation-flow/logic-split-node
Use a logic split node to branch a Retell conversation flow on rules — the agent evaluates conditions on entry and routes silently to the next node.
Logic split node is used to branch out the conversation flow based on the conditions. When entering this node, the agent will immediately evaluate the conditions and branch out to the corresponding destination nodes. The agent would not speak in this node, and the time spent in this node is minimal.
It can come in handy when you want to further split the conversation flow based on the conditions, and do not want to stack all your conditions in previous nodes. It can also be hard for agent to handle a bunch of conditions all at once, so this node can help break it down. It can also be useful when you want to branch out based on dynamic variables.
## When Can Transition Happen
Transition happens immediately when agent enters this node.
## Configure branching logic
* add conditions just like you would in other nodes.
* set up the else destination: there will always be an else condition, which will be the default destination if none of the conditions are met, because this node is designed to be a split point and you want to make sure the conversation flow is not stuck here.
## Rest of Node Settings
* **Global Node**: read more at [Global Node](/build/conversation-flow/global-node)
* **Fine-tuning Examples**: Can finetune transition. Read more at [Finetune Examples](/build/conversation-flow/finetune-examples)
# MCP node in conversation flow
Source: https://docs.retellai.com/build/conversation-flow/mcp-node
Add an MCP node to a Retell conversation flow to call remote MCP server tools during a live call, with custom headers and authentication.
The MCP node is used to call tools on your MCP server. It's not intended for having a conversation with the user, but the agent can still talk while in this node if needed. For single- and multi-prompt agents, connect an MCP server through the response engine instead — see [MCP tools for single and multi-prompt agents](/build/single-multi-prompt/mcp).
## Add MCP server
To create an MCP node, we first need to add an MCP server.
You can define custom headers to include with the request Retell sends to your MCP Server.
Headers and query parameters are the only way Retell authenticates to your MCP server — most often an auth header such as `Authorization: Bearer `. Retell doesn't run an interactive OAuth flow; if your server requires OAuth, obtain the access token outside of Retell and pass it in a header. Values can include [dynamic variables](/build/dynamic-variables), so you can pass a per-call token as `{{access_token}}`.
To restrict your MCP server to requests from Retell, allowlist Retell's outbound IP address: `100.20.5.228`.
You can define query parameters to include in the request URL that Retell appends to your MCP Server Endpoint.
Select MCP Tool
Extract values from the MCP tool response and save them as dynamic variables for use later in the conversation.
For example, you can extract a user’s name from the response and reference it later using \{\{user\_name}}.
Example response body
```javascript theme={"dark"}
{
"properties": {
"user": {
"name": "John Doe",
"age": 26
}
}
}
```
## Node Settings
* **Global Node**: read more at [Global Node](/build/conversation-flow/global-node)
* **Fine-tuning Examples**: Can finetune transition. Read more at [Finetune Examples](/build/conversation-flow/finetune-examples)
# Nodes and edges
Source: https://docs.retellai.com/build/conversation-flow/node
Nodes are the building blocks of Retell conversation flows. Learn every node type, how edges connect them, and how transition conditions advance calls.
A conversation flow is a graph of **nodes** connected by **edges**. Each node handles one step of the call — talking with the user, running a tool, branching on data, transferring, or ending the call. Each edge carries a [transition condition](/build/conversation-flow/transition-condition) that decides when the agent moves to the next node.
Breaking the conversation into nodes lets you control, fine-tune, and debug each step independently — you can change one part of the flow without touching the rest.
## Pick a node type
Each node type plays a specific role. Pick the type that matches what the step needs to do.
### Dialogue nodes
* [**Conversation Node**](/build/conversation-flow/conversation-node): Pure dialogue with the user — no tool calling. The agent can hold a multi-turn conversation within a single node, so you don't need a new node for every line. Supports two instruction modes: **Prompt** (the LLM generates responses dynamically) and **Static Sentence** (the agent says a fixed line first, then continues dynamically if the conversation stays in the node). Transitions are evaluated after each user response.
* [**Subagent Node**](/build/conversation-flow/subagent-node): Dialogue where the agent can also call tools/functions based on context. Unlike a function node (which executes deterministically on entry), the LLM decides **whether and when** to invoke each tool from what the user says. Supports multiple tools per node and stays active across multiple turns and tool calls before transitioning. Use a function node instead when a tool must always run at that step.
* [**Extract DV Node**](/build/conversation-flow/extract-dv-node): Extracts information from the conversation so far and stores it as dynamic variables on entry — not for dialogue. The LLM analyzes the full conversation to capture each value you define by name, description, and type (Text, Number, Enum, or Boolean). Useful for structured data that downstream nodes or Post Call Extraction need.
### Action nodes
* [**Function Node**](/build/conversation-flow/function-node): Executes a single tool/function deterministically on node entry — not for dialogue. Turn on **Wait for Result** when the next hop depends on the return value, then branch on the outcome directly from this node. The agent can optionally speak while it runs (e.g. "Let me check that for you").
* [**Code Node**](/build/conversation-flow/code-node): Runs JavaScript in Retell's sandbox on entry — no external server needed. Best for data transformation, calculations, formatting, and simple read-only lookups via `fetch()`. Has access to dynamic variables and call metadata, and can store return values into dynamic variables. For anything needing secrets, authentication, or writes, use a custom function instead.
* [**SMS Node**](/build/conversation-flow/sms-node): Sends an SMS during an active phone call, to the caller or another number. Requires an SMS-enabled or SMS-approved Retell number. Transitions once the SMS succeeds or fails. *(Voice agents only.)*
* [**MCP Node**](/build/conversation-flow/mcp-node): Calls a tool on an external MCP (Model Context Protocol) server on entry. Connect your server, select a tool, and optionally extract response values into dynamic variables for later nodes. The agent can speak while it runs.
### Call control nodes
* [**Call Transfer Node**](/build/conversation-flow/call-transfer-node): Transfers the call to another phone number or SIP URI. The agent doesn't speak in this node — put a conversation node with **Skip Response** before it for a line like "Let me transfer you." Supports cold, warm (with human detection, whisper/three-way messages), and agentic warm transfer. Use this node — not a conversation node — whenever the agent should transfer to a human. *(Voice agents only.)*
* [**Transfer Agent Node**](/build/conversation-flow/transfer-agent-node): Hands the conversation to a different Retell agent mid-call. Near-instant (no new phone call), the destination agent inherits the full conversation history, and no separate phone number is needed. Preferred over call transfer when routing between AI agents (e.g. front-desk → booking, or language-based routing).
* [**Press Digit Node**](/build/conversation-flow/press-digit-node): Navigates IVR menus by sending DTMF tones. The agent doesn't speak — it listens to IVR prompts and infers which digit to press from your instruction, evaluating on each IVR utterance. Give clear guidance on which options to choose and which to avoid. *(Voice agents only.)*
* [**End Node**](/build/conversation-flow/end-node): Ends the call. Enable **Speak During Execution** with an instruction so the agent gives a closing message before hanging up; otherwise the call ends abruptly.
### Logic and structure
* [**Logic Split Node**](/build/conversation-flow/logic-split-node): Evaluates conditions and branches the flow immediately on entry — the agent doesn't speak. Useful for splitting on dynamic variables or conditions without stacking everything onto the previous node. Always has an else destination as a fallback so the flow never gets stuck.
* [**Subflow**](/build/conversation-flow/components): Packages a reusable subflow (a group of nodes) that appears as a single node on the main canvas. Subflows can be local to one agent or shared across agents in a library; they run their internal nodes and return control to the main flow via an exit node. Tools defined inside a subflow stay scoped to it.
## How nodes connect
Every node except the end node moves the call forward through edges. A node can have several kinds:
* **Regular edges** each carry a transition condition — a prompt the LLM evaluates, or an equation over dynamic variables — plus a destination node.
* **Else edge** is the fallback: it fires when no other condition matches, so the flow never gets stuck.
* **Always edge** transitions unconditionally after the user responds.
* **Skip Response edge** appears when the node's Skip Response setting is on: the node transitions as soon as the agent finishes speaking, without waiting for a reply.
How conditions are written, and the exact order they're evaluated in, is covered in [transition conditions](/build/conversation-flow/transition-condition).
## Add a node
Click a node type in the left sidebar to add it to the canvas.
Click the node to open its settings on the right, and fill in the node instruction inside the node. See the guide for each node type for details.
Click the bottom part of the node to add edges, then write a [transition condition](/build/conversation-flow/transition-condition) for each.
Click and hold the circle to start a line that connects the node to another node, and another node to this node.
## Organize nodes
After adding many nodes, the canvas can get cluttered. Use the **Organize** button to arrange them automatically.
Name your nodes. Call transcripts in the history tab show transitions by node name, so descriptive names make past calls much easier to debug.
## Copy and paste nodes
You can copy nodes and paste them elsewhere on the canvas, into another flow, or even into a different browser tab. Retell uses your system clipboard, so a copy stays available across tabs and windows.
Click a node, or click and drag to select multiple nodes and sticky notes.
Press `Cmd/Ctrl + C`. A **Copied** confirmation shows how many items were copied. Any selected sticky notes, and the tools attached to the selected nodes, are copied along with them.
Move your cursor to where you want the items to appear and press `Cmd/Ctrl + V`. A **Pasted** confirmation appears, and the nodes, notes, and tools are added at your cursor, automatically positioned to avoid overlapping existing nodes.
A few things to keep in mind:
* If a copied tool already exists in the destination, it isn't added again — the pasted node reuses the existing tool.
* Component nodes can't be pasted inside a component editor. If you try, Retell shows a **Cannot Paste** message.
## FAQ
Consider breaking down a node when:
* The node handles multiple complex logic paths
* The LLM struggles with consistency (hallucinations or incorrect responses)
* You need different settings (model, temperature) for different parts
* The conversation flow becomes hard to follow or debug
Breaking complex nodes into smaller, focused nodes often improves reliability.
Use the scroll wheel on a mouse, or pinch on a touchpad.
No, you can add as many nodes as you want.
# Conversation flow agents: structured voice AI call control
Source: https://docs.retellai.com/build/conversation-flow/overview
Build structured Retell voice agents with conversation flow — nodes for dialogue, tools, transfers, code, and transitions for precise call control.
## What is a Conversation Flow Agent?
Conversation flow agents allow you to create multiple nodes to handle different scenarios in conversations. This approach provides more fine-grained control over the conversation flow compared to Single/Multi Prompt agents, enabling you to handle more complex scenarios with predictable outcomes.
### Key Benefits
* **Structured conversations**: Define exact paths and transitions
* **Predictable behavior**: Each node has specific logic and outcomes
* **Complex scenario handling**: Support for conditional branching and state management
* **Fine-tuning capabilities**: Improve performance with node-specific examples
## Components
* **Global Settings**: Configuration that applies to the entire conversation, including:
* Global prompt and personality
* Default voice and language settings
* Agent-wide parameters and behaviors
* **[Node](/build/conversation-flow/node)**: The basic unit of conversation flow. Multiple node types are available:
* Conversation nodes for dialogue without tool calling
* Subagent nodes for dialogue with tool calling
* Function nodes for deterministic API and tool execution
* Logic nodes for branching
* End nodes for call termination
* **[Edge](/build/conversation-flow/transition-condition)**: Connections between nodes that define transition logic:
* Condition-based transitions
* Default fallback paths
* Dynamic routing based on conversation context
* **Tools / Functions**: Reusable capabilities that can be attached to subagent nodes or invoked from function nodes. Conversation nodes do not use tools / functions:
* Custom API integrations
* Built-in utilities (calendar, SMS, transfers)
* External service connections
## How it Works
Every node defines a small set of logic, and the transition condition is used to determine which node to transition to. Once the condition is met when checked, the agent will transition to the next node. There are also finetune examples on nodes that can help you further improve the performance. It might take longer to set up, as you want to cover all the scenarios, but after that it's much easier to maintain and the performance is more stable and predictable.
## Navigating between agents and subflows
The builder has a **selector** at the top (it shows **Main flow** by default). Use it to move between everything you have open — each opens in its own tab:
* **Agents**: your main agent (**Main flow**) and any **transfer agents** you open from an [agentic warm transfer](/build/conversation-flow/call-transfer-node).
* **Subflows**: any [subflows](/build/conversation-flow/components) you open for editing.
You can return to **Main flow** at any time.
## Quickstart
Head to the Dashboard, create a new conversation flow agent and select a pre-built template to get started. You can view all options available to the agent within the Dashboard, with details of the options and any latency implications listed there. You can also view the estimated latency and cost of the agent. Modify the template to your needs, all changes are auto-saved.
## Reusing a flow across multiple agents
A conversation flow is a standalone resource — identified by a `conversation_flow_id` — that an agent references through its `response_engine`. The same flow can be linked to multiple agents, so you can share a single flow across, for example, a staging agent and a production agent, or across multiple language or channel variants.
* In the dashboard, create the flow once, then create or edit each agent and select the existing flow as the agent's response engine.
* Via API, set the same `response_engine.conversation_flow_id` on each agent when calling [Create Agent](/api-references/create-agent) or [Update Agent](/api-references/update-agent).
* Updates to the flow apply to every agent that references it, so publish changes carefully and test in a non-production agent first.
Agent-level settings (voice, language, webhook URL, data storage, Post Call Extraction, etc.) stay on the agent, so two agents sharing a flow can still differ in those areas.
## Pricing
Since the choice of model can be overridden within individual nodes, the pricing for each call is calculated based on:
* Time spent in each node (seconds)
* Model price per second for that specific node
* Total aggregated across all nodes visited during the call
This allows you to optimize costs by using different models for different parts of the conversation (e.g., cheaper models for simple routing, premium models for complex interactions).
# Press Digit Node
Source: https://docs.retellai.com/build/conversation-flow/press-digit-node
Press digit nodes let Retell agents navigate IVR menus by inferring and pressing the correct keypad digit silently during an outbound phone call.
The Press Digit Node is used to navigate through IVR (Interactive Voice Response) systems. When in this node, the agent will not speak. Instead, it evaluates whether it should press a digit and determines which specific digit to press.
The node evaluates whether to press a digit each time the user (IVR system) finishes speaking. This timing is also affected by the detection delay setting. If a digit press is needed, the agent will infer the appropriate digit and press it.
## Configure Press Digit Behavior
Provide clear instructions so the agent knows whether and what digit to press. Include keywords or phrases to listen for, as well as which ones to avoid.
**Sample prompt:**
```
Your goal is to reach the scheduling or appointments department.
Preferred navigation keywords:
• Scheduling
• Appointments
• New patients
• Front desk
Avoid:
• Billing
• Referrals
• Medical records
• Clinical departments
If you are unsure which IVR option is correct:
Choose the option most closely related to scheduling or appointments.
```
Some IVR systems speak slowly, so to make sure the agent does not make any decision prematurely, you can set a delay on pauses to make sure the whole IVR menu is captured. We recommend setting this to 1 second.
Transitions occur when the IVR system finishes speaking. When writing your transitions, ensure you cover both successful navigation and potential failure scenarios or edge cases.
**Success scenario:** Define when the agent has successfully navigated to the target. For example, write conditions like `Reached scheduling department`. If the digit press was correct, the IVR response will confirm this.
**Edge cases:** Cover scenarios like getting stuck in loops. For example, write conditions like `Menu repeated 3 times` to handle repetitive menus.
**Example transition conditions:**
```
You've reached the scheduling department.
```
```
Menu repeated 3 times.
```
```
You've reached the wrong department or company.
```
```
You've reached an after-hours or voicemail message.
```
## Rest of Node Settings
* **Global Node**: read more at [Global Node](/build/conversation-flow/global-node)
* **LLM**: choose a different model for this particular node. Will be used for determining whether and what digit to press.
* **Fine-tuning examples**: Add example conversations to train your AI agent how to handle specific scenarios. Read more at [finetune examples](/build/conversation-flow/finetune-examples).
# SMS Node
Source: https://docs.retellai.com/build/conversation-flow/sms-node
Use an SMS node to have a Retell voice agent send a text mid-call — to the caller or a different number — using an SMS-approved Retell or imported number.
SMS node is used to send an SMS during a phone call. You can send to the caller's number or a different number.
This node only works for phone numbers that have SMS enabled, or when using an SMS-approved Retell number. Read more about [enabling SMS](/deploy/enable-sms).
The SMS will be sent when entering this node.
## Choose where to send from
You can choose one of two options for the sending number:
* **SMS-approved Retell number**: Send from Retell's pool of numbers that are already approved for SMS. This bypasses the A2P application process entirely. The message content is a **preset template provided by Retell** — you cannot customize the text or use a prompt.
* **Agent's associated number**: Send from the phone number bound to the agent. This requires your number to have SMS enabled through the [A2P application](/deploy/enable-sms#enable-sms-capabilities), and the agent should [collect SMS consent](/deploy/sms-campaign-application#make-your-agent-collect-consent-the-same-way) before this node runs.
Agents can also **receive SMS during an active call** and understand the content, including text, images, audio, and video. This works out of the box for Retell Twilio numbers, and for custom telephony numbers that passed A2P applications. Read more at [Receive SMS during call](/deploy/enable-sms#receive-sms-during-call).
## Configure SMS content
* **When sending from the agent's associated number**: You can write a prompt to let the agent infer the SMS content, or use static SMS content. Dynamic variables are supported for static SMS content.
* **When sending from an SMS-approved Retell number**: The message is a preset template provided by Retell. You cannot edit the content.
## Configure SMS destination
By default, the SMS is sent to the caller's number. You can also choose to send to a different number — either a static number or a dynamic variable (e.g. `{{customer_phone}}`).
## When Can Transition Happen
The node would transition to the next node once the SMS is successfully sent or fails to send. It should take less than 2 seconds to get that result. It will transition out of the node purely based on the SMS result.
## Node Settings
* **Global Node**: read more at [Global Node](/build/conversation-flow/global-node)
* **LLM**: choose a different model for this particular node. Will be used for function argument generation, and potentially speak during execution message generation.
# Subagent node
Source: https://docs.retellai.com/build/conversation-flow/subagent-node
Subagent nodes let a Retell conversation flow combine dialogue with on-the-fly tool calls — the LLM decides whether and when to use each attached tool.
The subagent node holds a conversation with the user while letting the agent call tools (also called functions) during the dialogue. The LLM decides whether and when to use each attached tool based on what the user says.
If you only need dialogue without tool calling, use a [conversation node](/build/conversation-flow/conversation-node).
For example, a customer-support agent can use one subagent node to handle order questions: it chats with the caller, and only when the caller provides an order number does it invoke the order-lookup tool — no separate flow branch needed for callers who never ask.
## How it works
When a subagent node has tools attached, the LLM receives both the node instruction and the list of available tools. During the conversation, the LLM determines when a tool should be called based on context, extracts the required parameters, and invokes it while maintaining the dialogue with the user.
* Multiple tools can be added to a single subagent node.
* The agent can continue talking while a tool executes.
* Tool results are available to the LLM for generating follow-up responses.
## Write the instruction
Subagent nodes only support **Prompt** instructions. Unlike a conversation node, **Static Sentence** is not supported.
Write the instruction to define the task, what information the agent should gather, and when it should use the available tools.
```text theme={"dark"}
Help the user check their order status. If the user provides an order number,
use the available order lookup tool to retrieve the latest status.
```
## Subagent node vs function node
Subagent nodes and [function nodes](/build/conversation-flow/function-node) serve different purposes:
| | Function node | Subagent node |
| ------------------ | -------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| **Execution** | Deterministic — executes on node entry | LLM-driven — called when the LLM decides it's appropriate |
| **Tools per node** | One | Multiple |
| **Conversation** | Not intended for dialogue | Full dialogue with tools available |
| **Best for** | Always-execute actions (e.g. always look up an order on entry) | Context-dependent actions during dialogue (e.g. look up an order only if the user asks) |
**Use function nodes** when you want guaranteed execution every time the flow reaches that step.
**Use subagent nodes** when the agent should decide whether and when to call a tool based on what the user says.
## Add tools
Click a subagent node to open its settings panel on the right side.
In the settings panel, find the **Tools** section and click **+ Add**, then select the tool type from the dropdown.
Configure the tool based on its type — see the table below for each type's configuration guide.
Update the node prompt to guide the LLM on when to use the tool.
## Available tool types
| Tool type | Description | Configuration guide |
| ------------------------ | ------------------------------------------------------------- | ----------------------------------------------------------------- |
| Custom Function | Make HTTP requests to your external APIs | [Custom Function](/build/conversation-flow/custom-function) |
| Code Tool | Run JavaScript code directly without an external server | [Code Tool](/build/single-multi-prompt/code-tool) |
| Integration tools | Act on a connected provider like HubSpot, Zendesk, or Cal.com | [Integration tools](/build/conversation-flow/integration-tools) |
| End Call | Terminate the call | [End Call](/build/single-multi-prompt/end-call) |
| Transfer Call | Transfer to a phone number | [Transfer Call](/build/single-multi-prompt/transfer-call) |
| Transfer Agent | Transfer to another Retell agent | [Transfer Agent](/build/single-multi-prompt/transfer-agent) |
| Press Digit | Send DTMF tones | [Press Digit](/build/single-multi-prompt/press-digit) |
| Send SMS | Send a text message | [Send SMS](/build/single-multi-prompt/send-sms) |
| Extract Dynamic Variable | Extract variables from the conversation | [Extract Dynamic Variable](/build/single-multi-prompt/extract-dv) |
| MCP Tool | Call tools on your MCP server | [MCP Node](/build/conversation-flow/mcp-node) |
## Execution speech settings
Each tool has settings that control what the agent says while it runs and after it completes.
### Speak During Execution
When enabled, the agent says a message while the tool is executing, for example `One moment, let me check that for you.` This is recommended when the tool takes over 1 second, including network latency, so the agent remains responsive.
You can configure how the message is generated:
* **Prompt**: The LLM dynamically generates what to say based on a description you provide.
* **Static Sentence**: The agent speaks the exact text you provide.
### Speak After Execution
When enabled (the default), the agent calls the LLM after the tool returns a result so it can speak about the outcome to the user. Turn it off to run the tool silently.
* **Speak During Execution** is available on: Custom Function, Code Tool, Integration Tool, End Call, Transfer Call, Transfer Agent, Send SMS, and MCP Tool.
* **Speak After Execution** is available on: Custom Function, Code Tool, and MCP Tool.
## When transitions happen
* After the user finishes speaking, following the [evaluation order](/build/conversation-flow/transition-condition#evaluation-order).
* When **Skip Response** is enabled: as soon as the agent finishes speaking.
Tool execution happens within the subagent node, so the node can stay active across multiple turns and tool calls before it transitions.
## Node settings
* **Tools**: Attach the tools this subagent can use during the conversation.
* **Skip Response**: The node gets a single edge and transitions through it when the agent finishes speaking, without waiting for a reply.
* **Knowledge Base**: Attach node-level knowledge bases to combine topic-specific knowledge with the agent-level knowledge base. Read more at [knowledge base](/build/knowledge-base).
* **Global Node**: Make this node reachable from anywhere in the flow when its condition is met. Read more at [global node](/build/conversation-flow/global-node).
* **LLM**: Choose a different model for this node only. It's used for response generation, tool selection, and tool argument generation.
* **Speech overrides**: Override the agent-level speech settings for this node only — interruption sensitivity (0–1), response wait time (0–5.5s), voice speed (0.5–2), and whether keypad presses can interrupt the agent (DTMF interruption).
* **Fine-tuning examples**: Add example transcripts to improve this node's responses and transition decisions. Read more at [finetune examples](/build/conversation-flow/finetune-examples).
## Best practices
* **Be explicit in your node instruction.** Tell the agent when each tool should be used.
* **Use function nodes for guaranteed execution.** If a tool must always run at a certain point in the flow, use a [function node](/build/conversation-flow/function-node) instead.
* **Avoid stacking too many tools on one subagent node.** There's no hard limit, but the more tools the LLM can choose from, the more likely it picks the wrong one. Split them across multiple subagent nodes.
# Agent Transfer Node
Source: https://docs.retellai.com/build/conversation-flow/transfer-agent-node
Agent transfer nodes (agent swap) hand a Retell call from one AI agent to another, useful for specialized agents, language switches, and modular call flows.
In advanced call flows, it's common to switch the handling agent, transferring the conversation from one AI agent to another. **Agent Transfer** (also known as **Agent Swap**) enables you to modularize tasks and re-use specialized agents without relying on [traditional phone-based transfers](/build/single-multi-prompt/transfer-call). Examples include:
* Transferring from a front-desk agent to an appointment-booking agent based on task.
* Transferring from an agent speaking one language to another agent handling a different language, based on user preference.
## Why Use Agent Transfer Instead of Call Transfer?
Compared to transferring to another agent using [transfer call](/build/conversation-flow/call-transfer-node), **Agent Transfer** offers significant advantages:
* **Lower Latency**: The transition between agents is near-instant, much lower than transfer call.
* **Better Reliability**: No need to create a new phone call, avoiding potential telephony failures.
* **No Handoff Message Needed**: The destination agent has access to the full conversation history, eliminating the need for adding hand-off messages or repeated customer questions.
* **No Separate Numbers for Agents**: Agents receiving transfers don’t need their own phone numbers. One number is all you need, no matter how many agents you transfer to.
## What stays fixed and what switches
Some call-level settings are pinned to the **first** agent for the whole call, no matter how many swaps happen:
* **Recording access** (`opt_in_signed_url`)
* **Data storage setting** (`data_storage_setting`)
* **PII redaction** (`pii_config`)
* **Denoising mode**
All other settings (language, voice, ambient sound, voice model, LLM/model, prompt, and tools) reflect the currently active agent. Use **keep the same voice** or **keep the current language** to carry the current voice or language into the destination agent instead. Keeping the same voice also keeps the current ambient sound; otherwise the destination agent's ambient sound applies.
Webhook delivery is configurable per transfer: by default only the source agent's webhook fires, but you can send events to the transferred agent's webhook or both.
For a fuller walkthrough of what the destination agent inherits (transcript, metadata, and dynamic variables) and every configuration option, see [Agent Transfer](/build/single-multi-prompt/transfer-agent).
## Steps
Select "Agent Transfer" from the 'Add New Node' menu.
You can configure the following main settings:
* **Transfer agent**: the ID and version of a specific agent to transfer to. You can select the latest version as well.
* **Speak during execution and messages**: if the agent should speak something while performing the transfer.
* **Post Call Extraction setting**: choose which agent's Post Call Extraction applies after the transfer. Select **Only transferred agent** to run Post Call Extraction with the destination agent's full analysis configuration (its analysis fields, analysis model, and analysis prompts), or **Both this agent and transferred agent** to keep the analysis fields from both agents.
You can test agent transfer both in web call and playground.
# Transition conditions
Source: https://docs.retellai.com/build/conversation-flow/transition-condition
Control when a Retell conversation flow agent moves between nodes: prompt conditions evaluated by the LLM, deterministic equation conditions, and else edges.
Transition conditions decide whether and where the agent moves next. Each edge out of a node pairs a condition with a destination: when the condition is met, the agent transitions to that node. This is where you get the most control over a flow, and where careful testing pays off most.
## Condition types
There are two types of transition conditions:
* **Prompt**: A natural-language condition evaluated by the LLM against the conversation.
* **Equation**: A deterministic comparison over [dynamic variables](/build/dynamic-variables) — no LLM involved.
Example prompt conditions:
* `User said something about booking a meeting`
* `User said something about cancelling a meeting`
* `User claims to be over 18`
* `User said they lived in New York or Los Angeles`
Example equation conditions:
```
- {{user_age}} > 18
- {{current_time}} > 9 AND {{current_time}} < 18
- {{user_location}} == "New York"
- {{user_location}} != "New York"
- "New York, Los Angeles" CONTAINS {{user_location}}
- {{user_age}} < 18 OR {{user_location}} == "New York"
- {{name}} exists
```
Equation conditions can only reference dynamic variables. For information the agent learns during the call, either use a prompt condition or first capture the value with an [extract DV node](/build/conversation-flow/extract-dv-node) and branch on it afterwards.
## Special edges
Besides regular condition edges, a node can have:
* **Else edge**: The fallback. It fires when no other condition matches, so the flow never gets stuck. Use it deliberately — on a node with an else edge, any user turn that matches nothing else moves through it.
* **Always edge**: Transitions unconditionally as soon as the user responds, skipping condition evaluation entirely. Useful when the node just needs one reply (e.g. an acknowledgment) before moving on.
* **Skip Response edge**: Created when the node's **Skip Response** setting is on. The node transitions once the agent finishes speaking, without waiting for the user — good for disclaimers or hand-off lines.
## Evaluation order
When a transition check runs, conditions are evaluated in this order:
1. **Always edge** — if the node has one, the agent transitions through it right after the user responds. Nothing else is checked.
2. **Equation conditions** — evaluated deterministically before any prompt condition. Among equation edges, evaluation runs top to bottom and the first condition that evaluates true wins.
3. **Prompt conditions** — if no equation matched, the LLM evaluates all prompt conditions together, alongside any [global node](/build/conversation-flow/global-node) entry conditions, and picks the single best match — or none.
4. **Else edge** — if nothing matched and the node has an else edge, the agent transitions through it.
5. **Stay** — otherwise the agent stays in the current node and keeps the conversation going.
## When transitions are checked
The trigger depends on the node type:
* **Conversation and subagent nodes**: after each user turn — or, with Skip Response on, as soon as the agent finishes speaking.
* **Function, code, MCP, and extract DV nodes**: after the tool result arrives (when waiting for the result).
* **SMS node**: after the SMS succeeds or fails.
* **Call transfer and transfer agent nodes**: after the transfer result comes back (e.g. the transfer failed).
* **Logic split node**: immediately on entry.
When testing in the dashboard (audio or text), the current node is highlighted on the canvas, so you can watch exactly when each transition happens.
## Equation reference
An equation condition holds one or more equations — up to 50 — combined with **ANY** (at least one must be true) or **ALL** (all must be true).
| Operator | Compares | Behavior |
| -------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `==`, `!=` | Strings | Case-insensitive string match (or not) — `"new york"` equals `"New York"`. No numeric coercion — `"18"` and `"18.0"` are different strings. |
| `>`, `>=`, `<`, `<=` | Numbers | Numeric comparison. If either side is empty or not a number, the equation evaluates to **false** — it never throws. |
| `Contains`, `Not Contains` | Strings | Whether the left value includes the right value as a substring, ignoring case. |
| `exists`, `does not exist` | Variable | Whether the dynamic variable has been set. An empty string still counts as set. |
The `exists` check is useful when information may or may not be available before the call:
```
- {{user_email}} exists
- {{user_phone}} does not exist
```
## Add and edit conditions
Click the node, then click the **+** button to add a transition condition. Choose **Prompt** or **Equation**.
For a prompt condition, type the condition text directly on the edge.
For an equation condition, the equation editor opens. Click **Add equation** to add rows, the trash icon to delete one, and switch **ANY** to **ALL** to require every equation to be true.
Drag the six-dot handle on the left of an equation to move it up or down. Order matters: the first equation condition that evaluates true wins.
## Write conditions that transition reliably
The LLM sees the current node's instruction while evaluating conditions, but each condition should stand on its own — describe what the user said or the state reached, without leaning on the instruction text:
* `When user indicates they want to book a meeting`
* `User declines the invitation`
* `User responds to question of their age`
* For function nodes, you can reference the result: `CRM lookup returned successful result`
To keep the agent from getting stuck, cover every case you expect at that point in the conversation. General cases (like objection handling) can live on [global nodes](/build/conversation-flow/global-node), so node-level conditions only need to cover what's specific to that step.
For equation conditions, cover every branch the dynamic variables can take. For example, to treat callers from New York and Los Angeles differently when `{{user_location}}` is known before the call:
```
- {{user_location}} == "New York"
- {{user_location}} == "Los Angeles"
```
## Improve transition accuracy
If you observe an incorrect transition:
* Rewrite the condition to be more specific about what the user said.
* Add transition [finetune examples](/build/conversation-flow/finetune-examples) that show the model real transcripts with the correct decision.
## FAQ
In order: if a global node's condition matches, the agent transitions there. Otherwise, if the node has an else edge, the agent moves through it. If neither, the agent stays in the current node and responds from its instruction.
Open the call transcript in the history tab — it shows each transition with the node names it moved from and to. Name your nodes descriptively so this is easy to read.
There's no limit on conditions per node, though each equation condition holds at most 50 equations. Keep the list focused — the more prompt conditions a node has, the harder it is for the LLM to pick the right one.
Check your [global prompt](/build/conversation-flow/global-setting) and node instructions for numbered labels like "PRIORITY 1", "Step 2", or "ATTEMPT 1". When the agent picks the next node, numbers in these lists can be confused with its transition options, so a transition can fire even though its condition was never met. Rename the items to letters ("PRIORITY A") or descriptive names ("New service intent") — the instructions themselves can stay the same.
# Conversational Mode
Source: https://docs.retellai.com/build/conversational-mode
See how Conversational Mode rephrases your voice agent's replies — shorter, more natural, and more human than a typical assistant.
Conversational Mode is the **Professional + Conversational** tone in the [Agent Handbook](/build/agent-handbook). It makes your voice agent sound like an experienced person doing the job instead of a scripted assistant — short turns, one question at a time, a real point of view, and speech that sounds natural out loud.
Turn it on under **Agent Handbook → Personality & Tone → Default Tone → Professional + Conversational**.
Available for voice agents. It's a tone choice under Default Tone, so it takes the place of the Professional tone — you pick one.
## What changes
Here's how the same moments sound with a typical assistant versus with Conversational Mode on.
### Skip the canned greeting
> **Caller:** "I just moved here from Denver and I need to set up an account."
| Typical assistant | Conversational Mode |
| ----------------------------------------------------------------------------------------------------- | -------------------------- |
| "That's great! Welcome from Denver. I'd be happy to help you set up an account. Can I get your name?" | "Sure — what's your name?" |
### Don't read the whole list
> **Caller:** "What times do you have Friday?"
| Typical assistant | Conversational Mode |
| ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| "We have availability at 9:00 AM, 10:30 AM, 11:15 AM, 1:00 PM, 2:45 PM, and 4:30 PM." | "Morning's pretty open — nine or ten thirty. Afternoon works too if that's easier." |
### Take a position
> **Caller:** "Should I do the annual plan or monthly?"
| Typical assistant | Conversational Mode |
| ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| "Both options have their advantages. The annual plan offers savings while the monthly plan offers flexibility. It depends on your needs." | "If you're staying past three months, annual — it's about twenty percent cheaper. Otherwise just go monthly." |
### One thing at a time
> **Caller:** "OK so what do you need from me?"
| Typical assistant | Conversational Mode |
| ------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| "When does your contract expire, how many years have you used so far, and do you already have anything else in progress?" | "Couple quick basics. First one — when does it actually expire?" |
### Lead, don't dump
> **Caller:** "My coverage runs out in about a year and I haven't started anything. What do I do?"
| Typical assistant | Conversational Mode |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| "There are three main tracks to look at in parallel. First, file the paperwork: your provider submits the request, then once that's approved you can usually extend in chunks, or shorter chunks if…" | "Okay — a year is actually workable if you start now. There are a few tracks, but the urgent one is getting the paperwork started, since that's what unlocks everything else. Want me to walk through that one first?" |
### Real warmth, not cheerfulness
> **Caller:** "Sorry, it's been a rough week — my dad's in the hospital."
| Typical assistant | Conversational Mode |
| ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
| "I'm sorry to hear that! Now, regarding your appointment, would you like to proceed with the original time?" | "I'm really sorry — I hope he's okay. The appointment's easy, we can move it whenever. Want me to just hold off for now?" |
## When to use it
Reach for Conversational Mode on calls where sounding human and moving quickly matters — sales, front desk, scheduling, customer success. Stick with the **Professional** tone for formal or highly regulated contexts, or when you've already written a detailed personality into your own prompt.
Conversational Mode pairs well with **Natural Filler Words** and **High Empathy** in the same Agent Handbook panel.
# Create chat agent
Source: https://docs.retellai.com/build/create-chat-agent
Build a Retell chat agent in the dashboard or API for text conversations, convert a voice agent to chat, and deploy via web widget, SMS, or API.
Chat agents hold text conversations with your users. They use the same prompt types, [functions](/build/add-function-calling), and [knowledge bases](/build/knowledge-base) as voice agents, so you can offer the experience you built for calls over text as well — on your website, over SMS, or inside your own app.
Retell has no built-in integrations with third-party chat platforms such as WhatsApp or Messenger. Beyond the [website widget](/deploy/chat-widget) and [SMS](/deploy/enable-sms), you connect your own channel through the [chat API](/api-references/create-chat).
## When to use a chat agent
Use a chat agent when your users type instead of talk:
* **Website support** — answer product, billing, or order questions in a chat widget on your site.
* **SMS conversations** — run two-way text threads on a Twilio number, such as appointment reminders with reschedule handling.
* **In-app assistant** — power a chat UI you build into your own web or mobile app.
For example, an e-commerce store embeds a chat agent in its help center to handle order-status questions: the agent looks up the order with a custom function and replies with the tracking link, so support staff only see the conversations the agent escalates.
## Create a chat agent
In the dashboard, open **Agents**, click **Create an Agent**, and select **Chat Agent**.
Choose **Single prompt** for simple free-form conversations or **Conversational flow** for production-ready, deterministic flows. **Multi-Prompt (Legacy)** is available under **Other options**. Custom LLM is voice-only, so it can't power a chat agent.
Write the [prompt](/build/prompt-engineering-guide), attach functions and a knowledge base, and adjust the chat settings described below.
To create chat agents programmatically, call [Create Chat Agent](/api-references/create-chat-agent) with a Retell LLM or conversation flow response engine.
## Convert an existing voice agent
Converting creates a **new chat agent** based on your voice agent's setup. The original voice agent is not modified and keeps working as before.
In the agent builder, click the **More options** menu in the top-right corner and select **Convert to Chat Agent**.
A dialog confirms that a new chat agent will be created and your original agent stays unchanged. Click **Convert**.
Retell opens the new chat agent. Voice-only features are removed during conversion, so review the prompt and flow and adjust anything that referenced them.
Conversion removes what doesn't apply to text:
* **Nodes and functions**: call transfer (including bridge and cancel transfer), press digit (IVR navigation), and mid-call SMS are deleted, and any dangling flow connections are cleaned up.
* **Settings**: voice, speech, transcription, and call settings (voicemail detection, DTMF input, and similar) don't exist on chat agents.
Conversion works in both directions: a chat agent's **More options** menu offers **Convert to Voice Agent**.
## Chat settings
Chat agents share most configuration with voice agents — prompt, functions, knowledge base, [webhooks](/features/webhook-overview), security, and Post Chat Extraction — and add a **Chat Settings** section:
* **Auto-close inactive chats** — end the chat automatically when the user stops responding. Set the timeout anywhere from 2 minutes to 72 hours (`end_chat_after_silence_ms`; the API default is 1 hour).
* **Auto-close message** — an optional message sent when a chat is closed automatically (`auto_close_message`).
For chat events, webhooks fire `chat_started`, `chat_ended`, and `chat_analyzed` by default; add `transcript_updated` through the agent's `webhook_events` if you need per-message updates.
The agent's **Workflow** page works the same as it does for voice, with the two function slots named **pre-chat** and **post-chat functions**: look the user up in your CRM before the agent's first message, then log the outcome once the chat ends. See [agent workflow](/agent/agent-workflow).
## Deploy your chat agent
Chat agents are headless — Retell does not host a chat UI for end users, so you choose how to surface the agent:
* **Website widget** — embed a single `
```
### Chat Widget Attributes
**Required (at least one):**
* `data-public-key` - Your Retell public key (used for chat API)
* `data-voice-public-key` - Separate public key for voice/web call API. At least one of `data-public-key` or `data-voice-public-key` is required.
**Optional — Agent Configuration:**
* `data-agent-id` - Your chat agent ID (enables text chat)
* `data-voice-agent-id` - Your voice agent ID (enables real-time voice calls). When both `data-agent-id` and `data-voice-agent-id` are set, the widget displays a chooser screen letting users pick between voice and chat.
* `data-agent-version` - Agent version (if unset, uses latest version)
* `data-dynamic` - JSON string with dynamic variables for the chat agent (e.g., `'{"company": "Acme"}'`)
**Optional — Appearance:**
* `data-title` - Custom chat window title (default: `"Chat"`)
* `data-logo-url` - URL of your logo image
* `data-color` - Hex color code for widget theme (e.g., `"#FFA07A"`). Acts as a shorthand that sets both `data-theme-color` and `data-component-color`.
* `data-theme-color` - Hex color for the widget theme/background (default: `"#071a3e"`). Overrides `data-color` for the theme.
* `data-component-color` - Hex color for accent elements like buttons and links (default: `"#3E6AEF"`). Overrides `data-color` for components.
* `data-fab-text` - Text displayed on the floating action button (default: `"Need support?"`)
**Optional — Behavior:**
* `data-bot-name` - Bot name for popup messages (default: `"AI Assistant"`)
* `data-popup-message` - Popup message before users open chat
* `data-show-ai-popup` - Set to `"true"` to enable popup messages, `"false"` to disable (default: `"true"`)
* `data-show-ai-popup-time` - Seconds to delay before showing popup (default: `5`)
* `data-auto-open` - Set to `"true"` to auto-open chat widget on page load (default: `"false"`)
**Optional — Security & Branding:**
* `data-recaptcha-key` - Google reCAPTCHA v3 site key for bot protection (**Note: Only reCAPTCHA v3 is supported**)
* `data-white-label` - White-label token to hide "Powered by Retell" branding. Contact Retell to obtain your token.
### Color Customization
The chat widget supports flexible color customization through a fallback chain:
| Attribute | CSS Variable | Default | Description |
| ---------------------- | ------------------- | --------- | --------------------------------------------------- |
| `data-theme-color` | `--color-theme` | `#071a3e` | Widget theme/background color |
| `data-component-color` | `--color-component` | `#3E6AEF` | Accent color for buttons, links, etc. |
| `data-color` | — | — | Shorthand that sets both theme and component colors |
**Fallback logic:**
* `data-theme-color` falls back to `data-color` if not set
* `data-component-color` falls back to `data-color` if not set
This means you can use `data-color` alone for a simple single-color theme, or combine `data-theme-color` and `data-component-color` for fine-grained control.
### Voice + Chat Hybrid Mode
When both `data-agent-id` and `data-voice-agent-id` are provided, the widget enters **hybrid mode**:
1. Users see a **chooser screen** with "Voice Assistant" and "Chat Assistant" options
2. A **tab bar** at the bottom allows switching between voice and chat at any time
3. Voice calls use WebRTC for real-time audio with a built-in audio visualizer
4. Chat sessions are persisted locally and can be resumed
If only `data-agent-id` is set, the widget goes directly to the chat interface.
If only `data-voice-agent-id` is set, the widget goes directly to the voice call interface.
### reCAPTCHA Protection
The chat widget supports Google reCAPTCHA v3 for bot protection. **Important: Only reCAPTCHA v3 is supported.**
To enable reCAPTCHA:
1. Include the Google reCAPTCHA v3 script in your HTML `` tag:
```html theme={"dark"}
```
2. Add the `data-recaptcha-key` attribute to your widget script with your reCAPTCHA v3 site key
3. Enable reCAPTCHA protection for your public key in the [Retell Public Keys settings](/accounts/public-keys)
### How Chat Widget Works
1. User clicks the chat widget button (displays the FAB button with customizable text)
2. If voice agent is configured, a chooser screen appears; otherwise, chat opens directly
3. **Text chat**: User types messages and receives responses from the chat agent. Chat sessions are automatically persisted in the browser's localStorage and can be resumed later.
4. **Voice calls**: User clicks "Voice Assistant" to start a real-time WebRTC voice call with the agent. A built-in audio visualizer displays the conversation in real time.
5. If reCAPTCHA is enabled, bot protection is automatically applied to new chat sessions and voice calls
### Testing Chat Widget
After adding the widget to your website:
1. Load your website
2. Click the floating button (bottom right)
3. If both agents are configured, choose between voice or chat mode
4. Start a conversation with your agent
### Example: Chat Widget (Text Chat Only)
```html theme={"dark"}
Retell Chat Widget Example
```
### Example: Chat Widget (Voice + Chat Hybrid)
```html theme={"dark"}
Retell Voice + Chat Widget Example
```
## Callback Widget
The callback widget collects user information and initiates a phone call instead of a chat session. This mode requires a voice agent to handle the phone conversation.
### Setup
Add the following script tag to your HTML, within the `` tag:
```html theme={"dark"}
```
### Callback Widget Attributes
**Required:**
* `data-public-key` - Your Retell public key
* `data-agent-id` - Your voice agent ID (not chat agent)
* `data-widget="callback"` - Enables callback mode
* `data-phone-number` - Your Retell phone number that will make the outbound call
**Optional:**
* `data-title` - Custom widget title
* `data-color` - Hex color code for widget theme
* `data-countries` - Comma-separated country codes for country selector (e.g., "US,CA,GB")
* `data-tc` - URL to your terms and conditions page
* `data-recaptcha-key` - Google reCAPTCHA v3 site key for bot protection
### How Callback Widget Works
**Note:** The callback widget supports the same reCAPTCHA v3 protection as the chat widget. To enable it, follow the instructions in the [reCAPTCHA Protection](#recaptcha-protection) section above.
1. User clicks the callback widget button (displays a phone icon)
2. A form appears collecting:
* First name (required)
* Last name (required)
* Phone number (required)
* Privacy policy agreement checkbox (required)
3. User submits the form
4. If reCAPTCHA is enabled, the form submission is validated
5. The widget creates a phone call using the Retell API
6. User receives a call from your specified phone number
7. The conversation is handled by your configured voice agent
### Testing Callback Widget
After adding the widget to your website:
1. Load your website
2. Click the floating button (bottom right, phone icon)
3. Fill out the contact form
4. Submit and wait for the phone call
### Example: Callback Widget
```html theme={"dark"}
Retell Callback Widget Example
```
## Widget Behavior Summary
* **Chat Widget (text only)**: Shows FAB button, opens chat interface for text conversations with persisted chat history
* **Chat Widget (voice + chat)**: Shows FAB button, displays chooser screen for voice calls or text chat with tab switching
* **Callback Widget**: Shows phone icon, opens form to collect contact info and initiates phone call
# Understand concurrency & limits
Source: https://docs.retellai.com/deploy/concurrency
Understand Retell concurrency and rate limits: max simultaneous calls, calls per second, burst mode, and per-workspace quota increases.
Retell enforces some limits to keep your agents running reliably and prevent misuse of the service. You can adjust most limits to your operational needs, case by case.
## Concurrency
**Concurrency** refers to the number of simultaneous active voice calls that can be handled by your system at any given moment. For example, if 15 users are engaged in voice calls with your agents at the same time, that counts as 15 concurrent calls.
Concurrency limits apply **per workspace**, not per account. Each workspace has its own quota, burst settings, and reserved inbound capacity, and traffic in one workspace does not consume slots in another. Pay-As-You-Go workspaces are allocated a quota of **20 concurrent calls** by default.
If your operational needs require more concurrency, you can adjust your limit from the dashboard. See [Manage limits in the dashboard](#manage-limits-in-the-dashboard) below.
You can check your current number of concurrent calls in the dashboard.
* **Handling multiple calls per agent**:
You don't need multiple agents to handle multiple calls at once.
Each agent can handle an unlimited number of calls,
as long as total concurrency stays within your quota.
## Reserved inbound concurrency
Reserved inbound concurrency protects [inbound calls](/deploy/inbound-call) from being crowded out by [outbound](/deploy/outbound-call) traffic.
When `reserved_inbound_concurrency` is configured, outbound calls can use at most your concurrency
limit minus the reserved amount. Inbound calls can still use the full concurrency limit when capacity
is available.
For example, if your concurrency limit is **100** and reserved inbound concurrency is **20**:
* Outbound calls can use up to **80** slots.
* Inbound calls can use the reserved **20** slots, plus any other available slots up to the full **100**.
You can check the configured value with the [Get Concurrency API](/api-references/get-concurrency).
Reserved inbound concurrency must be lower than your standard concurrency limit.
## Inbound queue and fallback
When inbound call traffic reaches your concurrency limit, Retell briefly keeps new inbound calls
waiting for an available slot. If a slot opens, the inbound call proceeds.
If no slot opens after about **40 seconds**, Retell handles the call as follows:
1. If the phone number has a `fallback_number` configured, Retell transfers the caller to that number.
2. If there is no fallback number, or the fallback transfer fails, the call ends with
`concurrency_limit_reached`.
3. If the fallback transfer succeeds, the Retell call record ends with `no_concurrency_fallback`.
## Concurrency burst
**Concurrency burst** lets you temporarily exceed your standard concurrency limit during peak demand. When enabled, calls that would normally be rejected for hitting your concurrency limit proceed instead, with an added surcharge.
### How it works
When concurrency burst is enabled:
1. **Normal calls**: Calls within your standard concurrency limit proceed as usual with no additional charge.
2. **Burst calls**: Calls that exceed your normal limit but stay within the burst limit proceed with an added **\$0.10/min** surcharge applied to the entire call duration.
### Burst limit calculation
Your burst limit is calculated as the **lower** of:
* **3× your concurrency limit**, OR
* **Your concurrency limit + 300**
For example:
* If your limit is **50**, burst allows up to **150** concurrent calls (3 × 50 = 150)
* If your limit is **200**, burst allows up to **500** concurrent calls (200 + 300 = 500, which is less than 3 × 200 = 600)
### Enabling concurrency burst
You can enable or disable concurrency burst from the **Settings > Limits** page in your dashboard.
### Pricing
| Call Type | Additional Cost |
| ------------------------------ | ------------------------------------------- |
| Normal (within standard limit) | No additional charge |
| Burst (above standard limit) | **\$0.10/min** for the entire call duration |
The burst surcharge applies to the **entire duration** of any call that started while in burst mode, not just the portion of time spent above the normal limit.
### Use cases
Concurrency burst is a good fit for:
* **Unpredictable traffic spikes**: Handle sudden increases in call volume without rejected calls.
* **Campaign launches**: Support higher-than-normal call volumes during marketing campaigns.
* **Seasonal peaks**: Handle increased demand during busy periods without permanently raising your concurrency limit.
Consistent high usage above your normal limit may mean you should raise your base concurrency limit for better cost efficiency.
## Manage limits in the dashboard
You can view and adjust your concurrency and CPS limits from the **Settings > Limits** page. Purchased concurrency and CPS upgrades are billed monthly to your payment method on file; see the [billing overview](/accounts/billing) for how they appear on your invoice.
Adjusting concurrency and CPS limits is available to the **Admin** and **Developer** roles. Reserving inbound capacity and toggling concurrency burst change workspace settings and require the **Admin** role. See [Access Control](/accounts/access-control) for details.
### Adjust your concurrency limit
On the **Concurrent Calls Limit** card, click **Adjust Concurrency**. The dialog shows your current limit and how high you can go.
### Reserve inbound capacity
On the same card, click **Reserve Inbound Capacity** to set how many slots are held for inbound calls (see [Reserved Inbound Concurrency](#reserved-inbound-concurrency) above). The remainder is available to outbound and web calls.
### Adjust CPS (calls per second)
CPS is how quickly new calls can be started, set per telephony path. The Limits page has a card for **Telnyx CPS**, **Twilio CPS**, and **Custom Telephony CPS**. Click **Adjust Limit** on a card to change that provider's CPS; each has its own allowed range. Custom Telephony CPS scales with your concurrency, so a higher CPS there may require more concurrency.
### Estimate values with the calculator
If you're unsure what to set, open the **calculator** from the top of the Limits page. Enter your inbound and outbound traffic (calls per busy hour, average durations, and pickup rate) and it returns a recommended concurrency, inbound reservation, and CPS, each with headroom for spikes. These are suggestions; you still apply them with the cards above.
## Max call duration
The maximum duration of a call is **1 hour** by default, and the call will end automatically after 1 hour. You can increase this up to **2 hours** in your agent settings.
Should your operational needs require longer calls, please reach out to our team at
[support@retellai.com](mailto:support@retellai.com) to discuss options.
## Max prompt token length
The maximum prompt length when using the Retell LLM framework is **32768** tokens by default, and longer prompts are rejected
when creating or updating the LLM. Prompts over 4,000 tokens are charged extra; read more at [Billing Exceptions](/accounts/billing-exceptions).
Should your operational needs require longer context, please reach out to our team at
[support@retellai.com](mailto:support@retellai.com) to discuss options.
## FAQ
No. Concurrency limits apply per workspace, not per account. Each workspace has its own quota, burst settings, and reserved inbound capacity, and traffic in one workspace doesn't consume slots in another.
Yes. All active voice calls draw from the same concurrency pool. Reserved inbound concurrency only holds a portion of that pool for inbound calls; the remainder is available to outbound and web calls.
Yes. [Batch calls](/deploy/make-batch-call) are outbound calls, so they consume concurrency slots like any other outbound call. Batch calls are initiated as slots become available, so a batch runs no faster than your concurrency and CPS limits allow.
It's rejected, unless concurrency burst is enabled. With burst on, the call proceeds within your burst limit and the \$0.10/min surcharge applies to its entire duration.
# Implement chat with create chat completion
Source: https://docs.retellai.com/deploy/create-chat-completion
Step-by-step guide to implementing chat functionality with a Retell chat agent using the create chat completion API for web and SMS channels.
This guide explains how to implement chat functionality using Retell's Chat API. You'll learn how to start a chat session, generate responses, and end the session.
Before starting a chat session, you need a chat agent to handle the conversation.
For detailed instructions on creating a chat agent, refer to the [Create Chat Agent](/build/create-chat-agent) guide.
To start a chat session, use the `create-chat` API endpoint.
The API will return a `chat_id` that you'll need for subsequent requests.
For detailed API information, refer to the [Create Chat API Reference](/api-references/create-chat).
To generate a response from the chat agent, use the `create-chat-completion` API endpoint.
The API will return the agent's response in the `messages` array. All conversation history is automatically stored in Retell's database, so you don't need to manage conversation context yourself.
For detailed API information, refer to the [Create Chat Completion API Reference](/api-references/create-chat-completion).
You can retrieve details about a chat session using the `get-chat` API endpoint.
You can also list chat sessions using the `POST /v3/list-chats` endpoint.
For detailed API information, refer to the [Get Chat API Reference](/api-references/get-chat) and [List Chats API Reference](/api-references/list-chats).
When the conversation is complete, end the chat session using the `end-chat` API endpoint.
If Auto-Close Inactive Chats is enabled, chats will automatically end when the timeout is triggered, or you can end them anytime with the `end-chat` API.
For detailed API information, refer to the [End Chat API Reference](/api-references/end-chat).
## SMS Integration
Retell also supports Twilio SMS integration, allowing you to deploy your chat agents to receive and respond to text messages. To enable SMS functionality for your chat agents, refer to the [Enable SMS](/deploy/enable-sms) guide.
Once you complete the SMS integration setup, you'll have access to:
* **Make an outbound SMS** button to start a new SMS session
* Inbound SMS agent configuration
* Outbound SMS agent configuration
* Inbound webhook setup for receiving SMS messages
**SMS** conversations with your chat agent on a phone number can include **multimedia** (for example MMS). **Text chat** via the `create-chat-completion` API does not support images or other multimedia.
# Connect Retell voice agents to custom telephony
Source: https://docs.retellai.com/deploy/custom-telephony
Integrate Retell voice agents with your own telephony provider using elastic SIP trunking or imported numbers from Twilio, Telnyx, and Vonage.
## Overview
This guide shows how to integrate Retell agents with your telephony provider and use your own numbers. This works with any agent type.
There are two ways to integrate:
1. **Elastic SIP trunking**: The **recommended** option if your telephony provider supports elastic SIP trunking. You set up a SIP trunk, configure your number to point to it, and import that number to Retell.
2. **Dial to SIP URI**: If your provider does not support elastic SIP trunking, or you have a more complex telephony setup, you can dial the call to a specific SIP URI.
* Retell SIP server uri: `sip:sip.retellai.com`
* IP block for traffic: `18.98.16.120/30` (All regions), `3.42.144.0/23` (All regions), `153.57.128.0/18` (All regions), `143.223.88.0/21` (certain United States traffic), `161.115.160.0/19` (certain United States traffic)
Use these IP blocks to whitelist traffic to Retell's SIP server. Many telephony providers require this.
Transport method supported:
* TCP (Recommended)
* UDP
* TLS
* mTLS ([Learn more](#mutual-tls-mtls))
To use a different transport for inbound calls, append the transport method to the SIP server URL:
* For TCP, the SIP server URL needs to be `sip:sip.retellai.com;transport=tcp`
* For UDP, the SIP server URL needs to be `sip:sip.retellai.com;transport=udp`
* For TLS, the SIP server URL needs to be `sip:sip.retellai.com;transport=tls`
Media encryption supported:
* SRTP (Transport should be set to TLS to use SRTP)
Audio Codecs supported:
* PCMU
* PCMA
* G.722(HD)
## Method 1: Elastic SIP trunking (Recommended)
Elastic SIP trunking is a service offered by cloud communications platforms that lets
organizations connect their existing PBX (Private Branch Exchange) or VoIP (Voice over IP)
infrastructure to the Public Switched Telephone Network (PSTN) over the internet using
SIP (Session Initiation Protocol).
Here, elastic SIP trunking connects Retell's VoIP with PSTN so your agents can [make outbound calls](/deploy/outbound-call) and [receive inbound calls](/deploy/inbound-call). All telephony features supported by Retell numbers work here too, as long as your provider supports them.
Many telephony providers offer SIP trunking, so yours most likely supports it.
### Origination vs termination
A SIP trunk has two directions, and each is configured in a different place:
* **Origination (inbound)**: calls flowing from your provider into Retell. In your provider's trunk settings, set the origination SIP URI to Retell's SIP server, `sip:sip.retellai.com`.
* **Termination (outbound)**: calls flowing from Retell out through your provider. Your provider gives you a termination SIP URI for the trunk. You supply that URI (plus credentials, if used) to Retell when you import the number, and Retell dials it for outbound calls.
Retell does not have a termination SIP URI of its own. `sip:sip.retellai.com` is Retell's only SIP address, and it's used as the origination URI on your provider's side.
Here are detailed guides for some telephony providers:
* [Twilio](/deploy/twilio)
* [Telnyx](/deploy/telnyx)
* [Vonage](/deploy/vonage)
Other telephony providers that support SIP trunks work too; use these guides as a reference.
Using a contact center platform instead? See the dedicated guides for [Avaya](/deploy/avaya), [Genesys Cloud](/deploy/genesys), [Five9](/deploy/five9), and [Amazon Connect](/deploy/amazon-connect).
### FAQ
No, Retell will not be able to know if the setup you provide works or not until a call is made.
Please check your origination setting in your SIP trunking provider. Also check the logs in your telephony provider,
and perhaps open a ticket with them.
Please check your termination setting in your SIP trunking provider, and make sure you provide the right termination URL to Retell.
Also check the logs in your telephony provider, and perhaps open a ticket with them. See [Debug outbound call](/reliability/debug-outbound-call) for common SIP failures, and [Debug SIP calls with PCAP files](/reliability/debug-calls-pcap) to inspect the signaling in Wireshark.
Yes you can use the transfer call feature with SIP trunking. Please note that if you intend to use SIP REFER (cold transfer
with the transferee's number), you will need to configure that in your SIP trunking provider to allow SIP REFER and PSTN transfer,
with the transferee's number showing as caller id. To pass call metadata over the trunk, see [custom SIP headers](/build/telephony/sip-headers).
Yes, you can. See [Capture DTMF input](/build/user-dtmf) to read digits the caller presses.
Yes. Retell answers in-dialog re-INVITEs to renegotiate a live call without interrupting it: session refreshes, media re-anchoring (RTP address/port changes), codec changes, SRTP re-keys, hold and unhold, and offerless (no-SDP) re-INVITEs. This follows the SIP re-INVITE and SDP offer/answer model ([RFC 3261 §14](https://datatracker.ietf.org/doc/html/rfc3261#section-14), [RFC 3264](https://datatracker.ietf.org/doc/html/rfc3264)), with SDES SRTP re-keying per [RFC 4568](https://datatracker.ietf.org/doc/html/rfc4568).
Yes, you can.
Right now you'd need to delete and re-import the number.
## Method 2: Dial to SIP URI
If your telephony provider does not support elastic SIP trunking, or you have a more complicated
telephony setup that cannot use elastic SIP trunking, you can use this method.
When using this method, Retell does not directly make or receive calls, but instead
relies on your system to dial the call to the respective SIP URI. This would require you to
have some code to handle integration with your telephony provider. All traffic looks like
inbound to Retell in this case, so it's up to you to specify the call direction.
When using this method, you will not be able to use Retell's transfer call feature, as we do not
have access and control over the telephony provider and number, and cannot initiate transfer for you. You can however implement your own transfer logic and use a custom function to trigger a call transfer.
Here we assume you already have your call handling setup.
For Retell to know what agent to use to handle the call, and for you to
**obtain SIP URI to use** for this call, you would call the
[Register Phone Call API](/api-references/register-phone-call).
You will get a `call_id` back from this API, and you would use it to piece
together the SIP URI.
SIP URI: `sip:{call_id}@sip.retellai.com`
You must dial the call to the SIP URI within **5 minutes** of calling Register Phone Call. If the call is not connected within that window, it disconnects with `registered_call_timeout`. See [Debug call disconnection](/reliability/debug-call-disconnect) for disconnection reasons.
### Example code
Here's a simplified example that handles the inbound call
webhook from Twilio and dials the call to the SIP URI.
```Typescript Node theme={"dark"}
const client = new Retell({
apiKey: 'YOUR_RETELL_API_KEY',
});
server.app.post(
"/voice-webhook",
async (req: Request, res: Response) => {
// Register the phone call to get call id
const phoneCallResponse = await client.call.registerPhoneCall({
agent_id: 'oBeDLoLOeuAbiuaMFXRtDOLriTJ5tSxD',
from_number: "+12137771234", // optional
to_number: "+12137771235", // optional
direction: "inbound", // optional
});
// Start phone call websocket
const voiceResponse = new VoiceResponse();
const dial = voiceResponse.dial();
dial.sip(
`sip:${phoneCallResponse.call_id}@sip.retellai.com`,
);
res.set("Content-Type", "text/xml");
res.send(voiceResponse.toString());
},
);
```
### FAQ
You will need to handle the transfer call/end call logic yourself, meaning that you will need to write a custom function which internally
interacts with your telephony provider to transfer the call/end the call.
Yes.
Check your code that interacts with the telephony provider. Some providers like Twilio can call the webhook multiple times for a single call.
In those cases, you will need to dedupe the call.
Yes, using this method basically gives you full control over telephony functionalities, so you should be able to use telephony provider's
other features. Retell also has built-in [voicemail and IVR detection](/build/handle-voicemail) if you'd rather handle it on our side.
## Security center
Retell secures your voice communications. We recommend using **TLS 1.2 or higher** to secure all SIP signaling channels between your infrastructure and Retell's SIP servers.
To establish a secure TLS connection with Retell's SIP server, you may need to add Retell's server-side root CA to your trust store. You can download it [here](https://www.amazontrust.com/repository/G2-RootCA1.pem).
### Mutual TLS (mTLS)
Mutual TLS (mTLS) is an extension of the standard TLS protocol that provides two-way authentication between a client and a server. Unlike standard TLS — where only the server presents a certificate to prove its identity to the client — mTLS requires **both sides** to present and verify certificates. This ensures that only trusted, authenticated parties can establish a connection, significantly reducing the risk of man-in-the-middle attacks and unauthorized access.
In a typical TLS handshake:
1. The server presents its certificate to the client.
2. The client verifies the server's certificate and the connection is established.
With mTLS, an additional step is added:
1. The server presents its certificate to the client.
2. The **client also presents its certificate** to the server.
3. Both parties verify each other's certificates before the connection is established.
This mutual verification makes mTLS particularly valuable for SIP-based voice infrastructure, where securing signaling and media traffic between trusted systems is critical.
**Retell supports Mutual TLS (mTLS)** for SIP connections. If you require mTLS for your setup, reach out to us via the [Customer Support Portal](https://support.retellai.com/) or at [support@retellai.com](mailto:support@retellai.com) to get it configured for your account.
Retell uses a client certificate issued by AWS Private Certificate Authority (PCA). To validate Retell's TLS client certificate, you **must** add the [root certificate](https://retell-trust-store.s3.us-west-2.amazonaws.com/pca/client-root-ca.pem) to your SIP server's trusted certificate store — without this, your server will reject incoming connections from Retell.
## Video tutorials
### Elastic SIP trunking
Twilio
Telnyx
Vonage
### Dial to SIP URI
## Telephony partners
If you need to manipulate the SIP call flow or have custom needs Retell doesn't directly support, check our telephony partners to see if they can help.
* [Jambonz](https://www.retellai.com/app-partner/jambonz): Jambonz is a SIP server that supports static IP address and can be used to connect to Retell's SIP server to manipulate the SIP call flow. A dedicated Slack support channel is available for Retell users.
* [Cloudonix](https://www.retellai.com/app-partner/cloudonix): Cloudonix is a CPaaS that can be connected to Retell's SIP server to manipulate the SIP call flow.
# Enable SMS
Source: https://docs.retellai.com/deploy/enable-sms
Enable SMS on Retell with Twilio numbers to send messages during calls, receive texts and MMS mid-call, and run two-way text conversations with chat agents.
Retell agents can send SMS during calls, receive texts and MMS mid-call, and hold two-way SMS conversations through [chat agents](/build/create-chat-agent). This guide shows how to enable SMS with [Retell Twilio numbers](/deploy/purchase-number) or your own Twilio number.
SMS is available for Retell Twilio numbers and custom telephony numbers that have passed A2P applications; Telnyx is not supported yet. SMS (A2P 10DLC) is limited to US phone numbers, excluding toll-free numbers — the SMS add-on is disabled for non-US numbers in the dashboard. For other custom telephony providers, check your provider's documentation on enabling SMS.
## Enable SMS capabilities
This step is a prerequisite for sending SMS from your own number during calls and for two-way SMS conversations. Receiving SMS during calls works out of the box for Retell Twilio numbers without this step.
If you only need to send SMS during calls and want to skip the A2P application, you can send from an **SMS-approved Retell number** instead. See [Option 3](#option-3-use-an-sms-approved-retell-number-no-a2p-required) for details.
### Option 1: Enable SMS for Retell Twilio numbers
Enabling SMS on a Retell Twilio number goes through three application and approval steps:
* Get approved for a business profile (free)
* Get approved for the brand based on the business profile (\$4 one-time application fee for low-volume, \$45 for standard)
* Get approved for an SMS campaign (\$15 one-time application fee)
Once approved, the SMS add-on costs \$20/month per number plus \$0.01 per SMS sent.
The entire application takes around 2-3 weeks, sometimes longer, as it's a manual review process on the telephony provider side. We notify you via email when the application is approved or rejected with the reason. If your application is stuck in pending review for over a month, reach out to support and we will help escalate.
#### Detailed steps
The following video walks through the entire application. The written steps below cover the same flow in detail.
You can reuse an existing business profile or create a new one here. Follow the instructions at [Business Profile](/build/telephony/business-profile) to get approved for a business profile.
Only one brand can be created per business profile. If the brand is rejected you can correct and resubmit it, but the brand type is fixed once the brand is registered. Changing the brand type means creating a new business profile.
Here you get to select two types of brands:
* Low-volume:
* \$4 one-time application fee
* send fewer than 6,000 message segments per day to the US (2,000 message segments per day to T-Mobile)
* Standard:
* \$45 one-time application fee
* SMS limit may fall between 6,000 and 400,000 message segments per day to the US (2,000–200,000 per day to T-Mobile)
Depending on your company's type, you might be asked to fill out additional information.
Here you fill out the use case, a description, how end users opt in, and two sample messages. This is the step most applications fail on, so follow the field-by-field templates in [SMS campaign application](/deploy/sms-campaign-application) before you submit. SMS rules are strict: if your actual traffic doesn't match the sample messages, your number or telephony subaccount might be suspended, so provide answers that match your intended use case. If you ever need to send different SMS messages, you can create a new campaign (see the FAQ below).
Registering a campaign costs a \$15 fee, non-refundable even if the campaign isn't approved. Correcting a rejected campaign and resubmitting it under the same use case is free; changing the use case registers a new campaign and is charged again (as of September 2026).
#### Rejected application FAQ
There are a few common reasons for application rejection:
* The campaign application did not provide detailed information on the use case and sample messages.
* The opt-in workflow was not explained clearly.
* The business profile or brand was not approved, due to missing or incorrect business information.
For the exact wording reviewers use and how to fix each one, see [if your campaign is rejected](/deploy/sms-campaign-application#if-your-campaign-is-rejected). Twilio also lists reasons in [Why was my A2P 10DLC campaign registration rejected?](https://help.twilio.com/articles/15778026827291-Why-Was-My-A2P-10DLC-Campaign-Registration-Rejected-).
Read the rejection reason on the phone number page and correct whatever it points at, then resubmit. Anything already approved carries over, so you only redo the step that failed.
Fix the steps in order, because a rejection cascades downward: when a business profile is rejected, its brand and every campaign under it are marked rejected too, with the profile's reason. Repair the business profile first, then the brand, then the campaign. Until the brand is approved or pending, the campaign controls stay disabled.
For the campaign itself, click **Edit** on the rejected campaign, fix the fields the reason points at, click **Save**, then click **Next** to resubmit. See [if your campaign is rejected](/deploy/sms-campaign-application#if-your-campaign-is-rejected) for what each reviewer phrase means.
No. Under **Brand Registration** on the SMS panel, click the brand to reopen it, correct the details, and save to resubmit against the same business profile. You can change the company type and, for public companies, the stock exchange and ticker.
The brand type (low-volume or standard) is fixed once the brand is registered, so it's shown read-only. Changing it does require a new business profile.
If the reason blames the business profile — a name or address that doesn't match public records, for instance — fix the business profile first. The brand inherits the business name and contact from it.
No. The fee pays for the telephony provider's manual review, which happens whatever the outcome.
You are not charged twice for fixing a rejection, though. Correcting a rejected campaign and resubmitting it under the same use case is free, and so is correcting a rejected brand while keeping the same brand type. The fee is charged again only when the correction registers a brand-new record: a different use case, a different brand type, or a correction the provider declines to accept in place (as of September 2026).
You can delete the SMS capability on the number (this will not delete your approved business profile, brand, or campaign — those can still be reused), and create a new SMS application with a new campaign while reusing the business profile and brand.
### Option 2: Bring your own Twilio number
If you already have a Twilio number with SMS capabilities enabled, you can integrate it with Retell by following these steps:
Navigate to your phone number settings in the Retell dashboard and click **Setup SMS Function** under **Advanced Add-Ons**.
Enter the following information from your Twilio account:
* **Account SID**: your unique Twilio account identifier
* **Twilio Auth Token**: your Twilio authentication token for API access
You can find these credentials in your Twilio Console under Account Info.
Once your credentials are verified, your Twilio number is integrated with Retell's SMS capabilities: sending SMS during calls and two-way SMS conversations.
If you use two-way SMS, also update your Twilio Messaging Service so incoming messages are handled by the number's own webhook (the `useInboundWebhookOnNumber` setting) — otherwise inbound texts won't reach Retell.
Make sure your Twilio number already has SMS capabilities enabled and is compliant with SMS regulations before integration. For more information about A2P 10DLC compliance, see [Twilio's A2P 10DLC documentation](https://www.twilio.com/docs/proxy/flex-a2p-10dlc).
### Option 3: Use an SMS-approved Retell number (no A2P required)
If you only need to send SMS during active phone calls and want to skip the A2P application entirely, you can send from Retell's **pool of SMS-approved numbers**. No setup or approval steps are needed — select this option when configuring your SMS node or tool, and you can start sending right away. A number from the pool is automatically selected for each call, and all SMS sent within the same call use the same number.
When sending from an SMS-approved Retell number, the message content is a **preset template provided by Retell** — you cannot customize the text or use a prompt. This option is for in-call SMS only and does not support two-way SMS conversations.
## Send SMS during call
You can configure agents to send SMS messages to the user during an active phone call. Refer to the following docs for setup details:
* For Conversation Flow: [SMS Node](/build/conversation-flow/sms-node).
* For Single/Multi Prompt: [Send SMS](/build/single-multi-prompt/send-sms).
## Receive SMS during call
Agents can receive and understand SMS messages during an active phone call — even if the agent has not sent any SMS. This lets users send supplementary information mid-conversation, such as a photo of a document, a screenshot, or a reference number.
For Retell Twilio numbers, this works out of the box without enabling SMS. For custom telephony numbers that have passed A2P applications, this is also supported.
**Multimedia (MMS)** is supported in addition to plain text: users can send images, audio, and video as carrier-supported MMS attachments. Whatever is delivered to Retell as part of the message is incorporated into the ongoing voice conversation so the agent can respond with that context.
## Set up two-way SMS conversation
Once SMS capability is enabled for the number, you can set up a two-way SMS conversation by attaching [chat agents](/build/create-chat-agent) to the number, so it can receive SMS and reply to the user.
Once the chat agent is attached, inbound SMS starts working immediately. Use the inbound webhook to filter and add context to inbound SMS — read more at [Inbound Webhook](/features/inbound-call-webhook).
For outbound SMS, click **Make an outbound SMS** in the dashboard, or call the [Create Outbound SMS API](/api-references/create-sms-chat) to send it programmatically.
## FAQ
No. Receiving SMS and MMS during calls works out of the box for Retell Twilio numbers, with no A2P application. The A2P application is required to send SMS from your own number and to run two-way SMS conversations.
Not yet. SMS is available for Retell Twilio numbers and custom telephony numbers that have passed A2P applications.
No. SMS (A2P 10DLC) is limited to US phone numbers and excludes toll-free numbers, so the SMS add-on is disabled for non-US and toll-free numbers.
No. If you skip A2P and send from an SMS-approved Retell number, the content is a preset template provided by Retell that you can't customize, and it supports in-call SMS only. Custom message content requires enabling SMS on your own number.
Around 2-3 weeks, sometimes longer, since it's a manual review on the telephony provider side. You're notified by email when it's approved or rejected. If it's stuck in pending review for over a month, reach out to support to escalate.
# Connect Five9 to Retell
Source: https://docs.retellai.com/deploy/five9
Connect Five9 to Retell AI over the pre-built SIP trunk: import your Five9 number, add a ThirdPartyTransfer node, and route IVR calls to a voice agent.
Customers with an enterprise or paid support plan can contact us via the Customer Support Portal or [support@retellai.com](mailto:support@retellai.com) to receive dedicated assistance from Retell to connect your Five9 contact center solution.
## Overview
Retell and Five9 are connected over a pre-built SIP trunk that already exists between the two platforms, so you don't build or configure a trunk yourself. You import your Five9 number into Retell, then point a Five9 IVR script at that number.
Once the connection is in place you can:
* Transfer inbound calls from a Five9 IVR script to your Retell agent.
* Place outbound calls from Retell using the same number.
**Example:** a healthcare provider keeps their existing Five9 IVR as the front door. During business hours the IVR routes callers to live agents; after hours the same script hands the call to a Retell agent that takes prescription refill requests and books callbacks.
Budget about 20 minutes of configuration, plus Five9 Support turnaround time for the trunk routing request (as of August 2026).
For the general model of how Retell connects to a third-party provider over SIP, see [custom telephony](/deploy/custom-telephony).
## Before you start
| Requirement | Details |
| ----------------------- | ------------------------------------------------------------------------------------- |
| Retell account | With an [agent](/build/overview) already created. |
| Five9 VCC Administrator | The desktop application (v13.x), with permission to create IVR scripts and campaigns. |
| Your phone number | The number must already exist in your Five9 Numbers Inventory. |
| Trunk routing | Five9 Support must route that number to Retell over the underlying pre-built trunk. |
Trunk routing is managed by Five9 Support, so raise a ticket with them before you begin. If this isn't done, calls fail at the transfer even though your IVR script looks correct.
### Who does what
You won't complete this setup alone. Plan the work with your Five9 representative before you start:
| Step | Who runs it |
| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| 1. Import your Five9 number into Retell | You, in the Retell dashboard. |
| 2–5. IVR script, transfer node, campaign | **You and your Five9 representative**, in the Five9 VCC Administrator. Retell staff have no access to your Five9 environment. |
| 6. Test the integration | **You, Five9, and Retell together.** |
Book time with your Five9 representative for steps 2 through 5, and line up Five9 and Retell for the step 6 test. Waiting until you're mid-setup to arrange it is the most common reason a Five9 rollout stalls.
1. In the Retell dashboard, go to **Phone Numbers** and choose **Connect to your own number**.
2. Enter your number in E.164 format, for example `+19995550106`.
3. In **Termination URI**, enter the Five9 SBC signaling IP for your data center, followed by the TLS port `5061`, for example `208.69.29.18:5061`. See [Five9 SBC signaling IPs](#five9-sbc-signaling-ips) below.
4. Set **Outbound Transport** to `TLS`.
5. Leave **SIP Trunk User Name** and **SIP Trunk Password** blank. The pre-built trunk doesn't use digest authentication, so credentials here cause the call to fail. Optionally add a **Nickname** to make the number easier to identify.
6. Click **Save**, then assign your Retell agent to this number for inbound calls and, if required, outbound calls.
Import the **pseudo number** provisioned by Five9, not the customer-facing DID associated with it. Importing the DID is the most common cause of a setup that looks correct but never connects.
No IP whitelisting is required on either side. The pre-built trunk between Retell and Five9 is already permitted at both ends.
You can also import numbers programmatically via the [Import Number API](/api-references/import-phone-number).
### Five9 SBC signaling IPs
Use the SBC for the Five9 data center your domain is hosted in.
| Five9 data center | Region | Termination URI |
| ------------------- | -------------- | --------------------- |
| Atlanta (ATL06) | US East | `208.69.29.18:5061` |
| Santa Clara (SCL06) | US West | `162.213.153.61:5061` |
| Montréal (MTL10) | Canada | `74.114.192.15:5061` |
| Montréal (MTL3) | Canada | `74.114.193.15:5061` |
| London (LND03) | United Kingdom | `212.187.211.56:5061` |
| Frankfurt (FRK) | Germany | `185.111.41.18:5061` |
| Amsterdam (AMS03) | Netherlands | `185.111.42.17:5061` |
| Tokyo (TOK) | Japan | `103.169.228.18:5061` |
| Sydney (SYD) | Australia | `103.169.229.18:5061` |
| São Paulo (SAO) | Brazil | `209.14.129.18:5061` |
Confirm the termination IP for your data center with Five9 Support before you rely on it, and especially if you can't see your region listed. Five9 provisions trunk connectivity per data center, so the correct address for your domain is the one Five9 confirms.
**Steps 2 through 5 all run in the Five9 VCC Administrator, alongside your Five9 representative.** Retell staff can't access your Five9 environment.
The IVR script is what tells Five9 to hand the call over to Retell.
1. Open the Five9 **VCC Administrator** application.
2. In the left-hand tree, select **IVR Scripts**.
3. Click the **+** button in the toolbar to create a new script.
4. Give the script a recognizable name, for example `RetellAITransfer`.
Build your call flow to suit your business process. At the point where you want the call handed to your Retell agent, add a **ThirdPartyTransfer** node and connect it into the flow.
Then configure the transfer:
1. Double-click the **ThirdPartyTransfer** node to open its properties.
2. Leave **Third Party Number** set to **Constant**.
3. Enter the number you imported into Retell, in E.164 format, for example `+19995550106`.
4. Click **Save**, then save the IVR script.
The transfer is a blind, single-step transfer: Five9 hands the call to Retell and drops out of the conversation. Leave **Return After 3rd Party Call** unchecked unless you want the caller returned to the IVR after the Retell agent finishes.
The campaign is what connects an inbound number to your IVR script.
1. In the left-hand tree, select **Campaigns**.
2. Click the **+** button in the toolbar to create a new campaign.
3. Set the campaign type to **Inbound** and give it a recognizable name, for example `RetellAIAgentCampaign`.
1. Double-click your campaign to open its **Properties** window.
2. Go to the **IVR** tab and click **Add** to create a new schedule rule.
3. Give the rule a name, then open the **IVR Script** dropdown.
4. Select the script you created in step 2, for example `RetellAITransfer`.
5. Set the schedule. Choose **All day long** with your required days of the week, or a specific date range or time interval.
6. Under **Five9 VCC Channels**, tick **Voice**.
7. Click **OK**, then save the campaign.
**This step is run jointly by you, Five9, and Retell.** All three verify the Five9 campaign and the Retell agent's configuration and performance together before you go live.
1. Call the number assigned to your campaign.
2. Confirm the call transfers and that your Retell agent answers and holds a conversation.
3. Place an outbound call from Retell using the imported number and confirm it connects.
4. Cross-check the Retell call log against the Five9 call report to confirm both platforms recorded the same call.
## Troubleshooting
| Symptom | Likely cause and fix |
| -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The call reaches the IVR but drops at the transfer | The number isn't in your Five9 Numbers Inventory, or trunk routing hasn't been configured for it. Contact Five9 Support. |
| Calls don't connect at all | Check the **Termination URI** in Retell. It must be the correct SBC IP for your data center followed by `:5061`, with **Outbound Transport** set to `TLS`. |
| The transfer connects but the agent never answers | You imported the customer-facing DID instead of the Five9 pseudo number. Re-import using the pseudo number. |
| DTMF digits aren't detected | DTMF must be RFC 2833 with payload type `101`. See [debug SIP calls using PCAP files](/reliability/debug-calls-pcap) to confirm what's negotiated. |
| Transfer fails with an invalid number error | Both Retell and the ThirdPartyTransfer node need the number in E.164 format, including the `+` and country code. |
| The IVR script never runs | The campaign schedule rule is inactive or outside its configured hours, or the **Voice** channel isn't enabled on the rule. |
For Retell-side diagnostics, see [debug outbound connection issues](/reliability/debug-outbound-call) for `not_connected` failures and disconnection reasons.
## FAQ
No. The trunk between the two platforms already exists. You only need Five9 Support to route your number over it, then import that number into Retell.
The **pseudo number** provisioned by Five9. Don't use the customer-facing DID associated with it. Importing the DID produces a setup that looks correct but fails at the transfer.
No. The pre-built trunk is already permitted at both ends, so no firewall or IP allowlist changes are needed on either side.
The pre-built trunk doesn't use digest authentication. Entering credentials makes Retell attempt an authenticated registration the trunk doesn't expect, and the call fails.
Yes. Add a transfer to your Retell agent that hands the call to a number or SIP destination routing back into Five9. See [call transfer node](/build/conversation-flow/call-transfer-node) for conversation flow agents, or [transfer call](/build/single-multi-prompt/transfer-call) for single and multi-prompt agents. To carry context such as a caller or account ID across the handoff, use [custom SIP headers](/build/telephony/sip-headers).
Retell's limit is set by your plan's [concurrency](/deploy/concurrency), and Five9 applies its own capacity limits to the trunk. Size both before a high-volume launch.
## Related
* [Custom telephony](/deploy/custom-telephony) — how Retell's SIP integration works in general.
* [Receive inbound calls](/deploy/inbound-call) and [make outbound calls](/deploy/outbound-call) — using your imported number.
* [Custom SIP headers](/build/telephony/sip-headers) — pass metadata between Five9 and your agent.
* [Concurrency](/deploy/concurrency) — plan call capacity before launch.
# Connect Genesys Cloud to Retell
Source: https://docs.retellai.com/deploy/genesys
Set up a Genesys Cloud SIP phone trunk, number plans, and outbound routes to send inbound and outbound calls between Genesys and Retell agents.
Customers with an enterprise or paid support plan can contact us via the Customer Support Portal or [support@retellai.com](mailto:support@retellai.com) for step-by-step guidance to connect with your Genesys infrastructure.
## Overview
This guide walks through connecting Genesys Cloud to Retell using a **SIP Phone Trunk**. Retell registers with Genesys as a SIP endpoint, allowing Genesys to route inbound calls to Retell agents and allowing Retell to place outbound calls through Genesys numbers.
For the general model of how Retell connects to a third-party provider over SIP, see [custom telephony](/deploy/custom-telephony).
**Retell SIP details:**
* SIP server URI: `sip.retellai.com`
* IP ranges: `18.98.16.120/30` (all regions), `143.223.88.0/21` (certain US traffic), `161.115.160.0/19` (certain US traffic)
* Recommended transport: TCP (also supports UDP and TLS/SRTP)
* Supported audio codecs: PCMU (G.711 µ-law), PCMA (G.711 A-law), G.722
***
Before creating the trunk, ensure your network allows SIP signaling and RTP media between Genesys Cloud and Retell. Genesys Cloud best practice is to specify IP subnets or addresses rather than allowing all traffic — see [About Trunks](https://help.genesys.cloud/articles/about-trunks/) for Genesys security guidance.
**Whitelist Retell IP ranges (allow inbound SIP from Retell to Genesys):**
| CIDR Block | Coverage |
| ------------------ | ------------------ |
| `18.98.16.120/30` | All regions |
| `143.223.88.0/21` | Certain US traffic |
| `161.115.160.0/19` | Certain US traffic |
**Ports to open bidirectionally:**
| Protocol | Port | Purpose |
| --------- | ----------- | ----------------------------------------------- |
| TCP / UDP | 8060 | SIP signaling (Genesys SIP phone trunk default) |
| TCP | 8061 | SIP over TLS (if using TLS transport) |
| UDP | 16384–32766 | RTP / SRTP media |
Genesys Cloud uses ports **8060** (UDP/TCP) and **8061** (TLS) for SIP phone trunks — different from the standard SIP port 5060/5061. Ensure your firewall and any SBC rules use these Genesys-specific ports.
**Genesys Cloud media server IPs:**
Genesys Cloud media traffic originates from AWS regions. Whitelist the IP ranges for your deployed [AWS region](https://help.mypurecloud.com/articles/aws-regions-for-genesys-cloud-deployment/) so Retell's servers can receive RTP from Genesys. Your Genesys Cloud login URL indicates your region (e.g., `login.mypurecloud.com` = US East, `login.mypurecloud.de` = Frankfurt).
If you are routing calls through an on-premises SBC, apply these rules at the SBC rather than the perimeter firewall.
A SIP Phone Trunk in Genesys Cloud lets Retell register as a SIP endpoint that Genesys can send calls to and receive calls from. Follow the steps below, or refer to the official [Create a SIP Phone Trunk](https://help.genesys.cloud/articles/create-sip-phone-trunk/) guide.
1. In Genesys Cloud, go to **Admin** > **Telephony** > **Trunks** (or **Menu** > **Digital and Telephony** > **Telephony** > **Trunks**).
2. Select the **Phone Trunks** tab.
3. Click **Create New**.
4. Enter a name in the **Phone Trunk Name** field (e.g., `Retell-SIP-Trunk`).
5. Set **Type** to **SIP**.
6. Verify **Trunk State** is set to **In-Service**.
7. Under **Protocol and Listen Port**, select your transport and corresponding port:
* **UDP** or **TCP** → port `8060` (recommended)
* **TLS** → port `8061` (use this if you require SRTP media encryption)
8. Under **Registrations**, set the **Max Registration Rate** to limit REGISTER request frequency if required.
9. Under **SIP Access Control**, configure source IP restrictions:
* Set **Use Source Address** to **Yes**.
* Add each Retell IP subnet in CIDR notation:
* `18.98.16.120/30`
* `143.223.88.0/21`
* `161.115.160.0/19`
* Leave **Always Deny** blank.
Genesys best practice is to always specify IP subnets here rather than allowing all. This restricts SIP INVITE acceptance to Retell's known IP ranges.
10. Click **Save Phone Trunk**.
**Codec configuration:**
Genesys Cloud supports several codecs on SIP phone trunks (see [SIP Phone Trunk Settings](https://help.genesys.cloud/articles/sip-phone-trunk-settings/)). Configure at least one of the three codecs Retell supports — PCMU is recommended:
| Codec | Genesys format | Notes |
| ------------------ | -------------- | --------------------------------------- |
| PCMU (G.711 µ-law) | `audio/PCMU` | Recommended — standard in North America |
| PCMA (G.711 A-law) | `audio/PCMA` | Standard outside North America |
| G.722 | `audio/G722` | Wideband (HD voice) |
**TLS / SRTP (optional):**
If you selected TLS transport, Genesys defaults to TLS v1.2. You can choose from AES-based cipher suites and enable SRTP for media encryption. Mutual TLS authentication is disabled by default. See [SIP Phone Trunk Settings](https://help.genesys.cloud/articles/sip-phone-trunk-settings/) for the full list of TLS and SRTP cipher options.
**Advanced settings:**
For call limits, NAT traversal (FENT), inbound digest authentication, and diagnostic captures, refer to [Configure Advanced SIP Phone Trunk Settings](https://help.genesys.cloud/articles/configure-advanced-sip-phone-trunk-settings/). Genesys recommends using defaults unless you have a specific reason to change them.
After creating the trunk, assign or configure phone numbers so inbound calls are directed to Retell and outbound calls from Retell use the correct caller ID.
**Assign numbers to the SIP trunk:**
1. Go to **Admin** > **Telephony** > **Phone Numbers**.
2. Select the number you want to use with Retell.
3. Edit the number and assign it to the `Retell-SIP-Trunk` you created in Step 2.
**Import the number into Retell:**
Retell needs to know the number and how to reach your Genesys trunk for outbound calls.
1. In the Retell dashboard, go to **Phone Numbers** > **Import Number**.
2. Fill in the following fields:
* **Phone Number**: The E.164 format number (e.g., `+12137771234`).
* **Termination SIP URI**: Your Genesys SIP trunk endpoint. This is the SIP address Retell will dial for outbound calls — typically the FQDN or IP of your Genesys Edge or SBC, on port `8060` (e.g., `your-edge.genesys.com:8060`). Check your Genesys trunk configuration or contact your Genesys admin for the correct address.
* **SIP Username / Password**: If you configured inbound digest authentication on the Genesys trunk, enter those credentials here.
3. Save the number and assign a Retell agent to handle calls on it.
You can also import numbers programmatically via the [Import Number API](/api-references/import-phone-number).
Once imported, the number appears in your Retell dashboard and you can place and receive calls just like a Retell-purchased number — see [Make Outbound Calls](/deploy/outbound-call) and [Receive Inbound Calls](/deploy/inbound-call).
To pass metadata such as a caller or account ID between Genesys and your agent, use [custom SIP headers](/build/telephony/sip-headers).
For inbound calls arriving at your Genesys number that should be handled by a Retell agent:
1. In Genesys Cloud, go to **Admin** > **Routing** > **Call Routing** (or use **Architect** for more complex flows).
2. Create an **Inbound Call Flow** in Architect that transfers the call to the Retell SIP endpoint:
* Add a **Transfer to SIP** action.
* Set the SIP URI to `sip:{call_id}@sip.retellai.com`, where `call_id` comes from a prior call to the [Register Phone Call API](/api-references/register-phone-call).
* Alternatively, if you imported the number into Retell with the correct termination URI (Step 3), Retell handles call routing automatically when a call is placed to that number — Genesys forwards the SIP INVITE to `sip.retellai.com` via the trunk.
3. Under **Admin** > **Routing** > **Call Routing**, create a **DID Route** mapping your phone number to this call flow.
4. Publish the flow.
**Configure the Site so Genesys routes transferred calls through the Retell trunk:**
For calls that Genesys transfers out (for example, when a Retell agent hands the call back to Genesys and Genesys then transfers it to another PSTN destination), Genesys uses the caller's **Site** to decide which trunk to use. You must add a number plan and outbound route that select the Retell SIP trunk. See [Manage sites](https://help.genesys.cloud/articles/manage-sites/) and [About number plans](https://help.genesys.cloud/articles/about-number-plans/) for Genesys reference.
1. Go to **Admin** > **Telephony** > **Sites** and open the Site your users or flows are assigned to.
2. On the **Number Plans** tab, click **Add** and create a plan that matches the destinations you will transfer to:
* **Name**: e.g., `Retell-Outbound-US`.
* **Classification**: choose or create a classification (e.g., `US-Domestic`).
* **Match Type**: `E.164 Number List`, `Number List`, or `Regular Expression` depending on your dial pattern.
* **Numbers / Regex**: the destinations that should be routed through Retell (e.g., `^\+1\d{10}$` for US E.164, or a specific number list).
* **Normalization**: keep or set to E.164 so the number arrives at Retell in `+1XXXXXXXXXX` format.
3. Save the number plan and drag it above any other plans that would otherwise match the same numbers — plans are evaluated top-down.
4. On the **Outbound Routes** tab, click **Add** and create a route:
* **Name**: e.g., `Retell-Outbound-Route`.
* **Classifications**: select the classification you used in the number plan (e.g., `US-Domestic`).
* **External Trunks**: select `Retell-SIP-Trunk` (the trunk created in Step 2). If you have multiple trunks listed, order them so the Retell trunk is preferred.
* **Distribution**: `Sequential` (fail over in order) or `Random` if you want load-balancing across trunks.
5. Save the outbound route. Republish any Architect flows that reference the Site so the routing change takes effect.
If transferred calls are still leaving through the wrong carrier, the most common causes are: the classification on the number plan does not match any classification on the outbound route, another number plan higher in the list is matching first, or the user/flow initiating the transfer is assigned to a different Site than the one you edited.
**Placing outbound calls from Retell through Genesys:**
Use the Retell dashboard or the [Create Phone Call API](/api-references/create-phone-call) to place outbound calls. Retell sends a SIP INVITE to the Genesys termination URI you configured in Step 3, and Genesys routes the call to the PSTN with the assigned number as the caller ID.
Use TLS transport and SRTP media encryption when you need end-to-end signaling and media security between Genesys and Retell. This is optional — TCP is sufficient for most deployments.
**How it works:**
* **TLS** encrypts the SIP signaling channel (the INVITE, BYE, etc.).
* **SRTP** encrypts the RTP audio stream. Retell requires TLS transport to use SRTP; you cannot use SRTP over TCP or UDP.
**Retell SIP URI for TLS:**
When importing the number into Retell or configuring the termination URI, append the transport parameter:
```
sip:sip.retellai.com;transport=tls
```
**Genesys trunk configuration for TLS:**
1. In the trunk settings (**Admin** > **Telephony** > **Trunks** > **Phone Trunks** > your trunk), set **Protocol** to **TLS** and **Listen Port** to `8061`.
2. Under [SIP Phone Trunk Settings](https://help.genesys.cloud/articles/sip-phone-trunk-settings/), configure:
* **TLS Version**: `TLS v1.2` (Genesys default — matches Retell's supported version).
* **Cipher Suite**: Choose an AES cipher. Recommended: `TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384` or `TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256`.
* **Mutual TLS**: Disabled by default. Leave disabled unless your security policy requires client certificate authentication from Retell. If you need mutual TLS, contact the Customer Support Portal or [support@retellai.com](mailto:support@retellai.com) as this requires coordination with Retell's infrastructure team.
3. Enable **SRTP** under the media/security settings. Genesys supports the following SRTP cipher suites, all of which Retell accepts:
* `AES_CM_128_HMAC_SHA1_80` (recommended)
* `AES_CM_128_HMAC_SHA1_32`
* `AES_CM_256_HMAC_SHA1_80`
* `AES_CM_256_HMAC_SHA1_32`
**Firewall for TLS:**
Ensure port `8061` (TCP) is open bidirectionally between Retell's IP ranges and your Genesys Cloud edge, in addition to the RTP media ports.
| Protocol | Port | Purpose |
| -------- | ----------- | ------------------------------ |
| TCP | 8061 | SIP over TLS |
| UDP | 16384–32766 | SRTP media (same ports as RTP) |
Retell's SIP server presents a certificate signed by the **Amazon Trust Services** CA (Amazon runs Retell's infrastructure). Genesys Cloud validates this automatically for most deployments. If certificate validation fails and calls do not connect — particularly when an on-premises SBC or Edge device is in the path — you may need to install Retell's Root CA on that device.
Download the following certificates from the [Amazon Trust Services repository](https://www.amazontrust.com/repository/) and install them in the trusted CA store of your Genesys Edge or SBC:
* **Root CA**: Amazon Root CA 1 (`AmazonRootCA1.pem`)
* **Intermediate CA** (optional, install if your device requires the full chain): `C=US, O=Amazon, CN=Amazon RSA 2048 M01`
The intermediate certificate is typically not required if your device performs online certificate chain validation, but some on-premises SBCs and Edge appliances require the full chain to be pre-installed locally. If you see TLS handshake failures with an "unknown CA" or "incomplete chain" error, install both the root and the intermediate.
Also check that your SBC is not intercepting TLS with its own self-signed certificate before forwarding to Retell, as this will cause validation to fail on Retell's side.
**Inbound test (external call → Genesys number → Retell agent):**
1. Call the number assigned to the `Retell-SIP-Trunk` from an external phone.
2. Confirm the call appears in Retell's **Calls** dashboard and is handled by the correct agent.
3. Verify bidirectional audio and that the call completes cleanly.
**Outbound test (Retell → Genesys → PSTN):**
1. Initiate an outbound call from the Retell dashboard or API using the imported number.
2. Confirm the receiving party sees the correct caller ID.
3. Check audio quality in both directions.
If calls fail or have audio problems, use the following tools. For Retell-side diagnostics, see [debug outbound connection issues](/reliability/debug-outbound-call) for `not_connected` failures and disconnection reasons, and [debug SIP calls using PCAP files](/reliability/debug-calls-pcap) to analyze signaling and media in Wireshark.
**1. Interaction Detail Records**
Go to **Performance** > **Workspace** > **Interactions** (or **Contact Center** > **Interactions**). Search by ANI, DNIS, or time range. Click an interaction to see the full SIP event timeline, call path, and any error codes:
* `403 Forbidden` → SIP access control mismatch; verify Retell IP subnets are whitelisted on the trunk.
* `404 Not Found` → Incorrect SIP URI or trunk routing misconfiguration.
* `503 Service Unavailable` → Retell SIP server unreachable; check DNS for `sip.retellai.com` and firewall rules.
**2. Trunk Status**
Go to **Admin** > **Telephony** > **Trunks** > **Phone Trunks** and check the status of `Retell-SIP-Trunk`. A **Down** or **Degraded** state indicates a connectivity or registration problem. Verify firewall rules and that Retell's IPs are correctly added to SIP Access Control.
**3. Protocol Capture (SIP Trace)**
Enable protocol capture under [Advanced SIP Phone Trunk Settings](https://help.genesys.cloud/articles/configure-advanced-sip-phone-trunk-settings/) in the **Diagnostic** section. Use this together with Genesys Cloud Technical Support to inspect raw SIP messages. Look for:
* Codec mismatch in SDP offer/answer — ensure at least one common codec (e.g., PCMU) is in both Genesys and Retell's codec lists.
* Missing or rejected `Contact` / `Via` headers.
* Authentication challenges (401/407) — configure inbound digest auth credentials consistently on both sides.
* NAT issues — if Genesys is behind NAT, enable Far-End NAT Traversal (FENT) in the transport section of advanced trunk settings.
**4. One-way or no audio**
One-way audio is almost always a firewall or NAT issue blocking RTP in one direction:
* Confirm UDP 16384–32766 is open bidirectionally between Genesys media servers and Retell's IP ranges.
* Check the SIP trace SDP `c=` and `m=` lines to confirm the IP address and port Genesys is advertising for media.
* Verify Retell received and is sending audio by checking the call in the Retell dashboard.
**5. Quick reference**
| Symptom | Likely Cause | Fix |
| ------------------------ | ---------------------------------------- | ----------------------------------------------- |
| Call not reaching Retell | Trunk misconfigured or firewall blocking | Check SIP Access Control IPs and firewall rules |
| 403 Forbidden | IP not whitelisted on trunk | Add Retell CIDR blocks to SIP Access Control |
| 503 Service Unavailable | `sip.retellai.com` unreachable | Check DNS resolution and firewall on port 8060 |
| One-way audio | RTP blocked or NAT issue | Open UDP 16384–32766, check SDP IPs in trace |
| No audio (codec) | No shared codec in SDP | Add PCMU/PCMA to Genesys trunk codec list |
| Call drops at \~30s | Mid-call SIP re-INVITE blocked | Allow mid-call signaling through firewall |
| Call drops on register | Max Registration Rate too low | Increase Max Registration Rate on trunk |
If you cannot resolve the issue, capture the Genesys Interaction ID and the Retell Call ID (from the Retell dashboard) and contact the Customer Support Portal or [support@retellai.com](mailto:support@retellai.com).
# Receive calls
Source: https://docs.retellai.com/deploy/inbound-call
Bind voice agents to Retell or imported numbers to answer inbound calls, route SIP traffic, handle overflow, and pass per-call context.
Bind an agent to a [Retell-managed or imported number](/deploy/purchase-number) so it can answer inbound calls. If you route calls through your own carrier, see [custom telephony](/deploy/custom-telephony).
### Bind voice agents
* A number can receive and make calls only after you bind an agent to it.
* You can assign different inbound and outbound agents to the number.
* Leave an agent unset to disable that direction. For example, if you only make outbound calls and don't want callbacks, leave `inbound_agent_id` unset.
After you bind an inbound agent, the number can receive calls.
### Handle inbound overflow
Inbound calls count toward your workspace concurrency limit. To keep inbound calls available during
high outbound volume, you can reserve part of your concurrency for inbound calls. See
[Reserved Inbound Concurrency](/deploy/concurrency#reserved-inbound-concurrency).
If all concurrency slots are in use, Retell briefly waits for a slot to open. If no slot opens after
about 40 seconds, Retell transfers the call to the phone number's `fallback_number` when one is
configured. Without a fallback number, the call ends with `concurrency_limit_reached`.
### A/B testing
See [A/B Testing](/deploy/ab-testing).
### Inbound call webhook
You may want to use different agents for inbound calls to the same number, or provide dynamic variables and other per-call fields.
Read more at [Inbound Call Webhook](/features/inbound-call-webhook).
### Route calls to Retell's SIP endpoint
If you're routing inbound calls from your own carrier or PBX, point your SIP trunk or dial plan at Retell's SIP server:
* **SIP server URI**: `sip:sip.retellai.com`
* **Transports**: TCP (recommended), UDP, TLS, mTLS. Append `;transport=tcp`, `;transport=udp`, or `;transport=tls` to the URI to select one.
* **Media encryption**: SRTP (requires TLS transport)
* **Audio codecs**: PCMU, PCMA, G.722 (HD)
* **Re-INVITE support**: Retell answers in-dialog re-INVITEs (media re-anchor, codec change, SRTP re-key, hold/unhold, offerless) — see the [custom telephony FAQ](/deploy/custom-telephony#faq)
* **IP blocks to whitelist**: `18.98.16.120/30`, `3.42.144.0/23`, `153.57.128.0/18`, `143.223.88.0/21` (certain US traffic), `161.115.160.0/19` (certain US traffic)
For the full setup — elastic SIP trunking, dial-to-SIP-URI, mTLS, and provider-specific guides (Twilio, Telnyx, Vonage, Avaya, Genesys, Five9, Amazon Connect) — see [custom telephony](/deploy/custom-telephony).
### Inbound custom SIP headers
* You can use [custom SIP headers](/build/telephony/sip-headers) to pre-set dynamic variables for a call.
* Retell extracts any header starting with `sip.h.x-`, along with common SIP headers like `Diversion`, `History-Info`, `User-To-User` and `P-Asserted-Identity`, and converts each into a dynamic variable by stripping the `sip.h.` prefix.
* E.g. `sip.h.x-caller: abc` -> `x-caller: abc`
* E.g. `sip.h.p-asserted-identity: +12345678910` -> `p-asserted-identity: +12345678910`
* E.g. `sip.h.diversion: ;privacy=off;reason=no-answer;counter=1;screen=no` -> `diversion: ;privacy=off;reason=no-answer;counter=1;screen=no`
### Get call detail
* API: Use the [Get Call API](/api-references/get-call) to get information like transcript, recording, and latency tracking.
* Webhook: Set up webhooks to receive real-time updates when a call is initiated, ends, and is analyzed.
Read more at [Call Webhook Guide](/features/webhook-overview#event-types).
# International calling
Source: https://docs.retellai.com/deploy/international-call
International calling rates and supported countries for Retell-managed numbers, plus how to enable outbound calls to global destinations from your workspace.
You can use Retell-managed numbers to call US and international destinations. The tables below list the supported countries and their per-minute rates.
To place a call, use the standard [outbound calling](/deploy/outbound-call) flow — the destination country just needs to appear in the supported list below. For example, a US-based support team can use a Retell number to reach customers in India, the UK, and Australia without a local provider in each country.
Retell also offers international numbers to purchase. See [purchase a number](/deploy/purchase-number) for details.
| 🌍 Country | 💬 Rate/Min |
| :------------------ | ----------- |
| 🇺🇸 US | \$0.015 |
| 🇺🇸 US (Toll-Free) | \$0.06 |
| 🇮🇳 India | \$0.15 |
| 🇦🇺 Australia | \$0.10 |
| 🇩🇪 Germany | \$0.10 |
| 🇪🇸 Spain | \$0.10 |
| 🇬🇧 UK | \$0.10 |
| 🇲🇽 Mexico | \$0.05 |
| 🇫🇷 France | \$0.06 |
| 🇯🇵 Japan | \$0.28 |
| 🇨🇦 Canada | \$0.03 |
| 🇮🇹 Italy | \$0.06 |
| 🇮🇩 Indonesia | \$0.40 |
| 🇵🇭 Philippines | \$0.80 |
| 🇲🇾 Malaysia | \$0.20 |
| 🇹🇭 Thailand | \$0.45 |
| 🌍 Country | 💬 Rate/Min |
| ----------- | ----------- |
| 🇺🇸 US | \$0.03 |
| 🇮🇳 India | \$0.25 |
| 🇨🇦 Canada | \$0.03 |
# Create and schedule batch calls
Source: https://docs.retellai.com/deploy/make-batch-call
Create, schedule, and monitor Retell batch calls in bulk to run outbound campaigns, send updates, or contact large recipient lists from a single workflow.
## Overview
Batch calls let you run many [outbound calls](/deploy/outbound-call) as a single group. You can create, schedule, and monitor them in bulk, which is useful for campaigns, updates, or any time you need to contact many recipients.
## When to use it
Reach for batch calls when you need to place the same kind of outbound call to a list of recipients at once — appointment reminders, payment follow-ups, lead qualification, or survey outreach — with per-recipient details supplied through CSV columns. For a single call, or calls triggered one at a time from your own backend, use the [Create Phone Call API](/deploy/outbound-call) instead.
## Create a batch call
Go to the Batch Call tab in the Retell AI workspace and click the "Create Batch Call" button in the top-right corner.
* Provide a unique name for the batch call to differentiate it from others
* Select the "From Number" from the dropdown menu
* Ensure the number is bound to agents to enable batch calls
* Prepare your recipient list in CSV format with a header row including a "phone number" column
* Use the provided CSV template by clicking "Download the template," or upload your custom file
* For dynamic variables, add additional columns in the CSV with custom data for each recipient (e.g., a column header `first_name` can be referenced as `{{first_name}}`)
* If the number to call is not in E.164 format, you can choose to ignore E.164 validation by adding a column to the CSV named `ignore e164 validation` with value `true`. This only applies when you are using custom telephony and does not apply when you are using Retell Telephony.
The CSV also supports the following optional columns:
* `override agent id`: Override the agent used for this particular call.
* `override agent version`: Override the agent version for this particular call.
* `metadata`: A JSON string to store arbitrary data with the call (e.g., `{"customer_id":"cust_123"}`).
* `custom_sip_headers`: A JSON string of custom SIP headers, keys must start with `X-` (e.g., `{"X-Custom-Header":"value"}`).
* Any other columns are treated as dynamic variables injected into your Response Engine prompt and tool descriptions.
* Open the configuration modal to define the batch call time windows
Under "Reserved Concurrency for Other Calls," set how many [concurrency](/deploy/concurrency) slots to hold back for other calls, such as inbound. Type the number directly, or use the − and + buttons. The value must be at least 1 and at most your concurrency limit minus 1. The batch runs on the remaining slots, shown below the field as "Concurrency allocated to batch calling."
* Choose between "Send Now" to start the calls immediately or "Schedule" for a future time
* Click "Save as Draft" to revisit later or "Send" to initiate or schedule the calls
## Monitor batch calls
### Batch call status
Once your batch calls are created, you can monitor their progress and history in the Batch Call tab. Batch calls are classified by their status:
* Draft: Editable and unsent. Drafts will not trigger any calls until submitted.
* Planned: Scheduled for a future time. These cannot be edited once scheduled.
* Ongoing: Currently in progress, with calls initiated as [concurrency](/deploy/concurrency) slots become available.
* Sent: All calls in the batch have been successfully completed.
### Batch call metrics
You can view the following metrics:
* Sent: Total calls sent from the batch.
* Picked Up: Number of calls answered by recipients.
* Successful: Calls successfully completed based on the predefined criteria.
### Call details
Click the history icon to view the call details of each call in the batch.
## Retrieve individual call IDs from a batch
The [Create Batch Call](/api-references/create-batch-call) response returns a single `batch_call_id` — it does not return the individual `call_id` for each task in the batch. To get the individual calls (for example, to fetch transcripts, recordings, or Post Call Extraction for each one), query the [List Calls](/api-references/list-calls) API and filter by `batch_call_id`:
```json theme={"dark"}
{
"filter_criteria": {
"batch_call_id": {
"type": "string",
"op": "eq",
"value": "batch_call_dbcc4412483ebfc348abb"
}
}
}
```
Each item in the response is a full call object with its own `call_id`, transcript, and analysis fields. You can then pass any `call_id` to the [Get Call](/api-references/get-call) API for the complete record.
If you need to correlate each call in the batch back to your own records (for example, a customer ID from your CRM), pass a `metadata` JSON column in the CSV upload or a `metadata` object on each task in the API request. It is stored verbatim on the call and returned by both List Calls and Get Call — no LLM inference required.
## FAQ
Only while it's a draft. Draft batches are editable and don't trigger any calls until submitted. Once a batch is planned (scheduled for a future time), it can't be edited.
Add a column per variable in the CSV. Any column that isn't a reserved field becomes a dynamic variable injected into your Response Engine prompt and tool descriptions — for example, a `first_name` column is referenced as `{{first_name}}`.
The Create Batch Call response returns only a `batch_call_id`. To fetch per-call transcripts, recordings, or analysis, query [List Calls](/api-references/list-calls) filtered by `batch_call_id`. See [Retrieve individual call IDs from a batch](#retrieve-individual-call-ids-from-a-batch) above.
# Make outbound calls
Source: https://docs.retellai.com/deploy/outbound-call
Make outbound phone calls with Retell agents — bind agents to your numbers, prepare API credentials, and trigger calls programmatically or from the dashboard.
## Overview
Make outbound calls with your Retell agents: bind an agent to a phone number, then trigger calls from the dashboard or the API.
## Prerequisites
* **Phone Number**: A [Retell-managed or imported number](/deploy/purchase-number)
* **Agent**: A configured agent ready for outbound calls
* **API Key**: Your Retell API key for authentication
## Step 1: Bind agents to phone numbers
Before making calls, assign agents to your phone numbers. This configuration determines how your number handles both inbound and outbound calls.
### Configuration options
| Setting | Purpose | Use case |
| ------------------ | --------------------------------------- | ----------------------------- |
| **Inbound Agent** | Handles incoming calls to this number | Customer support, callbacks |
| **Outbound Agent** | Used when making calls from this number | Sales outreach, notifications |
### Flexible agent assignment
* **Different agents**: Use specialized agents for inbound vs outbound
* **Outbound only**: Leave inbound agent unset to prevent callbacks
* **Inbound only**: Configure only inbound agent for receive-only numbers
After binding an inbound agent, your number is immediately ready to receive calls. Test it by calling the number!
### A/B testing
See [A/B Testing](/deploy/ab-testing).
## Step 2: Make outbound calls
### International calling restrictions
**Retell-purchased numbers**: Retell supports calling to [15 countries](/deploy/international-call).
**Imported numbers**: International calling depends on your telephony provider's settings.
### Call parameters
When making outbound calls (v2), these parameters are supported:
| Parameter | Type | Example | Description |
| ------------------------------ | -------------- | ------------------------------- | ------------------------------------------------------------------------------------------- |
| `from_number` | string (E.164) | `+14157774444` | Your Retell-managed or imported number. |
| `to_number` | string (E.164) | `+12137774445` | Destination number. |
| `override_agent_id` | string | `agent_abc123` | Override the agent used for this call (optional). |
| `override_agent_version` | integer | `1` | Version of the override agent; defaults to latest if omitted. |
| `agent_override` | object | See below | Per-call partial overrides for agent/response engine (optional). |
| `metadata` | object | `{ "customer_id": "cust_123" }` | Free-form metadata stored with the call (size limit applies). |
| `retell_llm_dynamic_variables` | object | `{ "name": "John" }` | Key–value strings injected into prompts/tools (optional). |
| `custom_sip_headers` | object | `{ "X-Call-ID": "123" }` | Outbound [SIP headers](/build/telephony/sip-headers) forwarded to your provider (optional). |
| `ignore_e164_validation` | boolean | `false` | Only for custom telephony. Bypass E.164 validation for special routing. |
Agent overrides let you adjust per-call behavior without changing the saved agent. See “Agent Overrides” in the [Create Phone Call API](/api-references/create-phone-call) for supported fields and examples.
### API implementation
For complete parameter documentation, see [Create Phone Call API Reference](/api-references/create-phone-call).
```typescript Node theme={"dark"}
const registerCallResponse = await retell.call.createPhoneCall({
from_number: '+14157774444', // replace with the number you purchased
to_number: '+12137774445', // replace with the number you want to call
// Optional: per-call agent selection and overrides
override_agent_id: 'agent_abc123',
override_agent_version: 0, // or omit to use latest
agent_override: {
agent: {
voice_speed: 1.1,
enable_backchannel: true,
},
// retell_llm or conversation_flow overrides are also supported
},
retell_llm_dynamic_variables: { // dynamic variables (optional, string values only)
name: 'John Doe',
blood_group: 'B+'
},
custom_sip_headers: { // replace with custom sip headers you want to send (optional)
'X-Custom-Header': 'Custom Value'
}
});
console.log(registerCallResponse);
```
```python Python theme={"dark"}
# Initiate an outbound call using the newly created agent
call = client.call.create_phone_call(
from_number="+14157774444", # replace with the number you purchased
to_number="+12137774445", # replace with the number you want to call
# Optional: per-call agent selection and overrides
override_agent_id="agent_abc123",
override_agent_version=0, # or omit to use latest
agent_override={
"agent": {
"voice_speed": 1.1,
"enable_backchannel": True,
}
},
retell_llm_dynamic_variables={ # dynamic variables (optional, string values only)
"name": "John Doe",
"blood_group": "B+"
},
custom_sip_headers={ # replace with custom sip headers you want to send (optional)
"X-Custom-Header": "Custom Value"
}
)
print(call)
```
## Step 3: Configure CPS (calls per second)
### Understanding CPS limits
CPS (calls per second) controls how many outbound calls you can start each second.
### Default limits and scaling
| Provider | Default CPS | Maximum CPS | Notes |
| -------------------- | ----------- | ----------- | ---------------------------------------------------------------------------- |
| **Twilio** | 1 | 5 | Changes take up to 10 minutes |
| **Telnyx** | 1 | 16 | Instant updates |
| **Custom Telephony** | 1 | 150 | Check your custom provider's SIP trunk capacity before increasing this limit |
### Important considerations
1. **Throttling protection**: Exceeding limits results in rejected calls
2. **Gradual scaling**: Start low and increase based on actual needs
3. **Provider limits**: Your telephony provider may have additional restrictions
4. **Cost impact**: Higher CPS may increase telephony costs
5. **Concurrency**: CPS controls how fast calls start; the number that can run at once is capped separately by your [concurrency limit](/deploy/concurrency)
### Best practices for high-volume calling
**Implement retry logic** with exponential backoff to handle throttling gracefully:
```javascript theme={"dark"}
// Example retry logic
const maxRetries = 3;
let retryDelay = 1000; // Start with 1 second
for (let i = 0; i < maxRetries; i++) {
try {
await makeCall();
break;
} catch (error) {
if (error.code === 'rate_limited') {
await sleep(retryDelay);
retryDelay *= 2; // Exponential backoff
}
}
}
```
## Step 4: Monitor call details
### Available monitoring methods
#### 1. API polling
Use [Get Call API](/api-references/get-call) to retrieve:
* Full transcript
* Call recording
* Latency metrics
* Function call logs
* Call duration and status
#### 2. Real-time webhooks
Set up [webhooks](/features/webhook-overview#event-types) to receive instant notifications for:
* **Call Started**: When the call connects
* **Call Ended**: Final status and duration
* **Call Analyzed**: Transcript and analysis ready
* **Call Failed**: Error details and reasons
Webhooks provide real-time updates without polling, making them ideal for production systems.
Triggering outbound calls (using Make.com)
### Additional resources
* [Community Templates](https://docs.google.com/document/d/1hx6hdTEjAR4y4xXZ7RLMH2byQNVW1ABxC8S4FwvTx_Y/edit?tab=t.0#heading=h.wf5bktkelope): Real-world outbound calling examples
* [Batch calling](/deploy/make-batch-call): For high-volume campaigns
* [Handle voicemail and IVR](/build/handle-voicemail): Detect voicemail or IVR menus on outbound calls
* [Capture DTMF input](/build/user-dtmf): Collect keypad digits from the caller
* [Debug outbound calls](/reliability/debug-outbound-call): Troubleshoot connection issues
* [Webhook setup](/features/webhook-overview): Configure real-time notifications
# Purchase phone number
Source: https://docs.retellai.com/deploy/purchase-number
Purchase Retell-managed US or Canada phone numbers directly in the dashboard — no telephony provider account required, ready for inbound and outbound calling.
Purchase a phone number from Retell directly in the dashboard or through the API. Retell manages these numbers, so you don't have to set up any telephony infrastructure.
Currently we only support purchase of US and Canada numbers and support making calls to [15 countries](/deploy/international-call). If you are looking to use numbers from other countries, or to make calls to more countries, or to use your own telephony provider, check out [Custom Telephony guide](/deploy/custom-telephony).
### From the dashboard
You can purchase a number and bind agents to it from the dashboard. You can optionally specify the
area codes you want to purchase from.
After purchasing, you can change the number's nickname to make it easier to find and identify.
The number is ready to accept [inbound calls](/deploy/inbound-call) once you've assigned an inbound agent. Try calling it.
### From the API
Check out [Create Phone Number API Reference](/api-references/create-phone-number)
for all the parameters you can use programmatically.
* Phone numbers are yours once purchased, and can be used indefinitely.
Find numbers you own [here](/api-references/list-phone-numbers).
* You can assign different inbound and outbound agents to the number.
* If you don't want a user to be able to call this number (maybe you are doing [outbound calls](/deploy/outbound-call) and don't
want callbacks), you can leave `inbound_agent_id` unset.
```typescript Node theme={"dark"}
const phoneNumberResponse = await retell.phoneNumber.create({
inbound_agent_id: "oBeDLoLOeuAbiuaMFXRtDOLriTJ5tSxD", // replace with the agent id you want to assign
outbound_agent_id: "oBeDLoLOeuAbiuaMFXRtDOLriTJ5tSxD", // replace with the agent id you want to assign
});
console.log(phoneNumberResponse);
```
```python Python theme={"dark"}
# Purchase a phone number
phone_number = client.phone_number.create(
inbound_agent_id="oBeDLoLOeuAbiuaMFXRtDOLriTJ5tSxD", # replace with the agent id you want to assign
outbound_agent_id="oBeDLoLOeuAbiuaMFXRtDOLriTJ5tSxD", # replace with the agent id you want to assign
)
print(phone_number)
```
### Pricing
We support both **Twilio** and **Telnyx** numbers:
* **Twilio**
* US numbers: **\$2/month**
* US toll-free numbers: **\$5/month**
* Canadian numbers: **\$2/month**
* **Telnyx**
* US numbers only: **\$2/month**
Toll-free numbers cost \$0.06 per minute for inbound calls. Outbound calls are charged at the same rate as regular U.S. numbers.
The monthly number fee is billed to your payment method at the end of each billing cycle (prorated if purchased mid-month on credit-based accounts) and recurs until the number is released. See the [billing overview](/accounts/billing) for how number charges appear on your invoice.
## FAQ
Yes. Connect your own telephony provider through [custom telephony](/deploy/custom-telephony), or [import an existing number](/api-references/import-phone-number) into Retell.
Not directly. Retell-managed numbers are US and Canada only. To use numbers from other countries, connect your own provider via [custom telephony](/deploy/custom-telephony). Retell-managed numbers can still place calls to [15 countries](/deploy/international-call).
Delete it from the phone number page in the dashboard, or call the [Delete Phone Number API](/api-references/delete-phone-number). The monthly fee stops once the number is released.
# SMS campaign application
Source: https://docs.retellai.com/deploy/sms-campaign-application
Fill out the A2P 10DLC SMS campaign application for a Retell AI number: use case, description, opt-in flow, and sample messages that meet carrier requirements.
To send SMS from your own Retell number, you need an approved A2P 10DLC campaign. This page walks through each field of the campaign application, with templates and the rules reviewers apply, to help you write one that meets carrier requirements.
Human reviewers read your use case, description, opt-in flow, and sample messages together, so keep the four consistent with each other.
This page covers only the campaign form, which you reach after your [business profile](/build/telephony/business-profile) and brand are in place. The form also requires a card on file in Retell billing. For the full SMS setup, see [Enable SMS](/deploy/enable-sms).
## Form fields and limits
In the dashboard, open your phone number and go to **Advanced Add-Ons** > **SMS** > **Campaign Registration** > **Add Campaign Application**.
| Field | Limits (as of September 2026) | Notes |
| --------------------------------------------- | ----------------------------- | ----------------------------------------------------------------------- |
| Application Name | Any text | Internal label. Not sent to carriers. |
| Use Case | One value | Must match the description, samples, and opt-in flow. See step 1 below. |
| Description for the use of sms | 40 to 1,000 characters | Short or generic text is auto-rejected. Aim for 300 to 700. |
| Sample Message (two boxes) | 20 to 200 characters each | Both required. Each must name the business and include opt-out text. |
| How do end-users consent to receive messages? | 40 to 2,048 characters | Twilio calls this the message flow. The most-rejected field. |
| Privacy Policy URL | Full `https://` URL | Required. Must be on your own domain. |
| Terms and Conditions URL | Full `https://` URL | Required. Must be on your own domain. |
Two things Retell sets for you that affect how you write the samples:
* Your campaign is submitted with **"messages contain links" and "messages contain phone numbers" both set to yes**. A link or phone number in a sample is fine and expected. Links must point to your own domain. Public URL shorteners such as bit.ly or tinyurl are rejected.
* **STOP, UNSUBSCRIBE, CANCEL, END, QUIT, and HELP replies are handled by Twilio's default opt-out handling** on Retell numbers. You don't configure keywords, but every sample and your opt-in script must still tell people they can reply STOP.
## Fill out the application
Pick the one that describes what you will actually send. If more than one applies, choose **Mixed**. If your volume is small, choose **Low Volume Mixed**, which covers any combination up to 2,000 message segments per day. Retell's campaign fee is the same for every use case.
| Use case | Choose it when you send | Typical Retell scenario |
| ------------------------------------- | -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| Customer Care | Two-way support, account help, call follow-ups, requested info | Post-call summaries, "here's the link we discussed" |
| Account Notification | Account-status notices only, such as balance or plan changes | **Not appointment reminders.** Twilio's review team has rejected reminders filed here; use Mixed or Low Volume Mixed. |
| Delivery Notification | Order, shipping, or service-arrival status | Technician ETA, order ready for pickup |
| Marketing | Promotions, offers, announcements | Anything with a discount or a sales pitch |
| Mixed | Any combination of the above | Appointment reminders and confirmations, or support plus occasional offers from one number |
| Low Volume Mixed | Any combination, low daily volume | Same as Mixed for a small business or pilot. The right choice for appointment reminders at low volume. |
| 2FA | One-time passcodes | Verifying a caller's number |
| Security Alert, Fraud Alert Messaging | Account-security or suspicious-activity alerts | Rare for voice agents |
| Higher Education | Messages from a college or university | Admissions follow-ups |
| Polling and Voting | Non-political surveys and polls | Post-call satisfaction survey |
| Public Service Announcement | Informational messages for the public | Rare |
**Rejected regardless of use case:**
* Debt collection of any kind, including payment reminders that read like collections.
* Sex, hate, alcohol, firearms, and tobacco or cannabis content without age-gating.
* High-risk financial content such as loans, payday lending, credit repair, debt relief, and cryptocurrency.
* Lead generation, affiliate marketing, or messaging on behalf of a third party.
* More than one brand in a campaign.
Reviewers must be able to read this and know **who** is sending, **to whom**, **what** kinds of messages, and **how** recipients consented. Aim for three to six sentences.
```text Template wrap theme={"dark"}
[Business Name] ([Legal Name] doing business as [Website Name], if different) uses this campaign to send [message type 1], [message type 2] and [message type 3] to [existing customers / prospective customers who requested a call / patients with a booked appointment]. Recipients opt in [verbally during a phone call with our automated AI phone assistant / by submitting a form on our website at https://www.yourdomain.com/page / both]. Messages are [transactional and informational only / promotional / a mix of transactional and promotional] and are sent [after a call / before an appointment / when an order status changes]. Every message identifies [Business Name] and includes opt-out instructions (reply STOP).
```
Example for **Customer Care**:
> Acme Home Services uses this campaign to send two-way customer support messages, follow-ups to phone conversations, requested quotes and service information to existing customers and prospective customers who contact us by phone or request a call on our website at [https://www.acmehomeservices.com/contact](https://www.acmehomeservices.com/contact). Recipients opt in verbally during a call with our automated AI phone assistant, or by submitting our website contact form. Messages are transactional and informational only, sent after a call or when a customer requests information. Every message identifies Acme Home Services and includes opt-out instructions (reply STOP).
Example for **Low Volume Mixed** appointment reminders:
> Bright Smile Dental uses this campaign to send appointment confirmations, appointment reminders, reschedule notices and post-visit follow-ups to patients who have booked an appointment with our office. Patients opt in verbally when they book by phone with our automated scheduling assistant, or by checking the SMS consent box on our online booking form at [https://www.brightsmiledental.com/book](https://www.brightsmiledental.com/book). Messages are informational only and are sent when an appointment is booked, 24 hours before the appointment, and after the visit. Every message identifies Bright Smile Dental and includes opt-out instructions (reply STOP).
Say "automated AI phone assistant" plainly. Reviewers accept it, and hiding it reads as evasive. Name the content types you will send and stay consistent with the use case, rather than listing every message you might ever send.
This field must describe **where** a person encounters the chance to opt in, the **exact action** they take to consent, and every disclosure below. If consent is collected by voice, paste the exact script your agent speaks.
Mandatory disclosures:
1. Business name
2. What kind of messages they'll get
3. "Message and data rates may apply"
4. Frequency statement, such as "Message frequency varies" or "up to 4 msgs/month"
5. How to opt out (reply STOP) and get help (reply HELP)
6. Direct link to the opt-in page, or the full transcript of the verbal script
7. The customer's confirmation: their "Yes" or their checked box
8. Links to your Privacy Policy and Terms, in addition to the URL fields
9. For web forms: consent is optional, not a condition of purchase, and the box is unchecked by default
Pick the flow that matches how you collect consent. If you use both, paste both, numbered "1. WEBSITE FORM OPT-IN" and "2. VERBAL OPT-IN DURING CALL", followed by one "POST-OPT-IN CONFIRMATION" paragraph with the confirmation text.
**Verbal opt-in during a call with your agent**
```text Verbal opt-in flow wrap theme={"dark"}
End users opt in verbally during a phone call with [Business Name]'s automated AI phone assistant. Customers call [Business Name] at [your published business number] for [support / scheduling / quotes]. Before the call ends, the assistant reads the following script:
Assistant: "Before we finish up, would it be alright if [Business Name] sends you text messages regarding [your appointment / a summary of this call / the information you requested]? Message and data rates may apply. Message frequency varies. You can opt out at any time by replying STOP, or reply HELP for help. Our Terms of Service are at https://www.[yourdomain].com/terms and our Privacy Policy is at https://www.[yourdomain].com/privacy. Do I have your permission to send you these texts? Please say yes or no."
Customer: "Yes."
Assistant: "Great, you're opted in. You'll receive a confirmation text shortly. Reply STOP at any time to unsubscribe."
Only after an explicit "yes" does the assistant record consent and trigger the first message, an automated confirmation text:
"[Business Name]: You're opted in to receive [content type] texts. Msg & data rates may apply. Msg frequency varies. Reply STOP to cancel, HELP for help. https://www.[yourdomain].com/privacy"
If the customer says no or does not answer, no text messages are sent.
```
For outbound calls, replace the second sentence with: "\[Business Name]'s assistant calls customers who \[booked an appointment / requested a callback] at the number they provided."
If a website form only requests a callback, submitting it does not opt the user in to SMS. Say so in the flow, then describe how the agent collects consent on the call using the script above.
**Web form with a consent checkbox**
```text Web form opt-in flow wrap theme={"dark"}
End users opt in by visiting our website at https://www.[yourdomain].com/[page]. On the form, users enter their phone number and are presented with an optional, unchecked checkbox that reads:
"I agree to receive [transactional / promotional] SMS messages from [Business Name] regarding [content type]. Message and data rates may apply. Message frequency varies. Reply STOP to unsubscribe or HELP for help. View our Privacy Policy at https://www.[yourdomain].com/privacy and Terms at https://www.[yourdomain].com/terms."
Checking the box is optional and is not a condition of purchase; the form can be submitted with the box unchecked. When the form is submitted with the box checked, an automated confirmation text is sent to verify the opt-in:
"[Business Name]: You're opted in to receive [content type] texts. Msg & data rates may apply. Msg frequency varies. Reply STOP to cancel, HELP for help. https://www.[yourdomain].com/privacy"
```
Reviewers open your form and check that the checkbox is unchecked by default and not required to submit, that transactional and marketing texts each have their own checkbox, that SMS consent is separate from email consent and from accepting the Terms, and that the business name on the page matches the brand. If there is no checkbox because you send transactional messages only, the phone field must be optional with the disclosure directly under it.
Both URLs must be live, on your domain, start with `https://`, and appear in **both** the URL fields **and** inside the consent text.
Your Privacy Policy must state that mobile opt-in data is never shared. Paste this under the section about sharing data with third parties:
```text Privacy Policy clause wrap theme={"dark"}
No mobile information will be shared with third parties or affiliates for marketing or promotional purposes. All the above categories exclude text messaging originator opt-in data and consent; this information will not be shared with any third parties.
```
Your Terms of Service, or a dedicated "Mobile Messaging Terms" page, should state the business name, the kinds of messages, "message and data rates may apply", the frequency, how to opt out (STOP) and get help (HELP), and a support contact.
The reviewer opens the privacy URL and looks for the clause on that page. A link to a tab or anchor inside a long corporate privacy page gets rejected with "unable to locate the privacy policy details inside this provided link". Link to a page where the clause is visible without clicking tabs or expanding sections, or publish a short dedicated SMS privacy page.
Each sample must be 20 to 200 characters and must:
* Name **\[Business Name]** in the text.
* Include an opt-out line such as "Reply STOP to opt out".
* Show templated values in square brackets: `[Date]`, `[Time]`, `[Agent Name]`.
* Use only links on your own domain.
* Match the use case and description. No discount code under Customer Care.
* Use one frequency phrasing, such as "Msg frequency varies", across both samples and the consent text.
A good pairing is one opt-in confirmation plus one message that shows your real content. Recount after replacing the placeholders: a long business name can push a sample past the limit, and anything over 160 characters sends as two segments.
```text Opt-in confirmation (any use case) wrap theme={"dark"}
[Business Name]: You're opted in to receive [appointment reminders / call follow-ups] by text. Msg & data rates may apply. Msg frequency varies. Reply STOP to cancel, HELP for help.
```
```text Post-call summary (Customer Care, Mixed) wrap theme={"dark"}
[Business Name]: Thanks for calling today. As discussed, your quote is $[Amount]. Details: https://www.[yourdomain].com/quote/[ID]. Questions? Call [Phone]. Reply STOP to opt out.
```
```text Appointment reminder (Mixed, Low Volume Mixed) wrap theme={"dark"}
Reminder from [Business Name]: you have an appointment tomorrow, [Date] at [Time], at [Address]. Need to reschedule? Call [Phone]. Reply STOP to opt out.
```
```text Technician ETA (Delivery Notification) wrap theme={"dark"}
[Business Name]: Your technician [Name] is on the way and should arrive between [Start] and [End]. Track: https://www.[yourdomain].com/track/[ID]. Reply STOP to opt out.
```
```text Promotional (Marketing, Mixed) wrap theme={"dark"}
[Business Name]: Members get [Offer] on [Service] through [Date]. Book at https://www.[yourdomain].com/book. Msg frequency varies. Msg & data rates may apply. Reply STOP to opt out.
```
Click **Save**. Save stays disabled until every required field passes validation. Saving closes the dialog and fills in **Campaign Registration** with the new campaign. Back on the SMS panel, click **Next** to submit the SMS application for this number.
Registering the campaign costs \$15, charged whether or not it's approved (as of September 2026). Correcting a rejected campaign and resubmitting it under the same use case is free. Twilio's review usually takes three or four business days once the brand is approved. Retell emails you when the campaign is approved or rejected, with the reason.
## Make your agent collect consent the same way
Your application promises a specific script. Whenever an agent collects consent, it must say that script. Add this to that agent's prompt, and paste in the exact script you submitted in step 3.
```text Agent prompt: SMS consent wrap theme={"dark"}
## SMS consent
You may only send a text message after the caller explicitly agrees to receive texts. Before sending any SMS, say this, word for word:
"[Paste the consent script from your campaign application here.]"
- If the caller clearly says yes: say "Great, you're opted in. You'll receive a confirmation text shortly. Reply STOP at any time to unsubscribe." Then send the SMS.
- If the caller says no, is unsure, or does not answer: say "No problem, we won't send you any texts." Do not send any SMS, and do not ask again on this call.
- Never send a text to a number the caller did not confirm is their mobile number.
```
Make the first text the confirmation message from step 5, and include the business name and "Reply STOP to opt out" in every SMS the agent sends. If your real traffic doesn't match the samples you filed, the number can be suspended.
## If your campaign is rejected
Retell shows the rejection reason on the campaign in the dashboard and emails it to you. Click **Edit** on the rejected campaign, fix the fields the reason points at, click **Save**, then click **Next** on the SMS panel to resubmit. Only rejected campaigns can be edited.
**Resubmitting a corrected campaign under the same use case is free.** The campaign is edited in place rather than filed again, and your business profile and brand approvals carry over. The \$15 fee is charged a second time only when a new campaign has to be registered: the use case changed, the brand was re-registered, or Twilio declined the in-place edit (as of September 2026).
For that reason **Use Case is read-only once the campaign is registered**, and it's the one field a correction can't change. If the rejection reason blames the use case, editing won't help. Open **Campaign Registration**, click **Add Campaign Application** to file a fresh one under the right use case, select it, and click **Next**. The number's rejected application is re-filed against the new campaign, which costs another \$15. Your business profile and brand are kept and reused, and the old campaign stays in the list, rejected.
### Fix the steps in order
A rejection cascades downward. When a business profile is rejected, Retell marks its brand and every campaign under it rejected too, all carrying the profile's reason ("The business profile is rejected with reason ..."). Identical text on all three means the profile is the only thing actually at fault.
Repair whichever step really failed, working down:
1. **Business profile.** Nothing below it can be submitted while it's rejected. See [handle application rejection](/build/telephony/branded-call-rejection).
2. **Brand.** Reopen the brand under **Brand Registration** on the SMS panel and correct it. Free as long as the brand type stays the same, which is why the brand type is read-only after registration. While the brand is rejected, the campaign controls on the SMS panel stay disabled.
3. **Campaign.** Edit and resubmit it once the brand is approved or pending. Submitting the number while either the brand or the campaign is still rejected fails with "Please update and resubmit the brand first" or "...the campaign first", because a rejected record gets no further review updates.
### What does the rejection wording mean?
| Reviewer wording | Fix |
| --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| "Brand/Website mismatch: unable to validate the connection between the registered brand and the website domain" | The legal name on the brand differs from the name on the website. Add "\[Legal Name] doing business as (DBA) \[Website Name]" to the first sentence of the description and the consent text. |
| "CTA: unable to validate the opt-in process because proof of opt-in has not been included in the message flow" | The consent text described opt-in in general terms. Paste the exact opt-in page URL, or the full word-for-word voice script including the customer's "yes" and the agent's confirmation. |
| "Unable to locate the privacy policy details inside this provided link" | The clause isn't visible on the linked page. Link to a page where the no-sharing clause shows without tabs or expanders. |
| "Appointment reminders are not allowed under Account Notification use case" | Wrong use case, and the use case can't be edited. Add a new campaign as Mixed or Low Volume Mixed, select it, and resubmit (\$15). |
| "Please include in Message Flow links to the privacy policy & terms of use" | The URLs are only in the URL fields. Repeat both inside the consent text. |
| Description too short, vague, or generic | Rewrite it using the template in step 2 so it names the business, the recipients, the message types, and the opt-in method. |
| Opt-in page can't be found, has a pre-checked box, or requires consent to submit | Make the page reachable without logging in, leave the box unchecked by default, and let the form submit without it. |
| Use case doesn't match the content | Edit the description and samples to stay inside the use case, such as dropping a marketing sample under Customer Care. If the content is what you actually send, add a new campaign as Mixed instead (\$15). |
| Prohibited content | Remove debt collection, lending, credit, or other prohibited language from every field. See step 1. |
| Public URL shortener in a sample | Replace bit.ly, tinyurl, and similar links with a URL on your own domain. |
## FAQ
No. You can file the campaign as soon as the brand exists and isn't rejected. Retell holds it as pending and forwards it to Twilio automatically once the brand is approved.
No, as long as the use case is unchanged. The corrected campaign is edited in place and reviewed again at no extra charge. You're charged \$15 again only when a new campaign has to be registered, which happens if the use case changed, the brand was re-registered, or Twilio declined the in-place edit (as of September 2026).
The business profile, then the brand, then the campaign. A rejected business profile marks its brand and campaigns rejected as well, so the same reason text on all three points at the profile alone. Correcting a rejected brand is free while the brand type stays the same, and the campaign controls stay disabled until the brand is approved or pending.
No, Use Case is read-only after registration. File a new campaign under the right use case with **Add Campaign Application**, select it, and click **Next** to point the number at it. That new campaign costs \$15. Deleting the SMS capability first isn't necessary while the number's application is rejected.
No. Only rejected campaigns can be edited. To change what an approved number sends, delete the SMS capability on the number and create a new campaign. Your business profile and brand are kept and reused.
If you only need to send a fixed message during calls, you can send from an SMS-approved Retell number with no application. The content is a preset template you can't customize, and two-way SMS isn't supported. See [Option 3 on the SMS page](/deploy/enable-sms#option-3-use-an-sms-approved-retell-number-no-a2p-required).
No. Twilio's default opt-out handling applies to Retell numbers, so STOP, UNSUBSCRIBE, CANCEL, END, QUIT, and HELP are handled for you. Your samples and opt-in script must still tell people they can reply STOP.
For Twilio's own guidance, see the [campaign approval requirements](https://support.twilio.com/hc/en-us/articles/11847054539547-A2P-10DLC-Campaign-Approval-Requirements), the [list of campaign and use case types](https://help.twilio.com/articles/1260801844470-List-of-Campaign-Types-and-Use-Case-Types-for-A2P-10DLC-registration), and the [forbidden message categories](https://help.twilio.com/articles/360045004974-Forbidden-Message-Categories-in-the-US-and-Canada-Short-Code-Toll-Free-and-Long-Code).
# Telnyx
Source: https://docs.retellai.com/deploy/telnyx
Connect Retell to a Telnyx account with elastic SIP trunking — create the trunk, configure FQDN routing, set up outbound auth, and import Telnyx numbers.
Connect a Telnyx number to Retell with [elastic SIP trunking](/deploy/custom-telephony), so your agents can make and receive calls on numbers you own in Telnyx. Create an FQDN trunk, configure routing and codecs, set up outbound auth, then import the number into Retell.
1. Create the trunk, select FQDN as the type, and give it a name.
2. Add FQDN
Add the FQDN of Retell's SIP server: `sip.retellai.com`. Select `SRV` as the
DNS record type.
3. Set up outbound calls authentication
Select credentials as the authentication method, and add the username and password.
You will need to use this username and password when importing the number to Retell.
Telnyx requires the header `X-Telnyx-Username: ` to be included in the outbound calls when using credentials as the authentication mechanism.
See Telnyx [documentation](https://support.telnyx.com/en/articles/5271423-guide-to-sip-anchorsite-settings#h_0b59d6992a) for more details.
You can find details on adding [custom SIP headers](/build/telephony/sip-headers), or on the [Make outbound calls](/deploy/outbound-call#step-2-make-outbound-calls) page.
4. Set up inbound setting
* Select `+E.164` as the number format.
* Select `G722`, `G711U`, `G711A` as the codecs.
* Select `TCP` as the transport method. (TCP is recommended over UDP for reliability.)
* Select your SIP region.
5. Set up outbound setting
Create a new outbound voice profile
And select that in the outbound setting
You've created the elastic SIP trunk. Now purchase numbers or move
existing numbers to this trunk.
Now that the number is set up with your elastic SIP trunk, import it to Retell so we know how to route the call.
Here you will supply Telnyx's FQDN as the termination SIP URI. You can find your FQDN based on
your choice of SIP region in this [doc](https://sip.telnyx.com/) (e.g. sip.telnyx.com). You will also need to supply
the username, password and any additional SIP headers you set up earlier in the outbound authentication as well.
You can also import numbers programmatically via [Import Number API](/api-references/import-phone-number).
Now that the number is imported, you can make and receive calls with it just like a number
purchased from Retell. It shows up in your dashboard, and you can make phone calls from the
dashboard directly. You can also use the [Create Phone Call API](/api-references/create-phone-call)
to create calls programmatically.
To have Retell stop using this number, delete it from the dashboard or via
the [Delete Number API](/api-references/delete-phone-number).
## Troubleshooting
If an inbound call fails, check the FQDN and inbound settings on your Telnyx trunk. If an outbound call fails, check the termination FQDN, credentials, and the `X-Telnyx-Username` header you supplied to Retell. See [Debug outbound call](/reliability/debug-outbound-call) for common SIP failures, or [debug SIP calls with PCAP files](/reliability/debug-calls-pcap) to inspect the signaling in Wireshark.
# Connect a Twilio number to Retell with elastic SIP trunking
Source: https://docs.retellai.com/deploy/twilio
Connect Retell to your Twilio account using elastic SIP trunking. Set up termination, origination, credentials, and import Twilio numbers into Retell.
Connect a Twilio number to Retell with [elastic SIP trunking](/deploy/custom-telephony), so your agents can make and receive calls on numbers you own in Twilio. Set up termination (outbound) and origination (inbound), add credentials, then import the number into Retell.
## Steps
1. Create the trunk, give it a name, and toggle some general settings
2. Set up termination (this is for outbound)
* The termination SIP URI here is important; you'll use it in later steps. Use a localized termination URI near your region. You can expand and view your localized URIs in the Twilio console.
* For your elastic SIP trunk to accept our outbound request, you need to whitelist
an IP address or create an auth with a username and password.
* If you opt for the auth route, you need to specify the username and password
in the next step when importing the number to Retell.
* You need to whitelist Retell SIP SBC CIDR block 18.98.16.120/30 like the following:
3. Set up origination (this is for inbound)
* Here you will specify Retell's SIP server address as the origination SIP URI:
`sip:sip.retellai.com`.
You've created the elastic SIP trunk. Now purchase numbers or move
existing numbers to this trunk.
Now that the number is set up with your elastic SIP trunk, import it to Retell so we know how to route the call.
Here you supply the termination SIP URI you set up in Step 1. If you set up
auth via credentials, supply the username and password as well.
You can also import numbers programmatically via [Import Number API](/api-references/import-phone-number).
Now that the number is imported, you can make and receive calls with it just like a number
purchased from Retell. It shows up in your dashboard, and you can make phone calls from the
dashboard directly. You can also use the [Create Phone Call API](/api-references/create-phone-call)
to create calls programmatically.
To have Retell stop using this number, delete it from the dashboard or via
the [Delete Number API](/api-references/delete-phone-number).
## Common issues
**1. After connecting, inbound works but outbound does not?**
* Check your termination SIP URI.
If there's a space in it, remove it. Also use a localized termination URI near your region. See this [doc](https://www.twilio.com/docs/global-infrastructure/localized-uris/termination) to select one.
* Check your username and credentials.
Make sure you entered the username and credentials shown in this dialog.
Note that the username is not the friendly name shown in the credential list;
double-check that you didn't confuse the two.
**2. How do I enable call transfers on a Twilio trunk?**
Retell's [Call Transfer tool](/build/single-multi-prompt/transfer-call) works over your Twilio trunk, but two Twilio-side settings matter:
* **Cold transfer with SIP REFER**: your trunk must allow SIP REFER and PSTN transfer. Enable this in the Twilio console for the elastic SIP trunk. See [Custom telephony FAQ](/deploy/custom-telephony) for the exact setting.
* **Custom SIP headers on cold transfer**: Twilio does not honor custom SIP headers on SIP REFER, so headers you set in the Call Transfer tool are dropped for cold transfers over Twilio. Use SIP INVITE if you need those headers to reach the destination. See [SIP headers](/build/telephony/sip-headers).
* **Dialing outside your country**: enable the destination country under **Voice Geographic Permissions → Elastic SIP Trunking** (see below).
**3. How do I set up dialing to international countries?**
* Search "geo" to find the "Voice Geographic Permissions" setting.
* Choose "Elastic SIP Trunking" in the selector, and select the countries you would like to dial. See [International calling](/deploy/international-call) for Retell's supported destinations and fees.
Still stuck? See [Debug outbound call](/reliability/debug-outbound-call) for common SIP failures, or [debug SIP calls with PCAP files](/reliability/debug-calls-pcap) to inspect the signaling in Wireshark.
## Phone number masking
If you have a personal phone number or a trusted business number you'd like to display to the callee, you can import verified phone numbers into Twilio to serve as the caller ID.
### Add a caller ID
1. Go to the [Verified Caller IDs page](https://www.twilio.com/console/phone-numbers/verified?_gl=1*l511c3*_gcl_aw*R0NMLjE3NTQ0MTE4NjUuQ2p3S0NBancxZExEQmhCb0Vpd0FRTlJpUVk5QWVqRDFrc2hEZl9ycVdOVlI4bmR3RVNTei1nM3JBcGpORVFXX3BCLXdMRFZPTnM3ei14b0NWNmdRQXZEX0J3RQ..*_gcl_au*MTAzNDAwNDYyNi4xNzUyNTE4OTQy*_ga*MjA5ODY1MzAyMS4xNzUyNTE4OTQy*_ga_RRP8K4M4F3*czE3NTczNzcyODAkbzI0JGcxJHQxNzU3MzgwOTUxJGo2MCRsMCRoMA..)
2. Click **Add a new Caller ID**
3. Enter the desired phone number to verify, select the desired verification method, and then click **Verify Number**
4. The number entered will receive an OTP Authentication code for verification. Enter this verification code on the next window.
5. Once you click **Submit**, if the correct OTP code was entered, you will receive a **Successful** notification and the number will be added to your account as a verified caller ID.
### Configure SIP header rules to display caller ID
This uses Twilio's own header manipulation. To set or parse [custom SIP headers](/build/telephony/sip-headers) on the Retell side, see the SIP headers guide.
1. In the Twilio Console, navigate to Elastic SIP Trunking → Trunks → \[your trunk] → Termination
2. Scroll down to Header Manipulation and open **View all SIP header manipulation policies**
3. Click on **Create a policy** in the top right and give your policy a friendly name
4. Click on **+ Add request rule** and give the rule a friendly name
5. Under the Actions section, set the following values:
* **SIP header field**: **From number**
* **Action**: **Replace with**
* **Value**: Your caller ID in E.164 format (e.g. +18881230987)
6. Click **Add rule**, and then click **Save policy**
7. Return to the Termination tab within your trunk, and select the new header manipulation policy from the dropdown
8. Now, any number imported from this Twilio trunk into your Retell account will use your verified caller ID. Place an outbound test call to validate the changes.
* *Note:* You will need at least one number purchased from Twilio in your SIP trunk in order to apply the caller ID.
# Vonage
Source: https://docs.retellai.com/deploy/vonage
Connect Retell to a Vonage account using SIP trunking — configure termination and origination, set up credentials, and import Vonage numbers for calling.
Connect a Vonage number to Retell with [SIP trunking](/deploy/custom-telephony), so your agents can make and receive calls on numbers you own in Vonage. Set up termination (outbound) and origination (inbound), add credentials, then import the number into Retell.
1. Locate the SIP section in the Vonage dashboard, and select "Something else" for the provider type.
From here, you can follow these instructions step by step.
2. Set up termination (for outbound)
* The termination SIP URI, username, and password here are important; you'll use them in later steps, so note them down.
3. Set up origination (for inbound)
* Here you will specify Retell's SIP server address as the origination SIP URI:
`sip.retellai.com`.
You've created the elastic SIP trunk. Now purchase numbers or link
existing numbers to this trunk.
Now that the number is set up with your elastic SIP trunk, import it to Retell so we know how to route the call.
Here you supply the termination SIP URI, username, and password you set up in Step 1.
You can also import numbers programmatically via [Import Number API](/api-references/import-phone-number).
Now that the number is imported, you can make and receive calls with it just like a number
purchased from Retell. It shows up in your dashboard, and you can bind agents and make phone calls from the
dashboard directly.
Check out the [Make outbound calls](/deploy/outbound-call) guide to learn how to place calls, and [Receive inbound calls](/deploy/inbound-call) to bind an agent to your number.
## Troubleshooting
If an inbound call fails, check the origination SIP URI on your Vonage trunk. If an outbound call fails, check the termination SIP URI, username, and password you supplied to Retell. See [Debug outbound call](/reliability/debug-outbound-call) for common SIP failures, or [debug SIP calls with PCAP files](/reliability/debug-calls-pcap) to inspect the signaling in Wireshark.
# Make a web call
Source: https://docs.retellai.com/deploy/web-call
Make browser voice calls with Retell AI: use the JavaScript SDK to connect users to an agent, control microphone audio, and handle transcripts and call events.
A web call connects a user to your voice agent directly in the browser, using their microphone and speakers. The [Retell Web SDK](https://github.com/RetellAI/retell-client-js-sdk) creates the call and connects its audio; no phone number is involved.
## When to use web calls
* **Voice inside your product.** Add a support agent to your help center, a sales assistant to a landing page, or voice-guided onboarding, with a UI you fully control.
* **Custom call experiences.** Build call controls and audio visualizations. Enable live transcripts separately for captions or conversation flow updates.
* **Development and testing.** Talk to an agent while building it. For a quick test without writing code, use the dashboard's [web call testing](/test/test-web).
If you want a voice entry point on your website without building a frontend, embed the [website widget](/deploy/chat-widget). To reach users on their phones, see [outbound calls](/deploy/outbound-call) and [inbound calls](/deploy/inbound-call).
For example, an e-commerce site adds a "Talk to support" button to its order page. Clicking it starts a web call that passes the customer's name and order ID as [dynamic variables](/build/dynamic-variables), so the agent can look up the right order.
## Set up web calls
```bash theme={"dark"}
npm install retell-client-js-sdk@latest
```
Create a [public key](/accounts/public-keys) and allow your website's domain. Add `localhost` to test locally. Use the public key in your browser code; keep API keys on your server.
If your public key has reCAPTCHA enabled, obtain a fresh token before each call and pass it as `recaptchaToken` to `createWebCall()`.
Initialize `RetellClient` and call `createWebCall()` from your button's click handler. Replace the public key and agent ID with your own values.
```javascript theme={"dark"}
import { RetellClient } from "retell-client-js-sdk";
const client = new RetellClient({ key: "public_key_YOUR_PUBLIC_KEY" });
let call;
function startCall() {
if (call && call.status !== "ended") return;
call = client.createWebCall({
agent_id: "agent_oBeDLoLOeuAbiuaMFXRtDOLriTJ5tSxD",
retell_llm_dynamic_variables: { customer_name: "John Doe" },
metadata: { internal_customer_id: "cust_123" },
hooks: {
onStatus: (status) => console.log("Call status:", status),
onEnd: () => console.log("Call ended"),
onError: (error) => console.error("Call error:", error),
},
});
}
```
`createWebCall()` returns a session immediately with `status: "connecting"`, then reports `live` and `ended` through `onStatus`. Audio may take a moment to connect after `live`. Once creation succeeds, `call.callId` identifies the call.
The browser prompts for microphone permission. Serve your page over HTTPS; `localhost` also works during development.
`agent_id` is the only required call option. Common optional fields:
| Field | What it does |
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `retell_llm_dynamic_variables` | String key-value pairs injected into the agent's prompt and tool descriptions as [dynamic variables](/build/dynamic-variables). |
| `metadata` | An arbitrary object stored on the call, such as an internal customer ID. Not used for processing; returned when you retrieve the call later. |
| `agent_version` | The numeric [agent version](/agent/version) to use for this call. |
| `agent_override` | Override agent configuration for this call only. |
See [Create Web Call](/api-references/create-web-call) for the REST request fields, and [audio controls](#control-audio-during-the-call) for browser audio options.
```javascript theme={"dark"}
await call?.end();
```
The agent can also end the call. Either way, the session reports `ended` through `onStatus` and fires `onEnd`.
## Handle call events
Pass hooks when creating the call, as shown above, or register listeners on the returned session:
```javascript theme={"dark"}
call.on("end", () => {
console.log("Call ended");
});
```
| Hook | Event | When it fires |
| ------------------ | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `onStatus` | `status` | The session reports `live` or `ended`. Read the initial `call.status` for `connecting`. |
| `onAudio` | `audio` | An audio visualization snapshot is available. Requires `audio.emitRawAudioSamples`. |
| `onEnd` | `end` | The session ends, including after a failed connection. The payload may be empty. |
| `onError` | `error` | The SDK reports an error, such as a failed creation request or microphone permission denial. The payload is an `Error` object. |
| `onTranscript` | `transcript` | The live transcript changes. Requires the transcript connection described below. |
| `onNodeTransition` | `node_transition` | A conversation flow node first appears in the transcript, including nodes in the initial snapshot. Requires the transcript connection. |
Reset call controls in `onEnd`, including when no `onError` fires. Use [Get Call](/api-references/get-call) for the recorded disconnection reason.
## Enable live transcripts
Live transcripts use a separate [monitoring WebSocket](/api-references/monitor-call-websocket) and are off by default. With the public-key client configured above, add `transcript: true`:
```javascript theme={"dark"}
call = client.createWebCall({
agent_id: "agent_oBeDLoLOeuAbiuaMFXRtDOLriTJ5tSxD",
transcript: true,
hooks: {
onTranscript: (transcript, preSessionTranscript) => {
console.log("Conversation:", transcript);
console.log("Pre-session tools:", preSessionTranscript);
},
onNodeTransition: (transition) => console.log("Flow node:", transition),
onError: (error) => console.error("Call error:", error),
},
});
```
`onTranscript` receives the full transcript so far and a separate array of pre-session tool calls. The SDK merges updates by each item's stable ID. Items include spoken turns, tool calls, and flow nodes; see the [transcript item spec](/api-references/monitor-call-websocket#transcript-item-spec). Transcript text can change as a turn progresses and does not mark exact speaking boundaries.
If the optional transcript connection fails, the SDK logs a console warning and the audio call continues. This failure does not fire `onError`. See [WebSocket authentication](/api-references/monitor-call-websocket#authentication) if you create calls through your own backend.
## Control audio during the call
Mute and unmute the microphone without ending the call:
```javascript theme={"dark"}
call.mute();
call.unmute();
```
Some browsers block playback until the user interacts with the page. To resume audio, call this from a click handler:
```javascript theme={"dark"}
await call.startAudioPlayback();
```
Pass capture, playback, or visualization settings in the `audio` option of `createWebCall()`:
| Option | Type | Description |
| --------------------- | ------- | ------------------------------------------------------------------------------------------ |
| `sampleRate` | number | Requested audio sample rate. See [audio basics](/knowledge/audio-basics). |
| `captureDeviceId` | string | Microphone device ID. |
| `playbackDeviceId` | string | Speaker device ID. |
| `emitRawAudioSamples` | boolean | Emit `Float32Array` snapshots through `onAudio` or the `audio` event. Defaults to `false`. |
Use `onAudio` snapshots to calculate a level for an orb or volume meter. They contain incoming audio, including any background sound, and are sampled for visualization rather than continuous recording.
## After the call
Collect full results after the call ends:
* [Register a webhook](/features/register-webhook) to receive `call_started`, `call_ended`, and `call_analyzed` events on your server.
* Use `call.callId` with the [get call API](/api-references/get-call) to fetch the complete transcript, recording, and analysis, or review the call in [session history](/features/session-history).
## Example project
The [React and Node.js demo](https://github.com/RetellAI/retell-frontend-reactjs-demo) uses `RetellClient` with an API key kept on the Node.js backend.
## FAQ
Read the error passed to `onError`. Check your public key's allowed domains, the agent ID, and microphone permission. If reCAPTCHA is enabled, provide a fresh token for each call.
Browsers can block audio playback before user interaction. Start the call from a click handler, or call `call.startAudioPlayback()` inside one.
The user may have denied microphone permission, or the page isn't served over HTTPS. Microphone access requires a secure context; localhost works during development.
Use `retell_llm_dynamic_variables` for values the agent should use in conversation. Use `metadata` for values you only need to look up later, such as an internal customer ID. See [dynamic variables](/build/dynamic-variables).
Yes. Web calls draw from the same [concurrency](/deploy/concurrency) pool as outbound phone calls.
# Explicit cold transfer mode selection (01/23/2026)
Source: https://docs.retellai.com/deprecation-notice/2026/01-23_cold_transfer_mode_selection
On 01/23/2026 the `show_transferee_as_caller` field changes meaning — Retell switches to explicit cold transfer mode between SIP REFER and SIP INVITE.
## Cold Transfer Mode Selection
The behavior of the `show_transferee_as_caller` parameter in Cold Transfer options is changing. Previously, this parameter was used to toggle between SIP REFER and SIP INVITE transfer modes.
**Affected APIs:**
* [Create Retell LLM](/api-references/create-retell-llm)
* [Update Retell LLM](/api-references/update-retell-llm)
* [Create Conversation Flow](/api-references/create-conversation-flow)
* [Update Conversation Flow](/api-references/update-conversation-flow)
* [Create Conversation Flow Component](/api-references/create-conversation-flow-component)
* [Update Conversation Flow Component](/api-references/update-conversation-flow-component)
**What's changing:**
* The `show_transferee_as_caller` parameter will no longer control the transfer mode (SIP REFER vs SIP INVITE).
* Use the new `cold_transfer_mode` parameter to explicitly choose between `sip_refer` and `sip_invite`.
* The `show_transferee_as_caller` parameter will only control caller ID display and will only take effect when `cold_transfer_mode` is set to `sip_invite`.
**Migration:**
* Set `cold_transfer_mode` to `sip_refer` or `sip_invite` to choose the transfer method.
* Set `show_transferee_as_caller` to `true` only if you want to show the transferee as the caller when using `sip_invite` mode.
**Effective date:** After 01/23/2026, `show_transferee_as_caller` will no longer affect the transfer mode selection.
# Phone number single-agent fields removed (03/31/2026)
Source: https://docs.retellai.com/deprecation-notice/2026/03-31_phone_number_agent_fields
On 03/31/2026 Retell deprecates single-agent fields on phone numbers in favor of weighted inbound, outbound, and SMS agent lists across the phone number APIs.
## Phone number single-agent fields
The single-agent fields on phone number configuration are deprecated in favor of weighted agent lists for inbound/outbound calls and SMS.
**Affected APIs:**
* [Create Phone Number](/api-references/create-phone-number)
* [Import Phone Number](/api-references/import-phone-number)
* [Update Phone Number](/api-references/update-phone-number)
* [Get Phone Number](/api-references/get-phone-number)
* [List Phone Numbers](/api-references/list-phone-numbers)
**Deprecated fields:**
* `inbound_agent_id`, `inbound_agent_version`
* `outbound_agent_id`, `outbound_agent_version`
* `inbound_sms_agent_id`, `inbound_sms_agent_version`
* `outbound_sms_agent_id`, `outbound_sms_agent_version`
**Use instead:**
* `inbound_agents`
* `outbound_agents`
* `inbound_sms_agents`
* `outbound_sms_agents`
**Migration:**
* For a single agent, set the corresponding `*_agents` list to a single entry with `weight: 1`.
* For multiple agents, split weights so they sum to 1.
* For SMS, use `*_sms_agents` with the same weighting rules.
**Note:** Existing data is converted automatically and no action is required. Until the deprecation date, the APIs remain backwards-compatible as long as only a single agent is used for each of the deprecated fields.
**Example:**
* Before:
```json theme={"dark"}
{
"inbound_agent_id": "oBeDLoLOeuAbiuaMFXRtDOLriTJ5tSxD",
"inbound_agent_version": 3
}
```
* After:
```json theme={"dark"}
{
"inbound_agents": [
{ "agent_id": "oBeDLoLOeuAbiuaMFXRtDOLriTJ5tSxD", "agent_version": 3, "weight": 1 }
]
}
```
**Effective date:** After 03/31/2026, the deprecated single-agent fields will no longer be supported.
# OpenAI Realtime and Cartesia Sonic replaced (04/03/2026)
Source: https://docs.retellai.com/deprecation-notice/2026/04-03_model_replacements
On 04/03/2026 Retell replaces OpenAI Realtime models (gpt-4o-realtime, gpt-4o-mini-realtime) and Cartesia Sonic-2 and Sonic-Turbo with their newer counterparts.
* The following models are being deprecated and replaced with newer versions:
* **OpenAI Realtime models:**
* `gpt-4o-realtime` → replaced with `gpt-realtime-1.5`
* `gpt-4o-mini-realtime` → replaced with `gpt-realtime-mini`
* **Cartesia Sonic models:**
* `sonic-2` → replaced with `sonic-3`
* `sonic-turbo` → replaced with `sonic-3`
* After 4/3/2026, the deprecated models will no longer be available.
# Conversation node tools replaced by subagent (04/18/2026)
Source: https://docs.retellai.com/deprecation-notice/2026/04-18_conversation_node_tools
On 04/18/2026 Retell deprecates `tools` and `tool_ids` on conversation nodes in favor of the new subagent node for dialogue that calls tools mid-call.
## Deprecating tools on conversation nodes — use subagent nodes instead
The `tools` and `tool_ids` fields on conversation nodes are deprecated in favor of the new `subagent` node type. The conversation node type itself is **not** deprecated — it continues to work for nodes that don't use tools. The new `subagent` node type provides the same conversation-with-tools functionality — the agent converses with the user while being able to call tools — but as a dedicated node type.
**Affected APIs:**
* [Create Conversation Flow](/api-references/create-conversation-flow)
* [Update Conversation Flow](/api-references/update-conversation-flow)
* [Get Conversation Flow](/api-references/get-conversation-flow)
* [List Conversation Flows](/api-references/list-conversation-flows)
* [Create Conversation Flow Component](/api-references/create-conversation-flow-component)
* [Update Conversation Flow Component](/api-references/update-conversation-flow-component)
* [Get Conversation Flow Component](/api-references/get-conversation-flow-component)
* [List Conversation Flow Components](/api-references/list-conversation-flow-components)
**Deprecated fields on `type: "conversation"` nodes:**
* `tools`
* `tool_ids`
**Use instead:** Set `type: "subagent"` on nodes that need tools.
**What's changing on 04/18/2026:**
1. Existing conversation nodes with tools will be automatically migrated to `type: "subagent"` — API responses will return the new type for these nodes.
2. Conversation nodes with tools that use static text instructions will be converted to subagent nodes with prompt text instructions.
3. The API will stop accepting `tools` or `tool_ids` fields on `type: "conversation"` nodes.
**Migration:**
* For conversation nodes **with** tools: change `type` from `"conversation"` to `"subagent"`. If the node uses a `static_text` instruction, change it to `prompt`. All other fields remain the same.
* For conversation nodes **without** tools: no changes needed — these remain `type: "conversation"`.
**Example:**
* Before:
```json theme={"dark"}
{
"id": "handle_order",
"type": "conversation",
"instruction": {
"text": "Help the user check their order status.",
"type": "prompt"
},
"tools": [
{
"type": "custom_function",
"name": "check_order_status",
"description": "Look up order by order number",
"parameters": { "type": "object", "properties": { "order_number": { "type": "string" } } }
}
]
}
```
* After:
```json theme={"dark"}
{
"id": "handle_order",
"type": "subagent",
"instruction": {
"text": "Help the user check their order status.",
"type": "prompt"
},
"tools": [
{
"type": "custom_function",
"name": "check_order_status",
"description": "Look up order by order number",
"parameters": { "type": "object", "properties": { "order_number": { "type": "string" } } }
}
]
}
```
**Effective date:** After 04/18/2026, the API will no longer accept `tools` or `tool_ids` on `type: "conversation"` nodes. Use `type: "subagent"` instead.
# Claude Sonnet and Gemini Flash model upgrades (05/25/2026)
Source: https://docs.retellai.com/deprecation-notice/2026/05-25_model_replacements
On 05/25/2026 Retell replaces Claude 4.0 Sonnet with 4.6 Sonnet and migrates Gemini 2.0 Flash, Flash-Lite, and 2.5 Flash to the newer Gemini 3.0 and 3.1 models.
* The following models are being deprecated and replaced with newer versions:
* **Claude models:**
* `claude-4.0-sonnet` → replaced with `claude-4.6-sonnet`
* **Gemini models:**
* `gemini-2.0-flash` → replaced with `gemini-3.0-flash`
* `gemini-2.0-flash-lite` → replaced with `gemini-3.1-flash-lite`
* `gemini-2.5-flash` → replaced with `gemini-3.0-flash`
* These models are no longer available. On 5/25/2026, they will be automatically migrated to their replacements.
# Legacy list endpoints removed for v2/v3 (06/15/2026)
Source: https://docs.retellai.com/deprecation-notice/2026/06-15_legacy_list_endpoints
On 06/15/2026 Retell removes legacy list endpoints (batch tests, conversation flows, phone numbers, Retell LLMs) — migrate to versioned v2 and v3 endpoints.
## Legacy list endpoints are deprecated
The following list endpoints are deprecated in favor of newer versioned endpoints with unified pagination patterns.
**Affected APIs:**
* [List Batch Tests](/api-references/list-batch-tests)
* [List Conversation Flow Components](/api-references/list-conversation-flow-components)
* [List Conversation Flows](/api-references/list-conversation-flows)
* [List Phone Numbers](/api-references/list-phone-numbers)
* [List Retell LLMs](/api-references/list-retell-llms)
* [List Test Case Definitions](/api-references/list-test-case-definitions)
* [List Test Runs](/api-references/list-test-runs)
* [List Calls](/api-references/list-calls)
* [List Chats](/api-references/list-chats)
## Endpoint migrations
* `GET /list-batch-tests` -> `GET /v2/list-batch-tests` ([List Batch Tests](/api-references/list-batch-tests))
* `GET /list-conversation-flow-components` -> `GET /v2/list-conversation-flow-components` ([List Conversation Flow Components](/api-references/list-conversation-flow-components))
* `GET /list-conversation-flows` -> `GET /v2/list-conversation-flows` ([List Conversation Flows](/api-references/list-conversation-flows))
* `GET /list-phone-numbers` -> `GET /v2/list-phone-numbers` ([List Phone Numbers](/api-references/list-phone-numbers))
* `GET /list-retell-llms` -> `GET /v2/list-retell-llms` ([List Retell LLMs](/api-references/list-retell-llms))
* `GET /list-test-case-definitions` -> `GET /v2/list-test-case-definitions` ([List Test Case Definitions](/api-references/list-test-case-definitions))
* `GET /list-test-runs/{test_case_batch_job_id}` -> `GET /v2/list-test-runs/{test_case_batch_job_id}` ([List Test Runs](/api-references/list-test-runs))
* `POST /v2/list-calls` -> `POST /v3/list-calls` ([List Calls](/api-references/list-calls))
* `GET /list-chat` -> `POST /v3/list-chats` ([List Chats](/api-references/list-chats))
**What's changing on 06/15/2026:**
1. The legacy list endpoints above will no longer be supported.
2. `GET /list-chat` changes both method and path to `POST /v3/list-chats`.
3. Versioned list endpoints return unified pagination fields: `items`, `pagination_key`, and `has_more`.
**Migration:**
* Update each client call to the new method/path listed above.
* Update response handling to read `items` from the paginated response object (instead of expecting a top-level array from legacy endpoints).
* Keep using `pagination_key` and `has_more` for page traversal.
**Effective date:** After 06/15/2026, the legacy endpoints listed above will no longer be supported.
## Analysis prompt fields are deprecated
The following top-level analysis prompt fields on voice and chat agent configurations are deprecated in favor of the `post_call_analysis_data` and `post_chat_analysis_data` arrays.
**Deprecated fields:**
* `analysis_summary_prompt`
* `analysis_successful_prompt`
* `analysis_user_sentiment_prompt`
These fields exist on both voice agent and chat agent endpoints:
* [Create Agent](/api-references/create-agent) / [Update Agent](/api-references/update-agent)
* [Create Chat Agent](/api-references/create-chat-agent) / [Update Chat Agent](/api-references/update-chat-agent)
**What's changing on 06/15/2026:**
1. The three prompt fields above will be removed from the API.
2. Use system preset items inside `post_call_analysis_data` (voice agents) or `post_chat_analysis_data` (chat agents) to customize prompts for summary, success, and sentiment analysis.
**Migration:**
Replace each deprecated field with a system preset entry in the corresponding analysis data array. Set `type` to `system-presets`, `name` to the preset identifier, and `description` to your custom prompt.
| Deprecated field | Preset `name` (voice) | Preset `name` (chat) |
| -------------------------------- | --------------------- | -------------------- |
| `analysis_summary_prompt` | `call_summary` | `chat_summary` |
| `analysis_successful_prompt` | `call_successful` | `chat_successful` |
| `analysis_user_sentiment_prompt` | `user_sentiment` | `user_sentiment` |
For example, if you currently set `analysis_summary_prompt` on a voice agent:
```json Before theme={"dark"}
{
"analysis_summary_prompt": "Summarize the outcome of the conversation in two sentences."
}
```
```json After theme={"dark"}
{
"post_call_analysis_data": [
{
"type": "system-presets",
"name": "call_summary",
"description": "Summarize the outcome of the conversation in two sentences."
}
]
}
```
# ElevenLabs Turbo models replaced with Flash (07/12/2026)
Source: https://docs.retellai.com/deprecation-notice/2026/07-12_elevenlabs_turbo_models
On 07/12/2026, Retell replaces the ElevenLabs Turbo voice models (eleven_turbo_v2 and eleven_turbo_v2_5) with the Flash equivalents at lower latency.
* The following voice models are being deprecated and replaced with newer versions:
* **ElevenLabs voice models:**
* `eleven_turbo_v2` → replaced with `eleven_flash_v2`
* `eleven_turbo_v2_5` → replaced with `eleven_flash_v2_5`
* We have confirmed with ElevenLabs and verified that the Flash models deliver the same voice quality as the Turbo models at lower latency.
* These models are no longer available for new agents. On 7/12/2026, existing agents will be automatically migrated to their replacements.
# Unified publish-agent-version endpoint (07/20/2026)
Source: https://docs.retellai.com/deprecation-notice/2026/07-20_agent_version_endpoints
On 07/20/2026 Retell deprecates the legacy publish-agent and publish-chat-agent endpoints — migrate to the unified publish-agent-version endpoint.
## Legacy agent publish endpoints are deprecated
**Deprecation date:** 07/20/2026
The following APIs are deprecated as of 07/20/2026. Migrate to the listed replacements.
**Affected APIs:**
* `POST /publish-agent/{agent_id}`
* `POST /publish-chat-agent/{agent_id}`
## Endpoint migrations
* `POST /publish-agent/{agent_id}` -> `POST /publish-agent-version/{agent_id}` ([Publish Agent](/api-references/publish-agent))
* `POST /publish-chat-agent/{agent_id}` -> `POST /publish-agent-version/{agent_id}` ([Publish Chat Agent](/api-references/publish-chat-agent))
## Migration details
* Update client calls to use the replacement method and path listed above.
* Deprecation date: 07/20/2026.
# Legacy MCP server (retell.stlmcp.com) removed (07/20/2026)
Source: https://docs.retellai.com/deprecation-notice/2026/07-20_legacy_mcp_server
The legacy hosted MCP server at retell.stlmcp.com shuts down on 07/20/2026. Migrate your MCP client to mcp.retellai.com — auth is unchanged.
## The legacy hosted MCP server is being removed
**Removal date:** 07/20/2026
The hosted MCP server at `retell.stlmcp.com` will be shut down. Use the current
Retell MCP server at `mcp.retellai.com` instead — see the
[Retell MCP server](/get-started/mcp-server) guide for setup.
## Migration
* **Change the MCP server URL** from `https://retell.stlmcp.com` to `https://mcp.retellai.com`.
* **Auth is unchanged:** send your Retell API key as a `Bearer` token.
* **LLM clients (Claude, Cursor, etc.):** no code changes needed — they discover the available tools automatically.
* The local `npx @retell-ai/mcp-server` package is not affected.
# Legacy agent list endpoints removed (07/31/2026)
Source: https://docs.retellai.com/deprecation-notice/2026/07-31_agent_list_endpoints
On 07/31/2026 Retell removes GET /list-agents and GET /list-chat-agents. Migrate to the unified POST /v2/list-agents endpoint with filters.
## Legacy agent list endpoints are deprecated
**Deprecation date:** 07/31/2026
The following APIs are deprecated as of 07/31/2026. Migrate to the listed replacements.
**Affected APIs:**
* `GET /list-agents`
* `GET /list-chat-agents`
## Endpoint migrations
* `GET /list-agents` -> `POST /v2/list-agents`
* `GET /list-chat-agents` -> `POST /v2/list-agents`
## Migration details
* Update client calls to use `POST /v2/list-agents`.
* To list only voice agents, set `filter_criteria.channel` to `{ "type": "string", "op": "eq", "value": "voice" }`.
* To list only chat agents, set `filter_criteria.channel` to `{ "type": "string", "op": "eq", "value": "chat" }`.
* Read results from `items` instead of expecting a top-level array.
* Continue pagination with the returned `pagination_key` while `has_more` is `true`.
* Stop sending `pagination_key_version`; the new endpoint does not use it.
* Deprecation date: 07/31/2026.
The new endpoint returns one unified list for voice and chat agents. Use the `channel` filter to preserve the old voice-only or chat-only behavior, and update any array-based response handling to read from `items`.
# Multilingual agent locale array required (07/31/2026)
Source: https://docs.retellai.com/deprecation-notice/2026/07-31_legacy_multilingual_setting
On 07/31/2026, the scalar "multi" value of the agent language field is removed in favor of an explicit locale array for multilingual Retell agents.
## Legacy multilingual setting is deprecated
The legacy scalar value `"multi"` of the agent `language` field is deprecated in favor of the explicit locale-array form (e.g. `["en-US","es-ES"]`). The array form is supported on the same `language` field and lets you pick the exact set of languages your agent should handle, which improves accuracy compared to the static 10-language `"multi"` set.
**Affected APIs:**
* [Create Agent](/api-references/create-agent)
* [Update Agent](/api-references/update-agent)
* [Create Chat Agent](/api-references/create-chat-agent)
* [Update Chat Agent](/api-references/update-chat-agent)
**What's changing on 07/31/2026:**
1. The scalar `"multi"` value of `language` will be rejected by the Create Agent, Update Agent, Create Chat Agent, and Update Chat Agent endpoints.
2. The dashboard's legacy **Multilingual** toggle will be removed.
3. Existing agents that still have `language: "multi"` will be **automatically migrated by Retell** to the equivalent locale array (`["en-US", "es-ES", "fr-FR", "de-DE", "hi-IN", "ru-RU", "pt-PT", "ja-JP", "it-IT", "nl-NL"]`). No action is required on your existing agents.
**Action required for API integrators:**
If your client code constructs agent payloads with `"language": "multi"`, update it to send the locale array form before 07/31/2026. After that date, requests using the scalar `"multi"` value will be rejected. For better accuracy, narrow the array down to the languages your agent actually needs — see the accuracy trade-offs in [Configure a multilingual agent](/agent/multilingual).
```json Before theme={"dark"}
{
"language": "multi"
}
```
```json After theme={"dark"}
{
"language": ["en-US", "es-ES", "fr-FR", "de-DE", "hi-IN", "ru-RU", "pt-PT", "ja-JP", "it-IT", "nl-NL"]
}
```
Also update any code that reads agent configurations and branches on `language === "multi"`, since after the automatic migration the field will return the array form instead.
**Effective date:** After 07/31/2026, agents created or updated with `language: "multi"` will be rejected. Existing agents are migrated automatically on or before this date.
# Update Call restricted to ended calls (08/31/2026)
Source: https://docs.retellai.com/deprecation-notice/2026/08-31_update_call_ended_calls_only
Update Call becomes ended-calls-only on 08/31/2026; override_dynamic_variables and mid-call data_storage_setting changes move to Update Live Call.
## Update Call is moving to ended calls only
The [Update Call](/api-references/update-call) API (`PATCH /v2/update-call/{call_id}`) is changing to operate on **ended calls only**. As part of this change, the `override_dynamic_variables` input on Update Call is deprecated.
Previously, Update Call could change settings mid-call — for example, updating `data_storage_setting` would take effect on the ongoing call. After this change, updates you make through Update Call (including `data_storage_setting`) take effect only on **ended calls** and no longer affect ongoing calls. To override dynamic variables or change settings like `data_storage_setting` on an ongoing call, use the new [Update Live Call](/api-references/update-live-call) API (`PATCH /v2/update-live-call/{call_id}`).
**Affected APIs:**
* [Update Call](/api-references/update-call)
**Deprecated field:**
* `override_dynamic_variables` (on Update Call)
**Use instead:**
* For ongoing (live) calls: [Update Live Call](/api-references/update-live-call) (`PATCH /v2/update-live-call/{call_id}`). Dynamic variable overrides go under `fields_to_override.override_dynamic_variables`; the same request can also override `metadata` and `data_storage_setting`, and control the live agent via `call_control` (`trigger_response`, `additional_context`).
* For ended calls: continue using [Update Call](/api-references/update-call) to update `metadata`, `data_storage_setting`, and `custom_attributes`.
**What's changing on 08/31/2026:**
1. Update Call will only accept requests for calls that have already ended. Requests targeting ongoing calls will be rejected.
2. Updates you make through Update Call — including `data_storage_setting` — will take effect only on ended calls, not on ongoing calls.
3. The `override_dynamic_variables` input on Update Call will no longer be accepted.
4. To change settings on an ongoing call (dynamic variables, `data_storage_setting`, etc.), use the Update Live Call API instead.
**Migration:**
* If you call Update Call on an ongoing call to change dynamic variables, switch to [Update Live Call](/api-references/update-live-call) and move the values under `fields_to_override.override_dynamic_variables`.
* If you call Update Call on an ongoing call to change `data_storage_setting`, switch to [Update Live Call](/api-references/update-live-call); through Update Call, `data_storage_setting` takes effect only after the call ends.
* If you call Update Call on an ended call, no change is needed for `metadata`, `data_storage_setting`, or `custom_attributes`. Remove `override_dynamic_variables` from those requests — it has no effect on an ended call.
```json Before — PATCH /v2/update-call/{call_id} theme={"dark"}
{
"override_dynamic_variables": { "additional_discount": "15%" }
}
```
```json After — PATCH /v2/update-live-call/{call_id} theme={"dark"}
{
"fields_to_override": {
"override_dynamic_variables": { "additional_discount": "15%" }
}
}
```
**Effective date:** After 08/31/2026, Update Call will only work for ended calls and will no longer accept `override_dynamic_variables`. Use the [Update Live Call](/api-references/update-live-call) API (`PATCH /v2/update-live-call/{call_id}`) to update ongoing calls.
# Legacy get-agent-versions endpoints removed (09/15/2026)
Source: https://docs.retellai.com/deprecation-notice/2026/09-15_get_agent_versions
On 09/15/2026 Retell removes GET /get-agent-versions and GET /get-chat-agent-versions. Migrate to the unified GET /list-agent-versions endpoint.
## Get agent versions endpoints are deprecated
**Deprecation date:** 09/15/2026
The following APIs are deprecated as of 09/15/2026. Migrate to the listed replacements.
**Affected APIs:**
* `GET /get-agent-versions/{agent_id}`
* `GET /get-chat-agent-versions/{agent_id}`
## Endpoint migrations
* `GET /get-agent-versions/{agent_id}` -> [`GET /list-agent-versions/{agent_id}`](/api-references/list-agent-versions)
* `GET /get-chat-agent-versions/{agent_id}` -> [`GET /list-agent-versions/{agent_id}`](/api-references/list-agent-versions)
## Migration details
* Update client calls to use the replacement method and path listed above.
* Deprecation date: 09/15/2026.
# Legacy browser JavaScript SDK (09/30/2026)
Source: https://docs.retellai.com/deprecation-notice/2026/09-30_create_web_call_v2
Migrate from the deprecated Retell AI browser client SDK v2 to v3: replace RetellWebClient with RetellClient and update call creation and event handlers.
Version 2.x of the browser JavaScript SDK, `retell-client-js-sdk`, will be
deprecated on **September 30, 2026**. Upgrade to SDK 3.x and replace
`RetellWebClient` with `RetellClient` for call creation, audio, and event handling.
Existing integrations continue working during migration.
## Upgrade the browser SDK
Upgrade to the latest browser SDK:
```bash theme={"dark"}
npm install retell-client-js-sdk@latest
```
SDK 3.x retains `RetellWebClient` for compatibility. Upgrading the package alone
does not migrate your integration to `RetellClient`; update call creation and
event handlers as shown below.
## Replace call creation and connection
Replace the request to your server and `RetellWebClient.startCall()` with
`RetellClient.createWebCall()`. The SDK sends the v3 request and connects audio
automatically.
Create a [public key](/accounts/public-keys) and allow your website's domain
(`localhost` for local testing). Keep API keys on your server. If reCAPTCHA is
enabled for the public key, pass a fresh token as `recaptchaToken` with each call.
Add call controls to your page:
```html theme={"dark"}
```
In your frontend JavaScript, replace the public key and agent ID with your own.
Load this code after the buttons exist, using your frontend's module bundler.
```javascript app.js theme={"dark"}
import { RetellClient } from "retell-client-js-sdk";
const client = new RetellClient({ key: "public_key_YOUR_PUBLIC_KEY" });
let call;
document.getElementById("start-call").addEventListener("click", () => {
if (call && call.status !== "ended") return;
call = client.createWebCall({
agent_id: "agent_oBeDLoLOeuAbiuaMFXRtDOLriTJ5tSxD",
hooks: {
onStatus: (status) => console.log("Call status:", status),
onEnd: () => console.log("Call ended"),
onError: (error) => console.error("Call error:", error),
},
});
});
document.getElementById("end-call").addEventListener("click", async () => {
await call?.end();
});
```
Serve the page over HTTPS or on `localhost`, click **Start call**, and allow
microphone access. Talk to the agent, then click **End call**. The session ends
and fires `onEnd`. See the [web call guide](/deploy/web-call) for audio controls
and live transcripts.
## Update event handlers
Calls created through v3 do not deliver every event from the previous connection
type. Audit handlers as well as call creation:
| Existing behavior | Migration |
| ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| Call lifecycle handlers | Use `onStatus`, `onEnd`, and `onError`. Always reset the UI in `onEnd`, even when no error is reported. |
| `update` / `onUpdate` transcript updates | Enable `transcript: true` and use `onTranscript`. It receives merged transcript arrays; the old `turntaking` field is not supplied. |
| `node_transition` / `onNodeTransition` | With `RetellClient`, enable the transcript connection. Nodes from the initial snapshot also trigger the callback. |
| `agent_start_talking`, `agent_stop_talking`, or `isAgentTalking` | Use audio levels for visual activity estimates. Exact speaking boundaries and finalized-sentence events are not available. |
| `metadata` / `onMetadata` | No metadata event is delivered on the new connection. If you need the call's stored `metadata`, retrieve it with Get Call. |
| `audio` / `onAudio` | Set `audio.emitRawAudioSamples: true` for visualization snapshots. These are not a continuous audio stream. |
See the web call guide for [supported hooks](/deploy/web-call#handle-call-events),
[transcript authentication](/deploy/web-call#enable-live-transcripts), and
[audio options](/deploy/web-call#control-audio-during-the-call).
## Related endpoint change for server-created calls
As part of this migration, `POST /v2/create-web-call` will also be deprecated on
September 30, 2026 in favor of
[`POST /v3/create-web-call`](/api-references/create-web-call).
`RetellClient.createWebCall()` uses v3 automatically.
If you retain an existing server-created call flow, upgrade both your server
SDK and browser SDK. The server SDK's web-call creation method now uses v3 and
returns connection details instead of the full call object. If you make HTTP
requests directly, switch to `POST /v3/create-web-call` with the same JSON body.
Upgrade the browser SDK before switching the server to v3. For an existing
`RetellWebClient` integration on SDK 3.x, forward these fields to `startCall()`:
| API response field | `startCall()` option |
| ------------------ | -------------------- |
| `call_id` | `callId` |
| `access_token` | `accessToken` |
| `transport` | `transport` |
| `ice_servers` | `iceServers` |
Use [get call](/api-references/get-call) if your application needs call fields
such as `call_status`, `agent_id`, or `metadata` after creation.
# Legacy SIP endpoint removed (09/30/2026)
Source: https://docs.retellai.com/deprecation-notice/2026/09-30_sip_legacy_endpoint
Retell retires SIP endpoint sip:5t4n6j0wnrl.sip.livekit.cloud on 09/30/2026; migrate elastic trunks and dial-to-sip integrations to sip:sip.retellai.com.
## The legacy SIP endpoint is being removed
**Removal date:** 09/30/2026
Migrate from `sip:5t4n6j0wnrl.sip.livekit.cloud` to `sip:sip.retellai.com`.
The legacy SIP endpoint `sip:5t4n6j0wnrl.sip.livekit.cloud` has been deprecated since November 2025 and will be retired on 09/30/2026. Route your inbound calls to the supported Retell SIP server `sip:sip.retellai.com` instead. See the [Connect to custom telephony](/deploy/custom-telephony) guide for setup.
After 09/30/2026, calls sent to `sip:5t4n6j0wnrl.sip.livekit.cloud` may no longer connect.
## Who is affected
Only integrations that dial or route directly to the hardcoded `5t4n6j0wnrl.sip.livekit.cloud` host. If your trunk or code already targets `sip.retellai.com`, no change is needed.
## Migration
* **Elastic SIP trunking:** update the SIP server URI/Origination URI on your trunk from `sip:5t4n6j0wnrl.sip.livekit.cloud` to `sip:sip.retellai.com`. If you pin a transport, keep it on the new host: `sip:sip.retellai.com;transport=tls` (also `;transport=tcp` or `;transport=udp`).
* **Dial to SIP URI:** dial each call to `sip:{call_id}@sip.retellai.com`, replacing `{call_id}` with the `call_id` returned by [Register Phone Call](/api-references/register-phone-call) — previously `sip:{call_id}@5t4n6j0wnrl.sip.livekit.cloud`.
* **Allowlist Retell's IP ranges:** make sure your firewall permits Retell's SIP CIDR blocks, listed in the [custom telephony](/deploy/custom-telephony) guide. Many providers require this before they accept traffic to the new host.
* **TLS / mTLS:** `sip.retellai.com` presents Retell's server certificate; if you pin a root CA or use mTLS, follow the [Security center](/deploy/custom-telephony#security-center) section.
No other part of your Retell configuration changes — only the SIP hostname you route to.
# Cal.com built-in tools (09/30–10/31/2026)
Source: https://docs.retellai.com/deprecation-notice/2026/10-31_legacy_calcom_tools
Retell's built-in Cal.com tools are gone from the dashboard, frozen by the API on 09/30/2026, and migrated to the Cal.com integration on 10/31/2026.
## The Cal.com integration replaces the built-in Cal.com tools
The built-in Cal.com tool types `check_availability_cal` and `book_appointment_cal` are deprecated in favor of the [Cal.com integration](/integrations/cal-com). This lands in three stages: the dashboard no longer offers these tools, the API stops accepting them on **09/30/2026**, and on **10/31/2026** Retell migrates the tools you already have to the integration.
Each built-in tool carries its own `cal_api_key`, so a workspace with ten booking agents holds ten copies of the same key, and rotating that key means editing all ten tools. The integration holds one key per workspace connection, adds booking lookup, reschedule, and cancel on top of availability and booking, and works with EU-hosted **cal.eu** accounts, which the built-in tools never supported.
**Affected APIs:**
* [Create Retell LLM](/api-references/create-retell-llm), [Update Retell LLM](/api-references/update-retell-llm) — fields: `general_tools`, `states[].tools`
* [Create Conversation Flow](/api-references/create-conversation-flow), [Update Conversation Flow](/api-references/update-conversation-flow) — field: `tools` (flow-level and on `type: "subagent"` nodes)
* [Create Conversation Flow Component](/api-references/create-conversation-flow-component), [Update Conversation Flow Component](/api-references/update-conversation-flow-component) — field: `tools`
**Deprecated tool types:**
* `check_availability_cal` — fields `cal_api_key`, `event_type_id`, `timezone`
* `book_appointment_cal` — fields `cal_api_key`, `event_type_id`, `timezone`
**Use instead:** [connect Cal.com](/integrations/cal-com) once for the workspace, then add its [agent functions](/integrations/cal-com-functions) to each agent.
| Built-in tool | Integration replacement |
| ------------------------ | ------------------------------------------------------------------------------ |
| `check_availability_cal` | **Check Availability** (`check_calcom_availability`) |
| `book_appointment_cal` | **Book Appointment** (`book_calcom_appointment`) |
| No equivalent | **List Bookings**, **Get Booking**, **Reschedule Booking**, **Cancel Booking** |
**What changed on 08/23/2026:**
1. The dashboard's function menu no longer offers **Check Calendar Availability** or **Book Appointment**, so you can't add one to an agent from the dashboard.
2. The API still creates and updates built-in Cal.com tools until 09/30/2026.
**What's changing on 09/30/2026:**
1. The API stops accepting `check_availability_cal` and `book_appointment_cal` on both create and update. Requests that add one of these tools, or change one you already have, fail.
2. Existing tools keep running unchanged until the migration, but no surface can edit them any more, since the dashboard writes through those same endpoints. **09/30/2026 is your last day to change a built-in tool's API key, event type ID, or timezone.**
**What's changing on 10/31/2026:**
1. Retell migrates existing built-in Cal.com tools to the integration. One connection covers each distinct API key: tools that share a key all point at the same connection, and a key you've already connected reuses that connection instead of getting a duplicate. Any connection the migration creates shows up on the **Connected** tab of the dashboard's Integrations page.
2. Migrated tools keep their current function name, so prompt text that calls the function by name (for example, "call the `check_calendar_availability` function") keeps working. The event type ID and timezone carry over.
3. **Tools that can't be migrated are skipped and stop working.** A tool is skipped when Cal.com rejects its key at migration time, because the key expired or was revoked. A skipped tool fails mid-call, the first time the agent tries to check availability or book.
4. API responses stop returning `check_availability_cal` and `book_appointment_cal`. Read a migrated agent back and you'll get the integration's tools instead, so client code that keys off the old tool types needs updating.
If your Cal.com key expires before 10/31/2026, replace it on the built-in tool before 09/30/2026, the last day an edit is accepted. After that the only fix is migrating the agent to the integration by hand.
**Migration:**
1. Find the agents still on the built-in tools. On the dashboard, check each agent's **Functions** section for **Check Calendar Availability** or **Book Appointment**. Through the API, call [Get Retell LLM](/api-references/get-retell-llm) or [Get Conversation Flow](/api-references/get-conversation-flow) and look for tools with `type` set to `check_availability_cal` or `book_appointment_cal`.
2. [Connect Cal.com](/integrations/cal-com) on the dashboard's **Integrations** page. Create the API key with **Never expires** turned on, and set **Domain** to `Cal.com`, the instance the built-in tools used. Pick `Cal.eu` only if your account has since moved to Cal.com's EU instance.
3. On each agent, add the integration's **Check Availability** and **Book Appointment** tools and give them the same [event type ID](/integrations/cal-com-functions#find-an-event-type-id) the built-in tool used.
4. Delete the built-in tool. If your prompt names the old function, update it to the new tool's name.
5. Test the agent before you ship it. [Simulation testing](/test/llm-simulation-testing) can mock the booking tools so you don't create real bookings.
You can create the connection through the API: [Create App](/api-references/create-app) takes the Cal.com API key in `auth_config` and the instance in `tenant_url` (`cal.com` or `cal.eu`). Adding the resulting tools to an agent is dashboard-only (as of August 2026), so an agent built entirely through the API still needs one dashboard pass.
**Effective dates:** The dashboard stopped offering the built-in Cal.com tools on 08/23/2026. The API stops creating and updating them on 09/30/2026, after which existing tools keep running but can no longer be edited. On 10/31/2026 Retell migrates them to the [Cal.com integration](/integrations/cal-com).
# Retell deprecations and breaking changes
Source: https://docs.retellai.com/deprecation-notice/overview
Browse upcoming and past Retell API and SDK deprecations and breaking changes by date. Subscribe via RSS for new deprecation notices.
This page tracks Retell API and SDK deprecations and breaking changes. Click the RSS button at the top of the page to subscribe and receive notifications when new deprecations are announced.
API deprecations take effect in the API itself, so staying on an older [SDK](/get-started/sdk#versioning) version doesn't defer them. The SDKs drop a retired endpoint or field in the first release generated after the API removes it.
## Upcoming deprecations
Announced but not yet in effect. Migrate before the date listed on each entry.
Version 2.x of the browser JavaScript SDK, `retell-client-js-sdk`, will be
deprecated on September 30, 2026. Upgrade to SDK 3.x, replace `RetellWebClient`
with `RetellClient`, and update your event handlers. Existing integrations
continue working during migration.
The `POST /v2/create-web-call` endpoint will also be deprecated. If your server
creates calls, migrate to [v3](/api-references/create-web-call).
[Migration steps](/deprecation-notice/2026/09-30_create_web_call_v2).
The legacy SIP endpoint `sip:5t4n6j0wnrl.sip.livekit.cloud` has been deprecated since November 2025 and will be retired on 09/30/2026. Point your SIP trunk — and any dial-to-SIP routing — at the supported Retell SIP server `sip:sip.retellai.com`. For dial-to-SIP, dial `sip:{call_id}@sip.retellai.com`, using the `call_id` from [Register Phone Call](/api-references/register-phone-call). After 09/30/2026, calls to the legacy endpoint may no longer connect.
**Impacted:**
* `sip:5t4n6j0wnrl.sip.livekit.cloud` — retired 09/30/2026. Use `sip:sip.retellai.com` (see [custom telephony](/deploy/custom-telephony)).
[Read more](/deprecation-notice/2026/09-30_sip_legacy_endpoint).
The built-in Cal.com tool types `check_availability_cal` and `book_appointment_cal` are deprecated in favor of the [Cal.com integration](/integrations/cal-com). The dashboard no longer offers them, and the API stops creating and updating them on 09/30/2026. Existing tools keep running until 10/31/2026, when Retell migrates them to the integration. A tool whose Cal.com key has expired is skipped and stops working.
**Impacted API endpoints:**
* [Create Retell LLM](/api-references/create-retell-llm), [Update Retell LLM](/api-references/update-retell-llm), [Create Conversation Flow](/api-references/create-conversation-flow), [Update Conversation Flow](/api-references/update-conversation-flow), [Create Conversation Flow Component](/api-references/create-conversation-flow-component), [Update Conversation Flow Component](/api-references/update-conversation-flow-component) — tool types: `check_availability_cal`, `book_appointment_cal`
[Read more](/deprecation-notice/2026/10-31_legacy_calcom_tools).
New API deprecation notice: Get agent versions endpoints.
Deprecation date: 09/15/2026.
See the migration notice for affected APIs and replacement guidance.
**Impacted API endpoints:**
* [Get Agent Versions](/api-references/get-agent-versions)
* [Get Chat Agent Versions](/api-references/get-chat-agent-versions)
[Read more](/deprecation-notice/2026/09-15_get_agent_versions).
The `override_dynamic_variables` input on Update Call is deprecated, and Update Call becomes ended-calls-only. Changes you make through Update Call — including `data_storage_setting`, which previously took effect mid-call — now apply only to ended calls. Use the new [Update Live Call](/api-references/update-live-call) API to override dynamic variables or change `data_storage_setting` on ongoing calls.
**Impacted API endpoints:**
* [Update Call](/api-references/update-call) — field: `override_dynamic_variables`
[Read more](/deprecation-notice/2026/08-31_update_call_ended_calls_only).
## Past deprecations
Already in effect. If your integration still uses anything below, it's already broken or running on migrated defaults.
The scalar `"multi"` value of the agent `language` field was removed in favor of the explicit locale-array form (for example, `["en-US","es-ES"]`). Existing agents were migrated automatically to the equivalent 10-language array, but client code that sends `"multi"` is now rejected.
**Impacted API endpoints:**
* [Create Agent](/api-references/create-agent), [Update Agent](/api-references/update-agent), [Create Chat Agent](/api-references/create-chat-agent), [Update Chat Agent](/api-references/update-chat-agent) — field: `language` (scalar value `"multi"`)
[Read more](/deprecation-notice/2026/07-31_legacy_multilingual_setting).
Legacy agent list endpoints were removed. Use the unified [List Agents](/api-references/list-agents) API and filter by `channel` for voice or chat agents.
**Impacted API endpoints:**
* [List Voice Agents](/api-references/list-agents), [List Chat Agents](/api-references/list-chat-agents) — endpoints replaced by `POST /v2/list-agents`
[Read more](/deprecation-notice/2026/07-31_agent_list_endpoints).
The legacy hosted MCP server at `retell.stlmcp.com` was removed. Point your MCP client to `mcp.retellai.com` instead — see the [Retell MCP server](/get-started/mcp-server) guide.
**Impacted:**
* `retell.stlmcp.com` — removed 07/20/2026. Use `mcp.retellai.com` (same Bearer API-key auth).
[Read more](/deprecation-notice/2026/07-20_legacy_mcp_server).
Legacy agent publish endpoints were removed. See the migration notice for affected APIs and replacement guidance.
**Impacted API endpoints:**
* `POST /publish-agent/{agent_id}`
* `POST /publish-chat-agent/{agent_id}`
[Read more](/deprecation-notice/2026/07-20_agent_version_endpoints).
ElevenLabs Turbo voice models were deprecated and automatically migrated to their Flash counterparts on 7/12/2026. We have confirmed with ElevenLabs and verified that Flash delivers the same voice quality at lower latency.
[Read more](/deprecation-notice/2026/07-12_elevenlabs_turbo_models).
Legacy list endpoints and analysis prompt fields were removed. Migrate to the versioned list endpoints (v2/v3) and to `post_call_analysis_data` / `post_chat_analysis_data` system presets.
**Impacted API endpoints:**
* [List Batch Tests](/api-references/list-batch-tests), [List Conversation Flow Components](/api-references/list-conversation-flow-components), [List Conversation Flows](/api-references/list-conversation-flows), [List Phone Numbers](/api-references/list-phone-numbers), [List Retell LLMs](/api-references/list-retell-llms), [List Test Case Definitions](/api-references/list-test-case-definitions), [List Test Runs](/api-references/list-test-runs), [List Calls](/api-references/list-calls), [List Chats](/api-references/list-chats) — endpoints replaced by versioned (v2/v3) equivalents
* [Create Agent](/api-references/create-agent), [Update Agent](/api-references/update-agent), [Create Chat Agent](/api-references/create-chat-agent), [Update Chat Agent](/api-references/update-chat-agent) — fields: `analysis_summary_prompt`, `analysis_successful_prompt`, `analysis_user_sentiment_prompt`
[Read more](/deprecation-notice/2026/06-15_legacy_list_endpoints).
Claude and Gemini models were deprecated and automatically migrated to newer versions on 5/25/2026.
[Read more](/deprecation-notice/2026/05-25_model_replacements).
The `tools` and `tool_ids` fields on `type: "conversation"` nodes are deprecated in favor of the `subagent` node type.
**Impacted API endpoints:**
* [Create Conversation Flow](/api-references/create-conversation-flow), [Update Conversation Flow](/api-references/update-conversation-flow), [Get Conversation Flow](/api-references/get-conversation-flow), [List Conversation Flows](/api-references/list-conversation-flows), [Create Conversation Flow Component](/api-references/create-conversation-flow-component), [Update Conversation Flow Component](/api-references/update-conversation-flow-component), [Get Conversation Flow Component](/api-references/get-conversation-flow-component), [List Conversation Flow Components](/api-references/list-conversation-flow-components) — fields: `tools`, `tool_ids` (on `type: "conversation"` nodes)
[Read more](/deprecation-notice/2026/04-18_conversation_node_tools).
OpenAI Realtime and Cartesia Sonic models were deprecated and replaced with newer versions. The deprecated models are no longer available.
[Read more](/deprecation-notice/2026/04-03_model_replacements).
The single-agent fields on phone number configuration are deprecated in favor of the weighted `*_agents` lists.
**Impacted API endpoints:**
* [Create Phone Number](/api-references/create-phone-number), [Import Phone Number](/api-references/import-phone-number), [Update Phone Number](/api-references/update-phone-number), [Get Phone Number](/api-references/get-phone-number), [List Phone Numbers](/api-references/list-phone-numbers) — fields: `inbound_agent_id`, `inbound_agent_version`, `outbound_agent_id`, `outbound_agent_version`, `inbound_sms_agent_id`, `inbound_sms_agent_version`, `outbound_sms_agent_id`, `outbound_sms_agent_version`
[Read more](/deprecation-notice/2026/03-31_phone_number_agent_fields).
`show_transferee_as_caller` no longer toggles SIP REFER vs SIP INVITE in Cold Transfer options. Use the `cold_transfer_mode` parameter instead; `show_transferee_as_caller` now only controls caller ID display under `sip_invite`.
**Impacted API endpoints:**
* [Create Retell LLM](/api-references/create-retell-llm), [Update Retell LLM](/api-references/update-retell-llm), [Create Conversation Flow](/api-references/create-conversation-flow), [Update Conversation Flow](/api-references/update-conversation-flow), [Create Conversation Flow Component](/api-references/create-conversation-flow-component), [Update Conversation Flow Component](/api-references/update-conversation-flow-component) — field: `show_transferee_as_caller`
[Read more](/deprecation-notice/2026/01-23_cold_transfer_mode_selection).
# Alert rules for call and chat metrics
Source: https://docs.retellai.com/features/alerting-overview
Create Retell AI alert rules that email or webhook you when call or chat volume, success rate, cost, latency, or API errors cross a threshold you set.
Alerting watches your call and chat metrics and notifies you the moment one crosses a threshold you set, so you catch a volume spike, a drop in success rate, or a wave of API errors without watching a dashboard. You define rules; Retell evaluates them on a schedule and sends an email or webhook when a rule triggers.
## When to use it
Reach for alerting when you need to catch a change in your call or chat operations fast, without a person watching for it. Common cases:
* **Catch outages early.** Alert when call or chat success rate drops or API errors climb, so you react before customers complain.
* **Detect volume anomalies.** Alert on an unexpected spike or drop in call or chat volume, or when concurrency approaches your limit.
* **Control spend.** Alert when total call or chat cost over a window exceeds a budget.
* **Watch quality.** Alert on rising negative sentiment or a growing count of calls that fail QA.
Alerting evaluates *aggregate* metrics over a time window. It isn't built for reacting to a single call or chat; for per-session automation, send a [webhook](/features/webhook-overview) from the agent instead. To explore the same metrics interactively, use the [analytics dashboard](/features/analytics-dashboard).
Alerting is a dashboard feature. Rules are created and managed in the dashboard, not through the public API.
### Example
A support team wants to know the instant call quality slips. They create a rule on **Call success rate**, set it to trigger when the rate **is below 90%** over the last **1 hour**, checked every **5 minutes**, and add a webhook that posts to their on-call Slack channel. When success rate dips, the on-call engineer is paged within minutes instead of hearing about it from customers.
## Create an alert rule
Open the **Alerting** tab in the dashboard and select **Create Alert**.
Give it a descriptive name, such as `Call success rate drop`. The name appears in every notification.
Choose the [metric](#metrics-you-can-monitor) to watch; the picker groups them into **API**, **Call**, **Chat**, and **QA**. Then pick a [threshold type](#thresholds): **Compare to certain value** (absolute) or **Compare to last cycle** (relative). Choose the comparator (for example, *is above* or *is below*) and enter the threshold value.
Under **Time Configuration**, set how often the rule runs (**Check every**, the frequency) and how far back each check looks (**for the last**, the window). See [Evaluation window and frequency](#evaluation-window-and-frequency) for the valid combinations.
Restrict the metric to specific agents, agent [environment tags](/agent/version#environment-tags), disconnection reasons, API error codes, a QA cohort, or Post Call Extraction fields. See [Filters](#filters).
Add one or more email addresses, webhook URLs, or both. At least one is required. Use **Test** next to a webhook URL to send a sample payload and confirm your endpoint accepts it before saving.
Select **Save**. The rule starts evaluating on its next scheduled check.
## Metrics you can monitor
The `metric_type` value is what appears in the webhook payload.
| Metric | `metric_type` | What it measures |
| ---------------------------- | ------------------------------- | ----------------------------------------------------------------------------- |
| Number of calls | `call_count` | Count of calls that ended within the window. |
| Concurrency used | `concurrency_used` | Peak number of concurrent calls during the window. |
| Call success rate | `call_success_rate` | Percentage of calls marked successful (0–100). |
| Negative sentiment rate | `negative_sentiment_rate` | Percentage of calls with negative user sentiment (0–100). |
| Custom function latency | `custom_function_latency` | Average response time of custom function calls, in milliseconds. |
| Custom function failures | `custom_function_failure_count` | Number of custom function calls that failed. |
| Transfer call failures | `transfer_call_failure_count` | Number of call transfers that failed. |
| QA not passed | `qa_not_passed_count` | Number of analyzed calls in a QA cohort that did not pass. Requires a cohort. |
| Total call cost | `total_call_cost` | Total cost of calls in the window, in USD. |
| API error count | `api_error_count` | Number of API requests that returned an error status code. |
| Number of chats | `chat_count` | Count of chats that ended within the window. |
| Chat success rate | `chat_success_rate` | Percentage of chats marked successful (0–100). |
| Chat negative sentiment rate | `chat_negative_sentiment_rate` | Percentage of chats with negative user sentiment (0–100). |
| Total chat cost | `total_chat_cost` | Total cost of chats in the window, in USD. |
The metric decides which channel the rule watches. Call metrics aggregate over voice calls; chat metrics aggregate over [chat agent](/build/create-chat-agent) sessions, counting each chat when it ends, the same way call metrics count a call.
## Thresholds
Every rule uses one of two threshold types.
### Compare to certain value (absolute)
Compares the metric against a fixed number.
**Example:** trigger when `Number of calls` **is above** `100` in the last hour.
### Compare to last cycle (relative)
Compares the percentage change from the previous window against your threshold. Use it to catch sudden spikes or drops regardless of the baseline.
**Example:** trigger when `Number of calls` **increases by more than** `50%` compared to the previous hour.
The percentage change is calculated as:
```
((currentValue - previousValue) / previousValue) * 100
```
If the previous window had zero activity and the current window has activity, the change is treated as an infinite increase and shown as `+∞%`. Rules that trigger on an increase (*increases by more than* or *increases by at least*) fire in this case.
## Evaluation window and frequency
The **window** is how far back each check looks when aggregating the metric. The **frequency** is how often the rule runs. A shorter window can only be paired with faster frequencies:
| Window | Supported frequencies |
| ---------- | ----------------------------- |
| 1 minute | 1 minute |
| 5 minutes | 1 minute, 5 minutes |
| 30 minutes | 5 minutes, 30 minutes |
| 1 hour | 5 minutes, 30 minutes, 1 hour |
| 12 hours | 30 minutes, 1 hour, 12 hours |
| 24 hours | 1 hour, 12 hours, 24 hours |
| 3 days | 12 hours, 24 hours |
| 7 days | 24 hours |
Pick a frequency that balances responsiveness against noise. Checking every minute catches issues fast but is more likely to trigger on a brief spike.
## Filters
Filters narrow what data feeds the metric. Which filters are available depends on the metric.
| Filter | Applies to | What it does |
| -------------------- | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Agents | All metrics except Concurrency used, API error count, and QA not passed | Restrict the metric to specific agents, and optionally specific agent versions. The picker matches the metric's channel: chat metrics list chat agents, call metrics list voice agents. |
| Environment tags | Same metrics as the Agents filter | Restrict the metric to calls or chats handled by versions carrying the selected [environment tags](/agent/version#environment-tags), such as `prod` or `staging`. Select tags in the **Tags** section of the same agent picker; a tag applies across all agents that use it. |
| Disconnection reason | Number of calls | Count only calls that ended for the selected reasons (for example, User Hangup or Dial No Answer). |
| API error code | API error count | Count only requests that returned the selected HTTP status codes: `400`, `401`, `402`, `403`, `404`, `409`, `422`, `429`, `500`. |
| QA cohort | QA not passed | Choose which [QA cohort](/ai-qa/create-cohort) to watch. Required for this metric. |
| Post Call Extraction | Same metrics as the Agents filter | Filter calls or chats by [Post Call Extraction](/features/post-call-analysis-overview) fields, such as a boolean field `appointment_booked` set to `false`. |
## Notifications
When a rule triggers, Retell notifies every channel you configured. When a later check finds the condition no longer met, the incident resolves and the same channels are notified again.
### Email
Each recipient gets an email whose subject is `[Retell Alert] `. The body includes the workspace, the metric, the value that triggered the alert, the threshold that was breached, the trigger time, and a link back to the dashboard. For most metrics it also links to the call history, chat history, or QA results behind the incident; Concurrency used and API error count have no drill-down. When the incident resolves, a follow-up email with subject `[Retell Alert Resolved] ` confirms it.
### Webhook
Each webhook URL receives a `POST` request. The body wraps the incident under an `alert` object:
```json theme={"dark"}
{
"event": "alert_triggered",
"alert": {
"alert_incident_id": "alert_incident_0b1c2d3e4f5g6h7i8j9k0l1m",
"org_id": "org_abc123",
"alert_rule_id": "alert_rule_xyz789abcdef012345678901",
"name": "High call volume",
"metric_type": "call_count",
"filter": {
"agent": [{ "agent_id": "agent_abc123", "version": [3] }]
},
"threshold_type": "absolute",
"threshold_value": 100,
"comparator": "gt",
"frequency": "5m",
"window": "1h",
"emails": ["oncall@yourcompany.com"],
"webhook_urls": ["https://yourcompany.com/hooks/retell-alerts"],
"current_value": 148,
"previous_value": null,
"triggered_timestamp": 1714608475945,
"resolved_timestamp": null
}
}
```
Key fields:
* `event` is `alert_triggered` when an incident opens. When it resolves, the same URLs receive a second request with `alert_resolved`.
* `filter` echoes the filters set on the rule. An environment-tag filter appears as `agent_tag`, for example `"agent_tag": { "type": "enum", "op": "in", "value": ["prod", "staging"] }`.
* `comparator` is one of `gt`, `lt`, `ge`, `le` (the symbol forms `>`, `<`, `>=`, `<=` are also accepted).
* `current_value` is the aggregated value that triggered the rule. `previous_value` is populated only for relative thresholds.
* For `total_call_cost` and `total_chat_cost`, `current_value`, `previous_value`, and `threshold_value` are in **cents** (the dashboard displays them as dollars).
* `triggered_timestamp` and `resolved_timestamp` are Unix timestamps in milliseconds. `resolved_timestamp` is `null` while the incident is active.
Each request carries an `X-Retell-Signature` header. Verify it before trusting the payload. Alert webhooks use the same HMAC-SHA256 scheme as other Retell webhooks, so the [webhook verification steps and code](/features/secure-webhook) apply here too. Retell sends each request once and waits up to **10 seconds** for a response; it does not retry, so acknowledge quickly and do heavy work asynchronously.
Always verify the webhook signature in production so you only act on requests that genuinely come from Retell.
## Alert history
Each time a rule's condition is met, Retell opens an **incident** and sends notifications. Incidents are listed under the **Alert History** tab, with the active period, the rule name, the condition, the value that triggered it, and the channels notified.
Only one incident stays active per rule at a time. A new incident opens only after the previous one resolves.
## Limits
As of July 2026:
* Up to **10 alert rules** per workspace. Creating an 11th returns a `400` error.
* Up to **100 agents** in a single rule's agent filter.
## FAQ
The system checks for due rules every minute and runs each one on its own configured frequency (1 minute to 24 hours). A rule with a 5-minute frequency is evaluated roughly every 5 minutes, not every minute.
No. A notification is sent once when a new incident opens, and once more when it resolves. You won't get repeat notifications while the condition persists.
Editing a rule resets its evaluation schedule and clears any active incident, so it starts evaluating fresh on the next check. This prevents a stale incident from lingering after you've changed the condition.
When you open the rule to edit it, Retell removes any agent that no longer exists from the Agents filter and shows a notice that some agents were removed. Select **Save** to keep the cleaned-up filter. The rule keeps evaluating on its remaining agents; a transient fetch error (not a deletion) leaves the agent in place so it isn't dropped by mistake.
Yes. Select multiple agents in the Agents filter and the metric is aggregated across all of them. Leave the filter empty to include every agent of the metric's channel. A single rule can't mix voice and chat agents; create one rule per channel instead.
Yes. Pick one of the chat metrics (Number of chats, Chat success rate, Chat negative sentiment rate, or Total chat cost) and the rule evaluates chats instead of calls. Filters work the same way: narrow by chat agent, environment tag, or Post Call Extraction fields.
# Analytics dashboard
Source: https://docs.retellai.com/features/analytics-dashboard
Build custom Retell analytics dashboards to track call and chat metrics like success rate, latency, cost, and concurrency, with charts, filters, and breakdowns.
The Analytics dashboard charts your call and chat data so you can see how your agents are performing over time. Open it from the **Analytics** tab, build the charts you care about, then filter and break them down without leaving the page.
## When to use it
Reach for the analytics dashboard when you want to see a pattern across many sessions rather than the detail of one:
* **Track performance over time.** Watch success rate, duration, or latency by day or week and see whether a prompt change moved them.
* **Compare agents or versions.** Break a metric down by agent or agent version to see which one performs better.
* **Watch spend.** Chart combined cost by agent or by day to see where your money goes.
* **Report on business outcomes.** Chart any [custom Post Call Extraction](/features/post-call-analysis-create) field, so "booked appointments" sits next to call volume.
It isn't the right tool for everything. To inspect one session's transcript or recording, use [call and chat history](/features/session-history). To be told when a metric crosses a line instead of checking for yourself, set up [alerting](/features/alerting-overview). To follow a call that's still running, use [live monitoring](/features/live-monitoring).
### Example
A clinic runs an appointment-reminder agent. They chart **Call Successful Rate** by day, break it down by agent version, and add a **Call Counts** chart filtered to the `voicemail_reached` disconnection reason. When a new version ships, the two charts show within a day whether more calls are landing in voicemail and whether the success rate followed.
## Create a dashboard
Each dashboard is scoped to either call data or chat data, and the scope is fixed once you create it. You can keep up to **10 dashboards** per workspace; creating an 11th fails with "Dashboard limit reached."
Open the overflow menu next to **Add Chart**, choose **Add Dashboard**, then pick **Call** or **Chat**.
Select the tab name and type a new one. Names are how you'll tell tabs apart, so prefer "Outbound sales" over "Dashboard 2".
Drag tabs to reorder them. **Duplicate** in the overflow menu copies a dashboard with all of its charts, which is the quickest way to build a variant. **Delete** appears once you have more than one dashboard.
## Add a chart
Select **Add Chart**.
Set **Graph Type** to column, bar, line, donut, or number. Line suits trends over time; number suits a single headline figure.
Choose a [metric](#metrics-you-can-chart) under **Call Source Metrics**, then how to measure it. The measurement list changes with the metric: counts offer only **count**, while durations and costs offer **avg**, **median**, **p90**, **sum**, **min**, and **max**. Use **+ Add** to plot more than one metric on the same chart.
Pick the chart's own range, or leave it on **Follow dashboard time range** so it moves with the picker at the top. The **Hour / Day / Week / Month** toggle sets the time bucket. Tick **Previous period comparison** to overlay the preceding window.
Add filters and up to five breakdowns that apply to this chart only. See [Filters and breakdowns](#filters-and-breakdowns).
Save the chart. It lands at the end of the dashboard, where you can drag it into place.
## Metrics you can chart
Which metrics you get depends on whether the dashboard is scoped to calls or chats.
### Call metrics
| Metric | Measurements | What it covers |
| ---------------------------- | ------------------------------- | ----------------------------------------------------------------------------------- |
| Call Counts | count | Number of calls. |
| Call Successful/Unsuccessful | count | Calls split by whether analysis marked them successful. |
| Call Status | count | Calls split by status. |
| Phone Inbound/Outbound | count | Calls split by direction. |
| Disconnection Reason | count | Calls split by why they ended. |
| User Sentiment | count | Calls split by the sentiment analysis assigned. |
| Call Successful Rate | avg | Share of calls marked successful. |
| Call Picked Up Rate | avg | Share of calls answered. |
| Call Transfer Rate | avg | Share of calls transferred. |
| Voicemail Rate | avg | Share of calls that reached [voicemail](/build/handle-voicemail). |
| Call Duration | avg, median, p90, sum, min, max | Length of calls. |
| End to End Latency | avg, median, p90, min, max | Per-call p50 [latency](/reliability/check-actual-latency), aggregated across calls. |
| Concurrency Used | max | Peak [concurrency](/deploy/concurrency) in the period. |
| Combined cost | avg, sum, median, p90, min, max | Total cost of the call. |
### Chat metrics
| Metric | Measurements | What it covers |
| ---------------------------- | ------------------------------- | ------------------------------------------------------- |
| Chat Counts | count | Number of chats. |
| Chat Successful/Unsuccessful | count | Chats split by whether analysis marked them successful. |
| Chat Status | count | Chats split by status. |
| User Sentiment | count | Chats split by the sentiment analysis assigned. |
| Chat Successful Rate | avg | Share of chats marked successful. |
| Combined cost | avg, sum, median, p90, min, max | Total cost of the chat. |
## Set the time range
The date range picker at the top of the page sets the window for every chart that follows the dashboard. Pick a preset (**Today**, **Last 7 days**, **Last 4 weeks**, **Last 3 months**, **Week to date**, **Month to date**, **Year to date**, **All time**) or select start and end dates on the calendar, then **Apply**. Each end of the range carries its own time and timezone, so buckets line up with your working day. Future dates are disabled.
A chart can override that window with its own range: **Today**, **Last 1 week**, **Last 4 weeks**, **Last 3 months**, **Week to date**, **Month to date**, **Year to date**, or **All time**.
## Filters and breakdowns
Filters and breakdowns set in the header apply to every chart on the dashboard. Charts can add their own on top.
The filter list is split across four tabs. **Base** holds the built-in fields; **Post Call Extraction**, **Metadata**, and **Dynamic Variables** hold whatever your agents produce.
On a call dashboard, Base offers agent, call ID, batch call ID, type, duration, from number, to number, user sentiment, disconnection reason, call status, call successful, end-to-end latency, and combined cost. A chat dashboard offers agent, chat ID, chat status, chat successful, combined cost, and user sentiment.
**Breakdowns** group every chart by one or more dimensions: agent, agent version, disconnection reason, call status, call successful, call type, and Post Call Extraction fields. Chat dashboards offer agent, agent version, disconnection reason, chat status, chat successful, and Post Chat Extraction fields.
Filter and breakdown changes aren't saved for you. When you change one, **Save** and **Cancel** replace the usual controls in the header: save to keep the state for next time, cancel to revert to the last saved state. Moving and resizing charts saves on its own.
### Per-chart filters and breakdowns
Each chart can carry its own filters and up to five breakdowns, set in the chart editor. Chart filters are merged with the dashboard filters rather than replacing them, so both apply. Use them to narrow one chart without touching the rest of the dashboard.
Any field from your [custom Post Call Extraction](/features/post-call-analysis-create) can drive a chart, which is how you track business-specific outcomes next to the standard metrics.
## Chart types and layout
Five chart types are available: **column** for comparing categories, **bar** for the same comparison horizontally, **line** for trends over time, **donut** for proportions, and **number** for a single figure.
Arrange charts directly on the dashboard, without opening the editor:
* Drag a chart to move it within a row or into another row. A row holds up to five charts.
* Drag the divider between two charts to change their widths.
* Drag the handle at the bottom of a row to change its height.
* Drag a chart into empty space to start a new row.
## Who can use it
Viewing and editing are separate permissions. With view access you can read dashboards and adjust filters for your own session; without edit access, the **Add Chart** and **Save** controls don't appear. See [Access Control](/accounts/access-control) for how roles map to permissions.
## FAQ
Charts read your call and chat records directly, so a session shows up once it has ended and its analysis has finished. There's no scheduled refresh to wait for.
A chart renders at most 3,000 points. Narrow the time range, use a coarser **View by** unit, or drop a breakdown to bring the count down.
Expand the chart to see its breakdown, then open the matching sessions in [call history](/features/session-history).
No. The two are merged, so a chart filtered to one agent inside a dashboard filtered to last week shows that agent, last week.
No. Scope is set when you create the dashboard and can't be changed afterwards. Create a second dashboard for the other channel.
Not at the moment. Dashboards are built and read in the dashboard UI. To pull the underlying data out instead, use the [list calls](/api-references/list-calls) and [list chats](/api-references/list-chats) endpoints.
# Contacts
Source: https://docs.retellai.com/features/contacts
Manage the people your Retell agents call and text: contact records keyed by phone number, custom contact fields, post-call data mappings, and CRM sync.
Contacts is your workspace's record of the people your agents talk to. Each contact is keyed by phone number and collects the conversations you've had with that person, along with any fields you choose to store about them. Those fields reach the agent as [dynamic variables](/build/dynamic-variables) on the next phone call, so it can open with what it already knows instead of asking again.
Open it from the **Contacts** tab under **Data** in the dashboard.
## When to use it
* **Your CRM is the source of truth.** Sync contacts in from [your CRM](/integrations/crm-overview) and map their fields to agent variables once, instead of passing the same values on every API call.
* **Conversations span more than one call.** When a long call drops or the person calls back, the agent picks up with the context from last time rather than restarting discovery.
* **You run follow-up or chase sequences.** The next call can act on what the last one produced.
* **You want one set of fields across agents.** Post-call data mappings are set per workspace, not per agent, so every agent writes to the same contact fields.
### Example
A tutoring company runs 20-minute enrollment calls. They store `program_interest`, `budget_range`, and `last_topic_discussed` as contact fields, filled from [Post Call Extraction](/features/post-call-analysis-overview). When a prospect calls back after a dropped call, the agent already has all three and resumes at the pricing conversation instead of starting over.
## How contacts are created
Contacts arrive three ways, and all of them match on phone number:
* **Automatically, after a conversation.** When a phone call or SMS chat ends, Retell looks up the number (the caller's number on inbound, the number you dialed on outbound). If no contact matches, it creates one, then updates the contact's conversation count and last-conversation time. Web calls and web chats have no phone number, so they don't create contacts.
* **Manually**, from **Actions → Add contact**.
* **From your CRM**, if you've connected [a CRM integration](/integrations/crm-overview) and configured inbound sync.
A phone number identifies exactly one contact. Adding a contact whose number already exists fails with "A contact with phone number +14155551000 already exists."
## Add a contact
Select **Actions → Add contact**.
Required, and it has to be a valid number. Use E.164 format, for example `+14155552671`. This is the key everything else matches on.
First name, last name, and **Do not call** are optional, as are any custom fields you've defined. Custom field values are checked against the field's type, so a number field rejects text and an enum field rejects values outside its options.
Select **Create**. The contact appears in the table right away, with no conversations attached until one happens.
## Work with the contacts table
The table lists every contact in the workspace with these built-in columns, plus one for each custom field you've defined:
| Column | What it shows |
| ---------------------- | ------------------------------------------------------------------------ |
| Phone Number | The contact's number, and the key used to match conversations. |
| First Name / Last Name | Set manually, synced from your CRM, or written by Post Call Extraction. |
| Contact ID | The record's identifier, for example `contact_9f2c41ab77de05c3aa61e480`. |
| Related Conversations | How many calls and chats are attached to this contact. |
| Latest Conversation | When you last spoke to them. |
| Do Not Call | A flag you can set, filter on, and sync with your CRM. |
| External ID | The record ID in the connected CRM, when the contact came from one. |
The settings icon above the table, labelled **Manage table**, controls which columns appear and in what order. The arrangement is saved for the workspace, so everyone sees the same table.
**Search** matches phone number, first name, last name, external ID, and custom field values.
**Filters** narrow by phone number, external ID (including whether one exists at all), do-not-call, last conversation time, and any custom field.
## Open a contact
Selecting a row opens a panel with two halves:
* **Contact information** lists every field on the record and lets you edit them in place. Saving an edit on a CRM-linked contact also pushes the change back to your CRM, if outbound sync is configured.
* **Conversations** lists the calls and chats attached to that number, newest first, with total time and a split of inbound calls, outbound calls, and messages. Selecting one opens the full session.
Use the up and down arrow keys to step through contacts without closing the panel.
## Define contact fields
Beyond the built-in fields, you can define custom fields to store anything else you want to keep about a person. Open **Actions → Manage contact fields**.
The fields table shows, for each field, its type, the CRM field it syncs with, the Post Call Extraction field that writes to it, and when it last changed.
Select **Add Contact Field**, then pick a **type**: Text, Number, Boolean, Selector, Date, or Datetime. Type is fixed after creation, so choose deliberately.
Field names are snake\_case: lowercase letters, numbers, and underscores, starting with a letter. Names can't collide with the built-in fields, can't start with `contact` or `external` (both reserved), and can't repeat an existing field.
The description tells you what the field captures. It does more than document: for fields filled by Post Call Extraction with **Accumulate & summarize**, the description is the instruction the model follows when combining an old value with a new one.
Turn on the post-call mapping, choose the analysis field that feeds it, and pick how new values combine with existing ones:
* **Overwrite** replaces the stored value every time.
* **Fill only if empty** writes once and leaves it alone after that.
* **Accumulate & summarize** merges the old and new values with a model, following your field description.
Mappings apply to the whole workspace, so any agent producing that analysis field writes to this contact field. See [CRM data mappings](/integrations/crm-mappings) for the full picture, and [contact memory](/integrations/build-contact-memory) for building running context this way.
Built-in fields behave differently from custom ones. First name, last name, and do-not-call can be edited and mapped, but not deleted. Phone number is fixed. Custom fields can be edited or deleted.
## Use contact data in a conversation
When a phone call starts, Retell looks up the contact by number and passes its fields to the agent as dynamic variables: `first_name`, `last_name`, `do_not_call`, and one per custom field, each named after the field. Reference them in the prompt the same way as any other [dynamic variable](/build/dynamic-variables), for example `{{first_name}}`.
This applies to inbound calls, outbound calls, and [batch calls](/deploy/make-batch-call). Chats update contacts after they end, but don't receive contact fields as variables. If no contact matches the number, the variables are simply absent, so write prompts that read naturally without them.
## Backfill fields from past conversations
New mappings only apply going forward. To apply one to conversations that already happened, select **Actions → Backfill from Post-Call Data**, choose which fields to fill, and scope the run by agent and call time.
A few things to know before you start:
* You need at least one post-call data mapping configured, or the run is rejected.
* One backfill runs at a time per workspace. Starting a second returns "A backfill job is already running."
* Progress shows as a banner on the Contacts page while the job runs.
## Keep contacts in sync with your CRM
With [a CRM connected](/integrations/crm-overview), contacts sync both directions: CRM records flow in as contacts, and edits flow back out. Two entries in the **Actions** menu control it, both disabled until a CRM is connected:
* **Manage CRM sync** opens the field mapping settings. See [CRM data mappings](/integrations/crm-mappings).
* **Run full sync** re-imports the full contact set instead of waiting for the next scheduled sync.
While two-way sync is active, contacts that came from the CRM are locked against deletion, so the CRM stays the source of truth for those records.
## Who can use it
Contacts needs CRM view permission to open, and CRM edit permission for anything that changes data: adding contacts, editing fields, running a backfill, or triggering a full sync. Without edit permission the controls are visible but disabled. See [Access Control](/accounts/access-control).
Contacts are managed in the dashboard. There's no public API for creating or listing them at the moment.
## FAQ
No. Contacts come from CRM sync, from adding them in the dashboard, or automatically after a conversation. If your contacts live in a CRM, [connect the CRM](/integrations/crm-overview) and let inbound sync bring them in.
Ending a phone call or SMS chat with a number that has no contact creates one. That's what links a conversation to a person and lets the next call reuse what the last one learned.
No. The number is the identifier, and creating a duplicate is rejected.
Conversations attach by phone number, so check that the number on the call matches the contact exactly. Web calls and web chats carry no phone number and never attach to a contact.
The dashboard doesn't offer contact deletion today. You can clear a contact's field values by editing it, and you can delete custom contact fields from the contact fields page. Contacts synced from a CRM are locked against deletion while two-way sync is active.
The contacts stay. They keep their fields and their conversation history, and the External ID showing where they came from. Syncing stops, so later changes on either side no longer cross over.
# Inbound call & SMS webhook
Source: https://docs.retellai.com/features/inbound-call-webhook
Use the Retell inbound webhook to dynamically pick the agent, set dynamic variables, reject calls, and pass per-call context for inbound phone and SMS.
You often want to use different agents under the same number, or provide caller-specific context for a call. For outbound calls and chats, you supply that call-specific information in the API request. For inbound calls or SMS, you don't initiate the request, so you need a way to be notified when one arrives and then process it.
The inbound webhook handles this. Once set up, you can override the agent ID, set dynamic variables, and set other per-call fields, then process the call or SMS. It's part of your [number configuration](/deploy/inbound-call) and works for numbers you've purchased or imported. It applies to inbound [phone calls](/deploy/inbound-call) and inbound [SMS](/deploy/enable-sms).
This webhook does not apply to [dial to sip calls](/deploy/custom-telephony#method-2-dial-to-sip-uri), since you provide call-specific information when you register the phone call.
## Where to set the inbound webhook URL
The inbound webhook is configured **per phone number** (not at the account or agent level, which are for [call event webhooks](/features/register-webhook)):
1. Open the Retell dashboard and go to the **Phone Numbers** page.
2. Click the number you want to configure.
3. In the number's settings, set the **Inbound Webhook URL** field to your endpoint.
4. Save the changes.
Retell will `POST` the payload described below to that URL whenever an inbound call or SMS arrives on that number.
## Use cases
* Filter and reject unwanted inbound calls / SMS
* Add context (dynamic variables, metadata) to inbound calls / SMS
* Override agent id / version / specific agent settings for inbound calls / SMS
* Pause the call / SMS to pick it up with some delay
* Internal system records of the inbound call / SMS
## Webhook spec
The webhook `POST`s the payload to your endpoint with a 10-second timeout. If no success status (2xx) is received within 10 seconds, the webhook is retried up to 3 times.
Verify the webhook using your Retell API key to confirm it comes from Retell AI. Read more at [Secure the webhook](/features/secure-webhook).
### Request payload
These fields might be provided in the payload depending on your configuration:
* `call_id` (inbound call only): use `event` + `call_inbound.call_id` to deduplicate HTTP retries.
* `chat_id` (inbound SMS only): use `event` + `chat_inbound.chat_id` to deduplicate HTTP retries.
* `agent_id`: if the number has inbound agent id set, you will see it in payload
* `agent_version`: if the number has inbound agent version set, you will see it in payload
* `from_number`: this will always show up in payload, helps you identify the caller and process the call / SMS accordingly
* `to_number`: this will always show up in payload, helps you identify the receiver and process the call / SMS accordingly
* `custom_sip_headers` (inbound call only): an object containing custom SIP headers extracted from the inbound `INVITE`. Only headers whose name starts with `X-` (case-insensitive), plus the allowlisted headers `User-to-User`, `Diversion`, `History-Info`, and `P-Asserted-Identity`, are forwarded. Header names are emitted in lowercase. The object may be empty or omitted when no qualifying headers are present, so treat it as optional. Use this to pass per-call context from your SIP trunk into the webhook (for example, to programmatically manage sessions).
Note that the call / SMS is not connected, and a call / SMS object is not yet created (and if you decided not to take the call for example, the call object will not be created). Therefore you will not have a call / SMS object inside the payload, but it includes a preallocated `call_inbound.call_id` or `chat_inbound.chat_id`, which stays the same across HTTP retries. If the call / chat object is subsequently created, it will use this same ID.
Here's a sample payload for inbound call:
```json Inbound Call theme={"dark"}
{
"event": "call_inbound",
"event_timestamp": 1780012672105,
"call_inbound": {
"call_id": "call_12345",
"agent_id": "agent_12345",
"agent_version": 1,
"from_number": "+12137771234",
"to_number": "+12137771235",
"custom_sip_headers": {
"x-my-header": "my-value",
"user-to-user": "616263;encoding=hex"
}
}
}
```
```json Inbound SMS theme={"dark"}
{
"event": "chat_inbound",
"chat_inbound": {
"chat_id": "chat_12345",
"agent_id": "agent_12345",
"agent_version": 1,
"from_number": "+12137771234",
"to_number": "+12137771235"
}
}
```
### Response
We expect a JSON response with a successful status code (2xx) with fields grouped under `call_inbound` or `chat_inbound`. Here are the allowed fields (all of them are optional):
* `reject`: set to `true` to decline this inbound call / SMS. See [Reject an inbound call or SMS](#reject-an-inbound-call-or-sms) below.
* `override_agent_id`: if you want to override the agent id, you can set it here
* `override_agent_version`: if you want to override the agent version, you can set it here
* `dynamic_variables`: if you want to set dynamic variables for this inbound call, you can set it here
* `metadata`: if you want to set metadata for this inbound call, you can set it here
* `agent_override`: if you want to override the agent settings.
#### Agent override
You can also override per-call / per-chat agent behavior without modifying the saved agent by returning an `agent_override` object. The override applies only for this session.
Supported groups:
* `agent`: Partial Agent settings (voice agents). Useful fields include `voice_id`, `voice_model`, `fallback_voice_ids`, `voice_temperature`, `voice_speed`, `volume`, `language`, `pronunciation_dictionary`, `boosted_keywords`, `stt_mode`, `vocab_specialization`, `denoising_mode`, `responsiveness`, `interruption_sensitivity`, `enable_backchannel`, `backchannel_frequency`, `backchannel_words`, `end_call_after_silence_ms`, `max_call_duration_ms`, `begin_message_delay_ms`, `ring_duration_ms`, `reminder_trigger_ms`, `reminder_max_count`, `ambient_sound`, `ambient_sound_volume`, `allow_user_dtmf`, `user_dtmf_options`, `voicemail_option`, `webhook_url`, `webhook_timeout_ms`, `data_storage_setting`, `opt_in_signed_url`, `pii_config`, `post_call_analysis_data`, `post_call_analysis_model`.
* `retell_llm`: Partial Retell LLM settings. Supported keys include `model`, `s2s_model`, `model_temperature`, `knowledge_base_ids`, `kb_config`, `start_speaker`, `begin_after_user_silence_ms`, `begin_message`.
* `conversation_flow`: Partial Conversation Flow settings. Supported keys include `model_choice`, `model_temperature`, `knowledge_base_ids`, `kb_config`, `start_speaker`, `begin_after_user_silence_ms`, `begin_message`.
Notes:
* If both `override_agent_id`/`override_agent_version` and `agent_override` are provided, we first resolve the target agent by id/version, then apply `agent_override` on top for this call.
* Overrides must satisfy the same validation rules as agent creation (e.g. voice/language compatibility, value ranges). Invalid overrides may cause the call to be rejected.
* Overrides do not persist back to the saved agent.
Here's a sample response for inbound call, for inbound SMS, simply replace `call_inbound` with `chat_inbound`:
```json theme={"dark"}
{
"call_inbound": {
"override_agent_id": "agent_12345",
"override_agent_version": 1,
"agent_override": {
"agent": {
"voice_id": "11labs-Adrian",
"voice_temperature": 0.6,
"interruption_sensitivity": 0.8,
"max_call_duration_ms": 1800000
},
"retell_llm": {
"model": "gpt-4o-mini",
"model_temperature": 0.2,
"knowledge_base_ids": ["kb_abc123"],
"start_speaker": "agent",
"begin_message": "Hi {{customer_name}}, thanks for calling."
}
},
"dynamic_variables": {
"customer_name": "John Doe"
},
"metadata": {
"random_id": "12345"
}
}
}
```
### Reject an inbound call or SMS
To decline an inbound call or SMS, return `reject: true` in the response. This works whether or not the number has an inbound agent set, so you can keep a default agent configured and reject only the calls you don't want.
```json theme={"dark"}
{
"call_inbound": {
"reject": true
}
}
```
Behavior:
* Only the boolean `true` rejects. Any other value (including the strings `"true"` / `"false"`, `1`, or omitting the field) is ignored and the call / SMS proceeds as normal.
* Rejecting takes priority over agent selection — `override_agent_id` and `agent_override` are ignored when `reject` is `true`. We recommend returning only `{ "reject": true }` when declining: other fields such as `dynamic_variables` and `metadata` are still validated, and an invalid value there is treated as a bad response, in which case `reject` is not applied.
* For calls, Retell hangs up before any agent or voice setup. No call object is created.
* For SMS, Retell drops the message without replying. No chat object is created.
Use this to filter unwanted numbers, block traffic outside business hours, or turn away callers you can't serve.
## FAQ
The call would continue to stay in ringing state.
The SMS will not get a reply.
It would get retried up to 3 times. If all of those attempts fail, it will check whether this number has an inbound agent id set. If it does, it will then try to connect the call to that agent. If not, it will then disconnect the call.
Yes. Check `from_number` in the webhook request body, and respond with `reject: true` for the numbers you want to turn away. See [Reject an inbound call or SMS](#reject-an-inbound-call-or-sms). Unlike the older approach of omitting `override_agent_id`, `reject` works even when the number has an inbound agent set, so you can serve most callers with your default agent and decline only the ones you don't want.
# Monitor live calls
Source: https://docs.retellai.com/features/live-monitoring
Watch ongoing Retell calls in real time from the dashboard: follow the live transcript, listen in silently, take over from the agent, or end a call.
Live Monitoring shows the calls happening in your workspace right now. Open any active call to follow its transcript as the conversation unfolds, listen in silently, take over from the agent, or end the call.
[Call & chat history](/features/session-history) and [Post Call Extraction](/features/post-call-analysis-overview) work after a call ends. Live Monitoring is the view for a call that's still in progress.
## When to use it
Reach for Live Monitoring when you need eyes or ears on a call before it ends:
* **Supervise agents and QA in real time.** [Listen silently](#live-listen) to live calls to judge quality, tone, and where the agent needs coaching. The caller and the agent never know you're there.
* **Rescue a call.** When the agent is stuck or the caller asks for a person, [take over](#take-over) and speak with the caller yourself.
* **Spot-check compliance.** Audit sensitive or high-stakes conversations as they happen, instead of catching problems only in review.
For example, a support lead keeps Live Monitoring open during a hiring campaign's busy hours. When a candidate-screening call stalls (the agent keeps re-asking the same question), the lead opens the call, confirms the loop in the live transcript, and takes over to finish the screening in person.
Live Monitoring isn't built for after-the-fact review. To search and filter finished calls, use [call & chat history](/features/session-history); for automatic scoring and structured data on completed calls, use [Post Call Extraction](/features/post-call-analysis-overview).
## Access Live Monitoring
1. Go to the dashboard
2. Select **Live Monitoring** in the navigation
Live Monitoring is available to the **Admin** and **Developer** roles, and to any custom role granted call management (write) permission. The **Member** role can't access it. If you don't see Live Monitoring in the navigation, ask your workspace admin to update your role — see [Access Control](/accounts/access-control).
If an agent has [PII scrubbing](/accounts/privacy-disable) enabled, monitoring its live calls requires permission to view raw data, which the **Admin** and **Developer** roles have. Scrubbing runs only after a call ends, so no redacted transcript exists mid-call — users without raw-data access can't monitor, listen to, or take over those calls.
## Live call list
The main view lists every call in progress across your workspace, newest first. It covers phone calls (inbound and outbound) and web calls.
Each row shows the start **Time**, a live-ticking **Call Duration**, the **Type** (Phone Inbound, Phone Outbound, or Web), the **From** and **To** numbers (a dash for web calls), and the **Call ID** (with a copy button on hover).
* **Updates on its own.** The list refreshes about every 4 seconds: new calls appear as they start, briefly highlighted, and drop off as they end.
* **Keyboard navigation.** With a call selected, use the up and down arrow keys to move through the list.
When nothing is active, the view shows **No Ongoing Calls**. Select any call to open it.
## Follow a live transcript
Opening a call shows its details and a **live transcript** that streams in over the call as it happens.
Above the transcript you'll see the start time, call type, the agent handling the call, the Call ID, and the from/to numbers for phone calls. The transcript itself renders:
* **Agent and caller turns** as chat bubbles.
* **Tool calls** — each function the agent invokes, with its arguments and result in an expandable entry marked *Tool call succeeded* or *Tool call failed*.
* **Node transitions** — each step a flow-based agent moves into.
* **Keypad input** — DTMF digits the caller presses.
* **SMS messages** the caller sends during the call.
The view follows the newest message automatically. Scroll up to read earlier turns, and a **Jump to latest** control appears to snap back to the bottom.
To stream the same transcript into your own tools, connect to the [monitor call WebSocket](/api-references/monitor-call-websocket) with your API key.
## Listen, take over, or end a call
When you have call management permission, three actions sit below the transcript.
### Live Listen
Select **Live Listen** to hear the call audio in real time. Listening is silent and hidden: neither the caller nor the agent is notified, and your microphone is never transmitted. A **You're listening live** indicator shows while you're connected. Select **Exit Live Listen** to stop. Any number of people can listen to the same call at once.
### Take Over
Select **Take Over** to leave the agent behind and speak with the caller yourself. You confirm in a dialog (*"This permanently stops the AI agent and connects you directly to the caller. It can't be undone."*), then your browser prompts for microphone access.
Taking over **permanently stops the agent** for that call. The agent does not resume, the live transcript pauses (*"Transcription is paused while you're on the call"*), and the call ends when either side hangs up or you end it. This can't be undone.
* Retell asks for your microphone **before** it stops the agent, so if you deny the prompt the take-over is canceled and the agent keeps handling the call.
* Only **one person** can take over a given call. If someone else already has, you'll see *"Call already taken over by another participant."*
* Works for both phone and web calls.
* While you're on the call, leaving the view or closing the tab warns you first, since leaving ends the call for the caller.
### End Call
Select **End Call** to disconnect the caller and end the call for everyone. You confirm first (*"This disconnects the caller and ends the call for everyone."*). It's available whether or not you've taken over.
## FAQ
Live Monitoring is available to the **Admin** and **Developer** roles, and to any custom role granted call management (write) permission — the **Member** role can't access it. Contact your workspace admin if you need access. See [Access Control](/accounts/access-control) for details.
No. Live Listen is silent and hidden: neither the caller nor the agent is notified, and your microphone is never transmitted until you explicitly take over.
Up to **5 connections** can follow the same call's live transcript at the same time, counting both dashboard viewers and [monitor call WebSocket](/api-references/monitor-call-websocket) connections; a sixth is turned away with a *"max watchers reached"* message until a slot frees up. Any number of people can **Live Listen** to the audio. Only **one** person can **Take Over** a call.
If the agent has [PII scrubbing](/accounts/privacy-disable) enabled, monitoring, listening, and taking over require permission to view raw data — held by the **Admin** and **Developer** roles. Scrubbing runs only after the call ends, so there's no redacted transcript to show mid-call, and users without raw-data access are blocked from those live calls.
The agent is permanently stopped for that call. You're connected directly to the caller, and the agent does not resume. The call ends normally when either side hangs up or you end it.
The take-over is canceled and the agent keeps handling the call. Grant microphone access and try again to take over.
Yes. The [monitor call WebSocket](/api-references/monitor-call-websocket) streams the same transcript, tool calls, and node transitions to your server while the call is ongoing. Live Listen and Take Over are dashboard only; to end a call from the API, use [Stop Call](/api-references/stop-call).
No. Whispering or coaching the agent mid-call — sending guidance only the agent can hear — isn't supported. During a live call you can **Live Listen**, **Take Over**, or **End Call**.
# Consume the analysis data
Source: https://docs.retellai.com/features/post-call-analysis-consumption
Read Retell Post Call Extraction results from the dashboard, the call_analyzed webhook, or the Get Call API, and feed them into your CRM or reports.
We will not populate custom Post Call Extraction fields for calls that were not connected or where no conversation took place. Please check whether the field exists before using it.
After a call is analyzed, you can access the analysis results through three different methods:
1. Dashboard - Visual interface for quick access and review
2. Webhook - Real-time notifications with analysis results
3. API - Programmatic access to call analysis data
### Method 1: Dashboard
Access your analysis results directly through the dashboard's history tab. This provides a user-friendly interface to:
* View all analyzed conversations
* Filter and search through analysis results
Your defined analysis categories appear in the "Conversation Analysis" column, allowing for quick insights into each conversation.
### Method 2: Webhook
Receive real-time notifications when call analysis is complete. The webhook payload includes:
```json theme={"dark"}
{
"event": "call_analyzed",
"call": {
// Call object with call_analysis field populated
}
}
```
Analysis data lives on the `call_analysis` object, and the `call_ended` webhook payload excludes `call_analysis`. Listen to `call_analyzed` (not `call_ended`) to read fields like `call_summary`, `user_sentiment`, `call_successful`, and any custom extracted fields. Reading them from a `call_ended` payload returns `null` or `undefined`. See [Webhook overview](/features/webhook-overview) for the payload difference between events.
To set up webhooks, visit the [Webhook Configuration](/features/webhook-overview) section.
### Method 3: Get Call API
Retrieve analysis results programmatically using the [Get Call API](/api-references/get-call).
**Example Response:**
```json theme={"dark"}
{
"call_id": "123",
"call_analysis": {
// Analysis results object
}
}
```
For detailed API documentation and response schemas, refer to the [API Reference](/api-references/get-call).
### Video: Add Post Call Extraction to Excel Using Make.com
See community templates in [docs](https://docs.google.com/document/d/1hx6hdTEjAR4y4xXZ7RLMH2byQNVW1ABxC8S4FwvTx_Y/edit?tab=t.0#heading=h.wf5bktkelope).
# Define the information you want to extract
Source: https://docs.retellai.com/features/post-call-analysis-create
Define Retell Post Call Extraction fields with boolean, selector, text, and number categories to automatically extract structured data from every call.
Go to the agent detail page and click on the "Post Call Extraction" tab.
Select the type of analysis that best fits your needs:
### Boolean Analysis
Use for simple yes/no determinations
Example configuration:
* **Name**: user\_reached
* **Description**: Was the user reached or not? Set to false if voicemail is detected, if you are only asked for the reason of the call, or if you are only asked to leave a message. Otherwise, set to true.
### Text Analysis
Use for extracting detailed textual information
Example configuration:
* **Name**: detailed\_call\_summary
* **Description**: Provide a detailed summary of the call so that when the call is transferred, the new human agent has the full context
* **Format Example**: "Customer called about billing issue. Resolved by explaining the recent price changes. Follow-up needed in 2 weeks."
### Number Analysis
Use for extracting numerical values
Example configuration:
* **Name**: purchase\_intent\_amount
* **Description**: Extract the dollar amount the customer is interested in spending
### Selector Analysis
Use for categorizing from predefined options
Example configuration:
* **Name**: issue\_category
* **Description**: Categorize the main reason for the call.
* **Choices Example**: \["Technical Support", "Billing Question", "Sales Inquiry", "Product Information"]
Please note that you should write the explanation of the choices inside the description field. The choices should only contain the individual choice.
# Post Call Extraction overview and categories
Source: https://docs.retellai.com/features/post-call-analysis-overview
Retell Post Call Extraction automatically scores and structures customer calls after they end — built-in categories plus custom analysis for your workflow.
Post Call Extraction runs after a call ends. It reads the transcript with an LLM and writes structured results onto the call's `call_analysis` object, so you can filter, report on, or sync insights to your CRM without manually reviewing each conversation.
## How it works
1. The call ends and Retell fires the `call_ended` webhook.
2. Retell analyzes the transcript against your agent's Post Call Extraction configuration.
3. Retell fires the `call_analyzed` webhook. The `call_analysis` object is now populated on the call.
Because analysis runs after `call_ended`, the `call_ended` payload does not include `call_analysis`. Read results from the `call_analyzed` webhook or the [Get Call API](/api-references/get-call). See [Consume the analysis data](/features/post-call-analysis-consumption).
Retell does not populate custom Post Call Extraction fields for calls that were not connected or where no conversation took place. Check whether the field exists before using it.
## Built-in fields
Every call includes these built-in analysis fields. You can customize the prompts behind `call_summary` and `call_successful` and [rerun analysis](/features/rerun-call-analysis) on past calls.
* `call_summary` — high-level summary of the conversation.
* `user_sentiment` — the caller's overall sentiment.
* `call_successful` — whether the call met the success criteria you define.
* `in_voicemail` — whether the agent reached a voicemail instead of a live person.
## Custom analysis categories
Define your own fields on the agent's **Post-Call Data Extraction** tab. Each field has a name, a description that tells the LLM what to extract, and one of four types:
| Type | Returns | Use for |
| ------------ | --------------------------- | ------------------------------------------------------------------------ |
| **Boolean** | `true` / `false` | Yes/no determinations, like whether the customer is a first-time caller. |
| **Text** | String | Free-form output, like a custom summary or extracted action items. |
| **Number** | Numeric value | Quantitative values, like a transaction amount or satisfaction score. |
| **Selector** | One value from a fixed list | Categorization, like issue type or resolution status. |
To set up your first custom field, see [Define the information you want to extract](/features/post-call-analysis-create).
## Next steps
* [Define the information you want to extract](/features/post-call-analysis-create) — create custom fields on your agent.
* [Consume the analysis data](/features/post-call-analysis-consumption) — read results from the dashboard, `call_analyzed` webhook, or Get Call API.
* [Rerun Post Call Extraction](/features/rerun-call-analysis) — regenerate results after editing your prompts.
To follow or step into calls while they're still in progress, see [Live monitoring](/features/live-monitoring).
To score call quality across a sampled set of calls (hallucinations, resolution rate, latency, and more), see [AI Quality Assurance](/ai-qa/overview). You can also use Post Call Extraction fields as filters when [defining a QA cohort](/ai-qa/create-cohort).
# Register and set up a Retell webhook
Source: https://docs.retellai.com/features/register-webhook
Register a Retell webhook to receive real-time call_started, call_ended, and call_analyzed events, with Node.js, Python, and signature verification code.
Set up an HTTP or HTTPS endpoint function that can accept webhook requests with a POST method.
Example endpoint:
```typescript Node.js theme={"dark"}
// install the sdk: https://docs.retellai.com/get-started/sdk
import { Retell } from "retell-sdk";
import express, { Request, Response } from "express";
const app = express();
app.use(express.json());
app.post("/webhook", (req: Request, res: Response) => {
const {event, call} = req.body;
switch (event) {
case "call_started":
console.log("Call started event received", call.call_id);
break;
case "call_ended":
console.log("Call ended event received", call.call_id);
break;
case "call_analyzed":
console.log("Call analyzed event received", call.call_id);
break;
default:
console.log("Received an unknown event:", event);
}
// Acknowledge the receipt of the event
res.status(204).send();
});
```
```Python Python theme={"dark"}
# Install the SDK: https://docs.retellai.com/get-started/sdk
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from retell import Retell
retell = Retell(api_key=os.environ["RETELL_API_KEY"])
@app.post("/webhook")
async def handle_webhook(request: Request):
try:
post_data = await request.json()
if post_data["event"] == "call_started":
print("Call started event", post_data["call"]["call_id"])
elif post_data["event"] == "call_ended":
print("Call ended event", post_data["call"]["call_id"])
elif post_data["event"] == "call_analyzed":
print("Call analyzed event", post_data["call"]["call_id"])
else:
print("Unknown event", post_data["event"])
return JSONResponse(status_code=204)
except Exception as err:
print(f"Error in webhook: {err}")
return JSONResponse(
status_code=500, content={"message": "Internal Server Error"}
)
```
Before going live, test your application integration locally. For example, host the endpoint on `localhost:8080/webhook` and test with Postman:
Test using this CURL command:
```bash theme={"dark"}
curl --location 'localhost:8080/webhook' \
--header 'Content-Type: application/json' \
--data '{
"event": "call_ended",
"call": {
"call_type": "phone_call",
"from_number": "+12137771234",
"to_number": "+12137771235",
"direction": "inbound",
"call_id": "Jabr9TXYYJHfvl6Syypi88rdAHYHmcq6",
"agent_id": "oBeDLoLOeuAbiuaMFXRtDOLriTJ5tSxD",
"call_status": "registered",
"metadata": {},
"retell_llm_dynamic_variables": {
"customer_name": "John Doe"
},
"start_timestamp": 1714608475945,
"end_timestamp": 1714608491736,
"disconnection_reason": "user_hangup",
"transcript": "...",
"opt_out_sensitive_data_storage": false
}
}'
```
Deploy your endpoint using [Ngrok](https://ngrok.com/docs/getting-started/):
1. Install ngrok:
```bash theme={"dark"}
brew install ngrok/ngrok/ngrok
```
2. Start ngrok:
```bash theme={"dark"}
ngrok http http://localhost:8080
```
You'll see a console UI like this:
```
ngrok (Ctrl+C to quit)
Session Status online
Account inconshreveable (Plan: Free)
Version 3.0.0
Region United States (us)
Latency 78ms
Web Interface http://127.0.0.1:4040
Forwarding https://84c5df474.ngrok-free.dev -> http://localhost:8080
```
Your webhook endpoint will be `https://84c5df474.ngrok-free.dev/webhook`
You have two options:
**Option 1: Register an account level webhook**
Set up through the system settings' webhooks tab for events related to any agent under your account.
**Option 2: Register an agent level webhook**
Set up through the dashboard's agent detail page. Note: If set, account level webhooks will not be triggered for that agent.
The webhook URL field accepts [dynamic variables](/build/dynamic-variables). Type `{{` to open a list of available variables, or wrap a name in double curly braces yourself (for example `https://example.com/webhook?customer={{customer_name}}`). Each variable is replaced with its value per call before the event is delivered.
Use the **Test** button to send a sample request to your endpoint. If your URL uses variables whose values are only known during a live call (such as contact or CRM fields), the **Test** button is disabled and a tooltip explains why — the webhook will still work on real calls, but it can't be tested here because there are no values to substitute yet. Variables that Retell can resolve ahead of time, such as system variables and your version's environment variables, can be tested normally.
Start a web call in the dashboard to verify the webhook is triggered correctly.
# Rerun Post Call/Chat Extraction
Source: https://docs.retellai.com/features/rerun-call-analysis
Edit Retell Post Call or Post Chat Extraction prompts and rerun processing on past sessions to regenerate summaries and success metrics with new criteria.
You can now customize the prompts used to summarize conversations and determine call/chat success. After updating these prompts, you can rerun the analysis to regenerate results that better match your criteria.
When you rerun the analysis for a call, the system always uses the analysis prompts from the latest draft version of the agent—even if the call was originally handled by an older published version. To update the analysis results, make sure to edit the prompts in the latest draft agent.If the session included an [Agent Transfer](/build/single-multi-prompt/transfer-agent), rerunning the analysis follows the same **Post Call Extraction setting** chosen for that transfer. If the transfer used **Only transferred agent**, the rerun applies the destination agent's full analysis configuration—its analysis fields, analysis model, and analysis prompts—so the rerun results match what you'd get from the live call.
## Editing Analysis Prompts
In the agent's `Post-Call Data Extraction` section (formerly `Post Call Extraction`), you can customize prompts for the following built-in analysis categories:
### Call Summary
The `call_summary` field provides a high-level summary of the conversation. Adjust this prompt to extract the type of summary or details you care about.
### Call Successful
You can also customize the prompt that evaluates whether a call or chat was successful, allowing you to define your own success criteria.
## Rerunning Post Call Extraction
**Important**: Rerunning analysis will incur charges for *all* models, including those that were free during the initial post-call/post-chat run.
After updating your prompts, you can rerun the Post Call Extraction to generate new outputs based on the revised instructions.
Below is an example of a regenerated summary using the prompt: `Write a 1–5 word summary of the call.`
## Batch Rerun from Call/Chat History
You can rerun Post Call or Post Chat Extraction in bulk from [Call History or Chat History](/features/session-history). Use this after updating analysis prompts to regenerate results across past sessions.
On the **Call History** or **Chat History** page, click the **Actions** button in the top-right corner.
Choose **Backfill from Post-Call Data** (or **Backfill from Post-Chat Data** on the Chat History page) from the dropdown.
The backfill modal lets you scope which sessions to rerun analysis on. Add one or more filter criteria to narrow down the sessions. You can add multiple filter rows to combine criteria. Click **Add** to add another filter row.
Click **Save** to queue the batch rerun. The system will re-extract Post Call/Chat Extraction data for all matching sessions using the latest draft agent's analysis prompts.
**Important**: Batch rerun will incur charges. The cost scales with the number of matching sessions.
If you have [CRM integration](/integrations/crm-overview) enabled, the re-extracted analysis data will also flow through your configured [analysis data mappings](/integrations/crm-mappings) and update the corresponding contact fields.
# Secure the webhook
Source: https://docs.retellai.com/features/secure-webhook
Verify Retell AI webhook signatures with the raw request body, X-Retell-Signature header, API key, SDK helpers, and IP allowlisting.
You can use the `X-Retell-Signature` header together with your Retell API Key to verify the webhook comes from Retell AI, not from a malicious third party. We have provided a verify function in our SDKs to help you with this.
Only the API key that has a webhook badge next to it can be used to verify the webhook.
You can also check and allowlist Retell IP addresses: `100.20.5.228`.
The following code snippets demonstrate how to verify and handle the webhook in Node.js and Python.
### Install the SDK
Install the corresponding Python or Node.js SDK:
* [Node.js](/get-started/sdk)
* [Python](/get-started/sdk)
### Sample Code
```typescript Node.js theme={"dark"}
// Install the SDK: https://docs.retellai.com/get-started/sdk
import { Retell } from "retell-sdk";
import express from "express";
const app = express();
// Use raw body for signature verification, not JSON.stringify(req.body).
app.use(express.raw({ type: "application/json" }));
app.post("/webhook", async (req, res) => {
const rawBody = req.body.toString("utf-8");
const signature = req.headers["x-retell-signature"];
if (
typeof signature !== "string" ||
!(await Retell.verify(rawBody, process.env.RETELL_API_KEY, signature))
) {
console.error("Invalid signature");
return res.status(401).send("Unauthorized");
}
const { event, call } = JSON.parse(rawBody);
// process the webhook
// Acknowledge the receipt of the event
res.status(204).send();
});
```
```Python Python theme={"dark"}
# Install the SDK: https://docs.retellai.com/get-started/sdk
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from retell import Retell
retell = Retell(api_key=os.environ["RETELL_API_KEY"])
@app.post("/webhook")
async def handle_webhook(request: Request):
try:
# Use raw body for signature verification, not json.dumps(request.json()).
raw_body = (await request.body()).decode("utf-8")
valid_signature = retell.verify(
raw_body,
api_key=str(os.environ["RETELL_API_KEY"]),
signature=str(request.headers.get("X-Retell-Signature")),
)
if not valid_signature:
print("Received Unauthorized")
return JSONResponse(status_code=401, content={"message": "Unauthorized"})
post_data = json.loads(raw_body)
# process the webhook
return JSONResponse(status_code=204)
except Exception as err:
print(f"Error in webhook: {err}")
return JSONResponse(
status_code=500, content={"message": "Internal Server Error"}
)
```
## Verify Without SDK
If you're using a language without an official Retell SDK, you can verify the webhook signature manually. The signature uses HMAC-SHA256.
### How the Signature Works
Every webhook request includes an `X-Retell-Signature` header in the format:
```
v={timestamp},d={hex_digest}
```
* `v` is the Unix timestamp in milliseconds when the webhook was sent.
* `d` is the HMAC-SHA256 hex digest of the raw request body concatenated with the timestamp.
### Verification Steps
1. Extract the `X-Retell-Signature` header from the request.
2. Parse the timestamp (`v`) and digest (`d`) from the header using the pattern `v=(\d+),d=(.*)`.
3. Check that the timestamp is within **5 minutes** of the current time (to prevent replay attacks).
4. Compute `HMAC-SHA256(raw_body + timestamp, api_key)` where `+` is string concatenation.
5. Compare the computed hex digest with the `d` value from the header. If they match, the webhook is authentic.
You must use the **raw request body** string for verification, not a re-serialized version from parsed JSON. Re-serializing may change whitespace or key ordering, which will cause verification to fail.
### Sample Code
```go Go theme={"dark"}
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"math"
"net/http"
"os"
"regexp"
"strconv"
"time"
)
func verifyWebhook(rawBody string, apiKey string, signature string) bool {
re := regexp.MustCompile(`v=(\d+),d=(.*)`)
matches := re.FindStringSubmatch(signature)
if len(matches) != 3 {
return false
}
timestamp, err := strconv.ParseInt(matches[1], 10, 64)
if err != nil {
return false
}
digest := matches[2]
// Check timestamp is within 5 minutes
now := time.Now().UnixMilli()
if math.Abs(float64(now-timestamp)) > 5*60*1000 {
return false
}
// Compute HMAC-SHA256 and use constant-time comparison
mac := hmac.New(sha256.New, []byte(apiKey))
mac.Write([]byte(rawBody + matches[1]))
expectedMAC, _ := hex.DecodeString(digest)
return hmac.Equal(mac.Sum(nil), expectedMAC)
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
rawBody := string(body)
signature := r.Header.Get("X-Retell-Signature")
if !verifyWebhook(rawBody, os.Getenv("RETELL_API_KEY"), signature) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Process the webhook
fmt.Println("Webhook verified successfully")
w.WriteHeader(http.StatusNoContent)
}
func main() {
http.HandleFunc("/webhook", webhookHandler)
http.ListenAndServe(":8080", nil)
}
```
```ruby Ruby theme={"dark"}
require "openssl"
require "sinatra"
require "json"
API_KEY = ENV["RETELL_API_KEY"]
def verify_webhook(raw_body, api_key, signature)
match = signature.match(/v=(\d+),d=(.*)/)
return false unless match
timestamp = match[1]
digest = match[2]
# Check timestamp is within 5 minutes
now = (Time.now.to_f * 1000).to_i
return false if (now - timestamp.to_i).abs > 5 * 60 * 1000
# Compute HMAC-SHA256 and use constant-time comparison
expected = OpenSSL::HMAC.hexdigest("SHA256", api_key, raw_body + timestamp)
OpenSSL.secure_compare(expected, digest)
end
post "/webhook" do
raw_body = request.body.read
signature = request.env["HTTP_X_RETELL_SIGNATURE"]
unless verify_webhook(raw_body, API_KEY, signature)
halt 401, "Unauthorized"
end
# Process the webhook
status 204
end
```
```php PHP theme={"dark"}
5 * 60 * 1000) {
return false;
}
// Compute HMAC-SHA256 and use constant-time comparison
$expected = hash_hmac("sha256", $rawBody . $timestamp, $apiKey);
return hash_equals($expected, $digest);
}
if (!verifyWebhook($rawBody, $apiKey, $signature)) {
http_response_code(401);
echo "Unauthorized";
exit;
}
// Process the webhook
$data = json_decode($rawBody, true);
http_response_code(204);
?>
```
# Call & chat history
Source: https://docs.retellai.com/features/session-history
Browse Retell call and chat history in the dashboard: filter sessions, inspect transcripts and analysis, rerun analysis, and export to CSV.
The **Call History** and **Chat History** pages under **Data** in the dashboard show your sessions after they end, including transcripts, analysis results, costs, and outcomes. To follow a call that is still in progress, use [Live Monitoring](/features/live-monitoring).
## When to use call and chat history
Use these pages to:
* Investigate an unsuccessful or disconnected session.
* Review a recording, transcript, post-session analysis, or call logs.
* Compare sessions for an agent or agent version.
* Find sessions that match analysis fields, metadata, dynamic variables, or custom attributes.
* Export a filtered set of calls or chats for offline analysis.
For example, if callers report that an outbound campaign is failing, filter Call History by batch call ID and an unsuccessful outcome. Open individual calls to compare their disconnection reasons, transcripts, and detail logs.
## Browse and filter sessions
Both pages show one row per session with its time, cost, session ID, status, sentiment, phone numbers, and agent details. Call History also includes call-specific columns such as duration, channel type, direction, end reason, outcome, and end-to-end latency.
Use **Date Range** and **Filter** to narrow the list:
* **Call History** supports agent and version, session ID, batch call ID, transfer agent, call type, direction, duration, phone number, status, outcome, sentiment, disconnection reason, end-to-end latency, cost, analysis fields, custom attributes, dynamic variables, and metadata.
* **Chat History** supports agent and version, session ID, status, outcome, sentiment, disconnection reason, analysis fields, and custom attributes.
Chat statuses are `ongoing`, `ended`, or `error`. An ongoing chat becomes `ended` when the agent or user ends it, when you call the [End Chat API](/api-references/end-chat), or when the [inactivity timeout](/build/create-chat-agent#chat-settings) expires. Calls also include pre-connection statuses such as `registered` and `not_connected`.
Filters persist for each workspace in your browser and appear in the URL, so you can bookmark or share a filtered view with another workspace member. Use **Customize View** to show, hide, and reorder built-in columns, custom analysis fields, and workspace custom attributes. The table shows 50 rows by default, with options for 10, 20, 50, or 100 rows per page.
## Inspect a session
Select a row to open its details. Use the arrows at the top of the panel to move through sessions without closing it.
Every session can include:
* **Conversation Analysis:** session outcome, status, user sentiment, disconnection reason, and custom analysis fields.
* **Summary:** the generated session summary.
* **Transcription:** messages, tool calls and results, state or node transitions, in-call SMS, and knowledge base retrievals. You can copy the transcript or open the conversation in the test playground.
* **Data:** provided, overridden, and extracted dynamic variables, plus metadata.
* **Contact:** the matching [contact](/features/contacts) and their previous conversations. See [contact memory](/integrations/build-contact-memory) for how contacts accumulate context across sessions.
Call details can also include a recording player, end-to-end latency, **Detail Logs** for step-by-step execution, and **Packet Capture** for [debugging SIP connections](/reliability/debug-calls-pcap). Select a transcript timestamp to jump to that point in the recording. If an error disconnection reason links to a detail log, select the reason to open the matching error.
For the next debugging step, see [debug call disconnections](/reliability/debug-call-disconnect), [check call latency](/reliability/check-actual-latency), or [debug calls with PCAP](/reliability/debug-calls-pcap).
## Rerun analysis
Select **Rerun** in Conversation Analysis to re-extract analysis fields for one session after changing the agent's analysis configuration. Rerunning incurs the analysis cost again and does not resend webhooks.
To rerun analysis for many sessions, filter Call History or Chat History, then open **Actions** and choose **Backfill from Post-Call Data** or **Backfill from Chat Data**. The backfill uses the latest draft agent configuration and the active filters.
You can also rerun analysis through the [Rerun Call Analysis API](/api-references/rerun-call-analysis) or [Rerun Chat Analysis API](/api-references/rerun-chat-analysis). See [rerun Post Call and Post Chat Extraction](/features/rerun-call-analysis) for filtering, transfer-agent behavior, and billing details.
## Export sessions to CSV
Exporting uses the active filters, so narrow the history table before creating the export.
Open **Actions**, select **Export**, and choose the columns to include. Export-only fields include transcript data and PII-scrubbed transcripts. Call exports can also include recording and public log URLs.
Submit the request. Retell generates the CSV in the background. If the request exceeds your workspace's export limit, narrow the filters and try again.
Open **Actions → Export records** to check the request status and download the completed file.
Completed export records are kept for one week.
## Control sensitive data
When [PII scrubbing](/accounts/privacy-disable) is enabled, the table and detail panel show scrubbed content by default. Members with permission to view raw data can use **Show PII** to reveal the original version. The setting applies wherever scrubbed copies are available, including transcripts, recordings, analysis, logs, dynamic variables, and metadata.
Authorized members can open the trash menu in a call's detail panel to:
* **Remove Sensitive Data**, which permanently removes sensitive call artifacts while keeping basic call attributes.
* **Delete**, which permanently removes the call and its associated data.
Removing sensitive call data and deleting a call cannot be undone.
Chat History does not have the same delete menu. Use the [Delete Chat API](/api-references/delete-chat) to remove an individual chat. The agent's [data storage settings](/accounts/privacy-disable) and [data retention policy](/accounts/data-retention) determine which artifacts remain available for both calls and chats.
## FAQ
By default, Retell stores session data without automatic deletion. You can set a per-agent [data retention period](/accounts/data-retention) to delete data after 1–730 days. All session history is also permanently deleted if you [delete your account](/accounts/account#delete-your-account).
These artifacts become available after the session ends. They can also be unavailable because the call never connected, the agent stores only basic attributes, a retention period expired, sensitive data was removed, or your role cannot view the data.
Review the agent's [data storage settings](/accounts/privacy-disable) and ask a workspace admin to confirm your role.
A chat stays `ongoing` until the agent or user ends it, you call the [End Chat API](/api-references/end-chat), or the agent's [inactivity timeout](/build/create-chat-agent#chat-settings) expires. Long-running rows usually indicate a longer timeout.
Retell stores call recordings as WAV audio. A call can include a mixed single-channel recording, a recording with each party on a separate channel, and PII-scrubbed versions when configured.
Use the [Get Call API](/api-references/get-call) to retrieve the available recording fields.
Yes. Use the [List Calls API](/api-references/list-calls) to filter and paginate calls, then the [Get Call API](/api-references/get-call) for one call's complete details. Use [List Chats](/api-references/list-chats) and [Get Chat](/api-references/get-chat) for chats.
# Retell webhooks overview
Source: https://docs.retellai.com/features/webhook-overview
Overview of Retell webhooks — call_started, call_ended, and call_analyzed events delivered to your server so you can react to call activity without polling.
Webhooks allow your application to receive real-time notifications about events that occur in your Retell AI account. Instead of continuously polling our API, webhooks push data to your application as events happen, making your integrations more efficient and responsive.
## Event Types
Retell AI supports the following webhook events for voice calls:
If the call did not connect (like dial failed), the `call_started` webhook event will not be triggered.
| Event Type | Description | Payload |
| -------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `call_started` | Triggered when a new call begins | Basic call information |
| `call_ended` | Triggered when a call completes, transfers, or encounters an error | All fields from the [call object](/api-references/get-call) except `call_analysis`. |
| `call_analyzed` | Triggered when call analysis is complete | Full call data including `call_analysis` object |
| `transcript_updated` | Triggered on turn-taking transcript updates, plus a final update when the call ends | Full call data plus `transcript_with_tool_calls` |
| `transfer_started` | Triggered when a transfer is initiated | Full call data plus `transfer_destination` and `transfer_option` (if available) |
| `transfer_bridged` | Triggered when a transfer successfully bridges | Full call data plus `transfer_destination` and `transfer_option` (if available) |
| `transfer_cancelled` | Triggered when a transfer is cancelled or fails to connect | Full call data plus `transfer_destination` and `transfer_option` (if available) |
| `transfer_ended` | Triggered when the transfer leg ends | Full call data |
Retell AI supports the following webhook events for chat:
| Event Type | Description | Payload |
| --------------- | --------------------------------------------- | ----------------------------------------------------------------------------------- |
| `chat_started` | Triggered when a new chat begins | Basic chat information |
| `chat_ended` | Triggered when a chat completes or errors out | All fields from the [chat object](/api-references/get-chat) except `chat_analysis`. |
| `chat_analyzed` | Triggered when chat analysis is complete | Full chat data including `chat_analysis` object |
## Common Use Cases
1. **Real-time Analytics**
* Track call statistics and performance metrics
* Monitor call volumes and patterns
* Trigger alerts for specific call outcomes
2. **System Integration**
* Update CRM records when calls complete
* Trigger workflow automations based on call analysis
* Archive call transcripts in your data warehouse
3. **Call Monitoring**
* Get notified of failed or transferred calls
* Track call duration and completion status
* Monitor agent performance in real-time
## Webhook Spec
The webhook will `POST` the payload to your endpoint. The webhook has a timeout of 10 seconds. If within 10 seconds no success status (2xx) is received, the webhook will be retried, up to 3 times.
The webhook will be triggered in order, but is not blocking. For example, if the webhook for `call_started` is not successful, we can still trigger the `call_ended` webhook.
Your webhook endpoint must be publicly reachable. For security, Retell blocks requests to localhost, private IP ranges, and cloud metadata addresses, so expose a local endpoint with a tunneling service like [ngrok](https://ngrok.com/) before registering it.
When the call did not connect (like calls with `dial_failed`, `dial_no_answer`, `dial_busy` disconnection reason), it will not have its `call_started` webhook triggered. It will still have its `call_ended` and `call_analyzed` webhooks triggered.
### Request payload
The webhook will contain the event type and the call object. See a sample payload in the "Handle Webhook" section below.
### Event filtering
You can limit which events are delivered per agent using the `webhook_events` field when creating or updating an agent.
* Voice agent default: `call_started`, `call_ended`, `call_analyzed`
* Chat agent default: `chat_started`, `chat_ended`, `chat_analyzed`
This is useful when you only need high-signal events and want to reduce webhook traffic.
### Register Webhook
We offer two types of webhooks: Agent-Level and Account-Level, designed to streamline your event notification process. Here's a brief on each:
### Account-level webhooks
Set up through the dashboard's webhooks tab, these webhooks notify you of events related to any agent under your account. Specify your webhook URL in the dashboard's webhooks tab to activate.
### Agent-level webhooks
When you [create](/api-references/create-agent) the agent, you can set the `webhook_url` field.
Any event associated with that agent will be pushed to the agent webhook URL. If set, the account-level webhook URL will not be triggered for that agent.
Webhook URLs support [dynamic variables](/build/dynamic-variables). For example, setting `webhook_url` to `https://example.com/webhook?customer={{customer_name}}` resolves the `{{customer_name}}` placeholder per call before the event is delivered. This applies to both agent-level and account-level webhook URLs.
## Handle Webhook
After registering the webhook, you would want to verify the webhook is from Retell and handle it.
### Webhook Payload
The webhook will be `POST` to the URL you provided with a JSON payload.
The payload will contain the event type and the call detail associated with the event.
It will contain an `x-retell-signature` header to help you verify the webhook comes from Retell.
#### Sample Payload
The `call` field in the webhook payload contains the call details. It's the same content
you would receive when you fetch the call details using the [get-call](/api-references/get-call) API.
```javascript theme={"dark"}
{
"event": "call_ended",
"call": {
"call_type": "phone_call",
"from_number": "+12137771234",
"to_number": "+12137771235",
"direction": "inbound",
"call_id": "Jabr9TXYYJHfvl6Syypi88rdAHYHmcq6",
"agent_id": "oBeDLoLOeuAbiuaMFXRtDOLriTJ5tSxD",
"call_status": "registered",
"metadata": {},
"retell_llm_dynamic_variables": {
"customer_name": "John Doe"
},
"start_timestamp": 1714608475945,
"end_timestamp": 1714608491736,
"disconnection_reason": "user_hangup",
"transcript": "...",
"transcript_object": [ [Object], [Object], [Object], [Object] ],
"transcript_with_tool_calls": [ [Object], [Object], [Object], [Object] ],
"opt_out_sensitive_data_storage": false
}
}
```
If metadata and retell\_llm\_dynamic\_variables are not provided, they will be omitted from the webhook event payload.
For `transcript_updated` events, the payload also includes `transcript_with_tool_calls`. For transfer events, the payload includes `transfer_destination` and (when available) `transfer_option`.
#### Sample Transfer event payload
Here is a compact example of a transfer webhook payload (fields may vary by transfer type):
```json theme={"dark"}
{
"event": "transfer_started",
"call": {
"call_id": "Jabr9TXYYJHfvl6Syypi88rdAHYHmcq6"
},
"transfer_destination": {
"number": "+12137771235",
"extension": "1234"
},
"transfer_option": {
"type": "warm_transfer",
"showTransfereeAsCaller": true,
"publicHandoffOption": {
"type": "static_message",
"message": "Hi, I am transferring the caller now."
},
"agentDetectionTimeoutMs": 15000,
"onHoldMusic": {
"type": "default"
},
"enableBridgeAudioCue": true
}
}
```
### Verifying Webhook
You can use the `x-retell-signature` header together with your Retell API Key to verify the webhook.
We have provided
a verify function in our SDKs to help you with this.
You can also check and allowlist Retell IP addresses: `100.20.5.228`.
The following code snippets demonstrate how to verify and handle the webhook in Node.js and Python.
For languages without an official SDK, see [Verify Without SDK](/features/secure-webhook#verify-without-sdk).
### Sample Code
```typescript Node.js theme={"dark"}
// install the sdk: https://docs.retellai.com/get-started/sdk
import { Retell } from "retell-sdk";
import express from "express";
const app = express();
// Use raw body for signature verification, not JSON.stringify(req.body).
app.use(express.raw({ type: "application/json" }));
app.post("/webhook", async (req, res) => {
const rawBody = req.body.toString("utf-8");
const signature = req.headers["x-retell-signature"];
if (
typeof signature !== "string" ||
!(await Retell.verify(rawBody, process.env.RETELL_API_KEY, signature))
) {
console.error("Invalid signature");
return;
}
const {event, call} = JSON.parse(rawBody);
switch (event) {
case "call_started":
console.log("Call started event received", call.call_id);
break;
case "call_ended":
console.log("Call ended event received", call.call_id);
break;
case "call_analyzed":
console.log("Call analyzed event received", call.call_id);
break;
case "transcript_updated":
console.log("Transcript updated event received", call.call_id);
break;
case "transfer_started":
case "transfer_bridged":
case "transfer_cancelled":
case "transfer_ended":
console.log("Transfer event received", event, call.call_id);
break;
default:
console.log("Received an unknown event:", event);
}
// Acknowledge the receipt of the event
res.status(204).send();
});
```
```Python Python theme={"dark"}
# Install the SDK: https://docs.retellai.com/get-started/sdk
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from retell import Retell
retell = Retell(api_key=os.environ["RETELL_API_KEY"])
@app.post("/webhook")
async def handle_webhook(request: Request):
try:
# Use raw body for signature verification, not json.dumps(request.json()).
raw_body = (await request.body()).decode("utf-8")
valid_signature = retell.verify(
raw_body,
api_key=str(os.environ["RETELL_API_KEY"]),
signature=str(request.headers.get("X-Retell-Signature")),
)
if not valid_signature:
print("Received Unauthorized")
return JSONResponse(status_code=401, content={"message": "Unauthorized"})
post_data = json.loads(raw_body)
if post_data["event"] == "call_started":
print("Call started event", post_data["call"]["call_id"])
elif post_data["event"] == "call_ended":
print("Call ended event", post_data["call"]["call_id"])
elif post_data["event"] == "call_analyzed":
print("Call analyzed event", post_data["call"]["call_id"])
elif post_data["event"] == "transcript_updated":
print("Transcript updated event", post_data["call"]["call_id"])
elif post_data["event"] in [
"transfer_started",
"transfer_bridged",
"transfer_cancelled",
"transfer_ended",
]:
print("Transfer event", post_data["event"], post_data["call"]["call_id"])
else:
print("Unknown event", post_data["event"])
return JSONResponse(status_code=204)
except Exception as err:
print(f"Error in webhook: {err}")
return JSONResponse(
status_code=500, content={"message": "Internal Server Error"}
)
```
### Testing Locally
To test webhooks on your local machine, you can use [ngrok](https://ngrok.com/)
to generate a production URL forwarding requests to your local endpoints.
### Idempotency and deduplication
Retell retries each webhook up to 3 times if no `2xx` response is returned within 10 seconds, so your endpoint may receive the same event more than once. Make your consumer idempotent:
* **Per-call lifecycle events** (`call_started`, `call_ended`, `call_analyzed`, `chat_started`, `chat_ended`, `chat_analyzed`): use the combination of `event` + `call_id` (or `chat_id`) as a unique key. These events fire at most once per call/chat, so storing the processed key in a database or cache and skipping repeats is sufficient.
* **Transfer events** (`transfer_started`, `transfer_bridged`, `transfer_cancelled`, `transfer_ended`): a single call may contain multiple transfer attempts. Use `event` + `call_id` + `start_timestamp` (and, if needed, `transfer_destination`) so each transfer attempt is processed exactly once.
* **`transcript_updated`**: this event streams throughout the call and is expected to fire many times per `call_id`. Treat each delivery as an incremental update rather than deduplicating by `call_id` — store the latest payload per call, or use the final `transcript_updated` (sent when the call ends) as the canonical transcript.
Always return a `2xx` status as soon as you have safely accepted the payload (e.g., persisted it to a queue). Doing heavy work before responding can trigger Retell's 10-second retry and create duplicate processing.
## Privacy
Choosing to "Opt-Out of Personal and Sensitive Data Storage" means transcripts and recordings post-call won't be stored.
However, transcripts and recordings remain accessible via webhooks, allowing for alternative storage
solutions on your end.
The recording will also be available in the `recording_url` field.
The link will be accessible for 10 minutes and will be
deleted and become inaccessible after 10 minutes.
### Response
We expect a successful status code (2xx) to be returned. No body is expected.
## Video Tutorial
See community templates in [docs](https://docs.google.com/document/d/1hx6hdTEjAR4y4xXZ7RLMH2byQNVW1ABxC8S4FwvTx_Y/edit?tab=t.0#heading=h.wf5bktkelope).
# Security and compliance
Source: https://docs.retellai.com/general/compliance
Retell AI is HIPAA and GDPR compliant and SOC 2 Type 1 and Type 2 certified. Review our data handling, BAA, DPA, and GDPR erasure process.
At Retell, we deeply understand the importance of privacy and security for your team's information.
We recognize the trust you place in us daily to safeguard your data, and we take this responsibility very seriously.
Ensuring the safety and security of your data is not just a policy; it's a fundamental principle that we live by.
To align with our commitment to your privacy, we are proud to be **HIPAA compliant, GDPR compliant, SOC 2 Type 1 & Type 2 certified**.
This ensures your data remains protected, secure, and confidential within your Retell account.
## Certifications and reports
Retell maintains the following certifications and attestations:
* **HIPAA** — compliant for handling protected health information (PHI). A signed BAA is required before transmitting PHI through Retell.
* **SOC 2 Type 1 and Type 2** — independently audited controls for security, availability, and confidentiality.
* **GDPR** — compliant via AWS infrastructure with a GDPR-compliant Data Processing Addendum.
Access certificates, reports, and the latest audit status in the [Compliance Trust Center](https://app.vanta.com/re-tell.ai/trust/8nfvavp5klt9n4iz32h90).
## Sign a BAA, DPA, or SCCs
Retell's Business Associate Agreement (BAA) and Data Processing Addendum (DPA, including EU Standard Contractual Clauses) are available for **self-signing** at [click-agreements.retellai.com](https://click-agreements.retellai.com/).
* **BAA** — required for HIPAA-covered workloads before sending PHI through Retell.
* **DPA / SCCs** — required for processing personal data of EU/UK residents under GDPR.
There is no additional fee to sign these agreements. After signing, configure data handling on your workspace and agents:
* [Data Retention Policy](/accounts/data-retention) — set per-agent retention from 1 day up to 2 years for transcripts, recordings, and logs.
* [Privacy and PII controls](/accounts/privacy-disable) — choose what data is stored per agent (everything, exclude PII, or basic attributes only).
* [Signed and secure recording URLs](/accounts/signed-secure-url) — restrict access to call recordings.
## Is Retell GDPR compliant?
Yes. Retell complies with the General Data Protection Regulation (GDPR) by utilizing Amazon Web Services (AWS), which includes a GDPR-compliant Data Processing Addendum (DPA) in its Service Terms. However, please note that we do not currently operate services within the European Union.
## How do I request erasure of my personal data?
You can ask Retell to permanently erase your personal data (the GDPR right to erasure). First [delete your account](/accounts/account#delete-your-account) from the dashboard, which removes your workspace along with its call and chat data. Then email [support@retellai.com](mailto:support@retellai.com) to request full erasure.
On request, Retell permanently deletes your remaining account record and removes personal identifiers — such as your email and name — from the third-party services it uses to run the business, including its CRM, support, and identity-verification providers. Erasure is permanent and can't be undone.
Retell also excludes personal identifiers from its internal logs and product analytics by default, so this data isn't retained there in the first place. To control retention of call and chat data (transcripts, recordings, and logs), see the [Data Retention Policy](/accounts/data-retention).
## Need more info?
For compliance questions not covered above — including custom security reviews, vendor assessments, or subprocessor lists — contact us at [support@retellai.com](mailto:support@retellai.com). Our team is here to support you in protecting your security and data with the utmost care and diligence.
# Introduction to Retell
Source: https://docs.retellai.com/general/introduction
Retell is a platform to build, test, deploy, and monitor AI voice and chat agents with telephony, prompts, tools, and analytics built in.
## Build
Retell agents are node-based flows or single prompts, with call handling, a knowledge base, and integrations built in. See the full [build overview](/build/overview) for everything you can configure, starting with your agent type.
Drag-and-drop, node-based flows for structured, high-stakes calls.
Define your whole agent with one prompt.
Voice agents for calls, chat agents for text.
Warm transfer, IVR navigation, DTMF capture, and more.
Crawl a website or upload documents your agent retrieves from.
CRM, helpdesk, calendar, and file-storage integrations: connect once, available to every agent.
Built-in call success and sentiment scoring, plus custom fields synced to your CRM.
Retell's AI copilot that builds, tests, and refines your agent with you.
## Test
Test in text or real audio, manually or automatically, before an agent ever takes a real call. See the [testing overview](/test/test-overview) to compare every method.
Chat, call, or debug your agent in the Playground.
Run graded test cases to catch regressions before launch.
Compare agents or prompts by splitting live traffic between them.
## Deploy
Buy a Retell-managed US or Canada number, no provider account needed.
Connect your own provider over SIP trunking.
Embed a voice call widget on your website.
Connect a web call from your frontend with the Web SDK.
## Monitor
Follow live transcripts, listen in, or take over a call.
Build custom dashboards for your call and chat metrics.
Automatically score production calls for quality and accuracy.
## Data
Browse past calls and chats with transcripts and analysis.
Sync contacts and push call data to Salesforce, HubSpot, Dynamics 365, GoHighLevel, or Zoho CRM automatically.
## Developer friendly
Build entirely from the dashboard with no code, or go fully programmatic with the API, SDKs, and an MCP server.
Manage agents, calls, and numbers programmatically.
Official Node.js and Python client libraries.
Manage agents from Cursor, Claude Desktop, and more.
Real-time call events, no polling required.
## Next up
Ready to dive in? Start here.
Build your first phone agent in 15 minutes.
How STT, LLM, and TTS orchestrate into one voice stack.
# Retell CLI
Source: https://docs.retellai.com/get-started/cli
Install the Retell CLI to manage agents, phone numbers, knowledge bases, and other Retell resources from your terminal with simple commands.
## Overview
The Retell CLI lets you manage Retell resources from your terminal. See the [`@retell-ai/retell-cli` npm package](https://www.npmjs.com/package/@retell-ai/retell-cli) for the full CLI reference. For application code, use a [Retell SDK](/get-started/sdk).
## Get started
Requires [Node.js](https://nodejs.org) 22 or later.
```bash theme={"dark"}
npm install --global @retell-ai/retell-cli
```
With a Retell [API key](/accounts/manage-api-keys), run:
```bash theme={"dark"}
retell auth login
```
Enter your API key when prompted.
For example, list your agents:
```bash theme={"dark"}
retell list-agents
```
Run `retell --help` to see all available commands.
# Create voice agent with TypeScript SDK
Source: https://docs.retellai.com/get-started/create-agent-with-sdk
Create a Retell AI phone agent with the TypeScript SDK in Node.js: configure its prompt, voice, webhook, call limits, testing, and published version.
Create a Retell voice agent with the TypeScript SDK by creating a Retell LLM response engine and attaching it to a voice configuration. This example builds an after-hours receptionist, returns the resource IDs, and leaves the first agent version as a draft for testing.
## When to use this workflow
Use this workflow when you manage agent configuration in source code or provision agents for multiple customers. If you already have an agent and only need to place calls, use the [create phone call API](/api-references/create-phone-call) instead.
The example creates an after-hours receptionist for a heating company. It collects the caller's name, callback number, address, and reason for calling without promising a price or appointment time.
## Before you start
You need:
* Node.js 18.10 or later.
* A Retell [API key](/accounts/manage-api-keys).
* A voice ID from the [voice library](/build/platform-voices).
* An HTTPS endpoint if you want to receive [call event webhooks](/features/webhook-overview). The webhook is optional in this example.
## Create the agent
Create a Node.js project and install the dependencies:
```bash theme={"dark"}
npm init -y
npm install retell-sdk
npm install --save-dev tsx
```
Export the API key and voice ID you copied from the dashboard. Set the webhook URL only if your endpoint is ready to receive call events.
```bash theme={"dark"}
export RETELL_API_KEY=""
export RETELL_VOICE_ID=""
# Optional: uncomment after your webhook endpoint is ready.
# export RETELL_WEBHOOK_URL="https://example.com/retell/webhook"
```
Keep the API key in your server environment or secret manager. Do not expose it in browser code or commit it to source control.
Save this file as `create-agent.ts`:
```typescript theme={"dark"}
import Retell from "retell-sdk";
function requireEnv(name: "RETELL_API_KEY" | "RETELL_VOICE_ID"): string {
const value = process.env[name];
if (!value) {
throw new Error(`Set ${name} before running this script.`);
}
return value;
}
const apiKey = requireEnv("RETELL_API_KEY");
const voiceId = requireEnv("RETELL_VOICE_ID");
const webhookUrl = process.env.RETELL_WEBHOOK_URL;
const client = new Retell({ apiKey });
async function main() {
const llm = await client.llm.create({
start_speaker: "agent",
begin_message:
"Thanks for calling Northstar Heating after hours. How can I help?",
general_prompt: `
You are the after-hours receptionist for Northstar Heating.
Your job:
- Ask for the caller's full name, callback number, service address, and reason for calling.
- Ask one question at a time.
- Read the details back and ask the caller to confirm them.
- Do not promise pricing, appointment times, or technician availability.
- Tell the caller that the office will follow up during business hours.
- When the caller confirms there is nothing else they need, use the end_call tool.
`.trim(),
general_tools: [
{
type: "end_call",
name: "end_call",
description:
"End the call after the caller confirms the collected details and has no more questions.",
},
],
});
console.log(`Created Retell LLM: ${llm.llm_id}`);
const agent = await client.agent.create({
agent_name: "Northstar after-hours receptionist",
response_engine: {
type: "retell-llm",
llm_id: llm.llm_id,
},
voice_id: voiceId,
language: "en-US",
max_call_duration_ms: 15 * 60 * 1_000,
end_call_after_silence_ms: 2 * 60 * 1_000,
...(webhookUrl ? { webhook_url: webhookUrl } : {}),
});
console.log(`Created agent: ${agent.agent_id}`);
console.log(`Draft version: ${agent.version}`);
}
main().catch((error) => {
if (error instanceof Retell.APIError) {
console.error(`Retell API error ${error.status}: ${error.message}`);
} else {
console.error(error);
}
process.exitCode = 1;
});
```
The example sets English explicitly, caps calls at 15 minutes, and ends a call after 2 minutes of silence. As of July 2026, `max_call_duration_ms` accepts 1 minute to 2 hours, while `end_call_after_silence_ms` has a 10-second minimum and a 10-minute default. Because `model` is omitted, the Retell LLM uses the endpoint's default text model.
If `RETELL_WEBHOOK_URL` is set, the agent-level webhook replaces the account-level webhook for this agent.
Run the TypeScript file:
```bash theme={"dark"}
npx tsx create-agent.ts
```
A successful run prints the Retell LLM ID, agent ID, and draft version:
```text theme={"dark"}
Created Retell LLM: llm_...
Created agent: agent_...
Draft version: 0
```
## Test and publish the agent
Open the new agent in the dashboard and run a [web call test](/test/test-web). A draft can be tested without publishing or assigning a phone number.
When the draft is ready, publish it from the dashboard version panel or call the [publish agent API](/api-references/publish-agent) with the `agent_id` and `version` printed by the script. Published versions are immutable. See [agent versioning](/agent/version) before you attach a version or environment tag to a [phone number](/deploy/purchase-number).
If you configured a webhook, [verify its signature](/features/secure-webhook) before processing call events.
## Common questions
The response engine and voice agent are separate resources. `client.llm.create()` defines what the agent says and which tools it can use. `client.agent.create()` attaches that response engine to voice and call settings.
The Retell LLM remains available because the two create calls are not atomic. Reuse its `llm_id` on the next attempt, or remove it with the [delete Retell LLM API](/api-references/delete-retell-llm).
No. Test the draft with a browser web call first. Publish it when you are ready to use an immutable version for a phone number or environment tag.
# Retell MCP server for AI assistants
Source: https://docs.retellai.com/get-started/mcp-server
Use Retell's MCP server to build and manage voice agents from MCP-capable clients like Cursor, Claude Desktop, and Claude Code via Retell APIs.
## Overview
Retell supports the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) so you can build Retell AI voice agents directly from MCP-capable clients (Cursor, Claude Desktop, Claude Code, Codex-style tools, and more).
If you're already using Retell via REST or SDKs, MCP is simply a different interface to the same core functionality — optimized for agentic workflows in IDEs and assistants.
This page covers Retell's own MCP server, which lets your assistant manage your Retell account. To let one of your agents call an external MCP server's tools during a call, see [MCP tools for single and multi-prompt agents](/build/single-multi-prompt/mcp) or the [MCP node](/build/conversation-flow/mcp-node) for conversation flows.
## What can Retell's MCP server do?
Retell's MCP server gives your AI assistant native access to the Retell platform through a single integration. Depending on your API key permissions and what you enable in your MCP client, an assistant can:
* **Agents**: create, update, publish, list, and fetch agent versions.
* **Calls**: create phone/web calls, fetch call details, list calls, delete calls, update call metadata, run QA and review.
* **Phone numbers**: import, provision, list, and fetch phone numbers.
* **Knowledge base**: create KBs, attach sources, remove sources, list KBs.
* **Voices**: list voices, clone voices, search community voices.
* **Chats**: create chats, create chat agents, end chats, update chat metadata.
* **Testing & QA**: create test cases, run tests, list/rerun QA, submit scores.
* **Alerts & webhooks**: create/list alert rules, list incidents, test webhooks.
## How it works
* **Runtime**: MCP clients connect to a hosted (remote) MCP endpoint over Streamable HTTP, authenticate with your Retell API key, then call tools.
* **Tool discovery**: your MCP client can list available tools (via `tools/list`), including each tool's name, description, and JSON input schema.
## Prerequisites
* A Retell API key ([get one here](/accounts/manage-api-keys))
* An MCP client (Cursor, Claude Desktop, Claude Code, etc.)
* Retell MCP server URL: `https://mcp.retellai.com`
**Authentication header format:**
```
Authorization: Bearer
```
## Setup
Most clients support remote MCP over HTTP. Configure:
* **URL**: `https://mcp.retellai.com`
* **Headers**: `Authorization: Bearer `
## Client setup
The exact UI differs by client version, but the configuration concepts are the same: server name, URL/transport, and authentication.
Open the command palette and choose **Cursor Settings** → **MCP** → **Add new global MCP server**, then add:
```json theme={"dark"}
{
"mcpServers": {
"retell": {
"url": "https://mcp.retellai.com",
"headers": {
"Authorization": "Bearer "
}
}
}
}
```
Open Claude Desktop settings → **Developer** → **Edit Config**, then add:
```json theme={"dark"}
{
"mcpServers": {
"retell": {
"url": "https://mcp.retellai.com",
"headers": {
"Authorization": "Bearer "
}
}
}
}
```
Run the following command in your terminal:
```bash theme={"dark"}
claude mcp add --transport http retell https://mcp.retellai.com \
--header "Authorization: Bearer "
```
Add the following to your `~/.codex/config.toml`:
```toml theme={"dark"}
[mcp_servers.retell]
url = "https://mcp.retellai.com"
bearer_token_env_var = "RETELL_API_KEY"
```
Then set the environment variable:
```bash theme={"dark"}
export RETELL_API_KEY=""
```
If your client supports remote MCP URLs + headers, configure:
* **URL**: `https://mcp.retellai.com`
* **Header**: `Authorization: Bearer `
## Prompt examples
Try these prompts in your MCP client to get started:
* "List my agents and summarize what each one does."
* "Create a new agent for inbound sales qualification and publish it."
* "Show me the last 20 calls and flag any with low QA scores."
* "Create a knowledge base and attach these sources, then update my agent to use it."
* "Import this phone number and assign it to my agent."
* "Rerun QA on this call and summarize the failure reasons."
## Security considerations
Connecting an LLM to operational tools introduces new risks. The primary risk class unique to LLM workflows is **prompt injection**: untrusted content (like call transcripts, user messages, or knowledge base documents) can include instructions that try to trick the model into taking unintended actions.
Most MCP clients support **"confirm before running tools"**. Keep that enabled and review tool calls carefully.
### Recommendations
MCP makes it easy to connect powerful tools to an assistant. Treat MCP like giving a programmatic operator access.
* **Use least privilege**: create keys with the minimum permissions needed.
* **Keep keys out of prompts**: store API keys in client secrets/settings, never paste them into chat.
* **Prefer read-first workflows**: fetch the current resource before you update or delete it.
* **Review destructive actions**: tools like delete and publish should be gated by explicit user intent.
* **Be careful with PII**: call logs and transcripts may contain sensitive data — avoid sending full transcripts back to the model if you don't need them.
* **Use non-production data when possible**: if you're exploring agent behavior or testing workflows, prefer development/sandbox environments.
## Troubleshooting
| Issue | Solution |
| -------------------- | ------------------------------------------------------------------------------------------------------------------ |
| Tools not showing up | Verify the server URL, auth header, and that the client can reach the endpoint. |
| 401 Unauthorized | Confirm `Authorization: Bearer ` and that the key is active. |
| Tool call errors | Re-run with smaller inputs, then inspect the returned error payload (many clients surface structured tool errors). |
## Feedback
If you run into issues or have requests for additional MCP capabilities, reach out to our support team at [support@retellai.com](mailto:support@retellai.com).
# Build your first phone agent in 15 minutes
Source: https://docs.retellai.com/get-started/quick-start
Build your first Retell AI phone agent in 15 minutes: create an account, pick a template, test in the dashboard, deploy to a phone number, and make a call.
**Retell** is a platform for building, testing, and deploying **AI phone agents** that hold natural conversations over the phone.
1. Visit the [Retell Dashboard](https://dashboard.retellai.com)
2. Sign up for a new account
New accounts start with \$10 in free trial credits, no payment method required. You'll only need to add one if you want to purchase a phone number.
1. Navigate to the "Agents" tab
2. Click "Create an agent"
3. Pick the agent type: **Single prompt** (easy to start, simple free-form conversations) or **Conversational flow** (production-ready, deterministic conversations)
4. Choose how to start: click **Build from scratch** to build everything yourself, pick a ready-made template like Receptionist or Insurance Verification Caller, or click **Generate from prompt** (marked Suggested) to let [Conductor](/conductor/create-agent) draft an agent from a plain-English description of your use case
1. Click the "Test" button to start a web call with your agent
2. Allow microphone access when your browser prompts for it, then talk to your agent like a real caller
This step is free and doesn't need a phone number or payment method. You can test a draft agent this way as many times as you want before publishing it.
1. Go to the "Phone Numbers" tab
2. Click "Buy New Number"
3. If you don't have a payment method on file yet, add one: click "Add Payment Method" right on the "New Number" page, or go to the "Billing" tab and click "Manage Payment Methods" to add one through Stripe
4. (Optional) Enter the area code you want to buy the number for
5. Purchase your number
6. Assign your agent to the number in the configuration settings
Retell-managed numbers are US and Canada only: \$2 per month for normal phone numbers, \$5 per month for toll-free phone numbers. If you need a number from another country or want to bring your own telephony provider, see [custom telephony](/deploy/custom-telephony) instead.
1. Incoming Calls:
* Dial your purchased number
2. Outbound Calls:
* Click "Make an outbound call"
* Enter the phone number including the country code (e.g., `+12137774445`)
Congratulations! Your agent is now live. It can:
* Receive incoming calls
* Make outbound calls
* Handle natural conversations 24/7
## FAQ
Check your browser's site permissions and allow microphone access for the dashboard, then click "Test" again. If you dismissed the permission prompt, most browsers require you to re-enable it from the address bar's site settings rather than retriggering the prompt automatically.
No. Web call testing (step 3) works on a draft, unpublished agent with no payment method or phone number required. You only need a number once you want to make or receive real phone calls.
Not as a Retell-managed number. Connect your own provider instead. See [custom telephony](/deploy/custom-telephony).
Double-check the card details and try again, or add a different payment method from the "Billing" tab. See [add payment methods](/accounts/add-payment) for more detail.
## Next steps
Now that your agent works, keep going:
See everything you can configure, prompts, voice, knowledge, and more
Integrate APIs and external services into your agent
Track call metrics and analyze agent performance
Automatically extract structured data like call outcome and sentiment from every call
### Follow us on YouTube
Subscribe for tutorials, feature walkthroughs, and other educational content. See more in the [video hub](/videos/introduction).
# Retell SDKs for TypeScript and Python
Source: https://docs.retellai.com/get-started/sdk
Official Retell SDKs for Node.js and Python. Typed clients with API key auth, structured errors, and full voice and chat endpoint coverage.
## Overview
Retell provides official SDKs for Node.js and Python to simplify integration with our platform. While you can use our [REST API](/api-references/create-phone-call) directly, our SDKs offer:
* **Type safety**: Full TypeScript support with autocomplete
* **Simplified authentication**: Built-in API key handling
* **Error handling**: Structured error responses with detailed messages
* **Reduced boilerplate**: Cleaner, more maintainable code
## Available SDKs & Requirements
### Node.js TypeScript SDK
* **Package**: [retell-sdk on NPM](https://www.npmjs.com/package/retell-sdk)
* **Requirements**: Node.js version 18.10.0 or higher
* **Features**: Full TypeScript support, async/await, promise-based API
### Python SDK
* **Package**: [retell-sdk on PyPI](https://pypi.org/project/retell-sdk/)
* **Requirements**: Python 3.9 or higher
* **Features**: Type hints, async support, comprehensive error handling
Navigate to the "API Keys" tab in your dashboard to obtain your API key.
Choose your preferred language and install the SDK:
```bash Node Client theme={"dark"}
npm i retell-sdk
```
```bash Python Client theme={"dark"}
pip install retell-sdk
```
Create a new client instance using your API key:
```typescript Node Client theme={"dark"}
import Retell from 'retell-sdk';
const retellClient = new Retell({
apiKey: "YOUR_API_KEY",
});
```
```python Python Client theme={"dark"}
from retell import Retell
retell_client = Retell(
api_key="YOUR_API_KEY"
)
```
Here's an example of making a phone call using the SDK:
```typescript Node Client theme={"dark"}
try {
const response = await retellClient.call.createPhoneCall({
from_number: '+14157774444',
to_number: '+12137774445',
});
console.log('Call initiated:', response);
} catch (error) {
console.error('Error making call:', error);
}
```
```python Python Client theme={"dark"}
try:
response = retell_client.call.create_phone_call(
from_number="+14157774444",
to_number="+12137774445"
)
print(f"Call initiated: {response}")
except Exception as e:
print(f"Error making call: {e}")
```
## Versioning
The current release is **5.66.1** (11 September 2026), published as `retell-sdk` on both npm and PyPI.
The version number tracks releases, not compatibility. One release can add endpoints and drop retired ones at the same time, and holding an older version doesn't keep the old behavior: the API changes on the server, so an old client stops matching what the API accepts. Run the latest release, and follow the [deprecation notices](/deprecation-notice/overview) — by RSS if you want them as they're announced — because that's where a change that affects your code is announced ahead of time.
Each release notes what changed: [TypeScript releases](https://github.com/RetellAI/retell-typescript-sdk/releases) and [Python releases](https://github.com/RetellAI/retell-python-sdk/releases).
## Create an agent with the TypeScript SDK
Creating a voice agent requires a response engine and an agent configuration. Follow the [end-to-end TypeScript guide](/get-started/create-agent-with-sdk) to create both resources, test the draft, and publish a version.
## Best Practices
### 1. Error Handling
Always wrap SDK calls in try-catch blocks to handle potential errors gracefully:
```typescript theme={"dark"}
try {
const response = await retellClient.call.createPhoneCall(params);
// Handle success
} catch (error) {
if (error.code === 'insufficient_funds') {
// Handle specific error
}
// Log error details
}
```
### 2. Environment Variables
Store your API key securely using environment variables:
```typescript theme={"dark"}
const retellClient = new Retell({
apiKey: process.env.RETELL_API_KEY,
});
```
### 3. Type Safety
Leverage TypeScript types for better developer experience:
```typescript theme={"dark"}
import { Retell, AgentCreateParams } from 'retell-sdk';
const params: AgentCreateParams = {
// TypeScript will provide autocomplete here
};
```
## Rate Limits
Limits apply per **organization + route**, enforced at the HTTP layer.
| Endpoint group | Limit |
| -------------------------------------------------------- | -------------------------------------------- |
| Call creation | **1000 / 10s**, plus **60 4xx errors / min** |
| List endpoints (`list-*`, `batch-get-*`) | **30 / 10s** |
| All other SDK endpoints (get / create / update / delete) | **100 / 10s** |
| LLM / agent playground completions | **8 / 2s** |
Outbound calls are also subject to [CPS limits](/deploy/outbound-call#step-3-configure-cps-calls-per-second) (excess calls are queued, not rejected) and the per-org [concurrent call limit](/deploy/concurrency).
### 429 Response
```http theme={"dark"}
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limiter: general
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 7
{ "status": "error", "message": "Too many API requests, you are being throttled, please try again later." }
```
`X-RateLimit-Limiter` identifies which limiter fired: `general`, `list`, `call`, `call-error`, or `llm-playground`.
### Handling 429s
* Use `RateLimit-Reset` (seconds) for backoff; retry with jittered exponential backoff.
* Don't parallelize `list-*` calls — the per-route budget is small.
* For high call volume, design around CPS rather than HTTP throughput.
# Add function calling
Source: https://docs.retellai.com/integrate-llm/integrate-function-calling
Give a Retell custom LLM agent tools to book appointments, transfer to a human, press IVR digits, and end calls, all logged in the transcript.
Callers want to book something, reach a person, or hear an answer from your database. Function calling is how the model asks your code to do that work.
Keep two layers separate:
* **Your LLM's function calling** decides *when* to act. This is your provider's tool-use API: you define the tools, the model picks one. Retell isn't involved.
* **Retell's response fields** carry out the actions Retell owns: ending the call, transferring it, pressing digits. You attach these to a `response` event.
Everything else, like querying your database or hitting your booking API, is just code you run.
## A concrete example
Bright Smile Dental runs an appointment line. The agent needs to book into the practice management system while the caller waits, hand off to the front desk when someone's upset, and hang up cleanly when the caller says goodbye. That's three tools: `book_appointment`, `transfer_to_front_desk`, and `end_call`.
## Actions Retell performs for you
Set these fields on a `response` event. Retell runs the action **after the content in that response has been fully spoken**, so the field pairs naturally with a closing line.
| Field | Type | What happens |
| --------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `end_call` | boolean | Retell hangs up once the content finishes. If the caller interrupts before the agent finishes speaking, the hangup is discarded. |
| `transfer_number` | string | Cold-transfers the call to that number after the content finishes. Works for Retell numbers and imported numbers. On custom telephony via dial-to-SIP, implement transfer yourself. |
| `show_transferee_as_caller` | boolean | On transfer, shows the original caller's number to the transferee instead of the Retell number. Defaults to `false`. |
| `digit_to_press` | string | Sends DTMF tones for the given digits after the content finishes. Usually paired with empty content. |
| `no_interruption_allowed` | boolean | The caller can't interrupt this content. Retell drops interruption sensitivity to 0 for the duration, then restores your agent's setting. |
`end_call`, `transfer_number`, and `digit_to_press` are mutually exclusive. Retell runs at most one per `response_id`, and `end_call` takes precedence, then `transfer_number`. Set only the one you want.
## End the call
The simplest useful tool. Define a function with a `message` parameter so the model supplies a goodbye line. Most providers return either a tool call or text, not both, so without that parameter the agent hangs up in silence.
This replaces `draftResponse` from [connecting your LLM](/integrate-llm/integrate-llm#stream-the-response). It keeps that version's two guarantees, and both matter more now than they did before: `isStale` stops work on a response Retell has already discarded, and the `finally` block closes the response even when the model errors. Your OpenAI client and prompt constants don't change.
```typescript llm.ts theme={"dark"}
const tools: OpenAI.Chat.ChatCompletionTool[] = [
{
type: "function",
function: {
name: "end_call",
description: "End the call. Only when the caller has clearly said goodbye.",
parameters: {
type: "object",
properties: {
message: {
type: "string",
description: "What to say before hanging up.",
},
},
required: ["message"],
},
},
},
];
export interface ToolResult {
id: string;
name: string;
arguments: string;
result: string;
}
function buildMessages(
request: ResponseRequiredRequest,
toolResult?: ToolResult,
) {
const now = new Date().toLocaleString("en-US", { timeZone: TIME_ZONE });
const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{
role: "system",
content: `${SYSTEM_PROMPT}\n\nThe current date and time is ${now} (${TIME_ZONE}).`,
},
];
for (const utterance of request.transcript) {
messages.push({
role: utterance.role === "agent" ? "assistant" : "user",
content: utterance.content,
});
}
if (request.interaction_type === "reminder_required") {
messages.push({
role: "user",
content: "(The caller has gone quiet. Check in briefly.)",
});
}
// Replay the tool call and its result so the model answers from it.
if (toolResult) {
messages.push({
role: "assistant",
content: null,
tool_calls: [
{
id: toolResult.id,
type: "function",
function: {
name: toolResult.name,
arguments: toolResult.arguments,
},
},
],
});
messages.push({
role: "tool",
tool_call_id: toolResult.id,
content: toolResult.result,
});
}
return messages;
}
export async function draftResponse(
request: ResponseRequiredRequest,
ws: WebSocket,
isStale: () => boolean,
toolResult?: ToolResult,
) {
// Set once this call has closed the response, or abandoned a stale one.
let responseClosed = false;
try {
const stream = await openai.chat.completions.create({
model: "gpt-4.1-mini",
messages: buildMessages(request, toolResult),
stream: true,
temperature: 0,
max_tokens: 200,
tools,
});
let toolCallId = "";
let toolName = "";
let toolArguments = "";
for await (const chunk of stream) {
if (isStale()) {
responseClosed = true;
stream.controller.abort();
return;
}
const delta = chunk.choices[0]?.delta;
if (!delta) continue;
const toolCall = delta.tool_calls?.[0];
if (toolCall) {
// The id and name arrive once, then arguments stream in as JSON fragments.
if (toolCall.id) toolCallId = toolCall.id;
if (toolCall.function?.name) toolName = toolCall.function.name;
toolArguments += toolCall.function?.arguments ?? "";
} else if (delta.content) {
ws.send(
JSON.stringify({
response_type: "response",
response_id: request.response_id,
content: delta.content,
content_complete: false,
}),
);
}
}
if (toolName === "end_call") {
const { message } = JSON.parse(toolArguments);
ws.send(
JSON.stringify({
response_type: "response",
response_id: request.response_id,
content: message,
content_complete: true,
end_call: true,
}),
);
responseClosed = true;
}
} catch (err) {
console.error("LLM stream failed:", err);
} finally {
// Close the response even when the model errored, or the agent stays silent.
if (!responseClosed) {
ws.send(
JSON.stringify({
response_type: "response",
response_id: request.response_id,
content: "",
content_complete: true,
}),
);
}
}
}
```
```python llm.py theme={"dark"}
import json
TOOLS = [
{
"type": "function",
"function": {
"name": "end_call",
"description": "End the call. Only when the caller has clearly said goodbye.",
"parameters": {
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "What to say before hanging up.",
},
},
"required": ["message"],
},
},
},
]
async def draft_response(request: ResponseRequiredRequest, is_stale: Callable[[], bool]):
response_closed = False
try:
stream = await openai.chat.completions.create(
model="gpt-4.1-mini",
messages=build_messages(request),
stream=True,
temperature=0,
max_tokens=200,
tools=TOOLS,
)
tool_call_id = ""
tool_name = ""
tool_arguments = ""
async for chunk in stream:
if is_stale():
await stream.close()
return
if not chunk.choices:
continue
delta = chunk.choices[0].delta
if delta.tool_calls:
tool_call = delta.tool_calls[0]
# The id and name arrive once, then arguments stream in as JSON fragments.
if tool_call.id:
tool_call_id = tool_call.id
if tool_call.function and tool_call.function.name:
tool_name = tool_call.function.name
if tool_call.function:
tool_arguments += tool_call.function.arguments or ""
elif delta.content:
yield ResponseResponse(
response_id=request.response_id,
content=delta.content,
content_complete=False,
)
if tool_name == "end_call":
yield ResponseResponse(
response_id=request.response_id,
content=json.loads(tool_arguments)["message"],
content_complete=True,
end_call=True,
)
response_closed = True
except Exception as err:
print(f"LLM stream failed: {err}")
# Close the response even when the model errored, or the agent stays silent.
if not response_closed:
yield ResponseResponse(
response_id=request.response_id,
content="",
content_complete=True,
)
```
Set `temperature: 0` when the model has tools available. Lower temperature meaningfully improves how reliably the model picks the right function and formats its arguments.
## Do work while the agent talks
Ending a call is easy because nothing happens afterward. Booking an appointment is the harder, more common shape: say something so the caller isn't listening to silence, do the work, then say what happened.
`content_complete` is what makes this work. Send the holding line with `content_complete: false`. The agent speaks it and Retell keeps the response open, waiting for more. Do your work, then feed the result back to the model and stream the real answer under the same `response_id`.
The rest of this section is Node.js, but the sequence of events is the same in any language.
Declare the tool alongside `end_call`. Give it a `message` parameter for the holding line, the same way `end_call` carries the goodbye:
```typescript llm.ts theme={"dark"}
{
type: "function",
function: {
name: "book_appointment",
description: "Book or move an appointment once you have both a date and a time.",
parameters: {
type: "object",
properties: {
date: { type: "string", description: "ISO date, for example 2026-08-04." },
time: { type: "string", description: "24-hour time, for example 10:00." },
message: {
type: "string",
description: "A short line to say while the booking runs.",
},
},
required: ["date", "time", "message"],
},
},
},
```
Your booking function should return a failure as data rather than throwing. The model can offer another slot if it's told the slot was taken; it can't do anything with an exception.
```typescript llm.ts theme={"dark"}
async function bookAppointment(date: string, time: string) {
const res = await fetch("https://your-api.example.com/appointments", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ date, time }),
});
if (!res.ok) {
return { status: "failed", reason: "That slot is no longer available." };
}
return { status: "confirmed", ...(await res.json()) };
}
```
Then handle the tool call next to the `end_call` branch in `draftResponse`:
```typescript llm.ts theme={"dark"}
if (toolName === "book_appointment") {
const args = JSON.parse(toolArguments);
// Speak the holding line, but keep the response open. The trailing space keeps
// the follow-up from running into it in the transcript.
ws.send(
JSON.stringify({
response_type: "response",
response_id: request.response_id,
content: `${args.message} `,
content_complete: false,
}),
);
// Skip the write if Retell already superseded this response. This only catches a
// supersede that has already landed, so bookAppointment still needs to be idempotent.
if (isStale()) {
responseClosed = true;
return;
}
// Tell Retell a tool is running, so it shows up in the transcript.
ws.send(
JSON.stringify({
response_type: "tool_call_invocation",
tool_call_id: toolCallId,
name: "book_appointment",
arguments: toolArguments,
}),
);
const result = await bookAppointment(args.date, args.time);
ws.send(
JSON.stringify({
response_type: "tool_call_result",
tool_call_id: toolCallId,
content: JSON.stringify(result),
successful: result.status === "confirmed",
}),
);
// Feed the result back to the model and stream the follow-up. The nested call
// closes the response in its own finally block.
responseClosed = true;
await draftResponse(request, ws, isStale, {
id: toolCallId,
name: "book_appointment",
arguments: toolArguments,
result: JSON.stringify(result),
});
return;
}
```
While a response is open, Retell waits rather than filling the gap itself. It won't ask for a reminder or a new response until you send `content_complete: true`, so the only thing that can cut your work short is the caller speaking.
## Transfer and press digits
Both are `response` fields, so they follow the same pattern as `end_call`: the agent finishes speaking, then Retell acts.
```typescript Transfer to a human theme={"dark"}
ws.send(
JSON.stringify({
response_type: "response",
response_id: request.response_id,
content: "Let me get you to the front desk. One moment.",
content_complete: true,
transfer_number: "+14155550123",
show_transferee_as_caller: true,
}),
);
```
```typescript Press IVR digits theme={"dark"}
ws.send(
JSON.stringify({
response_type: "response",
response_id: request.response_id,
content: "",
content_complete: true,
digit_to_press: "1",
}),
);
```
Transfers initiated this way are cold transfers: the call is handed off and your agent drops out. For warm transfers or transfer logic on your own telephony, run it from your server.
## Record tool calls in the transcript
Retell doesn't see your tool calls, so by default they're invisible in the call record. Send `tool_call_invocation` and `tool_call_result` events and Retell weaves them into the transcript at the exact word where they happened.
That pays off in two places:
* **After the call**, `transcript_with_tool_calls` in the [Get Call API response](/api-references/get-call) shows what the agent did and when, not just what it said. This is what makes a custom LLM call debuggable.
* **During the call**, if you also set `transcript_with_tool_calls: true` in your `config` event, Retell includes the woven transcript in every `update_only` and `response_required` event. Retell then keeps that history for you, though you still map it into your provider's message format yourself.
Send the invocation as the tool starts and the result when it returns, using the same `tool_call_id` for both:
```json theme={"dark"}
{
"response_type": "tool_call_invocation",
"tool_call_id": "call_a1b2c3",
"name": "book_appointment",
"arguments": "{\"date\": \"2026-08-14\", \"time\": \"10:00\"}"
}
{
"response_type": "tool_call_result",
"tool_call_id": "call_a1b2c3",
"content": "Booked for Aug 14 at 10am with Dr. Chen.",
"successful": true
}
```
`arguments` is a stringified JSON object, and `content` is a plain string, so stringify structured results yourself. `successful` is optional and marks the outcome in the transcript. Leaving it out doesn't mean the call failed, it means you didn't say, so set it explicitly when you care about telling a failed tool call from a successful one after the fact.
## Going to production
The examples above are deliberately minimal. A few things they don't handle that a live agent needs:
* **Don't double-book.** This happens on ordinary calls: Retell asks for a response, discards it when the caller keeps talking, and asks again, so your tool runs twice for a single request. **An idempotency key is the only real fix.** The `isStale()` check above helps only when the newer request has already arrived, and a fast tool commits its write a second or two before that happens. Derive the key from the call ID plus the tool arguments, which are usually identical across the duplicate calls, and make the repeat a no-op.
* **Track state, don't rely on the prompt.** Once past two or three tools, a state machine that controls which tools and prompt are active at each step beats hoping the model reads the transcript correctly. See [best practices](/integrate-llm/llm-best-practice).
* **Report failures out loud.** When a tool errors, send the failure back to the model as a tool result, and mark it with `successful: false`, so the agent offers an alternative instead of going quiet.
## FAQ
You sent `end_call: true` with empty content, or the model returned a tool call with no `message` parameter to speak. Retell ends the call as soon as the content in that response finishes, and empty content finishes immediately. Give the tool a `message` parameter and put it in `content`.
Most likely the caller interrupted before the agent finished speaking, which discards the pending action. Otherwise, check that you didn't set both `end_call` and `transfer_number` on the same response; only `end_call` runs. Setting `no_interruption_allowed: true` on the closing line prevents the interruption case.
Retell asked for a response, discarded it when the caller kept talking, and asked again with a new `response_id`. Your code ran the tool on both, usually with identical arguments. This is the most common way a custom LLM agent double-books, and no setting turns it off. Make the write idempotent, keyed on the call ID plus the arguments. Checking `response_id` first helps, but only when the newer request has already arrived.
No, they're optional bookkeeping. Skip them and your tools still run; they just won't appear in the call transcript, which makes debugging a failed call much harder. Send them.
## Next steps
* [Best practices](/integrate-llm/llm-best-practice) for keeping latency low once tools are in the loop
* [Troubleshooting](/integrate-llm/troubleshooting) when the agent goes silent or the call drops
* [LLM WebSocket reference](/api-references/llm-websocket) for every field on every event
# Connect your LLM
Source: https://docs.retellai.com/integrate-llm/integrate-llm
Stream LLM completions to a Retell voice agent over the LLM WebSocket, build prompts from the live transcript, and cancel responses Retell discards.
Your server answers Retell's requests with a hardcoded sentence after [setting up the WebSocket](/integrate-llm/setup-websocket-server). Now generate those answers with an LLM.
Two parts need care: stream the output so the agent starts speaking sooner, and stop generating responses Retell has already discarded so you don't pay for tokens nobody hears.
## Why time to first sentence matters
Retell starts speaking as soon as it has your first complete sentence. So the caller's wait is your **time to first sentence** (time to first token plus however long the model takes to finish that sentence), not the time to generate the whole response.
## Build the prompt from the transcript
Retell sends the full conversation so far in `transcript`, as a list of utterances with `role` set to `agent` or `user`. Map `agent` to your provider's assistant role and everything else to user.
A voice prompt needs a few things a chat prompt doesn't. Tell the model to write speech (short sentences, no markdown, no lists), because anything else gets read aloud literally. Tell it the transcript comes from speech recognition and may contain errors, so it infers meaning instead of asking the caller to repeat themselves. And give it the current date and time in the appropriate timezone.
When `interaction_type` is `reminder_required`, the caller has gone quiet. Append a short instruction so the model nudges rather than answering a question nobody asked.
## Stream the response
```typescript llm.ts theme={"dark"}
import OpenAI from "openai";
import { WebSocket } from "ws";
import { ResponseRequiredRequest } from "./types";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const AGENT_PROMPT = `You are Ava, the scheduling assistant for Bright Smile Dental.
You book, move, and cancel appointments. Office hours are 9am to 5pm on weekdays.
If a caller asks for anything clinical, offer to take a message for the dentist.`;
const SYSTEM_PROMPT = `You are a voice agent on a live phone call. Everything you write is
spoken aloud, so write the way people talk: short sentences, one idea at a time, no lists,
no markdown, no emoji. Keep replies under 30 words unless the caller asks for detail.
The transcript comes from speech recognition and will contain errors. If you can tell what
the caller meant, respond to that. Only ask them to repeat when you genuinely cannot tell,
and when you do, say it conversationally ("sorry, you cut out there") rather than mentioning
transcription.
## Role
${AGENT_PROMPT}`;
const TIME_ZONE = "America/Los_Angeles";
function buildMessages(request: ResponseRequiredRequest) {
const now = new Date().toLocaleString("en-US", { timeZone: TIME_ZONE });
const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{
role: "system",
content: `${SYSTEM_PROMPT}\n\nThe current date and time is ${now} (${TIME_ZONE}).`,
},
];
for (const utterance of request.transcript) {
messages.push({
role: utterance.role === "agent" ? "assistant" : "user",
content: utterance.content,
});
}
if (request.interaction_type === "reminder_required") {
messages.push({
role: "user",
content: "(The caller has gone quiet. Check in briefly.)",
});
}
return messages;
}
export async function draftResponse(
request: ResponseRequiredRequest,
ws: WebSocket,
isStale: () => boolean,
) {
let abandoned = false;
try {
const stream = await openai.chat.completions.create({
model: "gpt-4.1-mini",
messages: buildMessages(request),
stream: true,
temperature: 0.3,
max_tokens: 200,
});
for await (const chunk of stream) {
if (isStale()) {
abandoned = true;
stream.controller.abort();
break;
}
const content = chunk.choices[0]?.delta?.content;
if (!content) continue;
ws.send(
JSON.stringify({
response_type: "response",
response_id: request.response_id,
content,
content_complete: false,
}),
);
}
} catch (err) {
console.error("LLM stream failed:", err);
} finally {
// Close the response even when the model errored, or the agent stays silent.
if (!abandoned) {
ws.send(
JSON.stringify({
response_type: "response",
response_id: request.response_id,
content: "",
content_complete: true,
}),
);
}
}
}
```
```python llm.py theme={"dark"}
import os
from datetime import datetime
from typing import Callable
from zoneinfo import ZoneInfo
from openai import AsyncOpenAI
from custom_types import ResponseRequiredRequest, ResponseResponse
openai = AsyncOpenAI(api_key=os.environ["OPENAI_API_KEY"])
AGENT_PROMPT = """You are Ava, the scheduling assistant for Bright Smile Dental.
You book, move, and cancel appointments. Office hours are 9am to 5pm on weekdays.
If a caller asks for anything clinical, offer to take a message for the dentist."""
SYSTEM_PROMPT = f"""You are a voice agent on a live phone call. Everything you write is
spoken aloud, so write the way people talk: short sentences, one idea at a time, no lists,
no markdown, no emoji. Keep replies under 30 words unless the caller asks for detail.
The transcript comes from speech recognition and will contain errors. If you can tell what
the caller meant, respond to that. Only ask them to repeat when you genuinely cannot tell,
and when you do, say it conversationally ("sorry, you cut out there") rather than mentioning
transcription.
## Role
{AGENT_PROMPT}"""
TIME_ZONE = "America/Los_Angeles"
def build_messages(request: ResponseRequiredRequest):
now = datetime.now(ZoneInfo(TIME_ZONE)).strftime("%A, %B %d, %Y at %I:%M %p")
messages = [
{
"role": "system",
"content": f"{SYSTEM_PROMPT}\n\nThe current date and time is {now} ({TIME_ZONE}).",
}
]
for utterance in request.transcript:
role = "assistant" if utterance.role == "agent" else "user"
messages.append({"role": role, "content": utterance.content})
if request.interaction_type == "reminder_required":
messages.append(
{
"role": "user",
"content": "(The caller has gone quiet. Check in briefly.)",
}
)
return messages
async def draft_response(request: ResponseRequiredRequest, is_stale: Callable[[], bool]):
try:
stream = await openai.chat.completions.create(
model="gpt-4.1-mini",
messages=build_messages(request),
stream=True,
temperature=0.3,
max_tokens=200,
)
async for chunk in stream:
if is_stale():
await stream.close()
return
if not chunk.choices:
continue
content = chunk.choices[0].delta.content
if not content:
continue
yield ResponseResponse(
response_id=request.response_id,
content=content,
content_complete=False,
)
except Exception as err:
print(f"LLM stream failed: {err}")
# Close the response even when the model errored, or the agent stays silent.
yield ResponseResponse(
response_id=request.response_id,
content="",
content_complete=True,
)
```
## Handle discarded responses
Retell asks for a response as soon as the caller sounds finished, which keeps the silence short by starting your generation before a response is expected. The cost is that some requests get discarded: when the caller was only pausing mid-thought and keeps going, Retell drops the pending response and asks again with a higher `response_id`.
Retell accepts content only for the `response_id` it most recently requested, and only until you mark that response complete. Content sent under an older `response_id` is dropped without an error, so a discarded response can never reach the caller.
Stopping a discarded generation early is still worth doing, because it costs tokens and provider capacity for audio nobody hears. Track the newest `response_id` per connection and stop as soon as yours is superseded.
```typescript Node.js theme={"dark"}
import { draftResponse } from "./llm";
app.ws("/llm-websocket/:call_id", (ws: WebSocket, req: Request) => {
let latestResponseId = 0;
// ... send config and begin message
ws.on("message", (data: RawData, isBinary: boolean) => {
if (isBinary) {
ws.close(1007, "Expected a text frame.");
return;
}
const request = JSON.parse(data.toString());
switch (request.interaction_type) {
case "ping_pong":
ws.send(
JSON.stringify({
response_type: "ping_pong",
timestamp: request.timestamp,
}),
);
break;
case "response_required":
case "reminder_required":
latestResponseId = request.response_id;
draftResponse(
request,
ws,
() => request.response_id !== latestResponseId,
);
break;
}
});
});
```
```python Python theme={"dark"}
import asyncio
import json
from llm import draft_response
@app.websocket("/llm-websocket/{call_id}")
async def llm_websocket(websocket: WebSocket, call_id: str):
await websocket.accept()
latest_response_id = 0
# ... send config and begin message
async def respond(request: ResponseRequiredRequest):
is_stale = lambda: request.response_id != latest_response_id
async for event in draft_response(request, is_stale):
if is_stale():
return
await websocket.send_json(event.model_dump(exclude_none=True))
try:
while True:
request = json.loads(await websocket.receive_text())
interaction_type = request["interaction_type"]
if interaction_type == "ping_pong":
await websocket.send_json(
{
"response_type": "ping_pong",
"timestamp": request["timestamp"],
}
)
elif interaction_type in ("response_required", "reminder_required"):
latest_response_id = request["response_id"]
asyncio.create_task(
respond(ResponseRequiredRequest(**request))
)
except WebSocketDisconnect:
print(f"LLM WebSocket closed for call: {call_id}")
```
## Use call context
Set `call_details: true` in your `config` event and Retell sends the whole call object as soon as the socket opens. It has what you need to personalize the first turn without an API round trip:
* `from_number`, `to_number`, `direction` — who's calling and which way. Phone calls only; these fields are absent on web calls.
* `retell_llm_dynamic_variables` — the [dynamic variables](/build/dynamic-variables) passed when the call was created, such as a customer name pulled from your CRM
* `metadata` — anything you attached at call creation
* `call_id`, `agent_id`, `call_type` — for your own logging and for branching on web versus phone
A common pattern: hold the begin message until `call_details` arrives, then greet by name. Drop the begin message from your connection setup and add this case to the message handler above.
```typescript Node.js theme={"dark"}
case "call_details": {
const name = request.call.retell_llm_dynamic_variables?.customer_name;
ws.send(
JSON.stringify({
response_type: "response",
response_id: 0,
content: name
? `Hi ${name}, thanks for calling Bright Smile Dental.`
: "Thanks for calling Bright Smile Dental. How can I help?",
content_complete: true,
}),
);
break;
}
```
Because `llm_websocket_url` itself supports dynamic variables, you can also route by call. A URL of `wss://your-domain.com/llm-websocket?tenant={{tenant_id}}` reaches your server with the tenant already resolved.
## Try it
Restart your server and start a call from the agent's **Test Audio** panel. The agent should hold a real conversation, start speaking within a beat of you finishing, and stop cleanly when you interrupt.
Check the call in [Call History](/features/session-history) afterward. The transcript and the [latency breakdown](/reliability/check-actual-latency) tell you whether your time to first sentence is where it needs to be. The `llm` field covers your generation including the WebSocket round trip, and `llm_websocket_network_rtt` isolates that round trip, so the difference is your own generation time.
## FAQ
Yes, unless a newer one has already arrived. Retell waits for content until you send `content_complete: true`, so an ignored request leaves the agent silent. Answering with an empty string plus `content_complete: true` is a valid way to say nothing.
Nothing on Retell's side; it just keeps waiting. Always send a final event with `content_complete: true` in a `finally` block, as the code above does. For provider outages, consider a fallback model or a spoken apology so the call degrades instead of going dead.
Yes, and it's often better. Retell sends the full transcript every time, but nothing stops you from keeping your own per-call state keyed on the call ID from the URL: retrieved documents, tool results, a state machine position. Use the transcript as the source of truth for what was said, and your own state for everything else.
That's turn-taking, which Retell controls, not your LLM. Tune `responsiveness` and `interruption_sensitivity` in the agent's [speech settings](/build/single-multi-prompt/configure-basic-settings). You can also change them mid-call by sending an [`update_agent` event](/api-references/llm-websocket#update-agent-event), which is useful for making the agent less eager while it waits on a slow lookup.
## Next step
[Add function calling](/integrate-llm/integrate-function-calling) so the agent can book appointments, transfer to a human, and end the call.
# Best practices
Source: https://docs.retellai.com/integrate-llm/llm-best-practice
Keep a Retell custom LLM agent fast and reliable: cut time to first sentence, write prompts for speech, control tool accuracy, and survive dropped connections.
How a custom LLM agent sounds on a real call depends less on the model than on how fast the first sentence arrives, how the prompt is written for speech, and what happens when something fails mid-call.
## Optimize time to first sentence
Retell starts speaking as soon as it has your first complete sentence. So the number that matters is **time to first token plus the time to finish that first sentence**, not total generation time.
* **Stream, don't buffer.** Sending the full response in one event puts the entire generation in the caller's silence.
* **Run close to Retell.** Retell's primary infrastructure is in the US. Host your server and pick your model region accordingly, since a cross-region round trip adds to every turn.
* **Watch provider-side preprocessing.** Anything that runs before generation adds to first-token time. On Azure OpenAI, for example, [content filtering](https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/content-filter) runs synchronously by default; switching it to asynchronous mode or turning it off returns responses noticeably faster.
* **Check your quota before you scale.** Provider rate limits produce queuing that looks exactly like a slow model.
## Write prompts for speech
Everything the model writes gets read aloud, which changes what a good prompt looks like.
* **Ban formatting.** No markdown, no bullet lists, no emoji, no headers. Say so explicitly, because models default to writing for a screen.
* **Cap the length.** Long turns feel like being lectured and are painful to interrupt. Ask for one idea per turn.
* **Plan for transcription errors.** The transcript comes from speech recognition and will contain mistakes. Tell the model to infer the caller's meaning rather than asking them to repeat, and when it must ask, to do it conversationally ("sorry, you cut out") instead of mentioning transcription.
* **Allow a little imperfection.** Occasional filler words and contractions make the agent sound human; perfectly polished prose doesn't.
* **Keep prompts short.** Longer prompts cost first-token time and often *reduce* instruction-following. If you have a large knowledge base, retrieve the few relevant chunks per turn instead of pasting everything.
The [prompt engineering guide](/build/prompt-engineering-guide) covers voice prompting in more depth. It's written for Retell agents, but the guidance applies to any model.
## Improve tool accuracy
* **Describe when *not* to call.** "End the call only when the caller has clearly said goodbye" prevents far more misfires than a description of what the function does.
* **Give tools a `message` parameter.** Most providers return either a tool call or text, not both. Without a parameter carrying something to say, the agent goes silent exactly when the caller is waiting.
* **Constrain the tool set.** Ten tools available at once invites wrong choices. Expose only what's reachable from the current point in the conversation.
## Track state on your server
Past a couple of tools, asking the model to infer where it is from the transcript gets unreliable. Keep explicit per-call state on your server, keyed on the call ID from the WebSocket URL, and use it to control what the model can do at each step, like an IVR tree with an LLM at each node.
At each state you decide the prompt, which tools are exposed, and which transitions are legal. That gets you shorter prompts, fewer wrong tool calls, and protection against duplicate side effects.
## Build for failure
* **Turn on `auto_reconnect`.** Retell already rebuilds a connection that closes abnormally. What keepalives add is catching a half-dead socket that never sends a close frame, which otherwise hangs the call until it times out. The cost is echoing `ping_pong`.
* **Never leave a response unclosed.** Send `content_complete: true` in a `finally` block. If your provider errors or times out and you skip it, the turn never finishes: the agent stops speaking, nothing errors, and it only recovers once the caller speaks again.
* **Have a fallback for provider outages.** A secondary model, or at minimum a spoken apology and a transfer, degrades better than silence.
* **Make side effects idempotent.** Retell discards responses when the caller keeps talking and asks again, so a side-effecting tool routinely runs twice for one request. Key the write on the call ID plus the arguments so the repeat is a no-op. Checking whether the `response_id` is still current helps too, but it can't catch a write that committed before the newer request arrived. See [don't double-book](/integrate-llm/integrate-function-calling#going-to-production).
* **Log the raw frames.** Store what you received and sent, keyed on call ID, so you can line your logs up against Retell's [Detail Logs](/features/session-history) when something goes wrong.
## Related
* [Troubleshooting](/integrate-llm/troubleshooting) — disconnection reasons and the silent-failure traps
* [Connect your LLM](/integrate-llm/integrate-llm) — streaming and discarded-response handling
* [LLM WebSocket reference](/api-references/llm-websocket) — every field on every event
# Custom LLM overview
Source: https://docs.retellai.com/integrate-llm/overview
Run your own LLM behind a Retell voice agent. Retell handles telephony, transcription, and turn-taking while your WebSocket server generates each reply.
With a custom LLM, you own response generation. Retell handles the call itself (telephony, transcription, turn-taking, speech synthesis) and opens a WebSocket to your server for each call. Retell sends you the live transcript and asks for responses; you stream back what the agent should say.
We recommend [single prompt](/build/prompt) or [conversation flow](/build/conversation-flow/overview) for most agents. They get the newest features, and they come already tuned for things like latency.
## When to use it
[Single prompt](/build/prompt) and [conversation flow](/build/conversation-flow/overview) agents give you built-in tool calling, a [knowledge base](/build/knowledge-base), warm transfers, and a testing playground with no server to run. They're the right choice for most agents.
Reach for a custom LLM when you need something those frameworks can't give you:
* **Compliance constraints.** Prompts and transcripts must be processed inside your own infrastructure, or by a model you host.
* **A model Retell doesn't offer.** A fine-tuned model, a self-hosted open-weights model, or a provider outside the supported options.
* **Response logic beyond what Retell's frameworks support.** Your own retrieval pipeline instead of Retell's [knowledge base](/build/knowledge-base), a multi-step agent loop that runs before the agent speaks, or orchestration that already lives in your own code.
You take on latency, uptime, and reconnection handling. You also give up the features that assume Retell generates the responses:
* The [LLM playground](/test/llm-playground) can't test a custom LLM agent, and [simulation and batch testing](/test/test-overview) reject them. Web and phone calls are the only way to test.
* [Agent transfer](/build/single-multi-prompt/transfer-agent) can't target a custom LLM agent.
* Your own tool calls only appear in the transcript if you [report them yourself](/integrate-llm/integrate-function-calling#record-tool-calls-in-the-transcript). Retell records the actions it runs (`end_call`, `transfer_call`, `press_digit`) automatically.
## How a call flows
One WebSocket is opened per call, to `llm_websocket_url` with the call ID appended as the last path segment. Retell drives the conversation and tells you when it needs something.
```mermaid theme={"dark"}
sequenceDiagram
autonumber
participant C as Caller
participant R as Retell
participant S as Your server
C->>R: Call starts
R->>S: Opens WebSocket at llm_websocket_url/{call_id}
S->>R: config — auto_reconnect, call_details (optional)
R->>S: call_details — the full call object (if enabled)
S->>R: response, response_id 0 — the begin message
R->>C: Speaks the begin message
C->>R: "I'd like to move my appointment"
R->>S: update_only — live transcript and turn taking
R->>S: response_required, response_id 1
S->>R: response chunks, last one with content_complete true
R->>C: Speaks the response
Note over R,S: Keepalives every 2s while auto_reconnect is on
```
Retell decides when the agent speaks, so not every response you generate gets used. If the caller keeps talking, Retell discards the response it asked for and asks again with a new `response_id`.
## What your server implements
Retell tags every message it sends with `interaction_type`:
| `interaction_type` | You must | What it carries |
| ------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `response_required` | Reply with a `response` | The live transcript and a `response_id`. Retell is waiting to speak. |
| `reminder_required` | Reply with a `response` | Same shape, but the caller has gone quiet and Retell wants a nudge. |
| `update_only` | Nothing | Transcript updates and `turntaking` changes. Read it or ignore it. |
| `call_details` | Nothing | The full call object, including [dynamic variables](/build/dynamic-variables) and metadata. Only sent if you ask for it in `config`. |
| `ping_pong` | Echo it back | A keepalive timestamp. Only sent if you set `auto_reconnect`. |
You tag everything you send with `response_type`:
| `response_type` | Required | What it does |
| ------------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `response` | Yes | Streams content for the agent to speak. Can also end the call, transfer, or press digits. |
| `config` | No | Sent once on connect to turn on keepalives, call details, and tool-call transcripts. |
| `ping_pong` | If `auto_reconnect` is on | Echoes the keepalive so Retell knows you're alive. |
| `agent_interrupt` | No | Makes the agent speak immediately, cutting off whoever is talking. Also cancels any response still in flight. |
| `tool_call_invocation` / `tool_call_result` | No | Records your tool calls in the call transcript. |
| `update_agent` | No | Changes responsiveness, interruption sensitivity, or reminder timing mid-call. |
| `metadata` | No | Forwards arbitrary data to a web call frontend. |
Always set `response_type` on messages you send. Retell treats a message with no `response_type` as a `response` for backward compatibility, but that fallback is the reason a malformed event fails silently instead of erroring.
The [LLM WebSocket reference](/api-references/llm-websocket) has the full field-by-field spec for every event above.
## Example servers
Runnable reference implementations, both with function calling:
* [Node.js (Express)](https://github.com/RetellAI/retell-custom-llm-node-demo) — OpenAI, Azure OpenAI, OpenRouter
* [Python (FastAPI)](https://github.com/RetellAI/retell-custom-llm-python-demo) — OpenAI, Claude
These repos might be outdated already. Follow the guides in this section wherever the two differ.
## Next steps
Get an agent talking with a hardcoded response, before adding an LLM.
Stream real completions back, and handle discarded responses correctly.
Let the agent book, transfer, press digits, and end the call.
Keep time-to-first-sentence low enough to sound natural.
# Set up a custom LLM WebSocket server for your Retell agent
Source: https://docs.retellai.com/integrate-llm/setup-websocket-server
Build the LLM WebSocket server that a Retell custom LLM agent connects to: handle config and begin messages, respond to response_required events.
By the end of this guide you'll have a Retell agent that greets you and replies to everything you say with a fixed sentence. No LLM yet. That proves the connection works before you add a model. [Connecting your LLM](/integrate-llm/integrate-llm) is the next guide.
Code below is Node.js (Express) and Python (FastAPI). Any stack that serves WebSockets works, since the protocol is the same in every language.
## Before you start
* **A server that can hold a WebSocket open.** Serverless and edge runtimes generally can't. Vercel edge functions and Lambda-style handlers won't work. Use a long-running process (a container, a VM, or a persistent Node/Python server).
* **TLS in production.** Use `wss://` or `https://`. Retell treats them the same, mapping `https:` to `wss:`. Plain `ws://` and `http://` work too, but send transcripts unencrypted.
* **Read the protocol.** The [LLM WebSocket reference](/api-references/llm-websocket) is the field-by-field spec. This guide covers only the fields you need to get a call working.
Install the dependencies for the code in this section, including the LLM client you'll add in the next guide:
```bash Node.js theme={"dark"}
npm install express express-ws ws openai
npm install -D typescript tsx @types/express @types/express-ws @types/ws @types/node
```
```bash Python theme={"dark"}
pip install "fastapi[standard]" openai
```
Retell sends no auth headers on this WebSocket. To restrict who can connect, allowlist Retell's outbound IP address `100.20.5.228`, or put a secret in the URL you configure (a path segment or query string) and reject connections that don't carry it.
Retell connects to your `llm_websocket_url` with the call ID appended as the final path segment. If you configure `wss://your-domain.com/llm-websocket`, Retell connects to `wss://your-domain.com/llm-websocket/{call_id}`. Capture that segment. It's how you tie a connection to a call.
```typescript Node.js theme={"dark"}
import express, { Request } from "express";
import expressWs from "express-ws";
import { RawData, WebSocket } from "ws";
const app = expressWs(express()).app;
const PORT = 3000;
app.ws("/llm-websocket/:call_id", (ws: WebSocket, req: Request) => {
const callId = req.params.call_id;
console.log("LLM WebSocket open for call:", callId);
ws.on("error", (err) => console.error("LLM WebSocket error:", err));
ws.on("close", () => console.log("LLM WebSocket closed for call:", callId));
ws.on("message", (data: RawData, isBinary: boolean) => {
if (isBinary) {
ws.close(1007, "Expected a text frame.");
return;
}
console.log(JSON.parse(data.toString()));
});
});
app.listen(PORT, () => console.log(`Listening on ${PORT}`));
```
```python Python theme={"dark"}
import json
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
app = FastAPI()
@app.websocket("/llm-websocket/{call_id}")
async def llm_websocket(websocket: WebSocket, call_id: str):
await websocket.accept()
print(f"LLM WebSocket open for call: {call_id}")
try:
while True:
print(json.loads(await websocket.receive_text()))
except WebSocketDisconnect:
print(f"LLM WebSocket closed for call: {call_id}")
```
Save this as `server.ts` or `server.py`, then run it with `npx tsx server.ts` or `uvicorn server:app --port 3000`. This guide adds `types.ts` or `custom_types.py` next to it, and the [next one](/integrate-llm/integrate-llm) adds `llm.ts` or `llm.py`. Keep all of them in the same directory, since the imports in each are flat.
Every message is a text frame containing stringified JSON. Retell never sends binary frames, and it closes the connection with code `1007` if you send one.
Your server speaks first. Send two messages as soon as the connection opens:
A **config** event turns on optional protocol features. `auto_reconnect` starts the keepalive exchange so a dropped connection gets rebuilt instead of killing the call. `call_details` makes Retell push the whole call object to you immediately, saving you a [Get Call API](/api-references/get-call) round trip.
A **response** event with `response_id: 0` is the begin message, the first thing the agent says. Set `content` to an empty string to have the agent wait for the caller to speak first.
```typescript Node.js theme={"dark"}
const BEGIN_MESSAGE = "How may I help you?";
ws.send(
JSON.stringify({
response_type: "config",
config: {
auto_reconnect: true,
call_details: true,
},
}),
);
ws.send(
JSON.stringify({
response_type: "response",
response_id: 0,
content: BEGIN_MESSAGE,
content_complete: true,
}),
);
```
```python Python theme={"dark"}
BEGIN_MESSAGE = "How may I help you?"
await websocket.send_json(
{
"response_type": "config",
"config": {
"auto_reconnect": True,
"call_details": True,
},
}
)
await websocket.send_json(
{
"response_type": "response",
"response_id": 0,
"content": BEGIN_MESSAGE,
"content_complete": True,
}
)
```
Setting `auto_reconnect: true` obligates you to echo `ping_pong` events, which you'll add in the next step. If Retell goes 5 seconds without one, it closes the connection and reconnects. After 2 reconnects it gives up and ends the call with `error_llm_websocket_lost_connection`.
If your greeting depends on call data, such as a caller's name from a dynamic variable, send `config` immediately but hold the begin message until the `call_details` event arrives.
Now handle what Retell sends. Only `response_required` and `reminder_required` need a spoken answer, and `ping_pong` needs an echo. `update_only` and `call_details` need no reply at all.
Reply with the same `response_id` Retell asked with. A response carrying any other `response_id` is discarded silently.
```typescript Node.js theme={"dark"}
ws.on("message", (data: RawData, isBinary: boolean) => {
if (isBinary) {
ws.close(1007, "Expected a text frame.");
return;
}
const request = JSON.parse(data.toString());
switch (request.interaction_type) {
case "ping_pong":
ws.send(
JSON.stringify({
response_type: "ping_pong",
timestamp: request.timestamp,
}),
);
break;
case "response_required":
case "reminder_required":
ws.send(
JSON.stringify({
response_type: "response",
response_id: request.response_id,
content: "I am sorry, can you say that again?",
content_complete: true,
}),
);
break;
case "call_details":
console.log("Call details:", request.call);
break;
case "update_only":
// Live transcript and turn taking. Nothing to answer.
break;
}
});
```
```python Python theme={"dark"}
while True:
request = json.loads(await websocket.receive_text())
interaction_type = request["interaction_type"]
if interaction_type == "ping_pong":
await websocket.send_json(
{
"response_type": "ping_pong",
"timestamp": request["timestamp"],
}
)
elif interaction_type in ("response_required", "reminder_required"):
await websocket.send_json(
{
"response_type": "response",
"response_id": request["response_id"],
"content": "I am sorry, can you say that again?",
"content_complete": True,
}
)
elif interaction_type == "call_details":
print("Call details:", request["call"])
# update_only carries the live transcript. Nothing to answer.
```
Set `content_complete: true` on the last event of a response. Retell keeps waiting for more content until it sees that flag, so a response that never completes leaves the turn hanging: the agent stops speaking, no error is raised, and it only recovers once the caller speaks again.
Retell has to reach your server over the public internet. For security, it blocks connections to localhost, private IP ranges, and cloud metadata addresses, so tunnel a local server rather than pointing the agent at it directly.
* **Deployed:** use your own domain, `wss://your-domain.com/llm-websocket`
* **Local:** tunnel it with [ngrok](https://ngrok.com/) (`ngrok http 3000`) and use the forwarding host, `wss://xxxx.ngrok-free.app/llm-websocket`
Create a Custom LLM agent in the dashboard, then paste the URL into the **Custom LLM URL** field on the agent.
The URL supports [dynamic variables](/build/dynamic-variables), so `wss://your-domain.com/llm-websocket?tenant={{tenant_id}}` resolves per call. Use it to route calls to different backends, or to pass a shared secret.
Open the **Test Audio** tab on the agent and click **Run Test**. It's the only test surface a custom LLM agent has: the **Test LLM** tab is hidden for them, and [simulation testing](/test/llm-simulation-testing) rejects them.
You should hear "How may I help you?", and every time you speak the agent should answer "I am sorry, can you say that again?". Your server logs should show a `call_details` event, then `update_only` events as you talk, then a `response_required` event for each reply.
## Message types
Typed definitions for the events in this guide. Reuse them across the rest of the section.
```typescript types.ts theme={"dark"}
export interface Utterance {
role: "agent" | "user";
content: string;
}
// Retell -> your server
export interface PingPongRequest {
interaction_type: "ping_pong";
timestamp: number;
}
export interface CallDetailsRequest {
interaction_type: "call_details";
call: Record;
}
export interface UpdateOnlyRequest {
interaction_type: "update_only";
transcript: Utterance[];
turntaking?: "agent_turn" | "user_turn";
}
export interface ResponseRequiredRequest {
interaction_type: "response_required" | "reminder_required";
transcript: Utterance[];
response_id: number;
}
export type CustomLlmRequest =
| PingPongRequest
| CallDetailsRequest
| UpdateOnlyRequest
| ResponseRequiredRequest;
// Your server -> Retell
export interface ConfigResponse {
response_type: "config";
config: {
auto_reconnect?: boolean;
call_details?: boolean;
transcript_with_tool_calls?: boolean;
};
}
export interface PingPongResponse {
response_type: "ping_pong";
timestamp: number;
}
export interface ResponseResponse {
response_type: "response";
response_id: number;
content: string;
content_complete: boolean;
no_interruption_allowed?: boolean;
end_call?: boolean;
transfer_number?: string;
show_transferee_as_caller?: boolean;
digit_to_press?: string;
}
export type CustomLlmResponse =
| ConfigResponse
| PingPongResponse
| ResponseResponse;
```
```python custom_types.py theme={"dark"}
from typing import Any, Dict, List, Literal, Optional, Union
from pydantic import BaseModel
class Utterance(BaseModel):
role: Literal["agent", "user"]
content: str
# Retell -> your server
class PingPongRequest(BaseModel):
interaction_type: Literal["ping_pong"]
timestamp: int
class CallDetailsRequest(BaseModel):
interaction_type: Literal["call_details"]
call: Dict[str, Any]
class UpdateOnlyRequest(BaseModel):
interaction_type: Literal["update_only"]
transcript: List[Utterance]
turntaking: Optional[Literal["agent_turn", "user_turn"]] = None
class ResponseRequiredRequest(BaseModel):
interaction_type: Literal["response_required", "reminder_required"]
transcript: List[Utterance]
response_id: int
CustomLlmRequest = Union[
PingPongRequest, CallDetailsRequest, UpdateOnlyRequest, ResponseRequiredRequest
]
# Your server -> Retell
class ConfigResponse(BaseModel):
response_type: Literal["config"] = "config"
config: Dict[str, bool]
class PingPongResponse(BaseModel):
response_type: Literal["ping_pong"] = "ping_pong"
timestamp: int
class ResponseResponse(BaseModel):
response_type: Literal["response"] = "response"
response_id: int
content: str
content_complete: bool
no_interruption_allowed: Optional[bool] = None
end_call: Optional[bool] = None
transfer_number: Optional[str] = None
show_transferee_as_caller: Optional[bool] = None
digit_to_press: Optional[str] = None
CustomLlmResponse = Union[ConfigResponse, PingPongResponse, ResponseResponse]
```
## FAQ
On the initial connection, Retell makes up to 3 attempts with a 7-second timeout each and 3 seconds between them. If all fail, the call ends with `error_llm_websocket_open`. Mid-call, Retell rebuilds a dropped connection instead of giving up: up to 2 reconnects when keepalives stop arriving, and up to 4 when the socket closes abnormally (code `1006`).
No. Skip it and you get the defaults: no keepalives, no call details, no tool-call transcripts. The begin message alone is enough to start a call. Send `config` first if you send it at all, since Retell acts on it as it arrives.
Not on serverless or edge functions. They can't hold a WebSocket open for the length of a call. Deploy a long-running process instead. Also watch for idle timeouts on hosts that have them: a platform that kills connections at 5 minutes will drop every call that runs longer.
Most often the `response_id` didn't match what Retell asked for, or `content_complete: true` never arrived. Retell also discards a response outright when the caller keeps talking and asks again with a new `response_id`, which is expected. See [handling discarded responses](/integrate-llm/integrate-llm#handle-discarded-responses).
Yes. Connect to it with Postman or `websocat` and send a JSON frame shaped like a `response_required` event. Your server should answer with a `response` event. This separates connectivity problems from protocol problems.
## Next step
Your agent answers with a fixed sentence. [Connect your LLM](/integrate-llm/integrate-llm) to stream real responses.
# Troubleshooting
Source: https://docs.retellai.com/integrate-llm/troubleshooting
Debug a Retell custom LLM integration: map error_llm_websocket disconnection reasons to fixes for silent agents, dropped calls, and WebSocket errors.
Start with the call record. Open the call in [Call History](/features/session-history) and check two things: the **disconnection reason**, and the **Detail Logs**, which contain the LLM WebSocket lifecycle for that call.
Useful log lines to search for:
| Log line | Means |
| ---------------------------------------- | --------------------------------------------------------- |
| `LLM Ws init attempt 0` | Retell is trying to open the connection |
| `LLM ws open` | The connection succeeded |
| `LLM Ws init timeout on attempt N` | Your server didn't accept the connection within 7 seconds |
| `Ws max retry attempt reached` | Retell gave up connecting |
| `LLM ws closed: ` | The connection closed, with the WebSocket close code |
| `Got 1006 and reconnecting` | Abnormal closure; Retell is rebuilding the connection |
| `Error in parsing LLM websocket message` | Retell received something from you it couldn't parse |
## Disconnection reasons
Four disconnection reasons point at the LLM WebSocket. The [full reason table](/reliability/debug-call-disconnect) covers the rest.
| Reason | What happened | Where to look |
| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `error_llm_websocket_open` | Retell never got the connection open. It makes 3 attempts, 7-second timeout each, 3 seconds apart, then gives up. You also get this reason mid-call if your server closes the socket without a status code (`1005`). | The URL itself, DNS, TLS, whether the process is listening, whether the path matches your route. For the mid-call case, whether your close path passes a code. |
| `error_llm_websocket_lost_connection` | Keepalives stopped arriving and Retell used up its reconnects, or a reconnect couldn't reach your server. | Whether you're echoing `ping_pong`, and whether your host or proxy has an idle timeout. |
| `error_llm_websocket_runtime` | The socket errored mid-call, your server closed it with an unexpected code, or it kept closing abnormally (`1006`) until Retell ran out of reconnects. | Unhandled exceptions in your handler, and the close code in `LLM ws closed`. |
| `error_llm_websocket_corrupt_payload` | Close code `1007`. Retell sends this when it receives a binary frame instead of text. | Whether you're sending text frames of stringified JSON, not buffers. |
Closing the socket with code `1000` from your side is treated as a deliberate hangup, so the call ends with `agent_hangup` rather than an error. Don't close with `1000` to signal a problem.
## The agent never speaks
Work down this list in order.
Look for `LLM ws open` in the Detail Logs. If you only see init attempts and timeouts, this is a connectivity problem, not a protocol one. Skip to [connection failures](#connection-failures).
Retell waits for a `response` event with `response_id: 0` before the agent says anything. If your greeting is empty by design, the agent stays silent until the caller speaks.
Retell keeps a response open until it receives an event with `content_complete: true`. Miss that flag and the turn never finishes: the agent stops speaking and no error is raised anywhere. It only recovers when the caller speaks again, which takes over the turn and triggers a fresh request. Send the flag as the final event of every response, including in your error path.
Retell only accepts content for the `response_id` it most recently asked for. Answer with `response_id: 4` when it asked with `5` and your content is dropped silently. Echo back the exact `response_id` from the request.
Retell rejects a `response` event when `content_complete` isn't a boolean, `content` isn't a string, or `response_id` isn't an integer. It logs the problem and keeps the connection open, so the event just vanishes. `"content_complete": "true"` is a string, not a boolean, and is the common version of this mistake.
`Error in parsing LLM websocket message` in the logs means your JSON was malformed or the frame wasn't what Retell expected. Retell logs it and carries on, so the call continues with an agent that says nothing.
## Connection failures
**Check the URL protocol.** Use `wss://` (or `https://`) for a TLS endpoint and `ws://` (or `http://`) for a plain one. Retell maps `https:` to `wss:` and `http:` to `ws:`, so those pairs are interchangeable. What fails is a mismatch: pointing `wss://` or `https://` at a server without TLS never completes the handshake.
**Don't append the call ID.** Configure the base path only: `wss://your-domain.com/llm-websocket`. Retell appends `/{call_id}`. A trailing slash is fine either way, since Retell normalizes it before appending.
**Confirm your route accepts the extra path segment.** Your handler must match a path with the call ID on the end, like `/llm-websocket/:call_id` in Express or `/llm-websocket/{call_id}` in FastAPI. A route registered at exactly `/llm-websocket` won't match what Retell connects to.
**Check that your framework can serve WebSockets at all.** On Python, `pip install fastapi uvicorn` installs no WebSocket library, and the failure looks exactly like a routing bug: uvicorn answers every upgrade request with `404 Not Found` and logs one `No supported WebSocket library detected` warning. Your route is fine. Install `fastapi[standard]`, `uvicorn[standard]`, or `websockets`, then look for `connection open` in the uvicorn log instead of a `404`.
**Test it without a call.** Connect to your full URL with Postman or `websocat` and send a JSON frame shaped like a `response_required` event:
```bash theme={"dark"}
websocat wss://your-domain.com/llm-websocket/test-call-id
```
```json theme={"dark"}
{ "interaction_type": "response_required", "response_id": 1, "transcript": [] }
```
If your server doesn't answer with a `response` event, the problem is in your code, not in Retell's connection. If it does answer, and Retell still can't connect, suspect a firewall or IP allowlist.
**Check your allowlist.** If you've restricted inbound traffic, Retell's outbound IP is `100.20.5.228`.
## The call drops after a few seconds
**Check whether you sent `end_call: true`.** It's easy to attach to the wrong response. Search your logs for it.
**Check whether your runtime can hold a WebSocket.** Serverless and edge functions can't. Vercel edge functions, Lambda-style handlers, and Cloudflare Workers all end the invocation instead of holding the connection. Deploy a long-running process.
**Check whether you're echoing `ping_pong`.** If you set `auto_reconnect: true` in your `config` event, you must send a `ping_pong` event back within 5 seconds of each one Retell sends. Miss the window and Retell closes the connection and reconnects; after 2 reconnects the call ends with `error_llm_websocket_lost_connection`.
A blocking response handler is the usual culprit here. If your code awaits a full LLM generation before reading the next frame, the keepalives queue up behind it and you fall outside the window. Handle each message concurrently.
## The call drops at a round number of minutes
That's an idle or lifetime timeout on the host or proxy in front of your server, not Retell. Common cases: Replit's non-reserved instances time out at 5 minutes, and load balancers, reverse proxies, and API gateways often have their own WebSocket idle timeouts.
Turning on `auto_reconnect` helps, because the 2-second keepalive traffic keeps a purely *idle* timeout from firing. It won't save you from a hard connection lifetime cap. For that, raise the timeout on the proxy.
## Responses get generated but never spoken
Usually correct behavior. Retell asks for a response whenever it thinks the agent's turn has arrived, and discards it if the caller keeps talking. It then asks again with a higher `response_id`. Several requests where only the last one gets spoken is what a normal call looks like.
To stop paying for the discarded ones, see [handling discarded responses](/integrate-llm/integrate-llm#handle-discarded-responses).
## Tool calls don't appear in the transcript
Retell can't see your tool calls. Send `tool_call_invocation` and `tool_call_result` events and it weaves them into the transcript. See [recording tool calls](/integrate-llm/integrate-function-calling#record-tool-calls-in-the-transcript).
## FAQ
Two options, and they combine well. Allowlist Retell's outbound IP `100.20.5.228` at your firewall. And because `llm_websocket_url` supports [dynamic variables](/build/dynamic-variables), you can put a secret in the URL, as a path segment or query string, and reject connections without it.
When Retell detects that the caller spoke but transcribes no words (background noise, a cough, speech too quiet to resolve), it sends the utterance with the content `(unintelligible audio)` instead of an empty string. That way your model knows a turn happened rather than seeing a blank. Pass it through to your prompt; a good voice system prompt handles it by asking the caller to repeat themselves conversationally.
The LLM WebSocket is identical for both, so look at telephony rather than your server. Check the disconnection reason: a `dial_*` or `sip_*` reason means the call never reached the point of needing your LLM. See [debug outbound calls](/reliability/debug-outbound-call).
Yes. Detail Logs record the events in both directions, with transcripts trimmed to the last 2 utterances to keep them readable. Log the raw frames on your side too, keyed on the call ID from the URL, so you can line the two up.
Check the [latency breakdown](/reliability/check-actual-latency) on the call to confirm the delay is yours and not Retell's. For a custom LLM, `llm` covers your generation *including* the WebSocket round trip, and `llm_websocket_network_rtt` isolates that round trip, so subtract one from the other to get your own generation time. Because Retell starts speaking at your first complete sentence, the number to optimize is time to first sentence, not total generation time. See [best practices](/integrate-llm/llm-best-practice).
# Give your AI agent persistent contact memory
Source: https://docs.retellai.com/integrations/build-contact-memory
Map Post Call Extraction fields to contact fields so your Retell AI agent remembers preferences, past issues, and commitments across every conversation.
Contact memory lets your AI agent recall information from previous conversations — preferences, past issues, commitments, and more — without any external database. By mapping [Post Call Extraction](/features/post-call-analysis-overview) fields to [contact](/features/contacts) fields, Retell automatically extracts and stores conversation insights after every call or chat, building a richer contact profile over time.
The next time your agent calls that contact, the accumulated memory is injected into the agent's context as [dynamic variables](/build/dynamic-variables), enabling personalized, context-aware conversations.
## How It Works
Contact memory is built through a pipeline that runs automatically after every conversation:
After each call or chat, your agent's [Post Call Extraction](/features/post-call-analysis-create) configuration extracts key information from the conversation transcript — things like preferences, action items, objections, or any custom fields you define.
The extracted data is written to contact fields using your configured [analysis data mappings](/integrations/crm-mappings#2-analysis-data-mapping-analysis-to-contact). Each mapping specifies an **update mode** that controls how new data combines with existing data.
On the next phone call with the same contact, all contact fields, including the accumulated memory, are automatically injected into the agent's prompt as [dynamic variables](/build/dynamic-variables). Chats write to contact fields but don't receive them as variables.
The **Merge** update mode is the key to building memory. Unlike "Overwrite" (which replaces the old value) or "Fill if empty" (which only writes once), Merge uses an LLM to intelligently combine the existing field value with new information from the latest conversation — deduplicating, reconciling conflicts, and maintaining a coherent, up-to-date record.
## Setup Guide
### Step 1: Define Post Call Extraction Fields
Create Post Call Extraction fields on your agent that extract the information you want your agent to remember. Navigate to your agent's **Post Call Extraction** tab and add fields.
**Example fields for contact memory:**
| Field Name | Type | Description |
| ---------------------- | ---- | ------------------------------------------------------------------------------------ |
| `customer_preferences` | Text | Extract any stated preferences, requirements, or constraints the customer mentioned. |
| `key_topics` | Text | Summarize the main topics discussed and any decisions made. |
| `action_items` | Text | List any follow-up actions, commitments, or next steps agreed upon. |
| `objections` | Text | Capture any concerns, objections, or hesitations the customer raised. |
Write descriptive extraction prompts. The more specific you are about what to extract, the more useful the memory will be. For example, instead of "Summarize the call," use "Extract the customer's stated budget, timeline, and any product preferences mentioned during the conversation."
### Step 2: Create Contact Custom Fields
On the [Contacts](/features/contacts) page, open **Actions → Manage contact fields** and create custom fields to store the accumulated memory. These fields will hold the merged data across all conversations.
**Example contact fields:**
| Field Name | Type | Description (used as the LLM merge instruction) |
| ------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `preferences` | String | A consolidated list of this contact's preferences and requirements across all conversations. When merging, update changed preferences and keep unchanged ones. Deduplicate. Format as a bullet list. |
| `interaction_history` | String | A running summary of all conversations with this contact. Include key topics, decisions, and outcomes. Keep the most recent interaction prominent while preserving important context from earlier conversations. Limit to the 10 most recent interactions. |
| `pending_actions` | String | Outstanding action items and commitments. When merging, mark completed items as done if the new conversation indicates completion. Add new action items. Remove items that are no longer relevant. |
| `objections_and_concerns` | String | A deduplicated list of objections or concerns raised across all conversations. When merging, add new concerns without repeating existing ones. Note if a previously raised concern has been resolved. |
The **field description is critical** when using the Merge update mode. The description acts as an instruction to the LLM that performs the merge — it determines how existing and new values are combined. See [How Merge Works](/integrations/crm-mappings#how-merge-works) for detailed guidance.
### Step 3: Map Analysis Fields to Contact Fields
On the same [contact fields](/features/contacts#define-contact-fields) page, configure **post-call data mappings** for each field. Map each Post Call Extraction field to its corresponding contact field and select the **Merge** update mode, labelled **Accumulate & summarize** in the dashboard.
| Analysis Field | → | Contact Field | Update Mode |
| ---------------------- | - | ------------------------- | ----------- |
| `customer_preferences` | → | `preferences` | Merge |
| `key_topics` | → | `interaction_history` | Merge |
| `action_items` | → | `pending_actions` | Merge |
| `objections` | → | `objections_and_concerns` | Merge |
Analysis data mappings are configured at the **organization level**, not per agent. As long as any agent's Post Call Extraction produces a field with the mapped name, the value will be written to the contact. This means memory accumulates across all agents that interact with the same contact.
### Step 4: Reference Memory in Your Agent Prompt
Use [dynamic variables](/build/dynamic-variables) to inject the accumulated memory into your agent's prompt. When a phone call connects to a known contact (matched by phone number), all contact fields are automatically available.
## Sync Memory to Your CRM
If you have a [CRM integration](/integrations/crm-overview) connected, whether [Salesforce](/integrations/salesforce) or [HubSpot](/integrations/hubspot), you can push the accumulated contact memory back to your CRM using [outbound sync mappings](/integrations/crm-mappings#3-outbound-sync-retell-to-crm). This keeps your CRM records enriched with insights from every AI conversation.
| Retell Contact Field | → | CRM Field |
| --------------------- | - | -------------------------------------------------- |
| `preferences` | → | `Customer_Preferences__c` / `customer_preferences` |
| `interaction_history` | → | `AI_Interaction_Notes__c` / `ai_interaction_notes` |
| `pending_actions` | → | `Pending_Actions__c` / `pending_actions` |
This creates a complete loop: CRM data flows into Retell, the agent uses it during conversations, Post Call Extraction extracts new insights, those insights merge into the contact profile, and the updated profile syncs back to your CRM.
## Best Practices
* **Keep memory fields focused.** A single field that tries to capture everything produces noisy, hard-to-use context. Create separate fields for distinct categories (preferences, history, action items) so each one stays clean and useful.
* **Write specific merge descriptions.** The field description controls how the LLM merges values. "A running summary of interactions" is too vague. "A chronological summary of the last 10 interactions — include key topics and outcomes, drop greetings and small talk" gives the LLM clear instructions.
* **Set size limits in merge descriptions.** Without limits, merged fields grow unboundedly. Include instructions like "Limit to 10 most recent interactions" or "Keep under 500 words" to prevent fields from becoming too large for the agent's context window.
* **Use "Overwrite" for status fields, "Merge" for history fields.** A field like `lead_status` should always reflect the latest value — use Overwrite. A field like `interaction_history` needs to accumulate across conversations — use Merge.
* **Use "Fill if empty" for stable facts.** Fields like email address or company name rarely change. Use "Fill if empty" so they're captured on first mention without being overwritten by later extractions.
* **Backfill from past conversations.** If you set up memory fields after conversations have already occurred, use the [batch rerun feature](/features/rerun-call-analysis#batch-rerun-from-callchat-history) to populate contact fields from historical conversation data.
* **Test with real conversations.** After configuring your memory pipeline, make a few test calls to the same contact and verify that the contact fields accumulate correctly. Check the contact detail page to confirm the merged values make sense.
# Connect Cal.com
Source: https://docs.retellai.com/integrations/cal-com
Connect Cal.com to Retell AI with an API key: create a never-expiring key, pick the cal.com or cal.eu domain, and verify the workspace connection.
Connecting Cal.com takes one API key and gives you [agent functions](/integrations/cal-com-functions): your agents check real availability, book appointments, and look up, reschedule, or cancel existing bookings. One workspace-level connection covers every agent, with no key pasted into individual tools. This page covers the API key and the connection itself.
This integration replaces Retell's built-in Cal.com availability and booking tools. The dashboard no longer offers them, the API stops creating and updating them on 09/30/2026, and on **10/31/2026** Retell migrates the ones you already have to this integration. Read the [deprecation notice](/deprecation-notice/2026/10-31_legacy_calcom_tools) before then: a tool whose Cal.com key has expired isn't migrated and stops working.
## When to use it
Connect Cal.com when your scheduling runs on it and you want callers handled end to end. It's the right choice when you want to:
* **Read live openings to the caller.** The agent checks the event type's slots and offers actual times.
* **Book while the caller is on the line.** After the caller confirms a time, the agent books it and Cal.com sends its usual confirmations.
* **Reschedule and cancel existing bookings.** The agent finds the caller's booking by email and moves it to a newly confirmed slot, or cancels it.
For example, a dental clinic's inbound agent looks up tomorrow's cleaning by the caller's email, offers the week's open slots, and moves the appointment to Friday 10am, all in one call.
## Prerequisites
* A Cal.com account that owns the event types you want to book.
* Know whether your account is on **cal.com** or **cal.eu** (the EU-hosted instance). Create the key on that instance and pick the matching domain when you connect.
Cal.com has announced that cal.eu shuts down on November 1, 2026; enterprise customers keep access through their contracted term. Migrating to cal.com is opt-in. If you migrate, reconnect with a cal.com key and the `Cal.com` domain.
## Step 1: Create an API key
In Cal.com, go to **Settings > Developer > API keys** and click **+ New**. Keep the **API key** tab selected, name the key something descriptive, for example `Retell AI`, and turn on **Never expires**; an expiring key silently kills the connection on its expiry date. Create it and copy it right away; Cal.com shows the key only once.
## Step 2: Connect Cal.com in Retell
In the Retell Dashboard, open **Integrations**, select the **Available** tab, find **Cal.com**, and click **Connect** (**Add Account** if a connection already exists).
Fill in the fields:
| Field | Value |
| ------------------- | -------------------------------------------------------------- |
| **Connection name** | Alias for this connection; prefilled with `Cal.com - API key`. |
| **API Key** | The key from Step 1. |
| **Domain** | `Cal.com`, or `Cal.eu` for EU-hosted accounts. |
Click **Connect** (**Add Account** if a connection already exists). Retell tests the key against the Cal.com API.
On success the connection appears on the **Connected** tab. Your agents can now book against the account — see [Cal.com agent functions](/integrations/cal-com-functions) for the tools and the event type ID the booking tools need.
## Troubleshooting
Confirm the key is current and the **Domain** matches the instance your account lives on (cal.com or cal.eu). If the key had an expiry date, it may simply have lapsed.
Retell flags a connection as errored when Cal.com rejects the key — usually expired or deleted. Open the connection's settings, paste a fresh key set to never expire, and click **Reconnect**.
## FAQ
No. The connection targets Cal.com's hosted API on cal.com or cal.eu; self-hosted instances aren't supported (as of August 2026).
## Next steps
Check availability, book appointments, and reschedule or cancel bookings mid-conversation.
See every provider Retell connects to and how integration tools work.
The other calendar option, if your scheduling lives there.
# Cal.com agent functions
Source: https://docs.retellai.com/integrations/cal-com-functions
Cal.com tools for Retell AI agents: check live availability, book appointments, and list, get, reschedule, or cancel bookings during the call.
A [connected Cal.com account](/integrations/cal-com) gives your agents the full booking lifecycle as live tools: check availability, book, and list, get, reschedule, or cancel bookings. Tools run during a conversation or [before and after it](/agent/agent-workflow). There's no synced copy of your calendar in Retell; every tool call reads or writes Cal.com live.
## Available tools
These tools appear in your agent's function menu once your account is connected. **Check Availability** and **Book Appointment** each take an [event type ID](#find-an-event-type-id), so they're pinned to one event type. See [use integration tools in an agent](/integrations/overview#use-integration-tools-in-an-agent) for how to add and configure them.
| Tool | What it does |
| ---------------------- | ----------------------------------------------------------------------------------- |
| **Check Availability** | List open slots for the configured event type in a time window |
| **Book Appointment** | Book the configured event type after the caller confirms a time |
| **List Bookings** | List a caller's upcoming bookings (up to 50) by attendee email within a time window |
| **Get Booking** | Fetch one booking by its UID |
| **Reschedule Booking** | Move an existing booking to a new confirmed time |
| **Cancel Booking** | Cancel an existing booking |
Chain the tools: **List Bookings** finds the booking UID by the caller's email, **Check Availability** confirms the new slot exists, and **Reschedule Booking** moves it. Map the UID from the list result to a [dynamic variable](/build/dynamic-variables) so the later tools can use it.
## Find an event type ID
In the Cal.com dashboard, open **Event Types** and select the one the agent should book.
The event type ID is the number in the address bar, for example `https://app.cal.com/event-types/1234567` — the ID is `1234567`.
Enter that number in the tool's event type ID input. Point a separate tool at each event type you want the agent to handle.
## Permissions
The API key grants the connected account's full API access; there are no per-tool scopes to grant. Every tool reads the connected account's event types and reads and writes its bookings.
## Troubleshooting
Check the event type ID on the tool: it must be an event type the connected account can book, and the ID must match the number in the event type's URL. Then check the event type on Cal.com: it needs open hours in the window the agent asked about, and its calendar (Google, Outlook, and so on) must be connected under **Apps** so Cal.com can read your busy times.
The lookup matches on the attendee email within the time window the agent supplies. Have the agent confirm the email used to book, and if the booking might sit outside the window, widen it.
## FAQ
You can no longer add a built-in Cal.com tool from the dashboard, and the API stops creating and updating them on 09/30/2026, so that's your last day to change a built-in tool's key or event type ID. Existing tools keep running until **10/31/2026**, when Retell migrates them to this integration: it sets up one connection per API key it finds, reusing a connection you've already made for that key, and keeps each tool's function name so your prompts don't break. A tool whose key Cal.com rejects, because it expired or was revoked, is skipped and stops working, so migrate that one by hand: [connect Cal.com](/integrations/cal-com), add **Check Availability** and **Book Appointment** with the same event type ID, and delete the built-in tool. See the [deprecation notice](/deprecation-notice/2026/10-31_legacy_calcom_tools) for the full timeline.
[Calendly](/integrations/calendly-functions) covers availability, booking, and cancellation, and [GoHighLevel calendars](/integrations/gohighlevel-functions#book-the-sub-accounts-calendars) cover the full lifecycle when your scheduling sits in the same sub-account as your contacts.
Yes. Bookings, reschedules, and cancellations go through Cal.com, so attendees get exactly the notifications and calendar invites your event type is configured to send.
## Next steps
Add Cal.com tools to a single- or multi-prompt agent and test them with live requests.
Call Cal.com tools from a function node and branch on the result.
Carry booking UIDs and confirmed times between tools and prompts.
Mock the booking tools to test your scheduling flow without creating real bookings.
# Connect Calendly
Source: https://docs.retellai.com/integrations/calendly
Connect Calendly to Retell AI with a personal access token so agents can check availability, book appointments, and cancel scheduled events.
Connect Calendly with a personal access token that has the [required scopes](#required-token-scopes). One connection gives you [agent functions](/integrations/calendly-functions): your agents check real availability, book appointments, and look up or cancel scheduled events.
## When to use it
Connect Calendly when your scheduling already runs on it and you want callers booked without a human in the loop. It's the right choice when you want to:
* **Read live openings to the caller.** The agent checks the event type's availability and offers actual slots.
* **Book while the caller is on the line.** After the caller confirms a time and gives their name and email, the agent books it; the invitee gets Calendly's usual confirmation.
* **Handle cancellations on the same call.** The agent finds the caller's upcoming events by email and cancels the one they name.
For example, a solar installer's inbound agent qualifies the caller, checks the sales team's consultation event type for this week, books Thursday 2pm with the caller's email, and the rep's calendar fills itself.
## Prerequisites
* A Calendly account that owns the event types you want to book. The token identifies one Calendly user, and Retell books and reads **that user's** event types and scheduled events.
* A **paid Calendly subscription** for booking. Availability checks, lookups, and cancellations work on any plan, but creating bookings uses Calendly's Scheduling API, which Calendly restricts to paid subscriptions (as of August 2026).
## Step 1: Create a personal access token
In Calendly, go to **Integrations & apps** and open the **API & webhooks** tile. On **Your personal access tokens**, click **Create a token** and name it something descriptive, for example `Retell AI`.
### Required token scopes
Before creating the token, select the scopes for your tools (as of September 2026):
| Scope | Used for |
| ------------------------ | ---------------------------------------------------------------------------------- |
| `users:read` | Validate every connection by fetching the token's Calendly user. |
| `event_types:read` | Resolve the scheduling link for **Check Availability** and **Book Appointment**. |
| `availability:read` | Read available times for **Check Availability**. |
| `scheduled_events:read` | Read scheduled events and invitees for **List Bookings** and **Get Booking**. |
| `scheduled_events:write` | Create bookings with **Book Appointment** and cancel them with **Cancel Booking**. |
To use all five tools, grant all the permissions above. Calendly's [`:write` scopes include the matching `:read` permission](https://developer.calendly.com/docs/authentication/scopes), so `scheduled_events:read` doesn't need to be selected separately if you grant `scheduled_events:write`. For read-only tools, omit `scheduled_events:write`.
Create the token and copy it right away; Calendly shows it only once. For the scopes needed by each tool, see [Calendly agent function permissions](/integrations/calendly-functions#permissions).
The token's access also depends on the Calendly user's role. An **admin** token can access data across the organization within its granted scopes. To limit the data it can reach, create it from a dedicated non-admin account.
## Step 2: Connect Calendly in Retell
In the Retell Dashboard, open **Integrations**, select the **Available** tab, find **Calendly**, and click **Connect** (**Add Account** if a connection already exists).
Fill in the fields:
| Field | Value |
| ------------------- | --------------------------------------------------------------- |
| **Connection name** | Alias for this connection; prefilled with `Calendly - API key`. |
| **API Key** | The personal access token from Step 1. |
Click **Connect** (**Add Account** if a connection already exists). Retell tests the token by [fetching the Calendly user it belongs to](https://developer.calendly.com/api-docs/calendly-api/users/get-current-user), which requires `users:read`. This check doesn't validate the other tool scopes.
On success the connection appears on the **Connected** tab. From here, add tools to an agent: see [Calendly agent functions](/integrations/calendly-functions) for what each tool does and how it's configured.
## Troubleshooting
Confirm you pasted a **personal access token**, not an OAuth client secret or a webhook signing key, and that it hasn't been revoked in Calendly.
A valid token can still fail with **Could not connect. Check your credentials and try again.** if it lacks `users:read`. Create a new token with `users:read` and the [scopes for your tools](#required-token-scopes), paste it into **API Key**, and try connecting again. For an existing connection, replace the token in its settings and click **Reconnect**.
Retell flags a connection as errored when Calendly rejects the token, usually because it was revoked, or because Calendly invalidates tokens when the account's password or email changes. If you replaced the token, check that the new one has the [required scopes](#required-token-scopes). Open the connection's settings, paste a current token, and click **Reconnect**.
## FAQ
Event types owned by the Calendly user whose token you connected. To book for several team members, connect a token per user (multiple connections are fine) or use a shared account that owns the event types.
## Next steps
Check availability, book appointments, and look up or cancel events mid-conversation.
See every provider Retell connects to and how integration tools work.
The other calendar option, with rescheduling support.
# Calendly agent functions
Source: https://docs.retellai.com/integrations/calendly-functions
Calendly tools for Retell AI agents: check real availability, book appointments, and look up or cancel scheduled events live during the call.
A [connected Calendly account](/integrations/calendly) gives your agents live scheduling tools: check real availability, book appointments, and look up or cancel scheduled events. Tools run during a conversation or [before and after it](/agent/agent-workflow), with no contact sync in between — each call hits Calendly live.
## Available tools
These tools appear in your agent's function menu once Calendly is connected. **Check Availability** and **Book Appointment** are configured with an event type's [scheduling link](#find-the-scheduling-link), so each tool is pinned to one event type. See [use integration tools in an agent](/integrations/overview#use-integration-tools-in-an-agent) for how to add and configure them.
**Book Appointment** needs a paid Calendly plan: it books through Calendly's Scheduling API, which Calendly restricts to paid subscriptions (as of August 2026). On a free account that one tool fails; availability checks, lookups, and cancellations keep working.
| Tool | What it does |
| ---------------------- | ------------------------------------------------------------------------------------ |
| **Check Availability** | List open slots for the configured event type |
| **Book Appointment** | Book the configured event type after collecting the invitee's name and email |
| **List Bookings** | List a caller's upcoming events (up to 50) by invitee email within a time window |
| **Get Booking** | Fetch one scheduled event and its invitees by event UUID |
| **Cancel Booking** | Cancel a scheduled event; for one-on-one event types this cancels the single invitee |
The availability tool checks a window of up to 7 days at a time. When a caller asks about "sometime next month," the agent makes a follow-up check with a later start date — the tool's description already tells it so.
## Find the scheduling link
The scheduling link is the public booking URL for one event type — the same link you'd send an invitee, in the form `https://calendly.com//`, for example `https://calendly.com/acme/30min`.
Go to your event types and open the one the agent should book.
Copy the event type's link, or open its booking page and copy the URL from the address bar.
Paste it into the tool's event type URL input. Retell ignores case, the scheme, `www.`, trailing slashes, and anything after a `?` or `#`, so a copied link works as-is. Point a separate tool at each event type you want the agent to handle.
Only event types owned by the connected Calendly user resolve; anything else fails with `Calendly event type not found on the connected account`. For a teammate's event type, connect a token for that teammate. Team round-robin and collective event types can't be booked through the integration at all.
## Permissions
The personal access token acts as the Calendly user it belongs to and must grant the scopes each tool needs. Every connection requires `users:read` to validate the token. Add the following scopes for the tools your agent uses:
| Tool | Additional scopes beyond `users:read` |
| ---------------------- | -------------------------------------------- |
| **Check Availability** | `event_types:read`, `availability:read` |
| **Book Appointment** | `event_types:read`, `scheduled_events:write` |
| **List Bookings** | `scheduled_events:read` |
| **Get Booking** | `scheduled_events:read` |
| **Cancel Booking** | `scheduled_events:write` |
**Check Availability** needs both scopes: Retell resolves the scheduling link to an event type, then [reads its available times](https://developer.calendly.com/api-docs/calendly-api/event-types/list-event-type-available-times). Calendly's [`scheduled_events:write` scope also grants `scheduled_events:read`](https://developer.calendly.com/docs/authentication/scopes).
Choose these scopes when [creating the token](/integrations/calendly#required-token-scopes). A successful connection only verifies `users:read`; a tool can still fail if its other scopes are missing.
## Troubleshooting
Create a new token with `users:read` and the [scopes for every tool you use](#permissions), paste it into the connection's settings, and click **Reconnect**. For example, a token with only `users:read` connects successfully but can't check availability or book appointments.
Check the scheduling link on the tool: it must be an event type belonging to the token's own Calendly user, spelled exactly as Calendly shows it. Then check the event type itself has open hours in the window the agent asked about.
Confirm the token includes `scheduled_events:write`; availability only uses read scopes. Booking also needs a [paid Calendly plan](/integrations/calendly#prerequisites) and the invitee's name and email. Make sure the agent collects both before calling the tool. If the event type asks the invitee to choose a location, the booking needs that answer too.
The lookup matches on the invitee email within the time window the agent supplies. Have the agent confirm the email used to book, and if the event might sit outside the window, widen it.
## FAQ
Yes. Bookings go through Calendly, so invitees get exactly the notifications, reminders, and calendar invites your event type is configured to send.
The tools take the caller's IANA timezone (for example `America/New_York`) alongside local times, so the agent can quote and book slots in the caller's own time. Prompt the agent to confirm the caller's timezone when it isn't obvious from context.
Not directly — there's no Calendly reschedule tool. Have the agent cancel the existing event and book a new time. If rescheduling in place matters, [Cal.com](/integrations/cal-com-functions) and [GoHighLevel calendars](/integrations/gohighlevel-functions#book-the-sub-accounts-calendars) both have a reschedule tool.
## Next steps
Add Calendly tools to a single- or multi-prompt agent and test them with live requests.
Call Calendly tools from a function node and branch on the result.
Reuse booking details later in the call, like reading back the confirmed time.
Mock the booking tools to test your scheduling flow without creating real events.
# CRM data mappings
Source: https://docs.retellai.com/integrations/crm-mappings
How data flows between your CRM, Retell AI contacts, and Post Call Extraction: inbound and outbound field mappings, update modes, and custom fields.
Four data flows connect your CRM, your Retell contacts, and Post Call Extraction results: inbound sync, analysis mapping, outbound sync, and activity logging. This page covers each flow and the field mappings and update modes that control it.
## Data model overview
The CRM integration involves three main entities and four data flows between them:
Your CRM's contact records with their native fields.
A unified contact record in Retell with default fields and custom fields.
Structured data extracted from each call/chat by your [Post Call Extraction](/features/post-call-analysis-overview) configuration.
| Data flow | Direction | Description |
| -------------------- | ------------------------------------- | ---------------------------------------------------------------------- |
| **Inbound sync** | External CRM → Retell contact | Import and update contacts from your CRM on a recurring schedule |
| **Analysis mapping** | Post Call Extraction → Retell contact | Map extracted conversation data to contact fields after each call/chat |
| **Outbound sync** | Retell contact → External CRM | Push updated contact fields back to your CRM |
| **Activity logging** | Retell contact → External CRM | Log call and chat records as activities on the CRM contact |
## How data flows
### 1. Inbound sync: CRM to Retell
Inbound sync imports contacts from your CRM into Retell. It runs every 5 minutes (as of August 2026) and picks up only records modified since the last run. A manual sync re-scans everything.
**Matching logic:** Contacts are matched between systems using **phone number** as the primary key. When Retell finds a CRM contact with a phone number that matches an existing Retell contact, it updates the existing record. Otherwise, it creates a new contact.
**Records Retell skips:** a CRM contact with no value in the mapped phone field, or with a phone number that can't be parsed into a valid E.164 number. Skipped records don't stop the sync.
**Inbound sync mappings** control which CRM fields are imported:
| CRM field (example) | Direction | Retell field |
| ---------------------------------------- | ------------ | ------------------------- |
| `Phone` (Salesforce) / `phone` (HubSpot) | CRM → Retell | `phone_number` (required) |
| `FirstName` / `firstname` | CRM → Retell | `first_name` |
| `LastName` / `lastname` | CRM → Retell | `last_name` |
| `Email` (Salesforce) / `email` (HubSpot) | CRM → Retell | Your custom field name |
| `Custom_Field__c` / `custom_property` | CRM → Retell | Your custom field name |
The `phone_number` mapping is required and hardcoded for inbound sync. Without it, Retell has no way to match CRM records to Retell conversations.
### 2. Analysis data mapping: analysis to contact
After each call or chat, [Post Call Extraction](/features/post-call-analysis-overview) extracts structured data from the conversation. You can map these analysis results to contact fields, building a richer contact profile over time.
For example, if your Post Call Extraction extracts a `lead_status` field, you can map it to a custom contact field so each contact's qualification status is automatically updated after every conversation.
**Example mappings:**
| Analysis data field | Direction | Contact field | Update mode |
| ------------------- | ------------------ | ---------------------- | ------------- |
| `lead_status` | Analysis → Contact | `qualification_status` | Overwrite |
| `email_address` | Analysis → Contact | `email` | Fill if empty |
| `customer_notes` | Analysis → Contact | `interaction_summary` | Merge |
#### Update modes
Each analysis data mapping has an **update mode** that controls how the new value interacts with the existing value. The names in parentheses are the labels shown on the [contact fields](/features/contacts#define-contact-fields) page in the dashboard.
Always replace the existing value with the new analysis result. Use this for fields where the latest value is always the most relevant (e.g., sentiment, status).
Only write the new value if the field is currently empty. Use this for fields you want to capture once and preserve (e.g., email address, company name).
Combine the existing and new values with an LLM. Only available for string fields. Use this for cumulative fields where you want to preserve context from previous conversations (e.g., interaction notes, preferences).
#### How merge works
The **Merge** update mode uses an LLM to combine the existing contact field value with the new analysis result from the latest conversation. Instead of appending or replacing text, the LLM reads both values and produces a single, coherent merged result.
The **field description** you set when creating or editing the custom field is the LLM's instruction for the merge: it tells the model what kind of data the field holds, what information to prioritize, and how the merged output should be structured.
**Example field descriptions for merge:**
| Field name | Good description | Why it works |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `interaction_notes` | `A running summary of all conversations with this contact. Include key topics discussed, action items, and any commitments made. Keep the most recent interaction details prominent while preserving important context from earlier conversations.` | Tells the LLM to maintain a chronological summary and what details matter. |
| `customer_preferences` | `The contact's stated preferences and requirements. Consolidate preferences across conversations — update changed preferences and keep unchanged ones. Format as a bullet list.` | Guides the LLM to reconcile conflicting information and specifies an output format. |
| `objections_raised` | `A deduplicated list of objections or concerns the contact has raised across all conversations. Merge new objections into the existing list without repeating ones already captured.` | Instructs the LLM to deduplicate rather than blindly concatenate. |
Think of the field description as a prompt to the LLM. The more specific you are about what to keep, what to discard, and how to structure the output, the better the merge results will be.
**Limits worth knowing.** Merge only applies to `string` fields; on any other type the mapping is skipped. The merged result is capped at roughly 512 tokens (as of August 2026), so a field set up as an ever-growing log eventually starts losing the oldest detail rather than expanding forever. Write the description to prioritize what matters most.
If the merge call fails or comes back empty, Retell falls back to keeping both values, appending the new one below the existing one separated by a blank line. You never lose data to a failed merge, but the field won't be as tidy as a successful one.
### 3. Outbound sync: Retell to CRM
When a contact's fields change in Retell, whether manually or through analysis data mapping, **outbound sync mappings** push the changed fields back to your CRM.
Outbound sync fires on two paths: a manual edit to a contact's fields, and a conversation's analysis results. Both need:
* The contact **imported from your CRM**, still carrying its CRM record ID.
* At least one **outbound sync mapping** configured.
* A mapped field that actually got **written**. A manual edit pushes as soon as you save; analysis results push whatever their update mode produced, so **Overwrite** pushes even when the new value matches the stored one, while **Fill if empty** skips fields that already hold a value.
Conversation-driven pushes also need the contact to have been imported by the **connection that's currently active**. If you delete a connection and set up a new one, contacts imported by the old one stop pushing on the conversation path until they're re-imported.
By default, outbound sync **updates existing CRM records only** and never creates or deletes contacts. Turn on **Create new contacts in CRM** on the Sync tab to let Retell create a new CRM record after a conversation when the matched contact isn't linked to one yet. This toggle is off by default, and even with it on, outbound sync never deletes contacts. Outbound sync also never writes back the phone number, which stays reserved as the matching key.
**Outbound sync mappings** control which Retell fields are pushed:
| Retell field | Direction | CRM field (example) |
| ---------------------- | ------------ | -------------------------------- |
| `qualification_status` | Retell → CRM | `Lead_Status__c` / `lead_status` |
| `interaction_summary` | Retell → CRM | `Notes__c` / `notes` |
### 4. Conversation activity logging
When enabled, Retell logs each call and chat as an activity record in your CRM, associated with the matched contact. This gives your sales team a complete conversation history directly in the CRM.
What lands in the record varies by provider (see [CRM integrations](/integrations/crm-overview) for each format), but every provider records:
* The conversation summary
* The Retell conversation ID, and the from and to phone numbers
Calls also carry their duration on Salesforce, HubSpot, Dynamics, and Zoho. HubSpot, Dynamics, and Zoho record the call direction as well.
Retell picks the number to match on from the call's direction: the caller's number on an inbound call, the dialed number on an outbound one.
Activity logging needs the contact to have been imported from the currently active CRM connection, the same way conversation-driven outbound sync does. Conversations with numbers that don't match a synced contact, and contacts Retell created itself, produce no activity record.
## End-to-end data flow example
Here's how a typical interaction flows through the system:
An inbound sync imports a contact from Salesforce with fields: `Phone: +1234567890`, `FirstName: Alice`, `LastName: Smith`, `Lead_Status__c: New`.
Retell creates a contact with: `phone_number: +1234567890`, `first_name: Alice`, `last_name: Smith`, `qualification_status: New`.
Your voice agent calls Alice. During the call, Retell injects contact fields as dynamic variables, so the agent knows Alice's name and qualification status.
After the call, Post Call Extraction extracts:
* `lead_status: Qualified`
* `customer_notes: Interested in enterprise plan, wants demo next week`
* `email_address: alice@example.com`
Retell applies the analysis results to Alice's contact based on your mappings:
* `qualification_status` is **overwritten** with `Qualified`
* `interaction_summary` is **merged** with the new notes, preserving previous conversation context
* `email` is **filled** (was previously empty)
Outbound sync pushes the updated fields to Salesforce:
* `Lead_Status__c` updated to `Qualified`
* `Notes__c` updated with the merged interaction summary
Retell creates a Task record in Salesforce with the call duration and summary, associated with Alice's contact record.
## Configure field mappings
### Custom fields
Before you can map CRM fields to non-default Retell fields, you need to create **custom fields** on your Retell contacts.
Custom fields support the following types:
| Type | Example values |
| ---------- | ---------------------------------- |
| `string` | `"Interested in enterprise plan"` |
| `number` | `42`, `99.5` |
| `boolean` | `true`, `false` |
| `date` | `2025-03-15` |
| `datetime` | `2025-03-15T10:30:00Z` |
| `enum` | One of a predefined set of options |
Custom field names are `snake_case` starting with a letter. They can't collide with the built-in field names or start with `contact` or `external` (both reserved). See [define contact fields](/features/contacts#define-contact-fields) for creating them.
### Field type compatibility
When configuring sync mappings, the CRM field type must be compatible with the Retell field type. Retell handles type coercion automatically:
* CRM date strings are parsed into Retell `date`/`datetime` fields
* CRM picklist values map to Retell `enum` fields
* CRM numeric strings are converted to Retell `number` fields
* CRM boolean-like values (`"true"`, `"false"`) are converted to Retell `boolean` fields
## Best practices
* **Start with essential mappings.** Map `first_name` and `last_name` first (phone number is already mapped for you), then add custom fields incrementally as you identify what data is valuable.
* **Use "Fill if empty" for stable data.** Fields like email address or company name rarely change — use "Fill if empty" to capture them on first mention without overwriting later.
* **Use "Merge" sparingly.** The merge update mode uses an LLM to combine values, which works well for free-text notes but adds latency and cost (\$0.005 per merge as of August 2026). Use "Overwrite" or "Fill if empty" when possible.
* **Write descriptive field descriptions for merge fields.** The field description acts as the LLM's instruction for how to merge values. A specific, well-written description (e.g., "A running summary of interactions; keep recent details prominent and deduplicate action items") produces much better results than a vague one. See [how merge works](#how-merge-works) for examples.
* **Use "Overwrite" for status fields.** Fields like lead status or sentiment should always reflect the most recent conversation.
* **Enable activity logging for sales visibility.** Your sales team can see the full conversation history in the CRM without switching to the Retell dashboard.
* **Use a dedicated CRM integration user.** For Salesforce, use a dedicated service account as the Run-As user to avoid permission issues when team members change roles.
# CRM integrations
Source: https://docs.retellai.com/integrations/crm-overview
Connect Salesforce, HubSpot, Dynamics 365, GoHighLevel, or Zoho to Retell AI: sync contacts both ways, map analysis to fields, and log conversations.
Retell's CRM integration syncs contacts both ways between your CRM and Retell. Import contacts, enrich them with [Post Call Extraction](/features/post-call-analysis-overview) results, and push conversation activity back, all automatically.
CRM is one category of Retell's [integrations](/integrations/overview); this page covers the sync features specific to CRM connections.
## When to use it
Set up contact sync when caller records already live in your CRM and you want agents and the CRM working from the same data. It's the right choice when you want to:
* **Greet callers by name.** Synced contact fields are available to the agent as [dynamic variables](/build/dynamic-variables), so it knows who's calling before the first word.
* **Keep the CRM current without manual entry.** Analysis results from each conversation write back to the CRM record.
* **Keep an activity trail where your team works.** Each call and chat can log to the CRM automatically.
For example, a lead-qualification agency syncs its GoHighLevel contacts into Retell and maps a `qualified` analysis field back to the CRM, so its follow-up workflows trigger from that field after every call — no one touches a record by hand.
If you only need the agent to look up or update CRM records, [integration tools](/integrations/overview#use-integration-tools-in-an-agent) do that without sync.
## Supported CRM platforms
Sync contacts and log call and chat activity as Salesforce Tasks.
Sync contacts and log call and chat activity to the HubSpot timeline.
Sync contacts and log calls as Phone Call activities. Connect with OAuth and your environment URL.
Sync sub-account contacts and log activity as contact notes. Connect with an API key and Location ID.
Sync Contacts and log calls as Call records. Connect with one OAuth sign-in, on any Zoho data center.
## How does the CRM integration work?
The integration has four parts, each configured separately.
### 1. Contact sync (CRM to Retell)
Retell imports contacts from your CRM and keeps them current. Imported records appear on the [Contacts](/features/contacts) page.
* **Phone number** is the key used to match contacts between systems. It's mapped by default and can't be unmapped.
* **Inbound sync mappings** control which CRM fields are imported.
* See [contact fields](#contact-fields) below for the built-in fields and the custom field types you can map.
Sync runs **every 5 minutes** (as of August 2026) and imports only records modified since the last run. A **manual sync re-scans every contact** from scratch, ignoring that cursor, so use it after changing mappings rather than as a routine refresh.
Contacts are skipped when the mapped phone field is empty, or when its value can't be parsed into a valid E.164 phone number. A contact that syncs but shows no name or custom fields usually means those fields aren't mapped, not that the sync failed.
### 2. Post Call Extraction mapping (analysis to contacts)
After each call or chat, Retell can map [Post Call Extraction](/features/post-call-analysis-overview) results onto contact fields, building a richer profile as your agents have more conversations.
Each mapping has an update mode. The dashboard labels these differently on the [contact fields](/features/contacts#define-contact-fields) page:
| Update mode | Dashboard label | Behavior |
| ----------------- | ---------------------- | ------------------------------------------------------------------------------------- |
| **Overwrite** | Overwrite | Always replace the existing value with the new analysis result |
| **Fill if empty** | Fill only if empty | Only write when the field currently has no value |
| **Merge** | Accumulate & summarize | Combine the existing and new values with an LLM (\$0.005 per merge as of August 2026) |
You configure analysis data mappings **per workspace**, not per agent, so the same rules apply no matter which agent handled the conversation.
See [CRM data mappings](/integrations/crm-mappings) for the full data flow and how to write field descriptions so merges produce accurate combined values.
### 3. Outbound field sync (Retell to CRM)
**Outbound sync mappings** push contact field updates from Retell back to your CRM. When a contact's fields change in Retell, whether manually or through analysis mapping, Retell writes the mapped fields to the matching CRM record.
By default, outbound sync **updates existing CRM records only** and never creates or deletes contacts. Turn on **Create new contacts in CRM** on the Sync tab to let Retell create a new CRM record after a conversation when the matched contact isn't linked to one yet. This toggle is off by default; deletion never happens either way.
Unlike contact sync, this isn't on a schedule. Retell pushes changes as they happen: right after a conversation ends, as part of applying its analysis results, or immediately when you edit a contact's fields.
### 4. Conversation activity logging (Retell to CRM)
With **Log activities automatically** enabled, Retell logs each call and chat to your CRM:
* **Salesforce** — a Task record carrying the call duration, with the summary, conversation ID, from and to numbers, and disconnection reason in the description.
* **HubSpot** — a Call engagement, or a Communication object for chats, on the contact's activity timeline.
* **Microsoft Dynamics 365** — a completed Phone Call activity with the direction and duration for calls, or a Task for chats.
* **GoHighLevel** — a note on the contact with the conversation ID, the from and to numbers, and the summary.
* **Zoho CRM** — a Call record with the direction and duration for calls, or a completed Task for chats.
Retell logs activity only for contacts imported from the currently connected CRM. A call from a number that doesn't match a synced contact produces no activity record, and neither do contacts Retell created on its own.
## Set up a CRM integration
Open **Integrations** in the Retell Dashboard, select the **Available** tab, and pick your provider. Follow the provider guide for the credentials:
* [Salesforce setup guide](/integrations/salesforce)
* [HubSpot setup guide](/integrations/hubspot)
* [Microsoft Dynamics 365 setup guide](/integrations/microsoft-dynamics)
* [GoHighLevel setup guide](/integrations/gohighlevel)
* [Zoho CRM setup guide](/integrations/zoho)
Creating a connection requires an Admin or Developer role (see [connect a provider](/integrations/overview#connect-a-provider) for the exact permissions).
After the connection test passes, select **Set up contact sync**. The dialog has two tabs: **Import contacts** (CRM to Retell) and **Sync to \[provider]** (Retell to CRM). Phone number is mapped for you and stays locked.
For CRM fields that don't correspond to a default Retell field, create a custom field in Retell to hold the value. You can do this inline from the mapping dropdown.
Map your [Post Call or Post Chat Extraction fields](/features/post-call-analysis-overview) to contact fields, choosing an update mode for each based on how you want data to accumulate.
Turn on **Log activities automatically** on the **Sync to \[provider]** tab to log each call and chat to your CRM.
Turn on **Create new contacts in CRM** on the same tab to have Retell create a CRM record after a conversation when the matched contact isn't linked to one yet. It's off by default.
Trigger a manual sync to import your existing contacts. This is a full scan, so a large CRM takes a while. It picks up where it left off if it doesn't finish in one pass.
You can connect several CRM accounts, but only one connection drives contact sync for your workspace at a time. The **Contact sync** toggle in a connection's settings decides which one; turning it on for one connection takes sync over from the previous one.
## Contact fields
Every Retell contact has four built-in fields:
| Field | Type | Description |
| -------------- | ------- | ----------------------------------------------------------------------------- |
| `phone_number` | string | Primary identifier for matching contacts across systems |
| `first_name` | string | Contact's first name |
| `last_name` | string | Contact's last name |
| `do_not_call` | boolean | Used for filtering only. It does **not** block outbound calls to the contact. |
Extend contacts with **custom fields** of type `string`, `number`, `boolean`, `date`, `datetime`, or `enum`.
`do_not_call` isn't mapped by default in either direction. Map it explicitly on both tabs if you want it to sync.
## Use contact data in agents
Contact fields are available as **dynamic variables** in your agent prompts. When a call matches a known contact by phone number, Retell injects the mapped contact fields into the agent's context, so your agent can personalize the conversation using CRM data like the contact's name, account status, or history.
See [dynamic variables](/build/dynamic-variables) for how to reference contact fields in a prompt.
## Troubleshooting
A connection is marked as errored when your CRM **rejects the credentials**, such as an expired secret or a revoked token, and whenever the connection test fails. Sync failures caused by a missing field permission or scope leave the connection reading as healthy while that one field or feature quietly stops working. If a specific field or activity type isn't syncing but the connection looks fine, check permissions and scopes on the CRM side rather than the connection itself. The [integrations overview](/integrations/overview#faq) FAQ covers the health model in full, including where GoHighLevel differs.
# Connect GoHighLevel
Source: https://docs.retellai.com/integrations/gohighlevel
Connect GoHighLevel to Retell AI with a Private Integration token and Location ID: the scopes to grant, where each value lives, and how to verify.
Connecting GoHighLevel takes a Private Integration token and the sub-account's Location ID. One connection covers both [contact sync](/integrations/gohighlevel-contact-sync) and [agent functions](/integrations/gohighlevel-functions): your agents work the sub-account's contacts, tags, and opportunities, and Retell keeps the records current. This page covers the GoHighLevel-side setup and the connection itself.
A GoHighLevel connection is scoped to **one sub-account (location)**. An agency managing several locations connects each one separately.
## When to use it
Connect GoHighLevel when it's your system of record and you want your agents working from it without anyone copying data between tools. It's the right choice when you want to:
* **Call or text people who already exist in GoHighLevel.** Contacts sync into Retell automatically, so your agent greets callers by name and knows their pipeline stage instead of asking.
* **Fire GoHighLevel workflows from conversation outcomes.** The **Add Contact Tags** tool tags the contact — tags are what GoHighLevel workflows trigger on, so a "booked" or "interested" tag can kick off your existing follow-up automation.
* **Work deals from the phone.** Your agent can list a contact's opportunities, move one to a different pipeline stage, or create a new one when a caller qualifies.
* **Book the sub-account's calendars.** The same connection reads free slots and books, reschedules, or cancels appointments, so one integration covers both the caller's record and their appointment.
Your agents can also look up and manage contacts, tasks, notes, and a contact's appointments through [integration tools](/integrations/gohighlevel-functions#available-tools), and book the sub-account's [calendars](/integrations/gohighlevel-functions#book-the-sub-accounts-calendars).
For example, an agency runs a med spa location's inbound line on Retell. The agent looks up the caller, answers from their record, tags them `booked` after scheduling, and the location's existing GoHighLevel workflow sends the confirmation text. Nothing about the workflow changes.
## Prerequisites
* Admin access to the GoHighLevel **sub-account** you want to connect. Private Integrations exist at both the agency and sub-account level, but Retell's connection is location-scoped, so create the token inside the sub-account.
## Step 1: Create a Private Integration token
In the sub-account, go to **Settings > Private Integrations** (under **Other Settings**) and click **Create new Integration**. Name it something descriptive, for example `Retell AI`.
If **Private Integrations** isn't in the Settings menu, enable the feature under **Settings > Labs** (agency or sub-account view) first. By default admins can create Private Integrations; access can be restricted per user under **Roles & Permissions** (**Settings > Team** at the agency, **Settings > My Staff** in a sub-account).
On the **Scopes** tab, open the **Select scopes** dropdown. Each of its entries pairs a label with the scope's slug, like "View Contacts - contacts.readonly", and the list is searchable, so search by the slugs below.
Grant the scopes covering what Retell uses. At minimum:
| Scope | Required for |
| ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `contacts.readonly` and `contacts.write` | Contact sync, the connection test, contact lookups and updates, tags, tasks, notes, and listing a contact's appointments |
| `opportunities.readonly` and `opportunities.write` | Listing, creating, and updating opportunities, and reading pipelines |
| `locations/customFields.readonly` | Reading custom field definitions so you can map custom fields |
| `calendars.readonly`, `calendars/events.readonly`, and `calendars/events.write` | The [calendar tools](/integrations/gohighlevel-functions#book-the-sub-accounts-calendars): checking availability, and booking, rescheduling, or cancelling appointments |
Grant the scopes up front. Without `contacts.readonly` the connection test itself fails. And when a tool call later hits a missing scope, GoHighLevel returns the same authorization error it returns for a revoked token, so the connection can end up flagged with a **Connection error** tag.
Save the integration and copy the token right away; GoHighLevel shows it only at creation.
## Step 2: Find the Location ID
The Location ID identifies the sub-account. Read it in the sub-account under **Settings > Business Profile**, or from your browser's address bar while inside the sub-account — it's the string after `/location/` in the URL, for example `ve9EPM428h8vShlRW1KT`.
## Step 3: Connect GoHighLevel in Retell
In the Retell Dashboard, open **Integrations**, select the **Available** tab, find **GoHighLevel**, and click **Connect** (**Add Account** if a connection already exists).
Fill in the fields:
| Field | Value |
| ------------------- | ------------------------------------------------------------------------------------------------------------ |
| **Connection name** | Alias for this connection; prefilled with `GoHighLevel - API key`. The location's name makes a better alias. |
| **API key** | The Private Integration token from Step 1. |
| **Location ID** | The sub-account ID from Step 2. |
Click **Connect** (**Add Account** if a connection already exists). Retell tests the credentials by searching the sub-account's contacts. On success the dialog offers **Set up contact sync**.
On the **Connected** tab, the GoHighLevel connection shows as connected. See [GoHighLevel contact sync](/integrations/gohighlevel-contact-sync) to import the sub-account's contacts, or start using [agent functions](/integrations/gohighlevel-functions) right away.
## Manage the token
* **Change scopes anytime.** Edit the Private Integration's scopes without regenerating the token — additions take effect immediately.
* **Rotate on your schedule.** Open the integration under **Settings > Private Integrations** to rotate its token; GoHighLevel recommends doing so every 90 days. **Rotate and expire this token later** keeps the old token valid for 7 days while you swap the credential over; **Rotate and expire this token now** kills it immediately, so Retell fails until it has the new one.
* **Reconnect after rotating.** On the **Connected** tab, open the connection's settings, paste the new token over the masked one, and click **Reconnect**. Retell verifies the new token before saving, and your field mappings and synced contacts are untouched.
## Troubleshooting
Confirm you pasted a **Private Integration token** from the same sub-account whose Location ID you entered, not an agency-level key or a legacy API key. A token from one location can't act on another.
Retell flags a connection as errored when GoHighLevel rejects the credentials — usually a deleted or rotated token, though a tool call that hits a missing scope reports the same way. Open the connection's settings, paste a current token (or grant the missing scope), and click **Reconnect**.
## FAQ
Yes, one connection per location, each with its own token and Location ID. Only one CRM connection in your workspace can drive [contact sync](/integrations/gohighlevel-contact-sync) at a time, across every provider — the **Contact sync** toggle in a connection's settings decides which one. Integration tools work on every connection.
## Next steps
Import the sub-account's contacts, write analysis results back, and log conversations as notes.
Look up callers, add tags that fire workflows, and work opportunities mid-conversation.
How contact sync, analysis mapping, and activity logging work across CRM providers.
See every provider Retell connects to and how integration tools work.
# GoHighLevel contact sync
Source: https://docs.retellai.com/integrations/gohighlevel-contact-sync
Sync GoHighLevel sub-account contacts with Retell AI: field mappings, Post Call Extraction write-back, and conversation logging as contact notes.
Contact sync imports the connected sub-account's contacts into Retell, writes [Post Call Extraction](/features/post-call-analysis-overview) results and field updates back, and logs each conversation as a note on the contact. This page covers the GoHighLevel-specific behavior; [CRM integrations](/integrations/crm-overview) explains the four data flows all CRM providers share.
Contact sync requires a [connected GoHighLevel sub-account](/integrations/gohighlevel). Integration tools work without it — see [GoHighLevel agent functions](/integrations/gohighlevel-functions).
## Required scopes
Contact sync uses the Private Integration's scopes, granted in [Step 1 of connecting](/integrations/gohighlevel#step-1-create-a-private-integration-token):
| Scope | Used for |
| --------------------------------- | -------------------------------------------------- |
| `contacts.readonly` | Importing contacts |
| `contacts.write` | Outbound sync and logging conversations as notes |
| `locations/customFields.readonly` | Reading custom field definitions for field mapping |
A missing scope never flags the connection when sync is what hits it; the affected part of sync silently stops working. A tool call behaves differently — see [required scopes](/integrations/gohighlevel-functions#required-scopes) on the agent functions page.
## Set up contact sync
After the connection test passes, click **Set up contact sync** to open the field mapping dialog, then map the GoHighLevel fields you want to import and the Retell fields you want to write back. For a connection made earlier, the same dialog opens from the **Connected** tab: open the connection's settings and click **Set up contact sync**.
Retell pre-fills one mapping in each direction: GoHighLevel `phone` to Retell `phone_number`. Phone number is how contacts are matched between the two systems, so it stays mapped and can't be removed. See [CRM data mappings](/integrations/crm-mappings) for how to map the rest, create custom fields, and choose update modes.
To log conversations to GoHighLevel, turn on **Log activities automatically** on the **Sync to GoHighLevel** tab.
## Verify it worked
* Open **Contacts**. After the first sync, the sub-account's contacts appear with correctly formatted phone numbers and your mapped fields populated.
* The first sync is a full scan of every contact that has the mapped phone field, so a large sub-account takes a while. After that, Retell polls every 5 minutes (as of August 2026) and imports only contacts modified since the last run.
Contacts with no value in the mapped phone field are excluded from the sync entirely, as are contacts whose number can't be parsed into a valid E.164 number.
## How are conversations logged in GoHighLevel?
Each call or chat becomes a **note** on the matched contact, carrying the conversation ID, the from and to numbers, the disconnection reason for calls, and the summary. Retell uses notes for two reasons: the history sits in the contact's Notes panel, and logging needs only the [scopes above](#required-scopes). It doesn't write to GoHighLevel's Conversations call log.
Retell logs activity only for contacts imported from the currently connected CRM. A conversation with a number that doesn't match a synced contact produces no note.
## FAQ
By default it only updates contacts that already exist, and it never deletes them. Turn on **Create new contacts in CRM** on the **Sync to GoHighLevel** tab to have Retell create a contact after a conversation when the matched contact isn't linked to one yet. Your agent can also create contacts through the **Create Contact** [tool](/integrations/gohighlevel-functions), but that's an explicit tool call, not sync.
## Next steps
Map GoHighLevel fields to Retell contacts, choose update modes, and control what syncs back.
Accumulate what your agents learn across conversations into the contact record.
Reference synced contact fields from your agent's prompt.
Look up callers, add tags that fire workflows, and work opportunities mid-conversation.
# GoHighLevel agent functions
Source: https://docs.retellai.com/integrations/gohighlevel-functions
GoHighLevel tools for Retell AI agents: look up and update contacts, add tags, work opportunities and tasks, and book the sub-account's calendars.
A [connected GoHighLevel sub-account](/integrations/gohighlevel) gives your agents live tools for the sub-account: identify the caller, tag them to fire workflows, work opportunities, tasks, and notes, and book the sub-account's calendars. Tools run during a conversation or [before and after it](/agent/agent-workflow), and no [contact sync](/integrations/gohighlevel-contact-sync) is required.
## Available tools
These tools appear in your agent's function menu once the sub-account is connected. See [use integration tools in an agent](/integrations/overview#use-integration-tools-in-an-agent) for how to add and configure them.
| Tool | What it does |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| **Search Contact** | Find a contact by phone number — typically the caller's number, to identify who's calling |
| **Get Contact** / **Update Contact** | Fetch a contact by ID, or update fields the caller confirms or corrects |
| **Create Contact** | Create a contact for a caller who isn't in the CRM yet |
| **Add Contact Tags** | Tag the contact so your workflows fire on the outcome |
| **List Contact Opportunities** | List the opportunities associated with a contact (up to 50) |
| **Get Opportunity** | Fetch an opportunity by ID |
| **Create Opportunity** / **Update Opportunity** | Create an opportunity in a pipeline you pick, or move one between stages and mark it won or lost |
| **List Contact Tasks** / **Create Task** | List a contact's follow-ups (up to 50), or create one when a follow-up is agreed |
| **Create Note** | Record a summary of the conversation in the contact's Notes panel |
| **List Contact Appointments** | List a contact's appointments (up to 50) when the caller asks about their bookings |
| **Check Availability** | List free slots on a configured calendar for a time window |
| **Book Appointment** | Book a configured calendar for a contact after the caller confirms a time |
| **Get Appointment** | Fetch one appointment by its ID |
| **Reschedule Appointment** | Move an existing appointment to a new start and end time |
| **Cancel Appointment** | Cancel an existing appointment |
Chain the tools to change an existing booking: **List Contact Appointments** returns the caller's appointments with their IDs, then **Reschedule Appointment** or **Cancel Appointment** acts on the one the caller names. Map the ID to a [dynamic variable](/build/dynamic-variables) so the second tool can use it.
## Book the sub-account's calendars
Appointments are created on the sub-account's own calendar and linked to the contact, so they behave like any other GoHighLevel appointment — which makes this the natural calendar choice when your scheduling and your contacts live in the same sub-account. If your team books through [Cal.com](/integrations/cal-com-functions) or [Calendly](/integrations/calendly-functions) instead, connect those.
The time inputs differ by tool, and none of them need UTC conversion — the agent supplies local ISO 8601 datetimes plus the caller's IANA timezone:
* **Check Availability** and **Reschedule Appointment** take a start time and an end time.
* **Book Appointment** takes the one time the caller agreed to, plus the contact ID (usually from an earlier **Search Contact** call) and an optional appointment title. It doesn't take an end time: GoHighLevel derives the appointment's length from the calendar's slot duration, and rejects a slot that's no longer free. Bookings are created with the status **confirmed**.
### Pick the calendar
**Check Availability** and **Book Appointment** each work against one calendar, set by the `calendar_id` input when you configure the tool: it lists the sub-account's calendars for you to pick from. Add one tool per calendar the agent should offer, or let the agent fill `calendar_id` from a [dynamic variable](/build/dynamic-variables) to route to a calendar decided mid-conversation.
If the `calendar_id` field asks you to type a value instead of offering a list, Retell couldn't read the sub-account's calendars. A missing `calendars.readonly` scope is the usual cause; a revoked token, the wrong Location ID, or a sub-account with no calendars produces the same result.
### How the booking tools behave
* **Availability windows are capped at 31 days.** GoHighLevel limits each availability check to 31 days (as of August 2026); if the agent asks for a longer range, Retell caps it at 31 days from the start time. For a caller asking about next month, have the agent check again with a later start date.
* **Cancelling sets the appointment's status to cancelled** rather than deleting it, so the appointment stays on the contact's timeline and your cancellation workflows still fire.
* **Rescheduling needs both a new start and end time,** and GoHighLevel validates them against the calendar's rules, so confirm the new slot with **Check Availability** first.
## Required scopes
Each tool works only if the Private Integration holds its scope, granted in [Step 1 of connecting](/integrations/gohighlevel#step-1-create-a-private-integration-token); scope changes take effect without regenerating the token:
| Scope | Used for |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `contacts.readonly` | Search Contact, Get Contact, List Contact Tasks, List Contact Appointments |
| `contacts.write` | Create Contact, Update Contact, Add Contact Tags, Create Task, Create Note |
| `opportunities.readonly` | List Contact Opportunities, Get Opportunity, and reading pipelines when you configure the opportunity tools |
| `opportunities.write` | Create Opportunity, Update Opportunity |
| `locations/customFields.readonly` | Reading custom field definitions when you configure the tools |
| `calendars.readonly` | Check Availability, and reading the calendar list when you configure the booking tools |
| `calendars/events.readonly` | Get Appointment |
| `calendars/events.write` | Book Appointment, Reschedule Appointment, Cancel Appointment |
GoHighLevel's task, note, tag, and appointment-listing endpoints all sit under the contact, so the two contact scopes cover them. The booking tools are separate and need the `calendars` scopes.
A tool call that hits a missing scope fails with an authorization error, and because GoHighLevel returns the same error for a revoked token, the connection can end up flagged with a **Connection error** tag.
Three scopes matter before any call is made, when Retell builds a tool's config dialog. Without `opportunities.readonly`, the opportunity tools can't offer a pipeline to pick; without `calendars.readonly`, the booking tools can't offer a calendar; and without `locations/customFields.readonly`, the contact tools' custom field list fails to load.
## Troubleshooting
That's almost always a missing scope on the Private Integration — `opportunities.readonly`/`opportunities.write` and `locations/customFields.readonly` are the ones most often skipped. Edit the integration in GoHighLevel and grant the scope for the capability that's failing; scope additions apply without regenerating the token. If the failing calls also flagged the connection with a **Connection error** tag, open the connection's settings and click **Reconnect** to re-test and clear it.
Check the calendar on the tool first: it must be the calendar that actually holds the openings. Then check that calendar's availability in GoHighLevel for the window the agent asked about, including its working hours and any date-specific overrides.
Retell books through GoHighLevel's own validation, so the calendar can still refuse the slot: someone else may have taken it between the two calls, or it may breach the calendar's minimum scheduling notice or per-day appointment limit. Have the agent re-check availability and offer another time.
Retell adds exactly the tag names the tool call supplies. GoHighLevel workflows trigger on exact tag matches, so configure the tool's tag values to match the tag your workflow watches, letter for letter.
## FAQ
Yes. The pipeline and stage are fixed when you configure the **Create Opportunity** tool; the agent fills in the contact and details, not the pipeline. If you work more than one pipeline, point a separate tool at each.
Yes, two ways. Add one **Book Appointment** tool per calendar, each with its own fixed `calendar_id`, and tell the agent in the prompt which to use for which request. Or configure a single tool whose `calendar_id` comes from a dynamic variable, and set that variable earlier in the conversation.
Yes. **Book Appointment** books for a specific contact, so the appointment appears on that contact in GoHighLevel and **List Contact Appointments** returns it.
## Next steps
Add GoHighLevel tools to a single- or multi-prompt agent and test them with live requests.
Call GoHighLevel tools from a function node and branch on the result.
Import the sub-account's contacts, write analysis results back, and log conversations as notes.
Map tool responses to variables your agent can use later in the conversation.
# Connect Google Drive
Source: https://docs.retellai.com/integrations/google-drive
Connect Google Drive to Retell AI with one OAuth sign-in. The drive.file scope grants per-file access only, so agents answer from files you pick.
Connecting Google Drive lets you [add Drive files to knowledge bases](/integrations/google-drive-knowledge-base), so your agent answers from Docs, Sheets, and files your team already maintains. Retell can only access the specific files you pick — never your whole Drive. This page covers the connection itself.
Google Drive adds no integration tools. Like [Microsoft OneDrive](/integrations/microsoft-onedrive) and [Notion](/integrations/notion), its job is feeding knowledge bases.
## When to use it
Connect Google Drive when the content your agent should know lives in Google Docs or Sheets that people keep editing. It's the right choice when you want to:
* **Stop re-uploading documents.** A price list or policy doc added from Drive re-syncs when the source changes, so the knowledge base follows the document instead of snapshotting it.
* **Let non-developers own the content.** The team edits the Doc they already work in; nobody exports PDFs or touches the dashboard to keep the agent current.
* **Use spreadsheets as knowledge.** Sheets sync with their cell values read directly, which beats exporting to CSV and uploading.
For example, a property management company keeps its leasing FAQ in a Google Doc the operations team edits weekly. The Doc is a knowledge base source, so the leasing agent quotes current pet policies and fees without anyone redeploying a thing.
## Prerequisites
* A Google account with access to the files you want to sync. Retell keeps acting as this account, so prefer a shared team account over a personal one — if the account loses access to a file (or gets deactivated), that source stops refreshing.
## Connect Google Drive
In the Retell Dashboard, open **Integrations**, select the **Available** tab, find **Google Drive**, and click **Connect** (**Add Account** if a connection already exists). You can also start from a knowledge base: the **Add** menu shows **Connect Google Drive** when no account is connected yet.
The dialog only asks for a connection name (prefilled with `Google Drive - OAuth`). Click **Connect** (**Add Account** if a connection already exists), and the browser goes to Google's sign-in page. Pick the account, grant the requested access, and you return to the dashboard. Retell stores a refresh token, so nobody has to sign in again.
Once connected, the account appears in every knowledge base's **Add** menu — see [Google Drive knowledge base sync](/integrations/google-drive-knowledge-base) for picking files and how refresh works.
## How access works
Retell requests Google's `drive.file` scope, which grants access **only to files you explicitly pick** in the Google file picker. Retell cannot list, search, or read anything else in the Drive. Revoking the app's access in your Google account settings cuts off all of it at once.
## FAQ
Content already synced stays in your knowledge bases and keeps answering; it just stops refreshing. Reconnecting and re-picking the files resumes updates.
No. The `drive.file` scope only grants access to files you picked in the file picker. Retell cannot browse, list, or search the rest of the Drive, and Google's consent screen reflects that.
Yes. Each connection is separate, and the knowledge base **Add** menu lists them all, so different teams can feed knowledge bases from different Drives.
## Next steps
Pick the Docs, Sheets, and files your agent answers from, and keep them re-syncing as they change.
See every provider Retell connects to and how integration tools work.
# Google Drive knowledge base sync
Source: https://docs.retellai.com/integrations/google-drive-knowledge-base
Sync Google Drive files into a Retell AI knowledge base: pick Docs and Sheets once, and changed files re-sync automatically on a 24-hour refresh cycle.
Files you add from Google Drive become [knowledge base](/build/knowledge-base) sources that follow the document: when the Doc or Sheet changes, Retell re-syncs it automatically, so your agent answers from the version your team edits. This page covers picking the files and keeping them current.
Adding Drive files requires a [connected Google Drive account](/integrations/google-drive).
## Add Drive files
Go to **Knowledge Base**, open or create one, and open the **Add** menu. Your connected Drive accounts are listed there.
Select the Drive account, then pick files in the Google file picker. This is Google's own picker embedded in the dashboard, so your browser must allow third-party cookies from Google for it to load (see [troubleshooting](#troubleshooting)).
Google Docs and Google Sheets work natively, and so does every file format the knowledge base supports for [document upload](/build/knowledge-base). Each file's type decides how its content is extracted.
Picked files are fetched, chunked, and embedded like any other source. The knowledge base shows each file with its processing status.
A knowledge base holds at most 25 Google Drive sources, each up to 50 MB. Beyond that, adding fails with `too many google drive sources, please reduce the number to below 25`.
## Keeping content in sync
With auto-refresh enabled on the knowledge base, Retell re-checks each Drive source's last-modified time on every refresh cycle (every 24 hours) and re-syncs only files that changed; unchanged files are skipped. If a file can't be re-fetched (deleted, permission lost, or the connection removed), the knowledge base keeps serving the previously synced content rather than dropping it.
## Troubleshooting
The file picker is Google's own picker, embedded in the dashboard from Google's servers, and it needs a signed-in Google session inside that embedded frame. If your browser blocks third-party cookies, which is the default in incognito or private windows and in strict privacy settings, the picker can't establish that session and the dashboard reports **Google Picker failed to load** after about 10 seconds. Allow third-party cookies for Google's domains, or add an exception for the Retell dashboard, then try again.
The picker shows what the connected Google account can see. If the file lives in someone else's Drive or a restricted shared drive, share it with the connected account first.
Check three things: the knowledge base has auto-refresh enabled, the Drive connection still exists on the **Connected** tab, and the connected account still has access to the file.
Sheets sync as cell values. Heavily formatted or sparse spreadsheets can chunk poorly for retrieval — see the [knowledge base formatting tips](/build/knowledge-base) for what retrieves well.
## FAQ
Google Docs and Sheets, plus any format the knowledge base accepts for [document upload](/build/knowledge-base) — PDF, DOCX, TXT, HTML, CSV, and the rest of the list on that page. If a file would upload fine, it syncs fine from Drive.
With auto-refresh enabled, changed files are re-synced on the knowledge base's refresh cycle, which runs every 24 hours. An edit made this morning reaches the agent after the next refresh, not instantly.
## Next steps
Create a knowledge base, tune retrieval, and see the formatting that retrieves well.
Set up the OAuth connection and see exactly what access the drive.file scope grants.
Do the same with Word, Excel, and PDF files from a connected Microsoft OneDrive.
Do the same with pages from a connected Notion workspace.
# Connect HubSpot
Source: https://docs.retellai.com/integrations/hubspot
Connect HubSpot to Retell AI with a private app access token: the scopes to grant, where the token lives, and how to verify the connection works.
Connecting HubSpot takes a private app access token. One connection covers both [contact sync](/integrations/hubspot-contact-sync) and [agent functions](/integrations/hubspot-functions): your agents work the portal's contacts, deals, and companies, and Retell keeps the records current. This page covers the HubSpot-side setup and the connection itself.
This is the setup guide for using HubSpot as a **CRM data source**. If you want to trigger outbound calls *from* HubSpot workflows instead, see the [HubSpot Marketplace app](/integrations/hubspot-marketplace). The two are independent and can be used together.
## When to use it
Connect HubSpot when it's your system of record and you want your agents working from it without anyone copying data between tools. It's the right choice when you want to:
* **Call or text people who already exist in HubSpot.** Contacts sync into Retell automatically, so your agent greets callers by name and knows their deal stage or lifecycle stage instead of asking.
* **Keep HubSpot current without manual data entry.** Analysis results from each conversation write back to contact properties.
* **Give your team call history where they already work.** Each call and chat appears on the contact's activity timeline with its summary and duration.
* **Let the agent act on HubSpot directly.** [Integration tools](/integrations/hubspot-functions) identify the caller by number, read their deals and companies, update records the caller corrects, and book follow-up tasks.
For example, an e-commerce brand's inbound line answers with a Retell agent that searches HubSpot for the caller's number, sees their open deal, and logs the call to the timeline; when the caller confirms a new shipping address, the agent updates the contact property on the spot.
## Prerequisites
* A HubSpot account with **Super Admin** permissions. Only a super admin can create a private app and grant it scopes.
Video walkthrough: connecting HubSpot and setting up contact sync end to end.
## Step 1: Create a private app
In HubSpot, click **Development** at the bottom of the left sidebar, then select **Legacy Apps**. Click **Create legacy app** in the top-right corner, then choose **Private** in the dialog.
HubSpot renamed the private apps section to **Legacy Apps** and steers new development toward its newer developer platform. Private apps still work and remain the supported way to connect HubSpot to Retell. Some accounts still show this section as **Private Apps** under **Settings > Integrations**; if you don't see **Development** in the sidebar, look there.
On the **Basic Info** tab, enter:
* **Name** — a descriptive name, for example `Retell AI Integration`.
* **Description** — optional, for example "Syncs contacts and logs call activity for Retell AI".
## Step 2: Grant scopes
On the **Scopes** tab, click **Add new scope**, search for each scope below in **Find a scope**, check it, then click **Update**.
Contact sync and activity logging need three scopes:
| Scope | Required for |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `crm.objects.contacts.read` | Importing contacts, the connection test Retell runs when you connect, and the contact lookup tools |
| `crm.schemas.contacts.read` | Reading contact property definitions: field mapping, tool configuration, and every contact read the tools make |
| `crm.objects.contacts.write` | Outbound sync, logging calls and chats to the timeline, and the contact, note, and task tools |
If your agents will use the deal and company [tools](/integrations/hubspot-functions#available-tools), also grant:
| Scope | Required for |
| ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- |
| `crm.objects.deals.read` | The **Get Deal** and **List Contact Deals** tools |
| `crm.objects.deals.write` | The **Update Deal** tool |
| `crm.objects.companies.read` | The **Get Company** and **List Contact Companies** tools |
| `crm.objects.companies.write` | The **Update Company** tool |
| `crm.schemas.deals.read`, `crm.schemas.companies.read` | Reading deal and company property definitions, both when you configure those tools and on every read they make |
HubSpot authorizes the note, task, and call engagement endpoints through the contact scopes, so there's no separate engagement scope to grant. If a tool fails because of a missing scope, HubSpot rejects the call with a `403` whose error body names the missing scope (category `MISSING_SCOPES`). Grant it under [**Edit app**](#change-scopes) and the tool starts working without reconnecting.
Grant the scopes up front. Once connected, a missing scope doesn't flag the connection, because HubSpot answers with a `403` rather than rejecting the credentials — the affected feature just stops working silently. The exception is `crm.objects.contacts.read`: the connection test reads contacts, so without it connecting fails outright.
## Step 3: Generate the access token
Click **Create app** in the top-right corner. Review the confirmation dialog and click **Continue creating**.
Click **Show token**, then copy it. If you need it again later, a super admin can reveal it anytime from the app's **Auth** tab.
## Step 4: Connect HubSpot in Retell
In the Retell Dashboard, open **Integrations**, select the **Available** tab, find **HubSpot**, and click **Connect** (**Add Account** if a connection already exists).
Fill in the fields:
| Field | Value |
| ------------------- | -------------------------------------------------------------- |
| **Connection name** | Alias for this connection; prefilled with `HubSpot - API key`. |
| **API Key** | The private app access token from Step 3. |
Click **Connect** (**Add Account** if a connection already exists). Retell creates the connection and immediately tests it by reading a page of contacts from HubSpot.
On success the dialog reports the connection as verified and offers **Set up contact sync**. On failure it shows HubSpot's own error and re-enables the field so you can paste a corrected token.
On the **Connected** tab, the HubSpot connection shows as connected. Next, set up [contact sync](/integrations/hubspot-contact-sync) to import your contacts, or start using [agent functions](/integrations/hubspot-functions) right away.
## Manage the private app
### Rotate the access token
Go to **Development > Legacy Apps** and click your Retell app's name. Next to the access token, click **Rotate**, then choose how the old token expires:
* **Rotate and expire later** keeps the old token valid for 7 days. Pick this one. Retell keeps working while you swap the credential over.
* **Rotate and expire now** kills the old token immediately, so contact sync and activity logging fail until Retell has the new one.
On the **Connected** tab, open the connection's settings. The saved token shows masked; paste the new token over it and click **Reconnect**. Retell verifies the new token before saving, and your field mappings and synced contacts are untouched.
### Change scopes
Go to **Development > Legacy Apps**, click your Retell app's name, then click **Edit app** in the top-right corner to change its scopes. Adding a scope takes effect without a new token. Removing one stops the corresponding Retell feature working, without flagging the connection as broken.
## Troubleshooting
Confirm the value in the **API Key** field is the private app **access token**, not HubSpot's legacy developer API key or an OAuth client secret. Check that the app has `crm.objects.contacts.read`, which the connection test needs, and that the token hasn't been rotated since you copied it.
Retell flags a connection as errored when HubSpot rejects the credentials with an HTTP 401. The usual cause is a rotated or deleted token. Open the connection's settings, paste a current token, and click **Reconnect**.
## FAQ
You can add multiple connections, but only one CRM connection in your workspace can drive [contact sync](/integrations/hubspot-contact-sync) at a time, across every provider. The **Contact sync** toggle in a connection's settings decides which one; turning it on for one connection takes sync over from the previous one.
No, they do different jobs. This integration syncs contacts and logs activity. The [Marketplace app](/integrations/hubspot-marketplace) adds a **Make a Phone Call** action to HubSpot workflows so HubSpot can trigger outbound calls. You can run both.
## Next steps
Import your HubSpot contacts, write analysis results back, and log calls and chats to the timeline.
Look up callers, read their deals and companies, and create tasks and notes mid-conversation.
Use the Marketplace app to start outbound calls from a HubSpot workflow.
How contact sync, analysis mapping, and activity logging work across CRM providers.
# HubSpot contact sync
Source: https://docs.retellai.com/integrations/hubspot-contact-sync
Sync HubSpot contacts with Retell AI: field mappings, Post Call Extraction write-back, and call and chat logging on the contact activity timeline.
Contact sync imports the connected portal's contacts into Retell, writes [Post Call Extraction](/features/post-call-analysis-overview) results back to their properties, and logs every call and chat on the contact's activity timeline. This page covers the HubSpot-specific behavior; [CRM integrations](/integrations/crm-overview) explains the four data flows all CRM providers share.
Contact sync requires a [connected HubSpot portal](/integrations/hubspot). Integration tools work without it — see [HubSpot agent functions](/integrations/hubspot-functions).
## Required scopes
Contact sync uses three of the private app's scopes, granted in [Step 2 of connecting](/integrations/hubspot#step-2-grant-scopes):
| Scope | Used for |
| ---------------------------- | --------------------------------------------------------- |
| `crm.objects.contacts.read` | Importing contacts |
| `crm.schemas.contacts.read` | Reading property definitions for field mapping |
| `crm.objects.contacts.write` | Outbound sync and logging calls and chats to the timeline |
A missing scope doesn't flag the connection; the affected part of sync silently stops working.
## Set up contact sync
After the connection test passes, click **Set up contact sync** to open the field mapping dialog, then map the HubSpot properties you want to import and the Retell fields you want to write back. For a connection made earlier, the same dialog opens from the **Connected** tab: open the connection's settings and click **Set up contact sync**.
Retell pre-fills one mapping in each direction: HubSpot `phone` to Retell `phone_number`. Phone number is how contacts are matched between the two systems, so it stays mapped and can't be removed. See [CRM data mappings](/integrations/crm-mappings) for how to map the rest, create custom fields, and choose update modes.
To log conversations to HubSpot, turn on **Log activities automatically** on the **Sync to HubSpot** tab.
## Verify it worked
* Open **Contacts**. After the first sync, HubSpot contacts appear with correctly formatted phone numbers and your mapped fields populated.
* The first sync is a full scan of every contact that has the mapped phone property, so a large portal takes a while. After that, Retell polls every 5 minutes (as of August 2026) and imports only contacts modified since the last run.
Contacts with no value in the mapped phone property are excluded from the sync entirely, as are contacts whose phone number can't be parsed into a valid E.164 number. Fix or remove malformed numbers in HubSpot before relying on two-way sync.
## How are conversations logged in HubSpot?
A call becomes a **Call** engagement associated with the matched contact, with its status set to `COMPLETED`, plus the duration and direction. A chat becomes a **Communication** object on the SMS channel, since HubSpot has no chat engagement type. Both carry a body with the conversation ID, the from and to numbers, the disconnection reason for calls, and the summary.
## Troubleshooting
A scope or permission error on a single property doesn't fail the sync or flag the connection, because Retell only treats credential rejections as connection errors. Check that the property is mapped on the right tab of the sync settings, and that the app has the write scope if you expect Retell to update it.
Activity logging needs three things: **Log activities automatically** enabled on the **Sync to HubSpot** tab of the sync settings, a Retell contact that was imported from this HubSpot connection, and the `crm.objects.contacts.write` scope on your private app. Retell attaches the activity to the contact it matched by phone number, so a call from a number that isn't a synced HubSpot contact is never logged.
Retell only imports contacts that have a value in the mapped phone property, and skips any whose number can't be parsed into E.164. A contact with a phone number stored in a different property than the one you mapped won't sync. Check which HubSpot property you mapped to `phone_number` in the sync settings.
## FAQ
Only if you opt in. By default, outbound sync updates contacts that already exist in HubSpot and never creates or deletes them. Turn on **Create new contacts in CRM** on the **Sync to HubSpot** tab to have Retell create a HubSpot contact after a conversation when the matched contact isn't linked to one yet. With the toggle off, contacts Retell creates on its own, for example from an inbound call from an unknown number, stay in Retell and aren't pushed to HubSpot. Your agent can also create a contact through the **Create Contact** [tool](/integrations/hubspot-functions), but that's an explicit tool call, not sync.
Yes. Map any HubSpot contact property, including custom ones, to a Retell [custom field](/integrations/crm-mappings#custom-fields). Objects other than contacts, such as deals and companies, are not part of contact sync.
They stay. Turning contact sync off stops future syncs, and deleting the connection removes the stored credentials. Contacts already imported remain in Retell either way.
## Next steps
Map HubSpot properties to Retell contacts, choose update modes, and control what syncs back.
Accumulate what your agents learn across conversations into the contact record.
Reference synced contact fields from your agent's prompt.
Look up callers, read their deals and companies, and create tasks and notes mid-conversation.
# HubSpot agent functions
Source: https://docs.retellai.com/integrations/hubspot-functions
HubSpot tools for Retell AI agents: look up and update contacts, work deals and companies, create tasks and notes, and log calls to the timeline.
A [connected HubSpot portal](/integrations/hubspot) gives your agents live tools for the portal: look up the caller, read their deals and companies, and create tasks and notes. Tools run during a conversation or [before and after it](/agent/agent-workflow), and no [contact sync](/integrations/hubspot-contact-sync) is required.
## Available tools
These tools appear in your agent's function menu once the portal is connected. See [use integration tools in an agent](/integrations/overview#use-integration-tools-in-an-agent) for how to add and configure them.
| Tool | What it does |
| ------------------------------------ | ----------------------------------------------------------------------------------------- |
| **Search Contact** | Find a contact by phone number — typically the caller's number, to identify who's calling |
| **Get Contact** | Fetch a contact's properties by ID |
| **Create Contact** | Create a contact for a caller who isn't in the CRM yet |
| **Update Contact** | Update properties the caller confirms or corrects |
| **List Contact Deals** | List the deals associated with a contact (up to 50) |
| **Get Deal** / **Update Deal** | Fetch a deal, or change its stage, amount, or other properties |
| **List Contact Companies** | List the companies associated with a contact (up to 50) |
| **Get Company** / **Update Company** | Fetch a company, or update its properties |
| **List Contact Tasks** | List a contact's follow-up tasks (up to 50) |
| **Create Task** | Create a task on the contact when a follow-up is agreed |
| **Create Note** | Record a summary of the conversation on the contact's timeline |
| **Log Call Activity** | Log the call as a Call engagement on the contact's timeline |
**Log Call Activity** is the per-call, agent-driven version of activity logging: the agent decides when to log and what summary to write. **Log activities automatically** in the [sync settings](/integrations/hubspot-contact-sync) logs every conversation with a synced contact without the agent doing anything. Use one or the other, or both if you want automatic logs plus richer agent-written ones.
## Required scopes
Each tool works only if the private app holds its scope, granted in [Step 2 of connecting](/integrations/hubspot#step-2-grant-scopes):
| Scope | Used for |
| ----------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `crm.objects.contacts.read` | Search Contact, Get Contact, List Contact Tasks |
| `crm.objects.contacts.write` | Create Contact, Update Contact, Create Task, Create Note, Log Call Activity |
| `crm.objects.deals.read` | List Contact Deals (with `crm.objects.contacts.read`), Get Deal |
| `crm.objects.deals.write` | Update Deal |
| `crm.objects.companies.read` | List Contact Companies (with `crm.objects.contacts.read`), Get Company |
| `crm.objects.companies.write` | Update Company |
| `crm.schemas.contacts.read`, `crm.schemas.deals.read`, `crm.schemas.companies.read` | Reading property definitions, both when you configure the tools and on every read call |
HubSpot authorizes the task, note, and call engagement endpoints through the contact scopes, so there's no separate engagement scope to grant. A missing scope doesn't flag the connection, because HubSpot answers with a `403` rather than rejecting the credentials. The error body names the missing scope (category `MISSING_SCOPES`); granting it under [**Edit app**](/integrations/hubspot#change-scopes) fixes the tool without reconnecting.
## FAQ
No. Tools work as soon as the connection is made, and each tool is bound to the specific connection you pick when configuring it. Contact sync, analysis mapping, and automatic activity logging are separate features you opt into.
## Next steps
Add HubSpot tools to a single- or multi-prompt agent and test them with live requests.
Call HubSpot tools from a function node and branch on the result.
Import your HubSpot contacts, write analysis results back, and log calls and chats to the timeline.
Map tool responses to variables your agent can use later in the conversation.
# Trigger Retell calls from HubSpot workflows
Source: https://docs.retellai.com/integrations/hubspot-marketplace
Install and configure the Retell AI HubSpot app to trigger outbound voice agent calls from HubSpot workflows, pass contact context, and sync call results back.
This guide provides instructions for setting up and using the **Retell AI** application within **HubSpot** to automate outbound phone calls using voice agents.
Open the Retell AI integration in the HubSpot Marketplace.
This app lets HubSpot trigger calls. If you want the reverse, HubSpot contacts synced into Retell and call activity written back to the timeline, set up the [HubSpot CRM integration](/integrations/hubspot) instead. The two are independent and can be used together.
## Overview
The Retell AI application enables the **Make a Phone Call** action in HubSpot workflows. This action creates an outbound call using your AI agents and pauses the workflow until the call is finished.
Once a call is completed, HubSpot is automatically updated with:
* **Activity Timeline**: Post Call Extraction and call summary
* **Call Log**: Recording and detailed call transcript
* **Company Record**: Call logs also appear on the associated company timeline
## Installing the Application
Click **Connect app** when prompted during installation.
You will be redirected to an external integration form.
Sign up on the Retell AI website to access your dashboard if you don't already have an account.
Navigate to **Settings → API Keys** in the Retell AI Dashboard.
Copy the **Secret Key (Webhook)** and paste it into the **Retell API Key** field on the installation form.
Copy the **Webhook URL** provided in the form and paste it into the Webhooks section of your Retell Dashboard (**Settings → Webhooks**).
Click **Save** to submit the form, then close the page and return to HubSpot.
## Using the Application
HubSpot workflows allow you to automatically trigger outbound calls based on various events. Common use cases include:
* **New lead qualification**: Call leads immediately after they submit a form to qualify interest
* **New contact created**: Reach out to new contacts added to your CRM
* **Deal stage changes**: Follow up when a deal moves to a specific stage
* **Re-engagement**: Call contacts who haven't been active for a set period
* **Appointment reminders**: Confirm upcoming meetings or demos
* **Post-purchase follow-up**: Check in with customers after a purchase
You must have a Retell AI account and an agent with a connected phone number.
### Step 1: Creating the HubSpot Workflow
Navigate to **Automation → Workflows** in HubSpot.
Create a new workflow and choose your trigger based on your use case:
* **Form submission**: Trigger when a lead fills out a specific form
* **Record created**: Trigger when a new contact is added to your CRM
* **Property value change**: Trigger when a deal stage or lead status changes
* **Date-based**: Trigger based on a specific date property (e.g., appointment date)
For this example, set the trigger to **Data Values → Record Created**.
Add a condition for **Phone number is known**. This ensures the workflow only triggers for contacts with valid phone numbers, preventing failed call attempts.
You can also add additional conditions to further qualify which contacts receive calls:
* **Lead status**: Only call contacts with a specific lead status
* **Lifecycle stage**: Target contacts at a particular stage (e.g., "Lead" or "Marketing Qualified Lead")
* **Contact owner**: Route calls based on the assigned sales rep
* **Custom properties**: Filter based on your business-specific criteria
The final trigger should look like the following:
Click the **(+)** button to add an action. Select **Retell AI → Make a Phone Call** under "Integrated apps".
Configure the call form with the following settings:
* **From**: Select the Retell AI agent/phone number
* **To**: Select the contact's phone number token
* **Dynamic Variables** (Optional): Pass data like the contact's name using JSON format. Ensure all values are surrounded by quotes.
Click **Save**.
After the call completes, you can branch your workflow based on the call outcome to automate follow-up actions.
Use the **Call Success** output from the Retell AI action to create branches:
**If call was successful:**
* Send a follow-up email with next steps
* Create a task for the sales rep to review the call
* Update the contact's lifecycle stage
* Add the contact to a nurture sequence
**If call was unsuccessful** (no answer, voicemail, etc.):
* Schedule a retry call for a later time
* Send an SMS or email as an alternative touchpoint
* Add to a "needs follow-up" list
You can also use other call outputs like **User Sentiment** or **Call Outcome** to create more granular branching logic.
Review and click **Review and publish** to activate.
### Step 2: Viewing Call Results in HubSpot
After a contact is enrolled in the workflow and the call completes, you can view the results directly in HubSpot.
Navigate to **CRM → Contacts** and open the contact that was enrolled in the workflow.
You can also find recently called contacts by filtering the contact list by the workflow enrollment date or checking the workflow history.
Check the contact's **Activity** tab to view the **Call Analysis**.
Ensure your activity filters include "Retell AI" as shown below:
Each call displays two types of analysis data:
**Default Call Results** — Automatically generated for every call:
* Summary
* Duration
* Voicemail detection
* User Sentiment
* Call Outcome
**Custom Analysis** — Additional insights you configure in Retell AI using [Post Call Extraction](/features/post-call-analysis-overview):
* Lead qualification status
* Custom scoring metrics
* Business-specific data extraction
* Any other fields you define
Check the **Calls** tab to view the full **Call Log** and recording.
## Uninstalling the Application
Go to **Connected Apps** and select **Retell AI**.
Navigate to the **General Settings** tab.
Click **Uninstall**.
Your data will be deleted from Retell AI records and the app will be removed from HubSpot.
# Connect Dynamics 365
Source: https://docs.retellai.com/integrations/microsoft-dynamics
Connect Microsoft Dynamics 365 to Retell AI with OAuth: find your Dataverse environment URL, sign in with Microsoft, and verify the connection works.
Connecting Microsoft Dynamics 365 takes your environment URL and a Microsoft sign-in. One connection covers both [contact sync](/integrations/microsoft-dynamics-contact-sync) and [agent functions](/integrations/microsoft-dynamics-functions): your agents work your Contacts, Tasks, and Notes live, and Retell keeps the records current. This page covers what you need on the Microsoft side and how to connect.
## When to use it
Connect Dynamics 365 when it's your system of record and you want your agents working from it without anyone copying data between tools. It's the right choice when you want to:
* **Call or text people who already exist in Dynamics.** Contacts sync into Retell automatically, so your agent greets callers by name and knows their details instead of asking.
* **Keep Dynamics current without manual data entry.** Analysis results from each conversation write back to fields on the Contact record.
* **Give your team call history where they already work.** Each call lands on the Contact's timeline as a completed Phone Call activity; each chat lands as a Task.
Your agents can also look up and manage Contacts, Tasks, and Notes through [integration tools](/integrations/microsoft-dynamics-functions#available-tools).
For example, a home-services company syncs its Dynamics Contacts into Retell. Its outbound reminder agent greets each customer by name, confirms tomorrow's appointment window, writes the confirmed time back to the Contact, and the call lands on the timeline as a completed Phone Call activity.
## Prerequisites
* A Dynamics 365 environment on Dataverse (Retell talks to its Web API, `/api/data/v9.2`).
* A Microsoft work account with access to that environment, holding a security role that can read and write **Contacts**, **Tasks**, **Phone Calls**, and **Notes**.
* Retell acts as whoever signs in, so tasks and activities it creates are attributed to that account. Use a dedicated integration user rather than a person's account, so the connection doesn't break when someone changes roles or leaves.
## Step 1: Find your environment URL
Retell needs the environment's Dataverse URL, which looks like `https://your-org.crm.dynamics.com` (the region segment varies, for example `crm4` or `crm11`).
* The easiest place to read it is your browser's address bar while you're using the Dynamics app — everything up to and including `.dynamics.com`.
* Admins can also find it in the **Power Platform admin center** under **Environments**, as the environment's **Environment URL**.
Enter just the origin, with no path after `.dynamics.com`.
## Step 2: Connect Dynamics 365 in Retell
In the Retell Dashboard, open **Integrations**, select the **Available** tab, find **Microsoft Dynamics 365**, and click **Connect** (**Add Account** if a connection already exists).
Fill in the fields:
| Field | Value |
| ---------------------------- | --------------------------------------------------------------------------- |
| **Connection name** | Alias for this connection; prefilled with `Microsoft Dynamics 365 - OAuth`. |
| **Dynamics environment URL** | The URL from Step 1, for example `https://your-org.crm.dynamics.com`. |
Click **Connect** (**Add Account** if a connection already exists). A Microsoft sign-in window opens; sign in with the integration account and accept the requested access. Retell stores a refresh token, so nobody has to sign in again.
If your organization restricts which apps users may consent to, Microsoft shows an approval-required message instead of the consent screen. A Microsoft Entra admin then has to grant consent for Retell before the connection can complete.
Retell tests the connection by calling the environment's WhoAmI endpoint as the signed-in user. On success the connection appears on the **Connected** tab and the dialog offers **Set up contact sync** — see [Dynamics 365 contact sync](/integrations/microsoft-dynamics-contact-sync) to import your Contacts, or start using [agent functions](/integrations/microsoft-dynamics-functions) right away.
## Troubleshooting
The signed-in account can authenticate against Microsoft without having access to the environment you entered. Confirm the account can open that environment in a browser, and that the URL is the environment's own Dataverse origin with nothing after `.dynamics.com`.
Your organization requires admin consent for new apps. Ask a Microsoft Entra admin to grant consent for Retell, then connect again. Separately, Power Platform admins can restrict which client apps may access a specific environment — if consent is granted but calls are still refused, check the environment's app access controls in the Power Platform admin center.
Retell flags a connection as errored when Microsoft rejects the stored token — typically because the integration account's password was reset, the account was disabled, or an admin revoked the app's access. Open the connection's settings and click **Reconnect** to sign in again with a working account.
## FAQ
Everything Retell creates is owned by the account that signed in when connecting. That's why a dedicated integration user is worth setting up: the timeline then reads "Retell integration" rather than a teammate's name.
Yes, add one connection per environment. Only one CRM connection in your workspace can drive [contact sync](/integrations/microsoft-dynamics-contact-sync) at a time, across every provider — the **Contact sync** toggle in a connection's settings decides which one. Integration tools work on every connection.
## Next steps
Import your Dynamics Contacts, write analysis results back, and log calls on the Contact's timeline.
Look up callers, update Contacts, and create Tasks and Notes mid-conversation.
See every provider Retell connects to and how integration tools work.
How contact sync, analysis mapping, and activity logging work across CRM providers.
# Dynamics 365 contact sync
Source: https://docs.retellai.com/integrations/microsoft-dynamics-contact-sync
Sync Microsoft Dynamics 365 Contacts with Retell AI: field mappings, Post Call Extraction write-back, and calls logged as Phone Call activities.
Contact sync imports your Dynamics Contacts into Retell, writes [Post Call Extraction](/features/post-call-analysis-overview) results back to their fields, and logs each call as a completed Phone Call activity on the Contact's timeline. This page covers the Dynamics-specific behavior; [CRM integrations](/integrations/crm-overview) explains the four data flows all CRM providers share.
Contact sync requires a [connected Dynamics 365 environment](/integrations/microsoft-dynamics). Integration tools work without it — see [Dynamics 365 agent functions](/integrations/microsoft-dynamics-functions).
## Required privileges
Sync runs as the [signed-in account](/integrations/microsoft-dynamics#prerequisites), whose security role needs:
* **Read** on Contact, including every field you import.
* **Write** on Contact for outbound sync.
* **Create** and **Write** on Phone Call and Task for activity logging, since Retell creates each activity and then closes it as completed. Attaching them to the Contact also needs **Append** on the activity and **Append To** on Contact.
A missing privilege doesn't flag the connection; that field or feature silently stops syncing.
## Set up contact sync
After the connection test passes, click **Set up contact sync** to open the field mapping dialog, then map the Dynamics fields you want to import and the Retell fields you want to write back. For a connection made earlier, the same dialog opens from the **Connected** tab: open the connection's settings and click **Set up contact sync**.
Retell pre-fills one mapping in each direction: Dynamics `telephone1` (Business Phone) to Retell `phone_number`. Phone number is how contacts are matched between the two systems, so it stays mapped and can't be removed. If your phone numbers live in a different field, such as `mobilephone`, change the external field on both tabs. See [CRM data mappings](/integrations/crm-mappings) for how to map the rest, create custom fields, and choose update modes.
To log conversations to Dynamics, turn on **Log activities automatically** on the **Sync to Microsoft Dynamics 365** tab.
## Verify it worked
* Open **Contacts**. After the first sync, Dynamics Contacts appear with correctly formatted phone numbers and your mapped fields populated.
* The first sync is a full scan of every contact that has the mapped phone field, so a large environment takes a while. After that, Retell polls every 5 minutes (as of August 2026) and imports only contacts modified since the last run.
Contacts with no value in the mapped phone field are excluded from the sync entirely, as are contacts whose number can't be parsed into a valid E.164 number.
## How are conversations logged in Dynamics 365?
A call becomes a **Phone Call** activity marked completed, with the direction, the duration rounded up to whole minutes, and a description carrying the call ID, the from and to numbers, the disconnection reason, and the summary. A chat becomes a completed **Task**. Both attach to the Contact Retell matched by phone number.
## Troubleshooting
Activity logging needs **Log activities automatically** enabled on the **Sync to Microsoft Dynamics 365** tab, a Retell contact that was imported from this connection, and a security role that can create and complete Phone Call and Task activities. A call from a number that doesn't match a synced contact is never logged.
## FAQ
By default it only updates Contacts that already exist in Dynamics, and it never deletes them. Turn on **Create new contacts in CRM** on the **Sync to Microsoft Dynamics 365** tab to have Retell create a Contact after a conversation when the matched contact isn't linked to one yet. Your agent can also create Contacts through the **Create Contact** [tool](/integrations/microsoft-dynamics-functions), but that's an explicit tool call, not sync.
## Next steps
Map Dynamics fields to Retell contacts, choose update modes, and control what syncs back.
Accumulate what your agents learn across conversations into the contact record.
Reference synced contact fields from your agent's prompt.
Look up callers, update Contacts, and create Tasks and Notes mid-conversation.
# Dynamics 365 agent functions
Source: https://docs.retellai.com/integrations/microsoft-dynamics-functions
Dynamics 365 tools for Retell AI agents: look up and update Contacts, create Tasks and Notes, and log calls as completed Phone Call activities.
A [connected Dynamics 365 environment](/integrations/microsoft-dynamics) gives your agents live tools for the environment: look up the caller, create Tasks and Notes, and log the call. Tools run during a conversation or [before and after it](/agent/agent-workflow), and no [contact sync](/integrations/microsoft-dynamics-contact-sync) is required.
## Available tools
These tools appear in your agent's function menu once the environment is connected. Every tool call runs as the [signed-in account](/integrations/microsoft-dynamics#prerequisites). See [use integration tools in an agent](/integrations/overview#use-integration-tools-in-an-agent) for how to add and configure them.
Records the tools create are owned by the account that signed in when connecting, so a dedicated integration user keeps the timeline reading "Retell integration" rather than a teammate's name.
| Tool | What it does |
| ------------------------------------ | ----------------------------------------------------------------------------------------- |
| **Search Contact** | Find a Contact by phone number — typically the caller's number, to identify who's calling |
| **Get Contact** / **Update Contact** | Fetch a Contact by ID, or update fields the caller confirms or corrects |
| **Create Contact** | Create a Contact for a caller who isn't in the CRM yet |
| **Create Task** | Create a Task on a Contact when a follow-up is agreed |
| **Create Note** | Attach a Note to a Contact with a summary of what was discussed |
| **Log Phone Call** | Log the call as a completed Phone Call activity on the Contact's timeline |
## Required privileges
Each tool needs the matching privilege on the signed-in account's security role:
| Tool | Security role privilege |
| --------------------------- | ------------------------------------------------------------------------------------------------- |
| Search Contact, Get Contact | **Read** on Contact |
| Create Contact | **Create** on Contact |
| Update Contact | **Write** on Contact |
| Create Task | **Create** on Task |
| Create Note | **Create** on Note |
| Log Phone Call | **Create** and **Write** on Phone Call (Retell creates the activity, then closes it as completed) |
Create Task, Create Note, and Log Phone Call each attach their record to a Contact, which in Dataverse also needs **Append** on the record being created and **Append To** on Contact. A missing privilege doesn't flag the connection; that one tool fails.
## Next steps
Add Dynamics 365 tools to a single- or multi-prompt agent and test them with live requests.
Call Dynamics 365 tools from a function node and branch on the result.
Import your Dynamics Contacts, write analysis results back, and log calls on the Contact's timeline.
Map tool responses to variables your agent can use later in the conversation.
# Connect OneDrive
Source: https://docs.retellai.com/integrations/microsoft-onedrive
Connect Microsoft OneDrive to Retell AI with a read-only Microsoft sign-in, then sync OneDrive files into knowledge bases your agent answers from.
Connecting Microsoft OneDrive lets you [add OneDrive files to knowledge bases](/integrations/microsoft-onedrive-knowledge-base), so your agent answers from the Word documents, Excel workbooks, and PDFs your team already maintains. Personal OneDrive and OneDrive for Business (work or school) both work, and the access Retell asks for is read-only. This page covers the connection itself.
OneDrive adds no integration tools. Like [Google Drive](/integrations/google-drive) and [Notion](/integrations/notion), its job is feeding knowledge bases.
## When to use it
Connect OneDrive when the content your agent should know already lives in Microsoft 365. It's the right choice when you want to:
* **Keep the knowledge base in step with the file.** A price sheet or policy document added from OneDrive re-syncs when the file changes, so the knowledge base follows the document instead of snapshotting it.
* **Let non-developers own the content.** The team edits the same Word or Excel file they always have; nobody exports a PDF or opens the dashboard to keep the agent current.
* **Stay inside your existing Microsoft tenant.** Sign-in, consent, and revocation all run through Microsoft Entra ID, so the connection is governed like every other app your organization has approved.
For example, an HVAC company keeps its service-plan pricing in an Excel workbook on the operations lead's OneDrive, updated whenever a supplier raises a rate. The workbook is a knowledge base source, so the booking agent quotes current plan pricing without anyone touching the dashboard.
## Prerequisites
* **A Microsoft account with a OneDrive**, either personal or work/school. Retell keeps acting as this account, so prefer a shared team account over an individual's: if the account loses access to a file or gets deactivated, those sources stop refreshing.
* **Permission to approve the app.** The permissions Retell requests are delegated and read-only, which a user can normally grant themselves. If your tenant turns off user consent for apps, Microsoft shows a **Need admin approval** screen instead, and an Entra ID admin has to approve Retell first.
## Connect OneDrive
In the Retell Dashboard, open **Integrations**, select the **Available** tab, find **Microsoft OneDrive**, and click **Connect** (**Add Account** if a connection already exists). You can also start from a knowledge base: the **Add** menu shows **Connect Microsoft OneDrive** when no account is connected yet.
The dialog only asks for a connection name (prefilled with `Microsoft OneDrive - OAuth`). Click **Connect** (**Add Account** if a connection already exists), and the browser goes to Microsoft's sign-in page, returning to the dashboard once consent completes.
Expect up to two approval prompts. The first sign-in tells Retell whether the account has a personal or a work/school OneDrive, and the second grants the file picker read access to that specific OneDrive. Both are read-only, and Microsoft skips the second prompt if the account has already approved it. Retell stores a refresh token, so nobody has to sign in again.
Once connected, the account appears in every knowledge base's **Add** menu. See [OneDrive knowledge base sync](/integrations/microsoft-onedrive-knowledge-base) for picking files and how refresh works.
## What you can pick
A connection reaches the signed-in account's own OneDrive and nothing else:
* **Your OneDrive files.** The picker opens on the connected account's own OneDrive, personal or business.
* **SharePoint is out of scope.** Only the picker's **My files** view is available; its Recent, Shared, Quick access, My organization, and site views are turned off because they reach into SharePoint. If a file still resolves to a SharePoint document library, the knowledge base rejects it while saving with `Selected file is in SharePoint, not OneDrive`. To use such a file, copy it into the connected account's OneDrive first.
* **Folders and OneNote notebooks can't be synced.** Neither is a downloadable file: folders are for browsing only, and a notebook that gets picked is rejected with `OneDrive item is not a downloadable file`.
## How access works
Retell requests two read-only Microsoft permissions, delegated to the account that signs in:
| Permission | What it covers |
| ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Files.Read.All` (Microsoft Graph) | Reads file metadata and content on behalf of the signed-in account. Retell uses it to check a file's name, size, and last-modified time, and to download the files you added to a knowledge base. |
| Picker access to your OneDrive — `OneDrive.ReadOnly` for a personal account, `-my.sharepoint.com/MyFiles.Read` for work or school | Runs Microsoft's own file picker against your OneDrive so you can browse and select files from inside Retell. |
Neither permission grants write access, so Retell can't create, change, or delete anything in OneDrive. Both are delegated permissions, capped at what the signed-in account can already access. They are account-wide rather than per-file, because Microsoft's file picker offers no per-file grant like Google's `drive.file` scope; what Retell actually fetches is still limited to the files you added to a knowledge base. It never lists, searches, or downloads anything else.
Deleting the connection on the **Connected** tab discards the stored refresh token, so Retell's access ends immediately. To revoke from Microsoft's side as well: for a personal account, remove Retell in your Microsoft account's privacy settings under **Apps and services that can access your data**; for work or school, remove it in the My Apps portal, or have an admin revoke it in Microsoft Entra ID under **Enterprise applications**.
## FAQ
The Graph permission is account-wide, so the token technically covers files the account can read. Retell only ever downloads the files you picked and added to a knowledge base. It doesn't list, search, or index the rest, and it never writes.
Personal and work OneDrive run on different Microsoft endpoints, and the file picker needs a permission scoped to whichever one you have. The first sign-in identifies your account type, and the second grants picker access to that OneDrive. Microsoft skips the second prompt when the account has already approved it.
Usually no: the permissions are delegated and read-only, which a user can normally grant. If your tenant turns off user consent for apps, Microsoft shows its **Need admin approval** screen instead, and an Entra ID admin has to approve Retell before the connection completes.
Yes. Each connection is separate and is pinned at connect time to the account's OneDrive type, so a personal Microsoft account and a work account are two connections. The knowledge base **Add** menu lists them all.
Content already synced stays in your knowledge bases and keeps answering; it just stops refreshing, and its source rows show **Sync not available**. Reconnecting and re-picking the files resumes updates.
Retell verifies at the end of the flow that the account it authorized is still the one it started with. Signing into a different account mid-flow, or an account that moved between a personal and a work OneDrive, trips this check. Start the connection again and sign in with a single account.
## Next steps
Pick the files your agent answers from, and keep them re-syncing as they change.
See every provider Retell connects to and how integration tools work.
# OneDrive knowledge base sync
Source: https://docs.retellai.com/integrations/microsoft-onedrive-knowledge-base
Sync Microsoft OneDrive files into a Retell AI knowledge base: pick files in Microsoft's file picker, and changed files re-sync every 24 hours.
Files you add from Microsoft OneDrive become [knowledge base](/build/knowledge-base) sources that follow the file: when the document changes in OneDrive, Retell re-syncs it on the next refresh, so your agent answers from the version your team edits. This page covers picking the files and keeping them current.
Adding OneDrive files requires a [connected OneDrive account](/integrations/microsoft-onedrive).
## Add OneDrive files
Go to **Knowledge Base**, open or create one, and open the **Add** menu. Your connected OneDrive accounts are listed there; with none connected yet, the menu offers **Connect Microsoft OneDrive** instead.
Select the OneDrive account. Microsoft's own file picker opens in a **pop-up window**, so allow pop-ups for the Retell dashboard. If the browser blocks it, Retell reports **Allow pop-ups for this site to pick files from OneDrive**.
Select one or more files, then confirm. The picker only offers [file types the knowledge base can parse](#supported-file-types), and only files in the connected account's own OneDrive — not SharePoint libraries, shared-with-me files, folders, or OneNote notebooks (see [what you can pick](/integrations/microsoft-onedrive#what-you-can-pick)).
Picked files are downloaded, chunked, and embedded like any other source. The knowledge base shows each file with its processing status, and its row is labeled **OneDrive**. A source that fails to process doesn't block the others.
## Limits
| Limit | Value |
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| File size | 50 MB per file. Oversized files are dropped before the knowledge base saves, with a **File too large** notice naming them. |
| OneDrive sources per knowledge base | 25. Adding beyond that fails with `too many OneDrive sources, please reduce the number to below 25`. |
| Refresh cadence | Every 24 hours, with auto-refresh enabled on the knowledge base. |
Adding the same file twice is skipped rather than duplicated: Retell identifies a file by its drive and item, and reports **Already added**.
## Supported file types
OneDrive stores plain files, so what syncs is exactly what the knowledge base accepts for [document upload](/build/knowledge-base):
| Kind | Formats |
| ------------- | ------------------------------------------------------------------------------- |
| Documents | `.pdf`, `.doc`, `.docx`, `.odt`, `.rtf`, `.epub`, `.txt`, `.md`, `.rst`, `.org` |
| Spreadsheets | `.xls`, `.xlsx`, `.csv`, `.tsv` |
| Presentations | `.ppt`, `.pptx` |
| Web and data | `.htm`, `.html`, `.xml` |
| Email | `.eml`, `.msg`, `.p7s` |
| Images | `.bmp`, `.heic`, `.jpg`, `.jpeg`, `.png`, `.tif`, `.tiff` |
A file with no extractable text produces no content to index and fails with `produced no content to index`. The common case is a scanned, image-only PDF: Retell doesn't run OCR on it.
## Keeping content in sync
With auto-refresh enabled on the knowledge base, Retell re-checks each OneDrive file's last-modified time every 24 hours and re-downloads only the files that changed; unchanged files are skipped. Adding a OneDrive file to a new knowledge base turns auto-refresh on for you, and the toggle then reads **Automatically sync OneDrive files every 24 hours**.
Renaming a file in OneDrive is safe. Retell reads the current name from OneDrive on every sync, so a renamed file keeps syncing and a changed extension still picks the right parser.
If a file can't be re-fetched (deleted, permission lost, or the connection removed), the knowledge base keeps serving the previously synced content rather than dropping it. When the connection behind a source is gone, its row shows **Sync not available**.
## Troubleshooting
Microsoft's picker runs in a pop-up rather than inside the page, so a pop-up blocker stops it and Retell reports **Allow pop-ups for this site to pick files from OneDrive**. Allow pop-ups for the dashboard and try again. Only one picker runs at a time: launching a second one refocuses the window that's already open instead of starting over.
The picker has 30 seconds to load and connect back to the dashboard. Missing that window means the pop-up couldn't reach Microsoft's picker page: a network block, a browser extension interfering with the pop-up, or your organization's policy blocking the page. Retry once those are ruled out, and reconnect the integration if it keeps failing.
The connection is missing the account details the picker needs, which happens when a connect flow was interrupted partway. Open the connection on the **Connected** tab and click **Reconnect** to run the [Microsoft sign-in](/integrations/microsoft-onedrive) again.
The picker shows only files in the connected account's own OneDrive, filtered to [supported types](#supported-file-types). Files in a SharePoint site or document library, files shared with you from someone else's OneDrive, folders, and OneNote notebooks are all excluded. Copy the file into the connected account's OneDrive to sync it.
The knowledge base detail view names the source and the reason. The common ones are `is too large to index (max 50 MB)`, `produced no content to index` (an empty file or a scanned PDF), `is not a downloadable file` (a folder or OneNote notebook), and `Selected file is in SharePoint, not OneDrive`. Fix the file or pick a different one; the other sources still finish.
Check three things: the knowledge base has auto-refresh enabled, the OneDrive connection still exists on the **Connected** tab, and the connected account still has access to the file in OneDrive.
## FAQ
With auto-refresh enabled, changed files re-sync on the knowledge base's refresh cycle, which runs every 24 hours. An edit made this morning reaches the agent after the next refresh, not instantly. You can also trigger a sync by hand from the knowledge base detail view.
No. A connection reaches the signed-in account's own OneDrive only, personal or business. Copy the file into that OneDrive, or connect an account that owns it.
Yes. OneDrive sources sit alongside uploaded documents, crawled URLs, custom text, [Google Drive files](/integrations/google-drive-knowledge-base), and [Notion pages](/integrations/notion-knowledge-base) in the same knowledge base, and the auto-refresh toggle covers whichever syncing source types it holds.
No. The file stays in OneDrive: the knowledge base row references the OneDrive item rather than a stored copy, so it has no download action. Retell keeps only the extracted, embedded content used for retrieval.
## Next steps
Create a knowledge base, tune retrieval, and see the formatting that retrieves well.
Set up the connection and see exactly what access Retell asks Microsoft for.
Do the same with pages from a connected Notion workspace.
# Connect Notion
Source: https://docs.retellai.com/integrations/notion
Connect Notion to Retell AI with an API key: create an internal connection, share pages with it, and sync them into knowledge bases your agent answers from.
Connecting Notion lets you [add Notion pages to knowledge bases](/integrations/notion-knowledge-base), so your agent answers from the wiki, policies, and FAQs your team already maintains in Notion. Notion connects with an API key: the token of an **internal connection** you create in Notion's developer portal. Retell reads only the pages you share with that connection and never writes to your workspace. This page covers getting the key, sharing pages, and making the connection.
Notion adds no integration tools. Like [Google Drive](/integrations/google-drive) and [Microsoft OneDrive](/integrations/microsoft-onedrive), its job is feeding knowledge bases.
## When to use it
Connect Notion when the content your agent should know lives in a Notion workspace that people keep editing. It's the right choice when you want to:
* **Stop exporting Notion to files.** A page added from Notion re-syncs when the page changes, so the knowledge base follows the page instead of a PDF or Markdown export that goes stale.
* **Let non-developers own the content.** The team edits the Notion page they already work in; nobody exports files or opens the dashboard to keep the agent current.
* **Decide page by page what the agent sees.** Access is granted in Notion by sharing a page with the connection. Everything else in the workspace stays invisible to Retell.
For example, a veterinary clinic group keeps its clinic handbook in Notion: hours and locations, vaccination schedules, pre-surgery fasting instructions, and pricing, each as its own page. Those pages are knowledge base sources, so when the practice manager changes the fasting instructions in Notion, the inbound agent gives the new guidance after the next refresh.
## Prerequisites
* **You need to be a Workspace Owner in Notion.** Only Workspace Owners can create internal connections. If you're a Member, ask an owner to follow [Get your Notion API key](#get-your-notion-api-key) and send you the token; you can still [share pages](#share-pages-with-the-connection) with the connection yourself.
* **You need an Admin or Developer role in Retell**, or a custom role with the **App.Write** and **CRM.Write** permissions.
* **Know which pages the agent should read.** A connection starts with access to nothing, so have the top page of each wiki or handbook in mind. Sharing a page shares everything nested under it.
## Get your Notion API key
The API key Retell asks for is the access token of a Notion internal connection: a bot identity that belongs to your workspace and can read only the pages you share with it. Notion recommends internal connections for team-owned automations in one workspace, which is what a knowledge base sync is.
Sign in to Notion as a Workspace Owner and open the developer portal at [app.notion.com/developers/connections](https://app.notion.com/developers/connections) (inside Notion, this is **Developer tools**, then **Connections**). Click **New connection**.
In the dialog, give the connection a name your team will recognize, such as `Retell`. This name is what people see when they share a page with it, so avoid something generic. Under **Authentication method**, choose **Access token**: Notion describes it as a workspace-scoped static API token, limited to one workspace, which is exactly what Retell needs. Click **Create connection**. Notion creates the connection immediately; its page has **Configuration**, **Content access**, and other tabs. The connection belongs to the workspace you created it in and can only ever reach pages inside it.
Open the connection's **Configuration** tab and review its capabilities. Retell only reads pages, so set them as follows:
| Capability | Setting |
| ----------------------------------------- | ------------------------------------------------------------------- |
| **Read content** | On. Retell needs it to list, check, and fetch pages. |
| **Update content** and **Insert content** | Off. Retell never edits or creates anything in Notion. |
| **Read comments** and **Insert comments** | Off. Comments aren't synced. |
| User information | **No user information**. Retell doesn't read your member directory. |
Leaving extra capabilities on doesn't break anything, but the key would then carry more power than Retell uses, which matters if it ever leaks.
Still on the **Configuration** tab, find the **Installation access token** and copy it. It starts with `ntn_`, and it is the value you paste into Retell's **API Key** field.
Treat the token like a password: anyone holding it can read every page shared with the connection. Don't paste it into shared documents or chat. If it's ever exposed, refresh it from the same tab to replace it, then paste the new token into Retell (see the [FAQ](#faq)).
## Share pages with the connection
A new connection has access to nothing. Until you share pages with it, Retell's page picker is empty and there's nothing to sync.
Open a page your agent should know, click the **•••** menu in the top-right corner, select **Connections**, and click **+ Add connection** (in some Notion versions the menu item is labeled **Add connections**). Search for the connection by the name you gave it, select it, and confirm.
Sharing a page also shares every page nested under it, so sharing the top page of a wiki shares the whole wiki. Share the top page rather than each child page individually.
Open the connection at [app.notion.com/developers/connections](https://app.notion.com/developers/connections) and select the **Content access** tab. It lists the pages and databases the connection is enabled for, grouped by workspace. Click **+ Add pages & databases**, search for a page, and select it. This tab is also the quickest way to see everything the connection can currently read.
In a knowledge base, open the **Add** menu and select the connection. The **Select pages** dialog lists the shared pages as a tree. Nothing there yet means the share hasn't landed; reopen the picker after sharing, since it lists pages fresh every time.
To take a page away from the connection later, open the page's **•••** menu, hover over the connection's name under **Connections**, and select **Disconnect**, or remove it on the **Content access** tab in the developer portal. Pages already synced from it keep their content in Retell but stop refreshing.
On Notion's Enterprise plan, workspace owners can limit which connections members may add to pages and which pages a connection can reach, under **Settings**, then **Connections**, on the **Manage** tab. If **Add connection** is missing from a page menu or your connection isn't offered, ask a workspace owner to allow it.
## Add the connection in Retell
In the Retell Dashboard, open **Integrations**, select the **Available** tab, find **Notion**, and click **Connect** (**Add Account** if a connection already exists). You can also start from a knowledge base: the **Add** menu shows **Connect Notion** when no account is connected yet.
The dialog asks for a connection name (prefilled with `Notion - API key`) and an **API Key**. Paste the Installation access token and click **Connect**. Retell checks the key against Notion right away and reports **Connected to Notion.** when it works; click **Done**. The check passes even before you've shared any pages, so you can connect first and share afterwards.
If Notion rejects the key, the dialog reports **Could not connect. Check your credentials and try again.** and nothing is saved. Copy the token again from the connection's **Configuration** tab, making sure you have the whole `ntn_` string, and retry.
Once connected, the account appears in every knowledge base's **Add** menu. See [Notion knowledge base sync](/integrations/notion-knowledge-base) for picking pages and how refresh works.
## How access works
Retell stores the API key encrypted and never returns it through the API. With it, Retell can reach exactly the pages shared with the connection, including pages nested under a shared page, and nothing else in the workspace.
Retell uses the key for three things:
* **Listing shared pages**, so the knowledge base picker can show them.
* **Reading a page's title and last-edited time**, to decide whether a page changed since its last sync.
* **Fetching a page's content**, as Markdown, to chunk and embed it.
Retell also checks the key by retrieving the connection's own bot user when you connect and before every refresh. All of this is read-only: with only **Read content** enabled, Retell can't create, edit, or delete anything in Notion.
To end Retell's access, delete the connection on the **Connected** tab, which discards the stored key. To revoke from Notion's side as well, disconnect the connection from individual pages, refresh the token from the connection's **Configuration** tab, or delete the connection in the developer portal.
## FAQ
The **Installation access token** of an internal connection, shown on the connection's **Configuration** tab in Notion's developer portal at [app.notion.com/developers/connections](https://app.notion.com/developers/connections). It starts with `ntn_`. Create the connection in the workspace that holds the pages, copy the token from that tab, and paste it into the **API Key** field in Retell.
To create the connection, yes: Notion only lets Workspace Owners create internal connections. If you're a Member, an owner can create it in a minute and send you the token, and you paste it into Retell. Sharing pages with an existing connection works for any Member from the page's **•••** menu, unless your Enterprise workspace restricts which connections members may add.
No. The connection sees nothing until a page is shared with it, and then only the shared pages and the pages nested under them. Pages you never shared don't appear in the picker and can't be synced. A page that is later unshared or moved to the trash stops refreshing, and Retell keeps the content it already synced. The connection's **Content access** tab in Notion's developer portal shows exactly what it can reach.
Yes. A connection reads only the workspace it was created in, so create an internal connection in each workspace and add each one to Retell as its own connection. Retell allows up to 20 Notion connections per Retell workspace (as of September 2026), and the knowledge base **Add** menu lists them all.
No. The Installation access token is static, so the connection keeps working until you refresh the token from the **Configuration** tab or delete the connection in Notion. After a refresh, update Retell with the new token: open the connection on the **Connected** tab and paste it. Until you do, Notion turns Retell away, the connection shows a red **Connection error** tag, and its refreshes pause.
Up to 25 Notion pages per knowledge base, with each nested page counting as its own source; split larger wikis across several knowledge bases, since an agent can use more than one. The page picker lists up to about 3,000 shared pages per connection. A database can't be added as a single source, though its rows can be added one by one. See the [knowledge base sync limits](/integrations/notion-knowledge-base#limits) for the full list and the error each one produces.
Content already synced stays in your knowledge bases and keeps answering; it just stops refreshing, and its source rows show **Sync not available**. Reconnecting and re-picking the pages resumes updates. To see which knowledge bases use a connection before deleting it, open it on the **Connected** tab and check its **Used by** tab.
Notion rejected the key: the token was refreshed from the **Configuration** tab, or the connection was deleted in Notion. Retell keeps the content already synced and skips refreshes for that connection's pages until the key works again. Open the connection on the **Connected** tab and paste the current token.
## Next steps
Pick the pages your agent answers from, and keep them re-syncing as they change.
See every provider Retell connects to and how integration tools work.
# Notion knowledge base sync
Source: https://docs.retellai.com/integrations/notion-knowledge-base
Sync Notion pages into a Retell AI knowledge base: pick the pages shared with your connection, and edited pages re-sync automatically every 24 hours.
Pages you add from Notion become [knowledge base](/build/knowledge-base) sources that follow the page: when someone edits it in Notion, Retell re-syncs it on the next refresh, so your agent answers from the version your team maintains. This page covers picking pages and keeping them current.
Adding Notion pages requires a [connected Notion account](/integrations/notion) with the pages [shared with the connection](/integrations/notion#share-pages-with-the-connection).
## Add Notion pages
Go to **Knowledge Base**, open or create one, and open the **Add** menu. Your connected Notion accounts are listed there below the built-in source types; with none connected yet, the menu offers **Connect Notion** instead.
Select the Notion account. The **Select pages** dialog opens inside the dashboard, so unlike the Google Drive and OneDrive pickers it needs no pop-up window or third-party cookies.
The dialog lists every page shared with the connection as a tree, with **Name** and **Last modified** columns. A page with pages nested under it carries an arrow to expand them, and the search box filters by title, expanding any branch that holds a match. Tick the pages you want and click **Select**; the footer counts them as you go.
**Each page is its own source.** Ticking a page adds that page's own content only, so tick each nested page you want as well. Sharing the parent in Notion is what makes the nested pages appear here; adding them is still one tick per page.
Pages already in the knowledge base open ticked. Unticking one removes it when you save.
Picked pages are fetched, chunked, and embedded like any other source. The knowledge base shows each page by its Notion title, with its row labeled **Notion**. A page that fails to process doesn't block the others.
## What syncs from a page
Retell fetches each page as Markdown, so headings, paragraphs, lists, and other text blocks come through as text your agent can retrieve. The page title is attached to every chunk, so a descriptive title helps retrieval.
What is left out:
* **Nested pages.** A child page's content isn't part of its parent. Add each nested page as its own source.
* **Databases.** A database embedded in a page is skipped, and a database can't be added as a single source. Its rows are pages, though: they appear at the top level of the picker and can be added one by one.
* **Images and attached files.** An image contributes only its caption or alt text; audio, video, PDF, and file blocks contribute only their caption. Retell doesn't run OCR or read attachments.
* **Bookmarks, embeds, and link previews** contribute only their URL.
A page with nothing left after that fails with `Notion page "" produced no content to index (empty page, or all content is in nested pages which must be added separately)`. The common case is an index page that holds only a heading and links to nested pages.
Notion pages that retrieve well look like good Markdown: clear headings, short focused sections, and related facts kept together on one page. See the [knowledge base formatting tips](/build/knowledge-base#best-practices).
## Limits
| Limit | Value |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Notion pages per knowledge base | 25. Adding beyond that fails with `too many notion pages, please reduce the number to below 25`. Split larger wikis across several knowledge bases; an agent can use more than one. |
| Page size | No byte limit. Retell fetches a page in segments of about 20,000 blocks, up to 20 segments; a page larger than that fails with `Notion page is too large to index; split it into smaller pages`. |
| Pages the picker can list | About 3,000 shared pages per connection. Beyond that the picker fails to load; share fewer pages with the connection, or use one connection per part of the workspace. |
| Refresh cadence | Every 24 hours, with auto-refresh enabled on the knowledge base. |
Figures as of September 2026.
## Keeping content in sync
With auto-refresh enabled on the knowledge base, Retell re-checks each Notion page's last-edited time every 24 hours and re-fetches only the pages that changed; unchanged pages are skipped. Adding a Notion page to a new knowledge base turns auto-refresh on for you, and the toggle then reads **Automatically sync Notion pages every 24 hours**. An edit made within a minute of a sync is still picked up on the following refresh.
Renaming or moving a page in Notion is safe. Retell reads the current title on every sync, and the page keeps its id when moved, as long as it stays shared with the connection. Pages you share later aren't added on their own: open the picker and tick them.
If a page can't be re-fetched, the knowledge base keeps serving the previously synced content rather than dropping it. That covers a page that was unshared, moved out of a shared tree, or moved to the trash, a connection whose token Notion no longer accepts (shown with a red **Connection error** tag on the **Connected** tab), and a connection that was deleted from Retell, whose rows show **Sync not available**.
## Troubleshooting
The connection exists but nothing has been shared with it. In Notion, open a page, click the **•••** menu, select **Connections**, and add the connection (see [share pages with the connection](/integrations/notion#share-pages-with-the-connection)). Then reopen the picker; it lists pages fresh every time.
Retell couldn't list the connection's pages. Check the **Connected** tab first: a red **Connection error** tag means Notion rejected the token, so paste the current one. If the connection is healthy, the connection may have more shared pages than the picker can list (about 3,000); share fewer pages with it.
The picker shows only pages shared with the connection, so share the page or one of its parents in Notion. Pages in the trash are hidden. A database itself never appears, only its rows, and a page that lives inside a toggle, column, or other block is listed at the top level rather than under the page that contains it. Use the search box when the tree is large.
The knowledge base detail view names the page and the reason. The common ones are `produced no content to index` (an empty page, or one whose content is all in nested pages), `was not found or is not shared with the integration` (the page was unshared or deleted after you picked it), `is in the trash`, and `is too large to index`. Fix the page in Notion or pick a different one; the other sources still finish.
Check three things: the knowledge base has auto-refresh enabled, the Notion connection on the **Connected** tab exists and carries no **Connection error** tag, and the page is still shared with the connection in Notion.
Retell lists the connection's pages from Notion when the picker opens, 100 pages per request, and Notion limits how fast those requests can run. A connection with thousands of shared pages takes several seconds to load. Sharing only the parts of the workspace the agent needs keeps it quick.
## FAQ
With auto-refresh enabled, changed pages re-sync on the knowledge base's refresh cycle, which runs every 24 hours. An edit made this morning reaches the agent after the next refresh, not instantly. You can also trigger a sync by hand from the knowledge base detail view.
Yes. A page's content doesn't include its child pages, and ticking a parent in the picker doesn't tick its children. Sharing the top page in Notion makes the whole tree visible in the picker; you still choose the pages, up to 25 per knowledge base.
Not as one source. A database embedded in a page is skipped, and the database itself isn't listed in the picker. Each row of a database is a page, so rows appear at the top level of the picker and can be added individually. For tabular content, a spreadsheet synced from [Google Drive](/integrations/google-drive-knowledge-base) or [OneDrive](/integrations/microsoft-onedrive-knowledge-base) is often the better fit.
Yes. Notion pages sit alongside uploaded documents, crawled URLs, custom text, [Google Drive files](/integrations/google-drive-knowledge-base), and [OneDrive files](/integrations/microsoft-onedrive-knowledge-base) in the same knowledge base, and the auto-refresh toggle covers whichever syncing source types it holds.
No. The page stays in Notion: the knowledge base row references the Notion page rather than a stored copy, so it has no download action. Retell keeps only the extracted, embedded content used for retrieval.
## Next steps
Create a knowledge base, tune retrieval, and see the formatting that retrieves well.
Create the internal connection, share pages with it, and see exactly what Retell can read.
# Integrations overview
Source: https://docs.retellai.com/integrations/overview
Connect HubSpot, Salesforce, Zendesk, Google Drive, Notion, Calendly, and more to Retell AI for agent tools, contact sync, and knowledge base sync.
Retell connects to the CRMs, helpdesks, calendars, and knowledge sources your agents work against: HubSpot, Salesforce, Microsoft Dynamics 365, GoHighLevel, and Zoho CRM for CRM, Zendesk for support tickets, Google Drive, Microsoft OneDrive, and Notion for knowledge bases, and Calendly, Cal.com, and GoHighLevel for scheduling. Connect a provider once on the dashboard's **Integrations** page, and every agent in your workspace can use its tools before, during, and after a conversation (or, for the three knowledge base providers, their synced knowledge base content). The [table below](#supported-providers) is the current list.
## What does an integration give you?
* **Integration tools.** Every provider except Google Drive, Microsoft OneDrive, and Notion adds tools your agent can call before, during, or after a call or chat: search a contact by the caller's number, check calendar availability, book an appointment, create a support ticket. See [use integration tools in an agent](#use-integration-tools-in-an-agent) and [agent workflow](/agent/agent-workflow).
* **Contact sync and activity logging** (CRM providers). Import CRM contacts into Retell, write [Post Call Extraction](/features/post-call-analysis-overview) results back to contact fields, and log each conversation to the CRM. [CRM integrations](/integrations/crm-overview) covers this in full.
* **Knowledge base sources** ([Google Drive](/integrations/google-drive), [Microsoft OneDrive](/integrations/microsoft-onedrive), [Notion](/integrations/notion)). Sync files or pages from a connected account into a [knowledge base](/build/knowledge-base) so your agent answers from their content, and re-syncs them as they change.
## Supported providers
| Provider | Category | What your agent can do | Credentials |
| ---------------------------------------------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| [HubSpot](/integrations/hubspot) | CRM | Look up and manage contacts, deals, companies, and tasks; log calls and notes to the timeline | Private app access token |
| [Salesforce](/integrations/salesforce) | CRM | Look up and manage contacts, leads, accounts, opportunities, cases, and custom objects; create tasks and notes; run SOQL queries | External Client App client ID and secret, plus your instance URL |
| [Microsoft Dynamics 365](/integrations/microsoft-dynamics) | CRM | Look up and manage contacts; create tasks and notes; log phone call activities | OAuth sign-in, plus your Dynamics environment URL |
| [GoHighLevel](/integrations/gohighlevel) | CRM | Look up and manage contacts, opportunities, and tasks; add tags; create notes; check availability and book, reschedule, or cancel appointments on its [calendars](/integrations/gohighlevel-functions#book-the-sub-accounts-calendars) | API key, plus the sub-account Location ID |
| [Zoho CRM](/integrations/zoho) | CRM | Look up and manage Contacts, read their Deals, create Tasks and Notes, and log calls to the timeline | OAuth sign-in |
| [Zendesk](/integrations/zendesk) | Support | Identify the caller, look up their tickets, create and update tickets, add public comments or private notes | OAuth sign-in, plus your Zendesk URL |
| [Google Drive](/integrations/google-drive) | Knowledge base | Answer questions from Docs, Sheets, and files synced into a knowledge base | Google OAuth sign-in |
| [Microsoft OneDrive](/integrations/microsoft-onedrive) | Knowledge base | Answer questions from OneDrive files synced into a knowledge base | Microsoft OAuth sign-in |
| [Notion](/integrations/notion) | Knowledge base | Answer questions from Notion pages synced into a knowledge base | API key (internal connection token) |
| [Calendly](/integrations/calendly) | Calendar | Check availability, book, cancel, and look up appointments | API key (personal access token) |
| [Cal.com](/integrations/cal-com) | Calendar | Check availability, book, reschedule, cancel, and look up appointments | API key, on cal.com or cal.eu |
## Connect a provider
In the Retell Dashboard, open **Integrations** and select the **Available** tab. Connecting requires an Admin or Developer role; custom roles need the **App.Write** and **CRM.Write** permissions.
Pick your provider and enter what it asks for: an API key, or an OAuth sign-in on the provider's own page. Some providers also ask for your tenant URL (Salesforce instance URL, Dynamics environment URL, Zendesk URL) or account ID (GoHighLevel Location ID). Retell encrypts stored credentials and never returns them through the API.
Retell tests the credentials as part of connecting. The connection then appears on the **Connected** tab; from there you can rename it, rotate credentials, or set up CRM contact sync. A healthy connection shows no tag; a broken one carries a red **Connection error** tag, and the connection driving contact sync carries a blue **Contacts sync on** tag.
For the provider-side setup (creating the app, granting scopes, finding the token), follow the provider's guide, linked in the table above.
## Use integration tools in an agent
Once a provider is connected, its tools show up when you add a function to an agent, whether in a prompt agent's **Functions** section or a conversation flow's function node; see [integration tools for prompt agents](/build/single-multi-prompt/integration-tools) and [integration tools in conversation flow](/build/conversation-flow/integration-tools) for each surface. Each configured tool is bound to one specific connection, so if you connect two HubSpot accounts, you choose which one the tool uses.
### Configure a tool's inputs and outputs
Every tool config carries a **Name**, a **Description** that tells the LLM when to call it, and a **Function fields** card with an **Input** tab and an **Output** tab.
On the **Input** tab, each field the provider accepts carries a mode pill that decides where its value comes from:
* **Value** — a literal you type, like a Cal.com event type ID. It can contain `{{variable}}` references, resolved when the tool runs.
* **Description** — a sentence the LLM uses to fill the value from the conversation, like "the caller's preferred appointment time."
* **Select**, **Boolean**, or **Array items** — the typed control the field's own shape gives it: a fixed list of options, a true/false toggle, or a list of rows. A field gets at most one of these.
Not every field offers all of these. Where the provider requires a fixed value, the field shows only the control it allows.
### Run a test to get a tool's output fields
The **Output** tab decides what the response gives back: which fields the agent sees, and which become [dynamic variables](/build/dynamic-variables). Most providers ship a response schema, so the field picker is already populated. Run a test when a tool has no schema, or when you want the real field names and values in front of you.
A test request is sent to the connected provider for real. A lookup reads live data, and testing a create or update tool writes a real record. Use values you don't mind touching.
On the **Output** tab, click **Run a test**, fill in the tool's inputs, and run it. Retell calls the provider and shows the actual response: a checkable field tree on the **Fields** tab, the raw payload on the **JSON** tab.
Enter concrete values here rather than `{{variable}}` references. A test resolves variables in the tool's pinned inputs, but not in the values you type into the test panel.
In the test panel, tick the fields worth keeping and click **Add selected outputs**. They land in the **Output** tab's list under **Fields for agent context**, the only part of the response the agent and the transcript see, which keeps a large provider payload from crowding the prompt. Leave the list empty to send the whole response.
**Select fields** on the **Output** tab opens the same picker against the tool's response schema, so you can choose fields without running anything; it confirms with its own **Select fields** button.
**Manually add fields** takes a dot-path the schema doesn't describe. A path names a branch and keeps everything under it; a plain segment reaches into every element of an array (`deals.properties.amount` keeps that field on all of them), while `deals[0].id` keeps only the first.
In the **Dynamic variable** column, name any field you want to reuse: in the prompt, in a later tool's input, or in a function on the [agent's workflow](/agent/agent-workflow). Variables are extracted from the raw response, so narrowing what the agent sees never breaks one you mapped.
For example, a dental clinic connects Cal.com and HubSpot. Its inbound agent searches HubSpot for the caller's number to greet them by name, checks Cal.com availability for the requested week, books the cleaning appointment, then logs the call on the HubSpot timeline, without a human touching either system.
## Run tools before or after the call or chat
Tools can also run outside the conversation. On the agent's **Workflow** page, add them as **pre-call functions**, which run before the call starts so the agent has context before it speaks, or as **post-call functions**, which run after the call ends to update your systems. Chat agents have the same two slots, named pre-chat and post-chat functions.
Each slot is a dependency graph rather than a list: functions with no dependency all start at once, and a function you add as sequential waits for the one above it and can use its output through a dynamic variable. Post-conversation functions can also be gated on a condition, so a follow-up task is created only when the session earned one.
[Agent workflow](/agent/agent-workflow) covers the whole surface: chaining, conditions, timing budgets, failure behavior, and worked examples for both voice and chat.
## FAQ
Yes. Each connection is separate, and each tool is bound to a specific connection. The one exception is CRM contact sync: only one CRM connection can drive [contact sync](/integrations/crm-overview) for your workspace at a time.
No. Integration tools work as soon as the connection is made. Contact sync and automatic activity logging are separate CRM features you opt into per connection; [analysis data mappings](/integrations/crm-mappings) are configured once per workspace.
Each tool call times out after 3 to 14 seconds, depending on the provider and tool (as of August 2026). If the provider doesn't respond in time or returns an error, the tool call fails and the agent continues the conversation; prompt your agent on what to say when a lookup or booking doesn't go through.
Retell marks a connection as errored when the provider rejects its credentials, flagging it with the red **Connection error** tag on the **Connected** tab. On most providers a missing scope or permission doesn't flag the connection, because the provider answers with a permission error rather than rejecting the credentials; the affected feature quietly stops working, so check provider-side permissions first when one tool misbehaves.
Two things do flag it. The connection test, run when you connect or reconnect, flags the connection whenever it fails, including on a scope the test itself needs. And on [GoHighLevel](/integrations/gohighlevel-functions#required-scopes) a missing scope comes back as an authorization error Retell can't tell apart from a dead token, so a tool call can flag the connection.
# Connect Salesforce
Source: https://docs.retellai.com/integrations/salesforce
Connect Salesforce to Retell AI with an External Client App: OAuth client credentials, the Run As user setup, and how to verify the connection works.
Connecting Salesforce takes an External Client App with the client credentials flow enabled and a Run As user. One connection covers both [contact sync](/integrations/salesforce-contact-sync) and [agent functions](/integrations/salesforce-functions): your agents work your Contacts, opportunities, and cases live, and Retell keeps the Contact records current. This page covers the Salesforce-side setup and the credentials Retell needs.
Retell authenticates with the **OAuth 2.0 client credentials flow**.
## When to use it
Connect Salesforce when Salesforce is your system of record and you want your agents to work from it without anyone copying data between tools. It's the right choice when you want to:
* **Call or text people who already exist in Salesforce.** Contacts sync into Retell automatically, so your agent greets callers by name and knows their account details instead of asking for them.
* **Keep Salesforce current without manual data entry.** Analysis results from each conversation (qualification status, stated preferences, a corrected email address) write back to the Contact record.
* **Give your sales team call history where they already work.** Each call and chat lands on the Contact's activity timeline as a Task, with the summary and duration.
* **Let the agent act on Salesforce directly.** [Integration tools](/integrations/salesforce-functions) identify the caller by number, read their opportunities and cases, create a Contact or Lead when a new caller comes in, run a SOQL query for anything else, and update records the caller corrects.
For example, an insurance agency's outbound agent works renewal lists from Salesforce: it greets each Contact by name, answers policy questions from the Account record, creates a Lead when a referral comes up, and every conversation lands on the timeline as a Task.
## Prerequisites
* A Salesforce edition with API access: Enterprise, Unlimited, Developer, or Performance. Professional needs Salesforce's paid API add-on; Essentials has no API access at all.
* **System Administrator** permissions in Salesforce, or a role that can create External Client Apps.
* A Salesforce user to run the integration as. Use a dedicated integration user rather than a person's account, so the connection doesn't break when someone changes roles or leaves.
Video walkthrough: connecting Salesforce and setting up contact sync end to end.
## Step 1: Create an External Client App
Log in to Salesforce as an administrator. Click the **gear icon** in the top-right corner, then select **Setup**.
In the left sidebar under **Platform Tools**, go to **Apps > External Client Apps > External Client App Manager**. Click **New External Client App** in the top-right corner.
**External Client Apps** replace the older **Connected Apps** for new integrations. If your org still creates apps under **App Manager > New Connected App**, the field names are the same but the screens are laid out differently, and the client credentials setting lives under **Manage > Edit Policies** instead of the Policies tab.
Under **Basic Information**, enter:
* **External Client App Name** — a descriptive name, for example `Retell AI`.
* **API Name** — auto-filled from the name; leave it as is.
* **Contact Email** — your admin email address.
Leave **Distribution State** set to `Local`. The app only needs to work inside your own org.
## Step 2: Enable OAuth and the client credentials flow
Still on the creation screen, expand **API (Enable OAuth Settings)** and turn OAuth on. This reveals the **App Settings** fields below.
Enter this **Callback URL**:
```
https://api.retellai.com/oauth-callback/salesforce
```
The client credentials flow never redirects a browser, so this value is never used. Salesforce requires the field regardless, and any valid HTTPS URL is accepted.
Move these from **Available OAuth Scopes** to **Selected OAuth Scopes**:
* **Manage user data via APIs (api)** — the only scope Retell requires. It covers every REST and SOQL call Retell makes.
* **Perform requests at any time (refresh\_token, offline\_access)** — optional. The client credentials flow doesn't issue refresh tokens, so this changes nothing for Retell, but it's harmless if your org adds it by default.
Don't select **Full access (full)** alone. The client credentials flow filters `full` out of the tokens it issues, and since Salesforce's Winter '26 release, a token request whose selected scopes are all unsupported is rejected with `invalid_grant: no valid scopes defined`.
Under **Flow Enablement**, check **Enable Client Credentials Flow**. This is the setting that lets Retell authenticate without an interactive login. Leave the other flows unchecked.
Click **Create**.
A new External Client App can take up to 30 minutes to become available (Salesforce cites 2 to 10 minutes for most apps). If connecting in Retell fails right after you create the app, wait and try again before assuming the credentials are wrong.
## Step 3: Copy the consumer key and secret
From the External Client App Manager, open the app you just created and select the **Settings** tab. Expand **OAuth Settings**, then under **App Settings** click **Consumer Key and Secret**.
Salesforce may ask you to verify your identity with a code sent to your email before showing the credentials.
Copy and securely store:
* **Consumer Key** — Retell's **Client ID**.
* **Consumer Secret** — Retell's **Client Secret**.
Treat the consumer secret like a password. Don't share it in plaintext or commit it to source control. Retell encrypts it at rest and never returns it once saved.
## Step 4: Set the Run As user
The client credentials flow has no logged-in user, so Salesforce needs to know whose permissions to apply. Every read and write Retell makes runs as this user.
On the app's detail page, select the **Policies** tab and click **Edit**.
Expand **OAuth Policies** and find **OAuth Flows and External Client App Enhancements**. Check **Enable Client Credentials Flow**, then enter your integration user's username in **Run As (Username)**.
Enter the user's **Username**, not their email address. They're separate fields, and because a username has to be unique across every Salesforce org, they often differ. A sandbox, for example, appends the sandbox name, so `you@acme.com` becomes `you@acme.com.dev`. Copy the exact value from the **Username** column under **Setup > Users > Users**.
You check **Enable Client Credentials Flow** in two places, and both are required. The checkbox at creation time turns the flow on for the app; this one binds it to a running user. Without a Run As user, token requests fail even though the flow looks enabled.
Every read and write Retell makes is checked against the Run As user's profile and permission sets, so grant only what you'll use:
* **API Enabled** on the user's profile or a permission set. Without it, every API call is refused.
* **Read** on Contact and on every field you plan to import.
* **Edit** on Contact and on every field you plan to write back, if you enable outbound sync.
* **Create** on Task, if you enable activity logging or the **Create Task** tool.
* For the [integration tools](/integrations/salesforce-functions) you plan to use: **Read** on Lead, Account, Opportunity, Case, and User; **Edit** on Contact, Lead, and Account for the update tools; **Create** on Contact and Lead for the create tools; **Read** on every object a **Query Records** SOQL query touches; and the same permissions on any custom object you configure a tool for. Creating a Note needs **Edit** on the record it attaches to, since Notes take their access from the parent record.
A missing object or field permission doesn't break the connection. It makes that field silently fail to sync, or that one tool fail, which is harder to spot, so check the profile or permission set before you rely on a mapping.
Click **Save**.
## Step 5: Connect Salesforce in Retell
In the Retell Dashboard, open **Integrations**, select the **Available** tab, find **Salesforce**, and click **Connect** (**Add Account** if a connection already exists).
Fill in the fields:
| Field | Value |
| ------------------- | ----------------------------------------------------------------- |
| **Connection name** | Alias for this connection; prefilled with `Salesforce - OAuth`. |
| **Instance URL** | Your My Domain URL, for example `https://acme.my.salesforce.com`. |
| **Client ID** | The **Consumer Key** from Step 3. |
| **Client Secret** | The **Consumer Secret** from Step 3. |
The instance URL has to be your My Domain URL in the form `https://.my.salesforce.com`, lowercase and with no path; a trailing slash is trimmed for you. A Lightning URL like `https://acme.lightning.force.com` is rejected with `Instance URL must be in the format https://.my.salesforce.com`. Sandbox domains such as `https://acme--dev.sandbox.my.salesforce.com` are accepted.
Click **Connect** (**Add Account** if a connection already exists). Retell creates the connection and immediately tests it against the Salesforce API.
On success the dialog reports the connection as verified and offers **Set up contact sync**. On failure it shows Salesforce's own error and re-enables the fields so you can correct them.
On the **Connected** tab, the Salesforce connection shows as connected. See [Salesforce contact sync](/integrations/salesforce-contact-sync) to import your Contacts and log conversations as Tasks, or start using [agent functions](/integrations/salesforce-functions) right away.
## Troubleshooting
Give a newly created External Client App up to 30 minutes to become available, then retry. If it still fails, confirm **Enable Client Credentials Flow** is checked on both the app's creation settings and its Policies tab, and that **Run As (Username)** is set.
Find the correct value in Salesforce under **Setup > Company Settings > My Domain**, then check it against the format above. Lightning URLs (`.lightning.force.com`) and bare `.salesforce.com` URLs are rejected.
Retell flags a connection as errored when Salesforce rejects the credentials: an HTTP 401, or Salesforce's `INVALID_SESSION_ID`. The usual causes are a rotated consumer secret, a deactivated Run As user, or the app being deleted or disabled in Salesforce. Reconnect with current credentials.
If your org enforces login IP ranges on the Run As user's profile, or the app's OAuth policies enforce IP restrictions, Salesforce refuses Retell's calls because they come from cloud IPs. On the app's **Policies** tab, set **IP Relaxation** to **Relax IP restrictions**, or exempt the integration user's profile from login IP ranges.
On the **Connected** tab, open the connection's settings. The saved secret shows masked; paste the new one over it (re-enter the Client ID too if it changed) and click **Reconnect**. Retell verifies the new credentials before saving, and your field mappings and synced contacts are untouched.
## FAQ
You can add multiple connections, but only one CRM connection in your workspace can drive [contact sync](/integrations/salesforce-contact-sync) at a time, across every provider. The **Contact sync** toggle in a connection's settings decides which one; turning it on for one connection takes sync over from the previous one.
## Next steps
Import your Contacts, write analysis results back, and log conversations as Tasks.
Look up the caller, read opportunities and cases, create contacts and leads, and run SOQL queries mid-conversation.
See every provider Retell connects to and how integration tools work.
How contact sync, analysis mapping, and activity logging work across CRM providers.
# Salesforce contact sync
Source: https://docs.retellai.com/integrations/salesforce-contact-sync
Sync Salesforce Contacts with Retell AI: phone-matched field mappings, Post Call Extraction write-back, and calls and chats logged as Salesforce Tasks.
Contact sync imports your Salesforce Contacts into Retell, writes [Post Call Extraction](/features/post-call-analysis-overview) results back to their fields, and logs every call and chat as a Salesforce Task on the matched Contact. This page covers the Salesforce-specific behavior; [CRM integrations](/integrations/crm-overview) explains the four data flows all CRM providers share.
Contact sync requires a [connected Salesforce org](/integrations/salesforce). Integration tools work without it — see [Salesforce agent functions](/integrations/salesforce-functions).
## Required permissions
Sync runs as the Run As user, set up in [Step 4 of connecting](/integrations/salesforce#step-4-set-the-run-as-user). Its profile or permission sets need:
* **API Enabled**.
* **Read** on Contact and on every field you import.
* **Edit** on Contact and on every field you write back, if you enable outbound sync.
* **Create** on Task, if you enable activity logging.
A missing object or field permission doesn't flag the connection; that field or feature silently stops syncing.
## Set up contact sync
After the connection test passes, click **Set up contact sync** to open the field mapping dialog, then map the Salesforce fields you want to import and the Retell fields you want to write back. For a connection made earlier, the same dialog opens from the **Connected** tab: open the connection's settings and click **Set up contact sync**.
Retell pre-fills one mapping in each direction: Salesforce `Phone` to Retell `phone_number`. Phone number is how contacts are matched between the two systems, so it stays mapped and can't be removed. See [CRM data mappings](/integrations/crm-mappings) for how to map the rest, create custom fields, and choose update modes.
To log conversations to Salesforce, turn on **Log activities automatically** on the **Sync to Salesforce** tab.
## Verify it worked
* Open **Contacts**. After the first sync, Salesforce Contacts appear with correctly formatted phone numbers and your mapped fields populated.
* The first sync is a full scan of every Contact that has the mapped phone field, so a large org takes a while. After that, Retell polls every 5 minutes (as of August 2026) and imports only Contacts modified since the last run.
Contacts with no value in the mapped phone field are skipped, as are Contacts whose number can't be parsed into a valid E.164 number. Fix or remove malformed numbers in Salesforce before relying on two-way sync.
## How are calls and chats represented in Salesforce?
Both become Task records associated with the matched Contact through `WhoId`, with `Status` set to `Completed`. A call uses the `Call` task subtype, a subject of `Call `, and sets `CallDurationInSeconds`. A chat uses the generic `Task` subtype, since Salesforce has no standard chat or SMS subtype. The description carries the conversation ID, the from and to numbers, the disconnection reason for calls, and the summary.
## Troubleshooting
A permission error on a single field doesn't fail the sync or flag the connection, because Retell only treats credential rejections as connection errors. Check that the [Run As user's](/integrations/salesforce#step-4-set-the-run-as-user) profile or permission set grants read (and edit, for outbound) on that specific field, and that the field is mapped on the right tab of the sync settings.
Activity logging needs three things: **Log activities automatically** enabled on the **Sync to Salesforce** tab of the sync settings, a Retell contact that was imported from this Salesforce connection, and **Create** permission on Task for the [Run As user](/integrations/salesforce#step-4-set-the-run-as-user). Retell attaches the Task to the Contact it matched by phone number, so a call from a number that isn't a synced Salesforce Contact is never logged.
## FAQ
Only if you opt in. By default, outbound sync updates Contacts that already exist in Salesforce and never creates or deletes them. Turn on **Create new contacts in CRM** on the **Sync to Salesforce** tab to have Retell create a Salesforce Contact after a conversation when the matched contact isn't linked to one yet. With the toggle off, contacts Retell creates on its own (for example from an inbound call from an unknown number) stay in Retell and aren't pushed to Salesforce. Your agent can also create a Contact mid-conversation with the **Create Contact** [tool](/integrations/salesforce-functions), but that's an explicit tool call, not sync.
Only `Contact`. Leads, Accounts, Opportunities, Cases, and custom objects are not part of contact sync — the [integration tools](/integrations/salesforce-functions) reach those objects live instead.
Yes. Map any Salesforce Contact field, including `__c` custom fields, to a Retell [custom field](/integrations/crm-mappings#custom-fields).
They stay. Turning contact sync off stops future syncs, and deleting the connection removes the stored credentials. Contacts already imported remain in Retell either way.
## Next steps
Map Salesforce fields to Retell contacts, choose update modes, and control what syncs back.
Accumulate what your agents learn across conversations into the contact record.
Reference synced contact fields from your agent's prompt.
Look up the caller, read opportunities and cases, and create leads and tasks mid-conversation.
# Salesforce agent functions
Source: https://docs.retellai.com/integrations/salesforce-functions
Salesforce tools for Retell AI agents: identify callers, read opportunities and cases, create contacts, leads, and tasks, run SOQL queries, and update records.
A [connected Salesforce org](/integrations/salesforce) gives your agents live Salesforce tools: look up the caller, read their opportunities and cases, create Contacts and Leads, run your own SOQL queries, and update records mid-call. Tools run during a conversation or [before and after it](/agent/agent-workflow), and no [contact sync](/integrations/salesforce-contact-sync) is required.
## Available tools
Once connected, these tools appear in your agent's function menu. Every tool call runs as the [Run As user](/integrations/salesforce#step-4-set-the-run-as-user), so its permissions decide what each tool can reach. See [use integration tools in an agent](/integrations/overview#use-integration-tools-in-an-agent) for how to add and configure them.
| Tool | What it does |
| --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| **Search Contact** | Find a Contact by phone number — typically the caller's number, to identify who's calling |
| **Get Contact** / **Create Contact** / **Update Contact** | Fetch a Contact by record Id, create one for a caller who isn't in the CRM yet, or update fields the caller confirms or corrects |
| **Get Lead** / **Create Lead** / **Update Lead** | Fetch a Lead, create one for a new prospect, or update it |
| **Get Account** / **Update Account** | Fetch an Account by record Id, or update its fields |
| **List Account Opportunities** | List an Account's Opportunities (up to 50), most recent first, when the conversation turns to the account's deals |
| **Get Opportunity** | Fetch an Opportunity, for example the deal the caller is asking about |
| **Get Case** | Fetch a Case, for example a support case the caller references |
| **Get User** | Fetch a User by record Id — a member of your Salesforce org, not the caller, for example the owner of an account from an earlier lookup |
| **Get Custom Object** / **Update Custom Object** | Fetch or update a record of a custom object you pick when configuring the tool |
| **Query Records** | Run a SOQL query you write, with dynamic variables filled in at run time, and return the matching records |
| **Create Task** | Create a Task on a Contact or Lead when a follow-up is agreed |
| **Create Note** | Attach a Note to a Contact or Lead with a summary of what was discussed |
## Required permissions
The [Run As user](/integrations/salesforce#step-4-set-the-run-as-user) always needs **API Enabled**; each tool then needs the matching object permission on the user's profile or permission sets:
| Tool | Run As permission |
| ------------------------------------------- | -------------------------------------------------------------------------------------------- |
| Search Contact, Get Contact | **Read** on Contact |
| Create Contact | **Create** on Contact |
| Update Contact | **Edit** on Contact |
| Get Lead | **Read** on Lead |
| Create Lead | **Create** on Lead |
| Update Lead | **Edit** on Lead |
| Get Account | **Read** on Account |
| Update Account | **Edit** on Account |
| List Account Opportunities, Get Opportunity | **Read** on Opportunity |
| Get Case | **Read** on Case |
| Get User | **Read** on User |
| Query Records | **Read** on every object the query selects from or filters on |
| Create Task | **Create** on Task |
| Create Note | **Edit** on the record the Note attaches to (Notes take their access from the parent record) |
| Get Custom Object | **Read** on the custom object you configure the tool for |
| Update Custom Object | **Edit** on the custom object you configure the tool for |
Field-level security applies on top: reads return only fields the user can see, and creates and updates need **Edit** on each field they set. A missing permission doesn't flag the connection; that one tool fails.
## FAQ
No. Tools work as soon as the connection is made, and each tool is bound to the specific connection you pick when configuring it. Contact sync, analysis mapping, and automatic activity logging are separate features you opt into.
Yes. The custom object tools work with any custom object you configure them for, as long as the [Run As user](/integrations/salesforce#step-4-set-the-run-as-user) has permissions on it, and **Query Records** can query any object by its API name. [Contact sync](/integrations/salesforce-contact-sync) is separate and reads only `Contact`.
The tool has one input, `soql`. It defaults to a fixed value you write when configuring the tool, and it can include [dynamic variables](/build/dynamic-variables) that Retell resolves when the tool runs:
```sql theme={"dark"}
SELECT Id, CaseNumber, Subject, Status
FROM Case
WHERE ContactId = '{{contact_id}}' AND IsClosed = false
ORDER BY CreatedDate DESC
LIMIT 5
```
Here `{{contact_id}}` comes from an earlier **Search Contact** call. If a variable in the query has no value when the tool runs, Retell skips the call rather than sending the placeholder to Salesforce, and the agent sees `Skipped: soql is required`, so make sure the lookup that sets the variable runs first.
You can also switch the input to **Description** mode and describe what to fetch, and the agent composes the query at call time. A fixed query is the safer default: you can test it before the first call, while a query the agent composes can name a field that doesn't exist or get the syntax wrong mid-conversation.
Salesforce's query result as is: `totalSize`, `done`, and a `records` array in which each record carries the fields you selected plus an `attributes` object with the object type and record URL. Only the first batch comes back: Salesforce returns at most 2,000 rows per query call, and Retell doesn't fetch further pages, so `done` is `false` when more rows exist.
Add a `LIMIT` clause and name the fields you need. A voice agent rarely needs more than a handful of rows, and the whole result counts toward the 30,000-character cap on [integration tool responses](/agent/agent-workflow#pass-a-response-from-one-function-to-the-next). `SELECT FIELDS(ALL)` works, but Salesforce requires it to be bounded with `LIMIT 200` or less, and it pulls every field into the agent's context.
The **Output** tab starts empty because the fields depend on your query. [Run a test](/integrations/overview#run-a-test-to-get-a-tools-output-fields) to see the real result and pick the fields to save as dynamic variables.
The query runs as the Run As user, so it returns only objects and fields that user can read. Salesforce's error comes back to the agent as the tool result, for example `MALFORMED_QUERY` for a syntax error, or `INVALID_FIELD` for a field that doesn't exist or that the Run As user can't see. Prompt the agent on what to say when a lookup doesn't come back.
Search Contact. A SOQL filter like `WHERE Phone = '{{user_number}}'` finds a Contact only when the stored value matches the caller's number character for character, while **Search Contact** uses Salesforce's search index and matches across phone formats. Use **Query Records** for what the dedicated tools don't cover: the caller's open Cases, Opportunities in a given stage, or a custom object filtered by something other than its record Id.
The Contact fields your org allows on create, read live from Salesforce, with the fields Salesforce itself requires (such as `LastName`) marked required. Fields default to **Description** mode, so the agent fills in what the caller gave. The response is the new record's `id` and a `success` flag; map `id` to a [dynamic variable](/build/dynamic-variables) and pass it to **Create Task**, **Create Note**, or **Update Contact** later in the conversation.
**Create Contact** creates the record the moment the agent calls it, mid-conversation, with the fields the agent collected. **Create new contacts in CRM** is part of [contact sync](/integrations/salesforce-contact-sync) and runs after the conversation, for a matched Retell contact that isn't linked to a Salesforce Contact yet. Both create the Contact as the Run As user.
Search Contact uses Salesforce's search index, which lags writes by a few seconds. Save the `id` that **Create Contact** returns as a dynamic variable and pass it to the tools that need it, instead of searching for the record again.
Between 3 and 8 seconds, depending on the tool (as of September 2026): 3 for the get tools, 6 for **Search Contact**, **List Account Opportunities**, and **Query Records**, and 8 for the tools that create or update a record. On a timeout or an error the agent carries on talking, so prompt it on what to say when a lookup doesn't come back.
## Next steps
Add Salesforce tools to a single- or multi-prompt agent and test them with live requests.
Call Salesforce tools from a function node and branch on the result.
Import your Contacts, write analysis results back, and log conversations as Tasks.
Map tool responses to variables your agent can use later in the conversation.
# Connect Zendesk
Source: https://docs.retellai.com/integrations/zendesk
Connect Zendesk to Retell AI with OAuth so agents can identify callers, look up their tickets, file new ones, and add comments or private notes.
Connecting Zendesk takes your Zendesk URL and an OAuth sign-in. One connection gives you [agent functions](/integrations/zendesk-functions): your agents identify callers, work their tickets, and record call outcomes as comments or notes.
This page covers connecting and what the signed-in account needs.
## When to use it
Connect Zendesk when your support runs on it and you want an agent handling the front of the queue. It's the right choice when you want to:
* **Identify callers without asking for a ticket number.** The agent searches Zendesk for the caller's number, finds their profile, and pulls up their active tickets before the caller finishes explaining.
* **File tickets from calls automatically.** When a caller reports a new issue, the agent creates the ticket with a subject and description summarized from the conversation.
* **Keep the ticket as the source of truth.** The agent adds the call's outcome to the ticket as a public comment the requester sees, or a private internal note for your team.
For example, a software company's support line answers with a Retell agent that searches the caller's number, reads back the status of their most recent ticket, and adds a private note summarizing the call — a human only picks up when the issue is new or escalated.
## Prerequisites
* A Zendesk account with an **agent or admin role**. Retell acts as whoever signs in, and end-user accounts can't read other people's tickets or work the queue.
* Your Zendesk URL, `https://your-subdomain.zendesk.com`.
* Use a dedicated integration account rather than a person's account, so tickets and comments are attributed to the integration and the connection doesn't break when someone leaves.
## Connect Zendesk in Retell
In the Retell Dashboard, open **Integrations**, select the **Available** tab, find **Zendesk**, and click **Connect** (**Add Account** if a connection already exists).
Fill in the fields:
| Field | Value |
| ---------------------- | ------------------------------------------------------------ |
| **Connection name** | Alias for this connection; prefilled with `Zendesk - OAuth`. |
| **Zendesk tenant URL** | Your Zendesk URL, for example `https://acme.zendesk.com`. |
Click **Connect** (**Add Account** if a connection already exists). A Zendesk window opens; sign in with the integration account and approve the request. Retell asks for Zendesk's `read` and `write` scopes, and the token can do only what those scopes and the account's role allow. Retell stores the resulting token, so nobody has to sign in again.
Admins can review or revoke this authorization anytime in Zendesk under **Admin Center > Apps and integrations > APIs > External OAuth clients**.
Retell tests the connection by fetching the signed-in user's own profile. On success the connection appears on the **Connected** tab, and Zendesk tools appear in your agents' function menus — see [Zendesk agent functions](/integrations/zendesk-functions) for what they can do.
## Troubleshooting
Check the tenant URL: it must be your own Zendesk origin, `https://your-subdomain.zendesk.com`, with nothing after the domain. A host-mapped vanity domain or a URL with a path won't work.
Retell flags a connection as errored when Zendesk rejects the stored token, usually because the account was suspended or the authorization was revoked under **Admin Center > Apps and integrations > APIs > External OAuth clients**. A role downgrade doesn't error the connection; it shrinks what the tools can do (see [required role](/integrations/zendesk-functions#required-role)). Open the connection's settings and click **Reconnect** to sign in again.
## Next steps
Identify callers by phone, read and update their tickets, and record the call as a comment or note.
See every provider Retell connects to and how integration tools work.
Let the same support agent answer FAQs from your help center content.
Hand off to a human when the issue needs one.
# Zendesk agent functions
Source: https://docs.retellai.com/integrations/zendesk-functions
Zendesk tools for Retell AI agents: identify callers by phone, read and update their tickets, create new ones, and add public comments or private notes.
A [connected Zendesk account](/integrations/zendesk) gives your agents live access to your helpdesk, during a conversation or [before and after it](/agent/agent-workflow): identify the caller by phone number, read their open tickets, create or update tickets, and record what was discussed as a public comment or private note.
## Available tools
These tools appear in your agent's function menu once the account is connected. See [use integration tools in an agent](/integrations/overview#use-integration-tools-in-an-agent) for how to add and configure them.
| Tool | What it does |
| ------------------------------- | ---------------------------------------------------------------------------------------------- |
| **Search User** | Find a Zendesk user by phone number — typically the caller's number, to identify who's calling |
| **Get User** | Fetch a user's profile by ID |
| **Get Ticket** | Fetch a ticket by ID, for example one the caller is asking about |
| **List User Requested Tickets** | List a user's active tickets, most recently updated first (up to 50) |
| **Create Ticket** | Open a new ticket with a subject and description from the conversation |
| **Update Ticket** | Change fields on a ticket, such as status or priority |
| **Add Ticket Comment** | Add a public comment the requester sees, or a private internal note |
A typical support agent uses **Search User** at the start of the call to identify the caller by `{{user_number}}`, **List User Requested Tickets** to read their open tickets, and **Create Ticket** or **Add Ticket Comment** to record the outcome.
Pass the caller's number as-is, for example `{{user_number}}`. **Search User** strips the country code and matches the remaining digits against any format Zendesk stores, so 10-digit and E.164 profiles both match.
## Required role
Every tool call runs as the account that signed in when connecting, under Zendesk's `read` and `write` OAuth scopes; there are no per-tool scopes to grant. The account's role sets what each tool can do:
* **Agent or admin** — every tool works.
* **Light agent** — the read tools work, and so does **Create Ticket**, including on behalf of an end user. **Add Ticket Comment** can add private notes but not public replies, and **Update Ticket** can change status or fields only on tickets the account itself requested. Because every light-agent comment is private, a ticket a light agent opens stays invisible to the requester until a full agent adds a public comment.
* **End user** — can't search users or work tickets through the API; reconnect with an agent or admin account.
If the role changes after connecting, the token's permissions change with it. The connection isn't flagged; the affected tools just start failing.
## Troubleshooting
The search strips the country code and matches the remaining digits against any stored format, so a format mismatch usually isn't the cause. Check that the number is on the Zendesk user profile's phone field at all, and that the search input is a real phone number rather than an extension or free text.
## FAQ
The **Add Ticket Comment** tool takes a `public` flag. When true, the requester sees the comment and gets Zendesk's usual notifications; when false, it's an internal note visible only to your team. Configure the flag as a fixed value on the tool, or let the agent decide from context.
By default, the account that signed in when connecting (see [Prerequisites](/integrations/zendesk#prerequisites) on using a dedicated integration account). The **Create Ticket** tool also takes an optional requester ID, so you can file the ticket as coming from the caller by passing the user ID a **Search User** call returned.
No. Zendesk tools read and write your helpdesk live. Nothing is copied into Retell, and there's no background sync to configure.
Yes. **Update Ticket** can set the status, including solved. Have the agent confirm with the caller before closing anything.
## Next steps
Add Zendesk tools to a single- or multi-prompt agent and test them with live requests.
Call Zendesk tools from a function node and branch on the result.
Set up the OAuth connection and what the signed-in account needs.
Pass the caller's number into tools and reuse tool results later in the call.
# Connect Zoho CRM
Source: https://docs.retellai.com/integrations/zoho
Connect Zoho CRM to Retell AI with one OAuth sign-in: Retell detects your Zoho data center, then syncs contacts and runs agent tools on your records.
Connecting Zoho CRM takes one sign-in and nothing else: authorize Retell in Zoho, and it works out your data center and API domain on its own. One connection covers both [contact sync](/integrations/zoho-contact-sync) and [agent functions](/integrations/zoho-functions), so your agents work your Contacts, Deals, Tasks, Notes, and Calls live while Retell keeps the records current.
## When to use it
Connect Zoho CRM when it's your system of record and you want your agents working from it without anyone copying data between tools. It's the right choice when you want to:
* **Call or text people who already exist in Zoho.** Contacts sync into Retell automatically, so your agent greets callers by name and knows their details instead of asking.
* **Keep Zoho current without manual data entry.** Analysis results from each conversation write back to fields on the Contact record.
* **Give your team call history where they already work.** Each call lands on the Contact's timeline as a Call record; each chat lands as a completed Task.
Your agents can also look up Contacts and their Deals, and create Tasks, Notes, and Calls, through [integration tools](/integrations/zoho-functions#available-tools).
For example, an insurance brokerage syncs its Zoho Contacts into Retell. Its renewal agent greets each policyholder by name, reads the Deals open on their record, books the callback they ask for as a Zoho Task, and the call lands on the contact's timeline as a Call record.
## Prerequisites
* A Zoho CRM account on a [data center Retell supports](#which-zoho-data-center-does-retell-use).
* A Zoho user whose profile holds **View**, **Create**, and **Edit** on **Contacts**, **Deals**, **Tasks**, **Notes**, and **Calls**, and can read field definitions in Setup.
* Retell acts as whoever authorizes the connection, so every record it creates is owned by that user. Use a dedicated integration user rather than a person's account, so the connection doesn't break when someone changes roles or leaves.
You don't need to register a client in the Zoho API console. Retell authorizes with its own Zoho client, so there's no client ID or secret to create, copy, or rotate.
## Connect Zoho CRM in Retell
In the Retell Dashboard, open **Integrations**, select the **Available** tab, find **Zoho CRM**, and click **Connect** (**Add Account** if a connection already exists). The dialog asks for one thing, a **Connection name**, prefilled with `Zoho CRM - OAuth`. There's no URL or region to enter, so the form is ready to submit as soon as it opens.
Click **Add Account**. A Zoho sign-in window opens: sign in with the integration user, pick the Zoho org if you belong to more than one, and accept the [access Retell asks for](#what-access-does-retell-ask-for). Retell stores a refresh token, so nobody has to sign in again.
Zoho's consent screen is where the data center is decided. Sign in as a user of the org whose records you want your agents working on: Zoho routes the sign-in to that user's home data center, and Retell binds the connection to it.
Retell tests the connection by reading the Contacts module's field definitions as the authorizing user. On success the connection appears on the **Connected** tab and the dialog offers **Set up contact sync** — see [Zoho contact sync](/integrations/zoho-contact-sync) to import your Contacts, or start using [agent functions](/integrations/zoho-functions) right away.
## What access does Retell ask for?
Zoho's consent screen lists three scopes:
| Scope | What it covers |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ZohoCRM.modules.ALL` | Read and write records across your CRM modules. It's a group scope, so Zoho grants it broadly; Retell uses it for Contacts, Deals, Tasks, Notes, and Calls |
| `ZohoCRM.settings.fields.READ` | Read each module's field definitions, which populate the field pickers when you configure a tool or a sync mapping |
| `ZohoSearch.securesearch.READ` | Zoho's search service, which the Search Records API requires on top of the module scope. It's what backs finding a Contact by phone number |
Granting a scope isn't the same as having the permission. The authorizing user's Zoho profile still decides which modules and fields they can touch, through its **View**, **Create**, **Edit**, and **Delete** permissions, and a profile restriction is narrower than the scope. A restriction doesn't flag the connection either, because Zoho answers with a permission error rather than rejecting the token, so that one tool or field quietly stops working.
## Which Zoho data center does Retell use?
The one your Zoho account is homed in. Zoho keeps each account in a single data center and serves its API from a matching domain, and Retell picks that up during the sign-in rather than asking you: consent starts at `accounts.zoho.com`, Zoho routes the user to their own data center, and the callback names it. Retell then stores that data center's API domain on the connection and uses it for every later API call and token refresh.
| Data center | Accounts server | API domain |
| ------------- | ----------------------- | --------------------- |
| United States | `accounts.zoho.com` | `www.zohoapis.com` |
| Europe | `accounts.zoho.eu` | `www.zohoapis.eu` |
| India | `accounts.zoho.in` | `www.zohoapis.in` |
| Australia | `accounts.zoho.com.au` | `www.zohoapis.com.au` |
| Japan | `accounts.zoho.jp` | `www.zohoapis.jp` |
| Saudi Arabia | `accounts.zoho.sa` | `www.zohoapis.sa` |
| Canada | `accounts.zohocloud.ca` | `www.zohoapis.ca` |
| China | `accounts.zoho.com.cn` | `www.zohoapis.com.cn` |
Retell calls Zoho CRM's v8 REST API on that domain (as of August 2026).
## Troubleshooting
Zoho reports a declined or cancelled consent back as an error, and Retell leaves the connection unmade. Start again from the **Available** tab and click **Accept** on the consent screen. Retell asks Zoho for a fresh consent on every connect, so an approval you gave earlier never silently skips the screen.
The test reads the Contacts module's field definitions. It fails when the authorizing user's profile can't see Contacts or its field setup, or when the user belongs to a different Zoho org than the records you expect. Confirm the user can open Contacts in Zoho CRM, then reconnect as a user with the right profile.
Retell flags a connection as errored when Zoho rejects the stored refresh token. That usually means the token was revoked under **Connected Apps**, or the authorizing user was deactivated or removed from the org. Open the connection's settings and click **Reconnect** to authorize again with a working user.
That's a Zoho profile permission, not the connection. Zoho answers a blocked module or field with a permission error, which leaves the connection reading as healthy. Check the authorizing user's profile permissions for the module the tool writes to, or for the specific field that isn't syncing.
## FAQ
The user who authorized the connection. That's why a dedicated integration user is worth setting up: the record owner and the contact's timeline then read "Retell integration" rather than a teammate's name.
Yes, add one connection per org, authorizing each as a user of that org. Only one CRM connection in your workspace can drive [contact sync](/integrations/zoho-contact-sync) at a time, across every provider — the **Contact sync** toggle in a connection's settings decides which one. Integration tools work on every connection.
No. Retell authorizes with its own registered Zoho client, so the whole setup is the consent screen. Nothing to create in the Zoho API console, and no client secret to rotate on your side.
Not today (as of August 2026). Contact sync and the agent tools work on the standard Contacts, Deals, Tasks, Notes, and Calls modules. Custom *fields* on those modules are fully supported — they show up in the field pickers alongside the standard ones.
## Next steps
Import your Zoho Contacts, write analysis results back, and log calls on the contact's timeline.
Look up callers, read their Deals, and create Tasks, Notes, and Calls mid-conversation.
See every provider Retell connects to and how integration tools work.
How contact sync, analysis mapping, and activity logging work across CRM providers.
# Zoho contact sync
Source: https://docs.retellai.com/integrations/zoho-contact-sync
Sync Zoho CRM Contacts with Retell AI: field mappings, Post Call Extraction write-back, and every call logged as a Zoho Call record on the timeline.
Contact sync imports your Zoho Contacts into Retell, writes [Post Call Extraction](/features/post-call-analysis-overview) results back to their fields, and logs each call as a **Call** record on the contact's timeline. This page covers the Zoho-specific behavior; [CRM integrations](/integrations/crm-overview) explains the four data flows all CRM providers share.
Contact sync requires a [connected Zoho org](/integrations/zoho). Integration tools work without it — see [Zoho agent functions](/integrations/zoho-functions).
## Required permissions
Sync runs as the [authorizing Zoho user](/integrations/zoho#prerequisites), whose profile needs:
* **View** on Contacts, including every field you import.
* **Edit** on Contacts for outbound sync, and **Create** if you turn on creating new contacts in Zoho.
* **Create** on Calls and Tasks for activity logging.
* Access to each mapped field. Zoho reports field permissions per user, so a field the authorizing user can't read or edit is skipped rather than synced.
A missing permission doesn't flag the connection; that field or feature silently stops syncing.
## Set up contact sync
After the connection test passes, click **Set up contact sync** to open the field mapping dialog, then map the Zoho fields you want to import and the Retell fields you want to write back. For a connection made earlier, the same dialog opens from the **Connected** tab: open the connection's settings and click **Set up contact sync**.
Retell pre-fills one mapping in each direction: Zoho `Phone` to Retell `phone_number`. Phone number is how contacts are matched between the two systems, so it stays mapped and can't be removed. If the numbers you call live in a different field, such as `Mobile`, change the external field on both tabs. See [CRM data mappings](/integrations/crm-mappings) for how to map the rest, create custom fields, and choose update modes.
To log conversations to Zoho, turn on **Log activities automatically** on the **Sync to Zoho CRM** tab.
### Which Zoho fields can you map?
The field pickers read your Zoho modules live, so custom fields appear beside the standard ones. Retell maps the Zoho field types it can round-trip as plain values:
| Zoho field type | Maps to |
| ------------------------------------------------------------- | ------------------------------------------------- |
| `text`, `textarea`, `email`, `phone`, `website`, `autonumber` | string |
| `integer`, `bigint`, `double`, `currency`, `percent` | number |
| `boolean` | boolean |
| `date` | date |
| `datetime` | datetime |
| `picklist` | enum, offering the options Zoho reports as in use |
Every other type is left out because it doesn't round-trip as a single flat value: `lookup`, `ownerlookup`, `multiselectpicklist`, `formula`, `subform`, and `fileupload`.
Inbound sync reads at most **49 mapped fields** (as of August 2026), a cap Zoho's API sets on how many fields one read can request. Mapping a 50th on the **Import contacts** tab fails the sync rather than dropping a field quietly.
## Verify it worked
* Open **Contacts**. After the first sync, Zoho Contacts appear with correctly formatted phone numbers and your mapped fields populated.
* The first sync is a full scan of every Contact in the module, read 200 at a time, so a large org takes a while. After that, Retell polls every 5 minutes (as of August 2026) and asks Zoho only for contacts modified since the last run.
Contacts with no value in the mapped phone field are excluded from the sync entirely, as are contacts whose number can't be parsed into a valid E.164 number.
## How are conversations logged in Zoho CRM?
A call becomes a **Call** record linked to the Contact, carrying the call type (`Inbound` or `Outbound`), the start time, the call duration, and a description holding the call ID, the from and to numbers, the disconnection reason, and the summary.
A chat becomes a **Task** with status **Completed**, whose description holds the chat ID, the from and to numbers, and the summary. Zoho has no chat activity type, so a Task is the closest equivalent.
Both attach to the Contact that Retell matched by phone number, through the record's `Who_Id` field.
## Troubleshooting
Activity logging needs **Log activities automatically** enabled on the **Sync to Zoho CRM** tab, a Retell contact that was imported from this connection, and a profile that can create Calls and Tasks. A call from a number that doesn't match a synced contact is never logged.
You're likely past the 49-field cap on the **Import contacts** tab. Retell fails the run rather than dropping a field, so remove a mapping you don't need and the next poll succeeds.
Check the field on the Zoho side first. A field the authorizing user has no access to reads as absent rather than as an error, and a field type Retell doesn't map (a lookup, a multi-select picklist, a formula) never appears in the picker to begin with.
## FAQ
By default it only updates Contacts that already exist in Zoho, and it never deletes them. Turn on **Create new contacts in CRM** on the **Sync to Zoho CRM** tab to have Retell create a Contact after a conversation when the matched contact isn't linked to one yet. Your agent can also create Contacts through the **Create Contact** [tool](/integrations/zoho-functions), but that's an explicit tool call, not sync.
Contacts for the records themselves, plus Calls and Tasks when activity logging is on. Leads aren't synced. If your callers live as Leads in Zoho, convert them or keep the numbers you dial on Contacts.
## Next steps
Map Zoho fields to Retell contacts, choose update modes, and control what syncs back.
Accumulate what your agents learn across conversations into the contact record.
Reference synced contact fields from your agent's prompt.
Look up callers, read their Deals, and create Tasks, Notes, and Calls mid-conversation.
# Zoho agent functions
Source: https://docs.retellai.com/integrations/zoho-functions
Zoho CRM tools for Retell AI agents: identify the caller, read their Deals, update Contacts, and create Tasks, Notes, and Call records on the timeline.
A [connected Zoho org](/integrations/zoho) gives your agents live tools for that org: look up the caller, read the Deals on their record, and create Tasks, Notes, and Calls. Tools run during a conversation or [before and after it](/agent/agent-workflow), and no [contact sync](/integrations/zoho-contact-sync) is required.
## Available tools
These tools appear in your agent's function menu once the org is connected. Every tool call runs as the [authorizing Zoho user](/integrations/zoho#prerequisites). See [use integration tools in an agent](/integrations/overview#use-integration-tools-in-an-agent) for how to add and configure them.
Records the tools create are owned by the user who authorized the connection, so a dedicated integration user keeps the record owner and the timeline reading "Retell integration" rather than a teammate's name.
| Tool | What it does |
| ---------------------- | ----------------------------------------------------------------------------------------- |
| **Search Contact** | Find a Contact by phone number — typically the caller's number, to identify who's calling |
| **Get Contact** | Fetch a Contact's fields by record ID |
| **Create Contact** | Create a Contact for a caller who isn't in the CRM yet |
| **Update Contact** | Update fields the caller confirms or corrects |
| **List Contact Deals** | List the Deals associated with a Contact (up to 100) |
| **Create Task** | Create a Task on a Contact when a follow-up is agreed |
| **Create Note** | Attach a Note to a Contact with a summary of what was discussed |
| **Log Call Activity** | Log the call as a Call record on the Contact's timeline |
**Log Call Activity** is the per-call, agent-driven version of activity logging: the agent decides when to log and what summary to write. **Log activities automatically** in the [sync settings](/integrations/zoho-contact-sync) logs every conversation with a synced contact without the agent doing anything. Use one or the other, or both if you want automatic logs plus richer agent-written ones.
## How the tools address a record
Zoho record IDs are long numeric strings, and Retell rejects anything else before it reaches the API.
* **Get Contact** and **Update Contact** take an `id`.
* **List Contact Deals**, **Create Task**, **Create Note**, and **Log Call Activity** take a `contact_id`, which is required: each one links its record to that Contact, through the `Who_Id` field on Tasks and Calls.
* **Search Contact** takes a `phone_number` and is how you get an ID in the first place. Point the rest at it with a [dynamic variable](/build/dynamic-variables) from its output.
**Search Contact** hands the number to Zoho's search, which matches it against every phone field on the module, and it retries with the number as the caller gave it plus its national and E.164 digit forms, so a number stored as `(415) 555-0142` still matches a caller dialing in as `+14155550142`. It returns a single Contact, preferring one whose Phone, Mobile, Home Phone, or Other Phone digits equal the number you searched.
**List Contact Deals** returns the Deals with a `count` and a `has_more` flag, so your agent can tell "no open deals" apart from "more than fit in one response."
Create and update tools return the record's `id` and a `success` flag. Map the `id` to a dynamic variable when a later tool in the same conversation needs to point at the record you just made.
## What fields can the tools write?
Retell reads the module's field definitions from your Zoho org when you configure a tool, so the input list is your schema, custom fields included. Read-only fields and fields Zoho won't accept on that operation are filtered out, and fields marked mandatory in Zoho come through as required inputs you have to fill.
Field types map the same way they do for [contact sync](/integrations/zoho-contact-sync#which-zoho-fields-can-you-map): text, number, checkbox, date, date-time, and picklist fields are all configurable, while lookups, multi-select picklists, formula fields, subforms, and file uploads aren't offered.
**Log Call Activity** is the one exception to Zoho's required fields. Zoho makes call start time and duration mandatory on a Call record, but Retell fills both in for you, so they stay optional inputs you configure only if you want to override them.
## Required permissions
Consent grants Retell the CRM scopes at connect time, but the authorizing user's Zoho profile decides what each tool can actually do:
| Tool | Zoho profile permission |
| --------------------------- | -------------------------------------------------------------------------------- |
| Search Contact, Get Contact | **View** on Contacts |
| Create Contact | **Create** on Contacts |
| Update Contact | **Edit** on Contacts |
| List Contact Deals | **View** on Contacts and Deals |
| Create Task | **Create** on Tasks |
| Create Note | **Create** on Notes, which Retell posts through the Contact's Notes related list |
| Log Call Activity | **Create** on Calls |
Every tool also needs the profile to read the module's field definitions, which is what populates the input list when you configure it. A missing permission doesn't flag the connection, because Zoho answers with a permission error rather than rejecting the token; that one tool fails.
## FAQ
No. Tools work as soon as the connection is made, and each tool is bound to the specific connection you pick when configuring it. Contact sync, analysis mapping, and automatic activity logging are separate features you opt into.
Between 6 and 10 seconds, depending on the tool (as of August 2026): 6 for **Get Contact**, 8 for the tools that write a record, and 10 for **Search Contact** and **List Contact Deals**, which do more work on Zoho's side. On a timeout or an error the agent carries on talking, so prompt it on what to say when a lookup doesn't come back.
Not today (as of August 2026). The tools cover Contacts, Deals, Tasks, Notes, and Calls. Custom fields on those modules are supported and show up in the input list; custom modules and Leads aren't.
## Next steps
Add Zoho tools to a single- or multi-prompt agent and test them with live requests.
Call Zoho tools from a function node and branch on the result.
Import your Zoho Contacts, write analysis results back, and log calls on the contact's timeline.
Map tool responses to variables your agent can use later in the conversation.
# Audio Basics
Source: https://docs.retellai.com/knowledge/audio-basics
Audio fundamentals for building voice AI — sampling, quantization, codecs, sample rates, and how audio is represented digitally in telephony systems.
### How is Audio Represented Digitally
Sound waves are captured by a microphone, which converts the acoustic energy
into electrical analog signals. The analog signals are then fed into an ADC
(Analog-to-Digital Conversion). Here, two critical processes occur - sampling
and quantization.
Sampling is the process of measuring the amplitude of an analog signal at
regular intervals. These intervals are determined by the sample rate,
expressed in Hertz (Hz). For example, a sample rate of 44.1 kHz means the
signal is sampled 44,100 times per second.
By sampling the audio signal, we create a series of discrete data points
that approximate the continuous analog waveform.
The Nyquist Theorem states that the sample rate must be at least twice the
highest frequency component in the audio signal to accurately reconstruct
the original signal. For example, human hearing typically ranges up to 20
kHz, hence the standard CD sample rate of 44.1 kHz.
Quantization is the process of converting each sampled amplitude value
into a digital value. This involves assigning a specific numerical value
(quantization level) to each sample, based on its amplitude.
The range of possible amplitude values is divided into discrete steps.
Each step is assigned a digital value. The bit depth determines the number
of possible quantization levels. For instance, a 16-bit system can
represent 65,536 (2^16) different levels.
Quantization introduces a small amount of error, known as quantization
noise, because the process involves rounding the true amplitude value to
the nearest quantization level. Higher bit depths can reduce this error,
leading to higher fidelity audio.
### Terminology
The sample rate is the number of samples of audio carried per second. It's
measured in Hertz (Hz).
This refers to the number of separate audio channels (e.g., mono, stereo,
surround sound) in the recording.
Mono means single channel. All audio is combined into one channel.
Bit depth refers to the number of bits used to represent each audio sample.
### Audio Encoding
Audio encoding refers to the process of converting audio data into a format that
can be easily stored, transmitted, and decoded by audio playback devices. This
process often involves compression to reduce file size while trying to maintain
the quality of the original audio. There are several popular audio encoding
formats, each with its own specific use cases and characteristics.
Here are some examples:
* `Description`: PCM (Pulse Code Modulation) is the most straightforward form of digital audio
encoding. It represents the amplitude of the audio signal at uniformly spaced
intervals.
* `Usage`: It's the standard form of digital audio in computers, CDs, digital
telephony, and other digital audio applications.
* `Description`: MP3 (MPEG Audio Layer III) is a lossy compression format that significantly reduces
file size by removing audio data considered less important to human hearing.
* `Usage`: It was widely used for music distribution and playback due to its
ability to reduce file size while maintaining a decent level of audio quality.
* `Description`: AAC (Advanced Audio Coding) is a more advanced form of lossy compression than MP3,
offering better audio quality at similar bitrates.
* `Usage`: It’s commonly used in online streaming services, Apple's iTunes, and
YouTube.
* `Description`: Opus is a versatile, open standard audio codec. It provides
low latency and high-quality audio.
* `Usage`: It’s widely used for real-time applications like video conferencing,
VoIP, and streaming.
* `Description`: μ-law (mu-law or ulaw) encoding is a non-linear audio encoding technique used
in telephony. It compresses dynamic range, emphasizing quieter sounds for
improved clarity.
* `Usage`: Predominantly used in North American and Japanese telephone systems,
it's integral to the G.711 telephony standard, enhancing voice transmission
quality.
Note that audio encoding is not the same as audio format. An audio format refers
to the entire structure of the audio file, which includes the encoding, but also
encompasses other elements like metadata, file headers, and containers. For
example, a WAV file typically uses PCM encoding, and has its own header that
specifies audio sample rate, number of samples, etc.
### PCM Audio Representation
When audio is played, it is typically decoded into PCM (Pulse Code Modulation).
This process is true for most digital audio systems, regardless of the original
audio format or encoding method.
There are generally two types of PCM audio representation:
* `Float 32 Array`: It uses a 32-bit floating-point format to represent each
sample. When capturing mic stream and setting up playback in web environment,
PCM will be represented in this format.
* `Unsigned 8 Array`: It uses an array of 8-bit unsigned integers (aka bytes),
and each sample can be multiple bytes. For example, for a mono PCM audio with
bit depth of 16 bit, each sample will be two bytes. This is a lower-level
representation and is often used in programming for audio processing.
Here's the code snippet to convert between these two formats:
```javascript theme={"dark"}
export function convertUnsigned8ToFloat32(array: Uint8Array): Float32Array {
const targetArray = new Float32Array(array.byteLength / 2);
// A DataView is used to read our 16-bit little-endian samples
// out of the Uint8Array buffer
const sourceDataView = new DataView(array.buffer);
// Loop through, get values, and divide by 32,768
for (let i = 0; i < targetArray.length; i++) {
targetArray[i] = sourceDataView.getInt16(i * 2, true) / Math.pow(2, 16 - 1);
}
return targetArray;
}
export function convertFloat32ToUnsigned8(array: Float32Array): Uint8Array {
const buffer = new ArrayBuffer(array.length * 2);
const view = new DataView(buffer);
for (let i = 0; i < array.length; i++) {
const value = array[i] * 32768;
view.setInt16(i * 2, value, true); // true for little-endian
}
return new Uint8Array(buffer);
}
```
### Audio Spec Retell AI Uses
* `Phone Calls`: Different telephony providers have different audio codecs.
Our telephony integrations handle that for you internally, and you don't need to worry about
encoding and decoding.
* `Web Calls`: The [frontend web JS SDK](https://www.npmjs.com/package/retell-client-js-sdk)
abstracts away audio complexity for you. The user audio
is captured in PCM format and sent to the backend for processing.
# Debug call transfer failure
Source: https://docs.retellai.com/reliability/call-performance
Debug Retell call transfer failures — verify the `transfer_call` function is configured correctly for single, multi-prompt, and conversation flow agents.
When experiencing call transfer issues, follow these troubleshooting steps to identify and resolve common problems.
## Agent-Specific Troubleshooting
### For Single/Multi-Prompt Agents
If call transfer is not triggered in your single or multi-prompt agent:
1. Check your agent configuration
2. Confirm that you've added the transfer\_call function to your agent's function list
3. Visit [Function Calling Guide](/build/single-multi-prompt/function-calling) for more details on implementing the function
1. Update your agent's prompt to clearly define transfer conditions
2. Ensure the transfer\_call function description is specific and unambiguous
3. Test with sample scenarios to validate transfer triggers
For more detailed guidance on specific features, visit [Call Transfer Setup](/build/single-multi-prompt/transfer-call).
### For Conversation Flow Agents
If call transfer is not triggered in your conversation flow agent:
1. Verify you have a transfer node in your conversation flow
2. Ensure the transfer node is properly connected to other nodes
3. Check that transition conditions are correctly set up
1. Confirm the transfer destination is correctly configured
2. Verify the transfer conditions in the node are clear and specific
3. Test the flow to ensure the transfer node is reachable
For more detailed guidance, visit [Conversation Flow Transfer Setup](/build/conversation-flow/call-transfer-node).
## General Troubleshooting
### Call Type Verification
Call transfer is only supported for phone calls, **not web calls**
### If Call Transfer is Triggered but Failed
* **Telephony issues**: Call transfer is similar to placing an outbound call, and failures occur for similar reasons as outbound call failures. The SIP connection log is available in the call logs and is useful for diagnosing the failure reason.
Please refer to [Understand Reasons for Outbound Call Failure](/reliability/debug-outbound-call) for more details.
* **Could not detect human**: If human detection is enabled, the call may fail if a human is not successfully detected. Possible reasons include:
* No human was actually present (e.g. it was an IVR or voicemail).
* The other party spoke, but only after the detection timeout expired.
* The speech was too similar to an IVR or voicemail and was not recognized as human.
# Check actual latency
Source: https://docs.retellai.com/reliability/check-actual-latency
Monitor per-call latency in the Retell Call History dashboard or via the Get Call API — review P50, P90, and P99 end-to-end latency to find slow calls.
You can monitor the latency of individual calls in [Call History](/features/session-history).
### Understanding latency metrics
End-to-end latency measures the total time from when the user stops speaking until the AI agent begins responding. This includes processing time, network delays, and model inference time.
### Key metrics explained
* **P90 (90th Percentile)**: 90% of calls have latency below this value.
* **Median (50th Percentile)**: Half of the calls have latency less than this value.
* **Min**: The fastest response time achieved in any call.
## Retrieve latency via the API
You can also retrieve detailed latency breakdowns programmatically using the [Get Call API](/api-references/get-call). After a call ends, the response includes a `latency` object with per-component metrics.
```bash cURL theme={"dark"}
curl -X GET "https://api.retellai.com/v2/get-call/CALL_ID" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```python Python theme={"dark"}
from retell import Retell
client = Retell(api_key="YOUR_API_KEY")
call = client.call.retrieve("CALL_ID")
print(call.latency)
```
```javascript Node.js theme={"dark"}
import Retell from "retell-ai";
const client = new Retell({ apiKey: "YOUR_API_KEY" });
const call = await client.call.retrieve("CALL_ID");
console.log(call.latency);
```
### Latency breakdown fields
The `latency` object contains the following components. Not all fields are present on every call — availability depends on the call type and features used.
| Field | Description |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `e2e` | End-to-end latency from when the user stops talking to when the agent starts talking. Only turns where the agent speaks are counted — turns where the agent just runs tool calls without saying anything are excluded, so tool execution time doesn't inflate this metric. Does not account for network trip time from the Retell server to the user's frontend. |
| `asr` | Transcription latency — the difference between the duration of audio chunks streamed and the duration of the transcribed portion. |
| `llm` | LLM latency from the start of the LLM call to the first speakable chunk received. When using a custom LLM, this includes the websocket roundtrip time. |
| `llm_websocket_network_rtt` | Websocket roundtrip latency between your server and the Retell server. Only populated for calls using a [custom LLM](/integrate-llm/overview). |
| `tts` | Text-to-speech latency from triggering TTS to the first audio byte received. |
| `knowledge_base` | Knowledge base retrieval latency from triggering retrieval to receiving all relevant context. Only populated when the agent uses the knowledge base feature. |
| `s2s` | Speech-to-speech latency from requesting a response to the first byte received. Only populated for calls using a speech-to-speech model (e.g., Realtime API). |
Each component is an object with these statistical fields:
| Field | Type | Description |
| -------- | --------- | -------------------------------------------------- |
| `p50` | number | 50th percentile (median) latency in milliseconds |
| `p90` | number | 90th percentile latency in milliseconds |
| `p95` | number | 95th percentile latency in milliseconds |
| `p99` | number | 99th percentile latency in milliseconds |
| `min` | number | Minimum latency in milliseconds |
| `max` | number | Maximum latency in milliseconds |
| `num` | number | Number of data points tracked |
| `values` | number\[] | All individual latency data points in milliseconds |
### Example response
Here is an example of the `latency` portion of a Get Call response:
```json theme={"dark"}
{
"latency": {
"e2e": {
"p50": 800,
"p90": 1200,
"p95": 1500,
"p99": 2500,
"min": 500,
"max": 2700,
"num": 10,
"values": [500, 620, 780, 800, 850, 900, 1100, 1200, 1500, 2700]
},
"llm": {
"p50": 400,
"p90": 650,
"p95": 800,
"p99": 1200,
"min": 250,
"max": 1300,
"num": 10,
"values": [250, 310, 380, 400, 420, 500, 600, 650, 800, 1300]
},
"tts": {
"p50": 150,
"p90": 250,
"p95": 300,
"p99": 400,
"min": 80,
"max": 420,
"num": 10,
"values": [80, 100, 130, 150, 160, 200, 230, 250, 300, 420]
}
}
}
```
# Check estimated latency
Source: https://docs.retellai.com/reliability/check-estimated-latency
View estimated end-to-end latency for a Retell agent before going live — see which turtle-icon features are pushing latency higher than expected.
The Retell platform achieves latency as low as 600ms, measured from when the user stops speaking to when the AI agent begins responding.
In the agent detail page, under the "Estimated Latency" section, you can view the average latency for the agent.
Please note that certain settings will increase latency. These settings are marked with a turtle icon. For example, any wait added by the [Response Wait time](/build/single-multi-prompt/configure-basic-settings) setting appears as its own row in the breakdown.
# Debug call disconnection
Source: https://docs.retellai.com/reliability/debug-call-disconnect
Diagnose why a Retell call disconnected — look up the `disconnection_reason` in the dashboard or Get Call API and follow the table of causes and fixes.
Open the call in [Call History](/features/session-history) or retrieve it with the [Get Call API](/api-references/get-call) to find its disconnection reason.
## Diagnosing disconnection reasons
Find the reason in the table below, then follow the fix. For deeper diagnosis of specific cases:
* **`not_connected` outbound calls** (`dial_failed`, `invalid_destination`, spam blocks, and other SIP failures) — see [debug outbound connection issues](/reliability/debug-outbound-call).
* **SIP-level signaling and media** — inspect the call's PCAP file, covered in [debug SIP calls using PCAP files](/reliability/debug-calls-pcap).
When a phone number makes many short calls in a short period, carriers might mark it as spam. The number then gets blocked and shows up as `dial_failed`.
| Disconnection reason | Call status | Description |
| -------------------------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `user_hangup` | ended | Expected behavior, user hung up the call. |
| `agent_hangup` | ended | Expected behavior, agent hung up the call. |
| `call_transfer` | ended | Expected behavior, agent transferred the call. |
| `transfer_bridged` | ended | Expected behavior, on the transfer agent's call of an [agentic warm transfer](/build/conversation-flow/call-transfer-node): the transfer agent decided to bridge, and the original caller was connected to the transfer target. |
| `transfer_cancelled` | ended | On the transfer agent's call of an [agentic warm transfer](/build/conversation-flow/call-transfer-node): the caller was NOT connected — the transfer agent cancelled the transfer, or it timed out without a decision (e.g. the transfer target did not pick up or went to voicemail). |
| `call_take_over` | ended | Expected behavior, a human took over the call from the agent, which permanently stops the agent for that call. |
| `voicemail_reached` | ended | Expected behavior, if the agent is configured with [voicemail settings](/build/handle-voicemail), and voicemail is reached. |
| `ivr_reached` | ended | Expected behavior, if the agent is configured to [hang up when encountering an IVR system](/build/handle-voicemail#ivr-hangup), and IVR is reached. |
| `inactivity` | ended | Expected behavior, call was terminated due to the "end\_call\_after\_silence\_ms" setting reached after long inactivity. |
| `max_duration_reached` | ended | Expected behavior, call was terminated due to [maximum duration](/deploy/concurrency#max-call-duration) reached. |
| `dial_busy` | not\_connected | Outbound call not connected, the number dialed is busy. |
| `dial_failed` | not\_connected | Outbound call not connected, dialing failed with no or unknown sip error code. |
| `dial_no_answer` | not\_connected | Outbound call not connected, the number dialed did not answer. |
| `invalid_destination` | not\_connected | Outbound call not connected, the number dialed is invalid. Can be due to spaces or invalid characters in the number. Or it can be your telephony provider requiring a specific format (like E.164 format). |
| `telephony_provider_permission_denied` | not\_connected | Outbound call not connected, the sip trunk credentials are not authenticated. |
| `telephony_provider_unavailable` | not\_connected | Outbound call not connected, the telephony provider is unavailable. |
| `sip_routing_error` | not\_connected | Outbound call not connected, the sip routing is going over too many hops or is in a loop. |
| `marked_as_spam` | not\_connected | Outbound call not connected, the number dialed is marked as spam. See [Spam Likely Overview](/build/telephony/call_efficiency_overview). |
| `user_declined` | not\_connected | Outbound call not connected, user declined the call. |
| `concurrency_limit_reached` | error | Error, [concurrency limit](/deploy/concurrency) reached, add a retry with exponential backoff. Or consider enterprise plan. |
| `no_concurrency_fallback` | ended | Inbound call could not get a concurrency slot and was transferred to the configured fallback number. |
| `no_valid_payment` | error | Error, no valid payment registered on file, or service shut down due to bill overdue. |
| `scam_detected` | error | Error, scam detected for that particular agent. |
| `error_llm_websocket_open` | error | Error, LLM websocket did not open between Retell server and your backend. Likely because the Custom LLM URL is incorrect or your LLM server is unreachable. See [custom LLM troubleshooting](/integrate-llm/troubleshooting#connection-failures). |
| `error_llm_websocket_lost_connection` | error | Error, LLM websocket connection broke during the call. Often a missing `ping_pong` echo or a host idle timeout. See [custom LLM troubleshooting](/integrate-llm/troubleshooting#the-call-drops-after-a-few-seconds). |
| `error_llm_websocket_runtime` | error | Error, LLM websocket received a closing signal other than `1000` from your server. |
| `error_llm_websocket_corrupt_payload` | error | Error, LLM websocket received unspecified payload, such as a binary frame instead of text. |
| `error_no_audio_received` | error | Error, has not received audio from Twilio or web frontend for a while after connection has been established. |
| `error_asr` | error | Error, Retell's ASR encountered a problem. |
| `error_retell` | error | Error, unspecified Retell side problem. |
| `error_unknown` | error | Error, unknown error. |
| `error_user_not_joined` | error | Error, user did not join web call within 30s after calling startWebCall. |
| `registered_call_timeout` | error | Error, phone call is 5 minutes or more apart from registration. |
# Debug SIP calls using PCAP file
Source: https://docs.retellai.com/reliability/debug-calls-pcap
Capture and analyze PCAP files in Wireshark and tshark to diagnose Retell SIP calls — codec negotiation, audio quality, one-way audio, and missed DTMF tones.
PCAP (Packet Capture) files record raw network traffic and are invaluable for diagnosing SIP call issues — including codec negotiation failures, audio quality problems, one-way audio, and missed DTMF tones. This guide walks through capturing and analyzing these files.
This is packet-level analysis for when you need to see the signaling and media. If you're starting from a call that ended unexpectedly, first check its [disconnection reason](/reliability/debug-call-disconnect); for `not_connected` outbound calls, see [debug outbound connection issues](/reliability/debug-outbound-call).
## Prerequisites
Install the tools you need:
* **[Wireshark](https://www.wireshark.org/download.html)** — GUI packet analyzer (includes `tshark` CLI)
Verify installation:
```bash theme={"dark"}
wireshark --version
tshark --version
```
***
## Step 1: Open and filter the PCAP in Wireshark
Double-click the PCAP file to open it in Wireshark, or run:
```bash theme={"dark"}
wireshark call_capture.pcap
```
### Filter for SIP traffic only
In the **Display Filter** bar, enter:
```
sip
```
This shows all SIP messages: `INVITE`, `100 Trying`, `180 Ringing`, `200 OK`, `ACK`, `BYE`, `CANCEL`, etc.
### Filter for a specific call (optional)
If you need to isolate a single call, find the `Call-ID` value in any SIP packet, then filter on it:
```
sip.Call-ID == "abc123@192.168.1.1"
```
### Filter for RTP media streams
```
rtp
```
Or combine SIP and RTP:
```
sip or rtp
```
***
## Step 2: Reconstruct the SIP call flow
After filtering SIP call(s), you can view the sequence (ladder) diagram by selecting **Telephony → VoIP Calls**:
Select the call(s) from the popup window and click **Flow Sequence**:
After clicking **Flow Sequence**, a new window opens with the ladder diagram showing the complete message exchange between endpoints:
The diagram shows the full call flow — `INVITE` → `100 Trying` → `180 Ringing` → `200 OK` → `ACK` → `BYE`.
### Read a SIP INVITE manually
Click the `INVITE` packet and expand **Session Initiation Protocol** in the packet detail pane. Key fields to inspect:
| Field | What to look for |
| ---------------- | ----------------------------------------------------------- |
| `Request-URI` | Destination SIP address |
| `From` / `To` | Caller and callee |
| `Call-ID` | Unique call identifier |
| `SDP → m=audio` | Negotiated RTP port and codec list |
| `SDP → a=rtpmap` | Codec payload type mappings (e.g., PCMU=0, PCMA=8, G.722=9) |
| `SDP → a=fmtp` | Codec parameters |
***
## Step 3: Common issues and what to look for
| Symptom | What to check in PCAP |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| One-way audio | RTP flowing only in one direction; check both streams, i.e., check RTP packets for both directions |
| No audio at all | `m=audio` port in SDP is `0` (call on hold), or RTP packets absent (where RTP is supposed to be captured) |
| DTMF not recognized | Payload type mismatch between INVITE SDP and actual RTP packets |
| Audio choppy or robotic | High jitter or packet loss in **RTP Streams** |
| Call drops unexpectedly | Look for `BYE` or `CANCEL`; check SIP response codes (4xx, 5xx) |
| Call hung up mid-conversation | Could be Media Timeout or Callee simply hung up. Look for the party that initiated the `BYE` |
| Codec mismatch | SDP `200 OK` `a=rtpmap` differs from INVITE; or RTP payload type not in SDP |
| SIP auth failure | `401/407 Proxy Authentication Required` or `403 Forbidden` in SIP flow |
| `408/477` response to INVITE | Remote SIP infrastructure may be unreachable — verify reachability, firewall settings, port (typically 5060 or 5061 for TLS), and SIP URI |
| `486` response to INVITE | Callee rejected the call. Call may be retried later |
| `500/503/603` response to INVITE | Check remote SIP infrastructure and downstream call routing status such as when call is routed to a downstream carrier for delivery; if you purchased phone numbers through Retell, contact [Retell support](/general/support) |
### Common SIP response code reference
| Code | Meaning |
| ------------- | -------------------------------------------------------------------------------------------------------- |
| `100` | Trying |
| `180` | Ringing |
| `200` | OK |
| `401` / `407` | Authentication required |
| `403` | Forbidden (Auth failure. Caller does not have permission to dial or transfer to the number/endpoint) |
| `404` | Not found (wrong number or SIP URI) |
| `408` | Request timeout (SIP message over UDP could not be delivered or the corresponding SIP Response was lost) |
| `477` | Send failed (SIP TCP or TLS transport error) |
| `486` | Busy here (Callee rejected the call) |
| `487` | Request terminated (caller did not pick up) |
| `500` | Server internal error |
| `503` | Service unavailable |
| `603` | Decline |
***
## Quick reference: filter cheatsheet
| Goal | Wireshark filter |
| -------------------- | ----------------------------------------- |
| All SIP | `sip` |
| Specific Call-ID | `sip.Call-ID == "id@host"` |
| SIP INVITE only | `sip.Method == "INVITE"` |
| SIP errors (4xx/5xx) | `sip.Status-Code >= 400` |
| All RTP | `rtp` |
| RFC 2833 DTMF | `rtp.p_type == 101` |
| SIP + RTP combined | `sip or rtp` |
| From specific IP | `ip.src == 192.168.1.10 and (sip or rtp)` |
***
## Advanced debugging
The sections below use additional tools:
* **`tcpdump`** — command-line capture (pre-installed on Linux/macOS; see [tcpdump.org](https://www.tcpdump.org/) for other platforms)
* **`sngrep`** — SIP-specific terminal UI (install instructions in the sngrep section below)
### Analyze RTP streams
This applies only when your PCAP file contains RTP media packets. Some captures include SIP signaling only — for example, the PCAP files available on the [Retell call details dashboard](/features/session-history) — in which case RTP and DTMF analysis are not available.
#### View all RTP streams
Go to **Telephony → RTP → RTP Streams**.
Wireshark lists each detected stream with:
| Column | Description |
| -------------------- | ----------------------------------------------- |
| Source / Destination | IP:port pairs |
| SSRC | Synchronization source ID |
| Payload type | Codec ID (e.g., 0 = PCMU, 8 = PCMA, 111 = Opus) |
| Packets | Total packets in stream |
| Lost | Packet loss count and percentage |
| Max jitter | Maximum inter-packet jitter in ms |
High packet loss (>3%) or jitter (>30ms) typically causes degraded audio quality or choppy speech on Retell calls. See [Choppy or unstable audio](/reliability/troubleshoot-latency#choppy-or-unstable-audio) for remediation steps.
#### Play back RTP audio
1. Select a stream in **RTP Streams**.
2. Click **Analyze → Play Streams**.
3. Wireshark decodes and plays back the audio. This lets you hear exactly what was sent or received.
#### Save RTP audio to a file
In the RTP player, click **Save payload** to export raw audio. You can then open it in [Audacity](https://www.audacityteam.org/) or convert it with [ffmpeg](https://ffmpeg.org/download.html). Install ffmpeg if needed: `brew install ffmpeg` (macOS) or `sudo apt install ffmpeg` (Debian/Ubuntu).
```bash theme={"dark"}
# Convert raw PCMU (G.711 ulaw, 8kHz, mono) to WAV
ffmpeg -f mulaw -ar 8000 -ac 1 -i rtp_payload.raw output.wav
```
***
### Extract and inspect DTMF events
#### Check for DTMF negotiation in SDP
In the `INVITE` SDP body, look for:
```
a=rtpmap:101 telephone-event/8000
a=fmtp:101 0-15
```
This means RFC 2833 DTMF is negotiated on payload type `101`. If this line is absent, in-band or SIP INFO DTMF may be used instead.
#### RFC 2833 / RFC 4733 DTMF (most common)
DTMF tones sent as RTP events show up as separate RTP packets with the negotiated telephone-event payload type (commonly `101`).
Filter for them in Wireshark:
```
rtp.p_type == 101
```
Click any matching packet and expand **Real-Time Transport Protocol → RFC 2833 RTP Event**:
| Field | Description |
| -------------- | ------------------------------------------------------------------ |
| `Event ID` | Digit pressed: 0–9, `*`=10, `#`=11, A–D=12–15 |
| `End of event` | `True` on the final packet for this digit |
| `Volume` | Signal level in dBm0 |
| `Duration` | Tone duration in RTP timestamp units (divide by clock rate for ms) |
#### SIP INFO DTMF (less common)
Some providers send DTMF as SIP INFO messages instead of RTP. Filter for them:
```
sip.Method == "INFO"
```
Expand the packet and look for a body like:
```
Signal=5
Duration=160
```
#### In-band DTMF (audio tones in RTP)
In-band DTMF is embedded in the audio stream as 350/440 Hz or 697–1633 Hz dual tones and cannot be filtered directly in Wireshark. To detect it:
1. Export the RTP audio as described in **Analyze RTP streams** above.
2. Analyze in Audacity (View → Spectrogram) or use a DTMF decoder library.
Retell captures RFC 2833 DTMF by default. Refer to [Capture DTMF input from user](/build/user-dtmf) for configuring DTMF completion options (digit limit, termination key, timeout).
***
### Capture a PCAP file
If you don't already have a PCAP, capture one at the network level.
#### Option A: Capture with `tcpdump`
`tcpdump` is pre-installed on Linux and macOS. For other platforms, see [tcpdump.org](https://www.tcpdump.org/).
Capture all SIP (port 5060) and RTP (UDP ports 10000–20000) traffic on your network interface:
```bash theme={"dark"}
sudo tcpdump -i eth0 -w call_capture.pcap \
'udp port 5060 or (udp portrange 10000-20000)'
```
| Flag | Description |
| --------------------------- | ---------------------------------------------------------- |
| `-i eth0` | Network interface to capture on (use `any` to capture all) |
| `-w call_capture.pcap` | Output file |
| `udp port 5060` | SIP signaling traffic |
| `udp portrange 10000-20000` | Typical RTP media port range |
Stop the capture with `Ctrl+C` once the call ends.
#### Option B: Capture with Wireshark (GUI)
1. Open Wireshark and select your network interface.
2. Set the capture filter: `udp port 5060 or udp portrange 10000-20000`
3. Click **Start** (blue shark fin icon).
4. Place and complete the test call.
5. Click **Stop**, then **File → Save As** to save as `.pcap` or `.pcapng`.
If you are using Retell with a custom SIP trunk, capture traffic on the server or gateway that terminates SIP — not your local machine. See [Custom Telephony](/deploy/custom-telephony) for Retell's SIP server IP ranges to filter for.
***
### Analyze with `tshark` (CLI)
For scripting and server-side analysis without a GUI:
#### Extract all SIP messages
```bash theme={"dark"}
tshark -r call_capture.pcap -Y sip -T fields \
-e frame.time \
-e ip.src \
-e ip.dst \
-e sip.Method \
-e sip.Status-Code \
-e sip.Call-ID
```
#### List all RTP streams with stats
```bash theme={"dark"}
tshark -r call_capture.pcap -q -z rtp,streams
```
#### Extract RFC 2833 DTMF events
```bash theme={"dark"}
tshark -r call_capture.pcap \
-Y "rtp.p_type == 101" \
-T fields \
-e frame.time \
-e ip.src \
-e rtpevent.event_id \
-e rtpevent.end_of_event
```
#### Export all RTP audio for a stream
```bash theme={"dark"}
tshark -r call_capture.pcap \
--export-objects rtp,/tmp/rtp_streams/
```
***
### Use `sngrep` for a quick terminal SIP view (optional)
`sngrep` provides a real-time or offline SIP ladder diagram in the terminal — no GUI needed.
```bash theme={"dark"}
# Install
brew install sngrep # macOS
sudo apt install sngrep # Debian/Ubuntu
# Read from PCAP
sngrep -I call_capture.pcap
# Live capture on SIP port
sudo sngrep -d eth0 port 5060
```
Navigate with arrow keys to select a call, then press **Enter** to view its full SIP flow and raw message content.
# Debug outbound connection issues
Source: https://docs.retellai.com/reliability/debug-outbound-call
Diagnose `not_connected` outbound Retell calls — work through `invalid_destination`, `no_valid_payment`, carrier blocks, and other disconnection reasons.
When an [outbound call](/deploy/outbound-call) has a status of `not_connected`, the call never established a connection to the destination number. The `disconnection_reason` field tells you why. This page covers the `not_connected` reasons; for the full list of reasons across all call statuses, see [debug call disconnection](/reliability/debug-call-disconnect).
## Disconnection reasons for not connected calls
* `invalid_destination`: the destination phone number is invalid. It may contain spaces or invalid characters, or your telephony provider requires a specific format (such as E.164).
* `telephony_provider_permission_denied`: the SIP trunk authentication failed.
* `telephony_provider_unavailable`: the telephony provider is unavailable or returning errors.
* `sip_routing_error`: there are loops or other issues in the SIP routing.
* `marked_as_spam`: the call was marked as spam. See the root cause and remediation below.
* `user_declined`: the user explicitly declined the call.
* `dial_failed`: no SIP error code is available, or the error is unknown.
* `dial_busy`: the number dialed is busy.
* `dial_no_answer`: the number dialed did not answer.
## Steps to troubleshoot
Check the call history and detailed log. It contains the disconnection reason, the error message and optionally a **[SIP error code](/reliability/debug-calls-pcap#common-sip-response-code-reference)**. If the logs contain a SIP error then you can see the details by checking the PCAP file available in the call logs — see [Debug calls with PCAP](/reliability/debug-calls-pcap) for information on how to debug using PCAP files.
In most cases, this would be enough for you to identify the root cause.
PCAP files are only available when the agent's data retention is set to **Everything** and the SIP transport is **UDP/TCP**. Calls using TLS transport or any other data retention setting will not have a PCAP available.
If the call detailed log and PCAP file do not provide enough information, you can try the following:
* If using custom telephony
1. Double check your configuration and make sure you imported the right information. Refer to [FAQ](/deploy/custom-telephony#faq) for more info.
2. If the configuration is not correct, delete the imported number and re-import.
3. If that does not solve it, please check with your telephony provider to see what's the error on their side.
* If using numbers purchased from Retell
1. Make sure the destination number can accept the call. Currently, numbers purchased from Retell can only make calls to US numbers.
### Number marked as spam
When a number experiences a high outbound call volume spike without any warmup, and/or has a low pickup rate, it might be marked as spam by carriers. When this happens, the number will get blocked frequently. To remedy this, you can try to:
* Purchase a new number, and warm it up before pouring all traffic to it (slowly add outbound traffic to it)
* Increase the pickup rate, read more at [Increase Pickup Rate](/build/telephony/call_efficiency_overview)
* Register the number with our spam remediation feature, read more at [verified phone number](/build/telephony/verified-phone)
# Fraud Protection
Source: https://docs.retellai.com/reliability/fraud-protection
Protect your Retell deployment with rate limiting by IP and destination number, geographic restrictions, and public key fraud protection for frontend traffic.
## Overview
Retell provides fraud protection features to help you prevent abuse of your voice AI agents. These features complement the general [abuse prevention measures](/reliability/prevent-abuse) and give you fine-grained control over how your agents are accessed.
## Rate Limiting
When using [public keys](/accounts/public-keys) to authenticate calls from your frontend, you can enable fraud protection to automatically rate limit requests based on IP address and destination phone number.
### Enabling Fraud Protection
You can enable fraud protection when creating or updating a public key:
1. Navigate to **Public Keys** in your Retell dashboard
2. Click on the public key you want to configure
3. Toggle on **Fraud Protection**
4. Save your changes
### How It Works
When fraud protection is enabled on a public key:
* Requests are rate limited based on the combination of the caller's IP address and the destination phone number
* This prevents bad actors from using the same IP to spam calls to premium rate numbers
* The rate limiting applies to outbound phone calls and SMS initiated via public key authentication
For maximum protection, combine fraud protection with [Google reCAPTCHA](/accounts/public-keys#google-recaptcha-v3-protection-optional) to prevent bot abuse.
## Geographic Restrictions
You can restrict which countries are allowed to make inbound calls to your Retell phone numbers, and which countries your phone numbers can make outbound calls to. This helps prevent International Revenue Sharing Fraud (IRSF) and limits your exposure to unwanted traffic.
### Allowed Inbound Countries
Restrict which countries can call your Retell phone numbers:
1. Navigate to **Phone Numbers** in your Retell dashboard
2. Click on the phone number you want to configure
3. Under **Allowed Inbound Countries**, add the countries that should be allowed to call this number
Changes are saved automatically.
When configured, calls from countries not on the list will be automatically rejected.
### Allowed Outbound Countries
Restrict which countries your phone numbers can call:
1. Navigate to **Phone Numbers** in your Retell dashboard
2. Click on the phone number you want to configure
3. Under **Allowed Outbound Countries**, add the countries this number should be allowed to call
Changes are saved automatically.
When configured, outbound calls to countries not on the list will be blocked.
### Configuring via API
You can also configure geographic restrictions via the [Update Phone Number API](/api-references/update-phone-number):
```bash theme={"dark"}
curl -X PATCH "https://api.retellai.com/update-phone-number/+14155551234" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"allowed_inbound_country_list": ["US", "CA", "GB"],
"allowed_outbound_country_list": ["US", "CA"]
}'
```
Use [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) country codes (e.g., "US" for United States, "CA" for Canada, "GB" for United Kingdom).
To remove restrictions, set the list to `null`:
```bash theme={"dark"}
curl -X PATCH "https://api.retellai.com/update-phone-number/+14155551234" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"allowed_inbound_country_list": null,
"allowed_outbound_country_list": null
}'
```
### Sanctioned Countries
The following countries are always blocked regardless of your configuration:
| Country | Code |
| ----------- | ---- |
| Cuba | CU |
| Iran | IR |
| North Korea | KP |
| Syria | SY |
| Russia | RU |
| Belarus | BY |
| Venezuela | VE |
Calls to or from these countries will be automatically rejected.
## Best Practices
1. **Enable fraud protection on all public keys** - This adds an extra layer of protection against abuse at minimal cost
2. **Combine with reCAPTCHA** - Use both fraud protection and reCAPTCHA for web-initiated calls to prevent bot abuse
3. **Start with restrictive country lists** - Begin with only the countries you need and expand as necessary
4. **Monitor for blocked calls** - Use [webhooks](/features/webhook-overview) to track when calls are blocked due to geographic restrictions
5. **Review regularly** - Periodically review your country restrictions to ensure they match your current business needs
# Prevent abuse
Source: https://docs.retellai.com/reliability/prevent-abuse
Defend Retell agents against International Revenue Sharing Fraud and other abuse — rate limit traffic, restrict destinations, and lock down public keys.
## Abuse scenarios
Retell AI has implemented several mechanisms to prevent bad actors from using our agents to conduct malicious activities. However, there are cases where bad actors may pretend to be a customer and spam your agents.
Malicious activity usually comes in the form of International Revenue Sharing Fraud (IRSF). Bad actors are incentivized to do so because they get kickbacks from carriers when they direct traffic to them. Common abuse scenarios include:
* making excessive outbound calls, usually to non-US numbers, either via your phone call widget or form submission. They usually rotate the destination phone number, and use a real human recording to avoid being detected.
* making outbound SMS (even 2FA SMS) messages, usually to non-US numbers.
* making a large amount of unwanted inbound calls into a number that you made public. This is less common as it's usually not going to bring them kickbacks.
* using robots to spam your chat widget. This is less common as it's usually not going to bring them kickbacks.
## Abuse prevention
Here are a few high level rules of thumb to prevent abuse. We will dive into more details below:
1. Never expose your API key to the public; always use a [public key](/accounts/public-keys) in frontend.
2. If your API key is exposed, always rotate and revoke the key.
3. Always use a reCAPTCHA if possible to prevent bots from abusing your endpoints.
4. Only allow functionalities or regions that you need.
5. Implement rate limiting (number-based, IP-based, etc.) for your endpoints if necessary.
6. Have user identification mechanisms (KYC measures) in place if necessary.
7. Have a prompt in your agent that can potentially detect unrelated calls and hang up quickly.
### Protecting outbound calling / chatting capabilities
There are two ways that you can secure the calling / chatting capabilities that you expose to the public:
* have your own user access management system, and keep the Retell API calls to your backend only.
* use the [Retell widgets](/deploy/chat-widget) to embed the calling / chatting capabilities into your website. It's highly recommended to enable [reCAPTCHA](/accounts/public-keys#google-recaptcha-v3-protection-optional) to prevent bots from abusing your endpoints.
### Protecting inbound calling
When a number is made public, it's possible to have unwanted traffic. You can set up [inbound webhooks](/features/inbound-call-webhook) to detect and block unwanted traffic based on the incoming number.
### Advanced Protection
For additional fraud protection features including rate limiting by IP/phone number and geographic restrictions per phone number, see [Fraud Protection](/reliability/fraud-protection).
# Reliability Overview
Source: https://docs.retellai.com/reliability/reliability-overview
How Retell AI maintains 99.9% uptime with enterprise infrastructure, monitoring, fallback mechanisms, and platform resilience features.
At Retell, we've made reliability our **top priority**. Our platform is built on enterprise-grade infrastructure to ensure consistent, high-quality performance for all your voice AI needs.
We focus on three key areas to maintain exceptional service quality:
1. **Phone Call Performance**
* Reliable handling of inbound and outbound calls
* Consistent connection throughout conversations
* High voice quality
2. **Agent Reliability**
* Consistent low latency during interactions
3. **Agent Performance**
* Accurate speech transcription
* Strict adherence to prompt instructions
### Our Commitment to Reliability
At Retell, we guarantee **>99.9% uptime**. To achieve this, we've invested in the following areas:
1. **Enterprise-grade infrastructure**
2. **Fallbacks and Resilience Features**
3. **24/7 Monitoring and Alerting**
4. **24/7 Support**
### Detailed Overview
1. **Enterprise-grade infrastructure**: We conduct extensive load testing on high traffic and maintain dedicated auto-scaling and provisioning to handle varying loads. Our enterprise-grade compute, networking, and infrastructure ensure stable performance.
* We guarantee **>99.9% uptime** - Subscribe to our [Status Page](https://status.retellai.com/) to get notified about any issues.
* Self-hosted models to reduce third-party dependencies.
* **Stable server cluster** (enterprise only): Opt in to route both calls and API requests to our stable server cluster, which receives delayed feature rollouts for added production stability. When enabled, point your API requests to `https://stable.retellai.com/` instead of `https://api.retellai.com/`. A \$0.02/min surcharge applies on calls. Contact [support](/general/support) to enable.
2. **Proactive Monitoring**: We maintain 24/7 latency monitoring and alerting systems to catch and address issues before they impact your operations.
* Including ASR, TTS, LLM, Knowledge base, time to first token, and network latency distribution, p75, p90, p95, p99 latencies.
* Failed calls count, ASR, LLM, TTS timeout, and error rate.
* Server CPU, GPU, memory, and network usage. Database, API response time.
3. **Resilience Features**: We've implemented fallbacks, retries, and other features to improve reliability:
* [Branded Call/Verified Phone Number Features](/build/telephony/call_efficiency_overview) to improve call pickup rate and allowlist carrier calls
* [TTS fallback and retries](/build/tts-fallback) are automatically built in, and can be manually configured.
* LLM fallback and retries are automatically built in.
4. [Testing Features](/test/test-overview)
5. **Support System**: Our dedicated support team is ready to assist if any issues arise.
* 7 days/week on-call schedule
* 24h SLA, with active support between 9 AM and 9 PM PST (lower for enterprise customers)
Read more about support [here](/general/support).
# Troubleshoot high latency
Source: https://docs.retellai.com/reliability/troubleshoot-latency
Reduce high Retell end-to-end latency — pick a faster LLM, shorten your prompt, enable fast tier, and diagnose slow LLM, ASR, and TTS components per call.
If your agent is slow to respond, work through this guide top to bottom. Across agents, the two most common causes of high latency are **LLM model choice** and **prompt length**, so those are listed first — but the dominant factor varies from one agent to the next, and these aren't always the culprit. Before concluding what's slow for a *specific* agent, confirm it against that agent's per-call [latency breakdown](/reliability/check-actual-latency) rather than assuming.
## Start with the biggest levers
These three changes resolve most latency issues. They tend to have the highest impact, but confirm which one actually applies to your agent using the per-call breakdown before acting — not every agent is bottlenecked by the same thing.
Model choice is often one of the largest factors in LLM response time, though how much it matters depends on the agent — verify against the call's LLM latency before assuming the model is the bottleneck.
* **Smaller models respond faster.** If your use case can tolerate slightly less reasoning, switching to a smaller model noticeably reduces latency.
* **Larger, reasoning-oriented models** are slower to first token — they trade latency for capability.
* Latency also varies between providers for models of a similar size, so if you've already trimmed model size and prompt and still see high LLM latency, trying a comparable model from a different provider can help.
Switching models changes your agent's behavior, not just its speed. Re-test your prompt and flows after switching — treat it as a deliberate change, not a one-click toggle.
Every token in your prompt adds to the time-to-first-token, and that cost is paid on **every turn** of the conversation.
* Keep system prompts tight and focused on the current task.
* Move rarely-needed detail into the [knowledge base](/build/knowledge-base) or into tool descriptions rather than the main prompt.
* [Agent Handbook](/build/agent-handbook) presets also add tokens to every interaction (the estimated count is shown on hover). Turn off any preset you don't need.
* Prompts beyond roughly **8k tokens** become noticeably slower. If you are well above that, trimming the prompt is one of the easiest wins available.
[Fast tier](/build/llm-options#fast-tier-premium-performance) routes your LLM calls through dedicated, high-priority infrastructure, reducing both average response time and call-to-call variance.
Fast tier costs more than the standard model rate, so weigh it against your use case — but it is one of the most reliable ways to tighten an inconsistent response time.
## Diagnose by component
If the levers above don't resolve it, use the per-call [latency breakdown](/reliability/check-actual-latency) to find which component dominates, then target that component.
### High LLM latency
If LLM latency dominates your end-to-end time and is suddenly worse than usual, your provider may be under transient heavy load.
1. Check the [Status Page](https://status.retellai.com/) for ongoing incidents.
2. If there is an active incident, wait for it to resolve.
3. Otherwise, revisit [model choice and prompt length](#start-with-the-biggest-levers) above — a consistently high LLM latency usually points to one of those.
### High ASR (transcription) latency
After you stop speaking, Retell waits a short silence window — called **endpointing** — to confirm you've actually finished before it responds. A longer endpointing setting waits for more context and produces more accurate transcripts, but it adds directly to perceived latency.
1. If you're using the accuracy-optimized [transcription mode](/build/transcription-mode), note that it intentionally waits longer (about **200ms** more) than the speed-optimized mode. Switch to the speed setting if responsiveness matters more than transcript precision.
2. Turn off **Boosted Keywords** if enabled, as it can add transcription latency.
### High TTS latency
The voice provider you choose affects how quickly the first audio byte is produced.
1. Some voice settings — such as enabling emotion — can increase TTS latency in certain cases. Disable them if you don't need them.
2. Latency also varies by provider, so if TTS dominates your latency, try a different voice provider.
### Choppy or unstable audio
This is usually network jitter or call-server load rather than model latency.
1. Run a ping test from your client to `api.retellai.com`. A round-trip time consistently above **300ms** can cause latency spikes.
2. To confirm jitter or packet loss on a specific call, [analyze its PCAP file](/reliability/debug-calls-pcap#analyze-rtp-streams) in Wireshark.
3. If your ping is low and the problem persists, contact support with an example call ID.
## Other factors
Features marked with a turtle icon 🐢 add [estimated latency](/reliability/check-estimated-latency). Aim to keep estimated latency under **1.5s**, and disable any turtle-marked feature you don't need.
The **Response Wait time** setting in [speech settings](/build/single-multi-prompt/configure-basic-settings) adds a deliberate pause before every response, up to 5.5 seconds at its most patient. Any wait it adds appears as its own row in the estimated latency breakdown. If the agent feels slow but the per-call components look normal, lower this setting.
International calls add latency from the physical distance between regions. If you're calling across countries or continents, use a phone number in the same region as your users.
## Contact support
If the steps above don't resolve your latency issues:
1. Locate your call ID.
2. Message [support](/general/support).
3. Include your call ID, the steps you've already tried, and your current latency measurements.
# Debug wrong response
Source: https://docs.retellai.com/reliability/wrong-response
Fix Retell agents that give incorrect responses — switch to a more capable LLM, simplify prompt structure, add finetune examples, and review function calls.
If your agent isn't following instructions correctly, especially with longer or complex prompts:
1. Check if you're using a lightweight model (e.g., 4.1-mini)
2. Switch to a more capable model like `gpt-4.1`
If the issue persists:
1. Check if your prompt structure is too complex
2. Follow [prompt engineering guide](https://www.promptingguide.ai/)
3. Break down complex tasks into clear, sequential steps
4. Add explicit transition conditions between different steps
# Increase transcription accuracy
Source: https://docs.retellai.com/reliability/wrong-transcript
Increase Retell transcription accuracy — use boosted keywords for niche terms, choose the right ASR provider, and tune transcription modes for clarity.
We take transcription quality seriously and understand its crucial importance for our customers. Our transcription accuracy primarily depends on the AI models we use, carefully selected to balance both accuracy and processing speed.
### Common Transcription Issues and Solutions
#### 1. Wrong Transcript for Special Words or Terms
**Issue**: Specific words (like "retell") or domain-specific terms (such as medical terminology) are missing from transcripts.
**Solution**: Use Boosted Keywords
* Add custom keywords to enhance the model's vocabulary
* Support for up to 100 custom keywords
#### 2. Transcription error due to background noise / speech
Play with [denoising mode setting](/build/handle-background-noise) to see if it helps.
#### 3. Transcription error due to sentence being cut off
Sometimes the transcription quality can be impacted if the sentence was cut off (the transcription spits out the finalized sentence before it should). In this case, you can turn on [transcription mode](/build/transcription-mode) to be optimized for accuracy.
## FAQ
If the background noise level is not particularly high, this is often caused by the denoising mode filtering out short, low-energy responses. Try setting the [denoising mode](/build/handle-background-noise) to **No Denoising** — this preserves more of the raw audio signal and can significantly improve ASR accuracy for brief utterances when the environment is relatively quiet.
# Batch test your agent
Source: https://docs.retellai.com/test/batch-test-simulation
Run Retell batch tests on many simulation cases at once, then review pass rate and per-case results in Batch Testing History to catch regressions.
A batch test run is a set of [test cases](/test/llm-simulation-testing) run together in a simulation, so you can check many scenarios in one pass instead of one at a time. Batches are how you use your saved test cases as a regression suite: run them after every prompt or flow change, and confirm the pass rate holds before you deploy.
For example, a support team keeps two dozen test cases covering refunds, escalations, and identity checks. Before each release they run the whole set as a batch, then compare the pass rate to the previous run to catch anything a prompt change broke.
## Run a batch
In the Test Cases tab, select the cases you want to run (one, several, or all) and click Run Test. Retell launches a batch, even for a single case, and sends the results to Batch Testing History.
## Track a running batch
You track and review batches under the Batch Testing History tab. A batch that's still running is marked **Ongoing** in the list on the left, and a **Running tests** indicator sits below its results table until the last case finishes.
Results stream in on a 10-second refresh rather than all at once: each case joins the results table as it completes, and the counts above the table go up with it. Cases that haven't finished yet stay hidden, so the table only ever shows completed runs.
## Review results
Runs land in Batch Testing History. The ledger on the left lists every run, each showing:
* the number of test cases in the run
* the date and time it finished
* the pass rate (the share of cases that met all their success criteria)
The list only ever shows batches run against one [agent version](/agent/version) at a time, and the version chip above it tells you which. It defaults to your latest version, so older batches won't be listed until you switch. To see another version's history, select that version in the agent's version panel; to go back to the latest, clear the chip with its X.
Select a run to open its results table, and filter it by All, Passed, or Failed. An **Error** count appears alongside them when a run failed to complete instead of being graded, such as a simulation that timed out. Its columns are:
| Column | What it shows |
| ------------ | ---------------------------------------------------------------------------------------- |
| Test Case | The name of the case that ran. |
| Test Call ID | The ID of the simulated run, for reference or API lookup. |
| Time | The time the simulation started. |
| Test Result | The grader's written explanation of the result, or the error message if the run errored. |
| Test Success | Whether the run met its success criteria. |
Select a test case row to open its details panel on the right, which shows:
* the test case ID and test result
* one explanation of why the run passed or failed across all of its success criteria
* the full run transcript: node transitions, tool calls and their responses, and the agent and user turns
A View In Test Playground button opens the agent in a new tab with that run's transcript loaded into the [Playground](/test/llm-playground#test-playground), where you can edit any turn and continue the conversation by hand.
## Manage batches with the API
Run and read batches programmatically to fold them into CI before you deploy:
* [Run a batch test](/api-references/create-batch-test) starts the batch and returns its ID.
* [Get batch test](/api-references/get-batch-test) returns the batch's status and its pass, fail, and error counts. Poll it until the status is `complete`.
* [List test runs](/api-references/list-test-runs) lists each run in the batch, and [Get a test run](/api-references/get-test-run) returns one run's transcript and explanation.
* [List batch tests](/api-references/list-batch-tests) lists past batches for an agent version.
The [simulation testing](/test/llm-simulation-testing#manage-test-cases-with-the-api) page walks through this as a full CI sequence.
## Next steps
* [Simulation testing](/test/llm-simulation-testing) covers building the test cases a batch runs.
* [Testing with Conductor](/test/testing-with-conductor) can generate those test cases for you.
* [Testing overview](/test/test-overview) compares simulation and batch testing with live web and phone call testing.
# Manually test your agent
Source: https://docs.retellai.com/test/llm-playground
Test a Retell agent in the LLM Playground: chat manually or with an AI-simulated user, inspect tool calls and transitions, and replay any turn.
The LLM Playground lets you test your agent in text in the dashboard, without placing a web or phone call. It's the Test LLM tab in your agent's Test panel, and it has two modes: Manual Chat, where you type each turn yourself, and AI Simulated Chat, where an AI plays the user from a prompt.
For example, after adding a "reschedule appointment" path, open the Playground, ask to reschedule, and confirm the agent calls `check_availability` with the right date before you ever test it on a call.
## Open the Playground
In the agent editor, open the Test panel and select the Test LLM tab. The panel's other tab, Test Audio, is for [live voice testing](/test/test-web). The Test LLM tab isn't available for [custom LLM](/integrate-llm/overview) agents or speech-to-speech voice agents.
You can also open the same Playground from four other places. The first three each load an existing transcript into it with View In Test Playground:
* A call's details panel in your [Call History](/features/session-history), or a conversation's details panel in Chat History.
* A call's details panel in [AI QA](/ai-qa/overview).
* A run's details panel in [Batch Testing History](/test/batch-test-simulation).
* The Test Subflow tab inside a [Conversation Flow](/build/conversation-flow/overview) component, to test that component on its own.
## Chat with the agent manually
Choose Manual Chat to drive the conversation yourself. Type each turn the user would say and read the agent's response. It doesn't grade anything: it's for exploring behavior turn by turn. Every turn shows the agent's node transitions, tool invocations, and tool results inline, so you see what the agent did, not just what it said.
For a multi-prompt or Conversation Flow agent, the Current State or Current Node selector above the transcript sets where the conversation sits. Use it to start partway through the flow instead of at the beginning, and to jump the conversation elsewhere mid-chat, which saves talking your way to a branch you want to check.
New takes you back to the Manual Chat and AI Simulated Chat choice, where selecting Manual Chat again starts a fresh conversation. Save keeps the current conversation as a thread, and you rename it with the pencil next to the thread name. The trash icon appears once a thread is saved and deletes it. Use the dropdown on the thread name to switch between saved threads and compare runs.
## Simulate a user
Choose AI Simulated Chat to have an AI play the caller. It asks for two things: a user prompt (who the caller is and what they want) and the LLM setting (the model that plays the user). The run also picks up whatever dynamic variables and function mocks you've set in the Dynamic Variables dropdown, so set those first if the scenario needs them. Run it to watch the agent and the simulated user talk in real time, with node transitions, tool invocations, and tool results shown along the way.
A good user prompt spells out who the caller is, what they want, and how they behave:
```markdown wrap theme={"dark"}
## Identity
Your name is Mike.
Your date of birth is June 10, 1999.
Your order number is 7891273.
## Goal
Your primary objective is to return the package you received and get a refund.
## Personality
You are a patient customer. However, if the conversation becomes too long or complicated, you will show signs of impatience. If the issue remains unresolved, you may become frustrated and angry.
```
From there you can retry the run, stop it mid-conversation, or edit the user prompt and run it again. To keep a scenario, click Save. A dialog opens for you to add the test case's success criteria, dynamic variables, and custom function mocks without leaving the page. The saved case then appears in the [Simulation tab's](/test/llm-simulation-testing) Test Cases list, ready to rerun or [batch](/test/batch-test-simulation).
## Test Playground
The Playground can change a conversation turn by turn. Open a transcript in it from [Batch Testing History](/test/batch-test-simulation), your [Call History](/features/session-history), Chat History, or [AI QA](/ai-qa/overview): open the details and click View In Test Playground. The agent opens in a new tab with that transcript loaded into Manual Chat, which is the editable mode. From there you can:
* **Edit a user turn.** Confirming the edit truncates the conversation there and continues from your new wording.
* **Rerun an agent response**, which also drops the turns below it.
* **Delete a single turn** without touching the rest of the transcript.
* **Replay the whole chat** with the refresh button below the transcript, which clears the agent's responses and resends every user turn in order against the current configuration.
* **Type a new turn** to carry the conversation on by hand.
A transcript loaded from history arrives as an ended conversation, so Send is disabled at first. Edit a turn, delete one, or rerun an agent response and the input unlocks.
Every turn still shows node transitions, tool invocations, and results, which makes this the way to debug a Conversation Flow turn by turn, especially for edge cases you can't reach from a fresh chat.
## Set dynamic variables and mocks
Open the Test Inputs dropdown next to the tabs to set inputs before you chat, then click Save to apply them. It has two tabs:
* Dynamic variables: give each [dynamic variable](/build/dynamic-variables) a test value so placeholders resolve the way they would on a real call.
* Custom function mocks: set a pretend result for one of your agent's functions. Your agent uses functions to do real things, like check availability, transfer a call, or send a text. A mock lets you tell the test what a function should return, so the agent reacts to that answer and the real action never runs.
These values apply to both Test LLM and Test Audio runs for this agent. They're kept in your browser rather than on the agent, so they don't follow the agent to a teammate or to another machine.
Both are covered in detail in [test variables and mocks](/test/llm-simulation-testing#test-variables-and-mocks).
## Debug a response
When a reply looks off, click the Debug button on that turn. Debug can regenerate the answer 10 times and show a count of how often each response came up, so you can see the agent's most common outputs on a non-deterministic turn. For the full fix workflow (finetune examples, node splits, temperature), see [Debug your agent response](/test/llm-playground-debug).
## Best practices
* Start simple, then work up to harder scenarios and edge cases.
* Mock the functions that take real action, like transferring a call or sending a text, so the test uses your pretend result and nothing actually happens.
* Use tool-call inspection to confirm parameters, not just the final wording.
* Save the threads worth revisiting, and turn the important ones into [simulation test cases](/test/llm-simulation-testing) for regression testing.
## Next steps
* [Debug your agent response](/test/llm-playground-debug) when a reply or transition is off.
* [Simulation testing](/test/llm-simulation-testing) saves scenarios as graded, rerunnable test cases.
* [Testing overview](/test/test-overview) compares the Playground with simulation and live call testing.
# Debug your agent response
Source: https://docs.retellai.com/test/llm-playground-debug
Debug a Retell agent in the LLM Playground: open Debug on any response or transition, apply suggested fixes, and regenerate the turn to verify.
When your agent answers wrong or takes the wrong path, Debug lets you fix that exact turn instead of guessing at the whole prompt or flow. Open it on a specific response or transition in the [LLM Playground](/test/llm-playground) to see the fixes that usually help, then regenerate the turn to check your change before it reaches a real call.
Debug is a Playground feature, and what it offers depends on the agent. [Conversation Flow](/build/conversation-flow/overview) agents get the full set, including transition debugging and node-level fixes. Single-prompt and multi-prompt agents get response and tool-call debugging, with temperature as the suggested fix.
## Fix a wrong response
Click the Debug button on the agent's response.
Debug the AI response lists the fixes that usually help. Each one links to a guide you follow to make the change yourself:
* [Add Fine-tuning Examples](/build/conversation-flow/debug-guide#add-conversation-finetune-examples) to teach the wording you want.
* [Split One Node into Two Nodes](/build/conversation-flow/debug-guide#split-the-node-into-multiple-nodes) when one node is doing too much.
* [Adjust LLM Temperature](/build/conversation-flow/debug-guide#adjust-the-llm-temperature) to trade variety for consistency.
On a single-prompt or multi-prompt agent, temperature is the only fix offered, since there are no nodes to split.
Click Regenerate the answer to produce a new response using the agent's current configuration, so once you've applied a fix you can see the new behavior. Use Regenerate 10 answers to check how consistent the responses are across runs.
## Fix a transition problem
When the agent moves to the wrong node, or doesn't move when it should:
Click Debug on the agent's response and select Didn't transition as expected?, or click Debug directly on the transition dialog.
Each fix links to a guide you follow to make the change yourself:
* [Add Fine-tuning Transition Examples](/build/conversation-flow/finetune-examples#finetune-examples-for-transition).
* [Split One Node into Two Nodes](/build/conversation-flow/debug-guide#split-the-node-into-multiple-nodes).
Click Regenerate the transition to try again with the agent's current configuration, or Regenerate 10 transitions to see which node the agent picks across 10 attempts.
## Fix a wrong tool call
When the agent calls the right function with the wrong arguments, or calls one it shouldn't, hover the Tool Invocation row in the transcript and click Debug. Debug the tool invocation works like the response popup: it links to the fixes that apply, then Regenerate the tool invocation retries that call with the agent's current configuration and Regenerate 10 tool invocations shows how often each set of arguments comes up.
## Fix inconsistent responses
When the same prompt gives different answers, click Debug on the response. The popup links to guides for the fixes that help most: [add fine-tuning examples](/build/conversation-flow/debug-guide#add-conversation-finetune-examples), [split the node](/build/conversation-flow/debug-guide#split-the-node-into-multiple-nodes), or [adjust the temperature](/build/conversation-flow/debug-guide#adjust-the-llm-temperature).
Each debug popup has a 10-run option: Regenerate 10 answers, Regenerate 10 transitions, and Regenerate 10 tool invocations. Each one replays that turn 10 times and lists every distinct result with a count out of 10, so you can see the agent's most common output on a non-deterministic turn and confirm it holds steady after a fix.
## Next steps
* [Manually test your agent](/test/llm-playground) covers the rest of the Playground.
* [Testing overview](/test/test-overview) compares the Playground with simulation and live call testing.
# Automatically test your agent
Source: https://docs.retellai.com/test/llm-simulation-testing
Test a Retell agent with LLM simulation: an AI-simulated user runs your scenario in a text conversation, and success criteria grade each run.
Simulation testing lives in the Simulation tab, where you build test cases and run them against your agent without placing a call. An AI plays the caller, and each run is graded pass or fail with a written explanation, so your saved test cases become a regression suite you rerun after every prompt or flow change, one at a time or as a [batch](/test/batch-test-simulation).
A test case is made up of:
* a **user prompt** that describes the simulated user (who they are, what they want, how they behave)
* **success criteria** that grade the run
* **dynamic variables** to preset for the run
* **custom function mocks**
* the **LLM** the simulated user runs on
This is different from the [LLM Playground](/test/llm-playground) on the side panel, where you chat with the agent by hand. Simulation testing is the repeatable, graded version.
For example, an e-commerce support team keeps a test case where the simulated caller wants to return a package and grows impatient if the conversation drags. Its criteria check that the agent processes the refund, ends the call with the `end_call` function, and keeps responses short. Any prompt change that breaks one of these behaviors fails the test before it reaches production.
Simulation testing works with single-prompt, multi-prompt, and Conversation Flow agents, and it runs as a text conversation. Agents using a custom LLM are not supported.
## The Simulation tab
Open your agent and select the Simulation tab in the top menu bar. It has two tabs:
* Test Cases holds every test case you've built for the agent, and is where you create or import them.
* Batch Testing History is the ledger of past runs, where you review results. See [Batch testing](/test/batch-test-simulation).
Short on time? Conductor can generate test cases for you from real calls or from scratch. See [Testing with Conductor](/test/testing-with-conductor).
## Create a test case
In the Test Cases tab, click Test Case to open the Add a Test Case dialog, or Import to load cases from a JSON file. Each test case captures the scenario and how to grade it.
Give it a name you'll recognize in a batch, like `return-refund-impatient-caller`.
Describe the person the AI should play. Include the identity details your agent asks for (name, date of birth, order number), the caller's goal, and a personality that shapes how they respond:
```markdown theme={"dark"}
## Identity
Your name is Mike.
Your date of birth is June 10, 1999.
Your order number is 7891273.
## Goal
Your primary objective is to return the package you received and get a refund.
## Personality
You are a patient customer. However, if the conversation becomes too long or complicated, you will show signs of impatience. If the issue remains unresolved, you may become frustrated and angry.
```
Fill in the [success criteria](#define-success-criteria) that grade the run, plus any [test variables and mocks](#test-variables-and-mocks) the scenario needs.
At the bottom of the dialog, pick which LLM generates the simulated user's replies, then save the test case. This only affects the simulated user. Your agent keeps its own [model configuration](/build/llm-options), and both bill per message.
Once you have cases, select them with the checkboxes to act on several at once: Run Test starts a batch, Duplicate copies them, Export downloads them as JSON you can re-import to another agent, and Delete removes them for good.
## Define success criteria
Success criteria are the checks that grade a test run. When a test case finishes, all of its criteria are judged together in a single pass against the transcript: the run passes only if every criterion is met, and you get one explanation covering the whole run rather than a verdict per criterion. Because it's one combined judgment, a single unmet criterion fails the run.
A run can also end in **Error** instead of being graded, which means the simulation never got as far as a verdict. A test times out after 10 minutes, and a conversation is cut off once it passes 400 utterances, the simulated user starts repeating itself, or the agent goes silent and stops responding to the caller. In that case the explanation is the error, not a grade.
This is not how [AI QA](/ai-qa/overview) scores real calls. AI QA evaluates each metric separately and reports which ones passed and which failed, with a reason for each. Simulation testing gives one verdict for the run. If you need a per-criterion breakdown, phrase each check as its own test case, or use AI QA on real calls.
Write one behavior per criterion, and be specific about what counts as success:
```markdown theme={"dark"}
1. Verify that the customer successfully returned the package and received a refund.
2. Confirm that the end_call function was called at the end of the conversation.
3. Ensure the agent's responses are conversational and contain 5 sentences or fewer.
```
Criteria can check outcomes (the refund was processed), function behavior (`end_call` was invoked), and conversation quality (response length, tone).
## Test variables and mocks
The Test Variables & Mocks section of a test case keeps runs realistic and repeatable. It applies to both simulation (LLM) tests and [web call tests](/test/test-web).
### Dynamic variables
If your agent uses [dynamic variables](/build/dynamic-variables), set a test value for each one so placeholders like `{{customer_name}}` resolve during the simulation, the same way they would on a real call.
Use them to play a specific caller or to exercise a specific branch. An agent that opens with "Hi `{{customer_name}}`, calling about your `{{appointment_date}}` appointment" behaves differently for a returning customer than for someone with no record. Give both variables real values to test the returning-customer path, or leave `appointment_date` empty to test the "no appointment on file" branch.
Instead of retyping the same values for every test case, load them from an [environment tag](/agent/version). Every agent ships with a `prod` and a `staging` tag, you can add your own, and each tag carries its own set of dynamic variable values. Pick one from Load Saved Values to fill the form with that tag's values, then adjust them for this test. Your edits stay on the test case and don't change the tag.
A tag's dynamic variables are not test-only. They're injected into every live call and chat running on that tag, so editing `prod` from the test panel changes production behavior. Load a tag's values freely; be deliberate about editing them.
### Custom function mocks
A mock intercepts a [function call](/build/add-function-calling) during the test and returns a set result instead of calling the real function, so the function doesn't reach live systems and returns the same result every run.
Mocks are honored for the functions that reach outside Retell: custom functions, [code tools](/build/single-multi-prompt/code-tool), [integration tools](/integrations/overview#use-integration-tools-in-an-agent), the built-in Cal.com tools, call transfers, and SMS. Built-in conversation actions are always simulated and ignore any mock you set for them, including end call, press digit, extract dynamic variables, and agent transfer. **MCP tools also ignore mocks**: the dropdown lists them, but a test calls your MCP server for real.
On the Custom Function Mocks tab, click + Add and pick the function from the Function dropdown:
* For a built-in action like a transfer, choose Successfully or Failed.
* For a custom function, enter the mock result it should return.
For example, a `check_availability` custom function might return:
```json theme={"dark"}
{
"availability_for_selected_time": [
{
"date": "Tuesday, February 25, 2025 (Pacific Standard Time)",
"availability_range": [
"From 1:00 PM to 3:00 PM",
"From 3:30 PM to 4:00 PM",
"From 7:00 PM to 7:30 PM"
]
}
]
}
```
Add a mock for each function you want to control. Any function you leave unmocked calls its real endpoint during the test.
An unmocked custom function, code tool, calendar tool, or MCP tool calls its real production endpoint during a test, so a test can create a real booking, charge, or record. Mock every function that reaches production before you run a test, and give each mock a non-empty result: a mock left blank is skipped and the real function runs.
Transfers and SMS are the exception. In a simulation they're always faked, whether you mock them or not, so an unmocked transfer or text can't reach a real phone.
## Run and review test cases
To run test cases, select them in the Test Cases tab and click Run Test, or run a single case with the Test button on its row. Retell launches a batch either way, even for one case, and grades each run in Batch Testing History.
A batch runs each selected case exactly once. To sample the same scenario several times, duplicate the case or run it again. [Batch testing](/test/batch-test-simulation) covers reading the results.
## Manage test cases with the API
Test cases and batch runs are both available over the API, so you can gate a deploy on your suite from CI:
[Create a test case definition](/api-references/create-test-case-definition) for each scenario, or [list](/api-references/list-test-case-definitions) and [update](/api-references/update-test-case-definition) the ones you already have.
[Run a batch test](/api-references/create-batch-test) with the case IDs you want. The response is the batch ID and a `status` of `in_progress`; the runs are queued and graded asynchronously, so nothing is finished yet.
Call [Get batch test](/api-references/get-batch-test) until `status` is `complete`, then read `pass_count`, `fail_count`, `error_count`, and `total_count` to decide whether to ship.
[List test runs](/api-references/list-test-runs) for the batch to get each run's ID and result, and [Get a test run](/api-references/get-test-run) for one run's transcript and explanation.
Running a single case still means creating a batch of one; there's no separate run-one-case endpoint.
## Best practices
* **Mock functions that reach production before you run.** An unmocked custom function, code tool, calendar tool, or MCP tool calls its real endpoint, so a test can create a real booking or record.
* **Write one behavior per success criterion**, phrased as a checkable outcome.
* **Mirror real call context with dynamic variables**, and keep a separate case per branch (returning customer versus no record on file).
* **Rerun a batch before you trust a single failure**, since the simulated user and the grader are both LLMs. Judge a scenario on its pass rate across runs, not one run.
* **Grow the suite from real calls.** Turn a failed production call into a regression case with [Testing with Conductor](/test/testing-with-conductor) or [AI QA](/ai-qa/overview).
* **Cover edge cases, not just happy paths**: an angry caller, a refusal to verify identity, a mid-call change of mind.
* **Rerun the suite after every prompt or flow change** before you deploy.
## Glossary
| Term | What it means |
| ------------------------- | -------------------------------------------------------------------------------------------------------- |
| **Test case** | A saved scenario (user prompt, success criteria, variables, and mocks) you can rerun against your agent. |
| **Simulated user** | The LLM that plays the caller, following your user prompt. |
| **User prompt (persona)** | The description of who the simulated user is, their goal, and how they behave. |
| **Success criteria** | The checks that grade a run. They're judged together for one pass or fail verdict and one explanation. |
| **Function mock** | A canned response that intercepts a function call so the real function doesn't run. |
| **Dynamic variables** | Placeholders like `{{customer_name}}` you set to test values so they resolve during a run. |
| **Batch test** | Running many test cases together; results land in Batch Testing History. |
| **Pass rate** | The share of runs that met all success criteria across a batch. |
## Next steps
* [Batch test your agent](/test/batch-test-simulation) to run many test cases at once and track the pass rate across releases.
* [Testing with Conductor](/test/testing-with-conductor) to generate and run test cases automatically.
* [Testing overview](/test/test-overview) compares simulation testing with the LLM Playground and live web or phone call testing.
# Testing overview
Source: https://docs.retellai.com/test/test-overview
Compare Retell's agent testing methods — LLM Playground, simulation, batch, web call, and phone call testing — and pick the right one for each stage.
Retell gives you several ways to test an agent before it takes real calls, from a quick text chat to a full phone call. Use the lighter, cheaper methods while you build, and the real-audio methods to validate before you go live.
## Testing methods
### LLM Playground
Chat with your agent in text, by hand or with an AI-simulated user, and inspect tool calls and transitions turn by turn. Best for building and debugging. It's the Test LLM tab in the agent's test panel. See [Manually test your agent](/test/llm-playground) and [Debug your agent response](/test/llm-playground-debug).
### Simulation testing
Save scenarios as graded test cases and run them, one at a time or as a [batch](/test/batch-test-simulation), to catch regressions automatically. [Conductor](/test/testing-with-conductor) can write the test cases, run the batch, and report the failures. See [Automatically test your agent](/test/llm-simulation-testing).
### Web call testing
Talk to your agent in the browser to hear real audio, latency, and interruptions, without a phone number. See [Web call testing](/test/test-web).
### Phone call testing
Place or receive a real phone call to validate telephony: carrier audio, DTMF, and transfers. See [Phone call testing](/test/test-phone).
## Which method fits each scenario
Match the method to what you're trying to do. Several methods can fit the same scenario, so pick the lightest one that covers it.
| I want to... | LLM Playground | Simulation testing | Web call | Phone call |
| -------------------------------------------------------- | :------------: | :----------------: | :------: | :--------: |
| Iterate on a prompt or flow change quickly | ✅ | ✅ | | |
| Inspect tool calls and node transitions turn by turn | ✅ | ✅ | | |
| Replay or edit a single turn to reproduce a bug | ✅ | | | |
| Role-play a specific caller or edge case with an AI user | ✅ | ✅ | | |
| Grade a run pass or fail against success criteria | | ✅ | | |
| Catch regressions across a suite, including from CI | | ✅ | | |
| Hear real voice, latency, and interruptions | | | ✅ | ✅ |
| Check transfers, DTMF, and carrier audio | | | | ✅ |
| Test without a phone number | ✅ | ✅ | ✅ | |
Only simulation testing grades a run. The Playground's AI Simulated Chat drives a conversation the same way but doesn't score it, which is why saving a run as a test case is what turns it into a check.
For exact rates, see [testing pricing](/test/testing-pricing).
## What your agent supports
* **Custom LLM agents** can only be tested with a web or phone call. The Test LLM tab is hidden for them, and simulation and batch testing reject them.
* **Speech-to-speech agents** are also audio-only, since there's no text turn to simulate.
* **Chat agents** are the reverse: they have no Test Audio tab, so you test them with the Playground and simulation testing.
## A workflow that works
Iterate in the [LLM Playground](/test/llm-playground): chat by hand, inspect tool calls, and [debug](/test/llm-playground-debug) specific turns.
Turn your key scenarios into [simulation test cases](/test/llm-simulation-testing) and run them as a [batch](/test/batch-test-simulation) after every change. Let [Conductor](/test/testing-with-conductor) draft cases from real calls.
Run a [web call](/test/test-web) to hear how the agent sounds and handles interruptions.
Place a [phone call](/test/test-phone) to check transfers, DTMF, and carrier audio before you go live.
To score real production calls after they happen (for hallucinations, resolution rate, latency, and more), use [AI Quality Assurance](/ai-qa/overview) alongside your test suites.
Keep a checklist of critical paths, both happy paths and edge cases, and run it before every deployment.
# Phone call testing
Source: https://docs.retellai.com/test/test-phone
Test a Retell voice agent on a real phone call: attach a number, place an outbound test call or dial in, and validate audio, latency, DTMF, and transfers.
A phone call test runs your agent over real telephony, so you can validate what a [web call](/test/test-web) can't: carrier audio quality, network latency, DTMF menu navigation, and live call transfers. Run one as a final check before you take the agent live.
Phone testing needs a [phone number](/deploy/purchase-number) attached to your agent. The test call runs whichever [agent version](/agent/version) the number is bound to: a specific version number, a tag, Latest Published, or Latest Created. Because Latest Created resolves to your newest version, published or not, you can dial a draft agent without publishing it first.
## Place an outbound test call
Have Retell call your phone so you can talk to the agent.
Newer workspaces have to pass identity verification before Retell will place an outbound call, including a test call. If yours hasn't, the number's page shows a verification prompt in place of the call button. See [identity verification](/accounts/kyc).
On the Phone Numbers page, open a number you own and set your agent as its Outbound agent. Pick the version you want to test here: a numeric version, a tag, Latest Published, or Latest Created for your current draft.
On the number's detail page, click Make an outbound call, enter your phone number, and place the call. Set any [dynamic variables](/build/dynamic-variables) in the same dialog so they render on the test call.
## Test with an inbound call
On the Phone Numbers page, open a number you own and set your agent as its Inbound agent.
Call that number from your own phone. The call connects to the attached agent.
## What to check on a phone call
A phone call is where telephony-specific behavior shows up:
* **DTMF and press-digit**: whether the agent navigates or responds to keypad input.
* **Call transfers**: whether a warm or cold transfer connects. Transfers to a phone number can't be tested over a web call.
* **[Voicemail](/build/handle-voicemail) and IVR detection**: whether the agent recognizes a machine and takes the action you configured. Detection only runs on phone calls, so this is the one behavior a web call can't approximate at all.
* **SMS**: whether a send-SMS step delivers, which also needs a real number.
* **Carrier audio and latency**: how the agent sounds and how it handles delay over the network.
For example, if your agent transfers billing questions to a human and reads out a reference number for the caller to enter, place a phone test call, trigger the transfer, and confirm the handoff connects and the digits register.
After a test call, ask [Conductor](/conductor/test-and-improve) to review it and suggest fixes.
## Next steps
* [Web call testing](/test/test-web) for a quick browser check without a phone number.
* [Simulation testing](/test/llm-simulation-testing) for automated pass or fail across many scenarios.
* [Testing overview](/test/test-overview) compares all the testing methods.
# Web call testing
Source: https://docs.retellai.com/test/test-web
Test a Retell voice agent with a browser web call from the dashboard: use the Test Audio panel to hear real audio, latency, and interruptions.
A web call test lets you talk to your agent in the browser, so you hear how it sounds and handles a live voice conversation before you put it on the phone. Where [simulation testing](/test/llm-simulation-testing) runs on text, a web call exercises real audio: latency, turn-taking, and interruptions. It needs no phone number, and you can test a draft agent without publishing it first.
## Start a web call test
In the agent editor, open the Test panel on the right and select Test Audio.
If your agent uses [dynamic variables](/build/dynamic-variables) or functions you'd rather not fire for real, open the Test Inputs dropdown next to the tabs, fill in the Dynamic Variables and Custom Function Mocks tabs, and click Save. The same values apply to [Test LLM](/test/llm-playground) runs.
For a multi-prompt or Conversation Flow agent, choose a starting state or node to begin partway through the conversation instead of at the beginning. This is the quickest way to hear one branch without talking your way to it.
Click Run Test and allow microphone access, then talk to your agent. Click End the Call to hang up.
## What a web call test is good for
Use it for the checks a text simulation can't make:
* **Latency and turn-taking**: how quickly the agent responds and whether it waits for you to finish.
* **Interruptions**: whether the agent stops and listens when you talk over it.
* **How the voice actually sounds**: pacing, pronunciation, and filler.
For example, after reworking your greeting and opening question, run a web call to hear the turn-taking, interrupt the agent mid-sentence, and confirm it yields before you ship the change.
## Limitations
Some telephony behavior can't happen in a browser call:
* **Transfers to a phone number fail**, whether cold, warm, or agentic warm. The dashboard warns you when your agent has a transfer, and the call returns a transfer failure if the agent tries. [Transfer to another agent](/build/single-multi-prompt/transfer-agent) does work, and the test panel follows the swap.
* **Sending an SMS fails**, since there's no phone number on the call.
* **Voicemail and IVR detection don't run**, so you can't check a voicemail drop or a phone-tree branch.
Use [phone call testing](/test/test-phone) for any of those, or for real carrier audio. For automated pass or fail across many scenarios at once, use [simulation testing](/test/llm-simulation-testing) and [batch testing](/test/batch-test-simulation).
A web call test is a real call for billing and limits: it bills per minute, takes one of your [concurrent call](/deploy/concurrency) slots, and won't start if your account has a payment problem.
After a test call, ask [Conductor](/conductor/test-and-improve) to review it and suggest fixes.
## Next steps
* [Make a web call](/deploy/web-call) adds browser-based calls like this to your own app.
* [Phone call testing](/test/test-phone) validates real telephony, DTMF, and transfers.
* [Testing overview](/test/test-overview) compares web calls with simulation and the LLM Playground.
# Testing pricing
Source: https://docs.retellai.com/test/testing-pricing
How Retell bills agent testing: text tests (Playground, simulation, batch) cost per message and voice tests (web, phone) cost per minute at production rates.
Testing an agent bills at the **same rates as production**. There's no separate test tier and no free testing sandbox, so a message or a minute spent testing costs what it would on a live call. Plan test usage like any other usage, and check the [Retell pricing page](https://www.retellai.com/pricing) for current rates.
## How testing is billed
| Method | Billed | Rate |
| ---------------------------------------------------------------------- | ------------------------- | --------------------------------------------------------------------------- |
| [LLM Playground](/test/llm-playground) (Manual Chat) | Per message | Chat rate for your agent's model |
| [Simulation testing](/test/llm-simulation-testing) (AI Simulated Chat) | Per message | Chat rate for your agent's model, plus the model playing the user |
| [Batch testing](/test/batch-test-simulation) | Per message, plus grading | Chat rates above, summed across every case, plus one analysis unit per case |
| [Web call testing](/test/test-web) | Per minute | Voice rate (no telephony) |
| [Phone call testing](/test/test-phone) | Per minute | Voice rate plus telephony |
## Text testing
The Playground, simulation testing, and batch testing run in text and bill **per message**, at the chat rate for the model that produced each message. Rates range from roughly **$0.001 to $0.05 per message** depending on the model, so a cheaper model keeps iteration inexpensive. See the [pricing page](https://www.retellai.com/pricing) for the per-model rates.
Three things that add up:
* **A simulated run bills two models.** The **LLM setting** you pick applies to the simulated user only. Your agent's replies bill at the model in the agent's own [LLM settings](/build/llm-options), so a cheap simulated user doesn't make a run cheap if the agent runs an expensive model.
* **Batches multiply.** A batch bills every case on every run, with no discount. Running 20 cases three times each bills 60 runs.
* **Grading bills too.** Each case in a batch adds one post-call-analysis unit for the pass or fail verdict, on top of the conversation. Unsaved Playground runs aren't graded, so they don't carry this.
## Voice testing
[Web call testing](/test/test-web) and [phone call testing](/test/test-phone) place a real voice call and bill **per minute**, at the same rate as a production call: voice infrastructure, text-to-speech, and the LLM add up to roughly **$0.07 to $0.31 per minute** depending on the model and voice.
* **Phone testing adds telephony** (about $0.015 per minute for a Retell number) and requires a [phone number](/deploy/purchase-number). Retell numbers cost $2.00 per month.
* **Web testing has no telephony line**, since it runs in the browser.
Because a voice minute costs far more than a text message, run most of your checks in text simulation and reserve web or phone calls for final validation.
## Conductor
[Conductor](/test/testing-with-conductor) is free up to a daily allowance: **30 messages per user and 200 per workspace each day**, reset at midnight Pacific. Past that, Conductor bills **\$0.20 per message**, but only if a workspace admin has turned on pay-as-you-go for Conductor. With it off, Conductor stops answering until the allowance resets. Your account also needs to be in good standing, so an expired trial or a failed payment blocks it either way.
Any simulations Conductor runs on your behalf bill as normal per-message simulation runs. Check Billing → Usage for the exact amount.
## Free allowances
* **\$10 in free credits** when you sign up.
* **20 free concurrent calls**.
* **Conductor**: 30 messages per user and 200 per workspace each day, reset at midnight Pacific.
* **AI QA**: the first 100 minutes are free, then \$0.10 per minute. See [AI QA](/ai-qa/overview).
Apart from Conductor's daily messages, testing draws on the same balance as production, so treat test usage as billable. One thing the signup credit doesn't cover: buying a [phone number](/deploy/purchase-number) requires a card on file, so [phone call testing](/test/test-phone) needs payment set up even if you still have credit left.
## See also
* [Retell pricing](https://www.retellai.com/pricing) for current per-model and per-minute rates.
* [Testing overview](/test/test-overview) compares the testing methods.
# Testing with Conductor
Source: https://docs.retellai.com/test/testing-with-conductor
Use Conductor, Retell's AI copilot, to generate simulation test cases from real calls, cold-start suites for new agents, and target tool calls.
[Conductor](/conductor/overview) is Retell's AI copilot in the dashboard. It can write [simulation test cases](/test/llm-simulation-testing) for you, so you build a regression suite from real calls and plain-language prompts instead of authoring every case from a blank slate.
Open the Conductor panel on the agent you want to test, then ask. It drafts the cases into that agent's Test Cases tab, where you can review, edit, and run them like any other test case.
Conductor can also work the suite, not just write it. Ask it to run a batch and it starts one, waits for the results, and reports the pass rate and each failure back in the panel. It can read a run's full transcript, update a case, and delete cases you no longer want.
Conductor authors test cases for single-prompt, multi-prompt, and Conversation Flow agents.
## Generate test cases from real calls
Point Conductor at calls that already happened and have it turn them into test cases. This is the fastest way to build a regression suite: every real failure becomes a case that guards against the same problem returning.
To point Conductor at a specific call, give it the call's **call ID** from your [Call History](/features/session-history) or [AI QA](/ai-qa/overview), or describe the calls you mean:
```text theme={"dark"}
Turn call_7a3d9f2b1c8e4056a9d1f3b6c2e8074a into a test case.
Make test cases from my last 10 calls where the caller hung up before booking.
```
Conductor reads the transcript and tool history from those calls and drafts matching test cases with a persona and success criteria. It can also set the dynamic variables and function mocks, so you don't have to add them by hand.
## Start a suite from scratch
On a brand-new agent with no call history, ask Conductor for a starter suite so you have coverage before the first real call:
```text theme={"dark"}
Write 5 test cases covering the happy path and common edge cases for booking an appointment.
```
Conductor drafts the cases scoped to the current agent. Review them, then add any [dynamic variables or mocks](/test/llm-simulation-testing#test-variables-and-mocks) the scenarios need.
## Target tool calls and functions
To test how your agent handles a specific tool result, ask Conductor to write a case that mocks the function and checks the agent's response:
```text theme={"dark"}
Add a test case where check_availability returns no open slots, and verify the agent offers a callback instead of ending the call.
```
Conductor writes the case with the [function mock](/test/llm-simulation-testing#custom-function-mocks) and a success criterion, so you can confirm the behavior without calling the real function.
A tool call that matches no mock falls through to the real tool. Conductor can read your agent's tool definitions, but it won't necessarily mock every one it should, so review each generated case and add a mock for any function that reaches a live endpoint before you run it.
## Next steps
* [Simulation testing](/test/llm-simulation-testing) covers editing and running the cases Conductor drafts.
* [Batch testing](/test/batch-test-simulation) runs the whole suite at once and reports the pass rate.
* [Test and improve with Conductor](/conductor/test-and-improve) covers reviewing real calls and running simulations from the Conductor panel.