Single, batch & async
Every RDCPASS verification service accepts the same four request modes. Send one subject or thousands, and choose whether to wait for the answer or have RDCPASS notify you when it is ready. The request and result shapes never change between modes — only the envelope around them does.
At a glance
- Four modes on every service: single or batch, each synchronous (default) or asynchronous.
- Asynchronous is opt-in per request with the
Prefer: respond-asyncheader. - Synchronous batches take up to 50 items; asynchronous batches up to 10,000.
- Results pages return up to 500 items, with cursor pagination.
Idempotency-Keymakes any POST safe to retry for 24 hours.- Every item is billed individually at a discounted batch rate, lower than a single call; asynchronous calls cost exactly the same as synchronous ones.
The four request modes
Pick the endpoint for the volume, and the header for the timing:
| Synchronous (default) | Asynchronous (Prefer: respond-async) | |
|---|---|---|
Single — POST /v1/<service> | 200 OK with the full result in the response body. | 202 Accepted with a job object. The result arrives by webhook or from GET /v1/jobs/{job_id}. |
Batch — POST /v1/<service>/batches | 200 OK with every item’s result — up to 50 items. | 202 Accepted with a batch object — up to 10,000 items. Results arrive by webhook or from GET /v1/batches/{batch_id}/results. |
Which mode should I use?
Start from what your user or process is waiting on:
| Situation | Recommended mode | Why |
|---|---|---|
| A customer is on your onboarding screen, waiting for a decision | Single, synchronous | Lowest latency, one round trip, the answer is in the response. |
| A branch agent verifies a small group — a family, a board of directors | Batch, synchronous (≤ 50) | One call, one response, every result together. |
| Face Recognition on large images, or a call on an unreliable mobile link | Single, asynchronous | Your request returns immediately; a webhook delivers the result even if your connection drops. |
| KYC refresh of an existing portfolio, a nightly AML re-screen, a SIM-registration backlog | Batch, asynchronous (≤ 10,000) | Submit once, let RDCPASS process in the background, collect results page by page. |
| More than 10,000 subjects | Several asynchronous batches | Split into batches of up to 10,000 and submit them one after another, each with its own Idempotency-Key. |
Limits
| Limit | Value | What happens beyond it |
|---|---|---|
| Items per synchronous batch | 50 | 413 batch_too_large — resend with Prefer: respond-async. |
| Items per asynchronous batch | 10,000 | 413 batch_too_large — split into several batches. |
Items per results page (limit) | 500 (default 100) | Values above 500 are capped at 500. |
base64 document delivery in batch results | Batches of up to 100 items | Use signed_url delivery for larger batches. |
Idempotency-Key replay window | 24 hours | After 24 hours the same key starts a new request. |
| Job and batch result retention | 30 days after completion | GET returns 404 job_not_found or 404 batch_not_found. |
Batches have their own quota, separate from single calls: a daily number of batch items and of batch submissions per service. Batch items never consume the single-call quota, and a batch submission counts as one request against the rate limit. See Rate limits & quotas.
Supported on every service
All verification services support all four modes with the same headers and objects:
| Service | Single | Batch | Sync | Async | Completion event |
|---|---|---|---|---|---|
| KYC Validation | /v1/kyc/validations | /v1/kyc/validations/batches | kyc_validation.completed | ||
| Face Recognition | /v1/face/recognitions | /v1/face/recognitions/batches | face_recognition.completed | ||
| Age Verification | /v1/age/verifications | /v1/age/verifications/batches | age_verification.completed | ||
| KYB Verification | /v1/kyb/verifications | /v1/kyb/verifications/batches | kyb_verification.completed | ||
| AML & CTF Screening | /v1/aml/screenings | /v1/aml/screenings/batches | aml_screening.completed | ||
| Credit Scoring | /v1/credit/scores | /v1/credit/scores/batches | credit_score.completed | ||
| Fraud Reporting | /v1/fraud/reports | /v1/fraud/reports/batches | fraud_report.created |
Login with RDCPASS is the eighth service. It is an interactive OpenID Connect flow in which the citizen approves each sign-in on their phone, so it is driven by redirects rather than by these request modes. Its server-side calls (/v1/oidc/token, /v1/oidc/userinfo) always answer synchronously.
Request headers
Prefer: respond-async
Add Prefer: respond-async to any single or batch POST to run it asynchronously. RDCPASS validates authentication, the body’s shape, your scopes and your purpose up front, and then responds 202 Accepted immediately. Anything wrong with the request as a whole is rejected synchronously with the usual error body, { "error": "<code>", "message": "..." }; problems with an individual item are reported per item in the results. Omit the header to get the default synchronous behaviour.
Idempotency-Key
Send a unique Idempotency-Key (a UUID v4 is ideal) on every POST you might retry. If RDCPASS receives the same key from the same application within 24 hours, it does not run the request again: it returns the original response — the same result, job or batch object — with the same status code. Always reuse the key with an identical body, and generate a new key for every new logical request.
Always send an Idempotency-Key with asynchronous requests
If a network error hides the202 response, you cannot tell whether the job or batch was created. Retrying with the same key returns the existing object instead of creating — and billing — a duplicate.Asynchronous single requests: the job object
An asynchronous single request returns 202 Accepted and a job. The request body is identical to the synchronous one; only the header changes:
# Plaintext body shown — encrypt it per /docs/authentication before sending.
curl https://api.rdcpass.cd/v1/kyc/validations \
-X POST \
-H "X-RDCPASS-Key-Id: key_live_8f2a1c0e9b" \
-H "X-RDCPASS-Timestamp: 1790417700" \
-H "X-RDCPASS-Nonce: 6f1c9b2e-4a3d-4e11-9c7a-2d8e5f1b0a44" \
-H "X-RDCPASS-Signature: 3f7a9c...e21d" \
-H "Idempotency-Key: 5d1a8c3e-2b7f-4e90-8a64-3c9e1f0b7d25" \
-H "Prefer: respond-async" \
-H "Content-Type: application/json" \
-d '{
"reference": "cust-0001",
"identifier": { "type": "rdcpass_id", "value": "COD-2103-0214-4937" },
"purpose": "customer_onboarding"
}'{
"id": "job_4b1d9e7a2c",
"object": "job",
"service": "kyc_validation",
"status": "pending",
"created_at": "2026-09-26T10:15:00Z",
"result_url": "/v1/jobs/job_4b1d9e7a2c"
}| Field | Type | Description |
|---|---|---|
| id | string | Unique job identifier, prefixed job_. |
| object | string | Always job. |
| service | string | The service that runs the job: kyc_validation, face_recognition, age_verification, kyb_verification, aml_screening, credit_score or fraud_report. |
| status | string enum | pending, processing, completed or failed. |
| created_at | string | RFC 3339 timestamp at which the job was accepted. |
| completed_at | string | null | RFC 3339 timestamp at which the job reached completed or failed; null until then. |
| result_url | string | Path to poll: /v1/jobs/{job_id}. |
| result | object | Present when status is completed: exactly the object the synchronous call returns. |
| error | object | Present when status is failed: { code, message } in the standard error format. |
Job lifecycle
A job starts pending, moves to processing when a worker picks it up, and ends in exactly one terminal state. completed means RDCPASS produced a result — including business outcomes such as not_found or no_match. failed means no result could be produced for this request, with the reason in error.
{
"id": "job_4b1d9e7a2c",
"object": "job",
"service": "kyc_validation",
"status": "completed",
"created_at": "2026-09-26T10:15:00Z",
"completed_at": "2026-09-26T10:15:03Z",
"result_url": "/v1/jobs/job_4b1d9e7a2c",
"result": {
"id": "kyc_8d2f6a1c93",
"object": "kyc_validation",
"livemode": true,
"reference": "cust-0001",
"status": "completed",
"result": "verified",
"certified_account": true,
"identifier": {
"type": "rdcpass_id",
"value": "COD-2103-0214-4937"
},
"match": {
"full_name": "not_provided",
"date_of_birth": "not_provided",
"age": "not_provided",
"score": null
},
"account": {
"rdcpass_id": "COD-2103-0214-4937",
"status": "active",
"certified": true,
"created_at": "2025-03-14T09:22:41Z",
"level_of_assurance": "LOA3"
},
"kyc": {
"basic": {
"full_name": "Kabeya Mwamba Tshisekedi",
"first_name": "Kabeya",
"last_name": "Tshisekedi",
"date_of_birth": "1988-04-12",
"age": 38,
"gender": "male",
"nationality": "COD"
}
},
"scopes_applied": [
"kyc.basic"
],
"scopes_withheld": [],
"purpose": "customer_onboarding",
"created_at": "2026-09-26T10:15:02Z"
}
}{
"id": "job_9e3a61c0d8",
"object": "job",
"service": "kyc_validation",
"status": "failed",
"created_at": "2026-09-26T10:16:00Z",
"completed_at": "2026-09-26T10:16:01Z",
"result_url": "/v1/jobs/job_9e3a61c0d8",
"error": {
"code": "invalid_identifier",
"message": "identifier.value \"COD-2103-02\" is not a valid RDCPASS ID."
}
}Batches
A batch request wraps the single-request fields in an items array. Each item must carry a reference — your own identifier for the subject, echoed back on its result so you can match results to your records without relying on order.
| Field | Type | Required | Description |
|---|---|---|---|
| items | object[] | Yes | The subjects to process. Each item accepts every field of the service’s single request, plus a required reference. |
| items[].reference | string | Yes | Your identifier for the item (max 128 characters), unique within the batch. |
| purpose | string | Yes* | Applies to every item. Required at batch level unless every item sets its own. |
| scopes | string[] | No | Applies to every item unless an item sets its own scopes. |
| document_delivery | string | No | signed_url (default) or base64; an item can override it. |
{
"purpose": "customer_onboarding",
"scopes": [
"kyc.phone_numbers"
],
"items": [
{
"reference": "cust-0001",
"identifier": {
"type": "rdcpass_id",
"value": "COD-2103-0214-4937"
}
},
{
"reference": "cust-0002",
"identifier": {
"type": "ceni",
"value": "1234567890123"
},
"match": {
"full_name": "Mbuyi Ilunga Nsimba"
}
},
{
"reference": "cust-0003",
"identifier": {
"type": "passport",
"value": "OB1234567"
},
"scopes": []
}
]
}In this example cust-0003 overrides the batch-level scopes with an empty list, so it returns Basic KYC only, while the other two items also receive kyc.phone_numbers.
Synchronous batches
Without Prefer: respond-async, a batch of up to 50 items returns 200 OK with { "object": "list", "data": [...], "next_cursor": null } — each entry has the same shape as on the results endpoint below: reference, status (succeeded or failed) and either result or error: { code, message }. A synchronous batch returns 200 even when some items fail: always check each item’s status.
The batch object
An asynchronous batch returns 202 Accepted and a batch object you can fetch at any time with GET /v1/batches/{batch_id}:
{
"id": "bat_7c2e91f04a",
"object": "batch",
"service": "kyc_validation",
"status": "pending",
"total_items": 2500,
"processed_items": 0,
"succeeded_items": 0,
"failed_items": 0,
"created_at": "2026-09-26T10:15:00Z",
"completed_at": null,
"results_url": "/v1/batches/bat_7c2e91f04a/results"
}| Field | Type | Description |
|---|---|---|
| id | string | Unique batch identifier, prefixed bat_. |
| object | string | Always batch. |
| service | string | The service processing the items. |
| status | string enum | pending, processing, completed, completed_with_errors or failed. |
| total_items | integer | Number of items accepted. |
| processed_items | integer | Items finished so far (succeeded + failed). |
| succeeded_items | integer | Items that produced a result. |
| failed_items | integer | Items that could not be processed; each carries an error in the results. |
| created_at | string | RFC 3339 timestamp at which the batch was accepted. |
| completed_at | string | null | RFC 3339 timestamp at which the batch reached a terminal status; null until then. |
| results_url | string | Path of the paginated results: /v1/batches/{batch_id}/results. |
Batch lifecycle
| Status | Meaning |
|---|---|
| pending | Accepted and queued; no item has started yet. |
| processing | Items are being processed. processed_items increases as they finish; results are readable as soon as each item is done. |
| completed | Every item succeeded. |
| completed_with_errors | Every item was processed, and at least one failed. Successful items are billed and available as usual. |
| failed | The batch as a whole could not be processed (for example, the organization’s balance ran out before any item was processed). No item is billed. |
Job and batch endpoints
| Method | Path | Description |
|---|---|---|
| GET | /v1/jobs/{job_id} | Retrieve a job, including its result or error once it is terminal. |
| GET | /v1/batches/{batch_id} | Retrieve a batch and its progress counters. |
| GET | /v1/batches/{batch_id}/results | List item results, oldest first, with cursor pagination. |
| POST | /v1/batches/{batch_id}/cancel | Stop processing the remaining items of a batch. |
These endpoints use the same authentication as every other call. A job or batch is only visible to the application that created it; any other key receives 404 job_not_found or 404 batch_not_found.
Reading batch results
Results can be read while the batch is still processing: each page contains the items finished so far. Pass limit (1–500, default 100) and, for every page after the first, the next_cursor from the previous page. next_cursor is null on the last page of a terminal batch.
| Query parameter | Type | Description |
|---|---|---|
| limit | integer | Items per page, 1–500. Default 100. |
| cursor | string | Opaque cursor from the previous page’s next_cursor. Omit for the first page. |
{
"object": "list",
"data": [
{
"reference": "cust-0001",
"status": "succeeded",
"result": {
"id": "kyc_8d2f6a1c93",
"object": "kyc_validation",
"livemode": true,
"reference": "cust-0001",
"status": "completed",
"result": "verified",
"certified_account": true,
"identifier": {
"type": "rdcpass_id",
"value": "COD-2103-0214-4937"
},
"match": {
"full_name": "not_provided",
"date_of_birth": "not_provided",
"age": "not_provided",
"score": null
},
"account": {
"rdcpass_id": "COD-2103-0214-4937",
"status": "active",
"certified": true,
"created_at": "2025-03-14T09:22:41Z",
"level_of_assurance": "LOA3"
},
"kyc": {
"basic": {
"full_name": "Kabeya Mwamba Tshisekedi",
"first_name": "Kabeya",
"last_name": "Tshisekedi",
"date_of_birth": "1988-04-12",
"age": 38,
"gender": "male",
"nationality": "COD"
},
"phone_numbers": [
{
"number": "+243812345678",
"operator": "Vodacom",
"is_primary": true,
"verified": true
}
]
},
"scopes_applied": [
"kyc.basic",
"kyc.phone_numbers"
],
"scopes_withheld": [],
"purpose": "customer_onboarding",
"created_at": "2026-09-26T10:15:04Z"
}
},
{
"reference": "cust-0002",
"status": "succeeded",
"result": {
"id": "kyc_3f7c0b92e5",
"object": "kyc_validation",
"livemode": true,
"reference": "cust-0002",
"status": "completed",
"result": "not_found",
"certified_account": false,
"identifier": {
"type": "ceni",
"value": "1234567890123"
},
"match": {
"full_name": "not_provided",
"date_of_birth": "not_provided",
"age": "not_provided",
"score": null
},
"scopes_applied": [],
"scopes_withheld": [],
"purpose": "customer_onboarding",
"created_at": "2026-09-26T10:15:04Z"
}
},
{
"reference": "cust-0007",
"status": "failed",
"error": {
"code": "invalid_identifier",
"message": "identifier.value \"OB12\" is not a valid passport number."
}
}
],
"next_cursor": "cur_Y3VzdC0wMDA3"
}Per-item errors
A failed item never fails the whole batch. It appears in the results with status: "failed" and an error object using the standard codes from Errors. The most common per-item codes:
| Code | Typical cause | What to do |
|---|---|---|
| invalid_identifier | The identifier value is malformed for its type. | Correct the value and resubmit the item. |
| unsupported_identifier_type | The identifier type is not accepted by this service. | Use a supported type, preferably rdcpass_id. |
| scope_not_granted | The item requests a scope your application does not hold. | Remove the scope or add it to the application. |
| purpose_not_approved | The item’s purpose is not approved for your application. | Use an approved purpose. |
| consent_required | Credit Scoring item without a valid citizen consent. | Obtain a fresh consent, then resubmit. |
| image_quality_insufficient | Face Recognition image below 480×640 or unusable. | Capture a new image. |
| no_face_detected / multiple_faces_detected | Face Recognition image with zero or several faces. | Capture an image with exactly one face. |
| liveness_failed | Passive liveness check did not pass. | Recapture the face in person. |
| batch_cancelled | The batch was cancelled before this item was processed. | Resubmit the item in a new batch if still needed. |
not_found is a result, not an error
An identity that does not exist in RDCPASS is a successful lookup: the item hasstatus: "succeeded" and its result.result is not_found. Only items for which RDCPASS could not perform the lookup are failed. Failed items are not billed.Errors on the request itself
| HTTP | Code | When |
|---|---|---|
| 413 | batch_too_large | More than 50 items without Prefer: respond-async, or more than 10,000 items. |
| 404 | job_not_found | Unknown job ID, another application’s job, or a job past its retention period. |
| 404 | batch_not_found | Unknown batch ID, another application’s batch, or a batch past its retention period. |
| 403 | service_not_enabled | The service is not enabled on the calling application. |
| 402 | insufficient_balance / credit_limit_reached | Prepaid wallet empty or postpaid credit limit reached — see Billing. |
| 429 | rate_limited / quota_exceeded | Too many requests, or the application’s quota is exhausted. |
Cancelling a batch
POST /v1/batches/{batch_id}/cancel stops RDCPASS from starting any further items and returns the updated batch object. Items already processed keep their results and are billed; every item not yet started is reported in the results as failed with the code batch_cancelled and is not billed. The batch then finishes as completed_with_errors (or failed if no item had been processed). Cancelling a batch that is already terminal has no effect and returns it unchanged.
curl https://api.rdcpass.cd/v1/batches/bat_7c2e91f04a/cancel \
-X POST \
-H "X-RDCPASS-Key-Id: key_live_8f2a1c0e9b" \
-H "X-RDCPASS-Timestamp: 1790417700" \
-H "X-RDCPASS-Nonce: 6f1c9b2e-4a3d-4e11-9c7a-2d8e5f1b0a44" \
-H "X-RDCPASS-Signature: 3f7a9c...e21d"Webhooks or polling
Webhooks are the recommended way to learn that asynchronous work has finished: they arrive within seconds, cost nothing, and do not consume your rate limit. Configure the webhook URL on your application, then subscribe to these events:
| Event | Sent when |
|---|---|
| job.completed | An asynchronous single request produced its result. data is the completed job object, with result embedded. |
| job.failed | An asynchronous single request failed. data is the job object with its error. |
| <service>.completed | Also sent for every completed asynchronous single request, e.g. kyc_validation.completed. data is the same completed job object, with result embedded. |
| batch.completed | Every item of a batch succeeded. data is the batch object. |
| batch.completed_with_errors | A batch finished with at least one failed item. data is the batch object. |
Every delivery uses the standard envelope { id, type, created_at, data }. Webhook payloads carry the job or batch object, not the full list of batch results: fetch those from results_url. Verify every delivery’s signature and deduplicate on the event id as described in Webhooks.
Polling fallback
If you cannot expose a webhook endpoint, poll the job or batch with exponential backoff: wait 2 seconds before the first poll, double the delay after each non-terminal response up to a ceiling of 60 seconds, add up to 20 % random jitter, and stop at a terminal status. For large batches, poll the batch object — never the results pages — until it is terminal. Polls count toward your rate limit.
// Polling fallback when you can't receive webhooks: exponential backoff
// with jitter, capped at 60 s, stopping at a terminal status.
const TERMINAL = new Set(['completed', 'completed_with_errors', 'failed'])
export async function waitForBatch(batchId, headers) {
let delay = 2000
for (;;) {
const res = await fetch(`https://api.rdcpass.cd/v1/batches/${batchId}`, { headers })
const batch = await res.json()
if (TERMINAL.has(batch.status)) return batch
const jitter = Math.random() * 0.2 * delay
await new Promise((r) => setTimeout(r, delay + jitter))
delay = Math.min(delay * 2, 60000)
}
}End-to-end: an asynchronous KYC refresh
A microfinance institution in Lubumbashi re-verifies its 2,500 borrowers every quarter. The complete flow:
1. Submit the batch
Send every borrower as an item — their RDCPASS ID as the identifier, your customer number as reference — with Prefer: respond-async and an Idempotency-Key stored alongside your job record:
# Plaintext body shown — encrypt it per /docs/authentication before sending.
# customers.json holds { "purpose": "...", "items": [ ... up to 10,000 items ... ] }
curl https://api.rdcpass.cd/v1/kyc/validations/batches \
-X POST \
-H "X-RDCPASS-Key-Id: key_live_8f2a1c0e9b" \
-H "X-RDCPASS-Timestamp: 1790417700" \
-H "X-RDCPASS-Nonce: 6f1c9b2e-4a3d-4e11-9c7a-2d8e5f1b0a44" \
-H "X-RDCPASS-Signature: 3f7a9c...e21d" \
-H "Idempotency-Key: 5d1a8c3e-2b7f-4e90-8a64-3c9e1f0b7d25" \
-H "Prefer: respond-async" \
-H "Content-Type: application/json" \
-d @customers.json{
"id": "bat_7c2e91f04a",
"object": "batch",
"service": "kyc_validation",
"status": "pending",
"total_items": 2500,
"processed_items": 0,
"succeeded_items": 0,
"failed_items": 0,
"created_at": "2026-09-26T10:15:00Z",
"completed_at": null,
"results_url": "/v1/batches/bat_7c2e91f04a/results"
}2. Receive the webhook (or poll)
While the batch runs you can follow its progress with GET /v1/batches/{batch_id}. When it finishes, RDCPASS delivers batch.completed or batch.completed_with_errors:
{
"id": "bat_7c2e91f04a",
"object": "batch",
"service": "kyc_validation",
"status": "processing",
"total_items": 2500,
"processed_items": 1180,
"succeeded_items": 1170,
"failed_items": 10,
"created_at": "2026-09-26T10:15:00Z",
"completed_at": null,
"results_url": "/v1/batches/bat_7c2e91f04a/results"
}{
"id": "evt_5a0c7e2f9b1d4386",
"object": "event",
"type": "batch.completed_with_errors",
"livemode": true,
"created_at": "2026-09-26T10:21:47Z",
"data": {
"object": {
"id": "bat_7c2e91f04a",
"object": "batch",
"service": "kyc_validation",
"status": "completed_with_errors",
"total_items": 2500,
"processed_items": 2500,
"succeeded_items": 2476,
"failed_items": 24,
"created_at": "2026-09-26T10:15:00Z",
"completed_at": "2026-09-26T10:21:46Z",
"results_url": "/v1/batches/bat_7c2e91f04a/results"
}
}
}3. Page through the results
Walk results_url until next_cursor is null, updating each borrower’s record by reference:
curl "https://api.rdcpass.cd/v1/batches/bat_7c2e91f04a/results?limit=500" \
-H "X-RDCPASS-Key-Id: key_live_8f2a1c0e9b" \
-H "X-RDCPASS-Timestamp: 1790417700" \
-H "X-RDCPASS-Nonce: 6f1c9b2e-4a3d-4e11-9c7a-2d8e5f1b0a44" \
-H "X-RDCPASS-Signature: 3f7a9c...e21d"
# Next page: pass the next_cursor from the previous response.
curl "https://api.rdcpass.cd/v1/batches/bat_7c2e91f04a/results?limit=500&cursor=cur_Y3VzdC0wMDA3" \
-H "X-RDCPASS-Key-Id: key_live_8f2a1c0e9b" \
-H "X-RDCPASS-Timestamp: 1790417700" \
-H "X-RDCPASS-Nonce: 6f1c9b2e-4a3d-4e11-9c7a-2d8e5f1b0a44" \
-H "X-RDCPASS-Signature: 3f7a9c...e21d"4. Handle failed items
Correct what you can — typically malformed identifiers — and submit only those items in a new batch with a new Idempotency-Key. Items that succeeded with not_found or not_certified are business outcomes for your compliance team, not candidates for retry.
The same flow for a single request
For one subject, send the single endpoint with Prefer: respond-async, keep the job.id, and wait for job.completed (or poll GET /v1/jobs/{job_id}), exactly as in the job example above.
Billing
Every batch item is billed at its service’s discounted batch rate — lower than the single-call price — plus the same scope surcharges as a single request. Asynchronous requests cost the same as synchronous ones, failed items are not billed, and not_found items are billed at the batch rate. See Billing.