POST /payouts, or automatically from a payout schedule. Both produce the same resource.
Paths are relative to https://api.southpay.io/api/v2.
Before you can pay out
POST /payouts needs a payout destination: a saved bank account or wallet address, referenced by fiat_payout_detail_id or crypto_payout_detail_id. You cannot create one over the public API; they are configured in the dashboard. See payout destinations for what each kind is, where the ids come from, and what a fiat destination requires.
The crypto path also accepts a raw destination_address instead of a saved detail id, which skips the saved-destination requirement but not the address validation.
Authentication and scope
Reads requirepayouts:read, writes require payouts:write. Publishable keys cannot call any of these endpoints.
Creating a crypto payout runs a step-up MFA check. For API keys and OAuth tokens that check is a no-op and never fires. It only gates dashboard sessions, which return
403 mfa_required without two-factor enabled, or 503 mfa_check_unavailable if the identity provider is down. The same applies to POST /balance_swaps and POST /payout_schedules/{id}/run.Create a payout
POST /payouts. Scope payouts:write. Idempotency-Key is required.
Parameters are sent at the top level, not wrapped in an object.
string
required
The asset to send, for example
ETH_USDC. Must be an active asset in your current mode.string
required
Amount in atomic units of
asset_id, as a string. Must be positive.string
Off-ramp to this saved bank account. Sending this selects the fiat path.
string
Send on-chain to this saved wallet. Its network must match the asset’s network.
string
Send on-chain to a raw address. Used only when
crypto_payout_detail_id is absent.fiat_payout_detail_id is present the payout takes the fiat path; otherwise it takes the crypto path and needs either crypto_payout_detail_id or destination_address.
Fiat off-ramp constraints
- The store must have confirmed KYC. Otherwise
403{"error": "Store KYC verification required"}. - Only USDT and USDC can be off-ramped. Anything else returns
422{"error": "BTC is not eligible for fiat off-ramp"}. - Minimums are 10 USDT and 0.1 USDC. Below that:
422{"error": "Minimum amount for USDT fiat payout is 10 USDT"}.
Response fields
string
required
Bare UUID.
string
required
crypto (on-chain), fiat_manual (off-ramp from an existing stablecoin balance) or fiat_autoswap (swap to a stablecoin, then off-ramp). POST /payouts produces crypto or fiat_manual; fiat_autoswap comes from schedules.string
required
Atomic units of
asset.asset_id.string
required
The same amount as a human decimal string with trailing zeros stripped.
string | null
On-chain destination. Null on fiat payouts.
string | null
On-chain hash, once broadcast.
string | null
Custody provider’s own reference.
string | null
Network fee charged for a crypto payout, as a decimal string. Null until known.
string | null
Currency of the receiving bank account. Fiat payouts only.
integer | null
What actually landed in the bank account, in minor units. Only known after settlement.
string | null
The rate actually applied, which may differ from the quoted rate.
integer | null
Off-ramp fee actually charged.
string | null
The bank reference on the outgoing transfer. Use this to match a SouthPay payout to a line on your bank statement.
string | null
Settlement timestamp.
object[]
required
Synthesized history, oldest first. Retries append a further
submitted/failed pair rather than replacing earlier entries.string | null
Null unless
status is failed. One of insufficient_funds, destination_invalid, temporarily_unavailable, unknown. Branch on this, not on failure_reason.string | null
The provider’s raw error text. Free-form and not stable. For humans only.
object
required
Quote a fiat payout
POST /payouts/quote. Scope payouts:write. Fiat only. Creates nothing.
string
required
Must be a USDT or USDC asset.
string
required
Atomic units. Must be positive.
string
required
The bank account you would pay out to. Its currency drives the quote.
estimated_fiat_cents and estimated_fee_cents are estimates. What actually lands is reported as paid_fiat_amount_cents and paid_fee_cents on the payout. Check meets_minimum before creating: a quote is still returned when the amount is below the minimum, but the payout would be rejected.
Retrieve a payout
GET /payouts/{id}. Scope payouts:read. Returns a bare payout object. Unknown id returns 404 {"error": "Payout not found"}.
List payouts
GET /payouts. Scope payouts:read. Newest first.
string
Filter by payout status.
string
crypto, fiat_manual or fiat_autoswap.string
Only payouts produced by one schedule.
string
Only payouts of one asset.
integer
default:"1"
integer
default:"25"
Between 1 and 100.
Response 200
Retry a payout
POST /payouts/{id}/retry. Scope payouts:write. No body, no MFA check.
Only a failed payout whose funds have not already been returned to your balance can be retried. Returns 200 with the updated payout.
Lifecycle
Create errors
Every one of these uses the flat
{"error": "<message>"} envelope, except the retry responses above, which add a code. Errors raised deeper in the service layer, most notably from POST /payout_schedules/{id}/run, surface through the global handler as {"error": {"message": "…", "code": null}}. Handle both.
