Skip to content

Geo-targeting & compliance

The geo family model

One template (a toplist "concept," e.g. "Best Casinos") can back multiple toplist rows, each targeting a different audience: one per country, one per market (a named group of countries), and/or one global/rest-of-world variant. All of them share the same template.id. GET /api/v1/toplists?template_id=X returns the whole family, fetched ahead of time per the Integration model. This endpoint is paginated too (default 15 per page, max 100): a family with more than 15 members needs a higher per_page, and a family larger than 100 needs multiple requests. Page through links/meta until exhausted (see the pagination note on Toplists endpoints). Stop after the first page and you'll silently work from an incomplete family.

The geo object

geo_typecode is...coveredCountries is...
"country"that country's ISO 3166-1 alpha-2 code (e.g. "IN", "GB")null
"market"the market's own short code (e.g. "EU", "NORDICS")every member country's alpha-2 code, as an array (e.g. ["DE","FR","IT",...])
"global"nullnull

name (e.g. "India", "Europe", "Global") is a display string only. Never match visitor geo against it. Always match on code / coveredCountries.

The alpha-2 format here is the same one both common visitor-geo sources already emit: Cloudflare's CF-IPCountry header and MaxMind's country.iso_code field both report uppercase ISO 3166-1 alpha-2 directly. No translation table is needed between either source and this code field.

The resolution algorithm your integration must implement

DataFlair does not run this for you. You fetch the data above ahead of time, and you implement this algorithm on your side, at the moment you actually render something to a real visitor.

Layer 1: Render safety gate (mandatory, every render)

Whichever toplist you're about to show, whether you picked it directly (e.g. you hard-coded "show toplist slug X on this page") or arrived at it via the optional Layer 2 cascade below, run this check first, every single time, before rendering anything:

function shouldRender(toplist, visitorCountryAlpha2):
    geo = toplist.geo
    if geo.geo_type == "global":        return true        // explicit "everyone" editorial choice
    if visitorCountryAlpha2 is null:    return false        // can't verify -> default-deny
    if geo.geo_type == "country":       return geo.code == visitorCountryAlpha2
    if geo.geo_type == "market":        return visitorCountryAlpha2 in geo.coveredCountries
    return false

No match means render nothing. Not an error page, not a fallback to some other toplist: empty output. Concretely: if you have a page pinned to an India-geo toplist and a UK visitor lands on it, that visitor sees nothing from that toplist, never the India-market brands.

To get visitorCountryAlpha2, try in order: a CF-IPCountry header (if you're behind Cloudflare) → an X-Geoip-Country header (common on other CDNs/reverse proxies that inject a geo header without being Cloudflare) → a GeoIP library/service of your choice → null if nothing resolves. Check both header names before falling back to a fresh GeoIP lookup: a visitor sitting behind a non-Cloudflare proxy that sets X-Geoip-Country already has a resolved geo available for free, and skipping straight to a GeoIP lookup wastes that free signal and can give a different, less accurate answer. Two details worth handling defensively regardless of source:

  • Treat known "unknown" sentinel values (Cloudflare emits XX for undetermined and T1 for Tor exit traffic) as unresolved: fall through the same as a missing value, don't compare them as if they were real country codes.
  • Uppercase and trim whatever you get before comparing: don't assume the source always returns clean, consistently-cased input.

Layer 2: Auto-select cascade (optional convenience)

If you want one embed to automatically adapt across a whole template family, rather than manually placing one embed per country/market, fetch the family (?template_id=X) and pick a candidate:

  1. Exact match: a geo_type="country" row whose code equals the visitor's country → pick it.
  2. Covering market: else, exactly one geo_type="market" row whose coveredCountries contains the visitor's country → pick it. If more than one market covers that visitor, treat it as ambiguous: pick nothing, log it. Never guess which one.
  3. Explicit global: else, a geo_type="global" row in the family, if one exists → pick it.
  4. Otherwise: no candidate.

Whatever this cascade picks (if anything) still has to pass Layer 1 before you render it. Layer 2 only narrows down which row to check next. It never replaces the check itself.

Caching

If you cache rendered output (a full-page cache, a CDN, anything that could serve one visitor's render to a different visitor later), any render whose outcome depended on the visitor's geo must be excluded from that cache, or scoped by country, and never served as one-size-fits-all. This is your responsibility; DataFlair has no visibility into your caching layer.

Compliance rationale

Geo-targeting here exists to enforce a regulatory boundary, not to personalize content. The underlying rule: if a visitor's location can't be shown brands compliantly for their jurisdiction, they see nothing. Never a best-effort guess at "the closest match."

  • Default-deny. An unresolved visitor country never matches a country or market toplist. Only an explicit global row renders unconditionally, and that's an editorial choice someone made when building that specific template family, not something the system does automatically to fill a gap.
  • A missing global/rest-of-world variant is not a bug. A template family with no global row is a valid, deliberate choice: it means "show this only in the specific markets/countries we've explicitly targeted, and nothing anywhere else." Don't build a fallback around this; treat "no candidate" as a legitimate outcome.

This algorithm restates DataFlair's internal geo-targeting contract for an external audience. It's independently worded from that internal document, which remains DataFlair's own source of truth on its side.