API, Webhooks Y MCP
Conecta Kora CRM con tu web, tu ERP, Zapier/Make/n8n o asistentes de IA como Claude. Todo queda limitado al negocio dueño de la clave.
Inicio Rápido
Prueba ahora con la clave pública de demostración (solo lectura, datos del negocio demo):
curl https://tu-dominio.com/api/v1/contacts?limit=5 \ -H "Authorization: Bearer kora_demo_solo_lectura_2026"
Para tu negocio: Panel → Configuración → Integraciones → Crear Clave. La clave se muestra una sola vez; guárdala como secreto.
Autenticación
Envía la clave en cada solicitud con Authorization: Bearer <clave>. Las claves pueden ser de solo lectura o de lectura y escritura, y se revocan al instante desde el panel. Guardamos solo un hash: si la pierdes, crea otra.
Errores Y Paginación
Las listas aceptan ?page=1&limit=25 (máx. 100) y responden con data, page, limit, total, has_more. Los errores siempre tienen la misma forma:
HTTP/1.1 403
{ "error": { "code": "read_only_key", "message": "Esta clave es de solo lectura. Crea una clave con permiso de escritura." } }Códigos: 400 JSON o id inválido · 401 clave ausente/revocada · 403 sin permiso de escritura · 404 no existe en tu espacio · 409 duplicado o ventana de 24 h cerrada · 422 validación · 500 error interno.
Endpoints
Base: https://tu-dominio.com/api/v1
| GET | /contacts | Lista contactos. Filtros: q, stage_id |
| POST | /contacts | Crea un contacto (phone obligatorio) |
| GET | /contacts/:id | Ver un contacto con sus etiquetas |
| PATCH | /contacts/:id | Edita nombre, teléfono, etiquetas, campos, agente |
| GET | /conversations | Conversaciones recientes. Filtros: unread=true, assigned_to |
| GET | /conversations/:id | Contacto + mensajes |
| PATCH | /conversations/:id | Asignar agente: { assigned_to } |
| GET | /messages?contact_id= | Mensajes de un contacto |
| GET | /messages/:id | Ver un mensaje |
| POST | /messages | Envía un WhatsApp (texto o plantilla) |
| GET | /tags | Lista etiquetas |
| POST | /tags | Crea etiqueta { name, color } |
| PATCH | /tags/:id | Edita etiqueta |
| GET | /pipelines | Embudos con etapas |
| GET | /pipelines/:id | Ver un embudo |
| GET | /opportunities | Oportunidades. Filtros: pipeline_id, stage_id |
| POST | /opportunities | Crea { contact_id, stage_id, value } |
| GET | /opportunities/:id | Ver oportunidad |
| PATCH | /opportunities/:id | Mover etapa / cambiar valor / motivo de pérdida |
| GET | /agents | Miembros del equipo |
| GET | /agents/:id | Ver agente |
| PATCH | /agents/:id | Cambiar rol { role: admin | agent } |
| GET | /summary?days=7 | Resumen de actividad |
| POST | /webhooks/in | Recibe eventos externos |
# Crear un contacto con etiquetas
curl -X POST https://tu-dominio.com/api/v1/contacts \
-H "Authorization: Bearer $KORA_KEY" -H "Content-Type: application/json" \
-d '{"name":"María Rojas","phone":"+506 8888 7777","tag_ids":["<uuid-etiqueta>"]}'
# Mover una oportunidad de etapa
curl -X PATCH https://tu-dominio.com/api/v1/opportunities/<contact_id> \
-H "Authorization: Bearer $KORA_KEY" -H "Content-Type: application/json" \
-d '{"stage_id":"<uuid-etapa>","value":350000}'Los agentes se invitan desde el panel por seguridad; la API permite listarlos, verlos y cambiar su rol.
Enviar Mensajes De WhatsApp
Texto libre solo dentro de la ventana de 24 h desde el último mensaje del cliente; fuera de ella usa una plantilla aprobada. Sin número de Meta conectado el envío se simula (modo demostración, "demo": true).
curl -X POST https://tu-dominio.com/api/v1/messages \
-H "Authorization: Bearer $KORA_KEY" -H "Content-Type: application/json" \
-d '{"phone":"50688887777","text":"¡Hola! Tu cita quedó confirmada."}'
# Con plantilla aprobada
-d '{"contact_id":"<uuid>","template_id":"<uuid>","variables":["María","10:00 a. m."]}'
# Respuesta
{ "message_id": "…", "contact_id": "…", "status": "sent", "demo": false }Webhooks Salientes
Configúralos en Integraciones. Eventos: contact.created, message.received, opportunity.moved, conversation.assigned. Enviamos un POST JSON; responde 2xx en menos de 10 s. Si falla, reintentamos a los 1, 5, 30, 120 y 360 minutos. Cada entrega queda en el registro del panel.
POST https://tu-servidor.com/kora
Content-Type: application/json
X-Kora-Event: message.received
X-Kora-Delivery: 7b1c…
X-Signature: t=1791456000,v1=5f2a9c…
{
"id": "7b1c…",
"event": "message.received",
"created_at": "2026-10-05T14:20:00Z",
"workspace_id": "…",
"data": { "id": "…", "contact_id": "…", "type": "text", "body": "Hola, ¿tienen cita mañana?" }
}// opportunity.moved
"data": { "contact_id": "…", "pipeline_id": "…", "from_stage_id": "…", "to_stage_id": "…", "value": 350000 }
// conversation.assigned
"data": { "contact_id": "…", "assigned_to": "…", "previous": null }
// contact.created
"data": { "id": "…", "name": "María Rojas", "phone": "50688887777" }Verificar La Firma
La cabecera X-Signature contiene t (segundos Unix) y v1 = HMAC-SHA256 en hex de `${t}.${cuerpo}` con el secreto del webhook. Usa el cuerpo crudo, compara en tiempo constante y rechaza firmas con más de 5 minutos.
// Node.js / Express
import crypto from "node:crypto";
app.post("/kora", express.raw({ type: "application/json" }), (req, res) => {
const { t, v1 } = Object.fromEntries(req.get("X-Signature").split(",").map(p => p.split("=")));
const expected = crypto.createHmac("sha256", process.env.KORA_WEBHOOK_SECRET)
.update(`${t}.${req.body}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
if (!fresh || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1))) return res.sendStatus(401);
const event = JSON.parse(req.body);
res.sendStatus(200);
});# Python / Flask
import hmac, hashlib, time
sig = dict(p.split("=") for p in request.headers["X-Signature"].split(","))
body = request.get_data(as_text=True)
expected = hmac.new(SECRET.encode(), f"{sig['t']}.{body}".encode(), hashlib.sha256).hexdigest()
ok = abs(time.time() - int(sig["t"])) < 300 and hmac.compare_digest(expected, sig["v1"])Webhooks Entrantes
Envía eventos de tus sistemas a https://tu-dominio.com/api/v1/webhooks/in con una clave de API como secreto (Authorization: Bearer o X-Webhook-Secret). Todo evento queda registrado en Integraciones. form.submitted crea el contacto si no existe (requiere clave con escritura).
curl -X POST https://tu-dominio.com/api/v1/webhooks/in \
-H "X-Webhook-Secret: $KORA_KEY" -H "Content-Type: application/json" \
-d '{"event":"form.submitted","data":{"name":"Luis Mora","phone":"+506 7000 1122","origen":"landing"}}'
# Respuesta 202
{ "id": "…", "event": "form.submitted", "result": "contacto creado …", "created_at": "…" }Otros eventos (por ejemplo whatsapp.message o whatsapp.status reenviados desde otra plataforma) se guardan tal cual para auditoría. Los mensajes reales de tu número oficial llegan por el webhook de Meta configurado en Configuración → WhatsApp.
Servidor MCP (Claude Y Otros Clientes)
URL: https://tu-dominio.com/mcp (Streamable HTTP, JSON-RPC). Usa la misma clave de API. Herramientas: buscar_contactos, listar_conversaciones, enviar_mensaje, crear_oportunidad, mover_etapa, asignar_agente, resumen_de_actividad, listar_embudos, listar_agentes.
Claude Desktop → Configuración → Desarrollador → Editar configuración, y pega:
{
"mcpServers": {
"kora-crm": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://tu-dominio.com/mcp",
"--header", "Authorization: Bearer ${KORA_API_KEY}"],
"env": { "KORA_API_KEY": "kora_demo_solo_lectura_2026" }
}
}
}Claude Code:
claude mcp add --transport http kora-crm https://tu-dominio.com/mcp \ --header "Authorization: Bearer TU_CLAVE"
Prueba manual:
curl -X POST https://tu-dominio.com/mcp -H "Authorization: Bearer kora_demo_solo_lectura_2026" \
-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"resumen_de_actividad","arguments":{"dias":30}}}'Con la clave demo las herramientas de escritura responden «solo lectura». Crea una clave con escritura para que Claude envíe mensajes o mueva oportunidades.