Address validation in one call
GET /v1/validate answers the question a form asks, not just a database question: “what does this address mean, how sure are we, and what is still missing?”. It returns the structured address (locality, street, house number, unit, postal code, coordinates) with the registry identifiers behind it (SYM, SYM_UL, TERC codes), a match kind per field (exact / normalized / partial / ambiguous / not-found), a verdict (valid / corrected / ambiguous / incomplete / invalid) and the list of discrepancies with a code for each.
Three input forms, as in GET /v1/geocode: fielded (sym + street + number), by locality name (locality), or one free-text string (address). One difference: the house number is OPTIONAL here — a missing number is an INCOMPLETE address, not a malformed request. The building verdict comes from the same pipeline as /v1/geocode, so the two endpoints can never disagree.
Two rules the endpoint never breaks: the postal code is ALWAYS the matched address point's own code (never guessed from a locality or a proximity), and a component the registries do not carry stays null and says so.
GET /v1/validate
One call: the validated address with a match kind per field. The postal code is verified against the address point, never overwritten.
Try it live: /v1/validate?sym=0965016&street=06114&number=5
| Name | Required | Description |
|---|---|---|
address | — | Free-text Polish address ('Wrocław-Fabryczna, ul. Eugeniusza Horbaczewskiego 35'), parsed into locality/street/number internally and resolved through the same pipeline as GET /v1/geocode. Mutually exclusive with sym, number, street, streetName, locality and postalCode. |
sym | — | Locality SYM (7 digits, from /v1/localities). Required unless `address` or `locality` is given; mutually exclusive with locality. An unknown SYM answers 404. |
locality | — | Locality/city NAME (diacritics-insensitive) instead of a SYM — resolved through the locality index. Several matches → verdict 'ambiguous' with a population-ordered shortlist in `candidates.localities` (capped at 50, `localitiesTruncated` when cut). Mutually exclusive with sym and address. |
postalCode | — | The caller's claim about the postal code. With `locality` it also narrows the candidates (the code's officially served localities); in every form it is VERIFIED against the matched building point's own code — a disagreement is reported as the `postal_code_mismatch` issue and the register's code is returned. Requires sym or locality. |
number | — | House number ('21A'). OPTIONAL here (unlike GET /v1/geocode): a missing number makes the address INCOMPLETE, and the response says so instead of rejecting the request. |
street | — | Street SYM_UL (5 digits, from /v1/streets). Mutually exclusive with streetName. |
streetName | — | Street name prefix (diacritics-insensitive; 'rynek' finds 'ul. Rynek'). A typed 'ul.'/'al.' prefix is stripped before the search and kept as the claimed street type. Several matches → verdict 'ambiguous' with candidates in `candidates.streets`. |
limit | — | Maximum matches (1–50). |
Example
A confirmed address (fielded)
curl "https://teryt24.pl/v1/validate?sym=0965016&street=06114&number=5"Response:
{
"verdict": "valid",
"address": {
"locality": {
"sym": "0965016", "name": "Opole", "type": "96", "typeLabel": "miasto",
"province": "OPOLSKIE", "county": "Opole", "municipality": "Opole",
"provinceCode": "16", "countyCode": "61", "municipalityCode": "01",
"lat": 50.679359517696255, "lon": 17.917784269176924,
"coordSource": "prg", "population": 127676, "populationSource": null
},
"street": { "id": "06114", "name": "ul. Grobla", "cecha": "ul." },
"houseNumber": "5", "unit": null, "postalCode": "45-236",
"lat": 50.69898395560351, "lon": 17.959149341026354
},
"match": { "locality": "exact", "street": "exact", "houseNumber": "exact", "postalCode": "exact" },
"issues": [],
"sources": { "locality": "teryt", "street": "ulic", "addressPoint": "prg", "postalCode": "prg" }
}One free-text string — the street name normalized
curl "https://teryt24.pl/v1/validate?address=Opole,%20ul.%20Grobla%205"Response (truncated — first rows shown):
{
"verdict": "corrected",
"address": {
"locality": { "sym": "0965016", "name": "Opole", "type": "96", "typeLabel": "miasto" },
"street": { "id": "06114", "name": "ul. Grobla", "cecha": "ul." },
"houseNumber": "5", "unit": null, "postalCode": "45-236",
"lat": 50.69898395560351, "lon": 17.959149341026354
},
"match": { "locality": "exact", "street": "normalized", "houseNumber": "exact", "postalCode": "exact" },
"issues": [
{
"code": "street_normalized", "field": "street",
"message": "The street name was matched after normalization (case, diacritics, punctuation or the street-type prefix).",
"expected": "ul. Grobla", "actual": "Grobla"
}
],
"sources": { "locality": "teryt", "street": "ulic", "addressPoint": "prg", "postalCode": "prg" },
"query": { "city": "Opole", "street": "Grobla", "number": "5", "numbers": ["5"], "range": false, "cityResolved": true }
}An ambiguous locality name
curl "https://teryt24.pl/v1/validate?locality=Nowa%20Wie%C5%9B&number=5"Response (truncated — first rows shown):
{
"verdict": "ambiguous",
"address": { "locality": null, "street": null, "houseNumber": null, "unit": null, "postalCode": null, "lat": null, "lon": null },
"match": { "locality": "ambiguous", "street": "not-found", "houseNumber": "ambiguous", "postalCode": "not-found" },
"issues": [
{ "code": "locality_ambiguous", "field": "locality", "message": "Several localities carry that name. Let the user pick one, or narrow the query with the postal code." }
],
"candidates": {
"localities": [
{ "sym": "0057477", "name": "Nowa Wieś", "type": "01", "typeLabel": "wieś", "province": "MAŁOPOLSKIE", "county": "oświęcimski", "municipality": "Kęty", "provinceCode": "12", "countyCode": "13", "municipalityCode": "04", "population": 3648 },
{ "sym": "0316909", "name": "Nowa Wieś", "type": "01", "typeLabel": "wieś", "province": "MAŁOPOLSKIE", "county": "myślenicki", "municipality": "Dobczyce", "provinceCode": "12", "countyCode": "09", "municipalityCode": "01", "population": 413 }
],
"localitiesTruncated": true
}
}A postal code that disagrees
curl "https://teryt24.pl/v1/validate?sym=0965016&street=06114&number=5&postalCode=00-950"Response (truncated — first rows shown):
{
"verdict": "corrected",
"address": { "postalCode": "45-236" },
"match": { "locality": "exact", "street": "exact", "houseNumber": "exact", "postalCode": "partial" },
"issues": [
{
"code": "postal_code_mismatch", "field": "postalCode",
"message": "The claimed postal code differs from the code the matched building point carries. The register's code is returned.",
"expected": "45-236", "actual": "00-950"
}
]
}| Status | Description |
|---|---|
200 | The validation answer: `verdict` (valid | corrected | ambiguous | incomplete | invalid), the structured `address`, the per-field `match` kinds, the `issues` that explain every discrepancy, the `candidates` for an unresolved ambiguity, and `sources` (which dataset each field came from). |
400 | Invalid query parameters. |
404 | Unknown locality SYM. |
429 | Rate limit exceeded (60 requests/min/IP) or a plan's monthly quota exhausted. Includes `Retry-After` and `X-RateLimit-*` headers; the body's `code` distinguishes the two kinds. |
500 | Unexpected server failure (e.g. the database is unreachable). The body is deliberately generic — error detail lives in the server logs, never in the response. |
503 | Address dataset not available (POST /admin/ingest-prg). |
A ready-made implementation of this whole workflow (locality and street suggestions, validation, a review of the result) ships as a widget: /assets/address-widget.js — see it working.
Browse the data
The same data has browsable pages — locality, street and postal-code cards:
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.