Authentication
RoxCustody authenticates API requests using an access token. Access tokens are short-lived and are renewed using a rotating refresh token, each refresh returns a new refresh token and invalidates the previous one.
The Dual-Token Model
Your one-time API key (generated from the Vault's Configure API menu, see Getting Started) is exchanged once for two tokens:
| Token | Purpose | Lifetime | Usage |
|---|---|---|---|
accessToken | Authenticate API requests | Short-lived (see expiresAt) | X-ACCESS-TOKEN header |
refreshToken | Obtain a new access token | Rotates on each use | POST /auth/refresh-token body |
Step 1, Generate Tokens
Use your one-time API key (generated from Vault → Configure API) to generate an access token and refresh token via POST /auth/generate-tokens. The API key is used only for this call, all subsequent requests use the access token.
| { |
| "apiKey": "YOUR-ACCESS-TOKEN" |
| } |
Language
| 1 | curl --request POST \ |
| 2 | --url https://YOUR-SUBDOMAIN.api.roxcustody.com/api/integration/auth/generate-tokens \ |
| 3 | --header 'Content-Type: application/json' \ |
| 4 | --data '{ "apiKey": "YOUR-ACCESS-TOKEN" }' |
Response, 201 Created
| { |
| "message": "Vault API Keys generated successfully", |
| "data": { |
| "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", |
| "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", |
| "expiresAt": 1767258052423, |
| "vaultId": 1 |
| }, |
| "status": 201 |
| } |
Step 2, Use the Access Token
All API requests must include a valid access token in the X-ACCESS-TOKEN request header. There is no Bearer prefix, pass the raw token string.
Language
| 1 | curl --request GET \ |
| 2 | --url https://YOUR-SUBDOMAIN.api.roxcustody.com/api/integration/vaults/details \ |
| 3 | --header 'X-ACCESS-TOKEN: YOUR-ACCESS-TOKEN' \ |
| 4 | --header 'Content-Type: application/json' |
Step 3, Refresh the Access Token
When the access token expires (HTTP 498 Token has expired), refresh it using the latest refresh token via POST /auth/refresh-token. You must store the new refresh token from the response, the old one is invalidated.
Language
| 1 | curl --request POST \ |
| 2 | --url https://YOUR-SUBDOMAIN.api.roxcustody.com/api/integration/auth/refresh-token \ |
| 3 | --header 'Content-Type: application/json' \ |
| 4 | --data '{ "refreshToken": "YOUR-REFRESH-TOKEN" }' |
Response, 201 Created
| { |
| "message": "Vault API Keys generated successfully", |
| "data": { |
| "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", |
| "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", |
| "expiresAt": 1767258052423 |
| }, |
| "status": 201 |
| } |
401 Refresh token is no longer valid. Please generate a new session.You will need to generate a new API key from the Vault's Configure API menu and start a new session.Implementation Examples
The following examples show production-ready patterns for managing token refresh automatically. Both use an interceptor/wrapper approach so you never need to manually handle 498 responses in your business logic.
| const axios = require('axios') |
| const BASE_URL = 'https://YOUR-SUBDOMAIN.api.roxcustody.com/api/integration' |
| let accessToken = null |
| let refreshToken = null |
| async function refreshAccessToken() { |
| const res = await axios.post(`${BASE_URL}/auth/refresh-token`, { |
| refreshToken: refreshToken, |
| }) |
| accessToken = res.data.data.accessToken |
| refreshToken = res.data.data.refreshToken // Always store the new refresh token |
| return accessToken |
| } |
| // Axios interceptor that auto-refreshes on 498 (Token has expired) |
| axios.interceptors.response.use( |
| (response) => response, |
| async (error) => { |
| if (error.response?.status === 498) { |
| const newToken = await refreshAccessToken() |
| error.config.headers['X-ACCESS-TOKEN'] = newToken |
| return axios(error.config) |
| } |
| return Promise.reject(error) |
| } |
| ) |
| import requests |
| BASE_URL = "https://YOUR-SUBDOMAIN.api.roxcustody.com/api/integration" |
| class RoxClient: |
| def __init__(self, api_key: str): |
| self.api_key = api_key |
| self.access_token = None |
| self.refresh_token = None |
| def authenticate(self): |
| """Exchange the one-time API key for the initial token pair.""" |
| res = requests.post(f"{BASE_URL}/auth/generate-tokens", json={"apiKey": self.api_key}) |
| res.raise_for_status() |
| data = res.json()["data"] |
| self.access_token = data["accessToken"] |
| self.refresh_token = data["refreshToken"] |
| def refresh(self): |
| """Obtain a new access token using the rotating refresh token.""" |
| res = requests.post(f"{BASE_URL}/auth/refresh-token", json={"refreshToken": self.refresh_token}) |
| res.raise_for_status() |
| data = res.json()["data"] |
| self.access_token = data["accessToken"] |
| self.refresh_token = data["refreshToken"] # MUST update, old token is now invalid |
| def get(self, path: str): |
| """Make an authenticated GET request, auto-refreshing on 498.""" |
| headers = {"X-ACCESS-TOKEN": self.access_token} |
| res = requests.get(f"{BASE_URL}{path}", headers=headers) |
| if res.status_code == 498: |
| self.refresh() |
| headers["X-ACCESS-TOKEN"] = self.access_token |
| res = requests.get(f"{BASE_URL}{path}", headers=headers) |
| res.raise_for_status() |
| return res.json() |
Authentication Errors
| Code | HTTP Status | Meaning | Action |
|---|---|---|---|
E00401 | 400 | Invalid API Key | Verify the key from Vault → Configure API; generate a new one if needed |
E00402 | 422 | Validation error (e.g. missing or malformed field) | Check the request body matches the documented schema |
E01401 | 401 | Invalid access token | Check the token is correct and not truncated |
E01402 | 401 | Invalid refresh token, or refresh token no longer valid | Generate a new API key and start a new session |
E01403 | 498 | Token has expired | Call POST /auth/refresh-token to renew |
Security Best Practices
- Store tokens securely and never expose them in client-side applications
- Store your API key in a secrets manager (AWS Secrets Manager, HashiCorp Vault, Doppler)
- Always persist the newest refresh token immediately, the previous one is invalidated on each refresh
- Implement token refresh as a singleton operation, concurrent refreshes can reuse an old token and revoke the session
- Monitor for
E01402errors, which indicate a refresh token was reused