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). 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| |
| 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). |
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/abc-defg-hij",
"salesRepEmail": "desarrollo03@clickbalance.com",
"customerInfo": { "contactName": "Pedro García" }
}'
{
"meetingId": "8c1b6e2f-...-c1f2e3a4b5c6",
"meetUrl": "https://meet.google.com/abc-defg-hij",
"scheduledAt": null,
"status": "JOINING"
}
/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.
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"
// 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"
}
/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. 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 sí requiere 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 →