Dynamic Number Insertion

Dynamic Number Insertion (DNI) replaces the phone numbers on your website with iovox tracking numbers, so every call can be attributed to the marketing source, campaign, or visitor that produced it.

You can integrate in two ways:

  • JavaScript library (recommended). Drop one script tag on your pages and DNI handles detection, number replacement, and session tracking for you.
  • Server-Side Replacement. Call the API directly from your backend and insert numbers yourself, with no client-side JavaScript.

Concepts

Per-Source / Per-Campaign Tracking

A fixed tracking number is shown based on a label you choose, such as the traffic source or campaign. The number is reused for everyone in that bucket.

You define the buckets. A filter is a category key/value pair configured on your iovox numbers, so you can organize them however you like: by medium (paid, organic, direct), by channel (google, facebook, tiktok), or by any custom labels you preset on the platform. The medium key used in the examples is just one option, not a required key.

Per-Session Tracking

Each visitor is given their own number from a pool, locked to their browser session. Custom data (campaign, landing page, referrer, and so on) is stored against that number, so a call can be traced back to the individual visit. Best when you need per-visitor attribution rather than per-bucket.

Static vs Dynamic Numbers

  • A static number is a single tracking number permanently assigned to a destination. It is what per-source tracking inserts: the number is fixed, but which static number you show can still vary per visitor (for example, a different one per campaign). Static numbers are dynamically inserted, not dynamically allocated.
  • A dynamic number is drawn from a pool at request time and held for a single visitor's session. It is what per-session tracking allocates.

Number Pools

A number pool is a group of tracking numbers reserved for per-session tracking. When a visitor arrives, DNI allocates one number from the pool and locks it to their session; the same number is reused for that visitor and not handed to anyone else until it is released. Pools are configured for you by iovox. Contact your account manager to set one up.

At allocation time, you can attach site_data to the number: any metadata about the visit (campaign, landing page, referrer, or your own custom fields) that you want to report on for that session later. It is stored against the allocated number and surfaces in reporting and call data.


Quick Start

Add this just before the closing </body> tag, so the numbers already exist in the page when the script runs. Both examples find every <a href="tel:..."> on the page and swap each number for an iovox tracking number.

Per-source tracking shows a fixed number based on the visitor's traffic source:

<script src="https://cdn.iovox.com/rest/v2/dni.js"></script>
<script>
  iovox.init({
    access_key: 'YOUR_ACCESS_KEY',
    action_key: 'YOUR_ACTION_KEY'
  });

  iovox.autoReplace({ medium: iovox.detectMedium() });
</script>

Per-session tracking allocates a unique number per visitor from a pool:

<script src="https://cdn.iovox.com/rest/v2/dni.js"></script>
<script>
  iovox.init({
    access_key: 'YOUR_ACCESS_KEY',
    action_key: 'YOUR_ACTION_KEY'
  });

  iovox.autoAllocate(
    { pool_id: 'my-pool' },
    { landing_page: location.href }
  );
</script>

Setup

iovox.init(config)

Call once, before any other method.

Parameter Required Default Description
access_key Yes none Your iovox JS access key
action_key Yes none Your iovox JS action key
api_url No https://platform.iovox.com API base URL
country_code No '44' Country code used to normalize local numbers to E.164
persist_filters No true Persist detected filters in a session cookie across page views
use_loading_class No false Toggle the iovox-loading class while fetching, instead of hiding elements with visibility: hidden
on_result No none Receive raw API results and skip automatic DOM replacement (see Manual Mode)
iovox.init({
  access_key: 'YOUR_ACCESS_KEY',
  action_key: 'YOUR_ACTION_KEY',
  country_code: '1'
});

Pointing at the Sandbox

Set api_url to the sandbox to test against your sandbox account. Remove it to go live.

iovox.init({
  access_key: 'YOUR_ACCESS_KEY',
  action_key: 'YOUR_ACTION_KEY',
  api_url: 'https://sandbox-platform.iovox.com'
});

Per-Source Tracking

Shows a fixed tracking number chosen by your filters. See the concept above for how buckets work.

iovox.autoReplace(filters, defaults)

Scans the page, deduplicates the numbers, and replaces each with the matching tracking number.

Parameter Type Required Description
filters Object Yes Category key/value pairs to match
defaults Object No Fallback filters for numbers the primary filters do not match
iovox.autoReplace({ medium: iovox.detectMedium() });

Each number is normalized to E.164 (e.g. 442079460001) and sent as a call_destination filter. The API returns the tracking number on the item whose forwarding number and categories match. If filters misses and defaults is supplied, the script retries with defaults; if both miss, the original number is left unchanged.

iovox.replaceNumber(selector, filters)

Replace a single element.

iovox.replaceNumber('#main-phone', { medium: 'paid_search', call_destination: '442079460001' });

iovox.replaceNumbers(items)

Replace several elements in one API call.

iovox.replaceNumbers([
  { selector: '#sales-phone',   filters: { medium: 'paid_search', call_destination: '442079460001' } },
  { selector: '#support-phone', filters: { medium: 'paid_search', call_destination: '442079460002' } }
]);

Filter Keys

Filters are key/value pairs that determine which item is matched. Most keys are matched against your item's categories, but a few have special behavior:

Filter key Description
call_destination Matches the item's forwarding number (E.164 digits, e.g. 442079460001). Automatically used by autoReplace() to scan and replace phone numbers on the page.
item_id Direct lookup by the item's ID. Skips category matching entirely.
anything else Matched as a category: the key is the category's ID, the value is the category value.

When item_id is provided, the API looks up the item directly and returns its tracking number; no other filters are needed. Otherwise, all provided filters must match for an item to be returned.

Enterprise: link_id (an alias for item_id) and node_id are also accepted.

Prefer to call the API from your own server? See Server-Side Replacement.


Per-Session Tracking

Allocates a pool number per visitor and stores custom data against it. See the concept above.

iovox.autoAllocate(filters, site_data, defaults)

Scans the page and allocates a pool number for each phone number found.

Parameter Type Required Description
filters Object Yes Selects which pool to allocate from (see below)
site_data Object No Custom key/value metadata stored against the allocated number for reporting
defaults Object No Fallback filters if the primary pool does not match

Choosing a pool. filters selects the pool in one of two ways:

  • By ID. Pass { pool_id: 'your-pool-id' } to use that specific pool directly.
  • By category. Pass category key/value pairs (see Filter Keys) and iovox uses the pool whose configuration matches those categories. This lets you route to different pools based on the visit without hard-coding a pool ID.
var params = new URLSearchParams(location.search);

iovox.autoAllocate(
  { pool_id: 'my-pool' },
  {
    utm_source: params.get('utm_source'),
    landing_page: location.href
  }
);

Session affinity. A unique session key is generated per visitor and stored in the iovox_session cookie. The same visitor keeps the same pool number until the session ends. A new browser or cleared cookies starts a new session.

Prefer to call the API from your own server? See Server-Side Replacement.


Source Detection

iovox.detectMedium()

Returns the visitor's traffic source, checked in this order:

Priority Detection Returns
1 gclid, msclkid, or fbclid in the URL 'paid'
2 utm_medium in the URL the utm_medium value
3 document.referrer matches a known domain mapped medium (below)
4 no referrer 'direct'
5 unrecognized referrer 'referral'

Built-in referrer mappings

Referrer Medium
google, bing, yahoo, duckduckgo, baidu, yandex organic
facebook, instagram, twitter, linkedin, tiktok, pinterest, youtube social

Set Custom Referrers

iovox.setReferrers(referrers) adds custom referrer mappings, which take priority over the defaults. Call before autoReplace().

iovox.setReferrers([
  { domain: 'reddit.com', medium: 'social' },
  { domain: 'newsletter.example.com', medium: 'email' }
]);

iovox.detectCampaign()

Returns the utm_campaign value, or an empty string.

var campaign = iovox.detectCampaign();

Element Selectors

DNI scans only <a href="tel:..."> by default. To also scan other elements, call setAutoSelectors() before autoReplace() or autoAllocate(). The tel: selector is always included.

iovox.setAutoSelectors(['span.phone-number', 'div.contact-number']);

Filter Persistence

With persist_filters enabled (the default), the detected source is saved to the iovox_filters session cookie. A visitor who lands on ?utm_medium=ppc keeps that source on later pages without the parameter. The cookie clears when the browser closes. Disable it in init.


Response Caching

Per-source results (/dni/fetch) are cached in sessionStorage for the tab, so the same request is never repeated in a session. Per-session results (/dni/allocate) are not cached, since each request can update session state.


Manual Mode

Pass an on_result callback to receive the results and handle DOM replacement yourself. The API call still runs; automatic replacement is skipped.

iovox.init({
  access_key: 'YOUR_ACCESS_KEY',
  action_key: 'YOUR_ACTION_KEY',
  on_result: function (numbers) {
    // replace numbers in your DOM here
  }
});

The shape of numbers depends on which method you called:

Method Each entry contains
replaceNumbers() { number, selector } — the selector you passed, so you know where each number goes
autoReplace() { number } — in the same order as the numbers found on the page
autoAllocate() { voxnumber, destination_number } — in the same order as the numbers found on the page

For full control, use replaceNumbers(): each result carries its own selector, so you can map results to elements directly. See the full manual-replacement example below.


Phone Number Formats

You do not need to specify a format. DNI reads the number already on the page and formats the replacement to match it: the same spacing, punctuation, prefix, and any surrounding text are preserved, for any country. The tracking number simply takes the place of the original.

A few examples of how the original format carries over to the replacement:

Original on page Replacement
020 7946 0001 020 7946 0500
+44 (0) 20 7946 0001 +44 (0) 20 7946 0500
(212) 555-0142 (212) 555-0199
020 7946 0001 Opt 2 020 7946 0500 Opt 2

Server-Side Replacement

The JavaScript library is optional. To avoid loading client-side JavaScript, call the DNI API directly from your server and insert the numbers into your HTML yourself.

Use this when you render pages server-side, cannot add third-party scripts, or want to control replacement and session handling in your backend. The request and response shapes are identical to what the library uses, so the Per-Source and Per-Session behavior applies here too.

You take on the work the library would otherwise do: detecting the source, normalizing numbers to E.164, generating and persisting a session key (per-session), and inserting numbers into your markup.

Authentication. Send your access_key and action_key in the JSON body of every request.

Base URL. https://platform.iovox.com for production, https://sandbox-platform.iovox.com for sandbox.

POST /dni/fetch

Per-source tracking. Returns a fixed tracking number for each destination and filter set.

Request

curl -X POST https://platform.iovox.com/dni/fetch \
  -H 'Content-Type: application/json' \
  -d '{
    "access_key": "YOUR_ACCESS_KEY",
    "action_key": "YOUR_ACTION_KEY",
    "replacements": [
      { "filters": { "call_destination": "442079460001", "medium": "paid_search" } },
      { "filters": { "call_destination": "442079460002", "medium": "website" } }
    ]
  }'

Body parameters

Field Type Required Description
access_key String Yes Your iovox access key
action_key String Yes Your iovox action key
replacements Array Yes One entry per number to look up
replacements[].filters.call_destination String Yes Destination number in E.164
replacements[].filters.* String No Any filter keys to match (see Filter Keys)

Response 200 OK

{
  "numbers": [
    { "number": "442079460100" },
    { "number": "442079460200" }
  ]
}

numbers matches replacements by position. An entry is null when no number matches.

Status codes

Code Meaning
200 OK Request processed; see numbers for matches
400 Bad Request Malformed JSON or missing required fields
401 Unauthorized Invalid access_key or action_key
405 Method Not Allowed Use POST
500 Internal Server Error Retry later

POST /dni/allocate

Per-session tracking. Allocates a pool number for a visitor session and stores your custom data against it.

Generate a unique session_key per visitor (any stable unique string, such as a UUID) and reuse it on later requests for the same visitor so they keep the same number.

Request

curl -X POST https://platform.iovox.com/dni/allocate \
  -H 'Content-Type: application/json' \
  -d '{
    "access_key": "YOUR_ACCESS_KEY",
    "action_key": "YOUR_ACTION_KEY",
    "allocations": [
      {
        "filters": { "pool_id": "my-pool", "call_destination": "442079460001" },
        "session_key": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
        "site_data": { "utm_source": "google", "utm_medium": "cpc", "landing_page": "https://example.com/contact" },
        "page_url": "https://example.com/contact"
      }
    ]
  }'

Body parameters

Field Type Required Description
access_key String Yes Your iovox access key
action_key String Yes Your iovox action key
allocations Array Yes One entry per number to allocate
allocations[].filters.pool_id String Yes Pool to allocate from
allocations[].filters.call_destination String Yes Destination number in E.164
allocations[].session_key String Yes Unique, stable per visitor
allocations[].site_data Object No Custom data stored against the number
allocations[].page_url String No URL where the number is shown

Response 200 OK

{
  "numbers": [
    { "voxnumber": "442079460900", "destination_number": "442079460001" }
  ]
}

numbers matches allocations by position. An entry is null when allocation fails (for example, an exhausted pool).

Status codes

Code Meaning
200 OK Request processed; see numbers for allocations
400 Bad Request Malformed JSON or missing required fields
401 Unauthorized Invalid access_key or action_key
405 Method Not Allowed Use POST
500 Internal Server Error Retry later

Full Examples

Per-Source Tracking

<!DOCTYPE html>
<html>
<body>

  <p>Sales: <a href="tel:02079460001">020 7946 0001</a></p>
  <p>Lettings: <a href="tel:+442079460010">+44 (0) 20 7946 0010</a></p>

  <script src="https://cdn.iovox.com/rest/v2/dni.js"></script>
  <script>
    iovox.init({
      access_key: 'YOUR_ACCESS_KEY',
      action_key: 'YOUR_ACTION_KEY'
    });

    iovox.autoReplace({ medium: iovox.detectMedium() });
  </script>

</body>
</html>

Per-Session Tracking

<!DOCTYPE html>
<html>
<body>

  <p>Call us: <a href="tel:02079460001">020 7946 0001</a></p>

  <script src="https://cdn.iovox.com/rest/v2/dni.js"></script>
  <script>
    iovox.init({
      access_key: 'YOUR_ACCESS_KEY',
      action_key: 'YOUR_ACTION_KEY'
    });

    var params = new URLSearchParams(location.search);

    iovox.autoAllocate(
      { pool_id: 'my-pool' },
      {
        utm_source: params.get('utm_source'),
        landing_page: location.href
      }
    );
  </script>

</body>
</html>

Both Modes on One Page

<!DOCTYPE html>
<html>
<body>

  <p>General: <a href="tel:02079460001" id="general">020 7946 0001</a></p>
  <p>Sales: <a href="tel:02079460010" id="sales">020 7946 0010</a></p>

  <script src="https://cdn.iovox.com/rest/v2/dni.js"></script>
  <script>
    iovox.init({
      access_key: 'YOUR_ACCESS_KEY',
      action_key: 'YOUR_ACTION_KEY'
    });

    iovox.replaceNumber('#general', {
      medium: iovox.detectMedium(),
      call_destination: '442079460001'
    });

    iovox.autoAllocate(
      { pool_id: 'sales-pool' },
      { landing_page: location.href }
    );
  </script>

</body>
</html>

Manual Replacement

Use on_result with replaceNumbers() when you want to update the DOM yourself. Each result includes the selector you passed, so you can place each tracking number exactly where it belongs.

<!DOCTYPE html>
<html>
<body>

  <p>Sales: <a href="tel:02079460001" id="sales-phone">020 7946 0001</a></p>
  <p>Support: <a href="tel:02079460002" id="support-phone">020 7946 0002</a></p>

  <script src="https://cdn.iovox.com/rest/v2/dni.js"></script>
  <script>
    iovox.init({
      access_key: 'YOUR_ACCESS_KEY',
      action_key: 'YOUR_ACTION_KEY',
      on_result: function (numbers) {
        numbers.forEach(function (result) {
          if (!result.number) return;
          var el = document.querySelector(result.selector);
          el.textContent = result.number;
          el.setAttribute('href', 'tel:' + result.number);
        });
      }
    });

    iovox.replaceNumbers([
      { selector: '#sales-phone',   filters: { medium: 'paid_search', call_destination: '442079460001' } },
      { selector: '#support-phone', filters: { medium: 'paid_search', call_destination: '442079460002' } }
    ]);
  </script>

</body>
</html>

Legacy Version

A previous version of the JavaScript API is maintained for existing integrations. If your site already uses it, see the Legacy JavaScript API. New integrations should use the current version above.