@vitrina/api 13.0.0 → 13.2.0

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.
Files changed (3) hide show
  1. package/dist/index.d.cts +3252 -286
  2. package/dist/index.d.ts +3252 -286
  3. package/package.json +1 -1
package/dist/index.d.cts CHANGED
@@ -2058,6 +2058,10 @@ interface paths {
2058
2058
  treatment_price?: "unset" | "first_ask" | "never";
2059
2059
  /** @description «Ofrecer lista de espera si la primera hora está a más de 7 días» (#3314). Default false. */
2060
2060
  far_slot_waitlist?: boolean;
2061
+ /** @description «Dolor: ofrecer las primeras horas de hoy o mañana» (D72). A patient in pain is offered today's / tomorrow's first slots in the same turn; a handoff becomes a heads-up to reception, not the answer. Default false; true for the agent a new clinic's onboarding creates. */
2062
+ urgency_slots?: boolean;
2063
+ /** @description «Especialista a más de 7 días: ofrecer también una evaluación general esta semana» (D74). `find_slots` then also reports the clinic's general evaluation slots of the next 7 days. Default false. */
2064
+ specialist_fallback?: boolean;
2061
2065
  };
2062
2066
  /** @description The `draft_version` version you last read (the `X-Config-Version` response header, or the version field in the body). If it is no longer current the write is refused with 409 `VERSION_CONFLICT` and nothing is written; the error carries the current version and a diff hint. Optional for backward compatibility (an omitted value is accepted and logged), but send it: without it two editors silently overwrite each other. The `If-Match` header is accepted as an alternative. */
2063
2067
  expected_version?: number | null;
@@ -2241,7 +2245,7 @@ interface paths {
2241
2245
  };
2242
2246
  /**
2243
2247
  * Read the agent’s behaviour policies
2244
- * @description Per-agent switches that change what the agent says (#3312/#3314): `treatment_price` (`unset` | `first_ask` | `never`, healthcare) and `far_slot_waitlist`. `live` is what runs today, `draft` what is staged (null = nothing staged). Every default is the agent’s behaviour before policies existed: nothing is added to its prompt while a policy is unset. `price_policy_conflicts` lists lines of the agent’s own instructions (draft when staged) and active skills that already state a price rule, so a client can reconcile them before publishing. Change them with `PUT /ai-agents/{id}/draft` (`behavior_policies`) and `POST /ai-agents/{id}/publish`.
2248
+ * @description Per-agent switches that change what the agent says (#3312/#3314): `treatment_price` (`unset` | `first_ask` | `never`, healthcare) and `far_slot_waitlist`, `urgency_slots` (D72) and `specialist_fallback` (D74). `live` is what runs today, `draft` what is staged (null = nothing staged). Every default is the agent’s behaviour before policies existed: nothing is added to its prompt while a policy is unset. `price_policy_conflicts` lists lines of the agent’s own instructions (draft when staged) and active skills that already state a price rule, so a client can reconcile them before publishing. Change them with `PUT /ai-agents/{id}/draft` (`behavior_policies`) and `POST /ai-agents/{id}/publish`.
2245
2249
  */
2246
2250
  get: {
2247
2251
  parameters: {
@@ -2265,11 +2269,15 @@ interface paths {
2265
2269
  * "data": {
2266
2270
  * "live": {
2267
2271
  * "treatment_price": "unset",
2268
- * "far_slot_waitlist": false
2272
+ * "far_slot_waitlist": false,
2273
+ * "urgency_slots": false,
2274
+ * "specialist_fallback": false
2269
2275
  * },
2270
2276
  * "draft": {
2271
2277
  * "treatment_price": "first_ask",
2272
- * "far_slot_waitlist": false
2278
+ * "far_slot_waitlist": false,
2279
+ * "urgency_slots": true,
2280
+ * "specialist_fallback": false
2273
2281
  * },
2274
2282
  * "price_policy_conflicts": [
2275
2283
  * {
@@ -2291,6 +2299,10 @@ interface paths {
2291
2299
  treatment_price: "unset" | "first_ask" | "never";
2292
2300
  /** @description «Ofrecer lista de espera si la primera hora está a más de 7 días» (#3314). Default false. */
2293
2301
  far_slot_waitlist: boolean;
2302
+ /** @description «Dolor: ofrecer las primeras horas de hoy o mañana» (D72). A patient in pain is offered today's / tomorrow's first slots in the same turn; a handoff becomes a heads-up to reception, not the answer. Default false; true for the agent a new clinic's onboarding creates. */
2303
+ urgency_slots: boolean;
2304
+ /** @description «Especialista a más de 7 días: ofrecer también una evaluación general esta semana» (D74). `find_slots` then also reports the clinic's general evaluation slots of the next 7 days. Default false. */
2305
+ specialist_fallback: boolean;
2294
2306
  };
2295
2307
  draft: {
2296
2308
  /**
@@ -2300,6 +2312,10 @@ interface paths {
2300
2312
  treatment_price: "unset" | "first_ask" | "never";
2301
2313
  /** @description «Ofrecer lista de espera si la primera hora está a más de 7 días» (#3314). Default false. */
2302
2314
  far_slot_waitlist: boolean;
2315
+ /** @description «Dolor: ofrecer las primeras horas de hoy o mañana» (D72). A patient in pain is offered today's / tomorrow's first slots in the same turn; a handoff becomes a heads-up to reception, not the answer. Default false; true for the agent a new clinic's onboarding creates. */
2316
+ urgency_slots: boolean;
2317
+ /** @description «Especialista a más de 7 días: ofrecer también una evaluación general esta semana» (D74). `find_slots` then also reports the clinic's general evaluation slots of the next 7 days. Default false. */
2318
+ specialist_fallback: boolean;
2303
2319
  } | null;
2304
2320
  price_policy_conflicts: {
2305
2321
  /** @enum {string} */
@@ -59389,6 +59405,1132 @@ interface paths {
59389
59405
  patch?: never;
59390
59406
  trace?: never;
59391
59407
  };
59408
+ "/clinic/budget-followups/today": {
59409
+ parameters: {
59410
+ query?: never;
59411
+ header?: never;
59412
+ path?: never;
59413
+ cookie?: never;
59414
+ };
59415
+ /**
59416
+ * «Llamar hoy»: unaccepted presupuestos to follow up today
59417
+ * @description The clinic's follow-up of unaccepted presupuestos, one row per LINEAGE (a re-issue never restarts it). A presupuesto is on the list on the clinic day after it was first sent, and again on day 7 and day 21 (`step` 1/2/3), until it is accepted, rejected, voided, expires (60 days, 90 for ortodoncia / implantología), a cita is booked, the patient answers a touch, or reception taps «Aceptó» / «No quiere». Rows carry what reception needs to call (patient, phone, items, total, professional) and the day's logged `outcome`. When no outcome was logged and nobody at the clinic wrote to the patient since the step opened, a WhatsApp `touch` is proposed at 17:00 clinic time (11:00 when `call_list` is off) — always for a person's approval (`touch.action_id` goes to `POST /outbound/approvals/{id}/approve`); at most three per lineage; never twice a day; never a price or a discount. `counts.unlogged` is «n sin registrar». `summary` covers the lineages issued in the last 30 days: followed up vs never contacted. `template` says whether the WhatsApp template for touches outside the 24 h window is approved. Each row carries the patient's `origin` (Meta with the ad, or Orgánico: the attribution Vitrina Ads reports); narrow the list with `origin=meta|organic|unknown` and `origin_ad_id` — `counts` follow the narrowing, `summary` does not. Requires `clinic:read`.
59418
+ *
59419
+ * **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`.
59420
+ */
59421
+ get: {
59422
+ parameters: {
59423
+ query?: {
59424
+ /** @description Origen of the patient: `meta` (came from a Meta ad), `organic` (a linked contact with no ad touch) or `unknown` («Sin dato»: no linked contact). Same attribution as Vitrina Ads' people from ads. */
59425
+ origin?: "meta" | "organic" | "unknown";
59426
+ /** @description Narrow `origin=meta` to one Meta ad (its platform ad id). */
59427
+ origin_ad_id?: string;
59428
+ };
59429
+ header?: never;
59430
+ path?: never;
59431
+ cookie?: never;
59432
+ };
59433
+ requestBody?: never;
59434
+ responses: {
59435
+ /** @description Today's follow-up list */
59436
+ 200: {
59437
+ headers: {
59438
+ [name: string]: unknown;
59439
+ };
59440
+ content: {
59441
+ /**
59442
+ * @example {
59443
+ * "data": {
59444
+ * "today": "2026-10-09",
59445
+ * "call_list": true,
59446
+ * "template": {
59447
+ * "name": "seguimiento_presupuesto",
59448
+ * "status": "approved",
59449
+ * "whatsapp_connected": true
59450
+ * },
59451
+ * "counts": {
59452
+ * "to_call": 1,
59453
+ * "logged": 0,
59454
+ * "unlogged": 1,
59455
+ * "by_priority": {
59456
+ * "P1": 1,
59457
+ * "P2": 2,
59458
+ * "P3": 3,
59459
+ * "P4": 4
59460
+ * },
59461
+ * "awaiting_approval": 0
59462
+ * },
59463
+ * "viewer": {
59464
+ * "may_log": true,
59465
+ * "may_approve": true,
59466
+ * "may_configure": false
59467
+ * },
59468
+ * "money_visible": true,
59469
+ * "rows": [
59470
+ * {
59471
+ * "id": "7b7b7b7b-0000-4000-8000-000000000001",
59472
+ * "lineage_id": "7c7c7c7c-0000-4000-8000-000000000001",
59473
+ * "source": "native",
59474
+ * "budget_id": "7d7d7d7d-0000-4000-8000-000000000001",
59475
+ * "mirror_plan_id": null,
59476
+ * "vendor": null,
59477
+ * "budget_display_id": "P-0412",
59478
+ * "patient_name": "María José Fuentes Lagos",
59479
+ * "phone": "+56960000001",
59480
+ * "contact_id": "1a73af9e-0000-4000-8000-000000000001",
59481
+ * "clinic_patient_id": "b1b0b8de-0000-4000-8000-000000000001",
59482
+ * "conversation_id": "c0c0c0c0-0000-4000-8000-000000000001",
59483
+ * "linea": {
59484
+ * "id": "d1d1d1d1-0000-4000-8000-000000000001",
59485
+ * "nombre": "Implantes"
59486
+ * },
59487
+ * "origin": {
59488
+ * "kind": "organic",
59489
+ * "label": "Orgánico",
59490
+ * "ad_id": null,
59491
+ * "ad_name": null,
59492
+ * "ad_set_name": null,
59493
+ * "campaign_name": null,
59494
+ * "first_touch_at": null,
59495
+ * "via": null
59496
+ * },
59497
+ * "professional_name": "Dra. Ana Soto",
59498
+ * "items": [
59499
+ * "Implante unitario"
59500
+ * ],
59501
+ * "item_count": 1,
59502
+ * "total_clp": 890000,
59503
+ * "issued_on": "2026-10-08",
59504
+ * "issued_label": "jueves 8 de octubre",
59505
+ * "days_since_issue": 1,
59506
+ * "step": 1,
59507
+ * "status": "active",
59508
+ * "stop_reason": null,
59509
+ * "touches_sent": 0,
59510
+ * "promised": false,
59511
+ * "outcome": null,
59512
+ * "touch": null
59513
+ * }
59514
+ * ],
59515
+ * "summary": {
59516
+ * "from": "2026-09-10",
59517
+ * "to": "2026-10-09",
59518
+ * "lineages": 12,
59519
+ * "followed_up": 9,
59520
+ * "never_contacted": 3,
59521
+ * "in_progress": 4,
59522
+ * "closed_after_followup": 5
59523
+ * }
59524
+ * }
59525
+ * }
59526
+ */
59527
+ "application/json": {
59528
+ data: {
59529
+ today: string;
59530
+ call_list: boolean;
59531
+ template: {
59532
+ name: string;
59533
+ /** @enum {string} */
59534
+ status: "approved" | "pending" | "rejected" | "missing" | "wrong_category";
59535
+ whatsapp_connected: boolean;
59536
+ };
59537
+ counts: {
59538
+ to_call: number;
59539
+ logged: number;
59540
+ /** @description «n sin registrar» */
59541
+ unlogged: number;
59542
+ /** @description Leads per Prioridad de llamada, whole section. */
59543
+ by_priority: {
59544
+ P1: number;
59545
+ P2: number;
59546
+ P3: number;
59547
+ P4: number;
59548
+ };
59549
+ awaiting_approval: number;
59550
+ };
59551
+ viewer: {
59552
+ may_log: boolean;
59553
+ may_approve: boolean;
59554
+ may_configure: boolean;
59555
+ };
59556
+ /** @description Whether rows carry items and totals (`clinic_money:read`). */
59557
+ money_visible: boolean;
59558
+ rows: {
59559
+ /** Format: uuid */
59560
+ id: string;
59561
+ /** Format: uuid */
59562
+ lineage_id: string;
59563
+ /**
59564
+ * @description `native`: a Vitrina presupuesto. `mirror`: a Dentalink / Medilink plan copied by the espejo de ingresos — it is accepted in the vendor, so there is no Vitrina accept link.
59565
+ * @enum {string}
59566
+ */
59567
+ source: "native" | "mirror";
59568
+ /**
59569
+ * Format: uuid
59570
+ * @description Native rows only; null on a mirror row.
59571
+ */
59572
+ budget_id: string | null;
59573
+ /**
59574
+ * Format: uuid
59575
+ * @description Mirror rows only: the `clinic_mirror_plan` the row follows.
59576
+ */
59577
+ mirror_plan_id: string | null;
59578
+ /**
59579
+ * @description Mirror rows only: where the plan lives («Plan en Dentalink»).
59580
+ * @enum {string|null}
59581
+ */
59582
+ vendor: "dentalink" | "medilink" | null;
59583
+ /** @description The folio, or the vendor plan number on a mirror row. */
59584
+ budget_display_id: string | null;
59585
+ patient_name: string | null;
59586
+ phone: string | null;
59587
+ /** Format: uuid */
59588
+ contact_id: string | null;
59589
+ /** Format: uuid */
59590
+ clinic_patient_id: string | null;
59591
+ /** Format: uuid */
59592
+ conversation_id: string | null;
59593
+ /** @description The patient's Línea (#4008): its open lead's, else its latest. */
59594
+ linea: {
59595
+ /** Format: uuid */
59596
+ id: string;
59597
+ nombre: string;
59598
+ } | null;
59599
+ /** @description The patient's Origen (null only if the read failed). */
59600
+ origin: {
59601
+ /**
59602
+ * @description `meta`: came from a Meta ad. `organic`: a linked contact with no ad touch. `unknown` («Sin dato»): no contact is linked to the person, so no ad touch can be read.
59603
+ * @enum {string}
59604
+ */
59605
+ kind: "meta" | "organic" | "unknown";
59606
+ /** @description «Meta · Implantes octubre», «Meta» (ad name unknown), «Orgánico». */
59607
+ label: string;
59608
+ /** @description Meta only: the platform ad id (what `origin_ad_id` takes). */
59609
+ ad_id: string | null;
59610
+ ad_name: string | null;
59611
+ ad_set_name: string | null;
59612
+ campaign_name: string | null;
59613
+ /** @description Meta only: when they first wrote through the ad (or came back). */
59614
+ first_touch_at: string | null;
59615
+ /**
59616
+ * @description `first_message`: the first thing they wrote came from the ad. `returned`: they were already writing and came back through one. Both count as people from ads.
59617
+ * @enum {string|null}
59618
+ */
59619
+ via: "first_message" | "returned" | null;
59620
+ } | null;
59621
+ professional_name: string | null;
59622
+ /** @description The first three item labels. */
59623
+ items: string[];
59624
+ item_count: number;
59625
+ /** @description `null` (and `items` empty) for a caller without `clinic_money:read`. */
59626
+ total_clp: number | null;
59627
+ issued_on: string;
59628
+ /** @description «lunes 21 de septiembre» */
59629
+ issued_label: string;
59630
+ days_since_issue: number;
59631
+ /** @description The step that falls today: 1 = día siguiente, 2 = día 7, 3 = día 21. */
59632
+ step: 1 | 2 | 3 | null;
59633
+ /** @enum {string} */
59634
+ status: "active" | "stopped";
59635
+ /** @enum {string|null} */
59636
+ stop_reason: "accepted" | "rejected" | "voided" | "expired" | "cita_booked" | "call_accepted" | "call_declined" | "replied" | "opted_out" | "no_marketing_consent" | "ladder_done" | "plan_paid" | null;
59637
+ touches_sent: number;
59638
+ promised: boolean;
59639
+ outcome: {
59640
+ /** @enum {string} */
59641
+ value: "accepted" | "declined" | "thinking" | "no_answer";
59642
+ at: string;
59643
+ } | null;
59644
+ touch: {
59645
+ /** @enum {string} */
59646
+ state: "awaiting_approval" | "scheduled" | "sent" | "skipped" | "refused" | "expired" | "failed";
59647
+ /** Format: uuid */
59648
+ action_id: string | null;
59649
+ message: string | null;
59650
+ reason: string | null;
59651
+ } | null;
59652
+ }[];
59653
+ summary: {
59654
+ from: string;
59655
+ to: string;
59656
+ lineages: number;
59657
+ followed_up: number;
59658
+ never_contacted: number;
59659
+ in_progress: number;
59660
+ closed_after_followup: number;
59661
+ };
59662
+ };
59663
+ };
59664
+ };
59665
+ };
59666
+ /** @description Validation error */
59667
+ 400: {
59668
+ headers: {
59669
+ [name: string]: unknown;
59670
+ };
59671
+ content: {
59672
+ "application/json": components["schemas"]["Error"];
59673
+ };
59674
+ };
59675
+ /** @description Unauthorized */
59676
+ 401: {
59677
+ headers: {
59678
+ [name: string]: unknown;
59679
+ };
59680
+ content: {
59681
+ "application/json": components["schemas"]["Error"];
59682
+ };
59683
+ };
59684
+ /** @description Forbidden */
59685
+ 403: {
59686
+ headers: {
59687
+ [name: string]: unknown;
59688
+ };
59689
+ content: {
59690
+ "application/json": components["schemas"]["Error"];
59691
+ };
59692
+ };
59693
+ /** @description Not found */
59694
+ 404: {
59695
+ headers: {
59696
+ [name: string]: unknown;
59697
+ };
59698
+ content: {
59699
+ "application/json": components["schemas"]["Error"];
59700
+ };
59701
+ };
59702
+ /** @description Conflict (incl. Idempotency-Key reuse with different body) */
59703
+ 409: {
59704
+ headers: {
59705
+ [name: string]: unknown;
59706
+ };
59707
+ content: {
59708
+ "application/json": components["schemas"]["Error"];
59709
+ };
59710
+ };
59711
+ /** @description Rate limited */
59712
+ 429: {
59713
+ headers: {
59714
+ [name: string]: unknown;
59715
+ };
59716
+ content: {
59717
+ "application/json": components["schemas"]["Error"];
59718
+ };
59719
+ };
59720
+ };
59721
+ };
59722
+ put?: never;
59723
+ post?: never;
59724
+ delete?: never;
59725
+ options?: never;
59726
+ head?: never;
59727
+ patch?: never;
59728
+ trace?: never;
59729
+ };
59730
+ "/clinic/budget-followups/{id}/outcome": {
59731
+ parameters: {
59732
+ query?: never;
59733
+ header?: never;
59734
+ path?: never;
59735
+ cookie?: never;
59736
+ };
59737
+ get?: never;
59738
+ put?: never;
59739
+ /**
59740
+ * Log the outcome of a follow-up call
59741
+ * @description One tap of reception, ledgered against the lineage. `accepted` and `declined` stop the follow-up (and cancel a touch waiting for approval); `thinking` skips today's WhatsApp; `no_answer` lets it go out this afternoon. A later tap on the same day supersedes the earlier one — a stop tapped by mistake is undone by tapping another outcome. Never accepts the presupuesto itself. Requires `clinic:write`.
59742
+ *
59743
+ * **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`.
59744
+ */
59745
+ post: {
59746
+ parameters: {
59747
+ query?: never;
59748
+ header?: {
59749
+ /** @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. */
59750
+ "Idempotency-Key"?: string;
59751
+ };
59752
+ path: {
59753
+ id: string;
59754
+ };
59755
+ cookie?: never;
59756
+ };
59757
+ requestBody?: {
59758
+ content: {
59759
+ /**
59760
+ * @example {
59761
+ * "outcome": "thinking"
59762
+ * }
59763
+ */
59764
+ "application/json": {
59765
+ /**
59766
+ * @description `accepted` «Aceptó» and `declined` «No quiere» stop the follow-up; `thinking` «Lo va a pensar» skips today's WhatsApp; `no_answer` «No contestó» sends it this afternoon.
59767
+ * @enum {string}
59768
+ */
59769
+ outcome: "accepted" | "declined" | "thinking" | "no_answer";
59770
+ };
59771
+ };
59772
+ };
59773
+ responses: {
59774
+ /** @description The updated row, or null when it left the list */
59775
+ 200: {
59776
+ headers: {
59777
+ [name: string]: unknown;
59778
+ };
59779
+ content: {
59780
+ /**
59781
+ * @example {
59782
+ * "data": {
59783
+ * "id": "7b7b7b7b-0000-4000-8000-000000000001",
59784
+ * "lineage_id": "7c7c7c7c-0000-4000-8000-000000000001",
59785
+ * "source": "native",
59786
+ * "budget_id": "7d7d7d7d-0000-4000-8000-000000000001",
59787
+ * "mirror_plan_id": null,
59788
+ * "vendor": null,
59789
+ * "budget_display_id": "P-0412",
59790
+ * "patient_name": "María José Fuentes Lagos",
59791
+ * "phone": "+56960000001",
59792
+ * "contact_id": "1a73af9e-0000-4000-8000-000000000001",
59793
+ * "clinic_patient_id": "b1b0b8de-0000-4000-8000-000000000001",
59794
+ * "conversation_id": "c0c0c0c0-0000-4000-8000-000000000001",
59795
+ * "linea": {
59796
+ * "id": "d1d1d1d1-0000-4000-8000-000000000001",
59797
+ * "nombre": "Implantes"
59798
+ * },
59799
+ * "origin": {
59800
+ * "kind": "organic",
59801
+ * "label": "Orgánico",
59802
+ * "ad_id": null,
59803
+ * "ad_name": null,
59804
+ * "ad_set_name": null,
59805
+ * "campaign_name": null,
59806
+ * "first_touch_at": null,
59807
+ * "via": null
59808
+ * },
59809
+ * "professional_name": "Dra. Ana Soto",
59810
+ * "items": [
59811
+ * "Implante unitario"
59812
+ * ],
59813
+ * "item_count": 1,
59814
+ * "total_clp": 890000,
59815
+ * "issued_on": "2026-10-08",
59816
+ * "issued_label": "jueves 8 de octubre",
59817
+ * "days_since_issue": 1,
59818
+ * "step": 1,
59819
+ * "status": "active",
59820
+ * "stop_reason": null,
59821
+ * "touches_sent": 0,
59822
+ * "promised": false,
59823
+ * "outcome": {
59824
+ * "value": "thinking",
59825
+ * "at": "2026-10-09T14:12:03.000Z"
59826
+ * },
59827
+ * "touch": null
59828
+ * }
59829
+ * }
59830
+ */
59831
+ "application/json": {
59832
+ data: {
59833
+ /** Format: uuid */
59834
+ id: string;
59835
+ /** Format: uuid */
59836
+ lineage_id: string;
59837
+ /**
59838
+ * @description `native`: a Vitrina presupuesto. `mirror`: a Dentalink / Medilink plan copied by the espejo de ingresos — it is accepted in the vendor, so there is no Vitrina accept link.
59839
+ * @enum {string}
59840
+ */
59841
+ source: "native" | "mirror";
59842
+ /**
59843
+ * Format: uuid
59844
+ * @description Native rows only; null on a mirror row.
59845
+ */
59846
+ budget_id: string | null;
59847
+ /**
59848
+ * Format: uuid
59849
+ * @description Mirror rows only: the `clinic_mirror_plan` the row follows.
59850
+ */
59851
+ mirror_plan_id: string | null;
59852
+ /**
59853
+ * @description Mirror rows only: where the plan lives («Plan en Dentalink»).
59854
+ * @enum {string|null}
59855
+ */
59856
+ vendor: "dentalink" | "medilink" | null;
59857
+ /** @description The folio, or the vendor plan number on a mirror row. */
59858
+ budget_display_id: string | null;
59859
+ patient_name: string | null;
59860
+ phone: string | null;
59861
+ /** Format: uuid */
59862
+ contact_id: string | null;
59863
+ /** Format: uuid */
59864
+ clinic_patient_id: string | null;
59865
+ /** Format: uuid */
59866
+ conversation_id: string | null;
59867
+ /** @description The patient's Línea (#4008): its open lead's, else its latest. */
59868
+ linea: {
59869
+ /** Format: uuid */
59870
+ id: string;
59871
+ nombre: string;
59872
+ } | null;
59873
+ /** @description The patient's Origen (null only if the read failed). */
59874
+ origin: {
59875
+ /**
59876
+ * @description `meta`: came from a Meta ad. `organic`: a linked contact with no ad touch. `unknown` («Sin dato»): no contact is linked to the person, so no ad touch can be read.
59877
+ * @enum {string}
59878
+ */
59879
+ kind: "meta" | "organic" | "unknown";
59880
+ /** @description «Meta · Implantes octubre», «Meta» (ad name unknown), «Orgánico». */
59881
+ label: string;
59882
+ /** @description Meta only: the platform ad id (what `origin_ad_id` takes). */
59883
+ ad_id: string | null;
59884
+ ad_name: string | null;
59885
+ ad_set_name: string | null;
59886
+ campaign_name: string | null;
59887
+ /** @description Meta only: when they first wrote through the ad (or came back). */
59888
+ first_touch_at: string | null;
59889
+ /**
59890
+ * @description `first_message`: the first thing they wrote came from the ad. `returned`: they were already writing and came back through one. Both count as people from ads.
59891
+ * @enum {string|null}
59892
+ */
59893
+ via: "first_message" | "returned" | null;
59894
+ } | null;
59895
+ professional_name: string | null;
59896
+ /** @description The first three item labels. */
59897
+ items: string[];
59898
+ item_count: number;
59899
+ /** @description `null` (and `items` empty) for a caller without `clinic_money:read`. */
59900
+ total_clp: number | null;
59901
+ issued_on: string;
59902
+ /** @description «lunes 21 de septiembre» */
59903
+ issued_label: string;
59904
+ days_since_issue: number;
59905
+ /** @description The step that falls today: 1 = día siguiente, 2 = día 7, 3 = día 21. */
59906
+ step: 1 | 2 | 3 | null;
59907
+ /** @enum {string} */
59908
+ status: "active" | "stopped";
59909
+ /** @enum {string|null} */
59910
+ stop_reason: "accepted" | "rejected" | "voided" | "expired" | "cita_booked" | "call_accepted" | "call_declined" | "replied" | "opted_out" | "no_marketing_consent" | "ladder_done" | "plan_paid" | null;
59911
+ touches_sent: number;
59912
+ promised: boolean;
59913
+ outcome: {
59914
+ /** @enum {string} */
59915
+ value: "accepted" | "declined" | "thinking" | "no_answer";
59916
+ at: string;
59917
+ } | null;
59918
+ touch: {
59919
+ /** @enum {string} */
59920
+ state: "awaiting_approval" | "scheduled" | "sent" | "skipped" | "refused" | "expired" | "failed";
59921
+ /** Format: uuid */
59922
+ action_id: string | null;
59923
+ message: string | null;
59924
+ reason: string | null;
59925
+ } | null;
59926
+ } | null;
59927
+ };
59928
+ };
59929
+ };
59930
+ /** @description Validation error */
59931
+ 400: {
59932
+ headers: {
59933
+ [name: string]: unknown;
59934
+ };
59935
+ content: {
59936
+ "application/json": components["schemas"]["Error"];
59937
+ };
59938
+ };
59939
+ /** @description Unauthorized */
59940
+ 401: {
59941
+ headers: {
59942
+ [name: string]: unknown;
59943
+ };
59944
+ content: {
59945
+ "application/json": components["schemas"]["Error"];
59946
+ };
59947
+ };
59948
+ /** @description Forbidden */
59949
+ 403: {
59950
+ headers: {
59951
+ [name: string]: unknown;
59952
+ };
59953
+ content: {
59954
+ "application/json": components["schemas"]["Error"];
59955
+ };
59956
+ };
59957
+ /** @description Not found */
59958
+ 404: {
59959
+ headers: {
59960
+ [name: string]: unknown;
59961
+ };
59962
+ content: {
59963
+ "application/json": components["schemas"]["Error"];
59964
+ };
59965
+ };
59966
+ /** @description Conflict (incl. Idempotency-Key reuse with different body) */
59967
+ 409: {
59968
+ headers: {
59969
+ [name: string]: unknown;
59970
+ };
59971
+ content: {
59972
+ "application/json": components["schemas"]["Error"];
59973
+ };
59974
+ };
59975
+ /** @description Rate limited */
59976
+ 429: {
59977
+ headers: {
59978
+ [name: string]: unknown;
59979
+ };
59980
+ content: {
59981
+ "application/json": components["schemas"]["Error"];
59982
+ };
59983
+ };
59984
+ };
59985
+ };
59986
+ delete?: never;
59987
+ options?: never;
59988
+ head?: never;
59989
+ patch?: never;
59990
+ trace?: never;
59991
+ };
59992
+ "/clinic/budget-followups/sin-hora": {
59993
+ parameters: {
59994
+ query?: never;
59995
+ header?: never;
59996
+ path?: never;
59997
+ cookie?: never;
59998
+ };
59999
+ /**
60000
+ * «Llamar hoy» · «Sin hora»: open leads without a cita
60001
+ * @description Every OPEN lead whose contact has no cita that counts, however old (no age cut-off: dental and aesthetic demand is sporadic). A cita counts when it is confirmed or attended, or a hold still running; a cancelled or missed cita, past or future, does not — the lead is still waiting for a time. A lead leaves the list the moment a cita is booked. Sorted by call priority first (`priority` P1 to P4: P1 picked or asked for a time and was not booked, P2 wrote last and the clinic owes the reply, P3 interested, P4 cold), then lead `score` (highest first, unscored last), then the oldest last message first. Paged: `limit` (default 50, max 100) and `cursor` = the previous `next_cursor`; the cursor is a position, not an offset, so a lead leaving mid-scroll never skips another. `counts` covers the whole section, not the page: `total`, `logged` (an outcome tapped today) and `unlogged`. Each row carries what reception reads before dialling (`patient_name`, `phone`, `score`, `score_reason`, `last_message_at`) and today's `outcome`. `line` is the lead's Línea; `origin` is the lead's Origen (Meta with the ad, or Orgánico: the attribution Vitrina Ads reports). Narrow the section with `origin=meta|organic|unknown`, `origin_ad_id` and `linea_id` (a Línea id, or `none` for leads without one); `counts` follow the narrowing. Requires `clinic:read` and, for an API key or app, `clinic_patients:read`.
60002
+ *
60003
+ * **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`.
60004
+ */
60005
+ get: {
60006
+ parameters: {
60007
+ query?: {
60008
+ /** @description Rows per page (default 50, max 100). */
60009
+ limit?: number;
60010
+ /** @description `next_cursor` of the previous page. */
60011
+ cursor?: string;
60012
+ /** @description Origen of the patient: `meta` (came from a Meta ad), `organic` (a linked contact with no ad touch) or `unknown` («Sin dato»: no linked contact). Same attribution as Vitrina Ads' people from ads. */
60013
+ origin?: "meta" | "organic" | "unknown";
60014
+ /** @description Narrow `origin=meta` to one Meta ad (its platform ad id). */
60015
+ origin_ad_id?: string;
60016
+ /** @description Narrow to one Línea: a clinic línea id, or `none` for leads without one. `counts` follow the narrowing. */
60017
+ linea_id?: string | "none";
60018
+ };
60019
+ header?: never;
60020
+ path?: never;
60021
+ cookie?: never;
60022
+ };
60023
+ requestBody?: never;
60024
+ responses: {
60025
+ /** @description One page of the Sin hora list */
60026
+ 200: {
60027
+ headers: {
60028
+ [name: string]: unknown;
60029
+ };
60030
+ content: {
60031
+ /**
60032
+ * @example {
60033
+ * "data": {
60034
+ * "today": "2026-10-09",
60035
+ * "call_list": true,
60036
+ * "day_ends_at": "2026-10-09T22:00:00.000Z",
60037
+ * "counts": {
60038
+ * "total": 10,
60039
+ * "logged": 0,
60040
+ * "unlogged": 10,
60041
+ * "by_priority": {
60042
+ * "P1": 1,
60043
+ * "P2": 2,
60044
+ * "P3": 3,
60045
+ * "P4": 4
60046
+ * }
60047
+ * },
60048
+ * "rows": [
60049
+ * {
60050
+ * "priority": "P2",
60051
+ * "priority_fact": "wrote_last",
60052
+ * "priority_reason": "Escribió hace 3 h, le debemos respuesta",
60053
+ * "id": "5e5e5e5e-0000-4000-8000-000000000001",
60054
+ * "display_id": "L-89",
60055
+ * "contact_id": "1a73af9e-0000-4000-8000-000000000001",
60056
+ * "conversation_id": "c0c0c0c0-0000-4000-8000-000000000001",
60057
+ * "patient_name": "María José Fuentes Lagos",
60058
+ * "phone": "+56960000001",
60059
+ * "score": 72,
60060
+ * "score_reason": "Pidió precio de implantes y preguntó por horarios.",
60061
+ * "temperature": "warm",
60062
+ * "stage_name": "Nuevo",
60063
+ * "line": "Implantes",
60064
+ * "linea": {
60065
+ * "id": "d1d1d1d1-0000-4000-8000-000000000001",
60066
+ * "nombre": "Implantes",
60067
+ * "source": "keyword"
60068
+ * },
60069
+ * "origin": {
60070
+ * "kind": "organic",
60071
+ * "label": "Orgánico",
60072
+ * "ad_id": null,
60073
+ * "ad_name": null,
60074
+ * "ad_set_name": null,
60075
+ * "campaign_name": null,
60076
+ * "first_touch_at": null,
60077
+ * "via": null
60078
+ * },
60079
+ * "lead_created_at": "2026-09-20T15:02:11.000Z",
60080
+ * "last_message_at": "2026-10-09T13:40:00.000Z",
60081
+ * "days_since_last_message": 0,
60082
+ * "outcome": null,
60083
+ * "recontacto": null
60084
+ * }
60085
+ * ],
60086
+ * "next_cursor": null,
60087
+ * "viewer": {
60088
+ * "may_log": true,
60089
+ * "may_approve": true
60090
+ * }
60091
+ * }
60092
+ * }
60093
+ */
60094
+ "application/json": {
60095
+ data: {
60096
+ today: string;
60097
+ call_list: boolean;
60098
+ /** @description When the clinic's day ends today (its latest closing among today's Horarios, clinic clock): if no call outcome is logged by then, the held WhatsApp is proposed for approval. `null` on a day the clinic does not open. */
60099
+ day_ends_at: string | null;
60100
+ counts: {
60101
+ /** @description Leads in the section. */
60102
+ total: number;
60103
+ /** @description Of those, with an outcome logged today. */
60104
+ logged: number;
60105
+ /** @description «n sin registrar» */
60106
+ unlogged: number;
60107
+ /** @description Leads per Prioridad de llamada, whole section. */
60108
+ by_priority: {
60109
+ P1: number;
60110
+ P2: number;
60111
+ P3: number;
60112
+ P4: number;
60113
+ };
60114
+ };
60115
+ rows: {
60116
+ /**
60117
+ * @description Prioridad de llamada, from conversation facts (no model): P1 picked or asked for a time and was not booked; P2 wrote last, the clinic owes the reply; P3 interested (score > 25); P4 cold (score <= 25).
60118
+ * @enum {string}
60119
+ */
60120
+ priority: "P1" | "P2" | "P3" | "P4";
60121
+ /**
60122
+ * @description The fact that decided the tier.
60123
+ * @enum {string}
60124
+ */
60125
+ priority_fact: "picked_time" | "asked_time" | "wrote_last" | "offered_unanswered" | "interested" | "cold";
60126
+ /** @description That fact as a Spanish sentence, e.g. «Escribió hace 3 h, le debemos respuesta». */
60127
+ priority_reason: string;
60128
+ /**
60129
+ * Format: uuid
60130
+ * @description The lead's id.
60131
+ */
60132
+ id: string;
60133
+ /** @description «L-89». */
60134
+ display_id: string;
60135
+ /** Format: uuid */
60136
+ contact_id: string;
60137
+ /** Format: uuid */
60138
+ conversation_id: string | null;
60139
+ patient_name: string | null;
60140
+ phone: string | null;
60141
+ /** @description Lead score 0–100, highest first; `null` = never scored. */
60142
+ score: number | null;
60143
+ /** @description Why it scored what it did — the line read before dialling. */
60144
+ score_reason: string | null;
60145
+ temperature: string | null;
60146
+ stage_name: string | null;
60147
+ /** @description Nombre of the lead's Línea; `null` when it has none. */
60148
+ line: string | null;
60149
+ /** @description The lead's Línea as an object (id, nombre, and why it carries it); `null` when it has none. */
60150
+ linea: {
60151
+ /** Format: uuid */
60152
+ id: string;
60153
+ nombre: string;
60154
+ source: string | null;
60155
+ } | null;
60156
+ /** @description Origen of the lead (null only if the read failed). */
60157
+ origin: {
60158
+ /**
60159
+ * @description `meta`: came from a Meta ad. `organic`: a linked contact with no ad touch. `unknown` («Sin dato»): no contact is linked to the person, so no ad touch can be read.
60160
+ * @enum {string}
60161
+ */
60162
+ kind: "meta" | "organic" | "unknown";
60163
+ /** @description «Meta · Implantes octubre», «Meta» (ad name unknown), «Orgánico». */
60164
+ label: string;
60165
+ /** @description Meta only: the platform ad id (what `origin_ad_id` takes). */
60166
+ ad_id: string | null;
60167
+ ad_name: string | null;
60168
+ ad_set_name: string | null;
60169
+ campaign_name: string | null;
60170
+ /** @description Meta only: when they first wrote through the ad (or came back). */
60171
+ first_touch_at: string | null;
60172
+ /**
60173
+ * @description `first_message`: the first thing they wrote came from the ad. `returned`: they were already writing and came back through one. Both count as people from ads.
60174
+ * @enum {string|null}
60175
+ */
60176
+ via: "first_message" | "returned" | null;
60177
+ } | null;
60178
+ lead_created_at: string;
60179
+ last_message_at: string | null;
60180
+ days_since_last_message: number | null;
60181
+ /** @description Today's newest logged outcome. */
60182
+ outcome: {
60183
+ /** @enum {string} */
60184
+ value: "accepted" | "declined" | "thinking" | "no_answer";
60185
+ at: string;
60186
+ } | null;
60187
+ /** @description Where this patient's Recontacto stands today. `held`: the call goes first, no WhatsApp until the clinic's day ends. `proposed`: nobody logged a call, the WhatsApp waits for a person's approval. `null`: nothing pending. */
60188
+ recontacto: {
60189
+ /** @enum {string} */
60190
+ state: "held";
60191
+ } | {
60192
+ /** @enum {string} */
60193
+ state: "proposed";
60194
+ /**
60195
+ * Format: uuid
60196
+ * @description The outbound action waiting for approval: approve or discard it through the approvals (`/outbound/approvals`), like the inbox.
60197
+ */
60198
+ action_id: string;
60199
+ /** Format: uuid */
60200
+ attempt_id: string;
60201
+ /** @description The drafted WhatsApp. */
60202
+ message: string | null;
60203
+ /** @description Why it is proposed. */
60204
+ reason: string | null;
60205
+ proposed_at: string;
60206
+ } | null;
60207
+ }[];
60208
+ next_cursor: string | null;
60209
+ viewer: {
60210
+ may_log: boolean;
60211
+ /** @description Whether the caller may approve or discard a proposed WhatsApp (`recontacto.state = proposed`) through the approvals. */
60212
+ may_approve: boolean;
60213
+ };
60214
+ };
60215
+ };
60216
+ };
60217
+ };
60218
+ /** @description Validation error */
60219
+ 400: {
60220
+ headers: {
60221
+ [name: string]: unknown;
60222
+ };
60223
+ content: {
60224
+ "application/json": components["schemas"]["Error"];
60225
+ };
60226
+ };
60227
+ /** @description Unauthorized */
60228
+ 401: {
60229
+ headers: {
60230
+ [name: string]: unknown;
60231
+ };
60232
+ content: {
60233
+ "application/json": components["schemas"]["Error"];
60234
+ };
60235
+ };
60236
+ /** @description Forbidden */
60237
+ 403: {
60238
+ headers: {
60239
+ [name: string]: unknown;
60240
+ };
60241
+ content: {
60242
+ "application/json": components["schemas"]["Error"];
60243
+ };
60244
+ };
60245
+ /** @description Not found */
60246
+ 404: {
60247
+ headers: {
60248
+ [name: string]: unknown;
60249
+ };
60250
+ content: {
60251
+ "application/json": components["schemas"]["Error"];
60252
+ };
60253
+ };
60254
+ /** @description Conflict (incl. Idempotency-Key reuse with different body) */
60255
+ 409: {
60256
+ headers: {
60257
+ [name: string]: unknown;
60258
+ };
60259
+ content: {
60260
+ "application/json": components["schemas"]["Error"];
60261
+ };
60262
+ };
60263
+ /** @description Rate limited */
60264
+ 429: {
60265
+ headers: {
60266
+ [name: string]: unknown;
60267
+ };
60268
+ content: {
60269
+ "application/json": components["schemas"]["Error"];
60270
+ };
60271
+ };
60272
+ };
60273
+ };
60274
+ put?: never;
60275
+ post?: never;
60276
+ delete?: never;
60277
+ options?: never;
60278
+ head?: never;
60279
+ patch?: never;
60280
+ trace?: never;
60281
+ };
60282
+ "/clinic/budget-followups/sin-hora/{lead_id}/outcome": {
60283
+ parameters: {
60284
+ query?: never;
60285
+ header?: never;
60286
+ path?: never;
60287
+ cookie?: never;
60288
+ };
60289
+ get?: never;
60290
+ put?: never;
60291
+ /**
60292
+ * Log the outcome of a call to a Sin hora lead
60293
+ * @description The same four one-tap outcomes as the presupuesto rows (`accepted`, `declined`, `thinking`, `no_answer`), recorded on the lead's timeline as a `call_outcome` activity. A later tap the same clinic day supersedes the earlier (the timeline keeps both). It records the call and cancels the WhatsApp proposed for this patient, if any. `declined` («No quiere») also closes the lead as Perdido: it moves to its pipeline's lost stage with «No quiere» as the reason, leaves Sin hora and is never chased by Recontacto again; moving it back to a stage undoes it. Returns the updated row, or null when the lead left the section (a cita was booked, or «No quiere»). A pipeline with no lost stage refuses `declined`. 404 when the lead is not open. Requires `clinic:write`.
60294
+ *
60295
+ * **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`.
60296
+ */
60297
+ post: {
60298
+ parameters: {
60299
+ query?: never;
60300
+ header?: {
60301
+ /** @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. */
60302
+ "Idempotency-Key"?: string;
60303
+ };
60304
+ path: {
60305
+ lead_id: string;
60306
+ };
60307
+ cookie?: never;
60308
+ };
60309
+ requestBody?: {
60310
+ content: {
60311
+ /**
60312
+ * @example {
60313
+ * "outcome": "no_answer"
60314
+ * }
60315
+ */
60316
+ "application/json": {
60317
+ /**
60318
+ * @description `accepted` «Aceptó» and `declined` «No quiere» stop the follow-up; `thinking` «Lo va a pensar» skips today's WhatsApp; `no_answer` «No contestó» sends it this afternoon.
60319
+ * @enum {string}
60320
+ */
60321
+ outcome: "accepted" | "declined" | "thinking" | "no_answer";
60322
+ };
60323
+ };
60324
+ };
60325
+ responses: {
60326
+ /** @description The updated row, or null when it left the list */
60327
+ 200: {
60328
+ headers: {
60329
+ [name: string]: unknown;
60330
+ };
60331
+ content: {
60332
+ /**
60333
+ * @example {
60334
+ * "data": {
60335
+ * "priority": "P2",
60336
+ * "priority_fact": "wrote_last",
60337
+ * "priority_reason": "Escribió hace 3 h, le debemos respuesta",
60338
+ * "id": "5e5e5e5e-0000-4000-8000-000000000001",
60339
+ * "display_id": "L-89",
60340
+ * "contact_id": "1a73af9e-0000-4000-8000-000000000001",
60341
+ * "conversation_id": "c0c0c0c0-0000-4000-8000-000000000001",
60342
+ * "patient_name": "María José Fuentes Lagos",
60343
+ * "phone": "+56960000001",
60344
+ * "score": 72,
60345
+ * "score_reason": "Pidió precio de implantes y preguntó por horarios.",
60346
+ * "temperature": "warm",
60347
+ * "stage_name": "Nuevo",
60348
+ * "line": "Implantes",
60349
+ * "linea": {
60350
+ * "id": "d1d1d1d1-0000-4000-8000-000000000001",
60351
+ * "nombre": "Implantes",
60352
+ * "source": "keyword"
60353
+ * },
60354
+ * "origin": {
60355
+ * "kind": "organic",
60356
+ * "label": "Orgánico",
60357
+ * "ad_id": null,
60358
+ * "ad_name": null,
60359
+ * "ad_set_name": null,
60360
+ * "campaign_name": null,
60361
+ * "first_touch_at": null,
60362
+ * "via": null
60363
+ * },
60364
+ * "lead_created_at": "2026-09-20T15:02:11.000Z",
60365
+ * "last_message_at": "2026-10-09T13:40:00.000Z",
60366
+ * "days_since_last_message": 0,
60367
+ * "outcome": {
60368
+ * "value": "no_answer",
60369
+ * "at": "2026-10-09T14:12:03.000Z"
60370
+ * },
60371
+ * "recontacto": null
60372
+ * }
60373
+ * }
60374
+ */
60375
+ "application/json": {
60376
+ data: {
60377
+ /**
60378
+ * @description Prioridad de llamada, from conversation facts (no model): P1 picked or asked for a time and was not booked; P2 wrote last, the clinic owes the reply; P3 interested (score > 25); P4 cold (score <= 25).
60379
+ * @enum {string}
60380
+ */
60381
+ priority: "P1" | "P2" | "P3" | "P4";
60382
+ /**
60383
+ * @description The fact that decided the tier.
60384
+ * @enum {string}
60385
+ */
60386
+ priority_fact: "picked_time" | "asked_time" | "wrote_last" | "offered_unanswered" | "interested" | "cold";
60387
+ /** @description That fact as a Spanish sentence, e.g. «Escribió hace 3 h, le debemos respuesta». */
60388
+ priority_reason: string;
60389
+ /**
60390
+ * Format: uuid
60391
+ * @description The lead's id.
60392
+ */
60393
+ id: string;
60394
+ /** @description «L-89». */
60395
+ display_id: string;
60396
+ /** Format: uuid */
60397
+ contact_id: string;
60398
+ /** Format: uuid */
60399
+ conversation_id: string | null;
60400
+ patient_name: string | null;
60401
+ phone: string | null;
60402
+ /** @description Lead score 0–100, highest first; `null` = never scored. */
60403
+ score: number | null;
60404
+ /** @description Why it scored what it did — the line read before dialling. */
60405
+ score_reason: string | null;
60406
+ temperature: string | null;
60407
+ stage_name: string | null;
60408
+ /** @description Nombre of the lead's Línea; `null` when it has none. */
60409
+ line: string | null;
60410
+ /** @description The lead's Línea as an object (id, nombre, and why it carries it); `null` when it has none. */
60411
+ linea: {
60412
+ /** Format: uuid */
60413
+ id: string;
60414
+ nombre: string;
60415
+ source: string | null;
60416
+ } | null;
60417
+ /** @description Origen of the lead (null only if the read failed). */
60418
+ origin: {
60419
+ /**
60420
+ * @description `meta`: came from a Meta ad. `organic`: a linked contact with no ad touch. `unknown` («Sin dato»): no contact is linked to the person, so no ad touch can be read.
60421
+ * @enum {string}
60422
+ */
60423
+ kind: "meta" | "organic" | "unknown";
60424
+ /** @description «Meta · Implantes octubre», «Meta» (ad name unknown), «Orgánico». */
60425
+ label: string;
60426
+ /** @description Meta only: the platform ad id (what `origin_ad_id` takes). */
60427
+ ad_id: string | null;
60428
+ ad_name: string | null;
60429
+ ad_set_name: string | null;
60430
+ campaign_name: string | null;
60431
+ /** @description Meta only: when they first wrote through the ad (or came back). */
60432
+ first_touch_at: string | null;
60433
+ /**
60434
+ * @description `first_message`: the first thing they wrote came from the ad. `returned`: they were already writing and came back through one. Both count as people from ads.
60435
+ * @enum {string|null}
60436
+ */
60437
+ via: "first_message" | "returned" | null;
60438
+ } | null;
60439
+ lead_created_at: string;
60440
+ last_message_at: string | null;
60441
+ days_since_last_message: number | null;
60442
+ /** @description Today's newest logged outcome. */
60443
+ outcome: {
60444
+ /** @enum {string} */
60445
+ value: "accepted" | "declined" | "thinking" | "no_answer";
60446
+ at: string;
60447
+ } | null;
60448
+ /** @description Where this patient's Recontacto stands today. `held`: the call goes first, no WhatsApp until the clinic's day ends. `proposed`: nobody logged a call, the WhatsApp waits for a person's approval. `null`: nothing pending. */
60449
+ recontacto: {
60450
+ /** @enum {string} */
60451
+ state: "held";
60452
+ } | {
60453
+ /** @enum {string} */
60454
+ state: "proposed";
60455
+ /**
60456
+ * Format: uuid
60457
+ * @description The outbound action waiting for approval: approve or discard it through the approvals (`/outbound/approvals`), like the inbox.
60458
+ */
60459
+ action_id: string;
60460
+ /** Format: uuid */
60461
+ attempt_id: string;
60462
+ /** @description The drafted WhatsApp. */
60463
+ message: string | null;
60464
+ /** @description Why it is proposed. */
60465
+ reason: string | null;
60466
+ proposed_at: string;
60467
+ } | null;
60468
+ } | null;
60469
+ };
60470
+ };
60471
+ };
60472
+ /** @description Validation error */
60473
+ 400: {
60474
+ headers: {
60475
+ [name: string]: unknown;
60476
+ };
60477
+ content: {
60478
+ "application/json": components["schemas"]["Error"];
60479
+ };
60480
+ };
60481
+ /** @description Unauthorized */
60482
+ 401: {
60483
+ headers: {
60484
+ [name: string]: unknown;
60485
+ };
60486
+ content: {
60487
+ "application/json": components["schemas"]["Error"];
60488
+ };
60489
+ };
60490
+ /** @description Forbidden */
60491
+ 403: {
60492
+ headers: {
60493
+ [name: string]: unknown;
60494
+ };
60495
+ content: {
60496
+ "application/json": components["schemas"]["Error"];
60497
+ };
60498
+ };
60499
+ /** @description Not found */
60500
+ 404: {
60501
+ headers: {
60502
+ [name: string]: unknown;
60503
+ };
60504
+ content: {
60505
+ "application/json": components["schemas"]["Error"];
60506
+ };
60507
+ };
60508
+ /** @description Conflict (incl. Idempotency-Key reuse with different body) */
60509
+ 409: {
60510
+ headers: {
60511
+ [name: string]: unknown;
60512
+ };
60513
+ content: {
60514
+ "application/json": components["schemas"]["Error"];
60515
+ };
60516
+ };
60517
+ /** @description Rate limited */
60518
+ 429: {
60519
+ headers: {
60520
+ [name: string]: unknown;
60521
+ };
60522
+ content: {
60523
+ "application/json": components["schemas"]["Error"];
60524
+ };
60525
+ };
60526
+ };
60527
+ };
60528
+ delete?: never;
60529
+ options?: never;
60530
+ head?: never;
60531
+ patch?: never;
60532
+ trace?: never;
60533
+ };
59392
60534
  "/clinic/lineas": {
59393
60535
  parameters: {
59394
60536
  query?: never;
@@ -85619,61 +86761,569 @@ interface paths {
85619
86761
  patch?: never;
85620
86762
  trace?: never;
85621
86763
  };
85622
- "/campaigns/{id}/test-send": {
86764
+ "/campaigns/{id}/test-send": {
86765
+ parameters: {
86766
+ query?: never;
86767
+ header?: never;
86768
+ path?: never;
86769
+ cookie?: never;
86770
+ };
86771
+ get?: never;
86772
+ put?: never;
86773
+ /**
86774
+ * Send a test of the campaign to up to five people
86775
+ * @description Sends the campaign, exactly as it will leave, to 1–5 recipients typed by hand — `phone` for a WhatsApp campaign, `email` for an email one — and answers one result per recipient: `sent` (with `provider_message_id`) or `failed` (with `error_code`). It writes NO recipient rows and does not touch the campaign’s status or counters, so it is safe on a scheduled, paused or sent campaign and leaves no trace in the stats; a `sending` or `cancelled` campaign answers 409 `campaign_not_testable`.
86776
+ *
86777
+ * **Readiness** is the real send’s, minus the audience: a WhatsApp test needs the template APPROVED on the campaign’s connected account and a resolvable media header (400 `header_media_missing` otherwise); an email test needs content and a verified sending domain.
86778
+ *
86779
+ * **Values.** Per template slot: `variables[slot]` when typed → else the campaign mapping read with the typed recipient as the contact (a `contact_field` slot gets the typed `name` / `phone` / `email`, else the mapping’s `fallback`; a `static` slot its text) → else the template’s example. A slot still empty refuses that recipient (`missing_variables`). The media header is always the campaign’s own (`template_params.header_media`) else the template default — never a test variable. Each result echoes `values` with the `source` of each.
86780
+ *
86781
+ * **WhatsApp tests are conversations.** The number’s contact and its active thread on the campaign’s account are found or created, and the message is stored there (`metadata.campaign_test: true`), so a reply reaches the AI agent with the campaign in context — and is never attributed to the campaign’s metrics. `conversation_id` points at it.
86782
+ *
86783
+ * **Refusals per recipient:** `invalid_phone`, `landline`, `invalid_email`, `duplicate`, `opted_out` (with `opt_out_reason`), `contact_blocked`, `safety_hold`, `missing_variables`, `email_not_verified` (SES sandbox: only verified addresses), `send_failed` (with `provider_code`).
86784
+ *
86785
+ * **Rate limit:** 20 test messages per campaign and 60 per workspace per hour, counted for the recipients actually sent to; past it the whole request is refused with 429 `RATE_LIMITED` (`details.retry_after_s`).
86786
+ */
86787
+ post: {
86788
+ parameters: {
86789
+ query?: never;
86790
+ header?: {
86791
+ /** @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. */
86792
+ "Idempotency-Key"?: string;
86793
+ };
86794
+ path: {
86795
+ id: string;
86796
+ };
86797
+ cookie?: never;
86798
+ };
86799
+ requestBody?: {
86800
+ content: {
86801
+ /**
86802
+ * @example {
86803
+ * "recipients": [
86804
+ * {
86805
+ * "phone": "+56 9 1234 5678",
86806
+ * "name": "María González",
86807
+ * "variables": {
86808
+ * "1": "María"
86809
+ * }
86810
+ * }
86811
+ * ]
86812
+ * }
86813
+ */
86814
+ "application/json": components["schemas"]["CampaignTestSendBody"];
86815
+ };
86816
+ };
86817
+ responses: {
86818
+ /** @description One result per recipient */
86819
+ 200: {
86820
+ headers: {
86821
+ [name: string]: unknown;
86822
+ };
86823
+ content: {
86824
+ /**
86825
+ * @example {
86826
+ * "data": {
86827
+ * "campaign_id": "7a7a7a7a-0000-4000-8000-0000000000c1",
86828
+ * "channel": "whatsapp",
86829
+ * "header_media_url": null,
86830
+ * "remaining_this_hour": 4,
86831
+ * "results": [
86832
+ * {
86833
+ * "phone": "+56912345678",
86834
+ * "name": "María González",
86835
+ * "status": "sent",
86836
+ * "conversation_id": "7a7a7a7a-0000-4000-8000-000000000201",
86837
+ * "contact_id": "7a7a7a7a-0000-4000-8000-0000000000f1",
86838
+ * "rendered_text": "Hola María, tu próximo control es el 15 de noviembre.",
86839
+ * "values": {
86840
+ * "1": {
86841
+ * "value": "María",
86842
+ * "source": "variable"
86843
+ * },
86844
+ * "2": {
86845
+ * "value": "15 de noviembre",
86846
+ * "source": "static"
86847
+ * }
86848
+ * }
86849
+ * }
86850
+ * ]
86851
+ * }
86852
+ * }
86853
+ */
86854
+ "application/json": {
86855
+ data: components["schemas"]["CampaignTestSendResult"];
86856
+ };
86857
+ };
86858
+ };
86859
+ /** @description Validation error */
86860
+ 400: {
86861
+ headers: {
86862
+ [name: string]: unknown;
86863
+ };
86864
+ content: {
86865
+ "application/json": components["schemas"]["Error"];
86866
+ };
86867
+ };
86868
+ /** @description Unauthorized */
86869
+ 401: {
86870
+ headers: {
86871
+ [name: string]: unknown;
86872
+ };
86873
+ content: {
86874
+ "application/json": components["schemas"]["Error"];
86875
+ };
86876
+ };
86877
+ /** @description Forbidden */
86878
+ 403: {
86879
+ headers: {
86880
+ [name: string]: unknown;
86881
+ };
86882
+ content: {
86883
+ "application/json": components["schemas"]["Error"];
86884
+ };
86885
+ };
86886
+ /** @description Not found */
86887
+ 404: {
86888
+ headers: {
86889
+ [name: string]: unknown;
86890
+ };
86891
+ content: {
86892
+ "application/json": components["schemas"]["Error"];
86893
+ };
86894
+ };
86895
+ /** @description Conflict (incl. Idempotency-Key reuse with different body) */
86896
+ 409: {
86897
+ headers: {
86898
+ [name: string]: unknown;
86899
+ };
86900
+ content: {
86901
+ "application/json": components["schemas"]["Error"];
86902
+ };
86903
+ };
86904
+ /** @description Rate limited */
86905
+ 429: {
86906
+ headers: {
86907
+ [name: string]: unknown;
86908
+ };
86909
+ content: {
86910
+ "application/json": components["schemas"]["Error"];
86911
+ };
86912
+ };
86913
+ };
86914
+ };
86915
+ delete?: never;
86916
+ options?: never;
86917
+ head?: never;
86918
+ patch?: never;
86919
+ trace?: never;
86920
+ };
86921
+ "/clinic/agenda/feed": {
86922
+ parameters: {
86923
+ query?: never;
86924
+ header?: never;
86925
+ path?: never;
86926
+ cookie?: never;
86927
+ };
86928
+ /**
86929
+ * Agenda for a window, any engine, by professional and branch
86930
+ * @description Mirrored AND native appointments between `from` (inclusive) and `to` (EXCLUSIVE), bare `YYYY-MM-DD` days in the clinic's timezone; the window may not exceed 8 days. Cancelled citas are INCLUDED (the grid draws them muted). Unlike `GET /clinic/agenda`, `professional_id` and `location_id` are VITRINA uuids — `clinic_professional.id` and `location.id` — so one query serves a Medilink, a Reservo and a native clinic. Each row still carries the vendor's own ids under `vendor` for provenance: a row with `vendor.professional_id` set and `professional: null` is a roster gap, never a cita nobody is attending. Never calls the clinic API. `flags` are the clinical alerts the clinic shows on the block: clinical-record content, returned to an API key or personal token only when it also holds `clinic_record:read`. `booking_kind` is the cita's Tipo de agendamiento: `new` (Nuevo: it would be a primera visita and the patient never booked before), `new_rebooked` (Nuevo · reagenda: a primera visita, but the patient had earlier bookings and never came) or `in_treatment` (En tratamiento: every other cita); null on a row that is not a patient's cita. It uses the primera-visita rule Vitrina Ads counts, so a patient back after 12 months without an attended cita is `new`. `origin` is the patient's Origen (`kind` `meta` with the ad, ad set and campaign names, or `organic`) — the attribution Vitrina Ads reports, one definition; null on a row that is not a patient's cita. Filter with `origin=meta|organic|unknown` and, for Meta, `origin_ad_id` (a Meta ad id): rows that are not a patient's cita never match. `is_diagnosis` marks a diagnosis cita — a primera visita (`booking_kind` `new` / `new_rebooked`), an entry prestación the clinic confirmed, or a cita the clinic named as an evaluación / diagnóstico (the prestación, or the vendor's tratamiento label) unless its prestación is confirmed not to be an entry; `diagnosis=true` returns only those. `booked_via` says how the cita was booked: `agent` (Vitrina's AI agent, with the conversation `channel`), `online` (the patient on a booking page), `whatsapp` (a person booked it within 48 h of a WhatsApp exchange with the patient), `staff` (a team member in Vitrina, `by`), `vendor` (keyed straight into the clinic's system) or `calendar`; `booked_at` is the vendor's own creation time when known. `patient_paid` is what the patient paid on their plans, all time (espejo de ingresos abonos plus presupuesto allocations), returned only to a caller holding `clinic_money:read`.
86931
+ *
86932
+ * **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`.
86933
+ */
86934
+ get: {
86935
+ parameters: {
86936
+ query: {
86937
+ from: string;
86938
+ to: string;
86939
+ professional_id?: string;
86940
+ location_id?: string;
86941
+ resource_id?: string;
86942
+ status?: ("pending_hold" | "confirmed" | "cancelled" | "completed" | "no_show")[];
86943
+ kind?: ("test_drive" | "external" | "block" | "clinic")[];
86944
+ /** @description Origen of the patient: `meta` (came from a Meta ad), `organic` (a linked contact with no ad touch) or `unknown` («Sin dato»: no linked contact). Same attribution as Vitrina Ads' people from ads. */
86945
+ origin?: "meta" | "organic" | "unknown";
86946
+ /** @description Narrow `origin=meta` to one Meta ad (its platform ad id). */
86947
+ origin_ad_id?: string;
86948
+ diagnosis?: "true" | "false";
86949
+ };
86950
+ header?: never;
86951
+ path?: never;
86952
+ cookie?: never;
86953
+ };
86954
+ requestBody?: never;
86955
+ responses: {
86956
+ /** @description Agenda feed */
86957
+ 200: {
86958
+ headers: {
86959
+ [name: string]: unknown;
86960
+ };
86961
+ content: {
86962
+ /**
86963
+ * @example {
86964
+ * "data": [
86965
+ * {
86966
+ * "id": "eeeeeeee-0000-4000-8000-000000000001",
86967
+ * "display_id": "A-1",
86968
+ * "engine": "native",
86969
+ * "external_id": null,
86970
+ * "kind": "clinic",
86971
+ * "status": "confirmed",
86972
+ * "clinic_status_id": null,
86973
+ * "status_label": null,
86974
+ * "status_color": null,
86975
+ * "starts_at": "2026-09-23T13:30:00.000Z",
86976
+ * "ends_at": "2026-09-23T14:00:00.000Z",
86977
+ * "arrival_state": "scheduled",
86978
+ * "professional": {
86979
+ * "id": "ffffffff-0000-4000-8000-000000000001",
86980
+ * "name": "Ana"
86981
+ * },
86982
+ * "location": {
86983
+ * "id": "b1b1b1b1-0000-4000-8000-000000000002",
86984
+ * "name": "Sucursal Maipú"
86985
+ * },
86986
+ * "resource": null,
86987
+ * "service": null,
86988
+ * "patient": {
86989
+ * "id": "12121212-0000-4000-8000-000000000001",
86990
+ * "name": "María José Fuentes Lagos",
86991
+ * "external_id": null
86992
+ * },
86993
+ * "contact": {
86994
+ * "id": "22222222-0000-4000-8000-000000000001",
86995
+ * "name": "María José Fuentes Lagos"
86996
+ * },
86997
+ * "customer_name": "María José Fuentes Lagos",
86998
+ * "notes": "Avisar al +56987654321 si se atrasa",
86999
+ * "money": null,
87000
+ * "booking_kind": "new",
87001
+ * "is_diagnosis": true,
87002
+ * "booked_via": {
87003
+ * "kind": "agent",
87004
+ * "channel": "whatsapp",
87005
+ * "by": null,
87006
+ * "booked_at": "2026-09-21T18:04:00.000Z"
87007
+ * },
87008
+ * "patient_paid": 250000,
87009
+ * "flags": [
87010
+ * {
87011
+ * "id": "13131313-0000-4000-8000-000000000001",
87012
+ * "kind": "alergia",
87013
+ * "label": "Alergia a penicilina",
87014
+ * "severity": "severa"
87015
+ * }
87016
+ * ],
87017
+ * "vendor": {
87018
+ * "professional_id": null,
87019
+ * "professional_name": null,
87020
+ * "sucursal_id": null,
87021
+ * "agenda_id": null,
87022
+ * "sillon": null,
87023
+ * "status_code": null
87024
+ * }
87025
+ * }
87026
+ * ]
87027
+ * }
87028
+ */
87029
+ "application/json": {
87030
+ data?: unknown;
87031
+ meta?: {
87032
+ [key: string]: unknown;
87033
+ };
87034
+ };
87035
+ };
87036
+ };
87037
+ /** @description Validation error */
87038
+ 400: {
87039
+ headers: {
87040
+ [name: string]: unknown;
87041
+ };
87042
+ content: {
87043
+ "application/json": components["schemas"]["Error"];
87044
+ };
87045
+ };
87046
+ /** @description Unauthorized */
87047
+ 401: {
87048
+ headers: {
87049
+ [name: string]: unknown;
87050
+ };
87051
+ content: {
87052
+ "application/json": components["schemas"]["Error"];
87053
+ };
87054
+ };
87055
+ /** @description Forbidden */
87056
+ 403: {
87057
+ headers: {
87058
+ [name: string]: unknown;
87059
+ };
87060
+ content: {
87061
+ "application/json": components["schemas"]["Error"];
87062
+ };
87063
+ };
87064
+ /** @description Not found */
87065
+ 404: {
87066
+ headers: {
87067
+ [name: string]: unknown;
87068
+ };
87069
+ content: {
87070
+ "application/json": components["schemas"]["Error"];
87071
+ };
87072
+ };
87073
+ /** @description Conflict (incl. Idempotency-Key reuse with different body) */
87074
+ 409: {
87075
+ headers: {
87076
+ [name: string]: unknown;
87077
+ };
87078
+ content: {
87079
+ "application/json": components["schemas"]["Error"];
87080
+ };
87081
+ };
87082
+ /** @description Rate limited */
87083
+ 429: {
87084
+ headers: {
87085
+ [name: string]: unknown;
87086
+ };
87087
+ content: {
87088
+ "application/json": components["schemas"]["Error"];
87089
+ };
87090
+ };
87091
+ };
87092
+ };
87093
+ put?: never;
87094
+ post?: never;
87095
+ delete?: never;
87096
+ options?: never;
87097
+ head?: never;
87098
+ patch?: never;
87099
+ trace?: never;
87100
+ };
87101
+ "/clinic/agenda/diagnosticos": {
87102
+ parameters: {
87103
+ query?: never;
87104
+ header?: never;
87105
+ path?: never;
87106
+ cookie?: never;
87107
+ };
87108
+ /**
87109
+ * Diagnósticos: one day of diagnosis citas with plans and abono
87110
+ * @description The closing routine's worklist. `date` (`YYYY-MM-DD`, the clinic's clock; default today) returns that day's diagnosis citas in time order — the same `is_diagnosis` rule as `GET /clinic/agenda/feed` — each with its attendance (`attended`, `no_show`, `cancelled`, `booked`), professional, Tipo de agendamiento and Origen, and the patient's plans of the last 12 months: from the espejo de ingresos (the vendor's plan total and abonado) for a Dentalink clinic, from its presupuestos (total and what was allocated to them) for a native one; plans of $0 are not plans. At most 3 plans per patient are listed, `plans_count`, `plan_total` and `paid_total` cover all of them. `summary` counts the day (cancelled citas are counted apart and never as Meta) and `upcoming` the next 5 working days (a Sunday appears only when it holds a diagnosis cita). Plans are money: without `clinic_money:read` every `money` is null and `money_withheld` is true; `plan_created_on_day` stays. Never calls the clinic API.
87111
+ *
87112
+ * **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`.
87113
+ */
87114
+ get: {
87115
+ parameters: {
87116
+ query?: {
87117
+ date?: string;
87118
+ location_id?: string;
87119
+ professional_id?: string;
87120
+ };
87121
+ header?: never;
87122
+ path?: never;
87123
+ cookie?: never;
87124
+ };
87125
+ requestBody?: never;
87126
+ responses: {
87127
+ /** @description Diagnosis day */
87128
+ 200: {
87129
+ headers: {
87130
+ [name: string]: unknown;
87131
+ };
87132
+ content: {
87133
+ /**
87134
+ * @example {
87135
+ * "data": {
87136
+ * "date": "2026-09-23",
87137
+ * "summary": {
87138
+ * "total": 1,
87139
+ * "attended": 1,
87140
+ * "no_show": 0,
87141
+ * "cancelled": 0,
87142
+ * "meta": 0,
87143
+ * "plan_created_on_day": 1
87144
+ * },
87145
+ * "rows": [
87146
+ * {
87147
+ * "appointment_id": "eeeeeeee-0000-4000-8000-000000000001",
87148
+ * "display_id": "A-1",
87149
+ * "starts_at": "2026-09-23T13:30:00.000Z",
87150
+ * "ends_at": "2026-09-23T14:00:00.000Z",
87151
+ * "status": "completed",
87152
+ * "status_label": "Atendido",
87153
+ * "attendance": "attended",
87154
+ * "patient": {
87155
+ * "id": "12121212-0000-4000-8000-000000000001",
87156
+ * "name": "María José Fuentes Lagos",
87157
+ * "external_id": null
87158
+ * },
87159
+ * "professional": {
87160
+ * "id": "ffffffff-0000-4000-8000-000000000001",
87161
+ * "name": "Ana"
87162
+ * },
87163
+ * "service": null,
87164
+ * "location": {
87165
+ * "id": "b1b1b1b1-0000-4000-8000-000000000002",
87166
+ * "name": "Sucursal Maipú"
87167
+ * },
87168
+ * "booking_kind": "new",
87169
+ * "origin": {
87170
+ * "kind": "organic",
87171
+ * "label": "Orgánico",
87172
+ * "ad_id": null,
87173
+ * "ad_name": null,
87174
+ * "ad_set_name": null,
87175
+ * "campaign_name": null,
87176
+ * "first_touch_at": null,
87177
+ * "via": null
87178
+ * },
87179
+ * "external_id": null,
87180
+ * "engine": "native",
87181
+ * "money": {
87182
+ * "plans": [
87183
+ * {
87184
+ * "source": "native",
87185
+ * "id": "eeeeeeee-0000-4000-8000-000000000002",
87186
+ * "label": "E-12",
87187
+ * "day": "2026-09-23",
87188
+ * "total": 1250000,
87189
+ * "paid": 250000,
87190
+ * "status": "accepted",
87191
+ * "professional_name": "Ana Rojas",
87192
+ * "created_on_day": true
87193
+ * }
87194
+ * ],
87195
+ * "plans_count": 1,
87196
+ * "plan_total": 1250000,
87197
+ * "paid_total": 250000
87198
+ * },
87199
+ * "plan_created_on_day": true
87200
+ * }
87201
+ * ],
87202
+ * "upcoming": [
87203
+ * {
87204
+ * "date": "2026-09-24",
87205
+ * "count": 4
87206
+ * },
87207
+ * {
87208
+ * "date": "2026-09-25",
87209
+ * "count": 3
87210
+ * },
87211
+ * {
87212
+ * "date": "2026-09-26",
87213
+ * "count": 1
87214
+ * },
87215
+ * {
87216
+ * "date": "2026-09-28",
87217
+ * "count": 5
87218
+ * },
87219
+ * {
87220
+ * "date": "2026-09-29",
87221
+ * "count": 2
87222
+ * }
87223
+ * ],
87224
+ * "money_withheld": false
87225
+ * }
87226
+ * }
87227
+ */
87228
+ "application/json": {
87229
+ data?: unknown;
87230
+ meta?: {
87231
+ [key: string]: unknown;
87232
+ };
87233
+ };
87234
+ };
87235
+ };
87236
+ /** @description Validation error */
87237
+ 400: {
87238
+ headers: {
87239
+ [name: string]: unknown;
87240
+ };
87241
+ content: {
87242
+ "application/json": components["schemas"]["Error"];
87243
+ };
87244
+ };
87245
+ /** @description Unauthorized */
87246
+ 401: {
87247
+ headers: {
87248
+ [name: string]: unknown;
87249
+ };
87250
+ content: {
87251
+ "application/json": components["schemas"]["Error"];
87252
+ };
87253
+ };
87254
+ /** @description Forbidden */
87255
+ 403: {
87256
+ headers: {
87257
+ [name: string]: unknown;
87258
+ };
87259
+ content: {
87260
+ "application/json": components["schemas"]["Error"];
87261
+ };
87262
+ };
87263
+ /** @description Not found */
87264
+ 404: {
87265
+ headers: {
87266
+ [name: string]: unknown;
87267
+ };
87268
+ content: {
87269
+ "application/json": components["schemas"]["Error"];
87270
+ };
87271
+ };
87272
+ /** @description Conflict (incl. Idempotency-Key reuse with different body) */
87273
+ 409: {
87274
+ headers: {
87275
+ [name: string]: unknown;
87276
+ };
87277
+ content: {
87278
+ "application/json": components["schemas"]["Error"];
87279
+ };
87280
+ };
87281
+ /** @description Rate limited */
87282
+ 429: {
87283
+ headers: {
87284
+ [name: string]: unknown;
87285
+ };
87286
+ content: {
87287
+ "application/json": components["schemas"]["Error"];
87288
+ };
87289
+ };
87290
+ };
87291
+ };
87292
+ put?: never;
87293
+ post?: never;
87294
+ delete?: never;
87295
+ options?: never;
87296
+ head?: never;
87297
+ patch?: never;
87298
+ trace?: never;
87299
+ };
87300
+ "/clinic/agenda/month": {
85623
87301
  parameters: {
85624
87302
  query?: never;
85625
87303
  header?: never;
85626
87304
  path?: never;
85627
87305
  cookie?: never;
85628
87306
  };
85629
- get?: never;
85630
- put?: never;
85631
87307
  /**
85632
- * Send a test of the campaign to up to five people
85633
- * @description Sends the campaign, exactly as it will leave, to 1–5 recipients typed by hand — `phone` for a WhatsApp campaign, `email` for an email one — and answers one result per recipient: `sent` (with `provider_message_id`) or `failed` (with `error_code`). It writes NO recipient rows and does not touch the campaign’s status or counters, so it is safe on a scheduled, paused or sent campaign and leaves no trace in the stats; a `sending` or `cancelled` campaign answers 409 `campaign_not_testable`.
87308
+ * Agenda month: citas per day, by Origen
87309
+ * @description Counts per day of one `month` (`YYYY-MM`, the clinic's clock), read in one pass: `citas` (patients' citas that still stand), split by Origen (`meta`, `organic`, `unknown` — the same resolver as the feed and Vitrina Ads), `diagnosis` (diagnosis citas among them, the feed's `is_diagnosis` rule) and `cancelled` (anuladas, counted apart). Blocks and the workspace's own events never count. Optional `location_id` / `professional_id` (Vitrina uuids). Never calls the clinic API.
85634
87310
  *
85635
- * **Readiness** is the real send’s, minus the audience: a WhatsApp test needs the template APPROVED on the campaign’s connected account and a resolvable media header (400 `header_media_missing` otherwise); an email test needs content and a verified sending domain.
85636
- *
85637
- * **Values.** Per template slot: `variables[slot]` when typed → else the campaign mapping read with the typed recipient as the contact (a `contact_field` slot gets the typed `name` / `phone` / `email`, else the mapping’s `fallback`; a `static` slot its text) → else the template’s example. A slot still empty refuses that recipient (`missing_variables`). The media header is always the campaign’s own (`template_params.header_media`) else the template default — never a test variable. Each result echoes `values` with the `source` of each.
85638
- *
85639
- * **WhatsApp tests are conversations.** The number’s contact and its active thread on the campaign’s account are found or created, and the message is stored there (`metadata.campaign_test: true`), so a reply reaches the AI agent with the campaign in context — and is never attributed to the campaign’s metrics. `conversation_id` points at it.
85640
- *
85641
- * **Refusals per recipient:** `invalid_phone`, `landline`, `invalid_email`, `duplicate`, `opted_out` (with `opt_out_reason`), `contact_blocked`, `safety_hold`, `missing_variables`, `email_not_verified` (SES sandbox: only verified addresses), `send_failed` (with `provider_code`).
85642
- *
85643
- * **Rate limit:** 20 test messages per campaign and 60 per workspace per hour, counted for the recipients actually sent to; past it the whole request is refused with 429 `RATE_LIMITED` (`details.retry_after_s`).
87311
+ * **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`.
85644
87312
  */
85645
- post: {
87313
+ get: {
85646
87314
  parameters: {
85647
- query?: never;
85648
- header?: {
85649
- /** @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. */
85650
- "Idempotency-Key"?: string;
85651
- };
85652
- path: {
85653
- id: string;
87315
+ query: {
87316
+ month: string;
87317
+ location_id?: string;
87318
+ professional_id?: string;
85654
87319
  };
87320
+ header?: never;
87321
+ path?: never;
85655
87322
  cookie?: never;
85656
87323
  };
85657
- requestBody?: {
85658
- content: {
85659
- /**
85660
- * @example {
85661
- * "recipients": [
85662
- * {
85663
- * "phone": "+56 9 1234 5678",
85664
- * "name": "María González",
85665
- * "variables": {
85666
- * "1": "María"
85667
- * }
85668
- * }
85669
- * ]
85670
- * }
85671
- */
85672
- "application/json": components["schemas"]["CampaignTestSendBody"];
85673
- };
85674
- };
87324
+ requestBody?: never;
85675
87325
  responses: {
85676
- /** @description One result per recipient */
87326
+ /** @description Agenda month */
85677
87327
  200: {
85678
87328
  headers: {
85679
87329
  [name: string]: unknown;
@@ -85682,35 +87332,35 @@ interface paths {
85682
87332
  /**
85683
87333
  * @example {
85684
87334
  * "data": {
85685
- * "campaign_id": "7a7a7a7a-0000-4000-8000-0000000000c1",
85686
- * "channel": "whatsapp",
85687
- * "header_media_url": null,
85688
- * "remaining_this_hour": 4,
85689
- * "results": [
87335
+ * "month": "2026-10",
87336
+ * "days": [
85690
87337
  * {
85691
- * "phone": "+56912345678",
85692
- * "name": "María González",
85693
- * "status": "sent",
85694
- * "conversation_id": "7a7a7a7a-0000-4000-8000-000000000201",
85695
- * "contact_id": "7a7a7a7a-0000-4000-8000-0000000000f1",
85696
- * "rendered_text": "Hola María, tu próximo control es el 15 de noviembre.",
85697
- * "values": {
85698
- * "1": {
85699
- * "value": "María",
85700
- * "source": "variable"
85701
- * },
85702
- * "2": {
85703
- * "value": "15 de noviembre",
85704
- * "source": "static"
85705
- * }
85706
- * }
87338
+ * "date": "2026-10-01",
87339
+ * "citas": 42,
87340
+ * "meta": 9,
87341
+ * "organic": 20,
87342
+ * "unknown": 13,
87343
+ * "diagnosis": 7,
87344
+ * "cancelled": 4
87345
+ * },
87346
+ * {
87347
+ * "date": "2026-10-02",
87348
+ * "citas": 38,
87349
+ * "meta": 6,
87350
+ * "organic": 19,
87351
+ * "unknown": 13,
87352
+ * "diagnosis": 5,
87353
+ * "cancelled": 2
85707
87354
  * }
85708
87355
  * ]
85709
87356
  * }
85710
87357
  * }
85711
87358
  */
85712
87359
  "application/json": {
85713
- data: components["schemas"]["CampaignTestSendResult"];
87360
+ data?: unknown;
87361
+ meta?: {
87362
+ [key: string]: unknown;
87363
+ };
85714
87364
  };
85715
87365
  };
85716
87366
  };
@@ -85770,13 +87420,15 @@ interface paths {
85770
87420
  };
85771
87421
  };
85772
87422
  };
87423
+ put?: never;
87424
+ post?: never;
85773
87425
  delete?: never;
85774
87426
  options?: never;
85775
87427
  head?: never;
85776
87428
  patch?: never;
85777
87429
  trace?: never;
85778
87430
  };
85779
- "/clinic/agenda/feed": {
87431
+ "/clinic/agenda/free-hours": {
85780
87432
  parameters: {
85781
87433
  query?: never;
85782
87434
  header?: never;
@@ -85784,25 +87436,17 @@ interface paths {
85784
87436
  cookie?: never;
85785
87437
  };
85786
87438
  /**
85787
- * Agenda for a window, any engine, by professional and branch
85788
- * @description Mirrored AND native appointments between `from` (inclusive) and `to` (EXCLUSIVE), bare `YYYY-MM-DD` days in the clinic's timezone; the window may not exceed 8 days. Cancelled citas are INCLUDED (the grid draws them muted). Unlike `GET /clinic/agenda`, `professional_id` and `location_id` are VITRINA uuids — `clinic_professional.id` and `location.id` — so one query serves a Medilink, a Reservo and a native clinic. Each row still carries the vendor's own ids under `vendor` for provenance: a row with `vendor.professional_id` set and `professional: null` is a roster gap, never a cita nobody is attending. Never calls the clinic API. `flags` are the clinical alerts the clinic shows on the block: clinical-record content, returned to an API key or personal token only when it also holds `clinic_record:read`. `booking_kind` is the cita's Tipo de agendamiento: `new` (Nuevo: it would be a primera visita and the patient never booked before), `new_rebooked` (Nuevo · reagenda: a primera visita, but the patient had earlier bookings and never came) or `in_treatment` (En tratamiento: every other cita); null on a row that is not a patient's cita. It uses the primera-visita rule Vitrina Ads counts, so a patient back after 12 months without an attended cita is `new`. `origin` is the patient's Origen (`kind` `meta` with the ad, ad set and campaign names, or `organic`) — the attribution Vitrina Ads reports, one definition; null on a row that is not a patient's cita. Filter with `origin=meta|organic|unknown` and, for Meta, `origin_ad_id` (a Meta ad id): rows that are not a patient's cita never match.
87439
+ * Horas libres: one day of free hours per professional
87440
+ * @description Every free slot of `date` from the clinic's own booking engine (the same read as `GET /clinic/agenda/availability`, whole day), grouped per professional with back-to-back slots merged into `ranges` and `slots` = how many cupos. `especialidad` narrows to the professionals whose specialties or note name it; `especialidades` lists the roster's own, for a picker. `professional_id` is the Vitrina uuid when the roster knows them. For a Dentalink / Medilink clinic this is one live vendor request per call (rate-limited upstream), so read it on demand rather than on a timer. A past day has no free hours.
85789
87441
  *
85790
87442
  * **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`.
85791
87443
  */
85792
87444
  get: {
85793
87445
  parameters: {
85794
87446
  query: {
85795
- from: string;
85796
- to: string;
85797
- professional_id?: string;
87447
+ date: string;
87448
+ especialidad?: string;
85798
87449
  location_id?: string;
85799
- resource_id?: string;
85800
- status?: ("pending_hold" | "confirmed" | "cancelled" | "completed" | "no_show")[];
85801
- kind?: ("test_drive" | "external" | "block" | "clinic")[];
85802
- /** @description Origen of the patient: `meta` (came from a Meta ad), `organic` (a linked contact with no ad touch) or `unknown` («Sin dato»: no linked contact). Same attribution as Vitrina Ads' people from ads. */
85803
- origin?: "meta" | "organic" | "unknown";
85804
- /** @description Narrow `origin=meta` to one Meta ad (its platform ad id). */
85805
- origin_ad_id?: string;
85806
87450
  };
85807
87451
  header?: never;
85808
87452
  path?: never;
@@ -85810,7 +87454,7 @@ interface paths {
85810
87454
  };
85811
87455
  requestBody?: never;
85812
87456
  responses: {
85813
- /** @description Agenda feed */
87457
+ /** @description Free hours */
85814
87458
  200: {
85815
87459
  headers: {
85816
87460
  [name: string]: unknown;
@@ -85818,61 +87462,37 @@ interface paths {
85818
87462
  content: {
85819
87463
  /**
85820
87464
  * @example {
85821
- * "data": [
85822
- * {
85823
- * "id": "eeeeeeee-0000-4000-8000-000000000001",
85824
- * "display_id": "A-1",
85825
- * "engine": "native",
85826
- * "external_id": null,
85827
- * "kind": "clinic",
85828
- * "status": "confirmed",
85829
- * "clinic_status_id": null,
85830
- * "status_label": null,
85831
- * "status_color": null,
85832
- * "starts_at": "2026-09-23T13:30:00.000Z",
85833
- * "ends_at": "2026-09-23T14:00:00.000Z",
85834
- * "arrival_state": "scheduled",
85835
- * "professional": {
85836
- * "id": "ffffffff-0000-4000-8000-000000000001",
85837
- * "name": "Ana"
85838
- * },
85839
- * "location": {
85840
- * "id": "b1b1b1b1-0000-4000-8000-000000000002",
85841
- * "name": "Sucursal Maipú"
85842
- * },
85843
- * "resource": null,
85844
- * "service": null,
85845
- * "patient": {
85846
- * "id": "12121212-0000-4000-8000-000000000001",
85847
- * "name": "María José Fuentes Lagos",
85848
- * "external_id": null
85849
- * },
85850
- * "contact": {
85851
- * "id": "22222222-0000-4000-8000-000000000001",
85852
- * "name": "María José Fuentes Lagos"
85853
- * },
85854
- * "customer_name": "María José Fuentes Lagos",
85855
- * "notes": "Avisar al +56987654321 si se atrasa",
85856
- * "money": null,
85857
- * "booking_kind": "new",
85858
- * "flags": [
85859
- * {
85860
- * "id": "13131313-0000-4000-8000-000000000001",
85861
- * "kind": "alergia",
85862
- * "label": "Alergia a penicilina",
85863
- * "severity": "severa"
85864
- * }
85865
- * ],
85866
- * "vendor": {
85867
- * "professional_id": null,
85868
- * "professional_name": null,
85869
- * "sucursal_id": null,
85870
- * "agenda_id": null,
85871
- * "sillon": null,
85872
- * "status_code": null
87465
+ * "data": {
87466
+ * "date": "2026-09-24",
87467
+ * "engine": "native-clinic",
87468
+ * "timezone": "America/Santiago",
87469
+ * "especialidad": null,
87470
+ * "professionals": [
87471
+ * {
87472
+ * "professional_id": "ffffffff-0000-4000-8000-000000000001",
87473
+ * "engine_professional_id": "ffffffff-0000-4000-8000-000000000001",
87474
+ * "name": "Ana Rojas",
87475
+ * "especialidad": "Implantología",
87476
+ * "ranges": [
87477
+ * {
87478
+ * "starts_at": "2026-09-24T12:00:00.000Z",
87479
+ * "ends_at": "2026-09-24T13:30:00.000Z"
87480
+ * },
87481
+ * {
87482
+ * "starts_at": "2026-09-24T17:00:00.000Z",
87483
+ * "ends_at": "2026-09-24T18:00:00.000Z"
87484
+ * }
87485
+ * ],
87486
+ * "slots": 5
85873
87487
  * }
85874
- * }
85875
- * ]
87488
+ * ],
87489
+ * "especialidades": [
87490
+ * "Endodoncia",
87491
+ * "Implantología",
87492
+ * "Ortodoncia"
87493
+ * ],
87494
+ * "truncated": false
87495
+ * }
85876
87496
  * }
85877
87497
  */
85878
87498
  "application/json": {
@@ -87677,6 +89297,149 @@ interface paths {
87677
89297
  patch?: never;
87678
89298
  trace?: never;
87679
89299
  };
89300
+ "/clinic/encounters/{id}/void": {
89301
+ parameters: {
89302
+ query?: never;
89303
+ header?: never;
89304
+ path?: never;
89305
+ cookie?: never;
89306
+ };
89307
+ get?: never;
89308
+ put?: never;
89309
+ /**
89310
+ * Void an atención (idempotent)
89311
+ * @description Voids an atención opened or closed in error; the row stays, with its reason. Voiding an already voided atención returns it unchanged. Refused on a locked atención. The cita lines that closing performed go back to not performed, so their insumos return to stock. The reason is not copied into the audit row. Every call writes a `clinic_record_access_log` row; a call that cannot write its access event fails.
89312
+ *
89313
+ * **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.
89314
+ */
89315
+ post: {
89316
+ parameters: {
89317
+ query?: never;
89318
+ header?: {
89319
+ /** @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. */
89320
+ "Idempotency-Key"?: string;
89321
+ };
89322
+ path: {
89323
+ id: string;
89324
+ };
89325
+ cookie?: never;
89326
+ };
89327
+ requestBody: {
89328
+ content: {
89329
+ /**
89330
+ * @example {
89331
+ * "reason": "Abierta por error"
89332
+ * }
89333
+ */
89334
+ "application/json": {
89335
+ reason: string;
89336
+ };
89337
+ };
89338
+ };
89339
+ responses: {
89340
+ /** @description Atención */
89341
+ 200: {
89342
+ headers: {
89343
+ [name: string]: unknown;
89344
+ };
89345
+ content: {
89346
+ /**
89347
+ * @example {
89348
+ * "data": {
89349
+ * "id": "e0e57121-0000-4000-8000-0000000007ab",
89350
+ * "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
89351
+ * "clinic_patient_id": "b1b0b8de-0000-4000-8000-000000000001",
89352
+ * "appointment_id": "d93e8eec-0000-4000-8000-000000000001",
89353
+ * "professional_id": null,
89354
+ * "location_id": null,
89355
+ * "treatment_plan_id": null,
89356
+ * "form_template_id": null,
89357
+ * "sterilization_lote": null,
89358
+ * "kind": "control",
89359
+ * "status": "voided",
89360
+ * "starts_at": "2026-10-09T18:00:00.000Z",
89361
+ * "ends_at": null,
89362
+ * "closed_at": "2026-10-09T18:40:00.000Z",
89363
+ * "opened_by_user_id": "5e5e5e5e-0000-4000-8000-000000000001",
89364
+ * "closed_by_user_id": "5e5e5e5e-0000-4000-8000-000000000001",
89365
+ * "voided_reason": "Abierta por error",
89366
+ * "author": null,
89367
+ * "created_at": "2026-10-09T18:00:00.000Z",
89368
+ * "updated_at": "2026-10-09T18:45:00.000Z",
89369
+ * "notes": []
89370
+ * }
89371
+ * }
89372
+ */
89373
+ "application/json": {
89374
+ data?: unknown;
89375
+ meta?: {
89376
+ [key: string]: unknown;
89377
+ };
89378
+ };
89379
+ };
89380
+ };
89381
+ /** @description Validation error */
89382
+ 400: {
89383
+ headers: {
89384
+ [name: string]: unknown;
89385
+ };
89386
+ content: {
89387
+ "application/json": components["schemas"]["Error"];
89388
+ };
89389
+ };
89390
+ /** @description Unauthorized */
89391
+ 401: {
89392
+ headers: {
89393
+ [name: string]: unknown;
89394
+ };
89395
+ content: {
89396
+ "application/json": components["schemas"]["Error"];
89397
+ };
89398
+ };
89399
+ /** @description Forbidden */
89400
+ 403: {
89401
+ headers: {
89402
+ [name: string]: unknown;
89403
+ };
89404
+ content: {
89405
+ "application/json": components["schemas"]["Error"];
89406
+ };
89407
+ };
89408
+ /** @description Not found */
89409
+ 404: {
89410
+ headers: {
89411
+ [name: string]: unknown;
89412
+ };
89413
+ content: {
89414
+ "application/json": components["schemas"]["Error"];
89415
+ };
89416
+ };
89417
+ /** @description Conflict (incl. Idempotency-Key reuse with different body) */
89418
+ 409: {
89419
+ headers: {
89420
+ [name: string]: unknown;
89421
+ };
89422
+ content: {
89423
+ "application/json": components["schemas"]["Error"];
89424
+ };
89425
+ };
89426
+ /** @description Rate limited */
89427
+ 429: {
89428
+ headers: {
89429
+ [name: string]: unknown;
89430
+ };
89431
+ content: {
89432
+ "application/json": components["schemas"]["Error"];
89433
+ };
89434
+ };
89435
+ };
89436
+ };
89437
+ delete?: never;
89438
+ options?: never;
89439
+ head?: never;
89440
+ patch?: never;
89441
+ trace?: never;
89442
+ };
87680
89443
  "/clinic/encounters/{id}/sterilization-lote": {
87681
89444
  parameters: {
87682
89445
  query?: never;
@@ -98135,45 +99898,554 @@ interface paths {
98135
99898
  options?: never;
98136
99899
  head?: never;
98137
99900
  /**
98138
- * Edit one family bond
98139
- * @description Partial update. Promoting a bond to `is_primary` demotes the incumbent first (the partial unique index allows exactly one per patient). Raising `can_read_record` needs `clinic_record:write`; lowering it never does — revoking access is always safe.
99901
+ * Edit one family bond
99902
+ * @description Partial update. Promoting a bond to `is_primary` demotes the incumbent first (the partial unique index allows exactly one per patient). Raising `can_read_record` needs `clinic_record:write`; lowering it never does — revoking access is always safe.
99903
+ *
99904
+ * **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.
99905
+ */
99906
+ patch: {
99907
+ parameters: {
99908
+ query?: never;
99909
+ header?: never;
99910
+ path: {
99911
+ id: string;
99912
+ };
99913
+ cookie?: never;
99914
+ };
99915
+ requestBody?: {
99916
+ content: {
99917
+ /**
99918
+ * @example {
99919
+ * "can_read_record": false
99920
+ * }
99921
+ */
99922
+ "application/json": {
99923
+ /** Format: uuid */
99924
+ related_patient_id?: string | null;
99925
+ /** @enum {string} */
99926
+ role?: "titular" | "apoderado" | "tutor" | "carga" | "pareja" | "otro";
99927
+ is_primary?: boolean;
99928
+ can_book?: boolean;
99929
+ can_read_record?: boolean;
99930
+ /** @enum {string} */
99931
+ verified_method?: "phone_match" | "id_challenge" | "manual" | "document";
99932
+ /** Format: date-time */
99933
+ expires_at?: string | null;
99934
+ note?: string | null;
99935
+ };
99936
+ };
99937
+ };
99938
+ responses: {
99939
+ /** @description Relationship */
99940
+ 200: {
99941
+ headers: {
99942
+ [name: string]: unknown;
99943
+ };
99944
+ content: {
99945
+ /**
99946
+ * @example {
99947
+ * "data": {
99948
+ * "id": "6a73a912-0000-4000-8000-000000000002",
99949
+ * "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
99950
+ * "clinic_patient_id": "b1b0b8de-0000-4000-8000-000000000001",
99951
+ * "contact_id": "1a73af9e-0000-4000-8000-000000000002",
99952
+ * "related_patient_id": null,
99953
+ * "role": "apoderado",
99954
+ * "is_primary": false,
99955
+ * "can_book": true,
99956
+ * "can_read_record": false,
99957
+ * "verified_method": "id_challenge",
99958
+ * "verified_at": "2026-09-22T22:21:17.284Z",
99959
+ * "expires_at": null,
99960
+ * "note": "Presentó cédula en el mesón",
99961
+ * "created_by": null,
99962
+ * "created_at": "2026-09-22T22:21:17.283Z",
99963
+ * "updated_at": "2026-09-22T22:21:17.298Z",
99964
+ * "contact_name": "Carmen Lagos Pinto",
99965
+ * "contact_phone": "+56955554444",
99966
+ * "patient_name": "María José Fuentes Lagos",
99967
+ * "expired": false
99968
+ * }
99969
+ * }
99970
+ */
99971
+ "application/json": {
99972
+ data?: unknown;
99973
+ meta?: {
99974
+ [key: string]: unknown;
99975
+ };
99976
+ };
99977
+ };
99978
+ };
99979
+ /** @description Validation error */
99980
+ 400: {
99981
+ headers: {
99982
+ [name: string]: unknown;
99983
+ };
99984
+ content: {
99985
+ "application/json": components["schemas"]["Error"];
99986
+ };
99987
+ };
99988
+ /** @description Unauthorized */
99989
+ 401: {
99990
+ headers: {
99991
+ [name: string]: unknown;
99992
+ };
99993
+ content: {
99994
+ "application/json": components["schemas"]["Error"];
99995
+ };
99996
+ };
99997
+ /** @description Forbidden */
99998
+ 403: {
99999
+ headers: {
100000
+ [name: string]: unknown;
100001
+ };
100002
+ content: {
100003
+ "application/json": components["schemas"]["Error"];
100004
+ };
100005
+ };
100006
+ /** @description Not found */
100007
+ 404: {
100008
+ headers: {
100009
+ [name: string]: unknown;
100010
+ };
100011
+ content: {
100012
+ "application/json": components["schemas"]["Error"];
100013
+ };
100014
+ };
100015
+ /** @description Conflict (incl. Idempotency-Key reuse with different body) */
100016
+ 409: {
100017
+ headers: {
100018
+ [name: string]: unknown;
100019
+ };
100020
+ content: {
100021
+ "application/json": components["schemas"]["Error"];
100022
+ };
100023
+ };
100024
+ /** @description Rate limited */
100025
+ 429: {
100026
+ headers: {
100027
+ [name: string]: unknown;
100028
+ };
100029
+ content: {
100030
+ "application/json": components["schemas"]["Error"];
100031
+ };
100032
+ };
100033
+ };
100034
+ };
100035
+ trace?: never;
100036
+ };
100037
+ "/clinic/patients/{id}/auto-links": {
100038
+ parameters: {
100039
+ query?: never;
100040
+ header?: never;
100041
+ path?: never;
100042
+ cookie?: never;
100043
+ };
100044
+ /**
100045
+ * Automatic contact links of a patient (Ads identity only)
100046
+ * @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; `chat_rut`: the patient's RUT typed by exactly one contact in its own ad chat, corroborated by the typed email or by the ficha appearing after it — 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.
100047
+ *
100048
+ * **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.
100049
+ */
100050
+ get: {
100051
+ parameters: {
100052
+ query?: never;
100053
+ header?: never;
100054
+ path: {
100055
+ id: string;
100056
+ };
100057
+ cookie?: never;
100058
+ };
100059
+ requestBody?: never;
100060
+ responses: {
100061
+ /** @description Patient auto-links */
100062
+ 200: {
100063
+ headers: {
100064
+ [name: string]: unknown;
100065
+ };
100066
+ content: {
100067
+ /**
100068
+ * @example {
100069
+ * "data": {
100070
+ * "data": [
100071
+ * {
100072
+ * "id": "a7a7a7a7-0000-4000-8000-000000000001",
100073
+ * "clinic_patient_id": "b1b0b8de-0000-4000-8000-000000000001",
100074
+ * "contact_id": "1a73af9e-0000-4000-8000-000000000001",
100075
+ * "patient_name": "María José Fuentes Lagos",
100076
+ * "contact_name": "María José Fuentes Lagos",
100077
+ * "status": "live",
100078
+ * "method": "phone_unique",
100079
+ * "matched_at": "2026-09-27T14:10:02.118Z",
100080
+ * "run_id": "a8a8a8a8-0000-4000-8000-000000000001",
100081
+ * "revoked_at": null,
100082
+ * "revoked_reason": null,
100083
+ * "sent_count": 3,
100084
+ * "last_sent_at": "2026-09-28T12:02:40.551Z"
100085
+ * }
100086
+ * ]
100087
+ * }
100088
+ * }
100089
+ */
100090
+ "application/json": {
100091
+ data?: unknown;
100092
+ meta?: {
100093
+ [key: string]: unknown;
100094
+ };
100095
+ };
100096
+ };
100097
+ };
100098
+ /** @description Validation error */
100099
+ 400: {
100100
+ headers: {
100101
+ [name: string]: unknown;
100102
+ };
100103
+ content: {
100104
+ "application/json": components["schemas"]["Error"];
100105
+ };
100106
+ };
100107
+ /** @description Unauthorized */
100108
+ 401: {
100109
+ headers: {
100110
+ [name: string]: unknown;
100111
+ };
100112
+ content: {
100113
+ "application/json": components["schemas"]["Error"];
100114
+ };
100115
+ };
100116
+ /** @description Forbidden */
100117
+ 403: {
100118
+ headers: {
100119
+ [name: string]: unknown;
100120
+ };
100121
+ content: {
100122
+ "application/json": components["schemas"]["Error"];
100123
+ };
100124
+ };
100125
+ /** @description Not found */
100126
+ 404: {
100127
+ headers: {
100128
+ [name: string]: unknown;
100129
+ };
100130
+ content: {
100131
+ "application/json": components["schemas"]["Error"];
100132
+ };
100133
+ };
100134
+ /** @description Conflict (incl. Idempotency-Key reuse with different body) */
100135
+ 409: {
100136
+ headers: {
100137
+ [name: string]: unknown;
100138
+ };
100139
+ content: {
100140
+ "application/json": components["schemas"]["Error"];
100141
+ };
100142
+ };
100143
+ /** @description Rate limited */
100144
+ 429: {
100145
+ headers: {
100146
+ [name: string]: unknown;
100147
+ };
100148
+ content: {
100149
+ "application/json": components["schemas"]["Error"];
100150
+ };
100151
+ };
100152
+ };
100153
+ };
100154
+ put?: never;
100155
+ post?: never;
100156
+ delete?: never;
100157
+ options?: never;
100158
+ head?: never;
100159
+ patch?: never;
100160
+ trace?: never;
100161
+ };
100162
+ "/clinic/contacts/{contactId}/auto-links": {
100163
+ parameters: {
100164
+ query?: never;
100165
+ header?: never;
100166
+ path?: never;
100167
+ cookie?: never;
100168
+ };
100169
+ /**
100170
+ * Patients automatically linked to a contact (Ads identity only)
100171
+ * @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; `chat_rut`: the patient's RUT typed by exactly one contact in its own ad chat, corroborated by the typed email or by the ficha appearing after it — 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.
100172
+ *
100173
+ * **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.
100174
+ */
100175
+ get: {
100176
+ parameters: {
100177
+ query?: never;
100178
+ header?: never;
100179
+ path: {
100180
+ contactId: string;
100181
+ };
100182
+ cookie?: never;
100183
+ };
100184
+ requestBody?: never;
100185
+ responses: {
100186
+ /** @description Contact auto-links */
100187
+ 200: {
100188
+ headers: {
100189
+ [name: string]: unknown;
100190
+ };
100191
+ content: {
100192
+ /**
100193
+ * @example {
100194
+ * "data": {
100195
+ * "data": [
100196
+ * {
100197
+ * "id": "a7a7a7a7-0000-4000-8000-000000000001",
100198
+ * "clinic_patient_id": "b1b0b8de-0000-4000-8000-000000000001",
100199
+ * "contact_id": "1a73af9e-0000-4000-8000-000000000001",
100200
+ * "patient_name": "María José Fuentes Lagos",
100201
+ * "contact_name": "María José Fuentes Lagos",
100202
+ * "status": "live",
100203
+ * "method": "phone_unique",
100204
+ * "matched_at": "2026-09-27T14:10:02.118Z",
100205
+ * "run_id": "a8a8a8a8-0000-4000-8000-000000000001",
100206
+ * "revoked_at": null,
100207
+ * "revoked_reason": null,
100208
+ * "sent_count": 3,
100209
+ * "last_sent_at": "2026-09-28T12:02:40.551Z"
100210
+ * }
100211
+ * ]
100212
+ * }
100213
+ * }
100214
+ */
100215
+ "application/json": {
100216
+ data?: unknown;
100217
+ meta?: {
100218
+ [key: string]: unknown;
100219
+ };
100220
+ };
100221
+ };
100222
+ };
100223
+ /** @description Validation error */
100224
+ 400: {
100225
+ headers: {
100226
+ [name: string]: unknown;
100227
+ };
100228
+ content: {
100229
+ "application/json": components["schemas"]["Error"];
100230
+ };
100231
+ };
100232
+ /** @description Unauthorized */
100233
+ 401: {
100234
+ headers: {
100235
+ [name: string]: unknown;
100236
+ };
100237
+ content: {
100238
+ "application/json": components["schemas"]["Error"];
100239
+ };
100240
+ };
100241
+ /** @description Forbidden */
100242
+ 403: {
100243
+ headers: {
100244
+ [name: string]: unknown;
100245
+ };
100246
+ content: {
100247
+ "application/json": components["schemas"]["Error"];
100248
+ };
100249
+ };
100250
+ /** @description Not found */
100251
+ 404: {
100252
+ headers: {
100253
+ [name: string]: unknown;
100254
+ };
100255
+ content: {
100256
+ "application/json": components["schemas"]["Error"];
100257
+ };
100258
+ };
100259
+ /** @description Conflict (incl. Idempotency-Key reuse with different body) */
100260
+ 409: {
100261
+ headers: {
100262
+ [name: string]: unknown;
100263
+ };
100264
+ content: {
100265
+ "application/json": components["schemas"]["Error"];
100266
+ };
100267
+ };
100268
+ /** @description Rate limited */
100269
+ 429: {
100270
+ headers: {
100271
+ [name: string]: unknown;
100272
+ };
100273
+ content: {
100274
+ "application/json": components["schemas"]["Error"];
100275
+ };
100276
+ };
100277
+ };
100278
+ };
100279
+ put?: never;
100280
+ post?: never;
100281
+ delete?: never;
100282
+ options?: never;
100283
+ head?: never;
100284
+ patch?: never;
100285
+ trace?: never;
100286
+ };
100287
+ "/clinic/auto-links/{id}/reject": {
100288
+ parameters: {
100289
+ query?: never;
100290
+ header?: never;
100291
+ path?: never;
100292
+ cookie?: never;
100293
+ };
100294
+ get?: never;
100295
+ put?: never;
100296
+ /**
100297
+ * «No es esta persona» — reject an automatic link for good
100298
+ * @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.
100299
+ *
100300
+ * **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.
100301
+ */
100302
+ post: {
100303
+ parameters: {
100304
+ query?: never;
100305
+ header?: {
100306
+ /** @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. */
100307
+ "Idempotency-Key"?: string;
100308
+ };
100309
+ path: {
100310
+ id: string;
100311
+ };
100312
+ cookie?: never;
100313
+ };
100314
+ requestBody?: {
100315
+ content: {
100316
+ /** @example {} */
100317
+ "application/json": Record<string, never>;
100318
+ };
100319
+ };
100320
+ responses: {
100321
+ /** @description Rejected auto-link */
100322
+ 200: {
100323
+ headers: {
100324
+ [name: string]: unknown;
100325
+ };
100326
+ content: {
100327
+ /**
100328
+ * @example {
100329
+ * "data": {
100330
+ * "id": "a7a7a7a7-0000-4000-8000-000000000001",
100331
+ * "clinic_patient_id": "b1b0b8de-0000-4000-8000-000000000001",
100332
+ * "contact_id": "1a73af9e-0000-4000-8000-000000000001",
100333
+ * "patient_name": "María José Fuentes Lagos",
100334
+ * "contact_name": "María José Fuentes Lagos",
100335
+ * "status": "rejected",
100336
+ * "method": "phone_unique",
100337
+ * "matched_at": "2026-09-27T14:10:02.118Z",
100338
+ * "run_id": "a8a8a8a8-0000-4000-8000-000000000001",
100339
+ * "revoked_at": "2026-09-28T15:31:09.004Z",
100340
+ * "revoked_reason": "rejected",
100341
+ * "sent_count": 3,
100342
+ * "last_sent_at": "2026-09-28T12:02:40.551Z",
100343
+ * "withheld_outcomes": 1
100344
+ * }
100345
+ * }
100346
+ */
100347
+ "application/json": {
100348
+ data?: unknown;
100349
+ meta?: {
100350
+ [key: string]: unknown;
100351
+ };
100352
+ };
100353
+ };
100354
+ };
100355
+ /** @description Validation error */
100356
+ 400: {
100357
+ headers: {
100358
+ [name: string]: unknown;
100359
+ };
100360
+ content: {
100361
+ "application/json": components["schemas"]["Error"];
100362
+ };
100363
+ };
100364
+ /** @description Unauthorized */
100365
+ 401: {
100366
+ headers: {
100367
+ [name: string]: unknown;
100368
+ };
100369
+ content: {
100370
+ "application/json": components["schemas"]["Error"];
100371
+ };
100372
+ };
100373
+ /** @description Forbidden */
100374
+ 403: {
100375
+ headers: {
100376
+ [name: string]: unknown;
100377
+ };
100378
+ content: {
100379
+ "application/json": components["schemas"]["Error"];
100380
+ };
100381
+ };
100382
+ /** @description Not found */
100383
+ 404: {
100384
+ headers: {
100385
+ [name: string]: unknown;
100386
+ };
100387
+ content: {
100388
+ "application/json": components["schemas"]["Error"];
100389
+ };
100390
+ };
100391
+ /** @description Conflict (incl. Idempotency-Key reuse with different body) */
100392
+ 409: {
100393
+ headers: {
100394
+ [name: string]: unknown;
100395
+ };
100396
+ content: {
100397
+ "application/json": components["schemas"]["Error"];
100398
+ };
100399
+ };
100400
+ /** @description Rate limited */
100401
+ 429: {
100402
+ headers: {
100403
+ [name: string]: unknown;
100404
+ };
100405
+ content: {
100406
+ "application/json": components["schemas"]["Error"];
100407
+ };
100408
+ };
100409
+ };
100410
+ };
100411
+ delete?: never;
100412
+ options?: never;
100413
+ head?: never;
100414
+ patch?: never;
100415
+ trace?: never;
100416
+ };
100417
+ "/clinic/insights/produccion": {
100418
+ parameters: {
100419
+ query?: never;
100420
+ header?: never;
100421
+ path?: never;
100422
+ cookie?: never;
100423
+ };
100424
+ /**
100425
+ * What was performed and what was collected
100426
+ * @description Producción, aggregates only. A Dentalink / Medilink clinic (revenue mirror on, `basis: mirror_lines`) reads the vendor's own plan lines marked realizado, by fecha de realización, at the line's total — Dentalink's «Ventas (acciones realizadas)», evaluación included, $0 lines out; `collected_clp` is every live payment received in the window (`null` with `professional_id`: a payment names no professional). Otherwise only ATTENDED citas count, valued from the cita's frozen `clinic_appointment_service` lines (a native clinic) or the catálogo's current price for the prestación the mirror names; `lines_without_price` names the hole, and `collected_clp` is what the ledger allocated to those citas. `by_professional` and the `professional_id` filter follow who PERFORMED the work, not who quoted the plan. Requires `clinic_money:read`; a caller holding only `clinic_insights:read` reads the last closed months with the minimum-count floor (`suppression`). Accepts `from` / `to` (bare `YYYY-MM-DD`, `to` INCLUSIVE), `location_id` and `professional_id`. The default window is the 90 days ending today in the clinic's own zone. A `professional_id` that is not yours is a 404, never an empty report. Also reachable with `clinic_insights:read` — the aggregates-only «Consultor» role (spec #2441 D11) — in addition to the scope named above. A caller WITHOUT the report’s own scope never receives a patient-identifiable row (`inasistencias.citas` and `packs.expiring` come back empty with `aggregate_only: true` and `rows_withheld` naming the field) and reads the minimum-count floor: always the last 3 closed calendar months with no professional or sucursal filter, whatever was asked (`suppression.filters_ignored`); a count or CLP resting on 1–4 cases is `null`; a rate over fewer than 5 cases, or whose numerator is withheld, is `null`; list rows that add up to a shown total get complementary suppression and leave the list (`suppression.withheld_rows`); per-professional/prestación/weekday outcome rows and per-channel confirmation rows carry rates only; producción serves no per-prestación rows (they nest inside the categorías); clinic-wide cash over whole months stays while its slices are floored. `suppression.min_count` is the floor.
98140
100427
  *
98141
- * **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.
100428
+ * **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`.
98142
100429
  */
98143
- patch: {
100430
+ get: {
98144
100431
  parameters: {
98145
- query?: never;
98146
- header?: never;
98147
- path: {
98148
- id: string;
100432
+ query?: {
100433
+ /** @description First day of the window, inclusive. Defaults to 90 days before `to`. */
100434
+ from?: string;
100435
+ /** @description Last day of the window, INCLUSIVE. Defaults to today in the clinic's zone. */
100436
+ to?: string;
100437
+ /** @description One sucursal. Omit for all of them. */
100438
+ location_id?: string;
100439
+ /** @description One profesional. A foreign id is a 404, never an empty report. */
100440
+ professional_id?: string;
98149
100441
  };
100442
+ header?: never;
100443
+ path?: never;
98150
100444
  cookie?: never;
98151
100445
  };
98152
- requestBody?: {
98153
- content: {
98154
- /**
98155
- * @example {
98156
- * "can_read_record": false
98157
- * }
98158
- */
98159
- "application/json": {
98160
- /** Format: uuid */
98161
- related_patient_id?: string | null;
98162
- /** @enum {string} */
98163
- role?: "titular" | "apoderado" | "tutor" | "carga" | "pareja" | "otro";
98164
- is_primary?: boolean;
98165
- can_book?: boolean;
98166
- can_read_record?: boolean;
98167
- /** @enum {string} */
98168
- verified_method?: "phone_match" | "id_challenge" | "manual" | "document";
98169
- /** Format: date-time */
98170
- expires_at?: string | null;
98171
- note?: string | null;
98172
- };
98173
- };
98174
- };
100446
+ requestBody?: never;
98175
100447
  responses: {
98176
- /** @description Relationship */
100448
+ /** @description Clinic production report */
98177
100449
  200: {
98178
100450
  headers: {
98179
100451
  [name: string]: unknown;
@@ -98182,34 +100454,59 @@ interface paths {
98182
100454
  /**
98183
100455
  * @example {
98184
100456
  * "data": {
98185
- * "id": "6a73a912-0000-4000-8000-000000000002",
98186
- * "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
98187
- * "clinic_patient_id": "b1b0b8de-0000-4000-8000-000000000001",
98188
- * "contact_id": "1a73af9e-0000-4000-8000-000000000002",
98189
- * "related_patient_id": null,
98190
- * "role": "apoderado",
98191
- * "is_primary": false,
98192
- * "can_book": true,
98193
- * "can_read_record": false,
98194
- * "verified_method": "id_challenge",
98195
- * "verified_at": "2026-09-22T22:21:17.284Z",
98196
- * "expires_at": null,
98197
- * "note": "Presentó cédula en el mesón",
98198
- * "created_by": null,
98199
- * "created_at": "2026-09-22T22:21:17.283Z",
98200
- * "updated_at": "2026-09-22T22:21:17.298Z",
98201
- * "contact_name": "Carmen Lagos Pinto",
98202
- * "contact_phone": "+56955554444",
98203
- * "patient_name": "María José Fuentes Lagos",
98204
- * "expired": false
100457
+ * "from": "2026-09-01",
100458
+ * "to": "2026-09-30",
100459
+ * "location_id": null,
100460
+ * "professional_id": null,
100461
+ * "basis": "mirror_lines",
100462
+ * "totals": {
100463
+ * "attended": 612,
100464
+ * "lines": 699,
100465
+ * "lines_without_price": 0,
100466
+ * "performed_clp": 57179516,
100467
+ * "collected_clp": 57172232,
100468
+ * "collected_basis": "mirror_payment.received"
100469
+ * },
100470
+ * "by_professional": [
100471
+ * {
100472
+ * "key": "00000000-0000-4000-8000-0000000000a1",
100473
+ * "label": "Ana Rojas",
100474
+ * "appointments": 141,
100475
+ * "lines": 168,
100476
+ * "performed_clp": 9623135
100477
+ * }
100478
+ * ],
100479
+ * "by_service": [
100480
+ * {
100481
+ * "key": "00000000-0000-4000-8000-0000000000b3",
100482
+ * "label": "Endodoncia molar",
100483
+ * "appointments": 38,
100484
+ * "lines": 38,
100485
+ * "performed_clp": 9880000
100486
+ * }
100487
+ * ],
100488
+ * "by_category": [
100489
+ * {
100490
+ * "key": "Endodoncia",
100491
+ * "label": "Endodoncia",
100492
+ * "appointments": 52,
100493
+ * "lines": 61,
100494
+ * "performed_clp": 12400000
100495
+ * }
100496
+ * ],
100497
+ * "months": [
100498
+ * {
100499
+ * "month": "2026-09",
100500
+ * "attended": 612,
100501
+ * "performed_clp": 57179516,
100502
+ * "collected_clp": 57172232
100503
+ * }
100504
+ * ]
98205
100505
  * }
98206
100506
  * }
98207
100507
  */
98208
100508
  "application/json": {
98209
- data?: unknown;
98210
- meta?: {
98211
- [key: string]: unknown;
98212
- };
100509
+ data: components["schemas"]["ClinicProduccionReport"];
98213
100510
  };
98214
100511
  };
98215
100512
  };
@@ -98269,9 +100566,15 @@ interface paths {
98269
100566
  };
98270
100567
  };
98271
100568
  };
100569
+ put?: never;
100570
+ post?: never;
100571
+ delete?: never;
100572
+ options?: never;
100573
+ head?: never;
100574
+ patch?: never;
98272
100575
  trace?: never;
98273
100576
  };
98274
- "/clinic/patients/{id}/auto-links": {
100577
+ "/clinic/insights/ingresos": {
98275
100578
  parameters: {
98276
100579
  query?: never;
98277
100580
  header?: never;
@@ -98279,23 +100582,30 @@ interface paths {
98279
100582
  cookie?: never;
98280
100583
  };
98281
100584
  /**
98282
- * Automatic contact links of a patient (Ads identity only)
98283
- * @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; `chat_rut`: the patient's RUT typed by exactly one contact in its own ad chat, corroborated by the typed email or by the ficha appearing after it — 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.
100585
+ * Ingresos: producción, presupuestado and recaudación by day
100586
+ * @description «Ingresos» (D56) for a Dentalink / Medilink clinic (revenue mirror on; otherwise `state: unavailable`). Per day: producción (performed plan lines by fecha de realización), presupuestado (plans issued, header total, evaluación included, as the vendor's panel) and recaudación (live payments by fecha de recepción), each split `nuevos` (the patient's primera visita starts in that month) vs `tratamiento`. `hoy` carries today's three numbers whatever the window. `by_professional`: producción by PERFORMER; presupuestos, aceptación and monto aceptado by the QUOTING professional under Números comerciales' acceptance rule ($0 and evaluación-only presupuestos out); abonado = paid so far on the plans issued in the period; new patients presented / accepted. Default window: the current month to today. With `professional_id` recaudación is `null` (a payment names no professional). Requires `clinic_money:read`. Accepts `from` / `to` (bare `YYYY-MM-DD`, `to` INCLUSIVE), `location_id` and `professional_id`. The default window is the 90 days ending today in the clinic's own zone. A `professional_id` that is not yours is a 404, never an empty report. Also reachable with `clinic_insights:read` — the aggregates-only «Consultor» role (spec #2441 D11) — in addition to the scope named above. A caller WITHOUT the report’s own scope never receives a patient-identifiable row (`inasistencias.citas` and `packs.expiring` come back empty with `aggregate_only: true` and `rows_withheld` naming the field) and reads the minimum-count floor: always the last 3 closed calendar months with no professional or sucursal filter, whatever was asked (`suppression.filters_ignored`); a count or CLP resting on 1–4 cases is `null`; a rate over fewer than 5 cases, or whose numerator is withheld, is `null`; list rows that add up to a shown total get complementary suppression and leave the list (`suppression.withheld_rows`); per-professional/prestación/weekday outcome rows and per-channel confirmation rows carry rates only; producción serves no per-prestación rows (they nest inside the categorías); clinic-wide cash over whole months stays while its slices are floored. `suppression.min_count` is the floor.
98284
100587
  *
98285
- * **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.
100588
+ * **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`.
98286
100589
  */
98287
100590
  get: {
98288
100591
  parameters: {
98289
- query?: never;
98290
- header?: never;
98291
- path: {
98292
- id: string;
100592
+ query?: {
100593
+ /** @description First day of the window, inclusive. Defaults to 90 days before `to`. */
100594
+ from?: string;
100595
+ /** @description Last day of the window, INCLUSIVE. Defaults to today in the clinic's zone. */
100596
+ to?: string;
100597
+ /** @description One sucursal. Omit for all of them. */
100598
+ location_id?: string;
100599
+ /** @description One profesional. A foreign id is a 404, never an empty report. */
100600
+ professional_id?: string;
98293
100601
  };
100602
+ header?: never;
100603
+ path?: never;
98294
100604
  cookie?: never;
98295
100605
  };
98296
100606
  requestBody?: never;
98297
100607
  responses: {
98298
- /** @description Patient auto-links */
100608
+ /** @description Clinic Ingresos */
98299
100609
  200: {
98300
100610
  headers: {
98301
100611
  [name: string]: unknown;
@@ -98304,31 +100614,111 @@ interface paths {
98304
100614
  /**
98305
100615
  * @example {
98306
100616
  * "data": {
98307
- * "data": [
100617
+ * "from": "2026-10-01",
100618
+ * "to": "2026-10-08",
100619
+ * "today": "2026-10-08",
100620
+ * "timezone": "America/Santiago",
100621
+ * "location_id": null,
100622
+ * "professional_id": null,
100623
+ * "state": "ready",
100624
+ * "state_reason": null,
100625
+ * "source": {
100626
+ * "kind": "mirror",
100627
+ * "synced_at": "2026-10-08T20:10:00.000Z"
100628
+ * },
100629
+ * "rules": {
100630
+ * "produccion": "Lo realizado en el box …",
100631
+ * "presupuestado": "Presupuestos creados en el día …",
100632
+ * "recaudacion": "Pagos recibidos en el día …",
100633
+ * "nuevos": "Paciente nuevo: su primera visita es en ese mismo mes …",
100634
+ * "aceptacion": "Aceptados con la misma regla de Números comerciales …",
100635
+ * "abonado": "Lo ya pagado a los presupuestos creados en el período …"
100636
+ * },
100637
+ * "hoy": {
100638
+ * "day": "2026-10-08",
100639
+ * "produccion_clp": 3670982,
100640
+ * "prestaciones": 47,
100641
+ * "presupuestado_clp": 4381028,
100642
+ * "presupuestos": 18,
100643
+ * "recaudado_clp": 3307992,
100644
+ * "pagos": 21
100645
+ * },
100646
+ * "totals": {
100647
+ * "produccion": {
100648
+ * "total_clp": 18626657,
100649
+ * "nuevos_clp": 412000,
100650
+ * "tratamiento_clp": 3258982,
100651
+ * "prestaciones": 47
100652
+ * },
100653
+ * "presupuestado": {
100654
+ * "total_clp": 25014395,
100655
+ * "nuevos_clp": 1072572,
100656
+ * "tratamiento_clp": 3308456,
100657
+ * "presupuestos": 18
100658
+ * },
100659
+ * "recaudacion": {
100660
+ * "total_clp": 14856381,
100661
+ * "nuevos_clp": 811084,
100662
+ * "tratamiento_clp": 2496908,
100663
+ * "pagos": 21
100664
+ * }
100665
+ * },
100666
+ * "days": [
98308
100667
  * {
98309
- * "id": "a7a7a7a7-0000-4000-8000-000000000001",
98310
- * "clinic_patient_id": "b1b0b8de-0000-4000-8000-000000000001",
98311
- * "contact_id": "1a73af9e-0000-4000-8000-000000000001",
98312
- * "patient_name": "María José Fuentes Lagos",
98313
- * "contact_name": "María José Fuentes Lagos",
98314
- * "status": "live",
98315
- * "method": "phone_unique",
98316
- * "matched_at": "2026-09-27T14:10:02.118Z",
98317
- * "run_id": "a8a8a8a8-0000-4000-8000-000000000001",
98318
- * "revoked_at": null,
98319
- * "revoked_reason": null,
98320
- * "sent_count": 3,
98321
- * "last_sent_at": "2026-09-28T12:02:40.551Z"
100668
+ * "day": "2026-10-08",
100669
+ * "produccion": {
100670
+ * "total_clp": 3670982,
100671
+ * "nuevos_clp": 412000,
100672
+ * "tratamiento_clp": 3258982,
100673
+ * "prestaciones": 47
100674
+ * },
100675
+ * "presupuestado": {
100676
+ * "total_clp": 4381028,
100677
+ * "nuevos_clp": 1072572,
100678
+ * "tratamiento_clp": 3308456,
100679
+ * "presupuestos": 18
100680
+ * },
100681
+ * "recaudacion": {
100682
+ * "total_clp": 3307992,
100683
+ * "nuevos_clp": 811084,
100684
+ * "tratamiento_clp": 2496908,
100685
+ * "pagos": 21
100686
+ * }
100687
+ * }
100688
+ * ],
100689
+ * "clinic": {
100690
+ * "produccion_clp": 18626657,
100691
+ * "prestaciones": 52,
100692
+ * "presupuestos": 14,
100693
+ * "presupuestado_clp": 5120000,
100694
+ * "aceptados": 9,
100695
+ * "aceptacion_pct": 64.3,
100696
+ * "aceptado_clp": 3010000,
100697
+ * "abonado_clp": 1240000,
100698
+ * "nuevos_presentados": 5,
100699
+ * "nuevos_aceptados": 3
100700
+ * },
100701
+ * "by_professional": [
100702
+ * {
100703
+ * "professional_id": "6b1f3c2e-8a4d-4f0e-9c1b-2d7e5a9f0c31",
100704
+ * "professional_name": "Ana Rojas",
100705
+ * "produccion_clp": 3685671,
100706
+ * "prestaciones": 52,
100707
+ * "presupuestos": 14,
100708
+ * "presupuestado_clp": 5120000,
100709
+ * "aceptados": 9,
100710
+ * "aceptacion_pct": 64.3,
100711
+ * "aceptado_clp": 3010000,
100712
+ * "abonado_clp": 1240000,
100713
+ * "nuevos_presentados": 5,
100714
+ * "nuevos_aceptados": 3
98322
100715
  * }
98323
100716
  * ]
98324
100717
  * }
98325
100718
  * }
98326
100719
  */
98327
100720
  "application/json": {
98328
- data?: unknown;
98329
- meta?: {
98330
- [key: string]: unknown;
98331
- };
100721
+ data: components["schemas"]["ClinicIngresosReport"];
98332
100722
  };
98333
100723
  };
98334
100724
  };
@@ -98396,7 +100786,7 @@ interface paths {
98396
100786
  patch?: never;
98397
100787
  trace?: never;
98398
100788
  };
98399
- "/clinic/contacts/{contactId}/auto-links": {
100789
+ "/clinic/insights/ingresos/months": {
98400
100790
  parameters: {
98401
100791
  query?: never;
98402
100792
  header?: never;
@@ -98404,23 +100794,25 @@ interface paths {
98404
100794
  cookie?: never;
98405
100795
  };
98406
100796
  /**
98407
- * Patients automatically linked to a contact (Ads identity only)
98408
- * @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; `chat_rut`: the patient's RUT typed by exactly one contact in its own ad chat, corroborated by the typed email or by the ficha appearing after it — 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.
100797
+ * Ingresos: doctor × month comparison
100798
+ * @description The last `months` calendar months (default 6, at most 12, the current one to today): per month the clinic's producción, recaudación, presupuestos, aceptación and monto aceptado; per doctor the same by month (producción by performer, the rest by the quoting professional over the presupuestos issued that month). Requires `clinic_money:read`.
98409
100799
  *
98410
- * **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.
100800
+ * **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`.
98411
100801
  */
98412
100802
  get: {
98413
100803
  parameters: {
98414
- query?: never;
98415
- header?: never;
98416
- path: {
98417
- contactId: string;
100804
+ query?: {
100805
+ /** @description Calendar months to compare, the current one included (default 6). */
100806
+ months?: number;
100807
+ location_id?: string;
98418
100808
  };
100809
+ header?: never;
100810
+ path?: never;
98419
100811
  cookie?: never;
98420
100812
  };
98421
100813
  requestBody?: never;
98422
100814
  responses: {
98423
- /** @description Contact auto-links */
100815
+ /** @description Clinic Ingresos by doctor and month */
98424
100816
  200: {
98425
100817
  headers: {
98426
100818
  [name: string]: unknown;
@@ -98429,31 +100821,49 @@ interface paths {
98429
100821
  /**
98430
100822
  * @example {
98431
100823
  * "data": {
98432
- * "data": [
100824
+ * "from": "2026-09-01",
100825
+ * "to": "2026-10-08",
100826
+ * "state": "ready",
100827
+ * "rules": {
100828
+ * "produccion": "Lo realizado en el box …",
100829
+ * "presupuestado": "Presupuestos creados en el día …",
100830
+ * "recaudacion": "Pagos recibidos en el día …",
100831
+ * "nuevos": "Paciente nuevo: su primera visita es en ese mismo mes …",
100832
+ * "aceptacion": "Aceptados con la misma regla de Números comerciales …",
100833
+ * "abonado": "Lo ya pagado a los presupuestos creados en el período …"
100834
+ * },
100835
+ * "months": [
98433
100836
  * {
98434
- * "id": "a7a7a7a7-0000-4000-8000-000000000001",
98435
- * "clinic_patient_id": "b1b0b8de-0000-4000-8000-000000000001",
98436
- * "contact_id": "1a73af9e-0000-4000-8000-000000000001",
98437
- * "patient_name": "María José Fuentes Lagos",
98438
- * "contact_name": "María José Fuentes Lagos",
98439
- * "status": "live",
98440
- * "method": "phone_unique",
98441
- * "matched_at": "2026-09-27T14:10:02.118Z",
98442
- * "run_id": "a8a8a8a8-0000-4000-8000-000000000001",
98443
- * "revoked_at": null,
98444
- * "revoked_reason": null,
98445
- * "sent_count": 3,
98446
- * "last_sent_at": "2026-09-28T12:02:40.551Z"
100837
+ * "month": "2026-09",
100838
+ * "produccion_clp": 57179516,
100839
+ * "recaudacion_clp": 57172232,
100840
+ * "presupuestos": 278,
100841
+ * "presupuestado_clp": 112745314,
100842
+ * "aceptacion_pct": 63.1,
100843
+ * "aceptado_clp": 51093178
100844
+ * }
100845
+ * ],
100846
+ * "professionals": [
100847
+ * {
100848
+ * "professional_id": "6b1f3c2e-8a4d-4f0e-9c1b-2d7e5a9f0c31",
100849
+ * "professional_name": "Ana Rojas",
100850
+ * "months": [
100851
+ * {
100852
+ * "month": "2026-09",
100853
+ * "produccion_clp": 9623135,
100854
+ * "presupuestos": 41,
100855
+ * "presupuestado_clp": 18200000,
100856
+ * "aceptacion_pct": 68.3,
100857
+ * "aceptado_clp": 9100000
100858
+ * }
100859
+ * ]
98447
100860
  * }
98448
100861
  * ]
98449
100862
  * }
98450
100863
  * }
98451
100864
  */
98452
100865
  "application/json": {
98453
- data?: unknown;
98454
- meta?: {
98455
- [key: string]: unknown;
98456
- };
100866
+ data: components["schemas"]["ClinicIngresosMonths"];
98457
100867
  };
98458
100868
  };
98459
100869
  };
@@ -98521,41 +100931,31 @@ interface paths {
98521
100931
  patch?: never;
98522
100932
  trace?: never;
98523
100933
  };
98524
- "/clinic/auto-links/{id}/reject": {
100934
+ "/clinic/insights/ingresos/days/{day}": {
98525
100935
  parameters: {
98526
100936
  query?: never;
98527
100937
  header?: never;
98528
100938
  path?: never;
98529
100939
  cookie?: never;
98530
100940
  };
98531
- get?: never;
98532
- put?: never;
98533
100941
  /**
98534
- * «No es esta persona» — reject an automatic link for good
98535
- * @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.
100942
+ * Ingresos: one day opened (its plans, payments and producción)
100943
+ * @description The plans issued and payments received on one clinic day, each marked `nuevo` / `tratamiento`, plans with the Números comerciales verdict, plus producción per performer. Patient names only for a caller allowed to see identity (`contacts:read` + `clinic:read`, plus `clinic_patients:read` for an API credential); otherwise `patient.name` is `null`. Requires `clinic_money:read`.
98536
100944
  *
98537
- * **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.
100945
+ * **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`.
98538
100946
  */
98539
- post: {
100947
+ get: {
98540
100948
  parameters: {
98541
100949
  query?: never;
98542
- header?: {
98543
- /** @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. */
98544
- "Idempotency-Key"?: string;
98545
- };
100950
+ header?: never;
98546
100951
  path: {
98547
- id: string;
100952
+ day: string;
98548
100953
  };
98549
100954
  cookie?: never;
98550
100955
  };
98551
- requestBody?: {
98552
- content: {
98553
- /** @example {} */
98554
- "application/json": Record<string, never>;
98555
- };
98556
- };
100956
+ requestBody?: never;
98557
100957
  responses: {
98558
- /** @description Rejected auto-link */
100958
+ /** @description One clinic day of Ingresos */
98559
100959
  200: {
98560
100960
  headers: {
98561
100961
  [name: string]: unknown;
@@ -98564,28 +100964,49 @@ interface paths {
98564
100964
  /**
98565
100965
  * @example {
98566
100966
  * "data": {
98567
- * "id": "a7a7a7a7-0000-4000-8000-000000000001",
98568
- * "clinic_patient_id": "b1b0b8de-0000-4000-8000-000000000001",
98569
- * "contact_id": "1a73af9e-0000-4000-8000-000000000001",
98570
- * "patient_name": "María José Fuentes Lagos",
98571
- * "contact_name": "María José Fuentes Lagos",
98572
- * "status": "rejected",
98573
- * "method": "phone_unique",
98574
- * "matched_at": "2026-09-27T14:10:02.118Z",
98575
- * "run_id": "a8a8a8a8-0000-4000-8000-000000000001",
98576
- * "revoked_at": "2026-09-28T15:31:09.004Z",
98577
- * "revoked_reason": "rejected",
98578
- * "sent_count": 3,
98579
- * "last_sent_at": "2026-09-28T12:02:40.551Z",
98580
- * "withheld_outcomes": 1
100967
+ * "day": "2026-10-08",
100968
+ * "identity": true,
100969
+ * "plans": [
100970
+ * {
100971
+ * "plan_id": "1d2c3b4a-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
100972
+ * "patient": {
100973
+ * "id": "7e6d5c4b-3a29-4180-9f8e-7d6c5b4a3928",
100974
+ * "name": "Paciente de ejemplo"
100975
+ * },
100976
+ * "professional_id": "6b1f3c2e-8a4d-4f0e-9c1b-2d7e5a9f0c31",
100977
+ * "professional_name": "Ana Rojas",
100978
+ * "total_clp": 640000,
100979
+ * "abonado_clp": 120000,
100980
+ * "deuda_clp": 520000,
100981
+ * "tipo": "nuevo",
100982
+ * "aceptado": true
100983
+ * }
100984
+ * ],
100985
+ * "payments": [
100986
+ * {
100987
+ * "payment_id": "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
100988
+ * "time": "11:42",
100989
+ * "patient": {
100990
+ * "id": "7e6d5c4b-3a29-4180-9f8e-7d6c5b4a3928",
100991
+ * "name": "Paciente de ejemplo"
100992
+ * },
100993
+ * "amount_clp": 120000,
100994
+ * "tipo": "nuevo"
100995
+ * }
100996
+ * ],
100997
+ * "produccion": [
100998
+ * {
100999
+ * "professional_id": "6b1f3c2e-8a4d-4f0e-9c1b-2d7e5a9f0c31",
101000
+ * "professional_name": "Ana Rojas",
101001
+ * "prestaciones": 9,
101002
+ * "produccion_clp": 812000
101003
+ * }
101004
+ * ]
98581
101005
  * }
98582
101006
  * }
98583
101007
  */
98584
101008
  "application/json": {
98585
- data?: unknown;
98586
- meta?: {
98587
- [key: string]: unknown;
98588
- };
101009
+ data: components["schemas"]["ClinicIngresosDay"];
98589
101010
  };
98590
101011
  };
98591
101012
  };
@@ -98645,6 +101066,8 @@ interface paths {
98645
101066
  };
98646
101067
  };
98647
101068
  };
101069
+ put?: never;
101070
+ post?: never;
98648
101071
  delete?: never;
98649
101072
  options?: never;
98650
101073
  head?: never;
@@ -99366,7 +101789,7 @@ interface paths {
99366
101789
  };
99367
101790
  /**
99368
101791
  * The month's Meta mensual and its progress
99369
- * @description Requires `clinic_money:read`. The owner's three targets for the month (primeras visitas booked, cierres, cobrado in CLP; `null` = not set) with what has been achieved so far: primeras visitas = citas created in the month whose Tipo de agendamiento is Nuevo or Nuevo · reagenda; cierres as Números comerciales counts them; cobrado = payments received. For the current month, `pace` carries the ritmo diario. The ritmo diario is a catch-up pace: (meta − achieved before today) ÷ working days left, today included. Working days come from the clinic's opening hours and holidays, in the clinic's timezone. A figure with no target has a count and no pace. This is not the Ads goal, which is marketing's own.
101792
+ * @description Requires `clinic_money:read`. The owner's three targets for the month (primeras visitas booked, cierres, cobrado in CLP; `null` = not set) with what has been achieved so far: primeras visitas = citas created in the month whose Tipo de agendamiento is Nuevo or Nuevo · reagenda; cierres as Números comerciales counts them; cobrado = payments received. For the current month, `pace` carries the ritmo diario. The ritmo diario is a catch-up pace: (meta − achieved before today) ÷ working days left, today included, each day weighed by its opening hours against the clinic's longest day (a 9–14 Saturday next to 9–19 weekdays weighs 0.5), times today's weight (`per_day`). Working days come from the clinic's opening hours and holidays, in the clinic's timezone. A figure with no target has a count and no pace. This is not the Ads goal, which is marketing's own.
99370
101793
  *
99371
101794
  * **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`.
99372
101795
  */
@@ -99409,6 +101832,20 @@ interface paths {
99409
101832
  * "total": 21,
99410
101833
  * "left": 10
99411
101834
  * },
101835
+ * "history": [
101836
+ * {
101837
+ * "date": "2026-10-01",
101838
+ * "booked": 108,
101839
+ * "primeras_visitas": 26,
101840
+ * "plata_clp": 1251950
101841
+ * },
101842
+ * {
101843
+ * "date": "2026-10-02",
101844
+ * "booked": 49,
101845
+ * "primeras_visitas": 11,
101846
+ * "plata_clp": 359086
101847
+ * }
101848
+ * ],
99412
101849
  * "pace": {
99413
101850
  * "primeras_visitas": {
99414
101851
  * "status": "open",
@@ -99416,6 +101853,8 @@ interface paths {
99416
101853
  * "achieved_before_today": 18,
99417
101854
  * "remaining": 22,
99418
101855
  * "working_days_left": 10,
101856
+ * "weighted_days_left": 9.5,
101857
+ * "day_weight": 1,
99419
101858
  * "per_day": 3
99420
101859
  * },
99421
101860
  * "cierres": null,
@@ -99425,7 +101864,9 @@ interface paths {
99425
101864
  * "achieved_before_today": 7450000,
99426
101865
  * "remaining": 10550000,
99427
101866
  * "working_days_left": 10,
99428
- * "per_day": 1055000
101867
+ * "weighted_days_left": 9.5,
101868
+ * "day_weight": 1,
101869
+ * "per_day": 1110527
99429
101870
  * }
99430
101871
  * }
99431
101872
  * }
@@ -99452,6 +101893,12 @@ interface paths {
99452
101893
  total: number;
99453
101894
  left: number;
99454
101895
  };
101896
+ history: {
101897
+ date: string;
101898
+ booked: number;
101899
+ primeras_visitas: number;
101900
+ plata_clp: number | null;
101901
+ }[];
99455
101902
  pace: {
99456
101903
  primeras_visitas: {
99457
101904
  /** @enum {string} */
@@ -99460,6 +101907,8 @@ interface paths {
99460
101907
  achieved_before_today: number;
99461
101908
  remaining: number;
99462
101909
  working_days_left: number;
101910
+ weighted_days_left: number;
101911
+ day_weight: number;
99463
101912
  per_day: number | null;
99464
101913
  } | null;
99465
101914
  cierres: {
@@ -99469,6 +101918,8 @@ interface paths {
99469
101918
  achieved_before_today: number;
99470
101919
  remaining: number;
99471
101920
  working_days_left: number;
101921
+ weighted_days_left: number;
101922
+ day_weight: number;
99472
101923
  per_day: number | null;
99473
101924
  } | null;
99474
101925
  cobrado_clp: {
@@ -99478,6 +101929,8 @@ interface paths {
99478
101929
  achieved_before_today: number;
99479
101930
  remaining: number;
99480
101931
  working_days_left: number;
101932
+ weighted_days_left: number;
101933
+ day_weight: number;
99481
101934
  per_day: number | null;
99482
101935
  } | null;
99483
101936
  } | null;
@@ -99601,6 +102054,20 @@ interface paths {
99601
102054
  * "total": 21,
99602
102055
  * "left": 10
99603
102056
  * },
102057
+ * "history": [
102058
+ * {
102059
+ * "date": "2026-10-01",
102060
+ * "booked": 108,
102061
+ * "primeras_visitas": 26,
102062
+ * "plata_clp": 1251950
102063
+ * },
102064
+ * {
102065
+ * "date": "2026-10-02",
102066
+ * "booked": 49,
102067
+ * "primeras_visitas": 11,
102068
+ * "plata_clp": 359086
102069
+ * }
102070
+ * ],
99604
102071
  * "pace": {
99605
102072
  * "primeras_visitas": {
99606
102073
  * "status": "open",
@@ -99608,6 +102075,8 @@ interface paths {
99608
102075
  * "achieved_before_today": 18,
99609
102076
  * "remaining": 22,
99610
102077
  * "working_days_left": 10,
102078
+ * "weighted_days_left": 9.5,
102079
+ * "day_weight": 1,
99611
102080
  * "per_day": 3
99612
102081
  * },
99613
102082
  * "cierres": null,
@@ -99617,7 +102086,9 @@ interface paths {
99617
102086
  * "achieved_before_today": 7450000,
99618
102087
  * "remaining": 10550000,
99619
102088
  * "working_days_left": 10,
99620
- * "per_day": 1055000
102089
+ * "weighted_days_left": 9.5,
102090
+ * "day_weight": 1,
102091
+ * "per_day": 1110527
99621
102092
  * }
99622
102093
  * }
99623
102094
  * }
@@ -99644,6 +102115,12 @@ interface paths {
99644
102115
  total: number;
99645
102116
  left: number;
99646
102117
  };
102118
+ history: {
102119
+ date: string;
102120
+ booked: number;
102121
+ primeras_visitas: number;
102122
+ plata_clp: number | null;
102123
+ }[];
99647
102124
  pace: {
99648
102125
  primeras_visitas: {
99649
102126
  /** @enum {string} */
@@ -99652,6 +102129,8 @@ interface paths {
99652
102129
  achieved_before_today: number;
99653
102130
  remaining: number;
99654
102131
  working_days_left: number;
102132
+ weighted_days_left: number;
102133
+ day_weight: number;
99655
102134
  per_day: number | null;
99656
102135
  } | null;
99657
102136
  cierres: {
@@ -99661,6 +102140,8 @@ interface paths {
99661
102140
  achieved_before_today: number;
99662
102141
  remaining: number;
99663
102142
  working_days_left: number;
102143
+ weighted_days_left: number;
102144
+ day_weight: number;
99664
102145
  per_day: number | null;
99665
102146
  } | null;
99666
102147
  cobrado_clp: {
@@ -99670,6 +102151,8 @@ interface paths {
99670
102151
  achieved_before_today: number;
99671
102152
  remaining: number;
99672
102153
  working_days_left: number;
102154
+ weighted_days_left: number;
102155
+ day_weight: number;
99673
102156
  per_day: number | null;
99674
102157
  } | null;
99675
102158
  } | null;
@@ -99749,7 +102232,7 @@ interface paths {
99749
102232
  };
99750
102233
  /**
99751
102234
  * The «Hoy» strip: is today on track?
99752
- * @description Requires `clinic:read` or `clinic_money:read`; cobrado figures need `clinic_money:read` and are `null` without it. Bookings made today (citas created today, for any date) with their Tipo de agendamiento split, the bookings of the last hour, the same counts for the previous working day, cobrado today and the part from new patients (those whose cycle began with a primera visita), and the paces of today's month. The ritmo diario is a catch-up pace: (meta − achieved before today) ÷ working days left, today included. Working days come from the clinic's opening hours and holidays, in the clinic's timezone. A figure with no target has a count and no pace. The pace of cobrado is the Meta mensual's, set against the clinic's total cobrado.
102235
+ * @description Requires `clinic:read` or `clinic_money:read`; cobrado figures need `clinic_money:read` and are `null` without it. Bookings made today (citas created today, for any date; an anulación is not a booking; a moved cita stays a booking of the day it was made, and its replacement counts on the day it is re-booked) with their Tipo de agendamiento judged at the moment of booking, the bookings of the last hour, the same counts for the previous working day, cobrado today and the part from new patients (patients with a primera visita this month; `null` when the clinic's vendor has no payment mirror, e.g. Reservo), and the paces of today's month. The ritmo diario is a catch-up pace: (meta − achieved before today) ÷ working days left, today included, each day weighed by its opening hours against the clinic's longest day (a 9–14 Saturday next to 9–19 weekdays weighs 0.5), times today's weight (`per_day`). Working days come from the clinic's opening hours and holidays, in the clinic's timezone. A figure with no target has a count and no pace. The pace of cobrado is the Meta mensual's, set against the clinic's total cobrado.
99753
102236
  *
99754
102237
  * **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`.
99755
102238
  */
@@ -99789,6 +102272,32 @@ interface paths {
99789
102272
  * "total": 2,
99790
102273
  * "primeras_visitas": 1
99791
102274
  * },
102275
+ * "by_hour": [
102276
+ * {
102277
+ * "hour": 9,
102278
+ * "total": 3,
102279
+ * "primeras_visitas": 1
102280
+ * },
102281
+ * {
102282
+ * "hour": 11,
102283
+ * "total": 4,
102284
+ * "primeras_visitas": 2
102285
+ * },
102286
+ * {
102287
+ * "hour": 14,
102288
+ * "total": 2,
102289
+ * "primeras_visitas": 1
102290
+ * }
102291
+ * ],
102292
+ * "origin": {
102293
+ * "meta": 2,
102294
+ * "organic": 1,
102295
+ * "unknown": 1
102296
+ * },
102297
+ * "from_whatsapp": {
102298
+ * "total": 3,
102299
+ * "primeras_visitas": 2
102300
+ * },
99792
102301
  * "cobrado_clp": 640000,
99793
102302
  * "cobrado_new_patients_clp": 90000
99794
102303
  * },
@@ -99815,6 +102324,8 @@ interface paths {
99815
102324
  * "achieved_before_today": 18,
99816
102325
  * "remaining": 22,
99817
102326
  * "working_days_left": 10,
102327
+ * "weighted_days_left": 9.5,
102328
+ * "day_weight": 1,
99818
102329
  * "per_day": 3
99819
102330
  * },
99820
102331
  * "cobrado_clp": null
@@ -99844,6 +102355,20 @@ interface paths {
99844
102355
  total: number;
99845
102356
  primeras_visitas: number;
99846
102357
  };
102358
+ by_hour: {
102359
+ hour: number;
102360
+ total: number;
102361
+ primeras_visitas: number;
102362
+ }[];
102363
+ origin: {
102364
+ meta: number;
102365
+ organic: number;
102366
+ unknown: number;
102367
+ };
102368
+ from_whatsapp: {
102369
+ total: number;
102370
+ primeras_visitas: number;
102371
+ };
99847
102372
  };
99848
102373
  previous_working_day: {
99849
102374
  date: string;
@@ -99869,6 +102394,8 @@ interface paths {
99869
102394
  achieved_before_today: number;
99870
102395
  remaining: number;
99871
102396
  working_days_left: number;
102397
+ weighted_days_left: number;
102398
+ day_weight: number;
99872
102399
  per_day: number | null;
99873
102400
  } | null;
99874
102401
  cobrado_clp: {
@@ -99878,6 +102405,8 @@ interface paths {
99878
102405
  achieved_before_today: number;
99879
102406
  remaining: number;
99880
102407
  working_days_left: number;
102408
+ weighted_days_left: number;
102409
+ day_weight: number;
99881
102410
  per_day: number | null;
99882
102411
  } | null;
99883
102412
  };
@@ -99949,6 +102478,172 @@ interface paths {
99949
102478
  patch?: never;
99950
102479
  trace?: never;
99951
102480
  };
102481
+ "/clinic/today/bookings": {
102482
+ parameters: {
102483
+ query?: never;
102484
+ header?: never;
102485
+ path?: never;
102486
+ cookie?: never;
102487
+ };
102488
+ /**
102489
+ * The citas behind the «Hoy» numbers
102490
+ * @description Requires `clinic:read`. The citas booked on one clinic day (`date`, default today; never a future day): when they were booked (the vendor's own creation time when the mirror carries it — Reservo — else when Vitrina first saw the cita, which is every Dentalink / Medilink cita), the hour of that in the clinic's clock, who the patient is, for when, with which professional, the Tipo de agendamiento judged as of the booking day, the Origen, whether the patient's linked contact had WhatsApp activity from two days before the booking day («Desde WhatsApp»), and the patient's last attended visit before that day. An anulación is not a booking; a moved cita («Cambio de fecha») stays a booking of the day it was made.
102491
+ *
102492
+ * **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`.
102493
+ */
102494
+ get: {
102495
+ parameters: {
102496
+ query?: {
102497
+ date?: string;
102498
+ };
102499
+ header?: never;
102500
+ path?: never;
102501
+ cookie?: never;
102502
+ };
102503
+ requestBody?: never;
102504
+ responses: {
102505
+ /** @description The day's bookings, in booking order */
102506
+ 200: {
102507
+ headers: {
102508
+ [name: string]: unknown;
102509
+ };
102510
+ content: {
102511
+ /**
102512
+ * @example {
102513
+ * "data": {
102514
+ * "date": "2026-10-13",
102515
+ * "timezone": "America/Santiago",
102516
+ * "rows": [
102517
+ * {
102518
+ * "appointment_id": "6f1c2c1e-1d2b-4c9a-9f3e-0a1b2c3d4e5f",
102519
+ * "booked_at": "2026-10-13T14:12:00.000Z",
102520
+ * "hour": 11,
102521
+ * "starts_at": "2026-10-20T13:00:00.000Z",
102522
+ * "patient": {
102523
+ * "id": "0d9e8f7a-6b5c-4d3e-2f1a-0b9c8d7e6f5a",
102524
+ * "name": "Paciente Ejemplo"
102525
+ * },
102526
+ * "professional_name": "Dra. Ejemplo",
102527
+ * "kind": "new",
102528
+ * "origin": null,
102529
+ * "from_whatsapp": true,
102530
+ * "last_visit": null
102531
+ * }
102532
+ * ]
102533
+ * }
102534
+ * }
102535
+ */
102536
+ "application/json": {
102537
+ data: {
102538
+ date: string;
102539
+ timezone: string;
102540
+ rows: {
102541
+ /** Format: uuid */
102542
+ appointment_id: string;
102543
+ booked_at: string;
102544
+ hour: number;
102545
+ starts_at: string;
102546
+ patient: {
102547
+ /** Format: uuid */
102548
+ id: string;
102549
+ name: string | null;
102550
+ } | null;
102551
+ professional_name: string | null;
102552
+ /** @enum {string|null} */
102553
+ kind: "new" | "new_rebooked" | "in_treatment" | null;
102554
+ /** @description Origen: the attribution Vitrina Ads reports (people from ads), asked of one person. A person with a linked contact and no ad touch is `organic`; one with no linked contact is `unknown`. Read-only. */
102555
+ origin: {
102556
+ /**
102557
+ * @description `meta`: came from a Meta ad. `organic`: a linked contact with no ad touch. `unknown` («Sin dato»): no contact is linked to the person, so no ad touch can be read.
102558
+ * @enum {string}
102559
+ */
102560
+ kind: "meta" | "organic" | "unknown";
102561
+ /** @description «Meta · Implantes octubre», «Meta» (ad name unknown), «Orgánico». */
102562
+ label: string;
102563
+ /** @description Meta only: the platform ad id (what `origin_ad_id` takes). */
102564
+ ad_id: string | null;
102565
+ ad_name: string | null;
102566
+ ad_set_name: string | null;
102567
+ campaign_name: string | null;
102568
+ /** @description Meta only: when they first wrote through the ad (or came back). */
102569
+ first_touch_at: string | null;
102570
+ /**
102571
+ * @description `first_message`: the first thing they wrote came from the ad. `returned`: they were already writing and came back through one. Both count as people from ads.
102572
+ * @enum {string|null}
102573
+ */
102574
+ via: "first_message" | "returned" | null;
102575
+ } | null;
102576
+ from_whatsapp: boolean;
102577
+ last_visit: string | null;
102578
+ }[];
102579
+ };
102580
+ };
102581
+ };
102582
+ };
102583
+ /** @description Validation error */
102584
+ 400: {
102585
+ headers: {
102586
+ [name: string]: unknown;
102587
+ };
102588
+ content: {
102589
+ "application/json": components["schemas"]["Error"];
102590
+ };
102591
+ };
102592
+ /** @description Unauthorized */
102593
+ 401: {
102594
+ headers: {
102595
+ [name: string]: unknown;
102596
+ };
102597
+ content: {
102598
+ "application/json": components["schemas"]["Error"];
102599
+ };
102600
+ };
102601
+ /** @description Forbidden */
102602
+ 403: {
102603
+ headers: {
102604
+ [name: string]: unknown;
102605
+ };
102606
+ content: {
102607
+ "application/json": components["schemas"]["Error"];
102608
+ };
102609
+ };
102610
+ /** @description Not found */
102611
+ 404: {
102612
+ headers: {
102613
+ [name: string]: unknown;
102614
+ };
102615
+ content: {
102616
+ "application/json": components["schemas"]["Error"];
102617
+ };
102618
+ };
102619
+ /** @description Conflict (incl. Idempotency-Key reuse with different body) */
102620
+ 409: {
102621
+ headers: {
102622
+ [name: string]: unknown;
102623
+ };
102624
+ content: {
102625
+ "application/json": components["schemas"]["Error"];
102626
+ };
102627
+ };
102628
+ /** @description Rate limited */
102629
+ 429: {
102630
+ headers: {
102631
+ [name: string]: unknown;
102632
+ };
102633
+ content: {
102634
+ "application/json": components["schemas"]["Error"];
102635
+ };
102636
+ };
102637
+ };
102638
+ };
102639
+ put?: never;
102640
+ post?: never;
102641
+ delete?: never;
102642
+ options?: never;
102643
+ head?: never;
102644
+ patch?: never;
102645
+ trace?: never;
102646
+ };
99952
102647
  "/ads/state": {
99953
102648
  parameters: {
99954
102649
  query?: never;
@@ -115848,7 +118543,7 @@ interface paths {
115848
118543
  };
115849
118544
  /**
115850
118545
  * Inspect the exact state the AI agent would receive
115851
- * @description Rebuilds, through the same assembler the worker uses, everything the agent sees on this conversation: the full system prompt, the gated tool catalogue, message history, temperature and step budget. The model and reasoning effort are platform-managed and not included. Read-only — no enrichment, no tool side effects. Answers a pretty-printed JSON **file download** (`Content-Disposition: attachment`), not the `{ data }` envelope the other reads use.
118546
+ * @description Rebuilds, through the same assembler the worker uses, everything the agent sees on this conversation: the full system prompt, the gated tool catalogue, message history, temperature and step budget. The model and reasoning effort are platform-managed and not included. Read-only — no enrichment, no tool side effects. Answers a JSON **file download** (`Content-Disposition: attachment`), not the `{ data }` envelope the other reads use. On a clinic, a connected app reads the patient in the prompt, history and tool results as a pseudonym unless the clinic allows patient names, like every other read.
115852
118547
  */
115853
118548
  get: {
115854
118549
  parameters: {
@@ -173447,6 +176142,277 @@ interface components {
173447
176142
  }[];
173448
176143
  }[];
173449
176144
  };
176145
+ ClinicProduccionReport: {
176146
+ from: string;
176147
+ to: string;
176148
+ /** Format: uuid */
176149
+ location_id: string | null;
176150
+ /** Format: uuid */
176151
+ professional_id: string | null;
176152
+ /**
176153
+ * @description Which arm produced the money. `mirror_lines`: a Dentalink / Medilink clinic — the vendor's plan lines marked realizado, by fecha de realización, at the line's total (exact). `service_lines`: the cita's frozen Vitrina lines (a native clinic). `catalog_price`: the catálogo's current price for the prestación a mirrored cita names (an estimate). `mixed`: both. `none`: nothing priced anything.
176154
+ * @enum {string}
176155
+ */
176156
+ basis: "service_lines" | "catalog_price" | "mixed" | "none" | "mirror_lines";
176157
+ totals: {
176158
+ /** @description Attended citas in the window. `null` only for a caller without `clinic_money:read`, when it rests on 1–4 cases. */
176159
+ attended: number | null;
176160
+ /** @description `null` only for a caller without `clinic_money:read`, when it rests on 1–4 cases. */
176161
+ lines: number | null;
176162
+ /** @description `null` only for a caller without `clinic_money:read`, when it rests on 1–4 cases. */
176163
+ lines_without_price: number | null;
176164
+ /** @description `null` only for a caller without `clinic_money:read`, when it rests on 1–4 cases. */
176165
+ performed_clp: number | null;
176166
+ /** @description `payment_allocation.appointment`: money the ledger allocated to these citas. `mirror_payment.received`: every live payment received in the window, by fecha de recepción; `null` with a professional filter (a payment names no professional). */
176167
+ collected_clp: number | null;
176168
+ /** @enum {string} */
176169
+ collected_basis: "payment_allocation.appointment" | "mirror_payment.received";
176170
+ };
176171
+ /** @description Per professional who PERFORMED the work (not who quoted the plan), most CLP first. */
176172
+ by_professional: {
176173
+ /** @description The professional / prestación id, or the categoría text; `__none__` when the line names none. */
176174
+ key: string;
176175
+ label: string | null;
176176
+ /** @description Citas that produced (`mirror_lines`: visits, patient × day). `null` only for a caller without `clinic_money:read`, when it rests on 1–4 cases. */
176177
+ appointments: number | null;
176178
+ /** @description Prestaciones performed. `null` only for a caller without `clinic_money:read`, when it rests on 1–4 cases. */
176179
+ lines: number | null;
176180
+ /** @description `null` only for a caller without `clinic_money:read`, when it rests on 1–4 cases. */
176181
+ performed_clp: number | null;
176182
+ }[];
176183
+ by_service: {
176184
+ /** @description The professional / prestación id, or the categoría text; `__none__` when the line names none. */
176185
+ key: string;
176186
+ label: string | null;
176187
+ /** @description Citas that produced (`mirror_lines`: visits, patient × day). `null` only for a caller without `clinic_money:read`, when it rests on 1–4 cases. */
176188
+ appointments: number | null;
176189
+ /** @description Prestaciones performed. `null` only for a caller without `clinic_money:read`, when it rests on 1–4 cases. */
176190
+ lines: number | null;
176191
+ /** @description `null` only for a caller without `clinic_money:read`, when it rests on 1–4 cases. */
176192
+ performed_clp: number | null;
176193
+ }[];
176194
+ /** @description `clinic_service.categoria` is free text: the label is the key. */
176195
+ by_category: {
176196
+ /** @description The professional / prestación id, or the categoría text; `__none__` when the line names none. */
176197
+ key: string;
176198
+ label: string | null;
176199
+ /** @description Citas that produced (`mirror_lines`: visits, patient × day). `null` only for a caller without `clinic_money:read`, when it rests on 1–4 cases. */
176200
+ appointments: number | null;
176201
+ /** @description Prestaciones performed. `null` only for a caller without `clinic_money:read`, when it rests on 1–4 cases. */
176202
+ lines: number | null;
176203
+ /** @description `null` only for a caller without `clinic_money:read`, when it rests on 1–4 cases. */
176204
+ performed_clp: number | null;
176205
+ }[];
176206
+ months: {
176207
+ /** @description `YYYY-MM`. */
176208
+ month: string;
176209
+ /** @description `null` only for a caller without `clinic_money:read`, when it rests on 1–4 cases. */
176210
+ attended: number | null;
176211
+ /** @description `null` only for a caller without `clinic_money:read`, when it rests on 1–4 cases. */
176212
+ performed_clp: number | null;
176213
+ collected_clp: number | null;
176214
+ }[];
176215
+ /** @description Present only for a caller without `clinic_money:read`. */
176216
+ suppression?: {
176217
+ min_count: number;
176218
+ filters_ignored: boolean;
176219
+ withheld_rows: {
176220
+ [key: string]: number;
176221
+ };
176222
+ };
176223
+ };
176224
+ ClinicIngresosReport: {
176225
+ from: string;
176226
+ to: string;
176227
+ today: string;
176228
+ timezone: string;
176229
+ /** Format: uuid */
176230
+ location_id: string | null;
176231
+ /** Format: uuid */
176232
+ professional_id: string | null;
176233
+ /** @enum {string} */
176234
+ state: "ready" | "unavailable";
176235
+ state_reason: string | null;
176236
+ source: {
176237
+ /** @enum {string} */
176238
+ kind: "mirror" | "none";
176239
+ synced_at: string | null;
176240
+ };
176241
+ rules: {
176242
+ [key: string]: string;
176243
+ };
176244
+ hoy: {
176245
+ day: string;
176246
+ produccion_clp: number;
176247
+ prestaciones: number;
176248
+ presupuestado_clp: number;
176249
+ presupuestos: number;
176250
+ recaudado_clp: number | null;
176251
+ pagos: number | null;
176252
+ };
176253
+ totals: {
176254
+ produccion: {
176255
+ total_clp: number;
176256
+ /** @description Patients whose primera visita (Nuevo / Nuevo · reagenda) starts in the same month. */
176257
+ nuevos_clp: number;
176258
+ tratamiento_clp: number;
176259
+ prestaciones: number;
176260
+ };
176261
+ presupuestado: {
176262
+ total_clp: number;
176263
+ /** @description Patients whose primera visita (Nuevo / Nuevo · reagenda) starts in the same month. */
176264
+ nuevos_clp: number;
176265
+ tratamiento_clp: number;
176266
+ presupuestos: number;
176267
+ };
176268
+ /** @description `null` with `professional_id`: a payment names no professional. */
176269
+ recaudacion: {
176270
+ total_clp: number;
176271
+ /** @description Patients whose primera visita (Nuevo / Nuevo · reagenda) starts in the same month. */
176272
+ nuevos_clp: number;
176273
+ tratamiento_clp: number;
176274
+ pagos: number;
176275
+ } | null;
176276
+ };
176277
+ days: {
176278
+ produccion: {
176279
+ total_clp: number;
176280
+ /** @description Patients whose primera visita (Nuevo / Nuevo · reagenda) starts in the same month. */
176281
+ nuevos_clp: number;
176282
+ tratamiento_clp: number;
176283
+ prestaciones: number;
176284
+ };
176285
+ presupuestado: {
176286
+ total_clp: number;
176287
+ /** @description Patients whose primera visita (Nuevo / Nuevo · reagenda) starts in the same month. */
176288
+ nuevos_clp: number;
176289
+ tratamiento_clp: number;
176290
+ presupuestos: number;
176291
+ };
176292
+ /** @description `null` with `professional_id`: a payment names no professional. */
176293
+ recaudacion: {
176294
+ total_clp: number;
176295
+ /** @description Patients whose primera visita (Nuevo / Nuevo · reagenda) starts in the same month. */
176296
+ nuevos_clp: number;
176297
+ tratamiento_clp: number;
176298
+ pagos: number;
176299
+ } | null;
176300
+ day: string;
176301
+ }[];
176302
+ clinic: {
176303
+ /** @description Performed by this doctor (the performer). */
176304
+ produccion_clp: number;
176305
+ prestaciones: number;
176306
+ /** @description Real presupuestos issued ($0 and evaluación-only out), by the quoting doctor. */
176307
+ presupuestos: number;
176308
+ /** @description Their value without evaluación lines. */
176309
+ presupuestado_clp: number;
176310
+ aceptados: number;
176311
+ aceptacion_pct: number | null;
176312
+ aceptado_clp: number;
176313
+ /** @description Already paid on the plans issued in the period. */
176314
+ abonado_clp: number;
176315
+ nuevos_presentados: number;
176316
+ nuevos_aceptados: number;
176317
+ };
176318
+ by_professional: {
176319
+ /** Format: uuid */
176320
+ professional_id: string;
176321
+ professional_name: string;
176322
+ /** @description Performed by this doctor (the performer). */
176323
+ produccion_clp: number;
176324
+ prestaciones: number;
176325
+ /** @description Real presupuestos issued ($0 and evaluación-only out), by the quoting doctor. */
176326
+ presupuestos: number;
176327
+ /** @description Their value without evaluación lines. */
176328
+ presupuestado_clp: number;
176329
+ aceptados: number;
176330
+ aceptacion_pct: number | null;
176331
+ aceptado_clp: number;
176332
+ /** @description Already paid on the plans issued in the period. */
176333
+ abonado_clp: number;
176334
+ nuevos_presentados: number;
176335
+ nuevos_aceptados: number;
176336
+ }[];
176337
+ };
176338
+ ClinicIngresosMonths: {
176339
+ from: string;
176340
+ to: string;
176341
+ /** @enum {string} */
176342
+ state: "ready" | "unavailable";
176343
+ rules: {
176344
+ [key: string]: string;
176345
+ };
176346
+ months: {
176347
+ /** @description `YYYY-MM`. */
176348
+ month: string;
176349
+ produccion_clp: number;
176350
+ presupuestos: number;
176351
+ presupuestado_clp: number;
176352
+ aceptacion_pct: number | null;
176353
+ aceptado_clp: number;
176354
+ recaudacion_clp: number;
176355
+ }[];
176356
+ professionals: {
176357
+ /** Format: uuid */
176358
+ professional_id: string;
176359
+ professional_name: string;
176360
+ months: {
176361
+ /** @description `YYYY-MM`. */
176362
+ month: string;
176363
+ produccion_clp: number;
176364
+ presupuestos: number;
176365
+ presupuestado_clp: number;
176366
+ aceptacion_pct: number | null;
176367
+ aceptado_clp: number;
176368
+ }[];
176369
+ }[];
176370
+ };
176371
+ ClinicIngresosDay: {
176372
+ day: string;
176373
+ identity: boolean;
176374
+ plans: {
176375
+ /** Format: uuid */
176376
+ plan_id: string;
176377
+ /** @description `name` only for a caller allowed to see patient identity (`contacts:read` + `clinic:read`, plus `clinic_patients:read` for an API credential). */
176378
+ patient: {
176379
+ /** Format: uuid */
176380
+ id: string | null;
176381
+ name: string | null;
176382
+ };
176383
+ /** Format: uuid */
176384
+ professional_id: string | null;
176385
+ professional_name: string | null;
176386
+ total_clp: number;
176387
+ abonado_clp: number;
176388
+ deuda_clp: number;
176389
+ /** @enum {string} */
176390
+ tipo: "nuevo" | "tratamiento";
176391
+ /** @description Números comerciales' verdict; `null` for a $0 or evaluación-only plan. */
176392
+ aceptado: boolean | null;
176393
+ }[];
176394
+ payments: {
176395
+ /** Format: uuid */
176396
+ payment_id: string;
176397
+ time: string | null;
176398
+ /** @description `name` only for a caller allowed to see patient identity (`contacts:read` + `clinic:read`, plus `clinic_patients:read` for an API credential). */
176399
+ patient: {
176400
+ /** Format: uuid */
176401
+ id: string | null;
176402
+ name: string | null;
176403
+ };
176404
+ amount_clp: number;
176405
+ /** @enum {string} */
176406
+ tipo: "nuevo" | "tratamiento";
176407
+ }[];
176408
+ produccion: {
176409
+ /** Format: uuid */
176410
+ professional_id: string | null;
176411
+ professional_name: string | null;
176412
+ prestaciones: number;
176413
+ produccion_clp: number;
176414
+ }[];
176415
+ };
173450
176416
  AgendaResults: {
173451
176417
  from: string;
173452
176418
  to: string;