Aller au contenu
Tous les fournisseurs

SasPay

paiement·🇧🇫🇧🇯🇨🇩🇨🇮🇨🇲🇬🇭🇬🇳🇰🇪🇲🇱🇲🇼🇳🇪🇳🇬🇷🇼🇸🇳🇹🇬🇹🇿🇺🇬🇿🇲·Sandbox disponible

SasPay est une API REST unique pour encaisser des paiements (mobile money, carte) et payer des bénéficiaires par mobile money en Afrique de l’Ouest, centrale et de l’Est, avec page de paiement hébergée, liens de paiement, portefeuilles par pays et webhooks signés.

À utiliser avec des agents IA

Après avoir installé le plugin ou ajouté le serveur MCP, donnez cette consigne à votre agent :

« Annule la session de paiement SasPay en attente pour la commande #1042 avant que le client ne paie. »
Installer le plugin →

Capacités

CapacitéTypeStatutiMéthodeExemple
cancel_checkout_sessionsynchroneVérifiéePOST
confirm_otpsynchroneDisponiblePOST
create_checkout_sessionsynchroneVérifiéePOST
create_paymentsynchroneVérifiéePOST
create_payment_linksynchroneVérifiéePOST
get_balancesynchroneDisponibleGET
get_checkout_session_statussynchroneVérifiéeGET
retry_paymentsynchroneVérifiéePOST
send_payoutsynchroneDisponiblePOST
send_wallet_transfersynchroneDisponiblePOST
verify_paymentsynchroneVérifiéeGET
verify_payoutsynchroneDisponibleGET
webhook_payment_completedwebhookDisponiblePOST

Gotchas

Les gotchas sont affichés tels qu’ils figurent dans les specs, dans leur langue d’origine.

cancel_checkout_session

  • ⚠Only a PENDING session can be cancelled: a session already paid, expired or cancelled returns 409 not_pending.

confirm_otp

  • ⚠This is a second step, only for networks that require a customer-side confirmation after the initial push (e.g. Wizall Senegal, Coris Benin). On other networks it returns 400.
  • ⚠A failed confirmation returns a deliberately generic 422 message: never expect the gateway's reason in it.
  • ⚠409 means the transaction cannot be confirmed in its current state (not_pending, no_gateway, no_attempt): check it with verify_payment instead of retrying the OTP.

create_checkout_session

  • ⚠Idempotency-Key is not supported here: calling twice creates two separate sessions (nothing is charged until one is paid).
  • ⚠fee_charge_mode is silently ignored unless the merchant account has ALLOW_CLIENT_OVERRIDE enabled.
  • ⚠The API key needs the PAYIN or BOTH scope, otherwise the call returns 403.
  • ⚠return_url only brings the customer back after a successful payment: after a failed or cancelled one it is not used, the customer sees a Retry button and must close the tab, so don't depend on a redirect to your site for failures.
  • ⚠Amounts are decimal strings ("5000.00"), never floats.

create_payment

  • ⚠Always check checkout_url in the response: for Wave, Orange Money, Djamo and cards you must redirect the customer to it; only when it is empty was a request pushed to the customer's phone.
  • ⚠Networks that need a pre-payment OTP (Orange Money Côte d'Ivoire and Burkina Faso: the customer dials a USSD code first) require the otp field, otherwise 422 prepayment_otp_missing. This is not the post-push SMS OTP, which goes through confirm_otp.
  • ⚠Send an Idempotency-Key (one UUID per payment attempt, reused on retries): without it a network retry creates a second real payment. Reusing a key with a different body, or while the first call is still running, returns 409.
  • ⚠Amounts are decimal strings ("2500.00"), never floats.
  • ⚠To know beforehand whether a network needs the pre-payment OTP (and show the right USSD instructions), read otp_required / otp_instructions from GET /pricing/my-rates/.
  • ⚠Valid network codes per country and the expected customer.phone format are listed on the docs' formats page (/api-reference/reference/formats).
  • ⚠A 400 validation error has another shape than business errors: error is a map of field names to messages ({"field": ["message"]}), not {message, code}.

create_payment_link

  • ⚠No server-side cross-validation: a FIXED link without amount is accepted, so always send amount with amount_type FIXED.
  • ⚠redirect_url is required when show_confirmation_page is false, otherwise 400.
  • ⚠Idempotency-Key is not supported here: calling twice creates two separate links.

get_balance

  • ⚠Balances are per country wallet: two countries with the same currency (e.g. Benin and Senegal, both XOF) still have separate balances.

get_checkout_session_status

  • ⚠Meant for polling when no webhook is configured: it re-checks the real gateway state on each call.
  • ⚠Authenticated calls are limited to 300 requests per minute per account; beyond that the API returns 429, so space out polling.
  • ⚠`success: true` only means the call worked; the outcome is in data.status (PAID for a paid session; session statuses PENDING | PAID | EXPIRED | CANCELLED) and data.transaction_status (SUCCESS for the payment; transaction statuses PENDING | SUCCESS | FAILED | CANCELLED).

retry_payment

  • ⚠Retrying reuses the same transaction and id; it never creates a new transaction.
  • ⚠Only incoming payments can be retried: a payout returns 400. A FAILED payment is retryable (that is what this endpoint is for); 409 is for the other terminal states.
  • ⚠Send an Idempotency-Key: without it a network retry relaunches the payment a second time. Reusing a key with a different body, or while the first call is still running, returns 409.
  • ⚠The retry can switch the customer to a hosted page: on the sandbox, a failed mtn_ci push retried with no body came back with a checkout_url (status still FAILED until the customer pays), so check checkout_url on every retry.

send_payout

  • ⚠With an API key, the calling server's IP must be whitelisted for the merchant, otherwise 403 ip_not_whitelisted.
  • ⚠No automatic currency conversion on payouts (unlike payins): the currency must match the beneficiary's country, otherwise 422 currency_country_mismatch.
  • ⚠Send an Idempotency-Key: without it a network retry creates a second disbursement. Reusing a key with a different body, or while the first call is still running, returns 409.
  • ⚠Amounts are decimal strings ("10000.00"), never floats.
  • ⚠Valid method codes per country and the expected recipient.msisdn format are listed on the docs' formats page (/api-reference/reference/formats).
  • ⚠A 400 validation error has a different shape from the business errors: `error` maps each invalid field to a list of messages ({"amount": ["Ce champ est obligatoire."]}) with no error.message or error.code, so read the field map, not just error.message (observed on the sandbox).
  • ⚠Payouts must be enabled for the merchant by SasPay: until then every call returns 403 payout_not_enabled, even with a whitelisted IP (observed on the sandbox).
  • ⚠The minimum payout is 200 XOF: below it, 400 with error.amount ["Le montant minimum est de 200 XOF."] (observed on the sandbox).

send_wallet_transfer

  • ⚠Only one PENDING transfer per merchant at a time: a new call while one awaits admin approval returns 422 transfer_already_pending.
  • ⚠The transfer is COMPLETED immediately only if WALLET_TRANSFER_AUTO_APPROVE is enabled; otherwise it stays PENDING until an admin approves it, and the target wallet is not credited before then.
  • ⚠Send an Idempotency-Key: without it a network retry reserves the amount a second time. Reusing a key with a different body, or while the first call is still running, returns 409.

verify_payment

  • ⚠Only deliver the product or grant access once this returns SUCCESS.
  • ⚠A PENDING payment is re-checked against the gateway on each call, so the status can change between two calls.
  • ⚠Every response is wrapped in the same envelope: read the payload from data, and on failure read error.message and error.code.
  • ⚠Only PENDING, SUCCESS and FAILED occur in practice: CANCELLED exists in the data model but no flow reaches it today, so treat it as a non-successful terminal state without expecting it.
  • ⚠For accounting, never use the requested amount alone: what left the payer and what you receive depend on fee_charge_mode. Read debited_amount and net_amount, or the amounts object (requested, fee, charged, net).
  • ⚠`success: true` only means the call worked; the outcome is in data.status (SUCCESS for a paid payment; transaction statuses PENDING | SUCCESS | FAILED | CANCELLED).

verify_payout

  • ⚠A PENDING payout is re-checked against the gateway on each call, so the status can change between two calls.
  • ⚠`success: true` only means the call worked; the outcome is in data.status (SUCCESS for a completed payout; transaction statuses PENDING | SUCCESS | FAILED | CANCELLED).

webhook_payment_completed

  • ⚠Verify X-Webhook-Signature before anything else: it is the lowercase hex HMAC-SHA256, keyed with your webhook's signing_secret (SASPAY_WEBHOOK_SECRET), of the X-Webhook-Timestamp value, a dot, then the raw body: `${timestamp}.${rawBody}`.
  • ⚠Reject the request when X-Webhook-Timestamp (Unix seconds) is more than 5 minutes from your clock: without this check an intercepted webhook stays replayable forever, since its signature never expires.
  • ⚠Compute the signature over the raw request body, never over a re-serialised parsed payload (key order or number formatting can differ). Express: mount express.raw({ type: 'application/json' }) on this route.
  • ⚠Compare signatures in constant time (crypto.timingSafeEqual, hmac.compare_digest, hash_equals), never with ===.
  • ⚠signing_secret is shown only once, in the creation or rotation response, and never returned by the read endpoints: copy it then. If lost, set a new one from the dashboard (Webhooks → Changer le secret).
  • ⚠Webhooks and subscriptions can only be created, changed or deleted from the dashboard; the API key can only list and read them.
  • ⚠Each event is attempted 5 times (immediately, then +30s, +5min, +30min, +2h) with a 15s timeout per attempt, then marked FAILED for good (only a manual resend from the dashboard retries it). Answer 2xx right away, process in the background, and make processing idempotent on data.id, since a retried event can arrive twice.
  • ⚠For accounting use data.charged (what left the payer) and data.net_amount (what you receive), never data.amount alone: which one includes the fee depends on fee_charge_mode.
  • ⚠Do not depend on the exact data shape of settlement.* events: SasPay states it is not guaranteed yet.
  • ⚠data.msisdn is empty on transactions created before 17 August 2026; country, network and the amounts are always present.

Détails

Catégorie
paiement
Sandbox
Oui

Contributeurs de la spec