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.
Endpoints
| Authorization | https://connect.payer.app/authorize |
| Token | https://api.payer.app/oauth/token |
| Revocation | https://api.payer.app/oauth/revoke |
| Discovery | https://api.payer.app/.well-known/oauth-authorization-server |
The consent screen sits on its own domain, so the authorization endpoint is not on api.payer.app like the rest. Most OAuth libraries can configure themselves from the discovery document (RFC 8414) rather than from hardcoded URLs.
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:
https://connect.payer.app/authorizeSend 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, never revealing whether the token existed, and revokes the token together with its linked pair.
Token summary
| Token | Prefix | Lifetime |
|---|---|---|
| Authorization code | none | 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. |
Try it live
The playground below runs the whole flow from your browser: it builds the PKCE challenge, sends you to the real consent screen, takes the code back, and exchanges it for tokens you can then spend against /graphql. Nothing is proxied through this site, and everything you paste stays in this browser tab.
Two things to do first:
- 1.Register an app on the Developers page in the dashboard.
- 2.Add
https://payer.mv/developers/docs/oauthto that app's redirect URIs. Redirect URIs are exact-match, and the playground shows you the value to copy.
Test and Live
An OAuth app belongs to one environment. Register it with the dashboard in sandbox mode and it exists in Test; register it in live mode and it exists in Live. The same client id is never valid in both, so the playground's Environment switch moves the consent screen and the token endpoint together:
| Environment | API | Consent screen |
|---|---|---|
| Test | https://api.nonprod.payer.app | https://connect.nonprod.payer.app |
| Live | https://api.payer.app | https://connect.payer.app |
Test runs against sandbox data with its own users and balances. In Live the consent screen signs in a real Payer user and grants real scopes, so start there with the read scopes only.
Register an app on the Developers page in the dashboard, then paste its client id here.
api.payer.appAn app exists in one environment only. A client id registered from the dashboard in sandbox mode works in Test, and one registered in live mode works in Live. Switching here clears any tokens you are holding, because they belong to the environment that issued them.
Your app can only request scopes in its allowed set. Anything else comes back as invalid_scope.
Everything you type here stays in this browser tab and is sent only to api.payer.app and connect.payer.app. Nothing is proxied through this site.
This generates a PKCE verifier, keeps it in this tab, and leaves the page for the real consent screen.
Add a client id and pick at least one scope to start the flow.
/oauth/token3 · Exchange/graphql4 · Call the APIComplete the flow above to get an access token, then run a query as the user who approved it.
/oauth/token5 · RefreshExchanges the refresh token for a fresh pair. Rotation means the token you just presented is dead the moment this succeeds, so the pair above is replaced. Replaying the old one revokes the whole chain.
/oauth/revoke6 · Revoke