Solution spec · Talent outreach · Global whitelist

Confirming candidate country without location permissions

Prompt We run global outreach for talent campaigns and must confirm the region applicants contact us from, for legal purposes, against a country whitelist. Location permission popups are too invasive — country-level only, no GPS. Compare what the candidate declares with the country their connection resolves to. Publish the solution as a simple HTML page on Cloudflare Pages.
Permission prompt
None — server-side only
Signal used
Country, ISO 3166-1 α-2
Check point
Deliberate click + final submit
Auto-reject
Never — review only

01 The approach: a two-stage check

Capture the network country when the candidate clicks Continue, then ask them to declare their country later in the application, and compare the two on submission. The browser Geolocation API and GPS are never used, so there is no permission popup — yet the comparison still separates honest answers from convenient ones.

STAGE 0

Signed link in the outreach email

Each email carries a candidate-specific signed link that binds the eventual submission to the intended recipient. No check happens on link open — email security scanners and mail proxies automatically open links from locations unrelated to the recipient.

STAGE 1

Landing page

The page shows the role, the neutral disclosure, and a single Continue button.

STAGE 2

Continue click → server-side capture

The click fires a POST to the backend. The server resolves the connection's IP to a country, opens an application session, and reveals nothing to the candidate — not the detected country, not the whitelist, not the scoring rule. Avoiding the initial question also keeps their later answer unprimed.

STAGE 3

Application form — declaration

Later in the flow the form asks: “Which country are you currently located in?” followed by a written attestation. The candidate answers freely, with no hint that a network check is waiting.

STAGE 4

Compare on submit

Declared country vs. captured country vs. whitelist → one of five statuses is recorded. The raw IP is discarded as soon as the country is derived.

Why two stages Asking “where are you?” and then checking would push people toward the answer they think you want. Capturing the network country first — silently but disclosed — keeps the declaration honest while the process stays transparent in the privacy notice. And because the check runs only on a deliberate POST, automated link-openers never pollute the signal.

02 Decision rules

The comparison is a plain table — no triangulation, no scoring model. Only the country pair and the whitelist matter.

Resolved (network)DeclaredOutcomeAction
WhitelistedSame country Verified Continue normally
WhitelistedDifferent whitelisted country Review Ask candidate to confirm; never auto-reject
Outside whitelistWhitelisted country Possible mismatch Review trigger
Outside whitelistOutside whitelist Ineligible Fails regional eligibility
Unknown / VPN / proxyAny Unverified Retry or manual review

Do not reveal which country was detected, and never accuse or reject automatically on a mismatch — VPNs, travel, corporate networks and mobile roaming all produce legitimate discrepancies.

03 Result taxonomy

One enum recorded per submission, alongside both country codes:

type LocationCheck =
  | "MATCH"
  | "MATCH_LOW_CONFIDENCE"
  | "MISMATCH"
  | "UNVERIFIABLE"
  | "INCOMPLETE";
MATCHDeclared and resolved country agree.
MATCH_LOW_CONFIDENCEThey agree, but VPN / proxy indicators are present. Review only if the legal sensitivity warrants it.
MISMATCHDeclared differs from resolved. A review trigger — never an automatic rejection.
UNVERIFIABLENo resolvable country, or Tor / unknown (XX, T1). Retry or request confirmation.
INCOMPLETEDeclared country missing. Require the field before submission.

04 Implementation notes

The core check is a few lines. Server-side only — no browser Geolocation API, no GPS, no client-side geo API:

// Cloudflare Worker — swap the first line for your host's header if not on CF
const resolvedCountry =
  typeof request.cf?.country === "string"
    ? request.cf.country.toUpperCase()
    : null;

const claimedCountry =
  form.claimedCountry?.trim().toUpperCase() || null;

const locationCheck =
  !claimedCountry                            ? "INCOMPLETE"
  : !resolvedCountry ||
    resolvedCountry === "XX" ||
    resolvedCountry === "T1"                 ? "UNVERIFIABLE"
  : claimedCountry === resolvedCountry        ? "MATCH"
                                              : "MISMATCH";

Where the country comes from

SourceWhen to use
request.cf.country (Cloudflare) Easiest if the site is already behind Cloudflare — country-level IP geolocation on all plans. It is an estimate and should not be the sole signal for compliance-critical decisions.
Platform header Vercel, Netlify or CloudFront already supply the request country as a header on their platforms — read it instead.
MaxMind GeoLite2, self-hosted If the host supplies nothing, resolve locally.
Third-party geo API Avoid — do not ship every applicant's IP to an additional processor unless necessary.

05 What to store

Data minimisation: country is all that is needed, so country is all that is kept. The raw IP is discarded once the country is derived.

Store

{
  "applicationSessionId": "sess_456",
  "candidateToken": "cand_123",
  "resolvedCountry": "IN",
  "claimedCountry": "IN",
  "countryStatus": "MATCH",
  "capturedAt": "2026-09-10T12:30:00Z"
}

Never store

  • City, coordinates or any precision below country
  • ISP or network operator name
  • Device fingerprints
  • Permanent IP history
  • Hashing an IP does not make it anonymous
  • If the raw IP is genuinely needed for fraud investigation: store it separately, access-restricted, with a short deletion window

06 Candidate-facing copy

The disclosure stays general. It must be transparent enough for the privacy notice to hold up, but must not reveal the whitelist, the matching rule, or the stage at which comparison happens — and it must not nudge the candidate toward any particular answer.

Landing-page disclosure — shown before Continue “When you continue, we process limited technical information to assess regional eligibility and protect the integrity of our recruitment process. See our Privacy Notice.”
Form attestation — below the country field “I confirm that I am currently located in the country selected above and that the information provided is accurate.”
Review-trigger message — sent only when a mismatch needs confirming “Your application was submitted through a network associated with a different country. This may happen when using a VPN, corporate network or while travelling. Please confirm your current country.”

The privacy notice should identify the purpose, the data used, the retention period, the processors, and the review / correction process. Whether consent or another lawful basis is required depends on the jurisdictions involved — have employment and privacy counsel approve the final wording, especially where the result can influence applicant eligibility.

07 Limits and cautions

This can establish “The application session originated from a network associated with India.”
This cannot establish “The applicant legally resides in India.”

That asymmetry is the whole design: a useful inconsistency signal, captured without invasive permission prompts, honest in what it claims, and paired with a human review step instead of an automated gate.