@wevion/cli 1.0.3612 → 1.0.3682

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 (2) hide show
  1. package/openapi.json +3126 -561
  2. package/package.json +1 -1
package/openapi.json CHANGED
@@ -13213,6 +13213,12 @@
13213
13213
  "enum": [
13214
13214
  "outbrain"
13215
13215
  ]
13216
+ },
13217
+ {
13218
+ "type": "string",
13219
+ "enum": [
13220
+ "exoclick"
13221
+ ]
13216
13222
  }
13217
13223
  ]
13218
13224
  },
@@ -13506,6 +13512,12 @@
13506
13512
  "enum": [
13507
13513
  "outbrain"
13508
13514
  ]
13515
+ },
13516
+ {
13517
+ "type": "string",
13518
+ "enum": [
13519
+ "exoclick"
13520
+ ]
13509
13521
  }
13510
13522
  ]
13511
13523
  },
@@ -14791,6 +14803,12 @@
14791
14803
  "enum": [
14792
14804
  "outbrain"
14793
14805
  ]
14806
+ },
14807
+ {
14808
+ "type": "string",
14809
+ "enum": [
14810
+ "exoclick"
14811
+ ]
14794
14812
  }
14795
14813
  ]
14796
14814
  }
@@ -15094,6 +15112,12 @@
15094
15112
  "enum": [
15095
15113
  "outbrain"
15096
15114
  ]
15115
+ },
15116
+ {
15117
+ "type": "string",
15118
+ "enum": [
15119
+ "exoclick"
15120
+ ]
15097
15121
  }
15098
15122
  ]
15099
15123
  },
@@ -15376,6 +15400,12 @@
15376
15400
  "enum": [
15377
15401
  "outbrain"
15378
15402
  ]
15403
+ },
15404
+ {
15405
+ "type": "string",
15406
+ "enum": [
15407
+ "exoclick"
15408
+ ]
15379
15409
  }
15380
15410
  ]
15381
15411
  },
@@ -15582,6 +15612,12 @@
15582
15612
  "enum": [
15583
15613
  "outbrain"
15584
15614
  ]
15615
+ },
15616
+ {
15617
+ "type": "string",
15618
+ "enum": [
15619
+ "exoclick"
15620
+ ]
15585
15621
  }
15586
15622
  ]
15587
15623
  },
@@ -104546,6 +104582,12 @@
104546
104582
  "enum": [
104547
104583
  "outbrain"
104548
104584
  ]
104585
+ },
104586
+ {
104587
+ "type": "string",
104588
+ "enum": [
104589
+ "exoclick"
104590
+ ]
104549
104591
  }
104550
104592
  ]
104551
104593
  },
@@ -123989,7 +124031,7 @@
123989
124031
  "creative-hub",
123990
124032
  "creative-generation"
123991
124033
  ],
123992
- "description": "Starts an asynchronous cinematic image-to-video generation. BREAKING: a start-frame image_url is now REQUIRED for every provider and text-to-video is no longer supported. Returns queued generation metadata for polling. The resolution field was removed: provider output resolution is fixed and not configurable via the Higgsfield CLI (DA CONFERMARE in staging).",
124034
+ "description": "Starts an asynchronous cinematic image-to-video generation. BREAKING: a start-frame image_url is now REQUIRED for every provider and text-to-video is no longer supported. An optional end_image_url closes the clip on a second frame (accepted by the provider for kling3_0; how faithfully each model honours it is DA CONFERMARE). Returns queued generation metadata for polling. The resolution field was removed: provider output resolution is fixed and not configurable via the Higgsfield CLI (DA CONFERMARE in staging).",
123993
124035
  "requestBody": {
123994
124036
  "required": true,
123995
124037
  "content": {
@@ -124012,6 +124054,11 @@
124012
124054
  "maxLength": 2000,
124013
124055
  "type": "string"
124014
124056
  },
124057
+ "end_image_url": {
124058
+ "minLength": 1,
124059
+ "maxLength": 2000,
124060
+ "type": "string"
124061
+ },
124015
124062
  "provider": {
124016
124063
  "anyOf": [
124017
124064
  {
@@ -130142,6 +130189,12 @@
130142
130189
  "enum": [
130143
130190
  "outbrain"
130144
130191
  ]
130192
+ },
130193
+ {
130194
+ "type": "string",
130195
+ "enum": [
130196
+ "exoclick"
130197
+ ]
130145
130198
  }
130146
130199
  ]
130147
130200
  },
@@ -130488,6 +130541,12 @@
130488
130541
  "enum": [
130489
130542
  "outbrain"
130490
130543
  ]
130544
+ },
130545
+ {
130546
+ "type": "string",
130547
+ "enum": [
130548
+ "exoclick"
130549
+ ]
130491
130550
  }
130492
130551
  ]
130493
130552
  },
@@ -130878,6 +130937,12 @@
130878
130937
  "enum": [
130879
130938
  "outbrain"
130880
130939
  ]
130940
+ },
130941
+ {
130942
+ "type": "string",
130943
+ "enum": [
130944
+ "exoclick"
130945
+ ]
130881
130946
  }
130882
130947
  ]
130883
130948
  },
@@ -140729,6 +140794,12 @@
140729
140794
  "enum": [
140730
140795
  "outbrain"
140731
140796
  ]
140797
+ },
140798
+ {
140799
+ "type": "string",
140800
+ "enum": [
140801
+ "exoclick"
140802
+ ]
140732
140803
  }
140733
140804
  ]
140734
140805
  },
@@ -141804,6 +141875,12 @@
141804
141875
  "enum": [
141805
141876
  "outbrain"
141806
141877
  ]
141878
+ },
141879
+ {
141880
+ "type": "string",
141881
+ "enum": [
141882
+ "exoclick"
141883
+ ]
141807
141884
  }
141808
141885
  ]
141809
141886
  },
@@ -143185,6 +143262,12 @@
143185
143262
  "enum": [
143186
143263
  "outbrain"
143187
143264
  ]
143265
+ },
143266
+ {
143267
+ "type": "string",
143268
+ "enum": [
143269
+ "exoclick"
143270
+ ]
143188
143271
  }
143189
143272
  ]
143190
143273
  },
@@ -162070,7 +162153,7 @@
162070
162153
  "tags": [
162071
162154
  "partner"
162072
162155
  ],
162073
- "description": "Registra una candidatura al Programma Partner. ⚠️ Risponde 403 finché il commutatore `partner_track_b_enroll` è spento: la coda candidature è chiusa e NESSUNA pratica viene creata — il corpo del 403 è una costante che non dipende dai dati inviati. Rotta PUBBLICA e senza account: non crea alcuna user_session, alcuna organizzazione e alcuna email di verifica l’account nasce solo allapprovazione. ★ Risponde SEMPRE 202 con lo stesso corpo, anche se l’email è già candidata o appartiene a un partner già approvato: nessun endpoint di questo programma conferma mai l’esistenza di un’email. Il link con cui seguire la pratica arriva per email, non nella risposta. Due limiti, due comportamenti diversi: 5/h per IP risponde 429; 3/h per identità email è applicato in silenzio, perché un 429 sull’email sarebbe esso stesso l’oracolo che questa rotta evita. ★ Dal 27/08/2026 servono TRE campi: `full_name`, `email`, `tos_accepted` alla `tos_version` corrente. La spunta accetta le **regole di promozione**, non il contratto: quello si firma al primo prelievo (POST /api/v1/partner/consent). Tutto il resto — paese, corsia, dati fiscali, risposte di ramo — resta accettato ma non è più preteso: serve a essere PAGATI, e lo chiedono il profilo di incasso e lo sportello del prelievo. Un partner che non ha firmato promuove e matura commissioni; non le incassa.",
162156
+ "description": "Registra una candidatura al Programma Partner. ⚠️ Risponde 403 finché il commutatore `partner_track_b_enroll` è spento: la coda candidature è chiusa e NESSUNA pratica viene creata — il corpo del 403 è una costante che non dipende dai dati inviati. Rotta PUBBLICA: non richiede alcun login e non apre alcuna sessione nella risposta. L’account nasce all’APPROVAZIONE, e il commutatore `partner_track_b_auto_approve` decide quando arriva. Acceso: la candidatura viene approvata in questa stessa richiesta e l’account nasce qui, insieme a partner, codice e link referral, e l’email di conferma dice come entrare. Spento: l’account nasce quando un revisore approva, e intanto lemail porta il link con cui seguire la pratica. In ENTRAMBI gli stati, se l’indirizzo è già di un partner attivo NESSUNA pratica nasce e l’email porta al suo pannello. ★ Risponde SEMPRE 202 con lo stesso corpo, in tutti questi casi e anche se l’email è già candidata: nessun endpoint di questo programma conferma mai l’esistenza di un’email, e la risposta non dice quale percorso è avvenuto — link e istruzioni arrivano per email, non nella risposta. Due limiti, due comportamenti diversi: 5/h per IP risponde 429; 3/h per identità email è applicato in silenzio, perché un 429 sull’email sarebbe esso stesso l’oracolo che questa rotta evita. ★ Dal 27/08/2026 servono QUATTRO campi: `full_name`, `email`, `phone` (cellulare in forma internazionale) e `tos_accepted` alla `tos_version` corrente. La spunta accetta le **regole di promozione**, non il contratto: quello si firma al primo prelievo (POST /api/v1/partner/consent). Tutto il resto — paese, corsia, dati fiscali, risposte di ramo — resta accettato ma non è più preteso: serve a essere PAGATI, e lo chiedono il profilo di incasso e lo sportello del prelievo. Un partner che non ha firmato promuove e matura commissioni; non le incassa.",
162074
162157
  "requestBody": {
162075
162158
  "required": true,
162076
162159
  "content": {
@@ -163369,162 +163452,14 @@
163369
163452
  }
163370
163453
  }
163371
163454
  },
163372
- "/api/v1/referral/bridge-offer": {
163373
- "get": {
163374
- "operationId": "getPartnerBridgeOffer",
163375
- "summary": "L’offerta di passaggio al Binario B che mi aspetta",
163376
- "tags": [
163377
- "partner"
163378
- ],
163379
- "description": "L’offerta di passaggio al Programma Partner che aspetta chi chiama, se ce n’è una ancora da accettare. `204` quando non c’è nulla da accettare — indistintamente perché non esiste alcuna offerta o perché è già stata accettata: distinguere i due casi renderebbe questa rotta un oracolo sull’esistenza di un’offerta. La risposta porta il documento e la versione da accettare, mai il token della pratica.",
163380
- "security": [
163381
- {
163382
- "bearerAuth": []
163383
- }
163384
- ],
163385
- "responses": {
163386
- "200": {
163387
- "description": "L’offerta di passaggio al Binario B che aspetta chi chiama. ⚠️ Non porta il token della pratica: quello è la chiave di lettura del fascicolo e non ha ragione di uscire da qui.",
163388
- "content": {
163389
- "application/json": {
163390
- "schema": {
163391
- "additionalProperties": false,
163392
- "description": "L’offerta di passaggio al Binario B che aspetta chi chiama. ⚠️ Non porta il token della pratica: quello è la chiave di lettura del fascicolo e non ha ragione di uscire da qui.",
163393
- "type": "object",
163394
- "required": [
163395
- "country_code",
163396
- "offered_at",
163397
- "reason",
163398
- "tc_document_id",
163399
- "tc_version"
163400
- ],
163401
- "properties": {
163402
- "country_code": {
163403
- "description": "Il paese già dichiarato sulla pratica, o `null` se va ancora chiesto. ★ Oggi è sempre `null`: l’offerta la genera un cron del Binario A e nessuno ha compilato niente. Esce comunque perché è ciò che dice al modulo che quel campo è **da chiedere** — il giorno in cui un percorso saprà il paese in anticipo, lo stesso campo lo pre-compila senza toccare né la pagina né il contratto.",
163404
- "anyOf": [
163405
- {
163406
- "maxLength": 2,
163407
- "minLength": 2,
163408
- "type": "string"
163409
- },
163410
- {
163411
- "type": "null"
163412
- }
163413
- ]
163414
- },
163415
- "offered_at": {
163416
- "description": "Quando l’offerta è nata. È l’unica data che esiste finché non la si accetta.",
163417
- "format": "date-time",
163418
- "type": "string"
163419
- },
163420
- "reason": {
163421
- "description": "Perché è nata: `cap_reached` (tetto del Binario A) o `third_matured_conversion`.",
163422
- "anyOf": [
163423
- {
163424
- "type": "string"
163425
- },
163426
- {
163427
- "type": "null"
163428
- }
163429
- ]
163430
- },
163431
- "tc_document_id": {
163432
- "description": "Il documento da accettare. Oggi sempre `partnerTerms`.",
163433
- "type": "string"
163434
- },
163435
- "tc_version": {
163436
- "description": "La versione proposta: è quella che va rimandata all’accettazione.",
163437
- "type": "string"
163438
- }
163439
- }
163440
- }
163441
- }
163442
- }
163443
- },
163444
- "204": {
163445
- "description": "Nessuna offerta da accettare."
163446
- },
163447
- "401": {
163448
- "description": "Errore standard del backend.",
163449
- "content": {
163450
- "application/json": {
163451
- "schema": {
163452
- "additionalProperties": false,
163453
- "description": "Errore standard del backend.",
163454
- "type": "object",
163455
- "required": [
163456
- "error",
163457
- "message",
163458
- "statusCode"
163459
- ],
163460
- "properties": {
163461
- "code": {
163462
- "description": "Codice macchina dell’errore, quando disponibile.",
163463
- "type": "string"
163464
- },
163465
- "error": {
163466
- "description": "Etichetta breve dell’errore.",
163467
- "type": "string"
163468
- },
163469
- "message": {
163470
- "description": "Messaggio leggibile.",
163471
- "type": "string"
163472
- },
163473
- "statusCode": {
163474
- "description": "Codice di stato HTTP ripetuto nel corpo.",
163475
- "type": "integer"
163476
- }
163477
- }
163478
- }
163479
- }
163480
- }
163481
- },
163482
- "429": {
163483
- "description": "Errore standard del backend.",
163484
- "content": {
163485
- "application/json": {
163486
- "schema": {
163487
- "additionalProperties": false,
163488
- "description": "Errore standard del backend.",
163489
- "type": "object",
163490
- "required": [
163491
- "error",
163492
- "message",
163493
- "statusCode"
163494
- ],
163495
- "properties": {
163496
- "code": {
163497
- "description": "Codice macchina dell’errore, quando disponibile.",
163498
- "type": "string"
163499
- },
163500
- "error": {
163501
- "description": "Etichetta breve dell’errore.",
163502
- "type": "string"
163503
- },
163504
- "message": {
163505
- "description": "Messaggio leggibile.",
163506
- "type": "string"
163507
- },
163508
- "statusCode": {
163509
- "description": "Codice di stato HTTP ripetuto nel corpo.",
163510
- "type": "integer"
163511
- }
163512
- }
163513
- }
163514
- }
163515
- }
163516
- }
163517
- }
163518
- }
163519
- },
163520
- "/api/v1/referral/bridge-offer/accept": {
163455
+ "/api/v1/partner/become-customer": {
163521
163456
  "post": {
163522
- "operationId": "acceptPartnerBridgeOffer",
163523
- "summary": "Accetto i termini e passo al Binario B",
163457
+ "operationId": "partnerBecomeCustomer",
163458
+ "summary": "Da partner-only a cliente: attivo il prodotto sul mio account",
163524
163459
  "tags": [
163525
163460
  "partner"
163526
163461
  ],
163527
- "description": "Completa e accetta l’offerta di passaggio al Binario B. È l’ATTO che il ponte non aveva: porta il **paese** dichiarato (`country_code`, obbligatorio l’offerta nasce senza, perché a generarla è un cron) e la prova di consenso. registra `tc_accepted_at`, l’IP, lo user-agent, l’impronta del testo e la versione gli stessi cinque campi che la candidatura pubblica raccoglie, perché una prova di consenso non può avere due forme a seconda di dove entra — e porta la pratica in `received`, cioè nella coda normale. Finché questo atto non avviene l’approvazione risponde `409 tc_not_accepted` e il vincolo `ck_papp_approved_needs_consent` rifiuta comunque la transizione: nessuno può essere approvato senza aver accettato. `404` quando non c’è nulla da accettare (nessuna offerta, o già accettata: i due casi rispondono identico). `409 stale_version` quando la versione inviata non è quella corrente; `409 open_application_exists` quando per la stessa email esiste già una candidatura aperta l’offerta non può portarne una seconda, e dirlo non è un oracolo perché quella pratica è di chi chiede. `400 invalid_country` quando `country_code` non è due lettere: è l’unico rifiuto non anti-oracolo di questa rotta, deciso prima di cercare l’offerta — un `404` al suo posto direbbe «non c’è niente per te» mentre manca solo un campo. `403` quando il programma è chiuso: accettare a porta chiusa scriverebbe un consenso write-once su un percorso che non può proseguire.",
163462
+ "description": "Trasforma la sessione partner-only che chiama in un account cliente: in una sola transazione il ruolo passa da `partner` a `owner` e nascono organizzazione, team e permesso di titolarità lo stesso provisioning del signup self-service. La riga `partner` resta com’è: l’account diventa un partner ibrido, con il pannello ancora raggiungibile da `/partner` e `partner_only: false` su `/auth/me` dalla chiamata successiva. Agisce SOLO sull’utente autenticato: nessun input nomina un utente, e sotto impersonificazione (`x-impersonate-user`) risponde `403 PARTNER_IMPERSONATION_DENIED` prima di ogni scrittura, perché `request.user` sarebbe il bersaglio. `409 PARTNER_ALREADY_CUSTOMER` quando il ruolo non è `partner` cioè il prodotto è già di chi chiama, anche per la seconda di due richieste concorrenti. `409 PARTNER_NOT_ACTIVE` se la posizione partner non è `approved` (sospesa o chiusa): il prodotto non si apre da lì. `404 PARTNER_USER_NOT_FOUND` se l’account non esiste più. Il cambio di ruolo è registrato in `audit_log` (`partner.became_customer`).",
163528
163463
  "requestBody": {
163529
163464
  "required": true,
163530
163465
  "content": {
@@ -163533,22 +163468,25 @@
163533
163468
  "additionalProperties": false,
163534
163469
  "type": "object",
163535
163470
  "required": [
163536
- "country_code",
163537
- "tos_version"
163471
+ "plan"
163538
163472
  ],
163539
163473
  "properties": {
163540
- "country_code": {
163541
- "description": "Il paese ISO-3166-1 alpha-2 in cui il partner è stabilito. ★ **Obbligatorio, e senza ripiego**: l’offerta di ponte nasce con `country_code` vuoto perché a generarla è un cron, e questa è l’unica porta da cui può arrivarne uno dichiarato. Non è un’etichetta: all’approvazione viene copiato in `partner.country_code`, e da `requiredChecksForLevel` decide se VIES è un controllo KYC dovuto o va sostituito da `company_registry`. Un valore indovinato sceglierebbe una verifica antiriciclaggio al posto di chi la deve subire.",
163542
- "maxLength": 2,
163543
- "minLength": 2,
163544
- "pattern": "^[A-Za-z]{2}$",
163545
- "type": "string"
163546
- },
163547
- "tos_version": {
163548
- "description": "La versione dei T&C partner che si sta accettando. Deve essere quella CORRENTE: accettare una versione vecchia scriverebbe una prova di consenso su un testo diverso da quello mostrato.",
163549
- "maxLength": 20,
163550
- "minLength": 1,
163551
- "type": "string"
163474
+ "plan": {
163475
+ "description": "Come si vuole iniziare: `free` (nessuna carta) o `trial` (prova gratuita su un piano a pagamento, carta richiesta). Non cambia ciò che questa rotta scrive ruolo e organizzazione sono gli stessi ma finisce nell’audit e decide dove `next` porta: la pagina di onboarding onora la scelta senza richiederla.",
163476
+ "anyOf": [
163477
+ {
163478
+ "type": "string",
163479
+ "enum": [
163480
+ "free"
163481
+ ]
163482
+ },
163483
+ {
163484
+ "type": "string",
163485
+ "enum": [
163486
+ "trial"
163487
+ ]
163488
+ }
163489
+ ]
163552
163490
  }
163553
163491
  }
163554
163492
  }
@@ -163569,14 +163507,447 @@
163569
163507
  "additionalProperties": false,
163570
163508
  "type": "object",
163571
163509
  "required": [
163572
- "status"
163510
+ "next",
163511
+ "role"
163573
163512
  ],
163574
163513
  "properties": {
163575
- "status": {
163576
- "description": "La pratica è entrata nella coda normale: da qui in poi è una candidatura come le altre.",
163514
+ "next": {
163515
+ "description": "Dove la SPA porta l’utente subito dopo: la pagina di onboarding del prodotto, con il piano scelto nella query. Lì si raccolgono i dati aziendali e l’accettazione dei termini di prodotto — chi arriva da qui ha accettato solo quelli del Programma Partner.",
163516
+ "type": "string"
163517
+ },
163518
+ "role": {
163519
+ "description": "Il ruolo di prodotto appena scritto sulla sessione. Da qui in poi `/auth/me` lo riporta.",
163577
163520
  "type": "string",
163578
163521
  "enum": [
163579
- "received"
163522
+ "owner"
163523
+ ]
163524
+ }
163525
+ }
163526
+ }
163527
+ }
163528
+ }
163529
+ },
163530
+ "400": {
163531
+ "description": "Errore standard del backend.",
163532
+ "content": {
163533
+ "application/json": {
163534
+ "schema": {
163535
+ "additionalProperties": false,
163536
+ "description": "Errore standard del backend.",
163537
+ "type": "object",
163538
+ "required": [
163539
+ "error",
163540
+ "message",
163541
+ "statusCode"
163542
+ ],
163543
+ "properties": {
163544
+ "code": {
163545
+ "description": "Codice macchina dell’errore, quando disponibile.",
163546
+ "type": "string"
163547
+ },
163548
+ "error": {
163549
+ "description": "Etichetta breve dell’errore.",
163550
+ "type": "string"
163551
+ },
163552
+ "message": {
163553
+ "description": "Messaggio leggibile.",
163554
+ "type": "string"
163555
+ },
163556
+ "statusCode": {
163557
+ "description": "Codice di stato HTTP ripetuto nel corpo.",
163558
+ "type": "integer"
163559
+ }
163560
+ }
163561
+ }
163562
+ }
163563
+ }
163564
+ },
163565
+ "401": {
163566
+ "description": "Errore standard del backend.",
163567
+ "content": {
163568
+ "application/json": {
163569
+ "schema": {
163570
+ "additionalProperties": false,
163571
+ "description": "Errore standard del backend.",
163572
+ "type": "object",
163573
+ "required": [
163574
+ "error",
163575
+ "message",
163576
+ "statusCode"
163577
+ ],
163578
+ "properties": {
163579
+ "code": {
163580
+ "description": "Codice macchina dell’errore, quando disponibile.",
163581
+ "type": "string"
163582
+ },
163583
+ "error": {
163584
+ "description": "Etichetta breve dell’errore.",
163585
+ "type": "string"
163586
+ },
163587
+ "message": {
163588
+ "description": "Messaggio leggibile.",
163589
+ "type": "string"
163590
+ },
163591
+ "statusCode": {
163592
+ "description": "Codice di stato HTTP ripetuto nel corpo.",
163593
+ "type": "integer"
163594
+ }
163595
+ }
163596
+ }
163597
+ }
163598
+ }
163599
+ },
163600
+ "403": {
163601
+ "description": "Errore standard del backend.",
163602
+ "content": {
163603
+ "application/json": {
163604
+ "schema": {
163605
+ "additionalProperties": false,
163606
+ "description": "Errore standard del backend.",
163607
+ "type": "object",
163608
+ "required": [
163609
+ "error",
163610
+ "message",
163611
+ "statusCode"
163612
+ ],
163613
+ "properties": {
163614
+ "code": {
163615
+ "description": "Codice macchina dell’errore, quando disponibile.",
163616
+ "type": "string"
163617
+ },
163618
+ "error": {
163619
+ "description": "Etichetta breve dell’errore.",
163620
+ "type": "string"
163621
+ },
163622
+ "message": {
163623
+ "description": "Messaggio leggibile.",
163624
+ "type": "string"
163625
+ },
163626
+ "statusCode": {
163627
+ "description": "Codice di stato HTTP ripetuto nel corpo.",
163628
+ "type": "integer"
163629
+ }
163630
+ }
163631
+ }
163632
+ }
163633
+ }
163634
+ },
163635
+ "404": {
163636
+ "description": "Errore standard del backend.",
163637
+ "content": {
163638
+ "application/json": {
163639
+ "schema": {
163640
+ "additionalProperties": false,
163641
+ "description": "Errore standard del backend.",
163642
+ "type": "object",
163643
+ "required": [
163644
+ "error",
163645
+ "message",
163646
+ "statusCode"
163647
+ ],
163648
+ "properties": {
163649
+ "code": {
163650
+ "description": "Codice macchina dell’errore, quando disponibile.",
163651
+ "type": "string"
163652
+ },
163653
+ "error": {
163654
+ "description": "Etichetta breve dell’errore.",
163655
+ "type": "string"
163656
+ },
163657
+ "message": {
163658
+ "description": "Messaggio leggibile.",
163659
+ "type": "string"
163660
+ },
163661
+ "statusCode": {
163662
+ "description": "Codice di stato HTTP ripetuto nel corpo.",
163663
+ "type": "integer"
163664
+ }
163665
+ }
163666
+ }
163667
+ }
163668
+ }
163669
+ },
163670
+ "409": {
163671
+ "description": "Errore standard del backend.",
163672
+ "content": {
163673
+ "application/json": {
163674
+ "schema": {
163675
+ "additionalProperties": false,
163676
+ "description": "Errore standard del backend.",
163677
+ "type": "object",
163678
+ "required": [
163679
+ "error",
163680
+ "message",
163681
+ "statusCode"
163682
+ ],
163683
+ "properties": {
163684
+ "code": {
163685
+ "description": "Codice macchina dell’errore, quando disponibile.",
163686
+ "type": "string"
163687
+ },
163688
+ "error": {
163689
+ "description": "Etichetta breve dell’errore.",
163690
+ "type": "string"
163691
+ },
163692
+ "message": {
163693
+ "description": "Messaggio leggibile.",
163694
+ "type": "string"
163695
+ },
163696
+ "statusCode": {
163697
+ "description": "Codice di stato HTTP ripetuto nel corpo.",
163698
+ "type": "integer"
163699
+ }
163700
+ }
163701
+ }
163702
+ }
163703
+ }
163704
+ },
163705
+ "429": {
163706
+ "description": "Errore standard del backend.",
163707
+ "content": {
163708
+ "application/json": {
163709
+ "schema": {
163710
+ "additionalProperties": false,
163711
+ "description": "Errore standard del backend.",
163712
+ "type": "object",
163713
+ "required": [
163714
+ "error",
163715
+ "message",
163716
+ "statusCode"
163717
+ ],
163718
+ "properties": {
163719
+ "code": {
163720
+ "description": "Codice macchina dell’errore, quando disponibile.",
163721
+ "type": "string"
163722
+ },
163723
+ "error": {
163724
+ "description": "Etichetta breve dell’errore.",
163725
+ "type": "string"
163726
+ },
163727
+ "message": {
163728
+ "description": "Messaggio leggibile.",
163729
+ "type": "string"
163730
+ },
163731
+ "statusCode": {
163732
+ "description": "Codice di stato HTTP ripetuto nel corpo.",
163733
+ "type": "integer"
163734
+ }
163735
+ }
163736
+ }
163737
+ }
163738
+ }
163739
+ }
163740
+ }
163741
+ }
163742
+ },
163743
+ "/api/v1/referral/bridge-offer": {
163744
+ "get": {
163745
+ "operationId": "getPartnerBridgeOffer",
163746
+ "summary": "L’offerta di passaggio al Binario B che mi aspetta",
163747
+ "tags": [
163748
+ "partner"
163749
+ ],
163750
+ "description": "L’offerta di passaggio al Programma Partner che aspetta chi chiama, se ce n’è una ancora da accettare. `204` quando non c’è nulla da accettare — indistintamente perché non esiste alcuna offerta o perché è già stata accettata: distinguere i due casi renderebbe questa rotta un oracolo sull’esistenza di un’offerta. La risposta porta il documento e la versione da accettare, mai il token della pratica.",
163751
+ "security": [
163752
+ {
163753
+ "bearerAuth": []
163754
+ }
163755
+ ],
163756
+ "responses": {
163757
+ "200": {
163758
+ "description": "L’offerta di passaggio al Binario B che aspetta chi chiama. ⚠️ Non porta il token della pratica: quello è la chiave di lettura del fascicolo e non ha ragione di uscire da qui.",
163759
+ "content": {
163760
+ "application/json": {
163761
+ "schema": {
163762
+ "additionalProperties": false,
163763
+ "description": "L’offerta di passaggio al Binario B che aspetta chi chiama. ⚠️ Non porta il token della pratica: quello è la chiave di lettura del fascicolo e non ha ragione di uscire da qui.",
163764
+ "type": "object",
163765
+ "required": [
163766
+ "country_code",
163767
+ "offered_at",
163768
+ "reason",
163769
+ "tc_document_id",
163770
+ "tc_version"
163771
+ ],
163772
+ "properties": {
163773
+ "country_code": {
163774
+ "description": "Il paese già dichiarato sulla pratica, o `null` se va ancora chiesto. ★ Oggi è sempre `null`: l’offerta la genera un cron del Binario A e nessuno ha compilato niente. Esce comunque perché è ciò che dice al modulo che quel campo è **da chiedere** — il giorno in cui un percorso saprà il paese in anticipo, lo stesso campo lo pre-compila senza toccare né la pagina né il contratto.",
163775
+ "anyOf": [
163776
+ {
163777
+ "maxLength": 2,
163778
+ "minLength": 2,
163779
+ "type": "string"
163780
+ },
163781
+ {
163782
+ "type": "null"
163783
+ }
163784
+ ]
163785
+ },
163786
+ "offered_at": {
163787
+ "description": "Quando l’offerta è nata. È l’unica data che esiste finché non la si accetta.",
163788
+ "format": "date-time",
163789
+ "type": "string"
163790
+ },
163791
+ "reason": {
163792
+ "description": "Perché è nata: `cap_reached` (tetto del Binario A) o `third_matured_conversion`.",
163793
+ "anyOf": [
163794
+ {
163795
+ "type": "string"
163796
+ },
163797
+ {
163798
+ "type": "null"
163799
+ }
163800
+ ]
163801
+ },
163802
+ "tc_document_id": {
163803
+ "description": "Il documento da accettare. Oggi sempre `partnerTerms`.",
163804
+ "type": "string"
163805
+ },
163806
+ "tc_version": {
163807
+ "description": "La versione proposta: è quella che va rimandata all’accettazione.",
163808
+ "type": "string"
163809
+ }
163810
+ }
163811
+ }
163812
+ }
163813
+ }
163814
+ },
163815
+ "204": {
163816
+ "description": "Nessuna offerta da accettare."
163817
+ },
163818
+ "401": {
163819
+ "description": "Errore standard del backend.",
163820
+ "content": {
163821
+ "application/json": {
163822
+ "schema": {
163823
+ "additionalProperties": false,
163824
+ "description": "Errore standard del backend.",
163825
+ "type": "object",
163826
+ "required": [
163827
+ "error",
163828
+ "message",
163829
+ "statusCode"
163830
+ ],
163831
+ "properties": {
163832
+ "code": {
163833
+ "description": "Codice macchina dell’errore, quando disponibile.",
163834
+ "type": "string"
163835
+ },
163836
+ "error": {
163837
+ "description": "Etichetta breve dell’errore.",
163838
+ "type": "string"
163839
+ },
163840
+ "message": {
163841
+ "description": "Messaggio leggibile.",
163842
+ "type": "string"
163843
+ },
163844
+ "statusCode": {
163845
+ "description": "Codice di stato HTTP ripetuto nel corpo.",
163846
+ "type": "integer"
163847
+ }
163848
+ }
163849
+ }
163850
+ }
163851
+ }
163852
+ },
163853
+ "429": {
163854
+ "description": "Errore standard del backend.",
163855
+ "content": {
163856
+ "application/json": {
163857
+ "schema": {
163858
+ "additionalProperties": false,
163859
+ "description": "Errore standard del backend.",
163860
+ "type": "object",
163861
+ "required": [
163862
+ "error",
163863
+ "message",
163864
+ "statusCode"
163865
+ ],
163866
+ "properties": {
163867
+ "code": {
163868
+ "description": "Codice macchina dell’errore, quando disponibile.",
163869
+ "type": "string"
163870
+ },
163871
+ "error": {
163872
+ "description": "Etichetta breve dell’errore.",
163873
+ "type": "string"
163874
+ },
163875
+ "message": {
163876
+ "description": "Messaggio leggibile.",
163877
+ "type": "string"
163878
+ },
163879
+ "statusCode": {
163880
+ "description": "Codice di stato HTTP ripetuto nel corpo.",
163881
+ "type": "integer"
163882
+ }
163883
+ }
163884
+ }
163885
+ }
163886
+ }
163887
+ }
163888
+ }
163889
+ }
163890
+ },
163891
+ "/api/v1/referral/bridge-offer/accept": {
163892
+ "post": {
163893
+ "operationId": "acceptPartnerBridgeOffer",
163894
+ "summary": "Accetto i termini e passo al Binario B",
163895
+ "tags": [
163896
+ "partner"
163897
+ ],
163898
+ "description": "Completa e accetta l’offerta di passaggio al Binario B. ★ È l’ATTO che il ponte non aveva: porta il **paese** dichiarato (`country_code`, obbligatorio — l’offerta nasce senza, perché a generarla è un cron) e la prova di consenso. registra `tc_accepted_at`, l’IP, lo user-agent, l’impronta del testo e la versione — gli stessi cinque campi che la candidatura pubblica raccoglie, perché una prova di consenso non può avere due forme a seconda di dove entra — e porta la pratica in `received`, cioè nella coda normale. Finché questo atto non avviene l’approvazione risponde `409 tc_not_accepted` e il vincolo `ck_papp_approved_needs_consent` rifiuta comunque la transizione: nessuno può essere approvato senza aver accettato. `404` quando non c’è nulla da accettare (nessuna offerta, o già accettata: i due casi rispondono identico). `409 stale_version` quando la versione inviata non è quella corrente; `409 open_application_exists` quando per la stessa email esiste già una candidatura aperta — l’offerta non può portarne una seconda, e dirlo non è un oracolo perché quella pratica è di chi chiede. `400 invalid_country` quando `country_code` non è due lettere: è l’unico rifiuto non anti-oracolo di questa rotta, deciso prima di cercare l’offerta — un `404` al suo posto direbbe «non c’è niente per te» mentre manca solo un campo. `403` quando il programma è chiuso: accettare a porta chiusa scriverebbe un consenso write-once su un percorso che non può proseguire.",
163899
+ "requestBody": {
163900
+ "required": true,
163901
+ "content": {
163902
+ "application/json": {
163903
+ "schema": {
163904
+ "additionalProperties": false,
163905
+ "type": "object",
163906
+ "required": [
163907
+ "country_code",
163908
+ "tos_version"
163909
+ ],
163910
+ "properties": {
163911
+ "country_code": {
163912
+ "description": "Il paese ISO-3166-1 alpha-2 in cui il partner è stabilito. ★ **Obbligatorio, e senza ripiego**: l’offerta di ponte nasce con `country_code` vuoto perché a generarla è un cron, e questa è l’unica porta da cui può arrivarne uno dichiarato. Non è un’etichetta: all’approvazione viene copiato in `partner.country_code`, e da lì `requiredChecksForLevel` decide se VIES è un controllo KYC dovuto o va sostituito da `company_registry`. Un valore indovinato sceglierebbe una verifica antiriciclaggio al posto di chi la deve subire.",
163913
+ "maxLength": 2,
163914
+ "minLength": 2,
163915
+ "pattern": "^[A-Za-z]{2}$",
163916
+ "type": "string"
163917
+ },
163918
+ "tos_version": {
163919
+ "description": "La versione dei T&C partner che si sta accettando. Deve essere quella CORRENTE: accettare una versione vecchia scriverebbe una prova di consenso su un testo diverso da quello mostrato.",
163920
+ "maxLength": 20,
163921
+ "minLength": 1,
163922
+ "type": "string"
163923
+ }
163924
+ }
163925
+ }
163926
+ }
163927
+ }
163928
+ },
163929
+ "security": [
163930
+ {
163931
+ "bearerAuth": []
163932
+ }
163933
+ ],
163934
+ "responses": {
163935
+ "200": {
163936
+ "description": "Successful response",
163937
+ "content": {
163938
+ "application/json": {
163939
+ "schema": {
163940
+ "additionalProperties": false,
163941
+ "type": "object",
163942
+ "required": [
163943
+ "status"
163944
+ ],
163945
+ "properties": {
163946
+ "status": {
163947
+ "description": "La pratica è entrata nella coda normale: da qui in poi è una candidatura come le altre.",
163948
+ "type": "string",
163949
+ "enum": [
163950
+ "received"
163580
163951
  ]
163581
163952
  }
163582
163953
  }
@@ -166445,15 +166816,18 @@
166445
166816
  "code",
166446
166817
  "collection_mode",
166447
166818
  "country_code",
166819
+ "country_locked",
166448
166820
  "coupon",
166449
166821
  "credit",
166450
166822
  "frozen",
166823
+ "grace_days",
166451
166824
  "kyc_level",
166452
166825
  "lane",
166453
166826
  "next_payout",
166454
166827
  "partner_id",
166455
166828
  "partner_profile",
166456
166829
  "payout_request",
166830
+ "payout_profile_saved",
166457
166831
  "payout_ready",
166458
166832
  "primary_link",
166459
166833
  "public_profile",
@@ -166540,7 +166914,7 @@
166540
166914
  "type": "string"
166541
166915
  },
166542
166916
  "country_code": {
166543
- "description": "Paese di stabilimento dichiarato, `null` finché non lo è. Decide la lingua che fa fede sul contratto (art. 19) e i controlli KYC dovuti. Lo scrive il profilo di incasso.",
166917
+ "description": "Paese di stabilimento dichiarato, `null` finché non lo è. Decide la lingua che fa fede sul contratto (art. 19) e i controlli KYC dovuti. Lo scrivono `PATCH /me/country` (una volta sola, dalla card della firma) e il profilo di incasso — che dopo una firma registrata non lo cambia più (`409 PARTNER_COUNTRY_LOCKED`): da lì in poi è il paese del contratto firmato.",
166544
166918
  "anyOf": [
166545
166919
  {
166546
166920
  "type": "string"
@@ -166550,6 +166924,10 @@
166550
166924
  }
166551
166925
  ]
166552
166926
  },
166927
+ "country_locked": {
166928
+ "description": "`true` = `country_code` è quello del contratto firmato (esiste una riga `partner_kyc_event` con `check_kind=signed_contract` ed esito `passed`, e un paese in tabella): il profilo di incasso lo mostra in sola lettura e `POST /payout-profile` rifiuta un valore diverso con `409 PARTNER_COUNTRY_LOCKED`. È la STESSA misura del server, non una deduzione dai blocchi di `next_payout`: chi ha firmato un contratto fuori piattaforma, o deve ri-firmare un testo nuovo, la vede uguale su entrambi i lati.",
166929
+ "type": "boolean"
166930
+ },
166553
166931
  "coupon": {
166554
166932
  "description": "Coupon del partner, `null` se non ne ha uno attivo.",
166555
166933
  "anyOf": [
@@ -166675,6 +167053,11 @@
166675
167053
  "description": "Kill-switch armato: il denaro è congelato, il tracking no.",
166676
167054
  "type": "boolean"
166677
167055
  },
167056
+ "grace_days": {
167057
+ "description": "Giorni dal signup entro cui il referito può ancora inserire il codice invito e risultare attribuito al partner (PRD §5.2, «grazia»). È il valore EFFETTIVO del motore — `PARTNER_GRACE_DAYS` o il suo default — non una costante di presentazione.",
167058
+ "minimum": 1,
167059
+ "type": "integer"
167060
+ },
166678
167061
  "kyc_level": {
166679
167062
  "description": "Livello KYC 0-3 (§6.5).",
166680
167063
  "maximum": 3,
@@ -166892,6 +167275,10 @@
166892
167275
  }
166893
167276
  ]
166894
167277
  },
167278
+ "payout_profile_saved": {
167279
+ "description": "`true` se il profilo di incasso (`POST /api/v1/partner/payout-profile`) è stato salvato almeno una volta: il fatto è l’impronta dell’IBAN in tabella. Ortogonale a `payout_ready`: un profilo salvato resta non pagabile finché una verifica (intestatario, VIES, registro imprese) è in corso. Il pannello lo usa per NON mandare a ricompilare un modulo già salvato — ogni salvataggio riapre 72 ore di raffreddamento sui pagamenti.",
167280
+ "type": "boolean"
167281
+ },
166895
167282
  "payout_ready": {
166896
167283
  "description": "Documenti fiscali a posto. Ortogonale allo stato delle commissioni (§6.2).",
166897
167284
  "type": "boolean"
@@ -168008,7 +168395,7 @@
168008
168395
  "tags": [
168009
168396
  "partner"
168010
168397
  ],
168011
- "description": "★ **È la firma del contratto, e dal 27/08/2026 è la porta del primo prelievo.** Decisione 1 di Salvatore: si entra prima, si firma dopo. La candidatura fa accettare le sole regole di promozione (`partnerContentAddendum`); il contratto (`partnerTerms`) — aliquote, finestra, fatturazione — si accetta qui, e finché non lo si fa il pannello espone il blocco `contract_not_signed` e lo sportello del prelievo resta chiuso. ⚠️ Non blocca la maturazione: chi non ha firmato promuove e accumula commissioni, non le incassa. È anche la sola porta che scrive `partner_program_enrollment`: l’approvazione ha smesso di iscrivere, perché iscriveva sulla base di un consenso a un ALTRO documento. Resta esposta anche per un’accettazione DELIBERATA fuori dal percorso del prelievo. Registra l’accettazione dei documenti di programma da parte del partner autenticato: una riga `legal_consent` per documento e, quando esiste una versione di programma pubblicata per il suo binario, una riga `partner_program_enrollment`. L’adesione è APPEND-ONLY (`trg_ppe_append_only`) e unica per versione (`uq_ppe_partner_version`): riaccettare la stessa versione è idempotente e restituisce `enrolled: false`. Ogni riga di `legal_consent` porta nei metadata la lingua che fa fede, l’impronta `sha256` e l’URL del testo firmato: senza, la riga proverebbe che qualcuno ha accettato ma non QUALE testo, e lo stesso documento esiste in due lingue con due impronte. Quando il corpo porta il documento che governa il binario, la firma scrive anche la prova KYC `partner_kyc_event(check_kind='signed_contract')`, richiesta dal livello 3: prima la scrivevano solo la creazione manuale e il backoffice, e chi firmava qui restava murato su un controllo che la sua firma soddisfa. Due cause di `409`, distinte dal campo `code`. `PARTNER_STALE_DOCUMENT_VERSION`: una versione diversa da quella corrente — un client fermo su un testo vecchio si dichiarerebbe in pari senza aver mai visto quello nuovo. `PARTNER_COUNTRY_NOT_DECLARED`: `partner.country_code` è ancora nullo e almeno uno dei documenti cambia testo con il paese (art. 19: fa fede l’inglese, salvo per chi è stabilito in Italia) — si registrerebbe l’impronta dell’inglese per ripiego, e il partner che poi dichiara `IT` avrebbe una prova write-once di un testo mai mostrato. Il paese si dichiara nel profilo di incasso (`PUT /api/v1/partner/payout-profile`). ⚠️ Non riguarda il Binario A: `referralTerms` è l’art. 23 dei ToS, un testo solo per entrambe le lingue.",
168398
+ "description": "★ **È la firma del contratto, e dal 27/08/2026 è la porta del primo prelievo.** Decisione 1 di Salvatore: si entra prima, si firma dopo. La candidatura fa accettare le sole regole di promozione (`partnerContentAddendum`); il contratto (`partnerTerms`) — aliquote, finestra, fatturazione — si accetta qui, e finché non lo si fa il pannello espone il blocco `contract_not_signed` e lo sportello del prelievo resta chiuso. ⚠️ Non blocca la maturazione: chi non ha firmato promuove e accumula commissioni, non le incassa. È anche la sola porta che scrive `partner_program_enrollment`: l’approvazione ha smesso di iscrivere, perché iscriveva sulla base di un consenso a un ALTRO documento. Resta esposta anche per un’accettazione DELIBERATA fuori dal percorso del prelievo. Registra l’accettazione dei documenti di programma da parte del partner autenticato: una riga `legal_consent` per documento e, quando esiste una versione di programma pubblicata per il suo binario, una riga `partner_program_enrollment`. L’adesione è APPEND-ONLY (`trg_ppe_append_only`) e unica per versione (`uq_ppe_partner_version`): riaccettare la stessa versione è idempotente e restituisce `enrolled: false`. Ogni riga di `legal_consent` porta nei metadata la lingua che fa fede, l’impronta `sha256` e l’URL del testo firmato: senza, la riga proverebbe che qualcuno ha accettato ma non QUALE testo, e lo stesso documento esiste in due lingue con due impronte. Quando il corpo porta il documento che governa il binario, la firma scrive anche la prova KYC `partner_kyc_event(check_kind='signed_contract')`, richiesta dal livello 3: prima la scrivevano solo la creazione manuale e il backoffice, e chi firmava qui restava murato su un controllo che la sua firma soddisfa. Due cause di `409`, distinte dal campo `code`. `PARTNER_STALE_DOCUMENT_VERSION`: una versione diversa da quella corrente — un client fermo su un testo vecchio si dichiarerebbe in pari senza aver mai visto quello nuovo. `PARTNER_COUNTRY_NOT_DECLARED`: `partner.country_code` è ancora nullo e almeno uno dei documenti cambia testo con il paese (art. 19: fa fede l’inglese, salvo per chi è stabilito in Italia) — si registrerebbe l’impronta dell’inglese per ripiego, e il partner che poi dichiara `IT` avrebbe una prova write-once di un testo mai mostrato. Il paese si dichiara da solo con `PATCH /api/v1/partner/me/country` (è ciò che la card della firma fa prima di chiamare questa rotta) o nel profilo di incasso (`POST /api/v1/partner/payout-profile`). ⚠️ Non riguarda il Binario A: `referralTerms` è l’art. 23 dei ToS, un testo solo per entrambe le lingue.",
168012
168399
  "requestBody": {
168013
168400
  "required": true,
168014
168401
  "content": {
@@ -168348,70 +168735,36 @@
168348
168735
  }
168349
168736
  }
168350
168737
  },
168351
- "/api/v1/partner/profile": {
168738
+ "/api/v1/partner/me/country": {
168352
168739
  "patch": {
168353
- "operationId": "updatePartnerPublicProfile",
168354
- "summary": "Aggiorna il profilo pubblico del partner",
168740
+ "operationId": "declarePartnerCountry",
168741
+ "summary": "Dichiara il paese di stabilimento (una volta sola)",
168355
168742
  "tags": [
168356
168743
  "partner"
168357
168744
  ],
168358
- "description": "Aggiorna il profilo PUBBLICO del partner. Ogni modifica riporta il profilo in `pending`: senza ri-approvazione un profilo già approvato potrebbe cambiare nome e aggirare il gate umano dell’identità reciproca (§4.5). Finché è `pending` le celle identità restano coperte.",
168745
+ "description": "Dichiara il paese di stabilimento del partner autenticato, scrivendo `partner.country_code` **solo se è ancora nullo**. È la precondizione della firma del contratto (`POST /api/v1/partner/consent`): il paese decide quale lingua del testo fa fede (art. 19: l’inglese, salvo per chi è stabilito in Italia), quindi di quale testo si registra l’impronta — e va dichiarato PRIMA di leggere e firmare. ⚠️ **Write-once**: una volta scritto, il paese non si cambia da qui — dopo la firma è la prova di quale testo fa fede, e la riga di consenso è append-only. `409 PARTNER_COUNTRY_ALREADY_DECLARED` = il paese era già in tabella (il messaggio dice quale); il client rilegge `GET /me` e usa quello. `422 PARTNER_COUNTRY_INVALID` = non sono due lettere (rete sotto lo schema: lo stesso controllo delle altre porte che raccolgono un paese). Il profilo di incasso (`POST /api/v1/partner/payout-profile`) segue la stessa regola dalla stessa sorgente: scrive il paese se è nullo, lo sostituisce solo finché nessuna firma è registrata, e dopo risponde `409 PARTNER_COUNTRY_LOCKED`.",
168359
168746
  "requestBody": {
168360
168747
  "required": true,
168361
168748
  "content": {
168362
168749
  "application/json": {
168363
168750
  "schema": {
168364
168751
  "additionalProperties": false,
168365
- "description": "Profilo PUBBLICO. Non è `partner_profile`, che è la configurazione a 4 valori della dashboard ed è in sola lettura per il partner (§12.5).",
168366
168752
  "type": "object",
168753
+ "required": [
168754
+ "country_code"
168755
+ ],
168367
168756
  "properties": {
168368
- "avatar_url": {
168369
- "description": "URL dellavatar pubblico.",
168370
- "anyOf": [
168371
- {
168372
- "maxLength": 500,
168373
- "type": "string"
168374
- },
168375
- {
168376
- "type": "null"
168377
- }
168378
- ]
168379
- },
168380
- "display_name": {
168381
- "description": "Nome mostrato al referito.",
168382
- "maxLength": 120,
168757
+ "country_code": {
168758
+ "description": "Paese di stabilimento, ISO 3166-1 alpha-2 (due lettere, in qualunque caso: si salva in maiuscolo). Decide quale lingua del contratto fa fede (art. 19: linglese, salvo per chi è stabilito in Italia) e quali controlli KYC sono dovuti.",
168759
+ "maxLength": 2,
168383
168760
  "minLength": 2,
168761
+ "pattern": "^[A-Za-z]{2}$",
168384
168762
  "type": "string"
168385
- },
168386
- "handle": {
168387
- "description": "Handle social pubblico.",
168388
- "anyOf": [
168389
- {
168390
- "maxLength": 80,
168391
- "type": "string"
168392
- },
168393
- {
168394
- "type": "null"
168395
- }
168396
- ]
168397
- },
168398
- "tagline": {
168399
- "description": "Una riga di presentazione.",
168400
- "anyOf": [
168401
- {
168402
- "maxLength": 160,
168403
- "type": "string"
168404
- },
168405
- {
168406
- "type": "null"
168407
- }
168408
- ]
168409
168763
  }
168410
168764
  }
168411
168765
  }
168412
168766
  }
168413
- },
168414
- "description": "Profilo PUBBLICO. Non è `partner_profile`, che è la configurazione a 4 valori della dashboard ed è in sola lettura per il partner (§12.5)."
168767
+ }
168415
168768
  },
168416
168769
  "security": [
168417
168770
  {
@@ -168420,124 +168773,429 @@
168420
168773
  ],
168421
168774
  "responses": {
168422
168775
  "200": {
168423
- "description": "Profilo pubblico del partner: è il gate dell’identità reciproca, e da qui il partner scopre a che punto è la sua revisione.",
168776
+ "description": "Il paese di stabilimento appena dichiarato.",
168424
168777
  "content": {
168425
168778
  "application/json": {
168426
168779
  "schema": {
168427
168780
  "additionalProperties": false,
168428
- "description": "Profilo pubblico del partner: è il gate dell’identità reciproca, e da qui il partner scopre a che punto è la sua revisione.",
168781
+ "description": "Il paese di stabilimento appena dichiarato.",
168429
168782
  "type": "object",
168430
168783
  "required": [
168431
- "approved",
168432
- "avatar_url",
168433
- "display_name",
168434
- "handle",
168435
- "rejected_at",
168436
- "rejection_code",
168437
- "review_state",
168438
- "tagline"
168784
+ "country_code"
168439
168785
  ],
168440
168786
  "properties": {
168441
- "approved": {
168442
- "description": "true solo dopo l’approvazione del backoffice (§4.5).",
168443
- "type": "boolean"
168787
+ "country_code": {
168788
+ "description": "Il paese scritto in `partner.country_code`, in maiuscolo.",
168789
+ "type": "string"
168790
+ }
168791
+ }
168792
+ }
168793
+ }
168794
+ }
168795
+ },
168796
+ "400": {
168797
+ "description": "Errore standard del backend.",
168798
+ "content": {
168799
+ "application/json": {
168800
+ "schema": {
168801
+ "additionalProperties": false,
168802
+ "description": "Errore standard del backend.",
168803
+ "type": "object",
168804
+ "required": [
168805
+ "error",
168806
+ "message",
168807
+ "statusCode"
168808
+ ],
168809
+ "properties": {
168810
+ "code": {
168811
+ "description": "Codice macchina dell’errore, quando disponibile.",
168812
+ "type": "string"
168444
168813
  },
168445
- "avatar_url": {
168446
- "description": "Avatar pubblico.",
168447
- "anyOf": [
168448
- {
168449
- "type": "string"
168450
- },
168451
- {
168452
- "type": "null"
168453
- }
168454
- ]
168814
+ "error": {
168815
+ "description": "Etichetta breve dell’errore.",
168816
+ "type": "string"
168455
168817
  },
168456
- "display_name": {
168457
- "description": "Nome mostrato al referito.",
168458
- "anyOf": [
168459
- {
168460
- "type": "string"
168461
- },
168462
- {
168463
- "type": "null"
168464
- }
168465
- ]
168818
+ "message": {
168819
+ "description": "Messaggio leggibile.",
168820
+ "type": "string"
168466
168821
  },
168467
- "handle": {
168468
- "description": "Handle social pubblico.",
168469
- "anyOf": [
168470
- {
168471
- "type": "string"
168472
- },
168473
- {
168474
- "type": "null"
168475
- }
168476
- ]
168822
+ "statusCode": {
168823
+ "description": "Codice di stato HTTP ripetuto nel corpo.",
168824
+ "type": "integer"
168825
+ }
168826
+ }
168827
+ }
168828
+ }
168829
+ }
168830
+ },
168831
+ "401": {
168832
+ "description": "Errore standard del backend.",
168833
+ "content": {
168834
+ "application/json": {
168835
+ "schema": {
168836
+ "additionalProperties": false,
168837
+ "description": "Errore standard del backend.",
168838
+ "type": "object",
168839
+ "required": [
168840
+ "error",
168841
+ "message",
168842
+ "statusCode"
168843
+ ],
168844
+ "properties": {
168845
+ "code": {
168846
+ "description": "Codice macchina dell’errore, quando disponibile.",
168847
+ "type": "string"
168477
168848
  },
168478
- "rejected_at": {
168479
- "description": "Quando il profilo è stato rimandato indietro. Valorizzato solo insieme a `rejection_code`, cioè solo con `review_state: rejected`.",
168480
- "anyOf": [
168481
- {
168482
- "format": "date-time",
168483
- "type": "string"
168484
- },
168485
- {
168486
- "type": "null"
168487
- }
168488
- ]
168849
+ "error": {
168850
+ "description": "Etichetta breve dell’errore.",
168851
+ "type": "string"
168489
168852
  },
168490
- "rejection_code": {
168491
- "description": "Il CODICE del motivo per cui il profilo è stato rimandato indietro (enum chiuso, `lib/partner-reason-codes.ts`), oppure `null`. ★ È un codice e non una frase perché la lingua la sceglie il client: il pannello lo traduce in due frasi — cosa non va, e cosa cambiare. La nota interna scritta dall’operatore NON esiste su questa superficie e non è ottenibile da qui. Non è mai valorizzato fuori da `review_state: rejected`, nemmeno su un profilo tornato in attesa dopo una modifica del partner.",
168492
- "anyOf": [
168493
- {
168494
- "type": "string"
168495
- },
168496
- {
168497
- "type": "null"
168498
- }
168499
- ]
168853
+ "message": {
168854
+ "description": "Messaggio leggibile.",
168855
+ "type": "string"
168500
168856
  },
168501
- "review_state": {
168502
- "description": "Il verdetto della revisione del profilo pubblico. ★ Esiste perché `approved: false` collassava tre situazioni in una — «non l’ho mai mandato», «lo stanno guardando», «me l’hanno rimandato indietro» — e chi era stato rifiutato si trovava davanti un interruttore fermo, senza sapere se ci fosse qualcosa da correggere.",
168503
- "anyOf": [
168504
- {
168505
- "type": "string",
168506
- "enum": [
168507
- "approved"
168508
- ]
168509
- },
168510
- {
168511
- "type": "string",
168512
- "enum": [
168513
- "pending"
168514
- ]
168515
- },
168516
- {
168517
- "type": "string",
168518
- "enum": [
168519
- "rejected"
168520
- ]
168521
- }
168522
- ]
168857
+ "statusCode": {
168858
+ "description": "Codice di stato HTTP ripetuto nel corpo.",
168859
+ "type": "integer"
168860
+ }
168861
+ }
168862
+ }
168863
+ }
168864
+ }
168865
+ },
168866
+ "403": {
168867
+ "description": "Errore standard del backend.",
168868
+ "content": {
168869
+ "application/json": {
168870
+ "schema": {
168871
+ "additionalProperties": false,
168872
+ "description": "Errore standard del backend.",
168873
+ "type": "object",
168874
+ "required": [
168875
+ "error",
168876
+ "message",
168877
+ "statusCode"
168878
+ ],
168879
+ "properties": {
168880
+ "code": {
168881
+ "description": "Codice macchina dell’errore, quando disponibile.",
168882
+ "type": "string"
168523
168883
  },
168524
- "tagline": {
168525
- "description": "Una riga di presentazione.",
168526
- "anyOf": [
168527
- {
168528
- "type": "string"
168529
- },
168530
- {
168531
- "type": "null"
168532
- }
168533
- ]
168884
+ "error": {
168885
+ "description": "Etichetta breve dell’errore.",
168886
+ "type": "string"
168887
+ },
168888
+ "message": {
168889
+ "description": "Messaggio leggibile.",
168890
+ "type": "string"
168891
+ },
168892
+ "statusCode": {
168893
+ "description": "Codice di stato HTTP ripetuto nel corpo.",
168894
+ "type": "integer"
168534
168895
  }
168535
168896
  }
168536
168897
  }
168537
168898
  }
168538
168899
  }
168539
168900
  },
168540
- "400": {
168901
+ "409": {
168902
+ "description": "Errore standard del backend.",
168903
+ "content": {
168904
+ "application/json": {
168905
+ "schema": {
168906
+ "additionalProperties": false,
168907
+ "description": "Errore standard del backend.",
168908
+ "type": "object",
168909
+ "required": [
168910
+ "error",
168911
+ "message",
168912
+ "statusCode"
168913
+ ],
168914
+ "properties": {
168915
+ "code": {
168916
+ "description": "Codice macchina dell’errore, quando disponibile.",
168917
+ "type": "string"
168918
+ },
168919
+ "error": {
168920
+ "description": "Etichetta breve dell’errore.",
168921
+ "type": "string"
168922
+ },
168923
+ "message": {
168924
+ "description": "Messaggio leggibile.",
168925
+ "type": "string"
168926
+ },
168927
+ "statusCode": {
168928
+ "description": "Codice di stato HTTP ripetuto nel corpo.",
168929
+ "type": "integer"
168930
+ }
168931
+ }
168932
+ }
168933
+ }
168934
+ }
168935
+ },
168936
+ "422": {
168937
+ "description": "Errore standard del backend.",
168938
+ "content": {
168939
+ "application/json": {
168940
+ "schema": {
168941
+ "additionalProperties": false,
168942
+ "description": "Errore standard del backend.",
168943
+ "type": "object",
168944
+ "required": [
168945
+ "error",
168946
+ "message",
168947
+ "statusCode"
168948
+ ],
168949
+ "properties": {
168950
+ "code": {
168951
+ "description": "Codice macchina dell’errore, quando disponibile.",
168952
+ "type": "string"
168953
+ },
168954
+ "error": {
168955
+ "description": "Etichetta breve dell’errore.",
168956
+ "type": "string"
168957
+ },
168958
+ "message": {
168959
+ "description": "Messaggio leggibile.",
168960
+ "type": "string"
168961
+ },
168962
+ "statusCode": {
168963
+ "description": "Codice di stato HTTP ripetuto nel corpo.",
168964
+ "type": "integer"
168965
+ }
168966
+ }
168967
+ }
168968
+ }
168969
+ }
168970
+ },
168971
+ "429": {
168972
+ "description": "Errore standard del backend.",
168973
+ "content": {
168974
+ "application/json": {
168975
+ "schema": {
168976
+ "additionalProperties": false,
168977
+ "description": "Errore standard del backend.",
168978
+ "type": "object",
168979
+ "required": [
168980
+ "error",
168981
+ "message",
168982
+ "statusCode"
168983
+ ],
168984
+ "properties": {
168985
+ "code": {
168986
+ "description": "Codice macchina dell’errore, quando disponibile.",
168987
+ "type": "string"
168988
+ },
168989
+ "error": {
168990
+ "description": "Etichetta breve dell’errore.",
168991
+ "type": "string"
168992
+ },
168993
+ "message": {
168994
+ "description": "Messaggio leggibile.",
168995
+ "type": "string"
168996
+ },
168997
+ "statusCode": {
168998
+ "description": "Codice di stato HTTP ripetuto nel corpo.",
168999
+ "type": "integer"
169000
+ }
169001
+ }
169002
+ }
169003
+ }
169004
+ }
169005
+ }
169006
+ }
169007
+ }
169008
+ },
169009
+ "/api/v1/partner/profile": {
169010
+ "patch": {
169011
+ "operationId": "updatePartnerPublicProfile",
169012
+ "summary": "Aggiorna il profilo pubblico del partner",
169013
+ "tags": [
169014
+ "partner"
169015
+ ],
169016
+ "description": "Aggiorna il profilo PUBBLICO del partner. Ogni modifica riporta il profilo in `pending`: senza ri-approvazione un profilo già approvato potrebbe cambiare nome e aggirare il gate umano dell’identità reciproca (§4.5). Finché è `pending` le celle identità restano coperte.",
169017
+ "requestBody": {
169018
+ "required": true,
169019
+ "content": {
169020
+ "application/json": {
169021
+ "schema": {
169022
+ "additionalProperties": false,
169023
+ "description": "Profilo PUBBLICO. Non è `partner_profile`, che è la configurazione a 4 valori della dashboard ed è in sola lettura per il partner (§12.5).",
169024
+ "type": "object",
169025
+ "properties": {
169026
+ "avatar_url": {
169027
+ "description": "URL dell’avatar pubblico.",
169028
+ "anyOf": [
169029
+ {
169030
+ "maxLength": 500,
169031
+ "type": "string"
169032
+ },
169033
+ {
169034
+ "type": "null"
169035
+ }
169036
+ ]
169037
+ },
169038
+ "display_name": {
169039
+ "description": "Nome mostrato al referito.",
169040
+ "maxLength": 120,
169041
+ "minLength": 2,
169042
+ "type": "string"
169043
+ },
169044
+ "handle": {
169045
+ "description": "Handle social pubblico.",
169046
+ "anyOf": [
169047
+ {
169048
+ "maxLength": 80,
169049
+ "type": "string"
169050
+ },
169051
+ {
169052
+ "type": "null"
169053
+ }
169054
+ ]
169055
+ },
169056
+ "tagline": {
169057
+ "description": "Una riga di presentazione.",
169058
+ "anyOf": [
169059
+ {
169060
+ "maxLength": 160,
169061
+ "type": "string"
169062
+ },
169063
+ {
169064
+ "type": "null"
169065
+ }
169066
+ ]
169067
+ }
169068
+ }
169069
+ }
169070
+ }
169071
+ },
169072
+ "description": "Profilo PUBBLICO. Non è `partner_profile`, che è la configurazione a 4 valori della dashboard ed è in sola lettura per il partner (§12.5)."
169073
+ },
169074
+ "security": [
169075
+ {
169076
+ "bearerAuth": []
169077
+ }
169078
+ ],
169079
+ "responses": {
169080
+ "200": {
169081
+ "description": "Profilo pubblico del partner: è il gate dell’identità reciproca, e da qui il partner scopre a che punto è la sua revisione.",
169082
+ "content": {
169083
+ "application/json": {
169084
+ "schema": {
169085
+ "additionalProperties": false,
169086
+ "description": "Profilo pubblico del partner: è il gate dell’identità reciproca, e da qui il partner scopre a che punto è la sua revisione.",
169087
+ "type": "object",
169088
+ "required": [
169089
+ "approved",
169090
+ "avatar_url",
169091
+ "display_name",
169092
+ "handle",
169093
+ "rejected_at",
169094
+ "rejection_code",
169095
+ "review_state",
169096
+ "tagline"
169097
+ ],
169098
+ "properties": {
169099
+ "approved": {
169100
+ "description": "true solo dopo l’approvazione del backoffice (§4.5).",
169101
+ "type": "boolean"
169102
+ },
169103
+ "avatar_url": {
169104
+ "description": "Avatar pubblico.",
169105
+ "anyOf": [
169106
+ {
169107
+ "type": "string"
169108
+ },
169109
+ {
169110
+ "type": "null"
169111
+ }
169112
+ ]
169113
+ },
169114
+ "display_name": {
169115
+ "description": "Nome mostrato al referito.",
169116
+ "anyOf": [
169117
+ {
169118
+ "type": "string"
169119
+ },
169120
+ {
169121
+ "type": "null"
169122
+ }
169123
+ ]
169124
+ },
169125
+ "handle": {
169126
+ "description": "Handle social pubblico.",
169127
+ "anyOf": [
169128
+ {
169129
+ "type": "string"
169130
+ },
169131
+ {
169132
+ "type": "null"
169133
+ }
169134
+ ]
169135
+ },
169136
+ "rejected_at": {
169137
+ "description": "Quando il profilo è stato rimandato indietro. Valorizzato solo insieme a `rejection_code`, cioè solo con `review_state: rejected`.",
169138
+ "anyOf": [
169139
+ {
169140
+ "format": "date-time",
169141
+ "type": "string"
169142
+ },
169143
+ {
169144
+ "type": "null"
169145
+ }
169146
+ ]
169147
+ },
169148
+ "rejection_code": {
169149
+ "description": "Il CODICE del motivo per cui il profilo è stato rimandato indietro (enum chiuso, `lib/partner-reason-codes.ts`), oppure `null`. ★ È un codice e non una frase perché la lingua la sceglie il client: il pannello lo traduce in due frasi — cosa non va, e cosa cambiare. La nota interna scritta dall’operatore NON esiste su questa superficie e non è ottenibile da qui. Non è mai valorizzato fuori da `review_state: rejected`, nemmeno su un profilo tornato in attesa dopo una modifica del partner.",
169150
+ "anyOf": [
169151
+ {
169152
+ "type": "string"
169153
+ },
169154
+ {
169155
+ "type": "null"
169156
+ }
169157
+ ]
169158
+ },
169159
+ "review_state": {
169160
+ "description": "Il verdetto della revisione del profilo pubblico. ★ Esiste perché `approved: false` collassava tre situazioni in una — «non l’ho mai mandato», «lo stanno guardando», «me l’hanno rimandato indietro» — e chi era stato rifiutato si trovava davanti un interruttore fermo, senza sapere se ci fosse qualcosa da correggere.",
169161
+ "anyOf": [
169162
+ {
169163
+ "type": "string",
169164
+ "enum": [
169165
+ "approved"
169166
+ ]
169167
+ },
169168
+ {
169169
+ "type": "string",
169170
+ "enum": [
169171
+ "pending"
169172
+ ]
169173
+ },
169174
+ {
169175
+ "type": "string",
169176
+ "enum": [
169177
+ "rejected"
169178
+ ]
169179
+ }
169180
+ ]
169181
+ },
169182
+ "tagline": {
169183
+ "description": "Una riga di presentazione.",
169184
+ "anyOf": [
169185
+ {
169186
+ "type": "string"
169187
+ },
169188
+ {
169189
+ "type": "null"
169190
+ }
169191
+ ]
169192
+ }
169193
+ }
169194
+ }
169195
+ }
169196
+ }
169197
+ },
169198
+ "400": {
168541
169199
  "description": "Errore standard del backend.",
168542
169200
  "content": {
168543
169201
  "application/json": {
@@ -168652,7 +169310,7 @@
168652
169310
  "tags": [
168653
169311
  "partner"
168654
169312
  ],
168655
- "description": "Salva l’identità fiscale e le coordinate di incasso del partner autenticato, poi tenta di renderlo pagabile da solo (verifica europea della partita IVA). ⚠️ **L’IBAN non viene conservato**: se ne verifica la cifra di controllo e se ne scrive la sola impronta `sha256(pepper || IBAN)` (§6.7). Ogni salvataggio apre un raffreddamento di 72 ore sul pagamento — è la finestra in cui il titolare vero si accorge di un cambio di conto che non ha fatto. `403` = questo profilo non appartiene a chi è compensato in credito di fatturazione. `409` = quella partita IVA o quel conto sono già di un altro partner (lo dice il vincolo del database, non una SELECT che due schede supererebbero entrambe). `422` = IBAN con cifra di controllo sbagliata, o partita IVA implausibile.",
169313
+ "description": "Salva l’identità fiscale e le coordinate di incasso del partner autenticato, poi tenta di renderlo pagabile da solo (verifica europea della partita IVA). ⚠️ **L’IBAN non viene conservato**: se ne verifica la cifra di controllo e se ne scrive la sola impronta `sha256(pepper || IBAN)` (§6.7). Ogni salvataggio apre un raffreddamento di 72 ore sul pagamento — è la finestra in cui il titolare vero si accorge di un cambio di conto che non ha fatto. `403` = questo profilo non appartiene a chi è compensato in credito di fatturazione. Due cause di `409`, distinte dal campo `code`. `PARTNER_DUPLICATE_IDENTITY`: quella partita IVA o quel conto sono già di un altro partner (lo dice il vincolo del database, non una SELECT che due schede supererebbero entrambe). `PARTNER_COUNTRY_LOCKED`: `country_code` è diverso da quello in tabella e una firma del contratto è già registrata (`partner_kyc_event` con `check_kind=signed_contract` ed esito `passed`) — il paese decide quale testo del contratto fa fede (art. 19) e la riga di consenso è append-only, quindi dopo la firma è il paese del contratto firmato e si cambia solo scrivendoci; il messaggio lo nomina e il profilo NON viene salvato. Finché nessuna firma è registrata, un paese diverso sostituisce quello in tabella; uguale, non lo tocca; nullo, lo scrive — la stessa regola di `PATCH /api/v1/partner/me/country`, dalla stessa sorgente. `422` = IBAN con cifra di controllo sbagliata, o partita IVA implausibile.",
168656
169314
  "requestBody": {
168657
169315
  "required": true,
168658
169316
  "content": {
@@ -170016,7 +170674,46 @@
170016
170674
  "tags": [
170017
170675
  "partner"
170018
170676
  ],
170019
- "description": "Mi iscrivo al programma referral come cliente (Binario A). ⚠️ Non crea un account, non cambia il mio ruolo, non manda email: fa nascere la mia anagrafica di referrer e il mio codice invito. È idempotente — chiamarla due volte restituisce lo stesso partner. Risponde `404` quando il programma non è acceso: a commutatore spento non esiste per il cliente, e un 403 direbbe che esiste ma non è per lui, che è falso.",
170677
+ "description": "Mi iscrivo al programma referral come cliente (Binario A). ⚠️ Non crea un account, non cambia il mio ruolo, non manda email: fa nascere la mia anagrafica di referrer e il mio codice invito. È idempotente — chiamarla due volte restituisce lo stesso partner. Risponde `404` quando il programma non è acceso: a commutatore spento non esiste per il cliente, e un 403 direbbe che esiste ma non è per lui, che è falso. ★ **Il corpo è FACOLTATIVO e porta la spunta sull’art. 23 dei Termini di Servizio**, che è il contratto del Binario A. Quando c’è, la riga `legal_consent` — con impronta `sha256`, URL e lingua che fa fede nei metadata — nasce **nella stessa transazione** che crea il partner: o nascono insieme, o non nasce nessuno dei due, perché un referrer senza la sua firma è esattamente lo stato che nessuna schermata segnala. Quando non c’è, l’iscrizione è identica a prima e nessun consenso viene scritto: una firma non si fabbrica per conto di chi non l’ha data. Il campo resta facoltativo perché questa rotta è pubblicata nell’SDK — renderlo obbligatorio manderebbe in `400` ogni client non ancora aggiornato. Gli errori: `400` corpo malformato — un documento diverso da `referralTerms`, o la versione mancante o vuota. ⚠️ Un campo NON dichiarato non produce `400`: viene tolto dal corpo prima del gestore (l’ajv di Fastify gira con `removeAdditional: true`), quindi nessun client può dichiarare da dove sta firmando — la superficie la decide la rotta. `409` con `code: PARTNER_STALE_DOCUMENT_VERSION` quando la versione dichiarata non è quella corrente — stesso codice di `POST /api/v1/partner/consent`, così il client ha una sola grammatica da imparare, e nessun partner viene creato; `409` con il motivo del rifiuto (`already_enrolled`, `duplicate_identity`, `existing_partner_without_track`, `existing_track_b_partner`) quando un’anagrafica partner esiste già; `422 no_organization`; `401 user_not_found`; `503 tooling_catalog_empty`.",
170678
+ "requestBody": {
170679
+ "required": true,
170680
+ "content": {
170681
+ "application/json": {
170682
+ "schema": {
170683
+ "description": "La spunta sull’art. 23 dei ToS, se il client la raccoglie. Assente ⇒ l’iscrizione avviene come prima e nessuna riga di consenso viene scritta: non si fabbrica una firma che nessuno ha dato.",
170684
+ "anyOf": [
170685
+ {
170686
+ "additionalProperties": false,
170687
+ "type": "object",
170688
+ "required": [
170689
+ "accepted_document",
170690
+ "accepted_version"
170691
+ ],
170692
+ "properties": {
170693
+ "accepted_document": {
170694
+ "description": "Il documento che si dichiara di accettare. Sul Binario A può essere solo l’art. 23 dei ToS.",
170695
+ "type": "string",
170696
+ "enum": [
170697
+ "referralTerms"
170698
+ ]
170699
+ },
170700
+ "accepted_version": {
170701
+ "description": "La versione LETTA dal cliente. Diversa da quella corrente ⇒ `409 PARTNER_STALE_DOCUMENT_VERSION`.",
170702
+ "maxLength": 20,
170703
+ "minLength": 1,
170704
+ "type": "string"
170705
+ }
170706
+ }
170707
+ },
170708
+ {
170709
+ "type": "null"
170710
+ }
170711
+ ]
170712
+ }
170713
+ }
170714
+ },
170715
+ "description": "La spunta sull’art. 23 dei ToS, se il client la raccoglie. Assente ⇒ l’iscrizione avviene come prima e nessuna riga di consenso viene scritta: non si fabbrica una firma che nessuno ha dato."
170716
+ },
170020
170717
  "security": [
170021
170718
  {
170022
170719
  "bearerAuth": []
@@ -170044,6 +170741,41 @@
170044
170741
  }
170045
170742
  }
170046
170743
  },
170744
+ "400": {
170745
+ "description": "Errore standard del backend.",
170746
+ "content": {
170747
+ "application/json": {
170748
+ "schema": {
170749
+ "additionalProperties": false,
170750
+ "description": "Errore standard del backend.",
170751
+ "type": "object",
170752
+ "required": [
170753
+ "error",
170754
+ "message",
170755
+ "statusCode"
170756
+ ],
170757
+ "properties": {
170758
+ "code": {
170759
+ "description": "Codice macchina dell’errore, quando disponibile.",
170760
+ "type": "string"
170761
+ },
170762
+ "error": {
170763
+ "description": "Etichetta breve dell’errore.",
170764
+ "type": "string"
170765
+ },
170766
+ "message": {
170767
+ "description": "Messaggio leggibile.",
170768
+ "type": "string"
170769
+ },
170770
+ "statusCode": {
170771
+ "description": "Codice di stato HTTP ripetuto nel corpo.",
170772
+ "type": "integer"
170773
+ }
170774
+ }
170775
+ }
170776
+ }
170777
+ }
170778
+ },
170047
170779
  "401": {
170048
170780
  "description": "Errore standard del backend.",
170049
170781
  "content": {
@@ -170529,6 +171261,8 @@
170529
171261
  "required": [
170530
171262
  "id",
170531
171263
  "label",
171264
+ "label_en",
171265
+ "label_it",
170532
171266
  "relative_path",
170533
171267
  "slug"
170534
171268
  ],
@@ -170539,221 +171273,23 @@
170539
171273
  "type": "string"
170540
171274
  },
170541
171275
  "label": {
170542
- "description": "Etichetta leggibile.",
171276
+ "description": "Etichetta leggibile in inglese (la lingua che fa fede). Il pannello preferisce `label_en`/`label_it` secondo la lingua dell’interfaccia.",
170543
171277
  "type": "string"
170544
171278
  },
170545
- "relative_path": {
170546
- "description": "Percorso relativo servito dal redirector.",
170547
- "type": "string"
170548
- },
170549
- "slug": {
170550
- "description": "Slug della destinazione nel catalogo.",
170551
- "type": "string"
170552
- }
170553
- }
170554
- }
170555
- }
170556
- }
170557
- }
170558
- }
170559
- }
170560
- },
170561
- "401": {
170562
- "description": "Errore standard del backend.",
170563
- "content": {
170564
- "application/json": {
170565
- "schema": {
170566
- "additionalProperties": false,
170567
- "description": "Errore standard del backend.",
170568
- "type": "object",
170569
- "required": [
170570
- "error",
170571
- "message",
170572
- "statusCode"
170573
- ],
170574
- "properties": {
170575
- "code": {
170576
- "description": "Codice macchina dell’errore, quando disponibile.",
170577
- "type": "string"
170578
- },
170579
- "error": {
170580
- "description": "Etichetta breve dell’errore.",
170581
- "type": "string"
170582
- },
170583
- "message": {
170584
- "description": "Messaggio leggibile.",
170585
- "type": "string"
170586
- },
170587
- "statusCode": {
170588
- "description": "Codice di stato HTTP ripetuto nel corpo.",
170589
- "type": "integer"
170590
- }
170591
- }
170592
- }
170593
- }
170594
- }
170595
- },
170596
- "403": {
170597
- "description": "Errore standard del backend.",
170598
- "content": {
170599
- "application/json": {
170600
- "schema": {
170601
- "additionalProperties": false,
170602
- "description": "Errore standard del backend.",
170603
- "type": "object",
170604
- "required": [
170605
- "error",
170606
- "message",
170607
- "statusCode"
170608
- ],
170609
- "properties": {
170610
- "code": {
170611
- "description": "Codice macchina dell’errore, quando disponibile.",
170612
- "type": "string"
170613
- },
170614
- "error": {
170615
- "description": "Etichetta breve dell’errore.",
170616
- "type": "string"
170617
- },
170618
- "message": {
170619
- "description": "Messaggio leggibile.",
170620
- "type": "string"
170621
- },
170622
- "statusCode": {
170623
- "description": "Codice di stato HTTP ripetuto nel corpo.",
170624
- "type": "integer"
170625
- }
170626
- }
170627
- }
170628
- }
170629
- }
170630
- }
170631
- }
170632
- }
170633
- },
170634
- "/api/v1/partner/links": {
170635
- "get": {
170636
- "operationId": "listPartnerLinks",
170637
- "summary": "Varianti di link del partner",
170638
- "tags": [
170639
- "partner"
170640
- ],
170641
- "description": "Varianti del partner. Le archiviate escono solo con `include_archived=true`.",
170642
- "parameters": [
170643
- {
170644
- "schema": {
170645
- "type": "boolean"
170646
- },
170647
- "in": "query",
170648
- "name": "include_archived",
170649
- "required": false,
170650
- "description": "Includi le varianti archiviate."
170651
- }
170652
- ],
170653
- "security": [
170654
- {
170655
- "bearerAuth": []
170656
- }
170657
- ],
170658
- "responses": {
170659
- "200": {
170660
- "description": "Varianti del partner, massimo 25 attive.",
170661
- "content": {
170662
- "application/json": {
170663
- "schema": {
170664
- "additionalProperties": false,
170665
- "description": "Varianti del partner, massimo 25 attive.",
170666
- "type": "object",
170667
- "required": [
170668
- "rows"
170669
- ],
170670
- "properties": {
170671
- "rows": {
170672
- "description": "Varianti del partner.",
170673
- "type": "array",
170674
- "items": {
170675
- "additionalProperties": false,
170676
- "description": "Una variante di link del partner.",
170677
- "type": "object",
170678
- "required": [
170679
- "archived",
170680
- "destination_id",
170681
- "id",
170682
- "is_default",
170683
- "name",
170684
- "slug",
170685
- "url",
170686
- "utm_campaign",
170687
- "utm_content",
170688
- "utm_medium",
170689
- "utm_source"
170690
- ],
170691
- "properties": {
170692
- "archived": {
170693
- "description": "Archiviata. Le varianti non si cancellano: orfanerebbero i click storici.",
170694
- "type": "boolean"
170695
- },
170696
- "destination_id": {
170697
- "description": "Destinazione del catalogo curato.",
170698
- "format": "uuid",
171279
+ "label_en": {
171280
+ "description": "Etichetta in inglese.",
170699
171281
  "type": "string"
170700
171282
  },
170701
- "id": {
170702
- "description": "Id della variante.",
170703
- "format": "uuid",
171283
+ "label_it": {
171284
+ "description": "Etichetta in italiano.",
170704
171285
  "type": "string"
170705
171286
  },
170706
- "is_default": {
170707
- "description": "Variante di default del partner.",
170708
- "type": "boolean"
170709
- },
170710
- "name": {
170711
- "description": "Nome scelto dal partner.",
171287
+ "relative_path": {
171288
+ "description": "Percorso relativo servito dal redirector.",
170712
171289
  "type": "string"
170713
171290
  },
170714
171291
  "slug": {
170715
- "description": "Slug pubblico della variante. `null` solo per le varianti create prima di D7.5, che non hanno un alias proprio.",
170716
- "anyOf": [
170717
- {
170718
- "type": "string"
170719
- },
170720
- {
170721
- "type": "null"
170722
- }
170723
- ]
170724
- },
170725
- "url": {
170726
- "description": "URL pubblico della variante, già risolto dal server. Il client non lo compone mai (§12.2).",
170727
- "anyOf": [
170728
- {
170729
- "type": "string"
170730
- },
170731
- {
170732
- "type": "null"
170733
- }
170734
- ]
170735
- },
170736
- "utm_campaign": {
170737
- "description": "UTM campagna della variante: quello chiesto in creazione, normalizzato, oppure quello derivato dal nome quando non è stato chiesto.",
170738
- "type": "string"
170739
- },
170740
- "utm_content": {
170741
- "description": "UTM content, quando presente.",
170742
- "anyOf": [
170743
- {
170744
- "type": "string"
170745
- },
170746
- {
170747
- "type": "null"
170748
- }
170749
- ]
170750
- },
170751
- "utm_medium": {
170752
- "description": "UTM medium generato da Wevion.",
170753
- "type": "string"
170754
- },
170755
- "utm_source": {
170756
- "description": "UTM source generato da Wevion.",
171292
+ "description": "Slug della destinazione nel catalogo.",
170757
171293
  "type": "string"
170758
171294
  }
170759
171295
  }
@@ -170764,41 +171300,247 @@
170764
171300
  }
170765
171301
  }
170766
171302
  },
170767
- "400": {
170768
- "description": "Errore standard del backend.",
170769
- "content": {
170770
- "application/json": {
170771
- "schema": {
170772
- "additionalProperties": false,
170773
- "description": "Errore standard del backend.",
170774
- "type": "object",
170775
- "required": [
170776
- "error",
170777
- "message",
170778
- "statusCode"
170779
- ],
170780
- "properties": {
170781
- "code": {
170782
- "description": "Codice macchina dell’errore, quando disponibile.",
170783
- "type": "string"
170784
- },
170785
- "error": {
170786
- "description": "Etichetta breve dell’errore.",
170787
- "type": "string"
170788
- },
170789
- "message": {
170790
- "description": "Messaggio leggibile.",
170791
- "type": "string"
170792
- },
170793
- "statusCode": {
170794
- "description": "Codice di stato HTTP ripetuto nel corpo.",
170795
- "type": "integer"
170796
- }
170797
- }
170798
- }
170799
- }
170800
- }
170801
- },
171303
+ "401": {
171304
+ "description": "Errore standard del backend.",
171305
+ "content": {
171306
+ "application/json": {
171307
+ "schema": {
171308
+ "additionalProperties": false,
171309
+ "description": "Errore standard del backend.",
171310
+ "type": "object",
171311
+ "required": [
171312
+ "error",
171313
+ "message",
171314
+ "statusCode"
171315
+ ],
171316
+ "properties": {
171317
+ "code": {
171318
+ "description": "Codice macchina dell’errore, quando disponibile.",
171319
+ "type": "string"
171320
+ },
171321
+ "error": {
171322
+ "description": "Etichetta breve dell’errore.",
171323
+ "type": "string"
171324
+ },
171325
+ "message": {
171326
+ "description": "Messaggio leggibile.",
171327
+ "type": "string"
171328
+ },
171329
+ "statusCode": {
171330
+ "description": "Codice di stato HTTP ripetuto nel corpo.",
171331
+ "type": "integer"
171332
+ }
171333
+ }
171334
+ }
171335
+ }
171336
+ }
171337
+ },
171338
+ "403": {
171339
+ "description": "Errore standard del backend.",
171340
+ "content": {
171341
+ "application/json": {
171342
+ "schema": {
171343
+ "additionalProperties": false,
171344
+ "description": "Errore standard del backend.",
171345
+ "type": "object",
171346
+ "required": [
171347
+ "error",
171348
+ "message",
171349
+ "statusCode"
171350
+ ],
171351
+ "properties": {
171352
+ "code": {
171353
+ "description": "Codice macchina dell’errore, quando disponibile.",
171354
+ "type": "string"
171355
+ },
171356
+ "error": {
171357
+ "description": "Etichetta breve dell’errore.",
171358
+ "type": "string"
171359
+ },
171360
+ "message": {
171361
+ "description": "Messaggio leggibile.",
171362
+ "type": "string"
171363
+ },
171364
+ "statusCode": {
171365
+ "description": "Codice di stato HTTP ripetuto nel corpo.",
171366
+ "type": "integer"
171367
+ }
171368
+ }
171369
+ }
171370
+ }
171371
+ }
171372
+ }
171373
+ }
171374
+ }
171375
+ },
171376
+ "/api/v1/partner/links": {
171377
+ "get": {
171378
+ "operationId": "listPartnerLinks",
171379
+ "summary": "Varianti di link del partner",
171380
+ "tags": [
171381
+ "partner"
171382
+ ],
171383
+ "description": "Varianti del partner. Le archiviate escono solo con `include_archived=true`.",
171384
+ "parameters": [
171385
+ {
171386
+ "schema": {
171387
+ "type": "boolean"
171388
+ },
171389
+ "in": "query",
171390
+ "name": "include_archived",
171391
+ "required": false,
171392
+ "description": "Includi le varianti archiviate."
171393
+ }
171394
+ ],
171395
+ "security": [
171396
+ {
171397
+ "bearerAuth": []
171398
+ }
171399
+ ],
171400
+ "responses": {
171401
+ "200": {
171402
+ "description": "Varianti del partner, massimo 25 attive.",
171403
+ "content": {
171404
+ "application/json": {
171405
+ "schema": {
171406
+ "additionalProperties": false,
171407
+ "description": "Varianti del partner, massimo 25 attive.",
171408
+ "type": "object",
171409
+ "required": [
171410
+ "rows"
171411
+ ],
171412
+ "properties": {
171413
+ "rows": {
171414
+ "description": "Varianti del partner.",
171415
+ "type": "array",
171416
+ "items": {
171417
+ "additionalProperties": false,
171418
+ "description": "Una variante di link del partner.",
171419
+ "type": "object",
171420
+ "required": [
171421
+ "archived",
171422
+ "destination_id",
171423
+ "id",
171424
+ "is_default",
171425
+ "name",
171426
+ "slug",
171427
+ "url",
171428
+ "utm_campaign",
171429
+ "utm_content",
171430
+ "utm_medium",
171431
+ "utm_source"
171432
+ ],
171433
+ "properties": {
171434
+ "archived": {
171435
+ "description": "Archiviata. Le varianti non si cancellano: orfanerebbero i click storici.",
171436
+ "type": "boolean"
171437
+ },
171438
+ "destination_id": {
171439
+ "description": "Destinazione del catalogo curato.",
171440
+ "format": "uuid",
171441
+ "type": "string"
171442
+ },
171443
+ "id": {
171444
+ "description": "Id della variante.",
171445
+ "format": "uuid",
171446
+ "type": "string"
171447
+ },
171448
+ "is_default": {
171449
+ "description": "Variante di default del partner.",
171450
+ "type": "boolean"
171451
+ },
171452
+ "name": {
171453
+ "description": "Nome scelto dal partner.",
171454
+ "type": "string"
171455
+ },
171456
+ "slug": {
171457
+ "description": "Slug pubblico della variante. `null` solo per le varianti create prima di D7.5, che non hanno un alias proprio.",
171458
+ "anyOf": [
171459
+ {
171460
+ "type": "string"
171461
+ },
171462
+ {
171463
+ "type": "null"
171464
+ }
171465
+ ]
171466
+ },
171467
+ "url": {
171468
+ "description": "URL pubblico della variante, già risolto dal server. Il client non lo compone mai (§12.2).",
171469
+ "anyOf": [
171470
+ {
171471
+ "type": "string"
171472
+ },
171473
+ {
171474
+ "type": "null"
171475
+ }
171476
+ ]
171477
+ },
171478
+ "utm_campaign": {
171479
+ "description": "UTM campagna della variante: quello chiesto in creazione, normalizzato, oppure quello derivato dal nome quando non è stato chiesto.",
171480
+ "type": "string"
171481
+ },
171482
+ "utm_content": {
171483
+ "description": "UTM content, quando presente.",
171484
+ "anyOf": [
171485
+ {
171486
+ "type": "string"
171487
+ },
171488
+ {
171489
+ "type": "null"
171490
+ }
171491
+ ]
171492
+ },
171493
+ "utm_medium": {
171494
+ "description": "UTM medium generato da Wevion.",
171495
+ "type": "string"
171496
+ },
171497
+ "utm_source": {
171498
+ "description": "UTM source generato da Wevion.",
171499
+ "type": "string"
171500
+ }
171501
+ }
171502
+ }
171503
+ }
171504
+ }
171505
+ }
171506
+ }
171507
+ }
171508
+ },
171509
+ "400": {
171510
+ "description": "Errore standard del backend.",
171511
+ "content": {
171512
+ "application/json": {
171513
+ "schema": {
171514
+ "additionalProperties": false,
171515
+ "description": "Errore standard del backend.",
171516
+ "type": "object",
171517
+ "required": [
171518
+ "error",
171519
+ "message",
171520
+ "statusCode"
171521
+ ],
171522
+ "properties": {
171523
+ "code": {
171524
+ "description": "Codice macchina dell’errore, quando disponibile.",
171525
+ "type": "string"
171526
+ },
171527
+ "error": {
171528
+ "description": "Etichetta breve dell’errore.",
171529
+ "type": "string"
171530
+ },
171531
+ "message": {
171532
+ "description": "Messaggio leggibile.",
171533
+ "type": "string"
171534
+ },
171535
+ "statusCode": {
171536
+ "description": "Codice di stato HTTP ripetuto nel corpo.",
171537
+ "type": "integer"
171538
+ }
171539
+ }
171540
+ }
171541
+ }
171542
+ }
171543
+ },
170802
171544
  "401": {
170803
171545
  "description": "Errore standard del backend.",
170804
171546
  "content": {
@@ -174391,6 +175133,12 @@
174391
175133
  "tiktok"
174392
175134
  ]
174393
175135
  },
175136
+ {
175137
+ "type": "string",
175138
+ "enum": [
175139
+ "exoclick"
175140
+ ]
175141
+ },
174394
175142
  {
174395
175143
  "type": "string",
174396
175144
  "enum": [
@@ -180689,6 +181437,119 @@
180689
181437
  }
180690
181438
  }
180691
181439
  },
181440
+ "/api/v1/public/trial-offer": {
181441
+ "get": {
181442
+ "operationId": "getPublicTrialOffer",
181443
+ "summary": "Offerta di prova gratuita di listino",
181444
+ "tags": [
181445
+ "public"
181446
+ ],
181447
+ "description": "Durata della prova gratuita e piani che la offrono. Rotta PUBBLICA: la legge la pagina di registrazione, dove una sessione non esiste ancora e `GET /billing/plans` non è raggiungibile. Risponde attraverso la stessa funzione che concede il trial al checkout, così la pagina di iscrizione non può promettere una prova che il backend non darebbe. Non dice nulla sull'eleggibilità di chi guarda: quella è `trial_eligible`, dentro l'app.",
181448
+ "responses": {
181449
+ "200": {
181450
+ "description": "Successful response",
181451
+ "content": {
181452
+ "application/json": {
181453
+ "schema": {
181454
+ "additionalProperties": false,
181455
+ "type": "object",
181456
+ "required": [
181457
+ "days",
181458
+ "plans"
181459
+ ],
181460
+ "properties": {
181461
+ "days": {
181462
+ "description": "Durata della prova in giorni. 0 quando nessun piano di listino ne offre una.",
181463
+ "minimum": 0,
181464
+ "type": "integer"
181465
+ },
181466
+ "plans": {
181467
+ "description": "Nomi visualizzati dei piani pubblicati che offrono la prova, in ordine di listino. Vuoto quando `days` è 0.",
181468
+ "type": "array",
181469
+ "items": {
181470
+ "type": "string"
181471
+ }
181472
+ }
181473
+ }
181474
+ }
181475
+ }
181476
+ }
181477
+ },
181478
+ "429": {
181479
+ "description": "Too many requests - rate limit exceeded",
181480
+ "content": {
181481
+ "application/json": {
181482
+ "schema": {
181483
+ "type": "object",
181484
+ "required": [
181485
+ "statusCode",
181486
+ "error",
181487
+ "message"
181488
+ ],
181489
+ "properties": {
181490
+ "statusCode": {
181491
+ "type": "integer"
181492
+ },
181493
+ "error": {
181494
+ "type": "string"
181495
+ },
181496
+ "message": {
181497
+ "type": "string"
181498
+ },
181499
+ "code": {
181500
+ "type": "string"
181501
+ },
181502
+ "request_id": {
181503
+ "type": "string"
181504
+ },
181505
+ "details": {
181506
+ "type": "object",
181507
+ "additionalProperties": {}
181508
+ }
181509
+ }
181510
+ }
181511
+ }
181512
+ }
181513
+ },
181514
+ "500": {
181515
+ "description": "Internal server error",
181516
+ "content": {
181517
+ "application/json": {
181518
+ "schema": {
181519
+ "type": "object",
181520
+ "required": [
181521
+ "statusCode",
181522
+ "error",
181523
+ "message"
181524
+ ],
181525
+ "properties": {
181526
+ "statusCode": {
181527
+ "type": "integer"
181528
+ },
181529
+ "error": {
181530
+ "type": "string"
181531
+ },
181532
+ "message": {
181533
+ "type": "string"
181534
+ },
181535
+ "code": {
181536
+ "type": "string"
181537
+ },
181538
+ "request_id": {
181539
+ "type": "string"
181540
+ },
181541
+ "details": {
181542
+ "type": "object",
181543
+ "additionalProperties": {}
181544
+ }
181545
+ }
181546
+ }
181547
+ }
181548
+ }
181549
+ }
181550
+ }
181551
+ }
181552
+ },
180692
181553
  "/api/v1/rbac/access-groups": {
180693
181554
  "get": {
180694
181555
  "operationId": "getApiV1RbacAccessGroups",
@@ -186208,6 +187069,12 @@
186208
187069
  "enum": [
186209
187070
  "outbrain"
186210
187071
  ]
187072
+ },
187073
+ {
187074
+ "type": "string",
187075
+ "enum": [
187076
+ "exoclick"
187077
+ ]
186211
187078
  }
186212
187079
  ]
186213
187080
  },
@@ -293461,6 +294328,12 @@
293461
294328
  "enum": [
293462
294329
  "outbrain"
293463
294330
  ]
294331
+ },
294332
+ {
294333
+ "type": "string",
294334
+ "enum": [
294335
+ "exoclick"
294336
+ ]
293464
294337
  }
293465
294338
  ]
293466
294339
  },
@@ -318455,6 +319328,1690 @@
318455
319328
  }
318456
319329
  }
318457
319330
  },
319331
+ "/api/v1/exoclick/connect-token": {
319332
+ "post": {
319333
+ "operationId": "exoclickConnectToken",
319334
+ "summary": "Connect ExoClick via pasted API token (no OAuth)",
319335
+ "tags": [
319336
+ "exoclick"
319337
+ ],
319338
+ "description": "Validates a manually-pasted ExoClick API token against GET /user, then persists it for the current session, stamping the destination workspace team (x-team-id header or active team scope — 400 SCOPE_TEAM_REQUIRED without it). ExoClick has no advertiser entity, so a single synthetic ad account is created from the token's user. An account already assigned to another workspace of the organization answers 409 ACCOUNT_ALREADY_ASSIGNED unless confirm_move=true. Goals (ExoClick's conversion primitive) are imported in the same call.",
319339
+ "requestBody": {
319340
+ "required": true,
319341
+ "content": {
319342
+ "application/json": {
319343
+ "schema": {
319344
+ "additionalProperties": false,
319345
+ "type": "object",
319346
+ "required": [
319347
+ "api_token"
319348
+ ],
319349
+ "properties": {
319350
+ "api_token": {
319351
+ "minLength": 16,
319352
+ "description": "Durable ExoClick API token, created by the buyer under Profile → API tokens.",
319353
+ "type": "string"
319354
+ },
319355
+ "confirm_move": {
319356
+ "description": "Confirm moving an account already assigned to another workspace of the organization into the active one (409 ACCOUNT_ALREADY_ASSIGNED otherwise). The move reassigns every row of the account and re-stamps its historical analytics.",
319357
+ "type": "boolean"
319358
+ }
319359
+ }
319360
+ }
319361
+ }
319362
+ }
319363
+ },
319364
+ "parameters": [
319365
+ {
319366
+ "schema": {
319367
+ "format": "uuid",
319368
+ "type": "string"
319369
+ },
319370
+ "in": "header",
319371
+ "name": "x-team-id",
319372
+ "required": false,
319373
+ "description": "Destination workspace team id."
319374
+ }
319375
+ ],
319376
+ "security": [
319377
+ {
319378
+ "bearerAuth": []
319379
+ }
319380
+ ],
319381
+ "responses": {
319382
+ "200": {
319383
+ "description": "Successful response",
319384
+ "content": {
319385
+ "application/json": {
319386
+ "schema": {
319387
+ "additionalProperties": false,
319388
+ "type": "object",
319389
+ "required": [
319390
+ "success",
319391
+ "account",
319392
+ "goals_synced"
319393
+ ],
319394
+ "properties": {
319395
+ "success": {
319396
+ "type": "boolean"
319397
+ },
319398
+ "account": {
319399
+ "additionalProperties": false,
319400
+ "type": "object",
319401
+ "required": [
319402
+ "id",
319403
+ "name",
319404
+ "currency",
319405
+ "timezone"
319406
+ ],
319407
+ "properties": {
319408
+ "id": {
319409
+ "description": "ExoClick user id — the synthetic ad account identifier.",
319410
+ "type": "string"
319411
+ },
319412
+ "name": {
319413
+ "type": "string"
319414
+ },
319415
+ "currency": {
319416
+ "type": "string"
319417
+ },
319418
+ "timezone": {
319419
+ "type": "string"
319420
+ }
319421
+ }
319422
+ },
319423
+ "goals_synced": {
319424
+ "description": "Number of ExoClick goals imported as conversion primitives.",
319425
+ "type": "integer"
319426
+ }
319427
+ }
319428
+ }
319429
+ }
319430
+ }
319431
+ },
319432
+ "400": {
319433
+ "description": "Explicit destination workspace required: send the x-team-id header or activate a team scope for this operation.",
319434
+ "content": {
319435
+ "application/json": {
319436
+ "schema": {
319437
+ "description": "Explicit destination workspace required: send the x-team-id header or activate a team scope for this operation.",
319438
+ "additionalProperties": false,
319439
+ "type": "object",
319440
+ "required": [
319441
+ "statusCode",
319442
+ "error",
319443
+ "message"
319444
+ ],
319445
+ "properties": {
319446
+ "statusCode": {
319447
+ "type": "number"
319448
+ },
319449
+ "error": {
319450
+ "type": "string"
319451
+ },
319452
+ "message": {
319453
+ "type": "string"
319454
+ },
319455
+ "code": {
319456
+ "type": "string",
319457
+ "enum": [
319458
+ "SCOPE_TEAM_REQUIRED"
319459
+ ]
319460
+ }
319461
+ }
319462
+ }
319463
+ }
319464
+ }
319465
+ },
319466
+ "401": {
319467
+ "description": "Unauthorized - missing or invalid bearer token",
319468
+ "content": {
319469
+ "application/json": {
319470
+ "schema": {
319471
+ "type": "object",
319472
+ "required": [
319473
+ "error"
319474
+ ],
319475
+ "properties": {
319476
+ "error": {
319477
+ "type": "string"
319478
+ },
319479
+ "message": {
319480
+ "type": "string"
319481
+ }
319482
+ }
319483
+ }
319484
+ }
319485
+ }
319486
+ },
319487
+ "403": {
319488
+ "description": "Forbidden - caller lacks the required role/permission",
319489
+ "content": {
319490
+ "application/json": {
319491
+ "schema": {
319492
+ "type": "object",
319493
+ "required": [
319494
+ "error"
319495
+ ],
319496
+ "properties": {
319497
+ "error": {
319498
+ "type": "string"
319499
+ },
319500
+ "message": {
319501
+ "type": "string"
319502
+ }
319503
+ }
319504
+ }
319505
+ }
319506
+ }
319507
+ },
319508
+ "409": {
319509
+ "description": "The external ad account is already assigned to another workspace of your organization. Retry with confirm_move=true to move it (and its historical analytics) into the active workspace.",
319510
+ "content": {
319511
+ "application/json": {
319512
+ "schema": {
319513
+ "additionalProperties": false,
319514
+ "description": "The external ad account is already assigned to another workspace of your organization. Retry with confirm_move=true to move it (and its historical analytics) into the active workspace.",
319515
+ "type": "object",
319516
+ "required": [
319517
+ "statusCode",
319518
+ "error",
319519
+ "message",
319520
+ "code",
319521
+ "current_team_id",
319522
+ "current_workspace_name",
319523
+ "external_account_id",
319524
+ "platform"
319525
+ ],
319526
+ "properties": {
319527
+ "statusCode": {
319528
+ "type": "number"
319529
+ },
319530
+ "error": {
319531
+ "type": "string"
319532
+ },
319533
+ "message": {
319534
+ "type": "string"
319535
+ },
319536
+ "code": {
319537
+ "type": "string",
319538
+ "enum": [
319539
+ "ACCOUNT_ALREADY_ASSIGNED"
319540
+ ]
319541
+ },
319542
+ "current_team_id": {
319543
+ "format": "uuid",
319544
+ "type": "string"
319545
+ },
319546
+ "current_workspace_name": {
319547
+ "anyOf": [
319548
+ {
319549
+ "type": "string"
319550
+ },
319551
+ {
319552
+ "type": "null"
319553
+ }
319554
+ ]
319555
+ },
319556
+ "external_account_id": {
319557
+ "type": "string"
319558
+ },
319559
+ "platform": {
319560
+ "type": "string"
319561
+ }
319562
+ }
319563
+ }
319564
+ }
319565
+ }
319566
+ },
319567
+ "429": {
319568
+ "description": "Too many requests - rate limit exceeded",
319569
+ "content": {
319570
+ "application/json": {
319571
+ "schema": {
319572
+ "type": "object",
319573
+ "required": [
319574
+ "error"
319575
+ ],
319576
+ "properties": {
319577
+ "error": {
319578
+ "type": "string"
319579
+ },
319580
+ "message": {
319581
+ "type": "string"
319582
+ }
319583
+ }
319584
+ }
319585
+ }
319586
+ }
319587
+ },
319588
+ "500": {
319589
+ "description": "Internal server error",
319590
+ "content": {
319591
+ "application/json": {
319592
+ "schema": {
319593
+ "type": "object",
319594
+ "required": [
319595
+ "error"
319596
+ ],
319597
+ "properties": {
319598
+ "error": {
319599
+ "type": "string"
319600
+ },
319601
+ "message": {
319602
+ "type": "string"
319603
+ }
319604
+ }
319605
+ }
319606
+ }
319607
+ }
319608
+ },
319609
+ "503": {
319610
+ "description": "Service unavailable",
319611
+ "content": {
319612
+ "application/json": {
319613
+ "schema": {
319614
+ "type": "object",
319615
+ "required": [
319616
+ "error"
319617
+ ],
319618
+ "properties": {
319619
+ "error": {
319620
+ "type": "string"
319621
+ },
319622
+ "message": {
319623
+ "type": "string"
319624
+ }
319625
+ }
319626
+ }
319627
+ }
319628
+ }
319629
+ }
319630
+ }
319631
+ }
319632
+ },
319633
+ "/api/v1/exoclick/ad-accounts": {
319634
+ "get": {
319635
+ "operationId": "exoclickListAdAccounts",
319636
+ "summary": "List connected ExoClick ad accounts",
319637
+ "tags": [
319638
+ "exoclick"
319639
+ ],
319640
+ "description": "Returns the ExoClick accounts stored for the active workspace. ExoClick has no advertiser entity, so this list holds at most one synthetic row per connected token; it exists so Asset Hub, plan quotas and the Ads Manager filters have an account to hang off.",
319641
+ "parameters": [
319642
+ {
319643
+ "schema": {
319644
+ "format": "uuid",
319645
+ "type": "string"
319646
+ },
319647
+ "in": "header",
319648
+ "name": "x-team-id",
319649
+ "required": false,
319650
+ "description": "Destination workspace team id."
319651
+ }
319652
+ ],
319653
+ "security": [
319654
+ {
319655
+ "bearerAuth": []
319656
+ }
319657
+ ],
319658
+ "responses": {
319659
+ "200": {
319660
+ "description": "Successful response",
319661
+ "content": {
319662
+ "application/json": {
319663
+ "schema": {
319664
+ "additionalProperties": false,
319665
+ "type": "object",
319666
+ "required": [
319667
+ "items",
319668
+ "total"
319669
+ ],
319670
+ "properties": {
319671
+ "items": {
319672
+ "type": "array",
319673
+ "items": {
319674
+ "additionalProperties": false,
319675
+ "type": "object",
319676
+ "required": [
319677
+ "id",
319678
+ "external_account_id",
319679
+ "name",
319680
+ "currency",
319681
+ "timezone",
319682
+ "account_status",
319683
+ "connected"
319684
+ ],
319685
+ "properties": {
319686
+ "id": {
319687
+ "description": "Internal row id.",
319688
+ "type": "string"
319689
+ },
319690
+ "external_account_id": {
319691
+ "type": "string"
319692
+ },
319693
+ "name": {
319694
+ "type": "string"
319695
+ },
319696
+ "currency": {
319697
+ "anyOf": [
319698
+ {
319699
+ "type": "string"
319700
+ },
319701
+ {
319702
+ "type": "null"
319703
+ }
319704
+ ]
319705
+ },
319706
+ "timezone": {
319707
+ "anyOf": [
319708
+ {
319709
+ "type": "string"
319710
+ },
319711
+ {
319712
+ "type": "null"
319713
+ }
319714
+ ]
319715
+ },
319716
+ "account_status": {
319717
+ "type": "string"
319718
+ },
319719
+ "connected": {
319720
+ "type": "boolean"
319721
+ }
319722
+ }
319723
+ }
319724
+ },
319725
+ "total": {
319726
+ "type": "integer"
319727
+ }
319728
+ }
319729
+ }
319730
+ }
319731
+ }
319732
+ },
319733
+ "401": {
319734
+ "description": "Unauthorized - missing or invalid bearer token",
319735
+ "content": {
319736
+ "application/json": {
319737
+ "schema": {
319738
+ "type": "object",
319739
+ "required": [
319740
+ "error"
319741
+ ],
319742
+ "properties": {
319743
+ "error": {
319744
+ "type": "string"
319745
+ },
319746
+ "message": {
319747
+ "type": "string"
319748
+ }
319749
+ }
319750
+ }
319751
+ }
319752
+ }
319753
+ },
319754
+ "403": {
319755
+ "description": "Forbidden - caller lacks the required role/permission",
319756
+ "content": {
319757
+ "application/json": {
319758
+ "schema": {
319759
+ "type": "object",
319760
+ "required": [
319761
+ "error"
319762
+ ],
319763
+ "properties": {
319764
+ "error": {
319765
+ "type": "string"
319766
+ },
319767
+ "message": {
319768
+ "type": "string"
319769
+ }
319770
+ }
319771
+ }
319772
+ }
319773
+ }
319774
+ },
319775
+ "503": {
319776
+ "description": "Service unavailable",
319777
+ "content": {
319778
+ "application/json": {
319779
+ "schema": {
319780
+ "type": "object",
319781
+ "required": [
319782
+ "error"
319783
+ ],
319784
+ "properties": {
319785
+ "error": {
319786
+ "type": "string"
319787
+ },
319788
+ "message": {
319789
+ "type": "string"
319790
+ }
319791
+ }
319792
+ }
319793
+ }
319794
+ }
319795
+ }
319796
+ }
319797
+ }
319798
+ },
319799
+ "/api/v1/exoclick/ad-accounts/sync": {
319800
+ "post": {
319801
+ "operationId": "exoclickSyncAdAccounts",
319802
+ "summary": "Re-validate the ExoClick token and refresh its goals",
319803
+ "tags": [
319804
+ "exoclick"
319805
+ ],
319806
+ "description": "Re-probes GET /user with the stored token and refreshes the goal list. There is nothing else to enumerate: ExoClick exposes exactly one account per token. A token the API now rejects is marked invalid so the UI can ask for a re-paste instead of failing silently on the next sync.",
319807
+ "parameters": [
319808
+ {
319809
+ "schema": {
319810
+ "format": "uuid",
319811
+ "type": "string"
319812
+ },
319813
+ "in": "header",
319814
+ "name": "x-team-id",
319815
+ "required": false,
319816
+ "description": "Destination workspace team id."
319817
+ }
319818
+ ],
319819
+ "security": [
319820
+ {
319821
+ "bearerAuth": []
319822
+ }
319823
+ ],
319824
+ "responses": {
319825
+ "200": {
319826
+ "description": "Successful response",
319827
+ "content": {
319828
+ "application/json": {
319829
+ "schema": {
319830
+ "additionalProperties": false,
319831
+ "type": "object",
319832
+ "required": [
319833
+ "success",
319834
+ "goals_synced"
319835
+ ],
319836
+ "properties": {
319837
+ "success": {
319838
+ "type": "boolean"
319839
+ },
319840
+ "goals_synced": {
319841
+ "type": "integer"
319842
+ }
319843
+ }
319844
+ }
319845
+ }
319846
+ }
319847
+ },
319848
+ "401": {
319849
+ "description": "Unauthorized - missing or invalid bearer token",
319850
+ "content": {
319851
+ "application/json": {
319852
+ "schema": {
319853
+ "type": "object",
319854
+ "required": [
319855
+ "error"
319856
+ ],
319857
+ "properties": {
319858
+ "error": {
319859
+ "type": "string"
319860
+ },
319861
+ "message": {
319862
+ "type": "string"
319863
+ }
319864
+ }
319865
+ }
319866
+ }
319867
+ }
319868
+ },
319869
+ "403": {
319870
+ "description": "Forbidden - caller lacks the required role/permission",
319871
+ "content": {
319872
+ "application/json": {
319873
+ "schema": {
319874
+ "type": "object",
319875
+ "required": [
319876
+ "error"
319877
+ ],
319878
+ "properties": {
319879
+ "error": {
319880
+ "type": "string"
319881
+ },
319882
+ "message": {
319883
+ "type": "string"
319884
+ }
319885
+ }
319886
+ }
319887
+ }
319888
+ }
319889
+ },
319890
+ "404": {
319891
+ "description": "Resource not found",
319892
+ "content": {
319893
+ "application/json": {
319894
+ "schema": {
319895
+ "type": "object",
319896
+ "required": [
319897
+ "error"
319898
+ ],
319899
+ "properties": {
319900
+ "error": {
319901
+ "type": "string"
319902
+ },
319903
+ "message": {
319904
+ "type": "string"
319905
+ }
319906
+ }
319907
+ }
319908
+ }
319909
+ }
319910
+ },
319911
+ "429": {
319912
+ "description": "Too many requests - rate limit exceeded",
319913
+ "content": {
319914
+ "application/json": {
319915
+ "schema": {
319916
+ "type": "object",
319917
+ "required": [
319918
+ "error"
319919
+ ],
319920
+ "properties": {
319921
+ "error": {
319922
+ "type": "string"
319923
+ },
319924
+ "message": {
319925
+ "type": "string"
319926
+ }
319927
+ }
319928
+ }
319929
+ }
319930
+ }
319931
+ },
319932
+ "500": {
319933
+ "description": "Internal server error",
319934
+ "content": {
319935
+ "application/json": {
319936
+ "schema": {
319937
+ "type": "object",
319938
+ "required": [
319939
+ "error"
319940
+ ],
319941
+ "properties": {
319942
+ "error": {
319943
+ "type": "string"
319944
+ },
319945
+ "message": {
319946
+ "type": "string"
319947
+ }
319948
+ }
319949
+ }
319950
+ }
319951
+ }
319952
+ },
319953
+ "503": {
319954
+ "description": "Service unavailable",
319955
+ "content": {
319956
+ "application/json": {
319957
+ "schema": {
319958
+ "type": "object",
319959
+ "required": [
319960
+ "error"
319961
+ ],
319962
+ "properties": {
319963
+ "error": {
319964
+ "type": "string"
319965
+ },
319966
+ "message": {
319967
+ "type": "string"
319968
+ }
319969
+ }
319970
+ }
319971
+ }
319972
+ }
319973
+ }
319974
+ }
319975
+ }
319976
+ },
319977
+ "/api/v1/exoclick/goals": {
319978
+ "get": {
319979
+ "operationId": "exoclickListGoals",
319980
+ "summary": "List ExoClick goals (the platform’s pixel equivalent)",
319981
+ "tags": [
319982
+ "exoclick"
319983
+ ],
319984
+ "description": "Returns the goals stored for the active workspace. A goal is what an ExoClick conversion is counted against, so the launch wizard needs one before it can bid on conversions. A goal of type 0 (No Value) never carries revenue and must not be offered for ROAS bidding.",
319985
+ "parameters": [
319986
+ {
319987
+ "schema": {
319988
+ "format": "uuid",
319989
+ "type": "string"
319990
+ },
319991
+ "in": "header",
319992
+ "name": "x-team-id",
319993
+ "required": false,
319994
+ "description": "Destination workspace team id."
319995
+ }
319996
+ ],
319997
+ "security": [
319998
+ {
319999
+ "bearerAuth": []
320000
+ }
320001
+ ],
320002
+ "responses": {
320003
+ "200": {
320004
+ "description": "Successful response",
320005
+ "content": {
320006
+ "application/json": {
320007
+ "schema": {
320008
+ "additionalProperties": false,
320009
+ "type": "object",
320010
+ "required": [
320011
+ "items",
320012
+ "total"
320013
+ ],
320014
+ "properties": {
320015
+ "items": {
320016
+ "type": "array",
320017
+ "items": {
320018
+ "additionalProperties": false,
320019
+ "type": "object",
320020
+ "required": [
320021
+ "id",
320022
+ "goal_id",
320023
+ "name",
320024
+ "goal_type"
320025
+ ],
320026
+ "properties": {
320027
+ "id": {
320028
+ "type": "string"
320029
+ },
320030
+ "goal_id": {
320031
+ "type": "string"
320032
+ },
320033
+ "name": {
320034
+ "type": "string"
320035
+ },
320036
+ "goal_type": {
320037
+ "description": "0 = No Value (never carries revenue), 1 = Fixed, 2 = Dynamic.",
320038
+ "anyOf": [
320039
+ {
320040
+ "type": "integer"
320041
+ },
320042
+ {
320043
+ "type": "null"
320044
+ }
320045
+ ]
320046
+ }
320047
+ }
320048
+ }
320049
+ },
320050
+ "total": {
320051
+ "type": "integer"
320052
+ }
320053
+ }
320054
+ }
320055
+ }
320056
+ }
320057
+ },
320058
+ "401": {
320059
+ "description": "Unauthorized - missing or invalid bearer token",
320060
+ "content": {
320061
+ "application/json": {
320062
+ "schema": {
320063
+ "type": "object",
320064
+ "required": [
320065
+ "error"
320066
+ ],
320067
+ "properties": {
320068
+ "error": {
320069
+ "type": "string"
320070
+ },
320071
+ "message": {
320072
+ "type": "string"
320073
+ }
320074
+ }
320075
+ }
320076
+ }
320077
+ }
320078
+ },
320079
+ "403": {
320080
+ "description": "Forbidden - caller lacks the required role/permission",
320081
+ "content": {
320082
+ "application/json": {
320083
+ "schema": {
320084
+ "type": "object",
320085
+ "required": [
320086
+ "error"
320087
+ ],
320088
+ "properties": {
320089
+ "error": {
320090
+ "type": "string"
320091
+ },
320092
+ "message": {
320093
+ "type": "string"
320094
+ }
320095
+ }
320096
+ }
320097
+ }
320098
+ }
320099
+ },
320100
+ "503": {
320101
+ "description": "Service unavailable",
320102
+ "content": {
320103
+ "application/json": {
320104
+ "schema": {
320105
+ "type": "object",
320106
+ "required": [
320107
+ "error"
320108
+ ],
320109
+ "properties": {
320110
+ "error": {
320111
+ "type": "string"
320112
+ },
320113
+ "message": {
320114
+ "type": "string"
320115
+ }
320116
+ }
320117
+ }
320118
+ }
320119
+ }
320120
+ }
320121
+ }
320122
+ }
320123
+ },
320124
+ "/api/v1/exoclick/toggle-account": {
320125
+ "post": {
320126
+ "operationId": "exoclickToggleAccount",
320127
+ "summary": "Enable or disable an ExoClick ad account",
320128
+ "tags": [
320129
+ "exoclick"
320130
+ ],
320131
+ "description": "Flips the Asset Hub `connected` flag for one ExoClick account. Disabling stops insight collection for it without revoking the token; use POST /api/v1/exoclick/disconnect to drop the credential entirely.",
320132
+ "requestBody": {
320133
+ "required": true,
320134
+ "content": {
320135
+ "application/json": {
320136
+ "schema": {
320137
+ "additionalProperties": false,
320138
+ "type": "object",
320139
+ "required": [
320140
+ "account_id",
320141
+ "enabled"
320142
+ ],
320143
+ "properties": {
320144
+ "account_id": {
320145
+ "minLength": 1,
320146
+ "type": "string"
320147
+ },
320148
+ "enabled": {
320149
+ "type": "boolean"
320150
+ }
320151
+ }
320152
+ }
320153
+ }
320154
+ }
320155
+ },
320156
+ "parameters": [
320157
+ {
320158
+ "schema": {
320159
+ "format": "uuid",
320160
+ "type": "string"
320161
+ },
320162
+ "in": "header",
320163
+ "name": "x-team-id",
320164
+ "required": false,
320165
+ "description": "Destination workspace team id."
320166
+ }
320167
+ ],
320168
+ "security": [
320169
+ {
320170
+ "bearerAuth": []
320171
+ }
320172
+ ],
320173
+ "responses": {
320174
+ "200": {
320175
+ "description": "Successful response",
320176
+ "content": {
320177
+ "application/json": {
320178
+ "schema": {
320179
+ "additionalProperties": false,
320180
+ "type": "object",
320181
+ "required": [
320182
+ "success"
320183
+ ],
320184
+ "properties": {
320185
+ "success": {
320186
+ "type": "boolean"
320187
+ }
320188
+ }
320189
+ }
320190
+ }
320191
+ }
320192
+ },
320193
+ "401": {
320194
+ "description": "Unauthorized - missing or invalid bearer token",
320195
+ "content": {
320196
+ "application/json": {
320197
+ "schema": {
320198
+ "type": "object",
320199
+ "required": [
320200
+ "error"
320201
+ ],
320202
+ "properties": {
320203
+ "error": {
320204
+ "type": "string"
320205
+ },
320206
+ "message": {
320207
+ "type": "string"
320208
+ }
320209
+ }
320210
+ }
320211
+ }
320212
+ }
320213
+ },
320214
+ "403": {
320215
+ "description": "Forbidden - caller lacks the required role/permission",
320216
+ "content": {
320217
+ "application/json": {
320218
+ "schema": {
320219
+ "type": "object",
320220
+ "required": [
320221
+ "error"
320222
+ ],
320223
+ "properties": {
320224
+ "error": {
320225
+ "type": "string"
320226
+ },
320227
+ "message": {
320228
+ "type": "string"
320229
+ }
320230
+ }
320231
+ }
320232
+ }
320233
+ }
320234
+ },
320235
+ "404": {
320236
+ "description": "Resource not found",
320237
+ "content": {
320238
+ "application/json": {
320239
+ "schema": {
320240
+ "type": "object",
320241
+ "required": [
320242
+ "error"
320243
+ ],
320244
+ "properties": {
320245
+ "error": {
320246
+ "type": "string"
320247
+ },
320248
+ "message": {
320249
+ "type": "string"
320250
+ }
320251
+ }
320252
+ }
320253
+ }
320254
+ }
320255
+ },
320256
+ "503": {
320257
+ "description": "Service unavailable",
320258
+ "content": {
320259
+ "application/json": {
320260
+ "schema": {
320261
+ "type": "object",
320262
+ "required": [
320263
+ "error"
320264
+ ],
320265
+ "properties": {
320266
+ "error": {
320267
+ "type": "string"
320268
+ },
320269
+ "message": {
320270
+ "type": "string"
320271
+ }
320272
+ }
320273
+ }
320274
+ }
320275
+ }
320276
+ }
320277
+ }
320278
+ }
320279
+ },
320280
+ "/api/v1/exoclick/campaigns/{campaignId}/status": {
320281
+ "post": {
320282
+ "operationId": "exoclickUpdateCampaignStatus",
320283
+ "summary": "Play or pause an ExoClick campaign",
320284
+ "tags": [
320285
+ "exoclick"
320286
+ ],
320287
+ "description": "Routes to PUT /campaigns/play or /campaigns/pause. ExoClick exposes no per-campaign status endpoint: both are bulk routes taking an array of ids, and this operation sends an array of one.",
320288
+ "requestBody": {
320289
+ "required": true,
320290
+ "content": {
320291
+ "application/json": {
320292
+ "schema": {
320293
+ "additionalProperties": false,
320294
+ "type": "object",
320295
+ "required": [
320296
+ "status"
320297
+ ],
320298
+ "properties": {
320299
+ "status": {
320300
+ "anyOf": [
320301
+ {
320302
+ "type": "string",
320303
+ "enum": [
320304
+ "ACTIVE"
320305
+ ]
320306
+ },
320307
+ {
320308
+ "type": "string",
320309
+ "enum": [
320310
+ "PAUSED"
320311
+ ]
320312
+ }
320313
+ ]
320314
+ }
320315
+ }
320316
+ }
320317
+ }
320318
+ }
320319
+ },
320320
+ "parameters": [
320321
+ {
320322
+ "schema": {
320323
+ "minLength": 1,
320324
+ "type": "string"
320325
+ },
320326
+ "in": "path",
320327
+ "name": "campaignId",
320328
+ "required": true
320329
+ },
320330
+ {
320331
+ "schema": {
320332
+ "format": "uuid",
320333
+ "type": "string"
320334
+ },
320335
+ "in": "header",
320336
+ "name": "x-team-id",
320337
+ "required": false,
320338
+ "description": "Destination workspace team id."
320339
+ }
320340
+ ],
320341
+ "security": [
320342
+ {
320343
+ "bearerAuth": []
320344
+ }
320345
+ ],
320346
+ "responses": {
320347
+ "200": {
320348
+ "description": "Successful response",
320349
+ "content": {
320350
+ "application/json": {
320351
+ "schema": {
320352
+ "additionalProperties": false,
320353
+ "type": "object",
320354
+ "required": [
320355
+ "success"
320356
+ ],
320357
+ "properties": {
320358
+ "success": {
320359
+ "type": "boolean"
320360
+ }
320361
+ }
320362
+ }
320363
+ }
320364
+ }
320365
+ },
320366
+ "401": {
320367
+ "description": "Unauthorized - missing or invalid bearer token",
320368
+ "content": {
320369
+ "application/json": {
320370
+ "schema": {
320371
+ "type": "object",
320372
+ "required": [
320373
+ "error"
320374
+ ],
320375
+ "properties": {
320376
+ "error": {
320377
+ "type": "string"
320378
+ },
320379
+ "message": {
320380
+ "type": "string"
320381
+ }
320382
+ }
320383
+ }
320384
+ }
320385
+ }
320386
+ },
320387
+ "403": {
320388
+ "description": "Forbidden - caller lacks the required role/permission",
320389
+ "content": {
320390
+ "application/json": {
320391
+ "schema": {
320392
+ "type": "object",
320393
+ "required": [
320394
+ "error"
320395
+ ],
320396
+ "properties": {
320397
+ "error": {
320398
+ "type": "string"
320399
+ },
320400
+ "message": {
320401
+ "type": "string"
320402
+ }
320403
+ }
320404
+ }
320405
+ }
320406
+ }
320407
+ },
320408
+ "404": {
320409
+ "description": "Resource not found",
320410
+ "content": {
320411
+ "application/json": {
320412
+ "schema": {
320413
+ "type": "object",
320414
+ "required": [
320415
+ "error"
320416
+ ],
320417
+ "properties": {
320418
+ "error": {
320419
+ "type": "string"
320420
+ },
320421
+ "message": {
320422
+ "type": "string"
320423
+ }
320424
+ }
320425
+ }
320426
+ }
320427
+ }
320428
+ },
320429
+ "429": {
320430
+ "description": "Too many requests - rate limit exceeded",
320431
+ "content": {
320432
+ "application/json": {
320433
+ "schema": {
320434
+ "type": "object",
320435
+ "required": [
320436
+ "error"
320437
+ ],
320438
+ "properties": {
320439
+ "error": {
320440
+ "type": "string"
320441
+ },
320442
+ "message": {
320443
+ "type": "string"
320444
+ }
320445
+ }
320446
+ }
320447
+ }
320448
+ }
320449
+ },
320450
+ "500": {
320451
+ "description": "Internal server error",
320452
+ "content": {
320453
+ "application/json": {
320454
+ "schema": {
320455
+ "type": "object",
320456
+ "required": [
320457
+ "error"
320458
+ ],
320459
+ "properties": {
320460
+ "error": {
320461
+ "type": "string"
320462
+ },
320463
+ "message": {
320464
+ "type": "string"
320465
+ }
320466
+ }
320467
+ }
320468
+ }
320469
+ }
320470
+ },
320471
+ "503": {
320472
+ "description": "Service unavailable",
320473
+ "content": {
320474
+ "application/json": {
320475
+ "schema": {
320476
+ "type": "object",
320477
+ "required": [
320478
+ "error"
320479
+ ],
320480
+ "properties": {
320481
+ "error": {
320482
+ "type": "string"
320483
+ },
320484
+ "message": {
320485
+ "type": "string"
320486
+ }
320487
+ }
320488
+ }
320489
+ }
320490
+ }
320491
+ }
320492
+ }
320493
+ }
320494
+ },
320495
+ "/api/v1/exoclick/campaigns/{campaignId}/variations/{variationId}/status": {
320496
+ "post": {
320497
+ "operationId": "exoclickUpdateVariationStatus",
320498
+ "summary": "Play or pause an ExoClick variation (the ad)",
320499
+ "tags": [
320500
+ "exoclick"
320501
+ ],
320502
+ "description": "A variation is ExoClick’s ad, and its status route is nested under the owning campaign — which is why the campaign id is part of the path rather than resolved server-side from the ad id alone.",
320503
+ "requestBody": {
320504
+ "required": true,
320505
+ "content": {
320506
+ "application/json": {
320507
+ "schema": {
320508
+ "additionalProperties": false,
320509
+ "type": "object",
320510
+ "required": [
320511
+ "status"
320512
+ ],
320513
+ "properties": {
320514
+ "status": {
320515
+ "anyOf": [
320516
+ {
320517
+ "type": "string",
320518
+ "enum": [
320519
+ "ACTIVE"
320520
+ ]
320521
+ },
320522
+ {
320523
+ "type": "string",
320524
+ "enum": [
320525
+ "PAUSED"
320526
+ ]
320527
+ }
320528
+ ]
320529
+ }
320530
+ }
320531
+ }
320532
+ }
320533
+ }
320534
+ },
320535
+ "parameters": [
320536
+ {
320537
+ "schema": {
320538
+ "minLength": 1,
320539
+ "type": "string"
320540
+ },
320541
+ "in": "path",
320542
+ "name": "campaignId",
320543
+ "required": true
320544
+ },
320545
+ {
320546
+ "schema": {
320547
+ "minLength": 1,
320548
+ "type": "string"
320549
+ },
320550
+ "in": "path",
320551
+ "name": "variationId",
320552
+ "required": true
320553
+ },
320554
+ {
320555
+ "schema": {
320556
+ "format": "uuid",
320557
+ "type": "string"
320558
+ },
320559
+ "in": "header",
320560
+ "name": "x-team-id",
320561
+ "required": false,
320562
+ "description": "Destination workspace team id."
320563
+ }
320564
+ ],
320565
+ "security": [
320566
+ {
320567
+ "bearerAuth": []
320568
+ }
320569
+ ],
320570
+ "responses": {
320571
+ "200": {
320572
+ "description": "Successful response",
320573
+ "content": {
320574
+ "application/json": {
320575
+ "schema": {
320576
+ "additionalProperties": false,
320577
+ "type": "object",
320578
+ "required": [
320579
+ "success"
320580
+ ],
320581
+ "properties": {
320582
+ "success": {
320583
+ "type": "boolean"
320584
+ }
320585
+ }
320586
+ }
320587
+ }
320588
+ }
320589
+ },
320590
+ "401": {
320591
+ "description": "Unauthorized - missing or invalid bearer token",
320592
+ "content": {
320593
+ "application/json": {
320594
+ "schema": {
320595
+ "type": "object",
320596
+ "required": [
320597
+ "error"
320598
+ ],
320599
+ "properties": {
320600
+ "error": {
320601
+ "type": "string"
320602
+ },
320603
+ "message": {
320604
+ "type": "string"
320605
+ }
320606
+ }
320607
+ }
320608
+ }
320609
+ }
320610
+ },
320611
+ "403": {
320612
+ "description": "Forbidden - caller lacks the required role/permission",
320613
+ "content": {
320614
+ "application/json": {
320615
+ "schema": {
320616
+ "type": "object",
320617
+ "required": [
320618
+ "error"
320619
+ ],
320620
+ "properties": {
320621
+ "error": {
320622
+ "type": "string"
320623
+ },
320624
+ "message": {
320625
+ "type": "string"
320626
+ }
320627
+ }
320628
+ }
320629
+ }
320630
+ }
320631
+ },
320632
+ "429": {
320633
+ "description": "Too many requests - rate limit exceeded",
320634
+ "content": {
320635
+ "application/json": {
320636
+ "schema": {
320637
+ "type": "object",
320638
+ "required": [
320639
+ "error"
320640
+ ],
320641
+ "properties": {
320642
+ "error": {
320643
+ "type": "string"
320644
+ },
320645
+ "message": {
320646
+ "type": "string"
320647
+ }
320648
+ }
320649
+ }
320650
+ }
320651
+ }
320652
+ },
320653
+ "500": {
320654
+ "description": "Internal server error",
320655
+ "content": {
320656
+ "application/json": {
320657
+ "schema": {
320658
+ "type": "object",
320659
+ "required": [
320660
+ "error"
320661
+ ],
320662
+ "properties": {
320663
+ "error": {
320664
+ "type": "string"
320665
+ },
320666
+ "message": {
320667
+ "type": "string"
320668
+ }
320669
+ }
320670
+ }
320671
+ }
320672
+ }
320673
+ },
320674
+ "503": {
320675
+ "description": "Service unavailable",
320676
+ "content": {
320677
+ "application/json": {
320678
+ "schema": {
320679
+ "type": "object",
320680
+ "required": [
320681
+ "error"
320682
+ ],
320683
+ "properties": {
320684
+ "error": {
320685
+ "type": "string"
320686
+ },
320687
+ "message": {
320688
+ "type": "string"
320689
+ }
320690
+ }
320691
+ }
320692
+ }
320693
+ }
320694
+ }
320695
+ }
320696
+ }
320697
+ },
320698
+ "/api/v1/exoclick/campaigns/{campaignId}/budget": {
320699
+ "post": {
320700
+ "operationId": "exoclickUpdateCampaignBudget",
320701
+ "summary": "Update an ExoClick campaign budget",
320702
+ "tags": [
320703
+ "exoclick"
320704
+ ],
320705
+ "description": "Updates the daily cap and/or the lifetime cap via PUT /campaigns/{id}. A daily cap is always sent together with daily_limit_type=1: ExoClick does not enforce a budget whose limit type is unset, so sending the amount alone silently does nothing.",
320706
+ "requestBody": {
320707
+ "required": true,
320708
+ "content": {
320709
+ "application/json": {
320710
+ "schema": {
320711
+ "additionalProperties": false,
320712
+ "minProperties": 1,
320713
+ "type": "object",
320714
+ "properties": {
320715
+ "max_daily_budget": {
320716
+ "exclusiveMinimum": 0,
320717
+ "description": "Daily cap in account currency. Sent with daily_limit_type=1, or ExoClick does not enforce it.",
320718
+ "type": "number"
320719
+ },
320720
+ "total_budget_limit": {
320721
+ "exclusiveMinimum": 0,
320722
+ "type": "number"
320723
+ }
320724
+ }
320725
+ }
320726
+ }
320727
+ }
320728
+ },
320729
+ "parameters": [
320730
+ {
320731
+ "schema": {
320732
+ "minLength": 1,
320733
+ "type": "string"
320734
+ },
320735
+ "in": "path",
320736
+ "name": "campaignId",
320737
+ "required": true
320738
+ },
320739
+ {
320740
+ "schema": {
320741
+ "format": "uuid",
320742
+ "type": "string"
320743
+ },
320744
+ "in": "header",
320745
+ "name": "x-team-id",
320746
+ "required": false,
320747
+ "description": "Destination workspace team id."
320748
+ }
320749
+ ],
320750
+ "security": [
320751
+ {
320752
+ "bearerAuth": []
320753
+ }
320754
+ ],
320755
+ "responses": {
320756
+ "200": {
320757
+ "description": "Successful response",
320758
+ "content": {
320759
+ "application/json": {
320760
+ "schema": {
320761
+ "additionalProperties": false,
320762
+ "type": "object",
320763
+ "required": [
320764
+ "success"
320765
+ ],
320766
+ "properties": {
320767
+ "success": {
320768
+ "type": "boolean"
320769
+ }
320770
+ }
320771
+ }
320772
+ }
320773
+ }
320774
+ },
320775
+ "400": {
320776
+ "description": "Invalid request - schema validation failed",
320777
+ "content": {
320778
+ "application/json": {
320779
+ "schema": {
320780
+ "type": "object",
320781
+ "required": [
320782
+ "error"
320783
+ ],
320784
+ "properties": {
320785
+ "error": {
320786
+ "type": "string"
320787
+ },
320788
+ "message": {
320789
+ "type": "string"
320790
+ }
320791
+ }
320792
+ }
320793
+ }
320794
+ }
320795
+ },
320796
+ "401": {
320797
+ "description": "Unauthorized - missing or invalid bearer token",
320798
+ "content": {
320799
+ "application/json": {
320800
+ "schema": {
320801
+ "type": "object",
320802
+ "required": [
320803
+ "error"
320804
+ ],
320805
+ "properties": {
320806
+ "error": {
320807
+ "type": "string"
320808
+ },
320809
+ "message": {
320810
+ "type": "string"
320811
+ }
320812
+ }
320813
+ }
320814
+ }
320815
+ }
320816
+ },
320817
+ "403": {
320818
+ "description": "Forbidden - caller lacks the required role/permission",
320819
+ "content": {
320820
+ "application/json": {
320821
+ "schema": {
320822
+ "type": "object",
320823
+ "required": [
320824
+ "error"
320825
+ ],
320826
+ "properties": {
320827
+ "error": {
320828
+ "type": "string"
320829
+ },
320830
+ "message": {
320831
+ "type": "string"
320832
+ }
320833
+ }
320834
+ }
320835
+ }
320836
+ }
320837
+ },
320838
+ "429": {
320839
+ "description": "Too many requests - rate limit exceeded",
320840
+ "content": {
320841
+ "application/json": {
320842
+ "schema": {
320843
+ "type": "object",
320844
+ "required": [
320845
+ "error"
320846
+ ],
320847
+ "properties": {
320848
+ "error": {
320849
+ "type": "string"
320850
+ },
320851
+ "message": {
320852
+ "type": "string"
320853
+ }
320854
+ }
320855
+ }
320856
+ }
320857
+ }
320858
+ },
320859
+ "500": {
320860
+ "description": "Internal server error",
320861
+ "content": {
320862
+ "application/json": {
320863
+ "schema": {
320864
+ "type": "object",
320865
+ "required": [
320866
+ "error"
320867
+ ],
320868
+ "properties": {
320869
+ "error": {
320870
+ "type": "string"
320871
+ },
320872
+ "message": {
320873
+ "type": "string"
320874
+ }
320875
+ }
320876
+ }
320877
+ }
320878
+ }
320879
+ },
320880
+ "503": {
320881
+ "description": "Service unavailable",
320882
+ "content": {
320883
+ "application/json": {
320884
+ "schema": {
320885
+ "type": "object",
320886
+ "required": [
320887
+ "error"
320888
+ ],
320889
+ "properties": {
320890
+ "error": {
320891
+ "type": "string"
320892
+ },
320893
+ "message": {
320894
+ "type": "string"
320895
+ }
320896
+ }
320897
+ }
320898
+ }
320899
+ }
320900
+ }
320901
+ }
320902
+ }
320903
+ },
320904
+ "/api/v1/exoclick/disconnect": {
320905
+ "post": {
320906
+ "operationId": "exoclickDisconnect",
320907
+ "summary": "Disconnect ExoClick",
320908
+ "tags": [
320909
+ "exoclick"
320910
+ ],
320911
+ "description": "Revokes the stored token and soft-deletes the ExoClick account and its platform_connection mirror. Historical insight rows are kept: disconnecting a source must not erase the spend already reported for it.",
320912
+ "parameters": [
320913
+ {
320914
+ "schema": {
320915
+ "format": "uuid",
320916
+ "type": "string"
320917
+ },
320918
+ "in": "header",
320919
+ "name": "x-team-id",
320920
+ "required": false,
320921
+ "description": "Destination workspace team id."
320922
+ }
320923
+ ],
320924
+ "security": [
320925
+ {
320926
+ "bearerAuth": []
320927
+ }
320928
+ ],
320929
+ "responses": {
320930
+ "200": {
320931
+ "description": "Successful response",
320932
+ "content": {
320933
+ "application/json": {
320934
+ "schema": {
320935
+ "additionalProperties": false,
320936
+ "type": "object",
320937
+ "required": [
320938
+ "success"
320939
+ ],
320940
+ "properties": {
320941
+ "success": {
320942
+ "type": "boolean"
320943
+ }
320944
+ }
320945
+ }
320946
+ }
320947
+ }
320948
+ },
320949
+ "401": {
320950
+ "description": "Unauthorized - missing or invalid bearer token",
320951
+ "content": {
320952
+ "application/json": {
320953
+ "schema": {
320954
+ "type": "object",
320955
+ "required": [
320956
+ "error"
320957
+ ],
320958
+ "properties": {
320959
+ "error": {
320960
+ "type": "string"
320961
+ },
320962
+ "message": {
320963
+ "type": "string"
320964
+ }
320965
+ }
320966
+ }
320967
+ }
320968
+ }
320969
+ },
320970
+ "403": {
320971
+ "description": "Forbidden - caller lacks the required role/permission",
320972
+ "content": {
320973
+ "application/json": {
320974
+ "schema": {
320975
+ "type": "object",
320976
+ "required": [
320977
+ "error"
320978
+ ],
320979
+ "properties": {
320980
+ "error": {
320981
+ "type": "string"
320982
+ },
320983
+ "message": {
320984
+ "type": "string"
320985
+ }
320986
+ }
320987
+ }
320988
+ }
320989
+ }
320990
+ },
320991
+ "503": {
320992
+ "description": "Service unavailable",
320993
+ "content": {
320994
+ "application/json": {
320995
+ "schema": {
320996
+ "type": "object",
320997
+ "required": [
320998
+ "error"
320999
+ ],
321000
+ "properties": {
321001
+ "error": {
321002
+ "type": "string"
321003
+ },
321004
+ "message": {
321005
+ "type": "string"
321006
+ }
321007
+ }
321008
+ }
321009
+ }
321010
+ }
321011
+ }
321012
+ }
321013
+ }
321014
+ },
318458
321015
  "/api/v1/google/insights/backfill": {
318459
321016
  "post": {
318460
321017
  "operationId": "googleSyncInsights",
@@ -320298,7 +322855,7 @@
320298
322855
  "tags": [
320299
322856
  "health"
320300
322857
  ],
320301
- "description": "Returns overall service status plus a live DB round-trip latency sample. `status` is `healthy` when the DB responds in <5s, `degraded` when slower, `unhealthy` when unreachable. Used by monitoring dashboards and alerting - NOT the K8s readiness probe (see `/ready`).",
322858
+ "description": "Returns overall service status plus a live DB round-trip latency sample. `status` is `healthy` when the DB responds in <5s, `degraded` when slower, `unhealthy` when unreachable. `version` is the global app release this process was built from (`dev` outside the deploy pipeline). Used by monitoring dashboards and alerting - NOT the K8s readiness probe (see `/ready`).",
320302
322859
  "responses": {
320303
322860
  "200": {
320304
322861
  "description": "Successful response",
@@ -320308,6 +322865,7 @@
320308
322865
  "type": "object",
320309
322866
  "required": [
320310
322867
  "status",
322868
+ "version",
320311
322869
  "timestamp",
320312
322870
  "uptimeMs",
320313
322871
  "db"
@@ -320335,6 +322893,9 @@
320335
322893
  }
320336
322894
  ]
320337
322895
  },
322896
+ "version": {
322897
+ "type": "string"
322898
+ },
320338
322899
  "timestamp": {
320339
322900
  "format": "date-time",
320340
322901
  "type": "string"
@@ -320403,6 +322964,7 @@
320403
322964
  "type": "object",
320404
322965
  "required": [
320405
322966
  "status",
322967
+ "version",
320406
322968
  "timestamp",
320407
322969
  "uptimeMs",
320408
322970
  "db"
@@ -320430,6 +322992,9 @@
320430
322992
  }
320431
322993
  ]
320432
322994
  },
322995
+ "version": {
322996
+ "type": "string"
322997
+ },
320433
322998
  "timestamp": {
320434
322999
  "format": "date-time",
320435
323000
  "type": "string"