Addresses
Lithuanian addresses and postal codes from the state register
Addresses
Address search backed by the Lithuanian Address Register (Registrų centras): roughly 1.1 million address points, 1.15 million flats, 60 000 streets, 26 000 localities and every postal code.
The data is a mirror of the state register, refreshed monthly. Nothing here is guessed — if an address is not in the register, the API returns nothing rather than inventing it. That matters for a shipping address: a plausible-looking postal code that does not exist fails silently, days later, at the courier.
Addresses are a paid add-on: €2 / month per partner. Enable it in the
Partners Portal → Billing → Services. Without it the
endpoints answer 403 with data.feature: "address_registry".
A trimmed, unauthenticated subset exists for sign-up forms that have no token yet —
search, autocomplete and the municipality / locality / street reference lists under
/{locale}/addresses/..., rate limited per IP. Everything the Shop API offers here —
batch validation, lookup by postal code, flats, an address by its registry code — needs
the add-on.
Free-form search
What a person types, not what a form expects. Typos still match — the search runs on
trigram similarity, so Gedimino pr 9 Vilnius and gedimino prosp. 9 vilnius find the
same address.
curl -G "https://api.loyalty.lt/lt/shop/addresses/search" \
--data-urlencode "q=Gedimino pr 9 Vilnius" \
--data-urlencode "limit=5" \
-H "X-API-Key: $LOYALTY_API_KEY" \
-H "X-API-Secret: $LOYALTY_API_SECRET"{
"success": true,
"data": {
"query": "Gedimino pr 9 Vilnius",
"results": [
{
"code": 156959662,
"full_address": "Gedimino pr. 9, Vilnius m.",
"number": "9",
"postal_code": "LT-01103",
"street": { "code": 1214286, "name": "Gedimino", "display_name": "Gedimino pr." },
"locality": { "code": 31003, "name": "Vilnius", "type": "m." },
"municipality": { "code": 13, "name": "Vilniaus miesto" },
"coordinates": { "latitude": 54.6882696, "longitude": 25.2730505, "accuracy": "street" },
"flats_count": 0,
"score": 0.692,
"updated_at": "2026-09-12T11:02:14.000000Z"
}
]
}
}code is the register's own address code (aob_kodas) — store it and you can always
match your record back to the register. score is the similarity, 1.0 being exact.
Street suggestions
For a field that completes as the user types. Pass a locality: without it Vilniaus g.
alone matches hundreds of streets across the country and the list is useless.
curl -G "https://api.loyalty.lt/lt/shop/addresses/autocomplete" \
--data-urlencode "q=Gedim" \
--data-urlencode "locality_code=31003" \
-H "X-API-Key: $LOYALTY_API_KEY" \
-H "X-API-Secret: $LOYALTY_API_SECRET"Addresses by postal code
Accepts 08106 and LT-08106 alike.
curl "https://api.loyalty.lt/lt/shop/addresses/postal-code/LT-08106" \
-H "X-API-Key: $LOYALTY_API_KEY" \
-H "X-API-Secret: $LOYALTY_API_SECRET"A code that does not exist in the register is a 404, never an empty 200 — an empty
list would read as "no addresses here", which is a different statement.
One address
curl "https://api.loyalty.lt/lt/shop/addresses/156959662" \
-H "X-API-Key: $LOYALTY_API_KEY" \
-H "X-API-Secret: $LOYALTY_API_SECRET"Returns the full hierarchy: street, locality, municipality, county and valid_from.
Validating an address
Search returns the nearest candidates, which is right for a suggestion list and wrong for a checkout. Validation answers the other question: is this address real?
curl -X POST "https://api.loyalty.lt/lt/shop/addresses/validate" \
-H "X-API-Key: $LOYALTY_API_KEY" \
-H "X-API-Secret: $LOYALTY_API_SECRET" \
-H "Content-Type: application/json" \
-d '{"address": "Gedimino pr 9, Vilnius"}'{
"data": {
"results": [
{
"input": "Gedimino pr 9, Vilnius",
"status": "valid",
"match": { "code": 156959662, "full_address": "Gedimino pr. 9, Vilnius m.", "postal_code": "LT-01103" },
"alternatives": [],
"suggestion": null
}
],
"summary": { "total": 1, "valid": 1, "ambiguous": 0, "not_found": 0 }
}
}Three statuses, because each needs a different thing from you:
| Status | What it means | What to do |
|---|---|---|
valid | Every word of the query matches one address | Use match |
ambiguous | Several addresses fit equally | Show alternatives, let the customer pick |
not_found | The register has no such address | Show suggestion as a correction, or ask again |
A house number is compared as a whole word, so 9 never validates against 19. A typo
comes back as not_found with a suggestion — we will not confirm an address we
had to guess at, but the nearest real one is worth offering.
Send addresses instead of address to check up to 50 at once — that is how an existing
customer list gets cleaned.
Searching with separate fields
If your form already splits the address, keep it split:
curl -G "https://api.loyalty.lt/lt/shop/addresses/search" \
--data-urlencode "street=Gedimino" \
--data-urlencode "house_number=9" \
--data-urlencode "city=Vilnius" \
-H "X-API-Key: $LOYALTY_API_KEY" -H "X-API-Secret: $LOYALTY_API_SECRET"q and the split fields are interchangeable; use whichever your form already produces.
Drop-in widget
One input, suggestions, and the postal code and city filled from the chosen address — without writing the autocomplete yourself. No dependencies, about 6 KB.
Try it here — this is the real widget calling the real API, not a recording:
<input id="address"><input id="flat" hidden><input id="city"><input id="postal">
<script src="https://docs.loyalty.lt/widget/loyalty-address.js"></script>
<script>
LoyaltyAddress.attach('#address', {
endpoint: '/api/address-search',
flatsEndpoint: '/api/address-flats/{code}',
fields: { city: '#city', postalCode: '#postal', flat: '#flat' },
onSelect: (address) => console.log(address.code, address.postal_code),
});
</script>The flat field stays hidden until the chosen address is a block of flats, and then it
fills with that building's real flat numbers — a <select> gets options, an <input>
gets a datalist. flatsEndpoint is optional; leave it out and the widget skips flats
entirely.
endpoint points at your server, not at us. A browser cannot hold an API secret —
anything it holds is public — so your backend proxies the call and adds the headers.
The widget dispatches input and change events on the fields it fills, so React, Vue
and Livewire forms pick the values up the same as if they had been typed.
Flats
A block of flats needs the flat number before a parcel can be delivered. Every flat carries its own registry code.
curl "https://api.loyalty.lt/lt/shop/addresses/156959662/flats?q=12" \
-H "X-API-Key: $LOYALTY_API_KEY" \
-H "X-API-Secret: $LOYALTY_API_SECRET"flats_count on any address tells you whether asking for a flat number makes sense
at all — zero means a house, not a block.
Validation reads the flat straight out of the address, in the forms people write it:
Gedimino pr. 9-12, Gedimino pr. 9, bt. 12. The number is then checked against the
building, so a flat that does not exist comes back not_found rather than confirmed:
{
"input": "Gedimino pr. 9-88",
"status": "not_found",
"match": null,
"suggestion": { "code": 156959662, "full_address": "Gedimino pr. 9, Vilnius m." },
"flat": { "number": "88", "found": false }
}Coordinates
Where we have them, coordinates come with an honest accuracy:
accuracy | What the point is |
|---|---|
address | The building itself |
street | The midpoint of the street — the house is somewhere along it |
null (no coordinates) | The register has no geometry for this street |
The state register publishes an address-point dataset but it is currently empty, so
today the points are computed from street geometry: about a third of streets, some
450 000 addresses. Saying street is the point of the field — a street midpoint
silently served as a house would put a pin on the wrong building and nobody would
know.
Browsing the hierarchy
For dependent dropdowns — municipality, then locality, then street.
| Endpoint | Returns |
|---|---|
GET /{locale}/shop/addresses/municipalities | All 60 municipalities |
GET /{locale}/shop/addresses/municipalities/{code}/localities | Localities in one municipality |
GET /{locale}/shop/addresses/localities/{code}/streets | Streets in one locality |
GET /{locale}/shop/addresses/{code}/flats | Flats in one building |
Both list endpoints take ?q= to filter by name prefix and paginate at 100 per page.
Limits
| Rate limit | 120 requests per minute, counted per API key |
| Page size | 100 for lists, 50 for search |
| Data refresh | Monthly, from the Address Register |
The rate limit is per key rather than per IP on purpose: an integrator usually calls from one server for many customers, and an IP-based limit would let one partner exhaust it for everyone else.
Errors
| Status | Meaning |
|---|---|
401 | Missing or invalid API credentials |
403 | The add-on is not enabled — data.feature says which one, data.how_to_enable where to turn it on |
404 | No such postal code, address, municipality or locality in the register |
422 | Query too short (search needs 3 characters, autocomplete 2) |
429 | Rate limit exceeded |
What this is not
It is not a geocoder. Coordinates come with an address only where the register carries them, and the API never estimates a point from a street name. If you need routing or distance, take the register code from here and geocode it with a mapping service.