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 time

Steps 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
done

limit accepts 1 to 500 and defaults to 100.

Batch statuses

statusMeaningWorth retrying?
queuedAccepted, not started
processingBeing enriched
completedEvery document resolvedNo
partialResolved, but an enrichment source did not answer for some documentsOnly for the affected CPFs
expiredThe processing window closed with documents still pendingYes — resubmit the pending ones
failedNothing was resolvedYes

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

statusMeaning
foundProfile returned
not_foundThe CPF is not in the Paag base
pendingThe 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.


Did this page help you?