Guide · Python · SDK · Compliance

Brazilian Tax Compliance

Three validators, one onboarding flow. Here's how to validate a Brazilian company's CNPJ, the owner's CPF, and optionally an EU VAT number for cross-border transactions — and why verifying both the business and the individual behind it is essential for KYC compliance.

1. Why Brazilian tax compliance requires multiple identifiers

Brazil's tax system is built on two foundational identifiers: the CNPJ (Cadastro Nacional da Pessoa Juridica) for businesses and the CPF (Cadastro de Pessoas Fisicas) for individuals. Every business transaction in Brazil requires a valid CNPJ, and every individual — including business owners and directors — is identified by their CPF.

For KYC onboarding of a Brazilian company, you need to verify both: the business itself (CNPJ) and the person behind it (CPF). The CNPJ validation includes a Receita Federal lookup that returns the company's legal name, status, address, and incorporation date. The CPF validation confirms the individual's tax ID is structurally valid.

For cross-border transactions — especially when a Brazilian company trades with EU entities — you may also need to validate an EU VAT number for the European counterparty. This enables proper VAT handling on international invoices.

ℹ️Brazilian tax identifiers use a dual check-digit algorithm. Both CNPJ (14 digits) and CPF (11 digits) have two verification digits computed via weighted modulo-11 — making them reliable to validate structurally before any registry lookup.

2. The identifiers you need

Each identifier covers a different layer of Brazilian tax compliance:

IdentifierIssuerWhat it provesAPI endpoint
CNPJReceita FederalThe business is registered with the Brazilian Federal RevenueGET /v0/br/cnpj
CPFReceita FederalThe individual has a valid Brazilian tax IDGET /v0/br/cpf
VATEU VIESThe EU counterparty has an active VAT registration (cross-border)GET /v0/vat
⚠️VAT validation is only needed for cross-border transactions with EU entities. For domestic Brazilian transactions, CNPJ + CPF is sufficient.

3. Step 1: Validate the CNPJ (business)

The CNPJ endpoint performs a multi-layer validation:

  • Dual check-digit validation — two sequential modulo-11 calculations verify the last two digits
  • Format validation — 14 digits with optional XX.XXX.XXX/XXXX-XX formatting
  • Receita Federal lookup — queries the federal registry for company name, status, legal nature, and address
  • Branch detection — identifies whether the CNPJ is a headquarters (0001) or branch (0002+)

Example response

{
  "valid": true,
  "cnpj": "11222333000181",
  "formatted": "11.222.333/0001-81",
  "found": true,
  "entity": {
    "name": "EMPRESA EXEMPLO LTDA",
    "tradeName": "EXEMPLO",
    "status": "ATIVA",
    "legalNature": "2062 - SOCIEDADE EMPRESARIA LIMITADA",
    "openingDate": "2015-06-20",
    "city": "SAO PAULO",
    "state": "SP"
  }
}

What to check in your code

1

valid === true — the CNPJ passes format and dual check-digit validation

2

found === true — the company exists in the Receita Federal registry

3

entity.status === "ATIVA" — the company is currently active, not suspended or closed


4. Step 2: Validate the CPF (individual)

The CPF endpoint validates the Brazilian individual tax ID:

  • Dual check-digit validation — two sequential modulo-11 calculations, similar to CNPJ
  • Format validation — 11 digits with optional XXX.XXX.XXX-XX formatting
  • Known-invalid detection — rejects CPFs with all identical digits (e.g., 111.111.111-11) which pass the checksum but are invalid
  • State of origin — the 9th digit identifies the issuing state (fiscal region)

Example response

{
  "valid": true,
  "cpf": "12345678909",
  "formatted": "123.456.789-09",
  "fiscalRegion": 9,
  "state": "PR"
}

What to check in your code

1

valid === true — the CPF passes format, check-digit, and known-invalid detection

2

state — cross-reference with the company's CNPJ state for geographic consistency

3

formatted — use the formatted version for display and storage consistency

⚠️CPFs with all identical digits (e.g., 000.000.000-00, 111.111.111-11) pass the modulo-11 checksum but are known to be invalid. The API catches these automatically.

5. Step 3: Validate the VAT number (cross-border)

When a Brazilian company trades with an EU entity, validating the EU counterparty's VAT number ensures proper tax handling on cross-border invoices:

  • Country-specific checksum — validates the EU VAT number format and check digits
  • VIES lookup — confirms the VAT registration is active in the EU system
  • Company details — returns the registered name and address for invoice verification

Example response

{
  "valid": true,
  "countryCode": "DE",
  "vatNumber": "123456789",
  "normalized": "DE123456789",
  "vies": {
    "checked": true,
    "valid": true,
    "name": "EUROPEAN PARTNER GMBH",
    "address": "MUSTERSTRASSE 1, 10115 BERLIN"
  }
}

What to check in your code

1

vies.valid === true — the EU counterparty's VAT registration is active

2

vies.name — matches the EU counterparty's name on the invoice

3

vies.address — confirms the registered business address for invoice accuracy


6. Putting it all together — KYC onboarding flow

The following Python example validates a Brazilian business (CNPJ + lookup) and its owner (CPF) in parallel. If the transaction involves an EU counterparty, it also validates the EU VAT number. Install with pip install isvalid-sdk or pip install requests.

import asyncio
import os
from isvalid_sdk import IsValidConfig, create_client

iv = create_client(IsValidConfig(api_key=os.environ["ISVALID_API_KEY"]))


async def verify_brazilian_business(cnpj: str, cpf: str, eu_vat: str | None = None) -> dict:
    calls = [
        iv.br.cnpj(cnpj),
        iv.br.cpf(cpf),
    ]

    # Optionally validate EU counterparty VAT for cross-border
    if eu_vat:
        calls.append(iv.vat(eu_vat, check_vies=True))

    results = await asyncio.gather(*calls)
    cnpj_result, cpf_result = results[0], results[1]
    vat_result = results[2] if eu_vat else None

    # ── Cross-reference: CPF state vs CNPJ state ──────────────────────────
    state_match = cpf_result.get("state") == (cnpj_result.get("entity") or {}).get("state")

    return {
        "cnpj": {
            "valid": cnpj_result["valid"],
            "found": cnpj_result.get("found"),
            "status": (cnpj_result.get("entity") or {}).get("status"),
            "name": (cnpj_result.get("entity") or {}).get("name"),
            "legal_nature": (cnpj_result.get("entity") or {}).get("legalNature"),
            "state": (cnpj_result.get("entity") or {}).get("state"),
        },
        "cpf": {
            "valid": cpf_result["valid"],
            "state": cpf_result.get("state"),
            "formatted": cpf_result.get("formatted"),
        },
        "vat": {
            "valid": vat_result["valid"],
            "confirmed": (vat_result.get("vies") or {}).get("valid"),
            "name": (vat_result.get("vies") or {}).get("name"),
        } if vat_result else None,
        "cross_checks": {
            "state_match": state_match,
        },
    }


# ── Example: KYC onboarding with EU cross-border ───────────────────────────
result = asyncio.run(verify_brazilian_business(
    cnpj="11222333000181",
    cpf="12345678909",
    eu_vat="DE123456789",
))

print("CNPJ:", f"✓ {result['cnpj']['name']} ({result['cnpj']['status']})" if result["cnpj"]["found"] else "✗ not found")
print("CPF :", f"✓ {result['cpf']['formatted']}" if result["cpf"]["valid"] else "✗ invalid")
if result["vat"]:
    print("VAT :", f"✓ {result['vat']['name']}" if result["vat"]["confirmed"] else "✗ not confirmed")
print("State match:", "✓" if result["cross_checks"]["state_match"] else "⚠ different states")

approved = (
    result["cnpj"]["valid"]
    and result["cnpj"]["found"]
    and result["cnpj"]["status"] == "ATIVA"
    and result["cpf"]["valid"]
    and (not result["vat"] or result["vat"]["confirmed"])
)
print("KYC:", "APPROVED" if approved else "REJECTED")
All API calls run in parallel via asyncio.gather (SDK) or ThreadPoolExecutor (requests). The total latency is the slowest single call. For domestic-only flows (no EU VAT), only two calls are made: CNPJ + CPF.

7. cURL examples

Validate a CNPJ number:

curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://api.isvalid.dev/v0/br/cnpj?value=11222333000181"

Validate a CPF number:

curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://api.isvalid.dev/v0/br/cpf?value=12345678909"

Validate an EU VAT number with VIES check (for cross-border):

curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://api.isvalid.dev/v0/vat?value=DE123456789&checkVies=true"

8. Handling edge cases

Brazilian tax compliance has unique challenges related to the dual-identifier system, company statuses, and cross-border scenarios:

CNPJ found but status is not ATIVA

The CNPJ exists in Receita Federal but has a status other than ATIVA. Brazilian companies can be SUSPENSA (suspended), INAPTA (unfit), BAIXADA (closed), or NULA (annulled). Only ATIVA companies should be onboarded.

entity = cnpj_result.get("entity") or {}
if cnpj_result.get("found") and entity.get("status") != "ATIVA":
    # Company exists but is not active — reject
    return {
        "status": "rejected",
        "reason": f"CNPJ status is {entity['status']} — only ATIVA companies can be onboarded",
    }

CPF valid but identical digits

CPFs where all 11 digits are the same (e.g., 111.111.111-11) pass the modulo-11 checksum algorithm but are known to be invalid. These are commonly entered as test data or placeholders. The API automatically rejects these, so valid will be false.

# The API handles this automatically — no special code needed
# CPFs like 000.000.000-00 or 111.111.111-11 will return valid: False
if not cpf_result["valid"]:
    return {
        "status": "rejected",
        "field": "cpf",
        "reason": "Invalid CPF — check digit validation failed",
    }

CNPJ is a branch, not headquarters

A CNPJ has a 4-digit branch identifier (digits 9-12). The headquarters is always 0001, while branches are 0002, 0003, etc. For KYC onboarding, you typically want to verify the headquarters CNPJ. If a branch CNPJ is provided, flag it and request the headquarters number.

# Extract branch number from CNPJ (digits 9-12 in the 14-digit number)
branch_code = cnpj_result["cnpj"][8:12]
is_headquarters = branch_code == "0001"

if not is_headquarters:
    return {
        "status": "needs_correction",
        "reason": (
            f"CNPJ {cnpj_result['formatted']} is a branch (unit {branch_code}). "
            f"Please provide the headquarters CNPJ."
        ),
    }

CPF and CNPJ in different states

The CPF's fiscal region identifies the state where the individual was registered, while the CNPJ contains the company's registered state. A mismatch is not necessarily a problem — business owners can live in a different state from their company. Log the mismatch as a data point but do not reject.

entity = cnpj_result.get("entity") or {}
if cpf_result.get("state") != entity.get("state"):
    # Owner and company are in different states — log but don't reject
    logger.info(f"State mismatch: CPF={cpf_result['state']}, CNPJ={entity['state']}")
    # This is common for multi-state businesses or remote owners
    risk_notes = result.get("risk_notes", [])
    risk_notes.append("owner_company_state_mismatch")

9. Summary checklist

Validate CNPJ with registry lookup — confirm the company exists and is ATIVA
Validate CPF for the owner/director — required for individual KYC
Check CNPJ status — only ATIVA companies should be onboarded
Run CNPJ and CPF validation in parallel — reduces onboarding latency
Do not skip branch detection — verify that the CNPJ is the headquarters (0001)
Do not reject CPF/CNPJ state mismatches — they are common for multi-state businesses

See also

Validate Brazilian tax identifiers with one API

Free tier includes 100 API calls per day. No credit card required. CNPJ, CPF, and VAT validation all included.