curl --request POST \
--url https://api.pay.aptahq.com/v1/beneficiaries \
--header 'Content-Type: application/json' \
--header 'X-AptaPay-Signature: <api-key>' \
--data @- <<EOF
{
"type": "momo",
"country": "UG",
"account_number": "+256781234567",
"network": "MTN",
"label": "Jane's payout number"
}
EOFconst options = {
method: 'POST',
headers: {'X-AptaPay-Signature': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
type: 'momo',
country: 'UG',
account_number: '+256781234567',
network: 'MTN',
label: 'Jane\'s payout number'
})
};
fetch('https://api.pay.aptahq.com/v1/beneficiaries', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.pay.aptahq.com/v1/beneficiaries"
payload = {
"type": "momo",
"country": "UG",
"account_number": "+256781234567",
"network": "MTN",
"label": "Jane's payout number"
}
headers = {
"X-AptaPay-Signature": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text){
"code": 201,
"message": "Created",
"data": {
"id": "bnf_8f14e45f",
"type": "momo",
"country": "UG",
"label": null,
"account_number_last4": "4567",
"bank_code": null,
"network": null
}
}{
"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": {}
}
}Save a payout recipient
A UI convenience for YOUR app, not a payout authorization. Nothing under POST /v1/payouts reads this collection — save a beneficiary so your users do not re-enter an account number, but resolve the recipient’s name at payout time yourself if your product needs that check.
The submitted account_number is envelope-encrypted (AES-256-GCM) before it reaches storage. Every response, on this and every other beneficiary endpoint, returns ONLY account_number_last4 — the full value is never returned again by this API, to anyone, at any point. Keep your own copy if you will need it again.
curl --request POST \
--url https://api.pay.aptahq.com/v1/beneficiaries \
--header 'Content-Type: application/json' \
--header 'X-AptaPay-Signature: <api-key>' \
--data @- <<EOF
{
"type": "momo",
"country": "UG",
"account_number": "+256781234567",
"network": "MTN",
"label": "Jane's payout number"
}
EOFconst options = {
method: 'POST',
headers: {'X-AptaPay-Signature': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
type: 'momo',
country: 'UG',
account_number: '+256781234567',
network: 'MTN',
label: 'Jane\'s payout number'
})
};
fetch('https://api.pay.aptahq.com/v1/beneficiaries', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.pay.aptahq.com/v1/beneficiaries"
payload = {
"type": "momo",
"country": "UG",
"account_number": "+256781234567",
"network": "MTN",
"label": "Jane's payout number"
}
headers = {
"X-AptaPay-Signature": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text){
"code": 201,
"message": "Created",
"data": {
"id": "bnf_8f14e45f",
"type": "momo",
"country": "UG",
"label": null,
"account_number_last4": "4567",
"bank_code": null,
"network": null
}
}{
"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
Response
Created, masked.
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