Hvorfor webhooks i stedet for opslag?
Hvis du vil vide, om en kunde er gået konkurs, kan du slå CVR-nummeret op hver nat. Det virker, men det koster opslag, og du opdager først ændringen næste gang, du kigger. Med webhooks vender det om: FirmaAPI kalder dig, når der er noget nyt.
Typiske brugssager er kreditkontrol i en økonomiafdeling, leverandørstyring, kundekartotek og compliance. Du får fx besked, når en leverandør skifter status, så du kan stoppe en betaling eller kontakte kunden.
Events og planer
| Event | Betyder | Plan |
|---|---|---|
| company.status.changed | Statussen er ændret | Standard+ |
| company.bankruptcy | Konkurs eller tvangsopløsning | Standard+ |
| company.name.changed | Navneændring | Professionel |
| company.address.changed | Adresseændring | Professionel |
| company.participant.changed | Ejere eller ledelse ændret | Professionel |
| company.financials.published | Nyt årsregnskab offentliggjort | Professionel |
| company.industry.changed | Branchekode ændret | Professionel |
| company.type.changed | Selskabsform ændret | Professionel |
| company.capital.changed | Selskabskapital ændret | Professionel |
| company.created | Ny virksomhed registreret | Professionel |
Professionel har også events for fusioner og spaltninger (company.merged og company.demerged) og for personer (person.*), fx når en person får en ny rolle eller udtræder. Se den fulde liste i dokumentationen.
Opret en webhook
URL'en skal bruge HTTPS og må ikke pege på localhost eller private IP-adresser. Svaret på oprettelsen indeholder dit webhook-secret. Det vises kun, når du opretter, så gem det sikkert med det samme.
curl -X POST https://firmaapi.dk/api/v1/webhooks \
-H "Authorization: Bearer cvr_live_din_api_nøgle" \
-H "Content-Type: application/json" \
-d '{
"url": "https://din-server.dk/webhooks/firmaapi",
"event_types": ["company.status.changed", "company.bankruptcy"],
"description": "Kundeovervågning"
}'Prøver du at oprette en event, som din plan ikke dækker, eller er planen for lav til webhooks, får du 403 PLAN_UPGRADE_REQUIRED med required_plan og upgrade_url.
Hvordan en levering ser ud
Alle events har samme konvolut med et unikt id, en type, et tidspunkt og et data-objekt. Ved ændringer indeholder changes den gamle og den nye værdi.
company.status.changed (eksempel)
{
"id": "evt_abc123",
"type": "company.status.changed",
"created_at": "2026-03-15T10:30:00.000Z",
"data": {
"company": {
"cvr": "12345678",
"name": "Firma A/S",
"status": "Under konkurs"
},
"changes": {
"status": { "old": "Aktiv", "new": "Under konkurs" }
}
}
}Verificér signaturen og modtag eventet
Hver levering har headerne X-FirmaAPI-Signature, X-FirmaAPI-Timestamp, X-FirmaAPI-Event og X-FirmaAPI-Delivery. Signaturen er en HMAC-SHA256 af tidsstemplet, et punktum og den rå body. Brug altid den rå body fra requesten, fordi en body, der er parset og skrevet ud igen, kan få en anden rækkefølge af felter eller mellemrum.
Node.js med Express
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.FIRMAAPI_WEBHOOK_SECRET;
const MINE_KUNDER = new Set(["12345678", "87654321"]); // hent fra din database
function erGyldig(rawBody, timestamp, signature) {
const forventet = crypto
.createHmac("sha256", SECRET)
.update(timestamp + "." + rawBody)
.digest("hex");
const a = Buffer.from(forventet);
const b = Buffer.from(signature || "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Vigtigt: brug den rå body. Signaturen passer ikke, hvis du parser og stringifier igen.
app.post("/webhooks/firmaapi", express.raw({ type: "application/json" }), (req, res) => {
const rawBody = req.body.toString("utf8");
const sig = req.get("X-FirmaAPI-Signature");
const ts = req.get("X-FirmaAPI-Timestamp");
if (!erGyldig(rawBody, ts, sig)) {
return res.status(401).send("Ugyldig signatur");
}
const event = JSON.parse(rawBody);
const cvr = event.data?.company?.cvr;
if (cvr && MINE_KUNDER.has(cvr)) {
// Læg i en kø, og svar med det samme. Brug event.id til at undgå dubletter.
console.log(event.type, cvr, event.data.changes);
}
res.status(200).send("OK");
});
app.listen(3000);Python med Flask
import hashlib
import hmac
import os
from flask import Flask, request
app = Flask(__name__)
SECRET = os.environ["FIRMAAPI_WEBHOOK_SECRET"].encode()
MINE_KUNDER = {"12345678", "87654321"} # hent fra din database
behandlede = set() # brug en database eller Redis i produktion
def er_gyldig(raw_body: bytes, timestamp: str, signature: str) -> bool:
forventet = hmac.new(
SECRET, f"{timestamp}.".encode() + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(forventet, signature or "")
@app.post("/webhooks/firmaapi")
def modtag():
raw = request.get_data()
if not er_gyldig(
raw,
request.headers.get("X-FirmaAPI-Timestamp", ""),
request.headers.get("X-FirmaAPI-Signature", ""),
):
return "Ugyldig signatur", 401
event = request.get_json()
if event["id"] in behandlede:
return "OK", 200
behandlede.add(event["id"])
cvr = event["data"].get("company", {}).get("cvr")
if cvr in MINE_KUNDER:
print(event["type"], cvr, event["data"].get("changes"))
return "OK", 200Modtageren svarer hurtigt og gør det tunge arbejde bagefter, fx fra en kø. Svar altid med en 2xx-status, når eventet er modtaget, også hvis det ikke angår en af dine kunder.
Overvåg kun dine egne kunder
På Professionel kan du give webhooken en liste med op til 1.000 CVR-numre i filteret cvr_numbers. Så får du kun events for netop de virksomheder. Du kan også indsnævre strømmen til bestemte brancher eller kommuner. På Standard får du statusændringer og konkurser for hele registret og filtrerer selv i modtageren på data.company.cvr, som i eksemplerne ovenfor.
{
"url": "https://din-server.dk/webhooks/firmaapi",
"event_types": ["company.status.changed", "company.address.changed"],
"filters": {
"cvr_numbers": ["24256790", "10150817"]
}
}Dagligt batch-opslag
curl -X POST https://firmaapi.dk/api/v1/company/batch \
-H "Authorization: Bearer cvr_live_din_api_nøgle" \
-H "Content-Type: application/json" \
-d '{"cvr_numbers": ["12345678", "87654321", "24256790"]}'Drift: genforsøg, dubletter og overvågning
- Genforsøg. Ved fejl prøver vi igen efter 1 minut, 5 minutter, 30 minutter, 2 timer, 6 timer, 12 timer og 24 timer. Hver levering forsøges højst 8 gange over cirka 45 timer. Fejler webhooken 8 gange i træk, sættes den på pause, og du får en e-mail. Fejlede leveringer kan sendes igen med redeliver.
- Dubletter. Gem id'et på de events, du har behandlet, og ignorer gengangere.
- Rækkefølge. Gå ikke ud fra, at events kommer i den rækkefølge, de skete. Brug
created_atog tjek om nødvendigt virksomhedens aktuelle status med et opslag. - Leveringslog.
GET /api/v1/webhooks/{id}/deliveriesviser, hvad der er sendt, og hvordan din server svarede. - Test.
POST /api/v1/webhooks/{id}/testsender et test-event.
Læs mere om, hvordan du reagerer på en konkurs med en webhook, i guiderne på bloggen, eller se hvilke data der følger med på siden om ejerdata.