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). Para polling incremental: ?updatedSince=<ISO 8601> devuelve, en orden ascendente, todo lo que cambió desde ese instante (análisis, fallos y la propia fila de la reunión); ?analysisUpdatedSince= hace lo mismo pero sólo con análisis y fallos. La respuesta puede traer nextSince (cursor opaco) para la siguiente llamada — nextSince ausente significa "estás al día".
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 de cualquier tipo de reunión. Body opcional: { sales_subtype: auto|, parts: [score|summary|action_items|opportunities|facts] }. El override de subtipo solo aplica si meeting_type=ventas. `parts` recalcula SOLO esas partes y deja el resto intacto — parts:["score"] corrige la rúbrica sin pisar resumen ni tareas (que pueden estar editados a mano). Omitir parts = análisis completo.
GET /api/meetings/:id/analysis-status JWT/X-API-Key Endpoint liviano para hacer polling de un reanálisis: { status, analysisUpdatedAt, errorCode }. No resuelve media (el detalle sí, y eso llama a Recall: ~900ms vs ~50ms). La señal de "terminó" es analysisUpdatedAt. errorCode viene null salvo status=FAILED.
POST /api/meetings/:id/analyze-call JWT/X-API-Key Análisis on-demand de una llamada importada de HubSpot (source=hubspot_call): encola descarga de audio + transcripción + análisis y responde 202. Sin body. Idempotente por minuto: un reintento devuelve el mismo jobId en vez de encolar otra transcripción.
GET /api/meetings/:id/call-audio JWT/X-API-Key Stream del audio de una llamada de HubSpot (soporta Range). Mismo guard de lectura que /audio y /video.
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/abc-defg-hij",
    "salesRepEmail": "desarrollo03@clickbalance.com",
    "customerInfo": { "contactName": "Pedro García" }
  }'
Response — 201
{
  "meetingId": "8c1b6e2f-...-c1f2e3a4b5c6",
  "meetUrl": "https://meet.google.com/abc-defg-hij",
  "scheduledAt": null,
  "status": "JOINING"
}
GET /api/meetings — Listar reuniones (paginado + filtros) — incluye el contrato de polling para el CRM

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.

Polling incremental — ?analysisUpdatedSince=<valor>: el mismo parámetro acepta dos cosas: (a) un ISO 8601 (offset permitido) en la primera llamada, o (b) el cursor opaco nextSince devuelto por una llamada anterior — se manda tal cual, sin interpretarlo. Cualquier otro valor responde 400 VALIDATION_ERROR, nunca filtra en silencio.

Devuelve, en orden ASCENDENTE por effectiveUpdatedAt (el campo que se agrega a cada item en este modo), las reuniones cuyo analyses.updated_at avanzó desde ese instante, más las FAILED cuyo ended_at cae en la ventana (esas no tienen fila en analyses; effectiveUpdatedAt en ese caso ES el ended_at). Las FAILED sin ended_at quedan afuera: no hay timestamp que comparar.

Semántica de borde: con un ISO 8601, la comparación es >= (inclusiva). Con un cursor nextSince, es ESTRICTAMENTE POSTERIOR al par (effectiveUpdatedAt, meetingId) que codifica — así un backfill con miles de filas con el mismo updated_at avanza igual en vez de repetir siempre el mismo lote.

nextSince en la respuesta: viene SOLO cuando hay más por traer. Ausente o null significa "estás al día" — no hace falta comparar nada más, sólo repetir la consulta más tarde con el mismo analysisUpdatedSince. No hay un tope fijo de candidatos: el pre-filtro pide limit + 1 filas a cada fuente (analyses / FAILED), así que el .in() de la query principal nunca lleva más de limit ids — esto es a propósito: con un tope fijo de 500 el .in() podía superar el límite de tamaño de headers de PostgREST (~16 KB) y la respuesta era 500 DB_ERROR en vez de datos.

?updatedSince=<valor>: la variante que además detecta cambios de la FILA de la reunión (título, customer_info, status, responsable) vía meetings.updated_at. Mismo formato de valor, mismo cursor, misma respuesta. analysisUpdatedSince se queda como está —sólo análisis y fallos— para no cambiarle el resultado en silencio a quien ya lo usa; mandar los dos a la vez responde 400. Si una reunión cambió en más de una fuente, sale una sola vez, fechada con el timestamp más reciente.

Sin ninguno de los dos, el listado no cambia: sigue ordenado por created_at DESC y sin effectiveUpdatedAt ni nextSince.

Verificado contra producción el 2026-08-12: una ventana de 517 reuniones cambiadas se recorrió en 3 llamadas encadenadas (limit=200: 200 + 200 + 117, la última con nextSince ausente), sin ids repetidos ni faltantes.

Request
curl "https://meet.cibi.clickbalance.net/api/meetings?analysisUpdatedSince=2026-08-10T00:00:00Z&limit=200" \
  -H "X-API-Key: $SERVICE_API_KEY"

# Siguiente llamada: reusar nextSince tal cual, sin interpretarlo
curl "https://meet.cibi.clickbalance.net/api/meetings?analysisUpdatedSince=eyJ2IjoxLCJ1IjoiMjAyNi0wOC0xMFQxNjoyOToyOC43MTU5NTUrMDA6MDAiLCJpZCI6ImZkZGUyNDM0LTkyY2MtNDE0Ni04NjYwLWRkYjFkZDg5NmExYyJ9" \
  -H "X-API-Key: $SERVICE_API_KEY"
Response — 200
// Capturada contra producción el 2026-08-12 (limit=2). customer_info y
// los datos de los reps y las URLs de Meet están anonimizados a mano; ids, timestamps,
// effectiveUpdatedAt y nextSince son exactamente lo que devolvió el servidor.
{
  "data": [
    {
      "id": "45e74186-5275-4cab-8068-078a4d52660c",
      "sales_rep_id": "af5abdee-6da5-4b1c-b157-429c61d94802",
      "created_by_rep_id": "af5abdee-6da5-4b1c-b157-429c61d94802",
      "meeting_url": "https://meet.google.com/abc-defg-hij",
      "platform": "Google Meet",
      "customer_info": {
        "company": "Acme S.A.",
        "contactName": "Persona de Contacto",
        "contactEmail": "contacto@ejemplo.com",
        "dealValue": 0,
        "dealCurrency": "MXN",
        "dealValueDisplay": "$0 MXN"
      },
      "status": "COMPLETED",
      "error_code": null,
      "error_message": null,
      "meeting_type": "general",
      "started_at": "2026-08-10T15:58:02.432161+00:00",
      "sales_reps": { "id": "af5abdee-...", "name": "Vendedor Ejemplo", "email": "vendedor@clickbalance.com" },
      "created_by": { "id": "af5abdee-...", "name": "Vendedor Ejemplo", "email": "vendedor@clickbalance.com" },
      "analyses": { "sales_subtype": null, "sales_rep_score": null, "close_probability": null },
      "effectiveUpdatedAt": "2026-08-10T16:14:18.56274+00:00"
    },
    {
      "id": "fdde2434-92cc-4146-8660-ddb1dd896a1c",
      "sales_rep_id": "60a84387-e134-4eb6-80b5-1d9c4dd47402",
      "created_by_rep_id": "60a84387-e134-4eb6-80b5-1d9c4dd47402",
      "meeting_url": "https://meet.google.com/klm-nopq-rst",
      "platform": "Google Meet",
      "customer_info": { "dealCurrency": "MXN", "dealValueDisplay": "$0 MXN" },
      "status": "COMPLETED",
      "error_code": null,
      "error_message": null,
      "meeting_type": "planificacion",
      "title": "Reunión de planificación semanal",
      "started_at": "2026-08-07T17:22:10.261504+00:00",
      "sales_reps": { "id": "60a84387-...", "name": "Organizador Ejemplo", "email": "organizador@clickbalance.com" },
      "created_by": { "id": "60a84387-...", "name": "Organizador Ejemplo", "email": "organizador@clickbalance.com" },
      "analyses": { "sales_subtype": null, "sales_rep_score": null, "close_probability": null },
      "effectiveUpdatedAt": "2026-08-10T16:29:28.715955+00:00"
    }
  ],
  "total": 2,
  "page": 1,
  "nextSince": "eyJ2IjoxLCJ1IjoiMjAyNi0wOC0xMFQxNjoyOToyOC43MTU5NTUrMDA6MDAiLCJpZCI6ImZkZGUyNDM0LTkyY2MtNDE0Ni04NjYwLWRkYjFkZDg5NmExYyJ9"
}
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. Aplica a cualquier meeting_type: el único requisito es que la reunión ya tenga transcripción. Body completamente opcional: si se omite —o si se manda sales_subtype: "auto"— el subtipo se autodetecta. Si se pasa un subtipo concreto, se usa ese en lugar del autodetectado, y eso requiere 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 →