Introduction

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.

Playground

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.

Sandbox playground
No key

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.


POST/v1/payments/collect
Currency
Provider
Success
Insufficient
Declined
Invalid payer
Pending

Authentication

The payment API authenticates with an API key pair sent as headers on every request:

X-Public-Keystringrequired
Your publishable key, e.g. pk_test_…
X-Secret-Keystringrequired
Your secret key, e.g. sk_test_… — keep this server-side only.

Create 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"

Making requests

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

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.

Idempotency

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" }'

Test numbers

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
Resolves SUCCESSFUL. Any number not listed below also succeeds.
…0001 (e.g. 237670000001)Failed
NOT_ENOUGH_FUNDS — the customer has insufficient funds.
…0002 (e.g. 237670000002)Failed
APPROVAL_REJECTED — the customer declined the prompt.
…0003 (e.g. 237670000003)Failed
EXPIRED — the request timed out before approval.
…0004 (e.g. 237670000004)Rejected
PAYER_NOT_FOUND — rejected immediately at request time.
…0009 (e.g. 237670000009)Pending
Stays PENDING (simulates a customer who never responds).
# 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 SDK

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);
}

Collect a payment

POST/v1/payments/collect

Request 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.

amountintegerrequired
Amount in minor units (e.g. 1500 = 1500 XAF).
providerstringrequired
MTN or ORANGE.
phoneNumberstringrequired
Customer number in international format, e.g. 237670000000.
currencystring
XAF (default), XOF, USD, or EUR.
descriptionstring
Shown on statements / your records.
metadataobject
Arbitrary key/values echoed back to you.
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: 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"
  }
}

Send a payout

POST/v1/payments/disburse

Pay 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.

amountintegerrequired
Amount in minor units.
providerstringrequired
MTN or ORANGE.
phoneNumberstringrequired
Recipient number, international format.
currencystring
XAF (default), XOF, USD, EUR.
curl 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" }'

Check status

GET/v1/payments/status/:reference

Fetch 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.

List transactions

GET/v1/transactions

List transactions for the key's environment, most recent first.

Query parameters
pageinteger
Page number (default 1).
limitinteger
Page size, max 100 (default 20).
statusstring
Filter by status.
typestring
COLLECTION or DISBURSAL.
curl "https://api.spheavy.com/v1/transactions?status=SUCCESSFUL&limit=20" \
  -H "X-Public-Key: pk_test_xxx" -H "X-Secret-Key: sk_test_xxx"

Refund a collection

POST/v1/transactions/:reference/refund

Refund 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"

Balance

GET/v1/balance

Return 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"

Hosted checkout (public)

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.

GET/v1/checkout/:token

Returns display data (business name, amount, currency, description).

POST/v1/checkout/:token/pay

Initiates the collection on the merchant's behalf — body { provider, phoneNumber } — then poll:

GET/v1/checkout/:token/status/:reference
curl -X POST https://api.spheavy.com/v1/checkout/plink_abc/pay \
  -H "Content-Type: application/json" \
  -d '{ "provider": "MTN", "phoneNumber": "237670000000" }'

Settlements

POST/v1/settlements

Pay 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" }'

Webhooks

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-Eventstring
The event type.
X-SPHeavy-Idstring
Unique event id (use it to dedupe).
X-SPHeavy-Timestampstring
Unix-ms timestamp the signature covers.
X-SPHeavy-Signaturestring
HMAC-SHA256 of `timestamp.rawBody`, keyed by your webhook secret.

Verify 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.