Payments API
The Payment object
{
"id": "8d6ff4fa-da3d-4006-8997-d2780d0e22fe",
"reference": "order-1234",
"amount": 125.00,
"currency": "KES",
"wallet_id": "wlt_DEFAULT00001",
"channel": "mpesa_stk_push",
"status": "succeeded",
"fee": 1.88,
"net_amount": 123.12,
"channel_receipt": "UEAL141VYG",
"callback_url": "https://your-domain.com/callbacks/order-1234",
"refunded_at": null,
"refunded": 0.00,
"refunds": [],
"created_at": "2026-05-19T10:23:11.512Z",
"updated_at": "2026-05-19T10:23:14.881Z"
}| Field | Type | Notes |
|---|---|---|
id | uuid | Internal payment id |
reference | string | Merchant-supplied; unique per merchant |
amount | number | Gross amount in decimal major units (e.g. 125.00 = KES 125.00) |
currency | string | ISO-4217 |
wallet_id | string | Wallet credited when the payment succeeds |
channel | enum | mpesa_stk_push / mpesa_c2b / card / bank_transfer / mobile_money |
status | enum | pending / processing / succeeded / failed / canceled |
fee | number | Platform fee, decimal major units. Locked in at creation |
net_amount | number | amount - fee |
channel_receipt | string | M-Pesa TransID, Stripe charge id, etc — channel-side reference |
callback_url | string | Per-payment callback URL, if one was supplied at creation |
refunded_at | timestamp | null | Set when cumulative refund reaches amount |
refunded | number | Cumulative refunded total (decimal major units) |
refunds | array | All refund slices (newest first) |
POST /v1/payments
Create a payment. Channel-aware: required fields vary.
Common fields
| Field | Type | Required | Notes |
|---|---|---|---|
reference | string | ✓ | Unique per merchant |
amount | number | ✓ | Decimal major units, positive (e.g. 125.00) |
currency | string | ✗ | Defaults to merchant's default_currency |
wallet_id | string | ✗ | Wallet to credit on success — its wlt_… id or its 8-digit number. Defaults to the default wallet; its currency must match currency |
channel | enum | ✗ | mpesa_stk_push / mobile_money / card. Default is currency-aware: TZS and UGX → mobile_money, otherwise mpesa_stk_push |
callback_url | string | ✗ | Absolute https:// URL. When set, this payment's payment.succeeded / payment.failed events are delivered only to this URL (overriding your registered webhook endpoints for this payment), HMAC-signed with your callback signing secret. Omit to fan out to registered endpoints |
M-Pesa STK Push (mpesa_stk_push, Kenya · KES) also requires:
| Field | Type | Notes |
|---|---|---|
payer_phone | string | E.164 with +, e.g. +254712345678 |
Mobile money (mobile_money, Tanzania · TZS and Uganda · UGX) also requires:
| Field | Type | Notes |
|---|---|---|
payer_phone | string | E.164 with + — +255… for TZS, +256… for UGX |
The mobile_money channel is selected for you when currency is TZS or UGX and channel is omitted. The customer approves the charge by entering their mobile-money PIN, so the payment resolves asynchronously (processing → succeeded/failed), just like STK Push — routed automatically to the payer's network by their number:
- TZS — Tanzanian mobile money (Vodacom / M-Pesa, Airtel Money, Mixx by Yas (Tigo), Halotel). Resolved via webhook.
- UGX — Ugandan mobile money (MTN, Airtel). Resolved via webhook.
Card also requires:
| Field | Type | Notes |
|---|---|---|
card_token | string | Tokenized card reference (Stripe payment-method id, etc) |
Example — STK Push (endpoint fan-out)
curl -X POST https://api.teronapay.com/v1/payments \
-u "$KEY:$SECRET" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1234" \
-d '{
"reference": "order-1234",
"amount": 125.00,
"currency": "KES",
"channel": "mpesa_stk_push",
"payer_phone": "+254712345678"
}'Example — STK Push with per-payment callback
curl -X POST https://api.teronapay.com/v1/payments \
-u "$KEY:$SECRET" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-5678" \
-d '{
"reference": "order-5678",
"amount": 200.00,
"currency": "KES",
"channel": "mpesa_stk_push",
"payer_phone": "+254712345678",
"callback_url": "https://your-domain.com/callbacks/order-5678"
}'When callback_url is set, payment.succeeded / payment.failed events go only to that URL (signed with your callback signing secret). Registered endpoints are skipped for this payment.
Example — Tanzania mobile money (TZS)
channel is optional here — currency: "TZS" selects mobile_money automatically. The customer approves on their phone; the payment resolves via webhook.
curl -X POST https://api.teronapay.com/v1/payments \
-u "$KEY:$SECRET" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-9012" \
-d '{
"reference": "order-9012",
"amount": 5000.00,
"currency": "TZS",
"channel": "mobile_money",
"payer_phone": "+255712345678"
}'Example — Uganda mobile money (UGX)
Same channel, currency: "UGX" and a +256… number. UGX amounts are whole shillings.
curl -X POST https://api.teronapay.com/v1/payments \
-u "$KEY:$SECRET" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-9013" \
-d '{
"reference": "order-9013",
"amount": 5000,
"currency": "UGX",
"channel": "mobile_money",
"payer_phone": "+256700000000"
}'Response 201: a Payment object (status processing for async channels, succeeded/failed for sync).
Errors
400 invalid_request— missing required field, wrong currency for channel, malformed phone401 unauthorized— bad API key429 rate_limited— write bucket exceeded (Retry-Afterheader included)
GET /v1/payments
List payments. Newest first.
Query parameters
| Param | Type | Notes |
|---|---|---|
status | enum | Filter to one status |
channel | enum | Filter to one channel |
reference | string | Exact match |
since | RFC3339 | created_at >= since |
before | RFC3339 | created_at < before — also the cursor for pagination |
limit | int | 1..100, default 25 |
Response 200:
{
"data": [ /* Payment objects */ ],
"next_before": "2026-05-19T10:20:00Z"
}next_before is set when the current page is full. Pass it as ?before=... to fetch the next (older) page.
GET /v1/payments/{id}
Retrieve one payment. Includes the full refunds[] array.
Errors
404 not_found— id doesn't exist or belongs to another merchant (we don't differentiate)
POST /v1/payments/{id}/refunds
Issue a refund. See Refunds API for full reference.
Status codes summary
| Code | Meaning |
|---|---|
200 | OK — GET succeeded |
201 | Created — payment created |
400 | Bad request |
401 | Missing or invalid API key |
404 | Payment not found |
409 | Conflict (duplicate reference without idempotency key) |
422 | Precondition failed (e.g. refunding a non-succeeded payment) |
429 | Rate limit exceeded |
500 | Internal error |
