Telephony API Migration (trunks & caller IDs)

View as Markdown

The model change in one paragraph

Previously, one attachment did two jobs. A number attached to an agent decided both who answers it and what the agent dials from. These are now separate. Who answers a number is durable configuration. Attach a number or inbound trunk to the agent that answers it. What a call dials from is a property of the call. The request, campaign, or transfer names its caller ID at call time. The agent no longer owns outbound numbers. A temporary caller-ID bridge (/agent/{agentId}/caller-ids) lets calls without an explicit fromNumber dial from attached defaults, but it is deprecated at introduction and sunsets with the window; treat fromNumber as the real surface.

The new surface

TaskEndpoint
Bring your own trunk (receive)POST /sip-trunk/inbound
Bring your own trunk (dial out)POST /sip-trunk/outbound
Make an agent answer a number or trunkPOST /agent/{agentId}/answers
Stop it answeringDELETE /agent/{agentId}/answers/{sourceId}
Pick a call’s caller IDfromNumber on POST /conversation/outbound
Pick a campaign’s caller IDsfromNumbers on POST /campaign
Give an agent default caller IDs (deprecated bridge, sunsets with the window)POST /agent/{agentId}/caller-ids
Set the number transfers dial fromPUT /agent/{agentId}/transfer-source

Each inbound trunk carries its own numbers, credentials, media-encryption policy, Krisp toggle, and ringing timeout, and can restrict which peer addresses may send it calls.

What still works during the migration window

The old API keeps working, translated to the new model underneath, for 45 days after the rework’s release. After that, everything in the deprecated list is removed in one change. The concrete sunset date is published at release on the Deprecation Notices page and in the changelog. Responses on deprecated paths carry a Deprecation: true header, so the affected code paths in your integration are discoverable from your own logs.

You are still doing thisStatus
POST /product/import-phone-numberWorks. Creates an inbound and an outbound trunk, returns the same product id shape
telephonyProductId on agent update (PATCH)Works. Applied as the agent’s answer sources (replace semantics; ids you do not own are refused)
telephonyProductId on agent create (POST)Silently ignored: the field is stripped, no bindings are written, and the response is a plain 200 with no Deprecation header. Attach numbers with POST /agent/{agentId}/answers after creating
allowInboundCall: false on an agentWorks. Honoured as a kill switch at call routing
allowInboundCall: trueUndoes a previous false; otherwise no effect
fromProductId on POST /conversation/outboundWorks. Legacy alias, resolved to its number
Omitting the from-number on an outbound callWorks via the caller-ID bridge during the window (first attached wins; the response carries Deprecation: true); otherwise 400
phoneNumberIds on POST /campaignWorks. Resolved to numbers at creation (numbers are no longer reassigned between agents)
Reading agentId on a numberWorks. Now means only “the agent that answers this number”
Reading customProducts on GET /product/all-numbersWorks. Derived live from your trunks

What to change in your integration

  1. Creating trunks: replace one import-phone-number call with the two sip-trunk calls (one per direction, and skip the direction you do not need).
  2. Attaching to an agent: replace telephonyProductId with POST /agent/{agentId}/answers ({ "sourceKind": "phoneNumber" | "sipTrunk", "sourceId": "..." }), one call per source. An agent can answer on any number of numbers and trunks; a source is answered by exactly one agent, and attaching a taken one returns 409 identifying the current agent by id.
  3. Outbound calls: send fromNumber (E.164, exact match against numbers you own; no normalization is applied). During the window, calls without it dial from the agent’s attached default caller IDs (POST /agent/{agentId}/caller-ids, first attached wins), and such calls respond with Deprecation: true; after the window fromNumber is required. There is no fallback to a platform number.
  4. Campaigns: send fromNumbers. Several numbers rotate; retries reuse the number the recipient already saw; the list freezes at creation. It is effectively required already, since a campaign that resolves to zero numbers is refused.
  5. Transfers: if an agent transfers calls, set its transfer caller ID once via PUT /agent/{agentId}/transfer-source. Migrated setups get this backfilled automatically. When unset, transfers on outbound calls use the call’s own line; transfers on inbound calls are refused.

Behavior changes worth knowing

  • Inbound phone numbers are unique across the platform; outbound caller IDs are unique within your organization, and one outbound trunk exists per carrier address.
  • An outbound trunk’s address and transport are immutable. Changing carriers means delete and recreate.
  • cpsLimit (1-50, default 1) paces outbound calls per carrier address, shared by every trunk on that address.
  • Deleting or releasing telephony resources is guarded, instead of silently breaking things. Deleting a trunk in use returns 409 naming what uses it; attaching a source another agent answers returns 409 identifying that agent by id; releasing a number in use is refused with 400 (“You cannot release this number because an agent answers on it or dials from it.”).
  • GET /agent/{agentId} keeps phoneNumber as string[], now meaning what the agent answers on. An agent answering on an extension-only trunk returns [] and is still reachable.
  • Default caller IDs (/agent/{agentId}/caller-ids) are the one shared attachment: many agents may present the same number. Answering sources stay exclusive. A trunk-backed caller ID must name which of the trunk’s numbers it dials from. The whole surface is a deprecated bridge: its responses carry Deprecation: true and it is removed with the window.

After the window: what gets removed

Everything below exists only to keep old integrations running and is deleted together when the window closes.

  • Endpoint: POST /product/import-phone-number (returns 404 after removal).
  • Request fields: telephonyProductId and allowInboundCall on agent writes, fromProductId on outbound calls, phoneNumberIds on campaigns. After removal they are ignored or refused; fromNumbers becomes required on campaign creation and fromNumber on outbound calls.
  • Endpoints: the caller-ID bridge (/agent/{agentId}/caller-ids routes and the no-fromNumber fallback) is removed with the window.
  • Response fields: agentId on number reads (use the agent object), customProducts on GET /product/all-numbers (use the /sip-trunk lists).
  • Internals: the productType: "custom" pointer records.

Campaigns are the one change that forces API integrators to ship code: after the window, fromNumbers is required at creation. If your release cycle is slow, start there.

Dashboard equivalents

SIP trunks moved from the Add Number dialog’s “Import SIP” tab to a dedicated SIP Trunks page (sidebar, under Deploy, next to Phone Numbers). An agent’s answering sources and transfer number live on the agent’s Telephony tab. Campaign creation has a Call from selector. Test calls have a Call from picker with a shared platform test number as the labelled default.