Loyalty.lt
API Basics

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.

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:

StatusWhat it meansWhat to do
validEvery word of the query matches one addressUse match
ambiguousSeveral addresses fit equallyShow alternatives, let the customer pick
not_foundThe register has no such addressShow 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:

accuracyWhat the point is
addressThe building itself
streetThe 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.

EndpointReturns
GET /{locale}/shop/addresses/municipalitiesAll 60 municipalities
GET /{locale}/shop/addresses/municipalities/{code}/localitiesLocalities in one municipality
GET /{locale}/shop/addresses/localities/{code}/streetsStreets in one locality
GET /{locale}/shop/addresses/{code}/flatsFlats in one building

Both list endpoints take ?q= to filter by name prefix and paginate at 100 per page.

Limits

Rate limit120 requests per minute, counted per API key
Page size100 for lists, 50 for search
Data refreshMonthly, 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

StatusMeaning
401Missing or invalid API credentials
403The add-on is not enabled — data.feature says which one, data.how_to_enable where to turn it on
404No such postal code, address, municipality or locality in the register
422Query too short (search needs 3 characters, autocomplete 2)
429Rate 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.

On this page