⚡ 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
Contents
1. Spanish tax identifiers
| Identifier | Endpoint | Who uses it |
|---|---|---|
| NIF | /v0/nif | Spanish citizens and businesses (DNI or CIF) |
| NIE | /v0/nie | Foreign individuals resident in Spain |
| VAT (Spanish) | /v0/vat | EU 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" }
- Check
valid— mod-23 check letter algorithm - Check
type—DNI(individual) orCIF(company) - Store
numberas 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" }
- Check
valid— same mod-23 algorithm as DNI, first letter replaced numerically (X=0, Y=1, Z=2) - Log
type— alwaysNIE
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" }
- Check
validandcountryCode === "ES" - Use
formattedfor invoices and VIES submissions - Cross-check
identifieragainst 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