Number pools and DNI

Dynamic number insertion shows each website visitor a number of their own, so a call can be tied back to the click, keyword and page that produced it — and a pool is how you size that.

After this page you can build a number pool, choose a session length and a capture list that suit your traffic, work out how many numbers the pool actually needs, and read what the console is telling you when it says visitors are sharing numbers.

A single number on a website tells you the site produced a call. Dynamic number insertion tells you which visit did. Each visitor is shown a number from a pool; when that number rings, the platform knows which visitor was holding it — and therefore which advert, keyword and landing page brought them.

How it works

  1. 01

    A visitor lands

    with utm_source, gclid and the rest on the URL

  2. 02

    The snippet asks for a number

    using the pool's public key

  3. 03

    A session leases one

    and stores the parameters the pool asked to keep

  4. 04

    The page shows it

    in place of the number already in the HTML

  5. 05

    The visitor calls

    and the session's parameters arrive as tags

The lease is kept alive while the visitor is on the site and returns to the pool afterwards.
  • A pool is a set of tracking numbers belonging to one campaign. It has a public key, pk_…, which is safe to publish in a web page: all it can do is lease a number from that pool.
  • A session is one visitor's lease on one number. It holds the landing URL, the referrer and the parameters the pool captures.
  • When a pooled number rings, the call is matched to the session holding it. The session's parameters become tags on the call with the source dni, ready for filters, reports and pixels.
  • A visitor keeps the same number for the whole visit, page two included. A number that changes under somebody's eyes is a number they stop trusting.

Building a pool

Create a pool
curl -X POST "https://api.buy3.io/api/tracking/pools" \
  -H "Authorization: Bearer $BUY3_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Health — Dallas landing pages",
    "campaignId": "6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
    "publisherId": null,
    "sessionTtlSeconds": 900,
    "allowedDomains": ["example.com"],
    "captureParams": ["utm_*", "gclid", "msclkid", "kw"]
  }'
201
{
  "pool": {
    "id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f",
    "name": "Acme Health — Dallas landing pages",
    "status": "active",
    "publicKey": "pk_4f1d2c3b4a5e6f708192a3b4",
    "campaign": { "id": "6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d", "name": "Acme Health inbound", "status": "live" },
    "publisher": null,
    "sessionTtlSeconds": 900,
    "allowedDomains": ["example.com"],
    "captureParams": ["utm_*", "gclid", "msclkid", "kw"],
    "fallbackNumber": null,
    "health": {
      "numbers": 0,
      "activeSessions": 0,
      "overflow24h": 0,
      "saturated": false,
      "warning": "No active number is in this pool, so the snippet leaves each page's own number in place. Assign tracking numbers to the pool."
    },
    "createdAt": "2026-09-20T14:03:11.000Z"
  }
}
SettingConsole labelWhat it does
campaignIdCampaignWhich campaign the pool's numbers ring. Fixed once the pool exists — moving it would either strand its numbers or silently re-route live traffic. Build a second pool instead.
publisherIdPublisherWho the pool's calls are credited to. It applies to every number in the pool, because a pool is one traffic source by definition. Empty is your own media.
sessionTtlSecondsSession lengthHow long a lease lasts after the last sign of life, from 60 seconds to a day. It defaults to half an hour.
allowedDomainsAllowed domainsWhich sites may lease from this pool. Empty admits every site.
captureParamsCaptured URL parametersWhich URL parameters are kept on the session and become tags. Nothing is captured unless it is listed.
fallbackNumberIdShown when no pool number is freeA static number on the same campaign, shown when every pool number is out.
statusLeasing numberspaused stops new leases and stops extending live ones, so pausing drains the pool within one session length.

Then add numbers. Any active tracking number on the pool's campaign can join it, from the pool's Numbers tab or by setting poolId on the number. A pooled number should not be printed anywhere else: it is lent out and taken back all day.

Allowed domains

  • Entries are stored as bare hosts, so https://www.Example.com/landing, *.example.com and example.com:443 all save as example.com.
  • A match is on whole labels: example.com admits example.com, www.example.com and a.b.example.com, and admits neither notexample.com nor example.com.evil.net.
  • A single label such as com is refused. Nobody who typed it meant every site on that suffix.
  • The browser's own Origin — or its Referer when there is no origin — is what is checked. A request that names no page at all is refused under a non-empty list.
  • This is the brake on somebody else's page embedding your snippet and draining your pool. The public key authorises nothing else, so it is not a secret and does not need to be one.

Captured parameters

  • It is an allowlist, not "everything on the query string". A landing URL routinely carries an email address or a session token, and whatever is captured is copied onto the call and out through every webhook and pixel you have.
  • List a parameter exactly, or use a trailing * as a prefix: utm_* keeps every utm_ parameter a campaign tool invents without your enumerating them. A bare * is refused, because it would turn the allowlist back into the whole query string.
  • Names are lower-cased, so UTM_Source and utm_source are one tag rather than two.
  • At most 20 parameters are kept per session, each value cut to 256 characters, scalars only. Your own order decides which win the twenty, so a padded query string cannot push gclid out.
  • A returning visitor's parameters are merged: page two has no query string and must not wipe the click that brought them, and a genuinely new value replaces the old one.

How big should a pool be?

Big enough that two visitors who might call at the same time are never holding the same number. The figure that matters is concurrent visitors, not daily traffic — and a visitor is concurrent for much longer than they are on the page, because their lease outlives their visit by the session length.

A starting point
numbers  ≈  visitors per hour  ×  (session length + time on page) ÷ 3600

60 visitors an hour, a 15-minute session, 3 minutes on the page
         60 × (900 + 180) ÷ 3600  =  18 numbers
  • Size for your peak hour, not your average. A pool that is comfortable on a Tuesday morning is the one that fails during the campaign you bought it for.
  • A longer session length costs numbers and buys attribution. Somebody reads the page, closes the tab and rings an hour later from their phone; only a lease that is still theirs — or one that ended less than ten minutes ago — can tie that call back to their visit.
  • A shorter session length is the cheaper half of the fix when a pool runs hot. Shorten it before buying numbers if most of your callers dial while the page is still open.
  • Leave headroom. Running at exactly the number of live sessions is running at the edge of sharing.

The pool's own page gives you the four figures to check against: Numbers in the pool, Holding one now, Calls, last 24h and Session length. The list of pools shows the same, and flags any pool that has run out of numbers.

What happens when a pool runs out

A visitor is never shown a blank where a number should be. Which of three things happens depends on what is free and whether the pool has a fallback.

SituationWhat the visitor seesWhat the call can be tied to
A number is freeThe free number seen longest ago — not the first free one, so a number released a moment ago is not immediately given to somebody else.That visitor's visit: the page, the referrer and every captured parameter.
Every number is held, and the pool has a fallbackThe fallback number. The session is recorded as an overflow and leases nothing.The campaign only. No visitor, no keyword, no click.
Every number is held, and there is no fallbackThe held number whose holder has been quiet longest, shared with them. Both leases are flagged.Best effort: a call on a shared number is attributed to the more recent of the two leases.
A shared number is a tag that is sometimes wrong. A page with no tracked number at all is a call with no pool, no publisher and no tags — wrong always. That is why sharing is preferred to giving up.

A fallback must be a tracking number on the pool's campaign that is not itself in a pool — a number in rotation is by definition the thing that has run out. If that number is later moved or taken out of service, the pool's fallback is cleared and the response names every pool that lost one.

With no numbers at all and no fallback, the lease is simply refused and the page keeps the number already in its HTML. Nothing breaks; nothing is measured either.

Reading the sessions list

The pool's Sessions tab lists leases, newest activity first, with three views: Holding one now, Finished and All. Each row shows the visitor, the number held, the captured parameters and landing page, when it started, when it was last seen and the call it produced.

One session
{
  "id": "48211",
  "visitorId": "v_9f2a4c7e1d0b8a6f5e4d3c2b",
  "number": { "id": "2f6b8c1d-4e5a-4b7c-9d8e-0f1a2b3c4d5e", "e164": "+12145550142", "formatted": "(214) 555-0142" },
  "params": { "utm_source": "search", "utm_campaign": "dallas-brand", "gclid": "Cj0KCQjw…", "kw": "urgent care dallas" },
  "landingUrl": "https://example.com/dallas?utm_source=search&gclid=Cj0KCQjw…",
  "referrer": "https://www.example.com/",
  "startedAt": "2026-09-20T14:01:02.000Z",
  "lastSeenAt": "2026-09-20T14:06:02.000Z",
  "expiresAt": "2026-09-20T14:21:02.000Z",
  "active": true,
  "fallback": false,
  "overflow": false,
  "callId": null
}
PillWhat it means
LiveHolding a number right now.
SharedGiven a number another visitor still held. Attribution on any call is best-effort.
FallbackNo number was free, so this visitor was shown the fallback. Nothing was leased.
Sessions are pruned after thirty days. The calls they produced keep their own pool, tags and session reference, so nothing a report reads is lost with the row.

Deleting a pool

Deleting a pool never deletes inventory. Its numbers become ordinary static tracking numbers on the same campaign — still owned, still rented, still ringing — and the response says how many came back and how many visitors were holding one at that moment. What is lost is per-visitor attribution on any page still carrying the tag.

A worked example

Acme Health run paid search to one landing page and want to know which keyword produces calls.

  1. Create the pool on Acme Health inbound, session length 15 minutes, allowed domain example.com, capturing utm_*, gclid, msclkid and kw. No publisher — this is their own media.
  2. Size it. Analytics says 60 visitors an hour at peak, about three minutes each. Eighteen numbers, so they buy twenty.
  3. Add the numbers to the pool from its Numbers tab. They are already on the campaign, so nothing is refused.
  4. Install the snippet — see The DNI snippet — and check a test visit appears under Sessions with the parameter they put on the URL.
  5. A fortnight later a promotion triples traffic for two days. The pool list flags it: Holding one now is 20 against 20 numbers. They shorten the session to 8 minutes that afternoon and buy six more numbers the next morning.
  6. At the end of the month they group a report by tag:kw. Two keywords account for most of the calls, and one of the expensive ones produces none.

Next steps