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
passWatch 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.propertiesThe 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.
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.