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
| Status | Kode | Hvad du gør |
|---|---|---|
| 400 | INVALID_CVR | Valider input, før du kalder |
| 401 | INVALID_API_KEY | Tjek miljøvariablen |
| 403 | PLAN_UPGRADE_REQUIRED | Planen mangler adgang. Se required_plan |
| 404 | NOT_FOUND | Virksomheden findes ikke |
| 429 | RATE_LIMIT_EXCEEDED | Vent Retry-After sekunder (håndteres af klienten) |
| 429 | MONTHLY_LIMIT_EXCEEDED | Kvoten 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.