⚡ Node.js🇪🇸 SpainTax Compliance

Spanish Business Compliance — NIF, NIE & VAT Validation

Validate Spanish tax identifiers — NIF (residents), NIE (foreign individuals), and EU VAT numbers — in Node.js. Covers entity type detection, cross-checking VAT against NIF, and handling all Spanish ID document types.

Also available in Python

1. Spanish tax identifiers

IdentifierEndpointWho uses it
NIF/v0/nifSpanish citizens and businesses (DNI or CIF)
NIE/v0/nieForeign individuals resident in Spain
VAT (Spanish)/v0/vatEU VAT = "ES" prefix + NIF/CIF number
ℹ️Spanish VAT numbers are structured as ES + NIF/CIF. The NIF endpoint and VAT endpoint validate the same underlying number — use both when you need both the local and EU representation.

2. Validate NIF

NIF covers both DNI (personal, 8 digits + letter) and CIF (companies, letter + 7 digits + check). Endpoint: GET /v0/nif?value=…

{
  "valid": true,
  "type": "DNI",
  "number": "12345678",
  "letter": "Z"
}
  1. Check valid — mod-23 check letter algorithm
  2. Check typeDNI (individual) or CIF (company)
  3. Store number as canonical form (without letter for DNI)

3. Validate NIE

NIE is for foreign nationals residing in Spain. Format: X/Y/Z + 7 digits + letter. Endpoint: GET /v0/nie?value=…

{
  "valid": true,
  "type": "NIE",
  "number": "X1234567",
  "letter": "L"
}
  1. Check valid — same mod-23 algorithm as DNI, first letter replaced numerically (X=0, Y=1, Z=2)
  2. Log type — always NIE

4. Validate Spanish VAT

For B2B invoicing and EU VAT compliance, validate the full ES… VAT number. Endpoint: GET /v0/vat?value=…

{
  "valid": true,
  "countryCode": "ES",
  "identifier": "B12345674",
  "formatted": "ESB12345674"
}
  1. Check valid and countryCode === "ES"
  2. Use formatted for invoices and VIES submissions
  3. Cross-check identifier against NIF when both are provided

5. Parallel validation with Promise.all

import { createClient } from '@isvalid-dev/sdk'

const iv = createClient({ apiKey: process.env.ISVALID_API_KEY! })

interface SpanishEntityInput {
  entityType: 'individual' | 'foreign_individual' | 'business'
  nif?: string
  nie?: string
  vat?: string
}

async function validateSpanishEntity(input: SpanishEntityInput) {
  const tasks: Promise<unknown>[] = []
  const keys: string[] = []

  if (input.nif) { tasks.push(iv.nif(input.nif)); keys.push('nif') }
  if (input.nie) { tasks.push(iv.nie(input.nie)); keys.push('nie') }
  if (input.vat) { tasks.push(iv.vat(input.vat)); keys.push('vat') }

  const settled = await Promise.allSettled(tasks)
  const results: Record<string, unknown> = {}
  keys.forEach((k, i) => {
    const r = settled[i]
    results[k] = r.status === 'fulfilled' ? r.value : { valid: false, error: r.reason?.message }
  })

  // For businesses: NIF should be present; cross-check with EU VAT if supplied
  if (input.entityType === 'business' && results.nif && results.vat) {
    const nifR = results.nif as { valid: boolean; number?: string }
    const vatR = results.vat as { valid: boolean; identifier?: string }
    if (nifR.valid && vatR.valid) {
      // Spanish VAT = "ES" + NIF, so they should share the same number portion
      if (vatR.identifier && nifR.number && vatR.identifier !== nifR.number) {
        throw new Error(`VAT identifier (${vatR.identifier}) does not match NIF (${nifR.number})`)
      }
    }
  }

  return results
}

// Example
const result = await validateSpanishEntity({
  entityType: 'business',
  nif: 'B12345674',
  vat: 'ESB12345674',
})
console.log(result)

6. Cross-checking NIF and VAT

When a business provides both NIF and EU VAT number, verify they refer to the same entity:

const [nif, vat] = await Promise.all([
  iv.nif('B12345674'),
  iv.vat('ESB12345674'),
])

if (nif.valid && vat.valid) {
  // VAT identifier (without country prefix) should equal NIF number
  const vatNumber = vat.identifier   // "B12345674"
  const nifNumber = `${nif.number}${nif.letter ?? ''}`  // "B12345674"
  if (vatNumber !== nifNumber) {
    throw new Error('NIF and VAT do not refer to the same entity')
  }
}

7. Edge cases

Accepting NIF, NIE, or either

💡If your form accepts any Spanish personal ID, try NIF first; if it starts with X, Y, or Z, route to NIE.
async function validateAnySpanishPersonalId(value: string) {
  const upper = value.toUpperCase()
  if (/^[XYZ]/.test(upper)) {
    return { type: 'NIE', result: await iv.nie(upper) }
  }
  return { type: 'NIF', result: await iv.nif(upper) }
}

CIF vs DNI distinction

ℹ️Companies have a CIF (starts with a letter like A, B, C…). Individuals have a DNI (starts with a digit). The type field in the NIF response distinguishes them automatically.

8. Summary checklist

Validate NIF format and check letter via API
Route X/Y/Z-prefixed IDs to NIE endpoint
Validate full ESxxxx VAT for B2B invoicing
Cross-check VAT identifier against NIF number
Run NIF + VAT in parallel with Promise.all
Use type field to distinguish DNI vs CIF
Use formatted VAT for VIES submissions
Return 422 with field-level error messages

See also

Ready to integrate?

Free tier — 1,000 requests/month. No credit card required.

Get your API key →