Coinbax has two API surfaces with different authentication:
| API | Base URL (staging) | Auth |
|---|---|---|
| Payments API | https://api-staging.coinbax.com/api/v2 |
OAuth 2.0 Bearer token |
| Workspace API | https://core-staging.coinbax.com/api/v1 |
Authorization: Bearer <JWT> |
Production bases are https://api.coinbax.com/api/v2 and
https://core.coinbax.com/api/v1. Staging credentials only work against
staging; the environments share nothing.
API keys are v1 only
The Payments API v2 accepts OAuth 2.0 Bearer tokens only. API keys still
work against /api/v1, which is deprecated and sunsets on 1 November
2026 — after that date there is no key-based path into the Payments API.
If you are on keys today, move to the client_credentials flow below. Short
lived tokens are the reason for the change: a leaked key is valid until
somebody notices and regenerates it, whereas a leaked token expires on its
own.
OAuth 2.0 client credentials (service-to-service)
For service-to-service integrations, and for any client where you want
short-lived credentials instead of a long-lived key, use the OAuth 2.0
client_credentials grant. You exchange a client ID and secret for a
Bearer token, then send that token on Payments API requests.
Create an OAuth client
OAuth clients are managed on the Workspace API, not the Payments API, and
authenticated with your workspace JWT. There is no client-creation endpoint on
/api/v2: a credential that can mint credentials belongs with the rest of
workspace administration.
curl -X POST https://core-staging.coinbax.com/api/v1/workspaces/$WORKSPACE_ID/oauth/clients \
-H "Authorization: Bearer $WORKSPACE_JWT" \
-H "Content-Type: application/json" \
-d '{
"name": "My Backend Service",
"scopes": ["read:transactions", "write:transactions"],
"grantTypes": ["client_credentials"]
}'
The response includes clientId and clientSecret. Like API keys, the
secret is shown once.
Exchange credentials for a token
curl -X POST https://api-staging.coinbax.com/api/v2/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "client_credentials",
"client_id": "coinbax_client_...",
"client_secret": "coinbax_secret_...",
"scope": "read:transactions write:transactions"
}'
scope is optional and space-separated; omit it to receive all scopes
granted to the client. The token arrives in the standard response envelope:
{
"success": true,
"data": {
"access_token": "eyJhbGciOi...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read:transactions write:transactions"
},
"meta": { "timestamp": "...", "requestId": "..." },
"error": null
}
Then call the API with the token:
curl https://api-staging.coinbax.com/api/v2/transactions \
-H "Authorization: Bearer eyJhbGciOi..."
Token lifecycle: cache, refresh, retry
Do not request a new token per API call. The correct pattern:
- Cache the
access_tokenwith its computed expiry (now + expires_inseconds). - Refresh early. Treat the token as expired about 60 seconds before its actual expiry so in-flight requests never race the deadline.
- Share in-flight refreshes. If multiple concurrent requests find the cache empty, they should await one token request, not fan out N of them.
- Retry on 401. If a request returns 401 with a token you believed valid (revocation, clock skew), discard the cached token, fetch a fresh one, and retry the request once.
let cached = null; // { token, expiresAt }
let inflight = null;
async function getAccessToken() {
if (cached && Date.now() < cached.expiresAt - 60_000) return cached.token;
inflight ??= fetchToken().finally(() => { inflight = null; });
cached = await inflight;
return cached.token;
}
async function fetchToken() {
const res = await fetch(`${BASE}/oauth/token`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "client_credentials",
client_id: process.env.COINBAX_CLIENT_ID,
client_secret: process.env.COINBAX_CLIENT_SECRET,
}),
});
const { data } = await res.json();
return { token: data.access_token, expiresAt: Date.now() + data.expires_in * 1000 };
}
Revoke a single token with POST /oauth/revoke on the Payments API.
Revoking every token for a client, and client management generally, lives on
the Workspace API:
POST /workspaces/{id}/oauth/clients/{clientId}/revoke-tokens. One resource,
one home — the Payments API used to publish a duplicate of this surface under
a slightly different name, and from v2 it does not.
JWT (Workspace API)
The Workspace API authenticates humans and dashboard-style integrations with JWTs:
# Log in to obtain tokens
curl -X POST https://core-staging.coinbax.com/api/v1/identity/login \
-H "Content-Type: application/json" \
-d '{ "email": "you@example.com", "password": "..." }'
# Use the access token
curl https://core-staging.coinbax.com/api/v1/workspaces/me \
-H "Authorization: Bearer <access-token>"
Log in against /identity/login. The identity service is the single source of
truth for accounts, and /auth/login is not a live route despite appearing in
older material — a call to it returns a bare Bad request. rather than a
Coinbax error envelope.
Access tokens expire after one hour. Use POST /identity/refresh to obtain a
new access token without re-authenticating, and POST /identity/logout to
invalidate the session. Both take the refresh token in the body.
Choosing a method
| You are building | Use |
|---|---|
| A backend that creates payments | API key, or OAuth for short-lived credentials |
| A service-to-service integration | OAuth 2.0 client credentials |
| A tool against workspace data (customers, settings, webhooks) | Workspace API with JWT |
| Anything in a browser or mobile app | OAuth via your own backend proxy; never embed keys or secrets client-side |
Next steps
- API scopes: what each scope grants and how the hierarchy works
- Errors and the response envelope: what 401 and 403 responses look like
- Payments API reference: the auth and OAuth endpoints in full