FA©26
29 July 2026·37 min readRetell AIvoice agentsAPI migrationn8nGoHighLevel

Retell AI's 2026 API Deprecations: How to Migrate Live-Call State, Routing, and Agent Versions Without Breaking Voice Ops

Why did the voice agent keep talking after your backend changed the call?

Retell has announced that on August 31, 2026, Update Call becomes an ended-call operation. Ongoing calls must use Update Live Call for dynamic-variable overrides and live changes such as data_storage_setting. The existing override_dynamic_variables input on Update Call is being removed. Retell documents the replacement as PATCH /v2/update-live-call/{call_id}, with live overrides nested under fields_to_override. The current details are in Retell's Update Call deprecation notice.

That is not a simple endpoint rename. It separates two different states that many integrations treated as one.

An ongoing call is a live control surface. A backend may add context, override dynamic variables, change data-storage behavior, or trigger an agent response while a person is waiting. An ended call is a record. You may attach metadata, custom attributes, or post-call handling information after the conversation is over. Combining both in one wrapper hid the distinction.

The failure becomes uncomfortable when your automation platform reports success. Suppose a Retell agent qualifies a caller, invokes a custom function, and waits for a backend decision. The backend calculates eligibility and updates a dynamic variable named approved_offer. The workflow receives a 2xx response from a mocked or older path during testing. In production after the change, the live call update is rejected or no longer affects the ongoing conversation. The agent's prompt still reads the old variable. There may be no audio failure, no dropped call, and no obvious alert.

The caller receives the wrong behavior because the integration equated “call record updated” with “agent state changed now.”

I model call state explicitly:

type RetellCallState =
  | "registered"
  | "ringing"
  | "ongoing"
  | "ended";

type CallMutation =
  | {
      kind: "live_context_update";
      callId: string;
      variables: Record<string, string | number | boolean>;
      additionalContext?: string;
    }
  | {
      kind: "ended_call_annotation";
      callId: string;
      metadata?: Record<string, unknown>;
      customAttributes?: Record<string, string>;
    };

The Retell adapter chooses the API from the mutation kind. The workflow does not accept an arbitrary endpoint string. If a job marked live_context_update runs after the call has ended, the adapter returns a domain error and routes it to reconciliation. It does not quietly send the data to the ended-call path and pretend the caller received it.

[DIAGRAM] Draw the call lifecycle from call creation to ringing, ongoing, ended, and analyzed. Place allowed mutations and webhook events on the correct states. Highlight the boundary where Update Live Call replaces Update Call for ongoing calls.

The first lesson is narrow. Your code must use the new live endpoint. The larger lesson is more valuable: every Retell API call needs an explicit relationship to call state, agent version, and business timing.

What is the real cost of a Retell deprecation in production?

A broken CRUD integration creates bad data. A broken voice integration creates bad data while a human is listening.

The direct cost is a failed action during the call. The agent cannot check availability, book the appointment, transfer to the right destination, apply updated context, or record the caller's decision. The caller waits through silence, hears a generic recovery phrase, or receives a confident answer based on stale information.

The indirect cost is harder to see. A call can complete and still be operationally wrong. The transcript exists. The recording exists. Retell sends call_ended and later call_analyzed. Your CRM receives a summary. Nobody notices that the transfer used an outdated route, the lead source was missing, or the agent published in production was not the version QA approved.

Deprecations also create version drift. Retell's 2026 changes have touched model replacements, phone-number agent fields, conversation-flow tools, list endpoints, analysis prompt fields, agent publishing, language configuration, and live call updates. If one service uses the current SDK, another uses raw HTTP, and a third uses an old n8n workflow, they can each speak a different version of the platform contract.

The cost lands in five operating areas.

First, caller trust. Voice agents operate in a channel where hesitation and inconsistency are obvious. A caller may forgive a slow page. They do not easily forgive an agent that confirms one time, books another, then transfers to the wrong team.

Second, client trust. Agency owners sell outcomes such as answered calls, booked appointments, qualified leads, and reduced missed-call volume. When an API change breaks those outcomes, the client does not care that the prompt remained unchanged.

Third, compliance and data handling. Retell's live-call change specifically includes data_storage_setting. If your system changes storage behavior during a call, using the wrong endpoint can turn a data-governance decision into a post-call annotation that did not apply when intended.

Fourth, support time. Voice incidents require joining telephony state, Retell call logs, function-call payloads, n8n executions, CRM writes, webhook delivery, and sometimes carrier or SIP behavior. Without a shared correlation ID and state model, the team listens to recordings and guesses.

Fifth, hidden financial waste. Failed calls may still consume provider usage, downstream task runs, CRM automation, and human follow-up. I will not invent a universal cost per call because it depends on the provider, model, telephony route, workflow, and client. The number belongs in the client's own incident report.

A concrete example is an appointment agent. It checks slots through a custom function, stores selected_slot, confirms it verbally, creates the appointment in GoHighLevel, and adds a note after the call. If the live dynamic-variable update fails, the agent may repeat a stale slot while the backend books the current one. Every API call can succeed. The caller and calendar disagree.

// [WORKFLOW JSON: n8n call-handling workflow that records Retell call state, custom-function input, live-variable update result, HighLevel appointment result, and spoken confirmation value under one correlation ID. This proves the caller-facing value matches the backend write.]

[REAL NUMBER] Insert the measured number of affected calls, failed function invocations, incorrect transfers, booking mismatches, manual recoveries, and client credits for a real incident. Label any incomplete count as unverified.

The cost of getting the migration wrong is not the time needed to update an endpoint. It is the time needed to prove which callers experienced a different business outcome.

Which Retell changes from 2026 require action now?

Retell maintains a central deprecation overview. It is the first page I check because the changes are not limited to one API version and staying on an older SDK does not defer server-side removals. Retell states that SDKs drop retired endpoints or fields in the first generated release after the API removal, but the API change itself occurs on the listed date.

The most urgent upcoming change is August 31, 2026. PATCH /v2/update-call/{call_id} becomes ended-call-only. Ongoing updates move to PATCH /v2/update-live-call/{call_id}. Dynamic-variable overrides move under fields_to_override.override_dynamic_variables. The live endpoint can also override metadata and data_storage_setting, and it exposes live controls such as trigger_response and additional_context.

Several July changes are already in effect.

On July 31, the scalar "multi" language value was removed in favor of an explicit locale array. Existing agents were migrated, but client code that still sends language: "multi" is rejected. That matters for provisioning scripts and agent-cloning workflows, even when the existing dashboard agents appear fine.

Also on July 31, GET /list-agents and GET /list-chat-agents were removed. The replacement is POST /v2/list-agents with filters. Voice and chat results now come through one endpoint, results are read from items, pagination continues with pagination_key while has_more is true, and the old pagination_key_version is not used. Retell documents the exact mapping in the legacy agent list endpoint notice.

On July 20, the legacy publish endpoints were replaced. POST /publish-agent/{agent_id} and POST /publish-chat-agent/{agent_id} moved to POST /publish-agent-version/{agent_id}. See the agent publishing deprecation notice.

On June 15, legacy list endpoints across batch tests, conversation flows, phone numbers, Retell LLMs, test definitions, test runs, calls, and chats were removed in favor of versioned alternatives. Retell also removed legacy analysis prompt fields such as analysis_summary_prompt, analysis_successful_prompt, and analysis_user_sentiment_prompt, directing integrations to post_call_analysis_data or post_chat_analysis_data system presets.

On April 18, tools attached to type: "conversation" nodes through tools and tool_ids were deprecated in favor of the subagent node type. This can change conversation-flow structure, not merely a field name.

On March 31, single-agent fields on phone numbers were replaced by weighted agent-list fields. The affected fields included inbound, outbound, and SMS agent IDs and versions. Any provisioning code that writes one inbound_agent_id may now be out of contract even if an existing number was migrated automatically.

Model changes have also occurred. Retell replaced older OpenAI Realtime and Cartesia Sonic models in April, migrated Claude and Gemini models in May, and moved ElevenLabs Turbo voice models to Flash counterparts in July. Model migrations may preserve configuration validity while changing latency, voice behavior, or response characteristics enough to require regression calls.

I turn this into a change register, not a reading list.

Effective date

Capability

Old contract

Current contract

Production owner

Evidence

2026-08-31

Live call mutation

Update Call with live overrides

Update Live Call with fields_to_override

Voice backend

Contract test and recorded QA call

2026-07-31

Agent listing

Separate GET endpoints

Unified POST endpoint, filters, items, cursor

Admin portal

Pagination test

2026-07-31

Multilingual config

language: "multi"

Explicit locale array

Provisioning service

Create and update tests

2026-07-20

Agent publishing

Legacy publish endpoints

publish-agent-version

Release pipeline

Version promotion test

2026-06-15

Post-call analysis

Legacy prompt fields

Analysis data presets

Analytics pipeline

Webhook schema test

2026-03-31

Phone routing

Single agent fields

Weighted agent arrays

Telephony provisioning

Inbound and outbound test calls

The register should include “not used” decisions with evidence. Silence is not evidence.

Why does replacing the deprecated endpoint still break the system?

Because the contract around the endpoint changed too.

Take agent listing. Replacing GET /list-agents with POST /v2/list-agents is only the first step. The method changes. Voice and chat agents share one endpoint. Filtering moves into a request body. The response is no longer the top-level array old code may expect. Pagination uses items, has_more, and pagination_key. A portal that replaces the URL but still maps response.data.map(...) can show an empty list or only the first page.

Take publishing. Replacing the path can still leave a release-control bug. The old deployment script may treat “publish agent” as publishing the mutable current configuration. The new endpoint name emphasizes publishing an agent version. Your system needs to know which version was tested, which version is being promoted, and whether phone-number routing references a pinned version or current published behavior.

Take multilingual agents. Replacing "multi" with a locale array requires a product decision. Which locales should the agent support? Retell automatically migrated existing agents to an equivalent array, but your provisioning code needs an explicit set. Copying every possible locale into every agent can affect detection and behavior. Sending too few can reject callers the client expects to serve.

Take conversation flows. Replacing tools with a subagent node can change control flow, context boundaries, and tool ownership. A flat field migration may produce a valid configuration that reasons differently.

Take models. An automatic model replacement keeps the agent runnable. It does not prove your interruptions, tool-call timing, pronunciation, transfer timing, or latency remain acceptable. Voice regression testing is partly technical and partly perceptual.

The mechanism behind most failures is hidden coupling. A Retell response feeds an admin portal, which feeds a version selector, which feeds phone-number routing, which feeds an inbound webhook, which injects dynamic variables from GoHighLevel. The deprecated endpoint is one node in that chain.

I avoid exposing Retell's raw API across the whole application. I define capability interfaces:

type AgentChannel = "voice" | "chat";

type AgentSummary = {
  agentId: string;
  channel: AgentChannel;
  displayName: string;
  publishedVersion?: number;
};

interface VoicePlatform {
  listAgents(input: {
    channel?: AgentChannel;
    cursor?: string;
  }): Promise<{
    items: AgentSummary[];
    nextCursor?: string;
  }>;

  publishAgentVersion(input: {
    agentId: string;
    expectedDraftVersion: number;
  }): Promise<{
    agentId: string;
    publishedVersion: number;
  }>;

  updateLiveCall(input: {
    callId: string;
    variables?: Record<string, string | number | boolean>;
    additionalContext?: string;
  }): Promise<void>;
}

The Retell adapter owns request methods, paths, filters, response parsing, pagination, and version-specific fields. The admin portal sees AgentSummary. n8n sees a named internal API. When Retell changes a list response, one adapter changes.

There is a tradeoff. An adapter can hide useful provider capabilities if it becomes too generic. I do not build a fictional universal voice API. I model the capabilities the product actually uses and allow Retell-specific extension fields where necessary. The boundary protects contracts. It should not erase the provider.

A concrete example is a deployment page that lists all voice agents. The old code fetches GET /list-agents, assumes an array, and populates a dropdown. The migrated code must send a channel = voice filter, loop until has_more is false, normalize items, and handle an agent created between pages. If the list is used for a release decision, stale pagination is a correctness issue, not a UI detail.

// [WORKFLOW JSON: n8n agent-inventory workflow using POST /v2/list-agents, a voice-channel filter, `items`, `has_more`, and `pagination_key`, with deduplication by agent ID. This proves the migration handles the new method, response shape, and all pages.]

[WARNING] Do not call a Retell deprecation complete because the replacement request returns 200. Verify every consumer of the normalized result and every side effect triggered by the changed capability.

How do you build a deprecation inventory around capabilities, not URLs?

Search the codebase, workflow exports, and provider configuration for more than paths.

Start with literal endpoint and field searches. Look for /v2/update-call, /list-agents, /list-chat-agents, /publish-agent/, /publish-chat-agent/, analysis_summary_prompt, analysis_successful_prompt, analysis_user_sentiment_prompt, language: "multi", the phone-number single-agent fields, and conversation-node tools or tool_ids.

Then search SDK method names. Generated SDKs can hide the path. A service may call retell.call.update() rather than contain /v2/update-call. Record package versions for Node, Python, and any other runtime. Search lockfiles and container images, not only package.json.

Then search automation platforms. In n8n, inspect HTTP Request nodes, Code nodes, credential names, sub-workflows, and expressions that construct Retell URLs. In Make, inspect HTTP modules and custom apps. In GoHighLevel, inspect custom workflow actions, webhooks, and fields used to pass Retell IDs. In Supabase, inspect Edge Functions, database webhooks, secrets, and scheduled functions.

Then inspect the Retell dashboard and API-managed resources. Existing agents may have been automatically migrated while provisioning code still sends old fields. Existing phone numbers may work while the “clone client setup” workflow is broken. Existing model selections may have changed without a code diff in your repository.

I organize the inventory by capability:

Capability

Call-time or control-plane

Producer

Consumer

Retell resource

Migration risk

Create outbound call

Call-time

n8n

Retell

Call

Dynamic variables and metadata

Update live context

Call-time

Backend function

Retell agent

Ongoing call

Timing and call state

Receive call events

Call-time

Retell

Webhook inbox

Call

Signature, retries, ordering

List agents

Control-plane

Admin API

Next.js portal

Agent

Pagination and response shape

Publish version

Control-plane

Release job

Retell

Agent version

Promoting wrong draft

Configure phone routing

Control-plane

Provisioner

Retell number

Phone number

Weighted arrays and versions

Configure analysis

Control-plane

Provisioner

Post-call pipeline

Agent

Field replacement and schema

Configure languages

Control-plane

Provisioner

Retell agent

Agent

Explicit locale policy

The call-time versus control-plane distinction matters. Call-time operations need low latency, idempotency, live state, and graceful degradation. Control-plane operations need versioning, review, drift detection, and safe retries. Do not run agent publishing through the same generic queue and retry policy used for post-call CRM updates.

For every capability, record:

  1. The internal business operation.

  2. The exact Retell request or SDK method.

  3. The expected agent and call state.

  4. The tenant and agent ownership rule.

  5. The request schema and response schema.

  6. The retry policy.

  7. The idempotency key or deduplication rule.

  8. The downstream consumers.

  9. The current deprecation status.

  10. The proof required before release.

A concrete hidden dependency is a Supabase table that stores agent_id but not agent_version. The phone number points to a versioned agent configuration. The admin portal publishes a new version and updates Retell, but the local database still describes the deployment as an unversioned agent. During an incident, nobody can prove which prompt and tools handled the call.

Add deployment records:

create table voice_agent_deployments (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null,
  retell_agent_id text not null,
  channel text not null,
  draft_version integer,
  published_version integer,
  phone_number_id text,
  configuration_hash text not null,
  status text not null,
  approved_by text,
  approved_at timestamptz,
  published_at timestamptz,
  unique (tenant_id, retell_agent_id, published_version)
);
// [WORKFLOW JSON: deprecation scanner that searches exported n8n workflows for Retell endpoints, old fields, SDK wrapper routes, and agent IDs, then joins them to a capability registry in Postgres. This proves hidden automation dependencies are included.]

[REAL NUMBER] Insert the exact count of Retell agents, phone numbers, active workflows, custom functions, webhook receivers, SDK versions, and deprecated references found. Do not infer counts from one dashboard page if pagination is incomplete.

The inventory is useful after this migration too. Retell is moving quickly. Capability ownership lets you answer the next notice without starting from zero.

How should live-call updates work after August 31, 2026?

Treat a live-call update as a time-sensitive command with a deadline, not as a normal background job.

The command needs the Retell call ID, tenant, expected call state, intended fields, reason, source event, and expiration time. A pricing update that arrives after the call ends should not be replayed into a future call or silently converted into metadata.

type LiveCallUpdateCommand = {
  commandId: string;
  tenantId: string;
  callId: string;
  agentId: string;
  sourceEventId: string;
  expiresAt: string;
  fieldsToOverride: {
    overrideDynamicVariables?: Record<
      string,
      string | number | boolean
    >;
    metadata?: Record<string, unknown>;
    dataStorageSetting?: string;
  };
  callControl?: {
    triggerResponse?: boolean;
    additionalContext?: string;
  };
};

Before sending the command, resolve the call from your local state or Retell if needed. Confirm that the call belongs to the tenant and agent expected by the workflow. Confirm it is ongoing. Confirm the command has not expired. Then send it through Update Live Call.

Do not make the voice agent wait on n8n for a chain of slow SaaS calls when the decision can live in a small backend. n8n is excellent for orchestration, post-call processing, and visible business workflows. Live call control benefits from a low-latency API with bounded work. I often use a Next.js route handler, Node service, or serverless function backed by Postgres or Supabase, with n8n consuming the durable result afterward.

A common pattern is:

Retell custom function
  -> low-latency backend
  -> validate tenant and call
  -> read or compute decision
  -> persist decision command
  -> update live call when required
  -> return bounded function result
  -> publish event for n8n and CRM sync

The function response and live-call update are related but not identical. A custom function may return data directly to the agent. Update Live Call changes fields or control outside that function's immediate return. Decide which channel owns the next utterance. Sending both without a clear rule can cause the agent to respond twice or combine stale and fresh context.

Retell's live update supports additional_context and trigger_response. These are powerful and easy to misuse. Additional context should be concise, scoped to the current call, and safe if repeated. Triggering a response should be deliberate. A retry of the same command must not cause the agent to repeat a sensitive statement.

Use an idempotency table even if Retell does not expose a provider idempotency key for the exact operation. Your system can guarantee that one source event produces one intended live update attempt and can distinguish safe transport retries from repeated business commands.

create table live_call_commands (
  command_id uuid primary key,
  tenant_id uuid not null,
  retell_call_id text not null,
  source_event_id text not null,
  command_type text not null,
  payload_hash text not null,
  expires_at timestamptz not null,
  status text not null,
  attempt_count integer not null default 0,
  last_http_status integer,
  last_error_code text,
  completed_at timestamptz,
  unique (tenant_id, retell_call_id, source_event_id, command_type)
);

Handle race conditions explicitly. The call may end between your state check and the Retell request. Classify that as call_ended_before_update, not a generic retryable failure. Decide whether the data should become ended-call metadata, be discarded, or trigger human review. That is a business rule.

A concrete example is a compliance opt-out. The caller says not to store the recording. Your extraction or function layer identifies the request. The live update changes data_storage_setting. This command should have a very short deadline, high priority, and explicit verification. Replaying it after the call ended does not repair what happened during the call.

// [WORKFLOW JSON: n8n post-processing workflow for live-call commands that receives the durable command result, updates GoHighLevel and Supabase, and routes expired or ended-before-update cases to review. The low-latency Retell call is performed by code, not a long visual chain.]

The core rule is timing. A correct value delivered after the decision point is still wrong.

How do you migrate agent listing, publishing, and version control?

Control-plane operations need a release process.

Agent listing now uses one POST endpoint for voice and chat. Build the request with an explicit channel filter when the caller expects one type. Normalize the items array. Continue with pagination_key while has_more is true. Deduplicate by agent ID because control-plane data can change while a long list is being paged.

type RetellListAgentsPage = {
  items: Array<{
    agent_id: string;
    channel?: string;
    agent_name?: string;
  }>;
  has_more: boolean;
  pagination_key?: string;
};

async function listAllVoiceAgents(client: RetellClient) {
  const byId = new Map<string, RetellListAgentsPage["items"][number]>();
  let paginationKey: string | undefined;

  do {
    const page = await client.listAgents({
      filter_criteria: {
        channel: {
          type: "string",
          op: "eq",
          value: "voice"
        }
      },
      pagination_key: paginationKey
    });

    for (const agent of page.items) {
      byId.set(agent.agent_id, agent);
    }

    paginationKey = page.has_more ? page.pagination_key : undefined;
  } while (paginationKey);

  return [...byId.values()];
}

The exact SDK method and request wrapper depend on the SDK version. The contract shown here is the important part: POST, filter, items, cursor, and has_more.

Publishing should be tied to an immutable configuration hash. Before publishing, fetch or construct the candidate agent configuration, normalize fields that do not affect behavior, hash it, and store the hash with the draft version. QA approves that exact hash. The release job checks that the current draft still matches before calling publish-agent-version.

Without this check, a prompt editor can change the agent after QA and before publication. The release pipeline then publishes “the agent” rather than the version people tested.

async function publishApprovedAgent(input: {
  tenantId: string;
  agentId: string;
  approvedConfigurationHash: string;
  expectedDraftVersion: number;
}) {
  const current = await retellAdapter.getAgent(input.agentId);
  const currentHash = hashNormalizedAgent(current);

  if (currentHash !== input.approvedConfigurationHash) {
    throw new Error("RETELL_AGENT_CHANGED_AFTER_APPROVAL");
  }

  return retellAdapter.publishAgentVersion({
    agentId: input.agentId,
    expectedDraftVersion: input.expectedDraftVersion
  });
}

Phone routing must then reference the intended published version according to the current Retell contract. Do not assume publishing automatically updates every number in the way your product expects. Fetch the phone-number configuration after publication and reconcile agent IDs, versions, weights, and direction.

The admin portal should display three separate facts: current draft, approved candidate, and deployed version. A single “Published” badge is not enough for incident analysis.

A concrete example is an agency with an inbound receptionist and an outbound follow-up agent. The old list endpoint populates one combined dropdown through separate calls. After migration, the portal sends no channel filter, and a chat agent with a similar name appears. An operator publishes or assigns the wrong resource. Type-safe channel filtering and IDs, not display names, prevent that class of error.

[DIAGRAM] Draw the release path from editable draft to normalized configuration hash, QA test call, approval, publish-agent-version, phone-number reconciliation, canary call, and deployment record.

[WARNING] Never select an agent for publishing or phone routing by display name alone. Names are editable and not tenant-safe identifiers.

How do you keep phone routing and multilingual agents from drifting?

Treat phone-number configuration as desired state.

Retell's March 2026 change replaced single-agent phone fields with weighted agent lists. Even when you want one agent, write the current list form with one weighted entry according to the documented schema. This makes the configuration compatible with routing expansion and removes reliance on retired fields.

Store the intended routing in your database or configuration repository. Include phone number ID, direction, channel, agent ID, agent version, weight, fallback policy, and tenant. Periodically fetch the Retell configuration and compare it with desired state.

Do not auto-correct every drift immediately. Some changes may be emergency edits made in the Retell dashboard. Detect, alert, and require an explicit reconciliation action. Blindly overwriting a human's incident fix is another failure mode.

Multilingual configuration also needs desired state. The old scalar "multi" did not force your application to name supported locales. The explicit array does. That is good because the product decision becomes visible.

Define locales per tenant and agent role. An inbound receptionist may support English and Spanish. A specialized scheduling agent may support English only because its knowledge base, pronunciation rules, and function responses are not validated in other languages. Do not copy a global array into every agent.

Test locale behavior at the boundaries. A caller may switch language after the greeting. A dynamic variable may contain a language preference from HighLevel. An inbound webhook may select an agent based on the caller's number or CRM record. Retell also supports inbound webhooks that can choose an agent and inject per-call context. The current behavior is documented in Retell's inbound call webhook guide.

A concrete routing flow is:

  1. Retell receives an inbound call on a shared number.

  2. Retell calls your inbound webhook.

  3. Your backend validates the number and tenant.

  4. It looks up the caller in HighLevel using normalized phone identity.

  5. It reads language preference and account state.

  6. It selects an allowed agent and published version from desired state.

  7. It supplies dynamic variables with defaults.

  8. It records the routing decision with a correlation ID.

  9. It returns the Retell response within the required operational window.

Do not put a long n8n workflow directly in this synchronous path unless it is tightly bounded. A HighLevel search, Supabase query, several routers, and a slow enrichment API can make inbound routing depend on the slowest SaaS. Use a small backend to perform the critical lookup and let n8n handle enrichment or post-call work.

create table retell_phone_routes (
  tenant_id uuid not null,
  phone_number_id text not null,
  direction text not null,
  agent_id text not null,
  agent_version integer not null,
  weight integer not null,
  locales text[] not null,
  enabled boolean not null default true,
  configuration_hash text not null,
  updated_at timestamptz not null default now(),
  primary key (tenant_id, phone_number_id, direction, agent_id, agent_version)
);
// [WORKFLOW JSON: drift-detection workflow that fetches Retell phone-number routing, compares weighted agent arrays and locale arrays with desired state in Postgres, and opens a review item instead of overwriting drift automatically. This proves provisioning stays current after dashboard edits.]

The migration is complete when new provisioning, cloned accounts, updates, and drift checks all use the current fields. Existing numbers merely continuing to ring is not proof.

How do you make Retell webhooks retry-safe and tenant-safe?

Retell's webhook events are part of your source of truth for call operations. They are not a command to run a long workflow inline.

Retell documents call events such as call_started, call_ended, and call_analyzed. The platform also documents signed webhook verification using the raw request body, the X-Retell-Signature header, your API key, and HMAC-SHA256. The current implementation guidance is in Retell's secure webhook documentation.

Read the raw body before JSON parsing. Verify the signature. Enforce an allowed timestamp window according to the current SDK or your security policy. Then parse the event, resolve tenant ownership from trusted mappings, and insert it into a durable inbox.

Do not derive tenant solely from arbitrary metadata in the payload. Map the Retell agent ID, phone-number ID, or account integration to your tenant record. If the event references a call or agent you do not own, quarantine it.

create table retell_webhook_inbox (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null,
  event_type text not null,
  retell_call_id text not null,
  payload_ciphertext text not null,
  payload_hash text not null,
  signature_verified boolean not null,
  provider_timestamp timestamptz,
  received_at timestamptz not null default now(),
  processing_status text not null default 'pending',
  attempt_count integer not null default 0,
  completed_at timestamptz,
  unique (tenant_id, event_type, retell_call_id, payload_hash)
);

The exact unique key depends on Retell's event identity fields and your observed delivery behavior. Do not invent a provider event ID if the payload does not supply one. A payload hash with call ID and event type can be a fallback, but document collision and mutation assumptions.

Return quickly after durable acceptance. Acknowledge only after the event is stored. Then let n8n or a worker process the inbox asynchronously.

Process call events as state observations, not a guaranteed ordered log. call_analyzed logically follows the call ending, but network delivery and retries can still make local processing order differ. Your handler should upsert state and check prerequisites. If analysis arrives before the local ended-call record, store it and reconcile rather than failing permanently.

Separate post-call side effects. Updating GoHighLevel, sending Slack, storing a transcript, creating a follow-up task, and scoring the call should not be one all-or-nothing transaction. Record each action with its own idempotency key and status. A failed Slack notification should not cause the CRM note to be inserted twice on replay.

A concrete pattern is a call-ended workflow:

Retell webhook receiver
  -> verify signature
  -> resolve tenant
  -> insert inbox event
  -> acknowledge
  -> n8n polls or receives internal command
  -> upsert call state
  -> fan out named side-effect commands
       -> HighLevel note
       -> opportunity update
       -> transcript storage
       -> QA sampling
       -> client notification

Each side effect uses tenant + call_id + action_type as an internal idempotency boundary.

// [WORKFLOW JSON: Retell webhook processing workflow that consumes a durable inbox row, upserts call state, fans out idempotent HighLevel, Supabase, and notification commands, and marks each side effect independently. This proves a retry cannot duplicate completed actions.]

[WARNING] Do not log full recordings, transcripts, dynamic variables, or raw webhook bodies into general application logs. Store only what the client has authorized, in the intended system, with access controls and retention rules.

What does a real voice-agent migration test plan include?

A real test plan includes calls.

Schema tests are necessary. They are not enough because voice behavior emerges from models, prompts, tools, timing, telephony, and caller speech.

Start with control-plane contract tests. Create or update an agent using current fields. List it through POST /v2/list-agents, including pagination. Publish the approved version through publish-agent-version. Fetch the phone number and verify weighted agent routing. Create or update multilingual agents with explicit locale arrays. Verify post-call analysis configuration uses current fields.

Then test live-call control. Start a synthetic call. Update dynamic variables through Update Live Call while the call is ongoing. Verify the agent uses the new value at the intended turn. Test additional_context without trigger_response, then with an intentional response trigger. End the call and verify the live endpoint rejects or classifies an ended-call update according to the current API behavior. Use Update Call for ended-call annotations and verify they do not affect an ongoing call.

Test custom functions. The agent should call the function with valid arguments, missing arguments, ambiguous caller input, and repeated requests. The backend should enforce schema and tenant. The function result should be bounded and safe to repeat. Simulate a timeout before processing and a timeout after the backend completed the side effect.

Test transfers. Warm and cold transfer behavior, caller ID display, destination availability, failure fallback, voicemail, and after-hours routing all need actual calls. Retell changed cold-transfer semantics earlier in 2026 by separating cold_transfer_mode from show_transferee_as_caller. Any flow using SIP behavior deserves a focused regression.

Test languages. Call in each supported locale. Test a switch mid-call. Verify dynamic variables, dates, prices, addresses, and confirmation phrases. Confirm custom-function responses do not force the agent back into the wrong language.

Test model changes. Use a stable call script and evaluate interruption handling, tool-call timing, pronunciation, silence, latency, transfer timing, and completion. Do not reduce evaluation to transcript text. The transcript can match while turn-taking feels broken.

Test webhooks. Verify signatures using raw bodies. Send duplicates. Delay call_ended. Process call_analyzed first in your replay test rig. Replay events after the CRM side effect succeeds but before the inbox row is marked complete. Confirm no duplicate note, task, or opportunity is created.

Test the admin and release path. Change an agent after QA approval and confirm publication is blocked by the configuration-hash check. List enough test agents to exercise pagination. Include both voice and chat agents and verify filtering.

Test client-specific configuration. A generic Retell test agent cannot prove a GoHighLevel location has the right calendar, pipeline, user IDs, custom fields, or timezone. Use a controlled test tenant that mirrors the production shape without real customer data.

A useful call-case table looks like this:

Case

What changes

Expected proof

Live pricing update

Dynamic variable during ongoing call

Transcript and recording show new value, command completed before decision turn

Ended-call annotation

Metadata after hangup

Call record updated, no live behavior expected

Duplicate function request

Same source event twice

One external side effect, repeat-safe response

Transfer destination unavailable

Tool or transfer failure

Approved fallback phrase and route

Locale switch

Caller changes language

Agent continues in supported locale, tools preserve values

Delayed webhook

call_analyzed processed before local end state

Event stored and reconciled without duplicate actions

Agent changed after QA

Draft hash differs

Publish blocked

Pagination

More than one result page

Every agent present once

[REAL NUMBER] Insert the number of call cases executed, supported locales tested, transfer paths tested, webhook replay cases, contract assertions, and unresolved behavioral differences. Do not convert subjective voice quality into a fake precision score.

What breaks under concurrent calls, delayed events, and partial outages?

Concurrency turns hidden assumptions into repeated failures.

A multi-tenant voice system may handle several calls for one agency and many agencies through shared infrastructure. Every command must carry tenant, call ID, agent ID, and correlation ID. Global variables in n8n, shared static fields, or “latest active call” lookups are unacceptable. The wrong call can receive the right update.

Live-call commands need per-call ordering. A caller changes an appointment time, then changes it again. Two backend jobs race. The older result arrives after the newer one and overwrites the variable. Add a monotonic sequence or expected state version to your command record. Reject stale updates locally before calling Retell.

alter table live_call_commands
add column sequence_number bigint not null,
add column superseded_by uuid;

create unique index live_call_sequence_unique
on live_call_commands (tenant_id, retell_call_id, sequence_number);

Custom functions need timeout budgets. A function that calls HighLevel, a scheduling API, a payment service, and an LLM in series has too many independent failure points for a live conversation. Precompute what you can. Cache read-only configuration with clear expiry. Return a controlled “cannot confirm now” result instead of leaving the agent waiting indefinitely.

Partial outages require capability-specific degradation. If GoHighLevel is unavailable, the agent may still collect information and promise a human follow-up, but it should not claim an appointment was booked. If your analytics store is unavailable, the call can continue and the event can be buffered. If Retell's live update fails, the agent may need a function response that contains the critical value directly.

Delayed webhooks create state gaps. The call ends, but your system still sees it as ongoing. A late pricing command attempts Update Live Call and fails. The error handler should query or reconcile call state once, classify the command, and stop. Retrying until a generic maximum wastes time and obscures the real issue.

Outages during token or key rotation are another risk. Retell signature verification uses the account API key in the documented flow. If you rotate credentials, your receiver may need a controlled overlap or synchronized deployment so valid webhooks are not rejected. Follow Retell's current key-management behavior rather than assuming overlap exists. Where the platform does not document an overlap, treat it as unverified and plan a coordinated change.

A concrete load failure is an outbound campaign. n8n creates calls, writes initial state, and schedules follow-up jobs. A burst causes Postgres connection pressure. Some create-call requests succeed at Retell, but local inserts time out. The retry creates additional calls because there is no durable command ID tied to the campaign recipient. The fix is to persist the call command before the provider request and reconcile uncertain outcomes.

create table outbound_call_commands (
  id uuid primary key,
  tenant_id uuid not null,
  campaign_id uuid not null,
  recipient_key text not null,
  agent_id text not null,
  agent_version integer not null,
  status text not null,
  retell_call_id text,
  attempt_count integer not null default 0,
  last_error text,
  created_at timestamptz not null default now(),
  unique (tenant_id, campaign_id, recipient_key)
);

[DIAGRAM] Draw concurrent calls through a shared backend. Show per-call command sequencing, per-tenant configuration, a durable outbound command table, webhook inbox, and separate degradation paths for CRM, analytics, and live Retell control.

[WARNING] A provider timeout after a create-call request is an uncertain outcome, not proof that no call was created. Reconcile before retrying a caller-facing action.

What does the safer Retell architecture cost you?

It costs a backend.

A basic Retell demo can connect an agent, a prompt, and a webhook. Production voice operations need tenant resolution, agent-version records, call-state storage, live-command deadlines, function schemas, idempotency, signature verification, event inboxes, side-effect tracking, and deployment controls.

You need Postgres or another durable store. Supabase is a good fit when the team already uses it, but the schema still needs constraints and transaction boundaries. A table without unique keys is not idempotency.

You need code in the synchronous path. I would avoid making n8n the only live-call backend when the workflow includes several slow providers and stateful retries. Use code for signature verification, tenant resolution, low-latency functions, live-call updates, and atomic state. Use n8n for visible orchestration, delayed work, CRM sync, QA routing, reporting, and operations where human review matters.

You need a release process for agents. Prompts and tool configurations become deployable artifacts. Somebody must approve versions, run call cases, publish the exact candidate, and reconcile phone routing. That feels heavier than editing the dashboard. It is lighter than explaining which untracked prompt handled a disputed call.

You need ongoing deprecation work. Retell's 2026 change list shows the platform is evolving across APIs and models. Maintenance is part of owning the integration. A one-time build price that assumes the API will remain fixed is not honest engineering.

The safer design may add latency if every call goes through your backend. Keep the synchronous path small. Do not put analytics, transcript storage, Slack, and CRM enrichment before the function response. Persist the critical command, perform the minimal decision, return, and fan out later.

There are cases where the simpler architecture is right. A single internal agent with no custom functions, one phone number, no live updates, and a straightforward post-call webhook may not need a full control plane. It still needs signature verification, idempotent post-call processing, version notes, and a deprecation owner.

I would avoid a generic “AI voice agent platform” abstraction that tries to make Retell, another voice provider, and raw telephony identical. Their live controls, event models, tools, versioning, and transfer semantics differ. Build a thin internal boundary around your product capabilities and let the Retell adapter remain Retell-aware.

Responsibility

Where I put it

Low-latency custom function

Node or Next.js backend

Live-call update

Retell adapter in code

Call and command state

Postgres or Supabase

Webhook verification and inbox

Backend endpoint

CRM and operations workflow

n8n

Agent release approval

Internal admin plus database

Phone-route drift check

Scheduled code or n8n with reviewed changes

Transcript and QA workflow

Asynchronous pipeline

[REAL NUMBER] Insert actual monthly infrastructure, provider, support, and QA costs after measurement. Separate Retell and telephony usage from your own backend and automation-platform costs.

The cost buys control over the moments when a caller is waiting and the platform contract is moving.

What would I do differently before the next Retell change?

I would subscribe to the deprecation feed and turn notices into code-search tickets automatically.

Retell's overview exposes an RSS feed. A maintenance workflow can create an issue with the effective date, affected endpoints, repository search terms, workflow search terms, owners, and required proof. It should not auto-close when a developer changes a URL. Closure requires contract tests and a staged call.

I would store agent configuration snapshots outside the dashboard. Every approved and published version should have a normalized JSON artifact, configuration hash, approval record, and test evidence. The dashboard remains the editing surface. Your database becomes the deployment ledger.

I would standardize call correlation from the first request. The same correlation ID should appear in the create-call command, Retell metadata, custom-function call, live-update command, webhook inbox, n8n execution, HighLevel record, and support view. A call ID alone is provider-specific and may not exist before creation.

I would split live and post-call code earlier. Live paths have deadlines and should fail closed around claims such as booking or payment. Post-call paths can retry, batch, and wait for human review. Mixing them leads to either a slow caller experience or weak post-call durability.

I would test provisioning, not only existing agents. Automatic provider migration can keep old resources working while create and update APIs reject old fields. Every client onboarding or clone workflow should be part of the regression suite.

I would keep a “current Retell contract” package in the codebase. It would include schemas for list responses, call commands, webhook events, agent configuration, and internal normalized types. n8n workflows would call the package through a backend API rather than duplicating raw payload logic.

I would add drift checks for agent versions, phone routes, locales, custom functions, and analysis configuration. Drift should open a review, not auto-correct by default. The review should show the current Retell state, desired state, and who changed the last approved configuration.

I would create failure phrases as product artifacts. When booking is uncertain, CRM is down, a transfer fails, or a function times out, the agent needs an approved response that does not claim success. Technical degradation and conversation design meet there.

A concrete improvement is an uncertain booking state. The function sends a create-appointment request and times out. It cannot tell whether the booking exists. Instead of retrying immediately or telling the caller it failed, the backend returns a controlled status such as booking_verification_required. The agent says the team will confirm, while a reconciliation workflow checks the calendar and follows up.

// [WORKFLOW JSON: uncertain-outcome reconciliation workflow that receives a timed-out booking command, queries the target calendar using a stable external reference, resolves created versus not-created, and sends exactly one follow-up. This proves voice recovery does not rely on blind retries.]

The most important change is cultural. A voice agent is not a prompt connected to a phone number. It is a versioned production service with a human in the latency budget.

What is still hard after every call completes?

The hardest part is proving what the caller experienced at the exact moment a backend decision changed.

A transcript is not a perfect record of timing. A webhook is not a perfect record of conversational meaning. A successful function call does not prove the agent used the returned value in the next turn. A published version does not prove the phone number routed that caller to it. A completed CRM write does not prove the agent's confirmation matched the record.

This closes the loop from the opening. The system updated a value. The caller still heard the old one. The missing proof was the relationship between command time, call state, agent version, transcript turn, and external side effect.

For critical calls, I want an evidence chain:

business event
  -> durable command
  -> tenant and call validation
  -> Retell request and response
  -> agent version and phone route
  -> transcript turn or call marker
  -> external side effect
  -> webhook state
  -> reconciliation result

You can automate much of that chain. You cannot fully automate the judgment that a voice interaction remained correct, clear, and honest under delay or failure. That still needs targeted listening and scenario review.

I will look at a Retell, n8n, GoHighLevel, or Supabase call path and point out where live state and post-call state are being confused.

Most migrations stop when the deprecated request disappears from the codebase. The hard part is proving no caller heard the gap it left behind.