Skip to main content
POST

Authorizations

X-AptaPay-Signature
string
header
required

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.

Headers

Idempotency-Key
string
required

Required. A missing key is a 400 — never a server-generated one.

Maximum string length: 255

Body

application/json
amount_minor
integer
required

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":

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_currency rather than assuming 2, because a silent wrong exponent is worse than a loud failure.
  • A non-exact amount is rejected, never rounded. KES 19.999 fails with inexact_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.

Example:

2000

currency
enum<string>
required
Available options:
UGX,
RWF,
XAF,
XOF,
KES,
GHS,
NGN,
TZS,
ZMW,
USD,
EUR,
GBP
country
string
required
Example:

"UG"

method
enum<string>
required
Available options:
momo,
bank,
eversend
destination
object
required

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.

destination_currency
enum<string>

Defaults to currency for same-currency payouts.

Available options:
UGX,
RWF,
XAF,
XOF,
KES,
GHS,
NGN,
TZS,
ZMW,
USD,
EUR,
GBP
amount_type
enum<string>

REQUIRED when currency != destination_currency.

Available options:
SOURCE,
DESTINATION
deduct_fee_from_amount
boolean

Treat amount_minor as the total BUDGET rather than what the recipient receives: the provider's fee is taken OUT of it instead of added on top. This is "withdraw everything" — a UGX 10,000 momo payout delivers 9,000 with a 1,000 fee, the full 10,000 leaves your float, and nothing is stranded. Defaults to false, where amount_minor is what the recipient gets and the fee is charged on top.

narration
string
metadata
object

Free-form correlation data, stored on the transaction and ECHOED BACK on every webhook delivery as data.metadata. This is how you map an AptaPay reference to one of your own: our reference is opaque and carries no structure you can parse, so put your order id or reference here at charge time and read it back off the callback. Must serialize to at most 4096 bytes — it is re-sent on every delivery attempt.

Response

Submitted to the provider.

The single response envelope, used by EVERY endpoint so a consuming app parses one shape everywhere.

code
integer
required
Example:

200

message
string
required
Example:

"OK"

data
any

Present on success. Shape varies per endpoint.

error
object

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.