Cibi Meet API backend-only

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 →

Probar la API

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.

Autenticación

TipoHeaderCuándo usar
JWT/X-API-KeyAuthorization: Bearer <jwt>Dashboard cb-panel (Supabase Auth, Google sign-in @clickbalance.com)
JWT/X-API-KeyX-API-Key: <SERVICE_API_KEY>n8n / AI agent. Requiere salesRepEmail en el body o query.
CRON_SECRETAuthorization: Bearer <CRON_SECRET>Solo para /api/cron/*. Vercel Cron lo inyecta.
Svixsvix-id, svix-timestamp, svix-signatureSolo para /api/webhooks/recall. Recall.ai firma con Svix.

Endpoints

MétodoPathAuthDescripció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).

Ejemplos por endpoint

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.

POST /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.

Request
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"
  }'
Response — 201
{
  "meetingId": "8c1b6e2f-...-...-...-c1f2e3a4b5c6",
  "meetUrl": "https://meet.google.com/abc-defg-hij",
  "scheduledAt": "2026-05-15T15:00:00-06:00",
  "status": "SCHEDULED"
}
Errores comunes
StatusCodeCuándo
400VALIDATION_ERRORfalta contactName, email inválido, scheduledAt sin offset, etc.
400MISSING_SALES_REP_EMAILusaste X-API-Key pero no mandaste salesRepEmail
401UNAUTHORIZEDX-API-Key incorrecto o JWT expirado
502N8N_ERRORno se mandó meetingUrl y n8n falló al generar Meet
POST /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.

Request
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" }
  }'
Response — 201
{
  "meetingId": "8c1b6e2f-...-c1f2e3a4b5c6",
  "meetUrl": "https://meet.google.com/qxb-xdam-uae",
  "scheduledAt": null,
  "status": "JOINING"
}
GET /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.

Request
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"
Response — 200
{
  "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
}
GET /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.

Request
curl "https://meet.cibi.clickbalance.net/api/meetings/8c1b6e2f-..." \
  -H "X-API-Key: $SERVICE_API_KEY"
Response — 200
{
  "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 }
        ]
      }
    }
  ]
}
DELETE /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.

Request
curl -X DELETE "https://meet.cibi.clickbalance.net/api/meetings/8c1b6e2f-...?salesRepEmail=desarrollo03@clickbalance.com" \
  -H "X-API-Key: $SERVICE_API_KEY"
Response — 200
{ "ok": true, "status": "CANCELLED" }
Errores comunes
StatusCodeCuándo
403FORBIDDENla reunión no pertenece al rep autenticado
404MEETING_NOT_FOUNDel id no existe
409INVALID_STATEla reunión ya no está en SCHEDULED
POST /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.

Request
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" }'
Response — 202
{
  "jobId": "uuid-del-job",
  "meetingId": "8c1b6e2f-...",
  "status": "PROCESSING"
}
Errores comunes
StatusCodeCuándo
400VALIDATION_ERRORsales_subtype inválido o no aplica al meeting_type de la reunión
403FORBIDDENla reunión no pertenece al rep autenticado
404MEETING_NOT_FOUNDel id no existe
409TRANSCRIPT_NOT_READYel transcript aún no está disponible
PATCH /api/me — Editar nombre del rep
Request
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" }'
Response — 200
{
  "id": "uuid-del-rep",
  "email": "desarrollo03@clickbalance.com",
  "name": "Emmanuel Hernández",
  "created_at": "2026-04-29T..."
}
GET /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.

Request
curl "https://meet.cibi.clickbalance.net/api/sales-reps/<rep-id>/performance?salesRepEmail=desarrollo03@clickbalance.com" \
  -H "X-API-Key: $SERVICE_API_KEY"
Response — 200
{
  "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": [...]
}
GET /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.

Request
curl https://meet.cibi.clickbalance.net/api/cron/process-queue \
  -H "Authorization: Bearer $CRON_SECRET"
Response — 200
{ "processed": 3, "failed": 0 }
Errores comunes
StatusCodeCuándo
401UNAUTHORIZEDCRON_SECRET incorrecto o falta el header
POST /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).

Request
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?" }'
Response — 200
data: {"delta":"Quedaron "}
data: {"delta":"dos compromisos: enviar la cotización..."}
data: {"done":true,"message_id":"8c1b6e2f-..."}
Errores comunes
StatusCodeCuándo
400MISSING_PARAMfalta question en el body
404AGENT_NOT_FOUNDagentKey no existe o no es de seguimiento
409TRANSCRIPT_NOT_READYel transcript de la reunión aún no está listo

Variables de entorno (Vercel)

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 →