Skip to main content
An OAuth application is the developer-side record (client_id, redirect URIs, requested scopes) that lets you run the Authorization Code with PKCE flow against a merchant’s SouthPay account. Tokens issued through it look like spo_… and reach any public endpoint the granted scopes allow. Register the app once, then implement the runtime flow described in Authentication.
Managing apps is a developer-portal task, not a runtime API. /api/v2/merchants/oauth_apps accepts a logged-in dashboard session only. An sp_ API key or an spo_ token gets 401. Create and edit apps at app.southpay.io; the request shapes below describe what the dashboard does on your behalf.

Public or confidential

PKCE with S256 is mandatory for both kinds. A confidential client secret does not exempt you from sending code_challenge, and plain is not accepted. A request without a challenge fails with invalid_request.
confidential is immutable after creation, as is client_id. To change either, register a new app.

Scopes

An app declares the scopes it may ever request. Eighteen exist; the merchant approves a subset at consent time.
offline_access changes the access token lifetime, and not in the direction people expect. With it, the access token lives one hour and you must refresh. Without it, the access token lives 90 days and there is no refresh token. Decide which model your app wants before you register it. See Token lifetimes.
One rough edge in the consent flow to work around:
The consent screen renders no description for accounts:read, accounts:write, accounts:act and agents:write. Merchants see the scope name and nothing else. If your app requests any of them, explain what they do in your own UI before you send the merchant to the authorize page.

Creating an app

client_secret is returned only here and from rotate_secret. Its prefix is spcs_. Every later read omits the field entirely and tells you only has_secret: true.
id is a bare UUID. client_id is spo_app_dev_ followed by 24 hex characters. Validation failures come back as {"errors": ["…"]} with 422, the list shape rather than the object shape. See Errors.

Reading and updating

GET /api/v2/merchants/oauth_apps returns a bare array of your apps, newest first. GET /api/v2/merchants/oauth_apps/{id} returns one. PATCH accepts name, website_url, logo_url, active, redirect_uris and scopes. It does not accept client_id or confidential. Narrowing scopes does not retroactively narrow tokens already issued. Existing grants keep whatever the merchant approved until they expire or are revoked.

Rotating the secret

Returns the app with a fresh client_secret. Public apps get 422 {"error": "not_confidential", "error_description": "Only confidential apps have secrets"}.
Rotation invalidates the old secret immediately, with no overlap window. Every server using it starts failing client authentication at the token endpoint the moment the request returns. Deploy the new value first, or accept a gap.

Deleting

Returns 204 and sets active: false. It is a deactivation, not a hard delete. Access tokens already issued keep working until they expire; refresh attempts against a deactivated app fail.

Discovery

https://api.southpay.io/.well-known/oauth-authorization-server advertises the authorize, token, revoke and userinfo endpoints, plus: Use it for dynamic configuration rather than hardcoding paths.
Last modified on August 21, 2026