> ## Documentation Index
> Fetch the complete documentation index at: https://docs.erdo.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Client Reference

> API reference for @erdoai/server

# Client Reference

The `@erdoai/server` package provides the `ErdoClient` class for invoking Erdo agents from server-side code.

## Installation

```bash theme={null}
npm install @erdoai/server
```

## ErdoClient

### Constructor

```typescript theme={null}
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.

```typescript theme={null}
const { token, tokenId, expiresAt } = await client.createToken(params: CreateTokenParams): Promise<TokenResponse>;
```

**CreateTokenParams:**

```typescript theme={null}
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)
}
```

<Note>
  **How External Users Work**

  Every 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.
</Note>

**TokenResponse:**

```typescript theme={null}
interface TokenResponse {
  tokenId: string;    // Token ID (for revocation)
  token: string;      // The scoped token
  expiresAt: string;  // ISO timestamp when token expires
}
```

**Example:**

```typescript theme={null}
// 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.

<Warning>
  **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).
</Warning>

```typescript theme={null}
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:**

```typescript theme={null}
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:**

```typescript theme={null}
interface InvokeResult {
  success: boolean;
  botId?: string;
  invocationId?: string;
  result?: {
    status?: string;
    output?: {
      content?: ContentItem[];
    };
  };
  messages: MessageContent[];
  events: SSEEvent[];
  steps: StepInfo[];
  error?: string;
}
```

**Example:**

```typescript theme={null}
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.

<Warning>
  **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).
</Warning>

```typescript theme={null}
const stream = client.invokeStream(botKey: string, params: InvokeParams): AsyncGenerator<SSEEvent>;
```

**SSEEvent:**

```typescript theme={null}
interface SSEEvent {
  type?: 'content' | 'status' | 'error' | 'done' | string;
  payload?: any;
  metadata?: {
    user_visibility?: 'visible' | 'hidden';
    content_type?: string;
    ui_content_type?: string;
  };
}
```

**Example:**

```typescript theme={null}
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.

<Tip>
  For persistent user threads (where users can return to their conversations), use `externalUserId` when creating tokens. This ensures the same user identity across sessions.
</Tip>

### createThread()

Create a new thread for the authenticated user.

```typescript theme={null}
const thread = await client.createThread(params?: CreateThreadParams): Promise<Thread>;
```

**CreateThreadParams:**

```typescript theme={null}
interface CreateThreadParams {
  name?: string;           // Optional thread name
  datasetIds?: string[];   // Dataset IDs to associate with the thread
}
```

**Thread:**

```typescript theme={null}
interface Thread {
  id: string;
  name: string;
  createdAt: string;
  updatedAt: string;
}
```

**Example:**

```typescript theme={null}
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:

```typescript theme={null}
const { threads } = await client.listThreads(params?: ListThreadsParams): Promise<ListThreadsResponse>;
```

**ListThreadsParams:**

```typescript theme={null}
interface ListThreadsParams {
  externalUserId?: string;  // Filter by external user (API key only)
}
```

<Tabs>
  <Tab title="Scoped Token">
    When using a scoped token, returns threads owned by the token's user.
    The `externalUserId` parameter is ignored (the token already identifies the user).

    ```typescript theme={null}
    // Client authenticated with scoped token
    const client = new ErdoClient({ token: scopedToken });

    // Returns only this user's threads
    const { threads } = await client.listThreads();
    ```
  </Tab>

  <Tab title="API Key">
    When using an API key, you can either list your own threads or filter by a specific external user.

    ```typescript theme={null}
    // 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'
    });
    ```

    <Warning>
      **Security: Never accept `externalUserId` from client requests**

      When 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.

      ```typescript theme={null}
      // 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 });
      ```
    </Warning>
  </Tab>
</Tabs>

### getThread()

Get a specific thread by ID.

```typescript theme={null}
const thread = await client.getThread(threadId: string): Promise<Thread>;
```

**Example:**

```typescript theme={null}
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.

```typescript theme={null}
const { messages } = await client.getThreadMessages(threadId: string): Promise<ListThreadMessagesResponse>;
```

**ListThreadMessagesResponse:**

```typescript theme={null}
interface ListThreadMessagesResponse {
  messages: ThreadMessage[];
}

interface ThreadMessage {
  id: string;
  role: 'user' | 'assistant';
  contents: ContentItem[];
  createdAt: string;
  updatedAt: string;
}
```

**Example:**

```typescript theme={null}
// 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}`);
  }
}
```

<Tip>
  Use `getThreadMessages()` to build a threads sidebar where users can click on previous conversations and see the full message history with all visualizations.
</Tip>

### sendMessage()

Send a message to a thread and stream the bot response.

```typescript theme={null}
const stream = client.sendMessage(threadId: string, params: SendMessageParams): AsyncGenerator<SSEEvent>;
```

**SendMessageParams:**

```typescript theme={null}
interface SendMessageParams {
  content: string;    // The message content
  botKey?: string;    // Optional bot to use (uses thread's default if not specified)
}
```

**Example:**

```typescript theme={null}
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).

```typescript theme={null}
const events = await client.sendMessageAndWait(threadId: string, params: SendMessageParams): Promise<SSEEvent[]>;
```

**Example:**

```typescript theme={null}
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:**

```typescript theme={null}
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

```typescript theme={null}
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

```typescript theme={null}
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

```typescript theme={null}
// 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 });
}
```

<Info>
  **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.
</Info>

### Client-side: Use Token for Threads

```typescript theme={null}
// 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();
```
