Batch queries
Asynchronous batches let you submit up to 1,000 CPFs in one call and get the identifier back immediately. We enrich the documents in the background and notify you when the batch closes.
Use this instead of POST /api/v1/players/batch whenever the volume goes past a few dozen documents. A thousand-document batch takes minutes to enrich, and a synchronous call would hold the connection open for all of it.
The flow
1. POST /api/v1/batches -> 202 with batch_id, immediately
2. we enrich in the background
3. webhook fires -> batch.completed at your URL
4. GET /api/v1/batches/{id} -> the results, one page at a timeSteps 3 and 4 are independent. The webhook tells you when; the GET gives you what. If you did not provide a webhook_url, poll the GET instead — it works at any point, including mid-processing.
1. Submit
curl -X POST https://kyg-api.paag.io/api/v1/batches \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"player_ids": ["12345678901", "98765432100"],
"webhook_url": "https://your-domain.com/webhooks/kyg"
}'{
"batch_id": "lote_3c6e0b8a9c15",
"status": "queued",
"total_requested": 1000,
"created_at": "2026-08-14T16:00:00Z",
"deadline_at": "2026-08-14T16:30:00Z",
"webhook_secret": "kGx7Qm2vZs9tR4wLpN1eYb6UaHc3JdFiOo0AzXyKlM8"
}webhook_url is optional. When you send one, the response carries a webhook_secret for verifying that batch's notification — returned only here, so store it with the batch_id. See Webhooks.
Duplicates are removed before anything else. The 1,000-document ceiling applies to the deduplicated list, and total_requested reports that number — send 1,200 CPFs of which 300 repeat, and you get a batch of 900. Compare total_requested against your own list length if the difference matters to you.
About deadline_at: the value returned here is an estimate made at submission. It is recalculated when a worker actually picks the batch up, so time spent waiting in the queue does not eat into the processing window. The authoritative value is the one in GET /api/v1/batches/{batch_id}.
2. Read the result
curl "https://kyg-api.paag.io/api/v1/batches/lote_3c6e0b8a9c15?limit=100" \
-H "Authorization: Bearer YOUR_API_KEY"The response carries the batch header plus one page of results. Each entry in results has the same four dimensions as a single-CPF query — you never have to parse two representations of the same profile.
Pagination is not optional
A thousand results exceed 1.4 MB, past what our storage layer returns in a single read. Keep following next_cursor until it comes back null. Reading only the first page silently gives you a fraction of the batch:
cursor=""
while :; do
page=$(curl -s -H "Authorization: Bearer YOUR_API_KEY" \
"https://kyg-api.paag.io/api/v1/batches/$BATCH_ID?limit=500&cursor=$cursor")
echo "$page" | jq -c '.results[]'
cursor=$(echo "$page" | jq -r '.next_cursor // empty')
[ -z "$cursor" ] && break
donelimit accepts 1 to 500 and defaults to 100.
Batch statuses
status | Meaning | Worth retrying? |
|---|---|---|
queued | Accepted, not started | — |
processing | Being enriched | — |
completed | Every document resolved | No |
partial | Resolved, but an enrichment source did not answer for some documents | Only for the affected CPFs |
expired | The processing window closed with documents still pending | Yes — resubmit the pending ones |
failed | Nothing was resolved | Yes |
expired and partial are deliberately different answers. expired means we ran out of time, so resubmitting the pending CPFs is worth it. partial means a source did not answer for that CPF — either it has nothing on file, or it was briefly unavailable. Which of the two it was is not distinguishable from the response, so retry partial sparingly and with backoff rather than in a loop.
Every document in the batch is enriched or accounted for in results either way — partial does not mean the batch was cut short.
Document statuses
status | Meaning |
|---|---|
found | Profile returned |
not_found | The CPF is not in the Paag base |
pending | The CPF is in our base, but the window closed before enrichment finished — dimensions come back null and pending_reasons says why |
pending_reasons values: income_unavailable, risk_unavailable, deadline_exceeded.
What you are charged for
Billing counts CPFs found in the Paag base, once per batch, regardless of whether enriching them required a paid external lookup.
The usage object in the batch header is a different number:
"usage": {
"documents_requested": 1000,
"income_from_cache": 52,
"income_queried": 948,
"risk_from_cache": 985,
"risk_queried": 14
}That is our internal cost against external sources, exposed for transparency. It is not the billing basis, and the two will rarely match.
Retention
Batches and their results are kept for 7 days. After that, GET /api/v1/batches/{batch_id} returns 404. Persist anything you need to keep.
Ordering
Results are not guaranteed to come back in the order you submitted them. Match on player_id, never on position.
Updated about 1 hour ago
