Dokumentasjon
One endpoint, one token, JSON or XML. Sourced from Erhvervsstyrelsen and Brønnøysundregistrene directly, never proxied.
This reference is in English. The rest of the site speaks Danish, Norwegian and English.
Getting started
-
1
Create an account
Pick a plan on the pricing page, or start on the free tier. Signing up needs an email address and a password — no card.
-
2
Verify your email address
We send you a link. Your API token is issued when you click it, not when you register: an unverified address never gets a working key.
-
3
Copy your token
It is shown exactly once, on the dashboard, immediately after verification. We store only a SHA-256 hash, so nobody — including us — can recover it. Lost it? Roll a new one.
-
4
Call the API
Send the token as a Bearer credential on every request. There is no anonymous tier.
-
5
Upgrade when you need more
Add your billing country and accept the data processing agreement, then pay by card. Your quota rises as soon as the payment settles.
Authentication
Every request carries your token in the Authorization header.
A request without one is rejected before it touches the register, and before it costs you any quota.
curl 'https://cvrfinder.webkonsulenter.dk/api?vat=12345678&country=dk' \
-H 'Authorization: Bearer cvr_your_token_here'
The endpoint
GET https://cvrfinder.webkonsulenter.dk/api
There is one. It returns a single company. country
is required, and exactly one search parameter must be supplied.
Query parameters
| Parameter | Required | Meaning |
|---|---|---|
| country | yes | dk or no. Which register to search. |
| vat | one of these | Registration number. 8 digits in Denmark, 9 in Norway. |
| name | one of these | Company name. Fuzzy, accent-insensitive. |
| produ | one of these | Production-unit number. |
| phone | one of these | Phone number, 8 digits. |
| ean | one of these | E-invoicing identifier (Peppol / EAN). Resolves to the registered company. |
| address | one of these | Street address. Returns the best-matching company at that address. |
| search | one of these | Any of the above. The type is detected from the value. |
| include | no | Comma-separated: owners, production_units, einvoicing. |
| format | no | json (default) or xml. |
Registration numbers are strings.
A leading zero is significant. 01234567 and
1234567 are different companies. Never store one as an integer.
How search decides
If you pass search instead
of a named parameter, the type is inferred: a number of the register's own length is a registration number,
ten digits in Denmark is a production unit, eight digits is a phone number, and anything else is a name.
When you know what you are looking for, name it — the inference is a convenience, not a contract.
Typed endpoints
Every search type is also its own endpoint, taking the term as
q. Same response,
same country/include/format
parameters, but nothing is ever inferred — /api/name?q=12345678 searches
names, not registration numbers.
GET https://cvrfinder.webkonsulenter.dk/api/vat?q=12345678&country=dk
GET https://cvrfinder.webkonsulenter.dk/api/name?q=b%C3%B8gh&country=dk
GET https://cvrfinder.webkonsulenter.dk/api/produ?q=1234567890&country=dk
GET https://cvrfinder.webkonsulenter.dk/api/phone?q=33112233&country=dk
GET https://cvrfinder.webkonsulenter.dk/api/ean?q=5798009811578&country=dk
GET https://cvrfinder.webkonsulenter.dk/api/address?q=Bredgade+12,+8000+Aarhus&country=dk
Verify an EAN order
GET https://cvrfinder.webkonsulenter.dk/api/verify
The checkout fraud check. An EAN order is paid by invoice after the goods ship, so a buyer who types a real EAN with someone else's CVR, company name or a drop-shipping delivery address gets the goods while the invoice lands on a company that never ordered them. This endpoint checks everything the buyer typed against the company the EAN actually invoices to — Peppol first, the Danish NemHandel register as fallback.
curl 'https://cvrfinder.webkonsulenter.dk/api/verify?ean=5798009811578&country=dk
&cvr=34170339
&name=Fogito
&address=Bredgade+12,+8000+Aarhus+C
&delivery_address=Lagervej+3,+7100+Vejle' \
-H 'Authorization: Bearer YOUR_TOKEN'
ean and
country are required;
cvr, name,
address and delivery_address are
optional — only what you provide is checked. The CVR compares digit-for-digit, names and
addresses by similarity, and the delivery address against the registered address and
every production unit, so shipping to the company's own warehouse still matches.
The response carries per-claim results and one verdict:
| Verdict | Meaning |
|---|---|
| match | Every provided claim belongs to the EAN's company. |
| review | Something is off but not damning — a fuzzy name, an address we could not evaluate, a CVR typo on an otherwise-matching company. A human should look before shipping. |
| mismatch | The claimed CVR belongs to a different company than the EAN invoices. The scam shape — hold the order. |
| not_found | The EAN is registered in neither Peppol nor NemHandel. |
| unknown | A register could not be reached. Nothing was verified either way — never treat this as a pass; hold and retry. |
Includes
Related records are opt-in, comma-separated.
| Value | Adds | Plan |
|---|---|---|
| production_units |
A production_units array: branches and sites.
|
All plans |
| einvoicing |
An einvoicing object: whether the company is registered in the
Peppol Directory to receive e-invoices, and the document types it accepts.
|
All plans |
| owners |
A participants array: directors, board and beneficial owners.
|
Business |
Note the asymmetry: you ask for owners
and receive a key named participants,
because a director is not an owner. Asking for it on a plan that does not carry it returns
UPGRADE_REQUIRED, never a silently empty list.
One caveat on einvoicing:
publishing to the Peppol Directory is optional, so
registered: false means "not found in the Peppol Directory", not
"cannot be invoiced electronically".
We never return a date of birth.
Both registries publish one for every person. We do not ingest it, do not store it, and cannot return it.
The response
{
"registration_number": "810034882",
"country": "no",
"name": "SANDNES ELEKTRISKE AS",
"status": "aktiv",
"legal_form": { "code": "AS", "text": "Aksjeselskap" },
"industry": { "code": "43.210", "text": "Elektrisk installasjonsarbeid" },
"address": {
"street": "Strandgata 21",
"postal_code": "4307",
"city": "SANDNES",
"municipality_code": "1108",
"municipality_name": "SANDNES",
"line": "Strandgata 21, 4307 SANDNES"
},
"email": "[email protected]",
"phone": "51 68 57 00",
"website": null,
"employees": 10,
"started_at": "1995-02-19",
"ended_at": null,
"bankrupt": false,
"ad_protected": true,
"ad_protection_known": false,
"synced_at": "2026-07-10T06:50:18+00:00"
}
Add format=xml for the same
document as XML, rooted at <company>.
Advertising protection
Two fields, always both present, and the second is the one that matters.
ad_protected
— may this company be contacted for marketing?
ad_protection_known
— do we actually know?
Read both, or you will break the law.
Denmark publishes a reklamebeskyttelse flag. Norway does not: its opt-out lives in a separate
reservation register we do not consume. So every Norwegian company comes back as
ad_protected: true, ad_protection_known: false — protected, because we do
not know otherwise. Treating an unknown as permission is the one mistake this field exists to prevent.
Errors
Every failure is a JSON object with error
and message. Switch on
error; the message is
for humans and may change.
{ "error": "NOT_FOUND", "message": "No company matched that query." }
| error | HTTP | When |
|---|---|---|
| MISSING_TOKEN | 401 | No Authorization header. |
| INVALID_TOKEN | 401 | The token is unknown or has been revoked. |
| UPGRADE_REQUIRED | 403 | This plan may not request include=owners. |
| NOT_FOUND | 404 | Nothing in the register matched. |
| INVALID_REQUEST | 422 | A parameter is missing or malformed. |
| INVALID_VAT | 422 | The registration number has the wrong shape for that country. |
| QUOTA_EXCEEDED | 429 | The daily quota is spent. |
| MONTHLY_QUOTA_EXCEEDED | 429 | The monthly quota is spent. |
Quotas
Counted per token. A daily quota resets at midnight; a monthly one on the first.
| Plan | Per day | Per month |
|---|---|---|
| Gratis | 50 | — |
| Hobby | 2.000 | — |
| Business | 6.000 | — |
A dash means no cap of that kind. Every response on a plan with a daily cap carries
X-RateLimit-Limit and
X-RateLimit-Remaining.
When the quota is spent you get 429; back off and retry after the reset.
How fresh is this?
We ingest the registers into our own database rather than proxying them, so we are never slower or
less available than the upstream and can search names the upstream index cannot. Every response carries
synced_at:
the moment that specific company was last read from its register.