Skip to content

Toplists endpoints

GET /api/v1/toplists

Returns every toplist visible to your authenticated site, paginated (default 15 per page, max 100, ordered newest-updated-first). Two optional, independent filters:

  • template_id: the filter you'll use most. Every toplist sharing one template_id is one geo family: one row per market, per country, and/or a global/rest-of-world variant, all built from the same template. This is how you fetch "everything I might need to show for this toplist concept" ahead of any specific visitor. If a family has more than 15 members, pass a higher per_page (up to 100). per_page caps at 100 per request, so a family larger than that is never returned in one call regardless of what you pass. Follow the links/meta pagination fields (see the response-wrapping note below) across successive requests, incrementing page, until links.next is null or meta.current_page == meta.last_page. Stopping after the first page silently drops the rest of the family, which can make a visitor whose country is only on a later page fall through to a global/no-match render.

    bash
    curl "https://{your-tenant-domain}/api/v1/toplists?template_id=55" \
      -H "Authorization: Bearer <token>" \
      -H "Accept: application/json"
  • geo_type + geo_code: filter to toplists matching a specific market or country directly, if you already know exactly which geo variant you want and don't need the whole family.

    bash
    curl "https://{your-tenant-domain}/api/v1/toplists?geo_type=country&geo_code=IN" \
      -H "Authorization: Bearer <token>" \
      -H "Accept: application/json"

GET /api/v1/toplists/by-slug/{slug} and GET /api/v1/toplists/{id}

Fetch exactly one toplist, by its slug or numeric ID respectively. Both return the identical per-toplist JSON shape documented below, or a 404 not_found if it doesn't exist or isn't currently available to your site.

Response shape

Every toplist, from any of the three endpoints above, has this shape:

jsonc
{
  "type": "toplist",
  "id": 482,
  "name": "Best Casinos India",
  "status": "published",
  "locked": false,
  "version": "20260214100300",
  "owner": { "id": 12, "name": "Jane Doe" },
  "createdAt": "2026-02-14T10:03:00+00:00",
  "updatedAt": "2026-07-01T09:12:44+00:00",
  "template": {
    "type": "listTemplate",
    "id": 55,
    "name": "Best Casinos",
    "productTypeId": 2,
    "productType": "Casino",
    "listClassificationTypeId": 1,
    "listClassificationType": "Best Of"
  },
  "site": { "id": 7, "domain": "example-operator.com" },
  "geo": {
    "geo_type": "country",
    "name": "India",
    "code": "IN",
    "coveredCountries": null
  },
  "slug": "best-casinos-india",
  "currentPeriod": "July 2026",
  "publishedAt": "2026-07-01T09:12:44+00:00",
  "shortcode": null,
  "items": [
    {
      "type": "topListItem",
      "id": 9931,
      "position": 1,
      "isLocked": false,
      "dealId": 204,
      "brand": {
        "type": "brand",
        "id": 341,
        "externalId": "brand-341",
        "name": "Lucky Spin Casino",
        "slug": "lucky-spin-casino",
        "rating": 4.6,
        "logo": {
          "rectangular": "https://cdn.dataflair.ai/brands/341/logo-rect.png",
          "square": "https://cdn.dataflair.ai/brands/341/logo-square.png",
          "backgroundColor": "#101820"
        },
        "licenses": ["Curacao"],
        "paymentMethods": ["UPI", "Visa", "Mastercard"],
        "restrictedCountries": ["United States", "France"],
        "classificationTypes": ["Casino"],
        "languages": { "website": ["en", "hi"], "support": ["en"], "livechat": ["en"] }
      },
      "offer": {
        "type": "offer",
        "id": 5820,
        "offerTypeId": 1,
        "offerTypeName": "Welcome Bonus",
        "offerText": "100% up to ₹20,000 + 100 Free Spins",
        "currencies": ["INR"],
        "has_free_spins": true,
        "bonus_wagering_requirement": 35,
        "bonus_expiry_date": "2026-12-31",
        "bonus_code": "LUCKY100",
        "minimum_deposit": 500,
        "max_payout": null,
        "max_bonus_amount": "20000.00",
        "is_sticky_bonus": false,
        "minimum_odds": null,
        "free_bet_value": null,
        "stake_returned": null,
        "bet_type": null,
        "tournament_ticket_value": null,
        "rakeback_percentage": null,
        "free_tickets": null,
        "geos": { "countries": ["India"], "markets": [] },
        "trackers": [
          {
            "id": 88,
            "campaignName": "India Launch",
            "trackerLink": "https://track.example.com/click?c=88",
            "tcLink": "https://example-operator.com/terms",
            "pageType": "LP",
            "geos": { "countries": ["India"], "markets": [] }
          }
        ]
      }
    }
  ]
}

Notes worth calling out explicitly:

  • status is always "published". The API only ever serves toplists it considers live. There's no draft/paused status value you need to branch on.
  • items is [], not omitted or null, when a toplist's live edition currently has zero items in it, a valid, safe-to-iterate response. This is different from a toplist having no live edition at all: the single-item endpoints (by-slug / {id}) 404 in that case instead of returning 200 with items: [], and such a toplist never appears in GET /api/v1/toplists either.
  • locked (toplist-level) and isLocked (per-item) both reflect editorial state, not geo, unrelated to the geo-targeting resolution algorithm.
  • pageType (inside each tracker) is normally one of a small set of internal short codes (LP, NDP, GP, RMP, RP, DEFAULT), but isn't a closed/validated enum. Treat it as an opaque internal code you pass through or ignore, never show it to a user directly.
  • shortcode is currently always null. The field exists on the resource but isn't backed by any stored value or generation logic yet. Don't build against it until your DataFlair contact confirms it's populated.
  • Money/precision offer fields are quoted strings, not JSON numbers. max_bonus_amount, max_payout, minimum_odds, free_bet_value, tournament_ticket_value, and rakeback_percentage (when non-null) all render like "20000.00": parse them with a decimal-safe type, not a native float. Plain counts such as minimum_deposit and free_tickets are real JSON integers and don't have this quirk.
  • This endpoint wraps its payload in Laravel's {"data": ...} envelope, with links/meta siblings around the data array: {"data": [...], "links": {...}, "meta": {"current_page", "per_page", "total", "last_page", ...}}. The two single-item endpoints wrap the same per-toplist shape in {"data": { ... }} too, just a single object, no links/meta alongside it. Always read from response.data, never the response root. This wrapping is specific to these resource endpoints: the auth exchange and every error response return their fields directly at the response root, with no data envelope.

See Geo-targeting & compliance for what the geo object means and how to use it before rendering anything.