@vitrina/api 11.35.0 → 11.36.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -13919,7 +13919,7 @@ interface paths {
13919
13919
  };
13920
13920
  /**
13921
13921
  * List the add-on catalog, this workspace's entitlements, and live usage
13922
- * @description The Complementos hub read. `catalog` is the typed feature catalog (prices are PR-reviewed code, never a runtime SKU table); `entitlements` is one row per add-on this workspace has ever held; `usage` carries the live meter for each metered add-on over the current **Santiago** month — `usage.tasador` is `{ used, free_allowance, period }`, where `used` is aggregated on read from `usage_event` (never a counter) and `free_allowance` reflects any per-workspace override, so it is the same pair of numbers the access gate decides on. Each entitlement carries `billed_by`: `vitrina` when Vitrina invoices the add-on, any other value when another party covers it (Vitrina charges nothing, so a client shows it as included rather than at the catalog price).
13922
+ * @description The Complementos hub read. `catalog` is the typed feature catalog (prices are PR-reviewed code, never a runtime SKU table); `entitlements` is one row per add-on this workspace has ever held; `usage` carries the live meter for each metered add-on over the current **Santiago** month — `usage.tasador` is `{ used, free_allowance, period }`, where `used` is aggregated on read from `usage_event` (never a counter) and `free_allowance` reflects any per-workspace override, so it is the same pair of numbers the access gate decides on. Each entitlement carries `billed_by`: `vitrina` when Vitrina invoices the add-on, any other value when another party covers it (Vitrina charges nothing, so a client shows it as included rather than at the catalog price). A row may also carry a time-boxed waiver: `waiver_until` is the last free calendar day (America/Santiago) and `waiver_active` says whether it covers the add-on right now; once it lapses the add-on bills normally with no further action.
13923
13923
  */
13924
13924
  get: {
13925
13925
  parameters: {
@@ -13963,9 +13963,11 @@ interface paths {
13963
13963
  * "cancel_at_period_end": false,
13964
13964
  * "canceled_at": null,
13965
13965
  * "metadata": {},
13966
+ * "waiver_until": "2026-10-31",
13966
13967
  * "created_at": "2026-09-01T12:00:00.000Z",
13967
13968
  * "updated_at": "2026-09-01T12:00:00.000Z",
13968
- * "billed_by": "vitrina"
13969
+ * "billed_by": "vitrina",
13970
+ * "waiver_active": true
13969
13971
  * }
13970
13972
  * ],
13971
13973
  * "usage": {
@@ -14005,10 +14007,14 @@ interface paths {
14005
14007
  metadata: {
14006
14008
  [key: string]: unknown;
14007
14009
  };
14010
+ /** @description Last calendar day (`YYYY-MM-DD`, America/Santiago) this add-on is free under a time-boxed waiver: no charge accrues through the end of that day and activation needs no payment method. It bills normally from 00:00 Santiago of the next day. `null` = no waiver. */
14011
+ waiver_until?: string | null;
14008
14012
  created_at: string;
14009
14013
  updated_at: string;
14010
14014
  /** @description Who invoices this add-on. `vitrina` (default) = Vitrina bills it at the catalog price. Any other value names the partner whose plan covers it; Vitrina charges nothing, so show the add-on as included and never render the partner value or the catalog price. */
14011
14015
  billed_by: string;
14016
+ /** @description True while the `waiver_until` waiver covers the add-on right now (resolved server-side in America/Santiago). While true, show the add-on as included until that date instead of the catalog price. */
14017
+ waiver_active: boolean;
14012
14018
  }[];
14013
14019
  usage: {
14014
14020
  tasador: {
@@ -14096,7 +14102,7 @@ interface paths {
14096
14102
  put?: never;
14097
14103
  /**
14098
14104
  * Self-serve activate an add-on (returns needs_enrollment without a payment mandate)
14099
- * @description Grants `feature` to the workspace once an active payment mandate exists (`{ status: "active", entitlement }`); with none, answers `{ status: "needs_enrollment" }` and grants nothing — the caller routes the dealer through the Fintoc/Mercado Pago enrollment step and retries. Idempotent on an already-active feature (re-answers the same `active` row rather than erroring).
14105
+ * @description Grants `feature` to the workspace once an active payment mandate exists (`{ status: "active", entitlement }`); with none, answers `{ status: "needs_enrollment" }` and grants nothing — the caller routes the dealer through the Fintoc/Mercado Pago enrollment step and retries. No payment method is needed when the add-on costs the workspace nothing: its row is covered by another biller (`billed_by` other than `vitrina`) or sits inside an active `waiver_until` waiver; such an activation answers `active` and accrues no charge for the covered days. Idempotent on an already-active feature (re-answers the same `active` row rather than erroring).
14100
14106
  */
14101
14107
  post: {
14102
14108
  parameters: {
@@ -14131,6 +14137,7 @@ interface paths {
14131
14137
  * "cancel_at_period_end": false,
14132
14138
  * "canceled_at": null,
14133
14139
  * "metadata": {},
14140
+ * "waiver_until": null,
14134
14141
  * "created_at": "2026-09-23T15:00:00.000Z",
14135
14142
  * "updated_at": "2026-09-23T15:00:00.000Z"
14136
14143
  * }
@@ -19946,6 +19953,9 @@ interface paths {
19946
19953
  * "temperature_reason": "respondió pidiendo confirmar la hora y no ha vuelto a escribir",
19947
19954
  * "temperature_at": "2026-09-21T14:02:11.000Z",
19948
19955
  * "won_lost_reason": null,
19956
+ * "lost_reason": null,
19957
+ * "lost_reason_source": null,
19958
+ * "lost_reason_at": null,
19949
19959
  * "closed_at": null,
19950
19960
  * "created_by": "11111111-0000-4000-8000-000000000001",
19951
19961
  * "created_at": "2026-09-18T13:44:02.115Z",
@@ -20000,6 +20010,9 @@ interface paths {
20000
20010
  * "temperature_reason": null,
20001
20011
  * "temperature_at": null,
20002
20012
  * "won_lost_reason": null,
20013
+ * "lost_reason": null,
20014
+ * "lost_reason_source": null,
20015
+ * "lost_reason_at": null,
20003
20016
  * "closed_at": null,
20004
20017
  * "created_by": null,
20005
20018
  * "created_at": "2026-09-19T09:12:44.000Z",
@@ -20194,6 +20207,9 @@ interface paths {
20194
20207
  * "temperature_reason": "respondió pidiendo confirmar la hora y no ha vuelto a escribir",
20195
20208
  * "temperature_at": "2026-09-21T14:02:11.000Z",
20196
20209
  * "won_lost_reason": null,
20210
+ * "lost_reason": null,
20211
+ * "lost_reason_source": null,
20212
+ * "lost_reason_at": null,
20197
20213
  * "closed_at": null,
20198
20214
  * "created_by": "11111111-0000-4000-8000-000000000001",
20199
20215
  * "created_at": "2026-09-18T13:44:02.115Z",
@@ -20374,6 +20390,9 @@ interface paths {
20374
20390
  * "temperature_reason": "respondió pidiendo confirmar la hora y no ha vuelto a escribir",
20375
20391
  * "temperature_at": "2026-09-21T14:02:11.000Z",
20376
20392
  * "won_lost_reason": null,
20393
+ * "lost_reason": null,
20394
+ * "lost_reason_source": null,
20395
+ * "lost_reason_at": null,
20377
20396
  * "closed_at": null,
20378
20397
  * "created_by": "11111111-0000-4000-8000-000000000001",
20379
20398
  * "created_at": "2026-09-18T13:44:02.115Z",
@@ -21064,6 +21083,9 @@ interface paths {
21064
21083
  * "temperature_reason": "respondió pidiendo confirmar la hora y no ha vuelto a escribir",
21065
21084
  * "temperature_at": "2026-09-21T14:02:11.000Z",
21066
21085
  * "won_lost_reason": null,
21086
+ * "lost_reason": null,
21087
+ * "lost_reason_source": null,
21088
+ * "lost_reason_at": null,
21067
21089
  * "closed_at": null,
21068
21090
  * "created_by": "11111111-0000-4000-8000-000000000001",
21069
21091
  * "created_at": "2026-09-18T13:44:02.115Z",
@@ -21128,6 +21150,9 @@ interface paths {
21128
21150
  * "temperature_reason": null,
21129
21151
  * "temperature_at": null,
21130
21152
  * "won_lost_reason": null,
21153
+ * "lost_reason": null,
21154
+ * "lost_reason_source": null,
21155
+ * "lost_reason_at": null,
21131
21156
  * "closed_at": null,
21132
21157
  * "created_by": null,
21133
21158
  * "created_at": "2026-09-19T09:12:44.000Z",
@@ -21405,6 +21430,9 @@ interface paths {
21405
21430
  * "temperature_reason": "respondió pidiendo confirmar la hora y no ha vuelto a escribir",
21406
21431
  * "temperature_at": "2026-09-21T14:02:11.000Z",
21407
21432
  * "won_lost_reason": null,
21433
+ * "lost_reason": null,
21434
+ * "lost_reason_source": null,
21435
+ * "lost_reason_at": null,
21408
21436
  * "closed_at": null,
21409
21437
  * "created_by": "11111111-0000-4000-8000-000000000001",
21410
21438
  * "created_at": "2026-09-18T13:44:02.115Z",
@@ -21534,6 +21562,8 @@ interface paths {
21534
21562
  /** @enum {string|null} */
21535
21563
  temperature?: "hot" | "warm" | "cold" | null;
21536
21564
  temperature_reason?: string | null;
21565
+ /** @enum {string} */
21566
+ lost_reason?: "bought_elsewhere" | "financing_rejected" | "unit_unavailable" | "price" | "not_ready" | "unresponsive" | "other";
21537
21567
  };
21538
21568
  };
21539
21569
  };
@@ -21575,6 +21605,9 @@ interface paths {
21575
21605
  * "temperature_reason": "volvió a escribir pidiendo la reserva",
21576
21606
  * "temperature_at": "2026-09-21T14:02:11.000Z",
21577
21607
  * "won_lost_reason": null,
21608
+ * "lost_reason": null,
21609
+ * "lost_reason_source": null,
21610
+ * "lost_reason_at": null,
21578
21611
  * "closed_at": null,
21579
21612
  * "created_by": "11111111-0000-4000-8000-000000000001",
21580
21613
  * "created_at": "2026-09-18T13:44:02.115Z",
@@ -21993,6 +22026,9 @@ interface paths {
21993
22026
  * "temperature_reason": "respondió pidiendo confirmar la hora y no ha vuelto a escribir",
21994
22027
  * "temperature_at": "2026-09-21T14:02:11.000Z",
21995
22028
  * "won_lost_reason": null,
22029
+ * "lost_reason": null,
22030
+ * "lost_reason_source": null,
22031
+ * "lost_reason_at": null,
21996
22032
  * "closed_at": null,
21997
22033
  * "created_by": "11111111-0000-4000-8000-000000000001",
21998
22034
  * "created_at": "2026-09-18T13:44:02.115Z",
@@ -22165,6 +22201,9 @@ interface paths {
22165
22201
  * "temperature_reason": "respondió pidiendo confirmar la hora y no ha vuelto a escribir",
22166
22202
  * "temperature_at": "2026-09-21T14:02:11.000Z",
22167
22203
  * "won_lost_reason": null,
22204
+ * "lost_reason": null,
22205
+ * "lost_reason_source": null,
22206
+ * "lost_reason_at": null,
22168
22207
  * "closed_at": null,
22169
22208
  * "created_by": "11111111-0000-4000-8000-000000000001",
22170
22209
  * "created_at": "2026-09-18T13:44:02.115Z",
@@ -22338,6 +22377,9 @@ interface paths {
22338
22377
  * "temperature_reason": "respondió pidiendo confirmar la hora y no ha vuelto a escribir",
22339
22378
  * "temperature_at": "2026-09-21T14:02:11.000Z",
22340
22379
  * "won_lost_reason": null,
22380
+ * "lost_reason": null,
22381
+ * "lost_reason_source": null,
22382
+ * "lost_reason_at": null,
22341
22383
  * "closed_at": null,
22342
22384
  * "created_by": "11111111-0000-4000-8000-000000000001",
22343
22385
  * "created_at": "2026-09-18T13:44:02.115Z",
@@ -22513,6 +22555,9 @@ interface paths {
22513
22555
  * "temperature_reason": "respondió pidiendo confirmar la hora y no ha vuelto a escribir",
22514
22556
  * "temperature_at": "2026-09-21T14:02:11.000Z",
22515
22557
  * "won_lost_reason": "cerró con financiamiento propio",
22558
+ * "lost_reason": null,
22559
+ * "lost_reason_source": null,
22560
+ * "lost_reason_at": null,
22516
22561
  * "closed_at": "2026-09-22T08:41:32.977Z",
22517
22562
  * "created_by": "11111111-0000-4000-8000-000000000001",
22518
22563
  * "created_at": "2026-09-18T13:44:02.115Z",
@@ -22686,6 +22731,9 @@ interface paths {
22686
22731
  * "temperature_reason": "respondió pidiendo confirmar la hora y no ha vuelto a escribir",
22687
22732
  * "temperature_at": "2026-09-21T14:02:11.000Z",
22688
22733
  * "won_lost_reason": "compró en otra parte",
22734
+ * "lost_reason": "bought_elsewhere",
22735
+ * "lost_reason_source": "jev",
22736
+ * "lost_reason_at": "2026-09-22T08:44:12.501Z",
22689
22737
  * "closed_at": "2026-09-22T08:44:10.331Z",
22690
22738
  * "created_by": "11111111-0000-4000-8000-000000000001",
22691
22739
  * "created_at": "2026-09-18T13:44:02.115Z",
@@ -22859,6 +22907,9 @@ interface paths {
22859
22907
  * "temperature_reason": "respondió pidiendo confirmar la hora y no ha vuelto a escribir",
22860
22908
  * "temperature_at": "2026-09-21T14:02:11.000Z",
22861
22909
  * "won_lost_reason": "buscaba un servicio que no ofrecemos",
22910
+ * "lost_reason": null,
22911
+ * "lost_reason_source": null,
22912
+ * "lost_reason_at": null,
22862
22913
  * "closed_at": "2026-09-22T08:44:10.331Z",
22863
22914
  * "created_by": "11111111-0000-4000-8000-000000000001",
22864
22915
  * "created_at": "2026-09-18T13:44:02.115Z",
@@ -23017,6 +23068,9 @@ interface paths {
23017
23068
  * "temperature_reason": "respondió pidiendo confirmar la hora y no ha vuelto a escribir",
23018
23069
  * "temperature_at": "2026-09-21T14:02:11.000Z",
23019
23070
  * "won_lost_reason": null,
23071
+ * "lost_reason": null,
23072
+ * "lost_reason_source": null,
23073
+ * "lost_reason_at": null,
23020
23074
  * "closed_at": null,
23021
23075
  * "created_by": "11111111-0000-4000-8000-000000000001",
23022
23076
  * "created_at": "2026-09-18T13:44:02.115Z",
@@ -28118,7 +28172,7 @@ interface paths {
28118
28172
  * A contact’s scope-aware consent / do-not-contact ledger
28119
28173
  * @description Append-only history, newest fact first — this is the surface that can answer «since when, and how do you know».
28120
28174
  *
28121
- * Each row is one fact about one `channel` (`whatsapp|email|voice|any`) in one `scope` (`marketing|service|promised_followup|all_proactive`). A marketing block does NOT block service messages; a current `all_proactive` block outranks every scope.
28175
+ * Each row is one fact about one `channel` (`whatsapp|email|voice|instagram|messenger|any`) in one `scope` (`marketing|service|promised_followup|all_proactive`). A marketing block does NOT block service messages; a current `all_proactive` block outranks every scope.
28122
28176
  *
28123
28177
  * A superseded row stays readable rather than being overwritten, so the evidence trail survives the revocation.
28124
28178
  */
@@ -28260,7 +28314,7 @@ interface paths {
28260
28314
  */
28261
28315
  "application/json": {
28262
28316
  /** @enum {string} */
28263
- channel: "whatsapp" | "email" | "voice" | "any";
28317
+ channel: "whatsapp" | "email" | "voice" | "instagram" | "messenger" | "any";
28264
28318
  /** @enum {string} */
28265
28319
  scope: "marketing" | "service" | "promised_followup" | "all_proactive";
28266
28320
  /** @enum {string} */
@@ -69292,7 +69346,7 @@ interface paths {
69292
69346
  };
69293
69347
  trace?: never;
69294
69348
  };
69295
- "/clinic/insights/comercial": {
69349
+ "/clinic/patients/{id}/auto-links": {
69296
69350
  parameters: {
69297
69351
  query?: never;
69298
69352
  header?: never;
@@ -69300,30 +69354,23 @@ interface paths {
69300
69354
  cookie?: never;
69301
69355
  };
69302
69356
  /**
69303
- * Commercial numbers: presupuesto acceptance, cierres, cash, botados
69304
- * @description The clinic's commercial numbers, aggregates only (counts, CLP, percentages, months and professional names; never a patient). `regime` says how acceptance is read: `evidencia` when the revenue mirror is on (a non-entry line performed, or a cita booked after the presupuesto, inside its window), `explicita` otherwise (the acceptance the clinic recorded). Acceptance is by COUNT of presupuestos, one per lineage; $0 and entry-only presupuestos are out of both sides; the window is 60 days (90 for ortodoncia and implantología); the rate goes to the professional who issued the presupuesto. Presupuestos ISSUED in the window form the acceptance cohort (`cohorts` by issue month with 30- and 60-day reads). A cierre is the first payment on an accepted presupuesto: once per presupuesto, dated on that payment, valued at the full accepted value. `headline` is the cash collected in the window against the previous window of the same length. `first_visit`, `second_visit` and `botados` carry `formula: propuesta`; `botados` is a live worklist over the last six months. Every KPI carries its `formula` and a printable `rule`; a rate with no denominator is `null`, never 0. `state` is `empty`, `settling` (presupuesto lines still being read; `state_reason` says so) or `ready`. Requires `clinic_money:read` (or `clinic_insights:read`, which reads the same aggregates). Accepts `from` / `to` (bare `YYYY-MM-DD`, `to` INCLUSIVE; default the 90 days ending today in the clinic's zone), `location_id` and `professional_id`. With `professional_id`, cash (`headline`, `months[].collected_clp`) is `null`: a payment belongs to the patient, not to a professional. A caller WITHOUT `clinic_money:read` always reads one fixed window — the last 3 closed calendar months — whatever range or filter it asks (`suppression.filters_ignored`), with every count or CLP resting on 1–4 cases `null`, rates over fewer than 5 cases `null`, complementary suppression over professionals, cohorts and botados lists, and month rows carrying only cash.
69357
+ * Automatic contact links of a patient (Ads identity only)
69358
+ * @description The contacts Vitrina matched to this ficha AUTOMATICALLY for Ads outcomes: an E.164 phone or an exact email held by exactly one patient and exactly one contact of the clinic, the patient an adult with a plausible birth date and not a carga, and no family bond or booking link on either side. It is identity for Ads only — it grants no booking and no access to the clinical record, and a family bond or a booking link always outranks it. Each row: `id`, `clinic_patient_id`, `contact_id`, `patient_name`, `contact_name`, `status` (`live` | `revoked` — a run found it no longer unique or ineligible — | `rejected` — a person said it is not this person, never re-opened), `method` (`phone_unique` | `email_unique`: which identifier was held by exactly one patient and one contact — the value itself is never stored), `matched_at`, `run_id` (the matching run, provenance), `revoked_at`, `revoked_reason`, `sent_count` / `last_sent_at` (Ads outcomes sent under the pair). Live first.
69305
69359
  *
69306
- * **Connected apps:** every patient and contact in the response is a Seudónimo de paciente — initials plus a stable number, `"M.F. · #1001"` — unless the clinic allowed patient names, with RUT, phone and email masked in free text and `meta.patient_privacy` saying so. Clinical alerts (`flags`) are withheld in both modes, and a non-JSON body (an export, a file) is refused with `403 CONNECTED_APP_SENSITIVE_DATA`.
69360
+ * **Connected apps:** refused with `403 CONNECTED_APP_SENSITIVE_DATA`, whatever scopes they hold — this operation carries a dato sensible (ADR 0106 §4). Only an API key or a personal token of the workspace that holds the scope reaches it.
69307
69361
  */
69308
69362
  get: {
69309
69363
  parameters: {
69310
- query?: {
69311
- /** @description First day of the window, inclusive. Defaults to 90 days before `to`. */
69312
- from?: string;
69313
- /** @description Last day of the window, INCLUSIVE. Defaults to today in the clinic's zone. */
69314
- to?: string;
69315
- /** @description One sucursal. Omit for all of them. */
69316
- location_id?: string;
69317
- /** @description One profesional. A foreign id is a 404, never an empty report. */
69318
- professional_id?: string;
69319
- };
69364
+ query?: never;
69320
69365
  header?: never;
69321
- path?: never;
69366
+ path: {
69367
+ id: string;
69368
+ };
69322
69369
  cookie?: never;
69323
69370
  };
69324
69371
  requestBody?: never;
69325
69372
  responses: {
69326
- /** @description Clinic commercial numbers */
69373
+ /** @description Patient auto-links */
69327
69374
  200: {
69328
69375
  headers: {
69329
69376
  [name: string]: unknown;
@@ -69332,151 +69379,538 @@ interface paths {
69332
69379
  /**
69333
69380
  * @example {
69334
69381
  * "data": {
69335
- * "from": "2026-06-01",
69336
- * "to": "2026-08-29",
69337
- * "timezone": "America/Santiago",
69338
- * "location_id": null,
69339
- * "professional_id": null,
69340
- * "state": "ready",
69341
- * "state_reason": null,
69342
- * "regime": "evidencia",
69343
- * "regime_label": "Aceptación por evidencia",
69344
- * "regime_rule": "Leído desde tu software de agenda: un presupuesto cuenta como aceptado cuando se realizó una de sus prestaciones o el paciente agendó una cita después de recibirlo.",
69345
- * "source": {
69346
- * "kind": "mirror",
69347
- * "synced_at": "2026-08-30T03:10:00.000Z"
69348
- * },
69349
- * "formulas": {
69350
- * "acceptance": "mentores_2026_09_25",
69351
- * "cierre": "cierre_2026_09_25",
69352
- * "first_visit": "propuesta",
69353
- * "second_visit": "propuesta",
69354
- * "botados": "propuesta"
69355
- * },
69356
- * "headline": {
69357
- * "collected_clp": 48500000,
69358
- * "previous_collected_clp": 44100000,
69359
- * "delta_pct": 10,
69360
- * "previous_from": "2026-03-03",
69361
- * "previous_to": "2026-05-31",
69362
- * "rule": "Lo cobrado en el período: todos los pagos recibidos, sin los anulados. Se compara con el período anterior del mismo largo."
69363
- * },
69364
- * "acceptance": {
69365
- * "presented": 420,
69366
- * "accepted": 231,
69367
- * "rate": 55,
69368
- * "same_day": 118,
69369
- * "later": 113,
69370
- * "cita_only": 14,
69371
- * "pending": 36,
69372
- * "presented_clp": 142000000,
69373
- * "accepted_clp": 71300000,
69374
- * "rate_30d": 47.2,
69375
- * "rate_60d": 53.8,
69376
- * "formula": "mentores_2026_09_25",
69377
- * "window_days": 60,
69378
- * "long_window_days": 90,
69379
- * "rule": "Presupuestos aceptados ÷ presupuestos presentados, cada presupuesto una vez."
69380
- * },
69381
- * "cierres": {
69382
- * "count": 180,
69383
- * "value_clp": 58900000,
69384
- * "accepted_without_payment": 51,
69385
- * "formula": "cierre_2026_09_25",
69386
- * "rule": "Un cierre es el primer pago de un presupuesto aceptado."
69387
- * },
69388
- * "cohorts": [
69389
- * {
69390
- * "month": "2026-06",
69391
- * "presented": 140,
69392
- * "accepted": 80,
69393
- * "rate": 57.1,
69394
- * "same_day": 41,
69395
- * "later": 39,
69396
- * "cita_only": 2,
69397
- * "pending": 0,
69398
- * "presented_clp": 47000000,
69399
- * "accepted_clp": 24800000,
69400
- * "rate_30d": 49.3,
69401
- * "rate_60d": 56.4,
69402
- * "mature": true
69403
- * }
69404
- * ],
69405
- * "by_professional": [
69382
+ * "data": [
69406
69383
  * {
69407
- * "professional_id": "0f3c2a9e-5b1d-4c7e-9a42-6d8e1f2b3c4d",
69408
- * "professional_name": "Dra. Camila Soto",
69409
- * "presented": 96,
69410
- * "accepted": 61,
69411
- * "rate": 63.5,
69412
- * "same_day": 30,
69413
- * "later": 31,
69414
- * "cita_only": 3,
69415
- * "pending": 8,
69416
- * "presented_clp": 33400000,
69417
- * "accepted_clp": 19900000,
69418
- * "cierres": 47,
69419
- * "cierres_clp": 15100000
69384
+ * "id": "a7a7a7a7-0000-4000-8000-000000000001",
69385
+ * "clinic_patient_id": "b1b0b8de-0000-4000-8000-000000000001",
69386
+ * "contact_id": "1a73af9e-0000-4000-8000-000000000001",
69387
+ * "patient_name": "María José Fuentes Lagos",
69388
+ * "contact_name": "María José Fuentes Lagos",
69389
+ * "status": "live",
69390
+ * "method": "phone_unique",
69391
+ * "matched_at": "2026-09-27T14:10:02.118Z",
69392
+ * "run_id": "a8a8a8a8-0000-4000-8000-000000000001",
69393
+ * "revoked_at": null,
69394
+ * "revoked_reason": null,
69395
+ * "sent_count": 3,
69396
+ * "last_sent_at": "2026-09-28T12:02:40.551Z"
69420
69397
  * }
69421
- * ],
69422
- * "unassigned_presented": 0,
69423
- * "unassigned_cierres": 0,
69424
- * "months": [
69398
+ * ]
69399
+ * }
69400
+ * }
69401
+ */
69402
+ "application/json": {
69403
+ data?: unknown;
69404
+ meta?: {
69405
+ [key: string]: unknown;
69406
+ };
69407
+ };
69408
+ };
69409
+ };
69410
+ /** @description Validation error */
69411
+ 400: {
69412
+ headers: {
69413
+ [name: string]: unknown;
69414
+ };
69415
+ content: {
69416
+ "application/json": components["schemas"]["Error"];
69417
+ };
69418
+ };
69419
+ /** @description Unauthorized */
69420
+ 401: {
69421
+ headers: {
69422
+ [name: string]: unknown;
69423
+ };
69424
+ content: {
69425
+ "application/json": components["schemas"]["Error"];
69426
+ };
69427
+ };
69428
+ /** @description Forbidden */
69429
+ 403: {
69430
+ headers: {
69431
+ [name: string]: unknown;
69432
+ };
69433
+ content: {
69434
+ "application/json": components["schemas"]["Error"];
69435
+ };
69436
+ };
69437
+ /** @description Not found */
69438
+ 404: {
69439
+ headers: {
69440
+ [name: string]: unknown;
69441
+ };
69442
+ content: {
69443
+ "application/json": components["schemas"]["Error"];
69444
+ };
69445
+ };
69446
+ /** @description Conflict (incl. Idempotency-Key reuse with different body) */
69447
+ 409: {
69448
+ headers: {
69449
+ [name: string]: unknown;
69450
+ };
69451
+ content: {
69452
+ "application/json": components["schemas"]["Error"];
69453
+ };
69454
+ };
69455
+ /** @description Rate limited */
69456
+ 429: {
69457
+ headers: {
69458
+ [name: string]: unknown;
69459
+ };
69460
+ content: {
69461
+ "application/json": components["schemas"]["Error"];
69462
+ };
69463
+ };
69464
+ };
69465
+ };
69466
+ put?: never;
69467
+ post?: never;
69468
+ delete?: never;
69469
+ options?: never;
69470
+ head?: never;
69471
+ patch?: never;
69472
+ trace?: never;
69473
+ };
69474
+ "/clinic/contacts/{contactId}/auto-links": {
69475
+ parameters: {
69476
+ query?: never;
69477
+ header?: never;
69478
+ path?: never;
69479
+ cookie?: never;
69480
+ };
69481
+ /**
69482
+ * Patients automatically linked to a contact (Ads identity only)
69483
+ * @description The contact side of the same automatic links — what the contact ficha shows as «Vinculado automáticamente». An empty list, not a 404, when there is none. Each row: `id`, `clinic_patient_id`, `contact_id`, `patient_name`, `contact_name`, `status` (`live` | `revoked` — a run found it no longer unique or ineligible — | `rejected` — a person said it is not this person, never re-opened), `method` (`phone_unique` | `email_unique`: which identifier was held by exactly one patient and one contact — the value itself is never stored), `matched_at`, `run_id` (the matching run, provenance), `revoked_at`, `revoked_reason`, `sent_count` / `last_sent_at` (Ads outcomes sent under the pair). Live first.
69484
+ *
69485
+ * **Connected apps:** refused with `403 CONNECTED_APP_SENSITIVE_DATA`, whatever scopes they hold — this operation carries a dato sensible (ADR 0106 §4). Only an API key or a personal token of the workspace that holds the scope reaches it.
69486
+ */
69487
+ get: {
69488
+ parameters: {
69489
+ query?: never;
69490
+ header?: never;
69491
+ path: {
69492
+ contactId: string;
69493
+ };
69494
+ cookie?: never;
69495
+ };
69496
+ requestBody?: never;
69497
+ responses: {
69498
+ /** @description Contact auto-links */
69499
+ 200: {
69500
+ headers: {
69501
+ [name: string]: unknown;
69502
+ };
69503
+ content: {
69504
+ /**
69505
+ * @example {
69506
+ * "data": {
69507
+ * "data": [
69425
69508
  * {
69426
- * "month": "2026-06",
69427
- * "accepted_clp": 24100000,
69428
- * "produced_clp": 19700000,
69429
- * "collected_clp": 16200000,
69430
- * "cierres": 62,
69431
- * "cierres_clp": 20300000
69509
+ * "id": "a7a7a7a7-0000-4000-8000-000000000001",
69510
+ * "clinic_patient_id": "b1b0b8de-0000-4000-8000-000000000001",
69511
+ * "contact_id": "1a73af9e-0000-4000-8000-000000000001",
69512
+ * "patient_name": "María José Fuentes Lagos",
69513
+ * "contact_name": "María José Fuentes Lagos",
69514
+ * "status": "live",
69515
+ * "method": "phone_unique",
69516
+ * "matched_at": "2026-09-27T14:10:02.118Z",
69517
+ * "run_id": "a8a8a8a8-0000-4000-8000-000000000001",
69518
+ * "revoked_at": null,
69519
+ * "revoked_reason": null,
69520
+ * "sent_count": 3,
69521
+ * "last_sent_at": "2026-09-28T12:02:40.551Z"
69432
69522
  * }
69433
- * ],
69434
- * "first_visit": {
69435
- * "formula": "propuesta",
69436
- * "patients": 210,
69437
- * "paid": 97,
69438
- * "rate": 46.2,
69439
- * "rule": "Fórmula propuesta."
69440
- * },
69441
- * "second_visit": {
69442
- * "formula": "propuesta",
69443
- * "eligible": 113,
69444
- * "converted": 29,
69445
- * "rate": 25.7,
69446
- * "rule": "Fórmula propuesta."
69447
- * },
69448
- * "botados": {
69449
- * "unaccepted": {
69450
- * "count": 88,
69451
- * "clp": 31200000
69452
- * },
69453
- * "accepted_unscheduled": {
69454
- * "count": 17,
69455
- * "clp": 6400000
69456
- * },
69457
- * "mid_treatment_no_future_cita": {
69458
- * "count": 23,
69459
- * "clp": 9800000
69460
- * },
69461
- * "formula": "propuesta",
69462
- * "basis": "live_last_6_months",
69463
- * "total": {
69464
- * "count": 128,
69465
- * "clp": 47400000
69466
- * },
69467
- * "focus": "unaccepted",
69468
- * "rule": "Fórmula propuesta."
69469
- * },
69470
- * "coverage": {
69471
- * "plans_without_lines": 0,
69472
- * "excluded_zero": 301,
69473
- * "excluded_entry_only": 92
69474
- * }
69523
+ * ]
69475
69524
  * }
69476
69525
  * }
69477
69526
  */
69478
69527
  "application/json": {
69479
- data: components["schemas"]["ClinicComercialReport"];
69528
+ data?: unknown;
69529
+ meta?: {
69530
+ [key: string]: unknown;
69531
+ };
69532
+ };
69533
+ };
69534
+ };
69535
+ /** @description Validation error */
69536
+ 400: {
69537
+ headers: {
69538
+ [name: string]: unknown;
69539
+ };
69540
+ content: {
69541
+ "application/json": components["schemas"]["Error"];
69542
+ };
69543
+ };
69544
+ /** @description Unauthorized */
69545
+ 401: {
69546
+ headers: {
69547
+ [name: string]: unknown;
69548
+ };
69549
+ content: {
69550
+ "application/json": components["schemas"]["Error"];
69551
+ };
69552
+ };
69553
+ /** @description Forbidden */
69554
+ 403: {
69555
+ headers: {
69556
+ [name: string]: unknown;
69557
+ };
69558
+ content: {
69559
+ "application/json": components["schemas"]["Error"];
69560
+ };
69561
+ };
69562
+ /** @description Not found */
69563
+ 404: {
69564
+ headers: {
69565
+ [name: string]: unknown;
69566
+ };
69567
+ content: {
69568
+ "application/json": components["schemas"]["Error"];
69569
+ };
69570
+ };
69571
+ /** @description Conflict (incl. Idempotency-Key reuse with different body) */
69572
+ 409: {
69573
+ headers: {
69574
+ [name: string]: unknown;
69575
+ };
69576
+ content: {
69577
+ "application/json": components["schemas"]["Error"];
69578
+ };
69579
+ };
69580
+ /** @description Rate limited */
69581
+ 429: {
69582
+ headers: {
69583
+ [name: string]: unknown;
69584
+ };
69585
+ content: {
69586
+ "application/json": components["schemas"]["Error"];
69587
+ };
69588
+ };
69589
+ };
69590
+ };
69591
+ put?: never;
69592
+ post?: never;
69593
+ delete?: never;
69594
+ options?: never;
69595
+ head?: never;
69596
+ patch?: never;
69597
+ trace?: never;
69598
+ };
69599
+ "/clinic/auto-links/{id}/reject": {
69600
+ parameters: {
69601
+ query?: never;
69602
+ header?: never;
69603
+ path?: never;
69604
+ cookie?: never;
69605
+ };
69606
+ get?: never;
69607
+ put?: never;
69608
+ /**
69609
+ * «No es esta persona» — reject an automatic link for good
69610
+ * @description Marks the pair `rejected`: no later matching run re-opens it, and the Ads outcomes sent under it that have not left Vitrina yet are withheld (`withheld_outcomes` says how many). What already reached the ad platforms is not recalled. Idempotent. Answers the row. 404 when the id is not this clinic’s.
69611
+ *
69612
+ * **Connected apps:** refused with `403 CONNECTED_APP_SENSITIVE_DATA`, whatever scopes they hold — this operation carries a dato sensible (ADR 0106 §4). Only an API key or a personal token of the workspace that holds the scope reaches it.
69613
+ */
69614
+ post: {
69615
+ parameters: {
69616
+ query?: never;
69617
+ header?: {
69618
+ /** @description Replay-safe retries: resending the SAME key with the SAME body returns the original response (`X-Idempotent-Replay: 1`) instead of creating a second copy — safe to send whenever a response might not have arrived. The same key with a DIFFERENT body answers `409 IDEMPOTENCY_KEY_CONFLICT`; use a fresh key per operation. */
69619
+ "Idempotency-Key"?: string;
69620
+ };
69621
+ path: {
69622
+ id: string;
69623
+ };
69624
+ cookie?: never;
69625
+ };
69626
+ requestBody?: {
69627
+ content: {
69628
+ /** @example {} */
69629
+ "application/json": Record<string, never>;
69630
+ };
69631
+ };
69632
+ responses: {
69633
+ /** @description Rejected auto-link */
69634
+ 200: {
69635
+ headers: {
69636
+ [name: string]: unknown;
69637
+ };
69638
+ content: {
69639
+ /**
69640
+ * @example {
69641
+ * "data": {
69642
+ * "id": "a7a7a7a7-0000-4000-8000-000000000001",
69643
+ * "clinic_patient_id": "b1b0b8de-0000-4000-8000-000000000001",
69644
+ * "contact_id": "1a73af9e-0000-4000-8000-000000000001",
69645
+ * "patient_name": "María José Fuentes Lagos",
69646
+ * "contact_name": "María José Fuentes Lagos",
69647
+ * "status": "rejected",
69648
+ * "method": "phone_unique",
69649
+ * "matched_at": "2026-09-27T14:10:02.118Z",
69650
+ * "run_id": "a8a8a8a8-0000-4000-8000-000000000001",
69651
+ * "revoked_at": "2026-09-28T15:31:09.004Z",
69652
+ * "revoked_reason": "rejected",
69653
+ * "sent_count": 3,
69654
+ * "last_sent_at": "2026-09-28T12:02:40.551Z",
69655
+ * "withheld_outcomes": 1
69656
+ * }
69657
+ * }
69658
+ */
69659
+ "application/json": {
69660
+ data?: unknown;
69661
+ meta?: {
69662
+ [key: string]: unknown;
69663
+ };
69664
+ };
69665
+ };
69666
+ };
69667
+ /** @description Validation error */
69668
+ 400: {
69669
+ headers: {
69670
+ [name: string]: unknown;
69671
+ };
69672
+ content: {
69673
+ "application/json": components["schemas"]["Error"];
69674
+ };
69675
+ };
69676
+ /** @description Unauthorized */
69677
+ 401: {
69678
+ headers: {
69679
+ [name: string]: unknown;
69680
+ };
69681
+ content: {
69682
+ "application/json": components["schemas"]["Error"];
69683
+ };
69684
+ };
69685
+ /** @description Forbidden */
69686
+ 403: {
69687
+ headers: {
69688
+ [name: string]: unknown;
69689
+ };
69690
+ content: {
69691
+ "application/json": components["schemas"]["Error"];
69692
+ };
69693
+ };
69694
+ /** @description Not found */
69695
+ 404: {
69696
+ headers: {
69697
+ [name: string]: unknown;
69698
+ };
69699
+ content: {
69700
+ "application/json": components["schemas"]["Error"];
69701
+ };
69702
+ };
69703
+ /** @description Conflict (incl. Idempotency-Key reuse with different body) */
69704
+ 409: {
69705
+ headers: {
69706
+ [name: string]: unknown;
69707
+ };
69708
+ content: {
69709
+ "application/json": components["schemas"]["Error"];
69710
+ };
69711
+ };
69712
+ /** @description Rate limited */
69713
+ 429: {
69714
+ headers: {
69715
+ [name: string]: unknown;
69716
+ };
69717
+ content: {
69718
+ "application/json": components["schemas"]["Error"];
69719
+ };
69720
+ };
69721
+ };
69722
+ };
69723
+ delete?: never;
69724
+ options?: never;
69725
+ head?: never;
69726
+ patch?: never;
69727
+ trace?: never;
69728
+ };
69729
+ "/clinic/insights/comercial": {
69730
+ parameters: {
69731
+ query?: never;
69732
+ header?: never;
69733
+ path?: never;
69734
+ cookie?: never;
69735
+ };
69736
+ /**
69737
+ * Commercial numbers: presupuesto acceptance, cierres, cash, botados
69738
+ * @description The clinic's commercial numbers, aggregates only (counts, CLP, percentages, months and professional names; never a patient). `regime` says how acceptance is read: `evidencia` when the revenue mirror is on (a non-entry line performed, or a cita booked after the presupuesto, inside its window), `explicita` otherwise (the acceptance the clinic recorded). Acceptance is by COUNT of presupuestos, one per lineage; $0 and entry-only presupuestos are out of both sides; the window is 60 days (90 for ortodoncia and implantología); the rate goes to the professional who issued the presupuesto. Presupuestos ISSUED in the window form the acceptance cohort (`cohorts` by issue month with 30- and 60-day reads). A cierre is the first payment on an accepted presupuesto: once per presupuesto, dated on that payment, valued at the full accepted value. `headline` is the cash collected in the window against the previous window of the same length. `first_visit`, `second_visit` and `botados` carry `formula: propuesta`; `botados` is a live worklist over the last six months. Every KPI carries its `formula` and a printable `rule`; a rate with no denominator is `null`, never 0. `state` is `empty`, `settling` (presupuesto lines still being read; `state_reason` says so) or `ready`. Requires `clinic_money:read` (or `clinic_insights:read`, which reads the same aggregates). Accepts `from` / `to` (bare `YYYY-MM-DD`, `to` INCLUSIVE; default the 90 days ending today in the clinic's zone), `location_id` and `professional_id`. With `professional_id`, cash (`headline`, `months[].collected_clp`) is `null`: a payment belongs to the patient, not to a professional. A caller WITHOUT `clinic_money:read` always reads one fixed window — the last 3 closed calendar months — whatever range or filter it asks (`suppression.filters_ignored`), with every count or CLP resting on 1–4 cases `null`, rates over fewer than 5 cases `null`, complementary suppression over professionals, cohorts and botados lists, and month rows carrying only cash.
69739
+ *
69740
+ * **Connected apps:** every patient and contact in the response is a Seudónimo de paciente — initials plus a stable number, `"M.F. · #1001"` — unless the clinic allowed patient names, with RUT, phone and email masked in free text and `meta.patient_privacy` saying so. Clinical alerts (`flags`) are withheld in both modes, and a non-JSON body (an export, a file) is refused with `403 CONNECTED_APP_SENSITIVE_DATA`.
69741
+ */
69742
+ get: {
69743
+ parameters: {
69744
+ query?: {
69745
+ /** @description First day of the window, inclusive. Defaults to 90 days before `to`. */
69746
+ from?: string;
69747
+ /** @description Last day of the window, INCLUSIVE. Defaults to today in the clinic's zone. */
69748
+ to?: string;
69749
+ /** @description One sucursal. Omit for all of them. */
69750
+ location_id?: string;
69751
+ /** @description One profesional. A foreign id is a 404, never an empty report. */
69752
+ professional_id?: string;
69753
+ };
69754
+ header?: never;
69755
+ path?: never;
69756
+ cookie?: never;
69757
+ };
69758
+ requestBody?: never;
69759
+ responses: {
69760
+ /** @description Clinic commercial numbers */
69761
+ 200: {
69762
+ headers: {
69763
+ [name: string]: unknown;
69764
+ };
69765
+ content: {
69766
+ /**
69767
+ * @example {
69768
+ * "data": {
69769
+ * "from": "2026-06-01",
69770
+ * "to": "2026-08-29",
69771
+ * "timezone": "America/Santiago",
69772
+ * "location_id": null,
69773
+ * "professional_id": null,
69774
+ * "state": "ready",
69775
+ * "state_reason": null,
69776
+ * "regime": "evidencia",
69777
+ * "regime_label": "Aceptación por evidencia",
69778
+ * "regime_rule": "Leído desde tu software de agenda: un presupuesto cuenta como aceptado cuando se realizó una de sus prestaciones o el paciente agendó una cita después de recibirlo.",
69779
+ * "source": {
69780
+ * "kind": "mirror",
69781
+ * "synced_at": "2026-08-30T03:10:00.000Z"
69782
+ * },
69783
+ * "formulas": {
69784
+ * "acceptance": "mentores_2026_09_25",
69785
+ * "cierre": "cierre_2026_09_25",
69786
+ * "first_visit": "propuesta",
69787
+ * "second_visit": "propuesta",
69788
+ * "botados": "propuesta"
69789
+ * },
69790
+ * "headline": {
69791
+ * "collected_clp": 48500000,
69792
+ * "previous_collected_clp": 44100000,
69793
+ * "delta_pct": 10,
69794
+ * "previous_from": "2026-03-03",
69795
+ * "previous_to": "2026-05-31",
69796
+ * "rule": "Lo cobrado en el período: todos los pagos recibidos, sin los anulados. Se compara con el período anterior del mismo largo."
69797
+ * },
69798
+ * "acceptance": {
69799
+ * "presented": 420,
69800
+ * "accepted": 231,
69801
+ * "rate": 55,
69802
+ * "same_day": 118,
69803
+ * "later": 113,
69804
+ * "cita_only": 14,
69805
+ * "pending": 36,
69806
+ * "presented_clp": 142000000,
69807
+ * "accepted_clp": 71300000,
69808
+ * "rate_30d": 47.2,
69809
+ * "rate_60d": 53.8,
69810
+ * "formula": "mentores_2026_09_25",
69811
+ * "window_days": 60,
69812
+ * "long_window_days": 90,
69813
+ * "rule": "Presupuestos aceptados ÷ presupuestos presentados, cada presupuesto una vez."
69814
+ * },
69815
+ * "cierres": {
69816
+ * "count": 180,
69817
+ * "value_clp": 58900000,
69818
+ * "accepted_without_payment": 51,
69819
+ * "formula": "cierre_2026_09_25",
69820
+ * "rule": "Un cierre es el primer pago de un presupuesto aceptado."
69821
+ * },
69822
+ * "cohorts": [
69823
+ * {
69824
+ * "month": "2026-06",
69825
+ * "presented": 140,
69826
+ * "accepted": 80,
69827
+ * "rate": 57.1,
69828
+ * "same_day": 41,
69829
+ * "later": 39,
69830
+ * "cita_only": 2,
69831
+ * "pending": 0,
69832
+ * "presented_clp": 47000000,
69833
+ * "accepted_clp": 24800000,
69834
+ * "rate_30d": 49.3,
69835
+ * "rate_60d": 56.4,
69836
+ * "mature": true
69837
+ * }
69838
+ * ],
69839
+ * "by_professional": [
69840
+ * {
69841
+ * "professional_id": "0f3c2a9e-5b1d-4c7e-9a42-6d8e1f2b3c4d",
69842
+ * "professional_name": "Dra. Camila Soto",
69843
+ * "presented": 96,
69844
+ * "accepted": 61,
69845
+ * "rate": 63.5,
69846
+ * "same_day": 30,
69847
+ * "later": 31,
69848
+ * "cita_only": 3,
69849
+ * "pending": 8,
69850
+ * "presented_clp": 33400000,
69851
+ * "accepted_clp": 19900000,
69852
+ * "cierres": 47,
69853
+ * "cierres_clp": 15100000
69854
+ * }
69855
+ * ],
69856
+ * "unassigned_presented": 0,
69857
+ * "unassigned_cierres": 0,
69858
+ * "months": [
69859
+ * {
69860
+ * "month": "2026-06",
69861
+ * "accepted_clp": 24100000,
69862
+ * "produced_clp": 19700000,
69863
+ * "collected_clp": 16200000,
69864
+ * "cierres": 62,
69865
+ * "cierres_clp": 20300000
69866
+ * }
69867
+ * ],
69868
+ * "first_visit": {
69869
+ * "formula": "propuesta",
69870
+ * "patients": 210,
69871
+ * "paid": 97,
69872
+ * "rate": 46.2,
69873
+ * "rule": "Fórmula propuesta."
69874
+ * },
69875
+ * "second_visit": {
69876
+ * "formula": "propuesta",
69877
+ * "eligible": 113,
69878
+ * "converted": 29,
69879
+ * "rate": 25.7,
69880
+ * "rule": "Fórmula propuesta."
69881
+ * },
69882
+ * "botados": {
69883
+ * "unaccepted": {
69884
+ * "count": 88,
69885
+ * "clp": 31200000
69886
+ * },
69887
+ * "accepted_unscheduled": {
69888
+ * "count": 17,
69889
+ * "clp": 6400000
69890
+ * },
69891
+ * "mid_treatment_no_future_cita": {
69892
+ * "count": 23,
69893
+ * "clp": 9800000
69894
+ * },
69895
+ * "formula": "propuesta",
69896
+ * "basis": "live_last_6_months",
69897
+ * "total": {
69898
+ * "count": 128,
69899
+ * "clp": 47400000
69900
+ * },
69901
+ * "focus": "unaccepted",
69902
+ * "rule": "Fórmula propuesta."
69903
+ * },
69904
+ * "coverage": {
69905
+ * "plans_without_lines": 0,
69906
+ * "excluded_zero": 301,
69907
+ * "excluded_entry_only": 92
69908
+ * }
69909
+ * }
69910
+ * }
69911
+ */
69912
+ "application/json": {
69913
+ data: components["schemas"]["ClinicComercialReport"];
69480
69914
  };
69481
69915
  };
69482
69916
  };
@@ -76199,7 +76633,8 @@ interface paths {
76199
76633
  * "display_name": "Garantía"
76200
76634
  * }
76201
76635
  * ],
76202
- * "last_call": null
76636
+ * "last_call": null,
76637
+ * "handoff_urgent": false
76203
76638
  * },
76204
76639
  * {
76205
76640
  * "id": "bbbbbbbb-0000-4000-8000-000000000002",
@@ -76335,7 +76770,8 @@ interface paths {
76335
76770
  * "captured_at": "2026-09-22T10:41:12.004Z"
76336
76771
  * },
76337
76772
  * "tags": [],
76338
- * "last_call": null
76773
+ * "last_call": null,
76774
+ * "handoff_urgent": false
76339
76775
  * }
76340
76776
  * ],
76341
76777
  * "meta": {
@@ -76740,7 +77176,9 @@ interface paths {
76740
77176
  *
76741
77177
  * Also carries `ad_origin_nudge: 'connect_meta_ads' | null`, non-null ONLY when the caller holds `integrations:write`, the conversation came from a Meta ad (click-to-WhatsApp or an Instagram DM ad), and the workspace has no connected `meta_ads` integration. THIS ROUTE ONLY — deliberately absent from the list, where the predicate would be an N+1.
76742
77178
  *
76743
- * And `handoff_resolved_suggested: boolean` (#2968) — true only on an open/pending AI-handoff thread whose customer is still waiting on us AND Jev already read their own pending words as saying the matter is resolved (checked once per customer message, at the next handoff re-notify tick). The inbox reads this to offer a one-click close. THIS ROUTE ONLY, same N+1 reasoning as `ad_origin_nudge`.
77179
+ * And `handoff_resolved_suggested: boolean` (#2968) — true only on an open/pending AI-handoff thread whose customer is still waiting on us AND Jev already read their own pending words as saying the matter is resolved (checked once per customer message, at the next handoff re-notify tick). The inbox reads this to offer a one-click close. THIS ROUTE ONLY, same N+1 reasoning as `ad_origin_nudge` — unlike `handoff_urgent` below, this one was never worth a column since only the open thread needs it.
77180
+ *
77181
+ * `handoff_urgent: boolean` (#2969) is a plain `conversation` column, not a per-route annotation: true while an open/pending AI-handoff thread's customer is still waiting on us and Jev read their newest pending message as urgent (a clinic post-treatment complication, or an explicit same-day deadline) — kept in sync by the same classification that also tightens the re-notify cadence, and present on every conversation read, list included, because the inbox shows the flag on the conversation ROW too, not only its header.
76744
77182
  *
76745
77183
  * Carries `ad_origin` exactly as `GET /conversations` rows do.
76746
77184
  */
@@ -76883,6 +77321,7 @@ interface paths {
76883
77321
  * }
76884
77322
  * ],
76885
77323
  * "last_call": null,
77324
+ * "handoff_urgent": false,
76886
77325
  * "ad_origin_nudge": null,
76887
77326
  * "handoff_resolved_suggested": false,
76888
77327
  * "message": [
@@ -92624,75 +93063,982 @@ interface paths {
92624
93063
  };
92625
93064
  };
92626
93065
  };
92627
- put?: never;
92628
- post?: never;
93066
+ put?: never;
93067
+ post?: never;
93068
+ delete?: never;
93069
+ options?: never;
93070
+ head?: never;
93071
+ patch?: never;
93072
+ trace?: never;
93073
+ };
93074
+ "/insights/generate-pdf": {
93075
+ parameters: {
93076
+ query?: never;
93077
+ header?: never;
93078
+ path?: never;
93079
+ cookie?: never;
93080
+ };
93081
+ get?: never;
93082
+ put?: never;
93083
+ /**
93084
+ * Render a report as a PDF
93085
+ * @description **The one endpoint on the tenant API that does not answer the `{ data }` envelope.** It replies with raw `application/pdf` bytes and a `Content-Disposition: attachment` header, so a client that pipes every response through the usual JSON parse will fail here. On an error it reverts to the JSON error envelope, which means the response content-type is what tells the two apart.
93086
+ *
93087
+ * Generation is SYNCHRONOUS: it queries the report, runs an LLM pass to write the narrative, then renders. The connection is held for all of that, there is no job id, and nothing is persisted — a client or proxy timeout loses the document with no way to retrieve it other than asking again. Treat a wide range as a slow request.
93088
+ *
93089
+ * The LLM pass degrades rather than fails: if the narrative cannot be generated you still get a 200 and a complete PDF, with a Spanish note in place of the executive summary and every raw figure intact. So a successful response does not by itself mean the analysis is there.
93090
+ *
93091
+ * `range.preset` is not a range selector — `from`/`to` alone decide the data. It is the window’s NAME: printed as the cover subtitle and passed into the summariser’s prompt, so a preset that disagrees with `from`/`to` mislabels the cover and misleads the narrative while the figures stay correct. Defaults to `custom`. Needs only `analytics:read`.
93092
+ */
93093
+ post: {
93094
+ parameters: {
93095
+ query?: never;
93096
+ header?: {
93097
+ /** @description Replay-safe retries: resending the SAME key with the SAME body returns the original response (`X-Idempotent-Replay: 1`) instead of creating a second copy — safe to send whenever a response might not have arrived. The same key with a DIFFERENT body answers `409 IDEMPOTENCY_KEY_CONFLICT`; use a fresh key per operation. */
93098
+ "Idempotency-Key"?: string;
93099
+ };
93100
+ path?: never;
93101
+ cookie?: never;
93102
+ };
93103
+ requestBody?: {
93104
+ content: {
93105
+ /**
93106
+ * @example {
93107
+ * "report": "conversations",
93108
+ * "window": {
93109
+ * "preset": "7d"
93110
+ * },
93111
+ * "range": {
93112
+ * "preset": "7d"
93113
+ * }
93114
+ * }
93115
+ */
93116
+ "application/json": {
93117
+ /** @enum {string} */
93118
+ report_type: "overview" | "conversations" | "agents" | "ai_agents" | "bots" | "tags" | "channels" | "teams" | "leads" | "csat" | "sla" | "clinic_ocupacion" | "clinic_inasistencias" | "clinic_produccion" | "clinic_packs";
93119
+ range: {
93120
+ from: string;
93121
+ to: string;
93122
+ preset?: string;
93123
+ };
93124
+ };
93125
+ };
93126
+ };
93127
+ responses: {
93128
+ /** @description The rendered PDF as an attachment */
93129
+ 200: {
93130
+ headers: {
93131
+ [name: string]: unknown;
93132
+ };
93133
+ content: {
93134
+ "application/pdf": string;
93135
+ };
93136
+ };
93137
+ /** @description Validation error */
93138
+ 400: {
93139
+ headers: {
93140
+ [name: string]: unknown;
93141
+ };
93142
+ content: {
93143
+ "application/json": components["schemas"]["Error"];
93144
+ };
93145
+ };
93146
+ /** @description Unauthorized */
93147
+ 401: {
93148
+ headers: {
93149
+ [name: string]: unknown;
93150
+ };
93151
+ content: {
93152
+ "application/json": components["schemas"]["Error"];
93153
+ };
93154
+ };
93155
+ /** @description Forbidden */
93156
+ 403: {
93157
+ headers: {
93158
+ [name: string]: unknown;
93159
+ };
93160
+ content: {
93161
+ "application/json": components["schemas"]["Error"];
93162
+ };
93163
+ };
93164
+ /** @description Not found */
93165
+ 404: {
93166
+ headers: {
93167
+ [name: string]: unknown;
93168
+ };
93169
+ content: {
93170
+ "application/json": components["schemas"]["Error"];
93171
+ };
93172
+ };
93173
+ /** @description Conflict (incl. Idempotency-Key reuse with different body) */
93174
+ 409: {
93175
+ headers: {
93176
+ [name: string]: unknown;
93177
+ };
93178
+ content: {
93179
+ "application/json": components["schemas"]["Error"];
93180
+ };
93181
+ };
93182
+ /** @description Rate limited */
93183
+ 429: {
93184
+ headers: {
93185
+ [name: string]: unknown;
93186
+ };
93187
+ content: {
93188
+ "application/json": components["schemas"]["Error"];
93189
+ };
93190
+ };
93191
+ };
93192
+ };
93193
+ delete?: never;
93194
+ options?: never;
93195
+ head?: never;
93196
+ patch?: never;
93197
+ trace?: never;
93198
+ };
93199
+ "/insights/schedules": {
93200
+ parameters: {
93201
+ query?: never;
93202
+ header?: never;
93203
+ path?: never;
93204
+ cookie?: never;
93205
+ };
93206
+ /**
93207
+ * List report schedules
93208
+ * @description The standing subscriptions that mail a rendered report out on a cadence. Unpaginated, and with no `meta.total` — this list is small by construction.
93209
+ */
93210
+ get: {
93211
+ parameters: {
93212
+ query?: never;
93213
+ header?: never;
93214
+ path?: never;
93215
+ cookie?: never;
93216
+ };
93217
+ requestBody?: never;
93218
+ responses: {
93219
+ /** @description Report schedules */
93220
+ 200: {
93221
+ headers: {
93222
+ [name: string]: unknown;
93223
+ };
93224
+ content: {
93225
+ /**
93226
+ * @example {
93227
+ * "data": [
93228
+ * {
93229
+ * "id": "cccccccc-2000-4000-8000-000000000001",
93230
+ * "report_type": "conversations",
93231
+ * "range_preset": "7d",
93232
+ * "cadence": "weekly",
93233
+ * "recipients": [
93234
+ * "gerencia@midealer.cl"
93235
+ * ],
93236
+ * "next_run_at": "2026-09-29T09:00:00.000Z",
93237
+ * "enabled": true
93238
+ * }
93239
+ * ]
93240
+ * }
93241
+ */
93242
+ "application/json": {
93243
+ data?: unknown;
93244
+ meta?: {
93245
+ [key: string]: unknown;
93246
+ };
93247
+ };
93248
+ };
93249
+ };
93250
+ /** @description Validation error */
93251
+ 400: {
93252
+ headers: {
93253
+ [name: string]: unknown;
93254
+ };
93255
+ content: {
93256
+ "application/json": components["schemas"]["Error"];
93257
+ };
93258
+ };
93259
+ /** @description Unauthorized */
93260
+ 401: {
93261
+ headers: {
93262
+ [name: string]: unknown;
93263
+ };
93264
+ content: {
93265
+ "application/json": components["schemas"]["Error"];
93266
+ };
93267
+ };
93268
+ /** @description Forbidden */
93269
+ 403: {
93270
+ headers: {
93271
+ [name: string]: unknown;
93272
+ };
93273
+ content: {
93274
+ "application/json": components["schemas"]["Error"];
93275
+ };
93276
+ };
93277
+ /** @description Not found */
93278
+ 404: {
93279
+ headers: {
93280
+ [name: string]: unknown;
93281
+ };
93282
+ content: {
93283
+ "application/json": components["schemas"]["Error"];
93284
+ };
93285
+ };
93286
+ /** @description Conflict (incl. Idempotency-Key reuse with different body) */
93287
+ 409: {
93288
+ headers: {
93289
+ [name: string]: unknown;
93290
+ };
93291
+ content: {
93292
+ "application/json": components["schemas"]["Error"];
93293
+ };
93294
+ };
93295
+ /** @description Rate limited */
93296
+ 429: {
93297
+ headers: {
93298
+ [name: string]: unknown;
93299
+ };
93300
+ content: {
93301
+ "application/json": components["schemas"]["Error"];
93302
+ };
93303
+ };
93304
+ };
93305
+ };
93306
+ put?: never;
93307
+ post?: never;
93308
+ delete?: never;
93309
+ options?: never;
93310
+ head?: never;
93311
+ patch?: never;
93312
+ trace?: never;
93313
+ };
93314
+ "/integrations/meta-ads": {
93315
+ parameters: {
93316
+ query?: never;
93317
+ header?: never;
93318
+ path?: never;
93319
+ cookie?: never;
93320
+ };
93321
+ /**
93322
+ * Read the workspace’s Meta Ads connection
93323
+ * @description The tenant’s single Meta Ads row, or `null` when it has never started a connect. A placeholder object with `status: "disconnected"` is deliberately NOT returned in its place: "never connected" and "was connected and is not any more" are different states and the card says different things about them.
93324
+ *
93325
+ * `handoff` is populated only while `status` is `pending` and `candidates` only while it is `pending_selection` — a spent consent URL would render a button leading to a dead page.
93326
+ *
93327
+ * Requires `integrations:read` or `ads:read` (owner, admin and the Marketing role); a `supervisor` or `agent` gets 403 here and never sees the card. The writes need `integrations:write` or `ads:write`.
93328
+ */
93329
+ get: {
93330
+ parameters: {
93331
+ query?: never;
93332
+ header?: never;
93333
+ path?: never;
93334
+ cookie?: never;
93335
+ };
93336
+ requestBody?: never;
93337
+ responses: {
93338
+ /** @description The Meta Ads integration, or `null` */
93339
+ 200: {
93340
+ headers: {
93341
+ [name: string]: unknown;
93342
+ };
93343
+ content: {
93344
+ /**
93345
+ * @example {
93346
+ * "data": null
93347
+ * }
93348
+ */
93349
+ "application/json": {
93350
+ data: components["schemas"]["MetaAdsIntegration"];
93351
+ };
93352
+ };
93353
+ };
93354
+ /** @description Validation error */
93355
+ 400: {
93356
+ headers: {
93357
+ [name: string]: unknown;
93358
+ };
93359
+ content: {
93360
+ "application/json": components["schemas"]["Error"];
93361
+ };
93362
+ };
93363
+ /** @description Unauthorized */
93364
+ 401: {
93365
+ headers: {
93366
+ [name: string]: unknown;
93367
+ };
93368
+ content: {
93369
+ "application/json": components["schemas"]["Error"];
93370
+ };
93371
+ };
93372
+ /** @description Forbidden */
93373
+ 403: {
93374
+ headers: {
93375
+ [name: string]: unknown;
93376
+ };
93377
+ content: {
93378
+ "application/json": components["schemas"]["Error"];
93379
+ };
93380
+ };
93381
+ /** @description Not found */
93382
+ 404: {
93383
+ headers: {
93384
+ [name: string]: unknown;
93385
+ };
93386
+ content: {
93387
+ "application/json": components["schemas"]["Error"];
93388
+ };
93389
+ };
93390
+ /** @description Conflict (incl. Idempotency-Key reuse with different body) */
93391
+ 409: {
93392
+ headers: {
93393
+ [name: string]: unknown;
93394
+ };
93395
+ content: {
93396
+ "application/json": components["schemas"]["Error"];
93397
+ };
93398
+ };
93399
+ /** @description Rate limited */
93400
+ 429: {
93401
+ headers: {
93402
+ [name: string]: unknown;
93403
+ };
93404
+ content: {
93405
+ "application/json": components["schemas"]["Error"];
93406
+ };
93407
+ };
93408
+ };
93409
+ };
93410
+ put?: never;
93411
+ post?: never;
93412
+ delete?: never;
93413
+ options?: never;
93414
+ head?: never;
93415
+ patch?: never;
93416
+ trace?: never;
93417
+ };
93418
+ "/integrations/meta-ads/connect": {
93419
+ parameters: {
93420
+ query?: never;
93421
+ header?: never;
93422
+ path?: never;
93423
+ cookie?: never;
93424
+ };
93425
+ get?: never;
93426
+ put?: never;
93427
+ /**
93428
+ * Start a Meta Ads connect and get the consent URL
93429
+ * @description Mints the connection provider’s **connect hand-off** for `meta_ads` and answers the session-less consent URL to redirect the WHOLE PAGE to. It is not an iframe target: Meta’s own consent dialog renders inside it.
93430
+ *
93431
+ * `return_to` must be an absolute URL on the app’s own origin. The backend appends `meta_ads=return` and a short opaque `state`, preserving any query the URL already carried, and passes the result to the provider as `return_url`. An origin that is not the app’s is a 400 here rather than a silent substitution — the provider itself only checks the origin when the human CLICKS, where it surfaces as a failed hand-off (`result.reason: origin_not_allowed`) after the dealer has already walked through a consent screen for nothing.
93432
+ *
93433
+ * **A second click reuses the live hand-off.** The provider’s URL is single-use and 45-minute-lived, and minting twice yields two hand-offs rather than an error, so a dealer who clicks, gets distracted and clicks again gets the SAME `url` and `handoff_id` back until it expires.
93434
+ *
93435
+ * Before anything is minted, the tenant’s attribution profile and its delegated key are ensured (ADR 0099). This rail never uses a platform-wide provider key.
93436
+ */
93437
+ post: {
93438
+ parameters: {
93439
+ query?: never;
93440
+ header?: {
93441
+ /** @description Replay-safe retries: resending the SAME key with the SAME body returns the original response (`X-Idempotent-Replay: 1`) instead of creating a second copy — safe to send whenever a response might not have arrived. The same key with a DIFFERENT body answers `409 IDEMPOTENCY_KEY_CONFLICT`; use a fresh key per operation. */
93442
+ "Idempotency-Key"?: string;
93443
+ };
93444
+ path?: never;
93445
+ cookie?: never;
93446
+ };
93447
+ requestBody?: {
93448
+ content: {
93449
+ /**
93450
+ * @example {
93451
+ * "return_to": "https://app.vitrinadev.com/settings/integraciones?meta_ads=return"
93452
+ * }
93453
+ */
93454
+ "application/json": components["schemas"]["MetaAdsConnectBody"];
93455
+ };
93456
+ };
93457
+ responses: {
93458
+ /** @description The consent URL, its hand-off id, and its expiry */
93459
+ 200: {
93460
+ headers: {
93461
+ [name: string]: unknown;
93462
+ };
93463
+ content: {
93464
+ /**
93465
+ * @example {
93466
+ * "data": {
93467
+ * "url": "https://consent.example.com/connect/meta-ads/ho_4d8f2a91c6b3",
93468
+ * "handoff_id": "ho_4d8f2a91c6b3",
93469
+ * "expires_at": "2026-09-23T15:45:00.000Z"
93470
+ * }
93471
+ * }
93472
+ */
93473
+ "application/json": {
93474
+ data: components["schemas"]["MetaAdsConnectResult"];
93475
+ };
93476
+ };
93477
+ };
93478
+ /** @description Validation error */
93479
+ 400: {
93480
+ headers: {
93481
+ [name: string]: unknown;
93482
+ };
93483
+ content: {
93484
+ "application/json": components["schemas"]["Error"];
93485
+ };
93486
+ };
93487
+ /** @description Unauthorized */
93488
+ 401: {
93489
+ headers: {
93490
+ [name: string]: unknown;
93491
+ };
93492
+ content: {
93493
+ "application/json": components["schemas"]["Error"];
93494
+ };
93495
+ };
93496
+ /** @description Forbidden */
93497
+ 403: {
93498
+ headers: {
93499
+ [name: string]: unknown;
93500
+ };
93501
+ content: {
93502
+ "application/json": components["schemas"]["Error"];
93503
+ };
93504
+ };
93505
+ /** @description Not found */
93506
+ 404: {
93507
+ headers: {
93508
+ [name: string]: unknown;
93509
+ };
93510
+ content: {
93511
+ "application/json": components["schemas"]["Error"];
93512
+ };
93513
+ };
93514
+ /** @description Conflict (incl. Idempotency-Key reuse with different body) */
93515
+ 409: {
93516
+ headers: {
93517
+ [name: string]: unknown;
93518
+ };
93519
+ content: {
93520
+ "application/json": components["schemas"]["Error"];
93521
+ };
93522
+ };
93523
+ /** @description Rate limited */
93524
+ 429: {
93525
+ headers: {
93526
+ [name: string]: unknown;
93527
+ };
93528
+ content: {
93529
+ "application/json": components["schemas"]["Error"];
93530
+ };
93531
+ };
93532
+ };
93533
+ };
93534
+ delete?: never;
93535
+ options?: never;
93536
+ head?: never;
93537
+ patch?: never;
93538
+ trace?: never;
93539
+ };
93540
+ "/integrations/meta-ads/return": {
93541
+ parameters: {
93542
+ query?: never;
93543
+ header?: never;
93544
+ path?: never;
93545
+ cookie?: never;
93546
+ };
93547
+ get?: never;
93548
+ put?: never;
93549
+ /**
93550
+ * Settle a Meta Ads connect after the dealer comes back
93551
+ * @description Reads the hand-off ONCE and settles the row:
93552
+ *
93553
+ * * `completed` with a connection id → the connection is read back from the provider and the row becomes `connected`, carrying the ad account’s name, id, currency, timezone and whether the platform has ever delivered spend.
93554
+ * * `completed` with a pending selection → the row becomes `pending_selection` and carries the candidates the picker renders. **A consent that resolved to exactly one account never reaches this branch** — the provider finalizes it in its own callback, which is why there is no auto-finalize step on this side.
93555
+ * * `expired` / `cancelled` / `failed` → the row becomes `error` with a retryable `reason` of `handoff_<status>`. The recovery is identical for all three: mint a fresh hand-off.
93556
+ * * still `pending` → nothing is settled and the row comes back unchanged.
93557
+ *
93558
+ * **Neither field is required.** The hand-off id only exists in the RESPONSE to the mint while `return_url` is an argument to that same mint, so the return URL structurally cannot carry it — it carries `meta_ads=return` and the opaque `state` instead. Send `handoff_id` if you kept it, `state` if you read it off the URL, or an empty body, in which case the backend settles the row’s own pending hand-off (unambiguous: a tenant has at most one).
93559
+ *
93560
+ * Safe to repeat — a settled hand-off stays readable at the provider, so a re-post on a refresh re-derives the same state.
93561
+ *
93562
+ * 409 `handoff_mismatch` when the id or state presented does not name the hand-off this tenant’s row is waiting on, and for a tenant with no connect in flight at all (the two are deliberately the same answer).
93563
+ *
93564
+ * 502 `UPSTREAM_ERROR` when the provider could not be read; the row is left unchanged and the call is safe to repeat.
93565
+ */
93566
+ post: {
93567
+ parameters: {
93568
+ query?: never;
93569
+ header?: {
93570
+ /** @description Replay-safe retries: resending the SAME key with the SAME body returns the original response (`X-Idempotent-Replay: 1`) instead of creating a second copy — safe to send whenever a response might not have arrived. The same key with a DIFFERENT body answers `409 IDEMPOTENCY_KEY_CONFLICT`; use a fresh key per operation. */
93571
+ "Idempotency-Key"?: string;
93572
+ };
93573
+ path?: never;
93574
+ cookie?: never;
93575
+ };
93576
+ requestBody?: {
93577
+ content: {
93578
+ /**
93579
+ * @example {
93580
+ * "state": "9f3a2d7c8b1e4a2f"
93581
+ * }
93582
+ */
93583
+ "application/json": components["schemas"]["MetaAdsReturnBody"];
93584
+ };
93585
+ };
93586
+ responses: {
93587
+ /** @description The settled integration */
93588
+ 200: {
93589
+ headers: {
93590
+ [name: string]: unknown;
93591
+ };
93592
+ content: {
93593
+ /**
93594
+ * @example {
93595
+ * "data": {
93596
+ * "id": "22222222-2222-4222-8222-222222222222",
93597
+ * "provider": "meta_ads",
93598
+ * "status": "connected",
93599
+ * "account_label": "Cuenta Meta Ads",
93600
+ * "connected_at": "2026-09-23T15:04:00.000Z",
93601
+ * "disconnected_at": null,
93602
+ * "account": {
93603
+ * "external_account_id": "act_1234567890",
93604
+ * "name": "Automotora Ejemplo",
93605
+ * "currency": "CLP",
93606
+ * "timezone": "America/Santiago",
93607
+ * "delivers_spend": true,
93608
+ * "provider_connection_id": "c0c0c0c0-0000-4000-8000-000000000001",
93609
+ * "atribu_connection_id": "c0c0c0c0-0000-4000-8000-000000000001"
93610
+ * },
93611
+ * "error": null,
93612
+ * "handoff": null,
93613
+ * "candidates": null,
93614
+ * "updated_at": "2026-09-23T15:04:00.000Z"
93615
+ * }
93616
+ * }
93617
+ */
93618
+ "application/json": {
93619
+ data: components["schemas"]["MetaAdsIntegration"] & Record<string, never>;
93620
+ };
93621
+ };
93622
+ };
93623
+ /** @description Validation error */
93624
+ 400: {
93625
+ headers: {
93626
+ [name: string]: unknown;
93627
+ };
93628
+ content: {
93629
+ "application/json": components["schemas"]["Error"];
93630
+ };
93631
+ };
93632
+ /** @description Unauthorized */
93633
+ 401: {
93634
+ headers: {
93635
+ [name: string]: unknown;
93636
+ };
93637
+ content: {
93638
+ "application/json": components["schemas"]["Error"];
93639
+ };
93640
+ };
93641
+ /** @description Forbidden */
93642
+ 403: {
93643
+ headers: {
93644
+ [name: string]: unknown;
93645
+ };
93646
+ content: {
93647
+ "application/json": components["schemas"]["Error"];
93648
+ };
93649
+ };
93650
+ /** @description Not found */
93651
+ 404: {
93652
+ headers: {
93653
+ [name: string]: unknown;
93654
+ };
93655
+ content: {
93656
+ "application/json": components["schemas"]["Error"];
93657
+ };
93658
+ };
93659
+ /** @description Conflict (incl. Idempotency-Key reuse with different body) */
93660
+ 409: {
93661
+ headers: {
93662
+ [name: string]: unknown;
93663
+ };
93664
+ content: {
93665
+ "application/json": components["schemas"]["Error"];
93666
+ };
93667
+ };
93668
+ /** @description Rate limited */
93669
+ 429: {
93670
+ headers: {
93671
+ [name: string]: unknown;
93672
+ };
93673
+ content: {
93674
+ "application/json": components["schemas"]["Error"];
93675
+ };
93676
+ };
93677
+ };
93678
+ };
93679
+ delete?: never;
93680
+ options?: never;
93681
+ head?: never;
93682
+ patch?: never;
93683
+ trace?: never;
93684
+ };
93685
+ "/integrations/meta-ads/disconnect": {
93686
+ parameters: {
93687
+ query?: never;
93688
+ header?: never;
93689
+ path?: never;
93690
+ cookie?: never;
93691
+ };
93692
+ get?: never;
93693
+ put?: never;
93694
+ /**
93695
+ * Disconnect the workspace’s Meta Ads ad account
93696
+ * @description Calls the provider’s disconnect for the connection on the delegated key and settles the row `disconnected`, keeping its history: `account_label` and `connected_at` stay, and `config.last_account` remembers which account this was so the card can still name it. Reconnecting afterwards is a plain new `/connect` — it mints a fresh hand-off. A tenant must always be able to walk away from a connection it made.
93697
+ *
93698
+ * A row with nothing at the provider to revoke — `pending` / `pending_selection` with no connection — is cancelled LOCALLY: no provider call, the row moves straight to `disconnected`, and the pending hand-off simply expires on its own (the provider has no endpoint to cancel one early). A row already `disconnected` answers 200 unchanged (idempotent).
93699
+ *
93700
+ * 409 `sync_in_flight` when the provider is mid-sync on the connection — the row is left exactly as it was; the whole recovery is trying again in a few minutes. 502 `disconnect_refused` when the provider’s own 403 says the delegated key is no longer good for this connection — also left unchanged. A 404 from the provider (the connection is already gone there) is treated as success: the state this call exists to ensure already holds.
93701
+ */
93702
+ post: {
93703
+ parameters: {
93704
+ query?: never;
93705
+ header?: {
93706
+ /** @description Replay-safe retries: resending the SAME key with the SAME body returns the original response (`X-Idempotent-Replay: 1`) instead of creating a second copy — safe to send whenever a response might not have arrived. The same key with a DIFFERENT body answers `409 IDEMPOTENCY_KEY_CONFLICT`; use a fresh key per operation. */
93707
+ "Idempotency-Key"?: string;
93708
+ };
93709
+ path?: never;
93710
+ cookie?: never;
93711
+ };
93712
+ requestBody?: never;
93713
+ responses: {
93714
+ /** @description The disconnected integration */
93715
+ 200: {
93716
+ headers: {
93717
+ [name: string]: unknown;
93718
+ };
93719
+ content: {
93720
+ /**
93721
+ * @example {
93722
+ * "data": {
93723
+ * "id": "22222222-2222-4222-8222-222222222222",
93724
+ * "provider": "meta_ads",
93725
+ * "status": "disconnected",
93726
+ * "account_label": "Cuenta Meta Ads",
93727
+ * "connected_at": "2026-09-20T12:00:00.000Z",
93728
+ * "disconnected_at": "2026-09-23T15:10:00.000Z",
93729
+ * "account": null,
93730
+ * "error": null,
93731
+ * "handoff": null,
93732
+ * "candidates": null,
93733
+ * "updated_at": "2026-09-23T15:10:00.000Z"
93734
+ * }
93735
+ * }
93736
+ */
93737
+ "application/json": {
93738
+ data: components["schemas"]["MetaAdsIntegration"] & Record<string, never>;
93739
+ };
93740
+ };
93741
+ };
93742
+ /** @description Validation error */
93743
+ 400: {
93744
+ headers: {
93745
+ [name: string]: unknown;
93746
+ };
93747
+ content: {
93748
+ "application/json": components["schemas"]["Error"];
93749
+ };
93750
+ };
93751
+ /** @description Unauthorized */
93752
+ 401: {
93753
+ headers: {
93754
+ [name: string]: unknown;
93755
+ };
93756
+ content: {
93757
+ "application/json": components["schemas"]["Error"];
93758
+ };
93759
+ };
93760
+ /** @description Forbidden */
93761
+ 403: {
93762
+ headers: {
93763
+ [name: string]: unknown;
93764
+ };
93765
+ content: {
93766
+ "application/json": components["schemas"]["Error"];
93767
+ };
93768
+ };
93769
+ /** @description Not found */
93770
+ 404: {
93771
+ headers: {
93772
+ [name: string]: unknown;
93773
+ };
93774
+ content: {
93775
+ "application/json": components["schemas"]["Error"];
93776
+ };
93777
+ };
93778
+ /** @description Conflict (incl. Idempotency-Key reuse with different body) */
93779
+ 409: {
93780
+ headers: {
93781
+ [name: string]: unknown;
93782
+ };
93783
+ content: {
93784
+ "application/json": components["schemas"]["Error"];
93785
+ };
93786
+ };
93787
+ /** @description Rate limited */
93788
+ 429: {
93789
+ headers: {
93790
+ [name: string]: unknown;
93791
+ };
93792
+ content: {
93793
+ "application/json": components["schemas"]["Error"];
93794
+ };
93795
+ };
93796
+ };
93797
+ };
93798
+ delete?: never;
93799
+ options?: never;
93800
+ head?: never;
93801
+ patch?: never;
93802
+ trace?: never;
93803
+ };
93804
+ "/integrations/meta-ads/finalize": {
93805
+ parameters: {
93806
+ query?: never;
93807
+ header?: never;
93808
+ path?: never;
93809
+ cookie?: never;
93810
+ };
93811
+ get?: never;
93812
+ put?: never;
93813
+ /**
93814
+ * Finalize a Meta Ads pending selection with a chosen candidate
93815
+ * @description Only meaningful while `status` is `pending_selection`. Calls the provider’s finalize on the parked provider token with the chosen `candidate_id` — a `MetaAdsCandidate.candidate_id` from the SAME row’s `candidates`, sent back verbatim — reads the resulting connection back (the provider’s Meta Ads connections read) and runs it through the same settle path `/return` uses, so the row reads identically whichever produced it: `connected`, with `account` populated.
93816
+ *
93817
+ * **Retrying the same `candidate_id` is safe** and answers 200 with the current row — the provider’s `already_finalized: true` on its own idempotent replay, reapplied through the same write path a fresh finalize uses.
93818
+ *
93819
+ * **A DIFFERENT `candidate_id`** after the provider’s parked token already finalized a choice is 409 `start_again` (the row moves to `error`, `reason: selection_conflict`, retryable) — switching accounts needs a fresh connect, there is no token left to retarget.
93820
+ *
93821
+ * **The picker’s 60-minute parked token already gone** (elapsed, or never existed for this profile) is 410 `reconnect` (the row moves to `error`, `reason: selection_expired`, retryable).
93822
+ *
93823
+ * **The row is not `pending_selection`** — nothing started, already connected, or already in `error` — is 409 `handoff_mismatch`, checked before the provider is ever called.
93824
+ *
93825
+ * 502 `UPSTREAM_ERROR` when the provider could not be read; the row is left unchanged and the call is safe to repeat.
93826
+ */
93827
+ post: {
93828
+ parameters: {
93829
+ query?: never;
93830
+ header?: {
93831
+ /** @description Replay-safe retries: resending the SAME key with the SAME body returns the original response (`X-Idempotent-Replay: 1`) instead of creating a second copy — safe to send whenever a response might not have arrived. The same key with a DIFFERENT body answers `409 IDEMPOTENCY_KEY_CONFLICT`; use a fresh key per operation. */
93832
+ "Idempotency-Key"?: string;
93833
+ };
93834
+ path?: never;
93835
+ cookie?: never;
93836
+ };
93837
+ requestBody?: {
93838
+ content: {
93839
+ /**
93840
+ * @example {
93841
+ * "candidate_id": "cnd_7b1f0a4e9d2c"
93842
+ * }
93843
+ */
93844
+ "application/json": components["schemas"]["MetaAdsFinalizeBody"];
93845
+ };
93846
+ };
93847
+ responses: {
93848
+ /** @description The finalized integration, now `connected` */
93849
+ 200: {
93850
+ headers: {
93851
+ [name: string]: unknown;
93852
+ };
93853
+ content: {
93854
+ /**
93855
+ * @example {
93856
+ * "data": {
93857
+ * "id": "22222222-2222-4222-8222-222222222222",
93858
+ * "provider": "meta_ads",
93859
+ * "status": "connected",
93860
+ * "account_label": "Cuenta Meta Ads",
93861
+ * "connected_at": "2026-09-23T15:04:00.000Z",
93862
+ * "disconnected_at": null,
93863
+ * "account": {
93864
+ * "external_account_id": "act_1234567890",
93865
+ * "name": "Automotora Ejemplo",
93866
+ * "currency": "CLP",
93867
+ * "timezone": "America/Santiago",
93868
+ * "delivers_spend": true,
93869
+ * "provider_connection_id": "c0c0c0c0-0000-4000-8000-000000000001",
93870
+ * "atribu_connection_id": "c0c0c0c0-0000-4000-8000-000000000001"
93871
+ * },
93872
+ * "error": null,
93873
+ * "handoff": null,
93874
+ * "candidates": null,
93875
+ * "updated_at": "2026-09-23T15:04:00.000Z"
93876
+ * }
93877
+ * }
93878
+ */
93879
+ "application/json": {
93880
+ data: components["schemas"]["MetaAdsIntegration"] & Record<string, never>;
93881
+ };
93882
+ };
93883
+ };
93884
+ /** @description Validation error */
93885
+ 400: {
93886
+ headers: {
93887
+ [name: string]: unknown;
93888
+ };
93889
+ content: {
93890
+ "application/json": components["schemas"]["Error"];
93891
+ };
93892
+ };
93893
+ /** @description Unauthorized */
93894
+ 401: {
93895
+ headers: {
93896
+ [name: string]: unknown;
93897
+ };
93898
+ content: {
93899
+ "application/json": components["schemas"]["Error"];
93900
+ };
93901
+ };
93902
+ /** @description Forbidden */
93903
+ 403: {
93904
+ headers: {
93905
+ [name: string]: unknown;
93906
+ };
93907
+ content: {
93908
+ "application/json": components["schemas"]["Error"];
93909
+ };
93910
+ };
93911
+ /** @description Not found */
93912
+ 404: {
93913
+ headers: {
93914
+ [name: string]: unknown;
93915
+ };
93916
+ content: {
93917
+ "application/json": components["schemas"]["Error"];
93918
+ };
93919
+ };
93920
+ /** @description Conflict (incl. Idempotency-Key reuse with different body) */
93921
+ 409: {
93922
+ headers: {
93923
+ [name: string]: unknown;
93924
+ };
93925
+ content: {
93926
+ "application/json": components["schemas"]["Error"];
93927
+ };
93928
+ };
93929
+ /** @description The picker’s parked provider token is gone — `{ error: { code: "reconnect" } }` */
93930
+ 410: {
93931
+ headers: {
93932
+ [name: string]: unknown;
93933
+ };
93934
+ content: {
93935
+ "application/json": components["schemas"]["Error"];
93936
+ };
93937
+ };
93938
+ /** @description Rate limited */
93939
+ 429: {
93940
+ headers: {
93941
+ [name: string]: unknown;
93942
+ };
93943
+ content: {
93944
+ "application/json": components["schemas"]["Error"];
93945
+ };
93946
+ };
93947
+ };
93948
+ };
92629
93949
  delete?: never;
92630
93950
  options?: never;
92631
93951
  head?: never;
92632
93952
  patch?: never;
92633
93953
  trace?: never;
92634
93954
  };
92635
- "/insights/generate-pdf": {
93955
+ "/integrations/meta-ads/ads": {
92636
93956
  parameters: {
92637
93957
  query?: never;
92638
93958
  header?: never;
92639
93959
  path?: never;
92640
93960
  cookie?: never;
92641
93961
  };
92642
- get?: never;
92643
- put?: never;
92644
93962
  /**
92645
- * Render a report as a PDF
92646
- * @description **The one endpoint on the tenant API that does not answer the `{ data }` envelope.** It replies with raw `application/pdf` bytes and a `Content-Disposition: attachment` header, so a client that pipes every response through the usual JSON parse will fail here. On an error it reverts to the JSON error envelope, which means the response content-type is what tells the two apart.
93963
+ * The tenant's click-to-WhatsApp ads, with connect/greeting checks
93964
+ * @description Reads the partner click-to-WhatsApp ads list with the tenant’s WhatsApp **partner** token — a different credential than the connect rail above’s delegated key — and derives two facts per ad: `checks.destination_connected` (the ad’s WhatsApp number, compared as E.164, is one of the tenant’s connected WhatsApp messaging accounts) and `checks.greeting_configured` (a "Saludo automático" — text or tappable ice breakers — is actually set).
92647
93965
  *
92648
- * Generation is SYNCHRONOUS: it queries the report, runs an LLM pass to write the narrative, then renders. The connection is held for all of that, there is no job id, and nothing is persisted — a client or proxy timeout loses the document with no way to retrieve it other than asking again. Treat a wide range as a slow request.
93966
+ * Requires `integrations:read` or `ads:read` (owner, admin and the Marketing role — not "any member"), free for every tenant (no Add-on gate).
92649
93967
  *
92650
- * The LLM pass degrades rather than fails: if the narrative cannot be generated you still get a 200 and a complete PDF, with a Spanish note in place of the executive summary and every raw figure intact. So a successful response does not by itself mean the analysis is there.
93968
+ * `data.state` is `not_connected` in TWO cases, told apart by an optional `reason`:
93969
+ * * no `reason` — the tenant has never connected a Meta Ads account (the integration row is missing or not `connected`).
93970
+ * * `reason: "whatsapp_not_connected"` — Meta Ads may be connected, but the tenant holds no `whatsapp_cloud` account carrying an partner token, so there is no scope to list ads under.
92651
93971
  *
92652
- * `range.preset` is not a range selector — `from`/`to` alone decide the data. It is the window’s NAME: printed as the cover subtitle and passed into the summariser’s prompt, so a preset that disagrees with `from`/`to` mislabels the cover and misleads the narrative while the figures stay correct. Defaults to `custom`. Needs only `analytics:read`.
93972
+ * `cursor` is the provider’s own opaque keyset cursor (its `pagination.cursor`, a Meta ad id ascending), passed straight through as `after`. `next_cursor` is `null` once the provider reports `has_next: false`. `last_synced_at` is the max over the returned PAGE only, not a tenant-wide sync timestamp.
93973
+ *
93974
+ * 502 `UPSTREAM_ERROR` on a provider 5xx, timeout, or a response shape this route does not recognise. 502 `partner_token_invalid` when the provider answers 401 on the WhatsApp partner token — that token is never auto-rotated; the dealer must reconnect WhatsApp.
92653
93975
  */
92654
- post: {
93976
+ get: {
92655
93977
  parameters: {
92656
- query?: never;
92657
- header?: {
92658
- /** @description Replay-safe retries: resending the SAME key with the SAME body returns the original response (`X-Idempotent-Replay: 1`) instead of creating a second copy — safe to send whenever a response might not have arrived. The same key with a DIFFERENT body answers `409 IDEMPOTENCY_KEY_CONFLICT`; use a fresh key per operation. */
92659
- "Idempotency-Key"?: string;
93978
+ query?: {
93979
+ cursor?: string;
93980
+ limit?: number;
92660
93981
  };
93982
+ header?: never;
92661
93983
  path?: never;
92662
93984
  cookie?: never;
92663
93985
  };
92664
- requestBody?: {
92665
- content: {
92666
- /**
92667
- * @example {
92668
- * "report": "conversations",
92669
- * "window": {
92670
- * "preset": "7d"
92671
- * },
92672
- * "range": {
92673
- * "preset": "7d"
92674
- * }
92675
- * }
92676
- */
92677
- "application/json": {
92678
- /** @enum {string} */
92679
- report_type: "overview" | "conversations" | "agents" | "ai_agents" | "bots" | "tags" | "channels" | "teams" | "leads" | "csat" | "sla" | "clinic_ocupacion" | "clinic_inasistencias" | "clinic_produccion" | "clinic_packs";
92680
- range: {
92681
- from: string;
92682
- to: string;
92683
- preset?: string;
92684
- };
92685
- };
92686
- };
92687
- };
93986
+ requestBody?: never;
92688
93987
  responses: {
92689
- /** @description The rendered PDF as an attachment */
93988
+ /** @description A `not_connected` state (with or without a `reason`), or the connected page of ads */
92690
93989
  200: {
92691
93990
  headers: {
92692
93991
  [name: string]: unknown;
92693
93992
  };
92694
93993
  content: {
92695
- "application/pdf": string;
93994
+ /**
93995
+ * @example {
93996
+ * "data": {
93997
+ * "state": "connected",
93998
+ * "items": [
93999
+ * {
94000
+ * "ad_id": "1234567890123",
94001
+ * "ad_name": "Test drive fin de semana",
94002
+ * "ad_status": "ACTIVE",
94003
+ * "campaign": {
94004
+ * "id": "6543210987654",
94005
+ * "name": "Leads · WhatsApp"
94006
+ * },
94007
+ * "ad_set": {
94008
+ * "id": "6543210987655",
94009
+ * "name": "Región Metropolitana"
94010
+ * },
94011
+ * "destination": {
94012
+ * "type": "whatsapp",
94013
+ * "whatsapp_phone_number": "+56912345678",
94014
+ * "page_id": "998877665544"
94015
+ * },
94016
+ * "cta_type": "WHATSAPP_MESSAGE",
94017
+ * "greeting": {
94018
+ * "text": "¡Hola! Cuéntanos qué auto buscas.",
94019
+ * "autofill_message": "Quiero agendar un test drive",
94020
+ * "ice_breakers": [
94021
+ * "Ver stock disponible",
94022
+ * "Agendar test drive"
94023
+ * ],
94024
+ * "landing_screen_type": "welcome_message"
94025
+ * },
94026
+ * "greeting_status": "present",
94027
+ * "checks": {
94028
+ * "destination_connected": true,
94029
+ * "greeting_configured": true
94030
+ * },
94031
+ * "last_synced_at": "2026-09-23T14:00:00.000Z"
94032
+ * }
94033
+ * ],
94034
+ * "next_cursor": null,
94035
+ * "last_synced_at": "2026-09-23T14:00:00.000Z"
94036
+ * }
94037
+ * }
94038
+ */
94039
+ "application/json": {
94040
+ data: components["schemas"]["MetaAdsAdsResponse"];
94041
+ };
92696
94042
  };
92697
94043
  };
92698
94044
  /** @description Validation error */
@@ -92751,13 +94097,15 @@ interface paths {
92751
94097
  };
92752
94098
  };
92753
94099
  };
94100
+ put?: never;
94101
+ post?: never;
92754
94102
  delete?: never;
92755
94103
  options?: never;
92756
94104
  head?: never;
92757
94105
  patch?: never;
92758
94106
  trace?: never;
92759
94107
  };
92760
- "/insights/schedules": {
94108
+ "/integrations/connections": {
92761
94109
  parameters: {
92762
94110
  query?: never;
92763
94111
  header?: never;
@@ -92765,8 +94113,10 @@ interface paths {
92765
94113
  cookie?: never;
92766
94114
  };
92767
94115
  /**
92768
- * List report schedules
92769
- * @description The standing subscriptions that mail a rendered report out on a cadence. Unpaginated, and with no `meta.total` — this list is small by construction.
94116
+ * List the workspace’s provider connections (CRM, payments)
94117
+ * @description One card per connectable provider — GoHighLevel, Mercado Pago — ALWAYS every provider, never-connected ones included (`state: "not_connected"`), so an integrations screen renders from this one read. Each card is exactly what `GET /integrations/connections/{provider}` returns.
94118
+ *
94119
+ * Requires `integrations:read` (owner and admin).
92770
94120
  */
92771
94121
  get: {
92772
94122
  parameters: {
@@ -92777,7 +94127,7 @@ interface paths {
92777
94127
  };
92778
94128
  requestBody?: never;
92779
94129
  responses: {
92780
- /** @description Report schedules */
94130
+ /** @description Every provider connection card */
92781
94131
  200: {
92782
94132
  headers: {
92783
94133
  [name: string]: unknown;
@@ -92787,24 +94137,72 @@ interface paths {
92787
94137
  * @example {
92788
94138
  * "data": [
92789
94139
  * {
92790
- * "id": "cccccccc-2000-4000-8000-000000000001",
92791
- * "report_type": "conversations",
92792
- * "range_preset": "7d",
92793
- * "cadence": "weekly",
92794
- * "recipients": [
92795
- * "gerencia@midealer.cl"
92796
- * ],
92797
- * "next_run_at": "2026-09-29T09:00:00.000Z",
92798
- * "enabled": true
94140
+ * "provider": "gohighlevel",
94141
+ * "display_name": "GoHighLevel",
94142
+ * "category": "crm",
94143
+ * "description": "Trae los formularios y las oportunidades de tu CRM para seguir cada lead hasta la venta.",
94144
+ * "last_account": null,
94145
+ * "last_attempt": null,
94146
+ * "disconnected_at": null,
94147
+ * "addon_active": true,
94148
+ * "state": "connecting",
94149
+ * "state_label": "Elige la subcuenta",
94150
+ * "account": null,
94151
+ * "reason": null,
94152
+ * "next_step": {
94153
+ * "action": "choose_account",
94154
+ * "label": "Elegir subcuenta"
94155
+ * },
94156
+ * "attempt": {
94157
+ * "step": "choose_account",
94158
+ * "handoff": {
94159
+ * "id": "0f2f6b0a-9a2c-4f4e-9a1e-6f7f2a5c3b21",
94160
+ * "expires_at": "2026-09-27T15:45:00.000Z"
94161
+ * },
94162
+ * "candidates": [
94163
+ * {
94164
+ * "external_id": "loc_8XkP2qR5tV",
94165
+ * "name": "Agencia Ejemplo — Ventas"
94166
+ * },
94167
+ * {
94168
+ * "external_id": "loc_3MnB7wQ1yZ",
94169
+ * "name": "Agencia Ejemplo — Soporte"
94170
+ * }
94171
+ * ],
94172
+ * "selection_expires_at": "2026-09-27T16:00:00.000Z",
94173
+ * "prompt": "Elige la subcuenta de GoHighLevel que quieres conectar."
94174
+ * },
94175
+ * "connected_at": null,
94176
+ * "last_synced_at": null,
94177
+ * "updated_at": "2026-09-27T15:02:00.000Z"
94178
+ * },
94179
+ * {
94180
+ * "provider": "mercadopago",
94181
+ * "display_name": "Mercado Pago",
94182
+ * "category": "payments",
94183
+ * "description": "Registra los pagos que recibes para saber cuántas ventas trae cada campaña.",
94184
+ * "state": "not_connected",
94185
+ * "state_label": "Sin conectar",
94186
+ * "account": null,
94187
+ * "last_account": null,
94188
+ * "reason": null,
94189
+ * "last_attempt": null,
94190
+ * "next_step": {
94191
+ * "action": "connect",
94192
+ * "label": "Conectar"
94193
+ * },
94194
+ * "attempt": null,
94195
+ * "connected_at": null,
94196
+ * "disconnected_at": null,
94197
+ * "last_synced_at": null,
94198
+ * "updated_at": null,
94199
+ * "addon_active": true
92799
94200
  * }
92800
94201
  * ]
92801
94202
  * }
92802
94203
  */
92803
94204
  "application/json": {
92804
- data?: unknown;
92805
- meta?: {
92806
- [key: string]: unknown;
92807
- };
94205
+ data: components["schemas"]["ProviderConnection"][];
92808
94206
  };
92809
94207
  };
92810
94208
  };
@@ -92872,7 +94270,7 @@ interface paths {
92872
94270
  patch?: never;
92873
94271
  trace?: never;
92874
94272
  };
92875
- "/integrations/meta-ads": {
94273
+ "/integrations/connections/{provider}": {
92876
94274
  parameters: {
92877
94275
  query?: never;
92878
94276
  header?: never;
@@ -92880,23 +94278,27 @@ interface paths {
92880
94278
  cookie?: never;
92881
94279
  };
92882
94280
  /**
92883
- * Read the workspace’s Meta Ads connection
92884
- * @description The tenant’s single Meta Ads row, or `null` when it has never started a connect. A placeholder object with `status: "disconnected"` is deliberately NOT returned in its place: "never connected" and "was connected and is not any more" are different states and the card says different things about them.
94281
+ * Read one provider connection card
94282
+ * @description Everything the card renders, in one read: the state and its label, the connected account, the reason with a ready-to-render sentence, how the latest re-authorization ended (`last_attempt`), the next step with its button label, when it connected / stopped / last synced, and — only while a connect is in flight — the attempt. Never null: a workspace that never connected gets `state: "not_connected"`.
92885
94283
  *
92886
- * `handoff` is populated only while `status` is `pending` and `candidates` only while it is `pending_selection` — a spent consent URL would render a button leading to a dead page.
94284
+ * A connection whose last sync failed stays `connected` with `reason.code: "sync_error"` (label «Problema de sincronización») and a `reconnect` next step; only a grant the provider no longer honours is `needs_reconnect`.
92887
94285
  *
92888
- * Requires `integrations:read` or `ads:read` (owner, admin and the Marketing role); a `supervisor` or `agent` gets 403 here and never sees the card. The writes need `integrations:write` or `ads:write`.
94286
+ * While an account picker is open the candidates are re-read from the provider when older than five minutes; every other state is a database read only.
94287
+ *
94288
+ * Requires `integrations:read` (owner and admin).
92889
94289
  */
92890
94290
  get: {
92891
94291
  parameters: {
92892
94292
  query?: never;
92893
94293
  header?: never;
92894
- path?: never;
94294
+ path: {
94295
+ provider: "gohighlevel" | "mercadopago";
94296
+ };
92895
94297
  cookie?: never;
92896
94298
  };
92897
94299
  requestBody?: never;
92898
94300
  responses: {
92899
- /** @description The Meta Ads integration, or `null` */
94301
+ /** @description The provider connection card */
92900
94302
  200: {
92901
94303
  headers: {
92902
94304
  [name: string]: unknown;
@@ -92904,11 +94306,35 @@ interface paths {
92904
94306
  content: {
92905
94307
  /**
92906
94308
  * @example {
92907
- * "data": null
94309
+ * "data": {
94310
+ * "provider": "gohighlevel",
94311
+ * "display_name": "GoHighLevel",
94312
+ * "category": "crm",
94313
+ * "description": "Trae los formularios y las oportunidades de tu CRM para seguir cada lead hasta la venta.",
94314
+ * "last_account": null,
94315
+ * "last_attempt": null,
94316
+ * "disconnected_at": null,
94317
+ * "addon_active": true,
94318
+ * "state": "connected",
94319
+ * "state_label": "Conectado",
94320
+ * "account": {
94321
+ * "external_id": "loc_8XkP2qR5tV",
94322
+ * "name": "Agencia Ejemplo"
94323
+ * },
94324
+ * "reason": null,
94325
+ * "next_step": {
94326
+ * "action": "none",
94327
+ * "label": null
94328
+ * },
94329
+ * "attempt": null,
94330
+ * "connected_at": "2026-09-27T15:04:00.000Z",
94331
+ * "last_synced_at": "2026-09-27T15:20:00.000Z",
94332
+ * "updated_at": "2026-09-27T15:20:00.000Z"
94333
+ * }
92908
94334
  * }
92909
94335
  */
92910
94336
  "application/json": {
92911
- data: components["schemas"]["MetaAdsIntegration"];
94337
+ data: components["schemas"]["ProviderConnection"];
92912
94338
  };
92913
94339
  };
92914
94340
  };
@@ -92930,6 +94356,31 @@ interface paths {
92930
94356
  "application/json": components["schemas"]["Error"];
92931
94357
  };
92932
94358
  };
94359
+ /** @description The Vitrina Ads add-on is not active and nothing is connected (or in flight) for this provider. `error.code` is `ENTITLEMENT_NOT_ACTIVE`, `error.details.feature` is `vitrina_ads`. A connection that outlived the add-on is never refused here: it stays readable, finishable and disconnectable. */
94360
+ 402: {
94361
+ headers: {
94362
+ [name: string]: unknown;
94363
+ };
94364
+ content: {
94365
+ /**
94366
+ * @example {
94367
+ * "error": {
94368
+ * "code": "ENTITLEMENT_NOT_ACTIVE",
94369
+ * "message": "El complemento Vitrina Ads no está activo en este espacio de trabajo.",
94370
+ * "details": {
94371
+ * "required": [
94372
+ * "vitrina_ads"
94373
+ * ],
94374
+ * "active": [],
94375
+ * "feature": "vitrina_ads",
94376
+ * "entitlement_state": "off"
94377
+ * }
94378
+ * }
94379
+ * }
94380
+ */
94381
+ "application/json": components["schemas"]["Error"];
94382
+ };
94383
+ };
92933
94384
  /** @description Forbidden */
92934
94385
  403: {
92935
94386
  headers: {
@@ -92976,7 +94427,7 @@ interface paths {
92976
94427
  patch?: never;
92977
94428
  trace?: never;
92978
94429
  };
92979
- "/integrations/meta-ads/connect": {
94430
+ "/integrations/connections/{provider}/connect": {
92980
94431
  parameters: {
92981
94432
  query?: never;
92982
94433
  header?: never;
@@ -92986,14 +94437,14 @@ interface paths {
92986
94437
  get?: never;
92987
94438
  put?: never;
92988
94439
  /**
92989
- * Start a Meta Ads connect and get the consent URL
92990
- * @description Mints the connection provider’s **connect hand-off** for `meta_ads` and answers the session-less consent URL to redirect the WHOLE PAGE to. It is not an iframe target: Meta’s own consent dialog renders inside it.
94440
+ * Start (or resume) a provider connect
94441
+ * @description Starts the provider’s consent and answers the URL to send the WHOLE page to — straight to the provider’s own consent screen, which names this app. Every ending (connected, several accounts to choose from, cancelled, refused, expired) comes back to `return_to` with `connection_return=<provider>`, `connection_state`, and the provider’s `status`, `reason` and `handoff_id`; pass all of them to `POST …/return`, then strip them from the address bar.
92991
94442
  *
92992
- * `return_to` must be an absolute URL on the app’s own origin. The backend appends `meta_ads=return` and a short opaque `state`, preserving any query the URL already carried, and passes the result to the provider as `return_url`. An origin that is not the app’s is a 400 here rather than a silent substitution — the provider itself only checks the origin when the human CLICKS, where it surfaces as a failed hand-off (`result.reason: origin_not_allowed`) after the dealer has already walked through a consent screen for nothing.
94443
+ * `return_to` must be an absolute URL on the app’s own origin (400 otherwise).
92993
94444
  *
92994
- * **A second click reuses the live hand-off.** The provider’s URL is single-use and 45-minute-lived, and minting twice yields two hand-offs rather than an error, so a dealer who clicks, gets distracted and clicks again gets the SAME `url` and `handoff_id` back until it expires.
94445
+ * **Idempotent.** A second click while a consent is live for the same `return_to` answers the SAME `url` and `handoff_id`. It works from every state: a first connect, a retry, a reconnect (`needs_reconnect`), a re-authorization of a connected account. Only a first connect moves the card to `connecting`; a reconnect keeps its state until the new consent lands.
92995
94446
  *
92996
- * Before anything is minted, the tenant’s attribution profile and its delegated key are ensured (ADR 0099). This rail never uses a platform-wide provider key.
94447
+ * Requires `integrations:write` (owner and admin).
92997
94448
  */
92998
94449
  post: {
92999
94450
  parameters: {
@@ -93002,21 +94453,23 @@ interface paths {
93002
94453
  /** @description Replay-safe retries: resending the SAME key with the SAME body returns the original response (`X-Idempotent-Replay: 1`) instead of creating a second copy — safe to send whenever a response might not have arrived. The same key with a DIFFERENT body answers `409 IDEMPOTENCY_KEY_CONFLICT`; use a fresh key per operation. */
93003
94454
  "Idempotency-Key"?: string;
93004
94455
  };
93005
- path?: never;
94456
+ path: {
94457
+ provider: "gohighlevel" | "mercadopago";
94458
+ };
93006
94459
  cookie?: never;
93007
94460
  };
93008
94461
  requestBody?: {
93009
94462
  content: {
93010
94463
  /**
93011
94464
  * @example {
93012
- * "return_to": "https://app.vitrinadev.com/settings/integraciones?meta_ads=return"
94465
+ * "return_to": "https://app.vitrinadev.com/w/agencia/settings/integraciones"
93013
94466
  * }
93014
94467
  */
93015
- "application/json": components["schemas"]["MetaAdsConnectBody"];
94468
+ "application/json": components["schemas"]["ProviderConnectBody"];
93016
94469
  };
93017
94470
  };
93018
94471
  responses: {
93019
- /** @description The consent URL, its hand-off id, and its expiry */
94472
+ /** @description The consent URL, its hand-off id and expiry, and the card */
93020
94473
  200: {
93021
94474
  headers: {
93022
94475
  [name: string]: unknown;
@@ -93025,14 +94478,45 @@ interface paths {
93025
94478
  /**
93026
94479
  * @example {
93027
94480
  * "data": {
93028
- * "url": "https://consent.example.com/connect/meta-ads/ho_4d8f2a91c6b3",
93029
- * "handoff_id": "ho_4d8f2a91c6b3",
93030
- * "expires_at": "2026-09-23T15:45:00.000Z"
94481
+ * "url": "https://connect.example.com/gohighlevel/start?h=4d8f2a91c6b3",
94482
+ * "handoff_id": "0f2f6b0a-9a2c-4f4e-9a1e-6f7f2a5c3b21",
94483
+ * "expires_at": "2026-09-27T15:45:00.000Z",
94484
+ * "connection": {
94485
+ * "provider": "gohighlevel",
94486
+ * "display_name": "GoHighLevel",
94487
+ * "category": "crm",
94488
+ * "description": "Trae los formularios y las oportunidades de tu CRM para seguir cada lead hasta la venta.",
94489
+ * "last_account": null,
94490
+ * "last_attempt": null,
94491
+ * "disconnected_at": null,
94492
+ * "addon_active": true,
94493
+ * "state": "connecting",
94494
+ * "state_label": "Conectando",
94495
+ * "account": null,
94496
+ * "reason": null,
94497
+ * "next_step": {
94498
+ * "action": "resume",
94499
+ * "label": "Continuar en GoHighLevel"
94500
+ * },
94501
+ * "attempt": {
94502
+ * "step": "consent",
94503
+ * "handoff": {
94504
+ * "id": "0f2f6b0a-9a2c-4f4e-9a1e-6f7f2a5c3b21",
94505
+ * "expires_at": "2026-09-27T15:45:00.000Z"
94506
+ * },
94507
+ * "candidates": null,
94508
+ * "selection_expires_at": null,
94509
+ * "prompt": "Termina la autorización en GoHighLevel para conectar."
94510
+ * },
94511
+ * "connected_at": null,
94512
+ * "last_synced_at": null,
94513
+ * "updated_at": "2026-09-27T15:00:00.000Z"
94514
+ * }
93031
94515
  * }
93032
94516
  * }
93033
94517
  */
93034
94518
  "application/json": {
93035
- data: components["schemas"]["MetaAdsConnectResult"];
94519
+ data: components["schemas"]["ProviderConnectResult"];
93036
94520
  };
93037
94521
  };
93038
94522
  };
@@ -93054,6 +94538,31 @@ interface paths {
93054
94538
  "application/json": components["schemas"]["Error"];
93055
94539
  };
93056
94540
  };
94541
+ /** @description The Vitrina Ads add-on is not active and nothing is connected (or in flight) for this provider. `error.code` is `ENTITLEMENT_NOT_ACTIVE`, `error.details.feature` is `vitrina_ads`. A connection that outlived the add-on is never refused here: it stays readable, finishable and disconnectable. */
94542
+ 402: {
94543
+ headers: {
94544
+ [name: string]: unknown;
94545
+ };
94546
+ content: {
94547
+ /**
94548
+ * @example {
94549
+ * "error": {
94550
+ * "code": "ENTITLEMENT_NOT_ACTIVE",
94551
+ * "message": "El complemento Vitrina Ads no está activo en este espacio de trabajo.",
94552
+ * "details": {
94553
+ * "required": [
94554
+ * "vitrina_ads"
94555
+ * ],
94556
+ * "active": [],
94557
+ * "feature": "vitrina_ads",
94558
+ * "entitlement_state": "off"
94559
+ * }
94560
+ * }
94561
+ * }
94562
+ */
94563
+ "application/json": components["schemas"]["Error"];
94564
+ };
94565
+ };
93057
94566
  /** @description Forbidden */
93058
94567
  403: {
93059
94568
  headers: {
@@ -93098,7 +94607,7 @@ interface paths {
93098
94607
  patch?: never;
93099
94608
  trace?: never;
93100
94609
  };
93101
- "/integrations/meta-ads/return": {
94610
+ "/integrations/connections/{provider}/return": {
93102
94611
  parameters: {
93103
94612
  query?: never;
93104
94613
  header?: never;
@@ -93108,19 +94617,19 @@ interface paths {
93108
94617
  get?: never;
93109
94618
  put?: never;
93110
94619
  /**
93111
- * Settle a Meta Ads connect after the dealer comes back
93112
- * @description Reads the hand-off ONCE and settles the row:
94620
+ * Settle a provider connect after the admin comes back
94621
+ * @description Settles the connect on the provider’s own record of the consent — the `status` and `reason` from the return URL are hints only (a URL can be typed):
93113
94622
  *
93114
- * * `completed` with a connection id → the connection is read back from the provider and the row becomes `connected`, carrying the ad account’s name, id, currency, timezone and whether the platform has ever delivered spend.
93115
- * * `completed` with a pending selection → the row becomes `pending_selection` and carries the candidates the picker renders. **A consent that resolved to exactly one account never reaches this branch** — the provider finalizes it in its own callback, which is why there is no auto-finalize step on this side.
93116
- * * `expired` / `cancelled` / `failed` → the row becomes `error` with a retryable `reason` of `handoff_<status>`. The recovery is identical for all three: mint a fresh hand-off.
93117
- * * still `pending` → nothing is settled and the row comes back unchanged.
94623
+ * * connected → `state: "connected"` with the account read back from the provider;
94624
+ * * several accounts (GoHighLevel) → `attempt.step: "choose_account"` with `attempt.candidates`;
94625
+ * * cancelled / refused / expired → the attempt ends with a SPECIFIC `reason` (`user_cancelled`, `account_connected_elsewhere`, `no_candidates`, `provider_denied`, …), never folded into one generic failure. On a card that was already connected the state is unchanged and the outcome is `last_attempt`;
94626
+ * * not finished yet → the card comes back unchanged. If the return URL says the attempt failed with the retryable `internal_error`, the reason is recorded and the SAME consent link stays live for a retry; any other failure ends the attempt.
93118
94627
  *
93119
- * **Neither field is required.** The hand-off id only exists in the RESPONSE to the mint while `return_url` is an argument to that same mint, so the return URL structurally cannot carry it — it carries `meta_ads=return` and the opaque `state` instead. Send `handoff_id` if you kept it, `state` if you read it off the URL, or an empty body, in which case the backend settles the row’s own pending hand-off (unambiguous: a tenant has at most one).
94628
+ * Every field is optional: a stripped query string still settles the workspace’s own pending consent (there is at most one). **Idempotent:** re-posting for a consent already settled answers the card as it stands. A consent finished in an OLDER tab after a newer connect answers the connected card.
93120
94629
  *
93121
- * Safe to repeat — a settled hand-off stays readable at the provider, so a re-post on a refresh re-derives the same state.
94630
+ * 409 `handoff_mismatch` when the `handoff_id`/`state` presented is neither this card’s consent nor its last one and nothing is connected. 502 when the provider could not be reached — nothing is settled; retry.
93122
94631
  *
93123
- * 409 `handoff_mismatch` when the id or state presented does not name the hand-off this tenant’s row is waiting on, and for a tenant with no connect in flight at all (the two are deliberately the same answer).
94632
+ * Requires `integrations:write`.
93124
94633
  */
93125
94634
  post: {
93126
94635
  parameters: {
@@ -93129,21 +94638,25 @@ interface paths {
93129
94638
  /** @description Replay-safe retries: resending the SAME key with the SAME body returns the original response (`X-Idempotent-Replay: 1`) instead of creating a second copy — safe to send whenever a response might not have arrived. The same key with a DIFFERENT body answers `409 IDEMPOTENCY_KEY_CONFLICT`; use a fresh key per operation. */
93130
94639
  "Idempotency-Key"?: string;
93131
94640
  };
93132
- path?: never;
94641
+ path: {
94642
+ provider: "gohighlevel" | "mercadopago";
94643
+ };
93133
94644
  cookie?: never;
93134
94645
  };
93135
94646
  requestBody?: {
93136
94647
  content: {
93137
94648
  /**
93138
94649
  * @example {
93139
- * "state": "9f3a2d7c8b1e4a2f"
94650
+ * "handoff_id": "0f2f6b0a-9a2c-4f4e-9a1e-6f7f2a5c3b21",
94651
+ * "state": "9f3a2d7c8b1e4a2f",
94652
+ * "status": "success"
93140
94653
  * }
93141
94654
  */
93142
- "application/json": components["schemas"]["MetaAdsReturnBody"];
94655
+ "application/json": components["schemas"]["ProviderReturnBody"];
93143
94656
  };
93144
94657
  };
93145
94658
  responses: {
93146
- /** @description The settled integration */
94659
+ /** @description The settled card */
93147
94660
  200: {
93148
94661
  headers: {
93149
94662
  [name: string]: unknown;
@@ -93152,30 +94665,34 @@ interface paths {
93152
94665
  /**
93153
94666
  * @example {
93154
94667
  * "data": {
93155
- * "id": "22222222-2222-4222-8222-222222222222",
93156
- * "provider": "meta_ads",
93157
- * "status": "connected",
93158
- * "account_label": "Cuenta Meta Ads",
93159
- * "connected_at": "2026-09-23T15:04:00.000Z",
94668
+ * "provider": "gohighlevel",
94669
+ * "display_name": "GoHighLevel",
94670
+ * "category": "crm",
94671
+ * "description": "Trae los formularios y las oportunidades de tu CRM para seguir cada lead hasta la venta.",
94672
+ * "last_account": null,
94673
+ * "last_attempt": null,
93160
94674
  * "disconnected_at": null,
94675
+ * "addon_active": true,
94676
+ * "state": "connected",
94677
+ * "state_label": "Conectado",
93161
94678
  * "account": {
93162
- * "external_account_id": "act_1234567890",
93163
- * "name": "Automotora Ejemplo",
93164
- * "currency": "CLP",
93165
- * "timezone": "America/Santiago",
93166
- * "delivers_spend": true,
93167
- * "provider_connection_id": "c0c0c0c0-0000-4000-8000-000000000001",
93168
- * "atribu_connection_id": "c0c0c0c0-0000-4000-8000-000000000001"
94679
+ * "external_id": "loc_8XkP2qR5tV",
94680
+ * "name": "Agencia Ejemplo"
93169
94681
  * },
93170
- * "error": null,
93171
- * "handoff": null,
93172
- * "candidates": null,
93173
- * "updated_at": "2026-09-23T15:04:00.000Z"
94682
+ * "reason": null,
94683
+ * "next_step": {
94684
+ * "action": "none",
94685
+ * "label": null
94686
+ * },
94687
+ * "attempt": null,
94688
+ * "connected_at": "2026-09-27T15:04:00.000Z",
94689
+ * "last_synced_at": "2026-09-27T15:20:00.000Z",
94690
+ * "updated_at": "2026-09-27T15:20:00.000Z"
93174
94691
  * }
93175
94692
  * }
93176
94693
  */
93177
94694
  "application/json": {
93178
- data: components["schemas"]["MetaAdsIntegration"] & Record<string, never>;
94695
+ data: components["schemas"]["ProviderConnection"];
93179
94696
  };
93180
94697
  };
93181
94698
  };
@@ -93197,6 +94714,31 @@ interface paths {
93197
94714
  "application/json": components["schemas"]["Error"];
93198
94715
  };
93199
94716
  };
94717
+ /** @description The Vitrina Ads add-on is not active and nothing is connected (or in flight) for this provider. `error.code` is `ENTITLEMENT_NOT_ACTIVE`, `error.details.feature` is `vitrina_ads`. A connection that outlived the add-on is never refused here: it stays readable, finishable and disconnectable. */
94718
+ 402: {
94719
+ headers: {
94720
+ [name: string]: unknown;
94721
+ };
94722
+ content: {
94723
+ /**
94724
+ * @example {
94725
+ * "error": {
94726
+ * "code": "ENTITLEMENT_NOT_ACTIVE",
94727
+ * "message": "El complemento Vitrina Ads no está activo en este espacio de trabajo.",
94728
+ * "details": {
94729
+ * "required": [
94730
+ * "vitrina_ads"
94731
+ * ],
94732
+ * "active": [],
94733
+ * "feature": "vitrina_ads",
94734
+ * "entitlement_state": "off"
94735
+ * }
94736
+ * }
94737
+ * }
94738
+ */
94739
+ "application/json": components["schemas"]["Error"];
94740
+ };
94741
+ };
93200
94742
  /** @description Forbidden */
93201
94743
  403: {
93202
94744
  headers: {
@@ -93215,131 +94757,20 @@ interface paths {
93215
94757
  "application/json": components["schemas"]["Error"];
93216
94758
  };
93217
94759
  };
93218
- /** @description Conflict (incl. Idempotency-Key reuse with different body) */
94760
+ /** @description `handoff_mismatch` — a stale tab finishing another consent. */
93219
94761
  409: {
93220
- headers: {
93221
- [name: string]: unknown;
93222
- };
93223
- content: {
93224
- "application/json": components["schemas"]["Error"];
93225
- };
93226
- };
93227
- /** @description Rate limited */
93228
- 429: {
93229
- headers: {
93230
- [name: string]: unknown;
93231
- };
93232
- content: {
93233
- "application/json": components["schemas"]["Error"];
93234
- };
93235
- };
93236
- };
93237
- };
93238
- delete?: never;
93239
- options?: never;
93240
- head?: never;
93241
- patch?: never;
93242
- trace?: never;
93243
- };
93244
- "/integrations/meta-ads/disconnect": {
93245
- parameters: {
93246
- query?: never;
93247
- header?: never;
93248
- path?: never;
93249
- cookie?: never;
93250
- };
93251
- get?: never;
93252
- put?: never;
93253
- /**
93254
- * Disconnect the workspace’s Meta Ads ad account
93255
- * @description Calls the provider’s disconnect for the connection on the delegated key and settles the row `disconnected`, keeping its history: `account_label` and `connected_at` stay, and `config.last_account` remembers which account this was so the card can still name it. Reconnecting afterwards is a plain new `/connect` — it mints a fresh hand-off. A tenant must always be able to walk away from a connection it made.
93256
- *
93257
- * A row with nothing at the provider to revoke — `pending` / `pending_selection` with no connection — is cancelled LOCALLY: no provider call, the row moves straight to `disconnected`, and the pending hand-off simply expires on its own (the provider has no endpoint to cancel one early). A row already `disconnected` answers 200 unchanged (idempotent).
93258
- *
93259
- * 409 `sync_in_flight` when the provider is mid-sync on the connection — the row is left exactly as it was; the whole recovery is trying again in a few minutes. 502 `atribu_refused` when the provider’s own 403 says the delegated key is no longer good for this connection — also left unchanged. A 404 from the provider (the connection is already gone there) is treated as success: the state this call exists to ensure already holds.
93260
- */
93261
- post: {
93262
- parameters: {
93263
- query?: never;
93264
- header?: {
93265
- /** @description Replay-safe retries: resending the SAME key with the SAME body returns the original response (`X-Idempotent-Replay: 1`) instead of creating a second copy — safe to send whenever a response might not have arrived. The same key with a DIFFERENT body answers `409 IDEMPOTENCY_KEY_CONFLICT`; use a fresh key per operation. */
93266
- "Idempotency-Key"?: string;
93267
- };
93268
- path?: never;
93269
- cookie?: never;
93270
- };
93271
- requestBody?: never;
93272
- responses: {
93273
- /** @description The disconnected integration */
93274
- 200: {
93275
94762
  headers: {
93276
94763
  [name: string]: unknown;
93277
94764
  };
93278
94765
  content: {
93279
94766
  /**
93280
94767
  * @example {
93281
- * "data": {
93282
- * "id": "22222222-2222-4222-8222-222222222222",
93283
- * "provider": "meta_ads",
93284
- * "status": "disconnected",
93285
- * "account_label": "Cuenta Meta Ads",
93286
- * "connected_at": "2026-09-20T12:00:00.000Z",
93287
- * "disconnected_at": "2026-09-23T15:10:00.000Z",
93288
- * "account": null,
93289
- * "error": null,
93290
- * "handoff": null,
93291
- * "candidates": null,
93292
- * "updated_at": "2026-09-23T15:10:00.000Z"
94768
+ * "error": {
94769
+ * "code": "handoff_mismatch",
94770
+ * "message": "Esta conexión ya no está esperando ese enlace. Actualiza la página y vuelve a conectar."
93293
94771
  * }
93294
94772
  * }
93295
94773
  */
93296
- "application/json": {
93297
- data: components["schemas"]["MetaAdsIntegration"] & Record<string, never>;
93298
- };
93299
- };
93300
- };
93301
- /** @description Validation error */
93302
- 400: {
93303
- headers: {
93304
- [name: string]: unknown;
93305
- };
93306
- content: {
93307
- "application/json": components["schemas"]["Error"];
93308
- };
93309
- };
93310
- /** @description Unauthorized */
93311
- 401: {
93312
- headers: {
93313
- [name: string]: unknown;
93314
- };
93315
- content: {
93316
- "application/json": components["schemas"]["Error"];
93317
- };
93318
- };
93319
- /** @description Forbidden */
93320
- 403: {
93321
- headers: {
93322
- [name: string]: unknown;
93323
- };
93324
- content: {
93325
- "application/json": components["schemas"]["Error"];
93326
- };
93327
- };
93328
- /** @description Not found */
93329
- 404: {
93330
- headers: {
93331
- [name: string]: unknown;
93332
- };
93333
- content: {
93334
- "application/json": components["schemas"]["Error"];
93335
- };
93336
- };
93337
- /** @description Conflict (incl. Idempotency-Key reuse with different body) */
93338
- 409: {
93339
- headers: {
93340
- [name: string]: unknown;
93341
- };
93342
- content: {
93343
94774
  "application/json": components["schemas"]["Error"];
93344
94775
  };
93345
94776
  };
@@ -93360,7 +94791,7 @@ interface paths {
93360
94791
  patch?: never;
93361
94792
  trace?: never;
93362
94793
  };
93363
- "/integrations/meta-ads/finalize": {
94794
+ "/integrations/connections/{provider}/finalize": {
93364
94795
  parameters: {
93365
94796
  query?: never;
93366
94797
  header?: never;
@@ -93370,16 +94801,17 @@ interface paths {
93370
94801
  get?: never;
93371
94802
  put?: never;
93372
94803
  /**
93373
- * Finalize a Meta Ads pending selection with a chosen candidate
93374
- * @description Only meaningful while `status` is `pending_selection`. Calls the provider’s finalize on the parked provider token with the chosen `candidate_id` — a `MetaAdsCandidate.candidate_id` from the SAME row’s `candidates`, sent back verbatim — reads the resulting connection back (the provider’s Meta Ads connections read) and runs it through the same settle path `/return` uses, so the row reads identically whichever produced it: `connected`, with `account` populated.
94804
+ * Choose the account to connect from an open picker
94805
+ * @description Completes an open account picker (`attempt.step: "choose_account"`) with one `attempt.candidates[].external_id`. Served only for providers whose consent can end in a choice (GoHighLevel sub-accounts); any other provider is 404. **Idempotent:** the same account again on a card it already connected answers the card.
93375
94806
  *
93376
- * **Retrying the same `candidate_id` is safe** and answers 200 with the current row — the provider’s `already_finalized: true` on its own idempotent replay, reapplied through the same write path a fresh finalize uses.
93377
- *
93378
- * **A DIFFERENT `candidate_id`** after the provider’s parked token already finalized a choice is 409 `start_again` (the row moves to `error`, `reason: selection_conflict`, retryable) — switching accounts needs a fresh connect, there is no token left to retarget.
93379
- *
93380
- * **The picker’s 60-minute parked token already gone** (elapsed, or never existed for this profile) is 410 `reconnect` (the row moves to `error`, `reason: selection_expired`, retryable).
94807
+ * * 409 `handoff_mismatch` — `handoff_id` was sent and is not the consent this card is waiting on (a stale tab); refresh the card;
94808
+ * * 409 `not_selecting` — no choice is owed (refresh the card);
94809
+ * * 409 `account_connected_elsewhere` — that account lives on another Vitrina workspace; the picker STAYS open, choose another;
94810
+ * * 409 `selection_conflict` — another account was already chosen on this consent; connect again to change it;
94811
+ * * 410 `selection_expired` — the picker’s 60-minute window ended; connect again;
94812
+ * * 422 `candidate_unavailable` — the account is no longer reachable; the next card read refreshes the list.
93381
94813
  *
93382
- * **The row is not `pending_selection`** — nothing started, already connected, or already in `error` — is 409 `handoff_mismatch`, checked before the provider is ever called.
94814
+ * Requires `integrations:write`.
93383
94815
  */
93384
94816
  post: {
93385
94817
  parameters: {
@@ -93388,21 +94820,24 @@ interface paths {
93388
94820
  /** @description Replay-safe retries: resending the SAME key with the SAME body returns the original response (`X-Idempotent-Replay: 1`) instead of creating a second copy — safe to send whenever a response might not have arrived. The same key with a DIFFERENT body answers `409 IDEMPOTENCY_KEY_CONFLICT`; use a fresh key per operation. */
93389
94821
  "Idempotency-Key"?: string;
93390
94822
  };
93391
- path?: never;
94823
+ path: {
94824
+ provider: "gohighlevel";
94825
+ };
93392
94826
  cookie?: never;
93393
94827
  };
93394
94828
  requestBody?: {
93395
94829
  content: {
93396
94830
  /**
93397
94831
  * @example {
93398
- * "candidate_id": "cnd_7b1f0a4e9d2c"
94832
+ * "external_id": "loc_8XkP2qR5tV",
94833
+ * "handoff_id": "0f2f6b0a-9a2c-4f4e-9a1e-6f7f2a5c3b21"
93399
94834
  * }
93400
94835
  */
93401
- "application/json": components["schemas"]["MetaAdsFinalizeBody"];
94836
+ "application/json": components["schemas"]["ProviderFinalizeBody"];
93402
94837
  };
93403
94838
  };
93404
94839
  responses: {
93405
- /** @description The finalized integration, now `connected` */
94840
+ /** @description The connected card */
93406
94841
  200: {
93407
94842
  headers: {
93408
94843
  [name: string]: unknown;
@@ -93411,30 +94846,34 @@ interface paths {
93411
94846
  /**
93412
94847
  * @example {
93413
94848
  * "data": {
93414
- * "id": "22222222-2222-4222-8222-222222222222",
93415
- * "provider": "meta_ads",
93416
- * "status": "connected",
93417
- * "account_label": "Cuenta Meta Ads",
93418
- * "connected_at": "2026-09-23T15:04:00.000Z",
94849
+ * "provider": "gohighlevel",
94850
+ * "display_name": "GoHighLevel",
94851
+ * "category": "crm",
94852
+ * "description": "Trae los formularios y las oportunidades de tu CRM para seguir cada lead hasta la venta.",
94853
+ * "last_account": null,
94854
+ * "last_attempt": null,
93419
94855
  * "disconnected_at": null,
94856
+ * "addon_active": true,
94857
+ * "state": "connected",
94858
+ * "state_label": "Conectado",
93420
94859
  * "account": {
93421
- * "external_account_id": "act_1234567890",
93422
- * "name": "Automotora Ejemplo",
93423
- * "currency": "CLP",
93424
- * "timezone": "America/Santiago",
93425
- * "delivers_spend": true,
93426
- * "provider_connection_id": "c0c0c0c0-0000-4000-8000-000000000001",
93427
- * "atribu_connection_id": "c0c0c0c0-0000-4000-8000-000000000001"
94860
+ * "external_id": "loc_8XkP2qR5tV",
94861
+ * "name": "Agencia Ejemplo"
93428
94862
  * },
93429
- * "error": null,
93430
- * "handoff": null,
93431
- * "candidates": null,
93432
- * "updated_at": "2026-09-23T15:04:00.000Z"
94863
+ * "reason": null,
94864
+ * "next_step": {
94865
+ * "action": "none",
94866
+ * "label": null
94867
+ * },
94868
+ * "attempt": null,
94869
+ * "connected_at": "2026-09-27T15:04:00.000Z",
94870
+ * "last_synced_at": "2026-09-27T15:20:00.000Z",
94871
+ * "updated_at": "2026-09-27T15:20:00.000Z"
93433
94872
  * }
93434
94873
  * }
93435
94874
  */
93436
94875
  "application/json": {
93437
- data: components["schemas"]["MetaAdsIntegration"] & Record<string, never>;
94876
+ data: components["schemas"]["ProviderConnection"];
93438
94877
  };
93439
94878
  };
93440
94879
  };
@@ -93456,6 +94895,31 @@ interface paths {
93456
94895
  "application/json": components["schemas"]["Error"];
93457
94896
  };
93458
94897
  };
94898
+ /** @description The Vitrina Ads add-on is not active and nothing is connected (or in flight) for this provider. `error.code` is `ENTITLEMENT_NOT_ACTIVE`, `error.details.feature` is `vitrina_ads`. A connection that outlived the add-on is never refused here: it stays readable, finishable and disconnectable. */
94899
+ 402: {
94900
+ headers: {
94901
+ [name: string]: unknown;
94902
+ };
94903
+ content: {
94904
+ /**
94905
+ * @example {
94906
+ * "error": {
94907
+ * "code": "ENTITLEMENT_NOT_ACTIVE",
94908
+ * "message": "El complemento Vitrina Ads no está activo en este espacio de trabajo.",
94909
+ * "details": {
94910
+ * "required": [
94911
+ * "vitrina_ads"
94912
+ * ],
94913
+ * "active": [],
94914
+ * "feature": "vitrina_ads",
94915
+ * "entitlement_state": "off"
94916
+ * }
94917
+ * }
94918
+ * }
94919
+ */
94920
+ "application/json": components["schemas"]["Error"];
94921
+ };
94922
+ };
93459
94923
  /** @description Forbidden */
93460
94924
  403: {
93461
94925
  headers: {
@@ -93474,21 +94938,54 @@ interface paths {
93474
94938
  "application/json": components["schemas"]["Error"];
93475
94939
  };
93476
94940
  };
93477
- /** @description Conflict (incl. Idempotency-Key reuse with different body) */
94941
+ /** @description `handoff_mismatch` (a stale tab), `not_selecting` (no choice owed), `account_connected_elsewhere` (the picker stays open) or `selection_conflict` (connect again). */
93478
94942
  409: {
93479
94943
  headers: {
93480
94944
  [name: string]: unknown;
93481
94945
  };
93482
94946
  content: {
94947
+ /**
94948
+ * @example {
94949
+ * "error": {
94950
+ * "code": "not_selecting",
94951
+ * "message": "Esta conexión no está esperando que elijas una subcuenta. Actualiza la página."
94952
+ * }
94953
+ * }
94954
+ */
93483
94955
  "application/json": components["schemas"]["Error"];
93484
94956
  };
93485
94957
  };
93486
- /** @description The picker’s parked provider token is gone — `{ error: { code: "reconnect" } }` */
94958
+ /** @description `selection_expired` — the picker’s 60-minute window ended; connect again. */
93487
94959
  410: {
93488
94960
  headers: {
93489
94961
  [name: string]: unknown;
93490
94962
  };
93491
94963
  content: {
94964
+ /**
94965
+ * @example {
94966
+ * "error": {
94967
+ * "code": "selection_expired",
94968
+ * "message": "Pasó demasiado tiempo para elegir la subcuenta. Vuelve a conectar GoHighLevel."
94969
+ * }
94970
+ * }
94971
+ */
94972
+ "application/json": components["schemas"]["Error"];
94973
+ };
94974
+ };
94975
+ /** @description `candidate_unavailable` — the account is no longer reachable; the next card read refreshes the list. */
94976
+ 422: {
94977
+ headers: {
94978
+ [name: string]: unknown;
94979
+ };
94980
+ content: {
94981
+ /**
94982
+ * @example {
94983
+ * "error": {
94984
+ * "code": "candidate_unavailable",
94985
+ * "message": "Esa subcuenta ya no está disponible. Elige otra de la lista."
94986
+ * }
94987
+ * }
94988
+ */
93492
94989
  "application/json": components["schemas"]["Error"];
93493
94990
  };
93494
94991
  };
@@ -93509,40 +95006,38 @@ interface paths {
93509
95006
  patch?: never;
93510
95007
  trace?: never;
93511
95008
  };
93512
- "/integrations/meta-ads/ads": {
95009
+ "/integrations/connections/{provider}/disconnect": {
93513
95010
  parameters: {
93514
95011
  query?: never;
93515
95012
  header?: never;
93516
95013
  path?: never;
93517
95014
  cookie?: never;
93518
95015
  };
95016
+ get?: never;
95017
+ put?: never;
93519
95018
  /**
93520
- * The tenant's click-to-WhatsApp ads, with connect/greeting checks
93521
- * @description Reads the partner click-to-WhatsApp ads list with the tenant’s WhatsApp **partner** token — a different credential than the connect rail above’s delegated key — and derives two facts per ad: `checks.destination_connected` (the ad’s WhatsApp number, compared as E.164, is one of the tenant’s connected WhatsApp messaging accounts) and `checks.greeting_configured` (a "Saludo automático" — text or tappable ice breakers — is actually set).
95019
+ * Disconnect a provider
95020
+ * @description Disconnects the account at the provider and settles the card `disconnected` with `reason.code: "disconnected_by_user"`, keeping `last_account` so the card can say what was disconnected. A connect in flight with nothing connected yet is cancelled locally. **Idempotent:** a card with nothing live answers as it stands.
93522
95021
  *
93523
- * Requires `integrations:read` or `ads:read` (owner, admin and the Marketing role — not "any member"), free for every tenant (no Add-on gate).
93524
- *
93525
- * `data.state` is `not_connected` in TWO cases, told apart by an optional `reason`:
93526
- * * no `reason` — the tenant has never connected a Meta Ads account (the integration row is missing or not `connected`).
93527
- * * `reason: "whatsapp_not_connected"` — Meta Ads may be connected, but the tenant holds no `whatsapp_cloud` account carrying an partner token, so there is no scope to list ads under.
95022
+ * 409 `sync_in_flight` — the provider is mid-sync; nothing changed, try again in a few minutes. 502 `disconnect_refused` — the provider refused; nothing changed. 502 `UPSTREAM_ERROR` — the provider could not be reached; nothing changed.
93528
95023
  *
93529
- * `cursor` is the provider’s own opaque keyset cursor (its `pagination.cursor`, a Meta ad id ascending), passed straight through as `after`. `next_cursor` is `null` once the provider reports `has_next: false`. `last_synced_at` is the max over the returned PAGE only, not a tenant-wide sync timestamp.
93530
- *
93531
- * 502 `UPSTREAM_ERROR` on a provider 5xx, timeout, or a response shape this route does not recognise. 502 `partner_token_invalid` when the provider answers 401 on the WhatsApp partner token — that token is never auto-rotated; the dealer must reconnect WhatsApp.
95024
+ * Requires `integrations:write`.
93532
95025
  */
93533
- get: {
95026
+ post: {
93534
95027
  parameters: {
93535
- query?: {
93536
- cursor?: string;
93537
- limit?: number;
95028
+ query?: never;
95029
+ header?: {
95030
+ /** @description Replay-safe retries: resending the SAME key with the SAME body returns the original response (`X-Idempotent-Replay: 1`) instead of creating a second copy — safe to send whenever a response might not have arrived. The same key with a DIFFERENT body answers `409 IDEMPOTENCY_KEY_CONFLICT`; use a fresh key per operation. */
95031
+ "Idempotency-Key"?: string;
95032
+ };
95033
+ path: {
95034
+ provider: "gohighlevel" | "mercadopago";
93538
95035
  };
93539
- header?: never;
93540
- path?: never;
93541
95036
  cookie?: never;
93542
95037
  };
93543
95038
  requestBody?: never;
93544
95039
  responses: {
93545
- /** @description A `not_connected` state (with or without a `reason`), or the connected page of ads */
95040
+ /** @description The disconnected card */
93546
95041
  200: {
93547
95042
  headers: {
93548
95043
  [name: string]: unknown;
@@ -93551,50 +95046,38 @@ interface paths {
93551
95046
  /**
93552
95047
  * @example {
93553
95048
  * "data": {
93554
- * "state": "connected",
93555
- * "items": [
93556
- * {
93557
- * "ad_id": "1234567890123",
93558
- * "ad_name": "Test drive fin de semana",
93559
- * "ad_status": "ACTIVE",
93560
- * "campaign": {
93561
- * "id": "6543210987654",
93562
- * "name": "Leads · WhatsApp"
93563
- * },
93564
- * "ad_set": {
93565
- * "id": "6543210987655",
93566
- * "name": "Región Metropolitana"
93567
- * },
93568
- * "destination": {
93569
- * "type": "whatsapp",
93570
- * "whatsapp_phone_number": "+56912345678",
93571
- * "page_id": "998877665544"
93572
- * },
93573
- * "cta_type": "WHATSAPP_MESSAGE",
93574
- * "greeting": {
93575
- * "text": "¡Hola! Cuéntanos qué auto buscas.",
93576
- * "autofill_message": "Quiero agendar un test drive",
93577
- * "ice_breakers": [
93578
- * "Ver stock disponible",
93579
- * "Agendar test drive"
93580
- * ],
93581
- * "landing_screen_type": "welcome_message"
93582
- * },
93583
- * "greeting_status": "present",
93584
- * "checks": {
93585
- * "destination_connected": true,
93586
- * "greeting_configured": true
93587
- * },
93588
- * "last_synced_at": "2026-09-23T14:00:00.000Z"
93589
- * }
93590
- * ],
93591
- * "next_cursor": null,
93592
- * "last_synced_at": "2026-09-23T14:00:00.000Z"
95049
+ * "provider": "gohighlevel",
95050
+ * "display_name": "GoHighLevel",
95051
+ * "category": "crm",
95052
+ * "description": "Trae los formularios y las oportunidades de tu CRM para seguir cada lead hasta la venta.",
95053
+ * "last_account": {
95054
+ * "external_id": "loc_8XkP2qR5tV",
95055
+ * "name": "Agencia Ejemplo"
95056
+ * },
95057
+ * "last_attempt": null,
95058
+ * "disconnected_at": "2026-09-28T10:00:00.000Z",
95059
+ * "addon_active": true,
95060
+ * "state": "disconnected",
95061
+ * "state_label": "Desconectado",
95062
+ * "account": null,
95063
+ * "reason": {
95064
+ * "code": "disconnected_by_user",
95065
+ * "label": "Desconectaste GoHighLevel.",
95066
+ * "retryable": true
95067
+ * },
95068
+ * "next_step": {
95069
+ * "action": "connect",
95070
+ * "label": "Conectar"
95071
+ * },
95072
+ * "attempt": null,
95073
+ * "connected_at": null,
95074
+ * "last_synced_at": "2026-09-27T15:20:00.000Z",
95075
+ * "updated_at": "2026-09-28T10:00:00.000Z"
93593
95076
  * }
93594
95077
  * }
93595
95078
  */
93596
95079
  "application/json": {
93597
- data: components["schemas"]["MetaAdsAdsResponse"];
95080
+ data: components["schemas"]["ProviderConnection"];
93598
95081
  };
93599
95082
  };
93600
95083
  };
@@ -93616,6 +95099,31 @@ interface paths {
93616
95099
  "application/json": components["schemas"]["Error"];
93617
95100
  };
93618
95101
  };
95102
+ /** @description The Vitrina Ads add-on is not active and nothing is connected (or in flight) for this provider. `error.code` is `ENTITLEMENT_NOT_ACTIVE`, `error.details.feature` is `vitrina_ads`. A connection that outlived the add-on is never refused here: it stays readable, finishable and disconnectable. */
95103
+ 402: {
95104
+ headers: {
95105
+ [name: string]: unknown;
95106
+ };
95107
+ content: {
95108
+ /**
95109
+ * @example {
95110
+ * "error": {
95111
+ * "code": "ENTITLEMENT_NOT_ACTIVE",
95112
+ * "message": "El complemento Vitrina Ads no está activo en este espacio de trabajo.",
95113
+ * "details": {
95114
+ * "required": [
95115
+ * "vitrina_ads"
95116
+ * ],
95117
+ * "active": [],
95118
+ * "feature": "vitrina_ads",
95119
+ * "entitlement_state": "off"
95120
+ * }
95121
+ * }
95122
+ * }
95123
+ */
95124
+ "application/json": components["schemas"]["Error"];
95125
+ };
95126
+ };
93619
95127
  /** @description Forbidden */
93620
95128
  403: {
93621
95129
  headers: {
@@ -93654,8 +95162,6 @@ interface paths {
93654
95162
  };
93655
95163
  };
93656
95164
  };
93657
- put?: never;
93658
- post?: never;
93659
95165
  delete?: never;
93660
95166
  options?: never;
93661
95167
  head?: never;
@@ -95153,7 +96659,13 @@ interface paths {
95153
96659
  * "verdict": "healthy_tracking",
95154
96660
  * "tracking_healthy": true,
95155
96661
  * "coverage_interpretation": "La mayoría de las ventas llegan con su origen publicitario.",
95156
- * "missing": []
96662
+ * "missing": [],
96663
+ * "tracker": {
96664
+ * "status": "live",
96665
+ * "web_touches": 1842,
96666
+ * "last_web_touch": "2026-08-31T21:14:00Z",
96667
+ * "fresh": true
96668
+ * }
95157
96669
  * },
95158
96670
  * "traceable_roas_trend": {
95159
96671
  * "recent_roas": 3.4,
@@ -96289,7 +97801,7 @@ interface paths {
96289
97801
  *
96290
97802
  * `window` (Creativos only) derives the rail on the same rolling creatives window as `GET /ads/creatives`; pass the same `window` to preview.
96291
97803
  *
96292
- * A `key` is `<kind>:<target external id>:<YYYY-MM-DD>` and is stable for the day. Dismissed actions stay on the list with `dismissed_until` set, sorted last. `include=adset_duplicate_swap` also lists the duplicate-and-swap actions (hidden by default).
97804
+ * A `key` is `<kind>:<target external id>:<YYYY-MM-DD>` and is stable for the day. Dismissed actions stay on the list with `dismissed_until` set, sorted last. `include=adset_duplicate_swap` also lists the duplicate-and-swap actions (hidden by default). `include=campaign_pause` (Campañas, Resumen) marks a client that renders the whole-campaign pause card (`campaign_pause`: one atomic change in Meta, undone with «Reanudar»). **Not offered yet** — its eligibility rule is pending — so today the list never contains it, with or without the include, and a campaign that spent with no results keeps the per-ad cards.
96293
97805
  *
96294
97806
  * Card state machine (client): `proposed → previewing → confirming → running → done | failed | refused → (done) reverted`; `dismissed` for 7 days; `simulated` replaces `done` in sample mode.
96295
97807
  */
@@ -96302,8 +97814,8 @@ interface paths {
96302
97814
  to: string;
96303
97815
  /** @description `creativos` only: the creatives window the rail is derived for — the same rolling lookback as `GET /ads/creatives?window=`, so a card’s figures (the worn-out ad’s spend) match the gallery. Defaults to `28d`. Sent with any other screen → 400. */
96304
97816
  window?: "7d" | "14d" | "28d" | "lifetime";
96305
- /** @description `adset_duplicate_swap` also lists the duplicate-and-swap actions (Salud). Hidden by default: it rebuilds an ad set (new id, learning reset) and is offered as the alternative after a `promoted_object_frozen` refusal. */
96306
- include?: "adset_duplicate_swap";
97817
+ /** @description `adset_duplicate_swap` also lists the duplicate-and-swap actions (Salud). Hidden by default: it rebuilds an ad set (new id, learning reset) and is offered as the alternative after a `promoted_object_frozen` refusal. `campaign_pause` (Campañas, Resumen): the client renders the whole-campaign pause card. Not offered yet (its rule is pending): accepted, and the list does not contain the card today. */
97818
+ include?: "adset_duplicate_swap" | "campaign_pause";
96307
97819
  /** @description `1` serves the deterministic sample dataset («Ver con datos de ejemplo»): same shapes, invented but internally consistent figures, no entitlement required (the scope still is). A sandbox workspace is always served the sample, with or without this parameter. */
96308
97820
  sample?: "1" | "true";
96309
97821
  };
@@ -96591,9 +98103,9 @@ interface paths {
96591
98103
  put?: never;
96592
98104
  /**
96593
98105
  * Preview an action before confirming it
96594
- * @description Reads the target’s live state and returns what will change (`rows`: before → after), the warnings, and — for a duplicate-and-swap or a URL-parameter fix — the consent lines the owner must accept. A `url_tags_fix` preview lists `untagged_ads` («{n} → 0») first, then one row per ad (`ad:<id>`): its current parameters → exactly what it gets; an ad changed in Meta after the preview is skipped at execute, never rewritten blind. It also records ONE single-use approval bound to the caller, the action and its exact parameters, valid for 10 minutes (`expires_at`): `preview_id` is what `execute` spends. No engine write happens here.
98106
+ * @description Reads the target’s live state and returns what will change (`rows`: before → after), the warnings, and — for a duplicate-and-swap or a URL-parameter fix — the consent lines the owner must accept. A `url_tags_fix` preview lists `untagged_ads` («{n} → 0») first, then one row per ad (`ad:<id>`): its current parameters → exactly what it gets; an ad changed in Meta after the preview is skipped at execute, never rewritten blind. A `campaign_pause` preview reads the campaign live ONCE and returns everything its sheet and card draw: `summary` («Pausar «{campaign}» · detiene {n} anuncios · ~$X al día»), `campaign` (status, the ads it stops, the mean daily spend and the budget as exact decimal strings with their currency, and where the budget lives) and the rows `status`, `delivering_ads`, `avg_daily_spend`, `budget`, plus its consent. It also records ONE single-use approval bound to the caller, the action and its exact parameters, valid for 10 minutes (`expires_at`): `preview_id` is what `execute` spends. No engine write happens here.
96595
98107
  *
96596
- * 409 `ADS_ACTION_NOT_AVAILABLE` when the action is no longer on today’s list (or is a `nav` card, which has nothing to execute).
98108
+ * 409 `ADS_ACTION_NOT_AVAILABLE` when the action is no longer on today’s list (or is a `nav` card, which has nothing to execute). A `campaign_pause` key answers it with `details.reason: not_offered` while that action is not offered; once it is, it also answers it when the live read says the pause cannot run now — `details.reason`: `not_active` (already paused), `not_pausable` (archived or deleted), `campaign_not_found`, `meta_connection_missing`, `permission_gap`, `reconnect_required`, `not_available` (any other refusal of the read), `too_recent` (less than 7 complete days of spend) — with `details.explanation`, Spanish copy for the owner; an already-paused, archived or missing campaign also hides the card for 7 days, and a too-recent one until the day it can be judged. 503 `ADS_ACTION_READ_UNAVAILABLE` (`details.reason: meta_unavailable`) when the campaign could not be read right now — retry in a moment.
96597
98109
  */
96598
98110
  post: {
96599
98111
  parameters: {
@@ -96754,6 +98266,15 @@ interface paths {
96754
98266
  "application/json": components["schemas"]["Error"];
96755
98267
  };
96756
98268
  };
98269
+ /** @description `campaign_pause` only: the campaign could not be read right now (no answer, an upstream error or a rate limit). `error.code` is `ADS_ACTION_READ_UNAVAILABLE`, `details.reason: meta_unavailable`, `details.explanation` Spanish copy for the owner. Nothing was recorded; retry in a moment. */
98270
+ 503: {
98271
+ headers: {
98272
+ [name: string]: unknown;
98273
+ };
98274
+ content: {
98275
+ "application/json": components["schemas"]["Error"];
98276
+ };
98277
+ };
96757
98278
  };
96758
98279
  };
96759
98280
  delete?: never;
@@ -96777,11 +98298,13 @@ interface paths {
96777
98298
  *
96778
98299
  * 409 `ADS_ACTION_REPLAY_BLOCKED` when the approval cannot be spent — `details.reason` is `expired` (preview older than 10 minutes), `consumed` (already confirmed, e.g. from another tab), `not_found` or `binding_mismatch` (another person’s preview, or another action — incl. a real preview sent with `sample: true`). In sample mode nothing reaches the engine: the execution is born `succeeded` with `simulated: true`, and only a sample preview can be spent.
96779
98300
  *
96780
- * **Duplicate-and-swap** and **`url_tags_fix`** need the owner’s acceptance of the preview’s `consent_terms` (the verbatim consent text): send `consent: { version, text_hash, accepted_at? }`. Missing, different or older than 24 h → 409 `ADS_ACTION_CONSENT_REQUIRED` (`details.reason` missing|mismatch|stale, `details.consent` = the current version and hash) — nothing is spent and nothing reaches the engine.
98301
+ * **`campaign_pause`**, **duplicate-and-swap** and **`url_tags_fix`** need the owner’s acceptance of the preview’s `consent_terms` (the verbatim consent text): send `consent: { version, text_hash, accepted_at? }`. Missing, different or older than 24 h → 409 `ADS_ACTION_CONSENT_REQUIRED` (`details.reason` missing|mismatch|stale, `details.consent` = the current version and hash) — nothing is spent and nothing reaches the engine.
96781
98302
  *
96782
98303
  * If the job cannot be queued the answer is still 202, with `status: failed`: the execution exists and says why (`error.code: queue_unavailable`).
96783
98304
  *
96784
98305
  * A `url_tags_fix` corrects each ad on its own: `outcome.summary` reads «{k} de {n} anuncios corregidos», `outcome.partial` is true when k < n, and `outcome.ads[]` says what happened to each ad. None corrected → `failed`/`refused` with the first ad’s reason, still with `outcome.ads[]`.
98306
+ *
98307
+ * A `campaign_pause` is ONE change in Meta. Each execution is its own request: a second confirm (another tab, an agent) finds the campaign already paused and answers `outcome.engine_outcome: no_change` — nothing written, no journal line, nothing to undo. Otherwise `engine_outcome` is `applied`, or `replayed` (a retry of this same execution); `outcome.affected_ads_count` is how many ads stopped.
96785
98308
  */
96786
98309
  post: {
96787
98310
  parameters: {
@@ -96810,7 +98333,7 @@ interface paths {
96810
98333
  */
96811
98334
  preview_id: string;
96812
98335
  sample?: boolean;
96813
- /** @description Required whenever the preview returned `consent_terms` (a duplicate-and-swap, a URL-parameter fix `url_tags_fix`, «Enviar a Meta»'s `send_setup` and `send_dataset`, and a `send_stage` off / `send_stop` that stops an event active ad sets optimise on), ignored otherwise: the owner's acceptance of `consent_terms` — echo its `version` and `text_hash`. The accepting person is always the caller. */
98336
+ /** @description Required whenever the preview returned `consent_terms` (a campaign pause `campaign_pause`, a duplicate-and-swap, a URL-parameter fix `url_tags_fix`, «Enviar a Meta»'s `send_setup` and `send_dataset`, and a `send_stage` off / `send_stop` that stops an event active ad sets optimise on), ignored otherwise: the owner's acceptance of `consent_terms` — echo its `version` and `text_hash`. The accepting person is always the caller. */
96814
98337
  consent?: {
96815
98338
  version: string;
96816
98339
  text_hash: string;
@@ -97219,7 +98742,7 @@ interface paths {
97219
98742
  };
97220
98743
  /**
97221
98744
  * The status of an action execution
97222
- * @description `queued → running → succeeded | failed | refused`; a succeeded execution becomes `rolled_back` when its rollback succeeds. `error.reason` is one of `promoted_object_frozen` (Meta froze the ad set’s conversion — the duplicate-and-swap is the alternative), `reconnect_required`, `permission_gap` (Meta connected without permission to manage ads — reconnect keeping it), `rate_limited`, `not_reversible`, `forbidden` (no person could be put on the record for the change — not a Meta grant problem), `consent_required` (the duplicate-and-swap consent is missing or stale — preview again), `engine_error` (incl. `code: budget_change_out_of_bounds` — outside the ±50 % allowed per change — `code: not_active` and `code: no_recommendation`). `error.message` is Spanish copy ready for the card. `target` is what the action touched (with `campaign_name`, the campaign the card named, on an ad-set budget change).
98745
+ * @description `queued → running → succeeded | failed | refused`; a succeeded execution becomes `rolled_back` when its rollback succeeds. `error.reason` is one of `promoted_object_frozen` (Meta froze the ad set’s conversion — the duplicate-and-swap is the alternative), `reconnect_required`, `permission_gap` (Meta connected without permission to manage ads — reconnect keeping it), `rate_limited`, `not_reversible`, `forbidden` (no person could be put on the record for the change — not a Meta grant problem), `consent_required` (the duplicate-and-swap consent is missing or stale — preview again), `engine_error` (incl. `code: budget_change_out_of_bounds` — outside the ±50 % allowed per change — `code: not_active` and `code: no_recommendation`). Campaign pause codes: `campaign_not_found` / `campaign_not_pausable` (archived or deleted — never written) / `meta_refused` (`engine_error`), `campaign_not_resumable` (`not_reversible`), a dead Meta token → `reconnect_required`, `meta_permission_denied` (`permission_gap` — Meta refused for a missing permission), `applied_by_not_allowed` (`forbidden`), `meta_rate_limited` (`rate_limited`, retried first). `error.message` is Spanish copy ready for the card. `target` is what the action touched (with `campaign_name`, the campaign the card named, on an ad-set budget change).
97223
98746
  */
97224
98747
  get: {
97225
98748
  parameters: {
@@ -97385,7 +98908,7 @@ interface paths {
97385
98908
  put?: never;
97386
98909
  /**
97387
98910
  * Undo an executed action
97388
- * @description Queues the engine’s rollback of a `succeeded` execution with `rollback_available: true` and answers the NEW execution (`rollback_of` = the original). The engine restores the pre-change state only while the object still carries what the action wrote; otherwise the rollback is `refused` with `reason: not_reversible` (`code: state_drifted`). A `url_tags_fix` is undone ad by ad (each ad back on its previous parameters): an ad changed in Meta since is left alone (final), an ad whose undo got no answer stays pending — the original keeps `rollback_available: true` and the next rollback sends only those ads; the original reads `rolled_back` once nothing is pending. **`Idempotency-Key` is required.** 409 `ADS_ACTION_NOT_REVERSIBLE` when the execution has nothing to undo.
98911
+ * @description Queues the engine’s rollback of a `succeeded` execution with `rollback_available: true` and answers the NEW execution (`rollback_of` = the original). The engine restores the pre-change state only while the object still carries what the action wrote; otherwise the rollback is `refused` with `reason: not_reversible` (`code: state_drifted`). A `url_tags_fix` is undone ad by ad (each ad back on its previous parameters): an ad changed in Meta since is left alone (final), an ad whose undo got no answer stays pending — the original keeps `rollback_available: true` and the next rollback sends only those ads; the original reads `rolled_back` once nothing is pending. A `campaign_pause` is undone with «Reanudar»: one resume in Meta, with a fresh key per request; ads that were paused on their own stay paused. A campaign already active answers `outcome.engine_outcome: no_change` (the pause is then no longer undoable, never marked as undone by Vitrina); an archived or deleted one is `refused` `not_reversible` (`code: campaign_not_resumable`), and so is one paused again by someone else since (`code: paused_since`): «Reanudar» only ever undoes this pause. **`Idempotency-Key` is required.** 409 `ADS_ACTION_NOT_REVERSIBLE` when the execution has nothing to undo.
97389
98912
  */
97390
98913
  post: {
97391
98914
  parameters: {
@@ -98217,7 +99740,24 @@ interface paths {
98217
99740
  * }
98218
99741
  * },
98219
99742
  * "credited_weight": 1,
98220
- * "touch_count": 3
99743
+ * "touch_count": 3,
99744
+ * "origin": {
99745
+ * "platform": "whatsapp",
99746
+ * "external_id": "120211000000000101",
99747
+ * "name": "Reel · Antes y después",
99748
+ * "ad_name": "Reel · Antes y después",
99749
+ * "headline": "Evaluación sin costo",
99750
+ * "campaign": {
99751
+ * "external_id": "120211000000000001",
99752
+ * "name": "Ortodoncia invisible · Septiembre"
99753
+ * },
99754
+ * "thumbnail_url": null,
99755
+ * "media_archived": false,
99756
+ * "first_message_at": "2026-09-20T13:02:30.000Z",
99757
+ * "matches_credit": true
99758
+ * },
99759
+ * "origin_source": "engine",
99760
+ * "attribution": "credited"
98221
99761
  * },
98222
99762
  * {
98223
99763
  * "id": "01926f3a-1b2c-7d3e-8f40-5a6b7c8d9e0f",
@@ -98234,7 +99774,21 @@ interface paths {
98234
99774
  * },
98235
99775
  * "ad": null,
98236
99776
  * "credited_weight": null,
98237
- * "touch_count": null
99777
+ * "touch_count": null,
99778
+ * "origin": {
99779
+ * "platform": "instagram",
99780
+ * "external_id": "120211000000000205",
99781
+ * "name": "Limpieza facial",
99782
+ * "ad_name": null,
99783
+ * "headline": "Limpieza facial",
99784
+ * "campaign": null,
99785
+ * "thumbnail_url": null,
99786
+ * "media_archived": false,
99787
+ * "first_message_at": "2026-09-22T17:40:05.000Z",
99788
+ * "matches_credit": null
99789
+ * },
99790
+ * "origin_source": "conversation",
99791
+ * "attribution": "pending"
98238
99792
  * }
98239
99793
  * ],
98240
99794
  * "meta": {
@@ -98456,6 +100010,31 @@ interface paths {
98456
100010
  * "channel": "meta"
98457
100011
  * },
98458
100012
  * {
100013
+ * "stage": null,
100014
+ * "detail": "Ortodoncia invisible · Septiembre",
100015
+ * "credited": false,
100016
+ * "join_method": "ctwa_referral",
100017
+ * "click_id_kind": "ctwa_clid",
100018
+ * "ad": {
100019
+ * "external_id": "120211000000000101",
100020
+ * "name": "Reel · Antes y después",
100021
+ * "ad_set_name": "Mujeres 30-45 · Providencia",
100022
+ * "campaign": {
100023
+ * "external_id": "120211000000000100",
100024
+ * "name": "Ortodoncia invisible · Septiembre"
100025
+ * },
100026
+ * "creative_thumbnail_url": null
100027
+ * },
100028
+ * "value": null,
100029
+ * "current": false,
100030
+ * "id": "7d7d7d7d-1111-4222-8333-444455556666",
100031
+ * "kind": "chat",
100032
+ * "origin": "vitrina",
100033
+ * "label": "Mensaje por el anuncio «Reel · Antes y después»",
100034
+ * "at": "2026-09-20T13:02:30.000Z",
100035
+ * "channel": "whatsapp"
100036
+ * },
100037
+ * {
98459
100038
  * "stage": "lead_created",
98460
100039
  * "detail": "Por WhatsApp",
98461
100040
  * "credited": false,
@@ -108794,6 +110373,151 @@ interface paths {
108794
110373
  patch?: never;
108795
110374
  trace?: never;
108796
110375
  };
110376
+ "/whatsapp-templates/category-check": {
110377
+ parameters: {
110378
+ query?: never;
110379
+ header?: never;
110380
+ path?: never;
110381
+ cookie?: never;
110382
+ };
110383
+ get?: never;
110384
+ put?: never;
110385
+ /**
110386
+ * Predict whether Meta will reclassify a UTILITY draft as MARKETING
110387
+ * @description Jev-calibrated preview the template editor calls before submit (#2972). Meta silently reclassifies UTILITY templates as MARKETING at review time; this warns before that happens instead of after. `category` other than `UTILITY` always answers `applicable: false` — Meta’s reclassification only runs that one direction, so nothing is asked. Never changes what gets submitted and never writes to the Jev ledger (a preview is not an acted-on decision — the actual submission is what gets ledgered and later graded against Meta’s verdict). `cause` is a deterministic guess (never Jev’s own answer) set only when `likely_marketing` is true.
110388
+ */
110389
+ post: {
110390
+ parameters: {
110391
+ query?: never;
110392
+ header?: {
110393
+ /** @description Replay-safe retries: resending the SAME key with the SAME body returns the original response (`X-Idempotent-Replay: 1`) instead of creating a second copy — safe to send whenever a response might not have arrived. The same key with a DIFFERENT body answers `409 IDEMPOTENCY_KEY_CONFLICT`; use a fresh key per operation. */
110394
+ "Idempotency-Key"?: string;
110395
+ };
110396
+ path?: never;
110397
+ cookie?: never;
110398
+ };
110399
+ requestBody?: {
110400
+ content: {
110401
+ /**
110402
+ * @example {
110403
+ * "category": "UTILITY",
110404
+ * "body_text": "Hola {{1}}, ¿sigues interesado en el {{2}}? Si quieres, coordinamos una visita para que lo veas.",
110405
+ * "footer_text": "Autos del Valle"
110406
+ * }
110407
+ */
110408
+ "application/json": {
110409
+ /** @enum {string} */
110410
+ category: "MARKETING" | "UTILITY" | "AUTHENTICATION";
110411
+ body_text: string;
110412
+ header_text?: string | null;
110413
+ footer_text?: string | null;
110414
+ buttons?: ({
110415
+ /** @enum {string} */
110416
+ type: "QUICK_REPLY";
110417
+ text: string;
110418
+ } | {
110419
+ /** @enum {string} */
110420
+ type: "URL";
110421
+ text: string;
110422
+ /** Format: uri */
110423
+ url: string;
110424
+ /** Format: uri */
110425
+ example?: string;
110426
+ } | {
110427
+ /** @enum {string} */
110428
+ type: "PHONE_NUMBER";
110429
+ text: string;
110430
+ phone_number: string;
110431
+ })[];
110432
+ };
110433
+ };
110434
+ };
110435
+ responses: {
110436
+ /** @description The prediction */
110437
+ 200: {
110438
+ headers: {
110439
+ [name: string]: unknown;
110440
+ };
110441
+ content: {
110442
+ /**
110443
+ * @example {
110444
+ * "data": {
110445
+ * "applicable": true,
110446
+ * "likely_marketing": true,
110447
+ * "cause": "promotional_wording"
110448
+ * }
110449
+ * }
110450
+ */
110451
+ "application/json": {
110452
+ data?: unknown;
110453
+ meta?: {
110454
+ [key: string]: unknown;
110455
+ };
110456
+ };
110457
+ };
110458
+ };
110459
+ /** @description Validation error */
110460
+ 400: {
110461
+ headers: {
110462
+ [name: string]: unknown;
110463
+ };
110464
+ content: {
110465
+ "application/json": components["schemas"]["Error"];
110466
+ };
110467
+ };
110468
+ /** @description Unauthorized */
110469
+ 401: {
110470
+ headers: {
110471
+ [name: string]: unknown;
110472
+ };
110473
+ content: {
110474
+ "application/json": components["schemas"]["Error"];
110475
+ };
110476
+ };
110477
+ /** @description Forbidden */
110478
+ 403: {
110479
+ headers: {
110480
+ [name: string]: unknown;
110481
+ };
110482
+ content: {
110483
+ "application/json": components["schemas"]["Error"];
110484
+ };
110485
+ };
110486
+ /** @description Not found */
110487
+ 404: {
110488
+ headers: {
110489
+ [name: string]: unknown;
110490
+ };
110491
+ content: {
110492
+ "application/json": components["schemas"]["Error"];
110493
+ };
110494
+ };
110495
+ /** @description Conflict (incl. Idempotency-Key reuse with different body) */
110496
+ 409: {
110497
+ headers: {
110498
+ [name: string]: unknown;
110499
+ };
110500
+ content: {
110501
+ "application/json": components["schemas"]["Error"];
110502
+ };
110503
+ };
110504
+ /** @description Rate limited */
110505
+ 429: {
110506
+ headers: {
110507
+ [name: string]: unknown;
110508
+ };
110509
+ content: {
110510
+ "application/json": components["schemas"]["Error"];
110511
+ };
110512
+ };
110513
+ };
110514
+ };
110515
+ delete?: never;
110516
+ options?: never;
110517
+ head?: never;
110518
+ patch?: never;
110519
+ trace?: never;
110520
+ };
108797
110521
  "/whatsapp-templates/{id}/param-meta": {
108798
110522
  parameters: {
108799
110523
  query?: never;
@@ -118859,11 +120583,11 @@ interface components {
118859
120583
  stage_count: number;
118860
120584
  /** @description Clinics: the Purchase volume of the first payment, counted before the switch too. `null` outside clinics or when it could not be read. */
118861
120585
  first_payment_candidates: {
118862
- /** @description First payments of accepted plans over the last 28 days in Vitrina’s payment ledger — what reaches Meta as Purchase once switched. */
120586
+ /** @description First payments of accepted plans over the last 28 days — in Vitrina’s payment ledger and in the clinic system’s mirror, one per sale — what reaches Meta as Purchase once switched. */
118863
120587
  count_28d: number;
118864
120588
  /** @description `count_28d / 4`, one decimal. */
118865
120589
  weekly_28d: number;
118866
- /** @description The same volume in the clinic system’s mirror, for information: mirrored payments do not reach Meta. `null` without a mirror opt-in. */
120590
+ /** @description Of `count_28d`, the first payments claimed from the clinic system’s mirror (they reach Meta like the ledger’s). `null` without a mirror opt-in. */
118867
120591
  mirror_count_28d: number | null;
118868
120592
  mirror_weekly_28d: number | null;
118869
120593
  } | null;
@@ -119171,6 +120895,107 @@ interface components {
119171
120895
  last_synced_at: string | null;
119172
120896
  };
119173
120897
  MetaAdsAdsResponse: components["schemas"]["MetaAdsAdsNotConnected"] | components["schemas"]["MetaAdsAdsConnected"];
120898
+ /** @description The connected account. Present on `connected` and `needs_reconnect` (the card must name what needs reconnecting). */
120899
+ ProviderConnectionAccount: {
120900
+ /** @description The provider’s own id for the account (GoHighLevel location id, Mercado Pago user id). Opaque; relayed verbatim. */
120901
+ external_id: string;
120902
+ /** @description The account’s display name at the provider. */
120903
+ name: string;
120904
+ } | null;
120905
+ /** @description Why the card is in this state: a failed first connect, `reconnect_required`, `sync_error` (connected, last sync failed), `access_revoked`, `disconnected_by_user`. */
120906
+ ProviderConnectionReason: {
120907
+ /**
120908
+ * @description A STABLE code — also the copy key. `user_cancelled`, `provider_denied`, `provider_error`, `token_exchange_failed`, `account_connected_elsewhere`, `no_candidates`, `connect_failed`, `internal_error`, `state_expired`, `state_invalid`, `handoff_expired`, `handoff_used`, `provider_not_configured`, `origin_not_allowed` come from the consent itself; `selection_expired`, `selection_conflict`, `reconnect_required`, `sync_error`, `access_revoked`, `disconnected_by_user`, `connection_not_found`, `handoff_not_found`, `unknown_error` are Vitrina’s own.
120909
+ * @enum {string}
120910
+ */
120911
+ code: "user_cancelled" | "provider_denied" | "provider_error" | "token_exchange_failed" | "account_connected_elsewhere" | "no_candidates" | "connect_failed" | "internal_error" | "state_expired" | "state_invalid" | "handoff_expired" | "handoff_used" | "provider_not_configured" | "origin_not_allowed" | "selection_expired" | "selection_conflict" | "reconnect_required" | "sync_error" | "access_revoked" | "disconnected_by_user" | "connection_not_found" | "handoff_not_found" | "unknown_error";
120912
+ /** @description A ready Spanish sentence for the card, naming the provider. */
120913
+ label: string;
120914
+ /** @description Is trying the same thing again, right away, likely to work? */
120915
+ retryable: boolean;
120916
+ } | null;
120917
+ ProviderConnection: {
120918
+ /** @enum {string} */
120919
+ provider: "gohighlevel" | "mercadopago";
120920
+ display_name: string;
120921
+ /** @enum {string} */
120922
+ category: "crm" | "payments";
120923
+ /** @description One sentence: what connecting it does for the workspace. */
120924
+ description: string;
120925
+ /**
120926
+ * @description `not_connected` (never connected) → `connecting` (first consent in flight) → `connected` → `needs_reconnect` (the provider stopped honouring the grant; data is not updating) → `disconnected` (was connected, is not any more).
120927
+ * @enum {string}
120928
+ */
120929
+ state: "not_connected" | "connecting" | "connected" | "needs_reconnect" | "disconnected";
120930
+ /** @description The state in Spanish, ready to render. */
120931
+ state_label: string;
120932
+ account: components["schemas"]["ProviderConnectionAccount"];
120933
+ last_account: components["schemas"]["ProviderConnectionAccount"] & unknown;
120934
+ reason: components["schemas"]["ProviderConnectionReason"];
120935
+ last_attempt: components["schemas"]["ProviderConnectionReason"] & unknown;
120936
+ /** @description The one thing the card should offer next. Never a dead end. */
120937
+ next_step: {
120938
+ /**
120939
+ * @description `connect` / `resume` / `reconnect` / `retry` → `POST …/connect`; `activate_addon` → the Vitrina Ads add-on; `choose_account` → `POST …/finalize`; `contact_support` and `none` call nothing. Disconnect is always available on a card that is not `not_connected`.
120940
+ * @enum {string}
120941
+ */
120942
+ action: "connect" | "resume" | "choose_account" | "reconnect" | "retry" | "activate_addon" | "contact_support" | "none";
120943
+ /** @description The primary button’s label, or null for `none`. */
120944
+ label: string | null;
120945
+ };
120946
+ /** @description A connect in flight, if any — a consent the admin is on, or an account choice still owed. Rides on top of `state`: reconnecting a connected account does not make the card read `connecting`. */
120947
+ attempt: {
120948
+ /** @enum {string} */
120949
+ step: "consent" | "choose_account";
120950
+ /** @description The consent this attempt belongs to. On `choose_account`, send its `id` to `/finalize` as `handoff_id`. */
120951
+ handoff: {
120952
+ /** Format: uuid */
120953
+ id: string;
120954
+ expires_at: string;
120955
+ } | null;
120956
+ /** @description Step `choose_account` only: the accounts the consent can connect. Send one `external_id` to `/finalize`. */
120957
+ candidates: (components["schemas"]["ProviderConnectionAccount"] & Record<string, never>)[] | null;
120958
+ selection_expires_at: string | null;
120959
+ /** @description The picker / waiting title, in Spanish. */
120960
+ prompt: string;
120961
+ } | null;
120962
+ connected_at: string | null;
120963
+ disconnected_at: string | null;
120964
+ /** @description When the provider’s data last finished syncing. */
120965
+ last_synced_at: string | null;
120966
+ updated_at: string | null;
120967
+ /** @description Whether the Vitrina Ads add-on is active. `false` only on a card that outlived a lapsed add-on (a connection or a consent in flight): it stays visible and can be disconnected, and any step that would start a new consent becomes `activate_addon`. */
120968
+ addon_active: boolean;
120969
+ };
120970
+ ProviderConnectResult: {
120971
+ /** @description Send the WHOLE page here: it goes straight to the provider’s consent screen. Single-use, 45 minutes. */
120972
+ url: string;
120973
+ /** Format: uuid */
120974
+ handoff_id: string;
120975
+ expires_at: string;
120976
+ connection: components["schemas"]["ProviderConnection"];
120977
+ };
120978
+ ProviderConnectBody: {
120979
+ /** Format: uri */
120980
+ return_to: string;
120981
+ };
120982
+ ProviderReturnBody: {
120983
+ /** Format: uuid */
120984
+ handoff_id?: string;
120985
+ state?: string;
120986
+ /** @enum {string} */
120987
+ status?: "success" | "pending_selection" | "failed" | "cancelled";
120988
+ reason?: string;
120989
+ };
120990
+ ProviderFinalizeBody: {
120991
+ /** @description An `attempt.candidates[].external_id`, verbatim. */
120992
+ external_id: string;
120993
+ /**
120994
+ * Format: uuid
120995
+ * @description The `attempt.handoff.id` of the picker being answered. When sent and it is not the consent this card is waiting on, the answer is 409 `handoff_mismatch` — a stale tab cannot finalize a newer consent’s choice. Omitted: the current picker is finalized.
120996
+ */
120997
+ handoff_id?: string;
120998
+ };
119174
120999
  AdsOverviewDay: {
119175
121000
  date: string;
119176
121001
  /** @description `null` exactly when the engine could not scope spend. */
@@ -119240,6 +121065,33 @@ interface components {
119240
121065
  /** @description `grain=day` only: every day of the previous window of equal length. */
119241
121066
  previous_days?: components["schemas"]["AdsOverviewDay"][];
119242
121067
  };
121068
+ /** @description Present on a conversational row: a message-only objective (`MESSAGES`), or conversations the platform reports, or conversations Vitrina counts. Also present with `data_complete: false` (figures `null`) when a read failed on a row that may be conversational (its objective unknown or in the messaging family) in a workspace with ad-opened conversations. ABSENT otherwise — never a row of zeros, never partial counts. */
121069
+ AdsConversational: {
121070
+ /** @description `false` when a read behind the line failed or timed out: the `null` figures are unknown, not zero («sin datos de Meta por ahora»). `true` = every figure was read. */
121071
+ data_complete: boolean;
121072
+ /** @description The campaign objective as the ad platform names it (`OUTCOME_ENGAGEMENT`, `MESSAGES`, …); `null` when unknown. */
121073
+ objective: string | null;
121074
+ /** @description The ad platform’s own count («conversaciones iniciadas», its 7-day window, every sender) — a DIFFERENT population from `vitrina`; never add or compare the two. `null` when it reports none. */
121075
+ started: {
121076
+ /** @description Conversations started, as the ad platform reports them. */
121077
+ conversations: number;
121078
+ /** @description The row’s `spend` ÷ conversations started, whole CLP — the same spend basis as `vitrina.cost_per_conversation`; `null` when unknown. */
121079
+ cost_per_conversation: number | null;
121080
+ } | null;
121081
+ /** @description Vitrina’s own conversations from the ad — deterministic counts, never a credited share. `null` when Vitrina counts none in the period (or the ad → campaign map could not be read now). */
121082
+ vitrina: {
121083
+ /** @description Conversations whose first message came from this ad (or an ad of this campaign), opened in the period (America/Santiago days) — Vitrina’s own count. */
121084
+ conversations: number;
121085
+ /** @description «Respondidas»: of those, answered at least once by the team, the AI agent or an integration. */
121086
+ replied: number;
121087
+ /** @description Of those, with a booked appointment (cita / visita) recorded against them, whenever it was booked. */
121088
+ appointments: number;
121089
+ /** @description Of those, closed (a sale / an accepted plan) recorded against them, whenever it closed. */
121090
+ closed: number;
121091
+ /** @description The row’s `spend` ÷ `conversations`, whole CLP — the same spend basis as `started.cost_per_conversation`. */
121092
+ cost_per_conversation: number | null;
121093
+ } | null;
121094
+ };
119243
121095
  AdsCampaigns: {
119244
121096
  data: {
119245
121097
  campaign_id: string;
@@ -119268,6 +121120,7 @@ interface components {
119268
121120
  /** @description Conversions credited primarily to the campaign that day (each conversion counted once, on its largest-credit campaign). `null` when unknown: credit not available yet, the campaign outside the credited set read (top 25 by outcomes), or a day before the credit window (the last 92 days). Under a multi-touch model the days can sum to less than the row’s `outcome_count`. */
119269
121121
  outcome_count: number | null;
119270
121122
  }[] | null;
121123
+ conversational?: components["schemas"]["AdsConversational"];
119271
121124
  }[];
119272
121125
  };
119273
121126
  AdsAttributedSales: {
@@ -119332,6 +121185,7 @@ interface components {
119332
121185
  /** @description Percent (clicks ÷ impressions × 100), like the row’s `ctr`; `null` on a day without impressions. */
119333
121186
  ctr: number | null;
119334
121187
  }[] | null;
121188
+ conversational?: components["schemas"]["AdsConversational"] & unknown;
119335
121189
  }[];
119336
121190
  };
119337
121191
  AdsHealth: {
@@ -119349,6 +121203,18 @@ interface components {
119349
121203
  tracking_healthy: boolean;
119350
121204
  coverage_interpretation: string;
119351
121205
  missing: string[];
121206
+ /** @description The website tag signal behind `missing[tracker]`. `null` when the engine did not report it (unknown, never «silent»). */
121207
+ tracker: {
121208
+ /**
121209
+ * @description `live` = 10 or more web-tag touches in the window and one in the last 7 days; `sparse` = at least one touch but not live (few visits, or none in the last 7 days); `missing` = no touch at all. `missing[]` lists `tracker` for both `sparse` and `missing` — only `missing` means the tag sent nothing.
121210
+ * @enum {string}
121211
+ */
121212
+ status: "live" | "sparse" | "missing";
121213
+ web_touches: number;
121214
+ last_web_touch: string | null;
121215
+ /** @description A web-tag touch inside the last 7 days. */
121216
+ fresh: boolean;
121217
+ } | null;
119352
121218
  };
119353
121219
  traceable_roas_trend: {
119354
121220
  recent_roas: number | null;
@@ -119530,7 +121396,7 @@ interface components {
119530
121396
  /** @description `<kind>:<target external id>:<YYYY-MM-DD>` — a composite key, stable for a day. */
119531
121397
  key: string;
119532
121398
  /** @enum {string} */
119533
- kind: "pause_ad" | "budget_change" | "adset_duplicate_swap" | "utm_refresh" | "wiring_recheck" | "url_tags_fix" | "send_setup" | "send_stage" | "send_dataset" | "send_stop" | "send_protected_move" | "nav";
121399
+ kind: "pause_ad" | "campaign_pause" | "budget_change" | "adset_duplicate_swap" | "utm_refresh" | "wiring_recheck" | "url_tags_fix" | "send_setup" | "send_stage" | "send_dataset" | "send_stop" | "send_protected_move" | "nav";
119534
121400
  screen: ("resumen" | "campanas" | "creativos" | "salud" | "envio")[];
119535
121401
  /** @description The one card the rail leads with. */
119536
121402
  primary: boolean;
@@ -119552,7 +121418,7 @@ interface components {
119552
121418
  external_id: string;
119553
121419
  name: string | null;
119554
121420
  } | null;
119555
- /** @description `url_tags_fix` (and its read-only `nav` fallback) only: the ads whose «Parámetros de URL» do not carry the ad id where attribution reads it, busiest first, at most what one fix accepts (25). Ad set and campaign are named, not identified (the audit carries names only). */
121421
+ /** @description `url_tags_fix` (and its read-only `nav` fallback): the ads whose «Parámetros de URL» do not carry the ad id where attribution reads it, busiest first, at most what one fix accepts (25). Ad set and campaign are named, not identified (the audit carries names only). `campaign_pause`: the campaign’s delivering ads, busiest first (at most 6 — the card’s thumbnails; the live count is the preview’s). */
119556
121422
  ads?: {
119557
121423
  /** @description The ad’s Meta id. */
119558
121424
  external_id: string;
@@ -119561,7 +121427,7 @@ interface components {
119561
121427
  campaign_name: string | null;
119562
121428
  }[];
119563
121429
  };
119564
- /** @description The engine’s own vocabulary, e.g. `{ budget_change_pct: 20 }`. `url_tags_fix`: `{ ads: [{ external_id, expected_url_tags, url_tags }] }` — each ad’s current parameters (null = none) and exactly what it gets. «Enviar a Meta»: `send_setup`/`send_dataset` `{ dataset_id }`, `send_stage` `{ stage, enabled, meta_event, active_ad_sets }`. */
121430
+ /** @description The engine’s own vocabulary, e.g. `{ budget_change_pct: 20 }`. `url_tags_fix`: `{ ads: [{ external_id, expected_url_tags, url_tags }] }` — each ad’s current parameters (null = none) and exactly what it gets. «Enviar a Meta»: `send_setup`/`send_dataset` `{ dataset_id }`, `send_stage` `{ stage, enabled, meta_event, active_ad_sets }`. `campaign_pause`: `{ period_spend, period_days, delivering_ads_count, avg_daily_spend_7d }` — the campaign’s spend in the listed period (the account currency’s major units), the period’s days and its delivering ads, what the card draws at rest (spend → 0, ~spend/days a day); `avg_daily_spend_7d` is the live 7-day daily mean the preview shows (`AdsMoney`: exact decimal + currency), `null` when the list could not know it (real mode reads it only at preview) — fall back to `period_spend / period_days`, labelled as the period. */
119565
121431
  params: {
119566
121432
  [key: string]: unknown;
119567
121433
  };
@@ -119571,7 +121437,7 @@ interface components {
119571
121437
  /** @description Which operation executes it; `null` for `nav`. */
119572
121438
  engine_ref: {
119573
121439
  /** @enum {string} */
119574
- op: "recommendations.apply" | "ads.pause" | "wiring.ad_sets" | "quality.utm_refresh" | "wiring.recheck" | "actions.rollback" | "quality.url_tags_apply" | "quality.url_tags_undo" | "send.setup" | "send.dataset" | "send.stage" | "send.stop" | "send.restore";
121440
+ op: "recommendations.apply" | "ads.pause" | "campaigns.pause" | "campaigns.resume" | "wiring.ad_sets" | "quality.utm_refresh" | "wiring.recheck" | "actions.rollback" | "quality.url_tags_apply" | "quality.url_tags_undo" | "send.setup" | "send.dataset" | "send.stage" | "send.stop" | "send.restore";
119575
121441
  recommendation_id?: string;
119576
121442
  } | null;
119577
121443
  /** @description `nav` only: a workspace-relative path (`/anuncios/atribuidos?c=…`, `/anuncios/activar?paso=tag`), or an absolute Meta Ads Manager URL built from Meta ids (`https://www.facebook.com/adsmanager/…`) — open it in a new tab; it changes nothing by itself. */
@@ -119586,6 +121452,43 @@ interface components {
119586
121452
  */
119587
121453
  disabled_reason?: "consent_pending";
119588
121454
  };
121455
+ /** @description Mean daily spend over the last `spend_window_days` complete days — the «~$X al día» the pause stops. `null` = nothing spent / unknown. */
121456
+ AdsMoney: {
121457
+ /** @description Exact decimal string in the currency’s MAJOR units (never a float): `"12345.5"`. Parse before arithmetic. */
121458
+ amount: string;
121459
+ /** @description ISO 4217, e.g. `CLP`. */
121460
+ currency: string | null;
121461
+ /** @description Formatted for display (`$12.346`); `null` when the currency is unknown. */
121462
+ display: string | null;
121463
+ } | null;
121464
+ /** @description `campaign_pause` only: the live campaign, from ONE status read — everything the sheet and the card’s delta draw, no second call. */
121465
+ AdsCampaignPauseSnapshot: {
121466
+ /** @description The campaign’s Meta id. */
121467
+ external_id: string;
121468
+ name: string | null;
121469
+ /** @description Configured status: ACTIVE | PAUSED | ARCHIVED | DELETED. */
121470
+ status: string | null;
121471
+ /** @description Meta’s delivery verdict (ACTIVE, IN_PROCESS, WITH_ISSUES, …). */
121472
+ effective_status: string | null;
121473
+ /**
121474
+ * @description `live` — read from Meta just now. A preview answers only on a live read: when Meta cannot be read it is 503 `ADS_ACTION_READ_UNAVAILABLE` (retry in a moment).
121475
+ * @enum {string}
121476
+ */
121477
+ status_source: "live" | "synced";
121478
+ /** @description Ads the pause stops right now — «detiene {n} anuncios». `null` = unknown. */
121479
+ delivering_ads_count: number | null;
121480
+ avg_daily_spend: components["schemas"]["AdsMoney"];
121481
+ spend_window_days: number | null;
121482
+ /**
121483
+ * @description `campaign` — one campaign budget; `ad_set` — each ad set has its own.
121484
+ * @enum {string|null}
121485
+ */
121486
+ budget_level: "campaign" | "ad_set" | null;
121487
+ daily_budget: components["schemas"]["AdsMoney"] & unknown;
121488
+ lifetime_budget: components["schemas"]["AdsMoney"] & unknown;
121489
+ /** @description When the status was read (ISO). */
121490
+ as_of: string | null;
121491
+ };
119589
121492
  AdsActionPreview: {
119590
121493
  /** Format: uuid */
119591
121494
  preview_id: string;
@@ -119596,11 +121499,18 @@ interface components {
119596
121499
  label: string;
119597
121500
  before: string;
119598
121501
  after: string;
121502
+ /**
121503
+ * @description `kept`: a fact the action does not change — render ONE value (`before`; `after` repeats it) with `note`. Absent: a before → after change.
121504
+ * @enum {string}
121505
+ */
121506
+ kind?: "kept";
121507
+ /** @description `kept` rows: why it stays («Se conserva …»). */
121508
+ note?: string;
119599
121509
  }[];
119600
121510
  warnings: string[];
119601
121511
  /** @description The consent-bearing previews only (duplicate-and-swap, `url_tags_fix`, «Enviar a Meta» when it asks): `consent_terms.text` split into lines, for display (verbatim). */
119602
121512
  consent: string[];
119603
- /** @description The consent-bearing previews only: what the owner accepts before confirming. Execute must send `consent: { version, text_hash }`. For `url_tags_fix` the text is one sentence: rewriting an ad’s parameters replaces its creative, which can restart the learning phase of the affected ad sets. «Enviar a Meta»: `send_setup` always carries the owner’s authorisation of the send; `send_dataset`, and a `send_stage` off or `send_stop` that stops an event active ad sets optimise on, carry the learning-reset line. */
121513
+ /** @description The consent-bearing previews only: what the owner accepts before confirming. Execute must send `consent: { version, text_hash }`. For `campaign_pause`: every ad stops at once; «Reanudar» brings it back with the same budget, ads paused on their own stay paused, and a pause of 7 days or more can restart Meta’s learning phase. For `url_tags_fix` the text is one sentence: rewriting an ad’s parameters replaces its creative, which can restart the learning phase of the affected ad sets. «Enviar a Meta»: `send_setup` always carries the owner’s authorisation of the send; `send_dataset`, and a `send_stage` off or `send_stop` that stops an event active ad sets optimise on, carry the learning-reset line. */
119604
121514
  consent_terms: {
119605
121515
  version: string;
119606
121516
  /** @enum {string} */
@@ -119612,14 +121522,17 @@ interface components {
119612
121522
  } | null;
119613
121523
  reversible: boolean;
119614
121524
  /** @enum {string|null} */
119615
- engine_op: "recommendations.apply" | "ads.pause" | "wiring.ad_sets" | "quality.utm_refresh" | "wiring.recheck" | "actions.rollback" | "quality.url_tags_apply" | "quality.url_tags_undo" | "send.setup" | "send.dataset" | "send.stage" | "send.stop" | "send.restore" | null;
121525
+ engine_op: "recommendations.apply" | "ads.pause" | "campaigns.pause" | "campaigns.resume" | "wiring.ad_sets" | "quality.utm_refresh" | "wiring.recheck" | "actions.rollback" | "quality.url_tags_apply" | "quality.url_tags_undo" | "send.setup" | "send.dataset" | "send.stage" | "send.stop" | "send.restore" | null;
121526
+ /** @description `campaign_pause` only: the line the sheet leads with — «Pausar «{campaign}» · detiene {n} anuncios · ~$X al día» (the count and the daily figure are dropped when unknown). */
121527
+ summary?: string;
121528
+ campaign?: components["schemas"]["AdsCampaignPauseSnapshot"];
119616
121529
  };
119617
121530
  AdsActionExecution: {
119618
121531
  /** Format: uuid */
119619
121532
  id: string;
119620
121533
  action_key: string;
119621
121534
  /** @enum {string} */
119622
- kind: "pause_ad" | "budget_change" | "adset_duplicate_swap" | "utm_refresh" | "wiring_recheck" | "url_tags_fix" | "send_setup" | "send_stage" | "send_dataset" | "send_stop";
121535
+ kind: "pause_ad" | "campaign_pause" | "budget_change" | "adset_duplicate_swap" | "utm_refresh" | "wiring_recheck" | "url_tags_fix" | "send_setup" | "send_stage" | "send_dataset" | "send_stop";
119623
121536
  /** @enum {string} */
119624
121537
  status: "queued" | "running" | "succeeded" | "failed" | "refused" | "rolled_back";
119625
121538
  target: {
@@ -119637,7 +121550,7 @@ interface components {
119637
121550
  name: string | null;
119638
121551
  }[];
119639
121552
  };
119640
- /** @description What happened. `url_tags_fix` adds `partial` (true when fewer ads were corrected than asked — never show «Hecho» then), `corrected_count`, `requested_count` and `ads[]`: per ad `{external_id, name, result: corrected|skipped|failed, code, message, url_tags_after, engine_action_id, undo_available}` (an ad that already carried the parameters counts as corrected; it is undoable when this fix wrote it). Its undo (a rollback row) reports `undone_count`, `drifted_count` (changed in Meta since — never touched), `gone_count` (already undone elsewhere — no longer undoable from here), `reconnect_count` (Meta asks to reconnect), `retryable_count` (no answer or reconnect — the original stays undoable and the next rollback sends only those ads), `requested_count`, `partial` and `ads[]` `{external_id, name, result: undone|drifted|gone|retryable|reconnect, code, message}`; a failed or refused fix still carries `ads[]`. */
121553
+ /** @description What happened. `url_tags_fix` adds `partial` (true when fewer ads were corrected than asked — never show «Hecho» then), `corrected_count`, `requested_count` and `ads[]`: per ad `{external_id, name, result: corrected|skipped|failed, code, message, url_tags_after, engine_action_id, undo_available}` (an ad that already carried the parameters counts as corrected; it is undoable when this fix wrote it). Its undo (a rollback row) reports `undone_count`, `drifted_count` (changed in Meta since — never touched), `gone_count` (already undone elsewhere — no longer undoable from here), `reconnect_count` (Meta asks to reconnect), `retryable_count` (no answer or reconnect — the original stays undoable and the next rollback sends only those ads), `requested_count`, `partial` and `ads[]` `{external_id, name, result: undone|drifted|gone|retryable|reconnect, code, message}`; a failed or refused fix still carries `ads[]`. `campaign_pause` (and its «Reanudar», a rollback row) adds `engine_outcome` (`applied` | `no_change` — already in that state, nothing written | `replayed`), `status`, `previous_status`, `campaign_name`, `affected_ads_count` (ads stopped, or freed by the resume; `null` when Meta gave no count) and `journal_line` («pausaste la campaña «X» · 4 anuncios» / «reanudaste …»; `null` on `no_change`). A «Reanudar» never brings back ads that were paused on their own. */
119641
121554
  outcome: {
119642
121555
  summary: string;
119643
121556
  engine_action_id: string | null;
@@ -119694,6 +121607,35 @@ interface components {
119694
121607
  */
119695
121608
  collector: "same_origin" | "cname" | "none" | null;
119696
121609
  };
121610
+ /** @description The ad that opened the originating conversation — Vitrina’s own fact from the first message (a click-to-WhatsApp or Instagram / Messenger ad referral), known at once. Not a credit claim: `ad` is the credit. `null` when no ad opened the thread (or it was a boosted post). */
121611
+ AdsFeedOrigin: {
121612
+ /**
121613
+ * @description Where the person wrote from the ad.
121614
+ * @enum {string}
121615
+ */
121616
+ platform: "whatsapp" | "instagram" | "messenger";
121617
+ /** @description The AD’s platform id (never a post id). */
121618
+ external_id: string;
121619
+ /** @description What to render: the ad’s name in the ad account, else its headline, else `null` («Mensaje por un anuncio de Instagram»). */
121620
+ name: string | null;
121621
+ /** @description The ad’s name in the ad account; `null` when unknown. */
121622
+ ad_name: string | null;
121623
+ /** @description The ad’s headline — the advertiser’s copy, never the customer’s words. */
121624
+ headline: string | null;
121625
+ /** @description The ad’s campaign, when the ads read knows it. */
121626
+ campaign: {
121627
+ /** @description The campaign’s platform id, when known. */
121628
+ external_id: string | null;
121629
+ name: string | null;
121630
+ } | null;
121631
+ /** @description Still image of the creative: Vitrina’s durable copy when `media_archived`, else the platform’s own link, which expires (render with a fallback). */
121632
+ thumbnail_url: string | null;
121633
+ media_archived: boolean;
121634
+ /** @description When the person first wrote from the ad (ISO 8601). */
121635
+ first_message_at: string | null;
121636
+ /** @description `true` when the credited campaign (`ad.campaign`) is this ad’s campaign, `false` when the credit went to another campaign, `null` when unknown or not credited yet. */
121637
+ matches_credit: boolean | null;
121638
+ } | null;
119697
121639
  AdsFeedItem: {
119698
121640
  /** Format: uuid */
119699
121641
  id: string;
@@ -119706,7 +121648,7 @@ interface components {
119706
121648
  occurred_at: string;
119707
121649
  /** @description When Vitrina recorded it — the cursor field, microsecond-exact. */
119708
121650
  created_at: string;
119709
- /** @description The engine accepted the outcome as a conversion it can attribute. `true` with `ad: null` = «anuncio por confirmar». */
121651
+ /** @description The engine accepted the outcome as a conversion it can attribute. Whether the credit can still arrive is `attribution`. */
119710
121652
  matched: boolean;
119711
121653
  /** @description CLP for money stages; `null` otherwise (never 0). */
119712
121654
  value: number | null;
@@ -119733,6 +121675,24 @@ interface components {
119733
121675
  } | null;
119734
121676
  credited_weight: number | null;
119735
121677
  touch_count: number | null;
121678
+ /** @description Set when the contact was matched to the patient AUTOMATICALLY (an identifier held by exactly one patient and one contact) — show «Vinculado automáticamente» with the method. `null`/absent = a family bond, a booking link or the contact itself. Undo with `POST /clinic/auto-links/{id}/reject`. */
121679
+ identity?: {
121680
+ /** @enum {string} */
121681
+ source: "auto_link";
121682
+ /** @enum {string} */
121683
+ method: "phone_unique" | "email_unique";
121684
+ } | null;
121685
+ origin: components["schemas"]["AdsFeedOrigin"];
121686
+ /**
121687
+ * @description Which source the row names its ad from: `engine` — `ad` (the credited campaign) is set; `conversation` — `ad` is `null` and `origin` is set: show the origin, never «anuncio por confirmar» (whether the credit may still come is `attribution`); `null` — neither is known.
121688
+ * @enum {string|null}
121689
+ */
121690
+ origin_source: "engine" | "conversation" | null;
121691
+ /**
121692
+ * @description Whether the credit (`ad`) can still arrive: `credited` — `ad` is set; `pending` — not yet («atribución por confirmar» beside `origin`, else «anuncio por confirmar»); `final` — it never will: the contact was removed from attribution, the outcome is not one an ad can be credited with, or 72 hours after the outcome’s time a complete credit read still names no campaign. Show `origin` alone, or «sin anuncio» — never «por confirmar». A row is never settled while the credit read is incomplete (`meta.enrichment` not `ready`).
121693
+ * @enum {string}
121694
+ */
121695
+ attribution: "credited" | "pending" | "final";
119736
121696
  };
119737
121697
  AdsJourneyStepAd: {
119738
121698
  /** @description The ad’s PLATFORM id; `null` when only the campaign is known. */
@@ -119751,7 +121711,7 @@ interface components {
119751
121711
  /** @description Stable key for the step. For a step recorded by Vitrina it is the feed item id (`GET /ads/feed` `data[].id`). */
119752
121712
  id: string;
119753
121713
  /**
119754
- * @description Touches: `ad` (an off-site ad touch — an ad click or a click-to-WhatsApp referral), `click` (a visit that landed from an ad click, or a tracked link click), `visit` (a site visit), `chat` (the first message, or a social interaction). Recorded steps, in funnel order: `chat` (first message), `booking`, `attended`, `quote`, `accepted` (a presupuesto accepted / a sale closed), `payment`; `outcome` for anything else.
121714
+ * @description Touches: `ad` (an off-site ad touch — an ad click or a click-to-WhatsApp referral), `click` (a visit that landed from an ad click, or a tracked link click), `visit` (a site visit), `chat` (the first message, or a social interaction). «Mensaje por el anuncio X»: a `chat` step with `origin: vitrina`, `stage: null`, `id` = the conversation’s id and the ad on `ad`, at the moment the person first wrote from the ad — one per ad-opened conversation; the same ad is never also on the first-message fact, and the engine’s touch of that same referral (the ad’s, or a click-id-only one) is folded into it, credit mark included. Recorded steps, in funnel order: `chat` (first message), `booking`, `attended`, `quote`, `accepted` (a presupuesto accepted / a sale closed), `payment`; `outcome` for anything else.
119755
121715
  * @enum {string}
119756
121716
  */
119757
121717
  kind: "ad" | "click" | "visit" | "chat" | "outcome" | "booking" | "attended" | "quote" | "accepted" | "payment";
@@ -119775,7 +121735,7 @@ interface components {
119775
121735
  /** @description The touch the outcome was credited to, under `model`. */
119776
121736
  credited: boolean;
119777
121737
  /**
119778
- * @description How the touch is tied to this person, when proven: `ctwa_referral` (the click-to-WhatsApp / Instagram ad referral), `click_id` (the ad click id the outcome carried), `link_token` (a link Vitrina sent), `anonymous_id` (the same browser). `null` on recorded steps.
121738
+ * @description How the touch is tied to this person, when proven: `ctwa_referral` (the click-to-WhatsApp / Instagram ad referral), `click_id` (the ad click id the outcome carried), `link_token` (a link Vitrina sent), `anonymous_id` (the same browser). `null` on recorded steps; `ctwa_referral` on the «Mensaje por el anuncio» step.
119779
121739
  * @enum {string|null}
119780
121740
  */
119781
121741
  join_method: "ctwa_referral" | "click_id" | "link_token" | "anonymous_id" | null;
@@ -119883,7 +121843,7 @@ declare function createClient(opts: CreateClientOptions): VitrinaClient;
119883
121843
  * is regenerated — narrow on the ones you handle, never assume this
119884
121844
  * list is exhaustive at runtime.
119885
121845
  */
119886
- type ErrorCode = 'VALIDATION_ERROR' | 'NOT_FOUND' | 'UNAUTHORIZED' | 'FORBIDDEN' | 'CONFLICT' | 'UNIQUE_CONFLICT' | 'IDEMPOTENCY_KEY_CONFLICT' | 'VERTICAL_NOT_ENABLED' | 'ENTITLEMENT_NOT_ACTIVE' | 'UNSUPPORTED_VEHICLE_TYPE' | 'NO_OFFERING_ADAPTER' | 'PAYLOAD_TOO_LARGE' | 'UNSUPPORTED_MEDIA_TYPE' | 'UPSTREAM_ERROR' | 'RATE_LIMITED' | 'GONE' | 'INTERNAL_ERROR' | 'MESSAGING_ACCOUNT_TENANT_MISSING' | 'COPILOT_APPROVALS_UNAVAILABLE' | 'COPILOT_APPROVAL_REPLAY_BLOCKED' | 'COPILOT_DISABLED' | 'COPILOT_BUDGET_EXCEEDED' | 'DEALER_SITES_NOT_CONFIGURED' | 'STOREFRONT_ZONES_NOT_CONFIGURED' | 'RECONTACTO_NOT_ENABLED' | 'SYNC_QUEUE_UNAVAILABLE' | 'SHARE_IMAGE_UNAVAILABLE' | 'ATTRIBUTION_PROFILE_UNAVAILABLE' | 'DOCUMENT_LOGO_UNAVAILABLE' | 'ATTRIBUTION_PROFILE_MISMATCH' | 'ADS_KEY_NEEDS_REMINT' | 'ADS_ACTION_REPLAY_BLOCKED' | 'ADS_ACTION_NOT_AVAILABLE' | 'ADS_ACTION_NOT_REVERSIBLE' | 'ADS_ACTION_CONSENT_REQUIRED' | 'MISSING_SIGNATURE' | 'MALFORMED_SIGNATURE' | 'STALE_SIGNATURE' | 'INVALID_SIGNATURE' | 'REPLAY_DETECTED' | 'TURNSTILE_UNAVAILABLE' | 'TASADOR_QUOTA_EXHAUSTED' | 'TASADOR_ANON_DAILY_LIMIT' | 'TASADOR_CAPACITY_REACHED' | 'CLINIC_PLAN_REQUIRED' | 'TENANT_BILLING_PAUSED' | 'FACEBOOK_MARKETPLACE_NOT_AVAILABLE' | 'WORKSPACE_MEMBER_INACTIVE' | 'DEVICE_AGENT_ALREADY_PAIRED' | 'DEVICE_AGENT_SELF_REQUIRES_USER' | 'DEVICE_AGENT_KIND_NOT_ALLOWED' | 'DEVICE_TASK_STATUS_NOT_ALLOWED' | 'FACEBOOK_MARKETPLACE_NO_PHONE_AGENT' | 'FACEBOOK_MARKETPLACE_THREAD_NOT_ADDRESSABLE' | 'FACEBOOK_MARKETPLACE_ATTACHMENT_UNSUPPORTED' | 'REPLY_PACING_LOCKED' | 'SANDBOX_CHANNEL_FORBIDDEN' | 'SANDBOX_BILLING_FORBIDDEN' | 'SANDBOX_NOT_PROVISIONED' | 'SANDBOX_RESET_UNAVAILABLE' | 'CONNECTED_APP_SENSITIVE_DATA' | 'handoff_mismatch' | 'sync_in_flight' | 'atribu_refused' | 'start_again' | 'reconnect' | 'partner_token_invalid' | 'OUTBOUND_BLOCKED' | 'OUTBOUND_WARNING' | 'dpa_acceptance_required' | 'EMAIL_SENDER_UNRESOLVED' | 'CAMPAIGN_FROM_CONNECTED_MAILBOX' | 'BANK_FEED_NOT_CONFIGURED' | 'ADDON_NOT_PRICED' | 'MIRROR_LINES_UNMAPPED';
121846
+ type ErrorCode = 'VALIDATION_ERROR' | 'NOT_FOUND' | 'UNAUTHORIZED' | 'FORBIDDEN' | 'CONFLICT' | 'UNIQUE_CONFLICT' | 'IDEMPOTENCY_KEY_CONFLICT' | 'VERTICAL_NOT_ENABLED' | 'ENTITLEMENT_NOT_ACTIVE' | 'UNSUPPORTED_VEHICLE_TYPE' | 'NO_OFFERING_ADAPTER' | 'PAYLOAD_TOO_LARGE' | 'UNSUPPORTED_MEDIA_TYPE' | 'UPSTREAM_ERROR' | 'RATE_LIMITED' | 'GONE' | 'INTERNAL_ERROR' | 'MESSAGING_ACCOUNT_TENANT_MISSING' | 'COPILOT_APPROVALS_UNAVAILABLE' | 'COPILOT_APPROVAL_REPLAY_BLOCKED' | 'COPILOT_DISABLED' | 'COPILOT_BUDGET_EXCEEDED' | 'DEALER_SITES_NOT_CONFIGURED' | 'STOREFRONT_ZONES_NOT_CONFIGURED' | 'RECONTACTO_NOT_ENABLED' | 'SYNC_QUEUE_UNAVAILABLE' | 'SHARE_IMAGE_UNAVAILABLE' | 'ATTRIBUTION_PROFILE_UNAVAILABLE' | 'DOCUMENT_LOGO_UNAVAILABLE' | 'ATTRIBUTION_PROFILE_MISMATCH' | 'ADS_KEY_NEEDS_REMINT' | 'ADS_ACTION_READ_UNAVAILABLE' | 'ADS_ACTION_REPLAY_BLOCKED' | 'ADS_ACTION_NOT_AVAILABLE' | 'ADS_ACTION_NOT_REVERSIBLE' | 'ADS_ACTION_CONSENT_REQUIRED' | 'MISSING_SIGNATURE' | 'MALFORMED_SIGNATURE' | 'STALE_SIGNATURE' | 'INVALID_SIGNATURE' | 'REPLAY_DETECTED' | 'TURNSTILE_UNAVAILABLE' | 'TASADOR_QUOTA_EXHAUSTED' | 'TASADOR_ANON_DAILY_LIMIT' | 'TASADOR_CAPACITY_REACHED' | 'CLINIC_PLAN_REQUIRED' | 'TENANT_BILLING_PAUSED' | 'FACEBOOK_MARKETPLACE_NOT_AVAILABLE' | 'WORKSPACE_MEMBER_INACTIVE' | 'DEVICE_AGENT_ALREADY_PAIRED' | 'DEVICE_AGENT_SELF_REQUIRES_USER' | 'DEVICE_AGENT_KIND_NOT_ALLOWED' | 'DEVICE_TASK_STATUS_NOT_ALLOWED' | 'FACEBOOK_MARKETPLACE_NO_PHONE_AGENT' | 'FACEBOOK_MARKETPLACE_THREAD_NOT_ADDRESSABLE' | 'FACEBOOK_MARKETPLACE_ATTACHMENT_UNSUPPORTED' | 'REPLY_PACING_LOCKED' | 'SANDBOX_CHANNEL_FORBIDDEN' | 'SANDBOX_BILLING_FORBIDDEN' | 'SANDBOX_NOT_PROVISIONED' | 'SANDBOX_RESET_UNAVAILABLE' | 'CONNECTED_APP_SENSITIVE_DATA' | 'handoff_mismatch' | 'sync_in_flight' | 'disconnect_refused' | 'start_again' | 'reconnect' | 'not_selecting' | 'selection_expired' | 'selection_conflict' | 'account_connected_elsewhere' | 'candidate_unavailable' | 'integration_managed' | 'partner_token_invalid' | 'OUTBOUND_BLOCKED' | 'OUTBOUND_WARNING' | 'dpa_acceptance_required' | 'EMAIL_SENDER_UNRESOLVED' | 'CAMPAIGN_FROM_CONNECTED_MAILBOX' | 'BANK_FEED_NOT_CONFIGURED' | 'ADDON_NOT_PRICED' | 'MIRROR_LINES_UNMAPPED';
119887
121847
  declare const ERROR_CODES: readonly ErrorCode[];
119888
121848
 
119889
121849
  /**