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) |
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 |
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.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 foritem_id) andnode_idare 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.
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.
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 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 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.