Authentication
The Nexus Platform uses OAuth2-style bearer tokens for authenticating requests to the public API. This guide covers how to obtain and use access tokens, manage refresh tokens, and follow security best practices.
All public API endpoints require authentication using bearer tokens via the
Authorization header, with the exception of health check and query
endpoints, which are publicly accessible.
Overview
The Nexus Platform supports two token-based authentication flows:
- Client Credentials Flow - Exchange client credentials for an access token
- Refresh Token Flow - Exchange a refresh token for a new access token (optional, must be enabled per client)
Token Types
| Token Type | Purpose | Lifetime |
|---|---|---|
| Access Token | Authenticate API requests | Configurable (default: 24h) |
| Refresh Token | Obtain new access tokens without re-authenticating | Configurable (default: 30d) |
Quick Start
Here’s how to get your first access token and make an authenticated request:
Step 1: Create an Access Token Client
First, create an access token client in your workspace settings:
-
Navigate to Workspace Settings > Access Tokens
-
Click Create Client
-
Configure the client:
- Name: A descriptive name (e.g., “Production API Client”)
- Scopes: Select the permissions your client needs (e.g.,
query:execute,sessions:read) - Expiration: Set when the client credentials expire (optional)
- Access Token TTL: How long each access token remains valid (default: 24h)
- Enable Refresh Tokens: Check this if you want long-lived sessions (optional)
-
Save the credentials - The
client_secretis only shown once! Store it securely.
Important: Save your client_id and client_secret immediately. The
secret cannot be retrieved later.
Step 2: Obtain an Access Token
// Obtain an access token using client credentials
const tokenResponse = await fetch("https://nexus-api.uat.knowbl.com/api/v2/auth/access-tokens", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
}),
});
const tokenData = await tokenResponse.json();
console.log("Access Token:", tokenData.access_token);
console.log("Expires In:", tokenData.expires_in, "seconds");
// Save the token for use in API requests
const accessToken = tokenData.access_token;
Alternative: Using HTTP Basic Authentication:
You can also authenticate using HTTP Basic Auth with the Authorization header:
const tokenResponse = await fetch("https://nexus-api.uat.knowbl.com/api/v2/auth/access-tokens", {
method: "POST",
headers: {
"Content-Type": "application/json",
// Base64 encode "client_id:client_secret"
Authorization: "Basic " + btoa(`${CLIENT_ID}:${CLIENT_SECRET}`),
},
body: JSON.stringify({
// Optional: request specific scopes (defaults to all client scopes)
scope: "query:execute sessions:read",
}),
});
if (!tokenResponse.ok) {
const error = await tokenResponse.json();
console.error("Authentication failed:", error);
throw new Error(error.error_description);
}
const { access_token, expires_in, scope } = await tokenResponse.json();
console.log("Access token obtained successfully");
console.log("Expires in:", expires_in, "seconds");
console.log("Scopes:", scope);
Step 3: Make Authenticated Requests
Use the access token to make API requests:
// Use the access token to make an authenticated API request
const response = await fetch("https://nexus-api.uat.knowbl.com/api/v2/query", {
method: "POST",
headers: {
"Content-Type": "application/json",
// Include the access token in the Authorization header
Authorization: `Bearer ${ACCESS_TOKEN}`,
},
body: JSON.stringify({
experienceId: "exp_abc123",
question: "What is your return policy?",
}),
});
const data = await response.json();
console.log("Query Response:", data);
Client Credentials Flow
The client credentials flow exchanges your client ID and secret for an access token. This is the primary authentication method for backend services and server-to-server communication.
How It Works
Request Format
// Alternative: Include credentials in request body
const tokenResponse = await fetch("https://nexus-api.uat.knowbl.com/api/v2/auth/access-tokens", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
// Optional: request specific scopes
scope: "query:execute sessions:read",
}),
});
if (!tokenResponse.ok) {
const error = await tokenResponse.json();
console.error("Authentication failed:", error);
throw new Error(error.error_description);
}
const { access_token, expires_in } = await tokenResponse.json();
Alternative: HTTP Basic Authentication
You can also authenticate using HTTP Basic Auth with the Authorization header:
const tokenResponse = await fetch("https://nexus-api.uat.knowbl.com/api/v2/auth/access-tokens", {
method: "POST",
headers: {
"Content-Type": "application/json",
// Base64 encode "client_id:client_secret"
Authorization: "Basic " + btoa(`${CLIENT_ID}:${CLIENT_SECRET}`),
},
body: JSON.stringify({
// Optional: request specific scopes (defaults to all client scopes)
scope: "query:execute sessions:read",
}),
});
if (!tokenResponse.ok) {
const error = await tokenResponse.json();
console.error("Authentication failed:", error);
throw new Error(error.error_description);
}
const { access_token, expires_in, scope } = await tokenResponse.json();
console.log("Access token obtained successfully");
console.log("Expires in:", expires_in, "seconds");
console.log("Scopes:", scope);
Response
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 86400,
"scope": "query:execute sessions:read sessions:write"
}If refresh tokens are enabled:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 86400,
"scope": "query:execute sessions:read sessions:write",
"refresh_token": "nxs_refresh_abc123...",
"refresh_token_expires_in": 2592000
}Refresh Token Flow
Refresh tokens allow your application to obtain new access tokens without re-authenticating with client credentials. This is useful for long-running applications and reduces credential exposure.
Refresh tokens must be explicitly enabled when creating an access token client. They are disabled by default.
How It Works
Token Rotation
The Nexus Platform implements automatic token rotation for security:
- Each refresh token can only be used once
- Using a refresh token returns a new access token and a new refresh token
- The old refresh token is immediately invalidated
- If an old refresh token is reused (replay attack), all tokens associated with the client are revoked
Request Format
// Exchange a refresh token for a new access token
const refreshResponse = await fetch("https://nexus-api.uat.knowbl.com/api/v2/auth/refresh", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
refresh_token: REFRESH_TOKEN,
}),
});
if (!refreshResponse.ok) {
const error = await refreshResponse.json();
if (
error.error === "invalid_token" ||
error.error === "token_reuse_detected"
) {
// Refresh token is invalid or was reused - re-authenticate with client credentials
console.error("Refresh failed:", error.error_description);
console.log("Re-authenticating with client credentials...");
// Fall back to client credentials flow
}
throw new Error(error.error_description);
}
const { access_token, refresh_token, expires_in, refresh_token_expires_in } =
await refreshResponse.json();
console.log("Token refreshed successfully");
console.log("New access token expires in:", expires_in, "seconds");
console.log(
"New refresh token expires in:",
refresh_token_expires_in,
"seconds",
);
// IMPORTANT: Store the new refresh token and discard the old one
// The old refresh token is now invalid and cannot be reused
Response
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 86400,
"scope": "query:execute sessions:read sessions:write",
"refresh_token": "nxs_refresh_xyz789...",
"refresh_token_expires_in": 2592000
}Access Token Scopes
Scopes control what operations your access token can perform. When creating a client, select only the scopes you need (principle of least privilege).
Available Scopes
| Scope | Description | Endpoints |
|---|---|---|
* | Full access to all endpoints | All endpoints |
query:execute | Execute queries and manage sessions | /v2/query, /v2/sessions/* |
sessions:read | Read session data | GET /v2/sessions/* |
sessions:write | Create and update sessions | POST /v2/sessions, PATCH /v2/sessions/:id/metadata |
sessions:complete | Mark sessions as complete | POST /v2/sessions/:id/complete |
data-ingestion:read | View ingestion jobs | GET /v2/data-ingestion/jobs/* |
data-ingestion:write | Submit ingestion jobs | POST /v2/data-ingestion/jobs |
data-ingestion:delete | Cancel ingestion jobs | DELETE /v2/data-ingestion/jobs/:id |
analytics:read | Access analytics data | GET /v2/analytics/* |
Scope Format
When making a token request, you can optionally request specific scopes:
{
"scope": "query:execute sessions:read"
}The returned token will be limited to the intersection of:
- Scopes requested in the token request
- Scopes configured on the client
If no scope is specified in the request, the token receives all scopes configured on the client.
Making Authenticated Requests
Once you have an access token, include it in the Authorization header of every API request:
Authorization: Bearer YOUR_ACCESS_TOKENExample: Execute a Query
// Execute a query with authentication
const queryResponse = await fetch("https://nexus-api.uat.knowbl.com/api/v2/query", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${ACCESS_TOKEN}`,
},
body: JSON.stringify({
experienceId: "exp_abc123",
question: "What are your business hours?",
sessionId: "session_xyz789", // Optional: continue existing session
}),
});
if (!queryResponse.ok) {
if (queryResponse.status === 401) {
console.error("Access token expired or invalid - refresh token needed");
// Refresh your access token using the refresh token flow
}
throw new Error(`Query failed: ${queryResponse.statusText}`);
}
const queryData = await queryResponse.json();
console.log("Answer:", queryData.answer);
console.log("Session ID:", queryData.sessionId);
Example: List Sessions
// List sessions for an experience with authentication
const sessionsResponse = await fetch(
"https://nexus-api.uat.knowbl.com/api/v2/sessions?experienceId=exp_abc123&limit=10",
{
method: "GET",
headers: {
Authorization: `Bearer ${ACCESS_TOKEN}`,
},
},
);
if (!sessionsResponse.ok) {
if (sessionsResponse.status === 401) {
console.error("Access token expired or invalid - refresh token needed");
// Refresh your access token using the refresh token flow
} else if (sessionsResponse.status === 403) {
console.error("Insufficient permissions - token lacks sessions:read scope");
// Obtain a new token with the required scope
}
throw new Error(`Request failed: ${sessionsResponse.statusText}`);
}
const { sessions, pagination } = await sessionsResponse.json();
console.log(`Found ${sessions.length} sessions`);
sessions.forEach((session) => {
console.log(`- ${session.sessionId}: ${session.turnCount} turns`);
});
Token Lifecycle Management
When to Refresh
Access tokens expire after their configured TTL (default: 24 hours). Your application should:
- Track expiration: Store the
expires_invalue from the token response - Refresh proactively: Refresh the token before it expires (e.g., 5 minutes before expiration)
- Handle 401 errors: If you receive a 401 Unauthorized, refresh immediately
Example: Token Manager
class TokenManager {
constructor(clientId, clientSecret) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.accessToken = null;
this.refreshToken = null;
this.expiresAt = null;
this.apiBase = "https://nexus-api.uat.knowbl.com/api";
}
async getAccessToken() {
// Return cached token if still valid (with 5-minute buffer)
if (this.accessToken && this.expiresAt > Date.now() + 5 * 60 * 1000) {
return this.accessToken;
}
// Try to refresh if we have a refresh token
if (this.refreshToken) {
try {
await this.refreshAccessToken();
return this.accessToken;
} catch (error) {
console.warn("Token refresh failed, re-authenticating:", error.message);
// Fall through to client credentials flow
}
}
// Authenticate with client credentials
await this.authenticate();
return this.accessToken;
}
async authenticate() {
const response = await fetch(`${this.apiBase}/v2/auth/access-tokens`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
client_id: this.clientId,
client_secret: this.clientSecret,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Authentication failed: ${error.error_description}`);
}
const data = await response.json();
this.accessToken = data.access_token;
this.refreshToken = data.refresh_token || null;
this.expiresAt = Date.now() + data.expires_in * 1000;
console.log("Authenticated successfully");
return this.accessToken;
}
async refreshAccessToken() {
if (!this.refreshToken) {
throw new Error("No refresh token available");
}
const response = await fetch(`${this.apiBase}/v2/auth/refresh`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
refresh_token: this.refreshToken,
}),
});
if (!response.ok) {
const error = await response.json();
// Clear tokens on refresh failure
this.accessToken = null;
this.refreshToken = null;
this.expiresAt = null;
throw new Error(`Token refresh failed: ${error.error_description}`);
}
const data = await response.json();
this.accessToken = data.access_token;
this.refreshToken = data.refresh_token; // New refresh token
this.expiresAt = Date.now() + data.expires_in * 1000;
console.log("Token refreshed successfully");
return this.accessToken;
}
async request(url, options = {}) {
const token = await this.getAccessToken();
const response = await fetch(url, {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${token}`,
},
});
// Handle 401 by refreshing token and retrying once
if (response.status === 401) {
console.log("Received 401, refreshing token and retrying...");
// Clear current token and get a fresh one
this.accessToken = null;
const newToken = await this.getAccessToken();
// Retry the request with new token
return fetch(url, {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${newToken}`,
},
});
}
return response;
}
}
// Usage example
const tokenManager = new TokenManager(
process.env.NEXUS_CLIENT_ID,
process.env.NEXUS_CLIENT_SECRET,
);
// Make authenticated requests
const queryResponse = await tokenManager.request("https://nexus-api.uat.knowbl.com/api/v2/query", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
experienceId: "exp_abc123",
question: "What is your return policy?",
}),
});
const data = await queryResponse.json();
console.log("Response:", data);
Error Responses
Authentication Errors
| Status | Error Code | Description | Solution |
|---|---|---|---|
| 401 | invalid_client | Invalid client credentials | Verify your client_id and client_secret |
| 401 | invalid_client | Base64 contains newlines | Use base64 -w0 to prevent line wrapping |
| 401 | invalid_client | Base64 contains invalid characters | Ensure single base64 encoding of client_id:client_secret |
| 401 | invalid_client | Missing colon separator | Format must be client_id:client_secret before encoding |
| 401 | invalid_token | Refresh token is invalid, expired, or already used | Obtain a new access token using client credentials |
| 401 | token_reuse_detected | Refresh token was reused (security violation) | Token family revoked. Re-authenticate with client credentials |
| 403 | insufficient_scope | Token lacks required scope for operation | Request token with appropriate scopes |
Example Error Response
{
"error": "invalid_client",
"error_description": "Invalid client credentials"
}Troubleshooting
Common HTTP Basic Auth Issues
When using HTTP Basic Authentication, the most common issues are related to base64 encoding:
Base64 Line Wrapping: Many command-line base64 tools wrap output at 64 or 76 characters by default. This breaks the Authorization header. Always use single-line base64.
Multi-line Base64 (Most Common)
Error: "Base64-encoded credentials contain newline characters..."
Cause: The base64 command on Linux/macOS wraps long output across multiple lines by default.
Solution: Use the -w0 flag to disable line wrapping:
# Wrong - wraps at 76 characters
echo -n 'client_id:client_secret' | base64
# Correct - single line output
echo -n 'client_id:client_secret' | base64 -w0Invalid Base64 Characters
Error: "Base64-encoded credentials contain invalid characters..."
Cause: The encoded string contains characters outside the base64 alphabet (A-Z, a-z, 0-9, +, /, =).
Solution: Ensure you’re encoding the raw credentials, not already-encoded data:
# Wrong - double encoding
echo -n 'client_id:client_secret' | base64 | base64
# Correct - single encoding
echo -n 'client_id:client_secret' | base64 -w0Missing Colon Separator
Error: "Decoded credentials missing ':' separator..."
Cause: The decoded string doesn’t contain a colon between client_id and client_secret.
Solution: Ensure the format before encoding is client_id:client_secret:
# Wrong - missing colon
echo -n 'client_idclient_secret' | base64 -w0
# Correct - colon separator
echo -n 'client_id:client_secret' | base64 -w0cURL Examples
Here’s a complete cURL example with correct base64 encoding:
# Step 1: Create properly encoded credentials
CREDENTIALS=$(echo -n "${CLIENT_ID}:${CLIENT_SECRET}" | base64 -w0)
# Step 2: Make the request
curl -X POST https://api.nexus.knowbl.com/api/v2/auth/access-tokens \
-H "Authorization: Basic ${CREDENTIALS}" \
-H "Content-Type: application/json" \
-d '{"grant_type": "client_credentials"}'The -n flag in echo -n prevents a trailing newline from being included in
the encoded string.
Security Best Practices
Credential Storage
Never commit credentials to version control or expose them in client-side code.
- Client Secret: Store in environment variables or a secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.)
- Access Tokens: Store in memory when possible. If persistence is needed, use secure encrypted storage.
- Refresh Tokens: Store in secure, encrypted persistent storage. These are long-lived and should be protected like passwords.
Token Expiration
- Use short-lived access tokens (24 hours or less) to minimize impact of token compromise
- Use refresh tokens for long-running applications instead of using long-lived access tokens
- Set appropriate expirations and rotate client credentials based on your security requirements
Scope Minimization
- Request only the scopes your application needs (principle of least privilege)
- Create separate clients for different services/environments with appropriate scope restrictions
- Audit scope usage regularly and create new clients with reduced scopes to replace over-privileged clients
Network Security
- Always use HTTPS in production - never send tokens over unencrypted connections
- Implement rate limiting in your application to prevent token exhaustion
- Monitor for unusual authentication patterns (many failed attempts, token reuse, etc.)
Token Rotation
- Implement automatic token rotation before expiration
- Store refresh tokens securely and never log them
- Handle token rotation failures gracefully (fall back to client credentials flow)
Revocation
If you suspect credential compromise:
- Immediately revoke the client in Workspace Settings > Access Tokens
- Generate new client credentials
- Update your application configuration
- Audit recent API activity for suspicious requests
- Report your suspicions to Knowbl so we can monitor for suspicious activity and system compromise
Additional Resources
- Query API Documentation
- Sessions API Documentation
- Data Ingestion API Documentation
- OAuth2 RFC 6749 (Client Credentials Grant)
Support
If you have questions or need assistance with authentication:
- Check the troubleshooting section above
- Review your client configuration in Workspace Settings > Access Tokens
- Contact support with your workspace ID and client ID (never share your client secret)