Claude-skill-registry interactor-agents
Create LLM-powered AI assistants with tools and data sources through Interactor. Use when building conversational AI, chatbots, tool-calling assistants, or agents that need to query databases and external APIs.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/interactor-agents" ~/.claude/skills/majiayu000-claude-skill-registry-interactor-agents && rm -rf "$T"
skills/data/interactor-agents/SKILL.mdInteractor AI Agents Skill
Build LLM-powered assistants that can have conversations, use tools, and access your data sources.
When to Use
- Conversational AI: Building chat interfaces with AI assistants
- Tool-Calling Agents: Creating assistants that can invoke custom functions
- Data-Connected AI: Connecting AI to databases for natural language queries
- Customer Support Bots: Building support assistants with domain knowledge
- Internal Tools: Creating AI assistants for internal operations
Prerequisites
- Interactor authentication configured (see
skill)interactor-auth - Understanding of LLM concepts (prompts, tools, context)
- Webhook endpoint for tool callbacks (optional, for custom tools)
- Database with network access from Interactor (optional, for data sources)
command-line tool (for bash examples)jq
Overview
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 |
Quick Start
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
for JSON parsing and a validjq. See$TOKENskill for authentication setup.interactor-auth
# 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"}'
Instructions
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.
Step 1: Create an Assistant
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" } }
Assistant Configuration Options
| Field | Type | Required | Description |
|---|---|---|---|
| string | Yes | Unique identifier (lowercase, underscores only) |
| string | Yes | Display name for users |
| string | No | What the assistant does |
| string | No | (default: ) |
| string | No | Model identifier (default: ) |
| number | No | Response randomness 0.0-1.0 (default: 0.7) |
| string | Yes | System prompt defining behavior |
| array | No | Tool names the assistant can use |
List Assistants
curl https://core.interactor.com/api/v1/agents/assistants \ -H "Authorization: Bearer <token>"
Get Assistant
curl https://core.interactor.com/api/v1/agents/assistants/asst_abc \ -H "Authorization: Bearer <token>"
Update Assistant
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 } }'
Delete Assistant
curl -X DELETE https://core.interactor.com/api/v1/agents/assistants/asst_abc \ -H "Authorization: Bearer <token>"
Chat Rooms
Rooms are conversations between a user and an assistant.
Create a Room
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" } }
List Rooms
curl https://core.interactor.com/api/v1/agents/rooms \ -H "Authorization: Bearer <token>"
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
| string | Filter by namespace |
| string | Filter by assistant |
| string | or |
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>"
Get Room
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" } }
Close Room
curl -X POST https://core.interactor.com/api/v1/agents/rooms/room_xyz/close \ -H "Authorization: Bearer <token>"
Messages
Send a 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": "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.
List Messages
curl https://core.interactor.com/api/v1/agents/rooms/room_xyz/messages \ -H "Authorization: Bearer <token>"
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
| integer | Max messages to return (default: 50, max: 100) |
| string | Opaque cursor for pagination (from ) |
| string | Sort order: (oldest first) or (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=" } }
Pagination Pattern
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:
- Always use
from the response; never construct cursors manuallynext_cursor - Cursors are opaque and may change format between API versions
- Cursors expire after 24 hours; for long-running jobs, re-fetch from the beginning
- Results are stable within a cursor session (new messages won't appear mid-pagination)
Message Roles
| Role | Description |
|---|---|
| Message from the user |
| Response from the AI assistant |
| System message (internal use) |
| Tool execution result |
Real-Time Responses
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 (
) is a common convention but may differ in your deployment. Check your API documentation or contact support./rooms/{id}/stream
Option 1: Server-Sent Events (SSE)
Subscribe to room events for streaming tokens as they're generated.
Note: The standard browser
API doesn't support custom headers. Use theEventSourcenpm package for Node.js, or pass the token as a query parameter if your API supports it.eventsource
Node.js with
package:eventsource
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
Tools are custom functions that assistants can invoke during conversations.
Register a Tool
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" } }
Tool Callback
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
header contains an HMAC-SHA256 signature of the raw request body, signed with yourX-Interactor-Signature. Always verify this signature before processing the request.callback_secret
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.
Verify Tool Callback Signature
Tool callbacks include three security headers for verification:
| Header | Description |
|---|---|
| HMAC-SHA256 signature of the raw request body |
| ISO 8601 timestamp when request was signed |
| Unique request ID (same as in body) |
Security Requirements:
- Verify HMAC signature matches
- Check timestamp is within acceptable window (recommended: 300 seconds)
- Use
as idempotency key to prevent replay attacksexecution_id
TypeScript (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})
List Tools
curl https://core.interactor.com/api/v1/tools \ -H "Authorization: Bearer <token>"
Get Tool
curl https://core.interactor.com/api/v1/tools/tool_abc \ -H "Authorization: Bearer <token>"
Update Tool
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" }'
Delete Tool
curl -X DELETE https://core.interactor.com/api/v1/tools/tool_abc \ -H "Authorization: Bearer <token>"
Webhook & Callback Contract
This section defines the canonical contract for all Interactor webhooks and tool callbacks.
Request Headers
All webhook/callback requests include these headers:
| Header | Format | Description |
|---|---|---|
| | HMAC-SHA256 of raw body |
| ISO 8601 | When request was signed |
| | Unique request identifier |
| | Always JSON |
Response Requirements
| Status | Meaning | Interactor Behavior |
|---|---|---|
| Success | Process result, no retry |
| Created | Same as 200 |
| Accepted | Async processing (see below) |
| Bad Request | Permanent failure, no retry |
| Unauthorized | Permanent failure, no retry |
| Not Found | Permanent failure, no retry |
| Rate Limited | Retry with backoff |
| Server Error | Retry with backoff |
| Gateway Error | Retry with backoff |
Expected Response Format:
{ "result": { ... } }
Timeout Policy
| 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
with a status URL, and Interactor will poll for completion.202 Accepted
Retry Policy
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:
- Request is marked as permanently failed
event is emitted (if webhooks configured)tool.callback.failed- Assistant receives an error and may retry or inform the user
Idempotency Requirements
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.
Dead Letter Handling
If all retries are exhausted:
- Interactor emits a
event (if webhooks configured)tool.callback.dead_letter - The failed request is logged in your Interactor dashboard
- You can manually retry from the dashboard or via API
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" } }
Streaming Contract
This section defines the canonical contract for Server-Sent Events (SSE) streaming.
Event Types
The streaming endpoint emits these event types:
| Event Type | Description | When Emitted |
|---|---|---|
| Incremental response token | During response generation |
| Full message available | When response finishes |
| Tool is being invoked | When assistant calls a tool |
| Tool execution completed | After tool callback returns |
| Error occurred | On any error |
| Stream complete | End of stream |
Event Schemas
Event:token
{ "type": "token", "token": "Hello", "index": 0, "message_id": "msg_123" }
| Field | Type | Description |
|---|---|---|
| string | Always |
| string | The text token (1-4 characters typically) |
| integer | Token position in the response |
| string | ID of the message being generated |
Event:message_complete
{ "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" } }
Event:tool_call
{ "type": "tool_call", "tool_call": { "id": "tc_456", "tool_name": "search_products", "parameters": { "query": "laptop" }, "status": "pending" }, "message_id": "msg_123" }
Event:tool_result
{ "type": "tool_result", "tool_call": { "id": "tc_456", "tool_name": "search_products", "status": "completed", "result": { "products": [...] } }, "message_id": "msg_123" }
Event:error
{ "type": "error", "error": { "code": "tool_callback_failed", "message": "Tool callback timed out", "tool_call_id": "tc_456" }, "message_id": "msg_123" }
Event:done
{ "type": "done", "message_id": "msg_123" }
SSE Wire Format
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.
Authentication for Streaming
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
Packageeventsource
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(); });
Reconnection Handling
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); };
Data Sources
Connect databases and APIs that assistants can query directly using natural language.
Register a Data Source
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.
Supported Data Source Types
| Type | Description |
|---|---|
| PostgreSQL database |
| MySQL database |
| Microsoft SQL Server |
| MongoDB database |
| REST API endpoint |
List Data Sources
curl https://core.interactor.com/api/v1/data-sources \ -H "Authorization: Bearer <token>"
Get Data Source
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": [...] } ] } }
Update Data Source
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" }'
Delete Data Source
curl -X DELETE https://core.interactor.com/api/v1/data-sources/ds_abc \ -H "Authorization: Bearer <token>"
Refresh Schema
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>"
Execute Query
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 } }
Semantic Mappings
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:
- "How many users signed up last month?" →
SELECT COUNT(*) FROM customers WHERE created_at >= '2026-01-01' - "What's the total sales this week?" →
SELECT SUM(total_amount) FROM orders WHERE created_at >= '2026-01-14'
Knowledge Base Search
Search for external services that assistants can connect to:
Search Services
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"] } ] } }
Lookup Service
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"}'
Get Service Details
curl https://core.interactor.com/api/v1/knowledge-base/services/google_calendar \ -H "Authorization: Bearer <token>"
Get Service OAuth Config
curl https://core.interactor.com/api/v1/knowledge-base/services/google_calendar/oauth \ -H "Authorization: Bearer <token>"
Complete Implementation Example
TypeScript Implementation
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; }
Chat Interface Example
// 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}`); } }
Webhook Events
Subscribe to agent events for real-time updates:
| Event | Description |
|---|---|
| New message in a room |
| Room was closed |
Note: Additional events like
may be available depending on your Interactor version. Check theagent.tool.invokedskill or your API documentation for the complete list of available events.interactor-webhooks
See
interactor-webhooks skill for webhook setup and SSE streaming.
Best Practices
DO
- Keep instructions focused - Clear, specific instructions produce better results
- Use semantic mappings - Help assistants understand your data schema
- Secure tool callbacks - Always verify signatures on tool callbacks
- Use read-only database users - Limit data source connections to read-only access
- Monitor tool usage - Track which tools are being called and their success rates
- Test conversations - Verify assistant behavior before deploying to users
- Use metadata - Pass user context (name, plan, etc.) to personalize responses
DON'T
- Don't expose sensitive data - Be careful what tools can access
- Don't use write access - Keep data sources read-only
- Don't skip signature verification - Always verify tool callback signatures
- Don't overload with tools - Only enable tools the assistant actually needs
- Don't use vague instructions - Specific instructions produce better results
Error Handling
Agent-Specific Errors
Common error patterns you may encounter:
| Error Code | HTTP Status | Description | Resolution |
|---|---|---|---|
| 404 | Assistant doesn't exist | Check assistant ID |
| 404 | Room doesn't exist | Check room ID |
| 400 | Room is already closed | Create a new room |
| 404 | Tool doesn't exist | Check tool ID |
| 500 | Tool callback returned error | Check your callback endpoint |
| 400 | Database connection failed | Check connection settings |
| 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).
Rate Limiting
The AI Agents API uses rate limiting to ensure fair usage and system stability.
Rate Limit Headers
Every API response includes these headers:
| Header | Type | Description |
|---|---|---|
| integer | Max requests allowed in window |
| integer | Requests remaining in current window |
| integer | Unix timestamp (UTC) when window resets |
| 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
Default Rate Limits
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
Rate Limit Response (HTTP 429)
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 } }
Handling Rate Limits
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(); }
Troubleshooting
Common Issues
Tool Callback Not Receiving Requests
Symptoms: Assistant says it's calling a tool, but your callback endpoint never receives the request.
Checklist:
- ✓ Verify
is publicly accessible (not localhost)callback_url - ✓ Check firewall/security group allows inbound HTTPS
- ✓ Ensure endpoint returns 200 status within 30 seconds
- ✓ Verify SSL certificate is valid (no self-signed in production)
Debug: Check Interactor dashboard for tool execution logs.
Tool Callback Signature Verification Failing
Symptoms: All tool callbacks return 401 Unauthorized.
Checklist:
- ✓ Verify
matches what you registeredcallback_secret - ✓ Ensure you're reading raw request body (not parsed JSON)
- ✓ Check signature header name:
X-Interactor-Signature - ✓ Verify HMAC algorithm is SHA256
Debug:
// 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')}`);
Data Source Connection Failed
Symptoms: Data source status shows "disconnected" or "error".
Checklist:
- ✓ Verify database host is reachable from Interactor's servers
- ✓ Check credentials are correct
- ✓ Ensure database user has SELECT permissions
- ✓ Verify SSL/TLS settings match your database configuration
- ✓ Check if IP allowlisting is required
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;
Assistant Not Using Tools
Symptoms: Assistant responds without using available tools when it should.
Checklist:
- ✓ Verify tool is in
array for the assistantenabled_tools - ✓ Check tool description is clear about when to use it
- ✓ Ensure instructions mention when to use tools
- ✓ Verify tool parameters schema is valid JSON Schema
Fix: Update assistant instructions to be explicit:
"When users ask about products, ALWAYS use the search_products tool to find current information."
Messages Not Appearing in Room
Symptoms: Sent messages don't show up when listing messages.
Checklist:
- ✓ Verify room status is "active" (not "closed")
- ✓ Check message was acknowledged (201 response)
- ✓ Ensure you're querying the correct room ID
- ✓ Wait for async processing (use streaming for real-time)
High Latency on Responses
Symptoms: Assistant takes a long time to respond.
Possible Causes:
- Tool callbacks taking too long → Optimize your callback endpoints
- Large context window → Close old rooms, start fresh conversations
- Complex instructions → Simplify system prompt
- Data source queries slow → Add indexes, optimize queries
Monitor:
const start = Date.now(); await sendMessage(roomId, content); console.log(`Message sent in ${Date.now() - start}ms`);
Security & Compliance
Data Retention
| 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.
Data Deletion (GDPR/CCPA)
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.
Secret Rotation
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 }'
PII Handling
Do NOT include PII in:
- Tool names or descriptions
- Assistant names
- Log messages
DO use:
- Namespaces for user identification (e.g.,
)user_123 - Encrypted metadata for sensitive context
- Room metadata (encrypted at rest) for PII when necessary
// Bad - PII in tool parameters { "user_email": "john@example.com" } // Good - Use references { "user_id": "user_123" } // Look up email in your backend
Observability & Correlation IDs
Request Correlation
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 }; }
Logging Recommendations
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; } }
Metrics to Track
| Metric | Description | Alert Threshold |
|---|---|---|
| Messages sent to assistants | N/A (volume) |
| Time to first token | > 5000ms |
| Tool invocations | N/A (volume) |
| Failed tool callbacks | > 5% error rate |
| Tool callback duration | > 10000ms |
| Currently active rooms | Depends on scale |
| 429 responses received | > 10/min |
Dashboard Integration
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 }); } });
Message Limits & Attachments
Message Size Limits
| 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.
Handling Large Content
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.` ); }
Attachment Support
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):
- Documents: PDF, DOCX, TXT, MD
- Images: PNG, JPG, GIF, WEBP
- Data: CSV, JSON, XML
Max file size: 10 MB per file, 25 MB total per message.
API Reference & Testing
OpenAPI Specification
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
Testing Your Integration
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'); }); });
Output Format
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
Related Skills
- interactor-auth: Setup authentication (prerequisite)
- interactor-sdk: TypeScript/JavaScript SDK for API integration
- interactor-credentials: Agents can use credentials to access external services
- interactor-workflows: Combine AI agents with automated workflows
- interactor-webhooks: Real-time message streaming and event subscriptions