breezeauth.com
EN
Sign inGet started free
Service usersAccess tokens

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.

On this page
  1. Request a token
  2. Call the API
  3. When the token expires: no refresh tokens
  4. What BreezeAuth checks
  5. Errors
  6. Code examples
  7. Node.js
  8. Python

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_in runs out, or when an API call returns 401 Unauthorized.
  • If a request still returns 401 with 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