teryt24.pl

teryt24.pl · API reference

Integration guide

This guide goes from zero to a working integration on the free teryt24.pl REST API: address autocomplete in a form, validation and geocoding, postal codes for deliveries, and points on a map. No key, no registration — the first query takes 10 seconds.

Every example is live: pasted into a terminal or an editor, it answers exactly as shown. The full parameter lists and response schemas have their own pages — each step links to its own.

First query in 10 seconds

Three endpoints cover most uses: localities, address validation, postal codes.

curl "https://teryt24.pl/v1/localities?q=warsz&limit=5"
curl "https://teryt24.pl/v1/geocode?address=Warszawa,%20ul.%20Anilinowa%202"
curl "https://teryt24.pl/v1/postal-codes?q=00-002"

The family pages describe the parameters and responses: localities, geocoding, postal codes.

Autocomplete in a form

One query per typing pause: cities, localities, streets and postal codes at once, with a display-ready label.

const input = document.querySelector("#city");
let timer = null;
input.addEventListener("input", () => {
  clearTimeout(timer);
  timer = setTimeout(async () => {
    const url = "https://teryt24.pl/v1/autosuggest?q=" +
      encodeURIComponent(input.value) + "&layers=city,locality&limit=8";
    const res = await fetch(url);
    const data = await res.json();
    // data.suggestions: [{ type, id, label, ... }] — label is display-ready,
    // id (SIMC / SYM_UL / postal code) is the key for the follow-up call
    showSuggestions(data.suggestions.map((s) => s.label));
  }, 200); // debounce — query on the typing pause, not on every keystroke
});

The layers= parameter narrows the layers; the ranking blends the match tier with real-world prominence (population, address points). Full parameters: autocomplete.

Address validation & geocoding

Three calling forms — fielded, by name, one free-text string — and one way to branch: on the result field.

import httpx

r = httpx.get("https://teryt24.pl/v1/geocode",
              params={"address": "Wrocław-Fabryczna, ul. Eugeniusza Horbaczewskiego 35"},
              timeout=10)
data = r.json()

if data["result"] == "exact":
    point = data["matches"][0]         # lat, lon, number, streetId, postalCode
elif data["result"] == "street":
    nearest = data["nearestNumbers"]   # number not in PRG — closest registered ones
elif data["result"] == "street-ambiguous":
    candidates = data["streets"]       # several streets match — re-query with street=<id>
elif data["result"] == "locality-ambiguous":
    shortlist = data["localities"]     # several localities match — re-query with sym=<sym>
else:                                  # "none" — genuinely unknown address
    pass

Watch out for divided cities: the name "Warszawa" matches ~90 SIMC records — the by-name mode then answers locality-ambiguous with a candidate shortlist; re-query with sym=. Full parameters: geocoding.

Postal codes for deliveries

One hundred codes in one request — bulk returns exactly what a hundred separate GETs would, minus the round-trips.

r = httpx.post("https://teryt24.pl/v1/postal-codes/bulk",
               json={"codes": ["00-002", "59-700"]},
               timeout=30)
for row in r.json()["results"]:
    if row["status"] == "ok":
        print(row["code"], "->", ", ".join(x["city"] for x in row["rows"]))

A single code has its own /v1/postal-codes/{code} detail with the PRG (GUGiK) data. Full parameters: postal codes.

A ready-made address form

The whole workflow — locality and street suggestions, validation, a review of the result — in one dependency-free file. The fastest route to a working form.

<link rel="stylesheet" href="/assets/address-widget.css">
<script src="/assets/address-widget.js"></script>
<div id="address"></div>
<script>
  Teryt24AddressForm.mount(document.getElementById("address"), {
    onValidated: function (result) {
      // result.address carries the identifiers: locality SYM, street SYM_UL,
      // postal code, coordinates. result.verdict is the answer's quality.
      console.log(result.verdict, result.address.locality.sym);
    },
  });
</script>

The widget stands alone: the labels are configurable, and the result arrives in onValidated — with the structured address and the identifiers (SYM, SYM_UL, postal code, coordinates). See it working. The data contract: address validation.

Bulk address normalisation from CSV

Upload a file of addresses, name its columns, and get a result file back: your original columns unchanged, plus the confirmed address, the match kinds and the reasons the rows that were not confirmed were not confirmed.

curl -X POST "https://teryt24.pl/v1/bulk/addresses?locality=miejscowosc&street=ulica&number=nr&id=id" \
  -H "X-API-Key: t24_..." -H "Content-Type: text/csv" \
  --data-binary @adresy.csv
# -> 202 { "id": "...", "status": "queued", "rows": 2 }

curl "https://teryt24.pl/v1/bulk/jobs/<job-id>" -H "X-API-Key: t24_..."
# -> { "status": "done", "rows": 2, "processed": 2, "matched": 1, "unmatched": 1, ... }

curl -OJ "https://teryt24.pl/v1/bulk/jobs/<job-id>/result.csv" -H "X-API-Key: t24_..."

It requires an API key (limits & keys) — a job holds personal data on the instance, so it is not for anonymous calls. A value the register did not confirm stays EMPTY in the file: a guessed address in a spreadsheet reads like a fact. The upload and the result are deleted after 24 hours. Full parameters: bulk normalisation.

The registry change feed

GUS publishes the diffs between catalogue states — the feed serves them as changes, with the catalogue state each belongs to.

curl "https://teryt24.pl/v1/changes?dataset=simc&since=2026-10-01&limit=5"
# -> { "changes": [ { "type": "renamed", "stanBefore": "2026-10-12",
#                    "stanAfter": "2026-11-09",
#                    "nameBefore": "...", "nameAfter": "..." } ],
#      "meta": { "coverage": { "from": "...", "to": "...", "complete": false } } }

Three meanings to know: a deletion means the object LEFT the catalogue (not that the address became invalid); the type reordered is a FORMATTING change (only the part of the street name that decides the alphabetical position) and can be filtered out; the dates are the catalogue states, not the moment the file was published. Full parameters: the change feed.

Points on a map — GeoJSON

format=geojson turns the response into an RFC 7946 FeatureCollection — geometries go straight into Leaflet or MapLibre.

const res = await fetch("https://teryt24.pl/v1/localities?q=warsz&limit=50&format=geojson");
const geo = await res.json();  // RFC 7946 FeatureCollection — points as [lon, lat]
L.geoJSON(geo).addTo(map);     // one marker per locality, row fields in feature.properties

The same parameter works on geocode, reverse-geocode and postal codes in nearest mode. Full parameters: localities.

Retrying 429s and the ETag cache

The limiter answers with a Retry-After header (seconds) — read it and wait instead of retrying immediately.

async function getJson(url, attempt = 0) {
  const res = await fetch(url);
  if (res.status === 429 && attempt < 3) {
    const wait = Number(res.headers.get("Retry-After") || "1");
    await new Promise((resolve) => setTimeout(resolve, wait * 1000));
    return getJson(url, attempt + 1);  // the limiter answered — wait, then retry
  }
  return res.json();
}

Successful GETs carry an ETag and a 1-day Cache-Control — a repeat with If-None-Match gets a 304 without a body. An API key brings a rate-limit bucket of its own: limits & keys.

All /v1 endpoints return JSON, speak CORS and are covered by the openapi.json document. Interactive version of this reference: /en/docs.

Limits: 60 requests/min per IP without a key; an API key (the X-API-Key header) gets its own bucket. Over the limit the endpoints answer HTTP 429 with a Retry-After header. Details: limits & keys · usage policy.

Attribution is a condition of use. When you present results in an application, website, map or report, display: Data: GUS TERYT, PRG address points (GUGiK), GeoNames postal codes — via teryt24.pl with a link to the usage policy. Server-side-only use is exempt.