curl --request POST \
--url https://api.pay.aptahq.com/v1/payouts/quote \
--header 'Content-Type: application/json' \
--header 'X-AptaPay-Signature: <api-key>' \
--data '
{
"amount_minor": 50000,
"currency": "UGX",
"destination_currency": "UGX",
"country": "UG",
"method": "momo",
"destination": {
"type": "momo",
"msisdn": "+256781234567",
"network": "MTN",
"country": "UG"
}
}
'const options = {
method: 'POST',
headers: {'X-AptaPay-Signature': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
amount_minor: 50000,
currency: 'UGX',
destination_currency: 'UGX',
country: 'UG',
method: 'momo',
destination: {type: 'momo', msisdn: '+256781234567', network: 'MTN', country: 'UG'}
})
};
fetch('https://api.pay.aptahq.com/v1/payouts/quote', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.pay.aptahq.com/v1/payouts/quote"
payload = {
"amount_minor": 50000,
"currency": "UGX",
"destination_currency": "UGX",
"country": "UG",
"method": "momo",
"destination": {
"type": "momo",
"msisdn": "+256781234567",
"network": "MTN",
"country": "UG"
}
}
headers = {
"X-AptaPay-Signature": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text){
"code": 200,
"message": "OK",
"data": "<unknown>",
"error": {
"code": "payout_rejected_by_provider",
"message": "<string>",
"terminal": true,
"retriable": true,
"ambiguous": true,
"details": {}
}
}{
"code": 200,
"message": "OK",
"data": "<unknown>",
"error": {
"code": "payout_rejected_by_provider",
"message": "<string>",
"terminal": true,
"retriable": true,
"ambiguous": true,
"details": {}
}
}{
"code": 200,
"message": "OK",
"data": "<unknown>",
"error": {
"code": "payout_rejected_by_provider",
"message": "<string>",
"terminal": true,
"retriable": true,
"ambiguous": true,
"details": {}
}
}Pre-quote a payout, for a confirm screen
Show the user what a payout will cost before they commit to it. This is the confirm-screen endpoint: “you send 50,000 UGX, they receive 1,340 KES, fee 500” — priced by the real provider, with no money moved and nothing recorded as a transaction.
Optional. POST /v1/payouts re-quotes internally, so you never have to call this first; it exists purely so a UI can display a rate before the user taps send.
Returns OUR quote_id plus the human-meaningful fields. The provider’s own quotation token NEVER leaves the gateway — holding it would let a caller go straight to the provider, bypassing our limits, idempotency and transaction records.
expires_at is ABSOLUTE and provider-reported. Do not assume a fixed TTL and do not hold a quote across a slow confirm screen — re-quote instead, or just send the payout and let the gateway price it.
curl --request POST \
--url https://api.pay.aptahq.com/v1/payouts/quote \
--header 'Content-Type: application/json' \
--header 'X-AptaPay-Signature: <api-key>' \
--data '
{
"amount_minor": 50000,
"currency": "UGX",
"destination_currency": "UGX",
"country": "UG",
"method": "momo",
"destination": {
"type": "momo",
"msisdn": "+256781234567",
"network": "MTN",
"country": "UG"
}
}
'const options = {
method: 'POST',
headers: {'X-AptaPay-Signature': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
amount_minor: 50000,
currency: 'UGX',
destination_currency: 'UGX',
country: 'UG',
method: 'momo',
destination: {type: 'momo', msisdn: '+256781234567', network: 'MTN', country: 'UG'}
})
};
fetch('https://api.pay.aptahq.com/v1/payouts/quote', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.pay.aptahq.com/v1/payouts/quote"
payload = {
"amount_minor": 50000,
"currency": "UGX",
"destination_currency": "UGX",
"country": "UG",
"method": "momo",
"destination": {
"type": "momo",
"msisdn": "+256781234567",
"network": "MTN",
"country": "UG"
}
}
headers = {
"X-AptaPay-Signature": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text){
"code": 200,
"message": "OK",
"data": "<unknown>",
"error": {
"code": "payout_rejected_by_provider",
"message": "<string>",
"terminal": true,
"retriable": true,
"ambiguous": true,
"details": {}
}
}{
"code": 200,
"message": "OK",
"data": "<unknown>",
"error": {
"code": "payout_rejected_by_provider",
"message": "<string>",
"terminal": true,
"retriable": true,
"ambiguous": true,
"details": {}
}
}{
"code": 200,
"message": "OK",
"data": "<unknown>",
"error": {
"code": "payout_rejected_by_provider",
"message": "<string>",
"terminal": true,
"retriable": true,
"ambiguous": true,
"details": {}
}
}Authorizations
v1={hex HMAC-SHA256} over the ten-field canonical string. Sent alongside X-AptaPay-Key, X-AptaPay-Timestamp and X-AptaPay-Nonce — all four are required. OpenAPI can only model one header per scheme, so the other three are described in the Authentication section above.
Body
An integer count of the currency's smallest unit. NEVER a float, and
never a decimal major amount. The _minor suffix on a field name is part
of the contract: amount could be read as 20.00 or 2000, whereas
amount_minor can only be read one way.
The exponent is PER CURRENCY. There is no universal "cents":
| Exponent | Currencies | 2000 means |
|---|---|---|
| 0 (zero-decimal) | UGX, RWF, XAF, XOF | 2,000 shillings/francs |
| 2 | KES, GHS, NGN, TZS, ZMW, USD, EUR, GBP | 20.00 |
So 2000 UGX is two thousand shillings, while 2000 KES is twenty.
Applying a blanket x100 to a zero-decimal currency is a 100x money
error — the kind that reconciles to nothing months later.
To send an amount, multiply the major amount by 10^exponent and confirm
the result is an exact integer (KES 19.99 -> 1999). To display one,
divide by 10^exponent for presentation only — keep the minor-unit
integer as the value you store, compare, and sum.
Rules the gateway enforces:
- An unknown currency is rejected, never defaulted. A currency with
no declared exponent fails with
unknown_currencyrather than assuming 2, because a silent wrong exponent is worse than a loud failure. - A non-exact amount is rejected, never rounded. KES
19.999fails withinexact_amount. AptaPay will not decide on your behalf which way to round someone's money. - Amounts are never floats internally. Providers send amounts as
strings (
"2000") AND sometimes as numbers, so parsing is deliberate at the adapter boundary and converts to an exact integer or fails.
A field's exponent always follows the currency named alongside it. On an
FX quote, source_amount_minor and destination_amount_minor routinely
use DIFFERENT exponents in the same response.
2000
UGX, RWF, XAF, XOF, KES, GHS, NGN, TZS, ZMW, USD, EUR, GBP momo, bank, eversend A momo destination on a PAYOUT. country is REQUIRED and is NOT inherited from the request's top-level country.
A payout pushes money to whoever answers that number, and once it lands it cannot be recalled. The field that decides which country's subscriber is paid must therefore be stated explicitly on the leg that spends the money, never defaulted from a field set for another reason.
- Option 1
- Option 2
Show child attributes
Show child attributes
UGX, RWF, XAF, XOF, KES, GHS, NGN, TZS, ZMW, USD, EUR, GBP SOURCE, DESTINATION Response
A quote. Note expires_at is ABSOLUTE and provider-reported — do not assume a fixed TTL, and re-quote rather than replaying a stale one.
The single response envelope, used by EVERY endpoint so a consuming app parses one shape everywhere.
200
"OK"
Present on success. Shape varies per endpoint.
Machine-readable error classification. EXACTLY ONE of terminal/retriable/ ambiguous is true. laces_api inferred this from the HTTP status and got it wrong, stranding real money twice (2026-08-15, 2026-08-16). Branch on these booleans, never on the status code:
terminal -> the provider refused. Reverse the debit and tell the user. retriable -> safe to retry with the SAME Idempotency-Key. ambiguous -> the outcome is UNKNOWN. Do NOT reverse. Poll instead.
Show child attributes
Show child attributes