@grundfast/sdk
v0.17.2
Published
TypeScript SDK for the Grundfast API — Danish property & geodata (BBR, Matriklen).
Maintainers
Readme
@grundfast/sdk
Official TypeScript SDK for the Grundfast API — the
convenient old services.datafordeler.dk shape (translated kodelister, re-joined
names, current-only data, WGS84 geometry, auto-pagination) over Denmark's new
entity-based Datafordeler backend. Register #1 is BBR, #2 is Matriklen.
Full API reference: grundfast.dk/docs/sdk.
Installation
npm install @grundfast/sdk
# or
bun add @grundfast/sdkShips dual ESM + CommonJS with bundled type declarations — works in Node, Bun, and the browser.
Quick Start
import { GrundfastClient } from '@grundfast/sdk';
const gf = new GrundfastClient({ apiKey: 'gf_live_…' });
// Resolve one property by its BFE (Bestemt Fast Ejendom) number.
const ejendom = await gf.ejendom(5651067);
console.log(ejendom.jordstykke.ejerlav_navn, ejendom.bygninger.length);The client defaults to https://api.grundfast.dk. Override baseUrl (or inject
a custom fetch) via the constructor options:
const gf = new GrundfastClient({
apiKey: 'gf_live_…',
baseUrl: 'https://api.grundfast.dk',
timeoutMs: 15000, // per-attempt abort timeout (default 15s; 0 disables)
maxRetries: 2, // extra attempts on a transient failure (default 2; 0 disables)
retryBaseMs: 250, // first backoff step, doubled per attempt + half-jittered
retryCapMs: 5000, // ceiling on one backoff sleep (also caps Retry-After)
});Every attempt is aborted after timeoutMs so a hung upstream never hangs your
await — a timeout throws GrundfastError with status: 408.
Retries
Transient failures are retried with jittered exponential backoff, honouring a
capped Retry-After. The policy is deliberately narrow:
- Retried:
500,502,503and the per-minute burst429— onGETrequests and the idempotent:batchreads. AGETadditionally retries408, a network failure and its own timeout. - Never retried: the monthly-quota
429(it means upgrade, not wait — itsRetry-Aftercan be weeks), any4xxyou can fix, a burst429asking to wait longer thanretryCapMs, non-idempotent POSTs likecreateExport(), and a:batchPOST whose request failed in transit — replaying one could charge your quota twice.
Cancellation
Every method takes a trailing { signal }, merged with the client's own timeout.
Abort it to cancel the in-flight request and any pending retry backoff — what a
type-ahead needs so a slow keystroke can't overwrite a newer result:
let inFlight: AbortController | undefined;
async function onKeystroke(q: string) {
inFlight?.abort(); // drop the superseded lookup
inFlight = new AbortController();
try {
const { results } = await gf.adresseSearch(q, 8, { signal: inFlight.signal });
render(results);
} catch (err) {
if ((err as Error).name === 'AbortError') return; // superseded — not an error
throw err;
}
}An abort rejects with the signal's own reason (an AbortError), never with
GrundfastError(408) — that status is reserved for the client's own timeout.
Try it without a key
The demo endpoints run offline from a bundled fixture — no API key required:
const gf = new GrundfastClient();
const demo = await gf.demoEjendom();
const fc = await gf.demoEjendomGeoJson(); // WFS-compatible GeoJSON FeatureCollection
const koder = await gf.kodeliste('varmeinstallation'); // translated code listKeyed endpoints
const gf = new GrundfastClient({ apiKey: 'gf_live_…' });
// Clean, re-joined property rollup.
const ejendom = await gf.ejendom(5651067);
// Same property as a WFS-compatible GeoJSON FeatureCollection (one Feature per building).
const geojson = await gf.ejendomGeoJson(5651067);
// Matriklen (cadastre) rollup.
const matrikel = await gf.matrikel(5651067);
// Building(s) for a BFE — same rollup shape as ejendom() (resolved from the same join).
const bygning = await gf.bygning(5651067);
// Per-building GeoDanmark footprint polygons (the real outlines BBR can't give).
const footprints = await gf.geodanmarkBygninger(5651067);
console.log(footprints.buildings[0]?.rings, footprints.attribution); // CC BY 4.0 credit
// The two arealudpegninger, on a WGS84 coordinate. Both answer a MEASURED yes/no:
// a `false` never comes from an upstream fault — the call fails instead — because a
// "no" is the answer somebody digs or builds on.
const jord = await gf.jordOmraadeklassificering(10.1763672, 56.1327761);
jord.omraadeklassificeret; // § 50 a covers the point
jord.analysefri; // true | false | null — null means "not covered", never "unknown"
const natur = await gf.naturSkovbyggelinje(8.5286062, 55.8988908);
natur.skovbyggelinje; // a line that STANDS covers the point (§ 17, 300 m from forest)
natur.ophaevet; // a separate question — a point can sit in both, and then the line applies
natur.ophaevede; // the lifted polygons: why, and the kommune's own afgørelse link
natur.usikker; // true = no ADOPTED registration behind the yes — confirm with the kommuneScope
This SDK is a typed data-access client for the read endpoints — every live register
(BBR, Matriklen, DAGI, DHM, Stednavne, DAR/adresse, CVR, jordforurening/områdeklassificering,
naturbeskyttelse/skovbyggelinjer), the DAWA-compatible drop-in
layer (postnumre, vejnavne, ejerlav, autocomplete, datavask, reverse-geocoding),
GeoDanmark building footprints, kodelister, point-in-time lookups (?asOf/historik),
and the batch + bulk-export helpers. Account, API-key, and billing management are done
in the dashboard (you mint keys there), so the SDK
intentionally exposes no auth/keys/me/billing methods.
Batch & streaming
// Up to 50 BFEs per call. Partial-failure aware: each BFE lands in results or errors,
// and the call itself succeeds even if some BFEs fail. One billable unit per deduped BFE.
const { results, errors } = await gf.ejendomBatch([5651067, 12345678]);
// Auto-paginate an arbitrarily large BFE array: chunks into <=50-sized batch calls
// and yields each successfully-resolved property (failures are skipped — use
// ejendomBatch directly if you need to inspect them).
for await (const item of gf.ejendomStream(thousandsOfBfes)) {
console.log(item.bfe, item.ejendom.antal_bygninger, item.stale);
}Error handling
Every non-2xx response (and a request timeout, as status: 408) throws a
GrundfastError carrying the HTTP status — after the retry policy above has given
up. On a 429 the retryAfter field holds the Retry-After seconds, so you can
back off over the waits the client deliberately won't sleep on itself:
import { GrundfastClient, GrundfastError } from '@grundfast/sdk';
try {
await gf.ejendom(999999999);
} catch (err) {
if (err instanceof GrundfastError) {
console.error(err.status, err.message); // e.g. 404 "property not found"
if (err.status === 429 && err.retryAfter) {
await new Promise((r) => setTimeout(r, err.retryAfter * 1000));
}
}
}Migrating from DAWA
DAWA (Danmarks Adressers Web API) closes 1 October 2026 at 10:00. The date moved twice — it was announced for 1 July, then 17 August — so older write-ups still quote the earlier ones; the current date is confirmed in Dataforsyningen's announcement (updated 12 June 2026).
Address identifiers come from DAR in both cases, so stored address UUIDs keep
resolving — there is no re-keying step. Point the base URL at
https://api.grundfast.dk/v1 and send a bearer key:
| DAWA | Grundfast | SDK |
| ------------------------------------- | ------------------------------- | ------------------------------------ |
| /autocomplete | /v1/autocomplete | gf.autocomplete(q) |
| /adgangsadresser/{id} | /v1/adresse/{uuid} | gf.adresse(uuid) |
| /adgangsadresser/reverse | /v1/adgangsadresser/reverse | gf.adgangsadresseReverse(lon, lat) |
| /datavask/adgangsadresser | /v1/datavask/adgangsadresser | gf.datavask(text) |
| /jordstykker/reverse | /v1/jordstykker/reverse | gf.jordstykkeReverse(lon, lat) |
| /postnumre | /v1/postnumre | gf.postnumre() |
| /vejnavne | /v1/vejnavne | gf.vejnavne() |
| /ejerlav | /v1/ejerlav | gf.ejerlav() |
| /kommuner, /regioner, /sogne, … | /v1/dagi/{tema} | gf.dagiList(tema) |
| — (not in DAWA) | /v1/adgangsadresser/bfe/{bfe} | gf.adgangsadresserByBfe(bfe) |
DAWA was keyless; Grundfast requires a key and meters usage. Beyond the like-for-like
swap, every address carries the BFE number of its property, so the same lookup
continues into BBR buildings and units and Matriklen parcels — see gf.ejendom(bfe).
Full endpoint-by-endpoint detail: grundfast.dk/en/dawa-replacement.
AI agents (MCP)
Grundfast also runs a Model Context Protocol server, published in the official MCP
registry as dk.grundfast/danish-address-property-data. It is a streamable-HTTP remote
at https://api.grundfast.dk/mcp with the same bearer key, exposing one read-only tool
per register. See grundfast.dk/en/mcp-server.
Links
- Grundfast — Danish property & geodata API
- SDK reference
- API documentation
