Create LLM-powered AI assistants with tools and data sources through Interactor...
Last verified: 2026-06-18
Build LLM-powered assistants that can have conversations, use tools, and access your data sources.
CRITICAL: Access Token Required
Every AI Agent API call requires an
access_tokenin the Authorization header. Without it, all requests fail with401 Unauthorized.The token (
access_token,interactor_access_token, or justtoken) is obtained by exchanging your OAuthclient_id+client_secretvia the token endpoint.
Required:
interactor-auth skill)access_token from OAuth token exchangeOptional:
jq command-line tool (for bash examples)The AI Agents system consists of:
| Component | Description |
|---|---|
| Assistants | Configured AI agents with specific behaviors and capabilities |
| Rooms | Chat sessions between users and assistants |
| Messages | Individual messages in a conversation |
| Tools | Custom functions that assistants can invoke |
| Data Sources | Databases and APIs that assistants can query |
The typical implementation flow:
1. Create an Assistant → Define behavior with instructions and model config
↓
2. Register Tools → (Optional) Add custom functions the assistant can call
↓
3. Connect Data Sources → (Optional) Connect databases for natural language queries
↓
4. Create a Room → Start a conversation session for a user
↓
5. Send/Receive Messages → Exchange messages, handle tool calls
↓
6. Close Room → End the conversation when complete
Minimal Example:
Prerequisites: This example requires
jqfor JSON parsing and a valid$TOKEN. Seeinteractor-authskill for authentication setup.
# Get your token first (see interactor-auth skill)
# export TOKEN="your_access_token_here"
# 1. Create assistant
ASSISTANT_ID=$(curl -s -X POST https://core.interactor.com/api/v1/agents/assistants \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "helper", "title": "Helper", "instructions": "You are a helpful assistant."}' \
| jq -r '.data.id')
echo "Created assistant: $ASSISTANT_ID"
# 2. Create room
ROOM_ID=$(curl -s -X POST https://core.interactor.com/api/v1/agents/$ASSISTANT_ID/rooms \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"namespace": "user_123"}' \
| jq -r '.data.id')
echo "Created room: $ROOM_ID"
# 3. Send message
curl -X POST https://core.interactor.com/api/v1/agents/rooms/$ROOM_ID/messages \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"content": "Hello!", "role": "user"}'
API Version Note: This skill documents the standard AI Agents API. Some operations (DELETE endpoints, advanced pagination options) may vary by Interactor version. Always verify endpoint availability against your specific API documentation or test in a development environment first.
curl -X POST https://core.interactor.com/api/v1/agents/assistants \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"name": "support_assistant",
"title": "Support Assistant",
"description": "Helps users with support questions",
"model_config": {
"provider": "openai",
"model": "gpt-4o",
"temperature": 0.7
},
"instructions": "You are a helpful support assistant. Be concise and friendly. Always try to resolve issues on the first response.",
"enabled_tools": ["search_knowledge_base", "create_ticket"]
}'
Response:
{
"data": {
"id": "asst_abc",
"name": "support_assistant",
"title": "Support Assistant",
"description": "Helps users with support questions",
"model_config": {
"provider": "openai",
"model": "gpt-4o",
"temperature": 0.7
},
"enabled_tools": ["search_knowledge_base", "create_ticket"],
"created_at": "2026-01-20T12:00:00Z"
}
}
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Unique identifier (lowercase, underscores only) |
title |
string | Yes | Display name for users |
description |
string | No | What the assistant does |
model_config.provider |
string | No | openai (default: openai) |
model_config.model |
string | No | Model identifier (default: gpt-4o) |
model_config.temperature |
number | No | Response randomness 0.0-1.0 (default: 0.7) |
instructions |
string | Yes | System prompt defining behavior |
enabled_tools |
array | No | Tool names the assistant can use |
curl https://core.interactor.com/api/v1/agents/assistants \
-H "Authorization: Bearer <token>"
curl https://core.interactor.com/api/v1/agents/assistants/asst_abc \
-H "Authorization: Bearer <token>"
curl -X PUT https://core.interactor.com/api/v1/agents/assistants/asst_abc \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"instructions": "Updated instructions with more detail...",
"model_config": {
"temperature": 0.5
}
}'
curl -X DELETE https://core.interactor.com/api/v1/agents/assistants/asst_abc \
-H "Authorization: Bearer <token>"
Rooms are conversations between a user and an assistant.
curl -X POST https://core.interactor.com/api/v1/agents/asst_abc/rooms \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"namespace": "user_123",
"metadata": {
"user_name": "John",
"context": "billing_question",
"plan": "premium"
}
}'
Response:
{
"data": {
"id": "room_xyz",
"assistant_id": "asst_abc",
"namespace": "user_123",
"status": "active",
"metadata": {
"user_name": "John",
"context": "billing_question",
"plan": "premium"
},
"created_at": "2026-01-20T12:00:00Z"
}
}
curl https://core.interactor.com/api/v1/agents/rooms \
-H "Authorization: Bearer <token>"
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
namespace |
string | Filter by namespace |
assistant_id |
string | Filter by assistant |
status |
string | active or closed |
Example - List active rooms for a user:
curl "https://core.interactor.com/api/v1/agents/rooms?namespace=user_123&status=active" \
-H "Authorization: Bearer <token>"
curl https://core.interactor.com/api/v1/agents/rooms/room_xyz \
-H "Authorization: Bearer <token>"
Response includes conversation history:
{
"data": {
"id": "room_xyz",
"assistant_id": "asst_abc",
"status": "active",
"metadata": {...},
"message_count": 5,
"created_at": "2026-01-20T12:00:00Z",
"last_message_at": "2026-01-20T12:05:00Z"
}
}
curl -X POST https://core.interactor.com/api/v1/agents/rooms/room_xyz/close \
-H "Authorization: Bearer <token>"
curl -X POST https://core.interactor.com/api/v1/agents/rooms/room_xyz/messages \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"content": "How do I update my billing information?",
"role": "user"
}'
Response:
{
"data": {
"id": "msg_123",
"role": "user",
"content": "How do I update my billing information?",
"created_at": "2026-01-20T12:00:00Z"
}
}
The assistant's response is generated asynchronously. Use streaming or webhooks to receive it.
Message Request Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
content |
string | Yes | The message text |
enabled_tools |
array | No | Tool names to enable for this message (overrides assistant's default_tools) |
enabled_data_sources |
array | No | Data source names to enable for this message (overrides assistant's default_data_sources) |
external_user_id |
string | No | User identifier for profile resolution (overrides room's external_user_id) |
profile_context |
string | No | Context profile ID for profile layering |
metadata |
object | No | Custom metadata to attach to the message |
Advanced example — sending a message with specific tools and data sources enabled:
curl -X POST https://core.interactor.com/api/v1/agents/rooms/room_xyz/messages \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"content": "What tables are in the marketing database?",
"enabled_tools": ["search_knowledge_base", "get_order_status"],
"enabled_data_sources": ["marketing_db", "product_api"]
}'
When enabled_tools or enabled_data_sources are provided, they override the assistant's defaults for that message only. If omitted, the assistant's default_tools and default_data_sources are used.
curl https://core.interactor.com/api/v1/agents/rooms/room_xyz/messages \
-H "Authorization: Bearer <token>"
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
limit |
integer | Max messages to return (default: 50, max: 100) |
cursor |
string | Opaque cursor for pagination (from next_cursor) |
order |
string | Sort order: asc (oldest first) or desc (newest first, default) |
Response:
{
"data": {
"messages": [
{
"id": "msg_123",
"role": "user",
"content": "How do I update my billing information?",
"created_at": "2026-01-20T12:00:00Z"
},
{
"id": "msg_124",
"role": "assistant",
"content": "To update your billing information, go to Settings > Billing...",
"tool_calls": [],
"created_at": "2026-01-20T12:00:05Z"
}
],
"has_more": true,
"next_cursor": "eyJpZCI6Im1zZ18xMjQiLCJ0cyI6MTcwNTc1MjQwNX0="
}
}
The API uses cursor-based pagination for stable, consistent results:
// Fetch all messages in a room
async function getAllMessages(roomId: string): Promise<Message[]> {
const allMessages: Message[] = [];
let cursor: string | undefined;
do {
const params = new URLSearchParams({ limit: '100' });
if (cursor) params.set('cursor', cursor);
const response = await fetch(
`https://core.interactor.com/api/v1/agents/rooms/${roomId}/messages?${params}`,
{ headers: { 'Authorization': `Bearer ${token}` } }
);
const { data } = await response.json();
allMessages.push(...data.messages);
cursor = data.has_more ? data.next_cursor : undefined;
} while (cursor);
return allMessages;
}
Pagination Best Practices:
next_cursor from the response; never construct cursors manually| Role | Description |
|---|---|
user |
Message from the user |
assistant |
Response from the AI assistant |
system |
System message (internal use) |
tool |
Tool execution result |
Assistant responses are generated asynchronously after you send a user message. To receive responses in real-time, you have three options:
API Note: Verify streaming endpoint availability with your Interactor version. The SSE endpoint pattern shown below (
/rooms/{id}/stream) is a common convention but may differ in your deployment. Check your API documentation or contact support.
Option 1: Server-Sent Events (SSE)
Subscribe to room events for streaming tokens as they're generated.
Note: The standard browser
EventSourceAPI doesn't support custom headers. Use theeventsourcenpm package for Node.js, or pass the token as a query parameter if your API supports it.
Node.js with eventsource package:
import EventSource from 'eventsource';
// Node.js: Use eventsource package which supports headers
const eventSource = new EventSource(
`https://core.interactor.com/api/v1/agents/rooms/${roomId}/stream`,
{ headers: { 'Authorization': `Bearer ${token}` } }
);
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'token') {
// Append token to response
process.stdout.write(data.token);
} else if (data.type === 'message_complete') {
// Full message available
console.log('\nComplete:', data.message);
} else if (data.type === 'tool_call') {
// Tool is being invoked
console.log('Tool call:', data.tool_name);
}
};
eventSource.onerror = (error) => {
console.error('Stream error:', error);
eventSource.close();
};
Browser with Fetch API (alternative):
// Browser: Use fetch with ReadableStream for SSE with auth
async function streamMessages(roomId: string, token: string) {
const response = await fetch(
`https://core.interactor.com/api/v1/agents/rooms/${roomId}/stream`,
{ headers: { 'Authorization': `Bearer ${token}` } }
);
const reader = response.body?.getReader();
const decoder = new TextDecoder();
while (reader) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
// Parse SSE format: "data: {...}\n\n"
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = JSON.parse(line.slice(6));
console.log('Received:', data);
}
}
}
}
Option 2: Webhooks
Configure a webhook to receive agent.room.message events at your endpoint. See the interactor-webhooks skill for complete webhook setup and payload handling.
Option 3: Polling (Not Recommended)
Poll the messages endpoint for new messages. Use only as a fallback:
async function pollForResponse(roomId: string, lastKnownMessageId: string) {
const maxAttempts = 30;
const delayMs = 1000;
for (let i = 0; i < maxAttempts; i++) {
// Fetch recent messages and check for new assistant response
const { messages } = await getMessages(roomId, { limit: 10 });
const newMessages = messages.filter(m =>
m.role === 'assistant' && m.id !== lastKnownMessageId
);
if (newMessages.length > 0) return newMessages[0];
await new Promise(resolve => setTimeout(resolve, delayMs));
}
throw new Error('Response timeout');
}
Tools are custom functions that assistants can invoke during conversations.
curl -X POST https://core.interactor.com/api/v1/tools \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"name": "search_products",
"description": "Search the product catalog by query and optional category",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query for product names or descriptions"
},
"category": {
"type": "string",
"description": "Product category to filter by",
"enum": ["electronics", "clothing", "home", "sports"]
},
"max_results": {
"type": "integer",
"description": "Maximum number of results to return",
"default": 10
}
},
"required": ["query"]
},
"callback_url": "https://yourapp.com/api/tools/search_products",
"callback_secret": "your_webhook_secret_here"
}'
Response:
{
"data": {
"id": "tool_abc",
"name": "search_products",
"description": "Search the product catalog by query and optional category",
"created_at": "2026-01-20T12:00:00Z"
}
}
When the assistant invokes your tool, Interactor POSTs to your callback_url:
Request Headers:
POST /api/tools/search_products HTTP/1.1
Content-Type: application/json
X-Interactor-Signature: sha256=abc123def456...
X-Interactor-Timestamp: 2026-01-20T12:00:00Z
Request Body:
{
"tool_name": "search_products",
"parameters": {
"query": "laptop",
"category": "electronics",
"max_results": 5
},
"execution_id": "exec_xyz",
"room_id": "room_xyz",
"assistant_id": "asst_abc",
"timestamp": "2026-01-20T12:00:00Z"
}
Security Note: The
X-Interactor-Signatureheader contains an HMAC-SHA256 signature of the raw request body, signed with yourcallback_secret. Always verify this signature before processing the request.
Your response:
{
"result": {
"products": [
{
"id": "prod_1",
"name": "MacBook Pro 14\"",
"price": 1999,
"in_stock": true
},
{
"id": "prod_2",
"name": "Dell XPS 15",
"price": 1499,
"in_stock": true
}
],
"total_count": 2
}
}
The assistant will use this result to formulate its response.
Tool callbacks include three security headers for verification:
| Header | Description |
|---|---|
X-Interactor-Signature |
HMAC-SHA256 signature of the raw request body |
X-Interactor-Timestamp |
ISO 8601 timestamp when request was signed |
X-Interactor-Request-Id |
Unique request ID (same as execution_id in body) |
Security Requirements:
execution_id as idempotency key to prevent replay attacksTypeScript (with replay protection):
import crypto from 'crypto';
interface VerificationResult {
valid: boolean;
error?: string;
}
function verifyToolCallback(
payload: string,
signature: string,
timestamp: string,
secret: string,
maxAgeSeconds: number = 300
): VerificationResult {
// 1. Check timestamp is not too old (replay protection)
const requestTime = Date.parse(timestamp);
const now = Date.now();
const ageSeconds = Math.abs(now - requestTime) / 1000;
if (ageSeconds > maxAgeSeconds) {
return { valid: false, error: `Request too old: ${ageSeconds}s > ${maxAgeSeconds}s` };
}
// 2. Verify HMAC signature
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
const signatureValid = crypto.timingSafeEqual(
Buffer.from(signature.replace('sha256=', '')),
Buffer.from(expected)
);
if (!signatureValid) {
return { valid: false, error: 'Invalid signature' };
}
return { valid: true };
}
// Express middleware with full security
app.post('/api/tools/search_products', express.raw({ type: 'application/json' }), async (req, res) => {
const signature = req.headers['x-interactor-signature'] as string;
const timestamp = req.headers['x-interactor-timestamp'] as string;
const requestId = req.headers['x-interactor-request-id'] as string;
const payload = req.body.toString();
// Verify signature and timestamp
const verification = verifyToolCallback(
payload,
signature,
timestamp,
process.env.TOOL_CALLBACK_SECRET!
);
if (!verification.valid) {
console.error('Callback verification failed:', verification.error, { requestId });
return res.status(401).json({ error: verification.error });
}
const data = JSON.parse(payload);
// 3. Idempotency check - prevent duplicate processing
const alreadyProcessed = await checkIdempotency(data.execution_id);
if (alreadyProcessed) {
console.log('Duplicate request ignored:', data.execution_id);
return res.status(200).json({ result: await getCachedResult(data.execution_id) });
}
// Execute your tool logic
const result = await searchProducts(data.parameters);
// Store result for idempotency (retain for 7 days)
await storeIdempotencyResult(data.execution_id, result, 7 * 24 * 60 * 60);
res.json({ result });
});
// Idempotency helpers (implement with Redis, database, etc.)
async function checkIdempotency(executionId: string): Promise<boolean> {
// Check if execution_id was already processed
return false; // Implement with your storage
}
async function getCachedResult(executionId: string): Promise<any> {
// Return cached result for duplicate request
return null; // Implement with your storage
}
async function storeIdempotencyResult(executionId: string, result: any, ttlSeconds: number): Promise<void> {
// Store result keyed by execution_id
// Implement with your storage
}
Python (with replay protection):
import os
import hmac
import hashlib
import time
from datetime import datetime
from flask import Flask, request, jsonify
app = Flask(__name__)
def verify_tool_callback(
payload: bytes,
signature: str,
timestamp: str,
secret: str,
max_age_seconds: int = 300
) -> tuple[bool, str | None]:
"""
Verify callback with replay protection.
Returns (is_valid, error_message).
"""
# 1. Check timestamp is not too old
try:
request_time = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
age_seconds = abs((datetime.now(request_time.tzinfo) - request_time).total_seconds())
if age_seconds > max_age_seconds:
return False, f'Request too old: {age_seconds}s > {max_age_seconds}s'
except ValueError as e:
return False, f'Invalid timestamp: {e}'
# 2. Verify HMAC signature
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
signature_value = signature.replace('sha256=', '')
if not hmac.compare_digest(signature_value, expected):
return False, 'Invalid signature'
return True, None
@app.route('/api/tools/search_products', methods=['POST'])
def handle_tool_callback():
signature = request.headers.get('X-Interactor-Signature', '')
timestamp = request.headers.get('X-Interactor-Timestamp', '')
request_id = request.headers.get('X-Interactor-Request-Id', '')
payload = request.get_data()
# Verify signature and timestamp
is_valid, error = verify_tool_callback(
payload,
signature,
timestamp,
os.environ['TOOL_CALLBACK_SECRET']
)
if not is_valid:
app.logger.error(f'Callback verification failed: {error}', extra={'request_id': request_id})
return jsonify({'error': error}), 401
data = request.get_json()
# Idempotency check
if is_duplicate_execution(data['execution_id']):
return jsonify({'result': get_cached_result(data['execution_id'])}), 200
# Execute your tool logic
result = search_products(data['parameters'])
# Store for idempotency
store_execution_result(data['execution_id'], result)
return jsonify({'result': result})
curl https://core.interactor.com/api/v1/tools \
-H "Authorization: Bearer <token>"
curl https://core.interactor.com/api/v1/tools/tool_abc \
-H "Authorization: Bearer <token>"
curl -X PUT https://core.interactor.com/api/v1/tools/tool_abc \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"description": "Updated description with more detail",
"callback_url": "https://yourapp.com/api/v2/tools/search_products"
}'
curl -X DELETE https://core.interactor.com/api/v1/tools/tool_abc \
-H "Authorization: Bearer <token>"
This section defines the canonical contract for all Interactor webhooks and tool callbacks.
All webhook/callback requests include these headers:
| Header | Format | Description |
|---|---|---|
X-Interactor-Signature |
sha256=<hex> |
HMAC-SHA256 of raw body |
X-Interactor-Timestamp |
ISO 8601 | When request was signed |
X-Interactor-Request-Id |
exec_<id> |
Unique request identifier |
Content-Type |
application/json |
Always JSON |
| Status | Meaning | Interactor Behavior |
|---|---|---|
200 |
Success | Process result, no retry |
201 |
Created | Same as 200 |
202 |
Accepted | Async processing (see below) |
400 |
Bad Request | Permanent failure, no retry |
401 |
Unauthorized | Permanent failure, no retry |
404 |
Not Found | Permanent failure, no retry |
429 |
Rate Limited | Retry with backoff |
500 |
Server Error | Retry with backoff |
502-504 |
Gateway Error | Retry with backoff |
Expected Response Format:
{
"result": { ... }
}
| Operation | Timeout | Behavior on Timeout |
|---|---|---|
| Tool callback | 30 seconds | Retry with backoff |
| Webhook delivery | 10 seconds | Retry with backoff |
| Async callback (202) | 5 minutes | Poll for completion |
Important: Your callback must respond within the timeout window. For long-running operations, return
202 Acceptedwith a status URL, and Interactor will poll for completion.
When a callback fails (5xx, timeout, network error), Interactor retries with exponential backoff:
| Attempt | Delay | Cumulative Time |
|---|---|---|
| 1 | Immediate | 0s |
| 2 | 1s | 1s |
| 3 | 2s | 3s |
| 4 | 4s | 7s |
| 5 | 8s | 15s |
| 6 (final) | 16s | 31s |
After 6 failed attempts:
tool.callback.failed event is emitted (if webhooks configured)You MUST implement idempotent handlers. Use execution_id as the idempotency key.
Idempotency Implementation:
// Redis-based idempotency example
import Redis from 'ioredis';
const redis = new Redis();
const IDEMPOTENCY_TTL = 7 * 24 * 60 * 60; // 7 days
async function handleWithIdempotency(
executionId: string,
handler: () => Promise<any>
): Promise<{ result: any; isDuplicate: boolean }> {
const cacheKey = `idempotency:${executionId}`;
// Check for existing result
const cached = await redis.get(cacheKey);
if (cached) {
return { result: JSON.parse(cached), isDuplicate: true };
}
// Execute handler
const result = await handler();
// Store result with TTL
await redis.setex(cacheKey, IDEMPOTENCY_TTL, JSON.stringify(result));
return { result, isDuplicate: false };
}
Recommended idempotency window: 7 days minimum. This covers retry scenarios and delayed processing.
If all retries are exhausted:
tool.callback.dead_letter event (if webhooks configured)Dead Letter Event Payload:
{
"event": "tool.callback.dead_letter",
"data": {
"execution_id": "exec_xyz",
"tool_name": "search_products",
"assistant_id": "asst_abc",
"room_id": "room_xyz",
"attempts": 6,
"last_error": "Connection timeout",
"first_attempt_at": "2026-01-20T12:00:00Z",
"last_attempt_at": "2026-01-20T12:00:31Z"
}
}
This section defines the canonical contract for Server-Sent Events (SSE) streaming.
The streaming endpoint emits these event types:
| Event Type | Description | When Emitted |
|---|---|---|
token |
Incremental response token | During response generation |
message_complete |
Full message available | When response finishes |
tool_call |
Tool is being invoked | When assistant calls a tool |
tool_result |
Tool execution completed | After tool callback returns |
error |
Error occurred | On any error |
done |
Stream complete | End of stream |
token Event:
{
"type": "token",
"token": "Hello",
"index": 0,
"message_id": "msg_123"
}
| Field | Type | Description |
|---|---|---|
type |
string | Always "token" |
token |
string | The text token (1-4 characters typically) |
index |
integer | Token position in the response |
message_id |
string | ID of the message being generated |
message_complete Event:
{
"type": "message_complete",
"message": {
"id": "msg_123",
"role": "assistant",
"content": "Hello! How can I help you today?",
"tool_calls": [],
"created_at": "2026-01-20T12:00:05Z"
}
}
tool_call Event:
{
"type": "tool_call",
"tool_call": {
"id": "tc_456",
"tool_name": "search_products",
"parameters": {
"query": "laptop"
},
"status": "pending"
},
"message_id": "msg_123"
}
tool_result Event:
{
"type": "tool_result",
"tool_call": {
"id": "tc_456",
"tool_name": "search_products",
"status": "completed",
"result": {
"products": [...]
}
},
"message_id": "msg_123"
}
error Event:
{
"type": "error",
"error": {
"code": "tool_callback_failed",
"message": "Tool callback timed out",
"tool_call_id": "tc_456"
},
"message_id": "msg_123"
}
done Event:
{
"type": "done",
"message_id": "msg_123"
}
Events follow the standard SSE format:
event: token
data: {"type":"token","token":"Hello","index":0,"message_id":"msg_123"}
event: token
data: {"type":"token","token":" world","index":1,"message_id":"msg_123"}
event: message_complete
data: {"type":"message_complete","message":{...}}
event: done
data: {"type":"done","message_id":"msg_123"}
Important: Each event has an event: line followed by a data: line, then a blank line.
The standard browser EventSource API does not support custom headers. Use one of these approaches:
Option 1: Short-Lived Token in Query Parameter (Recommended for Browsers)
// 1. Get a short-lived streaming token (valid 60 seconds)
const { streaming_token } = await fetch('/api/v1/agents/rooms/{room_id}/stream-token', {
method: 'POST',
headers: { 'Authorization': `Bearer ${accessToken}` }
}).then(r => r.json());
// 2. Connect with token in query string
const eventSource = new EventSource(
`https://core.interactor.com/api/v1/agents/rooms/${roomId}/stream?token=${streaming_token}`
);
Option 2: Fetch API with ReadableStream
async function streamWithAuth(roomId: string, accessToken: string) {
const response = await fetch(
`https://core.interactor.com/api/v1/agents/rooms/${roomId}/stream`,
{
headers: { 'Authorization': `Bearer ${accessToken}` }
}
);
const reader = response.body?.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (reader) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// Parse complete events from buffer
const events = buffer.split('\n\n');
buffer = events.pop() || ''; // Keep incomplete event in buffer
for (const eventBlock of events) {
if (!eventBlock.trim()) continue;
const lines = eventBlock.split('\n');
let eventType = 'message';
let data = '';
for (const line of lines) {
if (line.startsWith('event: ')) {
eventType = line.slice(7);
} else if (line.startsWith('data: ')) {
data = line.slice(6);
}
}
if (data) {
const parsed = JSON.parse(data);
handleStreamEvent(eventType, parsed);
}
}
}
}
function handleStreamEvent(eventType: string, data: any) {
switch (data.type) {
case 'token':
process.stdout.write(data.token);
break;
case 'message_complete':
console.log('\n[Complete]', data.message.content);
break;
case 'tool_call':
console.log('[Tool]', data.tool_call.tool_name);
break;
case 'error':
console.error('[Error]', data.error.message);
break;
case 'done':
console.log('[Stream ended]');
break;
}
}
Option 3: Node.js with eventsource Package
import EventSource from 'eventsource';
const eventSource = new EventSource(
`https://core.interactor.com/api/v1/agents/rooms/${roomId}/stream`,
{ headers: { 'Authorization': `Bearer ${accessToken}` } }
);
eventSource.addEventListener('token', (e) => {
const data = JSON.parse(e.data);
process.stdout.write(data.token);
});
eventSource.addEventListener('error', (e) => {
console.error('Stream error:', e);
eventSource.close();
});
If the stream disconnects, reconnect with a Last-Event-ID header (if supported):
let lastEventId: string | null = null;
eventSource.onmessage = (e) => {
lastEventId = e.lastEventId;
// ... handle event
};
eventSource.onerror = () => {
eventSource.close();
// Reconnect after delay
setTimeout(() => {
const newSource = new EventSource(
`${streamUrl}?last_event_id=${lastEventId}`,
{ headers: { 'Authorization': `Bearer ${token}` } }
);
// ... set up handlers
}, 1000);
};
Connect databases and APIs that assistants can query directly using natural language.
curl -X POST https://core.interactor.com/api/v1/data-sources \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"name": "sales_database",
"type": "postgresql",
"connection": {
"host": "db.yourcompany.com",
"port": 5432,
"database": "sales",
"username": "readonly_user",
"password": "secure_password"
},
"description": "Sales and customer data including orders, products, and customer information"
}'
Response:
{
"data": {
"id": "ds_abc",
"name": "sales_database",
"type": "postgresql",
"status": "connected",
"schema_status": "extracting",
"created_at": "2026-01-20T12:00:00Z"
}
}
Interactor automatically extracts the database schema for the assistant to understand.
| Type | Description |
|---|---|
postgresql |
PostgreSQL database |
mysql |
MySQL database |
mssql |
Microsoft SQL Server |
mongodb |
MongoDB database |
rest_api |
REST API endpoint |
curl https://core.interactor.com/api/v1/data-sources \
-H "Authorization: Bearer <token>"
curl https://core.interactor.com/api/v1/data-sources/ds_abc \
-H "Authorization: Bearer <token>"
Response includes schema information:
{
"data": {
"id": "ds_abc",
"name": "sales_database",
"type": "postgresql",
"status": "connected",
"schema_status": "ready",
"tables": [
{
"name": "customers",
"columns": [
{"name": "id", "type": "uuid", "nullable": false},
{"name": "email", "type": "varchar", "nullable": false},
{"name": "name", "type": "varchar", "nullable": true},
{"name": "created_at", "type": "timestamp", "nullable": false}
]
},
{
"name": "orders",
"columns": [...]
}
]
}
}
curl -X PUT https://core.interactor.com/api/v1/data-sources/ds_abc \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"description": "Updated description with more context"
}'
curl -X DELETE https://core.interactor.com/api/v1/data-sources/ds_abc \
-H "Authorization: Bearer <token>"
Re-extract the schema if your database structure changed:
curl -X POST https://core.interactor.com/api/v1/data-sources/ds_abc/refresh-schema \
-H "Authorization: Bearer <token>"
Run a query directly against the data source:
curl -X POST https://core.interactor.com/api/v1/data-sources/ds_abc/query \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"query": "SELECT * FROM customers WHERE created_at > $1 LIMIT 10",
"parameters": ["2026-01-01"]
}'
Response:
{
"data": {
"columns": ["id", "email", "name", "created_at"],
"rows": [
["uuid-1", "john@example.com", "John Doe", "2026-01-15T10:00:00Z"],
["uuid-2", "jane@example.com", "Jane Smith", "2026-01-16T11:00:00Z"]
],
"row_count": 2,
"execution_time_ms": 45
}
}
Add synonyms and descriptions to help the assistant understand your schema better:
curl -X PATCH https://core.interactor.com/api/v1/data-sources/ds_abc/semantic-mappings \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"mappings": {
"customers": {
"description": "Customer accounts and profiles",
"synonyms": ["users", "clients", "accounts", "members"]
},
"customers.created_at": {
"description": "When the customer signed up",
"synonyms": ["signup date", "registration date", "joined date"]
},
"orders": {
"description": "Customer purchase orders",
"synonyms": ["purchases", "transactions", "sales"]
},
"orders.total_amount": {
"description": "Total order value in USD",
"synonyms": ["order total", "purchase amount", "sale value"]
}
}
}'
This helps the assistant translate natural language questions like:
SELECT COUNT(*) FROM customers WHERE created_at >= '2026-01-01'SELECT SUM(total_amount) FROM orders WHERE created_at >= '2026-01-14'Search for external services that assistants can connect to:
curl -X POST https://core.interactor.com/api/v1/knowledge-base/services/search \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"query": "calendar scheduling",
"limit": 5
}'
Response:
{
"data": {
"services": [
{
"id": "google_calendar",
"name": "Google Calendar",
"description": "Calendar and scheduling service",
"auth_type": "oauth2",
"capabilities": ["create_event", "list_events", "update_event", "delete_event"]
},
{
"id": "microsoft_calendar",
"name": "Microsoft Outlook Calendar",
"description": "Microsoft 365 calendar service",
"auth_type": "oauth2",
"capabilities": ["create_event", "list_events", "update_event"]
}
]
}
}
curl -X POST https://core.interactor.com/api/v1/knowledge-base/services/lookup \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"service_id": "google_calendar"}'
curl https://core.interactor.com/api/v1/knowledge-base/services/google_calendar \
-H "Authorization: Bearer <token>"
curl https://core.interactor.com/api/v1/knowledge-base/services/google_calendar/oauth \
-H "Authorization: Bearer <token>"
import { InteractorClient } from './interactor-client';
export class AgentManager {
private client: InteractorClient;
constructor(client: InteractorClient) {
this.client = client;
}
// ============ Assistants ============
async createAssistant(config: {
name: string;
title: string;
description?: string;
instructions: string;
modelConfig?: {
provider?: 'openai';
model?: string;
temperature?: number;
};
enabledTools?: string[];
}): Promise<Assistant> {
return this.client.request('POST', '/agents/assistants', {
name: config.name,
title: config.title,
description: config.description,
instructions: config.instructions,
model_config: config.modelConfig,
enabled_tools: config.enabledTools
});
}
async listAssistants(): Promise<Assistant[]> {
const result = await this.client.request<{ assistants: Assistant[] }>(
'GET',
'/agents/assistants'
);
return result.assistants;
}
async getAssistant(id: string): Promise<Assistant> {
return this.client.request('GET', `/agents/assistants/${id}`);
}
async updateAssistant(id: string, updates: Partial<AssistantConfig>): Promise<Assistant> {
return this.client.request('PUT', `/agents/assistants/${id}`, updates);
}
async deleteAssistant(id: string): Promise<void> {
await this.client.request('DELETE', `/agents/assistants/${id}`);
}
// ============ Rooms ============
async createRoom(
assistantId: string,
userId: string,
metadata?: Record<string, any>
): Promise<Room> {
return this.client.request('POST', `/agents/${assistantId}/rooms`, {
namespace: `user_${userId}`,
metadata
});
}
async listRooms(filters?: {
userId?: string;
assistantId?: string;
status?: 'active' | 'closed';
}): Promise<Room[]> {
const params = new URLSearchParams();
if (filters?.userId) params.set('namespace', `user_${filters.userId}`);
if (filters?.assistantId) params.set('assistant_id', filters.assistantId);
if (filters?.status) params.set('status', filters.status);
const query = params.toString();
const result = await this.client.request<{ rooms: Room[] }>(
'GET',
`/agents/rooms${query ? '?' + query : ''}`
);
return result.rooms;
}
async getRoom(roomId: string): Promise<Room> {
return this.client.request('GET', `/agents/rooms/${roomId}`);
}
async closeRoom(roomId: string): Promise<void> {
await this.client.request('POST', `/agents/rooms/${roomId}/close`);
}
// ============ Messages ============
async sendMessage(roomId: string, content: string): Promise<Message> {
return this.client.request('POST', `/agents/rooms/${roomId}/messages`, {
content,
role: 'user'
});
}
async getMessages(roomId: string, options?: {
limit?: number;
before?: string;
}): Promise<{ messages: Message[]; has_more: boolean }> {
const params = new URLSearchParams();
if (options?.limit) params.set('limit', options.limit.toString());
if (options?.before) params.set('before', options.before);
const query = params.toString();
return this.client.request(
'GET',
`/agents/rooms/${roomId}/messages${query ? '?' + query : ''}`
);
}
// ============ Tools ============
async registerTool(tool: {
name: string;
description: string;
parameters: Record<string, any>;
callbackUrl: string;
callbackSecret: string;
}): Promise<Tool> {
return this.client.request('POST', '/tools', {
name: tool.name,
description: tool.description,
parameters: tool.parameters,
callback_url: tool.callbackUrl,
callback_secret: tool.callbackSecret
});
}
async listTools(): Promise<Tool[]> {
const result = await this.client.request<{ tools: Tool[] }>('GET', '/tools');
return result.tools;
}
async deleteTool(id: string): Promise<void> {
await this.client.request('DELETE', `/tools/${id}`);
}
// ============ Data Sources ============
async registerDataSource(config: {
name: string;
type: 'postgresql' | 'mysql' | 'mssql' | 'mongodb' | 'rest_api';
connection: Record<string, any>;
description?: string;
}): Promise<DataSource> {
return this.client.request('POST', '/data-sources', config);
}
async listDataSources(): Promise<DataSource[]> {
const result = await this.client.request<{ data_sources: DataSource[] }>(
'GET',
'/data-sources'
);
return result.data_sources;
}
async getDataSource(id: string): Promise<DataSource> {
return this.client.request('GET', `/data-sources/${id}`);
}
async refreshSchema(id: string): Promise<void> {
await this.client.request('POST', `/data-sources/${id}/refresh-schema`);
}
async executeQuery(
id: string,
query: string,
parameters?: any[]
): Promise<QueryResult> {
return this.client.request('POST', `/data-sources/${id}/query`, {
query,
parameters
});
}
async updateSemanticMappings(
id: string,
mappings: Record<string, { description?: string; synonyms?: string[] }>
): Promise<void> {
await this.client.request('PATCH', `/data-sources/${id}/semantic-mappings`, {
mappings
});
}
}
// Types
interface Assistant {
id: string;
name: string;
title: string;
description?: string;
model_config: {
provider: string;
model: string;
temperature: number;
};
enabled_tools: string[];
created_at: string;
}
interface AssistantConfig {
title?: string;
description?: string;
instructions?: string;
model_config?: {
provider?: string;
model?: string;
temperature?: number;
};
enabled_tools?: string[];
}
interface Room {
id: string;
assistant_id: string;
namespace: string;
status: 'active' | 'closed';
metadata?: Record<string, any>;
message_count: number;
created_at: string;
last_message_at?: string;
}
interface Message {
id: string;
role: 'user' | 'assistant' | 'system' | 'tool';
content: string;
tool_calls?: ToolCall[];
created_at: string;
}
interface ToolCall {
id: string;
tool_name: string;
parameters: Record<string, any>;
result?: any;
}
interface Tool {
id: string;
name: string;
description: string;
parameters: Record<string, any>;
callback_url: string;
created_at: string;
}
interface DataSource {
id: string;
name: string;
type: string;
status: 'connected' | 'disconnected' | 'error';
schema_status: 'extracting' | 'ready' | 'error';
tables?: TableSchema[];
created_at: string;
}
interface TableSchema {
name: string;
columns: ColumnSchema[];
}
interface ColumnSchema {
name: string;
type: string;
nullable: boolean;
}
interface QueryResult {
columns: string[];
rows: any[][];
row_count: number;
execution_time_ms: number;
}
// Example: Building a chat interface
async function chat(userId: string, assistantId: string) {
const agentManager = new AgentManager(interactorClient);
// Get or create a room for this user
let rooms = await agentManager.listRooms({
userId,
assistantId,
status: 'active'
});
let room: Room;
if (rooms.length > 0) {
room = rooms[0];
} else {
room = await agentManager.createRoom(assistantId, userId, {
user_name: 'John',
context: 'general_support'
});
}
// Send a message
await agentManager.sendMessage(room.id, 'How do I reset my password?');
// Wait for response (in real app, use streaming)
await new Promise(resolve => setTimeout(resolve, 3000));
// Get messages
const { messages } = await agentManager.getMessages(room.id);
// Display conversation
for (const msg of messages) {
console.log(`${msg.role}: ${msg.content}`);
}
}
Subscribe to agent events for real-time updates:
| Event | Description |
|---|---|
agent.room.message |
New message in a room |
agent.room.closed |
Room was closed |
Note: Additional events like
agent.tool.invokedmay be available depending on your Interactor version. Check theinteractor-webhooksskill or your API documentation for the complete list of available events.
See interactor-webhooks skill for webhook setup and SSE streaming.
Common error patterns you may encounter:
| Error Code | HTTP Status | Description | Resolution |
|---|---|---|---|
assistant_not_found |
404 | Assistant doesn't exist | Check assistant ID |
room_not_found |
404 | Room doesn't exist | Check room ID |
room_closed |
400 | Room is already closed | Create a new room |
tool_not_found |
404 | Tool doesn't exist | Check tool ID |
tool_callback_failed |
500 | Tool callback returned error | Check your callback endpoint |
data_source_disconnected |
400 | Database connection failed | Check connection settings |
query_execution_failed |
400 | SQL query failed | Check query syntax |
Note: Error codes and formats may vary. Always check the response body for detailed error messages. Standard HTTP status codes apply (4xx for client errors, 5xx for server errors).
The AI Agents API uses rate limiting to ensure fair usage and system stability.
Every API response includes these headers:
| Header | Type | Description |
|---|---|---|
X-RateLimit-Limit |
integer | Max requests allowed in window |
X-RateLimit-Remaining |
integer | Requests remaining in current window |
X-RateLimit-Reset |
integer | Unix timestamp (UTC) when window resets |
X-RateLimit-Resource |
string | Which limit bucket this applies to |
Example Response Headers:
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1705752000
X-RateLimit-Resource: agents:messages
Limits vary by plan. Typical defaults:
| Resource | Free | Pro | Enterprise |
|---|---|---|---|
| Assistants (create) | 10/min | 60/min | 300/min |
| Rooms (create) | 100/min | 600/min | 3000/min |
| Messages (send) | 60/min/room | 300/min/room | 1000/min/room |
| Tool registrations | 20/min | 100/min | 500/min |
| Data source queries | 100/min | 500/min | 2000/min |
| API calls (global) | 1000/min | 10000/min | 100000/min |
Check Your Limits: View your actual limits in the Interactor dashboard under Settings → API → Rate Limits, or call
GET /api/v1/account/limits.
When rate limited:
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705752000
Retry-After: 30
Content-Type: application/json
{
"error": {
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded for agents:messages",
"resource": "agents:messages",
"retry_after": 30
}
}
Implementation with proper header parsing:
interface RateLimitInfo {
limit: number;
remaining: number;
resetAt: Date;
resource: string;
}
function parseRateLimitHeaders(headers: Headers): RateLimitInfo {
return {
limit: parseInt(headers.get('X-RateLimit-Limit') || '0'),
remaining: parseInt(headers.get('X-RateLimit-Remaining') || '0'),
resetAt: new Date(parseInt(headers.get('X-RateLimit-Reset') || '0') * 1000),
resource: headers.get('X-RateLimit-Resource') || 'unknown'
};
}
async function withRateLimitHandling<T>(
fn: () => Promise<Response>,
maxRetries: number = 3
): Promise<T> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fn();
const rateLimit = parseRateLimitHeaders(response.headers);
// Log rate limit status for monitoring
console.log(`Rate limit: ${rateLimit.remaining}/${rateLimit.limit} for ${rateLimit.resource}`);
if (response.ok) {
return response.json();
}
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('Retry-After') || '1');
console.warn(`Rate limited. Waiting ${retryAfter}s before retry ${attempt + 1}/${maxRetries}`);
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
continue;
}
// Non-retryable error
throw new Error(`API error: ${response.status}`);
}
throw new Error('Max retries exceeded due to rate limiting');
}
// Proactive rate limit avoidance
async function sendMessageWithBackpressure(roomId: string, content: string): Promise<Message> {
const response = await fetch(`/api/v1/agents/rooms/${roomId}/messages`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ content, role: 'user' })
});
const rateLimit = parseRateLimitHeaders(response.headers);
// If getting close to limit, slow down proactively
if (rateLimit.remaining < rateLimit.limit * 0.1) {
const waitTime = Math.ceil((rateLimit.resetAt.getTime() - Date.now()) / 1000);
console.warn(`Approaching rate limit. Consider slowing down. Resets in ${waitTime}s`);
}
return response.json();
}
Symptoms: Assistant says it's calling a tool, but your callback endpoint never receives the request.
Checklist:
callback_url is publicly accessible (not localhost)Debug: Check Interactor dashboard for tool execution logs.
Symptoms: All tool callbacks return 401 Unauthorized.
Checklist:
callback_secret matches what you registeredX-Interactor-SignatureDebug:
// Log for debugging (remove in production)
console.log('Received signature:', signature);
console.log('Payload:', payload);
console.log('Expected:', `sha256=${crypto.createHmac('sha256', secret).update(payload).digest('hex')}`);
Symptoms: Data source status shows "disconnected" or "error".
Checklist:
Common Fixes:
-- Grant read-only access to Interactor user
GRANT SELECT ON ALL TABLES IN SCHEMA public TO interactor_readonly;
GRANT USAGE ON SCHEMA public TO interactor_readonly;
Symptoms: Assistant responds without using available tools when it should.
Checklist:
enabled_tools array for the assistantFix: Update assistant instructions to be explicit:
"When users ask about products, ALWAYS use the search_products tool to find current information."
Symptoms: Sent messages don't show up when listing messages.
Checklist:
Symptoms: Assistant takes a long time to respond.
Possible Causes:
Monitor:
const start = Date.now();
await sendMessage(roomId, content);
console.log(`Message sent in ${Date.now() - start}ms`);
| Data Type | Default Retention | Configurable |
|---|---|---|
| Messages | 90 days | Yes (30-365 days) |
| Room metadata | Until room deleted | N/A |
| Tool execution logs | 30 days | Yes (7-90 days) |
| Assistant configurations | Indefinite | N/A |
Enterprise: Contact support for custom retention policies and data residency requirements.
Configure retention in your Interactor dashboard under Settings → Data Management → Retention Policies.
To delete user data for compliance with GDPR "Right to Erasure" or CCPA:
Delete all rooms for a user:
# 1. List all rooms for the user
curl "https://core.interactor.com/api/v1/agents/rooms?namespace=user_123" \
-H "Authorization: Bearer <token>"
# 2. Delete each room (also deletes messages)
curl -X DELETE "https://core.interactor.com/api/v1/agents/rooms/room_xyz" \
-H "Authorization: Bearer <token>"
Bulk deletion via API:
curl -X POST https://core.interactor.com/api/v1/data/delete-user-data \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"namespace": "user_123",
"include_messages": true,
"include_tool_logs": true,
"confirmation": "DELETE_ALL_DATA_FOR_user_123"
}'
Response:
{
"data": {
"deletion_id": "del_abc123",
"status": "processing",
"estimated_completion": "2026-01-20T13:00:00Z",
"items_to_delete": {
"rooms": 5,
"messages": 142,
"tool_logs": 23
}
}
}
Important: Data deletion is asynchronous. Poll the deletion status endpoint to confirm completion before responding to user deletion requests.
Rotate callback_secret for tools without downtime:
Step 1: Add new secret (dual-secret mode):
curl -X PATCH https://core.interactor.com/api/v1/tools/tool_abc/secrets \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"add_secret": "new_secret_value_here",
"deprecate_old_after_hours": 24
}'
Step 2: Update your callback handler to accept both secrets:
function verifyWithRotation(
payload: string,
signature: string,
secrets: string[]
): boolean {
const signatureValue = signature.replace('sha256=', '');
for (const secret of secrets) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
if (crypto.timingSafeEqual(
Buffer.from(signatureValue),
Buffer.from(expected)
)) {
return true;
}
}
return false;
}
// Use both secrets during rotation
const SECRETS = [
process.env.TOOL_CALLBACK_SECRET_NEW!,
process.env.TOOL_CALLBACK_SECRET_OLD!
];
const isValid = verifyWithRotation(payload, signature, SECRETS);
Step 3: After transition period, remove old secret:
curl -X PATCH https://core.interactor.com/api/v1/tools/tool_abc/secrets \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"remove_deprecated": true
}'
Do NOT include PII in:
DO use:
user_123)// Bad - PII in tool parameters
{ "user_email": "john@example.com" }
// Good - Use references
{ "user_id": "user_123" } // Look up email in your backend
Every API request returns a correlation ID for tracing:
Response Headers:
X-Request-Id: req_abc123xyz
X-Trace-Id: trace_789def
Include these in support requests and logging:
async function apiRequest(path: string, options: RequestInit = {}) {
const response = await fetch(`https://core.interactor.com/api/v1${path}`, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`,
'X-Client-Request-Id': crypto.randomUUID() // Your own correlation ID
}
});
const requestId = response.headers.get('X-Request-Id');
const traceId = response.headers.get('X-Trace-Id');
// Log for debugging
console.log(`[${path}] Request: ${requestId}, Trace: ${traceId}`);
return { response, requestId, traceId };
}
import { Logger } from 'your-logging-library';
const logger = new Logger({
service: 'my-app',
version: process.env.APP_VERSION
});
// Include correlation IDs in all agent-related logs
async function sendMessageWithLogging(roomId: string, content: string) {
const correlationId = crypto.randomUUID();
logger.info('Sending message to AI agent', {
correlationId,
roomId,
contentLength: content.length
});
try {
const { response, requestId } = await apiRequest(
`/agents/rooms/${roomId}/messages`,
{
method: 'POST',
body: JSON.stringify({ content, role: 'user' })
}
);
if (!response.ok) {
logger.error('Message send failed', {
correlationId,
requestId,
status: response.status
});
throw new Error(`API error: ${response.status}`);
}
const data = await response.json();
logger.info('Message sent successfully', {
correlationId,
requestId,
messageId: data.data.id
});
return data;
} catch (error) {
logger.error('Message send exception', {
correlationId,
error: error.message
});
throw error;
}
}
| Metric | Description | Alert Threshold |
|---|---|---|
agent.messages.sent |
Messages sent to assistants | N/A (volume) |
agent.messages.latency_ms |
Time to first token | > 5000ms |
agent.tool_calls.total |
Tool invocations | N/A (volume) |
agent.tool_calls.errors |
Failed tool callbacks | > 5% error rate |
agent.tool_calls.latency_ms |
Tool callback duration | > 10000ms |
agent.rooms.active |
Currently active rooms | Depends on scale |
agent.rate_limits.hits |
429 responses received | > 10/min |
Export metrics to your monitoring system:
import { metrics } from 'your-metrics-library';
// Track message latency
const sendStart = Date.now();
await sendMessage(roomId, content);
metrics.histogram('agent.messages.latency_ms', Date.now() - sendStart, {
assistant_id: assistantId
});
// Track tool callback performance
app.post('/api/tools/:toolName', async (req, res) => {
const start = Date.now();
try {
const result = await handleToolCallback(req);
metrics.increment('agent.tool_calls.total', { tool: req.params.toolName, status: 'success' });
res.json({ result });
} catch (error) {
metrics.increment('agent.tool_calls.errors', { tool: req.params.toolName });
throw error;
} finally {
metrics.histogram('agent.tool_calls.latency_ms', Date.now() - start, {
tool: req.params.toolName
});
}
});
| Content Type | Max Size | Notes |
|---|---|---|
| Text message | 32 KB | UTF-8 encoded |
| Metadata object | 16 KB | JSON serialized |
| Tool result | 64 KB | JSON serialized |
| Total request | 128 KB | Including headers |
Messages exceeding these limits return 413 Payload Too Large.
For content that may exceed limits:
const MAX_MESSAGE_SIZE = 32 * 1024; // 32 KB
function truncateForAssistant(content: string): string {
if (content.length <= MAX_MESSAGE_SIZE) {
return content;
}
// Truncate with indicator
const truncated = content.slice(0, MAX_MESSAGE_SIZE - 100);
return truncated + '\n\n[Content truncated. Full content available via reference ID: ref_xxx]';
}
// For large documents, summarize or chunk
async function sendLargeDocument(roomId: string, document: string) {
if (document.length <= MAX_MESSAGE_SIZE) {
return sendMessage(roomId, document);
}
// Store full document and send reference
const docId = await storeDocument(document);
return sendMessage(roomId,
`I have a document to analyze (${document.length} characters). ` +
`Key sections:\n${extractKeySections(document)}\n\n` +
`Use the get_document tool with id="${docId}" to retrieve specific sections.`
);
}
API Note: File attachment support varies by Interactor version. Check your API documentation or contact support for availability.
If supported, the typical pattern:
# 1. Upload attachment
curl -X POST https://core.interactor.com/api/v1/attachments \
-H "Authorization: Bearer <token>" \
-F "file=@document.pdf" \
-F "purpose=message_attachment"
# Response:
# { "data": { "id": "att_xyz", "url": "https://...", "expires_at": "..." } }
# 2. Reference in message
curl -X POST https://core.interactor.com/api/v1/agents/rooms/room_xyz/messages \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"content": "Please analyze this document",
"role": "user",
"attachments": ["att_xyz"]
}'
Supported file types (when available):
Max file size: 10 MB per file, 25 MB total per message.
The AI Agents API follows OpenAPI 3.0 specification. Access the spec at:
https://core.interactor.com/api/v1/openapi.json
https://core.interactor.com/api/v1/openapi.yaml
Generate Client Libraries:
# TypeScript
npx openapi-typescript-codegen \
--input https://core.interactor.com/api/v1/openapi.json \
--output ./generated/interactor-client
# Python
pip install openapi-python-client
openapi-python-client generate \
--url https://core.interactor.com/api/v1/openapi.json
1. Use the Sandbox Environment:
# Sandbox base URL
export INTERACTOR_URL="https://sandbox.interactor.com"
# Sandbox credentials (from dashboard)
export INTERACTOR_SANDBOX_TOKEN="sandbox_token_here"
2. Test Tool Callbacks Locally:
Use a tunnel service to expose your local endpoint:
# Using ngrok
ngrok http 3000
# Register tool with ngrok URL
curl -X POST https://sandbox.interactor.com/api/v1/tools \
-H "Authorization: Bearer $INTERACTOR_SANDBOX_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "test_tool",
"description": "Test tool for development",
"parameters": {"type": "object", "properties": {}},
"callback_url": "https://abc123.ngrok.io/api/tools/test"
}'
3. Verify Webhook Signatures in Tests:
import { describe, it, expect } from 'vitest';
import crypto from 'crypto';
describe('Tool Callback Verification', () => {
const SECRET = 'test_secret';
it('should verify valid signature', () => {
const payload = JSON.stringify({ tool_name: 'test', parameters: {} });
const signature = 'sha256=' + crypto
.createHmac('sha256', SECRET)
.update(payload)
.digest('hex');
const result = verifyToolCallback(payload, signature, SECRET);
expect(result.valid).toBe(true);
});
it('should reject invalid signature', () => {
const payload = JSON.stringify({ tool_name: 'test', parameters: {} });
const signature = 'sha256=invalid';
const result = verifyToolCallback(payload, signature, SECRET);
expect(result.valid).toBe(false);
});
it('should reject expired timestamp', () => {
const payload = JSON.stringify({ tool_name: 'test', parameters: {} });
const oldTimestamp = new Date(Date.now() - 400000).toISOString(); // 400 seconds old
const signature = 'sha256=' + crypto
.createHmac('sha256', SECRET)
.update(payload)
.digest('hex');
const result = verifyToolCallback(payload, signature, oldTimestamp, SECRET, 300);
expect(result.valid).toBe(false);
expect(result.error).toContain('too old');
});
});
4. Integration Test Example:
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
describe('AI Agents Integration', () => {
let assistantId: string;
let roomId: string;
beforeAll(async () => {
// Create test assistant
const assistant = await createAssistant({
name: `test_${Date.now()}`,
title: 'Test Assistant',
instructions: 'You are a test assistant. Always respond with "Test OK".'
});
assistantId = assistant.id;
});
afterAll(async () => {
// Cleanup
if (roomId) await closeRoom(roomId);
if (assistantId) await deleteAssistant(assistantId);
});
it('should create a room and exchange messages', async () => {
// Create room
const room = await createRoom(assistantId, 'test_user');
roomId = room.id;
expect(room.status).toBe('active');
// Send message
const sent = await sendMessage(roomId, 'Hello');
expect(sent.role).toBe('user');
// Wait for response (in real test, use streaming or webhooks)
await new Promise(r => setTimeout(r, 3000));
// Get messages
const { messages } = await getMessages(roomId);
expect(messages.length).toBeGreaterThanOrEqual(2);
const assistantMessage = messages.find(m => m.role === 'assistant');
expect(assistantMessage).toBeDefined();
expect(assistantMessage?.content).toContain('Test OK');
});
});
When implementing AI agents, provide this summary:
## AI Agent Implementation Report
**Date**: YYYY-MM-DD
**Assistant**: support_assistant
### Configuration
| Setting | Value |
|---------|-------|
| Model | gpt-4o |
| Temperature | 0.7 |
| Tools | search_products, create_ticket |
### Tools Registered
| Tool | Callback URL | Status |
|------|--------------|--------|
| search_products | https://app.com/api/tools/search | ✓ Active |
| create_ticket | https://app.com/api/tools/ticket | ✓ Active |
### Data Sources Connected
| Name | Type | Status |
|------|------|--------|
| sales_database | PostgreSQL | ✓ Connected |
### Implementation Checklist
- [ ] Assistant created with instructions
- [ ] Tools registered with callbacks
- [ ] Tool callback signature verification
- [ ] Data sources connected
- [ ] Semantic mappings configured
- [ ] Room management implemented
- [ ] Message streaming setup
- [ ] Error handling implemented
### Next Steps
1. Test conversations with sample queries
2. Set up webhooks for real-time updates (see `interactor-webhooks` skill)
3. Implement streaming for real-time responses
Problem: API returns 401 Unauthorized for agent operations.
Cause: Missing, invalid, or expired access token.
Solution:
# 1. Verify you have a token
echo $TOKEN # Should not be empty
# 2. Get a fresh token
TOKEN=$(curl -s -X POST https://auth.interactor.com/api/v1/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "client_credentials",
"client_id": "'$INTERACTOR_CLIENT_ID'",
"client_secret": "'$INTERACTOR_CLIENT_SECRET'"
}' | jq -r '.data.access_token')
# 3. Test with the token
curl https://core.interactor.com/api/v1/agents/assistants \
-H "Authorization: Bearer $TOKEN"
Problem: Agent created locally but not synced with Interactor.
Cause: Token obtained but not passed to the API call function.
Solution: Ensure token flows through your entire call stack:
// ❌ Common mistake: Function doesn't receive token
async function createAgent(name: string) {
// Token is undefined here!
return interactorClient.post('/agents/assistants', { name });
}
// ✅ Correct: Token passed explicitly
async function createAgent(name: string, accessToken: string) {
return fetch('https://core.interactor.com/api/v1/agents/assistants', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ name })
});
}
Before deploying, verify these are all true:
client_id and client_secret are set in environmentaccess_tokenAuthorization: Bearer <token> header