Ad Verification: Region and Language Must Agree
Separate which ad-capture differences come from egress region, which from the language list, and which come from neither, then record the alignment per capture.
BotBrowser Team
Want the structured docs for Documentation?
This article lives in the editorial library. For step-by-step setup, reference material, and ongoing updates, jump into the docs section.
An ad-verification capture answers a narrow question: what creative and what landing page did a request with these declared properties receive in this region. BotBrowser documents proxy-derived geolocation defaults plus manual timezone, locale and language overrides, which the documentation gates behind an ENT Tier1 license, so the region a capture claims can be stated explicitly instead of inherited from whatever machine ran it. It does not generate ad traffic, place bids, or control what an ad server returns, and every capture still depends on the platform's terms permitting it.
TL;DR
- Two inputs decide what a capture represents: where the request exits, and what language the browser declares. A capture where they disagree describes no coherent region.
- An egress region change can alter the creative a request receives. A declared-language change can alter the landing page a request is sent to.
- A mismatch that changes neither still invalidates the comparison, because the two captures no longer describe the same request.
- Run an alignment check before capturing, and store the egress IP class, timezone and language list beside every accepted capture.
ads.txtdeclares which systems a publisher authorizes to sell inventory. It says nothing about whether a capture is permitted.
Contents
- The two inputs a capture actually declares
- What a disagreement changes, and what it does not
- Run the alignment check before the first capture
- Record the alignment with every accepted capture
- Where ads.txt applies and where it stops
- Terms and scope before tooling
- Practical conclusion
The two inputs a capture actually declares
Every HTTP request carries more than a URL. It carries an Accept-Language value that names the natural languages the client prefers, and it arrives from a network address that a server or a third party can resolve to a rough location. RFC 9110 defines the header as the set of natural languages preferred in a response, and defines Vary as the field that tells a cache which request fields influenced the representation. When a response includes Vary: accept-language, the server is stating that language selection played a part.
MDN's reference adds a detail that matters for capture work: browsers set the header from their active interface language, and both Accept-Language and navigator.languages may intentionally omit part of the user's preference list for fingerprinting reasons. A capture therefore observes a declared preference list, not a person's full linguistic profile.
The network side is separate. A proxy or a direct connection determines which address the request appears to come from, and BotBrowser's GeoIP guide describes translating that exit IP into geographic defaults such as timezone, locale, languages and approximate coordinates, with manual flags taking priority over the derived values. The documentation also states the accuracy boundary plainly: country-level mapping is generally strong while city-level mapping can resolve to a nearby city or region.
Two consequences follow from that pair. First, a capture can be internally consistent or not: the language list and the timezone can agree with the exit region, or they can describe a place the request does not come from. Second, the alignment is a property of the capture, not of the browser binary, so it has to be recorded rather than assumed.
What a disagreement changes, and what it does not
The useful split is not "consistent versus broken" but "what did this disagreement actually change in the response". Advertising systems vary, so the table below lists three disagreement classes and one control row, together with the observation each supports. It is a classification aid, not a claim about any named ad server's behaviour.
| Capture condition | Creative observed | Landing page observed | What the capture supports |
|---|---|---|---|
| Exit region changed, language list held fixed | May differ | May differ | Two regional observations; neither is the other's baseline |
| Language list changed, exit region held fixed | May differ | May differ | Content-negotiation observation in one region |
| Language list matches timezone, exit differs | No change observed | No change observed | No conclusion: the request does not describe one place |
| Control: all three agree, source changed | May differ | May differ | Source-side change, not a region effect |
The first row is the case that gets misread most often. A team keeps the browser's language list at en-US,en and moves the exit from one country to another. If the creative changes, the tempting conclusion is that the ad system targets geography. The observation supports only that the two captures differ, and the difference could equally come from scheduling, a campaign budget change, a frequency rule or a regional consent state. Separating those causes needs a third capture in the original region at the same time, not a stronger claim about the first two.
The second row is the reverse. Holding the exit fixed and changing the language list from en-US,en to de-DE,de can change which landing page a request is redirected to, because language negotiation is exactly what RFC 9110 describes. That makes a landing-page difference an expected outcome of a declared change, not evidence of anything unusual.
The third row is the one that wastes the most analysis time. Nothing visible changes, so the capture looks fine, and two months later the series is used to compare a market against itself. A capture whose language list names one region while the request exits from another does not describe either region, so it belongs in neither series.
The fourth row covers genuine source-side change. If all three inputs are identical and the creative still differs, the difference lives in the ad system's own state: targeting edits, flighting, caps or experiment assignment. Recording the alignment correctly is what makes that conclusion available, because it rules out the capture as the explanation.
A consent state is the confounder that most often survives the first three rows. A request from a country with a consent framework in force may return a different creative or an interstitial before the landing page, and that difference can look like regional targeting when it is actually a regulatory gate. Because the gate is usually keyed to a region rather than to a language, comparing language lists will not catch it. Keep a separate field for whether a consent interaction occurred and what mode it was in; a capture series that mixes gated and ungated runs is not a regional series, whatever its geo inputs say.
Frequency and flighting create a similar trap across runs rather than within one. A campaign can cap how many times a creative is served to a given identifier, so a series captured from one profile can see the creative disappear without any region or language change. Whether that applies to a verification capture depends on what identifier the system keys its cap to, which is a platform question rather than a browser question. The practical defence is unchanged: record the alignment, keep the observation time, and do not attribute an unexplained difference to geography when the source could have produced it alone.
Run the alignment check before the first capture
The check below runs before any capture and fails loudly on a mismatch. It compares three values: the expected region, the browser's primary declared language, and the timezone the browser reports. The primary entry decides, because a list that starts with another language asks for that language first even when a matching tag appears later. A tag-to-region comparison is deliberately coarse, because the point is to catch a request that describes two different places, not to validate a geolocation database.
const EXPECTED = 'DE';
const REGION_LANGUAGES = { DE: ['de-DE', 'de'], US: ['en-US', 'en'], JP: ['ja-JP', 'ja'] };
const REGION_TIMEZONES = { DE: 'Europe/Berlin', US: 'America/New_York', JP: 'Asia/Tokyo' };
const languages = navigator.languages.slice();
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
// The primary language decides the region, not any match anywhere in the list:
// ["en-US", "de-DE"] still asks for English first, which is the mix this check must reject.
const languageOk = REGION_LANGUAGES[EXPECTED].includes(languages[0]);
const timezoneOk = timezone === REGION_TIMEZONES[EXPECTED];
// navigator.languages is the preference list, not the transmitted header; some browsers add
// fallback tags. Treat this as an estimate and read the real header from the captured request.
const acceptLanguageEstimate = languages
.map((tag, i) => (i === 0 ? tag : `${tag};q=${(1 - i * 0.1).toFixed(1)}`))
.join(',');
console.log(
JSON.stringify({ expected: EXPECTED, languages, timezone, acceptLanguageEstimate, languageOk, timezoneOk }, null, 2)
);
if (!languageOk || !timezoneOk)
throw new Error('capture would describe more than one region; fix inputs before capturing');
The primary entry is what makes the check meaningful. A list of en-US,de-DE also contains a German tag, yet the request still asks for English first, so a check that accepted any match anywhere in the list would pass exactly the mixed request it is meant to stop. On a constructed fixture where the browser declares en-US,en and reports America/New_York while the operator expects DE, the output names both failures:
{
"expected": "DE",
"languages": ["en-US", "en"],
"timezone": "America/New_York",
"acceptLanguageEstimate": "en-US,en;q=0.9",
"languageOk": false,
"timezoneOk": false
}
Error: capture would describe more than one region; fix inputs before capturing
The check does not prove where the request exits, because a page cannot read its own public address reliably. Keep the egress check separate: confirm the exit IP class on the operator side, through the proxy provider's own reporting or an explicit --proxy-ip value as the BotBrowser GeoIP guide describes, and write that value into the capture record. The in-page check covers the two inputs a page can observe; the operator-side check covers the one it cannot.
Run both before the first capture of a series, and again whenever a proxy, a profile or a language setting changes. A check that only runs at setup time stops describing the run as soon as someone edits a flag.
Two failure modes survive a written check if nobody reads its output. The first is a stale expectation: the operator changes the target region in the runbook but leaves EXPECTED at the previous value, so the check passes while both the script and the capture describe the wrong place. The second is a partial override: a proxy is changed while an explicit --bot-timezone from an earlier run stays in the launch arguments, and the derived timezone silently loses to the flag. Both are cheap to catch by printing the resolved values rather than only a boolean, which is why the example logs the language list, the timezone and the composed header before it throws.
A resolved-value log also gives the record something to quote. When a later reviewer asks why a run was marked aligned, the answer is the printed timezone and header from that run, not a recollection of the configuration. Keep that line in the run log and reference it from alignment_result so the two artefacts stay tied together.
Record the alignment with every accepted capture
A capture record is only comparable to another record if it states the properties that selected the response. The fields below keep the three alignment inputs together with the observation, so a reviewer can see whether a difference could plausibly belong to the region or must belong to the source.
| Record field | What it holds | Why it cannot be omitted |
|---|---|---|
expected_region | ISO 3166-1 alpha-2 code the run targets | Gives every later comparison a fixed subject |
exit_ip_class | Provider-reported exit address or its range | Separates "same region" from "same provider, other city" |
accept_language_estimate | Header estimated from navigator.languages | Shows what the page expects to send; read the real header from the captured request |
timezone_reported | IANA zone the browser returned | Detects a profile that contradicts its own proxy |
creative_ref | Publisher-side identifier, hash or file reference | Lets a diff be checked without redistributing the asset |
landing_url | Final URL after redirects | Separates negotiation from client-side redirection |
observed_at | Capture time in UTC | Orders observations and exposes flighting changes |
alignment_result | aligned or the named mismatch | Prevents an unaligned row from entering a series |
Two details in that table are easy to skip and expensive to reconstruct. The first is the difference between the requested URL and the final landing URL. A redirect chain can move a request after negotiation, and recording only the requested address hides the step that actually explains a difference. The second is the creative reference. Storing a hash or a publisher-side identifier keeps the record reviewable while respecting retention limits, and lets a later run tell "same creative" from "same campaign, new asset".
This is the same context discipline that the SERP rank capture model applies to organic results: an ordinal value is meaningless without the context signature that produced it. An ad placement is comparable under the same rule, even though the outputs differ.
Retention deserves its own decision, because creative references can carry more weight than they appear to. A hash of a creative asset is usually enough to tell two observations apart, but a landing URL can include identifiers that belong to a specific delivery request. Strip or hash parameters that carry a delivery identifier before storing the URL, and keep the unmodified value only if the platform's terms and your own retention policy both allow it. The rotating proxy profile record uses the same approach for session identity: keep a stable reference, discard the per-request detail that adds risk without adding comparability.
A series also needs a stated end condition. Ad campaigns change on schedules that a capture does not observe, so an open-ended series will eventually compare two states of the same campaign and call it a regional difference. Declare the observation window when the series starts, mark the last capture when the window closes, and treat later captures as a new series with its own alignment record. That single discipline prevents most of the conclusions that a long-running, unlabelled capture tends to produce.
Where ads.txt applies and where it stops
ads.txt is frequently misread in capture work, in both directions. The specification describes a mechanism that lets a publisher publicly declare who is authorized to sell its advertising inventory, and the file format records authorized systems and reseller relationships at the domain level. Checking a publisher's file therefore answers a supply-chain question: is this seller relationship declared.
It does not answer three questions that a capture workflow tends to ask of it. It does not say whether collection is permitted, because the specification is about authorized sale of inventory rather than about who may request a page. It does not establish that an observed impression was billable, because the file describes declared authorization rather than settlement. And it does not indicate how a creative is selected, because creative selection is not in the file's scope at all.
The useful application is narrower and still worth doing: when a capture is compared against a partner's reported placement, an ads.txt record can show whether the domain's declared seller list includes the system that the partner names. A disagreement there is a supply-chain question for the partner, not an inference about region targeting. Keep the two findings in separate fields so a later reader does not merge them into one verdict.
The file's own maintenance matters for that comparison. A record can be listed with a relationship type that changes how a system is treated, and the specification expects entries to be readable at a stable path on the domain being observed. If a domain returns a different file between two captures, the seller question has changed independently of any region question, and the two captures are no longer describing the same supply chain. Record the file's retrieval time and a hash of the body alongside the seller comparison, so a supply-chain change cannot later be mistaken for an ad-delivery change.
Resist the temptation to treat a declared seller as a quality signal. The specification describes authorization to sell inventory, and its scope is deliberately narrow: it is not a statement about creative quality, viewability, brand safety or the accuracy of any report. A partner conversation that needs those answers has to use the partner's own reporting, and an ads.txt lookup contributes exactly one fact to it.
Terms and scope before tooling
An ad-verification run touches a publisher's page, an ad system's response and possibly a measurement vendor's script, so three sets of terms apply to one capture rather than one. Each set decides more than the tooling options do. A capture that is technically achievable can still be outside what a platform permits, and no amount of profile or proxy configuration changes that.
Two practical boundaries follow. First, observation is not participation: a GET that records what a page returned is a different act from emitting the impression and viewability beacons a measurement script sends, and the second act is out of scope for a capture pipeline. Second, a difference between two captures is an observation, not a finding of fraud or misdelivery; those conclusions belong to the parties that hold the delivery logs and the contractual relationship.
The ecommerce monitoring boundary draws the same line for price and availability observation: a rendered page and a partner feed answer different questions and should not be merged into one series. Ad verification needs that separation even more sharply, because three independent systems can each change the answer.
FAQ
Does a region mismatch invalidate a capture even when the creative looks identical?
Yes. The capture no longer describes one coherent place, so it cannot be compared against a properly aligned capture from either region. Identical output makes the record look clean while leaving it unusable for comparison.
Can the alignment check replace an egress verification?
No. A page can read its declared language list and its reported timezone, but it cannot reliably read its own public address. Confirm the exit on the operator side and store that value in the capture record.
Does ads.txt show whether an ad placement is legitimate?
No. It declares which systems a publisher authorizes to sell inventory on that domain. It does not describe creative selection, billable delivery or permission to collect.
Does BotBrowser make a capture come from a specific country?
It applies a proxy exit and derives timezone, locale and language defaults from that address. Manual overrides of those three values are documented as requiring an ENT Tier1 license. The resulting capture still reflects whatever the ad system decides to return, and city-level derivation can be approximate.
Sources
- IAB Tech Lab ads.txt
- ads.txt specification version 1.1
- MDN Accept-Language
- RFC 9110 HTTP semantics
- BotBrowser GeoIP database
- BotBrowser timezone, locale and language
Practical conclusion
Treat the egress region, the reported timezone and the declared language list as one property of a capture. Run the alignment check before the first capture and again after any proxy, profile or language change. Store the three inputs with the creative reference, the final landing URL and the observation time, and mark a row aligned only when all three agree with the region the run claims to represent. Where a difference appears with the alignment intact, the explanation is on the source side, and the record is what makes that conclusion available.
Related Articles
Selenium Grid: Why Two Nodes Disagree
NetworkVerify Proxy Egress IP in BotBrowser Launcher
NetworkBrowser Proxy Configuration: SOCKS5, HTTP, and HTTPS Guide
ComparisonBrowser Core vs JavaScript Overrides for Fingerprint Consistency
DeploymentBrowser Fingerprint Protection for Web Data Collection
NetworkNetwork-Layer Browser Consistency for Privacy Protection
Run the same check in BotBrowser
Load a profile, keep the signals you just read about consistent inside the browser, and confirm the result on your own machine before a rollout.