Overview
Many RDCPASS operations finish after your HTTP call returns: asynchronous requests (Prefer: respond-async), batches of up to 10,000 items, fraud cases investigated on RDCPASS's own timeline, ongoing AML monitoring, production reviews and prepaid-wallet balances. Register a webhook endpoint and RDCPASS notifies you the moment something happens, instead of you polling for it. See Single, batch & async for how asynchronous and batch requests work.
Registering an endpoint
Register one HTTPS endpoint URL per application from the console (Owners, Administrators and Developers can do this). Every event for that application is delivered to it, in sandbox and production separately — use the livemode field to tell them apart. Subscribe to all events, or select only the types you handle.
Event catalogue
| Event | Fires when | data.object |
|---|---|---|
| kyc_validation.completed | An asynchronous single KYC Validation finishes. | job |
| face_recognition.completed | An asynchronous Face Recognition finishes (match or no_match). | job |
| kyb_verification.completed | An asynchronous KYB Verification finishes. | job |
| aml_screening.completed | An asynchronous AML & CTF Screening finishes. | job |
| aml_screening.alert | Ongoing monitoring detects a new sanctions, PEP, adverse-media or watchlist hit for a monitored subject. | aml_screening |
| credit_score.completed | An asynchronous Credit Scoring request finishes. | job |
| age_verification.completed | An asynchronous Age Verification finishes. | job |
| fraud_report.created | A fraud report is received and assigned a case number. | fraud_report |
| fraud_report.status_changed | A fraud case moves between received, under_review, confirmed, dismissed and escalated_to_authorities. | fraud_report |
| job.completed | Any asynchronous single request reaches completed — the job embeds the result. | job |
| job.failed | An asynchronous single request fails; the job carries an error object. | job |
| batch.completed | Every item of a batch has succeeded. | batch |
| batch.completed_with_errors | A batch has finished and at least one item failed. | batch |
| application.production_status_changed | Your application’s production review changes status (submitted, in_review, changes_requested, approved, rejected). | application |
| billing.balance_low | Your prepaid wallet falls below the alert threshold set by your Financial officer. | wallet |
| billing.invoice_issued | A monthly postpaid invoice is issued. | invoice |
usurpation_report.status_changed event is still delivered for reports filed through the /v1/fraud/usurpation-reports alias. New integrations should use fraud_report.status_changed.Asynchronous requests and batches
For an asynchronous single request, RDCPASS sends two events when it finishes: the service event (for example kyc_validation.completed, whose data.object is the completed job — its result is exactly the object the synchronous call returns) and the generic job.completed or job.failed. Handle whichever suits your code — most integrations listen to the service events and ignore the job events.
For a batch, RDCPASS sends one batch.completed or batch.completed_with_errors event when the last item is processed. Its data.object is the batch summary, not the results: page through GET /v1/batches/{batch_id}/results (up to 500 items per page, using next_cursor) to read every item, and match items to your records with the reference you supplied. Results stay available for 30 days.
- Store the job or batch id returned in the 202 response alongside your own reference.
- Treat the webhook as the trigger, then read the authoritative state with GET /v1/jobs/{job_id} or GET /v1/batches/{batch_id} if you need to be certain.
- If a webhook never arrives (endpoint down beyond the retry schedule), the same data remains available by polling — webhooks are a notification, never the only copy.
Event envelope
Every event shares the same envelope. data.object holds the full resource the event is about, in exactly the shape the API returns for it:
{
"id": "evt_7f1e2a9c4b3d8f60",
"object": "event",
"type": "fraud_report.status_changed",
"livemode": true,
"created_at": "2026-09-26T14:32:00Z",
"data": {
"object": { "id": "frd_2c9e4a71b0", "object": "fraud_report", "...": "..." },
"previous_attributes": { "status": "under_review" }
}
}| Field | Type | Description |
|---|---|---|
| id | string | Unique identifier for this event. Use it to deduplicate — see Idempotency below. |
| object | string | Always "event". |
| type | string enum | The event type — one of the catalogue values above. |
| livemode | boolean | true for production, false for sandbox. |
| created_at | string | RFC 3339 timestamp the event occurred at. |
| data.object | object | The resource the event is about. Its object field names its type. |
| data.previous_attributes | object | Present on *.status_changed events only: the fields that changed, with their previous values. |
Sample payloads
kyc_validation.completed
{
"id": "evt_1a4c8e2f7b9d3605",
"object": "event",
"type": "kyc_validation.completed",
"livemode": true,
"created_at": "2026-09-26T10:15:04Z",
"data": {
"object": {
"id": "job_4b1d9e7a2c",
"object": "job",
"service": "kyc_validation",
"status": "completed",
"created_at": "2026-09-26T10:15:00Z",
"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": "match",
"date_of_birth": "match",
"age": "match",
"score": 0.98
},
"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:02Z"
}
}
}
}batch.completed
{
"id": "evt_5d0b3f8a1c6e2749",
"object": "event",
"type": "batch.completed",
"livemode": true,
"created_at": "2026-09-26T10:41:18Z",
"data": {
"object": {
"id": "bat_7c2e91f04a",
"object": "batch",
"service": "kyc_validation",
"status": "completed",
"total_items": 2500,
"processed_items": 2500,
"succeeded_items": 2500,
"failed_items": 0,
"created_at": "2026-09-26T10:15:00Z",
"completed_at": "2026-09-26T10:41:17Z",
"results_url": "/v1/batches/bat_7c2e91f04a/results"
}
}
}fraud_report.status_changed
{
"id": "evt_9e2d7c4a0b1f5836",
"object": "event",
"type": "fraud_report.status_changed",
"livemode": true,
"created_at": "2026-09-28T08:05:43Z",
"data": {
"object": {
"id": "frd_2c9e4a71b0",
"object": "fraud_report",
"case_number": "RDC-FRD-2026-004812",
"type": "identity_usurpation",
"status": "confirmed",
"reporter_reference": "INC-2026-0917",
"created_at": "2026-09-26T14:02:11Z",
"updated_at": "2026-09-28T08:05:42Z"
},
"previous_attributes": { "status": "under_review" }
}
}Delivery and headers
Every delivery is an HTTP POST with the event as the JSON body, plus three headers:
POST /your-webhook-endpoint HTTP/1.1
Host: yourapp.example.com
Content-Type: application/json
X-RDCPASS-Webhook-Id: evt_7f1e2a9c4b3d8f60
X-RDCPASS-Webhook-Timestamp: 1790432520
X-RDCPASS-Webhook-Signature: 9c3f7a1e2b6d4c85...
{ "id": "evt_7f1e2a9c4b3d8f60", "object": "event", "type": "fraud_report.status_changed", ... }| Field | Description |
|---|---|
| X-RDCPASS-Webhook-Id | Same value as the body’s id — read this without parsing the body, for logging or fast deduplication. |
| X-RDCPASS-Webhook-Timestamp | Unix seconds this delivery attempt was sent. Reject deliveries with a timestamp too far from your own clock, the same way you’d guard against a replayed request. |
| X-RDCPASS-Webhook-Signature | Hex-encoded HMAC-SHA256 — see Verifying the signature below. |
Verifying the signature
Compute HMAC-SHA256 over {timestamp}.{raw request body}, using the same Payload Encryption Key your application already has for request signing — hex-encode the result and compare it to X-RDCPASS-Webhook-Signature with a constant-time comparison, never ===.
const crypto = require('node:crypto')
function verifyWebhookSignature(payloadSecretKey, timestamp, rawBody, signature) {
const expected = crypto
.createHmac('sha256', payloadSecretKey)
.update(`${timestamp}.${rawBody}`)
.digest('hex')
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
}The same standard HMAC primitives used for request signing in every language — see Authentication — apply here identically; only the canonical string differs.
Retry behavior
Respond with any 2xx status within 5 seconds to acknowledge an event. Anything else — a non-2xx status, a timeout, or an unreachable endpoint — is treated as a failed delivery attempt and retried on this schedule:
| Attempt | Delay since previous attempt |
|---|---|
| 1 | Immediately |
| 2 | 1 minute |
| 3 | 10 minutes |
| 4 | 1 hour |
| 5 | 6 hours (final) |
Idempotency and ordering
Acknowledge an event as soon as you’ve durably queued it for processing, before doing any slow work — a delivery that times out on your end is indistinguishable from one that never arrived, so RDCPASS retries it. Because of this, the same id can arrive more than once. Store processed event IDs and skip any you’ve already handled.
Events are not guaranteed to arrive in the order they occurred — a retried fraud_report.status_changed can arrive after a later one. Compare created_at (or the resource’s updated_at) with what you have stored before applying a change.
Rate limits and billing
Webhook deliveries are neither metered against your application’s quota nor billed — see Rate limits & quotas for the limits that apply to the calls that create the events in the first place.