Skip to main content

Merchant Wallet Payouts

Merchant Wallet Payouts enable you to send transfers programmatically from your merchant wallet balance to supported beneficiary bank accounts using the PayIslands API.

PayIslands atomically reserves the payout amount and applicable fee, executes the payout asynchronously, and updates your wallet ledger upon completion.


Prerequisites​

Before initiating a payout:

  • Active Merchant Wallet: Your merchant account must have an active merchant wallet.
  • Funded Balance: Your wallet must be funded through its assigned static account. A successful transfer into your assigned static account is automatically credited to your wallet and becomes available for payouts.
  • Sufficient Available Balance: Your available balance must cover both the payout amount and the applicable fee (total_debit_amount = payout amount + payout fee).
  • Supported Beneficiary Bank: You must use a supported beneficiary bank code.
  • Supported Currency: Only NGN payouts are currently supported.

Base URL​

API requests use the standard PayIslands base URL for your environment:

export PAYISLANDS_API_BASE_URL="https://ags.payislands.com"

Authentication​

All public merchant payout API requests require your merchant secret key passed as a Bearer token in the Authorization HTTP header:

Authorization: Bearer <MERCHANT_SECRET_KEY>

Key authentication requirements:

  • Merchant Secret Key Only: The merchant secret key is sufficient to authenticate API payout requests.
  • Server-Side Security: The secret key must only be used from a secure server-side environment. It must never be embedded in browser or mobile client code, committed to source repositories, or exposed in client-facing logs.
export PAYISLANDS_MERCHANT_SECRET_KEY="your-merchant-secret-key"

Fee Schedule​

Applicable payout fees are detailed on the PayIslands Pricing page.

The total wallet debit is calculated as:

total_debit_amount = amount + fee_amount

Merchants are encouraged to call the Quote endpoint (POST /api/v1/transactions/in/merchant-wallet/payouts/quote) prior to submitting a payout to confirm the exact fee and total debit amount.


Response Envelope​

All API endpoints return standard PayIslands responses using this JSON envelope:

{
"status": true,
"message": "...",
"data": {},
"statusCode": 200
}

In error responses, status is false, data is null, and message describes the failure cause.


Public API Endpoints​

1. Get a Payout Quote​

POST /api/v1/transactions/in/merchant-wallet/payouts/quote

Calculates the fee and total wallet debit for a given payout amount. This endpoint is synchronous and does not reserve funds or initiate a transfer.

Request Body​

FieldRequiredTypeDescription
amountYesStringPositive decimal string with up to two decimal places (e.g. "100.00")
currencyNoStringDefaults to "NGN"; only "NGN" is currently supported
{
"amount": "100.00",
"currency": "NGN"
}

Response Example — HTTP 200​

{
"status": true,
"message": "Fee calculated successfully",
"data": {
"amount": "100.00",
"fee_amount": "10.00",
"total_debit_amount": "110.00",
"currency": "NGN"
},
"statusCode": 200
}

cURL Example​

curl --request POST \
"$PAYISLANDS_API_BASE_URL/api/v1/transactions/in/merchant-wallet/payouts/quote" \
--header "Authorization: Bearer $PAYISLANDS_MERCHANT_SECRET_KEY" \
--header "Content-Type: application/json" \
--data '{
"amount": "100.00",
"currency": "NGN"
}'

2. Get Supported Banks​

GET /api/v1/transactions/bank

Retrieves the list of supported financial institutions and their corresponding bank codes for initiating payouts and resolving beneficiary account names.

Path and Query Parameters​

None.

Response Example — HTTP 200​

{
"status": true,
"message": "Bank Fetched successfully",
"data": [
{
"id": 1,
"name": "Access Bank",
"bank_code": "044",
"institution_code": "000014"
},
{
"id": 2,
"name": "GTBANK PLC",
"bank_code": "058",
"institution_code": "000013"
}
],
"statusCode": 200
}

Response Fields​

FieldTypeDescription
idIntegerUnique identifier for the bank
nameStringFull registered name of the financial institution
bank_codeStringBank code used for payout creation and name resolution
institution_codeStringStandard NIP institution code

cURL Example​

curl --request GET \
"$PAYISLANDS_API_BASE_URL/api/v1/transactions/bank" \
--header "Authorization: Bearer $PAYISLANDS_MERCHANT_SECRET_KEY"

3. Resolve Beneficiary Account (Name Enquiry)​

POST /api/v1/transactions/in/merchant-wallet/payouts/name-enquiry

Resolves and returns the beneficiary account name before submitting a wallet payout.

  1. Retrieve bank codes from Get Supported Banks.
  2. Call name enquiry with the account number and bank code in the request body.
  3. Display the returned account name for confirmation.
  4. Submit the payout through the Create a Payout endpoint.

Request Body​

FieldRequiredTypeDescription
account_numberYesStringBeneficiary account number containing only digits (10 numeric characters)
bank_codeYesStringCode identifying the beneficiary's bank returned by the supported banks endpoint
{
"account_number": "0123456789",
"bank_code": "058"
}

Response Example — HTTP 200​

{
"status": true,
"message": "Account name resolved successfully",
"data": {
"account_name": "JOHN DOE",
"account_number": "0123456789",
"bank_code": "058",
"reference": "MWNE1234567890"
}
}

Merchants should use account_name only to display and confirm the intended beneficiary.

cURL Example​

curl --request POST \
"$PAYISLANDS_API_BASE_URL/api/v1/transactions/in/merchant-wallet/payouts/name-enquiry" \
--header "Authorization: Bearer $PAYISLANDS_MERCHANT_SECRET_KEY" \
--header "Content-Type: application/json" \
--data '{
"account_number": "0123456789",
"bank_code": "058"
}'

Failure Responses Examples​

Invalid request — HTTP 400

{
"status": false,
"message": "Account number must contain only digits",
"data": null,
"statusCode": 400
}

Invalid or unsupported bank — HTTP 400

{
"status": false,
"message": "Beneficiary bank could not be resolved",
"data": null,
"statusCode": 400
}

Name enquiry unsuccessful — HTTP 400

{
"status": false,
"message": "Could not resolve beneficiary account name",
"data": null,
"statusCode": 400
}

Authentication failure — HTTP 401

{
"status": false,
"message": "Unauthorized",
"data": null,
"statusCode": 401
}

Temporary service failure — HTTP 503

{
"status": false,
"message": "Name enquiry is temporarily unavailable",
"data": null,
"statusCode": 503
}

Error Guidance​

HTTP StatusMeaningRecommended Action
200Account resolved successfullyDisplay the returned name and allow the merchant to continue.
400Invalid account number, invalid bank code, or unresolved accountCorrect the beneficiary details before retrying.
401 / 403Authentication failedConfirm that the appropriate API secret key credential is being used.
503Name enquiry temporarily unavailableRetry after a short delay. Do not submit a payout until the beneficiary is confirmed.

Important Integration Notes​

  • No Payout Creation: The name enquiry endpoint does not create a payout.
  • No Fund Reservations: The endpoint does not reserve or debit wallet funds.
  • No Outcome Guarantee: A successful lookup does not guarantee that a subsequent payout will succeed.
  • Independent Validation: A successful lookup does not replace payout submission validation. PayIslands independently validates the beneficiary again during payout processing.
  • Do Not Submit account_name: Merchants must not submit or trust an account_name of their own. The create-payout request accepts account_number and bank_code only; it does not accept account_name.
  • Do Not Cache Indefinitely: Merchants should not cache a resolved account name indefinitely.
  • Provider-Agnostic: The merchant never selects or passes a payout provider.

4. Create a Payout​

POST /api/v1/transactions/in/merchant-wallet/payouts

Validates the beneficiary account, reserves amount + fee_amount from the available wallet balance, and accepts the payout request for asynchronous processing.

Request Body Contract​

FieldRequiredTypeDescription
account_numberYesStringBeneficiary bank account number (10 numeric characters)
bank_codeYesStringBeneficiary bank code (e.g., "058")
bank_idNoIntegerPositive integer bank identifier
amountYesStringPositive decimal string with up to two decimal places
currencyNoStringDefaults to "NGN"; only "NGN" is supported
narrationNoStringOptional transfer description (maximum 255 characters)
idempotency_keyYesStringUnique reference key for request deduplication (maximum 175 characters)
{
"account_number": "0123456789",
"bank_code": "058",
"amount": "1000.00",
"currency": "NGN",
"narration": "Vendor payment",
"idempotency_key": "merchant-order-12345"
}

Response Example — HTTP 202 Accepted​

{
"status": true,
"message": "Payout accepted for processing",
"data": {
"payout": {
"id": 1842,
"internal_reference": "MWPOexampleReference000001",
"beneficiary": {
"account_number": "0123456789",
"account_name": "EXAMPLE BENEFICIARY",
"bank_code": "058",
"bank_name": "Example Bank"
},
"currency": "NGN",
"amount": "1000.00",
"fee_amount": "10.00",
"total_debit_amount": "1010.00",
"status": "reserved",
"narration": "Vendor payment",
"error_message": null,
"created_at": "2026-09-18T10:00:00.000Z",
"completed_at": null,
"updated_at": "2026-09-18T10:00:00.000Z"
},
"duplicate": false
},
"statusCode": 202
}

[!IMPORTANT] An HTTP 202 response indicates that the payout has been accepted for processing and funds have been reserved. It does not mean the beneficiary account has been credited. The initial status is normally reserved.

cURL Example​

curl --request POST \
"$PAYISLANDS_API_BASE_URL/api/v1/transactions/in/merchant-wallet/payouts" \
--header "Authorization: Bearer $PAYISLANDS_MERCHANT_SECRET_KEY" \
--header "Content-Type: application/json" \
--data '{
"account_number": "0123456789",
"bank_code": "058",
"amount": "1000.00",
"currency": "NGN",
"narration": "Vendor payment",
"idempotency_key": "merchant-order-12345"
}'

5. List Payouts​

GET /api/v1/transactions/in/merchant-wallet/payouts

Returns a paginated list of payouts for your merchant wallet, sorted in newest-first order.

Query Parameters​

ParameterRequiredTypeDescription
currencyNoStringFilter by currency; defaults to "NGN"
pageNoIntegerPage number (defaults to 1)
limitNoIntegerItems per page (1 to 200, defaults to 50)
statusNoStringFilter by status (reserved, processing, dispatched, ambiguous, successful, failed)
from_dateNoStringLower bound creation timestamp (ISO 8601 format)
to_dateNoStringUpper bound creation timestamp (ISO 8601 format)
referenceNoStringSearch by internal reference or beneficiary details

Response Example — HTTP 200​

{
"status": true,
"message": "Merchant wallet payouts fetched successfully",
"data": {
"items": [
{
"id": 1842,
"internal_reference": "MWPOexampleReference000001",
"beneficiary": {
"account_number": "0123456789",
"account_name": "EXAMPLE BENEFICIARY",
"bank_code": "058",
"bank_name": "Example Bank"
},
"currency": "NGN",
"amount": "1000.00",
"fee_amount": "10.00",
"total_debit_amount": "1010.00",
"status": "successful",
"narration": "Vendor payment",
"error_message": null,
"created_at": "2026-09-18T10:00:00.000Z",
"completed_at": "2026-09-18T10:00:08.000Z",
"updated_at": "2026-09-18T10:00:08.000Z"
}
],
"pagination": {
"page": 1,
"limit": 50,
"total": 1,
"total_pages": 1,
"has_next_page": false,
"has_previous_page": false
}
},
"statusCode": 200
}

cURL Example​

curl --get \
"$PAYISLANDS_API_BASE_URL/api/v1/transactions/in/merchant-wallet/payouts" \
--header "Authorization: Bearer $PAYISLANDS_MERCHANT_SECRET_KEY" \
--data-urlencode "currency=NGN" \
--data-urlencode "page=1" \
--data-urlencode "limit=50" \
--data-urlencode "status=successful"

6. Get Payout Details​

GET /api/v1/transactions/in/merchant-wallet/payouts/:id

Retrieves detailed information for a single payout by its numeric ID.

Path Parameters​

ParameterRequiredDescription
idYesNumeric payout ID returned during payout creation

Response Example — HTTP 200​

{
"status": true,
"message": "Payout details fetched successfully",
"data": {
"id": 1842,
"internal_reference": "MWPOexampleReference000001",
"beneficiary": {
"account_number": "0123456789",
"account_name": "EXAMPLE BENEFICIARY",
"bank_code": "058",
"bank_name": "Example Bank"
},
"currency": "NGN",
"amount": "1000.00",
"fee_amount": "10.00",
"total_debit_amount": "1010.00",
"status": "processing",
"narration": "Vendor payment",
"error_message": null,
"created_at": "2026-09-18T10:00:00.000Z",
"completed_at": null,
"updated_at": "2026-09-18T10:00:05.000Z"
},
"statusCode": 200
}

cURL Example​

curl --request GET \
"$PAYISLANDS_API_BASE_URL/api/v1/transactions/in/merchant-wallet/payouts/1842" \
--header "Authorization: Bearer $PAYISLANDS_MERCHANT_SECRET_KEY"

7. Requery Payout Status​

POST /api/v1/transactions/in/merchant-wallet/payouts/:id/requery

Triggers a status check for a pending or ambiguous payout. Requery safely queries the current processing status without re-submitting the transfer.

Path Parameters​

ParameterRequiredDescription
idYesNumeric payout ID returned during payout creation

Response Example — HTTP 200 (Resolved to Success)​

{
"status": true,
"message": "Payout completed successfully",
"data": {
"id": 1842,
"internal_reference": "MWPOexampleReference000001",
"beneficiary": {
"account_number": "0123456789",
"account_name": "EXAMPLE BENEFICIARY",
"bank_code": "058",
"bank_name": "Example Bank"
},
"currency": "NGN",
"amount": "1000.00",
"fee_amount": "10.00",
"total_debit_amount": "1010.00",
"status": "successful",
"narration": "Vendor payment",
"error_message": null,
"created_at": "2026-09-18T10:00:00.000Z",
"completed_at": "2026-09-18T10:01:00.000Z",
"updated_at": "2026-09-18T10:01:00.000Z"
},
"statusCode": 200
}

[!NOTE] If status reconciliation is still ongoing or inconclusive, requery returns HTTP 202 with status: "ambiguous". The payout remains pending.

cURL Example​

curl --request POST \
"$PAYISLANDS_API_BASE_URL/api/v1/transactions/in/merchant-wallet/payouts/1842/requery" \
--header "Authorization: Bearer $PAYISLANDS_MERCHANT_SECRET_KEY"

Idempotency​

To prevent duplicate transfers caused by network timeouts or retries, every payout request requires an idempotency_key (maximum 175 characters). The key is scoped to your merchant wallet.

  • Identical Retries: Submitting the same idempotency_key with identical payout parameters returns the original payout object with "duplicate": true and HTTP status 202.
  • Key Conflicts: Submitting the same idempotency_key with different payout parameters returns HTTP 409 Conflict.
  • Handling Network Errors: If a request times out or disconnects before receiving a response, do not generate a new idempotency key. Instead, retry the request using the same idempotency_key or call the Get Payout Details / List Payouts endpoint to check the transaction status.
{
"status": true,
"message": "Payout request already accepted",
"data": {
"payout": {
"id": 1842,
"internal_reference": "MWPOexampleReference000001",
"status": "reserved",
"amount": "1000.00",
"fee_amount": "10.00",
"total_debit_amount": "1010.00"
},
"duplicate": true
},
"statusCode": 202
}

Payout Lifecycle & Asynchronous Statuses​

Payout execution is asynchronous. A payout transitions through the following lifecycle states:

Payout Lifecycle

Lifecycle Status Definitions​

StatusMeaningState Type
reservedPayout amount + fee reserved from available balance. Execution pending.Pending
processingPayout is queued and preparing for transfer dispatch.Pending
dispatchedPayout has been sent for transfer processing.Pending
ambiguousOutcome is not yet definitive. Funds remain safely reserved.Pending
successfulBeneficiary transfer confirmed. Reserved funds captured.Final
failedTransfer failed definitively. Reserved funds released back to available balance.Final

Status Handling Guidelines​

  • Final States: Only successful represents final success. Only failed represents final failure.
  • Pending States: Treat all other statuses (reserved, processing, dispatched, ambiguous) as pending.
  • Do Not Duplicate: Never create a replacement payout while an existing payout is pending or ambiguous.
  • Status Checks: Poll the details endpoint (GET /api/v1/transactions/in/merchant-wallet/payouts/:id) or trigger a status requery (POST /api/v1/transactions/in/merchant-wallet/payouts/:id/requery) to monitor pending payouts.

Wallet and Ledger Balance Effects​

PayIslands maintains strict ledger accountability for all wallet payout operations:

1. Payout Initiation & Reservation​

When a payout is submitted, total_debit_amount (amount + fee_amount) is deducted from your available balance and added to your reserved balance.

2. Successful Payout​

When a payout achieves successful status:

  • The reserved funds are captured.
  • A final payout debit ledger entry is recorded for the principal amount and fee.

3. Definitive Failure​

When a payout encounters a definitive failed status:

  • The reserved funds are immediately returned to your available balance.
  • A payout_reversal ledger entry is recorded.

4. Ambiguous Status​

If a transfer outcome is temporarily inconclusive (ambiguous):

  • Funds remain in reserved balance until final status resolution.
  • Funds are neither released nor charged twice while awaiting confirmation.

Troubleshooting & Error Handling​

Common HTTP response codes and error scenarios:

HTTP StatusError Message / CauseSolution
400Merchant wallet is not activeEnsure your merchant wallet has been activated.
400Insufficient available wallet balanceTop up your merchant wallet via your assigned static account to cover principal + fee.
400Only NGN payouts are currently supportedSpecify NGN or omit the currency field.
400Beneficiary bank could not be resolvedVerify bank_code and account_number.
401Invalid or missing secret keyEnsure Authorization: Bearer <MERCHANT_SECRET_KEY> header is included and valid.
404Payout record not foundVerify the payout ID and ensure it belongs to your merchant account.
409Idempotency key has already been used for a different payout requestUse a unique idempotency key for distinct payouts, or send matching payload details.
409Payout is not eligible for status requeryRequery is only applicable while payouts are in pending or ambiguous states.

Security Best Practices​

  1. Keep Secret Keys Server-Side: Always store and use your MERCHANT_SECRET_KEY on secure backend servers. Never expose it in web browsers, mobile apps, or client-side code.
  2. Never Expose Credentials in Logs: Ensure secret keys and Authorization headers are masked in application logs.
  3. Use Idempotency Keys: Generate a unique, deterministic idempotency key for every distinct logical payout.
  4. Handle Network Disruptions Safely: Always verify existing payout status via Get Payout Details or Requery before retrying failed network calls.