Référence de l'API
The Sonco Pay API lets your server create crypto payments, follow them until they are paid, read your balance and withdraw it. Every request and response is JSON over HTTPS.
https://soncopay.example
Points d'accès
| Point d'accès | Description |
|---|---|
POST/v1/payments | Créer un paiement |
GET/v1/payments | Lister les paiements |
GET/v1/payments/{id} | Consulter un paiement |
POST/v1/payments/{id}/method | Choisir un moyen |
POST/v1/payments/{id}/simulate | Simuler un paiement (mode test) |
GET/v1/payments/{id}/timeline | Chronologie d'un paiement |
GET/v1/methods | Moyens, actifs et réseaux disponibles |
GET/v1/rates | Cours actuels en USD |
GET/v1/balances | Solde par actif |
GET/v1/ledger | Mouvements du solde |
GET/v1/statements | Relevé de compte |
GET/v1/statements.csv | Relevé de compte (CSV) |
POST/v1/payouts | Créer un retrait |
GET/v1/payouts | Lister les retraits |
GET/v1/payouts/{id} | Consulter un retrait |
GET/v1/payouts/{id}/timeline | Chronologie d'un retrait |
GET/v1/payout-fees | Frais de réseau et retrait minimum |
GET/v1/payout-destinations | Lister les destinations de retrait |
POST/v1/payout-destinations | Enregistrer une destination de retrait |
GET/v1/payout-destinations/{id} | Consulter une destination de retrait |
PATCH/v1/payout-destinations/{id} | Renommer une destination de retrait |
DELETE/v1/payout-destinations/{id} | Désactiver une destination de retrait |
GET/v1/settlement | Mode de règlement et destinations |
GET/v1/settlement/items | Paiements à régler |
PUT/v1/settlement/routes | Choisir une destination de règlement |
GET/v1/fees | Vos frais et limites |
POST/v1/customers | Clients, factures, abonnements |
GET/v1/customers | Factures et abonnements |
POST/v1/invoices | Créer une facture |
GET/v1/invoices | Factures et abonnements |
POST/v1/invoices/{id}/send | Factures et abonnements |
POST/v1/invoices/{id}/remind | Factures et abonnements |
POST/v1/invoices/{id}/void | Factures et abonnements |
POST/v1/subscriptions | Créer un abonnement |
POST/v1/subscriptions/{id}/pause | Factures et abonnements |
POST/v1/subscriptions/{id}/cancel | Factures et abonnements |
GET/v1/billing/summary | Synthèse de facturation et horloge de test |
SDK
Official libraries for Node.js, PHP and Python call the API for you: authentication with your key, JSON errors turned into exceptions, and webhook signature checks.
Version 2.2.0 adds customers, invoices and subscriptions (customers, plans, invoices, subscriptions and billing resources, invoice.* and subscription.* events). It is backward compatible with 2.0.0, the first release under the Sonco Pay name.
npm install https://soncopay.example/assets/sdk/soncopay-node-2.2.0.tgz
curl -O https://soncopay.example/assets/sdk/soncopay-php-2.2.0.zip
unzip soncopay-php-2.2.0.zip
pip install https://soncopay.example/assets/sdk/soncopay-2.2.0-py3-none-any.whl
import SoncoPay from 'soncopay-node'; // const { SoncoPay } = require('soncopay-node'); const sp = new SoncoPay(process.env.SONCOPAY_API_KEY); const payment = await sp.payments.create({ amount: '25.00', currency: 'USD', order_id: 'demo-1042', return_url: 'https://shop.example/thanks', }); res.redirect(303, payment.checkout_url);
require __DIR__ . '/soncopay-php/autoload.php'; use SoncoPay\Client; $sp = new Client(getenv('SONCOPAY_API_KEY')); $payment = $sp->payments->create([ 'amount' => '25.00', 'currency' => 'USD', 'order_id' => 'demo-1042', 'return_url' => 'https://shop.example/thanks', ]); header('Location: ' . $payment['checkout_url'], true, 303);
import os from soncopay import SoncoPay sp = SoncoPay(os.environ["SONCOPAY_API_KEY"]) payment = sp.payments.create( amount="25.00", currency="USD", order_id="demo-1042", return_url="https://shop.example/thanks", ) return redirect(payment["checkout_url"], code=303)
Vérifier un webhook
sp_test_… key while you build: the SDKs work the same way in both modes.app.post('/webhooks/soncopay', express.raw({ type: 'application/json' }), (req, res) => { let event; try { event = SoncoPay.webhooks.constructEvent(req.body, req.headers, process.env.SONCOPAY_WEBHOOK_SECRET); } catch (err) { return res.status(400).send('invalid signature'); } if (event.event === 'payment.paid') { // deliver event.data.order_id } res.sendStatus(200); });
use SoncoPay\Webhook; use SoncoPay\Exception\SignatureVerificationException; $payload = file_get_contents('php://input'); $signature = $_SERVER['HTTP_X_SONCOPAY_SIGNATURE'] ?? null; try { $event = Webhook::constructEvent($payload, $signature, getenv('SONCOPAY_WEBHOOK_SECRET')); } catch (SignatureVerificationException $e) { http_response_code(400); exit('invalid signature'); } if ($event->event === 'payment.paid') { // deliver $event->data['order_id'] } http_response_code(200);
from soncopay import construct_event, SignatureVerificationError @app.post("/webhooks/soncopay") def soncopay_webhook(): try: event = construct_event(request.get_data(), request.headers.get("X-SoncoPay-Signature"), SECRET) except SignatureVerificationError: abort(400) if event.event == "payment.paid": # deliver event.data["order_id"] return "", 200
Authentification
Send your API key in the X-Api-Key header. Create keys in your dashboard under Developers.
Moves real money. Use it only on your production server. Live keys work once your business verification is approved.
Works on separate test data: no real payment is taken and payments can be simulated.
curl https://soncopay.example/v1/balances -H "X-Api-Key: sp_test_…"
import SoncoPay from 'soncopay-node'; const sp = new SoncoPay(process.env.SONCOPAY_API_KEY); // sp_test_… or sp_live_… const { balances } = await sp.balances();
$sp = new SoncoPay\Client(getenv('SONCOPAY_API_KEY')); // sp_test_… or sp_live_… $b = $sp->balances();
from soncopay import SoncoPay sp = SoncoPay() # reads SONCOPAY_API_KEY: sp_test_… or sp_live_… b = sp.balances()
Déroulement d'un paiement
- Your server creates a payment with the amount and your order ID.
- You redirect your customer to
checkout_url, or you choose the method yourself and display the details on your own page (white-label). - The customer pays in crypto on the chosen network (the exact amount), with Binance Pay, by mobile money or by bank card where it is available.
- We confirm the payment, send you a
payment.paidwebhook and credit your balance, minus the fee.
25.0137 USDT). That is how a deposit is matched to its order: tell your customer to send exactly this amount.Créer un paiement
POST/v1/payments
| Champ | Description |
|---|---|
amountobligatoirechaîne ou nombre | Amount to collect. |
currencyobligatoirechaîne | USD, EUR, XOF, XAF, or a crypto asset (USDT, USDC, BTC, ETH, BNB, TRX, LTC). Fiat amounts are converted at the current rate when the customer chooses a method. |
order_idchaîne | Your reference. Sending the same order_id again returns the same payment, so retries are safe. |
descriptionchaîne | Shown to the customer on the payment page. |
customer_emailchaîne | Your customer's email, for your records. |
return_urlchaîne | Where the customer goes back after paying. |
callback_urlchaîne | Webhook URL for this payment only, as a public HTTPS address. By default, the URL of your account is used. |
lifetime_minutesentier | Between 10 and 1440. By default, the lifetime set by Sonco Pay. |
methodstableau | Restrict the choices, for example ["USDT"], ["binance_pay"], ["USDT:TRON"], ["mobile_money"] or a method code such as ["orange_ci"]. |
metadataobjet | Any JSON object, returned as is. |
countrychaîne | Customer country (ISO alpha-2, for example CI, SN or CM): only the methods of this country are offered. |
methodchaîne | A mobile money method code (see Mobile money): the request is sent to the customer right away. |
phonechaîne | With method: the mobile money number, in international format. |
operatorchaîne | With method: the operator, when the method needs one. |
payerobjet | Information about the payer, all optional: name, email, phone (international format), country (ISO alpha-2) and reference (your own customer reference). See Payer information. |
Informations du payeur
In the dashboard (Settings), choose for each field whether it is not collected, optional or required. A required field that your call does not send is asked to the customer on the payment page, before they choose how to pay. An email address also receives the payment receipt. Only the fields you collect are stored; they are kept for a limited time and never shown to anyone else.
The payment object carries payer (the collected fields, or null) in every response and in the signed webhooks. If you choose the method yourself with the API while a required field is missing, the call fails with 422 and the code payer_required (with fields); an invalid value fails with payer_invalid (with field).
curl -X POST https://soncopay.example/v1/payments \ -H "X-Api-Key: sp_test_…" \ -H "Content-Type: application/json" \ -d '{"amount":"25","currency":"USD","order_id":"1042","return_url":"https://shop.example/thanks", "payer":{"name":"Demo Customer","email":"[email protected]","reference":"client-42"}}'
const payment = await sp.payments.create({ amount: '25', currency: 'USD', order_id: '1042', return_url: 'https://shop.example/thanks', payer: { name: 'Demo Customer', email: '[email protected]', reference: 'client-42' }, }); payment.checkout_url;
$payment = $sp->payments->create([
'amount' => '25',
'currency' => 'USD',
'order_id' => '1042',
'return_url' => 'https://shop.example/thanks',
'payer' => ['name' => 'Demo Customer', 'email' => '[email protected]', 'reference' => 'client-42'],
]);
$payment['checkout_url'];
payment = sp.payments.create(
amount="25",
currency="USD",
order_id="1042",
return_url="https://shop.example/thanks",
payer={"name": "Demo Customer", "email": "[email protected]", "reference": "client-42"},
)
payment["checkout_url"]
{
"id": "pay_3f9a5c0d21e4b7a6c8d1",
"object": "payment",
"livemode": false,
"status": "new",
"amount": "25", "currency": "USD",
"order_id": "1042",
"checkout_url": "…/pay/pay_3f9a5c0d21e4b7a6c8d1",
"payer": { "name": "Demo Customer", "email": "[email protected]", "reference": "client-42" },
"expires_at": "2026-10-01T15:30:00+00:00",
…
}
Choisir un moyen
Skip this step if you use checkout_url: the customer chooses on our page. To build your own page (white-label), list the available methods, then choose one.
GET/v1/methods
Each entry of methods has a method (crypto, binance_pay, mobile_money or card), a code and a kind. Mobile money entries add operator, countries, currency, redirect and fields (what to send: phone, sometimes operator). The payment_methods list gives the methods usable by your account, with your effective fees.
curl https://soncopay.example/v1/methods -H "X-Api-Key: sp_test_…"
const { methods, payment_methods } = await sp.methods();
$m = $sp->methods(); $m['methods']; $m['payment_methods'];
m = sp.methods() m["methods"], m["payment_methods"]
POST/v1/payments/{id}/method
| Champ | Description |
|---|---|
methodobligatoirechaîne | crypto, binance_pay, card, mobile_money (with code) or a mobile money method code. |
assetchaîne | For crypto: the asset to receive, for example USDT. |
networkchaîne | For crypto: TRON, BSC, ETH, POLYGON, BTC or LTC. |
codechaîne | With mobile_money: the method code, for example orange_ci or mtn_cm. |
phonechaîne | For mobile money: the customer number, in international format. |
operatorchaîne | For mobile money: the operator, when the method needs one. |
The bank card is offered where it is available: GET /v1/methods then lists it with kind set to card.
The QR code of any payment is available at /pay/{id}/qr.svg.
curl -X POST https://soncopay.example/v1/payments/pay_3f9a5c0d21e4b7a6c8d1/method \ -H "X-Api-Key: sp_test_…" \ -H "Content-Type: application/json" \ -d '{"method":"crypto","asset":"USDT","network":"TRON"}'
const p = await sp.payments.chooseMethod('pay_3f9a5c0d21e4b7a6c8d1', { method: 'crypto', asset: 'USDT', network: 'TRON', }); p.pay_address; p.pay_amount;
$p = $sp->payments->chooseMethod('pay_3f9a5c0d21e4b7a6c8d1', [ 'method' => 'crypto', 'asset' => 'USDT', 'network' => 'TRON', ]);
p = sp.payments.choose_method("pay_3f9a5c0d21e4b7a6c8d1", "crypto", asset="USDT", network="TRON") p["pay_address"], p["pay_amount"]
{ "method": "crypto", "asset": "USDT", "network": "TRON" }
# the payment now carries:
# pay_address, pay_amount (send exactly this amount), network, expires_at
{ "method": "binance_pay" }
# the payment now carries:
# pay_url (open it or show it as a QR code), pay_amount in USDT
{ "method": "card" }
# the payment now carries:
# pay_url = https://…/pay/{id}/redirect : send the customer there,
# they pay on the secure card page and come back to the payment page
Mobile money
Mobile money works in several countries through local operators (Wave, Orange Money, MTN MoMo, Moov Money, Free Money and others). Each method has a code made of the operator and the country (for example orange_ci or mtn_cm), the countries where it works and the currency it collects: XOF or XAF.
Pays, opérateurs et devises
| Pays | Opérateurs (codes des moyens) | Devise |
|---|---|---|
Côte d'Ivoire CI | Wave wave_ci, Orange Money orange_ci, MTN Mobile Money mtn_ci, Moov Money moov_ci | XOF |
Senegal SN | Wave wave_sn, Orange Money orange_sn, Free Money free_sn | XOF |
Benin BJ | MTN Mobile Money mtn_bj, Moov Money moov_bj | XOF |
Burkina Faso BF | Orange Money orange_bf, Moov Money moov_bf | XOF |
Cameroon CM | MTN Mobile Money mtn_cm, Orange Money orange_cm | XAF |
GET /v1/methods returns the methods open to your account right now, with kind set to mobile_money, code, operator, countries and currency.
- Choose the customer's country with
country(ISO alpha-2, for exampleCIorCM) and the operator withmethod(its code), plusphonein international format. You can also choose the method later withPOST /v1/payments/{id}/method. - The customer approves the request on their phone. Show them
instructions. When the method works in redirect mode,pay_urlis filled in: send the customer there. - When the operator confirms, the payment becomes
paidand you receive thepayment.paidwebhook. Your balance is credited in the collected currency:XOForXAF.
Champs du paiement
| Champ | Description |
|---|---|
method_code | The chosen method: crypto, binance_pay, card or a mobile money code. |
provider | Family of the chosen method: crypto, binance_pay, mobile_money or card, and null before the choice. It never names an internal processor. Base your logic on method, method_code and status. |
pay_url | Binance Pay: the Binance Pay link. Mobile money in redirect mode and bank card: a link to /pay/{id}/redirect on our domain, which sends the customer to the secure payment page while the payment waits for it, then back to the payment page. |
customer_phone | The number the request was sent to. |
operator | The mobile money operator, when known. |
country | The customer country (ISO alpha-2). |
instructions | Text to show the customer while they approve the request. |
last_error | Why the last request failed, for example a request declined by the customer. |
409. A declined request puts the payment back to new with last_error: the customer can try again, up to 5 attempts, then 429.99, the request is declined; ending in 98, it stays pending; any other number is paid after a few seconds, or at once with POST /v1/payments/{id}/simulate.
# Côte d'Ivoire: Orange Money, amount in XOF (no decimals) curl -X POST https://soncopay.example/v1/payments \ -H "X-Api-Key: sp_test_…" \ -H "Content-Type: application/json" \ -d '{"amount":"5000","currency":"XOF","country":"CI","order_id":"demo-1043","method":"orange_ci","phone":"+2250700000000"}' # Cameroon: MTN Mobile Money, amount in XAF (no decimals) curl -X POST https://soncopay.example/v1/payments \ -H "X-Api-Key: sp_test_…" \ -H "Content-Type: application/json" \ -d '{"amount":"5000","currency":"XAF","country":"CM","order_id":"demo-1044","method":"mtn_cm","phone":"+237670000000"}'
// Côte d'Ivoire: Orange Money, amount in XOF (no decimals) const p = await sp.payments.create({ amount: '5000', currency: 'XOF', country: 'CI', order_id: 'demo-1043', method: 'orange_ci', phone: '+2250700000000', }); p.method_code; // 'orange_ci' p.instructions; // text to show the customer if (p.pay_url) res.redirect(303, p.pay_url); // redirect mode // Cameroon: MTN Mobile Money, amount in XAF (no decimals) const q = await sp.payments.create({ amount: '5000', currency: 'XAF', country: 'CM', order_id: 'demo-1044', method: 'mtn_cm', phone: '+237670000000', }); // the operators of one country, from GET /v1/methods const { methods } = await sp.methods(); methods.filter((m) => m.kind === 'mobile_money' && m.countries.includes('SN')); // or later, on an existing payment (another operator of the same country) await sp.payments.chooseMethod(p.id, { method: 'mobile_money', code: 'wave_ci', phone: '+2250700000000' });
// Côte d'Ivoire: Orange Money, amount in XOF (no decimals) $p = $sp->payments->create([ 'amount' => '5000', 'currency' => 'XOF', 'country' => 'CI', 'order_id' => 'demo-1043', 'method' => 'orange_ci', 'phone' => '+2250700000000', ]); $p['method_code']; // 'orange_ci' $p['instructions']; // text to show the customer if ($p['pay_url'] !== null) { header('Location: ' . $p['pay_url'], true, 303); // redirect mode } // Cameroon: MTN Mobile Money, amount in XAF (no decimals) $q = $sp->payments->create([ 'amount' => '5000', 'currency' => 'XAF', 'country' => 'CM', 'order_id' => 'demo-1044', 'method' => 'mtn_cm', 'phone' => '+237670000000', ]); // or later, on an existing payment (another operator of the same country) $sp->payments->chooseMethod($p['id'], ['method' => 'mobile_money', 'code' => 'wave_ci', 'phone' => '+2250700000000']);
# Côte d'Ivoire: Orange Money, amount in XOF (no decimals) p = sp.payments.create(amount="5000", currency="XOF", country="CI", order_id="demo-1043", method="orange_ci", phone="+2250700000000") p["method_code"], p["instructions"] # "orange_ci", text to show the customer if p["pay_url"]: return redirect(p["pay_url"], code=303) # redirect mode # Cameroon: MTN Mobile Money, amount in XAF (no decimals) q = sp.payments.create(amount="5000", currency="XAF", country="CM", order_id="demo-1044", method="mtn_cm", phone="+237670000000") # or later, on an existing payment (another operator of the same country) sp.payments.choose_method(p["id"], "mobile_money", code="wave_ci", phone="+2250700000000")
Retraits mobile money
Send method (the method code of the country, for example wave_sn or orange_cm), phone and amount to POST /v1/payouts, plus operator when the method needs one. The currency is the one of the method: XOF or XAF, in whole amounts.
# Senegal: Wave, XOF curl -X POST https://soncopay.example/v1/payouts \ -H "X-Api-Key: sp_test_…" \ -H "Content-Type: application/json" \ -d '{"method":"wave_sn","phone":"+221770000000","amount":"10000","reference":"demo-payout-8"}' # Cameroon: Orange Money, XAF curl -X POST https://soncopay.example/v1/payouts \ -H "X-Api-Key: sp_test_…" \ -H "Content-Type: application/json" \ -d '{"method":"orange_cm","phone":"+237690000000","amount":"15000","reference":"demo-payout-9"}'
await sp.payouts.create({ method: 'wave_sn', phone: '+221770000000', amount: '10000', reference: 'demo-payout-8' }); // XOF await sp.payouts.create({ method: 'orange_cm', phone: '+237690000000', amount: '15000', reference: 'demo-payout-9' }); // XAF
$sp->payouts->create(['method' => 'wave_sn', 'phone' => '+221770000000', 'amount' => '10000', 'reference' => 'demo-payout-8']); // XOF $sp->payouts->create(['method' => 'orange_cm', 'phone' => '+237690000000', 'amount' => '15000', 'reference' => 'demo-payout-9']); // XAF
sp.payouts.create(method="wave_sn", phone="+221770000000", amount="10000", reference="demo-payout-8") # XOF sp.payouts.create(method="orange_cm", phone="+237690000000", amount="15000", reference="demo-payout-9") # XAF
Consulter et lister
GET/v1/payments/{id}
GET/v1/payments
| Paramètre | Description |
|---|---|
status | Only payments with this status, for example paid. |
q | Search by payment ID, order ID or customer email. |
limit | From 1 to 200. Default: 50. |
offset | Number of payments to skip. The response says has_more when there are more. |
POST /v1/payments/{id}/simulate marks a payment as paid with its exact amount, once a method is chosen, so you can test your webhook handler.
curl https://soncopay.example/v1/payments/pay_3f9a5c0d21e4b7a6c8d1 -H "X-Api-Key: sp_test_…" curl "https://soncopay.example/v1/payments?status=paid&limit=50" -H "X-Api-Key: sp_test_…" # test mode curl -X POST https://soncopay.example/v1/payments/pay_3f9a5c0d21e4b7a6c8d1/simulate -H "X-Api-Key: sp_test_…"
const p = await sp.payments.retrieve('pay_3f9a5c0d21e4b7a6c8d1'); const { items, has_more } = await sp.payments.list({ status: 'paid', limit: 50 }); await sp.payments.simulate(p.id); // test mode
$p = $sp->payments->retrieve('pay_3f9a5c0d21e4b7a6c8d1'); $page = $sp->payments->list(['status' => 'paid', 'limit' => 50]); $page['has_more']; $sp->payments->simulate($p['id']); // test mode
p = sp.payments.retrieve("pay_3f9a5c0d21e4b7a6c8d1") page = sp.payments.list(status="paid", limit=50) page["has_more"] sp.payments.simulate(p["id"]) # test mode
Statuts des paiements
| État | Signification |
|---|---|
| new | Created; the customer has not chosen a method yet. |
| waiting | Method chosen; waiting for the funds. |
| confirming | Deposit detected; waiting for confirmations. |
| paid | Confirmed and credited to your balance. Deliver the order. |
| expired | Not paid in time. A deposit that arrives within 24 hours of expiry still marks it paid, with late: true in the webhook. |
| failed | Paid with a wrong amount through Binance Pay. |
Soldes
GET/v1/balances
Returns your available balance per asset, in the mode of the key you use.
reserved gives, per asset, the amount and the fee of your payouts that are still open. They are already deducted from balances.
curl https://soncopay.example/v1/balances -H "X-Api-Key: sp_test_…"
const { balances, reserved } = await sp.balances();
$b = $sp->balances(); $b['balances']; $b['reserved'];
b = sp.balances() b["balances"], b["reserved"]
{
"livemode": false,
"balances": { "USDT": "124.5", "XOF": "35000" },
"reserved": { "USDT": "51" }
}
GET/v1/ledger
Lists every movement of your balance: payments, fees and payouts. Filter with asset, page with limit (up to 500) and offset.
/v1/ledger keeps its format but each id becomes the ID of the accounting line: IDs from before and after the switch cannot be compared. Use statements to reconcile.curl "https://soncopay.example/v1/ledger?asset=USDT&limit=100" -H "X-Api-Key: sp_test_…"
const { items } = await sp.ledger.list({ asset: 'USDT', limit: 100 }); for await (const entry of sp.ledger.listAll({ asset: 'USDT' })) { … }
$page = $sp->ledger->list(['asset' => 'USDT', 'limit' => 100]); foreach ($sp->ledger->listAll(['asset' => 'USDT']) as $entry) { … }
page = sp.ledger.list(asset="USDT", limit=100) for entry in sp.ledger.list_all(asset="USDT"): …
Frais
GET/v1/fees
Returns the fee applied to your payments, as a percentage plus a fixed amount in USD, for each method you can use in this mode, with the payout fees and the limits. Each part comes from your own rate first, then from the method, then from the platform default: percent_source and fixed_source say which one applies.
The values above are examples: your dashboard and this endpoint show the fees that apply to you.
curl https://soncopay.example/v1/fees -H "X-Api-Key: sp_test_…"
const fees = await sp.fees();
$fees = $sp->fees();
fees = sp.fees()
{
"livemode": false,
"payment": { "percent": "0.5", "fixed_usd": "0", "percent_source": "global", "fixed_source": "global" },
"per_method": [ { "code": "orange_ci", "name": "Orange Money", "kind": "mobile_money", "percent": "1.5", … } ],
"payout_fees": { "USDT:TRON": "1", … },
"limits": { "min_payment_usd": 1, "min_payout_usd": 5 }
}
Minimums et frais de retrait par réseau
Chaque retrait, règlement automatique compris, doit atteindre le minimum de son réseau ; les frais du réseau s'ajoutent au montant et sont pris sur votre solde.
Le règlement automatique part dès que le total dû sur un réseau atteint son minimum. En dessous, les montants s'additionnent sur votre solde et partent ensemble, en un seul retrait.
| Devise et réseau | Minimum par retrait | Commission | Total pris sur le solde au minimum |
|---|---|---|---|
| Chargement… | |||
Ces valeurs viennent de la configuration en direct et peuvent changer avec les réseaux. GET /v1/fees les renvoie pour votre compte (payout_limits).
Retraits
POST/v1/payouts
Withdraws from your balance to a crypto address, a mobile money number or a saved payout destination, bank account included. For crypto, the network fee from /v1/payout-fees is added to the amount; the same call returns the minimum payout.
| Champ | Description |
|---|---|
assetchaîne | Crypto: the asset to withdraw, for example USDT (required). Mobile money: the currency, by default the one of the method. |
networkchaîne | Crypto (required): the network to send on, for example TRON. |
amountobligatoirechaîne ou nombre | Amount the address receives. |
addresschaîne | Crypto (required): the destination address, checked against the network format. |
methodchaîne | Mobile money: the method code, for example wave_sn or orange_cm. |
phonechaîne | Mobile money: the number to pay, in international format. |
operatorchaîne | Mobile money: the operator, when the method needs one. |
memochaîne | Memo or tag, when the destination needs one. |
referencechaîne | Your reference. Sending the same reference again returns the same payout. |
destination_idchaîne | A saved and confirmed payout destination. Its details replace asset, network, address, method and phone; asset, when sent, must equal its currency. It is the only way to pay out to a bank account. |
curl -X POST https://soncopay.example/v1/payouts \ -H "X-Api-Key: sp_test_…" \ -H "Content-Type: application/json" \ -d '{ "asset": "USDT", "network": "TRON", "amount": "150", "address": "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE", "reference": "withdraw-77" }'
const payout = await sp.payouts.create({ asset: 'USDT', network: 'TRON', amount: '150', address: 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE', reference: 'withdraw-77', });
$payout = $sp->payouts->create([
'asset' => 'USDT', 'network' => 'TRON', 'amount' => '150',
'address' => 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE', 'reference' => 'withdraw-77',
]);
payout = sp.payouts.create(
asset="USDT", network="TRON", amount="150",
address="TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE", reference="withdraw-77",
)
| État | Signification |
|---|---|
| pending_review | A large payout waits for our team's review before it is sent. |
| queued | Accepted and waiting to be sent. |
| submitting | Being handed over for sending. If the answer is lost, the same request is retried safely: a payout is never sent twice. |
| sent | Sent; waiting for confirmation on the network. |
| unknown | No clear answer after sending: our team checks before anything else happens. The payout is never sent again automatically. |
| awaiting_manual | Bank transfer: waiting for our team to execute it. It then becomes completed with bank_reference. |
| completed | Done: tx_hash (crypto) or bank_reference (bank transfer) is filled in. |
| failed | The payout failed; the amount and the fee are back in your balance. |
| rejected | Refused after review; the amount and the fee are back in your balance. |
Champs du retrait
| Champ | Description |
|---|---|
rail | crypto, momo (mobile money) or bank. |
destination_id | The saved destination used, or null. |
destination | Copy of the destination at the time of the request (kind, label, currency, masked details), or null. |
bank_reference | Reference of the bank transfer, once it is executed. |
tx_hash | Transaction hash of a crypto payout. |
error | Why the payout failed, when it did. |
curl https://soncopay.example/v1/payouts/po_8b2d… -H "X-Api-Key: sp_test_…" curl https://soncopay.example/v1/payout-fees -H "X-Api-Key: sp_test_…"
const po = await sp.payouts.retrieve('po_8b2d…'); po.status; po.rail; po.tx_hash; const { fees, min_payout_usd } = await sp.payouts.fees();
$po = $sp->payouts->retrieve('po_8b2d…'); $po['status']; $fees = $sp->payouts->fees();
po = sp.payouts.retrieve("po_8b2d…") po["status"] fees = sp.payouts.fees()
Destinations de retrait
Save where your money goes once, confirm it in your dashboard, then pay out with destination_id only. A stolen API key cannot add an address and empty your balance to it.
| Point d'accès | Description |
|---|---|
GET/v1/payout-destinations | Your destinations (filters kind and status) and the settings that apply to them. |
POST/v1/payout-destinations | Saves a destination. It starts as pending_confirmation. |
GET/v1/payout-destinations/{id} | Consulter une destination de retrait |
PATCH/v1/payout-destinations/{id} | Changes the label only. New details make a new destination, to confirm again. |
DELETE/v1/payout-destinations/{id} | Disables a destination. It is kept for your history, never erased. |
Champs par type
| Champ | Description |
|---|---|
kindobligatoirechaîne | crypto, mobile_money or bank. |
labelchaîne | Your own name for this destination, up to 80 characters. |
assetnetworkaddress | Crypto (required): the asset, the network and the address, checked against the network format. |
memo | Crypto: memo or tag, when the network needs one. |
methodphone | Mobile money (required): the method code, for example wave_sn or orange_cm, and the number in international format. |
operator | Mobile money: the operator, when the method needs one. |
holderbank_name | Bank (required): the account holder and the name of the bank. |
ibanaccount_number | Bank: either an IBAN, which is checked, or a local account number (8 to 34 letters and digits). |
swift_bic | Bank, optional: the SWIFT/BIC code. |
country | Bank: two-letter country code. With an IBAN, it is taken from the IBAN. |
currency | Bank (required): one of settings.bank_currencies. Mobile money: by default the currency of the method. Payouts are never converted. |
pending_confirmation. A team member confirms it in the dashboard, under Payout destinations, by typing their password again. An API key can never confirm a destination. Depending on the platform settings, the confirmation must come from another team member than the one who added it.# 1. save it: status 'pending_confirmation' curl -X POST https://soncopay.example/v1/payout-destinations \ -H "X-Api-Key: sp_test_…" \ -H "Content-Type: application/json" \ -d '{"kind":"crypto","asset":"USDT","network":"TRON","address":"TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE","label":"Treasury"}' # 2. a team member confirms it in the dashboard (the password is asked again) # 3. pay out to it once usable is true curl -X POST https://soncopay.example/v1/payouts \ -H "X-Api-Key: sp_test_…" \ -H "Content-Type: application/json" \ -d '{"destination_id":"pd_5c1e…","amount":"50","reference":"demo-payout-9"}'
// 1. save it: status 'pending_confirmation' const dest = await sp.payoutDestinations.create({ kind: 'crypto', asset: 'USDT', network: 'TRON', address: 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE', label: 'Treasury', }); // 2. a team member confirms it in the dashboard (the password is asked again) // 3. pay out to it once dest.usable is true try { await sp.payouts.create({ destination_id: dest.id, amount: '50', reference: 'demo-payout-9' }); } catch (err) { if (err.code === 'destination_cooling_down') console.log(err.usableFrom); else throw err; } const { items, settings } = await sp.payoutDestinations.list({ status: 'active' }); await sp.payoutDestinations.update(dest.id, { label: 'Main wallet' }); await sp.payoutDestinations.disable(dest.id);
// mobile money destination: status 'pending_confirmation' $dest = $sp->payoutDestinations->create([ 'kind' => 'mobile_money', 'method' => 'wave_sn', 'phone' => '+221770000000', 'label' => 'Demo payouts', ]); // confirmed in the dashboard, then: try { $sp->payouts->create(['destination_id' => $dest['id'], 'amount' => '10000']); } catch (ApiException $e) { if ($e->getErrorCode() === 'destination_cooling_down') { echo $e->getUsableFrom(); } } $page = $sp->payoutDestinations->list(['kind' => 'mobile_money']); $page['settings']['cooldown_hours']; $sp->payoutDestinations->retrieve($dest['id']);
# bank account: status "pending_confirmation" dest = sp.payout_destinations.create( kind="bank", holder="Demo Store", bank_name="Demo Bank", account_number="BJ0610100100144390000769", country="BJ", currency="XOF", ) # confirmed in the dashboard, then: try: payout = sp.payouts.create(destination_id=dest["id"], amount="50000") payout["status"] # "awaiting_manual", then "completed" with bank_reference except ApiError as e: if e.code == "destination_cooling_down": print(e.usable_from) page = sp.payout_destinations.list(kind="bank") sp.payout_destinations.disable(dest["id"])
Délai de sécurité
In live mode, a confirmed destination can be used from usable_from, at the end of a security waiting period that starts at confirmation. usable tells you whether a payout can use it now. There is no waiting period in test mode.
| État | Signification |
|---|---|
| pending_confirmation | Saved, waiting for confirmation in the dashboard. |
| active | Confirmed. Usable from usable_from. |
| disabled | Disabled by you or by our team, kept for your history. |
| rejected | Refused by our team. |
Réglages renvoyés par la liste
GET /v1/payout-destinations returns items and settings. These settings are set by the platform and can change: read them rather than writing them in your code.
| Réglage | Signification |
|---|---|
cooldown_hours | Waiting period after confirmation, in hours (0 in test mode). |
require_saved_destination | When true, every payout must use destination_id. |
second_confirmer | When true, another team member must confirm a destination you added. |
api_keys_can_create | When false, destinations can only be added from the dashboard. |
bank_currencies | Currencies accepted for bank transfers. |
bank_payout_feesbank_payout_min | Fee and minimum amount of a bank transfer, per currency. |
encryption_ready | When false, mobile money and bank destinations cannot be saved right now. |
Coordonnées masquées
Full phone numbers, IBANs and account numbers are stored encrypted and never returned. details shows what you need to recognise a destination: address for crypto, phone_masked for mobile money, iban_masked or account_masked for a bank account.
{
"id": "pd_5c1e…", "object": "payout_destination", "livemode": true,
"kind": "mobile_money", "label": "Demo payouts", "currency": "XOF",
"status": "active", "usable": false, "usable_from": "2026-10-03T09:00:00+00:00",
"details": { "method_code": "wave_sn", "country": "SN", "phone_masked": "+221•••••000" },
"created_via": "api", "confirmed_at": "2026-10-02T09:00:00+00:00", …
}
Codes d'erreur
These errors carry a stable code in the body, next to detail.
| Code | HTTP | Signification |
|---|---|---|
destination_not_confirmed | 409 | The destination was not confirmed in the dashboard yet. |
destination_cooling_down | 409 | Confirmed, but the waiting period is not over: the body carries usable_from. |
destination_disabled | 409 | The destination is disabled. |
destination_mode_mismatch | 409 | A test destination with a live key, or the reverse. |
destination_duplicate | 409 | The same details are already saved: the body carries destination_id. |
destination_limit | 409 | 50 open destinations at most per mode. |
destination_currency_mismatch | 422 | asset differs from the currency of the destination: payouts are not converted. |
destination_required | 422 | Your account only pays out to saved destinations: send destination_id. |
destination_invalid | 422 | Invalid details: the body carries field, the field to fix. |
destination_not_found | 404 | Unknown destination. |
api_key_not_allowed | 403 | Destinations can only be added from the dashboard. |
data_key_missing | 503 | Mobile money and bank destinations cannot be saved right now. |
{
"detail": "…",
"code": "destination_cooling_down",
"usable_from": "2026-10-03T09:00:00+00:00"
}
Modes de règlement
Your account is in one of two settlement modes. You choose it in the dashboard (Settings, password required), unless Sonco Pay has locked the mode of your account:
- Custody (default): each paid payment is credited to your balance, minus the fee. You withdraw when you want.
- Automatic: each paid payment is credited, then its net amount is sent automatically to your settlement destination for that currency, as a normal payout (payout fee, review above a threshold, same statuses and webhooks). One payment gives at most one automatic payout; small amounts add up until they reach the minimum payout.
Choose a settlement destination for each currency (USDT, XOF…) or for a currency on one network (USDT:TRON, which wins over USDT) among your confirmed payout destinations, in the dashboard or with PUT /v1/settlement/routes. Without a usable destination, or when a limit or a block applies, the amount stays in your balance and the settlement waits; the reason is given in GET /v1/settlement and in the payment timeline.
GET /v1/payments/{id} carries settlement: mode, status (pending, held, settled, failed, cancelled), reason and payout_id. The payment.paid webhook carries settlement.mode; when the payout is created you receive payment.settled, then the usual payout.* webhooks. Payouts carry origin: request or settlement.
GET /v1/settlement also tells whether you can change the mode yourself: locked, can_change and blocked_by (locked or platform). Turning on automatic settlement needs a usable settlement destination; the change applies to payments paid after it, and the account owners receive a security email.
curl -X PUT https://soncopay.example/v1/settlement/routes \ -H "X-Api-Key: sp_test_…" \ -H "Content-Type: application/json" \ -d '{"scope":"USDT","destination_id":"pd_3c1e…"}'
{
"event": "payment.settled",
"data": {
"id": "pay_3f9a…", "status": "paid", "net_amount": "24.8886",
"settlement": {
"mode": "auto", "status": "settled",
"payout_id": "po_7b2d…", "payout_amount": "23.8886",
"payout_fee": "1", "asset": "USDT"
}, …
}
}
Chronologies
GET/v1/payments/{id}/timeline
GET/v1/payouts/{id}/timeline
The steps of a payment or a payout, in order, with their time and the status after each step. A timeline shows no internal detail: give its trace_id to our support when you ask about an operation.
step is a stable code: translate it in your interface. label is an English text, status the status after the step and data the useful public references (amounts, method, transaction hash).
Étapes d'un paiement
| Étape | Signification |
|---|---|
created | Créé |
method_selected | Moyen de paiement choisi |
awaiting_customer | En attente du client |
attempt_failed | Tentative refusée |
confirming | Transfert détecté, confirmation en cours |
deposit_attributed | Transfert rapproché par notre équipe |
paid | Payé et crédité |
expired | Expirée |
failed | Échouée |
webhook_sent | Notification livrée |
webhook_failed | Notification non livrée |
Étapes d'un retrait
| Étape | Signification |
|---|---|
requested | Retrait demandé |
under_review | En cours de vérification |
approved | Approuvée |
rejected | Refusée |
submitted | Envoyé pour traitement |
sent | Accepté pour envoi |
unknown | En cours de vérification par notre équipe |
completed | Terminée |
failed | Échouée |
webhook_sent | Notification livrée |
webhook_failed | Notification non livrée |
curl https://soncopay.example/v1/payouts/po_8b2d…/timeline -H "X-Api-Key: sp_test_…" # or /v1/payments/{id}/timeline
const tl = await sp.payouts.timeline(payout.id); // or sp.payments.timeline(payment.id) for (const e of tl.events) console.log(e.occurred_at, e.step, e.status);
$tl = $sp->payouts->timeline($payout['id']); // or $sp->payments->timeline($id) foreach ($tl['events'] as $e) { echo $e['occurred_at'], ' ', $e['step'], PHP_EOL; }
tl = sp.payouts.timeline(payout["id"]) # or sp.payments.timeline(payment_id) for e in tl["events"]: print(e["occurred_at"], e["step"], e["status"])
{
"object": "timeline",
"subject": { "type": "payout", "id": "po_8b2d…", "status": "completed" },
"livemode": true, "trace_id": "4f0c…",
"events": [
{ "step": "requested", "label": "Payout requested", "status": "queued",
"occurred_at": "2026-10-02T09:00:00+00:00", "data": { "amount": "50", "fee": "1", "asset": "USDT" } },
{ "step": "submitted", "label": "Sent for processing", "status": "submitting", … },
{ "step": "completed", "label": "Completed", "status": "completed", "data": { "tx_hash": "…" } }
]
}
Relevés
GET/v1/statements
GET/v1/statements.csv
Your account statement per currency, read from our double entry books: the opening balance, every line with the balance after it, and the closing balance. The opening balance plus the lines always equals the closing balance.
| Paramètre | Description |
|---|---|
currency | One currency, for example USDT. By default, every currency of your account. |
from | Start of the period: YYYY-MM-DD or ISO 8601. |
to | End of the period, included: YYYY-MM-DD or ISO 8601. |
limit | From 1 to 1000. Default: 200. |
cursor | The next_cursor of the previous page, while it is not null. |
Lignes du relevé
| Champ | Description |
|---|---|
id | ID of the accounting line. |
date | Posting time, ISO 8601. |
type | payment, fee, payout, payout_fee, payout_release or adjustment. |
label | Stable English label, for example Payment received: translate it in your interface. |
description | Reason of an adjustment, when there is one. |
amount | Signed amount: positive when it credits your balance, negative when it debits it. |
balance_after | Balance of the currency after this line. |
operation | The related payment or payout (type and id), or null. |
GET /v1/statements.csv takes the same currency, from and to and returns the whole period as CSV, with the columns date, currency, type, label, description, amount, balance_after, operation_type, operation_id and line_id.
curl "https://soncopay.example/v1/statements?currency=USDT&from=2026-10-01&to=2026-10-31" \ -H "X-Api-Key: sp_test_…" curl "https://soncopay.example/v1/statements.csv?currency=USDT&from=2026-10-01" \ -H "X-Api-Key: sp_test_…" -o statement.csv
const st = await sp.statements.list({ currency: 'USDT', from: '2026-10-01', to: '2026-10-31' }); st.opening; st.closing; for await (const line of sp.statements.listAll({ currency: 'USDT', from: '2026-10-01' })) { // follows next_cursor for you } const csv = await sp.statements.csv({ currency: 'USDT', from: '2026-10-01', to: '2026-10-31' });
$st = $sp->statements->list(['currency' => 'USDT', 'from' => '2026-10-01', 'to' => '2026-10-31']); foreach ($sp->statements->listAll(['currency' => 'USDT']) as $line) { // follows next_cursor for you } $csv = $sp->statements->csv(['currency' => 'USDT', 'from' => '2026-10-01']);
st = sp.statements.list(currency="USDT", from_="2026-10-01", to="2026-10-31") for line in sp.statements.list_all(currency="USDT", from_="2026-10-01"): pass # follows next_cursor for you csv = sp.statements.csv(currency="USDT", from_="2026-10-01", to="2026-10-31")
{
"livemode": true, "currency": "USDT",
"from": "2026-10-01T00:00:00+00:00", "to": "2026-11-01T00:00:00+00:00",
"currencies": ["USDT"],
"opening": { "USDT": "100" },
"lines": [
{ "id": 5120, "date": "2026-10-02T09:12:40+00:00", "currency": "USDT",
"type": "payment", "label": "Payment received", "description": null,
"amount": "25.0137", "balance_after": "125.0137",
"operation": { "type": "payment", "id": "pay_3f9a…" } },
…
],
"closing": { "USDT": "124.8886" },
"next_cursor": null
}
Factures et abonnements
POST/v1/invoices
POST /v1/invoices creates a draft (issue: true gives it a number, send: true also sends it). Lines carry description, quantity, unit_amount and an optional tax_rate (percent). The customer pays on hosted_url (/i/… short link in SMS) with every active method; each payment follows the usual flow (credit, commission, settlement).
Statuses: draft, open, partially_paid (with allow_partial), overdue, paid, void. Actions: /issue, /send, /remind (429 reminder_too_soon), /void, /mark-paid (paid outside Sonco Pay), /timeline and /pdf.
Créer un abonnement
POST/v1/subscriptions
A subscription creates one invoice per due date (interval: week, month, quarter, year or custom with interval_days), a few days in advance, then reminds the customer before the due date (reminders.days_before, for example 7, 3 and 1), on the day and after it (after_due). An unpaid invoice makes the subscription unpaid; after dunning.grace_days it is canceled, paused or kept, as you choose.
Automatic card debit is not available yet: auto_debit: true fails with auto_debit_unavailable. Card, crypto, mobile money and Binance Pay are paid from the invoice link. Reminders go by email, and by SMS or WhatsApp only to customers who agreed (consent); a STOP reply opts them out.
Creations accept the Idempotency-Key header (24 hours; the same key with another body returns 409 idempotency_key_reused) or an external_ref. Lists return items, has_more and next_offset. In test mode, POST /v1/billing/test-clock moves time forward to try due dates and reminders.
curl -X POST https://soncopay.example/v1/invoices \ -H "X-Api-Key: sp_test_…" \ -H "Idempotency-Key: demo-invoice-1042" \ -H "Content-Type: application/json" \ -d '{"customer":{"name":"Demo Customer","email":"[email protected]","phone":"+2250700000000", "consent":{"email":true,"sms":true}},"currency":"XOF","due_days":7, "lines":[{"description":"Demo service","quantity":1,"unit_amount":"15000"}],"send":true}'
const invoice = await sp.invoices.create({ customer: { name: 'Demo Customer', email: '[email protected]', phone: '+2250700000000' }, currency: 'XOF', due_days: 7, send: true, lines: [{ description: 'Demo service', quantity: 1, unit_amount: '15000' }], }, { idempotencyKey: 'demo-invoice-1042' }); invoice.hosted_url;
$invoice = $sp->invoices->create([
'customer' => ['name' => 'Demo Customer', 'email' => '[email protected]'],
'currency' => 'XOF', 'due_days' => 7, 'send' => true,
'lines' => [['description' => 'Demo service', 'quantity' => 1, 'unit_amount' => '15000']],
], ['idempotency_key' => 'demo-invoice-1042']);
$invoice['hosted_url'];
invoice = sp.invoices.create(
customer={"name": "Demo Customer", "email": "[email protected]"},
currency="XOF", due_days=7, send=True,
lines=[{"description": "Demo service", "quantity": 1, "unit_amount": "15000"}],
idempotency_key="demo-invoice-1042",
)
invoice["hosted_url"]
curl -X POST https://soncopay.example/v1/subscriptions \ -H "X-Api-Key: sp_test_…" \ -H "Content-Type: application/json" \ -d '{"customer_id":"cus_…","description":"Demo monthly plan","amount":"10000","currency":"XOF", "interval":"month","preferred_method":"mobile_money","reminders":{"days_before":[7,3,1]}, "dunning":{"grace_days":7,"action":"pause"}}'
const sub = await sp.subscriptions.create({ customer_id: customer.id, description: 'Demo monthly plan', amount: '10000', currency: 'XOF', interval: 'month', preferred_method: 'mobile_money', reminders: { days_before: [7, 3, 1] }, dunning: { grace_days: 7, action: 'pause' }, }); await sp.billing.advanceTestClock({ advance_days: 30 }); // test mode
$sub = $sp->subscriptions->create([
'customer_id' => $customer['id'], 'description' => 'Demo monthly plan',
'amount' => '10000', 'currency' => 'XOF', 'interval' => 'month',
'reminders' => ['days_before' => [7, 3, 1]], 'dunning' => ['grace_days' => 7, 'action' => 'pause'],
]);
sub = sp.subscriptions.create(
customer_id=customer["id"], description="Demo monthly plan", amount=10000, currency="XOF",
interval="month", reminders={"days_before": [7, 3, 1]}, dunning={"grace_days": 7, "action": "pause"},
)
sp.billing.advance_test_clock(days=30) # test mode
Webhooks
Set your webhook URL in the dashboard. We send a POST with a JSON body for each event, and retry for about three days until your server answers with a 2xx status.
| Événement | Quand |
|---|---|
payment.confirming | A deposit was detected and is being confirmed. |
payment.paid | The payment is confirmed. Fulfil the order. |
payment.settled | Automatic settlement: the payout of this payment was created (settlement.payout_id). |
payment.expired | The payment was not made in time. |
payout.completed | The payout is done: tx_hash (crypto) or bank_reference (bank transfer) is filled in. |
payout.failed | The payout failed or was rejected; the amount is back in your balance. |
invoice.* | An invoice was created, sent, reminded, partly paid, paid, became overdue or was voided. |
subscription.* | A subscription was created, updated, paused, resumed, became unpaid, was canceled or ended. |
ping | Sent when you test your webhook from the dashboard. |
En-têtes
| En-tête | Description |
|---|---|
X-SoncoPay-Signature | HMAC SHA512 of the raw body, in hexadecimal. See Verify a signature. |
X-SoncoPay-Event | The event name, also present in the body. |
Content-Type | application/json |
{
"event": "payment.paid",
"created_at": "2026-10-01T14:31:07+00:00",
"data": {
"id": "pay_3f9a…", "order_id": "1042", "status": "paid",
"received_amount": "25.0137", "asset": "USDT",
"fee": "0.1251", "net_amount": "24.8886",
"payer": { "email": "[email protected]" },
"settlement": { "mode": "custody", "status": null, "payout_id": null },
"late": false, …
}
}
Vérifier une signature
Each webhook carries X-SoncoPay-Signature: the HMAC SHA512, in hexadecimal, of the raw request body, keyed with your webhook signing secret. Compute it on the raw body, before parsing the JSON, and compare in constant time.
GET /v1/payments/{id} before shipping goods of high value.const crypto = require("crypto"); const expected = crypto.createHmac("sha512", process.env.SP_WEBHOOK_SECRET).update(rawBody).digest("hex"); const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(req.headers["x-soncopay-signature"] || ""));
$expected = hash_hmac('sha512', file_get_contents('php://input'), getenv('SP_WEBHOOK_SECRET')); $ok = hash_equals($expected, $_SERVER['HTTP_X_SONCOPAY_SIGNATURE'] ?? '');
import hmac, hashlib expected = hmac.new(secret.encode(), raw_body, hashlib.sha512).hexdigest() ok = hmac.compare_digest(expected, request.headers.get("X-SoncoPay-Signature", ""))
Erreurs
Errors use standard HTTP codes and a JSON body {"detail": "…"}. Some errors also carry a stable code that you can test in your code: {"detail": "…", "code": "kyb_required"}.
| Code | Signification |
|---|---|
401 | Missing or invalid API key. |
403 | Account suspended, payments or payouts blocked for your account, missing permission (permission_denied), or live mode before your business verification is approved (kyb_required). |
404 | Payment or payout not found in this mode. |
409 | Conflict: payment expired, already paid, or order_id reused with another amount. |
429 | Too many mobile money attempts for this payment. |
422 | Invalid input, unsupported currency, amount below the minimum, insufficient balance. |
502 | A payment provider or the exchange rate is temporarily unavailable. Retry later. |
503 | Payments or payouts are temporarily closed, or the service is under maintenance: the body then carries "maintenance": true and a Retry-After header. Retry later. |
Error messages (detail, last_error, the error of a payout) never name our internal processors. Payout destination errors are listed in Payout destinations.
{
"detail": "…",
"code": "kyb_required"
}