Payouts API
Withdraw funds from your Seev balance to a mobile money recipient from your own server, and track each payout to completion.
⬡ API product
Requires an API secret key with the payments:write scope
The Payouts API withdraws money from your organization's Seev balance to a mobile money recipient, initiated by your own server rather than from the Dashboard. It is the programmatic counterpart to the Dashboard Send Money flow. A payout debits your balance the moment you create it, so the request is authorized with a payments:write key and every create must carry an Idempotency-Key.
This page assumes you already hold a key. If you do not, create one first in API Keys, and read Idempotency before you send money.
A payout moves real money to a recipient. Amounts are expressed in the
currency's smallest unit, so 10000 means GHS 100.00. Send the wrong figure
and the funds leave your balance. Confirm the amount, the channel, and the
recipient before you call this endpoint.
Scope and limits at launch
| Property | Value | Description |
|---|---|---|
| Source balance | Main balance | Payouts debit your organization's main balance only. Subaccount balances cash out through the settlement flow, not this API. |
| Currency | GHS | The payout currency is GHS. A payout currency must match the balance currency. |
| Channels | mobile_money | Payouts are sent to a mobile money wallet. |
| Environment | sandbox, production | Determined by the key you authenticate with. Sandbox payouts move no real money and never touch production balances. |
Check your balance before a payout
Read your balance first so a payout is not rejected for insufficient funds. The balance endpoint is authorized with a payments:read key.
curl "https://api.seevplus.com/api/v1/merchants/balance" \
-H "Authorization: Bearer $SEEV_API_KEY"{
"total_deposits": 5000.0,
"total_withdrawals": 1200.0,
"total_fees": 38.0,
"total_merchant_fees": 12.0,
"available_balance": 3750.0,
"pending_balance": 100.0,
"paid_settlements": 0.0,
"reserved_settlements": 0.0,
"currency": "GHS",
"transaction_count": 42,
"balance_source": "wallet"
}| Field | Type | Required | Description |
|---|---|---|---|
available_balance | number | Yes | Spendable balance in major units (GHS, not pesewas). This is the figure a payout draws against, and it already excludes funds held by in-flight payouts and settlement reservations. |
pending_balance | number | Yes | Funds currently held for in-flight payouts and reservations, in major units. Present as 0 while the balance is read from the legacy derived source. |
currency | string | Yes | The balance currency, GHS. |
total_deposits | number | Yes | Lifetime deposits credited to the balance, in major units. |
total_withdrawals | number | Yes | Lifetime withdrawals from the balance, in major units. |
total_fees | number | Yes | Total fees charged, in major units. |
total_merchant_fees | number | Yes | The merchant-borne portion of fees, in major units. |
paid_settlements | number | Yes | Settlements already paid out, in major units. |
reserved_settlements | number | Yes | Settlements reserved and not yet paid, in major units. |
transaction_count | number | Yes | Number of transactions contributing to the balance. |
balance_source | string | No | wallet when the authoritative wallet balance is reported. Absent when the balance is derived from the legacy source. |
The balance endpoint returns amounts in major units (3750.0 means GHS
3,750.00), while the Payouts API takes and returns amounts in the smallest
unit (10000 means GHS 100.00). Convert before you compare a balance figure
against a payout amount: multiply the balance by 100, or divide the payout
amount by 100.
Check that available_balance covers your payout amount plus the withdrawal fee before you create the payout. Because available_balance already excludes funds held by earlier in-flight payouts, reading it immediately before each payout is the reliable way to avoid a 422 INSUFFICIENT_BALANCE.
Create a payout
Send the request from your server with an Idempotency-Key. The amount is in the currency's smallest unit, and the merchant is charged the withdrawal fee on top of the amount, so your available balance must cover amount + merchant fee.
curl -X POST "https://api.seevplus.com/api/v1/merchants/payouts" \
-H "Authorization: Bearer $SEEV_API_KEY" \
-H "Idempotency-Key: payout_order_123_attempt_1" \
-H "Content-Type: application/json" \
-d '{
"amount": 10000,
"channel": "mobile_money",
"recipient": {
"name": "Kwame Asante",
"phone": "0551234567",
"network": "MTN"
}
}'A successful create returns 202 Accepted. The payout is now processing; the funds are already reserved against your balance, and the transfer resolves asynchronously (see Follow a payout to completion).
Request headers
| Header | Type | Required | Description |
|---|---|---|---|
Authorization | string | Conditional | Bearer <secret key>. Required unless X-API-Key is sent. |
X-API-Key | string | Conditional | The secret key with no scheme prefix. Takes precedence over Authorization. |
Idempotency-Key | string | Yes | Retrying with the same key returns the original payout instead of sending a second one. Persisted per organization, so it is safe across restarts. Send a fresh key only for a genuinely new payout. |
Content-Type | string | Yes | application/json. |
Always send an Idempotency-Key on a payout create. Without a stable key, a
network retry or a double-click can send the money twice, and a mobile money
transfer is not something you can quietly undo.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | The amount to pay the recipient, in the currency's smallest unit and greater than zero. 10000 means GHS 100.00. |
channel | string | Yes | mobile_money. Any other value is rejected. |
recipient | object | Yes | The destination. |
recipient.name | string | Yes | The recipient's name. |
recipient.phone | string | Yes | Mobile money wallet number. |
recipient.network | string | Yes | Mobile money network, such as MTN, VODAFONE, TELECEL, or AIRTELTIGO. |
A mobile_money payout requires the recipient's phone and network.
Omitting either returns 400 VALIDATION_ERROR.
Read a payout response
Create, get, and list all return the same payout object. It carries the status, the amount, and the fee breakdown you are charged, and it deliberately omits the upstream provider name, provider reference, and raw provider payloads.
{
"id": "3f1c9a2e-...",
"reference": "PAY-20260915-3f1c9a2e-...",
"type": "payout",
"env": "production",
"status": "processing",
"currency": "GHS",
"amount": 10000,
"final_amount": 10000,
"channels": ["mobile_money"],
"phone": "0551234567",
"network": "MTN",
"recipient": { "name": "Kwame Asante", "phone": "0551234567" },
"transactions": [
{
"id": "b8e0...",
"channel": "mobile_money",
"status": "processing",
"amount": 10000,
"currency": "GHS",
"gross_amount": 10000,
"fee_amount": 100,
"net_amount": 9900,
"fee_bearer": "merchant",
"customer_fee": 0,
"merchant_fee": 100,
"failure_reason": "",
"created_at": "2026-09-15T10:12:00Z",
"updated_at": "2026-09-15T10:12:00Z"
}
],
"expires_at": null,
"completed_at": null,
"created_at": "2026-09-15T10:12:00Z",
"updated_at": "2026-09-15T10:12:00Z"
}| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The payout's unique identifier. |
reference | string | Yes | The payout reference. Use this to fetch or reconcile the payout, and store it against your own record. |
type | string | Yes | Always payout. |
env | string | Yes | sandbox or production, matching the key that created it. |
status | string | Yes | processing, completed, or failed. See the status table below. |
currency | string | Yes | The payout currency, GHS. |
amount | number | Yes | The amount sent to the recipient, in the smallest unit. |
final_amount | number | Yes | The amount the payout settled for, in the smallest unit. |
channels | array | Yes | The channel used, ["mobile_money"]. |
phone | string | No | The mobile money number, when the channel is mobile money. |
network | string | No | The mobile money network, when the channel is mobile money. |
recipient | object | Yes | The recipient's name and, for mobile money, phone. |
transactions | array | Yes | The disbursement attempts, each with its own status and fee breakdown. |
expires_at | string | No | When set, when the payout stops being actionable. |
completed_at | string | No | When the payout reached a final status. |
created_at | string | Yes | When the payout was created. |
updated_at | string | Yes | When the status last changed. |
The transactions[] entries expose the fee math you are charged (gross_amount, fee_amount, net_amount, merchant_fee, customer_fee, fee_bearer) and a failure_reason when a transfer fails. In the withdraw direction the recipient absorbs the customer-borne portion of the fee, and only the merchant-borne fee is added to what your balance must cover.
Follow a payout to completion
A payout create returns 202 and a processing status. The transfer itself completes asynchronously, and Seev resolves the payout when the provider confirms the result. Poll the payout by reference, or wait for the corresponding webhook, rather than treating the 202 as proof the money arrived.
curl "https://api.seevplus.com/api/v1/merchants/payouts/PAY-20260915-3f1c9a2e-..." \
-H "Authorization: Bearer $SEEV_API_KEY"| Status | Cause | What to do |
|---|---|---|
processing | The payout was created and the reserved funds are held while the transfer runs. No final result has arrived. | Keep the payout open and check again. A payout stuck in processing for an unusually long time is being re-checked by Seev; do not create a replacement. |
completed | The provider confirmed the transfer and the held funds have left your balance. | The recipient has been paid. Retain the reference for your records. |
failed | The transfer did not go through. The held funds are returned to your available balance. | Read the transaction failure_reason, correct the recipient details if needed, and create a new payout with a new Idempotency-Key. |
Do not create a second payout while the first is still processing. The
original funds are already reserved, and a replacement can send the money
twice. Wait for completed or failed, or fetch the payout by reference to
confirm its current status, before you retry.
List payouts
Fetch a paginated history for the authenticated organization, newest first.
curl "https://api.seevplus.com/api/v1/merchants/payouts?limit=20&offset=0" \
-H "Authorization: Bearer $SEEV_API_KEY"| Query | Type | Required | Description |
|---|---|---|---|
limit | number | No | Page size. Defaults to 20; values outside 1–100 fall back to 20. |
offset | number | No | Number of payouts to skip. Defaults to 0. |
The response wraps the payout objects with a total count:
{
"payouts": [ { "reference": "PAY-...", "status": "completed" } ],
"total": 42
}Fix a rejected payout
Errors return a code and a message. Branch on the HTTP status; the message is a human-readable detail for your logs, not a stable contract.
| Status | Code | Cause | What to do |
|---|---|---|---|
400 | VALIDATION_ERROR | The body is malformed, amount is not greater than zero, channel is not mobile_money, or the recipient is missing phone or network. | Fix the request and resend. Do not retry it unchanged. |
401 | UNAUTHORIZED | The request reached the endpoint without a valid organization context, or the key is not accepted. | Check the key, its product, and its environment against API Keys. |
422 | INSUFFICIENT_BALANCE | Your available balance does not cover amount + merchant fee. | Fund the balance or lower the amount, then retry. Reserved funds from in-flight payouts reduce what is available. |
404 | NOT_FOUND | The payout reference does not belong to this organization and environment. | Confirm the reference and that you are using the same environment that created it. |
500 | PAYOUT_FAILED | The disbursement provider rejected the request when the payout was created. The reserved funds are released back to your balance. | Retry with a new Idempotency-Key once the underlying issue is resolved. |
An INSUFFICIENT_BALANCE check counts money already reserved by in-flight
payouts as unavailable. Two rapid payouts for the same funds serialize, and
the second sees the reduced balance and is rejected. This is what stops a
balance from being spent twice.
Check before you send money in production
- Send a stable
Idempotency-Keyon every create, derived from your own order or payout intent plus an attempt counter. - Read
GET /api/v1/merchants/balanceand confirmavailable_balancecoversamount + merchant fee, converting for the major-unit difference. - Send the recipient fields mobile money requires:
phoneandnetwork. - Treat the
202as "accepted", not "paid". Confirmcompletedby polling the reference or handling the webhook before you tell anyone the recipient was paid. - Never create a replacement while a payout is still
processing. - Store the payout
referenceagainst your own record for reconciliation.