Payments
Collect money from a customer's mobile money wallet.
Initiate a payment. Returns immediately with a non-final status.
Request body#
{
"method": "mobile_money_mtn",
"country": "CG",
"amount": { "value": "11800", "currency": "XAF" },
"reference": "ORDER-123",
"idempotency_key": "a4f3c2d1-0000-0000-0000-000000000000",
"customer": { "phone": "242064661331" },
"description": "Payment for order ORDER-123",
"charge": {
"items": [
{ "name": "Product name", "quantity": 2, "unit_price": "5000", "total": "10000" }
],
"subtotal": "10000"
},
"tax": { "inclusive": false, "amount": "1800", "rate": "18.00", "type": "VAT" },
"metadata": { "order_id": "ORDER-123" }
}| Field | Required | Description |
|---|---|---|
method | Yes | Payment method code (from /payment-methods) |
country | For country-scoped methods | ISO 3166-1 alpha-2 country code |
amount.value | Yes | Total amount to charge, as a minor-units string |
amount.currency | Yes | ISO-4217 currency code |
idempotency_key | Yes | A unique UUID for this request |
customer | Yes | Customer details. The required properties are set by the selected method's required_customer_fields. For mobile money this is phone |
reference | No | Your own order or transaction reference |
description | No | Short description (may be shown to the customer) |
charge | No | Line items and subtotal, for receipts and reporting |
tax | No | Tax breakdown |
metadata | No | Any key/value pairs, stored and echoed back on webhooks |
Tax#
amount.value is always the total you are charging the customer. tax.inclusive describes how that total was arrived at.
tax.inclusive | Meaning |
|---|---|
true | Tax is already contained within amount.value. |
false | amount.value is the subtotal plus tax. In this case charge.subtotal + tax.amount must equal amount.value. |
In the example above, the subtotal is 10000, tax is 1800, and amount.value is 11800. The customer is charged 11800.
Customer phone number#
Send the phone number as digits only: the country code followed by the national number, with no plus sign, no spaces and no separators. For Republic of Congo this is 12 digits, the 242 country code followed by the 9-digit national number, keeping its leading zero.
Examples are illustrative. The phone numbers, identifiers and references shown throughout this documentation are sample values for illustration. Use your own customer and order data in live calls.
| Example | Valid? |
|---|---|
242064661331 | Valid |
+242064661331 | No plus sign |
064661331 | Country code missing |
242 064 661 331 | No spaces |
Tax#
When tax.inclusive is true, charge.subtotal equals amount.value and tax.amount is the portion already inside that total. When tax is sent with inclusive: false, charge.subtotal is required, because the rule charge.subtotal + tax.amount = amount.value depends on it. Send neither tax nor charge and no tax validation applies.
Tax is yours to determine. The gateway records the tax values you send. It does not calculate tax or determine the applicable treatment, and nothing here is tax advice.
Line-item arithmetic#
When you send charge, the figures must be internally consistent. For each item, quantity multiplied by unit_price must equal item.total, and the item totals must sum to charge.subtotal. In the example, 2 at 5000 is 10000, which is the subtotal.
Examples#
curl -X POST "{BASE_URL}/payments" \
-H "X-Api-Key: <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"method": "mobile_money_mtn",
"country": "CG",
"amount": { "value": "11800", "currency": "XAF" },
"reference": "ORDER-123",
"idempotency_key": "a4f3c2d1-0000-0000-0000-000000000000",
"customer": { "phone": "242064661331" }
}' const res = await fetch("{BASE_URL}/payments", {
method: "POST",
headers: {
"X-Api-Key": process.env.MALY_API_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({
method: "mobile_money_mtn",
country: "CG",
amount: { value: "11800", currency: "XAF" },
reference: "ORDER-123",
idempotency_key: crypto.randomUUID(),
customer: { phone: "242064661331" }
})
});
const payment = await res.json();
console.log(payment.id, payment.status);import os, uuid, requests
res = requests.post(
"{BASE_URL}/payments",
headers={
"X-Api-Key": os.environ["MALY_API_KEY"],
"Content-Type": "application/json",
},
json={
"method": "mobile_money_mtn",
"country": "CG",
"amount": {"value": "11800", "currency": "XAF"},
"reference": "ORDER-123",
"idempotency_key": str(uuid.uuid4()),
"customer": {"phone": "242064661331"},
},
)
payment = res.json()
print(payment["id"], payment["status"])<?php
$ch = curl_init("{BASE_URL}/payments");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"X-Api-Key: " . getenv("MALY_API_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"method" => "mobile_money_mtn",
"country" => "CG",
"amount" => ["value" => "11800", "currency" => "XAF"],
"reference" => "ORDER-123",
"idempotency_key" => bin2hex(random_bytes(16)),
"customer" => ["phone" => "242064661331"],
]),
]);
$payment = json_decode(curl_exec($ch), true);
echo $payment["id"], " ", $payment["status"];Response#
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "pending",
"method": "mobile_money_mtn",
"country": "CG",
"amount": { "value": "11800", "currency": "XAF" },
"reference": "ORDER-123",
"idempotency_key": "a4f3c2d1-0000-0000-0000-000000000000",
"provider_reference": null,
"failure_reason": null,
"created_at": "2026-06-27T09:00:00Z",
"updated_at": "2026-06-27T09:00:00Z"
}Get a payment's status#
Returns the same object shape, with the current status and, once final, a failure_reason if applicable.
curl "{BASE_URL}/payments/550e8400-e29b-41d4-a716-446655440000" \
-H "X-Api-Key: <your-api-key>"Payment statuses#
| Status | Meaning |
|---|---|
| pending | Awaiting customer approval |
| processing | Accepted by the provider and being processed |
| success | Funds received; your wallet has been credited |
| failed | Refused by the customer or provider |
| expired | Customer did not respond in time |
List payments#
Filterable list of your payments.
curl "{BASE_URL}/payments?status=success&method=mobile_money_mtn&limit=50&offset=0" \
-H "X-Api-Key: <your-api-key>"Optional filters: status, method, from_date, to_date, limit (max 200), offset.
List response#
List endpoints return a page wrapper, not a bare array. The same shape is used by payments, payouts and transactions.
{
"items": [ ... ],
"total": 143,
"limit": 50,
"offset": 0
}| Field | Meaning |
|---|---|
items | The results for this page |
total | Count of all matching records, before pagination |
limit | Page size applied (max 200) |
offset | Start position of this page |
To page, increase offset by limit until offset plus the number of returned items reaches total. A query with no matches returns an empty items array with total of 0, not a 404.
Date filters#
from_date and to_date take ISO 8601 timestamps in UTC. Bounds are inclusive. There is no maximum query window.
Maly Tech Ltd. This guide is provided for information. Where it differs from your signed agreement, the agreement applies.