Cashier integration · v1
AbePay as a cashier method
This is the contract for listing AbePay as a payment method inside a broker's own cashier. The client never leaves the broker's site, never creates an AbePay account, and never re-verifies their identity. Five endpoints and four webhooks cover the whole integration.
We would rather build to your specification. If you already have a cashier integration specification, send it and we will implement against it. This document exists so there is something concrete to review in the meantime.
The flow
Two rules govern both directions. A rail confirmation is never taken at face value. We verify every payment independently before we notify you of anything. And on withdrawals we are only ever instructed after you have verified and debited your own client, so a payout cannot exist without a matching debit.
Deposit
Withdrawal
Signing and security
Every request in both directions is signed. The signature covers the raw request body plus a timestamp and a nonce, so a message that is altered, replayed, or delivered late is rejected rather than processed.
Request headers
| Field | Type | Description |
|---|---|---|
X-Abe-Keyrequired | string | Your API key identifier. Identifies which signing secret to verify against. |
X-Abe-Timestamprequired | integer | Unix seconds. Requests more than 300 seconds old are rejected. |
X-Abe-Noncerequired | string | Unique per request. A repeated nonce within the timestamp window is rejected. |
X-Abe-Signaturerequired | string | hex(HMAC-SHA256(secret, timestamp + "." + nonce + "." + rawBody)). Compared in constant time. |
Verifying a signature
import crypto from 'crypto';
function sign(secret, timestamp, nonce, rawBody) {
return crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${nonce}.${rawBody}`)
.digest('hex');
}
const expected = sign(SECRET, ts, nonce, raw);
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));Additional controls
- ·Mutual TLS on request, in addition to signing.
- ·IP allow-listing in both directions: you give us your egress ranges, we give you ours.
- ·Per-environment keys, rotatable without downtime: both the old and new secret verify during a rotation window.
- ·No client PII required. We need an opaque client reference, an amount and a country. We do not want names, documents or dates of birth, and we will reject them if sent.
Available methods
/cashier/v1/methods?country=KE¤cy=USDCalled when the cashier renders. Returns what this client can actually use, with live limits and the rate they would get. A market where we hold no local rail connectivity returns stablecoin only, so the method never disappears entirely.
Response
{
"methods": [
{
"code": "mpesa_ke",
"label": "M-Pesa",
"currency": "KES",
"directions": ["deposit", "payout"],
"min_usd": 1,
"max_usd": 2000,
"rate": { "deposit": 130.0, "payout": 124.0 },
"eta_seconds": { "deposit": 45, "payout": 90 },
"destination_field": { "type": "msisdn", "pattern": "^254[0-9]{9}$" }
},
{
"code": "usdt_trc20",
"label": "USDT (TRC-20)",
"currency": "USD",
"directions": ["deposit", "payout"],
"min_usd": 5,
"max_usd": 50000,
"rate": { "deposit": 1.0, "payout": 1.0 },
"eta_seconds": { "deposit": 120, "payout": 60 },
"destination_field": { "type": "address", "pattern": "^T[A-Za-z0-9]{33}$" }
}
]
}destination_field tells your cashier what to ask a withdrawing client for, and how to validate it client-side. It differs per method: a mobile number, a bank account, a chain address. The cashier does not need to hard-code any of them.
Open a deposit
/cashier/v1/depositsCreates a deposit session and returns the URL your cashier opens, in an iframe, a modal or a redirect, whichever suits your interface. The client completes payment there and we notify you by webhook when the funds are confirmed.
Request body
| Field | Type | Description |
|---|---|---|
referencerequired | string | Your identifier for this deposit. Returned on every webhook and lookup. |
client_refrequired | string | An opaque, stable reference for the client. We never resolve it to a person. |
countryrequired | string | ISO 3166-1 alpha-2. Determines which methods are offered. |
amount_usd | number | Amount the client wants credited. Omit to let the client choose in our interface. |
method | string | Pre-select a method code from /methods. Omit to show the client the full list. |
return_urlrequired | string | Where to send the client when the flow ends, successfully or not. |
POST https://api.abepayy.com/cashier/v1/deposits
{
"reference": "DRV-DEP-91847362",
"client_ref": "c_9f3a20b1",
"country": "KE",
"amount_usd": 100,
"return_url": "https://cashier.deriv.com/return/91847362"
}{
"transaction_id": "abe_dep_01J8XK4M2Q",
"reference": "DRV-DEP-91847362",
"status": "pending",
"payment_url": "https://pay.abepayy.com/s/01J8XK4M2Q",
"expires_at": "2026-08-26T09:15:00Z"
}Do not credit on the return_url. The client returning to your site is a UI event, not a payment confirmation. They may return before paying, or close the tab after paying. Credit on the deposit.completed webhook only.
Instruct a payout
/cashier/v1/payoutsCall this after you have verified your client and debited their balance. We accept the instruction, disburse over the rail, verify the payout independently, and confirm by webhook.
Request body
| Field | Type | Description |
|---|---|---|
referencerequired | string | Your identifier for this payout. |
idempotency_keyrequired | string | A retried instruction with the same key pays exactly once and returns the original result. |
client_refrequired | string | The same opaque client reference. |
amount_usdrequired | number | Amount debited from the client, in USD. |
methodrequired | string | A method code from /methods that supports the payout direction. |
destinationrequired | string | The value collected from the client, matching that method’s destination_field pattern. |
POST https://api.abepayy.com/cashier/v1/payouts
{
"reference": "DRV-WDR-55120934",
"idempotency_key": "DRV-WDR-55120934",
"client_ref": "c_9f3a20b1",
"amount_usd": 100,
"method": "mpesa_ke",
"destination": "254712345678"
}{
"transaction_id": "abe_pay_01J8XK9F7B",
"reference": "DRV-WDR-55120934",
"status": "processing",
"amount_local": 12400,
"currency": "KES",
"rate": 124.0
}A 202 is an acceptance, not a completion. Treat the payout as in-flight until payout.completed arrives. If it fails, we send payout.failed with a reason, and you re-credit your client.
Look up a transaction
/cashier/v1/transactions/{id}The authoritative status for one transaction, by our id or your reference. Use it for reconciliation, for a support query, and any time a webhook did not arrive. Never assume a failure from a timeout.
{
"transaction_id": "abe_dep_01J8XK4M2Q",
"reference": "DRV-DEP-91847362",
"type": "deposit",
"status": "completed",
"client_ref": "c_9f3a20b1",
"amount_usd": 100,
"amount_local": 13000,
"currency": "KES",
"rate": 130.0,
"method": "mpesa_ke",
"provider_reference": "SGH4XY9Z12",
"created_at": "2026-08-26T09:00:04Z",
"completed_at": "2026-08-26T09:00:41Z"
}Webhooks we send you
Four events, signed with the same scheme as inbound requests. Respond 2xx to acknowledge. Anything else, or no response within 10 seconds, is retried with exponential backoff for 24 hours.
| Field | Type | Description |
|---|---|---|
deposit.completed | event | Funds confirmed and independently verified. Credit the client now. |
deposit.failed | event | The client did not pay, the payment was reversed, or the session expired. Credit nothing. |
payout.completed | event | The client has the money. The debit stands. |
payout.failed | event | The disbursement did not succeed. Re-credit the client; the reason is in the payload. |
{
"event": "deposit.completed",
"sent_at": "2026-08-26T09:00:41Z",
"data": {
"transaction_id": "abe_dep_01J8XK4M2Q",
"reference": "DRV-DEP-91847362",
"client_ref": "c_9f3a20b1",
"amount_usd": 100,
"amount_local": 13000,
"currency": "KES",
"rate": 130.0,
"method": "mpesa_ke",
"provider_reference": "SGH4XY9Z12"
}
}Webhooks may arrive more than once. A retry after your acknowledgement was lost in transit is indistinguishable from a first delivery. Key on transaction_id and make handling idempotent.
Status model
| Field | Type | Description |
|---|---|---|
pending | non-terminal | Deposit session open, awaiting the client. No funds have moved. |
processing | non-terminal | Funds are moving. Payment submitted, or a payout dispatched to the rail. |
completed | terminal | Verified and final. On a deposit, credit. On a payout, the client has the money. |
failed | terminal | Final, and no funds moved, or a reversal has completed. Safe to re-credit. |
expired | terminal | A deposit session the client never completed. Equivalent to failed. |
A transaction only ever moves forward, and never leaves a terminal state. If you see a terminal status on lookup, that is final regardless of what any later webhook retry appears to say.
Idempotency and retries
- ·Payouts require an idempotency key. Replaying the same key returns the original transaction rather than paying twice, including while the first is still in flight.
- ·A timeout is not a failure. If a request times out, look the transaction up by your reference before retrying. This is the single most common way a double payout happens in any integration.
- ·Deposits hold a per-client lock. A second session opened while one is live returns the live one instead of creating a duplicate.
- ·Reconciliation is continuous on our side. Every non-terminal transaction is re-checked every minute until it settles, so nothing sits in limbo waiting for someone to notice.
Settlement and reconciliation
/cashier/v1/settlements?date=2026-08-26A client's balance moves instantly on our webhook; the movement of real funds between us is net and periodic. Every day we publish a file listing every transaction with its rail reference, the gross in each direction, and the net position for the cycle.
{
"date": "2026-08-26",
"currency": "USD",
"deposits": { "count": 1284, "gross": 48210.00 },
"payouts": { "count": 902, "gross": 39115.00 },
"net_due_to_deriv": 9095.00,
"status": "open",
"transactions_url": "https://api.abepayy.com/cashier/v1/settlements/2026-08-26/transactions.csv"
}Settlement cadence, whether daily, weekly or on a threshold, is a commercial term rather than a technical one. We are equally willing to run the reverse model, where Deriv pre-funds and we draw against a float, or to settle gross rather than net.
Errors
Failures return a machine-readable code and a message already phrased for an end client, so your cashier can show it without translation.
{
"error": {
"code": "amount_below_minimum",
"message": "The minimum deposit for this method is $1.",
"retryable": false
}
}| Status | Meaning | What to do |
|---|---|---|
| 400 | Malformed request. | Fix it. Do not retry unchanged. |
| 401 | Signature, timestamp or nonce rejected. | Check clock skew first. It is almost always clock skew. |
| 409 | Idempotency key reused with a different payload. | Use a new key, or replay the identical payload. |
| 422 | Valid request, refused on business rules: limits, unsupported method, screened destination. | Show the message to the client. |
| 429 | Rate limited. | Back off and retry with jitter. |
| 503 | A rail is degraded and we are failing closed rather than accepting money we cannot settle. | Retry, or offer another method. /methods reflects availability live. |
Sandbox and certification
The sandbox runs the complete flow: sessions, rail simulation, webhooks and settlement files, with no real money. Amounts drive the outcome, so every branch is reachable deterministically without waiting for a rail to misbehave.
| Field | Type | Description |
|---|---|---|
$10.00 | deposit | Completes normally after a short delay. |
$10.01 | deposit | Client abandons; session expires. |
$10.02 | deposit | Rail reports paid, verification disagrees. No credit, no webhook. |
$10.03 | deposit | Completes, then reverses. Tests your handling of a late failure. |
$20.00 | payout | Completes normally. |
$20.01 | payout | Rail rejects the destination. payout.failed. |
$20.02 | payout | Webhook delivered twice. Tests your idempotency. |
Timeline
- 1
Specification agreed
Yours or ours. A thirty-minute call settles it.
- 2
Sandbox credentials issued
Within one business day of that call.
- 3
You integrate
Typically under a week against a spec this size.
- 4
Certification
We run your test cases and hand back evidence for each.
- 5
Capped pilot
One market, limits you set, shared monitoring.
- 6
Remaining markets
Configuration only. No further integration work.
Request sandbox credentials or ask a technical question:
Appendix: the API running in production today
The specification above is what we propose for a cashier listing. It is worth being explicit that it describes a new contract surface over a platform that is already live: AbePay processes real client money across five African markets today, and partners integrate through the endpoints below.
| Endpoint | Purpose |
|---|---|
| GET /api/widget/rates | Live rates and transaction minimums. Public. |
| POST /api/widget/session | Exchange a partner key for a client-scoped session token. |
| POST /api/widget/deposit | Send the payment prompt to the client’s handset. |
| POST /api/widget/withdraw | Two-step withdrawal with the verification code. |
| GET /api/transactions | Per-account history with status and rail references. |
The verification-before-credit ordering, the per-minute reconciliation, the source validation on inbound notifications and the operator alerting described throughout this document all run against these endpoints in production. None of it is planned work for the cashier build.