Det skal du bruge
Du skal bruge PHP 8.1 eller nyere med cURL og en API-nøgle. Den gratis plan giver 1.000 opslag om måneden. Læg nøglen i en miljøvariabel eller en konfigurationsfil uden for webroden:
export FIRMAAPI_KEY="cvr_live_din_api_nøgle"Alle kald går til https://firmaapi.dk/api/v1 med headeren Authorization: Bearer .... En nøgle med præfikset cvr_test_ giver syntetiske data uden at bruge af kvoten, så den er god til tests.
En klient i én fil
Gem koden som firmaapi.php. Klienten sætter en timeout, læser Retry-After fra svarheaderne, prøver igen ved midlertidige fejl og kaster en FirmaApiException med HTTP-status og fejlkode. Funktionen erGyldigtCvr tjekker modulus-11, så du ikke bruger opslag på tastefejl.
firmaapi.php
<?php
declare(strict_types=1);
final class FirmaApiException extends RuntimeException
{
public function __construct(
public readonly int $status,
public readonly string $errorCode,
string $message,
) {
parent::__construct($status . ' ' . $errorCode . ': ' . $message);
}
}
function erGyldigtCvr(string $input): bool
{
$cifre = preg_replace('/\D/', '', $input);
if (strlen($cifre) !== 8) {
return false;
}
$vaegte = [2, 7, 6, 5, 4, 3, 2, 1];
$sum = 0;
foreach (str_split($cifre) as $i => $ciffer) {
$sum += (int) $ciffer * $vaegte[$i];
}
return $sum % 11 === 0;
}
final class FirmaApi
{
private const BASE_URL = 'https://firmaapi.dk/api/v1';
public function __construct(
private string $apiKey,
private int $maxRetries = 3,
private int $timeout = 10,
) {
}
public function request(string $method, string $path, array $query = [], ?array $body = null): array
{
$query = array_filter($query, static fn ($v) => $v !== null);
$url = self::BASE_URL . $path . ($query ? '?' . http_build_query($query) : '');
for ($forsoeg = 0; ; $forsoeg++) {
$retryAfter = null;
$headers = ['Authorization: Bearer ' . $this->apiKey, 'Accept: application/json'];
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_HEADERFUNCTION => function ($handle, string $line) use (&$retryAfter): int {
if (stripos($line, 'Retry-After:') === 0) {
$retryAfter = (int) trim(substr($line, 12));
}
return strlen($line);
},
]);
if ($body !== null) {
$headers[] = 'Content-Type: application/json';
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body, JSON_THROW_ON_ERROR));
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$raw = curl_exec($ch);
if ($raw === false) {
$fejl = curl_error($ch);
if ($forsoeg < $this->maxRetries) {
sleep(2 ** $forsoeg);
continue;
}
throw new RuntimeException('Netværksfejl: ' . $fejl);
}
$status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode((string) $raw, true);
$data = is_array($data) ? $data : [];
if ($status >= 200 && $status < 300) {
return $data;
}
$fejl = $data['error'] ?? [];
$kode = $fejl['code'] ?? 'UNKNOWN';
$kanProevesIgen = ($status === 429 && $kode === 'RATE_LIMIT_EXCEEDED')
|| in_array($status, [502, 503], true);
if ($kanProevesIgen && $forsoeg < $this->maxRetries) {
sleep(max(1, $retryAfter ?? 2 ** $forsoeg));
continue;
}
throw new FirmaApiException($status, $kode, $fejl['message'] ?? 'Ukendt fejl');
}
}
public function virksomhed(string $cvr, ?string $include = null): array
{
return $this->request('GET', '/company/' . rawurlencode($cvr), ['include' => $include]);
}
public function soeg(string $q, string $status = 'Aktiv', int $limit = 10): array
{
return $this->request('GET', '/company/search', ['q' => $q, 'status' => $status, 'limit' => $limit]);
}
public function avanceretSoegning(array $filtre): array
{
return $this->request('GET', '/search/advanced', $filtre);
}
public function batch(array $cvrNumre): array
{
return $this->request('POST', '/company/batch', [], ['cvr_numbers' => array_values($cvrNumre)]);
}
}Genforsøg sker kun ved RATE_LIMIT_EXCEEDED, 502, 503 og netværksfejl. Er månedskvoten brugt (MONTHLY_LIMIT_EXCEEDED), rejses fejlen straks.
Slå en virksomhed op
<?php
require __DIR__ . '/firmaapi.php';
$api = new FirmaApi(getenv('FIRMAAPI_KEY') ?: '');
$cvr = '24256790';
if (!erGyldigtCvr($cvr)) {
exit("Ugyldigt CVR-nummer\n");
}
try {
$v = $api->virksomhed($cvr);
echo $v['name'] . ' (' . $v['status'] . '), ' . ($v['city'] ?? '') . "\n";
} catch (FirmaApiException $e) {
match ($e->errorCode) {
'NOT_FOUND' => print("CVR-nummeret findes ikke\n"),
'MONTHLY_LIMIT_EXCEEDED' => print("Månedskvoten er brugt\n"),
default => throw $e,
};
}Svaret er et array med blandt andet cvr, name, status, address, zipcode, city, industry_text og employees. Felter kan være null, så brug ?? til valgfrie felter. Vil du have historik eller regnskaber med, kalder du $api->virksomhed($cvr, 'history,finance'), hvis planen tillader det.
Søg og bladr
Navnesøgningen returnerer results og total. Den avancerede søgning har over 30 filtre og returnerer has_more, så du kan hente flere sider med offset.
// Find CVR-nummer ud fra et navn
$svar = $api->soeg('novo nordisk', 'Aktiv', 5);
foreach ($svar['results'] as $hit) {
echo $hit['cvr'] . ' ' . $hit['name'] . "\n";
}
// Avanceret søgning med paginering
$offset = 0;
do {
$side = $api->avanceretSoegning([
'industry' => '62',
'city' => 'Aarhus',
'status' => 'Aktiv',
'limit' => 100,
'offset' => $offset,
]);
foreach ($side['results'] as $v) {
echo $v['cvr'] . ' ' . $v['name'] . "\n";
}
$offset += 100;
} while ($side['has_more']);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.
function hentMange(FirmaApi $api, array $cvrNumre, int $blok = 100): array
{
$gyldige = array_values(array_filter(array_unique($cvrNumre), 'erGyldigtCvr'));
$fundet = [];
foreach (array_chunk($gyldige, $blok) as $gruppe) {
$svar = $api->batch($gruppe);
foreach ($svar['results'] as $v) {
$fundet[$v['cvr']] = $v;
}
// $svar['errors'] indeholder numre, der ikke blev fundet
}
return $fundet;
}Gem svar, så du sparer opslag
I en webapplikation vil du ikke slå den samme virksomhed op ved hver sidevisning. En simpel filcache er nok til at komme i gang. I et rigtigt system bruger du Redis, APCu eller din frameworks cache.
function virksomhedMedCache(FirmaApi $api, string $cvr, int $ttl = 86400): array
{
$fil = sys_get_temp_dir() . '/cvr_' . $cvr . '.json';
if (is_file($fil) && time() - filemtime($fil) < $ttl) {
return json_decode(file_get_contents($fil), true);
}
$data = $api->virksomhed($cvr);
file_put_contents($fil, json_encode($data), LOCK_EX);
return $data;
}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 nøglen i konfigurationen |
| 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 |
- Skriv aldrig API-nøglen i fejllogs eller i svar til brugeren.
- Kald API'et fra serveren og ikke fra JavaScript i browseren.
- Brug webhooks, hvis du vil have besked om ændringer i stedet for at slå op igen.