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
- Dashboard: Go to your agent settings in the Optimly dashboard
- Agent Configuration: Navigate to the "API Access" section
- 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
Authorization Header (Recommended)
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
- Agent Interaction - Start building with the enhanced agent endpoint
- Chat Management - Learn about managing chat sessions
- Tools Integration - Enable powerful automation features