# 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.

**Published:** November 13, 2026
**Author:** Lucas Vieira
**URL:** https://www.cpfhub.io/en/blog/cpf-lookup-api-nodejs

---


## 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`](https://nodejs.org/en/learn/getting-started/fetch) and `AbortSignal.timeout`.
- **An API key** from [**CPFHub.io**](/en). The Free plan has 50 credits and no credit card.
- **The key in an environment variable**, never in the code:

```bash
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

```javascript
// 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`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout_static) 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:

```json
{
  "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:

```javascript
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`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/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:

```javascript
// 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](https://expressjs.com/en/guide/routing.html) route:

```javascript
// 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](/en/documentation/reference/cpf-realtime) 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](/en/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](/en/documentation/reference/cpf).

---

## 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](https://www.planalto.gov.br/ccivil_03/_ato2015-2018/2018/lei/l13709.htm), Brazil's data protection law.

---

### Read also

- [How to Verify a Brazilian CPF Number](/en/blog/how-to-verify-a-brazilian-cpf)
- [Brazil Identity Verification API: What Data You Can Get from a CPF](/en/blog/brazil-identity-verification-api)
- [What Is a Brazilian CPF? A Guide for Foreign Companies](/en/blog/what-is-a-brazilian-cpf)
- [KYB in Brazil: How to Verify a Company with the CNPJ](/en/blog/kyb-brazil-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](/en).

