Webhooks: get notified when addresses change
Log in for a JWT, create, list and delete webhooks through /api/v1/webhooks, and store the secret that is shown only once.
A webhook sends a signed POST to your server when the entities you follow have changed. That saves you from asking for new events all the time. The webhook tells you what changed. You fetch the data itself with the events API if you need more than the ids.
Webhooks are managed through the API only. There is no page for them in the dashboard yet, so every step below is an HTTP call.
1. Prepare your endpoint
Before you create the webhook, you need a URL that:
- uses
https://. Other URLs are rejected with400 url_must_be_https. - is reachable from the internet. Deliveries come from our server.
- answers with a 2xx status within 6 seconds. Do the actual work in a queue afterwards.
- answers directly without a redirect. A 3xx status counts as delivered, but the content never reaches the page you redirect to.
- checks the signature. See Verify the webhook signature.
2. Log in and get a JWT
The webhook endpoints use your account login, not your API key. Log in with email and password:
curl -s https://api.danadresse.dk/api/v1/auth/login \ -H "Content-Type: application/json" \ -d '{"email": "dig@firma.dk", "password": "din-adgangskode"}'
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
"token_type": "Bearer",
"user": {"id": "…", "email": "dig@firma.dk", "plan": "free", "…": "…"}
}The token is valid for 24 hours. If you created the account with Google, it has no password. Set one through Forgot password first. A wrong login gives 401 invalid_credentials, and too many attempts give 429 too_many_attempts.
3. Create the webhook
Send the URL and the entities you want to follow. Valid entities are postnummer, kommune, navngivenvej, vejstykke, adgangsadresse and adresse. kommune can be selected, but there are currently no ongoing changes for municipalities:
curl -s https://api.danadresse.dk/api/v1/webhooks \ -H "Authorization: Bearer $DANADRESSE_JWT" \ -H "Content-Type: application/json" \ -d '{"url": "https://app.firma.dk/hooks/danadresse", "entities": ["adresse", "adgangsadresse"]}'
{
"id": "6f1c2a9e-1d2b-4c51-9a57-2f4f0d3e8b11",
"url": "https://app.firma.dk/hooks/danadresse",
"entities": ["adresse", "adgangsadresse"],
"is_active": true,
"failure_count": 0,
"created_at": "2026-10-15T12:20:04",
"last_delivery_at": null,
"secret": "9c4e…64 hex-tegn…",
"warning": "Save the secret now — it is shown only once."
}secret is only shown in this response. Store it in your secrets manager or in an environment variable on the server that receives the webhook. If you lose it, delete the webhook and create a new one.
An empty list or an unknown entity gives 400 invalid_entities, and details.invalid shows which names were wrong.
4. List and delete
# Vis dine webhooks (uden hemmeligheden) curl -s https://api.danadresse.dk/api/v1/webhooks \ -H "Authorization: Bearer $DANADRESSE_JWT" # Slet en curl -s -X DELETE https://api.danadresse.dk/api/v1/webhooks/6f1c2a9e-1d2b-4c51-9a57-2f4f0d3e8b11 \ -H "Authorization: Bearer $DANADRESSE_JWT"
The list shows is_active, failure_count and last_delivery_at for each webhook. Keep an eye on them, see Webhooks: idempotency and failures.
There is no endpoint to change a webhook. To change the URL or entities, or to re-enable a webhook that was switched off after too many failures, delete it and create a new one. The new one gets a new secret.
5. Do it from a script
The same in Python, so you can run it from your deploy pipeline. Email and password are read from environment variables:
"""webhook_admin.py: opret, vis og slet webhooks via /api/v1/webhooks. DANADRESSE_EMAIL=… DANADRESSE_PASSWORD=… python webhook_admin.py list … python webhook_admin.py create https://app.example.dk/hooks/danadresse adresse adgangsadresse … python webhook_admin.py delete <id> """ import json import os import sys import requests BASE_URL = os.environ.get("DANADRESSE_BASE_URL", "https://api.danadresse.dk") def login() -> str: r = requests.post(f"{BASE_URL}/api/v1/auth/login", timeout=10, json={ "email": os.environ["DANADRESSE_EMAIL"], "password": os.environ["DANADRESSE_PASSWORD"], }) if not r.ok: raise SystemExit(f"login fejlede: {r.status_code} {r.json().get('error', {}).get('code')}") return r.json()["access_token"] # JWT, gyldig i 24 timer def main(argv: list[str]) -> None: if not argv or argv[0] not in ("list", "create", "delete"): raise SystemExit(__doc__) auth = {"Authorization": f"Bearer {login()}"} if argv[0] == "list": r = requests.get(f"{BASE_URL}/api/v1/webhooks", headers=auth, timeout=10) elif argv[0] == "create": url, entities = argv[1], argv[2:] r = requests.post(f"{BASE_URL}/api/v1/webhooks", headers=auth, timeout=10, json={"url": url, "entities": entities}) else: r = requests.delete(f"{BASE_URL}/api/v1/webhooks/{argv[1]}", headers=auth, timeout=10) body = r.json() if not r.ok: err = body.get("error") or {} raise SystemExit(f"{r.status_code} {err.get('code')}: {err.get('message')}") if argv[0] == "create": # Hemmeligheden vises kun nu. Gem den, fx i din secrets manager. print("GEM DENNE HEMMELIGHED:", body["secret"], file=sys.stderr) body.pop("secret") print(json.dumps(body, indent=2, ensure_ascii=False)) if __name__ == "__main__": main(sys.argv[1:])
6. What you receive
When an entity you follow has changed, you get one POST per entity:
POST /hooks/danadresse HTTP/1.1
Host: app.firma.dk
Content-Type: application/json
User-Agent: danadresse-webhooks/1.0
X-Danadresse-Entity: adresse
X-Danadresse-Signature: 5d1f0c…(hex HMAC-SHA256 af hele body)
{"entity": "adresse", "sekvensnummer": 207922555, "count": 37, "changes": ["0a3f50ae-b4ee-32b8-e044-0003ba298018", "…"], "timestamp": "2026-10-15T13:08:11.512345+00:00"}| Field | Content |
|---|---|
entity | The entity that changed |
sekvensnummer | The highest sequence number in this round. Use it as a cursor |
count | Number of changes in this round |
changes | The ids of the changed rows, at most 1,000. If count is larger, fetch the rest with events. For vejstykke it only holds the municipality code, so use events to find the street |
timestamp | When the delivery was sent (UTC) |
Deliveries are sent when our sync has fetched new changes. That happens roughly once an hour. There is no button to send a test delivery. Instead, test your endpoint by signing a sample body yourself, as shown in Verify the webhook signature.
7. What to do with a delivery
The events behind a delivery are already in the event log when it is sent. So the usual reaction is: verify the signature, store the delivery, answer 204, and then fetch the events for that entity with /replikering/haendelser?entitet=… from your own cursor + 1 up to the delivery's sekvensnummer. The full flow is: full extract once, then events from your cursor, started by the webhook and by a cron job as a safety net. The receiver and worker are in Webhooks: idempotency and failures.
Related guides
Markdown source Spotted an error? Tell us API-reference (Swagger)