APIs
OAuth2 apps
Let another app act on a Payer user's behalf. A standard OAuth2 authorization-code flow with PKCE and rotating refresh tokens.
OAuth2 lets a third-party app act on behalf of a Payer user, with the user's explicit consent and never more access than they granted. Payer runs a standard authorization-code flow with PKCE, and issues rotating refresh tokens.
Use OAuth when your app serves other people's Payer accounts. If you only need your own data, a personal access token is simpler; if you're a merchant taking payments, use a secret key.
Registering an app
A merchant admin registers an app from the Developers page in the dashboard. Registration returns a client_id (prefixed oauthapp_) and, for confidential apps, a client_secret shown once.
| Setting | Notes |
|---|---|
redirect_uris | Exact-match only — the redirect_uri in a request must be identical to a registered one. |
allowed_scopes | The scopes the app may ever request. |
type | public (mobile/SPA, no secret, PKCE required) or confidential (server-side, has a secret). |
The flow
1. App → generates code_verifier + code_challenge (S256)
2. App → sends the user to Payer's consent screen
3. User → approves; Payer redirects back with ?code=...&state=...
4. App → POST /oauth/token with the code + code_verifier
5. Payer → returns access_token (oat_) + refresh_token (ort_)1 · Send the user to consent
Redirect the user to Payer's hosted authorization screen with a PKCE challenge. The screen's URL is shown when you register your app; send it these query parameters:
response_type=code
client_id=oauthapp_...
redirect_uri=https://yourapp.com/callback
scope=balances:read transactions:read
state=<opaque csrf value>
code_challenge=<base64url(sha256(verifier))>
code_challenge_method=S256Note
Only response_type=code is supported, and only the S256 challenge method. PKCE is required for public apps and recommended for all. Always send an unguessable state and check it on return.
The user reviews the requested scopes and approves or denies. Payer redirects back to your redirect_uri with ?code=...&state=... on approval, or ?error=access_denied&state=... on denial. Authorization codes are single-use and expire after 5 minutes.
2 · Exchange the code for tokens
POST /oauth/tokenAuthenticate the client with HTTP Basic (client_id:client_secret) or with client_id/client_secret in the body. Public apps send client_id and the code_verifier only.
curl https://api.payer.app/oauth/token \
-u "oauthapp_...:<client_secret>" \
-d "grant_type=authorization_code" \
-d "code=<code from the redirect>" \
-d "code_verifier=<original verifier>" \
-d "redirect_uri=https://yourapp.com/callback"{
"access_token": "oat_...",
"refresh_token": "ort_...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "balances:read transactions:read"
}Use the access token as a bearer token against /graphql, exactly like a PAT — it can read whatever scopes the user consented to:
curl https://api.payer.app/graphql \
-H "Authorization: Bearer oat_..." \
-H "Content-Type: application/json" \
-d '{"query":"query { balance { available } }"}'3 · Refresh the access token
Access tokens live 1 hour. When one expires, exchange the refresh token for a new pair. Refresh is rotating — the presented refresh token and its access token are revoked, and a fresh pair is returned. Store the new refresh token; the old one is now dead.
curl https://api.payer.app/oauth/token \
-u "oauthapp_...:<client_secret>" \
-d "grant_type=refresh_token" \
-d "refresh_token=ort_..."Refresh tokens live 60 days. If a used authorization code or a rotated refresh token is replayed, Payer revokes the whole chain issued for it — so a stolen token can't be quietly reused alongside the legitimate one.
4 · Revoke a token
POST /oauth/revokecurl https://api.payer.app/oauth/revoke \
-u "oauthapp_...:<client_secret>" \
-d "token=ort_..."Revocation always returns 200 with an empty body — it never reveals whether the token existed — and revokes the token together with its linked pair.
Token summary
| Token | Prefix | Lifetime |
|---|---|---|
| Authorization code | — | 5 minutes, single use |
| Access token | oat_ | 1 hour |
| Refresh token | ort_ | 60 days, rotating |
Errors
The token and revoke endpoints return RFC 6749 error bodies:
{
"error": "invalid_grant",
"error_description": "The authorization code is invalid or has expired."
}error | Status | When |
|---|---|---|
invalid_request | 400 | A required parameter is missing or malformed. |
invalid_client | 401 | Client authentication failed. |
invalid_grant | 400 | The code or refresh token is invalid, expired or already used. |
unauthorized_client | 400 | This client may not use this grant. |
unsupported_grant_type | 400 | grant_type isn't authorization_code or refresh_token. |
invalid_scope | 400 | A requested scope isn't in the app's allowed set. |