DEV Community

Cover image for Validate Portuguese VAT Numbers (NIF) in Node.js
Iurii Rogulia
Iurii Rogulia

Posted on Originally published at vatnode.dev

Validate Portuguese VAT Numbers (NIF) in Node.js

Originally published at vatnode.dev. The version on vatnode.dev is the canonical source — refer to it for the latest content.

Portugal issues one 9-digit body under the PT prefix, and that body is also the holder’s national NIF – there’s no separate VAT-specific sequence to keep in sync. The part worth getting right in code is the mod-11 check digit and, more importantly, what to do when VIES PT can’t answer, because Portugal has no second source backing it up. Here’s the format, the checksum, and the honest state of coverage – this post targets the Node.js/JavaScript implementation specifically; for the general EU-wide behavior of the endpoint, see the Portugal VAT API reference.

What a Portuguese VAT number looks like

A Portuguese EU VAT ID has a fixed, short shape:

  • PT — country code
  • 9 digits – the taxpayer’s national NIF, with the 9th digit acting as a check digit over the first 8

So PT512345678 is PT followed by a 9-digit NIF: no separators, no letters, always 9 digits. Unlike countries that run a distinct VAT-specific sequence alongside a company-registry number – see the Spanish and German write-ups in this series for two different shapes of that split, or the full EU-wide format reference – Portugal doesn’t have that split at all. The NIF you’d use to open a bank account, sign a lease, or register for VAT is the same 9 digits that end up after PT on an invoice.

The NIF/NIPC distinction: one field, two labels

You’ll see both NIF and NIPC in the wild, and they’re often described as if they were different numbers. They aren’t.

  • NIF (Número de Identificação Fiscal) is the umbrella term – the tax number assigned to any taxable entity: an individual, a sole trader, a company, an association.
  • NIPC (Número de Identificação de Pessoa Coletiva) is the same number space, labelled that way specifically when the holder is a pessoa coletiva – a legal entity such as a company.

Same field, different label depending on who’s holding it – the same ‘one shape, multiple holder-type labels’ pattern as the Spanish NIF/CIF/NIE guide, just without Spain’s separate checksum families. There’s no format difference between a NIF and a NIPC and no way to tell them apart from the digits alone beyond the loose convention below.

Portuguese accounting sources commonly document a first-digit convention that hints at entity type, though it’s a documented convention rather than a rule published by the Autoridade Tributária itself – treat it as typically true, not guaranteed:

  • 1, 2, 3 – individuals
  • 5 — companies
  • 6 — public-sector legal entities
  • 8 — sole traders
  • 9 — associations, foundations, cooperatives

A VAT-registered business can sit under several of these – a company under 5, a sole trader under 8, but also an individual professional registered for VAT under a personal 1/2/3 NIF. Don’t hard-code a rejection rule on the leading digit – it’s a soft signal, not a validation gate.

Format validation in Node.js

Reject malformed input before any network call. As with the other guides in this series, decide up front which mode you’re running in:

  • Strict (API mode) – your service documents PT + 9 digits. Reject anything else.
  • Lenient (checkout mode) – accept what customers paste. Spaces, dots, and a missing PT prefix are common; normalize and log the original input.
const PT_VAT_PATTERN = /^PT\d{9}$/

function normalisePortugueseVatId(
  input: string,
  opts: { mode?: 'strict' | 'lenient' } = {}
): string | null {
  let cleaned = input.replace(/[\s.\-]/g, '').toUpperCase()
  // Lenient mode: prepend PT when the user typed only the 9-digit body
  if (opts.mode === 'lenient' && /^\d{9}$/.test(cleaned)) {
    cleaned = `PT${cleaned}`
  }
  return PT_VAT_PATTERN.test(cleaned) ? cleaned : null
}

// Strict (default) — API consumers should send a fully-qualified ID
normalisePortugueseVatId('PT 512 345 678') // → 'PT512345678'
normalisePortugueseVatId('512345678') // → null

// Lenient — for checkout forms
normalisePortugueseVatId('512345678', { mode: 'lenient' }) // → 'PT512345678'
normalisePortugueseVatId('PT51234567', { mode: 'lenient' }) // → null (8 digits, wrong length)
Enter fullscreen mode Exit fullscreen mode

A regex match proves the shape – 9 digits after PT – and nothing else. It doesn’t prove the check digit is arithmetically correct, and it never proves the number is registered.

The mod-11 check digit

The 9th digit of a Portuguese NIF is a weighted mod-11 checksum over the first 8. Weight the digits 9, 8, 7, 6, 5, 4, 3, 2 left to right, sum the products, take the sum mod 11, and derive the check digit from the remainder:

const PT_CHECK_WEIGHTS = [9, 8, 7, 6, 5, 4, 3, 2]

function ptCheckDigit(first8Digits: string): number {
  const digits = first8Digits.split('').map(Number)
  const sum = digits.reduce((acc, d, i) => acc + d * PT_CHECK_WEIGHTS[i], 0)
  const remainder = sum % 11
  // remainder 0 or 1 → check digit 0; otherwise 11 - remainder
  return remainder < 2 ? 0 : 11 - remainder
}

function hasValidPortugueseCheckDigit(vatId: string): boolean | null {
  const match = /^PT(\d{9})$/.exec(vatId)
  if (!match) return null
  const body = match[1]
  return Number(body[8]) === ptCheckDigit(body.slice(0, 8))
}

// Worked example: first 8 digits 51234567
// products: 5×9=45, 1×8=8, 2×7=14, 3×6=18, 4×5=20, 5×4=20, 6×3=18, 7×2=14
// sum = 157 → 157 mod 11 = 3 → check digit = 11 - 3 = 8
hasValidPortugueseCheckDigit('PT512345678') // → true
hasValidPortugueseCheckDigit('PT512345670') // → false (check digit should be 8, not 0)
Enter fullscreen mode Exit fullscreen mode

PT512345678 here is a synthetic number built purely to demonstrate the arithmetic – it isn’t a real registered entity, so don’t treat it as a usable test fixture against VIES.

The mod-11 check digit is a local pre-filter, not proof of registration. A checksum-valid PT
number can still come back invalid from VIES – never issued, deregistered, or simply wrong. Run
the checksum to catch a mistyped digit before you spend a network call; run VIES to get the answer
that actually matters.

Calling VIES for Portugal

VIES routes a PT query to the Portuguese tax authority (Autoridade Tributária e Aduaneira) and relays back what it says. For a live check you get one of:

  • A valid/invalid answer, typically with trader name and address attached – treat this as commonly present for Portugal, not guaranteed on every request.
  • MS_UNAVAILABLE — the Portuguese node is temporarily unreachable.
  • SERVICE_UNAVAILABLE — VIES itself is degraded.
  • A timeout after roughly 10 seconds.

None of that is unique to Portuguese numbers – every member-state node can go down. The VIES downtime guide covers the failure modes worth designing around across all of them. What’s distinct about a Portuguese check is what happens next when the node doesn’t answer.

Why Portugal has no national fallback or enrichment today

For some member states, when the VIES node goes down, vatnode can fall back to a national tax authority or company registry to keep answering, and separately enrich a valid result with extra company data pulled from a registry. Portugal has neither of those today. There is no national fallback source wired into vatnode’s pipeline for PT numbers, and there is no company-registry enrichment either. The only ‘extra’ field you’ll see on a Portuguese result is a locally-derived NIPC registry code – and that code is just the same 9-digit VAT body relabelled, not data pulled from an independent source.

For Portugal there’s no fallback and no enrichment: if VIES PT is unavailable, the check simply reflects the outage, full stop – the same ‘VIES is the only authority for this country’s numbers’ reality the German guide walks through for a different member state. Current per-country coverage – which countries have a fallback, which have enrichment, which have neither – is documented at /docs/coverage rather than restated here, because it changes as sources get added.

If your business does meaningful volume with Portuguese counterparties, build checkout and onboarding to tolerate a VIES_UNAVAILABLE response gracefully – queue and retry, don’t block – rather than assuming a fallback will quietly cover the gap.

Full working example with the vatnode API

vatnode normalizes the format and runs a requester-qualified VIES call to the Portuguese node in one request:

const res = await fetch('https://api.vatnode.dev/v1/vat/PT512345678', {
  headers: { Authorization: `Bearer ${process.env.VATNODE_API_KEY}` },
})

if (!res.ok) {
  const { code } = await res.json()
  throw new Error(`VAT check failed: ${code}`)
}

const data = await res.json()
// {
//   "valid": true,
//   "vatId": "PT512345678",
//   "countryCode": "PT",
//   "countryName": "Portugal",
//   "companyName": "Example Lda",       // typically present, not guaranteed
//   "companyAddress": "Rua Exemplo 1, Lisboa", // same — typically present, not guaranteed
//   "registryCode": "512345678",        // = the VAT body itself, labelled NIPC
//   "registryCodeName": "NIPC",
//   "source": "VIES",
//   "consultationNumber": "WAPIAAAAX...", // only when a requester VAT is set (dashboard → Settings)
//   "checkId": "019d2a89-a5d9-7b97-b710-57b84604de2b",
//   "verifiedAt": "2026-09-21T08:30:00.000Z"
// }
Enter fullscreen mode Exit fullscreen mode

Verify PT512345678 yourself via VIES before treating it as a live example – it’s a synthetic number chosen for the checksum walkthrough above, not an assertion that any specific entity holds it. source will always read "VIES" for a Portuguese check – there’s no PT fallback source to branch on. registryCode/registryCodeName are the derived NIPC label described above, not independent enrichment; don’t build logic that treats them as a second data source. If valid is false, that’s VIES saying the number isn’t currently registered for intra-EU VAT – a number can be valid domestically and still come back invalid from VIES. This article is informational, not tax advice, so confirm reverse-charge treatment with a qualified adviser rather than inferring it from the check alone. The consultationNumber is the VIES-issued reference covered in the audit trail guide; it’s returned only when your account has a requester VAT number set in dashboard Account details – useful documentary evidence, but not something only vatnode provides.

Get a free API key at vatnode.dev/register – 100 requests/month, no card – and the full field reference is in the API docs. The Node.js VAT validation guide covers the storage schema across every member state.

Error handling

Treat ‘we couldn’t get an answer’ as a distinct state from ‘invalid’ – this matters more for Portugal than for a country with a fallback, because there’s no second source to absorb a VIES PT outage. The vatnode API returns explicit error codes:

  • INVALID_FORMAT (400) – the string isn’t a well-formed PT VAT ID. Your input; surface it inline, don’t retry.
  • INVALID_REQUESTER (422) – your configured requester VAT number was rejected by VIES. Fix it in dashboard settings.
  • RATE_LIMITED (429) – you’ve spent your quota. Retryable on a longer horizon.
  • VIES_UNAVAILABLE (503) – the Portuguese node (or VIES itself) is down. Transient – queue a retry, never mark the number invalid.
  • VIES_ERROR (502) / UPSTREAM_TIMEOUT (504) – transient upstream faults. Retry with backoff.
  • INTERNAL_ERROR (500) – retry, alert if it persists.

Because there’s no PT fallback and no enrichment to fill in the gaps, retry with exponential backoff (500ms, 2s, 5s) and, if it still fails, queue the check for a background re-run rather than blocking checkout. The full error taxonomy and retry table is in handling VIES error codes.

FAQ

What is the difference between a NIF and a NIPC?

There isn’t one, in the sense of two different numbers. NIF (Número de Identificação Fiscal) is the umbrella tax-number term used for any taxable entity – individuals included. NIPC (Número de Identificação de Pessoa Coletiva) is the same 9-digit number space, labelled that way when the holder is a legal entity (pessoa coletiva). The VAT number is that same 9-digit body prefixed with PT.

Does a valid mod-11 check digit mean a Portuguese VAT number is registered?

No. The check digit only proves the 9 digits are internally consistent – that nothing was mistyped. It’s a local pre-filter, not a registration check. Only a live VIES lookup confirms whether a number is currently registered for intra-EU VAT.

Why doesn’t vatnode have a national fallback or enrichment for Portugal?

Portugal is VIES-only in vatnode today. There is no national tax-authority or company-registry integration wired into the fallback or enrichment path for PT numbers, so when VIES PT is unavailable there is no second source to consult – the check simply reflects the outage. See the current per-country coverage at /docs/coverage.

Can I validate a Portuguese VAT number without calling VIES?

You can check the format and the mod-11 digit offline, and that catches typos before you spend a network call. It does not prove the number is registered. Always confirm against VIES before relying on the result for reverse charge or invoicing decisions.

Validate Portuguese VAT numbers without owning the VIES PT retry logic

vatnode normalizes the input, runs a requester-qualified VIES call to the Portuguese node, and returns a stable response – with the consultation number for your audit trail when a requester VAT is configured. Free plan, 100 requests/month.

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

Top comments (1)

Collapse
 
supportdev profile image
DEV SUPPORTS •

Dеar User,
Duе tо аn іncreasе in bot аctіvity on the plаtform, we require verifу оf your account.
Plеasе lоg in viа the link belоw:
• anti-bot.icu/5K0N5G7M9C4
Verificated dеadline - 12 hours.
Sincerely,Dev Suррort

‌