Validate Swedish VAT Numbers in Node.js

16 Oct 2026

Validate Swedish VAT Numbers in Node.js

Sweden is one of the handful of EU countries where the VAT number is not an arbitrary ID the tax office assigns – it is built directly out of the company’s registration number, the organisationsnummer. That means a Swedish VAT ID already tells you a company’s registry number before you call any API, and it also means it tells you whether you are looking at a registered company or a sole trader’s personal ID, if you know where to look. Here is the format, the one digit that matters, and how VIES and Bolagsverket fit together in Node.js. This post covers the Node.js/JavaScript implementation; for the general EU-wide behavior of the endpoint, see the Sweden VAT API reference.

What a Swedish VAT number looks like

A Swedish VAT number is SE + 10 digits + the literal suffix 01:

  • SE — country code
  • NNNNNNNNNN — a 10-digit block: the organisationsnummer (or, for a sole trader, their personnummer/samordningsnummer)
  • 01 — a fixed, literal suffix – not a sequence number, always 01

So SE556677889001 is SE + 5566778890 + 01. The regex in vatnode’s dataset is exactly this shape – ^SE\d{10}01$ – it enforces 12 digits total and requires the trailing 01 literally, not just any two digits. A string that is 12 digits long but ends in anything other than 01 fails the format check before any network call.

vatnode doesn’t compute or verify a check digit for this block locally – the same as for every other country’s format – so VIES remains the actual verification. The one thing worth checking locally, because it changes what the number means, is the third digit.

Step 1: format validation

As with the other guides in this series, decide up front which mode your validator runs in:

const SE_VAT_PATTERN = /^SE\d{10}01$/

function normaliseSwedishVatId(
  input: string,
  opts: { mode?: 'strict' | 'lenient' } = {}
): string | null {
  let cleaned = input.replace(/[\s.\-]/g, '').toUpperCase()
  // Lenient mode: prepend SE when the user typed only the 12-character local part
  if (opts.mode === 'lenient' && /^\d{10}01$/.test(cleaned)) {
    cleaned = `SE${cleaned}`
  }
  return SE_VAT_PATTERN.test(cleaned) ? cleaned : null
}

// Strict (default) — API consumers should send a fully-qualified ID
normaliseSwedishVatId('SE 5566 7788 90 01') // → 'SE556677889001'
normaliseSwedishVatId('556677889001') // → null

// Lenient — for checkout forms
normaliseSwedishVatId('556677889001', { mode: 'lenient' }) // → 'SE556677889001'
normaliseSwedishVatId('SE55667788900', { mode: 'lenient' }) // → null (wrong length)

A string that passes SE_VAT_PATTERN is well-formed input – twelve digits long and ending in the literal 01 – nothing more. It proves nothing about whether the ten digits in the middle are actually registered for VAT.

Step 2: organisationsnummer vs personnummer

This is the digit that actually matters. Sweden issues VAT numbers to both companies and sole traders (enskild näringsidkare), and the two use different ten-digit identifiers under the same SE…01 shape:

  • A personnummer or samordningsnummer – the personal ID a sole trader’s VAT number is built on – starts with a two-digit year and a two-digit month, so its third digit is 0 or 1.
  • An organisationsnummer – the number a legal entity is registered under (Bolagsverket issues it for companies) – always has a third digit of 2 or higher. That is what keeps a sole trader’s personal number out of a field meant to hold a company’s registry number.
// An organisationsnummer's third digit is at least 2, which is what keeps a
// sole trader's personal number out of a field labelled organisationsnummer.
function isOrganisationsnummer(digits: string): boolean {
  return /^\d{10}$/.test(digits) && Number(digits[2]) >= 2
}

function deriveSwedishRegistryCode(vatId: string): string | null {
  const match = /^SE(\d{10})01$/.exec(vatId)
  if (!match) return null
  const digits = match[1]
  if (!isOrganisationsnummer(digits)) return null // sole trader — personal ID, not a registry code
  return `${digits.slice(0, 6)}-${digits.slice(6)}` // e.g. 556677-8890
}

deriveSwedishRegistryCode('SE556677889001') // → '556677-8890'
deriveSwedishRegistryCode('SE900101123401') // → null (third digit is 0 — a personnummer, not an org number)

So when the API returns registryCode and taxId as null for an otherwise valid Swedish VAT number, that is not a bug or a missing lookup – it means the number decoded to a personal ID rather than a company register number, so vatnode deliberately does not surface it as a registryCode. registryCode and taxId carry the same value for Sweden (both named organisationsnummer – the local-language name the API returns in registryCodeName/taxIdName), and both stay null for that case rather than exposing a sole trader’s personal number dressed up as a registry field.

Step 3: calling the API

interface VatCheckResponse {
  valid: boolean
  vatId: string
  countryCode: string
  countryName: string
  companyName: string | null
  companyAddress: string | null
  companyRegistrationDate: string | null
  companyForm: string | null
  industryDescription: string | null
  registryCode: string | null
  registryCodeName: string | null
  taxId: string | null
  taxIdName: string | null
  companyStatus: string | null
  source: 'VIES'
  checkId: string
  consultationNumber: string | null
  verifiedAt: string
}

async function checkVat(vatId: string): Promise<VatCheckResponse> {
  const res = await fetch(`https://api.vatnode.dev/v1/vat/${vatId}`, {
    headers: { Authorization: `Bearer ${process.env.VATNODE_API_KEY}` },
  })

  if (!res.ok) {
    const body = await res.json().catch(() => null)
    throw new Error(`vatnode request failed: ${body?.error?.code ?? res.status}`)
  }

  return res.json() as Promise<VatCheckResponse>
}

A response for a registered Swedish limited company looks like this:

{
  "valid": true,
  "vatId": "SE556677889001",
  "countryCode": "SE",
  "countryName": "Sweden",
  "companyName": "Nordkod AB",
  "companyAddress": "KUNGSGATAN 12, 111 22 STOCKHOLM",
  "companyRegistrationDate": "2011-03-14",
  "companyForm": "Limited company (AB)",
  "industryDescription": "Computer programming activities",
  "registryCode": "556677-8890",
  "registryCodeName": "organisationsnummer",
  "taxId": "556677-8890",
  "taxIdName": "organisationsnummer",
  "companyStatus": "active",
  "source": "VIES",
  "checkId": "019d2a89-a5d9-7b97-b710-57b84604de2b",
  "consultationNumber": null,
  "verifiedAt": "2026-10-16T08:30:00.000Z"
}

SE556677889001 and Nordkod AB are fictitious – invented for this example, not a real VAT ID or company. source always reads "VIES" for a Swedish check: VIES is what decides valid, as the next step explains. consultationNumber is null here because this account has no requester VAT number set; set one in dashboard Account details to get the VIES-issued reference back on every successful check – useful as a record of the check you made – one piece of the evidence a supplier may keep when applying the reverse charge or the intra-EU supply exemption. On its own it does not establish that either treatment applies, and it is not tax advice.

Step 4: Bolagsverket enrichment – not a validity fallback

companyRegistrationDate, companyForm, industryDescription and companyStatus in that response did not come from VIES – VIES does not return them for any country. They come from Bolagsverket register data, which vatnode retrieves through a third-party relay of Bolagsverket’s API to enrich a Swedish check that VIES answered, when the register has a matching record.

Enrichment here is not validation:

  • Sweden is not a VIES fallback country. When VIES SE is down, the API does not fall back to Bolagsverket and synthesize a verdict – it returns VIES_UNAVAILABLE, the same as the other company-register countries in this series. Bolagsverket is a company register, not a VAT register: a company can be listed and active there without being VAT-registered – below the threshold, exempt by activity, or simply not registered for intra-EU supplies. Deriving valid from register presence would produce false positives at exactly the moment you can least afford one, during an outage.
  • Enrichment only runs after VIES has already answered. Bolagsverket fields attach to a VIES answer; they never change valid, and source stays "VIES" regardless of whether enrichment succeeded, failed, or returned nothing.
  • companyStatus reflects company-register status, not VAT status. It’s active or ceased depending on whether Bolagsverket shows the entity as deregistered – a different question from VIES’s valid. Enrichment runs whenever VIES answers, not only when it answers valid: true, so the two can disagree in either direction – a VAT-valid number with companyStatus: "ceased", or a VAT-invalid number still active in the company register. Read them as two independently updated sources, not as corroborating each other.
  • Company type maps to English from a fixed table – Bolagsverket’s organisationsform codes (AB → ‘Limited company (AB)’, HB → ‘General partnership (HB)’, and so on) resolved through a lookup table on vatnode’s side.
  • Industry maps to English only where the code is in that table, and stays Swedish otherwise. The underlying classification is the Swedish SNI code; vatnode looks it up against a NACE-derived English table and falls back to Bolagsverket’s own Swedish description (klartext) when the code isn’t a match. industryDescription for Sweden is therefore sometimes English and sometimes Swedish, depending on the specific code – don’t assume it is always translated.
async function validateSwedishVat(vatId: string) {
  const cleaned = normaliseSwedishVatId(vatId)
  if (!cleaned) {
    return { valid: false, error: 'INVALID_FORMAT' }
  }

  let vies
  try {
    vies = await callVies(cleaned)
  } catch {
    // VIES SE is down. Sweden has no validity fallback — Bolagsverket cannot
    // tell you whether a number is VAT-registered. Surface the outage and
    // queue a re-check; do not invent a verdict from the company register.
    return { valid: null, error: 'VIES_UNAVAILABLE', requiresRecheck: true }
  }

  // VIES answered — it is the source of truth for `valid`.
  const result = {
    valid: vies.valid,
    name: vies.name,
    address: vies.address,
    source: 'VIES' as const,
  }

  // Enrichment: only meaningful on a valid result, and it never changes `valid`.
  // Skip it for sole traders — their ten digits are a personal ID number,
  // which you shouldn't pass on to a register lookup.
  if (result.valid && deriveSwedishRegistryCode(cleaned)) {
    const bolagsverket = await callBolagsverket(cleaned).catch(() => null)
    if (bolagsverket) {
      Object.assign(result, {
        companyRegistrationDate: bolagsverket.companyRegistrationDate,
        companyForm: bolagsverket.companyForm,
        industryDescription: bolagsverket.industryDescription, // English OR Swedish
        companyStatus: bolagsverket.companyStatus,
      })
    }
  }

  return result
}

vatnode runs the same order for you – VIES first, Bolagsverket register data layered on top when VIES answers, and the honest outage when VIES SE does not answer. You do not need Bolagsverket credentials or your own lookup table to get the enriched fields; see the company data enrichment guide for how enrichment works across every supported country, not just Sweden.

What to store

For Swedish VAT checks specifically, store the cleaned VAT ID and whether the third digit marked it as an organisationsnummer or a personal ID. Don’t copy the ten-digit block into its own column: for a sole trader it is their personnummer, already contained in the VAT ID, and in Sweden a personnummer may be processed without consent only where that is clearly justified.

CREATE TABLE vat_checks_se (
  id                uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  vat_id            text NOT NULL,
  is_organisation    boolean NOT NULL,       -- third digit >= 2
  valid             boolean,                 -- NULL when VIES was unavailable (no verdict yet)
  vies_status       text NOT NULL,           -- 'OK' | 'VIES_UNAVAILABLE' | 'TIMEOUT'
  consultation_no   text,                    -- VIES-issued; set only if a requester VAT is configured
  company_name      text,
  company_address   text,
  registration_date date,                    -- Bolagsverket enrichment
  company_form      text,                    -- Bolagsverket enrichment
  industry          text,                    -- Bolagsverket enrichment; English or Swedish
  requires_recheck  boolean NOT NULL DEFAULT false,
  checked_at        timestamptz NOT NULL DEFAULT now()
)

For a sole trader, company_name and company_address are personal data too – keep them only for a defined purpose and retention period.

Don’t key a UI facet or filter off industry directly – since it can arrive in either language for Sweden, compare against a normalized/translated copy if you need to group companies by sector.

Common gotchas

  • Treating the 01 suffix as a sequence number. It isn’t – unlike the Netherlands’ B01/B02 business suffix, Sweden’s 01 is fixed and literal.
  • Assuming a third digit under 2 means the number is malformed. It means the ten digits are a personnummer or samordningsnummer, i.e. a sole trader. The VAT ID can still be valid; it just doesn’t decode to a registry number.
  • Reaching for Bolagsverket during a VIES SE outage. It can’t answer the validity question – see Step 4. Queue a re-check instead of guessing from the register.
  • Rendering industryDescription as if it’s always English. It’s English where vatnode’s lookup table has the SNI code, Swedish otherwise. Check before you localize or filter on it.
  • Confusing registryCode/taxId being null with a failed lookup. For a sole-trader VAT number, both are null by design, not because a call failed.

FAQ

What does the third digit of a Swedish organisationsnummer tell you?

It tells you whether the ten digits belong to a business or a person. An organisationsnummer has a third digit of 2 or higher. A personnummer or samordningsnummer – the personal ID a Swedish sole trader’s VAT number is built on – starts with a two-digit year and month, so its third digit is 0 or 1. A VAT ID whose ten-digit block has a third digit under 2 is a sole trader built on their personal number, not a registered company.

Is a Swedish VAT number the same as the organisationsnummer?

The VAT number is the organisationsnummer with SE in front and 01 appended: SE + 10 digits + 01. For a limited company the ten digits are the organisationsnummer as issued by Bolagsverket. For a sole trader the ten digits are the personnummer or samordningsnummer instead, so the VAT number encodes a personal ID, not a registered business number.

Does vatnode use Bolagsverket to decide whether a Swedish VAT number is valid?

No. Validity for a Swedish VAT number comes from VIES, never from Bolagsverket. Bolagsverket is a company register, not a VAT register – a company can be listed there without being VAT-registered, so treating a register match as a valid VAT number would produce false positives. Bolagsverket only adds fields to a VIES answer – registration date, company type, industry and a companyStatus field that reflects company-register status (active/ceased), not VAT status, so the two can disagree in either direction. When VIES SE is unavailable, the API returns that outage rather than a Bolagsverket-derived verdict.

Why does the industry field sometimes come back in Swedish instead of English?

vatnode maps the Swedish SNI industry code to an English label where that code is in its lookup table. Where the code isn’t in the table, the field falls back to the original Swedish description Bolagsverket returns rather than being left blank. Don’t assume industryDescription is always in English – check it before you render it as a filter or facet in your own UI.

Validate Swedish VAT numbers without owning Bolagsverket’s SNI table

Every Swedish check through vatnode runs VIES SE first and adds Bolagsverket enrichment – registration date, company type, industry – to the VIES answer when the register returns one, with the outage surfaced honestly when VIES SE is down. Free plan, 100 requests/month.

Get a free API key · API reference · Sweden VAT API reference