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

# Text Chat API

> Embed a text-based AI chat widget on any website using TalkifAI's REST API. No LiveKit or WebRTC required.

## Overview

The **Text Chat API** lets you embed a TalkifAI agent as a **text chat interface** on any website or application. Unlike voice sessions (which use LiveKit), the Chat API uses standard REST + Server-Sent Events (SSE) streaming.

**Best for:**

* Website chatbots and live chat widgets
* Mobile app chat interfaces
* Customer support portals
* Any text-first interaction (no microphone needed)

<Info>
  **Text agents only.** The Chat API requires agents with **text** architecture. Voice agents (Pipeline/Realtime) use the LiveKit-based voice flow instead.
</Info>

***

## How It Works

```
Your Website                         TalkifAI API
────────────                         ────────────

POST /v1/chat/sessions               1. Validate API key
  X-API-Key: tk_live_xxx    ──────→  2. Load agent config
  { agent_id: "..." }                3. Check credits (402 if insufficient)
                                     4. Create ChatAgent + load memory
                                     5. Start billing session
                            ←──────  6. Return { session_token, conversation_id, greeting }

POST /v1/chat/sessions/{id}/messages 1. Verify JWT token
  Authorization: Bearer jwt ──────→  2. Acquire session lock
  { message: "Hello" }               3. Process through LLM (streaming)
                                     4. Execute tools if needed
                            ←──────  5. SSE stream of response chunks

POST /v1/chat/sessions/{id}/end      1. End billing session + consume credits
  Authorization: Bearer jwt ──────→  2. Ingest into memory (Graphiti)
                                     3. Trigger post-call analysis
                                     4. Fire webhooks if configured
```

***

## Prerequisites

### 1. Create a Text Agent

1. Go to **Studio → Agents → Create Agent**
2. Under **Architecture**, select **Chat (Text Only)**
3. Configure your system prompt and LLM model
4. Save the agent and copy the **Agent ID**

### 2. Get Your API Key

1. Go to **Studio → Settings → API Keys**
2. Click **Create API Key**
3. Copy the key — it starts with `tk_live_`

<Warning>
  Keep your API key secret. Never expose it in frontend JavaScript. Use a backend server to make session creation requests.
</Warning>

***

## Step 1: Create a Session

Call this endpoint from your **backend server** to create a chat session:

```bash theme={null}
POST https://api.talkifai.dev/v1/chat/sessions
X-API-Key: tk_live_your_key_here
# OR (for TalkifAI Studio internal use)
# X-Studio-Token: better_auth_session_token
Content-Type: application/json

{
  "agent_id": "5b710eca-ee67-4c3a-aeb6-8b541f451b40",
  "user_identifier": "customer@example.com",
  "metadata": {
    "source": "website_chat",
    "page_url": "https://example.com/support"
  }
}
```

**Response:**

```json theme={null}
{
  "conversation_id": "chat_5b710eca_user123_1705312800000",
  "session_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "agent_name": "Support Agent",
  "greeting": "Hello! How can I help you today?",
  "agent_model": "gpt-4o-mini"
}
```

| Field             | Description                                                            |
| ----------------- | ---------------------------------------------------------------------- |
| `conversation_id` | Unique ID for this chat session                                        |
| `session_token`   | JWT token for authenticating messages (24h expiry)                     |
| `agent_name`      | Name of the agent                                                      |
| `greeting`        | Agent's opening message. `null` if agent waits for user to speak first |
| `agent_model`     | LLM model used by the agent                                            |

### Request Fields

| Field             | Type   | Required | Description                                                      |
| ----------------- | ------ | -------- | ---------------------------------------------------------------- |
| `agent_id`        | string | ✅        | Agent ID (UUID format)                                           |
| `user_identifier` | string | ❌        | **Important:** End-user email for memory linking across sessions |
| `metadata`        | object | ❌        | Optional metadata for tracking                                   |

<Warning>
  **Always provide `user_identifier`** (customer email) for returning user memory. Without it, every session is isolated and memory cannot be retrieved later.
</Warning>

### Authentication Modes

**Mode 1: API Key (External Websites)**

* Use `X-API-Key` header
* For customer-facing chat widgets
* API key stays on your backend (never expose to browser)

**Mode 2: Studio Token (Internal)**

* Use `X-Studio-Token` header
* For TalkifAI Studio agent preview/testing
* Token from Better Auth session

**Time:** \~300–500ms (agent initialization + memory loading)

***

## Step 2: Send a Message

Send messages from the **client browser** using the `session_token`:

```javascript theme={null}
const response = await fetch(
  `https://api.talkifai.dev/v1/chat/sessions/${conversationId}/messages`,
  {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${sessionToken}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ message: 'What services do you offer?' })
  }
);
```

### Parsing the SSE Stream

The response is a **Server-Sent Events (SSE) stream**. Each event has an `event:` type line and a `data:` line, separated by blank lines (`\n\n`). You must parse both to correctly handle all event types:

```javascript theme={null}
setIsLoading(true); // show loading state before fetch

const reader = response.body.getReader();
const decoder = new TextDecoder();
let fullResponse = '';
let buffer = '';

try {
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });

    // SSE events are separated by blank lines
    const blocks = buffer.split('\n\n');
    buffer = blocks.pop(); // keep any incomplete trailing block

    for (const block of blocks) {
      let eventType = '';
      let dataStr = '';

      for (const line of block.split('\n')) {
        if (line.startsWith('event: ')) eventType = line.slice(7).trim();
        else if (line.startsWith('data: ')) dataStr = line.slice(6);
      }

      if (!dataStr) continue;
      const data = JSON.parse(dataStr);

      if (eventType === 'stream_start') {
        // Agent started streaming — loading indicator already shown
      } else if (eventType === 'chunk') {
        fullResponse += data.delta;
        // Update UI with streaming text
        chatBubble.textContent = fullResponse;
      } else if (eventType === 'tool_call') {
        // Optionally show "Searching..." / "Looking that up..." indicator
        console.log('Tool executing:', data.name);
      } else if (eventType === 'tool_result') {
        console.log('Tool completed:', data.name);
      } else if (eventType === 'handoff') {
        // Agent handed off to a subagent: { from_agent, to_agent }
        console.log(`Handing off to ${data.to_agent}`);
      } else if (eventType === 'stream_end') {
        setIsLoading(false); // clear loading state
        console.log('Tokens used:', data.usage);
        if (data.end_session) {
          // Agent called end_chat tool — close the session
          await endSession();
        }
      } else if (eventType === 'error') {
        setIsLoading(false); // clear loading state so UI doesn't hang
        setErrorMessage(data.error || 'An error occurred. Please try again.');
        chatBubble.textContent = 'Something went wrong. Please try again.';
      }
    }
  }
} catch (err) {
  setIsLoading(false);
  setErrorMessage('Connection lost. Please try again.');
}
```

**SSE Event Types:**

| Event          | Data                                                                         | Description                                                                        |
| -------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `stream_start` | `{ "agent_name": "Support Agent" }`                                          | Agent started generating a response — show loading indicator                       |
| `chunk`        | `{ "delta": "Hello! " }`                                                     | Incremental text — append to the chat bubble                                       |
| `tool_call`    | `{ "name": "web_search", "status": "executing" }`                            | Agent called a tool — optionally show "Looking that up..."                         |
| `tool_result`  | `{ "name": "web_search", "status": "completed" }`                            | Tool finished — agent will continue responding                                     |
| `handoff`      | `{ "from_agent": "Main Bot", "to_agent": "Billing Agent" }`                  | Multi-agent handoff — a subagent is taking over the conversation                   |
| `stream_end`   | `{ "usage": {"input_tokens": N, "output_tokens": N}, "end_session": false }` | Response complete — clear loading. If `end_session: true`, call `/end` immediately |
| `error`        | `{ "error": "message" }`                                                     | Error mid-stream — **clear loading state and show error to user**                  |

***

## Step 3: Get Message History

Retrieve conversation history (useful for page refreshes or resuming sessions):

```bash theme={null}
GET https://api.talkifai.dev/v1/chat/sessions/{conversation_id}/messages
Authorization: Bearer {session_token}

Query Parameters:
- limit: Number of messages to return (default: 50)
- offset: Pagination offset (default: 0)
```

**Response:**

```json theme={null}
{
  "messages": [
    {
      "role": "assistant",
      "content": "Hello! How can I help you today?",
      "timestamp": "2024-01-15T10:00:00+00:00",
      "token_count": null
    },
    {
      "role": "user",
      "content": "What services do you offer?",
      "timestamp": "2024-01-15T10:00:05+00:00",
      "token_count": null
    },
    {
      "role": "assistant",
      "content": "We offer...",
      "timestamp": "2024-01-15T10:00:06+00:00",
      "token_count": 45
    }
  ],
  "count": 3
}
```

***

## Step 4: End the Session

Always end sessions explicitly to trigger cleanup, memory ingestion, and webhooks:

```bash theme={null}
POST https://api.talkifai.dev/v1/chat/sessions/{conversation_id}/end
Authorization: Bearer {session_token}
```

**What happens on end:**

1. Billing session closed and credits consumed
2. Memory ingested into Graphiti (for returning user context)
3. Post-call analysis triggered (if configured)
4. Webhook fired (if agent has webhook configured)

**Response:**

```json theme={null}
{
  "success": true,
  "message": "Session ended successfully"
}
```

<Note>
  Sessions automatically expire after **30 minutes of inactivity**. Always call `/end` explicitly to ensure billing finalization, memory ingestion, and webhooks fire.
</Note>

***

## Complete Integration Example

Here's a full working example with a simple chat UI:

```html theme={null}
<!DOCTYPE html>
<html>
<head>
  <title>TalkifAI Chat</title>
  <style>
    #chat { max-width: 600px; margin: 50px auto; font-family: sans-serif; }
    #messages { height: 400px; overflow-y: auto; border: 1px solid #ddd; padding: 15px; border-radius: 8px; }
    .message { margin: 10px 0; }
    .user { text-align: right; color: #6366F1; }
    .assistant { text-align: left; color: #111; }
    #input-area { display: flex; gap: 10px; margin-top: 10px; }
    #input-area input { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 6px; }
    #input-area button { padding: 10px 20px; background: #6366F1; color: white; border: none; border-radius: 6px; cursor: pointer; }
  </style>
</head>
<body>
  <div id="chat">
    <div id="messages"></div>
    <div id="input-area">
      <input type="text" id="user-input" placeholder="Type a message..." />
      <button onclick="sendMessage()">Send</button>
    </div>
  </div>

  <script>
    // State
    let sessionToken = null;
    let conversationId = null;

    // Initialize: Get session from YOUR BACKEND
    async function init() {
      // This calls your backend, which calls TalkifAI API with your secret key
      const res = await fetch('/api/chat/start', { method: 'POST' });
      const data = await res.json();

      sessionToken = data.session_token;
      conversationId = data.conversation_id;

      // Show greeting if agent speaks first
      if (data.greeting) {
        addMessage('assistant', data.greeting);
      }
    }

    // Send a message
    async function sendMessage() {
      const input = document.getElementById('user-input');
      const message = input.value.trim();
      if (!message || !sessionToken) return;

      input.value = '';
      addMessage('user', message);

      // Create assistant bubble for streaming
      const bubble = addMessage('assistant', '');

      const response = await fetch(
        `https://api.talkifai.dev/v1/chat/sessions/${conversationId}/messages`,
        {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${sessionToken}`,
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({ message })
        }
      );

      // Parse SSE stream — must track event type from `event:` line
      const reader = response.body.getReader();
      const decoder = new TextDecoder();
      let fullText = '';
      let buffer = '';

      while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        buffer += decoder.decode(value, { stream: true });

        const blocks = buffer.split('\n\n');
        buffer = blocks.pop();

        for (const block of blocks) {
          let eventType = '';
          let dataStr = '';

          for (const line of block.split('\n')) {
            if (line.startsWith('event: ')) eventType = line.slice(7).trim();
            else if (line.startsWith('data: ')) dataStr = line.slice(6);
          }

          if (!dataStr) continue;
          const data = JSON.parse(dataStr);

          if (eventType === 'chunk') {
            fullText += data.delta;
            bubble.textContent = fullText;
          } else if (eventType === 'stream_end') {
            if (data.end_session) {
              await endSession();
            }
          } else if (eventType === 'error') {
            bubble.textContent = 'An error occurred. Please try again.';
          }
        }
      }
    }

    async function endSession() {
      if (!sessionToken || !conversationId) return;
      await fetch(
        `https://api.talkifai.dev/v1/chat/sessions/${conversationId}/end`,
        {
          method: 'POST',
          headers: { 'Authorization': `Bearer ${sessionToken}` }
        }
      );
      sessionToken = null;
      conversationId = null;
    }

    function addMessage(role, text) {
      const msgs = document.getElementById('messages');
      const div = document.createElement('div');
      div.className = `message ${role}`;
      div.textContent = text;
      msgs.appendChild(div);
      msgs.scrollTop = msgs.scrollHeight;
      return div;
    }

    // Clean up when page closes
    window.addEventListener('beforeunload', () => {
      if (sessionToken && conversationId) {
        fetch(
          `https://api.talkifai.dev/v1/chat/sessions/${conversationId}/end`,
          {
            method: 'POST',
            headers: { 'Authorization': `Bearer ${sessionToken}` },
            keepalive: true
          }
        );
      }
    });

    init();
  </script>
</body>
</html>
```

***

## Backend Proxy Pattern

**Never expose your API key in frontend code.** Use a backend proxy:

<Tabs>
  <Tab title="Node.js / Express">
    ```javascript theme={null}
    // server.js
    app.post('/api/chat/start', async (req, res) => {
      const response = await fetch('https://api.talkifai.dev/v1/chat/sessions', {
        method: 'POST',
        headers: {
          'X-API-Key': process.env.TALKIFAI_API_KEY,  // Server-side env var
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          agent_id: process.env.TALKIFAI_AGENT_ID
        })
      });

      const data = await response.json();
      res.json(data);  // session_token is safe to send to frontend
    });
    ```
  </Tab>

  <Tab title="Next.js API Route">
    ```typescript theme={null}
    // app/api/chat/start/route.ts
    export async function POST() {
      const response = await fetch('https://api.talkifai.dev/v1/chat/sessions', {
        method: 'POST',
        headers: {
          'X-API-Key': process.env.TALKIFAI_API_KEY!,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          agent_id: process.env.TALKIFAI_AGENT_ID
        })
      });

      const data = await response.json();
      return Response.json(data);
    }
    ```
  </Tab>

  <Tab title="Python / FastAPI">
    ```python theme={null}
    # main.py
    @app.post("/api/chat/start")
    async def start_chat():
        async with httpx.AsyncClient() as client:
            response = await client.post(
                "https://api.talkifai.dev/v1/chat/sessions",
                headers={
                    "X-API-Key": os.environ["TALKIFAI_API_KEY"],
                    "Content-Type": "application/json"
                },
                json={"agent_id": os.environ["TALKIFAI_AGENT_ID"]}
            )
        return response.json()
    ```
  </Tab>
</Tabs>

***

## React / Next.js Integration

A minimal React hook that handles session lifecycle, SSE streaming, error display, and reliable cleanup on unmount or tab close:

```tsx theme={null}
import { useState, useEffect, useRef } from 'react';

export function useChatSession(agentId: string) {
  const [messages, setMessages] = useState<{ role: string; text: string }[]>([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const sessionTokenRef = useRef<string | null>(null);
  const conversationIdRef = useRef<string | null>(null);

  // Initialize session on mount
  useEffect(() => {
    async function init() {
      const res = await fetch('/api/chat/start', { method: 'POST' });
      const data = await res.json();
      sessionTokenRef.current = data.session_token;
      conversationIdRef.current = data.conversation_id;
      if (data.greeting) {
        setMessages([{ role: 'assistant', text: data.greeting }]);
      }
    }
    init();

    // End session on unmount or tab close — keepalive ensures the request
    // completes even if the page is being unloaded
    return () => {
      const token = sessionTokenRef.current;
      const convId = conversationIdRef.current;
      if (token && convId) {
        fetch(`https://api.talkifai.dev/v1/chat/sessions/${convId}/end`, {
          method: 'POST',
          headers: { Authorization: `Bearer ${token}` },
          keepalive: true, // survives tab close / component unmount
        });
      }
    };
  }, []);

  async function sendMessage(userText: string) {
    if (!sessionTokenRef.current || !conversationIdRef.current) return;

    setMessages(prev => [...prev, { role: 'user', text: userText }]);
    setIsLoading(true);
    setError(null);

    const response = await fetch(
      `https://api.talkifai.dev/v1/chat/sessions/${conversationIdRef.current}/messages`,
      {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${sessionTokenRef.current}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ message: userText }),
      }
    );

    let fullText = '';
    let buffer = '';
    const reader = response.body!.getReader();
    const decoder = new TextDecoder();

    // Add empty assistant bubble for streaming into
    setMessages(prev => [...prev, { role: 'assistant', text: '' }]);

    try {
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        buffer += decoder.decode(value, { stream: true });
        const blocks = buffer.split('\n\n');
        buffer = blocks.pop()!;

        for (const block of blocks) {
          let eventType = '';
          let dataStr = '';
          for (const line of block.split('\n')) {
            if (line.startsWith('event: ')) eventType = line.slice(7).trim();
            else if (line.startsWith('data: ')) dataStr = line.slice(6);
          }
          if (!dataStr) continue;
          const data = JSON.parse(dataStr);

          if (eventType === 'chunk') {
            fullText += data.delta;
            // Update the last (streaming) bubble in place
            setMessages(prev => [
              ...prev.slice(0, -1),
              { role: 'assistant', text: fullText },
            ]);
          } else if (eventType === 'stream_end') {
            setIsLoading(false);
            if (data.end_session) {
              await endSession();
            }
          } else if (eventType === 'error') {
            // Clear loading and surface the error — don't leave UI hanging
            setIsLoading(false);
            setError(data.error || 'Something went wrong. Please try again.');
            setMessages(prev => [
              ...prev.slice(0, -1),
              { role: 'assistant', text: 'Something went wrong. Please try again.' },
            ]);
          }
        }
      }
    } catch {
      setIsLoading(false);
      setError('Connection lost. Please try again.');
    }
  }

  async function endSession() {
    const token = sessionTokenRef.current;
    const convId = conversationIdRef.current;
    if (!token || !convId) return;
    await fetch(`https://api.talkifai.dev/v1/chat/sessions/${convId}/end`, {
      method: 'POST',
      headers: { Authorization: `Bearer ${token}` },
    });
    sessionTokenRef.current = null;
    conversationIdRef.current = null;
  }

  return { messages, isLoading, error, sendMessage };
}
```

<Note>
  The `keepalive: true` flag in the cleanup `fetch` is critical. Without it, browsers cancel in-flight requests when the page unloads — the `/end` call would never reach the server, leaving billing sessions open and memory ingestion skipped.
</Note>

***

## API Reference

### Create Session

```bash theme={null}
POST /v1/chat/sessions
X-API-Key: {your_api_key}
Content-Type: application/json

{
  "agent_id": "string (required)"
}
```

**Response:**

```json theme={null}
{
  "conversation_id": "chat_...",
  "session_token": "eyJhbGc...",
  "agent_name": "Support Bot",
  "greeting": "Hello! How can I help?",
  "agent_model": "gpt-4o-mini"
}
```

### Send Message

```bash theme={null}
POST /v1/chat/sessions/{conversation_id}/messages
Authorization: Bearer {session_token}
Content-Type: application/json

{
  "message": "string (required, max 10,000 chars)"
}
```

**Response:** SSE stream (`text/event-stream`)

### Get History

```bash theme={null}
GET /v1/chat/sessions/{conversation_id}/messages
Authorization: Bearer {session_token}

Query Parameters:
- limit: number (default: 50)
- offset: number (default: 0)
```

**Response:**

```json theme={null}
{
  "messages": [...],
  "count": 20
}
```

### End Session

```bash theme={null}
POST /v1/chat/sessions/{conversation_id}/end
Authorization: Bearer {session_token}
```

**Response:**

```json theme={null}
{
  "success": true,
  "message": "Session ended successfully"
}
```

***

## Error Codes

| Status | Code                    | Cause                                                               |
| ------ | ----------------------- | ------------------------------------------------------------------- |
| 400    | `not_text_agent`        | Agent is voice-only (Pipeline/Realtime)                             |
| 400    | `message_too_long`      | Message exceeds 10,000 characters                                   |
| 401    | `invalid_api_key`       | API key missing or invalid                                          |
| 401    | `invalid_token`         | Session JWT expired or invalid                                      |
| 402    | `insufficient_credits`  | Organization has no credits remaining                               |
| 403    | `conversation_mismatch` | `conversation_id` in URL doesn't match the session token            |
| 404    | `session_not_found`     | Session expired (idle >30 min) or wrong ID                          |
| 503    | —                       | Billing service temporarily unavailable — retry after a short delay |

***

## Session Management

### Session Lock

The API automatically acquires a **session lock** when processing messages. This prevents:

* Concurrent message processing
* Race conditions in message history
* Duplicate responses

If a previous message is still streaming, the new request will wait for the lock.

### Token Tracking

The API automatically tracks:

* Input tokens (user messages)
* Output tokens (assistant responses)
* Total token usage per session

Token counts are returned in the `stream_end` event's `usage` field and stored in the database.

### Memory Ingestion

When a session ends:

1. Credits consumed based on session cost
2. Conversation saved to `ChatMessages` table
3. Memory ingested into Graphiti (if enabled on agent)
4. Post-call analysis triggered (if configured)
5. Webhook fired (if agent has webhook URL)

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Create a Text Agent" icon="robot">
    Set up an agent with text architecture in Studio.
  </Card>

  <Card title="API Keys" icon="key">
    Manage your API keys and rate limits.
  </Card>

  <Card title="Conversation Memory" icon="brain">
    Enable long-term memory so agents remember returning users.
  </Card>

  <Card title="Webhooks" icon="webhook">
    Get notified when conversations end or tools are called.
  </Card>
</CardGroup>

***

## Related Documentation

* [Custom Functions](/guides/custom-functions) — Add API integrations to your chat agent
* [Conversation Memory](/platform/memory) — Enable long-term user memory
* [API Reference](/api-reference/chat) — Full Chat API specification
