Guides

Authentication

API key, OAuth 2.0 client credentials, and JWT authentication for the Coinbax Payments and Workspace APIs.

Last updated 2026-08-28View as Markdown

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:

  1. Cache the access_token with its computed expiry (now + expires_in seconds).
  2. Refresh early. Treat the token as expired about 60 seconds before its actual expiry so in-flight requests never race the deadline.
  3. Share in-flight refreshes. If multiple concurrent requests find the cache empty, they should await one token request, not fan out N of them.
  4. 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