PENDING to ACTIVE, and once active it becomes another rail the billing engine can charge on. Mandates hang off the same global account as the wallet and cards, so a mandate follows the person, not a single merchant.
Direct debit is a real banking product: it is live-mode only and only available when the Nomba client is configured. Everything below is on the
live schema.Creating a mandate
From the portal, the customer picks a bank, enters their account number and phone, and Duro does the rest:GET /portal/email/banksreturns the banks that support direct debit (a filtered, cached Nomba list, with a built-in fallback). Not every bank is eligible, so this is a shorter list than the payout bank list.POST /portal/email/resolve-accounttakes{ bankCode, accountNumber }and returns{ accountName }from a live Nomba lookup, so the customer confirms the right account before creating the mandate.POST /portal/email/direct-debittakes{ bankCode, accountNumber (10 digits), phone }, resolves the name, then creates the Nomba mandate withfrequency: VARIABLE(renewals differ in amount), a start ~2 hours out, and a ~5-year window. The mandate row is savedPENDING, and the customer is emailed the activation instruction.
Activation is a token payment the customer makes
A NIBSS e-mandate isn’t live the moment it’s created. The customer has to consent by making a small token payment (₦50) from the same account, via their bank app. Until they do, the mandate staysPENDING. The exact instruction (account numbers to pay, from the STANDARD_MANDATE_INSTRUCTION) is returned on creation and surfaced in the portal for any pending mandate.
The lifecycle Duro tracks
A mandate lives in one of three states, normalised from Nomba’s raw status fields:PENDING: created, waiting on the customer’s token payment / bank advice.ACTIVE: usable for charging. Reached only when Nomba reports the mandate lifecycleACTIVEand the advice statusADVICE_SENT.SUSPENDED: suspended or deleted at the bank; terminal for Duro’s purposes.
- A worker sweep. A background job scans
PENDINGmandates in batches, re-queries each one’s status at Nomba, and flips any that have goneACTIVE. When one activates, the customer is emailed a “direct debit is active” confirmation. - On-read refresh. When the customer opens the direct-debit screen (
GET /portal/email/direct-debit), Duro refreshes any non-suspended mandate’s status live before returning the list, and backfills a missing activation instruction for pending ones. So the portal never shows a stalePENDINGfor a mandate that has actually gone live.
POST /portal/email/direct-debit/{id}/refresh, and remove one with DELETE /portal/email/direct-debit/{id} (ownership is checked against the session’s account).
Each mandate view carries id, bankName, accountLast4, bankCode, status, and (for pending mandates only) the instruction.
Why it matters for recovery
Direct debit is one of the fallback rails in the dunning chain (RAIL_FALLBACK = [ussd, transfer, virtual_account, direct_debit]). A customer whose card keeps hard-declining can still be charged on a mandate they’ve authorised, pulling the renewal straight from the bank account, no card involved. An active mandate is exactly the kind of alternate rail that turns a lost subscription into a recovered one.
There are two mandate code paths in the backend. The customer-facing portal creates account-level mandates (
AccountMandate, keyed to the global account), the flow documented here. There is also a merchant-side DirectDebitService that creates a customer-scoped mandate as a PaymentMethod on a single tenant. Both talk to the same Nomba e-mandate API and share the same PENDING → ACTIVE normalisation.