Skip to main content

Call tracking & dynamic number insertion

Call tracking answers the question a click-level analytics tool cannot: which marketing source made the phone ring. DNI (dynamic number insertion) is the mechanism: your site shows a different inbound number to visitors from different campaigns, so every inbound call lands on a DID that maps back to exactly one source. On Orbit the loop has three parts — a tracking pool (a named container that binds a set of tracking DIDs to a source, campaign, and match rules), a resolve endpoint the website snippet calls to pick the right DID for the current visitor, and the call attribution report that rolls inbound calls on tracking DIDs up by source and campaign. This complements the channel-level Agent ROI attribution guide, which prices an agent’s resolved outcomes against its model-call cost. Call tracking is the offline→online attribution join for the inbound voice channel: nothing is written to the call ledger, and the source mapping is resolved at read time. Tracking DIDs are inbound-only. They terminate inbound calls; outbound (MT) voice and SMS never route through them. Reads need the numbers:read scope; creating pools and assigning DIDs need numbers:write. Pool config is stored in organization settings (JSONB), so it survives without any data migration and stays isolated to your organization.

1. When to use it

Use a tracking pool when paid or organic traffic drives inbound calls and you need per-source attribution on the phone channel: Google Ads PPC clicks that end in a call, a partner or affiliate link that dials, an email campaign with a click-to-call button. Skip it for your one permanent main line — DNI only helps when different visitors should see different numbers.

2. Create a tracking pool

Create one pool per source you want to attribute. The pool carries a label, the source it attributes calls to, an optional campaign and medium, and match rules that decide which visitors see its numbers.
A pool with no match rules matches every visitor — a catch-all fallback for any traffic not claimed by a more specific pool. When several pools match, the resolver picks the one with the most defined match conditions; ties break deterministically by creation time and id. Match rules available per pool: Update a pool later with PATCH, and list or inspect with GET:

3. The DNI snippet contract

The GET /numbers/tracking-pools/resolve endpoint is what your site’s swap snippet calls. Pass the visitor’s UTM parameters, referrer, and a stable session key (any cookie/session id you already have); it returns the DID to display, the pool that produced it, and the attributed source and campaign. When nothing matches it returns number: null and the page keeps its default number.
Response:
The query parameters — all optional, snake_case: utm_source, utm_medium, utm_campaign, utm_term, referrer, gclid, session. Before — a static site number:
After — swap by referrer/UTM/session:
Proxy /orbit/dni-resolve from your own backend to GET /numbers/tracking-pools/resolve with your API key, or use a publishable flow that adds the key server-side — an API key must never reach the browser. On failure or no-match, the snippet leaves the default number untouched.

4. Assign tracking DIDs to the pool

A pool with no DIDs never swaps. Assign owned DIDs with POST /numbers/tracking-pools/:id/assign:
The assignment rules:
  • The number must be in E.164 (+14155550142) — anything else returns 422.
  • The number must belong to your organization; an unknown or foreign DID returns 404 so you cannot enumerate another org’s numbers.
  • A DID belongs to at most one pool. Assigning it to a new pool moves it out of the old pool atomically, so one number always maps to exactly one source.
  • Remove a DID with POST /numbers/tracking-pools/:id/unassign and the same body shape.
  • Buy the DIDs first with the normal purchase flow — see Buy and provision numbers. Toll-free and local voice-capable DIDs both work as tracking numbers.

5. Read attribution

Inbound calls to tracking DIDs show up on GET /insights/call-attribution, grouped by source and campaign with per-source call, answered, and duration aggregates:
The response carries totals (calls, answered, per-DID rows, distinct sources) plus a sources array — one row per source/campaign pair — sorted by call volume. The same data renders under Insights → Call attribution in the dashboard. Because the join is read-time (DID → pool → source), a call to a DID that is not assigned to any pool does not attribute, and deleting a pool keeps history for the remaining pools but drops that source row going forward.

6. Constraints

  • Inbound-only. Tracking DIDs terminate inbound calls. No outbound (MT) voice or SMS path is created by a pool, and assignment only ever gates on number ownership.
  • Storage. Pools live in your organization’s settings JSONB, so no migration is needed and sibling sub-accounts never share pools.
  • Scope. Reads require numbers:read; create, update, delete, assign, and unassign require numbers:write. Resolve reads through numbers:read.
  • Quotas. Up to 200 pools per organization and 1,000 tracking DIDs per pool; over-quota writes return 429. Delete an unused pool or move DIDs to unblock.
  • One pool per DID. A DID maps to one source by design, so never assign the same DID to parallel pools expecting both to attribute.

7. Troubleshooting