Gå til hovedindhold
FirmaAPIFirmaAPI

Guide

CVR API i Node.js: opslag, søgning og fejlhåndtering

En kort guide til at slå danske virksomheder op fra Node.js med den indbyggede fetch. Du får en lille klient uden afhængigheder, som håndterer timeout, genforsøg og fejlkoder.

Det skal du bruge

Du skal bruge Node 18 eller nyere og en API-nøgle. Opret en gratis konto for at få 1.000 opslag om måneden. Sæt nøglen som miljøvariabel, så den ikke står i koden:

export FIRMAAPI_KEY="cvr_live_din_api_nøgle"

Alle kald går til https://firmaapi.dk/api/v1 med headeren Authorization: Bearer .... Brug en nøgle, der starter med cvr_test_, når du vil teste uden at bruge af kvoten.

En klient uden afhængigheder

Klienten er en enkelt fil. Den bygger URL'en med URL og searchParams, sætter en timeout med AbortSignal.timeout, prøver igen ved midlertidige fejl og kaster en FirmaApiError med status og kode, når noget går galt.

// firmaapi.mjs
const BASE_URL = "https://firmaapi.dk/api/v1";

export class FirmaApiError extends Error {
  constructor(status, code, message) {
    super(status + " " + code + ": " + message);
    this.name = "FirmaApiError";
    this.status = status;
    this.code = code;
  }
}

export function erGyldigtCvr(input) {
  const cifre = String(input).replace(/\D/g, "");
  if (cifre.length !== 8) return false;
  const vaegte = [2, 7, 6, 5, 4, 3, 2, 1];
  const sum = [...cifre].reduce((s, c, i) => s + Number(c) * vaegte[i], 0);
  return sum % 11 === 0;
}

const vent = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

export class FirmaApi {
  constructor({ apiKey = process.env.FIRMAAPI_KEY, maxRetries = 3, timeoutMs = 10000 } = {}) {
    if (!apiKey) throw new Error("FIRMAAPI_KEY mangler");
    this.apiKey = apiKey;
    this.maxRetries = maxRetries;
    this.timeoutMs = timeoutMs;
  }

  async request(path, { method = "GET", query = {}, body } = {}) {
    const url = new URL(BASE_URL + path);
    for (const [navn, vaerdi] of Object.entries(query)) {
      if (vaerdi !== undefined && vaerdi !== null) url.searchParams.set(navn, String(vaerdi));
    }

    for (let forsoeg = 0; ; forsoeg++) {
      const res = await fetch(url, {
        method,
        headers: {
          Authorization: "Bearer " + this.apiKey,
          ...(body ? { "Content-Type": "application/json" } : {}),
        },
        body: body ? JSON.stringify(body) : undefined,
        signal: AbortSignal.timeout(this.timeoutMs),
      });

      if (res.ok) return res.json();

      const fejl = (await res.json().catch(() => ({}))).error ?? {};
      const kode = fejl.code ?? "UNKNOWN";
      const kanProevesIgen =
        (res.status === 429 && kode === "RATE_LIMIT_EXCEEDED") ||
        res.status === 502 ||
        res.status === 503;

      if (kanProevesIgen && forsoeg < this.maxRetries) {
        const sekunder = Number(res.headers.get("Retry-After")) || 2 ** forsoeg;
        await vent(sekunder * 1000);
        continue;
      }
      throw new FirmaApiError(res.status, kode, fejl.message ?? res.statusText);
    }
  }

  virksomhed(cvr, include) {
    return this.request("/company/" + encodeURIComponent(cvr), { query: { include } });
  }

  soeg(q, { status = "Aktiv", limit = 10 } = {}) {
    return this.request("/company/search", { query: { q, status, limit } });
  }

  avanceretSoegning(filtre) {
    return this.request("/search/advanced", { query: filtre });
  }

  batch(cvrNumre) {
    return this.request("/company/batch", { method: "POST", body: { cvr_numbers: cvrNumre } });
  }
}

Genforsøg sker kun ved RATE_LIMIT_EXCEEDED, 502 og 503. Svarer API'et MONTHLY_LIMIT_EXCEEDED, får du fejlen med det samme, fordi en pause ikke hjælper.

Slå en virksomhed op

import { FirmaApi, FirmaApiError, erGyldigtCvr } from "./firmaapi.mjs";

const api = new FirmaApi();
const cvr = "24256790";

if (!erGyldigtCvr(cvr)) {
  throw new Error("Ugyldigt CVR-nummer");
}

try {
  const v = await api.virksomhed(cvr);
  console.log(v.name, v.status, v.city);
} catch (err) {
  if (err instanceof FirmaApiError && err.code === "NOT_FOUND") {
    console.log("CVR-nummeret findes ikke");
  } else if (err instanceof FirmaApiError && err.code === "MONTHLY_LIMIT_EXCEEDED") {
    console.log("Månedskvoten er brugt");
  } else {
    throw err;
  }
}

Svaret er et almindeligt objekt med blandt andet cvr, name, status, address, zipcode, city, industry_text og employees. Felter, der ikke er registreret, er null, så brug fx v.phone ?? "ukendt" i stedet for at antage, at de findes. Vil du have historik eller regnskaber med, sender du api.virksomhed(cvr, "history,finance"), hvis din plan tillader det.

Søg og gennemløb alle sider

Navnesøgningen returnerer results og total. Den avancerede søgning har over 30 filtre og returnerer has_more, så du kan bladre sidevis med offset. En asynkron generator holder koden kort:

// Find CVR-nummer ud fra et navn
const { results } = await api.soeg("novo nordisk", { limit: 5 });
for (const hit of results) console.log(hit.cvr, hit.name, hit.city);

// Gennemløb alle sider af en avanceret søgning
async function* alleHit(filtre) {
  let offset = 0;
  while (true) {
    const side = await api.avanceretSoegning({ ...filtre, limit: 100, offset });
    yield* side.results;
    if (!side.has_more) return;
    offset += 100;
  }
}

for await (const v of alleHit({ industry: "62", city: "Aarhus", status: "Aktiv" })) {
  console.log(v.cvr, v.name);
}

Batch-opslag

Batch kræver Basis. Grænsen er 100 CVR-numre pr. kald på Basis, 200 på Standard og 500 på Professionel. Hvert fundet CVR-nummer tæller som ét opslag, mens dubletter og numre, der ikke findes, ikke tæller.

async function hentMange(api, cvrNumre, blok = 100) {
  const gyldige = [...new Set(cvrNumre)].filter(erGyldigtCvr);
  const fundet = new Map();

  for (let i = 0; i < gyldige.length; i += blok) {
    const svar = await api.batch(gyldige.slice(i, i + blok));
    for (const v of svar.results) fundet.set(v.cvr, v);
    // svar.errors indeholder numre, der ikke blev fundet
  }
  return fundet;
}

Brug klienten i en API-route

Her er et eksempel fra Next.js, der eksponerer et lille endpoint til din egen frontend. Browseren taler med din route, og din route taler med FirmaAPI. Nøglen forlader aldrig serveren.

// app/api/virksomhed/[cvr]/route.ts (Next.js)
import { FirmaApi, FirmaApiError, erGyldigtCvr } from "@/lib/firmaapi.mjs";

const api = new FirmaApi();

export async function GET(_req: Request, { params }: { params: Promise<{ cvr: string }> }) {
  const { cvr } = await params;
  if (!erGyldigtCvr(cvr)) {
    return Response.json({ fejl: "Ugyldigt CVR-nummer" }, { status: 400 });
  }
  try {
    const v = await api.virksomhed(cvr);
    return Response.json({ cvr: v.cvr, navn: v.name, status: v.status, by: v.city });
  } catch (err) {
    if (err instanceof FirmaApiError && err.status === 404) {
      return Response.json({ fejl: "Findes ikke" }, { status: 404 });
    }
    return Response.json({ fejl: "Opslaget fejlede" }, { status: 502 });
  }
}

Fejl, du bør håndtere

StatusKodeHvad du gør
400INVALID_CVRValider input, før du kalder
401INVALID_API_KEYTjek miljøvariablen
403PLAN_UPGRADE_REQUIREDPlanen mangler adgang. Se required_plan
404NOT_FOUNDVirksomheden findes ikke
429RATE_LIMIT_EXCEEDEDVent Retry-After sekunder (håndteres af klienten)
429MONTHLY_LIMIT_EXCEEDEDKvoten er brugt
  • Cache svar i hukommelsen eller en database, og slå kun op igen, når data skal være friske.
  • Log status og fejlkode, men aldrig API-nøglen.
  • Brug webhooks, hvis du vil have besked om ændringer i stedet for at slå op igen.
Se den gratis plan og priserne for kvoter og rate limits.

Ofte stillede spørgsmål

Hvilken Node-version skal jeg bruge?

Node 18 eller nyere, fordi eksemplerne bruger den indbyggede fetch og AbortSignal.timeout. Du behøver ingen eksterne pakker. Koden er skrevet som ES-moduler, så brug filendelsen .mjs eller sæt type til module i package.json.

Kan jeg bruge API'et direkte fra browseren?

Nej, du bør ikke. Nøglen ville blive synlig for alle besøgende. Kald API'et fra din server, fx en API-route i Next.js eller en Express-rute, og send kun de felter videre til browseren, som den har brug for.

Hvordan håndterer jeg flere samtidige opslag?

Hold dig under din plans rate limit (2 requests i sekundet på Gratis og 5 på Basis). Med Promise.all på hundredvis af kald rammer du hurtigt grænsen. Brug batch-endpointet, eller begræns samtidigheden til få kald ad gangen.

Er der en officiel Node-pakke?

FirmaAPI er et almindeligt REST API uden SDK, så du bruger fetch eller din foretrukne HTTP-klient. Til AI-assistenter findes MCP-serveren firmaapi-mcp-server på npm, men den er til Claude, Cursor og lignende værktøjer og ikke til almindelig applikationskode.

Prøv det med din egen API-nøgle

Opret en gratis konto, og hent din API-nøgle med det samme. Intet kreditkort kræves.