Developers

Partner API reference

The sendEasy Partner API lets your platform originate mobile money collections, cross-border remittances, and bill payments 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), a Nigerian bank account, a cross-border mobile wallet (UEMOA/CEMAC/DR Congo), or sendEasy's international bank-transfer network (reaching every country that network supports, including Nigeria as an alternative to the virtual-account rail). Collections never convert currency. A Remittance only converts when you send sourceCurrency (your wallet currency) that differs from currency (the payout currency); otherwise amount and currency are the same on both sides.

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
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 mobile money network 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.

FieldTypeRequiredNotes
400Bad RequestOptionalA 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).
401UnauthorizedOptionalMissing 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.
404Not FoundOptionalGET /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).
500Internal Server ErrorOptionalSomething failed on our end. Safe to retry.
Error response
{
  "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.

Pricing and fees

sendEasy fees are charged on top of the amount, never taken out of it. Fees are set per service — collection, disbursement, remittance, and bills — and can differ per partner; ask your account manager for your rates. A fee can be flat, a percentage, or tiered by amount, with an optional minimum and maximum.

Collections: the full amount is credited to your wallet, then the fee is debited as a separate entry. Remittances: your wallet is debited the principal plus the fee. When sourceCurrency differs from currency, the principal is the converted amount plus your agreed FX margin, and the fee is calculated on that principal in your wallet currency. A failed remittance refunds the same total. Bills: the fee is added to the amount owed to the biller. Every operation response includes feeAmount, and cross-currency remittances also include sourceCurrency, sourceAmount, fxRate, and fxMarginPercent.

POST /partner/quote

Returns exactly what an operation would cost before you create it, so you can decide whether to proceed. Nothing is debited or created, and quotes are not binding: the FX rate and your pricing may change between a quote and the actual request.

For type COLLECTION, send amount and currency; the response has fee and netCredit. For type REMITTANCE, also send destinationRailType (and sourceCurrency to price a conversion); the response has principalDebit, fxRate, fxMarginPercent, fee, and totalDebit in your wallet currency.

FieldTypeRequiredNotes
typestringRequiredCOLLECTION | REMITTANCE.
amountstringRequiredDecimal string.
currencystringRequiredCollection currency, or the payout currency for a remittance.
sourceCurrencystringOptionalYour wallet currency, if different from currency (remittance only).
destinationRailTypestringOptionalRequired for REMITTANCE; selects the pricing service.
destinationCountryCountryCodeOptionalOptional destination country (remittance).
Request (remittance)
curl -X POST https://api.sendeasy.cash/v1/partner/quote \
  -H "X-Api-Key: pk_test_5f9a1c2b3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "REMITTANCE",
    "amount": "150000.00",
    "currency": "NGN",
    "sourceCurrency": "XOF",
    "destinationRailType": "VIRTUAL_ACCOUNT_BANK",
    "destinationCountry": "NG"
  }'
Response (remittance)
{
  "service": "REMITTANCE",
  "amount": "150000.0000",
  "currency": "NGN",
  "walletCurrency": "XOF",
  "principalDebit": "62500.0000",
  "fxRate": "2.4",
  "fxMarginPercent": "1",
  "fee": "625.0000",
  "feeCurrency": "XOF",
  "totalDebit": "63125.0000"
}
Response (collection)
{
  "service": "COLLECTION",
  "amount": "5000.0000",
  "currency": "XOF",
  "fee": "50.0000",
  "feeCurrency": "XOF",
  "netCredit": "4950.0000",
  "pricing": "PARTNER"
}

GET /partner/fx-rate

Returns the current exchange rate for a currency pair as it applies to you. from is your wallet currency and to is the payout currency. baseRate is how many units of to you get for 1 unit of from before your margin; fxMarginPercent is your agreed margin; effectiveRate is the rate after the margin (baseRate divided by 1 + margin). The rate is indicative: the binding figures are returned when you create the remittance (see sourceCurrency), and a POST /partner/quote gives the exact debit for a specific amount.

Optionally pass destinationRailType if your margin differs by payout type. An unsupported pair returns a 400.

FieldTypeRequiredNotes
fromstringRequiredYour wallet currency, e.g. "XOF".
tostringRequiredPayout currency, e.g. "NGN".
destinationRailTypestringOptionalSelects the pricing service whose margin applies.
Request
curl "https://api.sendeasy.cash/v1/partner/fx-rate?from=XOF&to=NGN" \
  -H "X-Api-Key: pk_test_5f9a1c2b3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a"
Response
{
  "from": "XOF",
  "to": "NGN",
  "baseRate": "2.4",
  "fxMarginPercent": "1",
  "effectiveRate": "2.37623762",
  "asOf": "2026-10-20T09:30:00.000Z"
}

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.

FieldTypeRequiredNotes
partnerReferencestringRequiredYour own idempotency key.
amountstringRequiredDecimal string, e.g. "5000.00".
currencystringRequirede.g. "XOF".
momoOperatorstringRequiredOne of: MTN, MOOV, CELTIIS, SENDEASY_WALLET (a linked sendEasy wallet, available regardless of network).
momoNumberstringRequiredThe end-customer’s mobile money number.
narrationstringOptionalShown to the end-customer where the rail supports it.
callbackUrlstringOptionalPer-request webhook URL override (must be public HTTPS). Falls back to your dashboard-configured webhook URL.
Request
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"
  }'
Response — 201
{
  "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 four 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.

FieldTypeRequiredNotes
partnerReferencestringRequiredYour own idempotency key.
amountstringRequiredDecimal string — both the amount debited from your wallet and the amount paid out.
currencystringRequired
sourceCurrencystringOptionalYour wallet currency. When it differs from currency, sendEasy converts at the current rate plus your FX margin and debits your wallet in this currency.
destinationCountryCountryCodeRequiredISO-2, e.g. "BJ", "NG", "CI".
destinationRailTypestringRequiredMOBILE_MONEY | VIRTUAL_ACCOUNT_BANK | MONEY_REMIT_MOBILE_WALLET | BANK_TRANSFER_INTERNATIONAL.
recipientFullNamestringRequired
callbackUrlstringOptionalPer-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.

Request — MOBILE_MONEY (Benin)
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".

Request — VIRTUAL_ACCOUNT_BANK (Nigeria)
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 (this rail requires the actual sender’s KYC, 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.

Request — MONEY_REMIT_MOBILE_WALLET (cross-border)
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"
}
Response — 201 (any remittance type)
{
  "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
}

BANK_TRANSFER_INTERNATIONAL destination (any supported country, including Nigeria)

For payouts straight into a real bank account anywhere sendEasy's international bank-transfer network reaches — this rail isn't limited to one destinationCountry the way VIRTUAL_ACCOUNT_BANK and MOBILE_MONEY are, it covers every country sendEasy lists banks for, and you choose it explicitly per request rather than sendEasy auto-selecting it. In Nigeria it's an alternative to VIRTUAL_ACCOUNT_BANK — some accounts route better one way or the other; try the other rail if a specific recipient bank is rejected.

Requires bankCode, bankName, and bankAccountNumber (bankName must match the exact label from sendEasy's bank list for that country — a mismatched label is rejected), plus momoNumber as the recipient's own mobile number. Recipient identity fields (recipientNationality as an ISO-2 code such as "NG", recipientAddress, recipientIdType in uppercase such as "PASSPORT", recipientIdNumber) are only required for destination countries that ask for them — they are not required for Nigeria — and when sent, recipientAddress must be a normal, valid street address (street, area, city), not a placeholder. Because the payer is a real individual, also send the sender context where you have it: senderFullName, senderPhoneNumber, senderCountryOfResidence, senderAddress, senderCity, senderStateProvince, senderPostalCode, plus remittancePurpose, sourceOfFunds, and relationshipSender (for example "Family Maintenance", "Salary", "Brother"). Missing mandatory details for the chosen corridor are rejected with a 400 naming the missing fields. Amounts are paid out in whole currency units, and each corridor enforces its own minimum and maximum per transaction (for Nigeria, NGN 1,500 to NGN 4,878,000); amounts outside the limits are rejected. originCountry (ISO-2, defaults to "BJ") tells sendEasy which of its own settlement markets the payment is considered to originate from — it is not the recipient's country and does not need to match destinationCountry.

Request — BANK_TRANSFER_INTERNATIONAL (any supported country)
POST /v1/partner/remittances

{
  "partnerReference": "payout-30024",
  "amount": "150000.00",
  "currency": "XOF",
  "destinationCountry": "CI",
  "destinationRailType": "BANK_TRANSFER_INTERNATIONAL",
  "recipientFullName": "Fatou Diabate",
  "bankCode": "CI001",
  "bankName": "Ecobank Côte d'Ivoire",
  "bankAccountNumber": "CI93CI0080101000012345678901",
  "momoNumber": "22507012345",
  "senderFullName": "Koffi Mensah",
  "senderPhoneNumber": "+22961234567",
  "senderCountryOfResidence": "BJ",
  "senderAddress": "Rue 12, Cadjehoun, Cotonou",
  "senderCity": "Cotonou",
  "remittancePurpose": "Family Maintenance",
  "sourceOfFunds": "Salary",
  "relationshipSender": "Brother",
  "originCountry": "BJ"
}
Request — Nigeria (no recipient identity fields needed)
POST /v1/partner/remittances

{
  "partnerReference": "payout-30025",
  "amount": "150000",
  "currency": "NGN",
  "destinationCountry": "NG",
  "destinationRailType": "BANK_TRANSFER_INTERNATIONAL",
  "recipientFullName": "Adaeze Okafor",
  "bankCode": "058",
  "bankName": "GTBank",
  "bankAccountNumber": "0123456789",
  "momoNumber": "+2349132754830",
  "senderFullName": "Koffi Mensah",
  "senderPhoneNumber": "+22961234567",
  "senderCountryOfResidence": "BJ",
  "remittancePurpose": "Family Maintenance",
  "sourceOfFunds": "Salary",
  "relationshipSender": "Brother",
  "originCountry": "BJ"
}

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.

Request
curl https://api.sendeasy.cash/v1/partner/operations/PO-2026-00012345 \
  -H "X-Api-Key: pk_test_5f9a1c2b3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a"
Response — 200 (SUCCESS)
{
  "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"
}

POST /partner/bill-payments/inquire

Pays a bill (utility, pay-TV, school fees, airtime top-up) on behalf of your own end-customer, drawing on your same prefunded sendEasy wallet — SANDBOX and PRODUCTION keys share one balance, only the rail call is simulated in sandbox. Unlike collections and remittances, bill payments are synchronous end to end: sendEasy's biller integrations don't have a pending/polling window, so both steps below return the final result directly, with no webhook involved.

The flow is two calls. First, inquire prices the bill and returns a BillPayment in status INQUIRED with the amount now known — nothing is debited yet. params is a loose object whose required keys depend on billerCode (a card number for CANALPLUS, a meter number for SBEE_PREPAID, a student ID for UP_SCOLARITE/UAC_SCOLARITE, and so on) — see your dashboard's sandbox console for the full per-biller field list, mirrored from the consumer app's own form.

FieldTypeRequiredNotes
billerCodestringRequiredOne of: CANALPLUS, CMA_CGM, UP_SCOLARITE, UAC_SCOLARITE, AIRTIME, SBEE_PREPAID, SBEE_POSTPAID.
paramsobjectRequiredBiller-specific fields (e.g. numCard, meterNum, studentMatricule) — validated per billerCode.
Request
curl -X POST https://api.sendeasy.cash/v1/partner/bill-payments/inquire \
  -H "X-Api-Key: pk_test_5f9a1c2b3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a" \
  -H "Content-Type: application/json" \
  -d '{
    "billerCode": "CANALPLUS",
    "params": {
      "numCard": "0021234567",
      "typeOperation": "1"
    }
  }'
Response — 201
{
  "id": "7b1e4a3c-...",
  "partnerId": "b1e0...",
  "environment": "SANDBOX",
  "status": "INQUIRED",
  "billerCode": "CANALPLUS",
  "externalReference": "BILL-CANALPLUS-9f2e...",
  "amount": "5000",
  "currency": "XOF",
  "operationCost": null,
  "sendEasyFeeAmount": null,
  "totalDebited": null,
  "providerReference": null,
  "providerStatus": null,
  "paidAt": null,
  "createdAt": "2026-09-10T12:00:00.000Z"
}

POST /partner/bill-payments/:id/pay

Confirms a previously-inquired bill payment (status must still be INQUIRED — inquire again if it's gone stale). This debits your wallet for the priced amount plus sendEasy's own margin (if any), then settles the bill synchronously and resolves to SUCCESS or REFUNDED in the same response — there is nothing to poll or wait on. :id is the id field returned by inquire, not partnerReference (bill payments have no partner-supplied idempotency key — retry the whole inquire → pay sequence if a request times out). There is no request body: unlike the consumer and agent apps, a Partner API call carries no transaction PIN — your X-Api-Key is the only authorization this endpoint checks.

If the biller rejects or errors on the payment, your wallet is refunded automatically and the response comes back with status REFUNDED and a failureReason — you don't need to reverse anything yourself.

Request
curl -X POST https://api.sendeasy.cash/v1/partner/bill-payments/7b1e4a3c-.../pay \
  -H "X-Api-Key: pk_test_5f9a1c2b3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a"
Response — 201
{
  "id": "7b1e4a3c-...",
  "environment": "SANDBOX",
  "status": "SUCCESS",
  "billerCode": "CANALPLUS",
  "externalReference": "BILL-CANALPLUS-9f2e...",
  "amount": "5000",
  "currency": "XOF",
  "sendEasyFeeAmount": "0.0000",
  "totalDebited": "5000.0000",
  "providerReference": "MOCK-BILL-CANALPLUS-9f2e...",
  "providerStatus": "CONFIRMED",
  "paidAt": "2026-09-10T12:00:05.000Z"
}

Other bill-payment endpoints

Two billers need a browse-before-price step with no BillPayment row created yet: GET /partner/bill-payments/canalplus/offers?numCard=... lists available Canal+ bouquets, and GET /partner/bill-payments/airtime/products?msisdn=...&productType=... lists eligible airtime/data/combo/pin products for a beneficiary number.

For bills, GET /partner/bill-payments/:id/quote on an inquired payment returns amount, operationCost, fee, and totalDebit before you call pay. GET /partner/bill-payments lists your own bill payments (paginated, newest first); GET /partner/bill-payments/:id fetches one. POST /partner/bill-payments/:id/refresh-status re-checks status with the biller network for a payment stuck in PROCESSING — in practice this should never be needed given the synchronous flow above, but it exists as a safety net for the rare case your connection drops between the debit and the biller's response.

Response
{
  "amount": "12000.0000",
  "operationCost": "0.0000",
  "fee": "120.0000",
  "currency": "XOF",
  "totalDebit": "12120.0000"
}

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.

POST to your webhookUrl
{
  "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"
  }
}
Headers sent with every delivery
Content-Type: application/json
X-SendEasy-Signature: t=1757505632,v1=8f3a1e0c9b7d6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4c3b2a1f
X-SendEasy-Event: collection.succeeded
X-SendEasy-Delivery: c9e5a2f0-...
Verify the signature (Node.js)
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 linked sendEasy wallet.

Remittance payouts: mobile money is Benin-only; the virtual-account bank rail (VIRTUAL_ACCOUNT_BANK) 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; the international bank-transfer rail (BANK_TRANSFER_INTERNATIONAL) reaches every country sendEasy's bank-transfer network supports — including Nigeria, as an alternative to VIRTUAL_ACCOUNT_BANK. 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.

Get API keys