Use case

CPF/CNPJ validation in a signup or checkout flow

Search volume for "CPF validation API" is high and the honest answer is usually "do it locally, for free, in twenty lines". Here is when that is wrong, and what to do instead.

Updated 2026-08-07 · 8 min read · by the FaturaPDF team

Straight answer first

If your requirement is "reject typos in a CPF/CNPJ field", you do not need an API. The check digits are computed from the number itself; validation is a pure function, offline, microseconds, free. Working code is below and there is a free browser tool to test against.

You need a remote service only when the question is different — see when you actually do need a lookup.

That said, "just validate it" hides several product decisions that are worth getting right, because each one shows up later as a support ticket: where to validate, what to store, how to display, and what to do about the fact that a structurally valid CPF says almost nothing about the person typing it.

Where to validate

Three layers, all cheap, each catching what the previous one missed:

LayerWhat it catchesCost
Client-side, on blurTypos, while the user still remembers what they meant to typeZero. Best UX-per-line-of-code in the whole flow.
Server-side, on writeAPI clients, imports, scripts, and anyone bypassing your formZero. Non-negotiable — client validation is a convenience, not a control.
Database constraintMigrations, backfills, that one-off SQL updateA check constraint on the normalized column.
Do not validate only at PDF/invoice time

The common failure: a customer record with a broken CNPJ sits in the database for eight months and surfaces the first time someone tries to generate a fiscal document for them — usually at month end, usually under time pressure, and the customer is now unreachable. Validate at the boundary where the data enters.

The code

Full derivation, worked arithmetic and the three classic mistakes are in the check-digit reference. The short version:

javascripttaxid.js
const digits = (s) => String(s ?? "").replace(/\D/g, "");

function checkDigit(nums, weights) {
  const sum = weights.reduce((acc, w, i) => acc + nums[i] * w, 0);
  const rest = sum % 11;
  return rest < 2 ? 0 : 11 - rest;          // NOT (11 - rest) % 10
}

export function isValidCPF(v) {
  const d = digits(v);
  if (d.length !== 11 || /^(\d)\1{10}$/.test(d)) return false;   // repeated-digit guard
  const n = [...d].map(Number);
  return checkDigit(n, [10, 9, 8, 7, 6, 5, 4, 3, 2]) === n[9]
      && checkDigit(n, [11, 10, 9, 8, 7, 6, 5, 4, 3, 2]) === n[10];
}

export function isValidCNPJ(v) {
  const d = digits(v);
  if (d.length !== 14 || /^(\d)\1{13}$/.test(d)) return false;
  const n = [...d].map(Number);
  const w1 = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
  return checkDigit(n, w1) === n[12] && checkDigit(n, [6, ...w1]) === n[13];
}

export const isValidTaxId = (v) =>
  digits(v).length === 11 ? isValidCPF(v) : isValidCNPJ(v);

Ports in Python, PHP, Laravel (as a Rule), Go, C# and Ruby.

Storage and display

The single highest-value decision, and it takes one line:

Store digits only. Mask at render time.

Persist "11222333000181", never "11.222.333/0001-81". Otherwise 11.222.333/0001-81 and 11222333000181 become two different rows, your unique index does nothing, and deduplication becomes a project. Add the index on the normalized column and mask in the view layer.

sqlpostgres
alter table customers
  add column tax_id_digits text
    generated always as (regexp_replace(tax_id, '\D', '', 'g')) stored;

create unique index customers_tax_id_unique
  on customers (tax_id_digits)
  where tax_id_digits is not null and tax_id_digits <> '';

A generated column keeps the normalization in the database, so an import script that forgets to normalize cannot create a duplicate.

Input masking

Mask progressively as the user types, and switch format at 11 digits — a CPF is 000.000.000-00 and a CNPJ is 00.000.000/0000-00, and a single field usually has to accept both:

javascriptmask.js
export function maskTaxId(raw) {
  const d = digits(raw).slice(0, 14);
  if (d.length <= 11) {
    return d
      .replace(/^(\d{3})(\d)/, "$1.$2")
      .replace(/^(\d{3})\.(\d{3})(\d)/, "$1.$2.$3")
      .replace(/\.(\d{3})(\d{1,2})$/, ".$1-$2");
  }
  return d
    .replace(/^(\d{2})(\d)/, "$1.$2")
    .replace(/^(\d{2})\.(\d{3})(\d)/, "$1.$2.$3")
    .replace(/\.(\d{3})(\d)/, ".$1/$2")
    .replace(/(\d{4})(\d{1,2})$/, "$1-$2");
}

Paste handling matters more than people expect: users paste from spreadsheets, with a leading apostrophe, non-breaking spaces or a trailing tab. Because digits() strips everything non-numeric, all of that just works — which is the argument for normalizing aggressively rather than validating the formatted string.

When you actually do need a remote lookup

Check digits answer "is this well-formed". They cannot answer:

  • Is this CNPJ registered and active, and what is its legal name and address?
  • Is the company's registration suspended or cancelled?
  • What are its activity codes (CNAE), and does its declared activity match what it is invoicing me for?
  • Does this CPF belong to the person presenting it? (No public API answers this; that is identity verification, a different and regulated product.)

Those are registry-lookup questions. If you are onboarding suppliers, releasing payouts, or have AML/KYC obligations, they are worth paying for — from a provider with a defensible data source.

Local check digitRegistry lookup
LatencyMicrosecondsNetwork round trip
Cost per checkZeroMetered
Works offlineYesNo
Rate limitsNoneYes
Catches typosYesYes
Catches valid-but-unregisteredNoYes
Sends personal data to a third partyNoYes

The right architecture is both, in order: local on every keystroke, remote once, at the moment the answer changes a decision. Calling a paid lookup on every form blur is a common and expensive anti-pattern — it turns a free operation into a metered one and adds latency to typing.

CPF is personal data

Under the LGPD, a CPF identifies a natural person. Transmitting one to a third-party lookup service is a processing activity that needs a legal basis and belongs in your privacy documentation. Local validation avoids the question entirely, which is a real and often-overlooked argument for doing it locally wherever it suffices.

Test data

Never put a real person's CPF in a fixture. Generate structurally valid synthetic numbers:

javascriptfixtures.js
export function fakeCPF() {
  const base = Array.from({ length: 9 }, () => Math.floor(Math.random() * 10));
  const d1 = checkDigit(base, [10, 9, 8, 7, 6, 5, 4, 3, 2]);
  const d2 = checkDigit([...base, d1], [11, 10, 9, 8, 7, 6, 5, 4, 3, 2]);
  return [...base, d1, d2].join("");
}

Or use the free generator and validator, which runs entirely in your browser — nothing is sent anywhere, which also makes it safe to paste a real number into when you are debugging.

Values worth having in your test table: a valid CPF, a valid CNPJ, one of each with the last digit incremented, 000.000.000-00, 111.111.111-11, a 10-digit string, a 15-digit string, an empty string, and a CPF pasted with surrounding whitespace.

Where document generation comes in

Validation at the form is prevention. There is a second place it matters: the moment a tax ID gets printed on something a customer or an accountant will read.

FaturaPDF runs the same check on every documento in the payload and returns a 400 naming the offending party rather than rendering the PDF. It is a last line of defence — the form should have caught it — but it is the line that prevents a wrong CNPJ from reaching a customer's bookkeeping:

json400 from POST /invoice
{
  "error": "invalid_params",
  "message": "emitente.documento \"11.222.333/0001-99\" tem formato de CNPJ mas digito verificador invalido"
}

To be clear about scope: there is no standalone validation endpoint, and we do not think there should be — you would be paying for a network round trip to run twenty lines of arithmetic. The validation is part of generating a correct document.

Frequently asked questions

Is there a free CPF/CNPJ validation API?

There are several, but you almost certainly do not need one: the check digits are computed from the number itself, so validation is a pure local function with no network call. Use the code above. Pay for a service only when you need registry data — whether the CNPJ exists, its legal name, its status.

Does check-digit validation prove the person exists?

No. It proves the number is internally consistent. Roughly one in eleven random 9-digit bases yields a structurally valid CPF, so it catches typos, not fabrication. Existence and status require a registry lookup; identity requires a regulated verification product.

Should I store the CPF masked or as digits?

Digits only, with a unique index on the normalized column, and mask at render time. Storing the mask means the same document can exist twice in different formats, which quietly breaks deduplication and lookups.

How do I handle the alphanumeric CNPJ?

Widen your column and input mask to 14 alphanumeric characters now — that part is safe and cheap. The check-digit rule uses the same mod-11 weights with each character valued at charCode - 48; see the reference, and confirm the current official rule before switching your validator.

Where FaturaPDF actually helps

We do not sell a validation endpoint — the code below is free and better. What FaturaPDF does is refuse to print an invalid CPF/CNPJ on a document: send a bad one and you get a 400 naming the party, instead of a PDF with a wrong tax ID that reaches a customer.

Get an API key on RapidAPI → Or try the free browser generator

Related guides