Skip to main content
A platform account lets you onboard other businesses under your own SouthPay account, take payments on their behalf, and collect a fee on each one. Each business is a sub-merchant: a separate store with its own balances, payouts and verification state. Paths are relative to https://api.southpay.io/api/v2.
Platform mode is not self-serve. It is not enabled by default, and only SouthPay staff can turn it on for your store. Until it is enabled on your store, every endpoint on this page returns 403 with {"error": {"message": "This account is not a platform", "code": "not_a_platform"}}. Contact support before integrating.

Authentication and scope

Reads require accounts:read, writes require accounts:write, and acting on a sub-merchant requires accounts:act. None of the three are in the default API key scope set. A key created before your store became a platform will not hold them. Issue a new key with the platform scopes.

Create a sub-merchant

POST /platform/accounts. Scope accounts:write. All fields go inside a required account object.
string
required
The sub-merchant’s business name. Whitespace is trimmed; a blank value is rejected.
string
Your own identifier for this merchant. Creation is idempotent on this value. Posting the same external_id twice returns the existing account rather than creating a second one.
string
Your fee on this sub-merchant’s payments, as a decimal fraction ("0.015" is 1.5%).
string
ISO country code. Used once, to seed the new store’s defaults. Ignored on update.

Response fields

string
required
The sub-merchant’s store id. Send this in X-Southpay-Account to act on their behalf.
string
required
string | null
Your identifier, as supplied at create time.
string | null
The effective fee as a string, not a number.
string | null
When verification was approved. Null until then.
boolean
required
True once KYC is confirmed. Until then the sub-merchant cannot take payments.
boolean
required
True once the sub-merchant has chosen a payout destination.

List sub-merchants

GET /platform/accounts. Scope accounts:read. Returns a bare array of active sub-merchants, newest first. No envelope, no pagination.
string
Exact match on your own identifier. The way to resolve one of your ids to a SouthPay account id.

Retrieve a sub-merchant

GET /platform/accounts/{id}. Scope accounts:read. Returns a bare account object. An unknown id returns 404 with a machine code: {"error": {"message": "Unknown sub-merchant account", "code": "account_not_found"}}.

Update a sub-merchant

PATCH /platform/accounts/{id}. Scope accounts:write. Applies name, platform_fee_percentage and external_id. Re-using an external_id that another of your sub-merchants already holds is refused with 422, and the account keeps its previous value.
country is accepted by the request and ignored. It only seeds default assets at creation time and cannot be changed afterwards.

Delete a sub-merchant

DELETE /platform/accounts/{id}. Scope accounts:write. Retires a sub-merchant you own. The account is soft-deleted, its API keys and agent credentials are revoked, and its webhook endpoints are disabled. Ledger history is preserved, and a store.deleted platform event is emitted. Returns 204 with no body. The account stops appearing in GET /platform/accounts and can no longer be acted on through the X-Southpay-Account header.
An account still holding money is refused with 409 and a blockers list naming what is in the way: store_balance, pending_payout, in_flight_payment_intent or pending_refund. Settle or pay out those first, then retry. There is no force flag on this endpoint. Funds are never stranded by a platform call.

Restore a sub-merchant

POST /platform/accounts/{id}/restore. Scope accounts:write. Reverses a delete and returns the account object. Revoked API keys are not reissued and disabled webhook endpoints stay disabled; re-create those as needed. Restoring an account that was never deleted returns 422 with code account_not_deleted.

Attest verification

POST /platform/accounts/{account_id}/verification. Scope accounts:write. Submits KYB details you have already collected, so the sub-merchant does not have to re-enter them. Fields may be sent inside a verification_request object or at the top level.
string
string
string
string
string
string
string
Returns 201 with the verification request: id, status, the submitted fields, plus rejection_reason, submitted_at, reviewed_at, created_at, updated_at. status is pending, approved or rejected. Approval is a human review. Attesting does not approve anything by itself. Subscribe to store.verification.approved and store.verification.rejected to learn the outcome; both fan out to the platform’s own webhook endpoints. POST /platform/accounts/{account_id}/onboarding_links. Scope accounts:write. Creates a single-use dashboard link the sub-merchant opens to complete a step themselves. Fields may be sent inside an onboarding_link object or at the top level.
string
onboarding, kyb or payout_destination. Pick payout_destination when the only thing missing is where their money goes. A platform is not allowed to set that itself.
string
Where the sub-merchant lands after finishing.
integer
Link lifetime in seconds. Defaults to 7 days and is capped at 30 days.
Response 201
token and url are returned only on create. Retrieving the link later gives you token_prefix and nothing usable. Hand the URL to the sub-merchant immediately or store it securely.
A validation failure returns 422 {"error": {"message": "…", "code": "invalid_onboarding_link"}}.

Platform overview

GET /platform/overview. Scope accounts:read. Aggregate figures across your sub-merchants for a date range.
string
ISO date. Defaults to 29 days before to.
string
ISO date. Defaults to today.
Ranges longer than 365 days are clamped. Unparseable dates fall back to the defaults.
Response 200
activated_count is the number of sub-merchants with confirmed KYC. platform_fee_earned is your accrued fee per asset in atomic units, net of debits. top_sub_merchants is capped at five.

Platform payments

GET /platform/payments. Scope accounts:read. Payments across every sub-merchant, newest first. This is a purpose-built summary, not the payment intent shape. There is no reference lookup, no addresses and no metadata.
string
Limit to one sub-merchant.
string
pending, processing, completed, failed, expired or refunded. Unrecognized values are ignored rather than rejected.
integer
default:"1"
integer
default:"25"
Between 1 and 100.
Response 200
settlement_amount_cents is an integer here, unlike the two-decimal settlement_amount string on a payment intent. platform_fee_percentage is the rate snapshotted at payment time, as a string, and may be null on older payments.

Acting on a sub-merchant

To call any other endpoint as a sub-merchant, add the header:
Your credential must hold accounts:act in addition to the scope the target endpoint needs. Everything then behaves as if the sub-merchant made the call, against their balances, their payments and their webhooks.
Request

Guardrails

Two things a platform can never do while acting, regardless of scope: For payout destinations, issue an onboarding link with purpose: "payout_destination" and let the sub-merchant set it themselves.

Header errors

Webhook fan-out

Payment and store-verification events for a sub-merchant are delivered twice: once to the sub-merchant’s own endpoints, and once to yours with an extra top-level account key holding the sub-merchant’s store id. Use that key to route the event. Billing events do not fan out to platforms.

Create and update errors

Last modified on August 21, 2026