Pagination with side and per_side

Fetch long lists page by page with side and per_side, know the limits per endpoint, and write a loop that stops at the last page.

4 min Level: Beginner ✓ Code tested

Lists such as /adresser, /vejnavne and /postnumre return their results in pages. You choose the page with side and the size with per_side, exactly as in DAWA. Each page is one API call, so fewer, larger pages cost less than many small ones.

The parameters

  • side is the page number. The first page is 1. side=0 gives a validation error.
  • per_side is the number of rows per page. The default is 100 on most lists. See the table for what you get without the two parameters.
Shell#page-curl
curl -s "https://api.danadresse.dk/adresser?postnr=8000&side=2&per_side=2" \
  -H "X-Api-Key: $DANADRESSE_API_KEY"

The response is a plain JSON list. There is no header or field with the total count, and no "next page" link. You know you are on the last page when it has fewer rows than per_side.

Limits per endpoint

EndpointWithout side and per_sideper_side maxside
/adresser, /adgangsadresserthe first 1001000yes
/postnumre, /sogne, /supplerendebynavne, /supplerendebynavne2the whole list1000yes
/vejnavne, /vejstykkerthe whole list with kommunekode or postnr, otherwise the first 1001000yes
/navngivneveje, /ejerlav, /jordstykker, /matrikelnumre, /steder, /stednavnethe first 1001000yes
/kommunerthe whole list500yes
/autocomplete20 suggestions100no
/adresser/autocomplete, /adgangsadresser/autocomplete, /postnumre/autocomplete, /vejnavne/autocomplete, /vejstykker/autocomplete, /ejerlav/autocomplete20 suggestions1000no
/kommuner/autocomplete20 suggestions500no
/regioner, /retskredse, /politikredse, /opstillingskredse, /landsdele, /storkredse, /valglandsdelethe whole listwhole listno

As in DAWA, the small lists come back whole when you send neither side nor per_side. /postnumre gives every postcode in one call, ready for a dropdown. If you send side or per_side, the default is 100 rows per page. On the reference lists in the last row side and per_side are ignored.

If you set per_side above the limit, you get status 400:

JSON#per-side-too-big
{
  "type": "QueryParameterFormatError",
  "title": "Invalid query parameter",
  "details": [
    {
      "type": "less_than_equal",
      "loc": ["query", "per_side"],
      "msg": "Input should be less than or equal to 1000",
      "input": "5000",
      "ctx": {"le": 1000}
    }
  ],
  "code": "invalid_query_parameter"
}

The loop: fetch until a short page

The recipe is the same in every language: start at page 1, fetch, and stop when the page has fewer rows than per_side. If the total is a multiple of per_side, the last page is empty, which costs one extra call.

A Python generator that yields the rows one at a time:

Python#paginate-python
import os
from collections.abc import Iterator

import requests

API = "https://api.danadresse.dk"
session = requests.Session()
session.headers["X-Api-Key"] = os.environ["DANADRESSE_API_KEY"]


def pages(path: str, params: dict | None = None, per_side: int = 1000) -> Iterator[dict]:
    """Yield every row of a DAWA list, page by page."""
    side = 1
    while True:
        r = session.get(
            API + path,
            params={**(params or {}), "side": side, "per_side": per_side},
            timeout=30,
        )
        r.raise_for_status()
        rows = r.json()
        yield from rows
        if len(rows) < per_side:
            return
        side += 1


everything = list(pages("/postnumre"))
print(f"Fetched {len(everything)} postnumre. First: {everything[0]['nr']} {everything[0]['navn']}")

An async iterator in JavaScript for Node 18+. The example fetches every municipality in pages of 50:

JavaScript#paginate-js
const API = "https://api.danadresse.dk";

export async function* pages(path, params = {}, perSide = 1000) {
  for (let side = 1; ; side++) {
    const url = new URL(API + path);
    for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
    url.searchParams.set("side", String(side));
    url.searchParams.set("per_side", String(perSide));

    const res = await fetch(url, {
      headers: { "X-Api-Key": process.env.DANADRESSE_API_KEY },
    });
    if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
    const rows = await res.json();
    yield* rows;
    if (rows.length < perSide) return;
  }
}

let count = 0;
for await (const kommune of pages("/kommuner", {}, 50)) count++;
console.log(`Fetched ${count} kommuner`);

To stop early, break out of the loop. The generator only fetches the next page when you ask for more rows.

Worth knowing

  • Order. Lists without q have a fixed order: /adresser and /adgangsadresser by id, the other lists by code or name. On /adresser and /adgangsadresser pages therefore never overlap and never skip rows. Address data is updated continuously, so use replication for a complete extract.
  • Free text q. With q the pages are sorted by relevance. Deep pages rarely make sense. Make the search more precise instead.
  • Circle and polygon. With cirkel= or polygon= the candidates are found first and then split into pages, nearest first for a circle. If the area covers more than 50,000 access addresses, you get 400 with the code area_too_large instead of a truncated answer. Make the area smaller, or add postnr or kommunekode.
  • Each page is a call. 2,500 addresses with per_side=100 is 25 calls. With per_side=1000 it is 3. See Limits, quotas and 429.

When you need everything: replication

Pagination is for lists a user or a job needs now: the addresses in a postcode, the streets in a municipality. If you need every address in Denmark, fetch them with /replikering/udtraek?entitet=adresse. It is one long JSON array in a single call (or NDJSON, one row per line, with &ndjson), and afterwards you can keep your data current with events. See Replication: the first extract.

format=ndjson on /adresser also gives NDJSON, but it still follows side and per_side. It is handy for writing one page straight to a file, but it does not fetch the whole data set.

Next steps

Related guides

Markdown source Spotted an error? Tell us API-reference (Swagger)

Try it with a free key

2,000 calls/month on the free plan. No credit card.

Create account →