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:
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.
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.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.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: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
SendX-Southpay-Account alongside your platform credentials to scope any request to one of your sub-merchants:
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.
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:
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.*orsubscription.*event on a sub-merchant reaches that merchant’s endpoints only, never yours. - Payout events do not exist.
payout.created,payout.confirmedandpayout.failedpass subscription validation and are never emitted, for platforms or merchants. PollGET /payoutsinstead. See Webhooks.

