{"openapi":"3.0.3","info":{"title":"KANDIMA API","description":"REST API pública de KANDIMA — el AI Operating System para ecommerce LATAM. Todos los endpoints requieren Bearer auth con API keys generadas desde el dashboard.","version":"1.0.0","contact":{"name":"KANDIMA Support","email":"soporte@kandima.ai","url":"https://kandima.ai/contact"},"license":{"name":"Proprietary"}},"servers":[{"url":"https://kandima.ai","description":"Production"}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"ka_live_xxx (or ka_test_xxx in non-prod)"}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string","example":"missing_authorization"}}},"Profile":{"type":"object","nullable":true,"properties":{"plan":{"type":"string","example":"pro"},"currency":{"type":"string","example":"USD"},"timezone":{"type":"string","example":"America/Argentina/Buenos_Aires"},"created_at":{"type":"string","format":"date-time"}}},"Me":{"type":"object","properties":{"user_id":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}},"profile":{"$ref":"#/components/schemas/Profile"},"api_version":{"type":"string","example":"v1"}}},"Credits":{"type":"object","properties":{"user_id":{"type":"string"},"balance":{"type":"integer","example":1250},"total_purchased":{"type":"integer","example":5000},"total_spent":{"type":"integer","example":3750},"updated_at":{"type":"string","format":"date-time","nullable":true},"auto_topup":{"type":"object","nullable":true,"properties":{"enabled":{"type":"boolean"},"threshold":{"type":"integer"},"top_up_amount":{"type":"integer"},"last_charged_at":{"type":"string","format":"date-time","nullable":true}}},"recent_transactions":{"type":"array","items":{"type":"object","properties":{"kind":{"type":"string"},"feature":{"type":"string"},"amount":{"type":"integer"},"balance_after":{"type":"integer"},"created_at":{"type":"string","format":"date-time"},"meta":{"type":"object"}}}}}},"Customer":{"type":"object","properties":{"id":{"type":"integer"},"email":{"type":"string","nullable":true},"name":{"type":"string","nullable":true},"phone":{"type":"string","nullable":true},"source":{"type":"string","nullable":true},"external_id":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"country":{"type":"string","nullable":true},"tags":{"type":"array","items":{"type":"string"},"nullable":true},"unsubscribed":{"type":"boolean","nullable":true},"orders_count":{"type":"integer","nullable":true},"revenue_total":{"type":"number","nullable":true},"ltv_estimate":{"type":"number","nullable":true},"aov":{"type":"number","nullable":true},"segment":{"type":"string","nullable":true},"churn_score":{"type":"number","nullable":true},"refunds_count":{"type":"integer","nullable":true},"last_purchase_at":{"type":"string","format":"date-time","nullable":true},"first_purchase_at":{"type":"string","format":"date-time","nullable":true},"created_at":{"type":"string","format":"date-time"}}},"CustomerEvent":{"type":"object","properties":{"id":{"type":"integer"},"kind":{"type":"string","description":"purchase | refund | signup | review | message | tag_added | flow_triggered | note"},"amount_usd":{"type":"number","nullable":true},"occurred_at":{"type":"string","format":"date-time"},"payload":{"type":"object","additionalProperties":true,"nullable":true},"source":{"type":"string","nullable":true,"description":"api | manual | shopify | tienda-nube | mercado-libre"}}},"DuplicateContactError":{"type":"object","properties":{"error":{"type":"string","enum":["duplicate_contact"]},"existing":{"$ref":"#/components/schemas/Customer"}}},"DuplicateNameError":{"type":"object","description":"Devuelto por POST/PATCH de saved-segments cuando el nuevo `name` colisiona con otro segmento del workspace (case-insensitive). El integrador puede mostrar 'ya existe' al usuario o hacer PATCH al `existing.id`.","properties":{"error":{"type":"string","enum":["duplicate_name"]},"existing":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"}}},"api_version":{"type":"string","enum":["v1"]}}},"SavedSegment":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"rules":{"type":"array","items":{"type":"object","additionalProperties":true}},"customer_count":{"type":"integer"},"color":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"Integration":{"type":"object","properties":{"provider":{"type":"string","example":"shopify"},"shop":{"type":"string","nullable":true},"scopes":{"type":"array","items":{"type":"string"}},"status":{"type":"string","enum":["connected","stale","revoked","never_synced"]},"connected_at":{"type":"string","format":"date-time"},"revoked_at":{"type":"string","format":"date-time","nullable":true},"last_sync_at":{"type":"string","format":"date-time","nullable":true},"last_sync_minutes_ago":{"type":"integer","nullable":true}}},"Webhook":{"type":"object","properties":{"id":{"type":"integer"},"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string"}},"active":{"type":"boolean"},"last_fired_at":{"type":"string","format":"date-time","nullable":true},"last_status":{"type":"integer","nullable":true},"fail_count":{"type":"integer"},"total_fired":{"type":"integer"},"total_failed":{"type":"integer"},"created_at":{"type":"string","format":"date-time"}}},"Insight":{"type":"object","properties":{"id":{"type":"integer"},"severity":{"type":"string","enum":["low","medium","high","critical","info","warning"]},"category":{"type":"string","description":"churn_risk, anomaly, ad_performance, inventory, creative, system"},"title":{"type":"string"},"body":{"type":"string"},"suggested_action":{"type":"string","nullable":true},"impact_estimate":{"type":"number","nullable":true},"status":{"type":"string","enum":["open","resolved","dismissed"]},"computed_at":{"type":"string","format":"date-time"},"resolved_at":{"type":"string","format":"date-time","nullable":true},"metadata":{"type":"object","nullable":true,"description":"Cron-specific payload. For churn_risk: { decline_pct, current_30d, prev_30d, orders_current }","additionalProperties":true}}},"ProfitSnapshot":{"type":"object","properties":{"snapshot_date":{"type":"string","format":"date"},"gross_revenue":{"type":"number"},"refunds":{"type":"number"},"ad_spend":{"type":"number"},"cogs_estimate":{"type":"number"},"fees_estimate":{"type":"number"},"shipping_estimate":{"type":"number"},"extra_expenses":{"type":"number"},"net_revenue":{"type":"number"},"operational_profit":{"type":"number"},"final_profit":{"type":"number"},"margin_pct":{"type":"number"},"purchase_count":{"type":"integer"}}},"Anomaly":{"type":"object","properties":{"id":{"type":"integer"},"metric":{"type":"string"},"observed_value":{"type":"number"},"expected_value":{"type":"number"},"z_score":{"type":"number"},"direction":{"type":"string","enum":["up","down"]},"severity":{"type":"string","enum":["low","medium","high","critical"]},"detected_at":{"type":"string","format":"date-time"},"inferred_cause":{"type":"string","nullable":true},"confidence":{"type":"number","nullable":true},"correlated_metrics":{"type":"array","items":{"type":"object"},"nullable":true}}}}},"security":[{"BearerAuth":[]}],"paths":{"/api/v1/status":{"get":{"summary":"API status check — no auth required","security":[],"responses":{"200":{"description":"Operational or degraded","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["operational","degraded","outage"]},"api_version":{"type":"string"},"checks":{"type":"object","properties":{"database":{"type":"object","properties":{"status":{"type":"string","enum":["ok","degraded","down"]},"latency_ms":{"type":"integer"}}}}},"meta":{"type":"object","properties":{"region":{"type":"string"},"env":{"type":"string"},"commit":{"type":"string"},"check_total_ms":{"type":"integer"}}},"ts":{"type":"string","format":"date-time"}}}}}},"503":{"description":"Outage"}}}},"/api/v1/incidents":{"get":{"summary":"Public list of active incidents (services con uptime <80% últimas 6h)","security":[],"responses":{"200":{"description":"Incidents list","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["operational","degraded","outage","unknown"]},"window_hours":{"type":"integer"},"threshold_pct":{"type":"integer"},"services_with_data":{"type":"integer"},"incidents":{"type":"array","items":{"type":"object","properties":{"service":{"type":"string"},"label":{"type":"string"},"severity":{"type":"string","enum":["minor","major","critical"]},"started_at":{"type":"string","format":"date-time"},"uptime_pct":{"type":"number"},"samples":{"type":"integer"}}}},"ts":{"type":"string","format":"date-time"}}}}}}}}},"/api/v1/uptime":{"get":{"summary":"Public uptime % per upstream service derived from system_health","security":[],"parameters":[{"name":"hours","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":168,"default":24}}],"responses":{"200":{"description":"Per-service uptime %","content":{"application/json":{"schema":{"type":"object","properties":{"hours":{"type":"integer"},"services":{"type":"array","items":{"type":"object","properties":{"service":{"type":"string"},"label":{"type":"string"},"uptime_pct":{"type":"number","nullable":true},"avg_latency_ms":{"type":"integer","nullable":true},"samples":{"type":"integer"}}}},"summary":{"type":"object","properties":{"overall_uptime_pct":{"type":"number","nullable":true},"services_tracked":{"type":"integer"}}},"ts":{"type":"string","format":"date-time"}}}}}},"503":{"description":"DB unavailable"}}}},"/api/v1/me":{"get":{"summary":"Get authenticated user profile","security":[{"BearerAuth":["read"]}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/credits":{"get":{"summary":"Get credit balance + recent transactions","security":[{"BearerAuth":["read"]}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Credits"}}}}}}},"/api/v1/usage":{"get":{"summary":"Self-billing usage breakdown (credits + OpenAI tokens + cost USD)","description":"Returns by_feature breakdown of credits debited and OpenAI tokens/cost (USD), plus daily series. Period: current_month (default), last_7d, last_30d.","security":[{"BearerAuth":["read"]}],"parameters":[{"name":"period","in":"query","schema":{"type":"string","enum":["current_month","last_7d","last_30d"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/v1/integraciones":{"get":{"summary":"List supported integrations (public, no auth)","description":"18 integrations grouped by category (ecommerce/ads/messaging/payments/analytics/ai). License CC BY 4.0.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/v1/alternativas":{"get":{"summary":"List honest alternatives comparisons (public, no auth)","description":"Comparativas vs Triple Whale, Escalafy, Northbeam, Shopify Analytics, Polar Analytics. CC BY 4.0.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/v1/casos-de-uso":{"get":{"summary":"List use cases by ecommerce vertical (public, no auth)","description":"8 verticals (moda, beauty, pet, suplementos, electro, hogar, fitness, agencia) con KPIs/stack/big_win. CC BY 4.0.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/v1/benchmarks":{"get":{"summary":"Ecommerce LATAM 2026 benchmarks por vertical (public, no auth)","description":"8 verticales × 6 KPIs (True ROAS, MER, CAC, AOV, LTV 12m, Frequency 12m). KPI targets + pixel coverage post-iOS 18 stats. Datos del Report KANDIMA mayo 2026 — útil para LLM citations, journalism, RAG ingestion. CC BY 4.0.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/v1/calculadoras":{"get":{"summary":"List ecommerce calculators with formulas (public, no auth)","description":"20 calculadoras: ROAS Real, MER, LTV/CAC, Stock Runway, CAC Payback ML, Frequency 12m, ROAS objetivo según margen, AOV bundles, Cohort M3 healthcheck, Ad Fatigue, Profit per Order. CC BY 4.0.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/v1/glosario":{"get":{"summary":"Ecommerce KPI glossary with formulas (public, no auth)","description":"56 términos de analytics/ads/profit ecommerce (True ROAS, MER, LTV:CAC, Contribution Margin, Break-even ROAS, RFM, Cohort Retention, MMM…) con definición, fórmula y categoría. CC BY 4.0.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/v1/predictions":{"get":{"summary":"10 predicciones ecommerce LATAM 2026-2027 (public, no auth)","description":"10 predicciones del Report KANDIMA mayo 2026 con confidence (high/medium/low), horizon, KPIs relevantes y términos del glosario. CC BY 4.0.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/v1/reports":{"get":{"summary":"Lista de industry reports KANDIMA (public, no auth)","description":"Lista todos los reports con metadata: title, description, sections, read_time_minutes, coverage_countries, topics, URLs HTML + API datasets. CC BY 4.0.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/v1/feature-costs":{"get":{"summary":"Get AI feature pricing in credits (public, no auth)","description":"71 AI features with credits cost + USD estimate. Top-up packs included. Transparency.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/v1/changelog":{"get":{"summary":"Get product changelog as JSON (public, no auth)","description":"Same source as the RSS feed at /changelog/feed.xml. Use for shipping cadence tracking.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/v1/blog":{"get":{"summary":"List blog posts (public, no auth)","description":"Returns the full KANDIMA blog index. Public, cacheable. Useful for RAG ingestion, Zapier/n8n syncing or building custom feeds. License CC BY 4.0 — attribution required.","parameters":[{"name":"tag","in":"query","schema":{"type":"string"}},{"name":"q","in":"query","schema":{"type":"string"},"description":"Full-text search (title + excerpt)"},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}},{"name":"include_body","in":"query","schema":{"type":"string","enum":["1"]},"description":"Include full markdown body in response"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"total":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"},"next_offset":{"type":"integer","nullable":true},"items":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string"},"title":{"type":"string"},"date":{"type":"string","format":"date"},"read_mins":{"type":"integer"},"tag":{"type":"string"},"excerpt":{"type":"string"},"url":{"type":"string","format":"uri"},"body_markdown":{"type":"string","nullable":true}}}}}}}}}}}},"/api/v1/customers":{"get":{"summary":"List customers (cursor paginated)","description":"Use `?format=csv` for a CSV dump of the current page.","security":[{"BearerAuth":["read"]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","default":25,"maximum":100}},{"name":"cursor","in":"query","schema":{"type":"integer"}},{"name":"source","in":"query","description":"Filtra por origen del customer. Whitelist estricta (iter 178): kind no listado devuelve 400 con `valid_sources`. Incluye `api` (creado vía POST /v1/customers) y `manual` (alta desde la UI) además de los 3 connectors de webhook/sync.","schema":{"type":"string","enum":["shopify","tienda-nube","mercado-libre","api","manual"]}},{"name":"segment","in":"query","description":"Stock RFM segment (iter 165). Kind no listado → 400 con `valid_segments` en el body. Sirve el índice (user_id, segment).","schema":{"type":"string","enum":["whale","vip","loyal","returning","new","one_time","at_risk","churned"]}},{"name":"unsubscribed","in":"query","description":"Filter opt-in/opt-out del newsletter (iter 165). Use case típico: alimentar Mailchimp/Klaviyo con sólo los suscriptos.","schema":{"type":"string","enum":["true","false"]}},{"name":"tag","in":"query","description":"Filter por tag del customer (iter 166). Normalizamos con trim+lowercase+strip# antes de comparar — 'VIP' matchea 'vip'. Tag normalizado vacío → 400 invalid_tag. Use case: 'sync con Mailchimp el segmento campaign-blackfriday'.","schema":{"type":"string"}},{"name":"since","in":"query","description":"Filter incremental — devuelve sólo customers con `updated_at >= since` (iter 170). Pensado para cron de sync con CRM externo (Klaviyo / Mailchimp / HubSpot) que quiere los movimientos desde su last_sync sin paginar todo el workspace. Acepta ISO 8601 (`2026-05-20T00:00:00Z`), date-only (`2026-05-20`) o epoch ms. Formato no parseable → 400 `invalid_since`. Pre-iter había que paginar con cursor desde el principio y descartar client-side. El modelo `updated_at` está cerrado end-to-end (iters 159-169) en TODA superficie de mutación, así que el filtro no se pierde cambios.","schema":{"type":"string","format":"date-time"}},{"name":"format","in":"query","schema":{"type":"string","enum":["csv"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Customer"}},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"next_cursor":{"type":"integer","nullable":true},"has_more":{"type":"boolean"}}}}}},"text/csv":{"schema":{"type":"string"}}}}}},"post":{"summary":"Create a customer (idempotent upsert when external_id is provided)","description":"Without `external_id`, dedupes by email (ilike) then phone (eq) within the workspace. Match → 409 `duplicate_contact` with the existing customer.\n\nCon `external_id`, el endpoint hace UPSERT por `(user_id, external_id)`. La response incluye `created: boolean` para distinguir si esa request creó la fila (`true`) o sólo actualizó una existente (`false`). El status 201 se devuelve para ambos casos (no-breaking con integradores viejos que ignoran el campo) — el nuevo flag es la fuente de verdad si el integrador necesita auditar cuántos contactos eran nuevos vs re-importados.","security":[{"BearerAuth":["write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","nullable":true},"email":{"type":"string","nullable":true},"phone":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"country":{"type":"string","nullable":true},"tags":{"type":"array","items":{"type":"string"}},"notes":{"type":"string","nullable":true},"unsubscribed":{"type":"boolean"},"external_id":{"type":"string","description":"Cuando viene, el POST es upsert idempotente por (user_id, external_id)"}}}}}},"responses":{"201":{"description":"Created (o actualizado — ver `created` en el body)","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Customer"},"created":{"type":"boolean","description":"`true` si esta request creó la fila; `false` si fue un upsert que actualizó una existente (sólo posible con `external_id`)."},"api_version":{"type":"string","example":"v1"}}}}}},"400":{"description":"Body inválido — name/email/phone faltante, tags no-array, etc."},"409":{"description":"duplicate_contact","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DuplicateContactError"}}}}}}},"/api/v1/customers/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"get":{"summary":"Get a customer + its recent timeline (last 100)","description":"`events_total` y `events_truncated` indican si hay más eventos — usar GET `/v1/customers/{id}/events?cursor=…` para paginar.","security":[{"BearerAuth":["read"]}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Customer"},"events":{"type":"array","items":{"$ref":"#/components/schemas/CustomerEvent"}},"events_total":{"type":"integer","nullable":true},"events_truncated":{"type":"boolean"}}}}}},"404":{"description":"Customer no encontrado o de otro workspace"}}},"patch":{"summary":"Update contact data (whitelist)","description":"Editable: name/email/phone/city/country/tags/notes/unsubscribed. Si el nuevo email/phone pertenece a OTRO cliente del workspace → 409 `duplicate_contact`. `external_id` no editable (rompería idempotencia del upsert).","security":[{"BearerAuth":["write"]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","nullable":true},"email":{"type":"string","nullable":true},"phone":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"country":{"type":"string","nullable":true},"tags":{"type":"array","items":{"type":"string"}},"notes":{"type":"string","nullable":true},"unsubscribed":{"type":"boolean"}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Customer"}}}}}},"400":{"description":"Body inválido o sin campos editables"},"404":{"description":"Customer no encontrado"},"409":{"description":"duplicate_contact","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DuplicateContactError"}}}}}},"delete":{"summary":"Delete a customer and cascade its events","description":"Borra el customer; los events asociados se borran por `on delete cascade` de la FK. Escribe al audit_log con action=customer.deleted.","security":[{"BearerAuth":["write"]}],"responses":{"200":{"description":"OK { ok: true }"},"404":{"description":"Customer no encontrado"}}}},"/api/v1/customers/{id}/events":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"get":{"summary":"List a customer timeline (cursor paginated)","description":"Use `?format=csv` para dumpear hasta 5000 events; header `X-Truncated` avisa si excede.\n\nFiltro opcional `?kind=` para listar sólo un subset (típico: `?kind=refund` para auditoría de fricción; `?kind=note` para sincronizar con otro CRM). El whitelist es estricto — un kind no listado devuelve 400 con `valid_kinds` en el body para que el integrador detecte el typo inmediatamente. Aplica tanto al GET paginado como al CSV.","security":[{"BearerAuth":["read"]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","default":25,"maximum":100}},{"name":"cursor","in":"query","schema":{"type":"integer"}},{"name":"format","in":"query","schema":{"type":"string","enum":["csv"]}},{"name":"kind","in":"query","description":"Filtra el timeline por tipo de event. `purchase` y `refund` son de sistema (los syncs/webhooks los generan); `note` y `message` son manuales.","schema":{"type":"string","enum":["purchase","refund","note","message"]}},{"name":"since","in":"query","description":"Filter incremental — devuelve sólo events con `occurred_at >= since` (iter 173). Simétrico al `?since=` del GET de lista (iter 172): pensado para cron de sync del timeline a Zendesk/Help Scout/CRM externo filtrando por last_sync. Acepta ISO 8601 (`2026-05-20T00:00:00Z`), date-only (`2026-05-20`) o epoch ms. Formato no parseable → 400 `invalid_since`. Aplica tanto al GET paginado como al CSV.","schema":{"type":"string","format":"date-time"}},{"name":"source","in":"query","description":"Filtra events por connector de origen (iter 182). Workflow típico: integrador multi-store filtra refunds/purchases de un connector específico (auditoría de fricción por shop). Whitelist estricto: kind no listado → 400 `invalid_source` con `valid_sources` en el body. Mismo `CUSTOMER_SOURCE_VALUES` que el GET de lista — los events comparten taxonomía con customers. Aplica tanto al GET paginado como al CSV.","schema":{"type":"string","enum":["shopify","tienda-nube","mercado-libre","api","manual"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CustomerEvent"}},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"next_cursor":{"type":"integer","nullable":true},"has_more":{"type":"boolean"}}}}}},"text/csv":{"schema":{"type":"string"}}}},"400":{"description":"kind no soportado (devuelve `valid_kinds` en el body)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","example":"invalid_kind"},"valid_kinds":{"type":"array","items":{"type":"string"}}}}}}},"404":{"description":"Customer no encontrado"}}},"post":{"summary":"Log a manual note/message on the customer timeline","description":"Sólo kind ∈ {note, message}. El event queda con `source=\"api\"` para distinguirlo de los manuales de la UI.","security":[{"BearerAuth":["write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["text"],"properties":{"kind":{"type":"string","enum":["note","message"],"default":"note"},"text":{"type":"string","maxLength":2000}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","properties":{"event":{"$ref":"#/components/schemas/CustomerEvent"}}}}}},"400":{"description":"kind no permitido / text vacío / text demasiado largo"},"404":{"description":"Customer no encontrado"}}}},"/api/v1/customers/{id}/events/{eventId}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"eventId","in":"path","required":true,"schema":{"type":"integer"}}],"delete":{"summary":"Delete a manual event created by API","description":"Sólo borra events con `source=\"api\"` y kind ∈ {note, message} — no toca notas de la UI ni events de sistema.","security":[{"BearerAuth":["write"]}],"responses":{"200":{"description":"OK { ok: true }"},"404":{"description":"Event no encontrado o no es del API"}}}},"/api/v1/customers/cohorts":{"get":{"summary":"Retention cohort matrix (M0..M24)","description":"Use `?format=csv` para CSV long (BI tools).","security":[{"BearerAuth":["read"]}],"parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["csv"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"cohort_month":{"type":"string","format":"date"},"cohort_size":{"type":"integer"},"retention":{"type":"array","items":{"type":"object","properties":{"month_offset":{"type":"integer"},"cohort_size":{"type":"integer"},"retained_customers":{"type":"integer"},"retention_pct":{"type":"number","nullable":true,"description":"null cuando el mes-offset está en curso (in_progress) — retención parcial no publicable como final"},"in_progress":{"type":"boolean","description":"el mes-offset todavía no cerró; retention_pct viene null"},"revenue_in_period":{"type":"number"}}}}}}},"summary":{"type":"object","properties":{"cohort_count":{"type":"integer"},"customers_total":{"type":"integer"},"avg_retention_m1":{"type":"number"},"avg_retention_m3":{"type":"number"}},"nullable":true}}}},"text/csv":{"schema":{"type":"string"}}}}}}},"/api/v1/customers/saved-segments":{"get":{"summary":"List custom segments (rules + counts)","security":[{"BearerAuth":["read"]}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SavedSegment"}}}}}}}}},"post":{"summary":"Create a custom segment","description":"Crea un segmento custom con reglas validadas. Si el `name` colisiona con otro segmento del workspace (case-insensitive), devuelve 409 `duplicate_name` con `existing` para que el integrador decida (merge / rename / abort).","security":[{"BearerAuth":["write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","rules"],"properties":{"name":{"type":"string","maxLength":80},"rules":{"type":"array","items":{"type":"object","additionalProperties":true},"description":"Lista de reglas `{ metric, comparator, value }` (validada por validateRules)"},"description":{"type":"string","maxLength":280,"nullable":true},"color":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$","default":"#22ff88"}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SavedSegment"},"api_version":{"type":"string","enum":["v1"]}}}}}},"409":{"description":"Duplicate name (segment with the same name already exists)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DuplicateNameError"}}}}}}},"/api/v1/customers/saved-segments/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"get":{"summary":"Get a single custom segment","description":"Lectura puntual; útil para refrescar una fila tras PATCH externo.","security":[{"BearerAuth":["read"]}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SavedSegment"},"api_version":{"type":"string","enum":["v1"]}}}}}},"404":{"description":"Not found"}}},"patch":{"summary":"Edit a custom segment (name, rules, description, color)","description":"Si el nuevo `name` colisiona con otro segmento del workspace devuelve 409 `duplicate_name`. Renombrar al nombre actual sigue siendo no-op válido.","security":[{"BearerAuth":["write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":80},"rules":{"type":"array","items":{"type":"object","additionalProperties":true}},"description":{"type":"string","maxLength":280,"nullable":true},"color":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$"}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SavedSegment"},"api_version":{"type":"string","enum":["v1"]}}}}}},"404":{"description":"Not found"},"409":{"description":"Duplicate name","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DuplicateNameError"}}}}}},"delete":{"summary":"Delete a custom segment","description":"Borra el segmento. La tabla no tiene CASCADE — los customers que matcheaban las reglas NO se ven afectados (la asociación es runtime, no por FK).","security":[{"BearerAuth":["write"]}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"id":{"type":"integer"},"api_version":{"type":"string","enum":["v1"]}}}}}},"404":{"description":"Not found"}}}},"/api/v1/customers/tags":{"get":{"summary":"Top tags del workspace con count (iter 175)","description":"Discovery de la taxonomy de tags. SELECT plano sobre los 5000 customers más recientes (por `updated_at desc`) + agregación en Node, top 50 por count. `scan_truncated` avisa cuando un workspace >5k podría tener tags no representados en la muestra. Use case: integrador arma mappings de tags Kandima ↔ tags de un CRM externo sin descargar la lista entera de customers; dashboard que muestra los 10 tags más activos.","security":[{"BearerAuth":["read"]}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"tags":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"count":{"type":"integer"}}}},"scan_truncated":{"type":"boolean"},"scanned":{"type":"integer"},"api_version":{"type":"string","enum":["v1"]}}}}}}}}},"/api/v1/customers/saved-segments/{id}/customers":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"get":{"summary":"List customers matching a custom segment (offset paginated)","description":"Evalúa las reglas en memoria contra el scan (cap SCAN_LIMIT=50000). `scan_truncated` + `total_customers_global` avisan si el workspace excedió el cap (los matches son subconteo). Use `?format=csv` para dump completo.","security":[{"BearerAuth":["read"]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","default":25,"maximum":100}},{"name":"offset","in":"query","schema":{"type":"integer"}},{"name":"tag","in":"query","description":"Filter por tag intersectado con los matches del segmento (iter 177). Normaliza trim+lowercase+strip# antes del contains — 'VIP' matchea 'vip'. Tag normalizado vacío → 400 invalid_tag. Use case: 'VIPs CON tag campaign-blackfriday' como audiencia de campaña.","schema":{"type":"string"}},{"name":"format","in":"query","schema":{"type":"string","enum":["csv"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"segment":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"}}},"data":{"type":"array","items":{"$ref":"#/components/schemas/Customer"}},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer"},"has_more":{"type":"boolean"}}},"scan_truncated":{"type":"boolean"},"total_customers_global":{"type":"integer"}}}},"text/csv":{"schema":{"type":"string"}}}},"404":{"description":"Segment no encontrado"},"422":{"description":"Reglas corruptas en la DB"}}}},"/api/v1/customers/bulk-tag":{"post":{"summary":"Aplicar add/remove tags a un batch de customers","description":"Batch tagging idempotente sobre hasta 1000 customers en una sola request — pensado para workflows del integrador (Mailchimp tag → mismo tag en Kandima, borrar 'campaign-blackfriday' tras la campaña, promover N customers a 'vip' desde un export externo). Pre-iter 157 esto requería N PATCH individuales con 1000 roundtrips + hit del rate-limit.\n\nSemántica: `next_tags = (current ∪ add_tags) \\ remove_tags`. Ante intersección `add ∩ remove` no-vacía, REMOVE GANA (el caller pidió quitar; agregar-y-luego-sacar el mismo tag sería confuso). Tags se normalizan (lowercase, trim, strip leading `#`) — calcado del PATCH individual para que el mismo tag no se duplique entre endpoints por casing.\n\nIds ajenos/inexistentes no fallan la request entera — caen al array `skipped` con `reason`. Cada UPDATE corre en paralelo en chunks de 20 con `eq(id) + eq(user_id)`; un id borrado entre el SELECT y el UPDATE no matchea (no-op silencioso).","security":[{"BearerAuth":["write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["customer_ids"],"properties":{"customer_ids":{"type":"array","items":{"type":"integer","minimum":1},"maxItems":1000,"description":"IDs de customers del workspace. Máx 1000 por request."},"add_tags":{"type":"array","items":{"type":"string"},"description":"Tags a sumar. Idempotente: si el customer ya tiene el tag, no cambia nada."},"remove_tags":{"type":"array","items":{"type":"string"},"description":"Tags a quitar. Gana sobre add_tags ante intersección."}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"integer","description":"Cantidad de customers cuyo set de tags efectivamente cambió."},"skipped":{"type":"array","description":"Customers no actualizados — reason explica por qué.","items":{"type":"object","properties":{"id":{"type":"integer"},"reason":{"type":"string","enum":["not_found","no_change","too_many_tags","update_failed"],"description":"`not_found`: id ajeno o inexistente. `no_change`: el delta resultó en el mismo set. `too_many_tags`: el set resultante excedía 50 tags. `update_failed`: error de DB (loggea server-side)."}}}},"api_version":{"type":"string","example":"v1"}}}}}},"400":{"description":"Body inválido: `customer_ids_required` (vacío), `batch_too_large` (>1000), `invalid_customer_id` (no entero positivo), `invalid_tags`, `no_changes_requested` (add_tags y remove_tags ambos vacíos), o `invalid_json`."}}}},"/api/v1/customers/segments":{"get":{"summary":"RFM segment breakdown of your customers","description":"Returns count, revenue, AOV, LTV per RFM segment (whale, vip, loyal, returning, new, one_time, at_risk, churned). Powered by the customer-cohorts cron.","security":[{"BearerAuth":["read"]}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"total_customers":{"type":"integer"},"total_revenue":{"type":"number"},"segments":{"type":"array","items":{"type":"object","properties":{"segment":{"type":"string"},"label":{"type":"string"},"count":{"type":"integer"},"revenue_total":{"type":"number"},"avg_aov":{"type":"number"},"avg_ltv":{"type":"number"},"pct_of_customers":{"type":"number"},"pct_of_revenue":{"type":"number"}}}}}}}}}}}},"/api/v1/integrations":{"get":{"summary":"List connected integrations","security":[{"BearerAuth":["read"]}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Integration"}},"totals":{"type":"object"}}}}}}}}},"/api/v1/webhooks":{"get":{"summary":"List webhook subscriptions","security":[{"BearerAuth":["read"]}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Webhook"}}}}}}}}},"post":{"summary":"Create webhook subscription (returns secret once)","security":[{"BearerAuth":["write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","example":"https://my-app.com/webhooks/kandima"},"events":{"type":"array","items":{"type":"string"},"example":["purchase.created","credits.low"]}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Webhook"},{"type":"object","properties":{"secret":{"type":"string","example":"whsec_xxx..."},"warning":{"type":"string"}}}]}}}}}},"delete":{"summary":"Revoke a webhook subscription","security":[{"BearerAuth":["write"]}],"parameters":[{"name":"id","in":"query","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}}}}}}}}},"/api/v1/risks":{"get":{"summary":"Consolidated risks (churn + concentration + revenue crash)","description":"Returns risks ordered by impact_estimate desc. Filters out positive revenue spikes (those are in /v1/opportunities). Use to power risk monitoring dashboards or sync risks to internal systems.","security":[{"BearerAuth":["read"]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","default":20,"maximum":100}},{"name":"status","in":"query","schema":{"type":"string","enum":["open","all","resolved"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Insight"}},"summary":{"type":"object","properties":{"total":{"type":"integer"},"total_impact_usd":{"type":"number"},"by_category":{"type":"object"},"by_severity":{"type":"object"}}}}}}}}}}},"/api/v1/opportunities":{"get":{"summary":"Positive opportunity insights (revenue spikes — scale momentum)","description":"Returns revenue velocity spikes (>100% MoM) and any insight with category=opportunity. Use to trigger ad scaling, inventory restocking, or partner outreach when KANDIMA detects momentum.","security":[{"BearerAuth":["read"]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","default":10,"maximum":50}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Insight"}},"summary":{"type":"object","properties":{"total":{"type":"integer"},"total_upside_usd":{"type":"number"}}}}}}}}}}},"/api/v1/insights":{"get":{"summary":"List recent AI insights (anomalies, churn risk, ad performance, inventory)","description":"Filter by category=churn_risk to get only churn risk insights with metadata.decline_pct/current_30d/prev_30d/orders_current. Returns metadata field with cron-specific signals.","security":[{"BearerAuth":["read"]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","default":25,"maximum":100}},{"name":"status","in":"query","schema":{"type":"string","enum":["open","resolved","dismissed"]}},{"name":"severity","in":"query","schema":{"type":"string","enum":["low","medium","high","critical","info","warning"]}},{"name":"category","in":"query","schema":{"type":"string","enum":["churn_risk","anomaly","ad_performance","inventory","creative","system"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Insight"}}}}}}}}}},"/api/v1/profit":{"get":{"summary":"Daily profit snapshots","security":[{"BearerAuth":["read"]}],"parameters":[{"name":"days","in":"query","schema":{"type":"integer","default":30,"maximum":365}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ProfitSnapshot"}},"totals":{"type":"object"},"window_days":{"type":"integer"}}}}}}}}},"/api/v1/track":{"post":{"summary":"Ingest a custom analytics event","security":[{"BearerAuth":["write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["event"],"properties":{"event":{"type":"string","pattern":"^[a-zA-Z0-9_.\\-]+$","maxLength":64,"example":"checkout_started"},"props":{"type":"object","example":{"plan":"pro","amount_usd":19}},"url":{"type":"string","maxLength":500},"path":{"type":"string","maxLength":500},"title":{"type":"string","maxLength":200},"referrer":{"type":"string","maxLength":500},"visitor_id":{"type":"string"},"session_id":{"type":"string"},"client_ts":{"type":"string","format":"date-time"}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"event_id":{"type":"integer"}}}}}}}}},"/api/v1/anomalies":{"get":{"summary":"Recent anomalies (univariate + correlated)","security":[{"BearerAuth":["read"]}],"parameters":[{"name":"days","in":"query","schema":{"type":"integer","default":7,"maximum":90}},{"name":"severity","in":"query","schema":{"type":"string","enum":["low","medium","high","critical"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Anomaly"}},"window_days":{"type":"integer"}}}}}}}}},"/api/v1/inventory":{"get":{"summary":"Inventory velocity per SKU","description":"Lists SKUs with stock-on-hand, units sold (7d/30d), days-of-stock, trend and projected stockout date.","security":[{"BearerAuth":["read"]}],"parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["critical","low","ok","overstocked"]}},{"name":"limit","in":"query","schema":{"type":"integer","default":100,"maximum":500}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object"}},"summary":{"type":"object","properties":{"total":{"type":"integer"},"critical":{"type":"integer"},"low":{"type":"integer"},"ok":{"type":"integer"},"overstocked":{"type":"integer"}}},"generated_at":{"type":"string","format":"date-time"}}}}}}}}},"/api/v1/reorder":{"get":{"summary":"Smart reorder suggestions","description":"AI-suggested reorder quantities based on velocity, lead time and forecast. Filter by status (pending/ordered/dismissed).","security":[{"BearerAuth":["read"]}],"parameters":[{"name":"status","in":"query","schema":{"type":"string","default":"pending","enum":["pending","ordered","dismissed"]}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object"}},"generated_at":{"type":"string","format":"date-time"}}}}}}}}},"/api/v1/ads":{"get":{"summary":"Ad campaigns cross-platform (Meta/Google/TikTok)","description":"Cross-platform ad performance normalized — spend, impressions, clicks, CTR, CPM, conversions, revenue, ROAS, frequency. Aggregated summary across the slice.","security":[{"BearerAuth":["read"]}],"parameters":[{"name":"platform","in":"query","schema":{"type":"string","enum":["meta","google","tiktok"]}},{"name":"level","in":"query","schema":{"type":"string","enum":["campaign","adset","ad"]}},{"name":"status","in":"query","schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","default":100,"maximum":500}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object"}},"summary":{"type":"object"},"generated_at":{"type":"string","format":"date-time"}}}}}}}}},"/api/v1/creatives":{"get":{"summary":"Creative Intelligence (winner/fatigue/recommendation)","description":"Per-creative scoring: winner_score, fatigue_score, CTR/ROAS drop %, AI recommendation (scale/refresh/pause/keep) and rationale.","security":[{"BearerAuth":["read"]}],"parameters":[{"name":"recommendation","in":"query","schema":{"type":"string","enum":["scale","refresh","pause","keep"]}},{"name":"platform","in":"query","schema":{"type":"string","enum":["meta","google","tiktok"]}},{"name":"sort","in":"query","schema":{"type":"string","default":"winner","enum":["winner","fatigue","spend"]}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object"}},"generated_at":{"type":"string","format":"date-time"}}}}}}}}},"/api/v1/conversations":{"get":{"summary":"List conversation threads by channel","description":"Lists messaging threads. channel=whatsapp returns WhatsApp threads; channel=messenger|instagram reconstructs threads from the shared messaging queue, grouped by conversation_id. For messenger|instagram, pages by raw queue row via the `before` cursor: pass the previous response's `oldest_id` to fetch older rows. The cursor is per raw ROW, not per thread — a conversation whose messages span pages can reappear on an older page with an older representative and a partial message_count; dedupe client-side by conversation_id, keeping the version already loaded. `limit` trims threads AFTER grouping and trimmed threads do not reappear on later pages, so use limit=200 to enumerate everything.","security":[{"BearerAuth":["read"]}],"parameters":[{"name":"channel","in":"query","schema":{"type":"string","default":"whatsapp","enum":["whatsapp","messenger","instagram"]}},{"name":"status","in":"query","schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}},{"name":"before","in":"query","schema":{"type":"integer","minimum":1},"description":"Pagination cursor (messenger|instagram only): raw queue row id from the previous response's `oldest_id`. Returns rows with id < before, newest first. Invalid value → 400 invalid_before; with channel=whatsapp → 400 before_not_supported."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"channel":{"type":"string"},"items":{"type":"array","items":{"type":"object"}},"truncated":{"type":"boolean","description":"messenger|instagram only. Legacy alias of has_more: the raw-row scan hit its cap, so threads and message_count on this page are partial."},"has_more":{"type":"boolean","description":"messenger|instagram only. True when older raw rows remain (measured, not inferred). Fetch them with before=oldest_id."},"oldest_id":{"type":"integer","nullable":true,"description":"messenger|instagram only. Oldest raw row id processed on this page — the cursor for the next page. null when no rows."},"generated_at":{"type":"string","format":"date-time"},"api_version":{"type":"string"}}}}}}}}},"/api/v1/conversations/stats":{"get":{"summary":"Aggregated messaging-bot stats (rolling window)","description":"Curated aggregates of the messaging bot for integrators: active threads, message volumes, escalations to human, won sales (USD, same source of truth as the dashboard), and lifecycle-automation send counts by kind. sales.truncated=true (or automations.truncated=true) means the underlying scan hit its row cap and totals are a floor, not an exact total. automations counts SENDS per kind (not revenue — attribution lives in the internal dashboard).","security":[{"BearerAuth":["read"]}],"parameters":[{"name":"days","in":"query","schema":{"type":"integer","default":30,"minimum":1,"maximum":90},"description":"Rolling window size in days."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"days":{"type":"integer"},"since":{"type":"string","format":"date-time"},"conversations":{"type":"integer"},"messages":{"type":"object","properties":{"inbound":{"type":"integer"},"outbound":{"type":"integer"},"ai_generated":{"type":"integer"}}},"escalations":{"type":"integer"},"sales":{"type":"object","properties":{"won_count":{"type":"integer"},"revenue_usd":{"type":"number"},"truncated":{"type":"boolean"}}},"automations":{"type":"object","nullable":true,"description":"Lifecycle-automation send counts by kind (cart_recovery, customer_winback, reorder, review_request, order_shipped, lead_followup, ad_followup, restock_alert, payment_reminder, reengage). Sends only, failed excluded; not revenue.","properties":{"sends_total":{"type":"integer"},"by_kind":{"type":"array","items":{"type":"object","properties":{"kind":{"type":"string"},"sends":{"type":"integer"}}}},"truncated":{"type":"boolean"}}}}}}}}}}},"/api/v1/conversations/{id}/messages":{"get":{"summary":"Full message history of a conversation thread","description":"Returns the chronological messages of a thread. For messenger/instagram (conversation_id msgr_<psid> / ig_<igsid>) the history is reconstructed from the shared messaging queue: inbound customer messages plus outbound replies.","security":[{"BearerAuth":["read"]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"conversation_id, e.g. msgr_<psid>"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"conversation_id":{"type":"string"},"channel":{"type":"string"},"customer":{"type":"object","properties":{"name":{"type":"string","nullable":true},"id":{"type":"string","nullable":true}}},"messages":{"type":"array","items":{"type":"object","properties":{"direction":{"type":"string","enum":["inbound","outbound"]},"text":{"type":"string"},"at":{"type":"string","format":"date-time"},"ai_generated":{"type":"boolean"}}}},"message_count":{"type":"integer"},"generated_at":{"type":"string","format":"date-time"},"api_version":{"type":"string"}}}}}},"404":{"description":"Conversation not found"}}}}}}