Skip to main content

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)
Text agents only. The Chat API requires agents with text architecture. Voice agents (Pipeline/Realtime) use the LiveKit-based voice flow instead.

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_
Keep your API key secret. Never expose it in frontend JavaScript. Use a backend server to make session creation requests.

Step 1: Create a Session

Call this endpoint from your backend server to create a chat session:
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:
{
  "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"
}
FieldDescription
conversation_idUnique ID for this chat session
session_tokenJWT token for authenticating messages (24h expiry)
agent_nameName of the agent
greetingAgent’s opening message. null if agent waits for user to speak first
agent_modelLLM model used by the agent

Request Fields

FieldTypeRequiredDescription
agent_idstringAgent ID (UUID format)
user_identifierstringImportant: End-user email for memory linking across sessions
metadataobjectOptional metadata for tracking
Always provide user_identifier (customer email) for returning user memory. Without it, every session is isolated and memory cannot be retrieved later.

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:
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:
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:
EventDataDescription
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):
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:
{
  "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:
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:
{
  "success": true,
  "message": "Session ended successfully"
}
Sessions automatically expire after 30 minutes of inactivity. Always call /end explicitly to ensure billing finalization, memory ingestion, and webhooks fire.

Complete Integration Example

Here’s a full working example with a simple chat UI:
<!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:
// 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
});

React / Next.js Integration

A minimal React hook that handles session lifecycle, SSE streaming, error display, and reliable cleanup on unmount or tab close:
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 };
}
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.

API Reference

Create Session

POST /v1/chat/sessions
X-API-Key: {your_api_key}
Content-Type: application/json

{
  "agent_id": "string (required)"
}
Response:
{
  "conversation_id": "chat_...",
  "session_token": "eyJhbGc...",
  "agent_name": "Support Bot",
  "greeting": "Hello! How can I help?",
  "agent_model": "gpt-4o-mini"
}

Send Message

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

GET /v1/chat/sessions/{conversation_id}/messages
Authorization: Bearer {session_token}

Query Parameters:
- limit: number (default: 50)
- offset: number (default: 0)
Response:
{
  "messages": [...],
  "count": 20
}

End Session

POST /v1/chat/sessions/{conversation_id}/end
Authorization: Bearer {session_token}
Response:
{
  "success": true,
  "message": "Session ended successfully"
}

Error Codes

StatusCodeCause
400not_text_agentAgent is voice-only (Pipeline/Realtime)
400message_too_longMessage exceeds 10,000 characters
401invalid_api_keyAPI key missing or invalid
401invalid_tokenSession JWT expired or invalid
402insufficient_creditsOrganization has no credits remaining
403conversation_mismatchconversation_id in URL doesn’t match the session token
404session_not_foundSession expired (idle >30 min) or wrong ID
503Billing 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

Create a Text Agent

Set up an agent with text architecture in Studio.

API Keys

Manage your API keys and rate limits.

Conversation Memory

Enable long-term memory so agents remember returning users.

Webhooks

Get notified when conversations end or tools are called.