Webhooks
Registrér et HTTPS-endpoint, og Grundfast sender en signeret JSON-besked, når der sker noget: en nøgle oprettes eller tilbagekaldes, planen ændres, forbruget passerer en grænse, en overvåget ejendom ændrer sig, adresser eller bygninger i dit område ændrer sig, eller et job bliver færdigt. Gik en levering tabt, kan du hente den eller sende den igen.
Kræver Pro
402 på en lavere plan, og der leveres ingen hændelser. Oversigt, leveringslog og sletning virker stadig, så du kan rydde op efter en nedgradering. Se /priser.Registrér et endpoint
Webhooks oprettes, rettes, testes og slettes med din dashboard-session — i dashboardet eller på /v1/me/webhooks — ikke med en API-nøgle. Opret et endpoint med mindst én hændelse:
POST /v1/me/webhooks
Content-Type: application/json
{
"url": "https://example.com/hooks/grundfast",
"events": ["key.created", "usage.threshold"],
"description": "prod-overvågning"
}
HTTP/1.1 201 Created
{
"webhook": {
"id": "wh_...",
"url": "https://example.com/hooks/grundfast",
"events": ["key.created", "usage.threshold"],
"enabled": true,
"description": "prod-overvågning",
"secretPrefix": "whsec_a1b2c",
"secret": "whsec_...",
"createdAt": "2026-07-03T10:00:00.000Z"
}
}secret) returneres kun i svaret på oprettelsen. Gem den sikkert; den kan ikke hentes igen. URL’en skal være en offentlig https-adresse — http, localhost og private eller interne adresser afvises. description er valgfri, højst 200 tegn.| Metode | Sti | Hvad |
|---|---|---|
| GET | /v1/me/webhooks | List dine endpoints (uden signeringsnøgle). |
| POST | /v1/me/webhooks | Opret et endpoint. Kræver Pro. |
| PATCH | /v1/me/webhooks/:id | Ret url, events, description, enabled, adresseFilter eller cvrFilter. Kræver Pro. |
| DELETE | /v1/me/webhooks/:id | Slet et endpoint. |
| GET | /v1/me/webhooks/:id/deliveries | De seneste leveringsforsøg. |
| POST | /v1/me/webhooks/:id/test | Send en signeret test-ping — eller et eksempel på én bestemt hændelse — og se resultatet med det samme. Kræver Pro. |
| GET | /v1/me/webhooks/events | Alle hændelser til dine endpoints med leveringsstatus — hent det, du gik glip af. |
| POST | /v1/me/webhooks/:id/deliveries/:deliveryId/redeliver | Send én levering igen. Kræver Pro. |
| POST | /v1/me/webhooks/:id/redeliver-failed | Send alle fejlede hændelser igen, højst 500 pr. kald. Kræver Pro. |
Oversigten, leveringsloggen, hændelsesfeedet og de to genleveringer kan også kaldes med en API-nøgle, der har scopet webhooks — så din modtager selv kan rydde op efter et nedbrud.
Hændelser
| Hændelse | Hvornår | data |
|---|---|---|
| key.created | En API-nøgle blev oprettet. | { key_id, name, permission, prefix, kind } |
| key.revoked | En API-nøgle blev tilbagekaldt (også ved "Tilbagekald alle nøgler", én hændelse pr. nøgle). | { key_id } |
| subscription.updated | Kontoens plan ændrede sig. | { tier } |
| usage.threshold | Månedens forbrug passerede 80 % eller 100 % af kvoten. Hver grænse sendes højst én gang pr. måned, og passeres begge inden for samme time, kommer 80 % først. | { tier, threshold_pct, used, allowance, month } |
| ejendom.aendret | En ejendom på din overvågningsliste har ændret sig i ét eller flere registre siden sidste tjek. | { bfe_nummer, note, aendrede_registre, aendringer, kontrolleret } |
| adresse.oprettet | Nye adgangsadresser i dit område, fra den daglige DAR-opdatering. | { antal, sekvens_fra, sekvens_til, flere, aendringer } |
| adresse.aendret | Adresser i dit område har fået ny betegnelse, vej, husnummer, postnummer, kommune eller placering. | { antal, sekvens_fra, sekvens_til, flere, aendringer } |
| adresse.nedlagt | Adresser i dit område er nedlagt eller henlagt i DAR. | { antal, sekvens_fra, sekvens_til, flere, aendringer } |
| cvr.oprettet | Nye virksomheder i dit område, fra den ugentlige CVR-opdatering. | { antal, sekvens_fra, sekvens_til, flere, aendringer } |
| cvr.aendret | Virksomheder på din liste eller i dit område har fået nyt navn, ny status, virksomhedsform, adresse, kommune eller hovedbranche. | { antal, sekvens_fra, sekvens_til, flere, aendringer } |
| cvr.ophoert | Virksomheder på din liste eller i dit område er ophørt (status ophørt eller tvangsopløst, eller en ophørsdato er registreret). | { antal, sekvens_fra, sekvens_til, flere, aendringer } |
| bygning.tilkommet | Bygninger i dit område er nu registreret som opført — fundet, når en ny udgave af bygningsindekset sammenlignes med den forrige. | { antal, sekvens_fra, sekvens_til, flere, aendringer } |
| bygning.aendret | Bygninger i dit område har fået ændret en registreret oplysning (anvendelse, år, arealer, etager, varme, tag, ydervæg, asbest, olietank, adresse, BFE eller placering). | { antal, sekvens_fra, sekvens_til, flere, aendringer } |
| bygning.udgaaet | Bygninger i dit område er ikke længere registreret som opført — revet ned, eller registreringen er rettet. | { antal, sekvens_fra, sekvens_til, flere, aendringer } |
| job.completed | Et job er færdigt: alle rækker er behandlet, og mindst én lykkedes. Et job, du selv stopper, sender den ikke. | { job } |
| job.failed | Et job er færdigt, men ingen række kunne besvares (job.status er failed). Abonnerer et endpoint kun på job.completed, får det stadig fejlede job dér, som det altid har. | { job } |
ejendom.aendret, adresse.*, bygning.* og cvr.* hører til ejendomsovervågning og kræver, ligesom webhooks, Pro-planen. Vil du hellere have adresse- og bygningsændringer skubbet ud over én åben forbindelse end som webhooks, så brug live-strømmen.
Test et endpoint
POST /v1/me/webhooks/:id/test uden body sender en ping med data { message, webhook_id }. Med { "event": "adresse.aendret" } (eller en anden hændelse) sender den i stedet et eksempel på netop den hændelse, så din modtager prøves på den form, den rigtigt får. Body’en har "test": true, og svaret siger, hvor dataene kom fra:
log— den nyeste rigtige ændring af den slags i loggen, inden for endpointets område, hvis der er én dér, ellers den nyeste i landet.konto— din egen konto: dit seneste job, din seneste nøgle eller din plan.eksempel— en fast eksempelværdi, når der ikke findes noget rigtigt endnu.
POST /v1/me/webhooks/wh_.../test
Content-Type: application/json
{ "event": "job.failed" }
HTTP/1.1 200 OK
{
"ok": true,
"status": 200,
"error": null,
"event": "job.failed",
"kilde": "konto",
"payload": { "id": "whd_...", "event": "job.failed", "created": "…", "test": true, "data": { "job": { … } } }
}En test logges ikke som en levering og kan ikke sendes igen.
Besked og headere
Body’en er altid { id, event, created, data }, hvor created er ISO 8601 i UTC. Hændelsesnavn og leverings-id sendes også som headere, så du kan route og fjerne dubletter uden at parse body’en:
POST https://example.com/hooks/grundfast
Content-Type: application/json
User-Agent: Grundfast-Webhooks/1.0
X-Grundfast-Event: key.created
X-Grundfast-Delivery: whd_...
X-Grundfast-Signature: sha256=<hex>
{
"id": "whd_...",
"event": "key.created",
"created": "2026-07-03T10:00:00.000Z",
"data": { "key_id": 42, "name": "prod", "permission": "read_only", "prefix": "gf_live_...", "kind": "secret" }
}Adresse- og bygningshændelser i bundter
Et endpoint, der abonnerer på en adresse.*- eller bygning.*-hændelse, skal have et område i adresseFilter: kommunekoder, postnumre, konkrete adgangsadresse-id’er og/eller BFE-numre. En ændring leveres, når den ligger i én af de valgte kommuner eller postnumre, gælder et af id’erne, eller hører til et af BFE-numrene. Uden et område afvises oprettelsen med 400 — ingen modtager hele landets ændringer.
Et BFE-nummer fanger bygningsændringer på ejendommen og adresseændringer på de adresser, vores adresseindeks placerer på den. Hændelsestyperne vælger du med events: abonnér fx kun på bygning.udgaaet, hvis det er nedrivninger, du vil vide besked om.
POST /v1/me/webhooks
Content-Type: application/json
{
"url": "https://example.com/hooks/adresser",
"events": ["adresse.oprettet", "adresse.aendret", "adresse.nedlagt"],
"adresseFilter": {
"kommunekoder": ["0101", "0147"],
"postnumre": ["2770"],
"adgangsadresseIds": [],
"bfeNumre": [5651067]
}
}- Højst 10 kommunekoder, 50 postnumre, 1.000 adgangsadresse-id’er og 1.000 BFE-numre pr. endpoint. En tre-cifret kommunekode udfyldes med 0 foran.
- Adresseændringerne kommer fra den daglige DAR-opdatering. Én levering bærer op til 100 ændringer af samme slags; flere ændringer giver flere leveringer.
- Bygningsændringerne findes, når en ny udgave af det nationale bygningsindeks indlæses og sammenlignes med den forrige — de kommer altså samlet, når indekset er bygget igen. Det sker automatisk, så snart Datafordeler har udgivet et nyt samlet BBR-udtræk; udtrækkene udkommer med nogle dages forsinkelse, så ændringerne er typisk få dage gamle. Én levering bærer op til 50 bygninger af samme slags, hver i samme form som i ændringsfeedet plus sit
sekvens-nummer. - Hver ændring har et globalt, stigende
sekvens-nummer. Brug det til at genkende dubletter, og til at hente efterslæb fra ændringsfeedet med?efter=. flere: truebetyder, at flere ændringer af samme slags følger i de næste leveringer.- Et nyt abonnement starter fra nu: du får ikke de ændringer, der allerede ligger i loggen.
{
"id": "whd_...",
"event": "adresse.aendret",
"created": "2026-10-01T05:12:40.112Z",
"data": {
"antal": 1,
"sekvens_fra": 48211,
"sekvens_til": 48211,
"flere": false,
"aendringer": [
{
"sekvens": 48211,
"haendelse": "adresse.aendret",
"adgangsadresse_id": "0a3f507a-b2e6-32b8-e044-0003ba298018",
"betegnelse": "Rådhuspladsen 1, 1550 København V",
"kommunekode": "0101",
"postnr": "1550",
"status": "gaeldende",
"aendrede_felter": ["koordinat"],
"foer": {
"betegnelse": "Rådhuspladsen 1, 1550 København V",
"vejnavn": "Rådhuspladsen",
"husnr": "1",
"postnr": "1550",
"postnrnavn": "København V",
"kommunekode": "0101",
"koordinat": { "lon": 12.56953, "lat": 55.67568 }
},
"efter": {
"betegnelse": "Rådhuspladsen 1, 1550 København V",
"vejnavn": "Rådhuspladsen",
"husnr": "1",
"postnr": "1550",
"postnrnavn": "København V",
"kommunekode": "0101",
"koordinat": { "lon": 12.56961, "lat": 55.67571 }
},
"dar_udtraek": "DAR_20261001040010",
"registreret": "2026-10-01T05:10:02.551Z"
}
]
}
}For adresse.oprettet er foer null, og for adresse.nedlagt er efter null. aendrede_felter er kun udfyldt for adresse.aendret.
Virksomhedshændelser (CVR)
Et endpoint, der abonnerer på en cvr.*-hændelse, skal have et cvrFilter: en liste CVR-numre (fx den portefølje af firmaer, du vil vide det om, når de ophører) og/eller kommunekoder. En ændring leveres, når virksomheden er på listen, eller dens adresse ligger — eller lå, før en flytning — i én af kommunerne. Uden filter afvises oprettelsen med 400.
POST /v1/me/webhooks
Content-Type: application/json
{
"url": "https://example.com/hooks/cvr",
"events": ["cvr.ophoert", "cvr.aendret"],
"cvrFilter": {
"cvrNumre": [12345678, 87654321],
"kommunekoder": []
}
}- Højst 10.000 CVR-numre og 10 kommunekoder pr. endpoint.
- Ændringerne findes, når hele CVR hver uge sammenlignes med Grundfasts kopi — en ændring kommer altså op til ca. en uge efter, CVR registrerede den. Kun felter vi udstiller tæller: navn, status, ophørsdato, virksomhedsform, adresse, kommune og hovedbranche.
cvr.ophoertsendes i stedet forcvr.aendret, når virksomheden går fra aktiv til ophørt;aendrede_felternævner alle felter, der flyttede sig samtidig. Abonnér på begge, hvis du vil have alt.- Samme bundter som adressehændelser: op til 100 ændringer af samme slags pr. levering, et globalt
sekvens-nummer, og efterslæb fra ændringsfeedet med?efter=. Et nyt abonnement starter fra nu.
{
"id": "whd_...",
"event": "cvr.ophoert",
"created": "2026-10-04T03:15:02.004Z",
"data": {
"antal": 1,
"sekvens_fra": 9120,
"sekvens_til": 9120,
"flere": false,
"aendringer": [
{
"sekvens": 9120,
"haendelse": "cvr.ophoert",
"cvr_nummer": 12345678,
"navn": "Eksempel ApS",
"kommunekode": "0101",
"status": "ophørt",
"aendrede_felter": ["status", "ophoersdato"],
"foer": { "navn": "Eksempel ApS", "status": "aktiv", "ophoersdato": null, "...": "..." },
"efter": { "navn": "Eksempel ApS", "status": "ophørt", "ophoersdato": "2026-09-30", "...": "..." },
"cvr_udtraek": "CVR_20261003",
"registreret": "2026-10-04T03:12:40.118Z"
}
]
}
}foer og efter har felterne navn, status, ophoersdato, virksomhedsform, adresse, kommunekode, kommune og branche ({ kode, tekst }). For cvr.oprettet er foer null.
Ændringer på overvågede ejendomme
ejendom.aendret sendes én gang pr. ejendom og tjek, når mindst ét af registrene BBR, vurdering, plan og jord har ændret sig. aendringer har et kort før/efter-sammendrag pr. ændret register:
{
"id": "whd_...",
"event": "ejendom.aendret",
"created": "2026-10-01T09:41:17.006Z",
"data": {
"bfe_nummer": 5651067,
"note": "Portefølje Nord",
"aendrede_registre": ["vurdering"],
"aendringer": [
{
"register": "vurdering",
"foer": {
"antal_vurderingsejendomme": 1,
"aar": 2022,
"ejendomsvaerdi_kr": 3150000,
"grundvaerdi_kr": 912000,
"vurderet_areal_m2": 812
},
"efter": {
"antal_vurderingsejendomme": 1,
"aar": 2024,
"ejendomsvaerdi_kr": 3400000,
"grundvaerdi_kr": 968000,
"vurderet_areal_m2": 812
}
}
],
"kontrolleret": "2026-10-01T09:41:16.880Z"
}
}Færdige job
job.completed bærer hele jobbet i samme form som GET /v1/jobs/:id, så du kan hente filerne med det samme uden at spørge først. Hændelsen sendes højst én gang pr. job.
{
"id": "whd_...",
"event": "job.completed",
"created": "2026-10-05T11:02:44.913Z",
"data": {
"job": {
"id": "job_4n8q2v7k1m9x3c5z0w6r2t8b",
"kind": "datavask",
"status": "completed",
"totalRows": 2,
"processedRows": 2,
"succeeded": 2,
"failed": 0,
"billedUnits": 2,
"progress": 1,
"resultsAvailable": true
}
}
}Verificér signaturen
X-Grundfast-Signature er sha256= efterfulgt af den hex-kodede HMAC-SHA256 af den rå request-body med din whsec_-nøgle. Beregn den over de præcise bytes, før du parser JSON, og sammenlign i konstant tid:
import { createHmac, timingSafeEqual } from 'node:crypto';
import express from 'express';
export function verifyGrundfastWebhook(
rawBody: string,
header: string | undefined,
secret: string,
): boolean {
if (!header) return false;
const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex');
const a = Buffer.from(header);
const b = Buffer.from(expected);
// Længden først: timingSafeEqual kaster ved forskellig længde.
return a.length === b.length && timingSafeEqual(a, b);
}
const secret = process.env.GRUNDFAST_WEBHOOK_SECRET ?? '';
const app = express();
// Læs den RÅ body — ikke et objekt, der er parset og serialiseret igen.
app.post('/hooks/grundfast', express.raw({ type: 'application/json' }), (req, res) => {
const rawBody = req.body.toString('utf8');
if (!verifyGrundfastWebhook(rawBody, req.header('X-Grundfast-Signature'), secret)) {
res.status(401).send('bad signature');
return;
}
const event = JSON.parse(rawBody);
// Håndtér event.event og event.data. Svar hurtigt med 2xx; lav det tunge arbejde bagefter.
res.sendStatus(200);
});Bruger du SDK’et (fra 0.24.0), gør verifyWebhookSignature det samme med Web Crypto — i Node, Bun, Deno og edge-miljøer:
import { verifyWebhookSignature } from '@grundfast/sdk';
export async function POST(request: Request): Promise<Response> {
const raw = await request.text();
const ok = await verifyWebhookSignature(
raw,
request.headers.get('X-Grundfast-Signature'),
process.env.GRUNDFAST_WEBHOOK_SECRET ?? '',
);
if (!ok) return new Response('bad signature', { status: 401 });
const event = JSON.parse(raw);
// Håndtér event.event og event.data.
return new Response(null, { status: 200 });
}Levering og nye forsøg
En levering er gennemført, når dit endpoint svarer 2xx inden for 10 sekunder. Alt andet — en anden status, timeout eller netværksfejl — prøves igen med stigende pause, op til 5 forsøg i alt:
forsøg 1 → fejl → vent 1 min
forsøg 2 → fejl → vent 5 min
forsøg 3 → fejl → vent 30 min
forsøg 4 → fejl → vent 2 timer
forsøg 5 → fejl → status "failed" (opgives)- Redirects følges ikke; et
30x-svar tæller som en fejlet levering. - Hver levering står i loggen som
pending,successellerfailedmed antal forsøg, seneste HTTP-status og eventuel fejl. Hent den påGET /v1/me/webhooks/:id/deliveries. - Den samme hændelse kan i sjældne tilfælde nå frem mere end én gang. Brug
X-Grundfast-Deliverytil at genkende dubletter, og gør din håndtering idempotent.
Send igen
Var din modtager nede, så send det, den gik glip af, igen — fra dashboardet eller med API’et. En genlevering er den samme hændelse: samme body, samme id og samme X-Grundfast-Delivery, plus headeren X-Grundfast-Redelivery: true. Genkender du dubletter på id’et, behandles den kun én gang.
POST /v1/me/webhooks/wh_.../deliveries/whd_.../redeliver
HTTP/1.1 202 Accepted
{ "delivery": { "id": "whd_...", "status": "pending", "redeliveryOf": "whd_...", … } }
POST /v1/me/webhooks/wh_.../redeliver-failed
Content-Type: application/json
{ "siden": "2026-10-10" }
HTTP/1.1 202 Accepted
{ "queued": 37, "hasMore": false }- Genleveringen sættes i kø og sendes inden for et minut, med de samme nye forsøg som en almindelig levering.
redeliver-failedtager hver hændelse, hvis levering endte somfailed, og som ikke allerede er sendt igen med held eller står i kø — ældste først, højst 500 pr. kald (antal). ErhasMoretrue, så kald igen. To kald efter hinanden sender intet to gange.409, hvis hændelsen allerede står i kø, eller hvis endpointet er sat på pause — genaktivér det først.- Højst 120 enkelte genleveringer og 10 kald til
redeliver-failedi timen pr. konto; derover svarer API’et429medRetry-After.
Hent det, du gik glip af
GET /v1/me/webhooks/events lister alle hændelser, Grundfast har dannet til dine endpoints, ældste først — med data præcis som den blev (eller bliver) sendt, og hvordan leveringen gik. Det er den sikre vej tilbage efter et nedbrud: læs fra dit sidste kendte tidspunkt, og behandl det, du ikke har set.
GET /v1/me/webhooks/events?status=failed&since=2026-10-10
HTTP/1.1 200 OK
{
"events": [
{
"id": "whd_...",
"event": "adresse.aendret",
"created": "2026-10-10T05:12:40.112Z",
"data": { "antal": 3, "sekvens_fra": 48211, "sekvens_til": 48230, "flere": false, "aendringer": [ … ] },
"webhookId": "wh_...",
"delivery": {
"id": "whd_...",
"status": "failed",
"attempts": 5,
"responseStatus": 503,
"error": "endpoint returned 503",
"lastAttemptAt": "2026-10-10T08:14:02.551Z",
"deliveredAt": null,
"redeliveries": 0
}
}
],
"nextCursor": "ZTo0ODI",
"hasMore": false,
"retentionDays": 30,
"coveredFrom": "2026-09-12T09:00:00.000Z"
}- Filtrér med
status(pending,success,failed),event,webhook(et endpoint-id) ogsince.statuser den seneste tilstand — en hændelse, der fejlede og siden blev sendt igen med held, står somsuccessmedredeliveries: 1. - Sid med
cursor=nextCursor, op til 100 pr. side (limit, standard 25). Du springer aldrig over og ser aldrig en hændelse to gange. - Hændelser gemmes i
retentionDaysdage, samme tid som leveringsloggen. EtsinceførcoveredFromlæser fracoveredFrom. - Kan kaldes med din session eller en API-nøgle med scopet
webhooks.