Skip to main content

Authentication & Security

Learn how to securely authenticate with the Optimly External API using agent access tokens and implement security best practices.

Authentication Methods​

Agent Access Token​

All external API endpoints require authentication using an Agent Access Token. This token is tied to a specific AI agent and provides access to that agent's functionality.

Where to Find Your Access Token​

  1. Dashboard: Go to your agent settings in the Optimly dashboard
  2. Agent Configuration: Navigate to the "API Access" section
  3. Copy Token: Copy your unique access token

Token Format​

agt_1234567890abcdef1234567890abcdef

Access tokens always start with agt_ followed by a 32-character alphanumeric string.

Authentication Headers​

The preferred method is using the Authorization header with Bearer authentication:

Authorization: Bearer agt_1234567890abcdef1234567890abcdef

Example Request​

curl -X POST https://api.optimly.io/external/agent/v2 \
-H "Authorization: Bearer agt_1234567890abcdef1234567890abcdef" \
-H "Content-Type: application/json" \
-d '{
"chat_id": "chat_123",
"content": "Hello!"
}'

Query Parameter (Alternative)​

For situations where headers cannot be set, you can pass the token as a query parameter:

POST /external/agent/v2?access_token_query=agt_1234567890abcdef1234567890abcdef

Example Request​

curl -X POST "https://api.optimly.io/external/agent/v2?access_token_query=agt_1234567890abcdef1234567890abcdef" \
-H "Content-Type: application/json" \
-d '{
"chat_id": "chat_123",
"content": "Hello!"
}'

Implementation Examples​

JavaScript/Node.js​

class OptimlyClient {
constructor(accessToken, baseUrl = 'https://api.optimly.io') {
this.accessToken = accessToken;
this.baseUrl = baseUrl;
this.defaultHeaders = {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
};
}

async makeRequest(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
const config = {
headers: this.defaultHeaders,
...options
};

try {
const response = await fetch(url, config);

if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || `HTTP error! status: ${response.status}`);
}

return await response.json();
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}

async sendMessage(chatId, content, metadata = {}) {
return await this.makeRequest('/external/agent/v2', {
method: 'POST',
body: JSON.stringify({
chat_id: chatId,
content,
metadata
})
});
}

async createChat(clientId, metadata = {}) {
return await this.makeRequest('/external/chat', {
method: 'POST',
body: JSON.stringify({
client_id: clientId,
ai_enabled: true,
metadata
})
});
}
}

// Usage
const client = new OptimlyClient('agt_1234567890abcdef1234567890abcdef');

// Send a message
const response = await client.sendMessage('chat_123', 'Hello, I need help!');
console.log('Agent response:', response.response);

Python​

import requests
from typing import Dict, Any, Optional

class OptimlyClient:
def __init__(self, access_token: str, base_url: str = 'https://api.optimly.io'):
self.access_token = access_token
self.base_url = base_url
self.default_headers = {
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json'
}

def _make_request(
self,
endpoint: str,
method: str = 'GET',
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""Make authenticated request to Optimly API."""
url = f"{self.base_url}{endpoint}"

try:
response = requests.request(
method=method,
url=url,
headers=self.default_headers,
json=data,
timeout=30
)
response.raise_for_status()
return response.json()

except requests.exceptions.RequestException as e:
print(f"API request failed: {e}")
raise

def send_message(
self,
chat_id: str,
content: str,
metadata: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""Send message to agent."""
return self._make_request(
'/external/agent/v2',
method='POST',
data={
'chat_id': chat_id,
'content': content,
'metadata': metadata or {}
}
)

def create_chat(
self,
client_id: str,
metadata: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""Create new chat session."""
return self._make_request(
'/external/chat',
method='POST',
data={
'client_id': client_id,
'ai_enabled': True,
'metadata': metadata or {}
}
)

# Usage
client = OptimlyClient('agt_1234567890abcdef1234567890abcdef')

# Create chat and send message
chat = client.create_chat('user_123')
response = client.send_message(chat['chat_id'], 'Hello, I need assistance!')
print(f"Agent response: {response['response']}")

React Environment Variables​

// .env.local
NEXT_PUBLIC_OPTIMLY_ACCESS_TOKEN=agt_1234567890abcdef1234567890abcdef

// components/OptimlyChat.jsx
import { useState, useEffect } from 'react';

export default function OptimlyChat({ clientId }) {
const [accessToken] = useState(process.env.NEXT_PUBLIC_OPTIMLY_ACCESS_TOKEN);
const [messages, setMessages] = useState([]);
const [currentChat, setCurrentChat] = useState(null);

const createOptimlyHeaders = () => ({
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
});

const createChat = async () => {
try {
const response = await fetch('/api/optimly/chat', {
method: 'POST',
headers: createOptimlyHeaders(),
body: JSON.stringify({
client_id: clientId,
ai_enabled: true
})
});

if (!response.ok) {
throw new Error('Failed to create chat');
}

const chat = await response.json();
setCurrentChat(chat);
return chat;
} catch (error) {
console.error('Failed to create chat:', error);
throw error;
}
};

// Component implementation...
}

Security Best Practices​

1. Never Expose Tokens in Frontend Code​

❌ Don't do this:

// This exposes your token to anyone viewing the page source
const accessToken = 'agt_1234567890abcdef1234567890abcdef';

✅ Do this instead:

// Use environment variables and backend proxies
const response = await fetch('/api/chat-proxy', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: userMessage })
});

2. Use Backend Proxy Pattern​

Create a backend endpoint that handles the Optimly API calls:

Node.js/Express Example​

// server.js
const express = require('express');
const axios = require('axios');

const app = express();
app.use(express.json());

const OPTIMLY_ACCESS_TOKEN = process.env.OPTIMLY_ACCESS_TOKEN;
const OPTIMLY_BASE_URL = 'https://api.optimly.io';

// Proxy endpoint for chat
app.post('/api/chat-proxy', async (req, res) => {
try {
const { chat_id, content, metadata } = req.body;

const response = await axios.post(
`${OPTIMLY_BASE_URL}/external/agent/v2`,
{
chat_id,
content,
metadata: {
source: 'backend_proxy',
user_ip: req.ip,
user_agent: req.get('User-Agent'),
...metadata
}
},
{
headers: {
'Authorization': `Bearer ${OPTIMLY_ACCESS_TOKEN}`,
'Content-Type': 'application/json'
},
timeout: 30000
}
);

res.json(response.data);
} catch (error) {
console.error('Optimly API error:', error.response?.data || error.message);

if (error.response?.status === 401) {
res.status(401).json({ error: 'Authentication failed' });
} else if (error.response?.status === 429) {
res.status(429).json({ error: 'Rate limit exceeded' });
} else {
res.status(500).json({ error: 'Internal server error' });
}
}
});

app.listen(3001, () => {
console.log('Chat proxy server running on port 3001');
});

Next.js API Route Example​

// pages/api/optimly/chat.js (or app/api/optimly/chat/route.js for App Router)
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}

try {
const { chat_id, content, metadata } = req.body;

const response = await fetch('https://api.optimly.io/external/agent/v2', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.OPTIMLY_ACCESS_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
chat_id,
content,
metadata: {
source: 'nextjs_api',
user_ip: req.headers['x-forwarded-for'] || req.connection.remoteAddress,
...metadata
}
})
});

if (!response.ok) {
const error = await response.json();
return res.status(response.status).json(error);
}

const data = await response.json();
res.json(data);

} catch (error) {
console.error('API error:', error);
res.status(500).json({ error: 'Internal server error' });
}
}

3. Environment Variable Management​

Development Environment​

# .env.local
OPTIMLY_ACCESS_TOKEN=agt_1234567890abcdef1234567890abcdef
OPTIMLY_BASE_URL=https://api.optimly.io

Production Environment​

Use your hosting platform's environment variable settings:

  • Vercel: Dashboard → Project → Settings → Environment Variables
  • Netlify: Site settings → Environment variables
  • Heroku: Settings → Config Vars
  • AWS: Lambda environment variables or AWS Systems Manager

4. Token Rotation Strategy​

class OptimlyClientWithRotation {
constructor(primaryToken, fallbackToken = null) {
this.primaryToken = primaryToken;
this.fallbackToken = fallbackToken;
this.currentToken = primaryToken;
}

async makeAuthenticatedRequest(endpoint, options = {}) {
try {
return await this.makeRequest(endpoint, {
...options,
headers: {
'Authorization': `Bearer ${this.currentToken}`,
...options.headers
}
});
} catch (error) {
if (error.status === 401 && this.fallbackToken && this.currentToken !== this.fallbackToken) {
console.log('Primary token failed, trying fallback token...');
this.currentToken = this.fallbackToken;

return await this.makeRequest(endpoint, {
...options,
headers: {
'Authorization': `Bearer ${this.currentToken}`,
...options.headers
}
});
}
throw error;
}
}
}

5. Request Validation and Sanitization​

function validateAndSanitizeMessage(content) {
// Basic validation
if (!content || typeof content !== 'string') {
throw new Error('Message content is required and must be a string');
}

// Length validation
if (content.length > 4000) {
throw new Error('Message content is too long (max 4000 characters)');
}

// Sanitize HTML/script content
const sanitized = content
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, '')
.trim();

return sanitized;
}

async function sendSafeMessage(chatId, content, metadata = {}) {
try {
const sanitizedContent = validateAndSanitizeMessage(content);

return await optimlyClient.sendMessage(chatId, sanitizedContent, {
...metadata,
sanitized: true,
original_length: content.length,
timestamp: new Date().toISOString()
});
} catch (error) {
console.error('Failed to send message:', error);
throw error;
}
}

Error Handling​

Authentication Errors​

async function handleOptimlyRequest(requestFunction) {
try {
return await requestFunction();
} catch (error) {
switch (error.status) {
case 401:
console.error('Authentication failed - check your access token');
// Redirect to token configuration or refresh token
break;

case 403:
console.error('Access forbidden - token may not have required permissions');
break;

case 429:
console.error('Rate limit exceeded - implementing backoff');
// Implement exponential backoff
await new Promise(resolve => setTimeout(resolve, 60000));
return await handleOptimlyRequest(requestFunction);

default:
console.error('Unexpected error:', error);
throw error;
}
}
}

Retry Logic with Exponential Backoff​

async function retryOptimlyRequest(requestFunction, maxRetries = 3) {
let retries = 0;

while (retries < maxRetries) {
try {
return await requestFunction();
} catch (error) {
if (error.status === 429 || error.status >= 500) {
retries++;
const delay = Math.pow(2, retries) * 1000; // Exponential backoff
console.log(`Request failed, retrying in ${delay}ms... (attempt ${retries}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
} else {
throw error; // Don't retry client errors (4xx except 429)
}
}
}

throw new Error(`Request failed after ${maxRetries} retries`);
}

Rate Limiting​

Understanding Rate Limits​

  • 100 requests per minute per access token
  • 1000 requests per hour per access token
  • Burst allowance: 20 requests in 10 seconds

Rate Limit Headers​

Monitor these headers in API responses:

function checkRateLimits(response) {
const headers = response.headers;

console.log('Rate limit info:', {
limit: headers.get('X-RateLimit-Limit'),
remaining: headers.get('X-RateLimit-Remaining'),
reset: headers.get('X-RateLimit-Reset'),
retryAfter: headers.get('Retry-After')
});

const remaining = parseInt(headers.get('X-RateLimit-Remaining'));
if (remaining < 10) {
console.warn('Approaching rate limit, consider slowing down requests');
}
}

Rate Limiting Implementation​

class RateLimitedOptimlyClient {
constructor(accessToken, maxRequestsPerMinute = 90) {
this.client = new OptimlyClient(accessToken);
this.maxRequestsPerMinute = maxRequestsPerMinute;
this.requestQueue = [];
this.processing = false;
}

async queueRequest(requestFunction) {
return new Promise((resolve, reject) => {
this.requestQueue.push({ requestFunction, resolve, reject });
this.processQueue();
});
}

async processQueue() {
if (this.processing || this.requestQueue.length === 0) {
return;
}

this.processing = true;
const intervalMs = (60 * 1000) / this.maxRequestsPerMinute;

while (this.requestQueue.length > 0) {
const { requestFunction, resolve, reject } = this.requestQueue.shift();

try {
const result = await requestFunction();
resolve(result);
} catch (error) {
reject(error);
}

// Wait before next request
if (this.requestQueue.length > 0) {
await new Promise(resolve => setTimeout(resolve, intervalMs));
}
}

this.processing = false;
}

async sendMessage(chatId, content, metadata = {}) {
return await this.queueRequest(() =>
this.client.sendMessage(chatId, content, metadata)
);
}
}

CORS Configuration​

Frontend CORS Headers​

When making requests from a browser, ensure your backend proxy includes appropriate CORS headers:

// Express.js CORS setup
const cors = require('cors');

app.use(cors({
origin: ['https://yourapp.com', 'https://www.yourapp.com'],
methods: ['GET', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true
}));

Preflight Handling​

// Handle preflight requests
app.options('/api/optimly/*', (req, res) => {
res.header('Access-Control-Allow-Origin', 'https://yourapp.com');
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.sendStatus(200);
});

Monitoring and Logging​

Request Logging​

class LoggingOptimlyClient extends OptimlyClient {
async makeRequest(endpoint, options = {}) {
const startTime = Date.now();
const requestId = Math.random().toString(36).substr(2, 9);

console.log(`[${requestId}] Starting request to ${endpoint}`, {
method: options.method || 'GET',
timestamp: new Date().toISOString()
});

try {
const result = await super.makeRequest(endpoint, options);
const duration = Date.now() - startTime;

console.log(`[${requestId}] Request completed in ${duration}ms`, {
status: 'success',
responseSize: JSON.stringify(result).length
});

return result;
} catch (error) {
const duration = Date.now() - startTime;

console.error(`[${requestId}] Request failed after ${duration}ms`, {
status: 'error',
error: error.message,
statusCode: error.status
});

throw error;
}
}
}

Next Steps​