Access Tokens
Access Tokens
Section titled “Access Tokens”Access tokens are required to authenticate with the Gateway API. This section covers how to generate tokens using the OAuth2 client credentials grant.
Overview
Section titled “Overview”The Gateway API uses OAuth2 Client Credentials Grant for application-level authentication. This grant type is ideal for:
- Server-to-server integrations
- Background jobs and automation
- Service accounts
- Applications without user context
Generating an Access Token
Section titled “Generating an Access Token”Endpoint
Section titled “Endpoint”POST /oauth/tokenRequest Headers
Section titled “Request Headers”Content-Type: application/jsonAccept: application/jsonRequest Body
Section titled “Request Body”| Field | Type | Required | Description |
|---|---|---|---|
grant_type | string | ✅ Yes | Must be client_credentials |
client_id | string | ✅ Yes | Your OAuth2 client ID |
client_secret | string | ✅ Yes | Your OAuth2 client secret |
scope | string | ✅ Yes | Space-separated list of scopes |
Available Scopes
Section titled “Available Scopes”Request only the scopes your application needs:
Account Scopes:
app:account-read- Read account informationapp:account-create- Create new accountsapp:account-update- Update existing accounts
Integration Scopes:
app:integration-read- List and view integrationsapp:integration-action- Execute integration actionsapp:integration-claim- Claim integrations for accountsapp:integration-create- Install new integrationsapp:integration-update- Update integration configurationapp:integration-delete- Delete integrations
Service & Flow Scopes:
app:service-attach- View available servicesapp:flow-read- Access flows
Intent Scopes:
app:intent-create- Generate intent tokens
Example Request
Section titled “Example Request”POST https://<GATEWAY_URL>/oauth/tokenContent-Type: application/json
{ "grant_type": "client_credentials", "client_id": "9a1b2c3d-4e5f-6789-abcd-ef0123456789", "client_secret": "your-secret-key-here", "scope": "app:account-read app:account-create app:integration-read app:integration-action app:integration-claim app:service-attach app:flow-read app:intent-create"}Response
Section titled “Response”Status: 200 OK
{ "token_type": "Bearer", "expires_in": 31536000, "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiI5YTFiMmMzZC00ZTVmLTY3ODktYWJjZC1lZjAxMjM0NTY3ODkiLCJqdGkiOiI4YTJiM2M0ZDVlNmY3ODlhYmNkZWYwMTIzNDU2Nzg5YWJjZGVmIiwiaWF0IjoxNjQwMDAwMDAwLCJuYmYiOjE2NDAwMDAwMDAsImV4cCI6MTY3MTUzNjAwMCwic3ViIjoiIiwic2NvcGVzIjpbImFwcDphY2NvdW50LXJlYWQiLCJhcHA6YWNjb3VudC1jcmVhdGUiLCJhcHA6aW50ZWdyYXRpb24tcmVhZCIsImFwcDppbnRlZ3JhdGlvbi1hY3Rpb24iLCJhcHA6aW50ZWdyYXRpb24tY2xhaW0iLCJhcHA6c2VydmljZS1hdHRhY2giLCJhcHA6Zmxvdy1yZWFkIiwiYXBwOmludGVudC1jcmVhdGUiXX0..."}Response Fields
Section titled “Response Fields”| Field | Type | Description |
|---|---|---|
token_type | string | Always Bearer |
expires_in | integer | Token lifetime in seconds (31536000 = 1 year) |
access_token | string | JWT access token for API requests |
Using the Access Token
Section titled “Using the Access Token”Include the access token in the Authorization header of all API requests:
GET /api/2.0/app/integrationsHost: <GATEWAY_URL>Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...Accept: application/jsonCode Examples
Section titled “Code Examples”#!/bin/bash
# Get access tokenTOKEN_RESPONSE=$(curl -s -X POST https://<GATEWAY_URL>/oauth/token \ -H "Content-Type: application/json" \ -d '{ "grant_type": "client_credentials", "client_id": "your-client-id", "client_secret": "your-client-secret", "scope": "app:account-read app:integration-read app:integration-action" }')
# Extract access tokenACCESS_TOKEN=$(echo $TOKEN_RESPONSE | jq -r '.access_token')
# Use token to make API requestcurl -X GET https://<GATEWAY_URL>/api/2.0/app/integrations \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Accept: application/json"PHP (Laravel)
Section titled “PHP (Laravel)”use Illuminate\Support\Facades\Http;
class GatewayClient{ private $baseUrl; private $clientId; private $clientSecret; private $accessToken; private $tokenExpiry;
public function __construct() { $this->baseUrl = config('gateway.url'); $this->clientId = config('gateway.client_id'); $this->clientSecret = config('gateway.client_secret'); }
public function getAccessToken(): string { // Return cached token if still valid if ($this->accessToken && $this->tokenExpiry > now()->addMinutes(5)) { return $this->accessToken; }
// Request new token $response = Http::post("{$this->baseUrl}/oauth/token", [ 'grant_type' => 'client_credentials', 'client_id' => $this->clientId, 'client_secret' => $this->clientSecret, 'scope' => 'app:account-read app:integration-read app:integration-action', ]);
if ($response->failed()) { throw new \Exception('Failed to obtain access token'); }
$data = $response->json(); $this->accessToken = $data['access_token']; $this->tokenExpiry = now()->addSeconds($data['expires_in']);
return $this->accessToken; }
public function getIntegrations(): array { $token = $this->getAccessToken();
$response = Http::withToken($token) ->get("{$this->baseUrl}/api/2.0/app/integrations");
return $response->json()['data']; }}JavaScript (Node.js)
Section titled “JavaScript (Node.js)”const axios = require('axios');
class GatewayClient { constructor(baseUrl, clientId, clientSecret) { this.baseUrl = baseUrl; this.clientId = clientId; this.clientSecret = clientSecret; this.accessToken = null; this.tokenExpiry = null; }
async getAccessToken() { // Return cached token if still valid if (this.accessToken && this.tokenExpiry > Date.now() + 300000) { return this.accessToken; }
// Request new token const response = await axios.post(`${this.baseUrl}/oauth/token`, { grant_type: 'client_credentials', client_id: this.clientId, client_secret: this.clientSecret, scope: 'app:account-read app:integration-read app:integration-action' });
this.accessToken = response.data.access_token; this.tokenExpiry = Date.now() + (response.data.expires_in * 1000);
return this.accessToken; }
async getIntegrations() { const token = await this.getAccessToken();
const response = await axios.get(`${this.baseUrl}/api/2.0/app/integrations`, { headers: { 'Authorization': `Bearer ${token}` } });
return response.data.data; }}
// Usageconst gateway = new GatewayClient( process.env.GATEWAY_URL, process.env.GATEWAY_CLIENT_ID, process.env.GATEWAY_CLIENT_SECRET);
const integrations = await gateway.getIntegrations();console.log(integrations);Python
Section titled “Python”import requestsimport timefrom typing import Optional
class GatewayClient: def __init__(self, base_url: str, client_id: str, client_secret: str): self.base_url = base_url self.client_id = client_id self.client_secret = client_secret self.access_token: Optional[str] = None self.token_expiry: Optional[float] = None
def get_access_token(self) -> str: """Get access token, using cached token if still valid.""" # Return cached token if still valid (with 5-minute buffer) if self.access_token and self.token_expiry > time.time() + 300: return self.access_token
# Request new token response = requests.post( f'{self.base_url}/oauth/token', json={ 'grant_type': 'client_credentials', 'client_id': self.client_id, 'client_secret': self.client_secret, 'scope': 'app:account-read app:integration-read app:integration-action' } ) response.raise_for_status()
data = response.json() self.access_token = data['access_token'] self.token_expiry = time.time() + data['expires_in']
return self.access_token
def get_integrations(self) -> list: """Fetch all integrations.""" token = self.get_access_token()
response = requests.get( f'{self.base_url}/api/2.0/app/integrations', headers={'Authorization': f'Bearer {token}'} ) response.raise_for_status()
return response.json()['data']
# Usageimport os
gateway = GatewayClient( os.getenv('GATEWAY_URL'), os.getenv('GATEWAY_CLIENT_ID'), os.getenv('GATEWAY_CLIENT_SECRET'))
integrations = gateway.get_integrations()print(integrations)Token Expiration
Section titled “Token Expiration”Access tokens expire after 1 year (31,536,000 seconds).
Best Practices
Section titled “Best Practices”- Cache Tokens: Store tokens in memory and reuse until near expiration
- Refresh Buffer: Request new token 5 minutes before expiration
- Handle 401 Errors: If you receive
401 Unauthorized, request a new token - Secure Storage: Never commit tokens to version control
Token Refresh Logic
Section titled “Token Refresh Logic”[!NOTE] Client Credentials Grant does NOT include a refresh token. Simply request a new access token when the current one expires.
async function makeAuthenticatedRequest(url) { try { const token = await getAccessToken(); return await axios.get(url, { headers: { 'Authorization': `Bearer ${token}` } }); } catch (error) { if (error.response?.status === 401) { // Token expired, get new one this.accessToken = null; // Clear cached token const token = await getAccessToken(); return await axios.get(url, { headers: { 'Authorization': `Bearer ${token}` } }); } throw error; }}Error Responses
Section titled “Error Responses”400 Bad Request
Section titled “400 Bad Request”Cause: Invalid credentials or malformed request
{ "error": "invalid_client", "error_description": "Client authentication failed", "message": "Client authentication failed"}Solutions:
- Verify
client_idandclient_secretare correct - Check request payload format
- Ensure
Content-Type: application/jsonheader is set
401 Unauthorized
Section titled “401 Unauthorized”Cause: Invalid client credentials
{ "message": "Unauthenticated."}Solutions:
- Verify credentials are correct
- Check that OAuth client exists and is active
Security Best Practices
Section titled “Security Best Practices”1. Store Credentials Securely
Section titled “1. Store Credentials Securely”GATEWAY_URL=https://your-gateway.comGATEWAY_CLIENT_ID=your-client-idGATEWAY_CLIENT_SECRET=your-client-secret[!CAUTION] Never commit
.envfiles to version control!
2. Use Environment Variables
Section titled “2. Use Environment Variables”// ❌ BAD - Hardcoded credentialsconst clientId = "hardcoded-id";
// ✅ GOOD - Environment variablesconst clientId = process.env.GATEWAY_CLIENT_ID;3. Limit Scope
Section titled “3. Limit Scope”Request only necessary scopes:
{ "scope": "app:integration-read"}4. Use HTTPS Only
Section titled “4. Use HTTPS Only”All requests must use HTTPS to protect tokens in transit.