The SP-Heavy API lets you accept mobile-money payments (MTN MoMo & Orange Money), send payouts, manage a wallet balance, create hosted-checkout links, and settle funds to your bank — over a clean, predictable REST API.
All requests are made to:
https://api.spheavy.com/v1
Responses are JSON. Successful responses wrap the result in a data field; errors use an error envelope (see Errors). Amounts are integers in the currency's minor unit — for XAF/XOF that is the franc itself (no decimals), so1500 means 1,500 FCFA.
Try the API right here — no setup. Click Generate test keys to spin up a throwaway sandbox account, pick an operation, fill the form, and hit Send request. You'll see the exact cURL you'd run and the live JSON response. Use the Test numbers to trigger success or specific failures.
Fill a form and send a real request to the sandbox API. Nothing is charged — use the test numbers to trigger each outcome. Every call shows the exact request and response.
/v1/payments/collectThe payment API authenticates with an API key pair sent as headers on every request:
X-Public-KeystringrequiredX-Secret-KeystringrequiredCreate and manage keys in the dashboard under API Keys. The secret is shown once at creation. Never embed the secret key in a browser or mobile app — call SP-Heavy from your server, or use a Payment Link / hosted checkout for client-side flows.
curl https://api.spheavy.com/v1/balance \ -H "X-Public-Key: pk_test_xxx" \ -H "X-Secret-Key: sk_test_xxx"
Send and receive JSON. A minimal request and its wrapped response:
curl https://api.spheavy.com/v1/payments/status/COL_abc123 \ -H "X-Public-Key: pk_test_xxx" \ -H "X-Secret-Key: sk_test_xxx"
{
"data": {
"reference": "COL_abc123",
"status": "SUCCESSFUL",
"amount": "1500",
"currency": "XAF"
}
}Every response includes an X-Request-Id header — quote it in support requests.
Errors return the appropriate HTTP status and a consistent envelope:
{
"error": {
"code": "PROVIDER_ERROR",
"message": "The provider did not respond in time",
"retryable": true,
"outcome": "unknown",
"details": null,
"requestId": "req_9f3a…"
}
}The table below is fetched live from GET /v1/errors, which is generated from the same source as the error responses — so it always matches what the API actually does.
Make money-moving requests safe to retry by sending an Idempotency-Key header oncollect, disburse and refunds. Replaying the same key returns the original result instead of charging twice.
curl https://api.spheavy.com/v1/payments/collect \
-H "X-Public-Key: pk_test_xxx" -H "X-Secret-Key: sk_test_xxx" \
-H "Idempotency-Key: 7c1d-collect-order-1024" \
-H "Content-Type: application/json" \
-d '{ "amount": 1500, "provider": "MTN", "phoneNumber": "237670000000" }'In sandbox (test keys), payments are handled by a deterministic simulator — no real money and no provider credentials needed. Use these magic phone numbers to trigger each outcome; they return the exact status & message live mode returns.
Numbers are matched on the last 4 digits, so any valid prefix works (MTN2376… or Orange 2376…). A collection is accepted asPENDING and resolves a few seconds later — exactly like the real async callback — so you can watch webhooks fire, or poll GET /v1/payments/status/:reference?sync=true for an instant result.
…0000 (e.g. 237670000000)Success…0001 (e.g. 237670000001)Failed…0002 (e.g. 237670000002)Failed…0003 (e.g. 237670000003)Failed…0004 (e.g. 237670000004)Rejected…0009 (e.g. 237670000009)Pending# Insufficient-funds scenario
curl https://api.spheavy.com/v1/payments/collect \
-H "X-Public-Key: pk_test_xxx" -H "X-Secret-Key: sk_test_xxx" \
-H "Content-Type: application/json" \
-d '{ "amount": 1500, "provider": "MTN", "phoneNumber": "237670000001" }'
# → PENDING, then resolves FAILED with message "NOT_ENOUGH_FUNDS"Official SDKs for JavaScript, TypeScript, Python, and PHP (Laravel). Pick your stack below — every method returns the unwrapped data and raises a typed error on failure. They share the same surface (payments, transactions, paymentLinks,settlements, …).
Install
npm install @yukwaindustries/sph-sdk
Collect a payment
import { SPHeavyClient, SPHeavyError } from '@yukwaindustries/sph-sdk';
const sp = new SPHeavyClient({
baseUrl: 'https://api.spheavy.com',
publicKey: process.env.SPHEAVY_PUBLIC_KEY!,
secretKey: process.env.SPHEAVY_SECRET_KEY!,
});
try {
const tx = await sp.payments.collect({
amount: 1500,
currency: 'XAF',
provider: 'MTN',
phoneNumber: '237670000000',
});
console.log(tx.reference, tx.status); // fully typed
} catch (err) {
if (err instanceof SPHeavyError) console.error(err.code, err.status, err.requestId);
}/v1/payments/collectRequest money from a customer's mobile-money account (a request-to-pay / STK push). The customer approves the prompt on their phone; the final status arrives by webhook.
amountintegerrequiredproviderstringrequiredphoneNumberstringrequiredcurrencystringdescriptionstringmetadataobjectcurl https://api.spheavy.com/v1/payments/collect \
-H "X-Public-Key: pk_test_xxx" -H "X-Secret-Key: sk_test_xxx" \
-H "Idempotency-Key: order-1024" -H "Content-Type: application/json" \
-d '{
"amount": 1500,
"currency": "XAF",
"provider": "MTN",
"phoneNumber": "237670000000",
"description": "Order #1024"
}'Response 202
{
"data": {
"reference": "COL_3094fe0589a6bf56",
"type": "COLLECTION",
"provider": "MTN",
"status": "PENDING",
"amount": "1500",
"fee": "23",
"net": "1477",
"currency": "XAF",
"phoneNumber": "237670000000"
}
}/v1/payments/disbursePay money out from your wallet balance to a mobile-money account. The amount is reserved from your balance immediately; a failed payout is automatically refunded.
amountintegerrequiredproviderstringrequiredphoneNumberstringrequiredcurrencystringcurl https://api.spheavy.com/v1/payments/disburse \
-H "X-Public-Key: pk_test_xxx" -H "X-Secret-Key: sk_test_xxx" \
-H "Idempotency-Key: payout-55" -H "Content-Type: application/json" \
-d '{ "amount": 5000, "currency": "XAF", "provider": "ORANGE", "phoneNumber": "237690000000" }'/v1/payments/status/:referenceFetch the authoritative status of a transaction. Pass ?sync=true to force a live check with the provider if it is still pending.
curl "https://api.spheavy.com/v1/payments/status/COL_3094fe0589a6bf56?sync=true" \ -H "X-Public-Key: pk_test_xxx" -H "X-Secret-Key: sk_test_xxx"
Status values: PENDING, PROCESSING, SUCCESSFUL, FAILED, REVERSED.
/v1/transactionsList transactions for the key's environment, most recent first.
pageintegerlimitintegerstatusstringtypestringcurl "https://api.spheavy.com/v1/transactions?status=SUCCESSFUL&limit=20" \ -H "X-Public-Key: pk_test_xxx" -H "X-Secret-Key: sk_test_xxx"
/v1/transactions/:reference/refundRefund a successful collection. The net amount (after fees) is debited from your wallet and the transaction is marked REVERSED. Idempotent.
curl -X POST https://api.spheavy.com/v1/transactions/COL_3094fe0589a6bf56/refund \ -H "X-Public-Key: pk_test_xxx" -H "X-Secret-Key: sk_test_xxx"
/v1/balanceReturn your wallet balances for the key's environment.
curl https://api.spheavy.com/v1/balance \ -H "X-Public-Key: pk_test_xxx" -H "X-Secret-Key: sk_test_xxx"
/v1/payment-linksCreate a shareable, hosted-checkout link for a fixed amount. Send customers the returned checkoutUrl — they pay on an SP-Heavy-hosted page with no integration on your side.
amountintegerrequiredcurrencystringdescriptionstringsuccessUrlstringcurl https://api.spheavy.com/v1/payment-links \
-H "X-Public-Key: pk_test_xxx" -H "X-Secret-Key: sk_test_xxx" \
-H "Content-Type: application/json" \
-d '{ "amount": 5000, "currency": "XAF", "description": "Invoice #42" }'These endpoints back the hosted page and need no API key — the link token is the capability. Useful if you build your own checkout UI. The amount is fixed by the link server-side and cannot be tampered with.
/v1/checkout/:tokenReturns display data (business name, amount, currency, description).
/v1/checkout/:token/payInitiates the collection on the merchant's behalf — body { provider, phoneNumber } — then poll:
/v1/checkout/:token/status/:referencecurl -X POST https://api.spheavy.com/v1/checkout/plink_abc/pay \
-H "Content-Type: application/json" \
-d '{ "provider": "MTN", "phoneNumber": "237670000000" }'/v1/settlementsPay your wallet balance out to the bank account on your (approved) KYC profile. The balance is debited immediately; an operator confirms the bank transfer. List past settlements with GET /v1/settlements.
curl https://api.spheavy.com/v1/settlements \
-H "X-Public-Key: pk_live_xxx" -H "X-Secret-Key: sk_live_xxx" \
-H "Content-Type: application/json" -d '{ "currency": "XAF" }'Set a callback URL on your account to receive transaction events (transaction.successful, transaction.failed,transaction.reversed). Deliveries are durable and retried with backoff.
Each request includes these headers:
X-SPHeavy-EventstringX-SPHeavy-IdstringX-SPHeavy-TimestampstringX-SPHeavy-SignaturestringVerify the signature before trusting a webhook (Node/Express example):
import crypto from 'crypto';
app.post('/webhooks/spheavy', express.raw({ type: '*/*' }), (req, res) => {
const ts = req.header('X-SPHeavy-Timestamp');
const sig = req.header('X-SPHeavy-Signature');
const body = req.body.toString('utf8'); // the EXACT bytes received
const expected = crypto
.createHmac('sha256', process.env.SPHEAVY_WEBHOOK_SECRET)
.update(`${ts}.${body}`)
.digest('hex');
if (sig !== expected) return res.status(400).send('bad signature');
const event = JSON.parse(body);
// event.event === 'transaction.successful' | ...
res.sendStatus(200); // ack quickly
});Looking for an exhaustive schema reference or a “try it” console? See the interactive API reference.