A customer fills in their CPF at your checkout, transposes two digits without noticing, and places the order. The address is right, the payment clears, everything looks done. Three days later the nota fiscal bounces because that tax ID does not exist. You email them; they reply the following week; the order sits in limbo the whole time. Every bit of that was catchable in the browser, the instant they left the field, for nothing — because a CPF is not eleven arbitrary digits.
The last two digits are a checksum
A CPF is nine base digits followed by two check digits. A CNPJ is twelve base digits followed by two. Those trailing digits are not chosen; they are computed from the ones in front of them with a weighted sum taken modulo 11. Change a digit, or swap two of them, and the number you recompute no longer matches the one that was typed. That is the entire mechanism, and it is why you can throw out the large majority of typos without calling a single external service.
It is worth being precise about what that buys you. A check digit is an error-detecting code, not a database. It reliably catches any single wrong digit and most two-digit transpositions — the mistakes humans actually make at a keyboard. That is exactly the failure you want to stop before it becomes a bounced invoice.
CPF, in one function
No dependency, no network. Strip the formatting, then recompute both check digits and compare:
// Catches typos. Does NOT prove the CPF is real or belongs to the buyer.
function isValidCPF(value) {
const d = value.replace(/\D/g, '');
if (d.length !== 11) return false;
if (/^(\d)\1{10}$/.test(d)) return false; // 111.111.111-11 passes the math - reject it
const digit = (len) => {
let sum = 0;
for (let i = 0; i < len; i++) sum += +d[i] * (len + 1 - i);
const r = sum % 11;
return r < 2 ? 0 : 11 - r;
};
return digit(9) === +d[9] && digit(10) === +d[10];
}
The first check digit weights the nine base digits by 10 down to 2; the second weights the first ten digits by 11 down to 2. In both cases you take the sum modulo 11, and the digit is 0 when that remainder is under 2, otherwise 11 - remainder. That < 2 rule is the part people get wrong when they reinvent it from memory.
CNPJ changes the weights
The shape is identical — weighted sum, modulo 11, same digit rule — but a CNPJ has twelve base digits and a different set of weights, so it needs its own function rather than a reused one:
function isValidCNPJ(value) {
const d = value.replace(/\D/g, '');
if (d.length !== 14) return false;
if (/^(\d)\1{13}$/.test(d)) return false; // 00.000.000/0000-00 passes the math - reject it
const digit = (len) => {
const weights = len === 12
? [5,4,3,2,9,8,7,6,5,4,3,2]
: [6,5,4,3,2,9,8,7,6,5,4,3,2];
let sum = 0;
for (let i = 0; i < len; i++) sum += +d[i] * weights[i];
const r = sum % 11;
return r < 2 ? 0 : 11 - r;
};
return digit(12) === +d[12] && digit(13) === +d[13];
}
Validating a CNPJ with the CPF rule — or the reverse — is a common and invisible bug: it accepts and rejects the wrong numbers without ever throwing. Pick the function from the customer type (pessoa física or pessoa jurídica), not from the length of whatever they pasted.
The number that passes the math and is still wrong
Run the arithmetic on 111.111.111-11 and it checks out. So does 000.000.000-00. Their recomputed check digits genuinely match, because a run of identical digits is a fixed point of the weighted sum. They are still not valid CPFs, and every real validator rejects them outright — which is why both functions above bail on a repeated-digit string before touching the math:
if (/^(\d)\1{10}$/.test(d)) return false; // CPF: all eleven digits the same
Leave that line out and you will cheerfully accept 000.000.000-00 — which is precisely what a bot, or a hurried human, drops into a required field it does not want to fill. The guard is one line and it is the difference between a validator that works and one that only looks like it does.
Validate twice: once for the human, once for you
Client-side, this is a courtesy. Mask the field as they type, validate on blur, and show the error inline so the fix happens while they are still looking at it — not in an email after the order fails. It makes the checkout feel like it was built for them.
Server-side, the same check is a gate, and it is not optional. Client validation protects honest users from their own typos; it protects you from nothing. Anyone can disable JavaScript or POST straight to your order endpoint. Run isValidCPF again on the server before you create the order, and treat the browser result as a hint you still have to confirm. The rule that never lets you down: never trust a value you have not validated on the server.
What a valid CPF still does not tell you
Here is the limit, stated plainly, because pretending otherwise is how stores talk themselves into trouble. A passing check digit means one thing: the number is well-formed. It does not mean the CPF exists in the Receita Federal's records, and it does not mean the person at your checkout is the one it belongs to. The checksum catches accidents, not invention — a determined person can hand-pick a number that validates in a few tries.
If you genuinely need to know a document is real and active — for regulated goods, for fraud screening — that is a paid lookup against Serpro or a licensed reseller. It costs money per query, it is slower, and it drags in real data-handling obligations about what you are allowed to store and why. That is a deliberate decision with a price attached, not a line of JavaScript. For the ordinary job, stopping typos before they turn into failed invoices, the free check digit is the right tool, and being honest that it proves form and not truth is worth more than a checkmark that overclaims.
The short version
Collecting a Brazilian tax ID is a small field with a sharp edge. Get the check-digit math right and you quietly kill a whole class of silent order failures; get the repeated-digit guard wrong and you wave 000.000.000-00 straight through. The two functions above are the entire client-side job, the server runs the same two, and neither of them pretends to be a background check.
We wired all of this — the CPF and CNPJ masks, the person-or-company switch that picks the right rule, and the identical validation on both sides — into Checkout Brasil, our WooCommerce plugin for Brazilian checkouts. If you would rather build it yourself, the code above is the part that actually matters.
We build and run small products, and write up what breaks. Checkout Brasil · Rebel Studios.
