@epilot/customer-portal-client 0.44.0 → 0.46.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/openapi.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.0.3",
3
3
  "info": {
4
4
  "title": "Portal API",
5
- "description": "Backend for epilot portals - End Customer Portal & Installer Portal",
5
+ "description": "Backend for epilot portals - End Customer Portal & Installer Portal\n",
6
6
  "version": "1.0.0"
7
7
  },
8
8
  "tags": [
@@ -281,6 +281,7 @@
281
281
  "PortalAuth": []
282
282
  }
283
283
  ],
284
+ "x-contact-identification-token": true,
284
285
  "responses": {
285
286
  "204": {
286
287
  "description": "Tokens is valid for the given organization."
@@ -543,10 +544,7 @@
543
544
  },
544
545
  "language": {
545
546
  "type": "string",
546
- "enum": [
547
- "de",
548
- "en"
549
- ]
547
+ "example": "de"
550
548
  }
551
549
  }
552
550
  }
@@ -962,11 +960,12 @@
962
960
  "PT1H",
963
961
  "P1D",
964
962
  "P1M",
965
- "P1Y"
963
+ "P1Y",
964
+ "custom"
966
965
  ]
967
966
  },
968
967
  "required": true,
969
- "description": "Interval between consumption data points (e.g., PT15M for 15 minutes, PT1H for hourly). Not all intervals have to be supported."
968
+ "description": "Interval between consumption data points (e.g., PT15M for 15 minutes, PT1H for hourly). Not all intervals have to be supported. `custom` is period-based retrieval for sources that advertise it in their `visualizationMetadata.intervals`: the App returns one record per period it has data for within `from`..`to`, each carrying `period`, and the portal renders every record as its own bar.\n"
970
969
  },
971
970
  {
972
971
  "in": "query",
@@ -1020,6 +1019,30 @@
1020
1019
  "en": "Billing period 1",
1021
1020
  "de": "Abrechnungszeitraum 1"
1022
1021
  }
1022
+ },
1023
+ "period": {
1024
+ "type": "object",
1025
+ "description": "The date range this value covers. Required for period-based sources (`interval=custom`), whose records don't sit on a fixed time grid: the portal renders one bar per record, orders them by `period.from`, and — unless `label` is set — labels each bar with the formatted `from` - `to` range. Ignored for interval-based retrieval.\n",
1026
+ "properties": {
1027
+ "from": {
1028
+ "type": "string",
1029
+ "format": "date-time",
1030
+ "description": "Start of the covered period."
1031
+ },
1032
+ "to": {
1033
+ "type": "string",
1034
+ "format": "date-time",
1035
+ "description": "End of the covered period. Shown as given in the fallback label, so pass the date the period visibly ends on (consecutive periods may share this boundary).\n"
1036
+ }
1037
+ },
1038
+ "required": [
1039
+ "from",
1040
+ "to"
1041
+ ],
1042
+ "example": {
1043
+ "from": "2024-01-03T00:00:00.000Z",
1044
+ "to": "2025-01-05T00:00:00.000Z"
1045
+ }
1023
1046
  }
1024
1047
  },
1025
1048
  "required": [
@@ -2130,7 +2153,23 @@
2130
2153
  "content": {
2131
2154
  "application/json": {
2132
2155
  "schema": {
2133
- "$ref": "#/components/schemas/PortalConfig"
2156
+ "allOf": [
2157
+ {
2158
+ "$ref": "#/components/schemas/PortalConfig"
2159
+ },
2160
+ {
2161
+ "type": "object",
2162
+ "properties": {
2163
+ "identity_providers": {
2164
+ "type": "array",
2165
+ "description": "SSO identity providers configured for the portal, reduced to the\nfields needed to render provider login buttons. Omitted when the\nportal has no providers.\n",
2166
+ "items": {
2167
+ "$ref": "#/components/schemas/PublicIdentityProvider"
2168
+ }
2169
+ }
2170
+ }
2171
+ }
2172
+ ]
2134
2173
  }
2135
2174
  }
2136
2175
  }
@@ -3293,6 +3332,7 @@
3293
3332
  "operationId": "getSchemas",
3294
3333
  "summary": "getSchemas",
3295
3334
  "description": "Retrieves the schemas. Only schemas usable in the private part of the portal are returned.",
3335
+ "x-contact-identification-token": true,
3296
3336
  "tags": [
3297
3337
  "ECP"
3298
3338
  ],
@@ -3663,6 +3703,7 @@
3663
3703
  "operationId": "getContact",
3664
3704
  "summary": "getContact",
3665
3705
  "description": "Retrieves the contact of the logged in user.",
3706
+ "x-contact-identification-token": true,
3666
3707
  "tags": [
3667
3708
  "ECP"
3668
3709
  ],
@@ -3893,6 +3934,9 @@
3893
3934
  "404": {
3894
3935
  "$ref": "#/components/responses/NotFound"
3895
3936
  },
3937
+ "429": {
3938
+ "$ref": "#/components/responses/TooManyRequests"
3939
+ },
3896
3940
  "500": {
3897
3941
  "$ref": "#/components/responses/InternalServerError"
3898
3942
  }
@@ -3979,6 +4023,78 @@
3979
4023
  "404": {
3980
4024
  "$ref": "#/components/responses/NotFound"
3981
4025
  },
4026
+ "429": {
4027
+ "$ref": "#/components/responses/TooManyRequests"
4028
+ },
4029
+ "500": {
4030
+ "$ref": "#/components/responses/InternalServerError"
4031
+ }
4032
+ }
4033
+ }
4034
+ },
4035
+ "/v3/portal/public/contact/identify": {
4036
+ "post": {
4037
+ "operationId": "identifyContact",
4038
+ "summary": "identifyContact",
4039
+ "description": "Identify a contact by the portal's configured registration identifiers and, on a match,\nissue a short-lived bearer token that acts as that contact.\n\nResolution is identical to `checkContactExistsV3`. The token does not grant a portal\nsession; it is accepted only on the operations listed in `allowed_operations`, and\nexpires at `expires_at`. Requires `surface_id` to name a surface of the portal whose\n`authentication` is `registration_identifiers`; returns 403 otherwise. The token is\nconfined to that surface's data access. Requests may be rate limited (429).\n",
4040
+ "security": [],
4041
+ "tags": [
4042
+ "Public"
4043
+ ],
4044
+ "parameters": [
4045
+ {
4046
+ "in": "query",
4047
+ "name": "portal_id",
4048
+ "required": false,
4049
+ "schema": {
4050
+ "$ref": "#/components/schemas/PortalId"
4051
+ },
4052
+ "description": "PortalId of the portal (required if domain is not provided)"
4053
+ },
4054
+ {
4055
+ "in": "query",
4056
+ "name": "domain",
4057
+ "required": false,
4058
+ "schema": {
4059
+ "type": "string"
4060
+ },
4061
+ "description": "Portal domain for identification (alternative to portal_id)"
4062
+ }
4063
+ ],
4064
+ "requestBody": {
4065
+ "description": "Request payload",
4066
+ "required": true,
4067
+ "content": {
4068
+ "application/json": {
4069
+ "schema": {
4070
+ "$ref": "#/components/schemas/ContactIdentifyRequest"
4071
+ }
4072
+ }
4073
+ }
4074
+ },
4075
+ "responses": {
4076
+ "200": {
4077
+ "description": "The identification result. On a match, `contact_id` and `token` are present. When no\ncontact matched, only `reason` is present - the response is deliberately shaped the same\nway for every negative outcome so it cannot be used to enumerate contacts.\n",
4078
+ "content": {
4079
+ "application/json": {
4080
+ "schema": {
4081
+ "$ref": "#/components/schemas/ContactIdentifyResponse"
4082
+ }
4083
+ }
4084
+ }
4085
+ },
4086
+ "400": {
4087
+ "$ref": "#/components/responses/InvalidRequest"
4088
+ },
4089
+ "403": {
4090
+ "$ref": "#/components/responses/Forbidden"
4091
+ },
4092
+ "404": {
4093
+ "$ref": "#/components/responses/NotFound"
4094
+ },
4095
+ "429": {
4096
+ "$ref": "#/components/responses/TooManyRequests"
4097
+ },
3982
4098
  "500": {
3983
4099
  "$ref": "#/components/responses/InternalServerError"
3984
4100
  }
@@ -4053,6 +4169,9 @@
4053
4169
  "404": {
4054
4170
  "$ref": "#/components/responses/NotFound"
4055
4171
  },
4172
+ "429": {
4173
+ "$ref": "#/components/responses/TooManyRequests"
4174
+ },
4056
4175
  "500": {
4057
4176
  "$ref": "#/components/responses/InternalServerError"
4058
4177
  }
@@ -7566,6 +7685,7 @@
7566
7685
  "PortalAuth": []
7567
7686
  }
7568
7687
  ],
7688
+ "x-contact-identification-token": true,
7569
7689
  "requestBody": {
7570
7690
  "content": {
7571
7691
  "application/json": {
@@ -7606,6 +7726,7 @@
7606
7726
  "operationId": "searchPortalUserEntities",
7607
7727
  "summary": "searchPortalUserEntities",
7608
7728
  "description": "Search all entities of a portal user",
7729
+ "x-contact-identification-token": true,
7609
7730
  "tags": [
7610
7731
  "ECP"
7611
7732
  ],
@@ -8485,6 +8606,7 @@
8485
8606
  "post": {
8486
8607
  "operationId": "getMeterReadings",
8487
8608
  "summary": "getMeterReadings",
8609
+ "x-contact-identification-token": true,
8488
8610
  "description": "Fetches meter readings for a counter and optionally resolves Handlebars\ntemplate strings against each reading object using @epilot/variables.\n",
8489
8611
  "tags": [
8490
8612
  "ECP"
@@ -8689,6 +8811,68 @@
8689
8811
  }
8690
8812
  }
8691
8813
  },
8814
+ "/v3/portal/public/sso/providers/{provider_slug}": {
8815
+ "get": {
8816
+ "operationId": "getPublicSSOProviderV3",
8817
+ "summary": "getPublicSSOProviderV3",
8818
+ "description": "Returns the public configuration of a single SSO identity provider with env var\nplaceholders (incl. secrets) already resolved against the organization's environment.\n\nPortal-scoped variant of getSSOProvider: the portal is identified by `org_id` +\n`portal_id` only, so callers without a portal domain (e.g. standalone journeys)\ncan resolve the provider.\nOnly the web OIDC configuration is returned: `mobile_oidc_config` is omitted\nentirely, and the web `client_secret` and `metadata.test_auth_*` credentials are\nnever returned — they are used server-side by the SSO callback to exchange the\nauthorization code for tokens. `oidc_config.has_client_secret` is set when either\nexists, so clients route the exchange through the callback.\n",
8819
+ "security": [],
8820
+ "tags": [
8821
+ "Public"
8822
+ ],
8823
+ "parameters": [
8824
+ {
8825
+ "in": "path",
8826
+ "name": "provider_slug",
8827
+ "required": true,
8828
+ "description": "Provider slug (organization-unique)",
8829
+ "schema": {
8830
+ "$ref": "#/components/schemas/ProviderSlug"
8831
+ }
8832
+ },
8833
+ {
8834
+ "in": "query",
8835
+ "name": "org_id",
8836
+ "required": true,
8837
+ "description": "epilot organization id",
8838
+ "schema": {
8839
+ "type": "string",
8840
+ "example": 123
8841
+ }
8842
+ },
8843
+ {
8844
+ "in": "query",
8845
+ "name": "portal_id",
8846
+ "required": true,
8847
+ "description": "ID of the Portal",
8848
+ "schema": {
8849
+ "$ref": "#/components/schemas/PortalId"
8850
+ }
8851
+ }
8852
+ ],
8853
+ "responses": {
8854
+ "200": {
8855
+ "description": "Resolved public SSO provider configuration, reduced to the web OIDC flow. `mobile_oidc_config` is omitted; `oidc_config.client_secret` and the `metadata.test_auth_*` credentials are stripped (kept server-side for the token exchange).",
8856
+ "content": {
8857
+ "application/json": {
8858
+ "schema": {
8859
+ "$ref": "#/components/schemas/ProviderPublicConfigV3"
8860
+ }
8861
+ }
8862
+ }
8863
+ },
8864
+ "400": {
8865
+ "$ref": "#/components/responses/InvalidRequest"
8866
+ },
8867
+ "404": {
8868
+ "$ref": "#/components/responses/NotFound"
8869
+ },
8870
+ "500": {
8871
+ "$ref": "#/components/responses/InternalServerError"
8872
+ }
8873
+ }
8874
+ }
8875
+ },
8692
8876
  "/v2/portal/public/sso/login": {
8693
8877
  "post": {
8694
8878
  "operationId": "ssoLogin",
@@ -8834,6 +9018,11 @@
8834
9018
  "properties": {
8835
9019
  "provider_slug": {
8836
9020
  "$ref": "#/components/schemas/ProviderSlug"
9021
+ },
9022
+ "login_only": {
9023
+ "type": "boolean",
9024
+ "default": false,
9025
+ "description": "Authenticate existing identities only. When no portal user and no contact matches the identity, the login fails with a 400 response carrying `reason: PORTAL_ACCOUNT_NOT_FOUND` instead of registering a new portal user. A matched contact is still provisioned a portal user; contacts and accounts are never created."
8837
9026
  }
8838
9027
  }
8839
9028
  }
@@ -8860,6 +9049,9 @@
8860
9049
  }
8861
9050
  }
8862
9051
  }
9052
+ },
9053
+ "400": {
9054
+ "$ref": "#/components/responses/InvalidRequest"
8863
9055
  }
8864
9056
  }
8865
9057
  }
@@ -9131,6 +9323,7 @@
9131
9323
  "EitherAuth": []
9132
9324
  }
9133
9325
  ],
9326
+ "x-contact-identification-token": true,
9134
9327
  "parameters": [
9135
9328
  {
9136
9329
  "in": "query",
@@ -10595,106 +10788,50 @@
10595
10788
  }
10596
10789
  }
10597
10790
  },
10598
- "/v3/portal/configs": {
10599
- "get": {
10600
- "operationId": "listAllPortalConfigs",
10601
- "summary": "listAllPortalConfigs",
10602
- "description": "Retrieves all portal configurations.",
10603
- "tags": [
10604
- "ECP Admin"
10605
- ],
10606
- "security": [
10607
- {
10608
- "EpilotAuth": []
10609
- }
10610
- ],
10611
- "responses": {
10612
- "200": {
10613
- "description": "All portal configs retrieved successfully.",
10614
- "content": {
10615
- "application/json": {
10616
- "schema": {
10617
- "type": "object",
10618
- "properties": {
10619
- "data": {
10620
- "type": "array",
10621
- "items": {
10622
- "$ref": "#/components/schemas/PortalConfigV3"
10623
- }
10624
- }
10625
- }
10626
- }
10627
- }
10628
- }
10629
- },
10630
- "401": {
10631
- "$ref": "#/components/responses/Unauthorized"
10632
- },
10633
- "403": {
10634
- "$ref": "#/components/responses/Forbidden"
10635
- },
10636
- "500": {
10637
- "$ref": "#/components/responses/InternalServerError"
10638
- }
10639
- }
10640
- }
10641
- },
10642
- "/v3/portal/config/swap": {
10791
+ "/v3/portal/config/{portal_id}/revisions": {
10643
10792
  "post": {
10644
- "operationId": "swapPortalConfig",
10645
- "summary": "swapPortalConfig",
10646
- "description": "Swaps the portal configuration of two portals.",
10793
+ "operationId": "createPortalRevision",
10794
+ "summary": "createPortalRevision",
10795
+ "description": "Creates a new revision — a complete, immutable snapshot of the portal's configuration, pages and email templates. Nothing about the live portal changes; the snapshot only becomes live when it is published.\n\nThe payload must be COMPLETE. This endpoint does not merge against live or against the previous revision: publishing a revision deletes every live page the revision does not contain. The server validates structure only — `pages` present, each page carrying `id`, `slug`, `order` and `blocks`, and no two pages sharing an `id` or a `slug`. Semantic completeness of the config is a promise the caller makes, and a partial payload is honoured rather than rejected.\n\n`email_templates` is optional, and absence means \"keep the portal's current templates\", never \"no templates\". A present but partial map is taken verbatim.\n",
10647
10796
  "tags": [
10648
10797
  "ECP Admin"
10649
10798
  ],
10650
- "security": [
10799
+ "parameters": [
10651
10800
  {
10652
- "EpilotAuth": []
10801
+ "in": "path",
10802
+ "name": "portal_id",
10803
+ "required": true,
10804
+ "schema": {
10805
+ "type": "string",
10806
+ "format": "uuid",
10807
+ "example": "5da0a718-c822-403d-9f5d-20d4584e0528"
10808
+ },
10809
+ "description": "Portal ID (readonly UUID generated on portal creation)"
10653
10810
  }
10654
10811
  ],
10655
10812
  "requestBody": {
10656
- "description": "Source and target portal IDs",
10813
+ "description": "Complete portal configuration snapshot",
10657
10814
  "required": true,
10658
10815
  "content": {
10659
10816
  "application/json": {
10660
10817
  "schema": {
10661
- "type": "object",
10662
- "required": [
10663
- "source_portal_id",
10664
- "target_portal_id"
10665
- ],
10666
- "properties": {
10667
- "source_portal_id": {
10668
- "$ref": "#/components/schemas/PortalId"
10669
- },
10670
- "target_portal_id": {
10671
- "$ref": "#/components/schemas/PortalId"
10672
- },
10673
- "items_to_swap": {
10674
- "type": "array",
10675
- "items": {
10676
- "$ref": "#/components/schemas/SwappableConfig"
10677
- },
10678
- "description": "Optional, opt-in configuration items to additionally swap on top of the always-swapped pages and functional config. Defaults to an empty list (nothing extra swapped). Domain and access/security settings can never be swapped."
10679
- }
10680
- }
10818
+ "$ref": "#/components/schemas/PortalRevisionRequest"
10681
10819
  }
10682
10820
  }
10683
10821
  }
10684
10822
  },
10823
+ "security": [
10824
+ {
10825
+ "EpilotAuth": []
10826
+ }
10827
+ ],
10685
10828
  "responses": {
10686
- "200": {
10687
- "description": "Domain and users swapped successfully.",
10829
+ "201": {
10830
+ "description": "Revision created successfully.",
10688
10831
  "content": {
10689
10832
  "application/json": {
10690
10833
  "schema": {
10691
- "type": "object",
10692
- "properties": {
10693
- "message": {
10694
- "type": "string",
10695
- "example": "Domain and users swapped successfully."
10696
- }
10697
- }
10834
+ "$ref": "#/components/schemas/PortalRevisionCreated"
10698
10835
  }
10699
10836
  }
10700
10837
  }
@@ -10711,55 +10848,67 @@
10711
10848
  "404": {
10712
10849
  "$ref": "#/components/responses/NotFound"
10713
10850
  },
10851
+ "409": {
10852
+ "$ref": "#/components/responses/Conflict"
10853
+ },
10714
10854
  "500": {
10715
10855
  "$ref": "#/components/responses/InternalServerError"
10716
10856
  }
10717
10857
  }
10718
- }
10719
- },
10720
- "/v3/portal/config/clone": {
10721
- "post": {
10722
- "operationId": "clonePortalConfig",
10723
- "summary": "clonePortalConfig",
10724
- "description": "Creates a new portal by cloning configuration and pages from an existing portal. The new portal gets its own domain, users, email templates, and authentication settings.",
10858
+ },
10859
+ "get": {
10860
+ "operationId": "listPortalRevisions",
10861
+ "summary": "listPortalRevisions",
10862
+ "description": "Lists a portal's revision history, newest first. Metadata only — no config blob, no page bodies. `is_published` says whether that revision is the one currently live, which is a different question from `published_at`, which records the last time it was published.\n",
10725
10863
  "tags": [
10726
10864
  "ECP Admin"
10727
10865
  ],
10866
+ "parameters": [
10867
+ {
10868
+ "in": "path",
10869
+ "name": "portal_id",
10870
+ "required": true,
10871
+ "schema": {
10872
+ "type": "string",
10873
+ "format": "uuid",
10874
+ "example": "5da0a718-c822-403d-9f5d-20d4584e0528"
10875
+ },
10876
+ "description": "Portal ID (readonly UUID generated on portal creation)"
10877
+ },
10878
+ {
10879
+ "in": "query",
10880
+ "name": "limit",
10881
+ "required": false,
10882
+ "schema": {
10883
+ "type": "integer",
10884
+ "minimum": 1,
10885
+ "maximum": 100,
10886
+ "default": 25
10887
+ },
10888
+ "description": "Maximum number of revisions to return"
10889
+ },
10890
+ {
10891
+ "in": "query",
10892
+ "name": "cursor",
10893
+ "required": false,
10894
+ "schema": {
10895
+ "type": "string"
10896
+ },
10897
+ "description": "Opaque pagination cursor, taken from a previous response's `next_cursor`"
10898
+ }
10899
+ ],
10728
10900
  "security": [
10729
10901
  {
10730
10902
  "EpilotAuth": []
10731
10903
  }
10732
10904
  ],
10733
- "requestBody": {
10734
- "description": "Source portal ID and optional name for the cloned portal",
10735
- "required": true,
10736
- "content": {
10737
- "application/json": {
10738
- "schema": {
10739
- "type": "object",
10740
- "required": [
10741
- "source_portal_id"
10742
- ],
10743
- "properties": {
10744
- "source_portal_id": {
10745
- "$ref": "#/components/schemas/PortalId"
10746
- },
10747
- "name": {
10748
- "type": "string",
10749
- "description": "Name for the cloned portal. Defaults to \"Copy of <source portal name>\"."
10750
- }
10751
- }
10752
- }
10753
- }
10754
- }
10755
- },
10756
10905
  "responses": {
10757
- "201": {
10758
- "description": "Portal cloned successfully.",
10906
+ "200": {
10907
+ "description": "Revision history retrieved successfully.",
10759
10908
  "content": {
10760
10909
  "application/json": {
10761
10910
  "schema": {
10762
- "$ref": "#/components/schemas/PortalConfigV3"
10911
+ "$ref": "#/components/schemas/PortalRevisionList"
10763
10912
  }
10764
10913
  }
10765
10914
  }
@@ -10782,86 +10931,49 @@
10782
10931
  }
10783
10932
  }
10784
10933
  },
10785
- "/v3/portal/partner/invite": {
10786
- "post": {
10787
- "operationId": "invitePartner",
10788
- "summary": "invitePartner",
10789
- "description": "Invites a partner to a portal",
10934
+ "/v3/portal/config/{portal_id}/revisions/{revision_id}": {
10935
+ "get": {
10936
+ "operationId": "getPortalRevision",
10937
+ "summary": "getPortalRevision",
10938
+ "description": "Returns the full content of one revision: the snapshotted config, its pages (in the live `Page` shape), email templates and identity providers with secrets redacted.\n\nThis is a pure read. The server records nothing about it: no \"loaded\" marker, no audit entry, and no change to which revision is live. Revision content re-enters the system only as a new `POST .../revisions`.\n\nSecret-typed extension option values are removed from `config` entirely, not masked.\n\nReturns `409` when the revision exists but its stored config or email templates row cannot be read, rather than a partial snapshot. Saving again produces a complete revision.\n",
10790
10939
  "tags": [
10791
- "ECP"
10940
+ "ECP Admin"
10792
10941
  ],
10793
- "security": [
10942
+ "parameters": [
10794
10943
  {
10795
- "PortalAuth": []
10944
+ "in": "path",
10945
+ "name": "portal_id",
10946
+ "required": true,
10947
+ "schema": {
10948
+ "type": "string",
10949
+ "format": "uuid",
10950
+ "example": "5da0a718-c822-403d-9f5d-20d4584e0528"
10951
+ },
10952
+ "description": "Portal ID (readonly UUID generated on portal creation)"
10953
+ },
10954
+ {
10955
+ "in": "path",
10956
+ "name": "revision_id",
10957
+ "required": true,
10958
+ "schema": {
10959
+ "type": "string",
10960
+ "example": "2026-08-25T14:03:11.482Z-a7f3c1d9"
10961
+ },
10962
+ "description": "Revision ID. Contains `:` characters — percent-encode it."
10796
10963
  }
10797
10964
  ],
10798
- "requestBody": {
10799
- "description": "Partner to invite",
10800
- "required": true,
10801
- "content": {
10802
- "application/json": {
10803
- "schema": {
10804
- "type": "object",
10805
- "required": [
10806
- "email"
10807
- ],
10808
- "properties": {
10809
- "email": {
10810
- "type": "string",
10811
- "description": "Email address of the partner to invite"
10812
- },
10813
- "contact_data": {
10814
- "type": "object",
10815
- "description": "Additional contact entity fields to set when creating the contact for the invited user.\nThese are mapped directly to contact entity attributes (e.g. first_name, last_name, phone).\nValues can be strings or arrays of strings (for multiselect attributes).\n",
10816
- "additionalProperties": {
10817
- "oneOf": [
10818
- {
10819
- "type": "string"
10820
- },
10821
- {
10822
- "type": "array",
10823
- "items": {
10824
- "type": "string"
10825
- }
10826
- }
10827
- ]
10828
- }
10829
- },
10830
- "portal_user_data": {
10831
- "type": "object",
10832
- "description": "Additional portal user entity fields to set when creating the portal user for the invited user.\nThese are mapped directly to portal_user entity attributes.\nValues can be strings or arrays of strings (for multiselect attributes).\n",
10833
- "additionalProperties": {
10834
- "oneOf": [
10835
- {
10836
- "type": "string"
10837
- },
10838
- {
10839
- "type": "array",
10840
- "items": {
10841
- "type": "string"
10842
- }
10843
- }
10844
- ]
10845
- }
10846
- }
10847
- }
10848
- }
10849
- }
10965
+ "security": [
10966
+ {
10967
+ "EpilotAuth": []
10850
10968
  }
10851
- },
10969
+ ],
10852
10970
  "responses": {
10853
10971
  "200": {
10854
- "description": "User invited successfully",
10972
+ "description": "Revision retrieved successfully.",
10855
10973
  "content": {
10856
10974
  "application/json": {
10857
10975
  "schema": {
10858
- "type": "object",
10859
- "properties": {
10860
- "message": {
10861
- "type": "string",
10862
- "example": "User invited successfully"
10863
- }
10864
- }
10976
+ "$ref": "#/components/schemas/PortalRevision"
10865
10977
  }
10866
10978
  }
10867
10979
  }
@@ -10878,142 +10990,177 @@
10878
10990
  "404": {
10879
10991
  "$ref": "#/components/responses/NotFound"
10880
10992
  },
10993
+ "409": {
10994
+ "$ref": "#/components/responses/Conflict"
10995
+ },
10881
10996
  "500": {
10882
10997
  "$ref": "#/components/responses/InternalServerError"
10883
10998
  }
10884
10999
  }
10885
11000
  }
10886
11001
  },
10887
- "/v3/portal/partner/list": {
10888
- "get": {
10889
- "operationId": "listBusinessPartners",
10890
- "summary": "listBusinessPartners",
10891
- "description": "Lists all business partners linked to the businessaccount",
11002
+ "/v3/portal/config/{portal_id}/publish": {
11003
+ "post": {
11004
+ "operationId": "publishPortalRevision",
11005
+ "summary": "publishPortalRevision",
11006
+ "description": "Makes one revision the portal's live configuration, atomically: either everything below takes effect or nothing does. Publish is a full-snapshot replace: it writes config, every page and email templates to match the revision exactly, and deletes every live page the revision does not contain.\n\nAccepts any valid `revision_id` for the portal, old or new. There is no separate rollback endpoint and none is needed.\n\nThe same atomic publish stamps `name`, `description` and `published_at` onto the revision being published — permanently, on that revision, and not on any later one that supersedes it as live. `name` is generated server-side when the request omits it, so every published revision carries one.\n\nConflict detection covers the live config only: publish is rejected with `409` when the live config changed after publish read it. Live pages, their redirect routes and the email templates are replaced outright — a concurrent edit to a live page is not detected and is overwritten. `409` is also returned when the revision's stored config or email templates cannot be read, or when two of its pages would resolve to the same slug. A domain the revision claims that another portal already owns is a `400`.\n",
10892
11007
  "tags": [
10893
- "ECP"
11008
+ "ECP Admin"
11009
+ ],
11010
+ "parameters": [
11011
+ {
11012
+ "in": "path",
11013
+ "name": "portal_id",
11014
+ "required": true,
11015
+ "schema": {
11016
+ "type": "string",
11017
+ "format": "uuid",
11018
+ "example": "5da0a718-c822-403d-9f5d-20d4584e0528"
11019
+ },
11020
+ "description": "Portal ID (readonly UUID generated on portal creation)"
11021
+ }
10894
11022
  ],
11023
+ "requestBody": {
11024
+ "description": "The revision to publish",
11025
+ "required": true,
11026
+ "content": {
11027
+ "application/json": {
11028
+ "schema": {
11029
+ "$ref": "#/components/schemas/PublishRevisionRequest"
11030
+ }
11031
+ }
11032
+ }
11033
+ },
10895
11034
  "security": [
10896
11035
  {
10897
- "PortalAuth": []
11036
+ "EpilotAuth": []
10898
11037
  }
10899
11038
  ],
10900
11039
  "responses": {
10901
11040
  "200": {
10902
- "description": "Business partners listed successfully",
11041
+ "description": "Revision published successfully.",
10903
11042
  "content": {
10904
11043
  "application/json": {
10905
11044
  "schema": {
10906
- "type": "object",
10907
- "properties": {
10908
- "data": {
10909
- "type": "array",
10910
- "items": {
10911
- "$ref": "#/components/schemas/BusinessPartnerItem"
10912
- }
10913
- }
10914
- }
11045
+ "$ref": "#/components/schemas/PublishResult"
10915
11046
  }
10916
11047
  }
10917
11048
  }
10918
11049
  },
11050
+ "400": {
11051
+ "$ref": "#/components/responses/InvalidRequest"
11052
+ },
10919
11053
  "401": {
10920
11054
  "$ref": "#/components/responses/Unauthorized"
10921
11055
  },
10922
11056
  "403": {
10923
11057
  "$ref": "#/components/responses/Forbidden"
10924
11058
  },
11059
+ "404": {
11060
+ "$ref": "#/components/responses/NotFound"
11061
+ },
11062
+ "409": {
11063
+ "$ref": "#/components/responses/Conflict"
11064
+ },
10925
11065
  "500": {
10926
11066
  "$ref": "#/components/responses/InternalServerError"
11067
+ },
11068
+ "503": {
11069
+ "$ref": "#/components/responses/ServiceUnavailable"
10927
11070
  }
10928
11071
  }
10929
11072
  }
10930
11073
  },
10931
- "/v3/portal/partner/{partner_id}/resend-invitation": {
10932
- "post": {
10933
- "operationId": "resendPartnerInvitation",
10934
- "summary": "resendPartnerInvitation",
10935
- "description": "Resends an invitation email to a partner",
11074
+ "/v3/portal/configs": {
11075
+ "get": {
11076
+ "operationId": "listAllPortalConfigs",
11077
+ "summary": "listAllPortalConfigs",
11078
+ "description": "Retrieves all portal configurations.",
10936
11079
  "tags": [
10937
- "ECP"
11080
+ "ECP Admin"
10938
11081
  ],
10939
11082
  "security": [
10940
11083
  {
10941
- "PortalAuth": []
10942
- }
10943
- ],
10944
- "parameters": [
10945
- {
10946
- "name": "partner_id",
10947
- "in": "path",
10948
- "required": true,
10949
- "schema": {
10950
- "type": "string",
10951
- "description": "ID of the partner to resend invitation to"
10952
- }
11084
+ "EpilotAuth": []
10953
11085
  }
10954
11086
  ],
10955
11087
  "responses": {
10956
11088
  "200": {
10957
- "description": "Partner invitation resent successfully",
11089
+ "description": "All portal configs retrieved successfully.",
10958
11090
  "content": {
10959
11091
  "application/json": {
10960
11092
  "schema": {
10961
11093
  "type": "object",
10962
11094
  "properties": {
10963
- "message": {
10964
- "type": "string",
10965
- "example": "Partner invitation resent successfully"
11095
+ "data": {
11096
+ "type": "array",
11097
+ "items": {
11098
+ "$ref": "#/components/schemas/PortalConfigV3"
11099
+ }
10966
11100
  }
10967
11101
  }
10968
11102
  }
10969
11103
  }
10970
11104
  }
10971
11105
  },
10972
- "400": {
10973
- "$ref": "#/components/responses/InvalidRequest"
10974
- },
10975
11106
  "401": {
10976
11107
  "$ref": "#/components/responses/Unauthorized"
10977
11108
  },
10978
11109
  "403": {
10979
11110
  "$ref": "#/components/responses/Forbidden"
10980
11111
  },
10981
- "404": {
10982
- "$ref": "#/components/responses/NotFound"
10983
- },
10984
11112
  "500": {
10985
11113
  "$ref": "#/components/responses/InternalServerError"
10986
11114
  }
10987
11115
  }
10988
11116
  }
10989
11117
  },
10990
- "/v3/portal/partner/{partner_id}/revoke": {
10991
- "delete": {
10992
- "operationId": "revokePartner",
10993
- "summary": "revokePartner",
10994
- "description": "Revokes a partner from a portal",
11118
+ "/v3/portal/config/swap": {
11119
+ "post": {
11120
+ "operationId": "swapPortalConfig",
11121
+ "summary": "swapPortalConfig",
11122
+ "description": "Swaps the portal configuration of two portals.",
10995
11123
  "tags": [
10996
- "ECP"
11124
+ "ECP Admin"
10997
11125
  ],
10998
11126
  "security": [
10999
11127
  {
11000
- "PortalAuth": []
11128
+ "EpilotAuth": []
11001
11129
  }
11002
11130
  ],
11003
- "parameters": [
11004
- {
11005
- "name": "partner_id",
11006
- "in": "path",
11007
- "required": true,
11008
- "schema": {
11009
- "type": "string",
11010
- "description": "ID of the partner to revoke from the portal"
11131
+ "requestBody": {
11132
+ "description": "Source and target portal IDs",
11133
+ "required": true,
11134
+ "content": {
11135
+ "application/json": {
11136
+ "schema": {
11137
+ "type": "object",
11138
+ "required": [
11139
+ "source_portal_id",
11140
+ "target_portal_id"
11141
+ ],
11142
+ "properties": {
11143
+ "source_portal_id": {
11144
+ "$ref": "#/components/schemas/PortalId"
11145
+ },
11146
+ "target_portal_id": {
11147
+ "$ref": "#/components/schemas/PortalId"
11148
+ },
11149
+ "items_to_swap": {
11150
+ "type": "array",
11151
+ "items": {
11152
+ "$ref": "#/components/schemas/SwappableConfig"
11153
+ },
11154
+ "description": "Optional, opt-in configuration items to additionally swap on top of the always-swapped pages and functional config. Defaults to an empty list (nothing extra swapped). Domain and access/security settings can never be swapped."
11155
+ }
11156
+ }
11157
+ }
11011
11158
  }
11012
11159
  }
11013
- ],
11160
+ },
11014
11161
  "responses": {
11015
11162
  "200": {
11016
- "description": "Partner revoked from portal successfully",
11163
+ "description": "Domain and users swapped successfully.",
11017
11164
  "content": {
11018
11165
  "application/json": {
11019
11166
  "schema": {
@@ -11021,7 +11168,7 @@
11021
11168
  "properties": {
11022
11169
  "message": {
11023
11170
  "type": "string",
11024
- "example": "Partner revoked from portal successfully"
11171
+ "example": "Domain and users swapped successfully."
11025
11172
  }
11026
11173
  }
11027
11174
  }
@@ -11046,43 +11193,49 @@
11046
11193
  }
11047
11194
  }
11048
11195
  },
11049
- "/v3/portal/partner/{partner_id}/disable": {
11196
+ "/v3/portal/config/clone": {
11050
11197
  "post": {
11051
- "operationId": "disablePartner",
11052
- "summary": "disablePartner",
11053
- "description": "Disables a partner from a portal",
11198
+ "operationId": "clonePortalConfig",
11199
+ "summary": "clonePortalConfig",
11200
+ "description": "Creates a new portal by cloning configuration and pages from an existing portal. The new portal gets its own domain, users, email templates, and authentication settings.",
11054
11201
  "tags": [
11055
- "ECP"
11202
+ "ECP Admin"
11056
11203
  ],
11057
11204
  "security": [
11058
11205
  {
11059
- "PortalAuth": []
11206
+ "EpilotAuth": []
11060
11207
  }
11061
11208
  ],
11062
- "parameters": [
11063
- {
11064
- "name": "partner_id",
11065
- "in": "path",
11066
- "required": true,
11067
- "schema": {
11068
- "type": "string",
11069
- "description": "ID of the partner to disable from the portal"
11209
+ "requestBody": {
11210
+ "description": "Source portal ID and optional name for the cloned portal",
11211
+ "required": true,
11212
+ "content": {
11213
+ "application/json": {
11214
+ "schema": {
11215
+ "type": "object",
11216
+ "required": [
11217
+ "source_portal_id"
11218
+ ],
11219
+ "properties": {
11220
+ "source_portal_id": {
11221
+ "$ref": "#/components/schemas/PortalId"
11222
+ },
11223
+ "name": {
11224
+ "type": "string",
11225
+ "description": "Name for the cloned portal. Defaults to \"Copy of <source portal name>\"."
11226
+ }
11227
+ }
11228
+ }
11070
11229
  }
11071
11230
  }
11072
- ],
11231
+ },
11073
11232
  "responses": {
11074
- "200": {
11075
- "description": "Partner disabled from portal successfully",
11233
+ "201": {
11234
+ "description": "Portal cloned successfully.",
11076
11235
  "content": {
11077
11236
  "application/json": {
11078
11237
  "schema": {
11079
- "type": "object",
11080
- "properties": {
11081
- "message": {
11082
- "type": "string",
11083
- "example": "Partner disabled from portal successfully"
11084
- }
11085
- }
11238
+ "$ref": "#/components/schemas/PortalConfigV3"
11086
11239
  }
11087
11240
  }
11088
11241
  }
@@ -11105,11 +11258,11 @@
11105
11258
  }
11106
11259
  }
11107
11260
  },
11108
- "/v3/portal/partner/{partner_id}/enable": {
11261
+ "/v3/portal/partner/invite": {
11109
11262
  "post": {
11110
- "operationId": "enablePartner",
11111
- "summary": "enablePartner",
11112
- "description": "Enables a partner from a portal",
11263
+ "operationId": "invitePartner",
11264
+ "summary": "invitePartner",
11265
+ "description": "Invites a partner to a portal",
11113
11266
  "tags": [
11114
11267
  "ECP"
11115
11268
  ],
@@ -11118,20 +11271,63 @@
11118
11271
  "PortalAuth": []
11119
11272
  }
11120
11273
  ],
11121
- "parameters": [
11122
- {
11123
- "name": "partner_id",
11124
- "in": "path",
11125
- "required": true,
11126
- "schema": {
11127
- "type": "string",
11128
- "description": "ID of the partner to enable from the portal"
11274
+ "requestBody": {
11275
+ "description": "Partner to invite",
11276
+ "required": true,
11277
+ "content": {
11278
+ "application/json": {
11279
+ "schema": {
11280
+ "type": "object",
11281
+ "required": [
11282
+ "email"
11283
+ ],
11284
+ "properties": {
11285
+ "email": {
11286
+ "type": "string",
11287
+ "description": "Email address of the partner to invite"
11288
+ },
11289
+ "contact_data": {
11290
+ "type": "object",
11291
+ "description": "Additional contact entity fields to set when creating the contact for the invited user.\nThese are mapped directly to contact entity attributes (e.g. first_name, last_name, phone).\nValues can be strings or arrays of strings (for multiselect attributes).\n",
11292
+ "additionalProperties": {
11293
+ "oneOf": [
11294
+ {
11295
+ "type": "string"
11296
+ },
11297
+ {
11298
+ "type": "array",
11299
+ "items": {
11300
+ "type": "string"
11301
+ }
11302
+ }
11303
+ ]
11304
+ }
11305
+ },
11306
+ "portal_user_data": {
11307
+ "type": "object",
11308
+ "description": "Additional portal user entity fields to set when creating the portal user for the invited user.\nThese are mapped directly to portal_user entity attributes.\nValues can be strings or arrays of strings (for multiselect attributes).\n",
11309
+ "additionalProperties": {
11310
+ "oneOf": [
11311
+ {
11312
+ "type": "string"
11313
+ },
11314
+ {
11315
+ "type": "array",
11316
+ "items": {
11317
+ "type": "string"
11318
+ }
11319
+ }
11320
+ ]
11321
+ }
11322
+ }
11323
+ }
11324
+ }
11129
11325
  }
11130
11326
  }
11131
- ],
11327
+ },
11132
11328
  "responses": {
11133
11329
  "200": {
11134
- "description": "Partner enabled from portal successfully",
11330
+ "description": "User invited successfully",
11135
11331
  "content": {
11136
11332
  "application/json": {
11137
11333
  "schema": {
@@ -11139,7 +11335,7 @@
11139
11335
  "properties": {
11140
11336
  "message": {
11141
11337
  "type": "string",
11142
- "example": "Partner enabled from portal successfully"
11338
+ "example": "User invited successfully"
11143
11339
  }
11144
11340
  }
11145
11341
  }
@@ -11164,49 +11360,32 @@
11164
11360
  }
11165
11361
  }
11166
11362
  },
11167
- "/v3/portal/verify-dns": {
11168
- "post": {
11169
- "operationId": "verifyDns",
11170
- "summary": "verifyDns",
11171
- "description": "Manually triggers DNS verification for a portal's domain setup. Runs the same verification logic as the scheduled processAllPendingNetworks lambda.",
11363
+ "/v3/portal/partner/list": {
11364
+ "get": {
11365
+ "operationId": "listBusinessPartners",
11366
+ "summary": "listBusinessPartners",
11367
+ "description": "Lists all business partners linked to the businessaccount",
11172
11368
  "tags": [
11173
- "ECP Admin"
11369
+ "ECP"
11174
11370
  ],
11175
11371
  "security": [
11176
11372
  {
11177
- "EpilotAuth": []
11178
- }
11179
- ],
11180
- "parameters": [
11181
- {
11182
- "in": "query",
11183
- "name": "portal_id",
11184
- "required": true,
11185
- "schema": {
11186
- "$ref": "#/components/schemas/PortalId"
11187
- },
11188
- "description": "PortalId of the portal"
11373
+ "PortalAuth": []
11189
11374
  }
11190
11375
  ],
11191
11376
  "responses": {
11192
11377
  "200": {
11193
- "description": "DNS verification result",
11378
+ "description": "Business partners listed successfully",
11194
11379
  "content": {
11195
11380
  "application/json": {
11196
11381
  "schema": {
11197
11382
  "type": "object",
11198
11383
  "properties": {
11199
- "domain_status": {
11200
- "type": "string",
11201
- "description": "The status of the custom domain verification",
11202
- "enum": [
11203
- "PENDING",
11204
- "SUCCEED"
11205
- ]
11206
- },
11207
- "message": {
11208
- "type": "string",
11209
- "description": "A message describing the result"
11384
+ "data": {
11385
+ "type": "array",
11386
+ "items": {
11387
+ "$ref": "#/components/schemas/BusinessPartnerItem"
11388
+ }
11210
11389
  }
11211
11390
  }
11212
11391
  }
@@ -11225,67 +11404,41 @@
11225
11404
  }
11226
11405
  }
11227
11406
  },
11228
- "/v2/portal/proxy/execute": {
11407
+ "/v3/portal/partner/{partner_id}/resend-invitation": {
11229
11408
  "post": {
11230
- "operationId": "portalProxyExecute",
11231
- "summary": "portalProxyExecute",
11232
- "description": "Execute an Integration Hub managed-call use case on behalf of a portal user.\nBridges PortalAuth to the Integration API by generating an internal token.\n",
11409
+ "operationId": "resendPartnerInvitation",
11410
+ "summary": "resendPartnerInvitation",
11411
+ "description": "Resends an invitation email to a partner",
11412
+ "tags": [
11413
+ "ECP"
11414
+ ],
11233
11415
  "security": [
11234
11416
  {
11235
11417
  "PortalAuth": []
11236
11418
  }
11237
11419
  ],
11238
- "tags": [
11239
- "ECP"
11420
+ "parameters": [
11421
+ {
11422
+ "name": "partner_id",
11423
+ "in": "path",
11424
+ "required": true,
11425
+ "schema": {
11426
+ "type": "string",
11427
+ "description": "ID of the partner to resend invitation to"
11428
+ }
11429
+ }
11240
11430
  ],
11241
- "requestBody": {
11242
- "required": true,
11243
- "content": {
11244
- "application/json": {
11245
- "schema": {
11246
- "type": "object",
11247
- "required": [
11248
- "integration_id",
11249
- "use_case_slug"
11250
- ],
11251
- "properties": {
11252
- "integration_id": {
11253
- "type": "string",
11254
- "format": "uuid",
11255
- "description": "Integration ID containing the managed-call use case"
11256
- },
11257
- "use_case_slug": {
11258
- "type": "string",
11259
- "description": "Use case slug (acts as the RPC method name)"
11260
- },
11261
- "payload": {
11262
- "type": "object",
11263
- "description": "Input data for the managed-call operation",
11264
- "additionalProperties": true
11265
- }
11266
- }
11267
- }
11268
- }
11269
- }
11270
- },
11271
11431
  "responses": {
11272
11432
  "200": {
11273
- "description": "Managed-call execution result envelope.",
11433
+ "description": "Partner invitation resent successfully",
11274
11434
  "content": {
11275
11435
  "application/json": {
11276
11436
  "schema": {
11277
11437
  "type": "object",
11278
- "required": [
11279
- "success"
11280
- ],
11281
11438
  "properties": {
11282
- "success": {
11283
- "type": "boolean"
11284
- },
11285
- "data": {
11286
- "type": "object",
11287
- "additionalProperties": true,
11288
- "description": "Managed-call response payload. Shape is defined by the use\ncase's JSONata response_mapping; if no mapping is\nconfigured the raw external API response is returned.\n"
11439
+ "message": {
11440
+ "type": "string",
11441
+ "example": "Partner invitation resent successfully"
11289
11442
  }
11290
11443
  }
11291
11444
  }
@@ -11301,56 +11454,59 @@
11301
11454
  "403": {
11302
11455
  "$ref": "#/components/responses/Forbidden"
11303
11456
  },
11457
+ "404": {
11458
+ "$ref": "#/components/responses/NotFound"
11459
+ },
11304
11460
  "500": {
11305
11461
  "$ref": "#/components/responses/InternalServerError"
11306
11462
  }
11307
11463
  }
11308
11464
  }
11309
11465
  },
11310
- "/v1/portal/mobile-config": {
11311
- "get": {
11312
- "operationId": "getMobileConfig",
11313
- "summary": "getMobileConfig",
11314
- "description": "Returns the portal's mobile app configuration. By default the response is build-ready (resolved): base info (display_name from the portal name, app_host from the domain, environment) and branding (logo from the portal images, colors from the design palette) are filled in. Pass raw=true to get only the stored mobile_config without resolution.",
11466
+ "/v3/portal/partner/{partner_id}/revoke": {
11467
+ "delete": {
11468
+ "operationId": "revokePartner",
11469
+ "summary": "revokePartner",
11470
+ "description": "Revokes a partner from a portal",
11315
11471
  "tags": [
11316
- "ECP Admin"
11472
+ "ECP"
11317
11473
  ],
11318
11474
  "security": [
11319
11475
  {
11320
- "EpilotAuth": []
11476
+ "PortalAuth": []
11321
11477
  }
11322
11478
  ],
11323
11479
  "parameters": [
11324
11480
  {
11325
- "in": "query",
11326
- "name": "portal_id",
11481
+ "name": "partner_id",
11482
+ "in": "path",
11327
11483
  "required": true,
11328
11484
  "schema": {
11329
- "type": "string"
11330
- },
11331
- "description": "Portal ID"
11332
- },
11333
- {
11334
- "in": "query",
11335
- "name": "raw",
11336
- "required": false,
11337
- "schema": {
11338
- "type": "boolean"
11339
- },
11340
- "description": "Return only the stored mobile_config without resolving base info/branding."
11485
+ "type": "string",
11486
+ "description": "ID of the partner to revoke from the portal"
11487
+ }
11341
11488
  }
11342
11489
  ],
11343
11490
  "responses": {
11344
11491
  "200": {
11345
- "description": "Mobile config retrieved successfully.",
11492
+ "description": "Partner revoked from portal successfully",
11346
11493
  "content": {
11347
11494
  "application/json": {
11348
11495
  "schema": {
11349
- "$ref": "#/components/schemas/MobileConfig"
11496
+ "type": "object",
11497
+ "properties": {
11498
+ "message": {
11499
+ "type": "string",
11500
+ "example": "Partner revoked from portal successfully"
11501
+ }
11502
+ }
11350
11503
  }
11351
11504
  }
11352
11505
  }
11353
11506
  },
11507
+ "400": {
11508
+ "$ref": "#/components/responses/InvalidRequest"
11509
+ },
11354
11510
  "401": {
11355
11511
  "$ref": "#/components/responses/Unauthorized"
11356
11512
  },
@@ -11364,48 +11520,45 @@
11364
11520
  "$ref": "#/components/responses/InternalServerError"
11365
11521
  }
11366
11522
  }
11367
- },
11368
- "put": {
11369
- "operationId": "putMobileConfig",
11370
- "summary": "putMobileConfig",
11371
- "description": "Merges the provided fields into the portal's mobile app configuration\n(deep merge). Only mobile_config is modified; all other portal settings\nare left untouched.\n",
11523
+ }
11524
+ },
11525
+ "/v3/portal/partner/{partner_id}/disable": {
11526
+ "post": {
11527
+ "operationId": "disablePartner",
11528
+ "summary": "disablePartner",
11529
+ "description": "Disables a partner from a portal",
11372
11530
  "tags": [
11373
- "ECP Admin"
11531
+ "ECP"
11374
11532
  ],
11375
11533
  "security": [
11376
11534
  {
11377
- "EpilotAuth": []
11535
+ "PortalAuth": []
11378
11536
  }
11379
11537
  ],
11380
11538
  "parameters": [
11381
11539
  {
11382
- "in": "query",
11383
- "name": "portal_id",
11540
+ "name": "partner_id",
11541
+ "in": "path",
11384
11542
  "required": true,
11385
11543
  "schema": {
11386
- "type": "string"
11387
- },
11388
- "description": "Portal ID"
11389
- }
11390
- ],
11391
- "requestBody": {
11392
- "description": "Editable mobile fields to merge into the existing mobile_config. Only mobile-relevant settings + app branding are applied; other fields are ignored.",
11393
- "required": true,
11394
- "content": {
11395
- "application/json": {
11396
- "schema": {
11397
- "$ref": "#/components/schemas/MobileConfigUpdate"
11398
- }
11544
+ "type": "string",
11545
+ "description": "ID of the partner to disable from the portal"
11399
11546
  }
11400
11547
  }
11401
- },
11548
+ ],
11402
11549
  "responses": {
11403
11550
  "200": {
11404
- "description": "Mobile config updated successfully.",
11551
+ "description": "Partner disabled from portal successfully",
11405
11552
  "content": {
11406
11553
  "application/json": {
11407
11554
  "schema": {
11408
- "$ref": "#/components/schemas/MobileConfig"
11555
+ "type": "object",
11556
+ "properties": {
11557
+ "message": {
11558
+ "type": "string",
11559
+ "example": "Partner disabled from portal successfully"
11560
+ }
11561
+ }
11409
11562
  }
11410
11563
  }
11411
11564
  }
@@ -11427,97 +11580,420 @@
11427
11580
  }
11428
11581
  }
11429
11582
  }
11430
- }
11431
- },
11432
- "components": {
11433
- "responses": {
11434
- "InvalidRequest": {
11435
- "description": "The request could not be validated",
11436
- "content": {
11437
- "application/json": {
11438
- "schema": {
11439
- "$ref": "#/components/schemas/ErrorResp"
11440
- }
11441
- }
11442
- }
11443
- },
11444
- "Unauthorized": {
11445
- "description": "Could not authenticate the user",
11446
- "content": {
11447
- "application/json": {
11448
- "schema": {
11449
- "$ref": "#/components/schemas/ErrorResp"
11450
- }
11451
- }
11452
- }
11453
- },
11454
- "Forbidden": {
11455
- "description": "The user is not allowed to access this resource",
11456
- "content": {
11457
- "application/json": {
11458
- "schema": {
11459
- "$ref": "#/components/schemas/ErrorResp"
11460
- }
11461
- }
11462
- }
11463
- },
11464
- "ForbiddenByRule": {
11465
- "description": "The user is not allowed to access this resource",
11466
- "content": {
11467
- "application/json": {
11468
- "schema": {
11469
- "oneOf": [
11470
- {
11471
- "$ref": "#/components/schemas/ErrorResp"
11472
- },
11473
- {
11474
- "$ref": "#/components/schemas/FailedRuleErrorResp"
11475
- }
11476
- ]
11477
- }
11583
+ },
11584
+ "/v3/portal/partner/{partner_id}/enable": {
11585
+ "post": {
11586
+ "operationId": "enablePartner",
11587
+ "summary": "enablePartner",
11588
+ "description": "Enables a partner from a portal",
11589
+ "tags": [
11590
+ "ECP"
11591
+ ],
11592
+ "security": [
11593
+ {
11594
+ "PortalAuth": []
11478
11595
  }
11479
- }
11480
- },
11481
- "Conflict": {
11482
- "description": "The request conflicts with the current state of the target resource.",
11483
- "content": {
11484
- "application/json": {
11596
+ ],
11597
+ "parameters": [
11598
+ {
11599
+ "name": "partner_id",
11600
+ "in": "path",
11601
+ "required": true,
11485
11602
  "schema": {
11486
- "$ref": "#/components/schemas/ErrorResp"
11603
+ "type": "string",
11604
+ "description": "ID of the partner to enable from the portal"
11487
11605
  }
11488
11606
  }
11489
- }
11490
- },
11491
- "ContractAssignmentConflict": {
11492
- "description": "Contract was found but is not assignable in its current state.",
11493
- "content": {
11494
- "application/json": {
11495
- "schema": {
11496
- "allOf": [
11497
- {
11498
- "$ref": "#/components/schemas/ErrorResp"
11499
- },
11500
- {
11607
+ ],
11608
+ "responses": {
11609
+ "200": {
11610
+ "description": "Partner enabled from portal successfully",
11611
+ "content": {
11612
+ "application/json": {
11613
+ "schema": {
11614
+ "type": "object",
11501
11615
  "properties": {
11502
- "reason": {
11616
+ "message": {
11503
11617
  "type": "string",
11504
- "description": "Reason why the contract is not assignable. If the reason is \"MULTIPLE\", the contract is not assignable because multiple contracts were found and the business logic does not allow it.",
11505
- "enum": [
11506
- "DRAFT",
11507
- "MULTIPLE"
11508
- ]
11618
+ "example": "Partner enabled from portal successfully"
11509
11619
  }
11510
- },
11511
- "required": [
11512
- "reason"
11513
- ]
11620
+ }
11514
11621
  }
11515
- ]
11622
+ }
11516
11623
  }
11624
+ },
11625
+ "400": {
11626
+ "$ref": "#/components/responses/InvalidRequest"
11627
+ },
11628
+ "401": {
11629
+ "$ref": "#/components/responses/Unauthorized"
11630
+ },
11631
+ "403": {
11632
+ "$ref": "#/components/responses/Forbidden"
11633
+ },
11634
+ "404": {
11635
+ "$ref": "#/components/responses/NotFound"
11636
+ },
11637
+ "500": {
11638
+ "$ref": "#/components/responses/InternalServerError"
11517
11639
  }
11518
11640
  }
11519
- },
11520
- "NotFound": {
11641
+ }
11642
+ },
11643
+ "/v3/portal/verify-dns": {
11644
+ "post": {
11645
+ "operationId": "verifyDns",
11646
+ "summary": "verifyDns",
11647
+ "description": "Manually triggers DNS verification for a portal's domain setup. Runs the same verification logic as the scheduled processAllPendingNetworks lambda.",
11648
+ "tags": [
11649
+ "ECP Admin"
11650
+ ],
11651
+ "security": [
11652
+ {
11653
+ "EpilotAuth": []
11654
+ }
11655
+ ],
11656
+ "parameters": [
11657
+ {
11658
+ "in": "query",
11659
+ "name": "portal_id",
11660
+ "required": true,
11661
+ "schema": {
11662
+ "$ref": "#/components/schemas/PortalId"
11663
+ },
11664
+ "description": "PortalId of the portal"
11665
+ }
11666
+ ],
11667
+ "responses": {
11668
+ "200": {
11669
+ "description": "DNS verification result",
11670
+ "content": {
11671
+ "application/json": {
11672
+ "schema": {
11673
+ "type": "object",
11674
+ "properties": {
11675
+ "domain_status": {
11676
+ "type": "string",
11677
+ "description": "The status of the custom domain verification",
11678
+ "enum": [
11679
+ "PENDING",
11680
+ "SUCCEED"
11681
+ ]
11682
+ },
11683
+ "message": {
11684
+ "type": "string",
11685
+ "description": "A message describing the result"
11686
+ }
11687
+ }
11688
+ }
11689
+ }
11690
+ }
11691
+ },
11692
+ "401": {
11693
+ "$ref": "#/components/responses/Unauthorized"
11694
+ },
11695
+ "403": {
11696
+ "$ref": "#/components/responses/Forbidden"
11697
+ },
11698
+ "500": {
11699
+ "$ref": "#/components/responses/InternalServerError"
11700
+ }
11701
+ }
11702
+ }
11703
+ },
11704
+ "/v2/portal/proxy/execute": {
11705
+ "post": {
11706
+ "operationId": "portalProxyExecute",
11707
+ "summary": "portalProxyExecute",
11708
+ "description": "Execute an Integration Hub managed-call use case on behalf of a portal user.\nBridges PortalAuth to the Integration API by generating an internal token.\n",
11709
+ "security": [
11710
+ {
11711
+ "PortalAuth": []
11712
+ }
11713
+ ],
11714
+ "tags": [
11715
+ "ECP"
11716
+ ],
11717
+ "requestBody": {
11718
+ "required": true,
11719
+ "content": {
11720
+ "application/json": {
11721
+ "schema": {
11722
+ "type": "object",
11723
+ "required": [
11724
+ "integration_id",
11725
+ "use_case_slug"
11726
+ ],
11727
+ "properties": {
11728
+ "integration_id": {
11729
+ "type": "string",
11730
+ "format": "uuid",
11731
+ "description": "Integration ID containing the managed-call use case"
11732
+ },
11733
+ "use_case_slug": {
11734
+ "type": "string",
11735
+ "description": "Use case slug (acts as the RPC method name)"
11736
+ },
11737
+ "payload": {
11738
+ "type": "object",
11739
+ "description": "Input data for the managed-call operation",
11740
+ "additionalProperties": true
11741
+ }
11742
+ }
11743
+ }
11744
+ }
11745
+ }
11746
+ },
11747
+ "responses": {
11748
+ "200": {
11749
+ "description": "Managed-call execution result envelope.",
11750
+ "content": {
11751
+ "application/json": {
11752
+ "schema": {
11753
+ "type": "object",
11754
+ "required": [
11755
+ "success"
11756
+ ],
11757
+ "properties": {
11758
+ "success": {
11759
+ "type": "boolean"
11760
+ },
11761
+ "data": {
11762
+ "type": "object",
11763
+ "additionalProperties": true,
11764
+ "description": "Managed-call response payload. Shape is defined by the use\ncase's JSONata response_mapping; if no mapping is\nconfigured the raw external API response is returned.\n"
11765
+ }
11766
+ }
11767
+ }
11768
+ }
11769
+ }
11770
+ },
11771
+ "400": {
11772
+ "$ref": "#/components/responses/InvalidRequest"
11773
+ },
11774
+ "401": {
11775
+ "$ref": "#/components/responses/Unauthorized"
11776
+ },
11777
+ "403": {
11778
+ "$ref": "#/components/responses/Forbidden"
11779
+ },
11780
+ "500": {
11781
+ "$ref": "#/components/responses/InternalServerError"
11782
+ }
11783
+ }
11784
+ }
11785
+ },
11786
+ "/v1/portal/mobile-config": {
11787
+ "get": {
11788
+ "operationId": "getMobileConfig",
11789
+ "summary": "getMobileConfig",
11790
+ "description": "Returns the portal's mobile app configuration. By default the response is build-ready (resolved): base info (display_name from the portal name, app_host from the domain, environment) and branding (logo from the portal images, colors from the design palette) are filled in. Pass raw=true to get only the stored mobile_config without resolution.",
11791
+ "tags": [
11792
+ "ECP Admin"
11793
+ ],
11794
+ "security": [
11795
+ {
11796
+ "EpilotAuth": []
11797
+ }
11798
+ ],
11799
+ "parameters": [
11800
+ {
11801
+ "in": "query",
11802
+ "name": "portal_id",
11803
+ "required": true,
11804
+ "schema": {
11805
+ "type": "string"
11806
+ },
11807
+ "description": "Portal ID"
11808
+ },
11809
+ {
11810
+ "in": "query",
11811
+ "name": "raw",
11812
+ "required": false,
11813
+ "schema": {
11814
+ "type": "boolean"
11815
+ },
11816
+ "description": "Return only the stored mobile_config without resolving base info/branding."
11817
+ }
11818
+ ],
11819
+ "responses": {
11820
+ "200": {
11821
+ "description": "Mobile config retrieved successfully.",
11822
+ "content": {
11823
+ "application/json": {
11824
+ "schema": {
11825
+ "$ref": "#/components/schemas/MobileConfig"
11826
+ }
11827
+ }
11828
+ }
11829
+ },
11830
+ "401": {
11831
+ "$ref": "#/components/responses/Unauthorized"
11832
+ },
11833
+ "403": {
11834
+ "$ref": "#/components/responses/Forbidden"
11835
+ },
11836
+ "404": {
11837
+ "$ref": "#/components/responses/NotFound"
11838
+ },
11839
+ "500": {
11840
+ "$ref": "#/components/responses/InternalServerError"
11841
+ }
11842
+ }
11843
+ },
11844
+ "put": {
11845
+ "operationId": "putMobileConfig",
11846
+ "summary": "putMobileConfig",
11847
+ "description": "Merges the provided fields into the portal's mobile app configuration\n(deep merge). Only mobile_config is modified; all other portal settings\nare left untouched.\n",
11848
+ "tags": [
11849
+ "ECP Admin"
11850
+ ],
11851
+ "security": [
11852
+ {
11853
+ "EpilotAuth": []
11854
+ }
11855
+ ],
11856
+ "parameters": [
11857
+ {
11858
+ "in": "query",
11859
+ "name": "portal_id",
11860
+ "required": true,
11861
+ "schema": {
11862
+ "type": "string"
11863
+ },
11864
+ "description": "Portal ID"
11865
+ }
11866
+ ],
11867
+ "requestBody": {
11868
+ "description": "Editable mobile fields to merge into the existing mobile_config. Only mobile-relevant settings + app branding are applied; other fields are ignored.",
11869
+ "required": true,
11870
+ "content": {
11871
+ "application/json": {
11872
+ "schema": {
11873
+ "$ref": "#/components/schemas/MobileConfigUpdate"
11874
+ }
11875
+ }
11876
+ }
11877
+ },
11878
+ "responses": {
11879
+ "200": {
11880
+ "description": "Mobile config updated successfully.",
11881
+ "content": {
11882
+ "application/json": {
11883
+ "schema": {
11884
+ "$ref": "#/components/schemas/MobileConfig"
11885
+ }
11886
+ }
11887
+ }
11888
+ },
11889
+ "400": {
11890
+ "$ref": "#/components/responses/InvalidRequest"
11891
+ },
11892
+ "401": {
11893
+ "$ref": "#/components/responses/Unauthorized"
11894
+ },
11895
+ "403": {
11896
+ "$ref": "#/components/responses/Forbidden"
11897
+ },
11898
+ "404": {
11899
+ "$ref": "#/components/responses/NotFound"
11900
+ },
11901
+ "500": {
11902
+ "$ref": "#/components/responses/InternalServerError"
11903
+ }
11904
+ }
11905
+ }
11906
+ }
11907
+ },
11908
+ "components": {
11909
+ "responses": {
11910
+ "InvalidRequest": {
11911
+ "description": "The request could not be validated",
11912
+ "content": {
11913
+ "application/json": {
11914
+ "schema": {
11915
+ "$ref": "#/components/schemas/ErrorResp"
11916
+ }
11917
+ }
11918
+ }
11919
+ },
11920
+ "Unauthorized": {
11921
+ "description": "Could not authenticate the user",
11922
+ "content": {
11923
+ "application/json": {
11924
+ "schema": {
11925
+ "$ref": "#/components/schemas/ErrorResp"
11926
+ }
11927
+ }
11928
+ }
11929
+ },
11930
+ "Forbidden": {
11931
+ "description": "The user is not allowed to access this resource",
11932
+ "content": {
11933
+ "application/json": {
11934
+ "schema": {
11935
+ "$ref": "#/components/schemas/ErrorResp"
11936
+ }
11937
+ }
11938
+ }
11939
+ },
11940
+ "ForbiddenByRule": {
11941
+ "description": "The user is not allowed to access this resource",
11942
+ "content": {
11943
+ "application/json": {
11944
+ "schema": {
11945
+ "oneOf": [
11946
+ {
11947
+ "$ref": "#/components/schemas/ErrorResp"
11948
+ },
11949
+ {
11950
+ "$ref": "#/components/schemas/FailedRuleErrorResp"
11951
+ }
11952
+ ]
11953
+ }
11954
+ }
11955
+ }
11956
+ },
11957
+ "Conflict": {
11958
+ "description": "The request conflicts with the current state of the target resource.",
11959
+ "content": {
11960
+ "application/json": {
11961
+ "schema": {
11962
+ "$ref": "#/components/schemas/ErrorResp"
11963
+ }
11964
+ }
11965
+ }
11966
+ },
11967
+ "ContractAssignmentConflict": {
11968
+ "description": "Contract was found but is not assignable in its current state.",
11969
+ "content": {
11970
+ "application/json": {
11971
+ "schema": {
11972
+ "allOf": [
11973
+ {
11974
+ "$ref": "#/components/schemas/ErrorResp"
11975
+ },
11976
+ {
11977
+ "properties": {
11978
+ "reason": {
11979
+ "type": "string",
11980
+ "description": "Reason why the contract is not assignable. If the reason is \"MULTIPLE\", the contract is not assignable because multiple contracts were found and the business logic does not allow it.",
11981
+ "enum": [
11982
+ "DRAFT",
11983
+ "MULTIPLE"
11984
+ ]
11985
+ }
11986
+ },
11987
+ "required": [
11988
+ "reason"
11989
+ ]
11990
+ }
11991
+ ]
11992
+ }
11993
+ }
11994
+ }
11995
+ },
11996
+ "NotFound": {
11521
11997
  "description": "The specified resource was not found",
11522
11998
  "content": {
11523
11999
  "application/json": {
@@ -11537,6 +12013,46 @@
11537
12013
  }
11538
12014
  }
11539
12015
  },
12016
+ "TooManyRequests": {
12017
+ "description": "The upstream service rate-limited the request",
12018
+ "content": {
12019
+ "application/json": {
12020
+ "schema": {
12021
+ "$ref": "#/components/schemas/ErrorResp"
12022
+ }
12023
+ }
12024
+ }
12025
+ },
12026
+ "BadGateway": {
12027
+ "description": "The upstream service failed to process the request",
12028
+ "content": {
12029
+ "application/json": {
12030
+ "schema": {
12031
+ "$ref": "#/components/schemas/ErrorResp"
12032
+ }
12033
+ }
12034
+ }
12035
+ },
12036
+ "ServiceUnavailable": {
12037
+ "description": "The requested feature is not configured",
12038
+ "content": {
12039
+ "application/json": {
12040
+ "schema": {
12041
+ "$ref": "#/components/schemas/ErrorResp"
12042
+ }
12043
+ }
12044
+ }
12045
+ },
12046
+ "GatewayTimeout": {
12047
+ "description": "The upstream service timed out",
12048
+ "content": {
12049
+ "application/json": {
12050
+ "schema": {
12051
+ "$ref": "#/components/schemas/ErrorResp"
12052
+ }
12053
+ }
12054
+ }
12055
+ },
11540
12056
  "ConfirmUserInvalidRequest": {
11541
12057
  "description": "The request could not be validated",
11542
12058
  "content": {
@@ -12571,6 +13087,17 @@
12571
13087
  "new_design": {
12572
13088
  "type": "boolean",
12573
13089
  "description": "Enable or disable the new design for the portal"
13090
+ },
13091
+ "mcp_enabled": {
13092
+ "type": "boolean",
13093
+ "description": "Enable the MCP (AI agent) connector channel for this portal"
13094
+ },
13095
+ "mcp_grant_version": {
13096
+ "type": "integer",
13097
+ "minimum": 0,
13098
+ "default": 0,
13099
+ "readOnly": true,
13100
+ "description": "Server-managed generation used to invalidate MCP grants after the connector is disabled or re-enabled"
12574
13101
  }
12575
13102
  }
12576
13103
  },
@@ -12866,6 +13393,13 @@
12866
13393
  }
12867
13394
  ]
12868
13395
  },
13396
+ "surfaces": {
13397
+ "type": "array",
13398
+ "description": "Surfaces this portal's data is reached from besides the portal UI itself (public journeys on the website, chat). Configured under Security > Surfaces. Each surface defines how a caller authenticates and what data access applies on it; the portal UI is the implicit default surface (login, default scope) and is not listed here. A surface with `authentication: registration_identifiers` is what makes `identifyContact` issue tokens for this portal: without one, `identifyContact` returns 403.\n",
13399
+ "items": {
13400
+ "$ref": "#/components/schemas/PortalSurface"
13401
+ }
13402
+ },
12869
13403
  "contact_identifiers_for_account": {
12870
13404
  "type": "array",
12871
13405
  "description": "Account-mode only. Identifiers on the contact entity of the primarily\nidentified account. Used to pick an existing related contact within the\nresolved account; if none matches, the values are written onto the new\ncontact that is created and linked to the account.\n",
@@ -13015,6 +13549,12 @@
13015
13549
  "type": "boolean",
13016
13550
  "description": "Whether this is a v3 portal configuration"
13017
13551
  },
13552
+ "published_revision_id": {
13553
+ "type": "string",
13554
+ "readOnly": true,
13555
+ "description": "The revision currently live on this portal. Absent until the first publish.",
13556
+ "example": "2026-08-25T14:03:11.482Z-a7f3c1d9"
13557
+ },
13018
13558
  "portal_id": {
13019
13559
  "$ref": "#/components/schemas/PortalId"
13020
13560
  },
@@ -13563,6 +14103,79 @@
13563
14103
  }
13564
14104
  }
13565
14105
  },
14106
+ "ContactIdentifyRequest": {
14107
+ "description": "ContactExistsRequest plus the surface the token is requested for.\n",
14108
+ "allOf": [
14109
+ {
14110
+ "$ref": "#/components/schemas/ContactExistsRequest"
14111
+ },
14112
+ {
14113
+ "type": "object",
14114
+ "required": [
14115
+ "surface_id"
14116
+ ],
14117
+ "properties": {
14118
+ "surface_id": {
14119
+ "type": "string",
14120
+ "description": "Id of the portal surface (see `surfaces` on the portal config) this token is\nfor. The surface must exist and use `authentication: registration_identifiers`;\notherwise the call returns 403. The issued token is bound to this surface and\nconfined to its data access settings.\n",
14121
+ "example": "website-journeys"
14122
+ }
14123
+ }
14124
+ }
14125
+ ]
14126
+ },
14127
+ "ContactIdentifyResponse": {
14128
+ "type": "object",
14129
+ "properties": {
14130
+ "contact_id": {
14131
+ "$ref": "#/components/schemas/EntityId",
14132
+ "description": "ID of the identified contact. Present only on a match."
14133
+ },
14134
+ "account_id": {
14135
+ "$ref": "#/components/schemas/EntityId",
14136
+ "description": "ID of the resolved account when the portal is configured for account-based\nregistration. Present only on a match.\n"
14137
+ },
14138
+ "token": {
14139
+ "type": "string",
14140
+ "description": "One-time bearer token scoped to the identified contact, to be sent as\n`Authorization: Bearer <token>` against the portal APIs. Present only on a match.\n",
14141
+ "example": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
14142
+ },
14143
+ "token_type": {
14144
+ "type": "string",
14145
+ "enum": [
14146
+ "contact_identification"
14147
+ ],
14148
+ "description": "Type of the issued token, matching its own `token_type` claim and the token type in access-token-api. Present only on a match."
14149
+ },
14150
+ "surface_id": {
14151
+ "type": "string",
14152
+ "description": "The surface the token is bound to; echoes the request. Present only on a match.",
14153
+ "example": "website-journeys"
14154
+ },
14155
+ "expires_at": {
14156
+ "type": "string",
14157
+ "format": "date-time",
14158
+ "description": "When the issued token stops being accepted. Present only on a match.",
14159
+ "example": "2026-08-11T10:35:00.000Z"
14160
+ },
14161
+ "allowed_operations": {
14162
+ "type": "array",
14163
+ "description": "The operationIds the issued token may call. Every other operation rejects the token\nwith 401, including read operations. Echoed so a caller does not have to infer the\nsurface from this specification, and so a change to the allowlist is visible at\nruntime. Present only on a match.\n",
14164
+ "items": {
14165
+ "type": "string"
14166
+ },
14167
+ "example": []
14168
+ },
14169
+ "reason": {
14170
+ "type": "string",
14171
+ "enum": [
14172
+ "TIMEOUT",
14173
+ "NOT_FOUND"
14174
+ ],
14175
+ "description": "Present only when no token was issued. NOT_FOUND means the given identifiers did not\nmatch any contact (definitive - the client should not retry). TIMEOUT means the contact\nwas not found within the processing window but may still be ingesting; the client may\nretry (ideally with trigger_identifiers_check=false).\n"
14176
+ }
14177
+ }
14178
+ },
13566
14179
  "AccountExistsRequest": {
13567
14180
  "type": "object",
13568
14181
  "required": [
@@ -15048,6 +15661,94 @@
15048
15661
  }
15049
15662
  }
15050
15663
  },
15664
+ "PortalSurface": {
15665
+ "type": "object",
15666
+ "description": "One surface a portal's data is reached from (see `surfaces` on the portal config).\n\nA surface names how callers authenticate on it and what they may reach once they\nhave. Data access is always a subset of the portal's own: a schema not in\n`allowed_portal_entities` cannot be opened up by a surface, and the portal's\ncontact-relation rules still apply underneath the surface's own.\n\nTokens minted for a surface (currently: contact identification tokens for\n`registration_identifiers` surfaces) carry the surface id. The surface's data\naccess is resolved from the portal config on every request, not baked into the\ntoken, so tightening a surface applies to tokens already in circulation.\n",
15667
+ "required": [
15668
+ "id",
15669
+ "name",
15670
+ "authentication"
15671
+ ],
15672
+ "properties": {
15673
+ "id": {
15674
+ "type": "string",
15675
+ "minLength": 1,
15676
+ "maxLength": 64,
15677
+ "pattern": "^[a-z0-9][a-z0-9_-]*$",
15678
+ "description": "Stable identifier, unique within the portal. Referenced by `identifyContact` and carried in issued tokens.",
15679
+ "example": "website-journeys"
15680
+ },
15681
+ "name": {
15682
+ "type": "string",
15683
+ "minLength": 1,
15684
+ "maxLength": 120,
15685
+ "example": "Website journeys"
15686
+ },
15687
+ "description": {
15688
+ "type": "string",
15689
+ "maxLength": 500,
15690
+ "description": "Free text for the configuring user, e.g. where this surface is embedded."
15691
+ },
15692
+ "authentication": {
15693
+ "type": "string",
15694
+ "enum": [
15695
+ "login",
15696
+ "registration_identifiers"
15697
+ ],
15698
+ "description": "How a caller on this surface proves who they are.\n\n- `login`: a logged-in portal user token (the portal's own authentication).\n- `registration_identifiers`: the caller supplies the portal's\n `registration_identifiers` to `identifyContact` and receives a short-lived\n contact identification token. Anyone who knows or guesses those values can\n use this surface, so its data access should be as narrow as the use case allows.\n\n`anonymous` (no proof of identity at all) is planned and not accepted yet.\n"
15699
+ },
15700
+ "token_ttl_seconds": {
15701
+ "type": "integer",
15702
+ "format": "int32",
15703
+ "default": 300,
15704
+ "minimum": 60,
15705
+ "maximum": 900,
15706
+ "description": "Lifetime of tokens minted for this surface. Only used with `registration_identifiers`."
15707
+ },
15708
+ "data_access": {
15709
+ "$ref": "#/components/schemas/PortalSurfaceDataAccess"
15710
+ }
15711
+ }
15712
+ },
15713
+ "PortalSurfaceDataAccess": {
15714
+ "type": "object",
15715
+ "description": "What a caller on a surface may reach. Every property is optional; omitting all of them is the portal's default scope.",
15716
+ "properties": {
15717
+ "entities": {
15718
+ "type": "array",
15719
+ "description": "Schemas reachable on this surface, each with the targets that always apply to\nit. Must be a subset of the portal's `allowed_portal_entities`; anything else\nis ignored. Empty or omitted means the surface reaches nothing - data access is\nopted into per schema.\n",
15720
+ "items": {
15721
+ "$ref": "#/components/schemas/PortalSurfaceEntityAccess"
15722
+ }
15723
+ },
15724
+ "role_id": {
15725
+ "type": "string",
15726
+ "description": "360 role whose grants apply on this surface (`<org_id>:<slug>`), giving\nvertical permissions - which attributes and actions are permitted. Omitted\nmeans the portal's default role.\n",
15727
+ "example": "728:public_journeys_readonly"
15728
+ }
15729
+ }
15730
+ },
15731
+ "PortalSurfaceEntityAccess": {
15732
+ "type": "object",
15733
+ "description": "One schema this surface reaches, with the targets that always apply to it.",
15734
+ "properties": {
15735
+ "schema": {
15736
+ "type": "string",
15737
+ "description": "Schema slug, from the portal's `allowed_portal_entities`.",
15738
+ "example": "contract"
15739
+ },
15740
+ "target_ids": {
15741
+ "type": "array",
15742
+ "description": "Targets (see the Targeting API) whose filters always apply to reads of this\nschema on this surface, giving horizontal permissions - which rows are\nreachable. They are applied in addition to any targets the caller passes. A\ntarget matches one schema, which is why they are configured per schema here.\n",
15743
+ "items": {
15744
+ "type": "string"
15745
+ }
15746
+ }
15747
+ },
15748
+ "required": [
15749
+ "schema"
15750
+ ]
15751
+ },
15051
15752
  "Grant": {
15052
15753
  "type": "object",
15053
15754
  "properties": {
@@ -15414,15 +16115,58 @@
15414
16115
  "items": {
15415
16116
  "$ref": "#/components/schemas/PortalWorkflowTask"
15416
16117
  }
16118
+ },
16119
+ "stages": {
16120
+ "type": "array",
16121
+ "description": "Customer-facing stages of the execution in strict order, each with a\nprogress status derived by the Workflows API. Present only for V2 flow\nexecutions whose template defines stages and whose boundaries resolve\ncleanly; omitted otherwise, so consumers must fall back to the flat\ntask timeline.\n",
16122
+ "items": {
16123
+ "$ref": "#/components/schemas/PortalWorkflowStage"
16124
+ }
16125
+ }
16126
+ },
16127
+ "required": [
16128
+ "id",
16129
+ "name",
16130
+ "status",
16131
+ "version",
16132
+ "is_path_complete",
16133
+ "tasks"
16134
+ ]
16135
+ },
16136
+ "PortalWorkflowStage": {
16137
+ "type": "object",
16138
+ "description": "A customer-facing stage of a flow execution, with a progress status derived\nfrom the execution's tasks by the Workflows API. Stages form a strict total\norder; the array order is the stage order.\n",
16139
+ "properties": {
16140
+ "id": {
16141
+ "type": "string",
16142
+ "description": "Stable unique identifier for the stage"
16143
+ },
16144
+ "name": {
16145
+ "type": "string",
16146
+ "description": "User-facing stage title"
16147
+ },
16148
+ "description": {
16149
+ "type": "string",
16150
+ "description": "Customer-facing description of what happens in this stage"
16151
+ },
16152
+ "status": {
16153
+ "type": "string",
16154
+ "enum": [
16155
+ "COMPLETED",
16156
+ "IN_PROGRESS",
16157
+ "UPCOMING"
16158
+ ],
16159
+ "description": "Derived progress status:\n- COMPLETED: every task of the stage is done, and the flow has moved past it\n- IN_PROGRESS: the flow's current work is inside this stage\n- UPCOMING: the flow has not reached this stage yet\n"
16160
+ },
16161
+ "completed_at": {
16162
+ "type": "string",
16163
+ "description": "Latest completion timestamp among the stage's tasks; set only when the\nstage is COMPLETED and at least one of its tasks recorded one\n"
15417
16164
  }
15418
16165
  },
15419
16166
  "required": [
15420
16167
  "id",
15421
16168
  "name",
15422
- "status",
15423
- "version",
15424
- "is_path_complete",
15425
- "tasks"
16169
+ "status"
15426
16170
  ]
15427
16171
  },
15428
16172
  "PortalWorkflowTask": {
@@ -15494,6 +16238,10 @@
15494
16238
  "type": "string",
15495
16239
  "description": "Name of the phase the underlying task belongs to, if any (V2 only)"
15496
16240
  },
16241
+ "stage_id": {
16242
+ "type": "string",
16243
+ "description": "Id of the stage the underlying task belongs to, when the execution\ncarries stages (derived by the Workflows API at read time)\n"
16244
+ },
15497
16245
  "completed_at": {
15498
16246
  "type": "string",
15499
16247
  "description": "Timestamp when the task was completed or skipped"
@@ -15959,7 +16707,7 @@
15959
16707
  },
15960
16708
  "q": {
15961
16709
  "type": "string",
15962
- "description": "Keyword search query",
16710
+ "description": "Free-text keyword search. This is plain text, **not** a query language: punctuation separates words rather than carrying any special meaning, and every word has to match. Use `q_fields` to restrict which fields are searched, and `filters`/`targets` for structured filtering. Overly long input is trimmed, and input with no words in it is ignored — the remaining parameters still apply.",
15963
16711
  "example": "contract"
15964
16712
  },
15965
16713
  "q_fields": {
@@ -16597,7 +17345,7 @@
16597
17345
  },
16598
17346
  "intervals": {
16599
17347
  "type": "array",
16600
- "description": "Intervals supported for the current context. If omitted, all intervals are assumed supported.",
17348
+ "description": "Intervals supported for the current context. If omitted, all intervals are assumed supported. `custom` marks a period-based consumption source: the portal requests the whole `data_range` once with `interval=custom` and renders one bar per returned record (see `period` on the consumption data point) instead of offering interval / date navigation or period comparison. When `custom` is present it takes precedence over any fixed intervals also listed.\n",
16601
17349
  "items": {
16602
17350
  "type": "string",
16603
17351
  "enum": [
@@ -16605,7 +17353,8 @@
16605
17353
  "PT1H",
16606
17354
  "P1D",
16607
17355
  "P1M",
16608
- "P1Y"
17356
+ "P1Y",
17357
+ "custom"
16609
17358
  ]
16610
17359
  }
16611
17360
  },
@@ -18426,6 +19175,41 @@
18426
19175
  "display_name"
18427
19176
  ]
18428
19177
  },
19178
+ "ProviderPublicConfigV3": {
19179
+ "type": "object",
19180
+ "description": "Web-only public provider configuration served by `getPublicSSOProviderV3`.\nUnlike `ProviderPublicConfig` there is no `mobile_oidc_config`, and\n`oidc_config` never carries `client_secret` or the `metadata.test_auth_*`\ncredentials — `oidc_config.has_client_secret` signals their presence so\nclients route the token exchange through the backend SSO callback.\n",
19181
+ "properties": {
19182
+ "slug": {
19183
+ "$ref": "#/components/schemas/ProviderSlug"
19184
+ },
19185
+ "display_name": {
19186
+ "$ref": "#/components/schemas/ProviderDisplayName"
19187
+ },
19188
+ "oidc_config": {
19189
+ "$ref": "#/components/schemas/OIDCProviderConfig"
19190
+ }
19191
+ },
19192
+ "required": [
19193
+ "slug",
19194
+ "display_name"
19195
+ ]
19196
+ },
19197
+ "PublicIdentityProvider": {
19198
+ "type": "object",
19199
+ "description": "Minimal public identity provider info — enough to render a provider login button.",
19200
+ "properties": {
19201
+ "slug": {
19202
+ "$ref": "#/components/schemas/ProviderSlug"
19203
+ },
19204
+ "display_name": {
19205
+ "$ref": "#/components/schemas/ProviderDisplayName"
19206
+ }
19207
+ },
19208
+ "required": [
19209
+ "slug",
19210
+ "display_name"
19211
+ ]
19212
+ },
18429
19213
  "AttributeMappingConfig": {
18430
19214
  "type": "object",
18431
19215
  "description": "Dictionary of epilot user attributes to claims",
@@ -19219,6 +20003,17 @@
19219
20003
  "new_design": {
19220
20004
  "type": "boolean",
19221
20005
  "description": "Enable or disable the new design for the portal"
20006
+ },
20007
+ "mcp_enabled": {
20008
+ "type": "boolean",
20009
+ "description": "Enable the MCP (AI agent) connector channel for this portal"
20010
+ },
20011
+ "mcp_grant_version": {
20012
+ "type": "integer",
20013
+ "minimum": 0,
20014
+ "default": 0,
20015
+ "readOnly": true,
20016
+ "description": "Server-managed generation used to invalidate MCP grants after the connector is disabled or re-enabled"
19222
20017
  }
19223
20018
  }
19224
20019
  },
@@ -19514,6 +20309,13 @@
19514
20309
  }
19515
20310
  ]
19516
20311
  },
20312
+ "surfaces": {
20313
+ "type": "array",
20314
+ "description": "Surfaces this portal's data is reached from besides the portal UI itself (public journeys on the website, chat). Configured under Security > Surfaces. Each surface defines how a caller authenticates and what data access applies on it; the portal UI is the implicit default surface (login, default scope) and is not listed here. A surface with `authentication: registration_identifiers` is what makes `identifyContact` issue tokens for this portal: without one, `identifyContact` returns 403.\n",
20315
+ "items": {
20316
+ "$ref": "#/components/schemas/PortalSurface"
20317
+ }
20318
+ },
19517
20319
  "contact_identifiers_for_account": {
19518
20320
  "type": "array",
19519
20321
  "description": "Account-mode only. Identifiers on the contact entity of the primarily\nidentified account. Used to pick an existing related contact within the\nresolved account; if none matches, the values are written onto the new\ncontact that is created and linked to the account.\n",
@@ -19663,6 +20465,12 @@
19663
20465
  "type": "boolean",
19664
20466
  "description": "Whether this is a v3 portal configuration"
19665
20467
  },
20468
+ "published_revision_id": {
20469
+ "type": "string",
20470
+ "readOnly": true,
20471
+ "description": "The revision currently live on this portal. Absent until the first publish.",
20472
+ "example": "2026-08-25T14:03:11.482Z-a7f3c1d9"
20473
+ },
19666
20474
  "portal_id": {
19667
20475
  "$ref": "#/components/schemas/PortalId"
19668
20476
  },
@@ -19750,137 +20558,414 @@
19750
20558
  }
19751
20559
  }
19752
20560
  },
19753
- "formatter": {
20561
+ "formatter": {
20562
+ "type": "string",
20563
+ "enum": [
20564
+ "text",
20565
+ "date",
20566
+ "money_cents",
20567
+ "enum",
20568
+ "address"
20569
+ ]
20570
+ },
20571
+ "enum_labels": {
20572
+ "type": "object",
20573
+ "description": "Localized value maps for the enum formatter, keyed by language then raw value.\n",
20574
+ "additionalProperties": {
20575
+ "type": "object",
20576
+ "additionalProperties": {
20577
+ "type": "string"
20578
+ }
20579
+ }
20580
+ }
20581
+ }
20582
+ },
20583
+ "UpsertPortalConfigV3": {
20584
+ "allOf": [
20585
+ {
20586
+ "$ref": "#/components/schemas/UpdateOnlyPortalConfigAttributes"
20587
+ },
20588
+ {
20589
+ "$ref": "#/components/schemas/CommonConfigAttributesV3"
20590
+ },
20591
+ {
20592
+ "properties": {
20593
+ "origin": {
20594
+ "$ref": "#/components/schemas/Origin"
20595
+ },
20596
+ "pages": {
20597
+ "type": "array",
20598
+ "items": {
20599
+ "$ref": "#/components/schemas/PageRequest"
20600
+ }
20601
+ }
20602
+ }
20603
+ }
20604
+ ]
20605
+ },
20606
+ "PortalConfigV3": {
20607
+ "allOf": [
20608
+ {
20609
+ "$ref": "#/components/schemas/UpdateOnlyPortalConfigAttributes"
20610
+ },
20611
+ {
20612
+ "$ref": "#/components/schemas/CommonConfigAttributesV3"
20613
+ },
20614
+ {
20615
+ "properties": {
20616
+ "organization_id": {
20617
+ "type": "string",
20618
+ "example": 12345,
20619
+ "description": "ID of the organization"
20620
+ },
20621
+ "org_settings": {
20622
+ "type": "object",
20623
+ "description": "Organization settings",
20624
+ "properties": {
20625
+ "canary": {
20626
+ "type": "object",
20627
+ "description": "Canary feature flag",
20628
+ "properties": {
20629
+ "enabled": {
20630
+ "type": "boolean",
20631
+ "description": "Enable/Disable the canary feature"
20632
+ }
20633
+ }
20634
+ },
20635
+ "notracking": {
20636
+ "type": "object",
20637
+ "description": "Disable Advanced Usage Metrics",
20638
+ "properties": {
20639
+ "enabled": {
20640
+ "type": "boolean",
20641
+ "description": "Disable browser-side scripts that track advanced usage metrics"
20642
+ }
20643
+ }
20644
+ }
20645
+ }
20646
+ },
20647
+ "feature_flags": {
20648
+ "type": "object",
20649
+ "description": "Feature flags for the portal",
20650
+ "additionalProperties": {
20651
+ "type": "boolean"
20652
+ }
20653
+ },
20654
+ "grants": {
20655
+ "type": "array",
20656
+ "description": "Permissions granted to a portal user while accessing entities",
20657
+ "items": {
20658
+ "$ref": "#/components/schemas/Grant"
20659
+ }
20660
+ },
20661
+ "identity_providers": {
20662
+ "type": "array",
20663
+ "items": {
20664
+ "$ref": "#/components/schemas/ProviderPublicConfig"
20665
+ }
20666
+ },
20667
+ "pages": {
20668
+ "type": "array",
20669
+ "items": {
20670
+ "$ref": "#/components/schemas/Page"
20671
+ }
20672
+ }
20673
+ }
20674
+ }
20675
+ ]
20676
+ },
20677
+ "JuiceSettings": {
20678
+ "type": "object",
20679
+ "properties": {
20680
+ "is_dummy": {
20681
+ "type": "boolean",
20682
+ "description": "Whether the org is in dummy mode"
20683
+ },
20684
+ "is_canary": {
20685
+ "type": "boolean",
20686
+ "description": "Whether the org is in canary mode"
20687
+ },
20688
+ "is_legacy_design": {
20689
+ "type": "boolean",
20690
+ "description": "Whether the portal still runs the old design (the `new_design` feature setting is off). Legacy portals are always served the frozen legacy bundle, even when the org is in canary."
20691
+ },
20692
+ "redirect_to": {
20693
+ "type": "string",
20694
+ "description": "The URL to redirect to",
20695
+ "example": "https://example.com"
20696
+ }
20697
+ }
20698
+ },
20699
+ "RevisionPageRequest": {
20700
+ "type": "object",
20701
+ "additionalProperties": true,
20702
+ "description": "A page inside a revision snapshot. `additionalProperties` is true on purpose — a page carries fields this schema does not name and they must survive into the snapshot and back onto the live page when it is published.\n",
20703
+ "required": [
20704
+ "id",
20705
+ "slug",
20706
+ "order",
20707
+ "blocks"
20708
+ ],
20709
+ "properties": {
20710
+ "id": {
20711
+ "type": "string",
20712
+ "format": "uuid",
20713
+ "description": "Stable page identity. Required because a revision diff correlates pages by id, not by slug.\n`format: uuid` is enforced, so a non-UUID id is rejected as a `400`.\n",
20714
+ "example": "c495fef9-eeca-4019-a989-8390dcd9825b"
20715
+ },
20716
+ "slug": {
20717
+ "type": "string",
20718
+ "example": "dashboard"
20719
+ },
20720
+ "order": {
20721
+ "type": "number",
20722
+ "example": 0
20723
+ },
20724
+ "blocks": {
20725
+ "type": "object",
20726
+ "additionalProperties": true,
20727
+ "description": "Required on purpose. A live save preserves an omitted `blocks` from the stored page; publish replaces each live page wholesale with no such guard, so a revision snapshotted without blocks would wipe them on publish. `{}` is a valid value — the field simply has to be present.\n"
20728
+ }
20729
+ }
20730
+ },
20731
+ "PortalRevisionRequest": {
20732
+ "allOf": [
20733
+ {
20734
+ "$ref": "#/components/schemas/UpdateOnlyPortalConfigAttributes"
20735
+ },
20736
+ {
20737
+ "$ref": "#/components/schemas/CommonConfigAttributesV3"
20738
+ },
20739
+ {
20740
+ "type": "object",
20741
+ "additionalProperties": true,
20742
+ "required": [
20743
+ "pages"
20744
+ ],
20745
+ "properties": {
20746
+ "email_templates": {
20747
+ "$ref": "#/components/schemas/EmailTemplates"
20748
+ },
20749
+ "identity_providers": {
20750
+ "type": "array",
20751
+ "items": {
20752
+ "$ref": "#/components/schemas/ProviderConfig"
20753
+ }
20754
+ },
20755
+ "based_on_revision_id": {
20756
+ "type": "string"
20757
+ },
20758
+ "pages": {
20759
+ "type": "array",
20760
+ "items": {
20761
+ "$ref": "#/components/schemas/RevisionPageRequest"
20762
+ }
20763
+ }
20764
+ }
20765
+ }
20766
+ ]
20767
+ },
20768
+ "PublishRevisionRequest": {
20769
+ "type": "object",
20770
+ "required": [
20771
+ "revision_id"
20772
+ ],
20773
+ "properties": {
20774
+ "revision_id": {
20775
+ "type": "string",
20776
+ "example": "2026-08-25T14:03:11.482Z-a7f3c1d9"
20777
+ },
20778
+ "name": {
19754
20779
  "type": "string",
19755
- "enum": [
19756
- "text",
19757
- "date",
19758
- "money_cents",
19759
- "enum",
19760
- "address"
19761
- ]
20780
+ "maxLength": 255,
20781
+ "description": "Label stamped onto the revision being published, permanently and atomically with the publish itself. Optional: when omitted the revision keeps whatever name it already has, possibly none; the server never generates one. An explicit name replaces any previous one. Clients derive a display label for unnamed revisions from `published_at` / `created_at`.\n",
20782
+ "example": "FAQ page launch"
19762
20783
  },
19763
- "enum_labels": {
19764
- "type": "object",
19765
- "description": "Localized value maps for the enum formatter, keyed by language then raw value.\n",
19766
- "additionalProperties": {
19767
- "type": "object",
19768
- "additionalProperties": {
19769
- "type": "string"
19770
- }
20784
+ "description": {
20785
+ "type": "string",
20786
+ "maxLength": 8000,
20787
+ "description": "Optional description stamped onto the revision at publish time"
20788
+ }
20789
+ }
20790
+ },
20791
+ "PortalRevisionSummary": {
20792
+ "type": "object",
20793
+ "required": [
20794
+ "revision_id",
20795
+ "created_at",
20796
+ "page_count",
20797
+ "is_published"
20798
+ ],
20799
+ "properties": {
20800
+ "revision_id": {
20801
+ "type": "string",
20802
+ "example": "2026-08-25T14:03:11.482Z-a7f3c1d9"
20803
+ },
20804
+ "created_at": {
20805
+ "type": "string",
20806
+ "format": "date-time"
20807
+ },
20808
+ "created_by": {
20809
+ "type": "string",
20810
+ "description": "May be absent — an internal-auth caller carries no user id."
20811
+ },
20812
+ "name": {
20813
+ "type": "string",
20814
+ "description": "Set at publish time only, and only when the publish request carried one. A revision can have been published and still have no name; `published_at` is the signal that a revision was live at some point, not this field.\n",
20815
+ "example": "FAQ page launch"
20816
+ },
20817
+ "description": {
20818
+ "type": "string"
20819
+ },
20820
+ "page_count": {
20821
+ "type": "number"
20822
+ },
20823
+ "published_at": {
20824
+ "type": "string",
20825
+ "format": "date-time",
20826
+ "description": "The last time this revision was published. NOT the same question as `is_published`: a revision that was live yesterday still carries a `published_at`.\n"
20827
+ },
20828
+ "published_by": {
20829
+ "type": "string",
20830
+ "description": "Who performed the last publish of this revision."
20831
+ },
20832
+ "is_published": {
20833
+ "type": "boolean",
20834
+ "description": "Whether this revision is the one currently live."
20835
+ }
20836
+ }
20837
+ },
20838
+ "PortalRevisionList": {
20839
+ "type": "object",
20840
+ "required": [
20841
+ "results"
20842
+ ],
20843
+ "properties": {
20844
+ "results": {
20845
+ "type": "array",
20846
+ "items": {
20847
+ "$ref": "#/components/schemas/PortalRevisionSummary"
19771
20848
  }
20849
+ },
20850
+ "next_cursor": {
20851
+ "type": "string",
20852
+ "description": "Opaque cursor to pass back as `cursor` to fetch the next page. When `next_cursor` is absent, the client has reached the end of the dataset.\n"
19772
20853
  }
19773
20854
  }
19774
20855
  },
19775
- "UpsertPortalConfigV3": {
20856
+ "PortalRevisionCreated": {
19776
20857
  "allOf": [
19777
20858
  {
19778
- "$ref": "#/components/schemas/UpdateOnlyPortalConfigAttributes"
19779
- },
19780
- {
19781
- "$ref": "#/components/schemas/CommonConfigAttributesV3"
20859
+ "$ref": "#/components/schemas/PortalRevisionSummary"
19782
20860
  },
19783
20861
  {
20862
+ "type": "object",
19784
20863
  "properties": {
19785
- "origin": {
19786
- "$ref": "#/components/schemas/Origin"
19787
- },
19788
- "pages": {
20864
+ "identity_providers": {
19789
20865
  "type": "array",
20866
+ "description": "The SSO identity providers just captured into this revision, with `client_secret` REDACTED exactly as the admin portal-config GET redacts it. Returned only here — not on list items or elsewhere — because the caller who just submitted providers is the one reader who needs to see what got captured, e.g. to warn that the revision was saved without a client secret.\n",
19790
20867
  "items": {
19791
- "$ref": "#/components/schemas/PageRequest"
20868
+ "$ref": "#/components/schemas/ProviderConfig"
19792
20869
  }
19793
20870
  }
19794
20871
  }
19795
20872
  }
19796
20873
  ]
19797
20874
  },
19798
- "PortalConfigV3": {
20875
+ "RevisionPage": {
20876
+ "description": "A page from a revision snapshot. Same shape as a live `Page`, without the server-managed fields (`past_routes`, `_created_at`, `_updated_at`, `is_deleted`), which publish re-derives from the live portal.\n",
19799
20877
  "allOf": [
19800
20878
  {
19801
- "$ref": "#/components/schemas/UpdateOnlyPortalConfigAttributes"
20879
+ "$ref": "#/components/schemas/Page"
19802
20880
  },
19803
20881
  {
19804
- "$ref": "#/components/schemas/CommonConfigAttributesV3"
20882
+ "type": "object",
20883
+ "required": [
20884
+ "id",
20885
+ "blocks"
20886
+ ],
20887
+ "properties": {
20888
+ "org_id": {
20889
+ "type": "string"
20890
+ }
20891
+ }
20892
+ }
20893
+ ]
20894
+ },
20895
+ "PortalRevision": {
20896
+ "allOf": [
20897
+ {
20898
+ "$ref": "#/components/schemas/PortalRevisionSummary"
19805
20899
  },
19806
20900
  {
20901
+ "type": "object",
20902
+ "required": [
20903
+ "config",
20904
+ "pages"
20905
+ ],
19807
20906
  "properties": {
19808
- "organization_id": {
19809
- "type": "string",
19810
- "example": 12345,
19811
- "description": "ID of the organization"
19812
- },
19813
- "org_settings": {
19814
- "type": "object",
19815
- "description": "Organization settings",
19816
- "properties": {
19817
- "canary": {
19818
- "type": "object",
19819
- "description": "Canary feature flag",
19820
- "properties": {
19821
- "enabled": {
19822
- "type": "boolean",
19823
- "description": "Enable/Disable the canary feature"
19824
- }
19825
- }
19826
- },
19827
- "notracking": {
19828
- "type": "object",
19829
- "description": "Disable Advanced Usage Metrics",
19830
- "properties": {
19831
- "enabled": {
19832
- "type": "boolean",
19833
- "description": "Disable browser-side scripts that track advanced usage metrics"
19834
- }
19835
- }
19836
- }
19837
- }
19838
- },
19839
- "feature_flags": {
20907
+ "config": {
19840
20908
  "type": "object",
19841
- "description": "Feature flags for the portal",
19842
- "additionalProperties": {
19843
- "type": "boolean"
19844
- }
20909
+ "additionalProperties": true,
20910
+ "description": "The snapshotted portal configuration. Secret-typed extension option values are removed entirely — not masked — before the response is assembled.\n"
19845
20911
  },
19846
- "grants": {
20912
+ "pages": {
19847
20913
  "type": "array",
19848
- "description": "Permissions granted to a portal user while accessing entities",
19849
20914
  "items": {
19850
- "$ref": "#/components/schemas/Grant"
20915
+ "$ref": "#/components/schemas/RevisionPage"
19851
20916
  }
19852
20917
  },
19853
20918
  "identity_providers": {
19854
20919
  "type": "array",
20920
+ "description": "The SSO identity providers captured in this revision, with `client_secret` REDACTED exactly as the admin portal-config GET redacts it (no read path returns a stored secret).\nPublishing this revision replaces every live provider on this portal's origin, not just this portal's own, with this set. Identity providers key on `IDP#{origin}#{slug}`, a partition shared by every portal on that origin, so any other portal sharing it (an ADDITIONAL_PORTAL clone, most commonly) is affected too. A client can diff this against the live providers (by `slug`, and on `oidc_config.oidc_issuer` / `oidc_config.client_id` for a changed provider) to warn that a publish would also change SSO configuration, for this portal and any others sharing its origin.\nABSENT and `[]` are different answers. `[]` means the revision has no providers and publishing it removes the live ones; absent means the revision was saved before provider versioning, carries no SSO opinion, and publishing it changes no provider.\n",
19855
20921
  "items": {
19856
- "$ref": "#/components/schemas/ProviderPublicConfig"
20922
+ "$ref": "#/components/schemas/ProviderConfig"
19857
20923
  }
19858
20924
  },
19859
- "pages": {
19860
- "type": "array",
19861
- "items": {
19862
- "$ref": "#/components/schemas/Page"
19863
- }
20925
+ "email_templates": {
20926
+ "$ref": "#/components/schemas/EmailTemplates"
20927
+ },
20928
+ "email_template_settings": {
20929
+ "type": "object",
20930
+ "additionalProperties": true,
20931
+ "description": "The portal's email template settings as they were live when the revision was saved. Read-only: this field cannot be set through the revision request and is always captured from live.\n"
19864
20932
  }
19865
20933
  }
19866
20934
  }
19867
20935
  ]
19868
20936
  },
19869
- "JuiceSettings": {
20937
+ "PublishResult": {
19870
20938
  "type": "object",
20939
+ "required": [
20940
+ "revision_id",
20941
+ "published_at",
20942
+ "post_publish_warnings"
20943
+ ],
19871
20944
  "properties": {
19872
- "is_dummy": {
19873
- "type": "boolean",
19874
- "description": "Whether the org is in dummy mode"
20945
+ "revision_id": {
20946
+ "type": "string"
19875
20947
  },
19876
- "is_canary": {
19877
- "type": "boolean",
19878
- "description": "Whether the org is in canary mode"
20948
+ "published_at": {
20949
+ "type": "string",
20950
+ "format": "date-time"
19879
20951
  },
19880
- "redirect_to": {
20952
+ "published_by": {
20953
+ "type": "string"
20954
+ },
20955
+ "name": {
19881
20956
  "type": "string",
19882
- "description": "The URL to redirect to",
19883
- "example": "https://example.com"
20957
+ "description": "The name now on the revision: the one sent in the request, or the one a previous publish set when this request omitted `name`. Absent when neither exists.\n",
20958
+ "example": "FAQ page launch"
20959
+ },
20960
+ "post_publish_warnings": {
20961
+ "type": "array",
20962
+ "description": "Stable, machine-readable codes for best-effort post-publish side effects that exhausted their retries. Publishing itself succeeded; these are not error messages and not meant for display as-is. Clients map each code to their own localized text.\nKnown codes: `allowed_entities_change` (portal entity grants and detail pages did not sync), `dns_records_clear` (the previous domain's DNS records were not cleared). New codes may be added without a client update, so treat an unrecognized code as a generic \"a follow-up step did not finish\" case rather than an error.\n",
20963
+ "items": {
20964
+ "type": "string"
20965
+ },
20966
+ "example": [
20967
+ "allowed_entities_change"
20968
+ ]
19884
20969
  }
19885
20970
  }
19886
20971
  },
@@ -19934,6 +21019,157 @@
19934
21019
  "example": true
19935
21020
  }
19936
21021
  }
21022
+ },
21023
+ "SupportReportType": {
21024
+ "type": "string",
21025
+ "enum": [
21026
+ "bug",
21027
+ "feedback"
21028
+ ]
21029
+ },
21030
+ "SupportRequestAttachment": {
21031
+ "type": "object",
21032
+ "required": [
21033
+ "filename",
21034
+ "mime_type",
21035
+ "contents"
21036
+ ],
21037
+ "properties": {
21038
+ "filename": {
21039
+ "type": "string",
21040
+ "maxLength": 255,
21041
+ "example": "screenshot.png"
21042
+ },
21043
+ "mime_type": {
21044
+ "type": "string",
21045
+ "example": "image/png"
21046
+ },
21047
+ "contents": {
21048
+ "type": "string",
21049
+ "description": "Base64-encoded file, optionally as a data URL"
21050
+ }
21051
+ }
21052
+ },
21053
+ "CleverPvContext": {
21054
+ "type": "object",
21055
+ "properties": {
21056
+ "screen": {
21057
+ "type": "string",
21058
+ "maxLength": 256,
21059
+ "description": "Current Clever PV screen identifier"
21060
+ },
21061
+ "version": {
21062
+ "type": "string",
21063
+ "maxLength": 64,
21064
+ "description": "Clever PV application version"
21065
+ },
21066
+ "connection_state": {
21067
+ "type": "string",
21068
+ "maxLength": 64,
21069
+ "description": "Device connectivity at the time of the report"
21070
+ },
21071
+ "locale": {
21072
+ "type": "string",
21073
+ "maxLength": 32,
21074
+ "description": "App locale at the time of the report"
21075
+ },
21076
+ "timezone": {
21077
+ "type": "string",
21078
+ "maxLength": 64,
21079
+ "description": "App timezone at the time of the report"
21080
+ },
21081
+ "firmware_version": {
21082
+ "type": "string",
21083
+ "maxLength": 64,
21084
+ "description": "Device firmware version when known"
21085
+ },
21086
+ "build": {
21087
+ "type": "string",
21088
+ "maxLength": 64,
21089
+ "description": "App build identifier when known"
21090
+ },
21091
+ "trace_id": {
21092
+ "type": "string",
21093
+ "maxLength": 128,
21094
+ "description": "Client-generated correlation id for the report"
21095
+ },
21096
+ "device": {
21097
+ "type": "object",
21098
+ "additionalProperties": true,
21099
+ "description": "Optional device snapshot from the portal. Attached as a JSON file on\nthe original Zendesk comment. Must include id when present. Serialized\nJSON is limited to 256 KiB.\n"
21100
+ },
21101
+ "vendor": {
21102
+ "type": "object",
21103
+ "additionalProperties": true,
21104
+ "properties": {
21105
+ "id": {
21106
+ "type": "string"
21107
+ },
21108
+ "name": {
21109
+ "type": "string"
21110
+ }
21111
+ },
21112
+ "description": "Optional onboarding vendor id and name. Included in the Zendesk\ncomment. Not a portal entity.\n"
21113
+ }
21114
+ }
21115
+ },
21116
+ "CreateSupportRequest": {
21117
+ "type": "object",
21118
+ "required": [
21119
+ "report_type",
21120
+ "description",
21121
+ "submission_id"
21122
+ ],
21123
+ "properties": {
21124
+ "report_type": {
21125
+ "$ref": "#/components/schemas/SupportReportType"
21126
+ },
21127
+ "description": {
21128
+ "type": "string",
21129
+ "minLength": 1,
21130
+ "maxLength": 8000
21131
+ },
21132
+ "submission_id": {
21133
+ "type": "string",
21134
+ "minLength": 8,
21135
+ "maxLength": 128,
21136
+ "pattern": "^[A-Za-z0-9._-]+$",
21137
+ "description": "Client-generated id for correlating the request"
21138
+ },
21139
+ "site_id": {
21140
+ "$ref": "#/components/schemas/EntityId"
21141
+ },
21142
+ "device_id": {
21143
+ "$ref": "#/components/schemas/EntityId"
21144
+ },
21145
+ "clever_pv": {
21146
+ "$ref": "#/components/schemas/CleverPvContext"
21147
+ },
21148
+ "attachments": {
21149
+ "type": "array",
21150
+ "maxItems": 5,
21151
+ "items": {
21152
+ "$ref": "#/components/schemas/SupportRequestAttachment"
21153
+ }
21154
+ }
21155
+ }
21156
+ },
21157
+ "SupportRequestResult": {
21158
+ "type": "object",
21159
+ "required": [
21160
+ "reference",
21161
+ "status"
21162
+ ],
21163
+ "properties": {
21164
+ "reference": {
21165
+ "type": "string",
21166
+ "example": "12345"
21167
+ },
21168
+ "status": {
21169
+ "type": "string",
21170
+ "example": "new"
21171
+ }
21172
+ }
19937
21173
  }
19938
21174
  }
19939
21175
  },