Backend de análisis de reuniones de ventas (Recall.ai + OpenAI + Supabase). Servido como serverless function en Vercel desde api/index.ts.
Análisis de costos y comparativa con DIIO/Gong/Chorus →
Importa la colección y el environment a Postman (o Insomnia, Bruno, etc.):
Descargar Postman Collection Descargar Environment
En Postman: Import → arrastra los dos archivos. Configura baseUrl, serviceApiKey (o authToken) y cronSecret en el environment.
| Tipo | Header | Cuándo usar |
|---|---|---|
| JWT/X-API-Key | Authorization: Bearer <jwt> | Dashboard cb-panel (Supabase Auth, Google sign-in @clickbalance.com) |
| JWT/X-API-Key | X-API-Key: <SERVICE_API_KEY> | n8n / AI agent. Requiere salesRepEmail en el body o query. |
| CRON_SECRET | Authorization: Bearer <CRON_SECRET> | Solo para /api/cron/*. Vercel Cron lo inyecta. |
| Svix | svix-id, svix-timestamp, svix-signature | Solo para /api/webhooks/recall. Recall.ai firma con Svix. |
| Método | Path | Auth | Descripción |
|---|---|---|---|
| POST | /api/meetings/schedule | JWT/X-API-Key | Agendar reunión (genera Meet vía n8n si no se pasa meetingUrl). Acepta links de Google Meet, Zoom o Microsoft Teams. |
| POST | /api/meetings/start | JWT/X-API-Key | Iniciar reunión ahora (legacy wrapper) |
| GET | /api/meetings | JWT/X-API-Key | Listar reuniones (paginado, filtros: status, dateFrom, dateTo, search) |
| GET | /api/meetings/:id | JWT/X-API-Key | Detalle: meeting + analyses + transcript + signed URLs (1h) |
| GET | /api/meetings/:id/video | JWT/X-API-Key | Signed URL del video (1h). 409 si aún no se descargó. |
| POST | /api/meetings/:id/reanalyze | JWT/X-API-Key | Re-corre el análisis IA. Body opcional: { sales_subtype: calificacion|demostracion|negociacion }. Solo válido si meeting_type=ventas. |
| DELETE | /api/meetings/:id | JWT/X-API-Key | Cancelar reunión SCHEDULED (ordena leave_call al bot) |
| GET | /api/notifications | JWT/X-API-Key | Últimas 20 notificaciones + unreadCount |
| PATCH | /api/notifications/:id/read | JWT/X-API-Key | Marcar una notificación como leída |
| POST | /api/notifications/mark-all-read | JWT/X-API-Key | Marcar todas como leídas |
| DELETE | /api/notifications/read | JWT/X-API-Key | Eliminar las notificaciones ya leídas. Devuelve { deleted: N } |
| GET | /api/me | JWT/X-API-Key | Perfil del rep autenticado |
| PATCH | /api/me | JWT/X-API-Key | Editar nombre del rep |
| GET | /api/sales-reps | JWT/X-API-Key | Listar todos los reps (admin only) |
| PATCH | /api/sales-reps/:id | JWT/X-API-Key | Toggle is_admin de otro rep (admin only). Body: { is_admin: bool } |
| GET | /api/sales-reps/by-email | JWT/X-API-Key | Buscar rep por email (?email=) |
| GET | /api/sales-reps/:id/performance | JWT/X-API-Key | Métricas: avgScore, estimatedCloseRate, topOpportunities, recentMeetings |
| GET | /api/sales-reps/:id/performance/breakdown | JWT/X-API-Key | Promedio por dimensión 0-20 sobre los últimos N días (?days=30) |
| GET | /api/sales-reps/:id/performance/weekly | JWT/X-API-Key | Score semanal ISO (Lunes CDMX) últimas N semanas (?weeks=8) |
| GET | /api/admin/team-performance | JWT/X-API-Key | Performance agregado de todo el equipo (admin only). Reemplaza N+1 de /admin. |
| GET | /api/cron/process-queue | CRON_SECRET | Procesa la cola de jobs (download_audio, analyze_transcript, sync_hubspot). Vercel lo dispara cada minuto. |
| POST | /api/webhooks/recall | Svix | Webhook firmado por Recall.ai (transcript.done, bot.done, etc.) |
| GET | /api/meetings/:id/agents | JWT/X-API-Key | Agentes disponibles para la reunión (según meeting_type) + preguntas sugeridas por agente. |
| GET | /api/meetings/:id/agents/:agentKey/messages | JWT/X-API-Key | Historial del chat (privado por usuario) con ese agente. |
| POST | /api/meetings/:id/agents/:agentKey/messages | JWT/X-API-Key | Pregunta al agente. Respuesta por SSE: frames {delta} y frame final {done, message_id}. |
| DELETE | /api/meetings/:id/agents/:agentKey/conversation | JWT/X-API-Key | Reinicia el chat: borra el thread de OpenAI y la conversación. |
| POST | /api/meetings/:id/agent-feedback | JWT/X-API-Key | 👍/👎 (+ corrección en dislike) sobre una respuesta del agente. source_ref = message_id del chat. |
| DELETE | /api/meetings/:id/agent-feedback | JWT/X-API-Key | Deshacer mi voto PENDIENTE de un origen (toggle off). Idempotente (200 aunque no haya voto). 409 si ya lo revisó un admin. |
| GET | /api/meetings/:id/my-agent-feedback | JWT/X-API-Key | Mis votos pendientes en la reunión (para pintar el estado del badge). |
| GET | /api/sales-subtypes | JWT/X-API-Key | Subtipos de venta activos (evaluadores) en orden de funnel, con labels. |
| GET | /api/admin/agents | JWT/X-API-Key | Listar agentes del registro, incl. inactivos (admin). |
| POST | /api/admin/agents | JWT/X-API-Key | Alta de agente de seguimiento (admin). Los evaluadores no se crean self-service. |
| GET | /api/admin/agents/models | JWT/X-API-Key | Allowlist de modelos permitidos (con flag file_search) (admin). |
| GET | /api/admin/agents/:key | JWT/X-API-Key | Detalle del agente + historial de versiones de instrucciones (admin). |
| PUT | /api/admin/agents/:key/instructions | JWT/X-API-Key | Editar instrucciones (versionado; OpenAI primero, luego BD) (admin). |
| PUT | /api/admin/agents/:key/suggested-questions | JWT/X-API-Key | Editar las preguntas sugeridas (chips) (admin). |
| POST | /api/admin/agents/:key/test | JWT/X-API-Key | Probar el agente con transcript/pregunta sin persistir (admin). |
| GET | /api/admin/agents/:key/files | JWT/X-API-Key | Archivos del vector store del agente + shared_with (admin). |
| POST | /api/admin/agents/:key/files | JWT/X-API-Key | Subir archivo (raw body + header X-Filename; pdf/txt/md/csv/docx, máx 20MB) (admin). |
| DELETE | /api/admin/agents/:key/files/:fileId | JWT/X-API-Key | Quitar un archivo del vector store (admin). |
| PUT | /api/admin/agents/:key/model | JWT/X-API-Key | Cambiar el modelo (guard de file_search si tiene store) (admin). |
| PUT | /api/admin/agents/:key/params | JWT/X-API-Key | Ajustar temperature (0-2) y top_p (0-1) (admin). |
| PUT | /api/admin/agents/:key/name | JWT/X-API-Key | Renombrar el agente (display_name + name del assistant) (admin). |
| PUT | /api/admin/agents/:key/active | JWT/X-API-Key | Baja lógica: archivar (false) o reactivar (true) (admin). |
| GET | /api/admin/corrections | JWT/X-API-Key | Cola de correcciones de agentes por estado (?status=pending) (admin). |
| PUT | /api/admin/corrections/:id | JWT/X-API-Key | Aprobar (lesson_text + pii_checked) o rechazar; aprobar regenera lecciones.md (admin). |
| POST | /api/meetings/invite-bot | JWT/X-API-Key | Invitar al bot a una reunión NO-ventas (general). Crea fila con sales_rep_id=NULL y meeting_type=general. |
| PATCH | /api/meetings/:id | JWT/X-API-Key | Editar title y/o customerInfo (y hubspotContactId; sales_rep_id solo admin). No toca el texto IA. |
| POST | /api/meetings/:id/sync-hubspot | JWT/X-API-Key | Re-disparar manualmente el sync a HubSpot (encola job force). 202 con jobId. |
| GET | /api/meetings/:id/audio | JWT/X-API-Key | Signed URL del audio (1h) con fallback a Recall. 409 AUDIO_NOT_READY si aún no se descargó. |
| GET | /api/meetings/:id/hubspot/candidates | JWT/X-API-Key | Picker de Contacts de HubSpot por full-text (?q=, ?limit=) cuando el match por email falló. |
| GET | /api/meetings/:id/hubspot/preview | JWT/X-API-Key | Preview del HTML que tendrían la Nota y el Meeting engagement si se sincronizara ahora. No toca HubSpot. |
| GET | /api/hubspot/contacts/search | JWT/X-API-Key | Búsqueda full-text de Contacts en HubSpot (?q=, ?limit=) para autocompletar el form de agendar. |
| GET | /api/me/hubspot/owner | JWT/X-API-Key | Estado del vínculo del rep con su HubSpot owner (auto-resolución lazy por email si está vacío). |
| GET | /api/me/hubspot/owner/candidates | JWT/X-API-Key | Picker de owners de HubSpot por texto (?q=, ?limit=) cuando el match automático por email falla. |
| PATCH | /api/me/hubspot/owner | JWT/X-API-Key | El rep setea/limpia su override de HubSpot owner (null = volver a auto-resolución). Valida el id contra HubSpot. |
| PATCH | /api/sales-reps/:id/hubspot-owner | JWT/X-API-Key | Admin asigna el HubSpot owner de otro rep (null = limpiar). Valida el id contra HubSpot (admin). |
| DELETE | /api/admin/meetings/:id | JWT/X-API-Key | Borrado físico de la reunión en cualquier estado: BD + Recall + Storage (best-effort) (admin). |
| GET | /api/admin/cron-health | JWT/X-API-Key | Métricas del processing_queue para detectar si el cron está atrasado (admin). |
| GET | /api/cron/cleanup-storage | CRON_SECRET | Barrido diario: purga media de Storage vencida + filas viejas de processing_queue. Vercel Cron. |
| GET | /api/transcript-corrections | JWT/X-API-Key | Listar correcciones de transcript (?onlyActive=true). Lectura abierta a cualquier autenticado. |
| POST | /api/transcript-corrections | JWT/X-API-Key | Crear corrección de transcript (admin). |
| PATCH | /api/transcript-corrections/:id | JWT/X-API-Key | Actualizar una corrección de transcript (admin). |
| DELETE | /api/transcript-corrections/:id | JWT/X-API-Key | Borrar una corrección de transcript (admin). |
Cada bloque expandible muestra request real (curl), response esperada con el JSON exacto que devuelve cb-dio, y los códigos de error más comunes con su causa. Copia el curl, exporta SERVICE_API_KEY=<tu valor> y prueba.
/api/meetings/schedule
— Agendar reunión (futuro o ahora)
Si omites meetingUrl, cb-dio llama a n8n para generar un Google Meet (default). Si lo mandas, debe ser un link de Google Meet, Zoom o Microsoft Teams — otros providers (Webex, Whereby, etc.) son rechazados con 400. Si omites scheduledAt, agenda para ahora mismo. customerInfo.contactName es el único campo realmente obligatorio. meetingPassword es opcional: úsalo si pegas un link de Zoom sin el ?pwd=... embebido; se reenvía a Recall y no se persiste en BD.
curl -X POST https://meet.cibi.clickbalance.net/api/meetings/schedule \
-H "X-API-Key: $SERVICE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"salesRepEmail": "desarrollo03@clickbalance.com",
"salesRepName": "Emmanuel Hernández",
"customerInfo": {
"company": "Acme S.A.",
"contactName": "Pedro García",
"dealValue": 50000,
"dealCurrency": "MXN"
},
"scheduledAt": "2026-05-15T15:00:00-06:00"
}'
{
"meetingId": "8c1b6e2f-...-...-...-c1f2e3a4b5c6",
"meetUrl": "https://meet.google.com/abc-defg-hij",
"scheduledAt": "2026-05-15T15:00:00-06:00",
"status": "SCHEDULED"
}
| Status | Code | Cuándo |
|---|---|---|
| 400 | VALIDATION_ERROR | falta contactName, email inválido, scheduledAt sin offset, etc. |
| 400 | MISSING_SALES_REP_EMAIL | usaste X-API-Key pero no mandaste salesRepEmail |
| 401 | UNAUTHORIZED | X-API-Key incorrecto o JWT expirado |
| 502 | N8N_ERROR | no se mandó meetingUrl y n8n falló al generar Meet |
/api/meetings/start
— Iniciar reunión ahora (legacy)
Wrapper de compatibilidad: scheduledAt fijo en null y meetingUrl obligatorio (Google Meet, Zoom o Microsoft Teams). meetingPassword opcional para links Zoom "limpios". Para nuevos integradores usa /schedule.
curl -X POST https://meet.cibi.clickbalance.net/api/meetings/start \
-H "X-API-Key: $SERVICE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"meetingUrl": "https://meet.google.com/qxb-xdam-uae",
"salesRepEmail": "desarrollo03@clickbalance.com",
"customerInfo": { "contactName": "Pedro García" }
}'
{
"meetingId": "8c1b6e2f-...-c1f2e3a4b5c6",
"meetUrl": "https://meet.google.com/qxb-xdam-uae",
"scheduledAt": null,
"status": "JOINING"
}
/api/meetings
— Listar reuniones (paginado + filtros)
Non-admin ve sus propias + reuniones meeting_type en (general, standup, tecnica, documentacion, planificacion) ajenas (read-only). Admin ve todo. status: SCHEDULED|JOINING|IN_PROGRESS|PROCESSING|COMPLETED|FAILED|CANCELLED. search: busca en title, company y contactName.
curl "https://meet.cibi.clickbalance.net/api/meetings?page=1&limit=20&status=COMPLETED&search=Acme&salesRepEmail=desarrollo03@clickbalance.com" \ -H "X-API-Key: $SERVICE_API_KEY"
{
"data": [
{
"id": "8c1b6e2f-...",
"sales_rep_id": "uuid|null",
"created_by_rep_id": "uuid|null",
"meeting_url": "https://meet.google.com/abc-defg-hij",
"platform": "Google Meet",
"customer_info": {
"company": "Acme S.A.",
"contactName": "Pedro García",
"dealValue": 50000,
"dealCurrency": "MXN",
"dealValueDisplay": "$50,000 MXN"
},
"status": "COMPLETED",
"error_code": null,
"error_message": null,
"meeting_type": "ventas",
"started_at": "2026-05-15T15:00:00-06:00",
"started_at_formatted": "15 may 2026, 15:00",
"video_duration_minutes": 47,
"duration": "47 min",
"sales_reps": { "id": "uuid", "name": "Vendedor", "email": "..." },
"created_by": { "id": "uuid", "name": "Quien programó", "email": "..." }
}
],
"total": 1,
"page": 1
}
/api/meetings/:id
— Detalle completo (meeting + analyses + transcript + signed URLs)
Devuelve TODO lo necesario para renderizar la página de detalle del dashboard en una sola request. Las URLs firmadas (video/audio) son válidas por 1h.
curl "https://meet.cibi.clickbalance.net/api/meetings/8c1b6e2f-..." \ -H "X-API-Key: $SERVICE_API_KEY"
{
"meeting": { "id": "...", "status": "COMPLETED", "customer_info": {...}, ... },
"transcript": {
"id": "...",
"language": "es",
"raw_transcript": [
{ "speaker": "Orador 0 (Emmanuel)", "text": "Hola Pedro...", "start_ms": 0, "end_ms": 3200 }
],
"full_text": "Hola Pedro... [transcript completo]"
},
"media": {
"videoUrl": "https://...supabase.co/storage/v1/sign/...",
"audioUrl": "https://...supabase.co/storage/v1/sign/...",
"expiresAt": "2026-05-15T16:00:00.000Z"
},
"analyses": [
{
"summary": "...",
"action_items": [...],
"sales_rep_score": 78,
"score_breakdown": { "scores": {...}, "justifications": {...}, "meeting_type": "ventas" },
"opportunities": [...],
"close_probability": 70,
"key_objections": [...],
"next_steps": "...",
"participation": {
"rep_identified": true,
"total_speaking_seconds": 1842,
"participants": [
{ "name": "Juan Pérez", "email": "juan@clickbalance.com", "role": "rep", "speaking_seconds": 1102, "percentage": 59.8 },
{ "name": "Pedro García","email": "pedro@acme.com", "role": "guest", "speaking_seconds": 580, "percentage": 31.5 },
{ "name": "María López", "email": null, "role": "guest", "speaking_seconds": 160, "percentage": 8.7 }
]
}
}
]
}
/api/meetings/:id
— Cancelar reunión agendada
Solo funciona en estado SCHEDULED. Si la reunión ya empezó (JOINING/IN_PROGRESS/etc.), responde 409 INVALID_STATE.
curl -X DELETE "https://meet.cibi.clickbalance.net/api/meetings/8c1b6e2f-...?salesRepEmail=desarrollo03@clickbalance.com" \ -H "X-API-Key: $SERVICE_API_KEY"
{ "ok": true, "status": "CANCELLED" }
| Status | Code | Cuándo |
|---|---|---|
| 403 | FORBIDDEN | la reunión no pertenece al rep autenticado |
| 404 | MEETING_NOT_FOUND | el id no existe |
| 409 | INVALID_STATE | la reunión ya no está en SCHEDULED |
/api/meetings/:id/reanalyze
— Re-analizar reunión (override de subtipo)
Re-corre el pipeline IA contra la customer_info y meeting_type actuales. Body completamente opcional: si se omite, el subtipo se autodetecta. Si se pasa sales_subtype, se usa ese valor en lugar del autodetectado — solo válido cuando meeting_type === 'ventas'. El análisis anterior queda intacto hasta que el cron termina y hace upsert del nuevo.
curl -X POST "https://meet.cibi.clickbalance.net/api/meetings/8c1b6e2f-.../reanalyze" \
-H "X-API-Key: $SERVICE_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "sales_subtype": "demostracion" }'
{
"jobId": "uuid-del-job",
"meetingId": "8c1b6e2f-...",
"status": "PROCESSING"
}
| Status | Code | Cuándo |
|---|---|---|
| 400 | VALIDATION_ERROR | sales_subtype inválido o no aplica al meeting_type de la reunión |
| 403 | FORBIDDEN | la reunión no pertenece al rep autenticado |
| 404 | MEETING_NOT_FOUND | el id no existe |
| 409 | TRANSCRIPT_NOT_READY | el transcript aún no está disponible |
/api/me
— Editar nombre del rep
curl -X PATCH "https://meet.cibi.clickbalance.net/api/me?salesRepEmail=desarrollo03@clickbalance.com" \
-H "X-API-Key: $SERVICE_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Emmanuel Hernández" }'
{
"id": "uuid-del-rep",
"email": "desarrollo03@clickbalance.com",
"name": "Emmanuel Hernández",
"created_at": "2026-04-29T..."
}
/api/sales-reps/:id/performance
— Métricas agregadas del rep
Suma reuniones COMPLETED, promedia scores, estima close rate (close_probability >= 60), y rescata top oportunidades de las 5 reuniones más recientes.
curl "https://meet.cibi.clickbalance.net/api/sales-reps/<rep-id>/performance?salesRepEmail=desarrollo03@clickbalance.com" \ -H "X-API-Key: $SERVICE_API_KEY"
{
"salesRepId": "uuid",
"name": "Emmanuel Hernández",
"totalMeetings": 12,
"avgScore": 76,
"estimatedCloseRate": 58,
"topOpportunities": ["Cliente abierto a multi-año", "Interés en módulo X", ...],
"recentMeetings": [...]
}
/api/cron/process-queue
— Procesar cola de jobs (Vercel Cron)
Vercel lo dispara automáticamente cada minuto. Para probar manualmente desde Postman necesitas el CRON_SECRET como Bearer.
curl https://meet.cibi.clickbalance.net/api/cron/process-queue \ -H "Authorization: Bearer $CRON_SECRET"
{ "processed": 3, "failed": 0 }
| Status | Code | Cuándo |
|---|---|---|
| 401 | UNAUTHORIZED | CRON_SECRET incorrecto o falta el header |
/api/meetings/:id/agents/:agentKey/messages
— Preguntar al agente de seguimiento (chat SSE)
Respuesta en streaming SSE (Content-Type: text/event-stream): cero o más frames data: {"delta":"..."} con los tokens, y un frame final data: {"done":true,"message_id":"uuid"}. El message_id es el source_ref para el feedback 👍/👎. Si el run falla después de abrir el stream, sale data: {"error":true,"code":"..."}. El body lleva question (string).
curl -N -X POST "https://meet.cibi.clickbalance.net/api/meetings/<id>/agents/seguimiento_general/messages?salesRepEmail=desarrollo03@clickbalance.com" \
-H "X-API-Key: $SERVICE_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "question": "¿Qué compromisos quedaron pendientes?" }'
data: {"delta":"Quedaron "}
data: {"delta":"dos compromisos: enviar la cotización..."}
data: {"done":true,"message_id":"8c1b6e2f-..."}
| Status | Code | Cuándo |
|---|---|---|
| 400 | MISSING_PARAM | falta question en el body |
| 404 | AGENT_NOT_FOUND | agentKey no existe o no es de seguimiento |
| 409 | TRANSCRIPT_NOT_READY | el transcript de la reunión aún no está listo |
RECALL_API_KEY RECALL_WEBHOOK_SECRET OPENAI_API_KEY OPENAI_MODEL=gpt-4o-mini OPENAI_MODEL_HEAVY=gpt-4o SUPABASE_URL SUPABASE_SERVICE_ROLE_KEY SUPABASE_STORAGE_BUCKET=meeting-recordings CRON_SECRET N8N_WEBHOOK_URL N8N_WEBHOOK_SECRET SERVICE_API_KEY # genera con: openssl rand -hex 32 ALLOWED_EMAIL_DOMAIN=clickbalance.com LOG_LEVEL=info # NO pongas PORT ni NODE_ENV — Vercel los maneja # (BACKEND_URL es del front cb-panel, no aplica aquí)
cb-dio · backend serverless en Vercel · Costos →