Developers
Partner API reference
The sendEasy Partner API lets your platform originate mobile money collections and cross-border remittances programmatically, with wallet-backed settlement and signed webhook status updates. This page covers every endpoint, field, and error your integration will see.
Overview
Base URL: https://api.sendeasy.cash/v1
All request and response bodies are JSON. Unknown fields in a request body are rejected with a 400 — the API will not silently drop a typo.
A "Collection" debits an end-customer’s Benin mobile money account and credits the result into your own sendEasy wallet. A "Remittance" debits your sendEasy wallet and pays a recipient out via mobile money (Benin), bank transfer (Nigeria), or cross-border mobile wallet (UEMOA/CEMAC/DR Congo). Neither product applies FX conversion — amount and currency are the same on both sides of the operation.
Authentication
Every operations-API request needs an X-Api-Key header. Get your keys from the partner dashboard after you register — a SANDBOX key is issued automatically and works immediately; a PRODUCTION key can be created once your account is approved (status ACTIVE).
Sandbox keys start with pk_test_, production keys with pk_live_. Treat both as secrets: don’t commit them, and only ever send them from your backend, never from a browser or mobile app.
The dashboard itself (register, login, API key and webhook management) uses a separate Bearer JWT — that’s for the humans on your team managing the integration, not for the operations API your servers call.
curl https://api.sendeasy.cash/v1/partner/operations/PO-2026-00012345 \
-H "X-Api-Key: pk_test_5f9a1c2b3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a"Sandbox mode
Every operation created with a SANDBOX key is fully simulated — it never touches a real MTN, B-MO, or bank rail, and never shares sendEasy’s own production credentials or quota. A simulated collection or remittance sits at PENDING for a few seconds (so your polling and webhook-handling code gets genuinely exercised), then resolves to SUCCESS.
This means you can build and test your entire integration — requests, polling, webhooks, error handling — without ever moving real money or needing real recipient accounts.
Errors
Every error — validation failures, auth failures, not-found, or an unexpected server error — is returned with the same envelope shape.
| Field | Type | Required | Notes |
|---|---|---|---|
| 400 | Bad Request | Optional | A required field is missing or the wrong type, an unknown field was sent, or a business rule was violated (e.g. a MOBILE_MONEY remittance to a country other than BJ). |
| 401 | Unauthorized | Optional | Missing or invalid X-Api-Key, or your account is SUSPENDED/REJECTED. A PENDING_REVIEW account can still use its sandbox key — only production keys are blocked until approval. |
| 404 | Not Found | Optional | GET /partner/operations/:reference with a reference that doesn’t exist, or belongs to a different partner (same message either way — you can’t enumerate another partner’s operations). |
| 500 | Internal Server Error | Optional | Something failed on our end. Safe to retry. |
{
"statusCode": 400,
"path": "/v1/partner/remittances",
"timestamp": "2026-09-10T12:00:00.000Z",
"message": "momoOperator and momoNumber are required for a MOBILE_MONEY destination"
}Idempotency
partnerReference is your own idempotency key, required on both create-collection and create-remittance and unique per partner (not globally). If you POST the same partnerReference twice, the second call returns the already-existing operation unchanged instead of creating a duplicate — safe to retry a request after a timeout without double-charging or double-paying out.
The reference field in the response (e.g. "PO-2026-00012345") is sendEasy’s own identifier — use it, not partnerReference, when calling GET /partner/operations/:reference.
POST /partner/collections
Debits an end-customer’s Benin mobile money account for the given amount and credits your sendEasy wallet once it succeeds. Processing happens asynchronously — the response is always status PENDING; poll GET /partner/operations/:reference or wait for a collection.succeeded / collection.failed webhook.
| Field | Type | Required | Notes |
|---|---|---|---|
| partnerReference | string | Required | Your own idempotency key. |
| amount | string | Required | Decimal string, e.g. "5000.00". |
| currency | string | Required | e.g. "XOF". |
| momoOperator | string | Required | One of: MTN, MOOV, CELTIIS, BMO. |
| momoNumber | string | Required | The end-customer’s mobile money number. |
| narration | string | Optional | Shown to the end-customer where the rail supports it. |
| callbackUrl | string | Optional | Per-request webhook URL override (must be public HTTPS). Falls back to your dashboard-configured webhook URL. |
curl -X POST https://api.sendeasy.cash/v1/partner/collections \
-H "X-Api-Key: pk_test_5f9a1c2b3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a" \
-H "Content-Type: application/json" \
-d '{
"partnerReference": "order-88421",
"amount": "5000.00",
"currency": "XOF",
"momoOperator": "MTN",
"momoNumber": "22997123456",
"narration": "Invoice #88421",
"callbackUrl": "https://example.com/webhooks/sendeasy"
}'{
"id": "3fa8b6c2-...",
"partnerId": "b1e0...",
"type": "COLLECTION",
"environment": "SANDBOX",
"status": "PENDING",
"reference": "PO-2026-00012345",
"partnerReference": "order-88421",
"amount": "5000.00",
"currency": "XOF",
"counterpartyMomoOperator": "MTN",
"counterpartyMomoNumber": "22997123456",
"providerRef": null,
"rail": null,
"failureReason": null,
"callbackUrl": "https://example.com/webhooks/sendeasy",
"createdAt": "2026-09-10T12:00:00.000Z",
"updatedAt": "2026-09-10T12:00:00.000Z",
"completedAt": null
}POST /partner/remittances
Debits your sendEasy wallet and pays a recipient out. Which fields are required depends on destinationRailType — the three supported types are shown below. Like collections, this is asynchronous: the response is always status PENDING, and the debit is reserved upfront so two concurrent requests can’t overdraw your balance.
If your wallet balance is too low, you get an immediate 400 (“Insufficient {currency} balance”). In the rare case the balance check races with another debit, the reservation itself is refused and the operation fails asynchronously (status FAILED, a remittance.failed webhook) rather than returning a synchronous error.
| Field | Type | Required | Notes |
|---|---|---|---|
| partnerReference | string | Required | Your own idempotency key. |
| amount | string | Required | Decimal string — both the amount debited from your wallet and the amount paid out. |
| currency | string | Required | |
| destinationCountry | CountryCode | Required | ISO-2, e.g. "BJ", "NG", "CI". |
| destinationRailType | string | Required | MOBILE_MONEY | VIRTUAL_ACCOUNT_BANK | MONEY_REMIT_MOBILE_WALLET. |
| recipientFullName | string | Required | |
| callbackUrl | string | Optional | Per-request webhook URL override. |
MOBILE_MONEY destination (Benin only)
Requires momoOperator and momoNumber. destinationCountry must be "BJ" — mobile money payout is not yet available for other countries.
POST /v1/partner/remittances
{
"partnerReference": "payout-30021",
"amount": "12000.00",
"currency": "XOF",
"destinationCountry": "BJ",
"destinationRailType": "MOBILE_MONEY",
"recipientFullName": "Awa Kone",
"momoOperator": "MTN",
"momoNumber": "22997654321"
}VIRTUAL_ACCOUNT_BANK destination (Nigeria only)
Requires bankCode and bankAccountNumber (bankName is recommended for display purposes). destinationCountry must be "NG".
POST /v1/partner/remittances
{
"partnerReference": "payout-30022",
"amount": "45000.00",
"currency": "NGN",
"destinationCountry": "NG",
"destinationRailType": "VIRTUAL_ACCOUNT_BANK",
"recipientFullName": "Chidinma Okafor",
"bankCode": "058",
"bankName": "GTBank",
"bankAccountNumber": "0123456789"
}MONEY_REMIT_MOBILE_WALLET destination (cross-border)
For payouts to a mobile wallet outside Benin/Nigeria — UEMOA (BF, CI, GW, ML, NE, SN, TG), CEMAC (CM, CF, TD, CG, GQ, GA), or DR Congo (CD). Requires momoNumber as the recipient’s wallet number, plus the real individual sender’s identity: senderFullName, senderPhoneNumber, and senderCountryOfResidence are mandatory (our upstream compliance partner requires the actual sender’s KYC for this rail, not a corporate/partner identity) — senderNationality, senderDateOfBirth, senderIdType, senderIdExpiryDate, senderAddress, and senderCity are optional but recommended, since missing fields may cause the payout to require manual review. recipientComplianceProfile is an optional free-form object for any additional recipient-side compliance fields.
POST /v1/partner/remittances
{
"partnerReference": "payout-30023",
"amount": "20000.00",
"currency": "XOF",
"destinationCountry": "CI",
"destinationRailType": "MONEY_REMIT_MOBILE_WALLET",
"recipientFullName": "Jean Kouassi",
"momoNumber": "22507012345",
"recipientComplianceProfile": {
"idType": "NATIONAL_ID",
"idNumber": "CI0123456789"
},
"senderFullName": "Awa Kone",
"senderPhoneNumber": "22997654321",
"senderCountryOfResidence": "BJ",
"senderNationality": "BJ",
"senderDateOfBirth": "1990-04-12",
"senderIdType": "PASSPORT",
"senderIdExpiryDate": "2029-01-01",
"senderAddress": "12 Rue des Cocotiers",
"senderCity": "Cotonou"
}{
"id": "9c21a4e0-...",
"type": "REMITTANCE",
"environment": "SANDBOX",
"status": "PENDING",
"reference": "PO-2026-00012399",
"partnerReference": "payout-30023",
"amount": "20000.00",
"currency": "XOF",
"counterpartyCountry": "CI",
"counterpartyRailType": "MONEY_REMIT_MOBILE_WALLET",
"counterpartyFullName": "Jean Kouassi",
"providerRef": null,
"rail": null,
"failureReason": null,
"createdAt": "2026-09-10T12:05:00.000Z",
"updatedAt": "2026-09-10T12:05:00.000Z",
"completedAt": null
}GET /partner/operations/:reference
Looks up a collection or remittance by its sendEasy reference (the value returned as reference when you created it). status is one of PENDING, PROCESSING, SUCCESS, FAILED — poll this if you’d rather not rely on webhooks, or use it to reconcile after a webhook arrives.
curl https://api.sendeasy.cash/v1/partner/operations/PO-2026-00012345 \
-H "X-Api-Key: pk_test_5f9a1c2b3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a"{
"id": "3fa8b6c2-...",
"type": "COLLECTION",
"status": "SUCCESS",
"reference": "PO-2026-00012345",
"partnerReference": "order-88421",
"amount": "5000.00",
"currency": "XOF",
"providerRef": "mock-momo-7e2f...",
"rail": "SANDBOX",
"failureReason": null,
"createdAt": "2026-09-10T12:00:00.000Z",
"updatedAt": "2026-09-10T12:00:32.000Z",
"completedAt": "2026-09-10T12:00:32.000Z"
}Webhooks
Configure a webhook URL from the partner dashboard (or override it per-request with callbackUrl). sendEasy POSTs a signed JSON payload to it when an operation reaches a terminal state — event is one of collection.succeeded, collection.failed, remittance.succeeded, remittance.failed.
Deliveries are retried up to 8 times with exponential backoff (starting at 10s) until your endpoint returns a 2xx. Every attempt — success or failure — is visible in the delivery log on your dashboard, and you can manually trigger a redelivery from there.
Verify every delivery: recompute the HMAC-SHA256 of "{timestamp}.{raw request body}" using your webhook signing secret (shown once at registration, or rotatable from the dashboard), and compare it to the v1 value in X-SendEasy-Signature using a constant-time comparison. Reject anything that doesn’t match, and reject deliveries with a timestamp too far in the past to guard against replay.
{
"id": "c9e5a2f0-...",
"event": "collection.succeeded",
"createdAt": "2026-09-10T12:00:32.000Z",
"data": {
"reference": "PO-2026-00012345",
"partnerReference": "order-88421",
"type": "COLLECTION",
"status": "SUCCESS",
"amount": "5000.00",
"currency": "XOF",
"providerRef": "mock-momo-7e2f...",
"failureReason": null,
"completedAt": "2026-09-10T12:00:32.000Z"
}
}Content-Type: application/json
X-SendEasy-Signature: t=1757505632,v1=8f3a1e0c9b7d6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4c3b2a1f
X-SendEasy-Event: collection.succeeded
X-SendEasy-Delivery: c9e5a2f0-...const crypto = require('crypto');
function verifySendEasySignature(rawBody, signatureHeader, webhookSecret) {
const parts = Object.fromEntries(
signatureHeader.split(',').map((p) => p.split('='))
);
const expected = crypto
.createHmac('sha256', webhookSecret)
.update(`${parts.t}.${rawBody}`)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(parts.v1, 'hex')
);
}Country & rail coverage
Collections (debiting an end-customer) are Benin-only today, via MTN, Moov, Celtiis, or a B-MO/BestCash-linked wallet.
Remittance payouts: mobile money is Benin-only; bank transfer is Nigeria-only; the cross-border mobile-wallet rail covers Burkina Faso, Côte d’Ivoire, Guinea-Bissau, Mali, Niger, Senegal, Togo, Cameroon, Central African Republic, Chad, Republic of the Congo, Equatorial Guinea, Gabon, and DR Congo. Reach out to sendEasy support if you need a corridor that isn’t listed — coverage expands regularly.
Ready to integrate?
Register for a free sandbox API key — no approval needed to start testing.