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 | ✗ | Defaults to 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 also requires:
| Field | Type | Notes |
|---|---|---|
payer_phone | string | E.164 with +, e.g. +254712345678 |
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.
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 |
