Obtain access tokens
Exchange a service user credential for an access token at the /oauth/token endpoint, use it to call the BreezeAuth API and request a new one when it expires.
A service user doesn’t send its client secret to every API call. Instead, it exchanges its credential for an access token: a short-lived key that it then sends with each request. This follows the OAuth 2.0 client credentials flow, the standard way for machines to authenticate to APIs.
This page is technical. If you are setting up a service user in the admin console, share it with the developers building the integration.
Request a token
Send a POST request to the token endpoint with the credential’s client ID and client secret:
curl -X POST https://api.breezeauth.com/oauth/token \
-H 'Content-Type: application/json' \
-d '{
"grant_type": "client_credentials",
"client_id": "5f0c3a8e-2b7d-4e91-9a6c-1d2e3f4a5b6c",
"client_secret": "<client secret>"
}'
| Field | Value |
|---|---|
grant_type |
Always client_credentials. |
client_id |
The credential’s Client ID. |
client_secret |
The credential’s Client secret, shown when it was created. |
A successful request returns 200 OK:
{
"access_token": "eyJhbGciOiJFUzI1NiIsImtpZCI6Ij…",
"token_type": "Bearer",
"expires_in": 900
}
| Field | Description |
|---|---|
access_token |
The token to send with API requests. Treat it as confidential, like the secret. |
token_type |
Always Bearer. |
expires_in |
How many seconds the token is valid from now. Always read it from the response; don’t hard-code it. |
Call the API
Send the token in the Authorization header of every request:
curl https://api.breezeauth.com/organizations/<organizationsUuid>/memberships \
-H 'Authorization: Bearer <access token>'
Replace <organizationsUuid> with your organization ID, shown under the organization’s name on the
Manage Organization page.
What the service user may do depends on its administrative role. A request
outside that role is refused with 401 Unauthorized.
When the token expires: no refresh tokens
Access tokens are short-lived on purpose: if one leaks, it is useful to an attacker only for a short time.
When a token expires, the integration simply requests a new one from /oauth/token with the same credential. That’s
why BreezeAuth does not issue refresh tokens to service users, and the token response has no refresh_token field:
a refresh token exists so that people don’t have to sign in again, but a service user holds its credential and can
always obtain a new token on its own.
Recommended approach:
- Reuse a token for as long as it is valid. Don’t request a new token before every API call.
- Request a new token shortly before
expires_inruns out, or when an API call returns401 Unauthorized. - If a request still returns
401with a brand-new token, stop and alert someone: the credential or the service user has probably been deactivated.
What BreezeAuth checks
When issuing a token, BreezeAuth checks that:
- the client ID exists and the client secret matches;
- the credential is active and has not passed its Expires at date;
- the service user is Active;
- the request comes from an allowed network, if the organization has a network access restriction.
On every API request, BreezeAuth checks again that:
- the token is valid and has not expired;
- the credential that issued it is still active and not expired, so deactivating a credential cuts off its tokens immediately;
- the service user is still Active and its role allows the operation;
- the request comes from an allowed network and country, if the organization restricts them.
Errors
| Response | Meaning | What to do |
|---|---|---|
400 Bad Request from /oauth/token |
The request is malformed: a field is missing, grant_type isn’t client_credentials, or client_id isn’t a valid identifier. |
Fix the request. |
401 Unauthorized from /oauth/token |
The credential can’t be used: wrong client ID or secret, credential inactive or expired, or service user inactive. For security, the response doesn’t say which. | Check the credential and the service user in the admin console. |
401 Unauthorized with network_not_authorized |
The request comes from a network outside the organization’s allowed network zones. | Run the integration from an allowed network, or ask an administrator to update the zones. |
401 Unauthorized from an API call |
The token has expired, its credential or service user has been deactivated, or the service user’s role doesn’t allow the operation. | Request a new token and retry once. If it fails again, check the credential, the service user and its role. |
403 Forbidden with access_not_allowed_from_country |
The organization restricts access by country, and the request comes from a country that isn’t allowed. | Run the integration from an allowed country, or ask an administrator to review the restriction. |
429 Too Many Requests |
Too many requests in a short time. | Reuse tokens instead of requesting one per call, and slow down. |
Code examples
The examples below request a token, keep it in memory, renew it before it expires and retry once when the API returns
401. They read the credential from environment variables.
Node.js
Uses the built-in fetch (Node.js 18 or later).
const API_URL = 'https://api.breezeauth.com'
let cachedToken = null // { value, renewAt }
async function getAccessToken() {
if (cachedToken && Date.now() < cachedToken.renewAt) {
return cachedToken.value
}
const response = await fetch(`${API_URL}/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'client_credentials',
client_id: process.env.BREEZEAUTH_CLIENT_ID,
client_secret: process.env.BREEZEAUTH_CLIENT_SECRET,
}),
})
if (!response.ok) {
throw new Error(`BreezeAuth token request failed with status ${response.status}`)
}
const { access_token, expires_in } = await response.json()
// Renew when 90% of the lifetime has passed, so the token never expires mid-request.
cachedToken = { value: access_token, renewAt: Date.now() + expires_in * 1000 * 0.9 }
return cachedToken.value
}
export async function breezeAuthRequest(path, options = {}) {
const send = async () =>
fetch(`${API_URL}${path}`, {
...options,
headers: { ...options.headers, Authorization: `Bearer ${await getAccessToken()}` },
})
let response = await send()
if (response.status === 401) {
// The token may have expired or been invalidated: get a new one and retry once.
cachedToken = null
response = await send()
}
return response
}
// Example: list the organization's users.
// const response = await breezeAuthRequest(`/organizations/${organizationId}/memberships`)
Python
Uses requests.
import os
import time
import requests
API_URL = "https://api.breezeauth.com"
_cached_token = None # (value, renew_at)
def get_access_token():
global _cached_token
if _cached_token and time.time() < _cached_token[1]:
return _cached_token[0]
response = requests.post(
f"{API_URL}/oauth/token",
json={
"grant_type": "client_credentials",
"client_id": os.environ["BREEZEAUTH_CLIENT_ID"],
"client_secret": os.environ["BREEZEAUTH_CLIENT_SECRET"],
},
timeout=10,
)
response.raise_for_status()
body = response.json()
# Renew when 90% of the lifetime has passed, so the token never expires mid-request.
_cached_token = (body["access_token"], time.time() + body["expires_in"] * 0.9)
return _cached_token[0]
def breezeauth_request(method, path, headers=None, **kwargs):
global _cached_token
def send():
all_headers = {**(headers or {}), "Authorization": f"Bearer {get_access_token()}"}
return requests.request(method, f"{API_URL}{path}", headers=all_headers, timeout=30, **kwargs)
response = send()
if response.status_code == 401:
# The token may have expired or been invalidated: get a new one and retry once.
_cached_token = None
response = send()
return response
# Example: list the organization's users.
# response = breezeauth_request("GET", f"/organizations/{organization_id}/memberships")
Last updated: October 13, 2026