# Webhooks: få besked, når adresser ændrer sig

> Log ind for at få en JWT, opret, vis og slet webhooks via /api/v1/webhooks, og gem den hemmelighed, der kun vises én gang.
>
> https://danadresse.dk/guides/webhooks-opsaetning · verified 2026-11-02
En webhook sender et signeret `POST` til din server, når de entiteter, du følger, har ændret sig. Så slipper du for selv at spørge efter nye hændelser hele tiden. Webhooken fortæller, hvad der er ændret. Selve data henter du med [hændelses-API'et](/guides/replikering-haendelser), hvis du har brug for mere end id'erne.

Webhooks administreres kun via API'et. Der er ingen side til dem i dashboardet endnu, så alle trin herunder er HTTP-kald.

## 1. Forbered dit endpoint

Før du opretter webhooken, skal du have en URL, der:

- bruger `https://`. Andre URL'er afvises med `400 url_must_be_https`.
- kan nås fra internettet. Leveringerne kommer fra vores server.
- svarer med en 2xx-status inden for 6 sekunder. Gør selve arbejdet i en kø bagefter.
- svarer direkte uden redirect. En 3xx-status tæller som leveret, men indholdet når aldrig frem til den side, der redirectes til.
- tjekker signaturen. Se [Verificér webhook-signaturen](/guides/webhooks-signatur).

## 2. Log ind og hent en JWT

Webhook-endpoints bruger din kontos login, ikke din API-nøgle. Log ind med e-mail og adgangskode:

```bash id=login run=no
curl -s https://api.danadresse.dk/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "dig@firma.dk", "password": "din-adgangskode"}'
```

```json id=login-response run=no
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
  "token_type": "Bearer",
  "user": {"id": "…", "email": "dig@firma.dk", "plan": "free", "…": "…"}
}
```

Tokenet er gyldigt i 24 timer. Oprettede du kontoen med Google, har den ingen adgangskode. Sæt en via [Glemt adgangskode](/dashboard/forgot) først. Forkert login giver `401 invalid_credentials`, og for mange forsøg giver `429 too_many_attempts`.

## 3. Opret webhooken

Send URL'en og de entiteter, du vil følge. Gyldige entiteter er `postnummer`, `kommune`, `navngivenvej`, `vejstykke`, `adgangsadresse` og `adresse`. `kommune` kan vælges, men der kommer i øjeblikket ingen løbende ændringer for kommuner:

```bash id=create run=no
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"]}'
```

```json id=create-response run=no
{
  "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` vises kun i dette svar. Gem den i din secrets manager eller i en miljøvariabel på serveren, der modtager webhooken. Mister du den, skal du slette webhooken og oprette en ny.

En tom liste eller en ukendt entitet giver `400 invalid_entities`, og `details.invalid` viser, hvilke navne der var forkerte.

## 4. Vis og slet

```bash id=list-delete run=no
# 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"
```

Listen viser `is_active`, `failure_count` og `last_delivery_at` for hver webhook. Hold øje med dem, se [Webhooks: idempotens og fejl](/guides/webhooks-idempotens-og-fejl).

Der er ikke et endpoint til at ændre en webhook. Vil du skifte URL eller entiteter, eller genaktivere en webhook, der er slået fra efter for mange fejl, så slet den og opret en ny. Den nye får en ny hemmelighed.

## 5. Gør det fra et script

Det samme i Python, så du kan køre det fra din deploy-pipeline. E-mail og adgangskode læses fra miljøvariabler:

```python id=admin-script run=syntax
"""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. Hvad du modtager

Når en entitet, du følger, har ændret sig, får du ét `POST` pr. entitet:

```http id=delivery run=no
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"}
```

| Felt | Indhold |
|---|---|
| `entity` | Entiteten, der har ændret sig |
| `sekvensnummer` | Det højeste sekvensnummer i denne runde. Brug det som cursor |
| `count` | Antal ændringer i denne runde |
| `changes` | Id'erne på de ændrede rækker, højst 1.000. Er `count` større, så hent resten med hændelser. For `vejstykke` står der kun kommunekoden, så brug hændelser til at finde vejen |
| `timestamp` | Hvornår leveringen blev sendt (UTC) |

Leveringerne sendes, når vores synkronisering har hentet nye ændringer. Det sker omtrent én gang i timen. Der er ikke en knap til at sende en testlevering. Test i stedet dit endpoint ved at signere en eksempel-body selv, som vist i [Verificér webhook-signaturen](/guides/webhooks-signatur).

## 7. Hvad du gør med en levering

Hændelserne bag en levering ligger allerede i hændelsesloggen, når den sendes. Den normale reaktion er derfor: verificér signaturen, gem leveringen, svar `204`, og hent så hændelserne for den entitet med `/replikering/haendelser?entitet=…` fra din egen cursor + 1 op til leveringens `sekvensnummer`. Hele forløbet er: [fuldt udtræk](/guides/replikering-foerste-udtraek) én gang, derefter [hændelser](/guides/replikering-haendelser) fra din cursor, startet af webhooken og af et cron-job som sikkerhedsnet. Modtageren og workeren står i [Webhooks: idempotens og fejl](/guides/webhooks-idempotens-og-fejl).
