Webhook sent when an asynchronous batch reaches a final state.
Event: batch.completed
Sent when a batch submitted through POST /api/v1/batches reaches a final state — whether it succeeded or not. The body carries counters, never player profiles: read those with GET /api/v1/batches/{batch_id}.
Full request
This is a complete delivery as it arrives at your endpoint. Note that the signature is an HTTP header, not a field in the JSON body.
POST /webhooks/kyg HTTP/1.1
Host: your-domain.com
Content-Type: application/json
Content-Length: 191
X-KYG-Event: batch.completed
X-KYG-Attempt: 1
x-paag-webhook-signature: ODdmYTE0MzViMGY0ODZlYjdlMTc4N2FmMjc3YTk2NjllN2E0ZjAxZjg5NTk3ZjlmOTQ3MTAyOTg0YzA1ODdlNA==
{"event": "batch.completed", "batch_id": "lote_3c6e0b8a9c15", "status": "completed", "total_requested": 1000, "total_found": 999, "total_not_found": 1, "completed_at": "2026-08-14T16:05:34Z"}Headers
| Header | Value | What it is for |
|---|---|---|
x-paag-webhook-signature | Base64 (88 chars) | Proof the request came from us. Same header and scheme as the Payments API. See Signature verification |
X-KYG-Event | batch.completed | The event name, mirroring the event field |
X-KYG-Attempt | 1, 2, 3… | Which retry this is. 1 is the first attempt |
Body
| Field | Type | Description |
|---|---|---|
event | string | Always batch.completed, including when the batch failed — it names the notification, not the outcome |
batch_id | string | The identifier returned at submission |
status | string | How it finished: completed, partial, expired or failed |
total_requested | integer | CPFs in the batch, after duplicates were removed |
total_found | integer | CPFs found in the Paag base — this is what you are billed for |
total_not_found | integer | CPFs absent from the base |
completed_at | string (ISO 8601, UTC) | When the batch closed |
Read status, not event, to decide what happened. event is the same string on every delivery. A batch that resolved nothing arrives as "event": "batch.completed" with "status": "failed".
Final statuses
status | Meaning | Worth retrying? |
|---|---|---|
completed | Every document resolved | No |
partial | Resolved, but an enrichment source did not answer for some documents | Only the affected CPFs, sparingly |
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 time ran out, so resubmitting helps. partial means a source did not answer for that CPF — either it has nothing on file, or it was briefly unavailable — so a retry may produce the same result.
What to do next
Verify the signature, acknowledge with 2xx, then read the results:
curl "https://kyg-api.paag.io/api/v1/batches/lote_3c6e0b8a9c15?limit=500" \
-H "Authorization: Bearer YOUR_API_KEY"The result is paginated — keep following next_cursor until it comes back null. A thousand results exceed what a single read returns.
