Introduction

A REST API for Norwegian postal codes, addresses and administrative geography, built on the official registries — Bring's postal code file, Kartverket's Matrikkel and Kommuneinfo, and Geonorge's map data.

Kilda answers the questions a Norwegian address field asks: does this postal code exist, which city does it belong to, what does this address look like in the official register, and where is it on a map. The data comes from the registries themselves and is re-imported on a schedule, so what you read here is what Kartverket and Bring published — not a copy that was correct once.

Products

Endpoints are grouped by the product they belong to — the same products the pricing page names, so what a plan includes is what you see documented here.

Location API is the product documented below. Its postal code, address, county and municipality endpoints all require the same location.read scope:

  • Postal codes — look up a code, verify it, read what Bring has changed about it, or fetch its delivery area as GeoJSON. Keyed on the four digit code, no query parameters.
  • Addresses — search the full address register for autocomplete, look up a single address by its localId, verify one, or find what is near a coordinate. Search is folded for case and accents, so storgata 10 oslo finds what a user typed it as.
  • Counties and municipalities — read the administrative geography and its borders.

The location.read scope grants access to all currently documented Location API endpoints. A key does not receive separate scopes for these endpoint families.

Conventions

  • Every request needs Accept: application/json. Responses are always JSON, UTF-8.
  • A successful payload is wrapped in data, except the /geometry endpoints, which return a bare GeoJSON Feature so it stays valid GeoJSON.
  • Coordinates are WGS 84 (EPSG:4326) decimal degrees. GeoJSON puts longitude first; the latitude and longitude fields elsewhere are named, so there is nothing to get the wrong way round.
  • Dates are YYYY-MM-DD. Timestamps are ISO 8601 in UTC.
  • Enum-like fields (status, type, change_type) are stable upper case identifiers meant to be branched on. A value the API has not mapped yet arrives as UNKNOWN rather than as the raw Norwegian source text, so parsing prose is never necessary.

Errors

Every error, from validation to rate limiting, uses one envelope:

{
    "error": {
        "code": "validation_failed",
        "message": "The given data was invalid",
        "fields": {
            "postal_code": ["The postal code must be four digits."]
        }
    }
}

error.code is the stable, machine readable part — branch on it, not on message, which is written for a human and may be reworded. fields is present only when a specific input was at fault.

Status error.code Meaning
401 unauthenticated The token is missing, malformed or revoked.
403 forbidden The token is valid but lacks the scope this endpoint needs.
404 resource_not_found Nothing matched. Read message: /geometry distinguishes an unknown code from a known one with no outline.
422 validation_failed An input failed validation. fields says which.
429 rate_limit_exceeded Too many requests this minute. Wait Retry-After seconds.
429 quota_exceeded The plan's monthly quota is used up. message states when it resets.

Rate limits and quota

Every answered request carries its own limits in the response headers, so a client can pace itself before it is ever refused:

Header Meaning
X-RateLimit-Limit Requests allowed per minute on your plan.
X-RateLimit-Remaining Requests left in the current minute.
X-RateLimit-Reset Unix timestamp when the minute window resets.
X-Quota-Period The billing period being counted, as YYYY-MM.
X-Quota-Limit Requests included in the plan this month. Absent on uncapped plans.
X-Quota-Used Requests used this month, including this one.
X-Quota-Remaining Requests left this month.
X-Quota-Reset Unix timestamp when the monthly quota resets.

The budget belongs to the team, not to the key — issuing more keys does not buy more requests. The quota headers are absent entirely on an uncapped plan; treat a missing X-Quota-Limit as "no monthly cap", not as zero.

Every response counts against your quota except a 5xx, which is ours to fix. That includes a 404 ("this postal code does not exist" is still an answer your software acted on), a 304 Not Modified (see Conditional requests below) and a 422. 401, 403 and 429 never count, because they are refused before reaching an endpoint.

Caching

Responses are cached server side and refreshed whenever the source data changes, so repeated lookups are cheap and still current. Postal code data changes a few times a year and addresses daily at most — caching responses on your side is both safe and encouraged, particularly for the geometry payloads, which are large.

The postal code and geography endpoints say so in the response, so most HTTP clients will do it for you:

Cache-Control: max-age=86400, private
Vary: Authorization
ETag: W/"f2801cb7118249b57e442169993dab29"

max-age is how long the answer may be reused without asking again, and it varies by endpoint: a day for lookups and lists, a week for geometry. It is private because the answer belongs to your key — a shared cache must not serve it to anyone else, which is also what Vary: Authorization enforces.

The address endpoints do not send these headers. Their answers are query-shaped and short lived, so there is nothing stable to reuse.

Conditional requests

ETag identifies the exact body you received. Send it back in If-None-Match and, if nothing has changed, you get 304 Not Modified with no body at all:

curl -i https://kilda.test/v1/counties/03 \
  -H 'Authorization: Bearer <your key>' \
  -H 'If-None-Match: W/"f2801cb7118249b57e442169993dab29"'
HTTP/2 304
ETag: W/"f2801cb7118249b57e442169993dab29"
Cache-Control: max-age=86400, private

Keep using the copy you already have. If the data has changed you get 200 with the new body and a new ETag instead, so a client that always sends the header is never stale and never pays for a payload twice.

The comparison is weak, as HTTP defines it: send the tag back exactly as you received it, including the W/ prefix. Several tags separated by commas are fine, and so is *. An unrecognised tag simply yields the full response.

Data sources

Kilda republishes data from the official Norwegian registries and claims no ownership of it:

Source What it provides
Bring The postal code file and its change log — codes, postal places and categories.
Kartverket (Matrikkelen) The official address register: every street address and matrikkeladresse, with its cadastral numbers and coordinates.
Kartverket (Kommuneinfo) The county and municipality register: numbers, names and which county a municipality belongs to.
Geonorge The postal code area outlines served by /postal-codes/{code}/geometry, and the county and municipality borders served by their own /geometry endpoints.

Kartverket's and Geonorge's open data is published under the Norwegian Licence for Open Government Data (NLOD). If you republish what you receive from this API, credit the original source accordingly.

Authenticating requests

To authenticate requests, include an Authorization header with the value "Bearer {YOUR_API_KEY}".

All authenticated endpoints are marked with a requires authentication badge in the documentation below.

Create a key in your dashboard under API-nøkler. The key is shown once, at the moment it is created, and is stored only as a hash afterwards — so copy it then, and if you lose it, issue a new one and revoke the old.

Send it as a bearer token on every request:

Authorization: Bearer {YOUR_API_KEY}
Accept: application/json

Scope. A key must be granted the location.read scope to use the Location API. This scope covers every documented endpoint:

Scope Grants
location.read Every /postal-codes, /addresses, /counties and /municipalities endpoint.

A missing or revoked key answers 401 unauthenticated; a valid key without location.read answers 403 forbidden. The two are worth distinguishing in your error handling — the first means re-authenticate, the second means issue a key with the required scope.

Keys carry the quota and rate limit of the team that owns them, so a key belongs on a server. Do not ship one in a mobile app or in browser JavaScript, where anyone can read it and spend your quota.

Location API

Addresses, postal codes and the geography they sit in, taken from the official Norwegian registries — Bring's postal code file, Kartverket's Matrikkel and Geonorge's map data — and re-imported on a schedule.

The product answers the questions a Norwegian address field asks: does this postal code exist, which city does it belong to, what does this address look like in the official register, and where is it on a map.

The product includes three endpoint families, all covered by the same location.read scope:

  • Postal codes — keyed on the four digit code.
  • Addresses — search, lookup and proximity over the full address register.
  • Counties and Municipalities — the administrative geography those two report, as reference lists with borders.

A key with location.read can access every currently documented endpoint in this product; these endpoint families do not use separate scopes.

Postal codes

Norwegian postal codes as Bring publishes them, joined to the municipality and county they belong to and to the area outline Geonorge publishes for them.

Each endpoint answers a different question:

  • /postal-codes — the register itself, filtered by city, municipality or county.
  • /postal-codes/{code} — everything known about a code.
  • /postal-codes/{code}/verify — is this code in use, and what is its city.
  • /postal-codes/{code}/history — what has Bring changed about this code.
  • /postal-codes/{code}/geometry — the code's area as a GeoJSON Feature.

The four keyed on a code take no query parameters to get wrong, and each refuses a code that is not four digits with 422.

All five require a token carrying the location.read scope.

GET /v1/postal-codes Requires authentication

List postal codes

The register itself, ordered by code and served a page at a time: every postal code, or the subset matching a city fragment, a municipality or a county.

q matches anywhere in the postal place and ignores case, so bergen finds BERGEN. It is a plain substring match rather than the folded token search /addresses uses — a postal place is one name from a closed list, not free text.

municipality_number and county_number take the same numbers a lookup reports under municipality.number and county.number, so every postal code in Oslo is municipality_number=0301.

Only active codes are listed unless status asks for others. status takes a comma separated list, so status=active,inactive returns both in one request. A code is unique per municipality and city rather than on its own, so a retired entry can sit alongside the active one that replaced it, and listing both by default would hand a dropdown the same code twice.

Entries carry the same fields as /postal-codes/{code}. The area outlines are left out — /postal-codes/{code}/geometry serves those one at a time.

Request

Headers
Authorization        

Example: Bearer {YOUR_API_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query parameters
q   string  optional    

Postal place fragment, matched anywhere in the name, ignoring case. At least 2 characters. Example: bergen

municipality_number   string  optional    

Restrict to one municipality, by its four digit number (kommunenummer).

county_number   string  optional    

Restrict to one county, by its two digit number (fylkesnummer).

status   string  optional    

Which codes to list: active, retired or inactive, or a comma separated list of them, such as active,inactive. Defaults to active. Example: active,inactive

page   integer  optional    

Page to return, from 1.

per_page   integer  optional    

Results per page, 1–500. Defaults to 50.

Body parameters
q   string  optional    

Must not be greater than 255 characters. Example: b

municipality_number   string  optional    

Must match the regex /^\d{4}$/. Example: 5642

county_number   string  optional    

Must match the regex /^\d{2}$/. Example: 56

status   string  optional    

Example: architecto

page   integer  optional    

Must be at least 1. Example: 22

per_page   integer  optional    

Must be at least 1. Example: 67

Example request

curl --request GET \
    --get "https://kilda.test/v1/postal-codes?q=bergen&status=active%2Cinactive" \
    --header "Authorization: Bearer {YOUR_API_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"q\": \"b\",
    \"municipality_number\": \"5642\",
    \"county_number\": \"56\",
    \"status\": \"architecto\",
    \"page\": 22,
    \"per_page\": 67
}"
const url = new URL(
    "https://kilda.test/v1/postal-codes"
);

const params = {
    "q": "bergen",
    "status": "active,inactive",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_API_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "q": "b",
    "municipality_number": "5642",
    "county_number": "56",
    "status": "architecto",
    "page": 22,
    "per_page": 67
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example responses

200 Matching postal codes, ordered by code.
{
    "data": [
        {
            "postal_code": "0150",
            "city": "OSLO",
            "type": "STREET_ADDRESSES",
            "status": "active",
            "municipality": {
                "number": "0301",
                "name": "Oslo"
            },
            "county": {
                "number": "03",
                "name": "Oslo"
            },
            "country": "NO",
            "latitude": 59.9127,
            "longitude": 10.7461
        }
    ],
    "meta": {
        "total": 1,
        "page": 1,
        "per_page": 50,
        "last_page": 1
    }
}
422 The search term was too short, or a filter was not the right shape.
{
    "error": {
        "code": "validation_failed",
        "message": "The given data was invalid",
        "fields": {
            "q": [
                "The search term must be 2 characters or longer."
            ]
        }
    }
}

Response fields

data   object     
postal_code   string     

The four digit postal code.

city   string     

The postal place (poststed), as Bring writes it — upper case.

type   string     

What the code is used for: STREET_ADDRESSES, PO_BOX, COMBINED, SPECIAL_SERVICE or UNKNOWN. Omitted when the source did not state one.

status   string     

active for a code in use, retired for one that has been withdrawn, inactive for one not currently in service.

municipality   object     

The municipality number and name.

county   object     

The county number and name.

country   string     

Always NO. Present only alongside coordinates.

latitude   number     

Latitude of the area centre, WGS 84. Omitted for a code that has no area outline.

longitude   number     

Longitude of the area centre, WGS 84.

meta   object     
total   integer     

How many postal codes match the filters, across every page.

page   integer     

The page returned.

per_page   integer     

Results per page.

last_page   integer     

The last page number available.

GET /v1/postal-codes/{code} Requires authentication

Look up a postal code

Returns the city, the postal code's category and status, the municipality and county it belongs to, and the coordinates of its centre.

A code is unique per municipality and city rather than on its own, so a retired entry can sit alongside the active one that replaced it. The active entry is the one served; a retired code with nothing replacing it is still served, marked status: retired, because a caller cleaning up old records needs to know what the code used to mean.

The area outline is deliberately left out. It is an order of magnitude larger than the rest of the payload and most callers never need it, so it is served by /postal-codes/{code}/geometry instead.

This is the endpoint that answers whether a code exists at all.

Request

Headers
Authorization        

Example: Bearer {YOUR_API_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL parameters
code   string     

The four digit postal code. Example: 0150

Example request

curl --request GET \
    --get "https://kilda.test/v1/postal-codes/0150" \
    --header "Authorization: Bearer {YOUR_API_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://kilda.test/v1/postal-codes/0150"
);

const headers = {
    "Authorization": "Bearer {YOUR_API_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example responses

200 The postal code was found.
{
    "data": {
        "postal_code": "0150",
        "city": "OSLO",
        "type": "STREET_ADDRESSES",
        "status": "active",
        "municipality": {
            "number": "0301",
            "name": "Oslo"
        },
        "county": {
            "number": "03",
            "name": "Oslo"
        },
        "country": "NO",
        "latitude": 59.9127,
        "longitude": 10.7461
    }
}
404 No postal code carries that code.
{
    "error": {
        "code": "resource_not_found",
        "message": "Postal code not found"
    }
}
422 The code is not four digits.
{
    "error": {
        "code": "validation_failed",
        "message": "The given data was invalid",
        "fields": {
            "postal_code": [
                "The postal code must be four digits."
            ]
        }
    }
}

Response fields

data   object     
postal_code   string     

The four digit postal code.

city   string     

The postal place (poststed), as Bring writes it — upper case.

type   string     

What the code is used for: STREET_ADDRESSES, PO_BOX, COMBINED, SPECIAL_SERVICE or UNKNOWN. Omitted when the source did not state one.

status   string     

active for a code in use, retired for one that has been withdrawn, inactive for one not currently in service.

municipality   object     
number   string     

The four digit municipality number (kommunenummer).

name   string     

The municipality name.

county   object     
number   string     

The two digit county number (fylkesnummer).

name   string     

The county name.

country   string     

Always NO. Present only alongside coordinates.

latitude   number     

Latitude of the area centre, WGS 84. Omitted for a code that has no area outline.

longitude   number     

Longitude of the area centre, WGS 84. Omitted for a code that has no area outline.

GET /v1/postal-codes/{code}/verify Requires authentication

Verify a postal code

The cheap check for a form or a checkout: is this code currently in use, and which city does it belong to.

A miss is a 200 with valid: false, not a 404 — the same contract as /addresses/verify. "No" is an answer to the question, and a form asking on every blur should not have to treat it as a failure. A 404 from this endpoint would mean the route is wrong, not that the code is.

Only active codes count. A retired code answers valid: false here even though /postal-codes/{code} still serves it, because it is no longer a code anyone should be entering.

The payload carries the code and the city and nothing else — use /postal-codes/{code} when the municipality, county or coordinates matter.

Request

Headers
Authorization        

Example: Bearer {YOUR_API_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL parameters
code   string     

The four digit postal code. Example: 0150

Example request

curl --request GET \
    --get "https://kilda.test/v1/postal-codes/0150/verify" \
    --header "Authorization: Bearer {YOUR_API_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://kilda.test/v1/postal-codes/0150/verify"
);

const headers = {
    "Authorization": "Bearer {YOUR_API_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example responses

200 The code is in use.
{
    "data": {
        "valid": true,
        "postal_code": "0150",
        "city": "OSLO"
    }
}
200 No active postal code carries that code. Note the status is 200, not 404.
{
    "data": {
        "valid": false
    }
}
422 The code is not four digits.
{
    "error": {
        "code": "validation_failed",
        "message": "The given data was invalid",
        "fields": {
            "postal_code": [
                "The postal code must be four digits."
            ]
        }
    }
}

Response fields

data   object     
valid   boolean     

Whether an active postal code carries that code. This is the only field guaranteed to be present — the rest appear only when it is true.

postal_code   string     

The four digit postal code.

city   string     

The postal place (poststed) the code belongs to, upper case.

GET /v1/postal-codes/{code}/history Requires authentication

Read a postal code's history

Everything Bring's change log records about one postal code, newest first. Each entry is shaped as a before and an after state, so a caller can tell what the change actually did rather than only that something happened.

Both sides of the log are matched: a change that turned some other code into this one is logged against the old code, and that is half of what a history is asked for. So 0150 returns both the changes made to 0150 and the change that created it out of something else.

A code with nothing logged answers 200 with an empty list rather than 404, even when no such postal code exists. The log only reaches back to the changes Bring publishes, so "nothing has happened to it" and "it was never a postal code" are not distinguishable here, and the honest answer to both is an empty history. Use /postal-codes/{code} to ask whether a code exists.

The whole history comes in one payload and is not paginated: a code carries a handful of changes at most.

Request

Headers
Authorization        

Example: Bearer {YOUR_API_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL parameters
code   string     

The four digit postal code. Example: 0150

Example request

curl --request GET \
    --get "https://kilda.test/v1/postal-codes/0150/history" \
    --header "Authorization: Bearer {YOUR_API_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://kilda.test/v1/postal-codes/0150/history"
);

const headers = {
    "Authorization": "Bearer {YOUR_API_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example responses

200 A code that was retired and replaced.
{
    "data": [
        {
            "change_date": "2023-01-01",
            "change_type": "RETIRED_AND_REPLACED",
            "type": "STREET_ADDRESSES",
            "old": {
                "postal_code": "0150",
                "city": "OSLO",
                "type": "STREET_ADDRESSES",
                "municipality": {
                    "number": "0301",
                    "name": "Oslo"
                }
            },
            "new": {
                "postal_code": "0151",
                "city": "OSLO",
                "type": "STREET_ADDRESSES",
                "municipality": {
                    "number": "0301",
                    "name": "Oslo"
                }
            }
        }
    ],
    "meta": {
        "postal_code": "0150",
        "total": 1,
        "latest_change_date": "2023-01-01"
    }
}
200 Nothing has been logged about the code.
{
    "data": [],
    "meta": {
        "postal_code": "9999",
        "total": 0,
        "latest_change_date": null
    }
}
422 The code is not four digits.
{
    "error": {
        "code": "validation_failed",
        "message": "The given data was invalid",
        "fields": {
            "postal_code": [
                "The postal code must be four digits."
            ]
        }
    }
}

Response fields

data   object     
change_date   string     

The date the change took effect, YYYY-MM-DD.

change_type   string     

What happened: CREATED, RETIRED, RETIRED_AND_REPLACED, CODE_CHANGED, CITY_CHANGED, MUNICIPALITY_CHANGED, CATEGORY_CHANGED, or UNKNOWN for a label this API has not mapped yet. Branch on this value, never on the prose.

type   string     

The postal code category at the time of the change.

old   object     

The state before the change. Null only for an entry with nothing recorded on that side.

new   object     

The state after the change. Null when the change has no after state at all — a newly created code, or one retired without a replacement.

meta   object     
postal_code   string     

The code that was asked for, echoed back.

total   integer     

The number of entries in data.

latest_change_date   string     

The date of the newest entry, or null when there are none.

GET /v1/postal-codes/{code}/geometry Requires authentication

Get a postal code's area

The outline of the postal code's delivery area as a GeoJSON Feature, ready to hand straight to Leaflet, Mapbox or anything else that reads GeoJSON. Coordinates are WGS 84 (EPSG:4326), longitude before latitude, as GeoJSON requires.

The response is the Feature itself, not wrapped in a data key, so that it is valid GeoJSON on its own. Geometry is Polygon for a code that covers one area and MultiPolygon for one split across several. properties repeats the code, city, municipality and county so a map can label the shape without a second request.

Not every code has an area: post box and special service codes are addresses rather than territories, and Geonorge publishes no outline for them. Those answer 404 with Postal code geometry not found, which is a different message from the 404 for a code that does not exist at all.

These payloads are large — a detailed outline runs to hundreds of kilobytes. Request them only when you are drawing a map, and cache what you get.

Request

Headers
Authorization        

Example: Bearer {YOUR_API_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL parameters
code   string     

The four digit postal code. Example: 0150

Example request

curl --request GET \
    --get "https://kilda.test/v1/postal-codes/0150/geometry" \
    --header "Authorization: Bearer {YOUR_API_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://kilda.test/v1/postal-codes/0150/geometry"
);

const headers = {
    "Authorization": "Bearer {YOUR_API_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example responses

200 The area outline, as GeoJSON. Coordinates are truncated here for readability.
{
    "type": "Feature",
    "geometry": {
        "type": "Polygon",
        "coordinates": [
            [
                [
                    10.7361,
                    59.9087
                ],
                [
                    10.7561,
                    59.9087
                ],
                [
                    10.7561,
                    59.9167
                ],
                [
                    10.7361,
                    59.9087
                ]
            ]
        ]
    },
    "properties": {
        "postal_code": "0150",
        "city": "OSLO",
        "municipality": "Oslo",
        "municipality_number": "0301",
        "county": "Oslo",
        "county_number": "03",
        "accuracy": 100
    }
}
404 The code exists, but no area outline has been published for it.
{
    "error": {
        "code": "resource_not_found",
        "message": "Postal code geometry not found"
    }
}
404 No postal code carries that code.
{
    "error": {
        "code": "resource_not_found",
        "message": "Postal code not found"
    }
}
422 The code is not four digits.
{
    "error": {
        "code": "validation_failed",
        "message": "The given data was invalid",
        "fields": {
            "postal_code": [
                "The postal code must be four digits."
            ]
        }
    }
}

Response fields

type   string     

Always Feature.

geometry   object     
type   string     

Polygon for a single area, MultiPolygon for a code split across several.

coordinates   object     

GeoJSON coordinate rings in WGS 84, longitude before latitude.

properties   object     
postal_code   string     

The four digit postal code.

city   string     

The postal place (poststed).

municipality   string     

The municipality name.

municipality_number   string     

The four digit municipality number.

county   string     

The county name.

county_number   string     

The two digit county number.

accuracy   integer     

The positional accuracy of the outline in centimetres, as recorded by Geonorge. Null when the source did not state one.

Addresses

Every official Norwegian address from Kartverket's Matrikkel — both street addresses and matrikkeladresser — searchable by free text and identified by the localId the register itself assigns.

Search folds case and accents on both sides, so what a user types matches what the register stores. Tokens may arrive in any order: storgata 10 oslo and oslo storgata 10 find the same address.

All four endpoints require a token carrying the location.read scope.

GET /v1/addresses Requires authentication

Search addresses

The endpoint an address autocomplete is built on. The free text is folded and split into tokens, and every token has to appear somewhere in the address, its postnummer, its poststed or its kommune. That is what makes storgata 10 oslo work in any order, and why the postnummer is searchable even though it is not part of the address line.

q, street and postal_city are all folded into the same token list, so they are three ways of feeding the same search rather than three separate filters — sending street=Storgata and postal_city=Oslo is equivalent to q=Storgata Oslo. postal_code, municipality_number, number, letter and type are exact filters applied on top.

At least one token or one filter is required, and at least one token must be three characters or longer. Only one has to be — storgata 10 passes, because storgata is long enough on its own.

Only active addresses are returned. Results are ordered by street, number, letter and then id, so paging is stable.

Request

Headers
Authorization        

Example: Bearer {YOUR_API_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query parameters
q   string  optional    

Free text. Folded for case and accents, split into tokens, and every token must match. Tokens may be in any order. Example: storgata 10 oslo

street   string  optional    

Street name. Folded into the same token list as q.

postal_city   string  optional    

Postal place (poststed). Folded into the same token list as q.

postal_code   string  optional    

Exact four digit postal code.

municipality_number   string  optional    

Exact four digit municipality number (kommunenummer).

number   integer  optional    

Exact house number.

letter   string  optional    

Exact house letter, as in 10B.

type   string  optional    

Restrict to one address type.

Must be one of:
  • vegadresse
  • matrikkeladresse
page   integer  optional    

Page to return, from 1.

per_page   integer  optional    

Results per page, 1–100. Defaults to 10.

Body parameters
q   string  optional    

Must not be greater than 255 characters. Example: b

street   string  optional    

Must not be greater than 255 characters. Example: n

postal_city   string  optional    

Must not be greater than 255 characters. Example: g

postal_code   string  optional    

Must match the regex /^\d{4}$/. Example: 5642

municipality_number   string  optional    

Must match the regex /^\d{4}$/. Example: 5642

number   integer  optional    

Must be at least 1. Example: 16

letter   string  optional    

Must not be greater than 5 characters. Example: d

type   string  optional    

Example: vegadresse

Must be one of:
  • vegadresse
  • matrikkeladresse
page   integer  optional    

Must be at least 1. Example: 40

per_page   integer  optional    

Must be at least 1. Example: 4

Example request

curl --request GET \
    --get "https://kilda.test/v1/addresses?q=storgata+10+oslo" \
    --header "Authorization: Bearer {YOUR_API_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"q\": \"b\",
    \"street\": \"n\",
    \"postal_city\": \"g\",
    \"postal_code\": \"5642\",
    \"municipality_number\": \"5642\",
    \"number\": 16,
    \"letter\": \"d\",
    \"type\": \"vegadresse\",
    \"page\": 40,
    \"per_page\": 4
}"
const url = new URL(
    "https://kilda.test/v1/addresses"
);

const params = {
    "q": "storgata 10 oslo",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_API_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "q": "b",
    "street": "n",
    "postal_city": "g",
    "postal_code": "5642",
    "municipality_number": "5642",
    "number": 16,
    "letter": "d",
    "type": "vegadresse",
    "page": 40,
    "per_page": 4
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example responses

200 Matching addresses, ordered by street, number and letter.
{
    "data": [
        {
            "id": "101234567",
            "uuid": "c3f0a1b2-4d5e-4a6b-8c7d-9e0f1a2b3c4d",
            "type": "vegadresse",
            "address_text": "Storgata 10",
            "address_text_without_additional_name": "Storgata 10",
            "street": "Storgata",
            "number": 10,
            "letter": null,
            "additional_name": null,
            "cadastral": {
                "unit_number": 208,
                "property_number": 12,
                "leasehold_number": 0,
                "sub_number": 0
            },
            "postal_code": "0155",
            "postal_city": "OSLO",
            "municipality": {
                "number": "0301",
                "name": "Oslo"
            },
            "basic_district": {
                "number": "030103",
                "name": "Grønland"
            },
            "parish": {
                "number": "03010301",
                "name": "Grønland"
            },
            "urban_settlement": {
                "number": "0001",
                "name": "Oslo"
            },
            "electoral_district": {
                "number": "0301",
                "name": "Oslo"
            },
            "country": "NO",
            "latitude": 59.9139,
            "longitude": 10.7522,
            "updated_at": "2026-03-14T00:00:00+00:00"
        }
    ],
    "meta": {
        "total": 1,
        "page": 1,
        "per_page": 10,
        "last_page": 1
    }
}
422 Neither a search term nor a filter was given.
{
    "error": {
        "code": "validation_failed",
        "message": "The given data was invalid",
        "fields": {
            "q": [
                "A search term or at least one filter is required."
            ]
        }
    }
}
422 Every search term was too short to be selective.
{
    "error": {
        "code": "validation_failed",
        "message": "The given data was invalid",
        "fields": {
            "q": [
                "At least one search term must be 3 characters or longer."
            ]
        }
    }
}

Response fields

data   object     
id   string     

The Matrikkel lokalid. This is the identifier to hand back to /addresses/{localId}, and the one to store — Matrikkelen assigns it, and it stays stable.

uuid   string     

The Matrikkel UUID for the same address.

type   string     

vegadresse for an address on a named road, matrikkeladresse for one identified by its cadastral numbers.

address_text   string     

The official address line, including the tilleggsnavn when the address has one.

address_text_without_additional_name   string     

The same line with the tilleggsnavn stripped. For a matrikkeladresse this is only the cadastral numbers, so it is rarely the form to show a user.

street   string     

The street name (adressenavn) on its own. Null for a matrikkeladresse.

number   integer     

The house number. Null for a matrikkeladresse.

letter   string     

The house letter, as in 10B. Null when there is none.

additional_name   string     

The tilleggsnavn — a farm or property name such as Myklebust. Null when there is none.

cadastral   object     

The Matrikkel numbers: unit_number (gårdsnummer), property_number (bruksnummer), leasehold_number (festenummer) and sub_number (undernummer).

postal_code   string     

The four digit postal code.

postal_city   string     

The postal place (poststed), upper case.

municipality   object     

The municipality number and name.

basic_district   object     

Grunnkrets number and name, the smallest statistical unit Statistisk sentralbyrå publishes.

parish   object     

Kirkesogn number and name.

urban_settlement   object     

Tettsted number and name. Null members for an address outside any.

electoral_district   object     

Valgkrets number and name.

country   string     

Always NO.

latitude   number     

Latitude of the address point, WGS 84.

longitude   number     

Longitude of the address point, WGS 84.

updated_at   string     

When Matrikkelen last changed this address, ISO 8601 in UTC. Not when Kilda imported it.

meta   object     
total   integer     

Total matches across all pages.

page   integer     

The page returned.

per_page   integer     

Results per page.

last_page   integer     

The last page number available.

GET /v1/addresses/{localId} Requires authentication

Look up an address

The full record for one address, by the Matrikkel lokalid that every other endpoint here hands out as id. This is the second half of an autocomplete: search for suggestions, then look up the one the user picked.

Store the lokalid, not the address line. It is what Matrikkelen assigns and it stays stable, so it is the only reliable way to refer back to an address.

Unlike search and verify, this endpoint does not filter on status: an address that has since been retired is still served, because a caller resolving a stored id needs to know what it used to mean rather than get a 404.

A non numeric id answers 422 rather than 404, so a malformed identifier is distinguishable from one that simply does not exist.

Request

Headers
Authorization        

Example: Bearer {YOUR_API_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL parameters
localId   string     

The Matrikkel lokalid, up to 20 digits. This is the id field of any address in a search, verify or nearby response. Example: 101234567

Example request

curl --request GET \
    --get "https://kilda.test/v1/addresses/101234567" \
    --header "Authorization: Bearer {YOUR_API_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://kilda.test/v1/addresses/101234567"
);

const headers = {
    "Authorization": "Bearer {YOUR_API_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example responses

200 The address was found.
{
    "data": {
        "id": "101234567",
        "uuid": "c3f0a1b2-4d5e-4a6b-8c7d-9e0f1a2b3c4d",
        "type": "vegadresse",
        "address_text": "Storgata 10",
        "address_text_without_additional_name": "Storgata 10",
        "street": "Storgata",
        "number": 10,
        "letter": null,
        "additional_name": null,
        "cadastral": {
            "unit_number": 208,
            "property_number": 12,
            "leasehold_number": 0,
            "sub_number": 0
        },
        "postal_code": "0155",
        "postal_city": "OSLO",
        "municipality": {
            "number": "0301",
            "name": "Oslo"
        },
        "basic_district": {
            "number": "030103",
            "name": "Grønland"
        },
        "parish": {
            "number": "03010301",
            "name": "Grønland"
        },
        "urban_settlement": {
            "number": "0001",
            "name": "Oslo"
        },
        "electoral_district": {
            "number": "0301",
            "name": "Oslo"
        },
        "country": "NO",
        "latitude": 59.9139,
        "longitude": 10.7522,
        "updated_at": "2026-03-14T00:00:00+00:00"
    }
}
404 No address carries that identifier.
{
    "error": {
        "code": "resource_not_found",
        "message": "Address not found"
    }
}
422 The identifier is not numeric.
{
    "error": {
        "code": "validation_failed",
        "message": "The given data was invalid",
        "fields": {
            "id": [
                "The address id must be numeric."
            ]
        }
    }
}

Response fields

data   object     

The same address object /addresses returns in its list, without distance_metres.

GET /v1/addresses/verify Requires authentication

Verify an address

Answers whether one specific address exists, and nothing more. The check to run against a shipping address at checkout, or against a field a user has finished typing.

A miss is a 200 with valid: false, not a 404. "No" is an answer to the question, and a form asking on every blur should not have to treat it as a failure. A 404 from this endpoint would mean the route is wrong, not that the address is.

Give the address either as one address_text line or as street plus number and letter; one of the two is required, as is postal_code. Both forms are folded the same way, so case, accents and spacing do not matter — storgata 10b and Storgata 10B verify alike. The letter belongs against the number, as Matrikkelen writes it: Storgata 10B, not Storgata 10 B.

An address pasted back from an earlier response still verifies even though it carries a tilleggsnavn that the folded key leaves out.

Only active addresses count. A retired address answers valid: false.

Request

Headers
Authorization        

Example: Bearer {YOUR_API_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query parameters
address_text   string  optional    

The whole address line. Required unless street is given. Example: Storgata 10

street   string  optional    

Street name. Required unless address_text is given.

number   integer  optional    

House number. Only used alongside street.

letter   string  optional    

House letter. Only used alongside street.

postal_code   string     

The four digit postal code. Always required — it is what makes the address unique. Example: 0155

Body parameters
address_text   string  optional    

This field is required when street is not present. Must not be greater than 255 characters. Example: b

street   string  optional    

This field is required when address_text is not present. Must not be greater than 255 characters. Example: n

number   integer  optional    

Must be at least 1. Example: 67

letter   string  optional    

Must not be greater than 5 characters. Example: z

postal_code   string     

Must match the regex /^\d{4}$/. Example: 5642

Example request

curl --request GET \
    --get "https://kilda.test/v1/addresses/verify?address_text=Storgata+10&postal_code=0155" \
    --header "Authorization: Bearer {YOUR_API_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"address_text\": \"b\",
    \"street\": \"n\",
    \"number\": 67,
    \"letter\": \"z\",
    \"postal_code\": \"5642\"
}"
const url = new URL(
    "https://kilda.test/v1/addresses/verify"
);

const params = {
    "address_text": "Storgata 10",
    "postal_code": "0155",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_API_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "address_text": "b",
    "street": "n",
    "number": 67,
    "letter": "z",
    "postal_code": "5642"
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example responses

200 The address exists.
{
    "data": {
        "valid": true,
        "id": "101234567",
        "address_text": "Storgata 10",
        "postal_code": "0155",
        "postal_city": "OSLO",
        "municipality_number": "0301"
    }
}
200 No active address matches. Note the status is 200, not 404.
{
    "data": {
        "valid": false
    }
}
422 Neither `address_text` nor `street` was given, or the postal code is not four digits.
{
    "error": {
        "code": "validation_failed",
        "message": "The given data was invalid",
        "fields": {
            "postal_code": [
                "The postal code field format is invalid."
            ]
        }
    }
}
422 The address was given, but nothing readable was left of it once folded.
{
    "error": {
        "code": "validation_failed",
        "message": "The given data was invalid",
        "fields": {
            "address_text": [
                "The address could not be read."
            ]
        }
    }
}

Response fields

data   object     
valid   boolean     

Whether an active address matched. This is the only field guaranteed to be present — the rest appear only when it is true.

id   string     

The Matrikkel lokalid of the address that matched. Pass it to /addresses/{localId} for the full record.

address_text   string     

The official address line, which may differ from what you sent — this is the form to store.

postal_code   string     

The four digit postal code.

postal_city   string     

The postal place (poststed), upper case.

municipality_number   string     

The four digit municipality number.

GET /v1/addresses/nearby Requires authentication

Find addresses near a point

Every active address within a radius of a coordinate, nearest first. Use it to turn a GPS fix or a map click into the address the user is standing at.

The radius is in metres and defaults to 100, with a ceiling of 2000.

Each result carries distance_metres, which only this endpoint reports — the other address endpoints measure nothing, so the field is absent there.

Results are truncated at per_page, not paginated. There is no page two: if the nearest ten are not enough, narrow or widen the radius instead.

Request

Headers
Authorization        

Example: Bearer {YOUR_API_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query parameters
latitude   number     

Latitude of the point, WGS 84. Between -90 and 90. Example: 59.9139

longitude   number     

Longitude of the point, WGS 84. Between -180 and 180. Example: 10.7522

radius   integer  optional    

Search radius in metres, 1–2000. Defaults to 100. Example: 250

per_page   integer  optional    

Maximum results to return, 1–100. Defaults to 10.

Body parameters
latitude   number     

Must be between -90 and 90. Example: -89

longitude   number     

Must be between -180 and 180. Example: -179

radius   integer  optional    

Must be at least 1. Example: 67

per_page   integer  optional    

Must be at least 1. Example: 66

Example request

curl --request GET \
    --get "https://kilda.test/v1/addresses/nearby?latitude=59.9139&longitude=10.7522&radius=250" \
    --header "Authorization: Bearer {YOUR_API_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"latitude\": -89,
    \"longitude\": -179,
    \"radius\": 67,
    \"per_page\": 66
}"
const url = new URL(
    "https://kilda.test/v1/addresses/nearby"
);

const params = {
    "latitude": "59.9139",
    "longitude": "10.7522",
    "radius": "250",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_API_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "latitude": -89,
    "longitude": -179,
    "radius": 67,
    "per_page": 66
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example responses

200 The addresses within the radius, nearest first.
{
    "data": [
        {
            "id": "101234567",
            "uuid": "c3f0a1b2-4d5e-4a6b-8c7d-9e0f1a2b3c4d",
            "type": "vegadresse",
            "address_text": "Storgata 10",
            "address_text_without_additional_name": "Storgata 10",
            "street": "Storgata",
            "number": 10,
            "letter": null,
            "additional_name": null,
            "cadastral": {
                "unit_number": 208,
                "property_number": 12,
                "leasehold_number": 0,
                "sub_number": 0
            },
            "postal_code": "0155",
            "postal_city": "OSLO",
            "municipality": {
                "number": "0301",
                "name": "Oslo"
            },
            "basic_district": {
                "number": "030103",
                "name": "Grønland"
            },
            "parish": {
                "number": "03010301",
                "name": "Grønland"
            },
            "urban_settlement": {
                "number": "0001",
                "name": "Oslo"
            },
            "electoral_district": {
                "number": "0301",
                "name": "Oslo"
            },
            "country": "NO",
            "latitude": 59.9139,
            "longitude": 10.7522,
            "distance_metres": 12.4,
            "updated_at": "2026-03-14T00:00:00+00:00"
        }
    ],
    "meta": {
        "radius_metres": 250,
        "returned": 1
    }
}
422 The coordinate is missing or out of range.
{
    "error": {
        "code": "validation_failed",
        "message": "The given data was invalid",
        "fields": {
            "latitude": [
                "The latitude field is required."
            ]
        }
    }
}

Response fields

data[]   object     

The same address object /addresses returns, with distance_metres added.

data   object     
distance_metres   number     

Straight line distance from the point you gave, in metres, to one decimal.

meta   object     
radius_metres   integer     

The radius actually searched, so a caller who omitted it learns the default.

returned   integer     

How many addresses are in data. Equal to per_page when the radius held more, so treat that as "there may be more" rather than as a total.

Counties

The Norwegian counties (fylker) as Kartverket's Kommuneinfo publishes them, each with the municipalities it contains and the outline Geonorge draws for it.

A county is identified by its two digit number (fylkesnummer). Anything that is not two digits is refused with 422 rather than looked up.

  • /counties — every county, in one response.
  • /counties/{code} — one county.
  • /counties/{code}/municipalities — the municipalities it contains.
  • /counties/{code}/geometry — its outline as a GeoJSON Feature.

Svalbard (21) and Jan Mayen (22) are served as counties because addresses and postal codes there are registered under them, but Geonorge publishes no outline for either. has_geometry says so before you ask.

All four require a token carrying the location.read scope.

GET /v1/counties Requires authentication

List counties

Every Norwegian county in one response, ordered by name. There are seventeen — the fifteen counties plus Svalbard and Jan Mayen — so there is nothing to paginate and no query parameters to get wrong.

This is a reference list, not a search: it changes when the counties themselves are reorganised, which last happened on the first of January 2024. Fetch it once, cache it, and build your dropdown from it.

The outlines are left out — /counties/{code}/geometry serves those one at a time, and has_geometry tells you which counties have one.

Request

Headers
Authorization        

Example: Bearer {YOUR_API_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Example request

curl --request GET \
    --get "https://kilda.test/v1/counties" \
    --header "Authorization: Bearer {YOUR_API_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://kilda.test/v1/counties"
);

const headers = {
    "Authorization": "Bearer {YOUR_API_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example responses

200 Every county, ordered by name. Truncated here.
{
    "data": [
        {
            "number": "42",
            "name": "Agder",
            "municipality_count": 25,
            "country": "NO",
            "latitude": 58.4627,
            "longitude": 7.9956,
            "has_geometry": true
        },
        {
            "number": "34",
            "name": "Innlandet",
            "municipality_count": 46,
            "country": "NO",
            "latitude": 61.2361,
            "longitude": 10.3789,
            "has_geometry": true
        }
    ],
    "meta": {
        "total": 17
    }
}

Response fields

data   object     
number   string     

The two digit county number (fylkesnummer).

name   string     

The county name.

municipality_count   integer     

How many municipalities the county contains.

country   string     

Always NO.

latitude   number     

Latitude of the county's representative point, WGS 84. Null when it has no outline to derive one from.

longitude   number     

Longitude of the county's representative point, WGS 84.

has_geometry   boolean     

Whether /counties/{code}/geometry will return an outline. False for Svalbard and Jan Mayen.

meta   object     
total   integer     

The number of counties returned.

GET /v1/counties/{code} Requires authentication

Look up a county

One county by its two digit number, with the number of municipalities it contains and the coordinates of its representative point.

Use /counties/{code}/municipalities to list those municipalities, and /counties/{code}/geometry for the outline.

Request

Headers
Authorization        

Example: Bearer {YOUR_API_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL parameters
code   string     

The two digit county number (fylkesnummer). Example: 03

Example request

curl --request GET \
    --get "https://kilda.test/v1/counties/03" \
    --header "Authorization: Bearer {YOUR_API_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://kilda.test/v1/counties/03"
);

const headers = {
    "Authorization": "Bearer {YOUR_API_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example responses

200 The county was found.
{
    "data": {
        "number": "03",
        "name": "Oslo",
        "municipality_count": 1,
        "country": "NO",
        "latitude": 59.9127,
        "longitude": 10.7461,
        "has_geometry": true
    }
}
404 No county carries that number.
{
    "error": {
        "code": "resource_not_found",
        "message": "County not found"
    }
}
422 The number is not two digits.
{
    "error": {
        "code": "validation_failed",
        "message": "The given data was invalid",
        "fields": {
            "county_number": [
                "The county number must be two digits."
            ]
        }
    }
}

Response fields

data   object     
number   string     

The two digit county number (fylkesnummer).

name   string     

The county name.

municipality_count   integer     

How many municipalities the county contains.

country   string     

Always NO.

latitude   number     

Latitude of the county's representative point, WGS 84.

longitude   number     

Longitude of the county's representative point, WGS 84.

has_geometry   boolean     

Whether /counties/{code}/geometry will return an outline.

GET /v1/counties/{code}/municipalities Requires authentication

List a county's municipalities

Every municipality in one county, ordered by name. The county is the URL, so it is reported once in meta rather than repeated on every entry.

An unknown county number answers 404 rather than an empty list: a county with no municipalities does not exist, so an empty response here would only ever mean the number was wrong.

This is the same data as /municipalities?county_number={code}, in the shape you want when you are drilling down rather than searching.

Request

Headers
Authorization        

Example: Bearer {YOUR_API_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL parameters
code   string     

The two digit county number (fylkesnummer). Example: 03

Example request

curl --request GET \
    --get "https://kilda.test/v1/counties/03/municipalities" \
    --header "Authorization: Bearer {YOUR_API_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://kilda.test/v1/counties/03/municipalities"
);

const headers = {
    "Authorization": "Bearer {YOUR_API_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example responses

200 The municipalities in the county.
{
    "data": [
        {
            "number": "0301",
            "name": "Oslo",
            "country": "NO",
            "latitude": 59.9127,
            "longitude": 10.7461,
            "has_geometry": true,
            "updated_at": "2026-01-01T00:00:00+00:00"
        }
    ],
    "meta": {
        "county": {
            "number": "03",
            "name": "Oslo"
        },
        "total": 1
    }
}
404 No county carries that number.
{
    "error": {
        "code": "resource_not_found",
        "message": "County not found"
    }
}
422 The number is not two digits.
{
    "error": {
        "code": "validation_failed",
        "message": "The given data was invalid",
        "fields": {
            "county_number": [
                "The county number must be two digits."
            ]
        }
    }
}

Response fields

data   object     
number   string     

The four digit municipality number (kommunenummer).

name   string     

The municipality name.

country   string     

Always NO.

latitude   number     

Latitude of the municipality's representative point, WGS 84.

longitude   number     

Longitude of the municipality's representative point, WGS 84.

has_geometry   boolean     

Whether /municipalities/{code}/geometry will return an outline.

updated_at   string     

When the source last changed this municipality, ISO 8601 in UTC.

meta   object     
county   object     
number   string     

The county that was asked for, echoed back.

name   string     

That county's name.

total   integer     

The number of municipalities returned.

GET /v1/counties/{code}/geometry Requires authentication

Get a county's outline

The county's border as a GeoJSON Feature, ready to hand straight to Leaflet, Mapbox or anything else that reads GeoJSON. Coordinates are WGS 84 (EPSG:4326), longitude before latitude, as GeoJSON requires.

The response is the Feature itself, not wrapped in a data key, so that it is valid GeoJSON on its own. Geometry is Polygon for a county in one piece and MultiPolygon for one that includes islands. properties repeats the number and name so a map can label the shape without a second request.

This is the real border, not a bounding box. Svalbard and Jan Mayen have no published outline and answer 404 with County geometry not found, which is a different message from the 404 for a county that does not exist at all. has_geometry on the lookup tells you which you are dealing with.

These payloads are large — a detailed county border runs to hundreds of kilobytes. Request them only when you are drawing a map, and cache what you get.

Request

Headers
Authorization        

Example: Bearer {YOUR_API_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL parameters
code   string     

The two digit county number (fylkesnummer). Example: 03

Example request

curl --request GET \
    --get "https://kilda.test/v1/counties/03/geometry" \
    --header "Authorization: Bearer {YOUR_API_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://kilda.test/v1/counties/03/geometry"
);

const headers = {
    "Authorization": "Bearer {YOUR_API_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example responses

200 The county border, as GeoJSON. Coordinates are truncated here for readability.
{
    "type": "Feature",
    "geometry": {
        "type": "Polygon",
        "coordinates": [
            [
                [
                    10.4894,
                    59.8093
                ],
                [
                    10.9524,
                    59.8093
                ],
                [
                    10.9524,
                    60.1357
                ],
                [
                    10.4894,
                    59.8093
                ]
            ]
        ]
    },
    "properties": {
        "county_number": "03",
        "county": "Oslo"
    }
}
404 The county exists, but no outline has been published for it.
{
    "error": {
        "code": "resource_not_found",
        "message": "County geometry not found"
    }
}
404 No county carries that number.
{
    "error": {
        "code": "resource_not_found",
        "message": "County not found"
    }
}
422 The number is not two digits.
{
    "error": {
        "code": "validation_failed",
        "message": "The given data was invalid",
        "fields": {
            "county_number": [
                "The county number must be two digits."
            ]
        }
    }
}

Response fields

type   string     

Always Feature.

geometry   object     
type   string     

Polygon for a county in one piece, MultiPolygon for one that includes islands.

coordinates   object     

GeoJSON coordinate rings in WGS 84, longitude before latitude.

properties   object     
county_number   string     

The two digit county number.

county   string     

The county name.

Municipalities

Every Norwegian municipality (kommune) as Kartverket's Kommuneinfo publishes them, with the county it belongs to and the outline Geonorge draws for it.

A municipality is identified by its four digit number (kommunenummer), the same number an address or a postal code reports under municipality.number. Anything that is not four digits is refused with 422 rather than looked up.

  • /municipalities — every municipality, optionally filtered by name or county.
  • /municipalities/{code} — one municipality.
  • /municipalities/{code}/geometry — its outline as a GeoJSON Feature.

Municipality numbers change when the counties are reorganised. Trondheim was 1601 until 2018 and has been 5001 since, and the 2024 county split moved every 38xx municipality to 39xx or 40xx. Store the number the register gave you, but resolve the name from here rather than storing both.

All three require a token carrying the location.read scope.

GET /v1/municipalities Requires authentication

List municipalities

Every Norwegian municipality in one response, ordered by name, or the subset matching a name fragment or a county.

There are 359 rows — the 357 municipalities plus Svalbard and Jan Mayen, which are registered as municipalities so that addresses and postal codes there have one. The whole list is returned in one payload rather than paginated: a caller building a dropdown wants all of it, and paging would cost them 36 requests to learn nothing new. It is a reference list that changes on the first of January and almost never otherwise — fetch it once and cache it.

q matches anywhere in the name and ignores case, so trond finds Trondheim. It is a plain substring match rather than the folded token search /addresses uses: a municipality name is one word from a closed list, not free text.

The outlines are left out — /municipalities/{code}/geometry serves those one at a time, and has_geometry tells you which have one.

Request

Headers
Authorization        

Example: Bearer {YOUR_API_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query parameters
q   string  optional    

Name fragment, matched anywhere in the name, ignoring case. At least 2 characters. Example: trond

county_number   string  optional    

Restrict to one county, by its two digit number.

Body parameters
q   string  optional    

Must not be greater than 255 characters. Example: b

county_number   string  optional    

Must match the regex /^\d{2}$/. Example: 56

Example request

curl --request GET \
    --get "https://kilda.test/v1/municipalities?q=trond" \
    --header "Authorization: Bearer {YOUR_API_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"q\": \"b\",
    \"county_number\": \"56\"
}"
const url = new URL(
    "https://kilda.test/v1/municipalities"
);

const params = {
    "q": "trond",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_API_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "q": "b",
    "county_number": "56"
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example responses

200 Matching municipalities, ordered by name.
{
    "data": [
        {
            "number": "0301",
            "name": "Oslo",
            "county": {
                "number": "03",
                "name": "Oslo"
            },
            "country": "NO",
            "latitude": 59.9127,
            "longitude": 10.7461,
            "has_geometry": true,
            "updated_at": "2026-01-01T00:00:00+00:00"
        }
    ],
    "meta": {
        "total": 1
    }
}
422 The search term was too short, or the county number was not two digits.
{
    "error": {
        "code": "validation_failed",
        "message": "The given data was invalid",
        "fields": {
            "q": [
                "The search term must be 2 characters or longer."
            ]
        }
    }
}

Response fields

data   object     
number   string     

The four digit municipality number (kommunenummer).

name   string     

The municipality name.

county   object     

The county number and name.

country   string     

Always NO.

latitude   number     

Latitude of the municipality's representative point, WGS 84. Null when it has no outline to derive one from.

longitude   number     

Longitude of the municipality's representative point, WGS 84.

has_geometry   boolean     

Whether /municipalities/{code}/geometry will return an outline.

updated_at   string     

When the source last changed this municipality, ISO 8601 in UTC. Not when Kilda imported it.

meta   object     
total   integer     

The number of municipalities returned.

GET /v1/municipalities/{code} Requires authentication

Look up a municipality

One municipality by its four digit number, with the county it belongs to and the coordinates of its representative point.

This is the endpoint to resolve the municipality.number an address or a postal code reports into a name, a county and a point on a map.

Request

Headers
Authorization        

Example: Bearer {YOUR_API_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL parameters
code   string     

The four digit municipality number (kommunenummer). Example: 0301

Example request

curl --request GET \
    --get "https://kilda.test/v1/municipalities/0301" \
    --header "Authorization: Bearer {YOUR_API_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://kilda.test/v1/municipalities/0301"
);

const headers = {
    "Authorization": "Bearer {YOUR_API_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example responses

200 The municipality was found.
{
    "data": {
        "number": "0301",
        "name": "Oslo",
        "county": {
            "number": "03",
            "name": "Oslo"
        },
        "country": "NO",
        "latitude": 59.9127,
        "longitude": 10.7461,
        "has_geometry": true,
        "updated_at": "2026-01-01T00:00:00+00:00"
    }
}
404 No municipality carries that number.
{
    "error": {
        "code": "resource_not_found",
        "message": "Municipality not found"
    }
}
422 The number is not four digits.
{
    "error": {
        "code": "validation_failed",
        "message": "The given data was invalid",
        "fields": {
            "municipality_number": [
                "The municipality number must be four digits."
            ]
        }
    }
}

Response fields

data   object     
number   string     

The four digit municipality number (kommunenummer).

name   string     

The municipality name.

county   object     

The county number and name.

country   string     

Always NO.

latitude   number     

Latitude of the municipality's representative point, WGS 84.

longitude   number     

Longitude of the municipality's representative point, WGS 84.

has_geometry   boolean     

Whether /municipalities/{code}/geometry will return an outline.

updated_at   string     

When the source last changed this municipality, ISO 8601 in UTC.

GET /v1/municipalities/{code}/geometry Requires authentication

Get a municipality's outline

The municipality's border as a GeoJSON Feature, ready to hand straight to Leaflet, Mapbox or anything else that reads GeoJSON. Coordinates are WGS 84 (EPSG:4326), longitude before latitude, as GeoJSON requires.

The response is the Feature itself, not wrapped in a data key, so that it is valid GeoJSON on its own. Geometry is Polygon for a municipality in one piece and MultiPolygon for one that includes islands. properties repeats the municipality and county so a map can label the shape without a second request.

This is the real border, not a bounding box. A municipality with no published outline answers 404 with Municipality geometry not found, which is a different message from the 404 for a number that belongs to no municipality at all. has_geometry on the lookup tells you which you are dealing with.

These payloads are large — a detailed border runs to hundreds of kilobytes. Request them only when you are drawing a map, and cache what you get.

Request

Headers
Authorization        

Example: Bearer {YOUR_API_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL parameters
code   string     

The four digit municipality number (kommunenummer). Example: 0301

Example request

curl --request GET \
    --get "https://kilda.test/v1/municipalities/0301/geometry" \
    --header "Authorization: Bearer {YOUR_API_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://kilda.test/v1/municipalities/0301/geometry"
);

const headers = {
    "Authorization": "Bearer {YOUR_API_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example responses

200 The municipality border, as GeoJSON. Coordinates are truncated here for readability.
{
    "type": "Feature",
    "geometry": {
        "type": "Polygon",
        "coordinates": [
            [
                [
                    10.4894,
                    59.8093
                ],
                [
                    10.9524,
                    59.8093
                ],
                [
                    10.9524,
                    60.1357
                ],
                [
                    10.4894,
                    59.8093
                ]
            ]
        ]
    },
    "properties": {
        "municipality_number": "0301",
        "municipality": "Oslo",
        "county_number": "03",
        "county": "Oslo"
    }
}
404 The municipality exists, but no outline has been published for it.
{
    "error": {
        "code": "resource_not_found",
        "message": "Municipality geometry not found"
    }
}
404 No municipality carries that number.
{
    "error": {
        "code": "resource_not_found",
        "message": "Municipality not found"
    }
}
422 The number is not four digits.
{
    "error": {
        "code": "validation_failed",
        "message": "The given data was invalid",
        "fields": {
            "municipality_number": [
                "The municipality number must be four digits."
            ]
        }
    }
}

Response fields

type   string     

Always Feature.

geometry   object     
type   string     

Polygon for a municipality in one piece, MultiPolygon for one that includes islands.

coordinates   object     

GeoJSON coordinate rings in WGS 84, longitude before latitude.

properties   object     
municipality_number   string     

The four digit municipality number.

municipality   string     

The municipality name.

county_number   string     

The two digit county number.

county   string     

The county name.