Integración browser-direct
Tu sk_live_* nunca debe vivir en el navegador. Para chats embebidos en una web seguí el diseño tipo Stripe payment_intent.client_secret: tu backend genera un token efímero csk_eph_* scoped a un session_id específico y se lo entrega al cliente. El browser usa ese token para hablar directo con el Canal API, sin tu intermediación en cada mensaje.
# ── Paso 1: tu backend genera el token con su sk_live_ ──────────────────POST /api/v1/channel/session-tokensAuthorization: Bearer sk_live_YOUR_KEYContent-Type: application/jsonX-Overtaker-Version: 1
{ "session_id": "user-uuid-del-cliente", // requerido — scope del token "ttl_minutes": 30 // opcional · default 30 · máximo 240 (4h)}
# 200 OK — el token completo se muestra UNA SOLA VEZ{ "token_id": "uuid-del-registro", "token": "csk_eph_xxxxxxxxxxxxxxxx", // ← entregar al browser "session_id": "user-uuid-del-cliente", "expires_at": "2026-05-27T12:30:00Z"}# ── Paso 2: el browser usa el token para hablar directo con el Canal API ─POST /api/v1/channel/messageAuthorization: Bearer csk_eph_xxxxxxxxxxxxxxxxContent-Type: application/jsonX-Overtaker-Version: 1
{ "session_id": "user-uuid-del-cliente", "message": "Hola desde el browser" }# ── Paso 3 (cuando termina): revocar manualmente ────────────────────────DELETE /api/v1/channel/session-tokens/{token_id}Authorization: Bearer sk_live_YOUR_KEY # ← debe ir con la sk_live_ dueñaX-Overtaker-Version: 1
# 204 No ContentScope estricto del token efímero
Sección titulada «Scope estricto del token efímero»Un csk_eph_* solo puede:
POST /channel/messagecon el mismosession_iddel token (otro session_id →403)POST /channel/message/streamcon el mismosession_idGET /channel/conversations/{session_id}/messagesde su sesiónGET /channel/ping
Los endpoints administrativos (rotación de webhook secret, sandbox, test de webhooks, GET lead, crear/revocar tokens) rechazan el token efímero con 403.
Ejemplo — React
Sección titulada «Ejemplo — React»// 1) Endpoint en TU backend que pide el token con tu sk_live_:// POST /api/chat/session → devuelve { token, session_id, expires_at }
// 2) En el componente del clienteimport { useEffect, useRef, useState } from 'react'
export function ChatWidget({ userId }: { userId: string }) { const tokenRef = useRef<string | null>(null) const sessionIdRef = useRef<string | null>(null) const [reply, setReply] = useState('')
useEffect(() => { // Pedir token a TU backend (que internamente llama a /channel/session-tokens) fetch('/api/chat/session', { method: 'POST', body: JSON.stringify({ userId }) }) .then(r => r.json()) .then(({ token, session_id }) => { tokenRef.current = token // ← en memoria, NO en localStorage sessionIdRef.current = session_id }) }, [userId])
async function send(text: string) { const res = await fetch('https://api.overtaker.online/api/v1/channel/message', { method: 'POST', headers: { 'Authorization': `Bearer ${tokenRef.current}`, 'Content-Type': 'application/json', 'X-Overtaker-Version': '1', }, body: JSON.stringify({ session_id: sessionIdRef.current, message: text }), }) const data = await res.json() // { message_id, status: "received" } // La respuesta del bot llega a TU webhook — desde ahí podés pushearla // al browser via WebSocket / SSE / polling del history. return data }
return <button onClick={() => send('hola').then(d => setReply(JSON.stringify(d)))}>{reply || 'Enviar'}</button>}