{"info":{"name":"Cibi Meet Backend","description":"Colección completa para probar el backend de Cibi Meet (Recall.ai + OpenAI + Supabase). Auth por defecto: X-API-Key + ?salesRepEmail (service-to-service). Para llamadas como usuario humano usa Bearer JWT en Authorization. Configura las variables del environment antes de usar (`Local` o `Prod`).","schema":"https://schema.getpostman.com/json/collection/v2.1.0/collection.json"},"auth":{"type":"apikey","apikey":[{"key":"key","value":"X-API-Key","type":"string"},{"key":"value","value":"{{serviceApiKey}}","type":"string"},{"key":"in","value":"header","type":"string"}]},"variable":[{"key":"baseUrl","value":"{{baseUrl}}","type":"string"},{"key":"serviceApiKey","value":"{{serviceApiKey}}","type":"string"},{"key":"authToken","value":"{{authToken}}","type":"string"},{"key":"cronSecret","value":"{{cronSecret}}","type":"string"},{"key":"salesRepEmail","value":"{{salesRepEmail}}","type":"string"},{"key":"salesRepId","value":"{{salesRepId}}","type":"string"},{"key":"targetRepId","value":"{{targetRepId}}","type":"string"},{"key":"meetingId","value":"{{meetingId}}","type":"string"},{"key":"notificationId","value":"{{notificationId}}","type":"string"}],"item":[{"name":"Meetings","item":[{"name":"POST /api/meetings/schedule — agendar (con o sin meetingUrl)","event":[{"listen":"test","script":{"type":"text/javascript","exec":["const res = pm.response.json();","if (res.meetingId) pm.collectionVariables.set('meetingId', res.meetingId);"]}}],"request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/meetings/schedule?salesRepEmail={{salesRepEmail}}","host":["{{baseUrl}}"],"path":["api","meetings","schedule"],"query":[{"key":"salesRepEmail","value":"{{salesRepEmail}}"}]},"body":{"mode":"raw","raw":"{\n  \"salesRepEmail\": \"{{salesRepEmail}}\",\n  \"salesRepName\": \"Emmanuel Hernández\",\n  \"customerInfo\": {\n    \"company\": \"Acme S.A.\",\n    \"contactName\": \"Pedro García\",\n    \"dealValue\": 50000,\n    \"dealCurrency\": \"MXN\"\n  },\n  \"scheduledAt\": \"2026-05-15T15:00:00-06:00\"\n}"},"description":"Agenda una reunión. Si omites meetingUrl, el back pide a n8n que genere un Google Meet. Si omites scheduledAt, agenda para ahora. Persiste created_by_rep_id = caller (admin que actúa como otro rep queda como creador, sales_rep_id = target). Si Recall rechaza createBot, la fila queda con status=FAILED y error_code='BOT_CREATE_FAILED' antes de propagar el 502."}},{"name":"POST /api/meetings/start — iniciar ahora (legacy)","request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/meetings/start?salesRepEmail={{salesRepEmail}}","host":["{{baseUrl}}"],"path":["api","meetings","start"],"query":[{"key":"salesRepEmail","value":"{{salesRepEmail}}"}]},"body":{"mode":"raw","raw":"{\n  \"meetingUrl\": \"https://meet.google.com/qxb-xdam-uae\",\n  \"salesRepEmail\": \"{{salesRepEmail}}\",\n  \"salesRepName\": \"Emmanuel Hernández\",\n  \"customerInfo\": {\n    \"contactName\": \"Pedro García\"\n  }\n}"}}},{"name":"POST /api/meetings/invite-bot — reunión general (sin ventas)","request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/meetings/invite-bot?salesRepEmail={{salesRepEmail}}","host":["{{baseUrl}}"],"path":["api","meetings","invite-bot"],"query":[{"key":"salesRepEmail","value":"{{salesRepEmail}}","description":"solo para auth con X-API-Key; no se guarda en la reunión"}]},"body":{"mode":"raw","raw":"{\n  \"meetingUrl\": \"https://meet.google.com/qxb-xdam-uae\",\n  \"title\": \"Sync interno con Antonio y José Daniel\"\n}"},"description":"Invita el bot a una reunión que no es de ventas. Se guarda con sales_rep_id=null, customer_info=null, meeting_type='general', y created_by_rep_id = rep autenticado (para que la UI muestre 'Organizada por X'). El cron de análisis omite el pipeline de ventas (no se generan score/oportunidades). Solo persisten transcript, video y duración. La reunión es visible en lectura para todos los reps (non-admin incluido) pero sólo el creador o admin pueden editarla/cancelarla."}},{"name":"GET /api/meetings — listar (paginado, filtros)","request":{"method":"GET","url":{"raw":"{{baseUrl}}/api/meetings?page=1&limit=20&salesRepEmail={{salesRepEmail}}","host":["{{baseUrl}}"],"path":["api","meetings"],"query":[{"key":"page","value":"1"},{"key":"limit","value":"20","description":"max 200"},{"key":"salesRepEmail","value":"{{salesRepEmail}}"},{"key":"salesRepId","value":"{{salesRepId}}","disabled":true,"description":"admin only — filtrar por otro rep"},{"key":"status","value":"COMPLETED","disabled":true,"description":"SCHEDULED|JOINING|IN_PROGRESS|PROCESSING|COMPLETED|FAILED|CANCELLED"},{"key":"type","value":"ventas","disabled":true,"description":"ventas|tecnica|documentacion|planificacion|standup|general"},{"key":"dateFrom","value":"2026-01-01","disabled":true},{"key":"dateTo","value":"2026-12-31","disabled":true},{"key":"search","value":"Acme","disabled":true,"description":"busca en title, customer_info.company y customer_info.contactName"}]},"description":"Cada item incluye `analyses[]` con sales_rep_score y close_probability, `sales_reps` joined (rep dueño de la conversación de ventas, NULL en generales), `created_by` joined (rep que programó/creó la entrada, NULL en filas pre-migración 013), `duration` (es), `started_at_formatted`, `meeting_type`, `error_code` + `error_message` (poblados sólo cuando status=FAILED). Admin sin salesRepId ve todo el equipo; con salesRepId filtra estrictamente por ese rep. Non-admin ve los suyos PLUS reuniones meeting_type ∈ (general, standup, tecnica, documentacion, planificacion) ajenas (read-only)."}},{"name":"PATCH /api/meetings/:id — editar título y/o cliente","request":{"method":"PATCH","header":[{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/meetings/{{meetingId}}?salesRepEmail={{salesRepEmail}}","host":["{{baseUrl}}"],"path":["api","meetings","{{meetingId}}"],"query":[{"key":"salesRepEmail","value":"{{salesRepEmail}}"}]},"body":{"mode":"raw","raw":"{\n  \"title\": \"Sync interno (corregido)\",\n  \"customerInfo\": {\n    \"company\": \"Acme S.A. de C.V.\",\n    \"dealValue\": 75000\n  }\n}"},"description":"Edita SOLO title y/o customerInfo. customerInfo es merge — manda solo las llaves que quieres cambiar; mándalo como null para borrar. Validación type-aware: meeting_type='general' rechaza customerInfo no-null (400 INVALID_FOR_TYPE). Owner o admin (incluso para reuniones generales/no-ventas ajenas — non-admin que LEE no implica permiso de escritura, devuelve 403 FORBIDDEN). dealValueDisplay se recalcula si cambia dealValue/dealCurrency. Para regenerar el resumen/key_points/action_items usa POST /api/meetings/:id/reanalyze."}},{"name":"POST /api/meetings/:id/reanalyze — regenerar análisis con AI","request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/meetings/{{meetingId}}/reanalyze?salesRepEmail={{salesRepEmail}}","host":["{{baseUrl}}"],"path":["api","meetings","{{meetingId}}","reanalyze"],"query":[{"key":"salesRepEmail","value":"{{salesRepEmail}}"}]},"description":"Re-corre el pipeline de AI sobre la transcripción existente. Borra el row de analyses, encola un job analyze_transcript y devuelve 202 con jobId + status PROCESSING. El cron lo procesa en el próximo tick (~1 min). Cuesta tokens de OpenAI por cada llamada. Errores: 409 TRANSCRIPT_NOT_READY si no hay transcript; 400 INVALID_FOR_TYPE para meeting_type='general' (no hay pipeline). Cuando termina, GET /api/meetings/:id devuelve el análisis nuevo."}},{"name":"GET /api/meetings/:id — detalle completo","request":{"method":"GET","url":{"raw":"{{baseUrl}}/api/meetings/{{meetingId}}?salesRepEmail={{salesRepEmail}}","host":["{{baseUrl}}"],"path":["api","meetings","{{meetingId}}"],"query":[{"key":"salesRepEmail","value":"{{salesRepEmail}}"}]},"description":"Devuelve { meeting, transcript, media }. meeting incluye sales_reps (dueño venta), created_by (creador), duration, duration_seconds, started_at_formatted, error_code/error_message si status=FAILED. transcript incluye speakers (string[]) y raw_transcript. media trae signed URLs (1h). Non-admin recibe 404 sólo en reuniones de ventas ajenas; reuniones tipo general/standup/tecnica/documentacion/planificacion son visibles aunque sean de otro rep (lectura). Para escribir (PATCH/DELETE/reanalyze) sigue siendo owner+admin."}},{"name":"GET /api/meetings/:id/video — signed URL del video","request":{"method":"GET","url":{"raw":"{{baseUrl}}/api/meetings/{{meetingId}}/video?salesRepEmail={{salesRepEmail}}","host":["{{baseUrl}}"],"path":["api","meetings","{{meetingId}}","video"],"query":[{"key":"salesRepEmail","value":"{{salesRepEmail}}"}]},"description":"URL firmada por 1h. 409 VIDEO_NOT_READY si el download cron aún no copió el MP4 ni Recall lo tiene. source='supabase' = del bucket (permanente); source='recall' = presigned URL de Recall (expira ~5h). Recall borra la grabación a los 30 días post-creada (config retention timed=720h), después de eso sólo queda lo que esté en Supabase Storage. Misma regla de visibilidad que GET /:id (non-admin puede ver video de generales/no-ventas ajenas)."}},{"name":"DELETE /api/meetings/:id — cancelar agenda","request":{"method":"DELETE","url":{"raw":"{{baseUrl}}/api/meetings/{{meetingId}}?salesRepEmail={{salesRepEmail}}","host":["{{baseUrl}}"],"path":["api","meetings","{{meetingId}}"],"query":[{"key":"salesRepEmail","value":"{{salesRepEmail}}"}]},"description":"Solo cuando status=SCHEDULED. Llama a Recall best-effort para sacar el bot. Owner o admin — non-admin con visibilidad de lectura sobre una general ajena recibe 403, no puede cancelarla."}}]},{"name":"Notifications","item":[{"name":"GET /api/notifications — últimas 20 + unreadCount","request":{"method":"GET","url":{"raw":"{{baseUrl}}/api/notifications?salesRepEmail={{salesRepEmail}}","host":["{{baseUrl}}"],"path":["api","notifications"],"query":[{"key":"salesRepEmail","value":"{{salesRepEmail}}"}]}}},{"name":"PATCH /api/notifications/:id/read","request":{"method":"PATCH","url":{"raw":"{{baseUrl}}/api/notifications/{{notificationId}}/read?salesRepEmail={{salesRepEmail}}","host":["{{baseUrl}}"],"path":["api","notifications","{{notificationId}}","read"],"query":[{"key":"salesRepEmail","value":"{{salesRepEmail}}"}]}}},{"name":"POST /api/notifications/mark-all-read","request":{"method":"POST","url":{"raw":"{{baseUrl}}/api/notifications/mark-all-read?salesRepEmail={{salesRepEmail}}","host":["{{baseUrl}}"],"path":["api","notifications","mark-all-read"],"query":[{"key":"salesRepEmail","value":"{{salesRepEmail}}"}]}}}]},{"name":"Profile","item":[{"name":"GET /api/me — perfil del rep (incluye isAdmin)","request":{"method":"GET","url":{"raw":"{{baseUrl}}/api/me?salesRepEmail={{salesRepEmail}}","host":["{{baseUrl}}"],"path":["api","me"],"query":[{"key":"salesRepEmail","value":"{{salesRepEmail}}"}]},"description":"Devuelve { id, email, name, isAdmin, created_at }."}},{"name":"PATCH /api/me — editar nombre","request":{"method":"PATCH","header":[{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/me?salesRepEmail={{salesRepEmail}}","host":["{{baseUrl}}"],"path":["api","me"],"query":[{"key":"salesRepEmail","value":"{{salesRepEmail}}"}]},"body":{"mode":"raw","raw":"{\n  \"name\": \"Emmanuel Hernández\"\n}"}}}]},{"name":"Sales Reps","item":[{"name":"GET /api/sales-reps — lista (admin only)","request":{"method":"GET","url":{"raw":"{{baseUrl}}/api/sales-reps?salesRepEmail={{salesRepEmail}}","host":["{{baseUrl}}"],"path":["api","sales-reps"],"query":[{"key":"salesRepEmail","value":"{{salesRepEmail}}"}]},"description":"403 si caller no es admin. Devuelve { data: [{ id, name, email, is_admin, created_at }] }."}},{"name":"GET /api/sales-reps/by-email","request":{"method":"GET","url":{"raw":"{{baseUrl}}/api/sales-reps/by-email?email={{salesRepEmail}}&salesRepEmail={{salesRepEmail}}","host":["{{baseUrl}}"],"path":["api","sales-reps","by-email"],"query":[{"key":"email","value":"{{salesRepEmail}}","description":"rep a buscar"},{"key":"salesRepEmail","value":"{{salesRepEmail}}","description":"auth"}]}}},{"name":"PATCH /api/sales-reps/:id — toggle is_admin (admin only)","request":{"method":"PATCH","header":[{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/sales-reps/{{targetRepId}}?salesRepEmail={{salesRepEmail}}","host":["{{baseUrl}}"],"path":["api","sales-reps","{{targetRepId}}"],"query":[{"key":"salesRepEmail","value":"{{salesRepEmail}}"}]},"body":{"mode":"raw","raw":"{\n  \"is_admin\": true\n}"},"description":"403 si caller no es admin. 400 CANNOT_MODIFY_SELF si caller_id===:id. 404 si rep no existe. Devuelve el rep actualizado."}},{"name":"GET /api/sales-reps/:id/performance","request":{"method":"GET","url":{"raw":"{{baseUrl}}/api/sales-reps/{{salesRepId}}/performance?salesRepEmail={{salesRepEmail}}","host":["{{baseUrl}}"],"path":["api","sales-reps","{{salesRepId}}","performance"],"query":[{"key":"salesRepEmail","value":"{{salesRepEmail}}"}]},"description":"Devuelve { salesRepId, name, totalMeetings, avgScore, estimatedCloseRate, topOpportunities, recentMeetings }. Non-admin solo puede ver su propio :id."}},{"name":"GET /api/sales-reps/:id/performance/breakdown","request":{"method":"GET","url":{"raw":"{{baseUrl}}/api/sales-reps/{{salesRepId}}/performance/breakdown?days=30&salesRepEmail={{salesRepEmail}}","host":["{{baseUrl}}"],"path":["api","sales-reps","{{salesRepId}}","performance","breakdown"],"query":[{"key":"days","value":"30","description":"default 30, max 365"},{"key":"salesRepEmail","value":"{{salesRepEmail}}"}]},"description":"Promedio de las 5 dimensiones (apertura, descubrimiento, presentacion_valor, manejo_objeciones, cierre) escala 0-20 c/u. { data, sampleSize, windowDays }. Excluye abstains y meetings no-ventas. Non-admin solo su :id."}},{"name":"GET /api/sales-reps/:id/performance/weekly","request":{"method":"GET","url":{"raw":"{{baseUrl}}/api/sales-reps/{{salesRepId}}/performance/weekly?weeks=8&salesRepEmail={{salesRepEmail}}","host":["{{baseUrl}}"],"path":["api","sales-reps","{{salesRepId}}","performance","weekly"],"query":[{"key":"weeks","value":"8","description":"default 8, max 52"},{"key":"salesRepEmail","value":"{{salesRepEmail}}"}]},"description":"Score evolution por semana ISO (lunes, CDMX). { data: [{ weekStart, avgScore, count }], windowWeeks }. Semanas vacías NO se emiten — el front rellena gaps si quiere."}}]},{"name":"Admin","item":[{"name":"GET /api/admin/team-performance","request":{"method":"GET","url":{"raw":"{{baseUrl}}/api/admin/team-performance?salesRepEmail={{salesRepEmail}}","host":["{{baseUrl}}"],"path":["api","admin","team-performance"],"query":[{"key":"salesRepEmail","value":"{{salesRepEmail}}"}]},"description":"Reemplaza el N+1 de /admin (1 call por rep). Admin only. Devuelve { data: [{ id, name, email, is_admin, totalMeetings, completedMeetings, avgScore, estimatedCloseRate, lastMeetingAt }] }."}}]},{"name":"Cron (Vercel)","auth":{"type":"bearer","bearer":[{"key":"token","value":"{{cronSecret}}","type":"string"}]},"item":[{"name":"GET /api/cron/process-queue","request":{"method":"GET","url":{"raw":"{{baseUrl}}/api/cron/process-queue","host":["{{baseUrl}}"],"path":["api","cron","process-queue"]},"description":"Vercel lo dispara cada minuto. Procesa un job por tick. Auth: Authorization: Bearer ${CRON_SECRET}."}}]},{"name":"Webhooks (referencia)","auth":{"type":"noauth"},"item":[{"name":"POST /api/webhooks/recall — webhook firmado por Recall","request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"svix-id","value":"<set-by-svix>"},{"key":"svix-timestamp","value":"<set-by-svix>"},{"key":"svix-signature","value":"<set-by-svix>"}],"url":{"raw":"{{baseUrl}}/api/webhooks/recall","host":["{{baseUrl}}"],"path":["api","webhooks","recall"]},"body":{"mode":"raw","raw":"{\n  \"event\": \"transcript.done\",\n  \"data\": { \"bot\": { \"id\": \"recall_bot_id\" } }\n}"},"description":"Solo referencia. Las pruebas reales se disparan desde el dashboard de Recall.ai con firma Svix válida."}}]}]}