Introduction
To look up a Brazilian CPF (Brazil's individual taxpayer ID) from Node.js, send a GET request to https://api.cpfhub.io/cpf/{cpf} with your key in the x-api-key header, using the native fetch that ships with Node 18 and later. The response gives you the holder's name, gender and date of birth, which you compare with what the user typed. No SDK is required.
This guide builds that integration step by step: a lookup function with a timeout, error handling for every status code, a name and date of birth match, and an Express route your frontend can call.
Before you start
- Node.js 18 or later, for the global
fetchandAbortSignal.timeout. - An API key from CPFHub.io. The Free plan has 50 credits and no credit card.
- The key in an environment variable, never in the code:
export CPFHUB_API_KEY="YOUR_API_KEY"
The key stays on your server. Browser JavaScript must never hold it: your frontend calls your own backend route, and that route calls the API.
Step 1: a lookup function with a timeout
// cpfhub.js
const BASE_URL = 'https://api.cpfhub.io'
export async function lookupCpf(cpf) {
const digits = String(cpf).replace(/\D/g, '')
const response = await fetch(`${BASE_URL}/cpf/${digits}`, {
headers: {
'x-api-key': process.env.CPFHUB_API_KEY,
Accept: 'application/json',
},
signal: AbortSignal.timeout(10_000),
})
if (response.status === 404) return null // CPF not found: no credit is used
const body = await response.json()
if (!response.ok) {
const message = typeof body.error === 'string' ? body.error : body.error?.message
const error = new Error(message || `CPFHub.io error ${response.status}`)
error.status = response.status
error.retryAfter = Number(response.headers.get('retry-after')) || null
throw error
}
return body.data
}
AbortSignal.timeout cancels the request after 10 seconds, so a slow network never hangs your signup. When it fires, fetch rejects with a TimeoutError.
A successful response looks like this:
{
"success": true,
"data": {
"cpf": "00000000000",
"name": "Fulano de Tal",
"nameUpper": "FULANO DE TAL",
"gender": "M",
"birthDate": "15/06/1990",
"day": 15,
"month": 6,
"year": 1990
}
}
gender can be null, so do not assume it is always there.
Step 2: handle every status code
Decide by the HTTP status, never by the message text. Error bodies come in two shapes: lookup errors return error as an object with a message, and authentication or account errors return error as a string. The function above already reads both.
| Status | Meaning | What to do |
|---|---|---|
| 200 | CPF found | Compare the data with the form |
| 400 | The CPF does not have 11 digits | Ask the user to fix it |
| 401 | Missing, invalid or inactive key | Check your configuration, do not retry |
| 403 | No credits left, or account blocked | Alert your team |
| 404 | CPF not found | Treat as a failed check. No credit used |
| 422 | Wrong check digits | Ask the user to fix it |
| 429 | Per-minute limit reached | Wait for Retry-After, then retry. No credit used |
| 500, 503 | Transient failure | Retry with backoff |
A small retry wrapper for 429 and transient errors:
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
export async function lookupCpfWithRetry(cpf, attempts = 3) {
for (let i = 0; i < attempts; i++) {
try {
return await lookupCpf(cpf)
} catch (error) {
const retryable = error.status === 429 || error.status >= 500 || error.name === 'TimeoutError'
if (!retryable || i === attempts - 1) throw error
const waitMs = error.retryAfter ? error.retryAfter * 1000 : 2000 * 2 ** i
await sleep(waitMs)
}
}
}
The Retry-After header gives the wait in seconds. Without it, the wrapper backs off 2, 4 and 8 seconds.
Step 3: match the name and date of birth
A lookup that returns data does not mean the CPF belongs to your user. Compare the result with the form:
// match.js
const normalize = (text) =>
text
.normalize('NFD')
.replace(/[̀-ͯ]/g, '')
.toUpperCase()
.replace(/\s+/g, ' ')
.trim()
export function matchesUser(data, form) {
const typed = normalize(form.name).split(' ')
const registry = normalize(data.nameUpper).split(' ')
const sameFirst = typed[0] === registry[0]
const sameLast = typed.at(-1) === registry.at(-1)
const sameBirthDate = form.birthDate === data.birthDate // both in DD/MM/YYYY
return sameFirst && sameLast && sameBirthDate
}
Brazilian names often have several middle names that users abbreviate or skip, so comparing first and last names is more forgiving than an exact match. The date of birth should match exactly: a real CPF with a different date of birth is one of the strongest fraud signals you will get. Send a mismatch to review instead of rejecting it outright.
Step 4: expose an Express route
Your frontend posts the form to your backend, never to the API. A minimal Express route:
// server.js
import express from 'express'
import { lookupCpfWithRetry } from './cpfhub.js'
import { matchesUser } from './match.js'
const app = express()
app.use(express.json())
app.post('/api/verify-cpf', async (req, res) => {
const { cpf, name, birthDate } = req.body ?? {}
if (!cpf || !name || !birthDate) return res.status(400).json({ ok: false })
try {
const data = await lookupCpfWithRetry(cpf)
if (!data) return res.json({ ok: false, reason: 'not_found' })
const ok = matchesUser(data, { name, birthDate })
return res.json({ ok, reason: ok ? null : 'mismatch' })
} catch (error) {
if (error.status === 400 || error.status === 422) {
return res.status(400).json({ ok: false, reason: 'invalid_cpf' })
}
console.error('CPF lookup failed', error.status, error.message)
return res.status(502).json({ ok: false, reason: 'unavailable' })
}
})
app.listen(3000)
The route returns only a yes or no and a reason. Do not send the registry name or date of birth back to the browser: the person typing might not be the CPF holder, and echoing the data would hand it to them.
Going further
- Registration status. For the CPF status at the Receita Federal (Brazil's federal tax authority) and the year of death, call the Real-Time Lookup with
POST /cpf/realtime, the CPF and the date of birth. Use a 60 second timeout. - Many CPFs. For cleaning an existing database, the bulk CPF lookup takes up to 10,000 CPFs per request, and only CPFs found use credits.
- Balance.
GET /quotareturns your remaining credits and never uses one.
Full reference in the CPF lookup documentation.
Frequently asked questions
Do I need an SDK to call the API from Node.js?
No. The API is a single GET request with one header, and Node's native fetch covers it. The functions above are the whole client.
Can I call the API directly from React or the browser?
No. The API key would be visible to anyone who opens the browser's developer tools. Call your own backend route, as in the Express example, and keep the key in CPFHUB_API_KEY on the server.
Does a 404 use a credit?
No. A CPF that is not found returns 404 and does not use a credit. Errors such as 400, 422 and 429 do not use credits either.
How do I test without real personal data?
In development, look up only CPFs you are allowed to process, such as your own, and never log full registry responses. Treat every response as personal data under the LGPD, Brazil's data protection law.
Read also
- How to Verify a Brazilian CPF Number
- Brazil Identity Verification API: What Data You Can Get from a CPF
- What Is a Brazilian CPF? A Guide for Foreign Companies
- KYB in Brazil: How to Verify a Company with the CNPJ
Conclusion
A CPF lookup in Node.js is a fetch call with a timeout, plus the parts that make it production-ready: handling each status code, retrying on 429 after Retry-After, matching the name and date of birth, and keeping the key behind your own route.
Get an API key with 50 free credits and no credit card at cpfhub.io.
CPFHub.io
Ready to integrate the API?
50 free credits to test now. No credit card. Instant access to the documentation.




