Pay-ins API
Last updated: 2026-09-25
Pay-in operations use a secret key (sk_*). Monetary amounts use minor units
and must follow the amount representation rules.
Initialize a hosted pay-in
POST /merchant/api/v1/payins/initialize
Auth: Secret key
| Body field | Type | Presence | Meaning |
|---|---|---|---|
amount | string (int64) | Required | Positive amount in minor units |
currencyCode | string | Required | ISO 4217 currency |
countryCode | string | Required | ISO 3166-1 alpha-2 country code of the selected payment market; it is never inferred from currency or payment method |
paymentSubMethod | string | Optional | Supported only for bank transfer; current value is moniepoint |
paymentMethod | enum | Required | Selected PAYMENT_METHOD_* value |
merchantReference | string | Required | Merchant-side payment reference |
redirectUrl | string | Required | Customer return URL |
trafficVertical | enum | Optional | Traffic classification for this request; an explicit supported value takes precedence over the account default |
customerSegment | enum | Optional | Customer segment; defaults when omitted |
mobileMoneyOperator | string | Conditional | Public operator code for operator-required standard mobile money; required transfer rail code for mobile money transfer |
{
"amount": "10000",
"currencyCode": "NGN",
"countryCode": "NG",
"paymentMethod": "PAYMENT_METHOD_BANK_ACCOUNT",
"merchantReference": "MR-1001",
"redirectUrl": "https://merchant.example/return",
"trafficVertical": "TRAFFIC_VERTICAL_OTHER"
}
The response contains paymentOrder and expiresAt. A new, unsubmitted hosted
form has a 30-minute window from payment creation; expiresAt marks that window.
It is not a payment-completion or cancellation signal. Once the payment is
PENDING, follow its current status and any instruction expiry. Redirect the
customer only when paymentOrder.paymentUrl is returned, and reconcile the
payment after the customer returns. See the retry rules below for the special
case of pending bank transfers.
Create a direct pay-in
POST /merchant/api/v1/payins/create
Auth: Secret key
The core fields are the same as hosted initialization, except that direct
requests put method-specific input in exactly one matching detail block. Use
paymentSubMethod only with bank transfer; the current supported value is
moniepoint.
| Detail block | Used with | Fields |
|---|---|---|
bankAccountDetails | PAYMENT_METHOD_BANK_ACCOUNT | name and bankCode required; email optional |
bankTransferDetails | PAYMENT_METHOD_BANK_TRANSFER | email optional |
mobileMoneyDetails | PAYMENT_METHOD_MOBILE_MONEY | mobileNumber required; operator and voucherPin conditional; email optional |
mobileMoneyTransferDetails | PAYMENT_METHOD_MOBILE_MONEY_TRANSFER | operator and mobileNumber required |
cardDetails | PAYMENT_METHOD_CARD | Schema-visible only; card creation is not currently supported |
Example direct mobile money request:
{
"amount": "8500",
"currencyCode": "KES",
"countryCode": "KE",
"paymentMethod": "PAYMENT_METHOD_MOBILE_MONEY",
"merchantReference": "MM-2001",
"redirectUrl": "https://merchant.example/return",
"mobileMoneyDetails": {
"mobileNumber": "<customer_msisdn>",
"operator": "<operator_from_discovery>"
}
}
Method-specific response details can include:
bankAccountDetails.redirectUrlfor an authorization redirect;- bank transfer account instructions and
expiresAt; - mobile money instructions,
displayMessage, andisOtpAuthRequired; - mobile money transfer
instructions,expiresAt, anddisplayMessage.
Treat displayMessage as optional plain-text customer guidance, and display it
only for a flow whose customer-facing copy your integration has reviewed. Do
not parse it as a payment status, failure reason, or payment requisite. It can
be absent while a pending payment is being reconciled. Later payment reads do
not include mobileMoneyDetails, so retain the create response
if you need to keep showing the guidance.
isOtpAuthRequired is a continuation signal and does not identify an OTP
operation by itself. When discovery returned ORANGE for the current account
and realm in one of the markets below, a create response with true requires
the OTP authorization operation. For another operator, follow its
method-specific contract.
See Mobile money transfer objects for its direct request, response, and instruction fields.
Retry pay-in creation
Retry the same endpoint in the same realm with the same merchantReference
and an equivalent request. For POST /merchant/api/v1/payins/create, a replay
uses the existing payment attempt without submitting a replacement payment.
A replay can return the current payment or repeat the original error. Reusing
a reserved reference with non-equivalent request data is rejected.
For POST /merchant/api/v1/payins/initialize, an equivalent retry returns the
same payment page while the form is unexpired and the payment is CREATED.
A PAYMENT_METHOD_BANK_TRANSFER page also replays while PENDING. This
exception does not apply to other payment methods, including mobile money
transfer. Other PENDING, COMPLETED, or FAILED
hosted payments keep the reference reserved but return a reference conflict;
use a payment read to reconcile them.
After a pay-in becomes CANCELLED or EXPIRED, reusing its reference can
create a new attempt with a new payment ID. Persist the ID from every successful
response. Do not treat an unknown outcome as cancellation or expiry, and do not
switch between create and initialize to bypass a reference conflict.
For example, if a create request times out, keep its reference and request data. Read the payment by reference. An equivalent retry of the same endpoint reuses that attempt. Do not generate a new reference just because the response was lost.
A reserved-reference conflict returns HTTP 400 / INVALID_ARGUMENT with a
google.rpc.BadRequest violation for PayIn/#merchant_reference and description
Taken. See the reference conflict example.
If retries repeat the same error, including HTTP 500, stop automatic retries
and read the payment by reference. Ask support if its outcome remains unclear.
Changing mobileMoneyDetails.voucherPin while keeping the same reference does
not start a new attempt or correct the original payment. Reconcile the existing
attempt first; contact support if the PIN needs to be corrected after a failed
attempt. Do not create a replacement while the earlier payment may still complete.
Authorize pay-in OTP
POST /merchant/api/v1/payins/authorize-otp
Auth: Secret key
Use this operation when discovery selected ORANGE for a direct pay-in in a
market listed below, the create response returns
mobileMoneyDetails.isOtpAuthRequired: true, and your flow has collected the
customer's OTP.
The operator contract covers nine country/currency pairs: BF / XOF, CI /
XOF, SN / XOF, ML / XOF, CM / XAF, CF / XAF, GQ / XAF,
CD / CDF, and SL / SLE. This is not an availability list: use ORANGE
only when discovery returns it for the current account and realm. There is no
separate OTP activation.
First create the payment with the public operator code ORANGE when discovery
returns it for the current account and realm. When the create response returns
mobileMoneyDetails.isOtpAuthRequired: true, submit the customer-provided OTP
within 15 minutes of payment creation.
| Body field | Type | Presence |
|---|---|---|
paymentOrderId | string (UUID) | Required |
token | string | Required |
{
"paymentOrderId": "<payment_order_id>",
"token": "<customer_provided_otp>"
}
The response contains:
isAuthorized— whether OTP authorization was accepted;isStkPromptRequired— whether the flow continues with an STK prompt;displayMessage— optional plain-text customer guidance; do not parse it as payment state or a failure reason.
{
"isAuthorized": true,
"isStkPromptRequired": false,
"displayMessage": "Payment is being processed."
}
isAuthorized: true means the OTP was accepted. It does not mean the payment
is complete: the payment can remain PENDING and must still be reconciled.
Outcomes
Pay-in operations can return INVALID_ARGUMENT for missing, malformed, or
unavailable request values. A route that is temporarily unavailable can return
the public method_unavailable reason. OTP authorization can also return
NOT_FOUND for an unknown payment order. An invalid OTP returns
INVALID_ARGUMENT for token; a corrected OTP can be submitted before the
authorization deadline. A retryable authorization condition returns HTTP 503 /
UNAVAILABLE with Retry-After: 2. Handle all non-success responses using
Errors.
Related guide: Pay-ins.