Appearance
Mandates
Register a customer's standing consent to be debited
POST
/api/v1/tenant/mandatesScope mandates:write · Write — send Idempotency-Key and X-Request-Id
Records the customer's acceptance of the consent document (agreementType = mandate) and the mandate that cites it, together or not at all. A customer authentication assertion is required because creating a mandate is itself consent (see the Customer authentication guide). The mandate is active when validFrom has passed and pending until then. beneficiaryEntityCode must identify a beneficiary that can collect mandate debits — a business with a collection wallet at Neo Wallet, or a beneficiary Neo Wallet has configured; anything else is a 422. replacesMandateUid supersedes an existing mandate atomically — it is revoked with reason superseded and its agreement moved to superseded, never edited. mandate.created is delivered to your webhook.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
customerUid | string | yes | The customer giving the standing consent. |
walletUid | string | yes | The customer's wallet the mandate debits. |
beneficiaryEntityCode | string | yes | Code of the beneficiary the debits pay; it must be a beneficiary set up with Neo Wallet to collect payments. At most 40 characters. |
clientReference | string | yes | Your own reference; stored and returned verbatim, never interpreted. |
amountRule | enum | yes | How the amount of each debit is bounded. One of fixed, up_to, formula. |
fixedOrMaxAmount | string | no | A positive amount with at most two decimal places; the exact (fixed) or maximum (up_to) debit. Required unless amountRule is formula. |
formula | object | no | The formula rule's document. Not supported yet: amountRule formula is refused (422). |
frequency | enum | yes | How often the mandate may be debited. One of one_time, daily, weekly, monthly, on_due_date, ad_hoc. |
currency | string | yes | ISO-4217; must equal the wallet's currency. |
validFrom | string | yes | ISO-8601; the mandate is pending until then and active after. |
validTo | object | no | ISO-8601; must be after validFrom. Absent = open-ended. |
retryPolicy | object | no | Advisory, for your own use; Neo Wallet never schedules or retries a debit. |
agreement | object | yes | The consent document the customer accepted for this mandate. |
agreement.documentType | string | yes | Kind of document accepted, e.g. debit_mandate_terms. At most 40 characters. |
agreement.documentVersion | string | yes | Version of the accepted document. At most 40 characters. |
agreement.documentUri | string | yes | URI of the accepted document. At most 500 characters. |
agreement.documentHash | string | yes | SHA-256 of the accepted document, 64 hex characters. |
agreement.acceptedAt | string | yes | When the customer accepted the document, ISO-8601. |
agreement.acceptedChannel | string | yes | Your channel the consent was captured in; portal is not accepted. |
agreement.evidence | object | yes | Your proof of the acceptance; hashed and stored, never returned. |
assertion | object | yes | The customer's authentication for giving this consent. |
assertion.customerUid | string | yes | The customer who authenticated. |
assertion.identifierUid | object | no | The customer identifier the customer authenticated with, if known. |
assertion.assuranceLevel | enum | yes | How strongly your channel authenticated the customer. One of single_factor, two_factor, step_up. |
assertion.factor1Type | string | yes | The first factor used, e.g. pin, password, biometric. At most 32 characters. |
assertion.factor2Type | object | no | Required unless assuranceLevel is single_factor. At most 32 characters. |
assertion.authenticatedAt | string | yes | ISO-8601 — when the customer authenticated. |
assertion.expiresAt | string | yes | ISO-8601; by default at most 10 minutes after authenticatedAt. An expired assertion is refused (422). |
assertion.channelCode | string | yes | Your channel the customer authenticated on. At most 40 characters. |
assertion.deviceHash | object | no | SHA-256 hex of the device fingerprint, if your channel has one. |
assertion.sourceReference | string | yes | Opaque channel evidence reference (a session or login id). Presenting one again while it is live is allowed only for the same customer. At most 160 characters. |
assertion.transactionBinding | object | no | Hash your channel computed over the request this assertion authorizes; a mismatch with the request received is refused. |
assertion.transactionBinding.hash | string | yes | SHA-256 hex (64 characters) over the canonical string of this request. |
assertion.transactionBinding.algorithm | enum | yes | One of sha256. |
assertion.transactionBinding.canonical | enum | yes | The field template hashed: the six values joined with |, UTF-8. The fifth is the payout destination, the mandate for a debit, or empty; clientReference is empty when absent. One of `customerUid |
replacesMandateUid | object | no | An existing mandate of this customer to supersede; it is revoked as this one is created. |
Example (placeholder values)
json
{
"customerUid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"walletUid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"beneficiaryEntityCode": "string",
"clientReference": "string",
"amountRule": "fixed",
"frequency": "one_time",
"currency": "ETB",
"validFrom": "2026-09-03T10:15:30.000Z",
"agreement": {
"documentType": "string",
"documentVersion": "string",
"documentUri": "string",
"documentHash": "string",
"acceptedAt": "2026-09-03T10:15:30.000Z",
"acceptedChannel": "string",
"evidence": {}
},
"assertion": {
"customerUid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"assuranceLevel": "single_factor",
"factor1Type": "string",
"authenticatedAt": "2026-09-03T10:15:30.000Z",
"expiresAt": "2026-09-03T10:15:30.000Z",
"channelCode": "string",
"sourceReference": "string"
}
}Response · 201
| Field | Type | Description |
|---|---|---|
uid | string | |
customerUid | string | |
walletUid | string | |
beneficiaryEntityCode | string | |
clientReference | object | null | Your reference, exactly as you supplied it. |
amountRule | enum | One of fixed, up_to, formula. |
fixedOrMaxAmount | string | null | A decimal amount with two decimal places. |
frequency | enum | One of one_time, daily, weekly, monthly, on_due_date, ad_hoc. |
currency | string | |
validFrom | string | |
validTo | object | null | |
status | enum | One of pending, active, suspended, revoked, expired, completed. |
agreementUid | object | null | The acceptance recorded with this mandate. |
replacesMandateUid | object | null | The mandate this one superseded. |
revokedAt | object | null | When the mandate was revoked; null unless status is revoked. |
createdAt | string |
Example (placeholder values)
json
{
"uid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"customerUid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"walletUid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"beneficiaryEntityCode": "string",
"clientReference": {},
"amountRule": "fixed",
"fixedOrMaxAmount": "2500.00",
"frequency": "one_time",
"currency": "ETB",
"validFrom": "2026-09-03T10:15:30.000Z",
"validTo": {},
"status": "pending",
"agreementUid": {},
"replacesMandateUid": {},
"revokedAt": {},
"createdAt": "2026-09-03T10:15:30.000Z"
}Errors
400 401 403 404 409 422 429 500 503 — one body shape for all of them, see Errors.
Get one of your mandates
GET
/api/v1/tenant/mandates/{uid}Scope mandates:read
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
uid | string | yes |
Response · 200
| Field | Type | Description |
|---|---|---|
uid | string | |
customerUid | string | |
walletUid | string | |
beneficiaryEntityCode | string | |
clientReference | object | null | Your reference, exactly as you supplied it. |
amountRule | enum | One of fixed, up_to, formula. |
fixedOrMaxAmount | string | null | A decimal amount with two decimal places. |
frequency | enum | One of one_time, daily, weekly, monthly, on_due_date, ad_hoc. |
currency | string | |
validFrom | string | |
validTo | object | null | |
status | enum | One of pending, active, suspended, revoked, expired, completed. |
agreementUid | object | null | The acceptance recorded with this mandate. |
replacesMandateUid | object | null | The mandate this one superseded. |
revokedAt | object | null | When the mandate was revoked; null unless status is revoked. |
createdAt | string |
Example (placeholder values)
json
{
"uid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"customerUid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"walletUid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"beneficiaryEntityCode": "string",
"clientReference": {},
"amountRule": "fixed",
"fixedOrMaxAmount": "2500.00",
"frequency": "one_time",
"currency": "ETB",
"validFrom": "2026-09-03T10:15:30.000Z",
"validTo": {},
"status": "pending",
"agreementUid": {},
"replacesMandateUid": {},
"revokedAt": {},
"createdAt": "2026-09-03T10:15:30.000Z"
}Errors
400 401 403 404 429 500 503 — one body shape for all of them, see Errors.
Revoke one of your mandates
POST
/api/v1/tenant/mandates/{uid}/revokeScope mandates:write · Write — send Idempotency-Key and X-Request-Id
reason is always required and recorded. Without assertion you revoke on your own authority; with one, the revocation is recorded as the customer's, and the assertion must be the mandate's customer's (see the Customer authentication guide). The mandate moves to revoked with revokedAt, the acceptance it cites moves to revoked, and mandate.revoked is delivered to your webhook. A revoked mandate is terminal — it is never reactivated; register a new one instead. Revoking a mandate that is already revoked, expired or completed is a 409.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
uid | string | yes |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
reason | string | yes | Why the mandate is revoked; recorded on the mandate. At most 500 characters. |
assertion | object | no | The customer's authentication when you revoke on the customer's behalf; it must be for the mandate's customer. Omit when you revoke on your own authority. |
assertion.customerUid | string | yes | The customer who authenticated. |
assertion.identifierUid | object | no | The customer identifier the customer authenticated with, if known. |
assertion.assuranceLevel | enum | yes | How strongly your channel authenticated the customer. One of single_factor, two_factor, step_up. |
assertion.factor1Type | string | yes | The first factor used, e.g. pin, password, biometric. At most 32 characters. |
assertion.factor2Type | object | no | Required unless assuranceLevel is single_factor. At most 32 characters. |
assertion.authenticatedAt | string | yes | ISO-8601 — when the customer authenticated. |
assertion.expiresAt | string | yes | ISO-8601; by default at most 10 minutes after authenticatedAt. An expired assertion is refused (422). |
assertion.channelCode | string | yes | Your channel the customer authenticated on. At most 40 characters. |
assertion.deviceHash | object | no | SHA-256 hex of the device fingerprint, if your channel has one. |
assertion.sourceReference | string | yes | Opaque channel evidence reference (a session or login id). Presenting one again while it is live is allowed only for the same customer. At most 160 characters. |
assertion.transactionBinding | object | no | Hash your channel computed over the request this assertion authorizes; a mismatch with the request received is refused. |
assertion.transactionBinding.hash | string | yes | SHA-256 hex (64 characters) over the canonical string of this request. |
assertion.transactionBinding.algorithm | enum | yes | One of sha256. |
assertion.transactionBinding.canonical | enum | yes | The field template hashed: the six values joined with |, UTF-8. The fifth is the payout destination, the mandate for a debit, or empty; clientReference is empty when absent. One of `customerUid |
Example (placeholder values)
json
{
"reason": "string"
}Response · 200
| Field | Type | Description |
|---|---|---|
uid | string | |
customerUid | string | |
walletUid | string | |
beneficiaryEntityCode | string | |
clientReference | object | null | Your reference, exactly as you supplied it. |
amountRule | enum | One of fixed, up_to, formula. |
fixedOrMaxAmount | string | null | A decimal amount with two decimal places. |
frequency | enum | One of one_time, daily, weekly, monthly, on_due_date, ad_hoc. |
currency | string | |
validFrom | string | |
validTo | object | null | |
status | enum | One of pending, active, suspended, revoked, expired, completed. |
agreementUid | object | null | The acceptance recorded with this mandate. |
replacesMandateUid | object | null | The mandate this one superseded. |
revokedAt | object | null | When the mandate was revoked; null unless status is revoked. |
createdAt | string |
Example (placeholder values)
json
{
"uid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"customerUid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"walletUid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"beneficiaryEntityCode": "string",
"clientReference": {},
"amountRule": "fixed",
"fixedOrMaxAmount": "2500.00",
"frequency": "one_time",
"currency": "ETB",
"validFrom": "2026-09-03T10:15:30.000Z",
"validTo": {},
"status": "pending",
"agreementUid": {},
"replacesMandateUid": {},
"revokedAt": {},
"createdAt": "2026-09-03T10:15:30.000Z"
}Errors
400 401 403 404 409 422 429 500 503 — one body shape for all of them, see Errors.
Debit a wallet on a standing mandate, customer not present
POST
/api/v1/tenant/mandates/{uid}/debitsScope wallet-debits:write · Write — send Idempotency-Key and X-Request-Id
Moves the amount from the customer's wallet to the mandate's beneficiary, like POST /tenant/wallet-debits, but on the standing consent alone: no authentication assertion is accepted, and the mandate's frequency is enforced, so a second debit inside the current period, or any debit under a one_time mandate that has already completed one, is a 422. Neo Wallet never schedules these — you trigger each one, and the mandate's retryPolicy is advisory to you. A wallet that cannot fund the debit is not an error: the 201 carries the operation in state failed and you also receive wallet_debit.failed; a failed attempt does not consume the mandate's period, so it may be retried inside it under a fresh clientReference.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
uid | string | yes |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
walletUid | string | yes | The wallet to debit; must be the mandate's own wallet. |
amount | string | yes | A positive amount with at most two decimal places; must satisfy the mandate's amount rule. |
currency | string | yes | ISO-4217; must equal the wallet's currency. |
clientReference | string | yes | Your own reference; stored and returned verbatim, never interpreted; unique within your account. |
Example (placeholder values)
json
{
"walletUid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"amount": "2500.00",
"currency": "ETB",
"clientReference": "string"
}Response · 201
| Field | Type | Description |
|---|---|---|
uid | string | |
type | string | Operation type, e.g. wallet_credit, wallet_debit, payout_b2c or topup_c2b. |
state | string | Operation state: created, awaiting_authorization, authorized, held, processing, pending_provider, confirmed, failed, reversed or completed. |
amount | string | A decimal amount with two decimal places. |
fee | string | A decimal amount with two decimal places. |
tax | string | A decimal amount with two decimal places. |
currency | string | ISO-4217. |
clientReference | string | null | Your reference, exactly as you supplied it. |
sourceWalletUid | string | null | Wallet transfers only: the wallet the money left. |
destinationWalletUid | string | null | Wallet transfers only: the wallet the money reached. |
createdAt | string | |
completedAt | string | null | When the operation first reached a final state. |
links | object | |
links.self | string | API-relative path of this operation (GET /tenant/operations/{uid}). |
links.receipt | string | API-relative path of its receipt (GET /tenant/operations/{uid}/receipt). |
Example (placeholder values)
json
{
"uid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"type": "string",
"state": "string",
"amount": "2500.00",
"fee": "12.50",
"tax": "1.88",
"currency": "ETB",
"clientReference": "string",
"sourceWalletUid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"destinationWalletUid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"createdAt": "2026-09-03T10:15:30.000Z",
"completedAt": "2026-09-03T10:15:30.000Z",
"links": {
"self": "string",
"receipt": "string"
}
}Errors
400 401 403 404 409 422 429 500 503 — one body shape for all of them, see Errors.