curl --request POST \
--url https://api.pay.aptahq.com/v1/collections/{reference}/authorize \
--header 'Content-Type: application/json' \
--header 'X-AptaPay-Signature: <api-key>' \
--data '
{
"authorization": {
"type": "otp",
"otp": {
"pin_id": "{{otp_pin_id}}",
"pin": "{{otp_pin}}"
}
},
"source": {
"type": "momo",
"msisdn": "+256763494235",
"network": "MTN",
"country": "UG"
}
}
'const options = {
method: 'POST',
headers: {'X-AptaPay-Signature': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
authorization: {type: 'otp', otp: {pin_id: '{{otp_pin_id}}', pin: '{{otp_pin}}'}},
source: {type: 'momo', msisdn: '+256763494235', network: 'MTN', country: 'UG'}
})
};
fetch('https://api.pay.aptahq.com/v1/collections/{reference}/authorize', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.pay.aptahq.com/v1/collections/{reference}/authorize"
payload = {
"authorization": {
"type": "otp",
"otp": {
"pin_id": "{{otp_pin_id}}",
"pin": "{{otp_pin}}"
}
},
"source": {
"type": "momo",
"msisdn": "+256763494235",
"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": {}
}
}OTP / PIN / 3DS step
Finish a charge that came back requires_action. When a collection needs a second step from the customer — an SMS OTP, a momo PIN, a 3-D Secure result — you collect that value in your UI and submit it here against the transaction’s gateway reference.
Returns the updated transaction. The status may still be pending afterwards: authorizing proves the customer consented, it does not prove the provider has settled, so keep watching the webhook or poll GET /v1/collections/{reference}.
This call validates the code AND charges. For Eversend there is no separate “authorize” step — authorizing re-sends the charge with the code attached, so a correct code debits the customer immediately: they get the handset PIN prompt (STK push), and success or failure arrives by webhook. Expect pending here and treat the webhook as the answer.
pin_id comes from the next_action on the charge (or from POST /v1/collections/otp if you drove that yourself); pin is what the customer typed. A wrong code fails this call without failing the transaction — prompt again and retry.
source must be resupplied. Re-sending the charge needs the payer’s full number, and the gateway never stores one — only its last 4 digits (so a leaked database cannot become a list of customer phone numbers). The last4 of what you send is checked against the transaction, so a reference cannot be redirected to a different payer. The amount, currency and country come from the stored transaction and cannot be changed here.
curl --request POST \
--url https://api.pay.aptahq.com/v1/collections/{reference}/authorize \
--header 'Content-Type: application/json' \
--header 'X-AptaPay-Signature: <api-key>' \
--data '
{
"authorization": {
"type": "otp",
"otp": {
"pin_id": "{{otp_pin_id}}",
"pin": "{{otp_pin}}"
}
},
"source": {
"type": "momo",
"msisdn": "+256763494235",
"network": "MTN",
"country": "UG"
}
}
'const options = {
method: 'POST',
headers: {'X-AptaPay-Signature': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
authorization: {type: 'otp', otp: {pin_id: '{{otp_pin_id}}', pin: '{{otp_pin}}'}},
source: {type: 'momo', msisdn: '+256763494235', network: 'MTN', country: 'UG'}
})
};
fetch('https://api.pay.aptahq.com/v1/collections/{reference}/authorize', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.pay.aptahq.com/v1/collections/{reference}/authorize"
payload = {
"authorization": {
"type": "otp",
"otp": {
"pin_id": "{{otp_pin_id}}",
"pin": "{{otp_pin}}"
}
},
"source": {
"type": "momo",
"msisdn": "+256763494235",
"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.
Path Parameters
Body
The provider-agnostic envelope. Branch on the next_action.type you were given and post the matching payload back — the wrapper is the same on every rail.
Show child attributes
Show child attributes
REQUIRED for a momo authorize — the payer, resupplied. Its last 4 digits must match the transaction.
- Option 1
- Option 2
Show child attributes
Show child attributes
Legacy unwrapped form. Still accepted.
Show child attributes
Show child attributes
Response
The code was accepted and the charge placed. pending means the customer now has the handset PIN prompt; watch the webhook.
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