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
01
A visitor lands
with
utm_source,gclidand the rest on the URL02
The snippet asks for a number
using the pool's public key
03
A session leases one
and stores the parameters the pool asked to keep
04
The page shows it
in place of the number already in the HTML
05
The visitor calls
and the session's parameters arrive as tags
- 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
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"]
}'{
"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"
}
}| Setting | Console label | What it does |
|---|---|---|
campaignId | Campaign | Which 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. |
publisherId | Publisher | Who 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. |
sessionTtlSeconds | Session length | How long a lease lasts after the last sign of life, from 60 seconds to a day. It defaults to half an hour. |
allowedDomains | Allowed domains | Which sites may lease from this pool. Empty admits every site. |
captureParams | Captured URL parameters | Which URL parameters are kept on the session and become tags. Nothing is captured unless it is listed. |
fallbackNumberId | Shown when no pool number is free | A static number on the same campaign, shown when every pool number is out. |
status | Leasing numbers | paused 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.comandexample.com:443all save asexample.com. - A match is on whole labels:
example.comadmitsexample.com,www.example.comanda.b.example.com, and admits neithernotexample.comnorexample.com.evil.net. - A single label such as
comis refused. Nobody who typed it meant every site on that suffix. - The browser's own
Origin— or itsRefererwhen 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 everyutm_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_Sourceandutm_sourceare 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
gclidout. - 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.
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.
| Situation | What the visitor sees | What the call can be tied to |
|---|---|---|
| A number is free | The 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 fallback | The 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 fallback | The 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 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.
{
"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
}| Pill | What it means |
|---|---|
| Live | Holding a number right now. |
| Shared | Given a number another visitor still held. Attribution on any call is best-effort. |
| Fallback | No number was free, so this visitor was shown the fallback. Nothing was leased. |
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.
- Create the pool on Acme Health inbound, session length 15 minutes, allowed domain
example.com, capturingutm_*,gclid,msclkidandkw. No publisher — this is their own media. - Size it. Analytics says 60 visitors an hour at peak, about three minutes each. Eighteen numbers, so they buy twenty.
- Add the numbers to the pool from its Numbers tab. They are already on the campaign, so nothing is refused.
- Install the snippet — see The DNI snippet — and check a test visit appears under Sessions with the parameter they put on the URL.
- 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.
- 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.
