Payments, in your code.
A REST API and signed webhooks for verifying Mobile Money payments. Integrate in an afternoon. Ship with confidence.
One API.
Every network.
MTN MoMo. Telecel Cash. AT Money. A single endpoint handles all three. You write the integration once and never think about it again.
Initialize a payment
in four lines.
Send the amount and a reference. Get back a short code your customer can pay to. We take it from there.
import requests
res = requests.post(
'https://api.harpoonsms.com/v1/payments/initialize',
headers={'Authorization': f'Bearer {API_KEY}'},
json={
'amount': 250.00,
'currency': 'GHS',
'reference': 'order_8821',
'customer_phone': '+233241234567',
},
)
payment = res.json()
# payment['hpn_code'] -> 'HKMVR2'
# payment['status'] -> 'pending'
# payment['expires_at'] -> '2026-04-10T12:00:00Z'Everything you need.
Nothing you don't.
REST API
A small, predictable set of endpoints. JSON in, JSON out. Idempotent by default.
Signed Webhooks
HMAC-SHA256 signatures on every payload. Exponential backoff on retries. At-least-once delivery.
Scoped API Keys
Separate keys for live and test. Scoped permissions per key. Rotate without downtime.
Works with
your stack.
If your language can speak HTTP, it can speak Harpoon. No SDKs to install. No lock-in.
Webhooks that
keep their promises.
When a payment lands, we tell you. Signed with HMAC-SHA256. Retried with exponential backoff across eight attempts. Delivered until acknowledged.
{
"event": "payment.succeeded",
"created_at": "2026-04-09T14:22:08Z",
"data": {
"hpn_code": "HKMVR2",
"reference": "order_8821",
"amount": 250.00,
"currency": "GHS",
"network": "mtn",
"payer_phone": "+233241234567",
"verified_at": "2026-04-09T14:22:06Z"
}
}const res = await fetch(
`https://api.harpoonsms.com/v1/payments/${ref}`,
{ headers: { Authorization: `Bearer ${API_KEY}` } }
);
const payment = await res.json();
if (payment.status === 'succeeded') {
await fulfillOrder(payment.reference);
}Verify on demand.
Prefer polling to webhooks? Query any payment by reference or HPN code. The response tells you exactly where it stands — pending, succeeded, partial, overpaid, or expired.
Secure by default.
Every request authenticated. Every webhook signed. Every key scoped. No exceptions.
HMAC signatures
Every webhook is signed with SHA-256. Verify in constant time, reject the rest.
Key rotation
Generate new keys, overlap the old, revoke on your schedule. Zero downtime.
Rate limiting
Predictable limits with clear headers. Burst-friendly, abuse-resistant.