CPFHub.io
Start for free

Batch Lookup

Send a list of up to 10,000 CPFs (Brazil's individual taxpayer ID numbers) and get the name, gender and date of birth for each one, processed in the background. It costs 1 credit per CPF found.

Using Cursor, Lovable, v0 or another AI? Copy the batch lookup prompt and paste it into your assistant.

Open in Cursor

Not the one you need? Compare the lookups. If you prefer no code, the dashboard accepts CSV/TXT files or pasted CPFs and exports the result as CSV.

Two-step flow:

  1. POST /cpf/bulk sends the list and responds immediately with a jobId.
  2. GET /cpf/bulk/{jobId} returns the progress and the results. Repeat until status is "done" or "failed".

Endpoint

POSThttps://api.cpfhub.io/cpf/bulk

Parameters

WhereNameRequiredDescription
Headerx-api-keyYesYour API key. See Authentication.
HeaderContent-TypeYesapplication/json
BodycpfsYesstring[] of CPFs, with or without formatting. Not empty, maximum 10,000. Items that are not strings count as invalid.
BodyfileNameNoFree-form label. Comes back as file_name in the status.
BodyformatNoFree-form label for the source format (for example, "csv"). Comes back as format.

What happens on submission

StepEffect
NormalizationRemoves the formatting. CPFs without 11 digits or with an incorrect check digit go to invalid and are not charged.
DuplicatesRemoved. Each CPF is looked up and charged once. They do not count in totalRecords or in invalid.
Per-minute limitEach submission counts as one request against the Simple Lookup limit (30 per minute by default).
BalanceNo overage: the balance must cover all valid CPFs (totalRecords × the Simple Lookup cost, 1 credit by default). If it does not, you get 403 and nothing is charged.

More than 10,000 CPFs: split them into chunks and send one POST for each.

Example

Check the status before reading data: on error, the response has no jobId.

curl -X POST "https://api.cpfhub.io/cpf/bulk" \
  -H "x-api-key: $CPFHUB_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "cpfs": ["12345678909", "11144477735", "52998224725"]
  }'

Response

HTTP 202 Accepted

JSON
{
  "success": true,
  "data": {
    "jobId": "4a1ef25e-052c-4464-9657-4148f53bb0fc",
    "status": "processing",
    "totalRecords": 3,
    "invalid": 0
  }
}
FieldTypeDescription
data.jobIdstringBatch ID. Keep it to check the status.
data.statusstringAlways "processing"
data.totalRecordsnumberValid, unique CPFs that will be looked up
data.invalidnumberDiscarded items (format, check digit or not a string)

The submission responds in camelCase. The status responds in snake_case.

Status and results

GEThttps://api.cpfhub.io/cpf/bulk/{jobId}

Use the same API key as the submission. It does not use credits, does not count toward the per-minute limit and works with a zero balance.

Polling:

  • Start at every 5 s and increase up to 30 s on large batches. Each call returns the whole cpfs array (up to ~2 MB for 10,000 CPFs).
  • Set a maximum deadline in your client (for example, 30 min). The batch time depends on how many CPFs you submitted, and each lookup stays in the same range as the Simple Lookup (~150 ms).
  • On a 5xx or a network error, try again on the next cycle. The GET is safe to repeat.
curl "https://api.cpfhub.io/cpf/bulk/4a1ef25e-052c-4464-9657-4148f53bb0fc" \
  -H "x-api-key: $CPFHUB_API_KEY"

HTTP 200 OK

JSON
{
  "success": true,
  "data": {
    "id": "4a1ef25e-052c-4464-9657-4148f53bb0fc",
    "status": "done",
    "file_name": null,
    "format": null,
    "total_records": 2,
    "processed": 2,
    "found": 1,
    "not_found": 1,
    "skipped": 0,
    "error": null,
    "created_at": "2026-10-02T14:15:16.000Z",
    "updated_at": "2026-10-02T14:15:18.000Z",
    "cpfs": [
      {
        "cpf": "12345678909",
        "found": true,
        "name": "Fulano de Tal",
        "nameUpper": "FULANO DE TAL",
        "gender": "M",
        "birthDate": "15/06/1990",
        "day": 15,
        "month": 6,
        "year": 1990
      },
      {
        "cpf": "52998224725",
        "found": false,
        "name": null,
        "nameUpper": null,
        "gender": null,
        "birthDate": null,
        "day": null,
        "month": null,
        "year": null
      }
    ]
  }
}
FieldDescription
data.idBatch ID (the jobId from the submission)
data.status"processing", "done" or "failed"
data.total_recordsValid, unique CPFs in the batch
data.processedCPFs already processed (includes skipped)
data.foundFound. Each one uses 1 credit by default (your plan's cost is in the dashboard)
data.not_foundNot found. They do not use credits.
data.skippedNot looked up because the balance ran out during the batch (no overage). They do not use credits.
data.errorFailure reason when status is "failed"
data.file_name, data.formatLabels sent in the POST
data.created_at, data.updated_atISO 8601 dates
data.cpfs[]Result for each processed CPF: cpf (11 digits), found, name, nameUpper, gender, birthDate, day, month, year (null when found is false)

While the status is "processing", the counters update every 50 CPFs and may lag behind cpfs.

Errors

The API only responds in Portuguese. Error messages are shown as returned, with the translation in parentheses the first time.

StatusWhen it happensWhat to do
400cpfs missing, empty, not an array or with more than 10,000 itemsFix the body.
401Missing, invalid or inactive API key (error is a string)Check the x-api-key header.
403No overage: zero balance (error string "Limite de créditos excedido", credit limit exceeded) or balance smaller than the batch (error.message: "Créditos insuficientes para o lote: N necessários.", not enough credits for the batch: N needed). Also inactive/blocked account. POST only.Add credits or reduce the batch.
404jobId does not exist or was created with another API keyCheck the jobId and the key.
422No valid CPF in the listFix the list.
429Per-minute limit exceeded (POST only)Wait the seconds given in the Retry-After header.
500, 503Transient failure. No batch was created.Try again. Report it if it persists.

Error format, retries and how to test without spending credits: Error Codes. Per-minute limit and credits: Usage Limits.

Details

Batch states

StateHow to identify itWhat to do
Processingstatus: "processing"Keep polling. cpfs already has partial results.
Completed"done" and skipped: 0Use cpfs.
Completed with skipped items"done" and skipped > 0The balance ran out during the batch. Add credits and resend only the CPFs that are missing.
Failed"failed"Read error. The ones found in cpfs have already been charged. Resend only the ones that are missing.

Values of error when the batch fails:

errorMeaning
"Banco de dados indisponível" (database unavailable)Nothing was processed or charged.
"Falha ao registrar validação" (failed to record the lookup)Failed to save a result. Processing stopped.
"Worker interrompido (job órfão)" (worker interrupted, orphaned job)Processing was interrupted. Marked this way after ~10 min without an update.

With overage, no CPF is skipped: all of them are looked up and the overage is billed.

Matching the results to your list

cpfs comes in processing order and does not include skipped or unprocessed items. Normalize your list to 11 digits and match on the cpf field. The ones that are missing are the ones to resend (this includes invalid ones, which cost nothing).

const normalize = (cpf: string) => cpf.replace(/\D/g, '')

const results = new Map(job.cpfs.map((item) => [item.cpf, item]))

for (const original of myCpfs) {
  const result = results.get(normalize(original))
  console.log(original, result ? (result.found ? result.name : 'not found') : 'no result')
}

// CPFs to resend in a new batch (if the batch failed or had skipped items)
const missing = [...new Set(myCpfs.map(normalize))].filter((cpf) => !results.has(cpf))

More

  • Non-idempotent submission: each accepted POST creates a new batch and charges the ones found. If the connection drops without a jobId, check the dashboard or GET /quota before resending. If you got an error response, no batch was created.
  • Credits: only found: true is charged. Invalid, duplicate, not found and skipped items are not.
  • CSV export: only in the dashboard, with the columns CPF, Nome (name), Nascimento (date of birth) and Encontrado (found), without gender.

Updated on October 4, 2026