The DNI snippet

Install the dynamic number insertion script, tell it which numbers on the page to replace, drive it from a single-page app, and work out why a number did not swap.

After this page you can put the snippet on a site, control exactly which numbers it rewrites, keep it working in a single-page app, decide where it sits relative to your consent banner, and diagnose a page where the number did not change.

The DNI snippet is a small script you add to your website. It asks the platform for a number from a pool, swaps it into the page in place of the number already there, and keeps the lease alive while the visitor stays.

The tag

Copy it from the pool's Snippet tab — it already contains the pool's public key, so there is nothing to fill in. Paste it once per page, anywhere; just before the closing body tag is fine. It loads asynchronously and never blocks rendering.

The tag
<script
  src="https://api.buy3.io/api/public/dni/b3dni.js"
  data-pool="pk_4f1d2c3b4a5e6f708192a3b4"
  data-swap=".b3-number"
  async></script>
AttributeRequiredWhat it does
data-poolYesThe pool's public key, pk_…. It identifies the pool and authorises nothing else, which is why it is safe in page source.
data-swapNoA CSS selector for the elements to rewrite. It defaults to .b3-number. Elements carrying data-b3-number are always included as well.
data-apiNoOverrides the origin the script calls. It is read off the script's own src otherwise, so the same file works on every deployment and custom domain.
asyncNoRecommended. Nothing on the page waits for the script.
One file serves every pool and is cached for an hour, so a fix reaches your visitors without anybody re-pasting a tag.

What it rewrites

Put your usual number in the page as normal and mark the element. For each matching element the script rewrites the first phone-shaped run of text inside it, and sets the href of any tel: link on or inside it. An element with no phone-shaped text and no child elements has its whole text replaced.

On your page
<!-- text and the tel: link are both rewritten -->
<a class="b3-number" href="tel:+18005550100">(800) 555-0100</a>

<!-- the attribute works instead of the class -->
<span data-b3-number>(800) 555-0100</span>

<!-- written in E.164 wherever this element is used -->
<span class="b3-number" data-b3-format="e164">+18005550100</span>
  • data-b3-format="e164" writes +12145550142; data-b3-format="national" writes (214) 555-0142. Without it the number is written the way the platform displays it.
  • A rewritten element gets data-b3-swapped="+1…", which is the quickest way to confirm a swap in the browser's inspector.
  • tel: links are always set to E.164, whatever the visible format is.
  • The selector goes into an attribute you paste, so the console only accepts the characters a selector needs. Keep it simple: a class is easier to get right than a structural selector.

The two requests it makes

RequestWhenWhat it does
LeasePOST /api/public/dni/leaseOnce, as the script loads.Sends the pool key, the visitor id, the page URL, the referrer and the whole query string. Answers with the number to show and a session id.
HeartbeatPOST /api/public/dni/heartbeatEvery 60 seconds while the tab is visible.Keeps the lease alive. When the heartbeats stop, the number returns to the pool after the pool's session length.
Both are documented in the DNI public endpoints reference.
A lease
{
  "number": { "e164": "+12145550142", "national": "(214) 555-0142", "display": "(214) 555-0142" },
  "visitorId": "v_9f2a4c7e1d0b8a6f5e4d3c2b",
  "sessionId": "48211",
  "ttlSeconds": 900,
  "overflow": false
}
  • sessionId is null when the pool was full and the fallback number was shown. There is no lease to keep alive, so no heartbeat is sent.
  • overflow: true means the number is shared with another visitor, or is the fallback. Any call on it is attributed best-effort at most.
  • Heartbeats are skipped while the tab is hidden and sent immediately when it becomes visible again after a gap, so a background tab does not hold a number for hours.
  • A heartbeat for a lease that has already expired is answered expired, and the script leases again rather than reviving it. Reviving would put two people on one number with nothing flagged.
  • The whole query string is sent and the server keeps only what the pool's capture list allows, so changing that list takes effect without anybody re-pasting a tag.

The visitor id

The script mints a random id such as v_9f2a4c7e1d0b8a6f5e4d3c2b and keeps it in localStorage under b3dni_vid, falling back to a first-party cookie of the same name when storage is blocked. It identifies a browser to one pool's owner and nothing else, and it is what makes page two of a visit show the number page one did.

Single-page apps

The script runs once. A router that replaces the markup without a page load leaves the new markup unswapped, so call the global after each render.

Driving it yourself
// After your router renders a new view
if (window.b3dni && window.b3dni.loaded) {
  window.b3dni.swap();          // rewrite the numbers in the new markup
}

// Only when the new view is a genuinely new visit — it leases again
// window.b3dni.refresh();

// React to the number as soon as it is known
document.addEventListener("b3dni:number", function (event) {
  console.log("leased", event.detail.number.e164, "shared:", event.detail.overflow);
});
On window.b3dniWhat it does
loadedtrue once the script has started. Check it before calling anything else.
numberThe leased number as { e164, national, display }, or null before the lease answers.
overflowtrue when the number is shared or is the fallback.
swap()Rewrites every matching element again. Cheap, and safe to call as often as you render.
refresh()Leases again. Use it for a genuinely new visit, not for a new view — a returning visitor keeps the lease they already hold.
A b3dni:number event fires on document when a number is set, with detail.number and detail.overflow. It is the hook to use when you need the number before your own markup exists.
  • The script sends the page URL, the referrer and the query string, and stores a random id. It reads no cookie of yours and never sends credentials — the lease endpoint refuses credentialed requests by design.
  • Only the parameters on the pool's capture list are stored. Everything else is discarded server-side, which is the control to use when a landing URL can carry personal data.
  • The visitor's address and user agent are recorded with the session, and the pool's owner can read them. They are your own site's traffic.
  • Where consent is required before storage or measurement, load the tag after consent — through your tag manager's consent gate, or by injecting it once the banner is accepted. There is no half-on mode, and there should not be.
  • Restrict the pool with Allowed domains so the key in your page source cannot be used to lease from anywhere else.

Check it before you ship

  1. 1

    Open the page with a parameter on the URL

    Use a private window and something obvious, such as ?utm_source=dni-test. The number on the page should change.

  2. 2

    Check the sessions list

    The pool's Sessions tab should show a new session holding a number, carrying that parameter and your landing URL.

  3. 3

    Reload the page

    The number should stay the same. If it changes, the visitor id is not persisting.

  4. 4

    Dial the number that is on the page

    The call should appear in the call log with the parameter as a tag, and the session row should link to it.

  5. 5

    Open the page in a second browser

    A different number should appear. The same number in both means the pool has one leasable number, or is out.

When the number does not change

The script fails silent: on any error the page simply keeps the number already in its HTML. A tracking tag must never be the reason a phone number is missing from a page. That makes the browser's network tab the place to look — the lease request's status says which of these it is.

What you seeWhat it meansWhat to do
No request to /lease at allThe script did not load or did not start: a blocked request, or a tag with no data-pool.Check the tag is on the page and not stripped by a tag manager, and that data-pool carries the key.
403 origin_not_allowedThe page's host is not covered by the pool's allowed domains.Add the domain. Remember that a subdomain is covered by its parent, and that a page sending no Origin or Referer is refused under a non-empty list.
404 pool_not_foundThe key does not match a pool.Copy the tag again from the pool's Snippet tab.
409 pool_pausedLeasing is switched off on the pool.Resume it. Live leases stop being extended while it is paused, so the pool drains within one session length.
409 pool_emptyThe pool has no leasable number and no usable fallback.Add active tracking numbers on the pool's campaign, or set a fallback.
400The pool key is missing or malformed.The key is pk_ followed by 24 hexadecimal characters. Anything else is refused.
A lease, but no swapNothing on the page matched the selector, or the matched element held no phone-shaped text and had child elements.Check data-swap against the markup, or mark the element with data-b3-number. Look for data-b3-swapped in the inspector.
The number changes on every viewThe visitor id is not persisting, or a router is calling refresh() per view.Check storage is not blocked, and call swap() rather than refresh() between views.
A call with no session tagsThe lease had ended more than ten minutes before the call, or the number was the fallback.Lengthen the pool's session, or add numbers so fewer visitors overflow.

A worked example

Acme Health have a landing page at https://example.com/dallas that shows (800) 555-0100 in the header and again in the footer.

  1. They give both elements class="b3-number" and leave the existing markup — text and tel: link — exactly as it was.
  2. They paste the tag before the closing body tag, with the pool's key and the default selector.
  3. They open https://example.com/dallas?utm_source=dni-test&kw=urgent+care in a private window. Both numbers change to (214) 555-0142 and both tel: links become tel:+12145550142.
  4. The pool's Sessions tab shows one live session for that visitor, carrying utm_source and kw, with the landing URL beside it.
  5. They dial the number. Within seconds the call appears with utm_source=dni-test and kw=urgent care as tags with the source dni, and the session row now links to it.
  6. Their page is a single-page app, so they add window.b3dni.swap() to their router's after-render hook. Navigating from the hero to the pricing view keeps the same number.

Next steps