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 events​
- mandate_created_event
- mandate_approved_event
- mandate_paused_event
- mandate_canceled_event
- mandate_expire_event
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": "mandate_created_event",
"data": {
"businessId": "730c8be89121212121212121",
"borrowerId": null,
"appId": "730c8be89131cd1111111111",
"reference": "563315af-f0a4-420f-8e5e-df9b1c28973b",
"productId": "768fc9999999999999999999",
"bvn": "22357999900",
"startDate": "2024-07-07",
"endDate": "2024-10-31",
"frequency": "monthly",
"totalAmountDue": 15000,
"collectionMode": "auto",
"rcNumber": null,
"corporateAccount": false,
"businessName": null,
"businessEmail": null,
"businessPhone": null,
"debitType": "recurring",
"mandateType": "variable",
"status": "pending",
"balanceEnquiry": false,
"creditInsurance": false,
"consents": [],
"acceptedTerms": true,
"isDemo": true,
"_id": "77192739b3cf770000000000",
"createdAt": "2024-10-23T16:41:29.085Z",
"updatedAt": "2024-10-23T16:41:29.085Z",
"__v": 0,
"outstandingBalance": 15000,
"collectedAmount": 0,
"authorizationType": "e-signature"
}
}
Payload fields
| Field | Type | Description |
|---|---|---|
_id | string | The mandate ID. This is the mandateId used in all other events and API calls. |
reference | string | Unique reference for the mandate arrangement. Use it to correlate the mandate with the record on your platform. |
businessId | string | Your CreditChek business ID. |
appId | string | The app the mandate was created under. |
borrowerId | string | null | The borrower the mandate belongs to, where the payer already exists on your CreditChek borrower record. |
productId | string | The product the e-mandate is associated with on your merchant profile. |
bvn | string | BVN of the payer the mandate is placed against. |
startDate | string | Date (YYYY-MM-DD) the direct debit arrangement becomes collectable. |
endDate | string | Date (YYYY-MM-DD) the arrangement lapses. Reaching this date triggers mandate_expire_event. |
frequency | string | Collection frequency for recurring debits — daily, weekly, or monthly. Not applicable to one-time debits. |
totalAmountDue | number | Total amount collectable over the lifetime of the mandate. |
outstandingBalance | number | Amount still to be recovered. Equal to totalAmountDue at creation. |
collectedAmount | number | Amount recovered so far. 0 at creation. |
collectionMode | string | auto for scheduler-driven collection, or manual where you trigger each debit yourself. |
debitType | string | one-time or recurring. |
mandateType | string | fixed or variable. See Categories of e-Mandates. |
authorizationType | string | How the payer authorized the mandate — e-signature or micro-deposit. |
status | string | pending at creation. |
corporateAccount | boolean | true where the payer is a corporate account. rcNumber, businessName, businessEmail, and businessPhone are then set. |
balanceEnquiry | boolean | Whether balance enquiry is enabled on the mandate. |
creditInsurance | boolean | Whether the ERM credit-risk add-on is attached to the mandate. |
acceptedTerms | boolean | Whether the payer accepted the direct debit terms. |
isDemo | boolean | true for mandates created with test credentials. Always confirm this is false before acting on an event in production. |
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.
{
"event": "mandate_approved_event",
"data": {
"businessId": "730c8be89121212121212121",
"appId": "730c8be89131cd1111111111",
"mandateId": "77192739b3cf770000000000",
"bankCode": "044",
"bankName": "Fidelity Bank",
"bankLogo": "https://res.cloudinary.com/creditchek-africa/image/upload/v1650378721/banks/fidelity-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 — the _id from mandate_created_event. |
reference | string | The same mandate reference sent at creation. |
businessId | string | Your CreditChek business ID. |
appId | string | The app the mandate was created under. |
bankCode | string | CBN code of the bank holding the debited account. |
bankName | string | Name of the bank holding the debited account. |
bankLogo | string | URL of the bank's logo, for display in your app. |
accountNumber | string | The payer's account number the mandate is placed on. |
accountName | string | Account name as held by the bank. |
consentType | string | The consent instrument placed on the account — emandate. |
active | boolean | true once the mandate is live on the payer's bank record. |
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.
{
"event": "mandate_paused_event",
"data": {
"mandateId": "66d1f2a3b4c5d6e7f8091011",
"businessId": "66aa00119922334455667788",
"status": "paused"
}
}
Payload fields
| Field | Type | Description |
|---|---|---|
mandateId | string | The mandate that was paused. |
businessId | string | Your CreditChek business ID. |
status | string | paused. |
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.
{
"event": "mandate_canceled_event",
"data": {
"mandateId": "66d1f2a3b4c5d6e7f8091011",
"businessId": "66aa00119922334455667788",
"status": "deactivated"
}
}
Payload fields
| Field | Type | Description |
|---|---|---|
mandateId | string | The mandate that was canceled. |
businessId | string | Your CreditChek business ID. |
status | string | 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": "mandate_expire_event",
"data": {
"mandateId": "66d1f2a3b4c5d6e7f8091011",
"businessId": "66aa00119922334455667788",
"reference": "3f29a7c4-8e21-4c3a-9b7e-1a2b3c4d5e6f",
"endDate": "2026-08-01",
"status": "closed"
}
}
Payload fields
| Field | Type | Description |
|---|---|---|
mandateId | string | The mandate that expired. |
businessId | string | Your CreditChek business ID. |
reference | string | The mandate reference sent at creation. |
endDate | string | The arrangement end date (YYYY-MM-DD) that was reached. |
status | string | closed. |
Mandate service events​
| Event | Mandate state | Description |
|---|---|---|
mandate_created_event | pending | The e-mandate has been created and submitted to the payer's bank for authorization. Not yet collectable. |
mandate_approved_event | active | The payer's bank honoured the request and placed the mandate on the account. Collection can begin. |
mandate_paused_event | paused | Auto-collection has been suspended on an active mandate. Reversible with a reinstate. |
mandate_canceled_event | deactivated | The mandate was terminated by the merchant before its end date. Final. |
mandate_expire_event | closed | The mandate reached its endDate and was closed. Final. |
Handling mandate events​
const secret = process.env.CREDITCHEK_SECRET_KEY;
function verifyWebhook(req, res, next) {
if (req.headers["x-auth-signature"] !== secret) {
return res.status(401).json({
message: "Unauthorized request.",
});
}
next();
}
router.post("/recova/webhook", verifyWebhook, (req, res) => {
// Acknowledge first, process afterwards.
res.sendStatus(200);
const { event, data } = req.body;
const mandateId = data.mandateId || data._id;
switch (event) {
case "mandate_created_event":
// Mandate submitted for authorization — keep the payer waiting on `pending`.
break;
case "mandate_approved_event":
// Mandate is live on data.accountNumber — activate the loan or subscription.
break;
case "mandate_paused_event":
// Collection suspended — pause your repayment schedule.
break;
case "mandate_canceled_event":
case "mandate_expire_event":
// Final states — close out the schedule and stop expecting collections.
break;
}
});
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.