Cómo funciona
Cada mensaje entra por POST /api/message, se autentica y pasa al pipeline compartido del agente. La API conserva el contexto por userId, ejecuta tools si hacen falta y devuelve todas las partes de la respuesta en el mismo request.
La API hereda prompt, memoria, herramientas, guardrails y deduplicación. No necesitas mantener una implementación paralela.
Elige la credencial correcta
Todos los requests indican el agente con X-Agent-Id. La credencial viaja como Bearer token y depende de dónde corre tu integración.
Key secreta
Server-to-server. Puede operar para distintos usuarios según sus permisos. Nunca la envíes al navegador.
Key publicable
Empieza con pk_ y solo puede crear una sesión anónima. Segura para un widget.
Sesión
Token kas1. limitado a una identidad. Ideal cuando el usuario ya inició sesión.
curl https://agents.kaitools.dev/api/sessions \
-H "Authorization: Bearer pk_widget.SECRETO" \
-H "X-Agent-Id: mi-agente" \
-H "Content-Type: application/json" \
-d '{ "mode": "anonymous" }'{
"token": "kas1.eyJhZ2VudERvY0lkIjo...",
"userId": "anon_K8x2mQp4...",
"expiresAt": 1789106400,
"expiresIn": 3600
}| CAMPO | REGLA DE POST /api/sessions |
|---|---|
mode | anonymous con key publicable; el servidor genera el usuario |
previousToken | Opcional; conserva el usuario anónimo al renovar, incluso si expiró |
ttlSeconds | Opcional; mínimo 60 s y máximo 24 h |
Puede responder 400 invalid_request, 401 unauthorized, 429 rate_limited con Retry-After o 503 sessions_disabled.
Una key secreta puede apuntar a cualquier conversación. Para frontend usa una key pk_ o una sesión emitida por tu backend.
Tu primer request
Define una identidad estable
Usa el identificador del usuario en tu sistema. El mismo userId continúa la misma conversación.
Envía el mensaje
Añade un messageId único para que cualquier reintento sea idempotente.
Renderiza las partes
reply trae el texto agregado; parts conserva texto, imágenes, documentos y otros resultados en orden.
curl https://agents.kaitools.dev/api/message \
-H "Authorization: Bearer $KAI_API_KEY" \
-H "X-Agent-Id: mi-agente" \
-H "Content-Type: application/json" \
-d '{
"userId": "cliente_123",
"messageId": "pedido_8f31",
"profileName": "Ana",
"message": {
"type": "text",
"text": "¿Qué horarios manejan?"
}
}'{
"code": "success",
"conversationId": "api:cliente_123",
"messageId": "pedido_8f31",
"reply": "Abrimos de lunes a viernes, de 9 a 18 h.",
"parts": [
{ "kind": "text", "text": "Abrimos de lunes a viernes, de 9 a 18 h." }
]
}Mensajes aceptados
| TYPE | CAMPOS |
|---|---|
text | text |
image / video | url HTTPS, mimeType, caption? |
audio | url HTTPS, mimeType |
document | url HTTPS, mimeType, filename?, caption? |
location | latitude, longitude, name?, address? |
Con sesión, userId se omite porque está firmado en el token. Si se envía debe coincidir. messageId es opcional, pero conviene generarlo para que los reintentos respondan duplicate en vez de crear otro turno.
Streaming con SSE
Agrega Accept: text/event-stream al mismo endpoint. Recibirás eventos message, tool y finalmente done. El evento final tiene exactamente el mismo contrato que la respuesta JSON normal.
curl -N https://agents.kaitools.dev/api/message \
-H "Authorization: Bearer $KAI_API_KEY" \
-H "X-Agent-Id: mi-agente" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{ "userId": "cliente_123", "message": { "type": "text", "text": "Cotiza mi pedido" } }'| EVENTO | CONTENIDO |
|---|---|
message | Una parte estructurada lista para renderizar |
tool | Nombre, fase call/result y duración cuando aplica |
done | El AgentReply completo que cerró el turno |
error | Fallo ocurrido después de abrir el stream |
No es streaming token por token: cada evento representa una parte completa que el agente decidió entregar. Esto mantiene estable la experiencia entre HTTP, WhatsApp, Messenger e Instagram.
Todas las rutas públicas
| MÉTODO | RUTA | USO |
|---|---|---|
| POST | /api/sessions | Crear identidad segura para frontend |
| POST | /api/message | Enviar un mensaje o abrir SSE |
| POST | /api/uploads | Subir adjuntos de hasta 20 MB |
| POST | /api/voice/sessions | Crear un ticket efímero para voz realtime |
| WS | /api/voice/realtime | Abrir audio bidireccional usando el ticket efímero |
| GET | /api/conversations/:id | Leer la conversación de la propia sesión |
| GET | /api/conversations/:id/messages | Paginar el historial visible de la propia sesión |
Una sesión solo puede leer y escribir su conversación. Intentar acceder a otra identidad responde 404 para no revelar que existe.
Archivos, voz e historial
Subir archivos
POST /api/uploads recibe multipart/form-data con un campo file de hasta 20 MB. Devuelve { url, type, mimeType, filename }; usa esa URL en un mensaje de tipo image, video, audio o document.
curl https://agents.kaitools.dev/api/uploads \
-H "Authorization: Bearer $SESSION_TOKEN" \
-H "X-Agent-Id: mi-agente" \
-F "file=@menu.pdf"{
"url": "https://storage.googleapis.com/.../menu.pdf",
"type": "document",
"mimeType": "application/pdf",
"filename": "menu.pdf"
}El MIME determina type; si falta, se infiere de la extensión. Los fallos propios son 400 invalid_file/invalid_multipart, 413 file_too_large, 415 unsupported_media_type y 500 upload_failed.
Crear una llamada de voz
POST /api/voice/sessions acepta mode como hands_free o push_to_talk. Devuelve callId, ticket, expiresAt, protocol y websocketUrl. El cliente abre el WebSocket con los subprotocolos indicados por la respuesta.
curl https://agents.kaitools.dev/api/voice/sessions \
-H "Authorization: Bearer $SESSION_TOKEN" \
-H "X-Agent-Id: mi-agente" \
-H "Content-Type: application/json" \
-d '{ "mode": "hands_free", "profileName": "Ana" }'{
"callId": "e7c87a89-...",
"expiresAt": 1789102860,
"protocol": "kai.voice.v1",
"ticket": "kvt1...",
"websocketUrl": "wss://voice.agents.kaitools.dev/api/voice/realtime"
}Abre websocketUrl ofreciendo kai.voice.v1 y kai.ticket.<ticket>. Envía PCM16 mono de 16 kHz y recibe PCM16 mono de 24 kHz. hands_free usa detección de voz; push_to_talk requiere eventos activity.start y activity.end.
El socket emite session.ready, state, transcript, tool, message, interrupted, pong, error y session.ended. Puede rechazar la creación con 403 voice_disabled, 409 voice_requires_post_validation, 429 rate_limited o 503 voice_unavailable.
Leer el propio historial
GET /api/conversations/:id devuelve los metadatos del hilo y GET /api/conversations/:id/messages?limit=50&cursor=… pagina sus mensajes visibles en orden cronológico. limit admite 1–100 y el cursor debe tratarse como opaco.
{
"conversationId": "api:anon_K8x2mQp4...",
"items": [
{
"id": "msg_01",
"role": "user",
"kind": "user_message",
"content": "¿Qué horarios manejan?",
"createdAt": "2026-09-11T08:00:00.000Z"
}
],
"nextCursor": "eyJpZCI6Im1zZ18wMS..."
}Una sesión solo puede consultar su propio id de conversación. Estas rutas devuelven 400 invalid_request para ids o cursores inválidos y 404 not_found tanto si el hilo no existe como si pertenece a otra sesión.
Errores y reintentos
Reintenta fallos de red, respuestas 5xx y 409 conversation_busy. Conserva siempre el mismo messageId: el servidor lo deduplicará y evitará una segunda respuesta.
| HTTP | CODE | ACCIÓN |
|---|---|---|
| 400 | invalid_request | Corrige body, id o cursor; no reintentes igual |
| 401 | unauthorized / session_expired | Renueva la sesión o revisa la credencial |
| 403 | forbidden | Respeta el bloqueo o corrige el alcance |
| 409 | conversation_busy | Espera retryAfterMs y reutiliza el messageId |
| 429 | rate_limited | Espera el header Retry-After |
| 5xx | Error de servicio | Reintenta con backoff exponencial y jitter |
Incluye ids automáticos, timeout de 130 segundos, backoff con jitter y errores tipados.