---
url: https://docs.dataflair.ai/dataflair/toplist-api/geo-targeting.md
description: >-
  The geo object and the resolution algorithm your integration must implement
  before rendering.
---

# Geo-targeting & compliance

## The geo family model

One **template** (a toplist "concept," e.g. "Best Casinos") can back multiple **toplist** rows —
different geos (country / market / global), and also multiple live rows for the **same** geo when
those are separate page embeds (each with its own permanent toplist ID). 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](/dataflair/toplist-api/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](/dataflair/toplist-api/toplists-endpoint)). Stop after the first page and you'll
silently work from an incomplete family.

## The `geo` object

| `geo_type` | `code` 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"` | `null` | `null` |

`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**: exactly one `geo_type="country"` row whose `code` equals the visitor's country → pick it.
   More than one exact country match → pick nothing, log it. Never guess.
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, exactly one `geo_type="global"` row in the family → pick it.
   More than one global → pick nothing, log 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. Prefer pinned
embeds by toplist ID when a site has multiple live siblings for the same audience.

### 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.
