Skip to main content

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 fieldTypePresenceMeaning
amountstring (int64)RequiredPositive amount in minor units
currencyCodestringRequiredISO 4217 currency
countryCodestringRequiredISO 3166-1 alpha-2 country code of the selected payment market; it is never inferred from currency or payment method
paymentSubMethodstringOptionalSupported only for bank transfer; current value is moniepoint
paymentMethodenumRequiredSelected PAYMENT_METHOD_* value
merchantReferencestringRequiredMerchant-side payment reference
redirectUrlstringRequiredCustomer return URL
trafficVerticalenumOptionalTraffic classification for this request; an explicit supported value takes precedence over the account default
customerSegmentenumOptionalCustomer segment; defaults when omitted
mobileMoneyOperatorstringConditionalPublic 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 blockUsed withFields
bankAccountDetailsPAYMENT_METHOD_BANK_ACCOUNTname and bankCode required; email optional
bankTransferDetailsPAYMENT_METHOD_BANK_TRANSFERemail optional
mobileMoneyDetailsPAYMENT_METHOD_MOBILE_MONEYmobileNumber required; operator and voucherPin conditional; email optional
mobileMoneyTransferDetailsPAYMENT_METHOD_MOBILE_MONEY_TRANSFERoperator and mobileNumber required
cardDetailsPAYMENT_METHOD_CARDSchema-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.redirectUrl for an authorization redirect;
  • bank transfer account instructions and expiresAt;
  • mobile money instructions, displayMessage, and isOtpAuthRequired;
  • mobile money transfer instructions, expiresAt, and displayMessage.

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 fieldTypePresence
paymentOrderIdstring (UUID)Required
tokenstringRequired
{
"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.