Det skal du bruge
Du skal bruge en API-nøgle, Python 3.10 eller nyere og pakken requests. Du kan oprette en nøgle gratis og få 1.000 opslag om måneden. Gem nøglen i en miljøvariabel.
pip install requests
export FIRMAAPI_KEY="cvr_live_din_api_nøgle"Alle kald går til https://firmaapi.dk/api/v1, og nøglen sendes i headeren Authorization: Bearer cvr_live_.... Vil du først teste uden at bruge af kvoten, bruger du nøglens separate test-nøgle, der starter med cvr_test_.
En lille klient med genforsøg
Klienten herunder samler det, du har brug for i alle kald: en Session, der genbruger forbindelsen, en timeout, genforsøg ved 429 og 502/503 og en fejlklasse med kode og besked. Funktionen er_gyldigt_cvr tjekker modulus-11, så du ikke bruger opslag på CVR-numre med tastefejl.
firmaapi.py
import os
import time
import requests
BASE_URL = "https://firmaapi.dk/api/v1"
class FirmaApiError(Exception):
"""Fejl fra API'et. status er HTTP-statussen, code er fx NOT_FOUND."""
def __init__(self, status: int, code: str, message: str):
super().__init__(f"{status} {code}: {message}")
self.status = status
self.code = code
self.message = message
def er_gyldigt_cvr(cvr: str) -> bool:
"""Tjekker format og modulus 11, så du sparer opslag på tastefejl."""
cifre = "".join(ch for ch in cvr if ch.isdigit())
if len(cifre) != 8:
return False
vaegte = [2, 7, 6, 5, 4, 3, 2, 1]
return sum(int(c) * w for c, w in zip(cifre, vaegte)) % 11 == 0
class FirmaApi:
def __init__(self, api_key: str | None = None, timeout: float = 10, max_retries: int = 3):
self.session = requests.Session()
self.session.headers["Authorization"] = f"Bearer {api_key or os.environ['FIRMAAPI_KEY']}"
self.timeout = timeout
self.max_retries = max_retries
def _fejl(self, res: requests.Response) -> dict:
try:
return res.json().get("error", {})
except ValueError:
return {}
def _request(self, method: str, path: str, **kwargs) -> dict:
for forsoeg in range(self.max_retries + 1):
res = self.session.request(method, BASE_URL + path, timeout=self.timeout, **kwargs)
if res.ok:
return res.json()
fejl = self._fejl(res)
kode = fejl.get("code", "UNKNOWN")
kan_proeves_igen = (
(res.status_code == 429 and kode == "RATE_LIMIT_EXCEEDED")
or res.status_code in (502, 503)
)
if kan_proeves_igen and forsoeg < self.max_retries:
ventetid = float(res.headers.get("Retry-After", 2 ** forsoeg))
time.sleep(ventetid)
continue
raise FirmaApiError(res.status_code, kode, fejl.get("message", res.text[:200]))
raise RuntimeError("Uventet tilstand")
def virksomhed(self, cvr: str, include: str | None = None) -> dict:
params = {"include": include} if include else None
return self._request("GET", f"/company/{cvr}", params=params)
def soeg(self, q: str, status: str | None = "Aktiv", limit: int = 10) -> dict:
params = {"q": q, "limit": limit}
if status:
params["status"] = status
return self._request("GET", "/company/search", params=params)
def avanceret_soegning(self, **filtre) -> dict:
return self._request("GET", "/search/advanced", params=filtre)
def batch(self, cvr_numre: list[str]) -> dict:
return self._request("POST", "/company/batch", json={"cvr_numbers": cvr_numre})Genforsøg gælder kun fejl, der kan forsvinde af sig selv. Ved RATE_LIMIT_EXCEEDED venter koden det antal sekunder, headeren Retry-After angiver. Ved månedskvoten, MONTHLY_LIMIT_EXCEEDED, giver det ingen mening at prøve igen, så fejlen rejses med det samme.
Slå en virksomhed op
api = FirmaApi()
cvr = "24256790"
if not er_gyldigt_cvr(cvr):
raise SystemExit("Ugyldigt CVR-nummer")
try:
v = api.virksomhed(cvr)
print(v["name"], v["status"], v["city"])
except FirmaApiError as e:
if e.code == "NOT_FOUND":
print("CVR-nummeret findes ikke")
elif e.code == "MONTHLY_LIMIT_EXCEEDED":
print("Månedskvoten er brugt")
else:
raiseSvaret er et almindeligt dict med felter som cvr, name, status, address, zipcode, city, industry_text og employees. Felter kan være None, når oplysningen ikke er registreret, så brug .get() til valgfrie felter. Tunge blokke som historik og regnskaber hentes med include, fx api.virksomhed(cvr, include="history,finance"), hvis din plan giver adgang.
Søg og filtrér
Navnesøgningen returnerer et objekt med results og total. Den avancerede søgning har mere end 30 filtre og returnerer results, limit, offset og has_more. Brug offset til at hente næste side, mens has_more er sand.
# Find CVR-nummer ud fra et navn
svar = api.soeg("novo nordisk", limit=5)
for hit in svar["results"]:
print(hit["cvr"], hit["name"], hit["city"])
# Avanceret søgning: aktive ApS'er i København med mindst 10 ansatte
side = api.avanceret_soegning(
company_type="ApS",
municipality="København",
employees_min=10,
status="Aktiv",
limit=100,
offset=0,
)
print(len(side["results"]), side["has_more"])Mange CVR-numre ad gangen
Batch kræver Basis og tager op til 100 CVR-numre pr. kald på Basis, 200 på Standard og 500 på Professionel. Hvert fundet CVR-nummer tæller som ét opslag. Dubletter, ugyldigt formaterede numre og CVR-numre, der ikke findes, tæller ikke. Numre, der ikke findes, kommer i listen errors.
def hent_mange(api: FirmaApi, cvr_numre: list[str], blok: int = 100) -> dict[str, dict]:
"""Slår CVR-numre op i blokke. Basis tillader 100 pr. kald, Professionel 500."""
fundet: dict[str, dict] = {}
gyldige = [c for c in dict.fromkeys(cvr_numre) if er_gyldigt_cvr(c)]
for i in range(0, len(gyldige), blok):
svar = api.batch(gyldige[i : i + blok])
for v in svar["results"]:
fundet[v["cvr"]] = v
# svar["errors"] indeholder numre, der ikke blev fundet
return fundetFejl, 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 nøglen i miljøvariablen |
| 403 | PLAN_UPGRADE_REQUIRED | Planen mangler adgang. Se required_plan i svaret |
| 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 |
| 502/503 | ERST_ERROR / MAINTENANCE | Prøv igen lidt senere |
- Sæt altid en timeout, så en langsom forbindelse ikke blokerer din applikation.
- Log HTTP-status og fejlkode, men aldrig API-nøglen.
- Brug sandbox-nøglen i automatiske tests, så de ikke bruger af kvoten.