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.
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:
POST /cpf/bulksends the list and responds immediately with ajobId.GET /cpf/bulk/{jobId}returns the progress and the results. Repeat untilstatusis"done"or"failed".
Endpoint
https://api.cpfhub.io/cpf/bulkParameters
| Where | Name | Required | Description |
|---|---|---|---|
| Header | x-api-key | Yes | Your API key. See Authentication. |
| Header | Content-Type | Yes | application/json |
| Body | cpfs | Yes | string[] of CPFs, with or without formatting. Not empty, maximum 10,000. Items that are not strings count as invalid. |
| Body | fileName | No | Free-form label. Comes back as file_name in the status. |
| Body | format | No | Free-form label for the source format (for example, "csv"). Comes back as format. |
What happens on submission
| Step | Effect |
|---|---|
| Normalization | Removes the formatting. CPFs without 11 digits or with an incorrect check digit go to invalid and are not charged. |
| Duplicates | Removed. Each CPF is looked up and charged once. They do not count in totalRecords or in invalid. |
| Per-minute limit | Each submission counts as one request against the Simple Lookup limit (30 per minute by default). |
| Balance | No 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
{
"success": true,
"data": {
"jobId": "4a1ef25e-052c-4464-9657-4148f53bb0fc",
"status": "processing",
"totalRecords": 3,
"invalid": 0
}
}| Field | Type | Description |
|---|---|---|
data.jobId | string | Batch ID. Keep it to check the status. |
data.status | string | Always "processing" |
data.totalRecords | number | Valid, unique CPFs that will be looked up |
data.invalid | number | Discarded items (format, check digit or not a string) |
The submission responds in camelCase. The status responds in snake_case.
Status and results
https://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
cpfsarray (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
5xxor a network error, try again on the next cycle. TheGETis 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
{
"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
}
]
}
}| Field | Description |
|---|---|
data.id | Batch ID (the jobId from the submission) |
data.status | "processing", "done" or "failed" |
data.total_records | Valid, unique CPFs in the batch |
data.processed | CPFs already processed (includes skipped) |
data.found | Found. Each one uses 1 credit by default (your plan's cost is in the dashboard) |
data.not_found | Not found. They do not use credits. |
data.skipped | Not looked up because the balance ran out during the batch (no overage). They do not use credits. |
data.error | Failure reason when status is "failed" |
data.file_name, data.format | Labels sent in the POST |
data.created_at, data.updated_at | ISO 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.
| Status | When it happens | What to do |
|---|---|---|
400 | cpfs missing, empty, not an array or with more than 10,000 items | Fix the body. |
401 | Missing, invalid or inactive API key (error is a string) | Check the x-api-key header. |
403 | No 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. |
404 | jobId does not exist or was created with another API key | Check the jobId and the key. |
422 | No valid CPF in the list | Fix the list. |
429 | Per-minute limit exceeded (POST only) | Wait the seconds given in the Retry-After header. |
500, 503 | Transient 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
| State | How to identify it | What to do |
|---|---|---|
| Processing | status: "processing" | Keep polling. cpfs already has partial results. |
| Completed | "done" and skipped: 0 | Use cpfs. |
| Completed with skipped items | "done" and skipped > 0 | The 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:
error | Meaning |
|---|---|
"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
POSTcreates a new batch and charges the ones found. If the connection drops without ajobId, check the dashboard orGET /quotabefore resending. If you got an error response, no batch was created. - Credits: only
found: trueis charged. Invalid, duplicate, not found andskippeditems are not. - CSV export: only in the dashboard, with the columns
CPF,Nome(name),Nascimento(date of birth) andEncontrado(found), without gender.
Updated on October 4, 2026