← Geomaxxing · api

Geomaxxing API

The same numbers the site runs on, as JSON. One key, HTTPS, GET only.

Census age-by-sex counts for 124,602 districts across 107 markets, aggregated to a fixed 20–39 band, plus cost, safety, salary and traits for 983 cities. Every response says what level each figure was measured at.

Start

  1. Sign in and create a key on the Pro page. The key is shown once.
  2. Send it as a Bearer token.
  3. Call /api/v1 to see which endpoints your key opens.
curl -H "Authorization: Bearer gmx_live_..." \
     https://geomaxxing.org/api/v1

Keys are for server-side use. Anything pasted into a browser page is public, and so is a key committed to a repository. Revoke on the Pro page.

What a key opens

The API is for Pro accounts. Anyone signed in can create a key, and it starts working the moment the account unlocks.

WhatProWithout Pro
City measurements, country indicators, national age bandsyesno
Geomaxxing Score, rank and component breakdownyesno
District census layer, per unit, all 17 age bandsyesno
Dating-pool model and unmatched-women countsyesno
Fiance visa rate per 100,000 womenyesno
Per-market census sourceyesno
Requests per minute30
Requests per day2,000

Your account is read on every request, so unlocking Pro starts an existing key working without reissuing it, and a lapsed subscription stops it. A request from an account without Pro returns 402 payment_required.

Endpoints

All GET. All return JSON.

PathReturns
/api/v1Your tier, your remaining quota, the endpoint list.
/api/v1/metaField units and directions, level definitions, dataset counts. The scoring model on a Pro key.
/api/v1/countriesEvery market. ?status=live
/api/v1/countries/{iso}One market, with its 17-band national age arrays.
/api/v1/cities983 cities. ?iso= ?country= ?region= ?max_ratio= ?min_ratio= ?max_cost= ?min_safety= ?min_pop= ?sort= ?limit= ?offset=
/api/v1/cities/{id}One city, for example phl-metro-manila.
/api/v1/districts/{iso}The finest census level for that market. ?level= ?from= ?to= ?q= ?bands=0 ?limit= ?offset=
/api/v1/poolThe dating-pool model. ?iso= ?from= ?to=
/api/v1/fianceK-1 admissions per 100,000 women aged 20–39. ?iso=

Response shape

Every list answers in the same envelope. meta.filters echoes the parameters the server actually understood, so a filter that did nothing is visible rather than silent.

GET /api/v1/cities?iso=THA&sort=ratio&limit=2

{
  "object": "list",
  "data": [
    {
      "id": "tha-chiang-mai",
      "name": "Chiang Mai",
      "country": "Thailand",
      "iso": "THA",
      "ya": 94.1,
      "ya_level": "city",
      "cost": 730,
      "safety": 66,
      "score": 78,
      "rank": 41,
      "score_coverage": 1,
      "score_provisional": false
    }
  ],
  "meta": {
    "count": 1, "total": 12, "limit": 2, "offset": 0,
    "sort": "ratio", "tier": "pro",
    "filters": { "iso": "THA" },
    "version": "2026-08-30"
  }
}

ya is men per 100 women aged 20–39: lower means more women. ya_level says what it was measured at, in that market's own words: bashki, municipio, county, and 55 more. Do not match a list of them. Exactly one value is special: country means no finer measurement exists for that city, and it must not be presented as a city figure. /api/v1/meta returns the full observed list.

Python

The whole city table into a dataframe. Keep the key in an environment variable: a key in a notebook is a key in your shell history and in every copy of that notebook.

import os, time, requests, pandas as pd

S = requests.Session()
S.headers["Authorization"] = f"Bearer {os.environ['GEOMAXXING_KEY']}"
BASE = "https://geomaxxing.org/api/v1"

def get(path, **params):
    """One request, waiting out a 429 rather than failing on it."""
    while True:
        r = S.get(f"{BASE}/{path}", params=params)
        if r.status_code == 429:
            # Retry-After points at the REAL reset: the next minute for a burst,
            # the next UTC midnight if the daily quota is gone. Never guess it.
            time.sleep(int(r.headers.get("Retry-After", 10)))
            continue
        r.raise_for_status()
        return r.json()

def pull(path, **params):
    """Walk every page. meta.total says how many rows exist."""
    rows, offset = [], 0
    while True:
        body = get(path, **params, limit=1000, offset=offset)
        rows += body["data"]
        offset += len(body["data"])
        if offset >= body["meta"]["total"] or not body["data"]:
            return rows

cities = pd.DataFrame(pull("cities"))

# ya = men per 100 women aged 20-39. Lower means more women.
#
# ya_level says WHAT IT WAS MEASURED AT, in that market's own words: "bashki"
# in Albania, "municipio" in Mexico, "SA3 (statistical area)" in Australia.
# There are 58 such values, so DO NOT match a list of them. Exactly one value
# is special: "country" means no finer measurement exists for that city.
# Drop those before you rank, or you rank a country under a city's name.
sub = cities[cities["ya_level"].notna() & (cities["ya_level"] != "country")]

print(sub.nsmallest(10, "ya")[["name", "country", "ya", "ya_level", "cost"]])

That returns 907 rows of the 983: 62 are country-level and 14 carry no ratio at all. Neither is thrown away silently, and neither is guessed.

Districts, and what a big market costs

One market's finest census level, one row per unit. This is the call worth caching.

districts = pd.DataFrame(pull("districts/GBR", bands=0))
print(len(districts), districts["sex_ratio"].median())

The United Kingdom is 46,386 units. At 1,000 a page that is 47 requests, and the limit is 30 a minute, so the loop above sleeps through two blocks and finishes in about 80 seconds (measured). It is not stuck. Most markets are one or two pages.

bands=0 drops the 17-band age arrays and is roughly a tenth of the bytes. Leave it out when you want the full age structure per unit.

Rate limits

Two limits run at once: a per-minute burst and a daily quota. Both are counted per key, and the counting is approximate under a burst.

HeaderMeaning
RateLimit-LimitRequests allowed this minute.
RateLimit-RemainingWhat is left of it.
RateLimit-ResetSeconds until the minute rolls over.
X-Quota-LimitRequests allowed today.
X-Quota-RemainingWhat is left of it. Resets at 00:00 UTC.
Retry-AfterOn a 429 only. Seconds to wait.

A 429 carries Retry-After pointing at the real reset: the next minute for a burst, the next UTC midnight for a quota. Read it rather than retrying on a fixed timer.

Repeatedly sending a key that does not work is refused separately, with too_many_bad_keys, and that limit is per network rather than per key. If a key stops working, create a new one instead of retrying the old one.

A second limit runs at the network edge, before any of this: 5 requests per 10 seconds per IP. It returns Cloudflare's own block page rather than JSON, so a client that receives HTML from this API is being shaped there. Stay under 30 requests a minute and you will not meet it.

Errors

Every failure returns {"error":{"code","message","docs"}}. Branch on code: the message is prose and will be reworded.

StatusCodeCause
400invalid_isoNot an ISO 3166-1 alpha-3 code. Three letters, for example THA.
400invalid_bandfrom is not below to.
401no_credentialNo key was sent.
401invalid_keyUnknown, malformed or revoked. The three are not distinguished.
402payment_requiredThe account is not on Pro.
404not_foundNo such city or market.
404no_district_layerThat market publishes nothing below the national level.
405method_not_allowedThe API is read-only.
429rate_limitedBurst or daily quota. scope says which.
429too_many_bad_keysToo many invalid keys from one network. Stop retrying a key that does not work.
503api_disabledThe API is not open yet.
503data_unavailableA dataset could not be read. Retry.

A missing measurement is null, never a substituted average and never zero. A country with no district layer is a 404 with a reason, never an empty list.

Keys and terms