Skip to main content
Platform mode is reviewed before it is switched on. Read this first.
Platform mode is off until someone at SouthPay approves it. Until it is enabled on your store, every endpoint on this page returns:
403
The same 403 comes back from any request carrying X-Southpay-Account.
Ask for it in the dashboard under Settings > Platform, or by API:
One request can be under review at a time; a second returns 409 access_request_pending, and a store that is already a platform returns 409 already_a_platform. GET /platform/access_request returns { "request": null } until you have asked, then the latest request with its status. A rejected request carries a rejection_reason and you can ask again. Review is a person, not a queue timer, because a platform takes a cut of its merchants’ payments and stands behind their verification. A platform is a SouthPay account that onboards and manages other merchants, called sub-merchants. You integrate once, create accounts by API, and earn a fee on every payment your merchants take. Each sub-merchant is a full SouthPay account. It has its own balances, payout destinations, accepted assets, webhook endpoints, refunds and verification. Nothing about the payment API changes for them. Use platform accounts when you resell SouthPay under your own brand and your merchants never sign up with us directly. If your merchants already have their own SouthPay accounts and authorize you to act for them, use OAuth apps instead.

Scopes

Platform endpoints need scopes that a normal secret key is not created with. Once your store is enabled, issue a key that carries them. A request missing one returns 403 insufficient_scope, which is a different failure from not_a_platform. Read the code, not the status. Scopes are fixed when a key is created. A key you made before your store was enabled keeps the non-platform set for good, so issue a new one after approval rather than reusing the old key. The dashboard flags any key in that state, and the insufficient_scope message says so too.

How money splits

Every payment a sub-merchant takes is split three ways:
Both rates apply to the gross amount, so they add rather than compound. If SouthPay charges 1.5% and your platform fee is 1%, the merchant pays 2.5% in total. Your fee is credited to your own SouthPay balance in the same asset. You withdraw it exactly like any other balance, through your normal payouts. There is no separate settlement, and no on-chain transfer for the fee itself. Rates resolve from most specific to least: A rate negotiated with your platform applies to every merchant you onboard, so you do not have to set it per account. Both rates are snapshotted onto the payment when it is created, so changing a rate never alters payments already in flight.
When a payment is refunded, your fee is reversed along with it. If you have already withdrawn the fee and your balance cannot cover the reversal, the shortfall is recorded against your account and recovered from later fees.

Creating a sub-merchant

external_id is your own identifier for the merchant. Sending the same one again returns the existing account rather than creating a duplicate, so the call is safe to retry. platform_fee_percentage is optional and a decimal rate, so 0.005 is 0.5%. Leave it out to inherit your platform default. Accounts come back with: id, name, external_id, platform_fee_percentage, kyc_confirmed_at, charges_enabled, payouts_enabled, verification_status, created_at. verification_status is not_started, pending, approved or rejected. It reads approved once KYC is confirmed, whatever the underlying verification request says.
name, platform_fee_percentage and external_id all change on a PATCH. country is accepted and ignored. It only seeds default assets at creation time.Retire an account with DELETE /platform/accounts/{id}. It is a soft delete: keys are revoked, webhooks disabled, ledger history kept, and POST /platform/accounts/{id}/restore brings it back. An account still holding a balance, a pending payout, an in-flight payment or an open refund is refused with 409 account_has_active_funds and a list of what is blocking it.
See the API reference for the full endpoint set.

Getting a merchant verified

A sub-merchant cannot receive payouts until it has been verified and has a payout destination. Sub-merchants have no SouthPay login of their own, so there are two ways to get them there.

Hosted onboarding

SouthPay collects the details and reviews them. This keeps the compliance work off you.
The response contains a url. Redirect the merchant to it. They enter their business details and a payout address, and are sent back to your return_url when they finish. Links are single use and expire after seven days by default. Pass expires_in in seconds to change that, up to a hard maximum of 30 days. Anything larger is clamped, not rejected. The token is shown once, in the create response, and is not retrievable afterwards. A link that has been used or has expired returns onboarding_link_unusable, as 404 or 410 depending on which. Mint a fresh one rather than retrying the old token.

Platform attestation

If you have already run your own diligence, submit the merchant’s details directly:
The request is recorded as attested by your platform. SouthPay still reviews anything that touches bank payouts.

Reopening one step

Mint a link scoped to a single step when a merchant needs to fix something. A scoped link only permits that step and refuses the other.
Subscribe to verification events to know when a merchant is approved or rejected, rather than polling.

Acting on a sub-merchant

Send X-Southpay-Account alongside your platform credentials to scope any request to one of your sub-merchants:
Every merchant endpoint accepts this header, so you can create payments, read balances, issue refunds and request payouts on a merchant’s behalf without a separate integration. Requires the accounts:act scope. Three distinct 403s come out of this header, and the code tells them apart: not_a_platform if your store is not enabled, insufficient_scope if the credential lacks accounts:act, and account_not_found if the id is not one of your sub-merchants.
Two things are blocked while acting as a sub-merchant, whatever your scopes.Payout destinations. You cannot add, select or remove a sub-merchant’s bank account or wallet address. 403 platform_cannot_change_payout_destination. Only the merchant can, through an onboarding link with purpose: "payout_destination".Credentials. You cannot create, rotate or revoke a sub-merchant’s API keys or agent credentials. 403 platform_cannot_issue_credentials.Both exist so a compromised platform credential cannot redirect a merchant’s settlements or mint a credential that outlives your access.

Seeing the whole book

GET /platform/overview returns your merchant counts, how many are verified, processed volume, your fee earned per asset, and your busiest merchants. GET /platform/payments lists payments across every sub-merchant, filterable to one account with ?account_id=. Each row carries id, reference, status, settlement_currency, settlement_amount_cents, platform_fee_percentage, created_at and an account object. Paginated with page and per_page (default 25, maximum 100) inside a meta envelope.

Verification events

Subscribe your platform’s webhook endpoint to these to follow onboarding without polling: Payment, transaction and store-verification events for your sub-merchants are also delivered to your platform’s endpoints, in addition to the merchant’s own. Any event that concerns a sub-merchant carries a top-level account field naming it:
Events for your platform’s own payments do not carry account, so you can route on its presence. See Webhooks for signature verification and delivery behaviour. Two gaps to plan around:
  • Billing events do not fan out. An invoice.* or subscription.* event on a sub-merchant reaches that merchant’s endpoints only, never yours.
  • Payout events do not exist. payout.created, payout.confirmed and payout.failed pass subscription validation and are never emitted, for platforms or merchants. Poll GET /payouts instead. See Webhooks.
Last modified on August 21, 2026