Client Reference
The @erdoai/server package provides the ErdoClient class for invoking Erdo agents from server-side code.
Installation
npm install @erdoai/server
ErdoClient
Constructor
import { ErdoClient } from '@erdoai/server';
// Server-side: Use authToken (API key)
const serverClient = new ErdoClient({
endpoint?: string; // API endpoint (default: ERDO_ENDPOINT env or https://api.erdo.ai)
authToken?: string; // API key (default: ERDO_AUTH_TOKEN env)
});
// Client-side: Use scoped token (created via createToken)
const clientClient = new ErdoClient({
endpoint: 'https://api.erdo.ai',
token: scopedToken, // Scoped token from createToken()
});
You must provide either authToken or token. Use authToken for server-side code with full API access, and token for client-side code with limited scope.
createToken()
Create a scoped token for client-side use. Requires authToken (API key) authentication.
const { token, tokenId, expiresAt } = await client.createToken(params: CreateTokenParams): Promise<TokenResponse>;
CreateTokenParams:
interface CreateTokenParams {
botKeys?: string[]; // Bot keys to grant access (e.g., ["my-org.data-analyst"])
datasetIds?: string[]; // Dataset IDs the token can access
threadIds?: string[]; // Thread IDs the token can access (for pre-existing threads)
externalUserId?: string; // Your user identifier (optional, for user reuse across tokens)
expiresInSeconds?: number; // Token lifetime (default: 3600, max: 86400)
}
How External Users WorkEvery scoped token is linked to an external user - a real user identity in Erdo’s system. This enables proper RBAC and resource ownership.
-
With
externalUserId: If you provide the same externalUserId across multiple tokens, they all authenticate as the same user. This is useful when your users need persistent access to their threads and resources.
-
Without
externalUserId: A new user is created for each token. Use this for one-off or anonymous interactions where user persistence isn’t needed.
The externalUserId is your own user identifier (e.g., your database user ID). It’s only used for matching - Erdo maintains its own internal user IDs.
TokenResponse:
interface TokenResponse {
tokenId: string; // Token ID (for revocation)
token: string; // The scoped token
expiresAt: string; // ISO timestamp when token expires
}
Example:
// Server-side: Create a token for the frontend
const serverClient = new ErdoClient({
authToken: process.env.ERDO_AUTH_TOKEN,
});
const { token, tokenId, expiresAt } = await serverClient.createToken({
botKeys: ['my-org.data-analyst'], // Bot keys
externalUserId: 'user_123', // Your user's ID
expiresInSeconds: 3600,
});
// Pass token to frontend for client-side use
invoke()
Invoke an agent and wait for the complete result.
Server-only: This method uses /bots/{key}/invoke which returns raw SSE events without message wrapping. For React UI rendering with the Content component, use thread-based messaging (sendMessage() or the useThread hook).
const result = await client.invoke(botKey: string, params: InvokeParams): Promise<InvokeResult>;
Parameters:
| Parameter | Type | Description |
|---|
botKey | string | The agent identifier (e.g., 'data-analyst') |
params | InvokeParams | Invocation parameters |
InvokeParams:
interface InvokeParams {
messages?: Message[]; // Messages to send to the agent
parameters?: Record<string, any>; // Additional parameters
datasets?: string[]; // Dataset slugs to include
mode?: InvocationMode; // 'live' | 'replay' | 'manual'
}
interface Message {
role: 'user' | 'assistant' | 'system';
content: string;
}
InvokeResult:
interface InvokeResult {
success: boolean;
botId?: string;
invocationId?: string;
result?: {
status?: string;
output?: {
content?: ContentItem[];
};
};
messages: MessageContent[];
events: SSEEvent[];
steps: StepInfo[];
error?: string;
}
Example:
const result = await client.invoke('data-analyst', {
messages: [
{ role: 'user', content: 'What were our top 10 products by revenue?' }
],
datasets: ['sales-data'],
});
if (result.success) {
console.log('Status:', result.result?.status);
console.log('Content:', result.result?.output?.content);
}
invokeStream()
Invoke an agent and stream results as they arrive.
Server-only: This method uses /bots/{key}/invoke which returns raw SSE events without message wrapping. For React UI rendering with the Content component, use thread-based messaging (sendMessage() or the useThread hook).
const stream = client.invokeStream(botKey: string, params: InvokeParams): AsyncGenerator<SSEEvent>;
SSEEvent:
interface SSEEvent {
type?: 'content' | 'status' | 'error' | 'done' | string;
payload?: any;
metadata?: {
user_visibility?: 'visible' | 'hidden';
content_type?: string;
ui_content_type?: string;
};
}
Example:
const events: SSEEvent[] = [];
for await (const event of client.invokeStream('data-analyst', {
messages: [{ role: 'user', content: 'Analyze trends in our data' }],
})) {
events.push(event);
switch (event.type) {
case 'content':
// New content item (chart, text, etc.)
console.log('Content:', event.payload);
break;
case 'status':
// Status update (step started, completed, etc.)
console.log('Status:', event.payload);
break;
case 'error':
console.error('Error:', event.payload);
break;
case 'done':
console.log('Stream complete');
break;
}
}
Thread Methods
Thread methods work with any scoped token. Each token is linked to an external user, and threads created are owned by that user.
For persistent user threads (where users can return to their conversations), use externalUserId when creating tokens. This ensures the same user identity across sessions.
createThread()
Create a new thread for the authenticated user.
const thread = await client.createThread(params?: CreateThreadParams): Promise<Thread>;
CreateThreadParams:
interface CreateThreadParams {
name?: string; // Optional thread name
datasetIds?: string[]; // Dataset IDs to associate with the thread
}
Thread:
interface Thread {
id: string;
name: string;
createdAt: string;
updatedAt: string;
}
Example:
const client = new ErdoClient({
endpoint: 'https://api.erdo.ai',
token: scopedToken, // Token with externalUserId
});
const thread = await client.createThread({ name: 'Support Chat' });
console.log('Created thread:', thread.id);
listThreads()
List threads. The behavior depends on your authentication method:
const { threads } = await client.listThreads(params?: ListThreadsParams): Promise<ListThreadsResponse>;
ListThreadsParams:
interface ListThreadsParams {
externalUserId?: string; // Filter by external user (API key only)
}
When using a scoped token, returns threads owned by the token’s user.
The externalUserId parameter is ignored (the token already identifies the user).// Client authenticated with scoped token
const client = new ErdoClient({ token: scopedToken });
// Returns only this user's threads
const { threads } = await client.listThreads();
When using an API key, you can either list your own threads or filter by a specific external user.// Client authenticated with API key
const client = new ErdoClient({ authToken: apiKey });
// List your own threads
const { threads } = await client.listThreads();
// Filter to a specific external user's threads
const { threads } = await client.listThreads({
externalUserId: 'user_123'
});
Security: Never accept externalUserId from client requestsWhen using the proxy pattern with API key auth, you must get externalUserId from your own authentication system (session, JWT, etc.). Never trust client-provided user IDs—this would allow users to access other users’ threads.// CORRECT: Get user from YOUR auth system
const session = await getServerSession(authOptions);
const { threads } = await client.listThreads({
externalUserId: session.user.id // From your auth, NOT from request
});
// WRONG: Never do this!
const { externalUserId } = await request.json(); // Attacker-controlled!
const { threads } = await client.listThreads({ externalUserId });
getThread()
Get a specific thread by ID.
const thread = await client.getThread(threadId: string): Promise<Thread>;
Example:
const thread = await client.getThread('thread_abc123');
console.log(thread.name, thread.updatedAt);
getThreadMessages()
Get all messages in a thread. Useful for loading conversation history when a user returns to a previous thread.
const { messages } = await client.getThreadMessages(threadId: string): Promise<ListThreadMessagesResponse>;
ListThreadMessagesResponse:
interface ListThreadMessagesResponse {
messages: ThreadMessage[];
}
interface ThreadMessage {
id: string;
role: 'user' | 'assistant';
contents: ContentItem[];
createdAt: string;
updatedAt: string;
}
Example:
// Load conversation history for a thread
const { messages } = await client.getThreadMessages(threadId);
for (const message of messages) {
console.log(`${message.role}: ${message.contents.length} content items`);
// Render content items (charts, tables, text, etc.)
for (const content of message.contents) {
console.log(` - ${content.content_type}`);
}
}
Use getThreadMessages() to build a threads sidebar where users can click on previous conversations and see the full message history with all visualizations.
sendMessage()
Send a message to a thread and stream the bot response.
const stream = client.sendMessage(threadId: string, params: SendMessageParams): AsyncGenerator<SSEEvent>;
SendMessageParams:
interface SendMessageParams {
content: string; // The message content
botKey?: string; // Optional bot to use (uses thread's default if not specified)
}
Example:
for await (const event of client.sendMessage(threadId, {
content: 'What insights can you find in my data?',
botKey: 'my-org.data-analyst',
})) {
switch (event.type) {
case 'content':
console.log('Content:', event.payload);
break;
case 'status':
console.log('Status:', event.payload);
break;
case 'done':
console.log('Stream complete');
break;
}
}
sendMessageAndWait()
Send a message and wait for the complete response (non-streaming).
const events = await client.sendMessageAndWait(threadId: string, params: SendMessageParams): Promise<SSEEvent[]>;
Example:
const events = await client.sendMessageAndWait(threadId, {
content: 'Summarize recent trends',
});
console.log('Received', events.length, 'events');
Content Types
Agents can return various content types:
| Type | Description |
|---|
text | Plain text response |
json | Structured JSON data |
markdown | Formatted markdown text |
chart | Chart configuration with data |
table | Tabular data |
code | Code snippets |
ContentItem:
interface ContentItem {
content_type: 'text' | 'json' | 'code' | 'table' | 'image' | 'error';
ui_content_type?: 'chart' | 'bar_chart' | 'line_chart' | 'pie_chart' | 'table' | 'markdown';
content?: string;
data?: any;
}
Error Handling
try {
const result = await client.invoke('data-analyst', {
messages: [{ role: 'user', content: 'Analyze data' }],
});
if (!result.success) {
console.error('Invocation failed:', result.error);
}
} catch (error) {
// Network errors, auth errors, etc.
console.error('Request failed:', error);
}
Node.js Example
import { ErdoClient } from '@erdoai/server';
async function main() {
const client = new ErdoClient({
authToken: process.env.ERDO_AUTH_TOKEN,
});
console.log('Invoking agent...');
for await (const event of client.invokeStream('data-analyst', {
messages: [{ role: 'user', content: 'What insights can you find?' }],
})) {
if (event.type === 'content') {
console.log('Received:', event.payload?.content_type);
}
}
}
main();
B2B Integration Example
For B2B applications where your customers’ users need to interact with Erdo agents, use scoped tokens to provide secure, limited access.
Server-side: Create Token for User
// api/authorize/route.ts (Next.js API route)
import { ErdoClient } from '@erdoai/server';
export async function POST(request: Request) {
const { userId, botId } = await request.json();
// Your own authorization logic here
// e.g., check if userId belongs to authenticated session
const serverClient = new ErdoClient({
authToken: process.env.ERDO_AUTH_TOKEN,
});
const { token, tokenId, expiresAt } = await serverClient.createToken({
botKeys: [botKey], // Bot keys
externalUserId: userId, // Your user ID - enables persistent threads
expiresInSeconds: 3600,
});
return Response.json({ token, tokenId, expiresAt });
}
Why use externalUserId?When you pass your user’s ID as externalUserId, Erdo creates a persistent user identity. This means:
- The same user can have multiple tokens over time (e.g., after token expiry)
- All tokens with the same
externalUserId see the same threads and resources
- Perfect for apps where users need to return to their conversation history
If you omit externalUserId, each token creates a new isolated user - useful for anonymous or one-time interactions.
Client-side: Use Token for Threads
// Get token from your server
const { token } = await fetch('/api/authorize', {
method: 'POST',
body: JSON.stringify({ userId: currentUser.id }),
}).then(r => r.json());
// Create client with scoped token
const client = new ErdoClient({
endpoint: 'https://api.erdo.ai',
token,
});
// Create a thread for the user
const thread = await client.createThread({ name: 'Data Analysis' });
// Send messages and stream responses
for await (const event of client.sendMessage(thread.id, {
content: 'What were our top products last quarter?',
botKey: 'my-org.data-analyst',
})) {
if (event.type === 'content') {
// Render content to UI
console.log(event.payload);
}
}
// Later: List user's threads
const { threads } = await client.listThreads();