Gå til hovedindhold
FirmaAPIFirmaAPI

Overvågning

CVR webhooks: få besked, når en virksomhed ændrer sig

Få et HTTP-kald til din server, når en virksomhed går konkurs, skifter status, navn, adresse eller får et nyt regnskab. Du slipper for at slå kunderne op igen og igen for at opdage ændringer.

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

EventBetyderPlan
company.status.changedStatussen er ændretStandard+
company.bankruptcyKonkurs eller tvangsopløsningStandard+
company.name.changedNavneændringProfessionel
company.address.changedAdresseændringProfessionel
company.participant.changedEjere eller ledelse ændretProfessionel
company.financials.publishedNyt årsregnskab offentliggjortProfessionel
company.industry.changedBranchekode ændretProfessionel
company.type.changedSelskabsform ændretProfessionel
company.capital.changedSelskabskapital ændretProfessionel
company.createdNy virksomhed registreretProfessionel

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", 200

Modtageren 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"]
  }
}
Er du på Standard eller Basis og vil følge en fast kundeliste, kan du i stedet slå listen op en gang i døgnet med batch-endpointet og sammenligne status med det, du har gemt. Batch kræver Basis og tager op til 100 CVR-numre pr. request på Basis og 500 på Professionel.

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_at og tjek om nødvendigt virksomhedens aktuelle status med et opslag.
  • Leveringslog. GET /api/v1/webhooks/{id}/deliveries viser, hvad der er sendt, og hvordan din server svarede.
  • Test. POST /api/v1/webhooks/{id}/test sender 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.

Ofte stillede spørgsmål

Hvilken plan skal jeg bruge til webhooks?

Webhooks starter på Standard (299 kr om måneden) med events for statusændringer og konkurs. Professionel (469 kr) giver adgang til alle events og til filtre på branche, kommune og personer.

Kan jeg overvåge en bestemt liste af CVR-numre?

Ja, på Professionel. Sæt filteret cvr_numbers med op til 1.000 CVR-numre på webhooken, så får du kun events for de virksomheder. Du kan også filtrere på branchekoder og kommunekoder. På Standard får du statusændringer og konkurser for hele registret og filtrerer selv i din modtager på data.company.cvr.

Hvordan ved jeg, at en webhook kommer fra FirmaAPI?

Hver levering er signeret med HMAC-SHA256. Signaturen sendes i headeren X-FirmaAPI-Signature og beregnes over tidsstemplet fra X-FirmaAPI-Timestamp, et punktum og den rå request-body, med dit webhook-secret som nøgle.

Hvad sker der, hvis min server er nede?

Svarer din modtager ikke med en 2xx-status, forsøger vi igen med stigende pauser: 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 en webhook 8 gange i træk, sættes den på pause, og du får en e-mail. Brug event-id'et til at undgå at behandle den samme hændelse to gange.

Kan jeg teste en webhook, før en rigtig virksomhed ændrer sig?

Ja. Endpointet POST /api/v1/webhooks/{id}/test sender et test-event til din URL. Du kan se alle leveringer og deres status med GET /api/v1/webhooks/{id}/deliveries.

Sæt din første webhook op

Webhooks kræver mindst Standard-planen. Med test-nøglen kan du afprøve webhook-API'et og se eksempler på leveringer, men den sender ingen rigtige hændelser.