How do I make my first BRREG API call step by step?
By Antony Richard Grov, founder of Apier
Five steps, each one command or less. First, pick an organisation number and check its shape: nine digits carrying a MOD-11 control digit, with 999999999 as the placeholder in every example here. Second, call the zero-auth sandbox mirror at /api/v1/sandbox/public/company/999999999/summary; it needs no key and answers a production-shaped envelope. Third, read what came back: a success flag, a data block of normalised registry fields, and a _meta block that marks the answer as simulated with is_sandbox set to true. Fourth, create an API key and call the live route with an Authorization: Bearer header. Fifth, learn the errors every first integration hits, from a failing checksum to a missing header.
Step 1: which organisation number do I start with?
Every Norwegian company lookup keys on the organisasjonsnummer, the 9-digit identifier Brønnøysund assigns at registration. The ninth digit is a MOD-11 control digit computed from the first eight, which means the number carries its own typo detector. Validate the shape before anything touches the wire: exactly nine digits, checksum passing. The documentation placeholder is 999999999, and it is the number this tutorial uses throughout.
Keep the two failure kinds apart, because they need different fixes. A number that fails the checksum was mistyped somewhere between the source and your code, and no API call can repair it; correct the digits instead. A number that passes the checksum but is absent from the register is an existence question, not a shape question, and surfaces later as a not-found response.
If you are starting from a company name rather than a number, resolve it first instead of constructing anything: the organisation number lookup guide covers name search end to end. An invented value that happens to pass MOD-11 can belong to a completely different company, which is a worse outcome than an error.
Step 2: how do I make the first call without any key?
Point curl at the sandbox mirror: GET /api/v1/sandbox/public/company/999999999/summary on https://www.apier.no. The surface is zero-auth by design: no signup, no token, no header to assemble; the command at the bottom of this page is the whole request. What answers is simulated data shaped exactly like the live route's response, which makes it the safest place to learn the envelope.
The envelope has three parts. A success flag tells you whether the call worked. The data block carries the company facts: org_number, entity_type, data_tier, an obligations list, a deadlines calendar and an upgrade_path pointer. The _meta block carries provenance: is_sandbox set to true, a sandbox_noticesaying this is Apier's internal mock, plus data_freshness, source and a rulebook_version of sandbox.
One honesty detail before you experiment: the public mirror serves the fixture company 999999999 only. A malformed path value answers 400 with VALIDATION_FAILED, and a well-formed 9-digit number other than the fixture is refused with an explicit code. Simulated data never masquerades as a real lookup.
Step 3: what did the response actually give me?
Normalised registry fields, not the raw Enhetsregisteret payload. Brønnøysund's own REST responses use registry field names and nesting that you would otherwise map into your model by hand; the summary answers a stable, already-normalised company object instead, with the same names across every upstream register. Your parsing code binds to that one shape, and the mapping work stays on the API side of the boundary.
The deep field story, which registers exist behind the surface, what the open tier does and does not contain, and how freshness and change detection work, is deliberately not repeated here: the Brønnøysund data access guide owns it. For this tutorial it is enough that the fields you just read are the ones you will read in production.
Steps 4 and 5: how do I go live, and which errors come first?
Going live is a header and a path change. Create an API key in the dashboard, keep it in an environment variable such as process.env.APIER_API_KEY, and send it as Authorization: Bearer on GET /api/v1/company/999999999/summary. The envelope keeps its shape; the content now comes from the real register, and _meta states freshness and source instead of the sandbox markers. What the status fields mean for a company you are about to trust is covered by the business status verification guide.
Errors keep the same discipline as successes. Every non-2xx response is the same structured envelope: a machine-readable error_code plus an explanation block with a summary and fix steps, never a bare status code or an HTML page. Your error handling is one code path, and the table below maps the codes a first integration meets, roughly in order.
The one retryable entry is the rate limit: a 429 arrives with a Retry-After header stating how many seconds to wait, so back off exactly that long rather than hammering. Everything else is non-retryable: an identical request fails identically, and the fix is always in your input or your header, never in repetition.
| Error | What it means | What to do |
|---|---|---|
| 400 VALIDATION_FAILED | The org number is not 9 digits, or another field failed shape validation. | Correct the field named in explanation.details; the same request fails identically. |
| Checksum failure | 9 digits, but the MOD-11 control digit does not match: a typo, not a missing company. | Re-check the digits against your source; the profile route rejects it up front as ORG_NUMBER_INVALID_CHECKSUM. |
| 404 NOT_FOUND / COMPANY_NOT_FOUND | A well-formed number that Enhetsregisteret does not hold: deregistered, never registered, or wrong. | Verify the number, or resolve it from the company name; do not retry. |
| 401 AUTH_MISSING | The Authorization: Bearer header is missing or malformed on a live route. | Send a valid API key in the header; retrying without one fails identically. |
| 429 RATE_LIMIT_EXCEEDED | Too many requests in the current window for your key or IP. | Wait the number of seconds in the Retry-After header, then retry; back off, do not hammer. |
Make the first call
The whole path in two commands. The curl line is step two: the zero-auth sandbox summary, simulated and marked as such. The TypeScript snippet is step four: the same summary from the live register, with the key in the header and the same envelope on both sides.
# Step 2 in one command: zero-auth sandbox, simulated but production-shaped.
curl -s https://www.apier.no/api/v1/sandbox/public/company/999999999/summary// Step 4 in one request: the same summary, now live, with one key.
const res = await fetch(
"https://www.apier.no/api/v1/company/999999999/summary",
{ headers: { Authorization: `Bearer ${process.env.APIER_API_KEY}` } },
);
if (!res.ok) {
// Every non-2xx answers the same structured envelope.
const { error_code, explanation } = await res.json();
throw new Error(`${error_code}: ${explanation.summary}`);
}
const { data, _meta } = await res.json();
// is_sandbox is absent here; freshness and version are stated instead.
console.log(data.org_number, data.entity_type, _meta.data_freshness);Frequently asked questions
- Do I need an API key for the first call?
- No. The sandbox mirror under /api/v1/sandbox/public is zero-auth by design, so the very first request is a single curl command with no signup in front of it. It serves the fixture company 999999999 and answers the same envelope shape the live route uses. The key only enters the picture at step four, when you want real register data instead of the simulated fixture, and it travels in an ordinary Authorization: Bearer header.
- Is the sandbox data real?
- No, and the response says so itself. Every sandbox answer carries _meta.is_sandbox set to true plus a sandbox_notice string stating that this is Apier's internal mock, not a government test environment. The shape, however, matches the live route: the same success flag, the same data fields, the same _meta structure. That is the point of the mirror: you write your parsing code once against simulated data and keep it unchanged when you go live.
- Why does my syntactically valid number come back as not found?
- Because passing the checksum only proves the shape, not existence. The MOD-11 control digit catches typos, but a value that happens to validate is not thereby registered in Enhetsregisteret. The live summary route answers 404 with the NOT_FOUND code for such a number, and the company profile route uses COMPANY_NOT_FOUND for the same situation. Neither is retryable: verify the number against the register, or resolve it from the company name, instead of repeating the call.
- What does the _meta block tell me?
- Provenance for the answer you just received. It states the rulebook_version the evaluation ran under, data_freshness for when the underlying data was fetched, last_verified for the rule review date, and the source of the response. On sandbox responses it additionally carries is_sandbox: true and the sandbox_notice string, which is how your code and your logs distinguish a simulated answer from a live one without comparing URLs.
- How do I go from sandbox to live?
- Three small edits. Create an API key in the dashboard, add it as an Authorization: Bearer header, and drop the /sandbox/public segment from the path, so the call becomes GET /api/v1/company/999999999/summary. The envelope keeps its shape, so nothing in your parsing changes; what changes is the content, which now comes from the real register, and the _meta block, which states freshness and source instead of the sandbox markers.