Skip to main content

Overview

Explicitly ends a chat session and triggers all cleanup operations. Always call this endpoint when the user closes the chat or navigates away. Authentication: Authorization: Bearer {session_token} Important: Sessions auto-expire after 30 minutes of inactivity, but explicit ending ensures proper cleanup.

Request

Path Parameters:
ParameterTypeRequiredDescription
conversation_idstringSession ID from create session response
Headers:
Authorization: Bearer {session_token}
Body: None

Response

Status: 200 OK
{
  "success": true,
  "message": "Session ended successfully"
}

What Happens on End

When you call /end, the following cleanup operations run:

1. Billing Finalization

# End billing session
await end_billing_session(
    conversation_id,
    user_message_count=session.user_message_count,
    total_message_count=session.total_message_count,
)

# Calculate session cost
session_cost = await get_session_cost(conversation_id)

# Consume credits from organization balance
await consume_credits(
    organization_id=organization_id,
    session_cost=session_cost,
    session_id=billing_session_id
)
Result: Credits consumed based on actual usage

2. Memory Ingestion (Graphiti)

# Get all messages
messages = await get_messages(conversation_id, limit=1000)

# Format for Graphiti
graphiti_messages = [
    {"role": msg["role"], "content": msg["content"]}
    for msg in messages
]

# Ingest into knowledge graph
result = await ingest_conversation_async(
    session_id=conversation_id,
    messages=graphiti_messages,
    agent_id=agent_id,
    org_id=organization_id,
    user_id=memory_id  # From user_identifier
)
Result: Conversation memory available for future sessions Timeout: 25 seconds (fire-and-forget)

3. Post-Call Analysis

# Trigger analysis (fire-and-forget)
asyncio.create_task(
    trigger_post_call_analysis(conversation_id, agent_id, user_id)
)
Result: QA analysis if configured on agent

4. Webhook Notification

# Call agent webhook if configured
if agent_config.webhook_url:
    await call_agent_webhook(
        conversation_id,
        agent_config,
        user_id,
        event_type="chat"
    )
Webhook Payload:
{
  "event": "chat.ended",
  "timestamp": "2024-01-15T10:35:00Z",
  "data": {
    "conversation_id": "chat_abc123",
    "agent_id": "agent_xyz",
    "agent_name": "Support Bot",
    "message_count": 20,
    "input_tokens": 150,
    "output_tokens": 300,
    "total_cost": 0.0045
  }
}
Timeout: 7 seconds

5. Session Removal

# Remove from in-memory store
removed = await session_store.end_session(conversation_id)
Result: Session removed from memory, conversation input cleared

Error Responses

401 Unauthorized

{
  "detail": "Invalid token"
}

403 Forbidden

{
  "detail": "Conversation ID does not match session token"
}

500 Server Error

{
  "detail": "Failed to end session"
}

Examples

cURL

curl -X POST "https://api.talkifai.dev/v1/chat/sessions/chat_abc123/end" \
  -H "Authorization: Bearer eyJhbGc..."

JavaScript (Fetch)

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

    if (!response.ok) {
      throw new Error('Failed to end session');
    }

    console.log('Session ended successfully');
  } catch (error) {
    console.error('Error ending session:', error);
  }
}

React (Cleanup on Unmount)

useEffect(() => {
  return () => {
    // Cleanup when component unmounts
    if (sessionTokenRef.current && conversationIdRef.current) {
      fetch(
        `/v1/chat/sessions/${conversationIdRef.current}/end`,
        {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${sessionTokenRef.current}`
          }
        }
      ).catch(err => console.error('Cleanup error:', err));
    }
  };
}, []);

React (With keepalive for Tab Close)

async function endSession(conversationId, token) {
  await fetch(`/v1/chat/sessions/${conversationId}/end`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`
    },
    keepalive: true // Critical: survives tab close
  });
}

beforeunload Handler

window.addEventListener('beforeunload', () => {
  if (sessionToken && conversationId) {
    fetch(`/v1/chat/sessions/${conversationId}/end`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${sessionToken}`
      },
      keepalive: true // Ensures request completes
    });
  }
});

Next.js (Route Handler)

// app/api/chat/end/route.ts
export async function POST(request: Request) {
  const { conversationId, sessionToken } = await request.json();

  try {
    const response = await fetch(
      `${process.env.TALKIFAI_API_URL}/v1/chat/sessions/${conversationId}/end`,
      {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${sessionToken}`
        }
      }
    );

    const data = await response.json();

    if (!response.ok) {
      return Response.json(data, { status: response.status });
    }

    return Response.json(data);
  } catch (error) {
    return Response.json(
      { error: 'Failed to end session' },
      { status: 500 }
    );
  }
}

When to Call /end

✅ Always Call When:

  1. User Closes Chat Widget
    onCloseChatWidget = () => {
      endSession(conversationId, token);
      hideWidget();
    }
    
  2. User Navigates Away
    window.addEventListener('beforeunload', handleCleanup);
    
  3. Agent Calls end_chat Tool
    if (eventType === 'stream_end' && data.end_session) {
      await endSession(conversationId, token);
    }
    
  4. Session Timeout (Idle)
    • Handled automatically by server
    • But explicit is better than implicit

❌ Don’t Call When:

  1. Session Already Expired
    • Token invalid, will return 401
    • No cleanup needed
  2. Mid-Conversation
    • User is still chatting
    • Wait for natural end

Auto-Expiry vs Explicit End

AspectAuto-Expiry (30 min)Explicit /end
TriggerInactivity timeoutUser action
BillingFinalizedFinalized
MemoryIngestedIngested
AnalysisTriggeredTriggered
WebhookFiredFired
ReliabilityDepends on cleanup loopImmediate
Always call /end explicitly. Auto-expiry relies on background cleanup loops that may have delays. Explicit ending ensures immediate cleanup.

Best Practices

1. Use keepalive

// ✅ Good
fetch('/end', {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${token}` },
  keepalive: true // Survives tab close
});

// ❌ Bad (may be cancelled by browser)
fetch('/end', {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${token}` }
});

2. Handle Errors Gracefully

try {
  await endSession(conversationId, token);
} catch (error) {
  // Log but don't show to user
  console.error('Cleanup error (non-fatal):', error);
  // Session will auto-expire anyway
}

3. Clear Local State

async function endSession(conversationId, token) {
  await fetch(`/v1/chat/sessions/${conversationId}/end`, {
    method: 'POST',
    headers: { 'Authorization': `Bearer ${token}` },
    keepalive: true
  });

  // Clear local state
  sessionStorage.removeItem('conversation_id');
  sessionStorage.removeItem('session_token');
  setMessages([]);
}

4. Debounce Rapid Calls

const endSessionRef = useRef(null);

const endSessionWithDebounce = useCallback((conversationId, token) => {
  if (endSessionRef.current) return; // Already ending

  endSessionRef.current = setTimeout(async () => {
    await endSession(conversationId, token);
    endSessionRef.current = null;
  }, 1000);
}, []);


Next Steps

Create New Session

Start a fresh conversation with a new session.

View Analytics

Check session costs and usage in dashboard.