# Authentication

> API key, OAuth 2.0 client credentials, and JWT authentication for the Coinbax Payments and Workspace APIs.
> Source: https://developers.coinbax.com/docs/api/authentication
> Last updated: 2026-07-16

Coinbax has two API surfaces with different authentication:

| API | Base URL (staging) | Auth |
|---|---|---|
| Payments API | `https://api-staging.coinbax.com/api/v1` | `X-API-Key` header, or 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/v1` and
`https://core.coinbax.com/api/v1`. Staging credentials only work against
staging; the environments share nothing.

## API keys (Payments API)

The simplest way to call the Payments API from a backend:

```bash
curl https://api-staging.coinbax.com/api/v1/transactions \
  -H "X-API-Key: $COINBAX_API_KEY"
```

Key facts:

- **Shown once.** The key is returned a single time at creation and cannot
  be retrieved later. Store it in a secret manager immediately.
- **Scoped.** Every key carries a set of [scopes](/docs/api/scopes) that
  bound what it can do. New keys default to read-only scopes; grant write
  scopes deliberately.
- **Server-side only.** Never ship an API key in a browser, mobile app, or
  public repository. If a key leaks, regenerate it via
  `POST /auth/api-key/regenerate`.

## 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 created with an admin-scoped API key:

```bash
curl -X POST https://api-staging.coinbax.com/api/v1/oauth/clients \
  -H "X-API-Key: $COINBAX_ADMIN_KEY" \
  -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

```bash
curl -X POST https://api-staging.coinbax.com/api/v1/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:

```json
{
  "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:

```bash
curl https://api-staging.coinbax.com/api/v1/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.

```javascript
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 };
}
```

Tokens can be revoked with `POST /oauth/revoke`, and all tokens for a
client with `POST /oauth/clients/{clientId}/revoke-all-tokens`.

## JWT (Workspace API)

The Workspace API authenticates humans and dashboard-style integrations
with JWTs:

```bash
# Log in to obtain tokens
curl -X POST https://core-staging.coinbax.com/api/v1/auth/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>"
```

Access tokens expire after one hour. Use `POST /auth/refresh` to obtain a
new access token without re-authenticating, and `POST /auth/logout` to
invalidate the session.

## 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](/docs/api/scopes): what each scope grants and how the hierarchy works
- [Errors and the response envelope](/docs/api/errors): what 401 and 403 responses look like
- [Payments API reference](/reference/coinbax-api/): the auth and OAuth endpoints in full