NewFacial identity verification

How to Use a CPF Lookup API in Node.js

Call a Brazilian CPF lookup API from Node.js with native fetch: timeouts, error handling, retries on 429, name and date of birth matching, and an Express route.

Lucas Vieira
Lucas Vieira
··7 min read
How to Use a CPF Lookup API in Node.js

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 fetch and AbortSignal.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.

StatusMeaningWhat to do
200CPF foundCompare the data with the form
400The CPF does not have 11 digitsAsk the user to fix it
401Missing, invalid or inactive keyCheck your configuration, do not retry
403No credits left, or account blockedAlert your team
404CPF not foundTreat as a failed check. No credit used
422Wrong check digitsAsk the user to fix it
429Per-minute limit reachedWait for Retry-After, then retry. No credit used
500, 503Transient failureRetry 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 /quota returns 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

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.

WhatsApp