RecovaPro Webhooks
E-mandate operations on RecovaPRO are asynchronous. A mandate is created instantly, but the payer's bank decides whether to honour it (up to 24 hours for micro-deposit authorization, and up to 48 hours for e-signature), and its status keeps changing over the lifetime of the direct debit arrangement. Rather than polling Verify mandate, CreditChek submits a POST request to the webhook URL configured for your RecovaPRO profile each time the state of a mandate changes, so your platform can trigger the matching workflow — activate a loan, notify a payer, close out a repayment schedule.
Your endpoint should respond to webhooks as quickly as possible. To acknowledge receipt of a webhook, your endpoint must return a 2xx HTTP status code. This status code should only indicate receipt of the message, not an acknowledgement that it was successfully processed by your system. Any other information returned in the response headers or response body is ignored.
RecovaPRO events are only dispatched once Webhook settings for RecovaPRO events have been configured on your merchant profile in the CreditChek SaaS Portal. See Integration options.
Security​
All webhook requests are sent with an x-auth-signature header for verification. It should match the secret key pair of the app secret key you used when creating the mandate. See Webhooks for the shared verification and retry behaviour that applies across all CreditChek services.
Webhook structure​
Every RecovaPRO webhook follows the same envelope as the rest of the CreditChek platform:
| Field | Type | Description |
|---|---|---|
event | string | The event that determines the structure of data. |
data | object | The mandate payload for the event. |
Mandate lifecycle​
The events below map onto the lifecycle of a single e-mandate. Use mandateId (returned as _id on mandate_created_event) or your own reference to correlate them with the mandate on your side:
mandate_created_event → status: pending (awaiting the payer's bank)
↓
mandate_approved_event → active: true (mandate placed on the payer's bank record; collection can begin)
↓
mandate_paused_event → status: paused (auto-collection suspended, mandate still valid)
↓
mandate_canceled_event → status: deactivated (terminated by the merchant, final)
mandate_expire_event → status: closed (endDate reached, final)
Webhooks are not guaranteed to arrive in order, and a mandate can move to paused and back any number of times before it is closed or deactivated. Treat each event as a state update keyed on mandateId, and ignore an event that describes a state you have already moved past.
Supported Webhook Events​
RecovaPRO dispatches three tiers of real-time webhooks via our System: Mandate-Level Events (tracking the overarching payment contract), Consent-Level Events (tracking each individual enrolled bank account), and Collection-Level Events (tracking each repayment attempt, whether scheduled by the platform or triggered manually).
Mandate-Level Events​
| Event Name | Trigger Stage | Description |
|---|---|---|
recova.mandate.initialize | Mandate Creation | Fires immediately when a mandate contract is initiated via POST /consent/create. |
recova.mandate.approved | Mandate Activated | Fires when the primary account authorization is confirmed by the bank, putting the mandate into active status. |
recova.mandate.paused | Collection Paused | Fires when scheduled collections are suspended via /pause-mandate. |
recova.mandate.resumed | Collection Resumed | Fires when collections are reinstated via /resume-mandate. |
recova.mandate.cancelled | Mandate Cancelled | Fires when the mandate contract is permanently deactivated via /cancel-mandate. |
recova.mandate.expire | Mandate Expired | Fires when the mandate reaches its endDate and transitions to closed. |
Consent-Level Events (Per Linked Bank Account)​
| Event Name | Trigger Stage | Description |
|---|---|---|
recova.consent.created | Account Enrolled | Fires each time a bank account is attached via /micro-deposit/create/mandate or /gsm. |
recova.consent.approved | Account Verified | Fires when the payer completes the ₦50 transfer from that specific account and the bank verifies authorization. |
recova.consent.rejected | Account Declined | Fires if the payer's bank rejects authorization for that specific bank account (e.g. invalid account or bank decline). |
recova.consent.expired | Authorization Expired | Fires if the ₦50 micro-deposit authorization window elapses without completion. |
recova.consent.cancelled | Account Revoked | Fires if authorization for that specific bank account is cancelled or revoked. |
Collection-Level Events (Per Repayment — Scheduled or Auto)​
| Event Name | Trigger Stage | Description |
|---|---|---|
recova.collection.notification | Advance Notice | Fires ahead of a scheduled repayment to notify the payer of the upcoming debit, normally 24 hours before it is due. |
recova.collection.success | Collection Successful | Fires when a scheduled or manual repayment is debited successfully. More than one event can be sent for a single repayment as partial collections complete. |
recova.collection.failed | Collection Failed | Fires when a scheduled or manual repayment attempt is declined by the bank, e.g. Insufficient funds. |
Mandate-Level Webhook Events​
- recova.mandate.initialize
- recova.mandate.approved
- recova.mandate.paused
- recova.mandate.resumed
- recova.mandate.cancelled
- recova.mandate.expire
Dispatched immediately upon mandate initialization.
Sent as soon as the e-mandate request is created and submitted for authorization. The mandate arrives with status: "pending" — no debit can be attempted against it yet. The _id in this payload is the mandateId referenced by every subsequent event and by the mandate API endpoints.
{
"event": "recova.mandate.initialize",
"data": {
"collectedAmount": 0,
"collectionMode": "auto",
"debitType": "recurring",
"mandateType": "variable",
"authorizationType": "micro-deposit",
"status": "pending",
"corporateAccount": false,
"rcNumber": null,
"businessName": null,
"businessEmail": null,
"businessPhone": null,
"balanceEnquiry": false,
"creditInsurance": false,
"consents": [],
"acceptedTerms": true,
"isDemo": false,
"_id": "77192739b3cf770000000000",
"createdAt": "2026-09-22T12:00:00.000Z",
"updatedAt": "2026-09-22T12:00:00.000Z",
"__v": 0
}
}
Payload fields
| Field | Type | Description |
|---|---|---|
_id | string | The mandate ID. This is the mandateId referenced in all other events and API calls. |
reference | string | Unique merchant transaction reference for the mandate arrangement. |
businessId | string | Your CreditChek business ID. |
appId | string | The app identifier the mandate was created under. |
borrowerId | string | null | The borrower identifier where the payer exists on your CreditChek customer records. |
productId | string | The product or loan package ID associated with the mandate. |
bvn | string | 11-digit Bank Verification Number of the payer. |
startDate | string | Start date (YYYY-MM-DD) when collections can begin. |
endDate | string | Expiry date (YYYY-MM-DD) of the direct debit arrangement. |
frequency | string | Collection frequency for recurring debits (daily, weekly, or monthly). |
totalAmountDue | number | Total amount collectable over the lifetime of the mandate (in Naira). |
outstandingBalance | number | Remaining balance to be collected (in Naira). |
collectedAmount | number | Total amount successfully debited to date (in Naira). |
collectionMode | string | Collection mode (auto for automated scheduler, manual for merchant-triggered debits). |
debitType | string | Debit cadence (one-time or recurring). |
mandateType | string | Mandate installment structure (fixed or variable). |
authorizationType | string | Payer authorization channel (micro-deposit or e-signature). |
status | string | Initial mandate status (pending awaiting account enrollment and bank authorization). |
corporateAccount | boolean | true if the mandate is placed against a corporate bank account. |
rcNumber | string | null | Corporate CAC registration number (when corporateAccount is true). |
businessName | string | null | Registered corporate entity name (when corporateAccount is true). |
businessEmail | string | null | Corporate billing email (when corporateAccount is true). |
businessPhone | string | null | Corporate contact telephone number (when corporateAccount is true). |
balanceEnquiry | boolean | Indicates whether automated balance checks before debits are enabled. |
creditInsurance | boolean | Indicates whether the ERM credit-risk protection add-on is attached. |
acceptedTerms | boolean | Confirmation that the payer accepted terms and conditions. |
isDemo | boolean | true if initiated using sandbox test credentials. Always verify false in production. |
createdAt | string | ISO 8601 timestamp of mandate creation. |
updatedAt | string | ISO 8601 timestamp of last record update. |
Sent when the payer's bank honours the request and the e-mandate is placed on the payer's bank record. This is the point at which the arrangement is collectable — automated debits begin on the schedule derived from startDate, endDate, and frequency, and manual collections can be triggered from your platform.
This payload carries the bank account the mandate was placed on, which is not known at creation time. Where the same payer has mandates on several accounts (see Multi-bank mandates), you receive one approval event per account.
The mandate transitions to active, and collections can now begin according to credit disbursement auto-reporting to the Credit Bureau (via Spectrum) and then schedule or manual debit requests.
{
"event": "recova.mandate.approved",
"data": {
"businessId": "730c8be89121212121212121",
"appId": "730c8be89131cd1111111111",
"mandateId": "77192739b3cf770000000000",
"bankCode": "044",
"bankName": "ACCESS BANK PLC",
"bankLogo": "https://res.cloudinary.com/creditchek-africa/image/upload/v1650378721/banks/access-bank.png",
"accountNumber": "1234567890",
"accountName": "John Smith",
"reference": "563315af-f0a4-420f-8e5e-df9b1c28973b",
"consentType": "emandate",
"active": true
}
}
Payload fields
| Field | Type | Description |
|---|---|---|
mandateId | string | The mandate that was approved (corresponds to _id from recova.mandate.initialize). |
reference | string | The same merchant reference provided at mandate creation. |
businessId | string | Your CreditChek business ID. |
appId | string | The app identifier the mandate was created under. |
bankCode | string | CBN 3-digit code of the bank holding the debited account. |
bankName | string | Name of the financial institution holding the debited account. |
bankLogo | string | Direct URL of the bank's brand logo for UI presentation. |
accountNumber | string | The payer's bank account number placed under the mandate. |
accountName | string | Account title as registered on bank and NIBSS records. |
consentType | string | The direct debit authorization instrument (emandate). |
active | boolean | true indicating the mandate is placed on the payer's bank record and collection can begin. |
bankName is spelled exactly as shown in the payload. Read the bank name from that key rather than from bankName.
Sent when auto-collection is suspended on an active mandate, either from the Merchant Web Portal or through Pause mandate. The mandate remains valid on the payer's bank record and no scheduled debit is attempted while it is paused. Use Reinstate mandate to resume collection.
Dispatched when scheduled collections on an active mandate are suspended, either via the merchant web portal or through POST /v1/recova/consent/pause-mandate. The mandate remains valid on the payer's bank record, but scheduled debits are halted until reinstated.
{
"event": "recova.mandate.paused",
"data": {
"mandateId": "77192739b3cf770000000000",
"businessId": "730c8be89121212121212121",
"status": "paused"
}
}
Payload fields
| Field | Type | Description |
|---|---|---|
mandateId | string | Unique identifier of the mandate whose automated collection was paused. |
businessId | string | Your CreditChek business ID. |
status | string | Current mandate status (paused). |
Dispatched when collections are reinstated on a paused mandate via POST /v1/recova/consent/resume-mandate. Automated collections immediately resume on their regular cycle.
{
"event": "recova.mandate.resumed",
"data": {
"mandateId": "77192739b3cf770000000000",
"businessId": "730c8be89121212121212121",
"status": "active"
}
}
Payload fields
| Field | Type | Description |
|---|---|---|
mandateId | string | Unique identifier of the mandate reinstated back into active collection. |
businessId | string | Your CreditChek business ID. |
status | string | Current mandate status (active). |
Sent when a mandate is terminated before its endDate through Cancel mandate or from the Merchant Web Portal. The mandate is deactivated on the payer's bank record and no further collection — automated or manual — is possible against it. This is a final state; a new mandate must be created to resume collection from the payer.
Dispatched when a mandate is permanently deactivated by the merchant before its endDate via POST /v1/recova/consent/cancel-mandate. The mandate is terminated on the payer's bank record, and no further collection (automated or manual) can occur. This is a final state.
{
"event": "recova.mandate.cancelled",
"data": {
"mandateId": "77192739b3cf770000000000",
"businessId": "730c8be89121212121212121",
"status": "deactivated"
}
}
Payload fields
| Field | Type | Description |
|---|---|---|
mandateId | string | Unique identifier of the mandate permanently terminated by the merchant. |
businessId | string | Your CreditChek business ID. |
status | string | Final mandate status (deactivated). |
Sent when a mandate reaches the endDate of the direct debit arrangement and is closed. No further debits are attempted against it. Where an outstanding balance remains at this point, recovery has to continue under a new mandate, or through the dispute and credit-risk (ERM) options on your merchant profile.
{
"event": "recova.mandate.expire",
"data": {
"mandateId": "77192739b3cf770000000000",
"businessId": "730c8be89121212121212121",
"reference": "563315af-f0a4-420f-8e5e-df9b1c28973b",
"endDate": "2026-12-31",
"status": "closed"
}
}
Payload fields
| Field | Type | Description |
|---|---|---|
mandateId | string | Unique identifier of the mandate that reached its scheduled endDate. |
businessId | string | Your CreditChek business ID. |
reference | string | The merchant transaction reference provided at creation. |
endDate | string | The direct debit arrangement end date (YYYY-MM-DD) that was reached. |
status | string | Final mandate status (closed). |
Consent-Level Webhook Events​
- recova.consent.created
- recova.consent.approved
- recova.consent.rejected
- recova.consent.expired
- recova.consent.cancelled
Dispatched each time a bank account is attached to a mandate (single account or each individual account within a Multi-Bank GSM enrollment).
{
"event": "recova.consent.created",
"data": {
"_id": "77192739b3cf770000000001",
"businessId": "730c8be89121212121212121",
"appId": "730c8be89131cd1111111111",
"mandateId": "77192739b3cf770000000000",
"bankCode": "044",
"bankName": "ACCESS BANK PLC",
"accountNumber": "1234567890",
"accountName": "John Smith",
"reference": "563315af-f0a4-420f-8e5e-df9b1c28973b",
"email": "[email protected]",
"primary": true,
"createdAt": "2026-09-22T12:05:00.000Z"
}
}
Payload fields
| Field | Type | Description |
|---|---|---|
_id | string | Unique identifier for this consent record (consentId). |
businessId | string | Your CreditChek business ID. |
appId | string | The app identifier the mandate was created under. |
mandateId | string | The overarching mandate ID to which this bank account is attached. |
bankCode | string | CBN 3-digit code of the bank. |
bankName | string | Name of the bank where this account resides. |
accountNumber | string | The enrolled 10-digit NUBAN bank account number. |
accountName | string | Account holder name verified by the bank rail. |
reference | string | The merchant reference associated with the mandate. |
email | string | Payer contact email for notifications. |
primary | boolean | true if designated as primary collection account; false for secondary GSM backup accounts. |
createdAt | string | ISO 8601 timestamp of consent enrollment. |
Dispatched when the payer transfers ₦50 from this specific account and the bank rail confirms account authorization.
{
"event": "recova.consent.approved",
"data": {
"consentId": "77192739b3cf770000000001",
"mandateId": "77192739b3cf770000000000",
"accountNumber": "1234567890",
"status": "approved"
}
}
Payload fields
| Field | Type | Description |
|---|---|---|
consentId | string | Unique identifier of the consent record verified and activated by the bank rail. |
mandateId | string | The overarching mandate ID. |
accountNumber | string | The bank account number that was verified. |
status | string | Status of the bank account consent (approved). |
Dispatched if the payer's bank rejects or declines authorization for this specific account (e.g., account type ineligible for direct debit, signature mismatch, or explicit bank decline).
{
"event": "recova.consent.rejected",
"data": {
"consentId": "77192739b3cf770000000001",
"mandateId": "77192739b3cf770000000000",
"accountNumber": "1234567890",
"status": "rejected"
}
}
Payload fields
| Field | Type | Description |
|---|---|---|
consentId | string | Unique identifier of the consent record declined by the payer's bank. |
mandateId | string | The overarching mandate ID. |
accountNumber | string | The bank account number that failed authorization. |
status | string | Status of the bank account consent (rejected). |
Dispatched if the payer fails to complete the ₦50 micro-deposit from this account within the allocated 1-hour authorization window.
{
"event": "recova.consent.expired",
"data": {
"consentId": "77192739b3cf770000000001",
"mandateId": "77192739b3cf770000000000",
"accountNumber": "1234567890",
"status": "expired"
}
}
Payload fields
| Field | Type | Description |
|---|---|---|
consentId | string | Unique identifier of the consent record whose micro-deposit authorization window elapsed. |
mandateId | string | The overarching mandate ID. |
accountNumber | string | The bank account number associated with the expired authorization. |
status | string | Status of the bank account consent (expired). |
Dispatched when an individual bank account consent is cancelled or revoked (either during multi-bank rearrangement or upon payer revocation).
{
"event": "recova.consent.cancelled",
"data": {
"consentId": "77192739b3cf770000000001",
"mandateId": "77192739b3cf770000000000",
"accountNumber": "1234567890",
"status": "cancelled"
}
}
Payload fields
| Field | Type | Description |
|---|---|---|
consentId | string | Unique identifier of the consent record that was cancelled or revoked. |
mandateId | string | The overarching mandate ID. |
accountNumber | string | The bank account number that was revoked. |
status | string | Status of the bank account consent (cancelled). |
Collection-Level Webhook Events​
- recova.collection.notification
- recova.collection.success
- recova.collection.failed
Sent to the payer ahead of a scheduled collection to notify them of an upcoming debit. Typically dispatched 24 hours before the collection is due.
{
"event": "recova.collection.notification",
"data": {
"name": "John Musa",
"email": "[email protected]",
"mandateId": "6287757348cc6c0013f4e0c2",
"totalAmount": 25000,
"dueDate": "2026-09-22",
"reference": "er090405"
}
}
Payload fields
| Field | Type | Description |
|---|---|---|
mandateId | string | The mandate the upcoming collection will be attempted against. |
reference | string | The mandate reference sent at creation. |
name | string | Name of the payer the notification is addressed to. |
email | string | Email address of the payer the notification is addressed to. |
totalAmount | number | Amount that will be debited when the collection runs. |
dueDate | string | Date (YYYY-MM-DD) the collection is scheduled for. |
Sent when a scheduled or manual collection is debited successfully. amountCollected carries the amount debited and status is success.
There's the possibility of multiple collection.success sent for the same repayment, as there could be multiple partial collection until the whole sum is paid off.
{
"event": "recova.collection.success",
"data": {
"businessId": "6a7499c3d1afde2C4c9b912b",
"consentId": "6aa419b2b1042e9730adc360",
"mandateId": "6aa418a9b1032a9830bdeb98",
"accountName": "BOLS MUSA DAVE",
"accountNumber": "2007318478",
"bankName": "First Bank",
"totalAmount": 200,
"providerMessage": "Payment successful",
"amountCollected": 200,
"amountTried": 0,
"status": "success",
"collectedOn": "2026-09-22T12:13:43.120Z",
"collectionType": "auto",
"instalmentReference": "c043e84b-464f-4522-baa9-f2db529af5ff",
"reference": "1790079202640",
"createdAt": "2026-09-22T12:13:43.121Z"
}
}
Payload fields
| Field | Type | Description |
|---|---|---|
mandateId | string | The mandate the collection was attempted against — the _id from recova.mandate.initialize. |
consentId | string | The consent (linked bank account) the collection was attempted against. |
reference | string | The mandate reference sent at creation. |
instalmentReference | string | Reference of the instalment this collection corresponds to. |
businessId | string | Your CreditChek business ID. |
accountName | string | Name on the bank account debited. |
accountNumber | string | The payer's account number that was debited. |
bankName | string | Name of the bank holding the debited account. |
totalAmount | number | Total amount scheduled to be collected. |
amountTried | number | Amount the collection attempt tried to debit. |
amountCollected | number | Amount actually debited from the account. |
status | string | success for a completed collection. |
providerMessage | string | Message returned by the bank for the collection. |
collectedOn | string | ISO 8601 timestamp for when the collection was executed. |
collectionType | string | auto for scheduler-driven collection, or manual where the debit was triggered from your platform. |
createdAt | string | ISO 8601 timestamp for when the collection record was created. |
Sent when a scheduled or manual collection is attempted and the bank declines the debit. providerMessage carries the bank's reason (for example Insufficient funds) and amountCollected is 0.
{
"event": "recova.collection.failed",
"data": {
"businessId": "66b53ea0e6b49a3b813f5deb",
"consentId": "6a5f79e0ba3d5d5efe98433c",
"mandateId": "6a5f777eba5d3d5efe487c89",
"accountName": "SANDRA ALABI MUSA",
"accountNumber": "2048065843",
"bankName": "Zenith Bank",
"totalAmount": 130000,
"providerMessage": "Insufficient funds",
"amountCollected": 0,
"amountTried": 65000,
"status": "failed",
"collectedOn": "2026-09-22T12:12:44.949Z",
"collectionType": "auto",
"instalmentReference": "b1bce933-7fca-47a5-a1a9-20b2df077a64",
"reference": "1790079158318",
"createdAt": "2026-09-22T12:12:44.950Z"
}
}
Payload fields
| Field | Type | Description |
|---|---|---|
mandateId | string | The mandate the collection was attempted against — the _id from recova.mandate.initialize. |
consentId | string | The consent (linked bank account) the collection was attempted against. |
reference | string | The mandate reference sent at creation. |
instalmentReference | string | Reference of the instalment this collection corresponds to. |
businessId | string | Your CreditChek business ID. |
accountName | string | Name on the bank account debited. |
accountNumber | string | The payer's account number that was debited. |
bankName | string | Name of the bank holding the debited account. |
totalAmount | number | Total amount scheduled to be collected. |
amountTried | number | Amount the collection attempt tried to debit. |
amountCollected | number | Amount actually debited. 0 when the collection failed. |
status | string | failed for a declined collection. |
providerMessage | string | Reason returned by the bank for the decline, e.g. Insufficient funds. |
collectedOn | string | ISO 8601 timestamp for when the collection was executed. |
collectionType | string | auto for scheduler-driven collection, or manual where the debit was triggered from your platform. |
createdAt | string | ISO 8601 timestamp for when the collection record was created. |
Handling Webhook Events​
Below is a complete Node.js Express implementation pattern showing how to securely verify signatures, immediately acknowledge receipt with a 200 status, and process lifecycle events idempotently.
const express = require("express");
const router = express.Router();
const secret = process.env.CREDITCHEK_SECRET_KEY;
function verifyWebhook(req, res, next) {
// Verify HMAC signature
if (req.headers["x-auth-signature"] !== secret) {
return res.status(401).json({
message: "Unauthorized request.",
});
}
next();
}
router.post("/recova/webhook", verifyWebhook, (req, res) => {
// 1. Acknowledge receipt immediately before downstream processing
res.sendStatus(200);
const { event, data } = req.body;
const mandateId = data.mandateId || data._id;
switch (event) {
case "recova.mandate.initialize":
// Mandate submitted for authorization — keep payer in pending state
break;
case "recova.mandate.approved":
// Mandate is active on data.accountNumber — activate loan or subscription
break;
case "recova.mandate.paused":
// Collection suspended — pause automated repayment schedule
break;
case "recova.mandate.resumed":
// Collection reinstated — resume active repayment schedule
break;
case "recova.mandate.cancelled":
case "recova.mandate.expire":
// Final states — close out schedule and cease collections
break;
case "recova.consent.approved":
// Secondary bank account authorized successfully
break;
case "recova.consent.rejected":
case "recova.consent.expired":
case "recova.consent.cancelled":
// Account-level authorization declined or revoked
break;
default:
console.log(`Unhandled webhook event: ${event}`);
}
});
module.exports = router;
[!TIP] Idempotency Webhook events may occasionally be redelivered due to network retries or manual resends from your merchant dashboard. Always key your event processor on
mandateId/consentIdand the target state so that reprocessing duplicate deliveries is safe and idempotent.
Simulating Mandate Approval in Test Mode​
To simulate bank approval for your sandbox mandate and test your webhook receiver:
POST /v1/recova/consent/business/test/approve-mandate
Content-Type: application/json
token: <YOUR_API_KEY>
{
"mandateId": "77192739b3cf770000000000"
}
This immediately triggers a mock recova.mandate.approved webhook to your configured webhook URL, allowing you to test your downstream loan disbursement workflows safely.
The same event may reach you more than once, for example when a failed delivery is re-sent from your dashboard audit logs. Key your handler on mandateId and the resulting state so that reprocessing an event is harmless.
The values in these payloads are illustrative. Use the field names and nesting as the integration contract, and treat the values as mandate-specific.
Contact [email protected] if an expected event does not reach your webhook URL.