@epilot/customer-portal-client 0.50.0 → 0.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/openapi.json CHANGED
@@ -55,6 +55,7 @@
55
55
  "/v2/portal/portal": {
56
56
  "post": {
57
57
  "operationId": "upsertPortal",
58
+ "deprecated": true,
58
59
  "summary": "upsertPortal",
59
60
  "description": "Upserts the settings for a portal of an organization.",
60
61
  "security": [
@@ -135,6 +136,9 @@
135
136
  "/v2/portal/public/user": {
136
137
  "post": {
137
138
  "operationId": "createUser",
139
+ "x-epilot-agent": {
140
+ "expose": false
141
+ },
138
142
  "summary": "createUser",
139
143
  "description": "Registers a portal user",
140
144
  "deprecated": true,
@@ -199,6 +203,9 @@
199
203
  "/v3/portal/public/user": {
200
204
  "post": {
201
205
  "operationId": "createUserV3",
206
+ "x-epilot-agent": {
207
+ "expose": false
208
+ },
202
209
  "summary": "createUserV3",
203
210
  "description": "Registers a portal user.\nSupports two identification methods:\n1. Using portal_id\n2. Using domain\n",
204
211
  "security": [],
@@ -271,6 +278,10 @@
271
278
  "/v2/portal/token/validate": {
272
279
  "post": {
273
280
  "operationId": "validateToken",
281
+ "x-epilot-agent": {
282
+ "expose": false
283
+ },
284
+ "x-portal-preview-read": true,
274
285
  "summary": "validateToken",
275
286
  "description": "Validates Portal Token is valid. Pass the token via Authorization Header.",
276
287
  "tags": [
@@ -298,6 +309,10 @@
298
309
  "/v2/portal/token/revoke": {
299
310
  "post": {
300
311
  "operationId": "revokeToken",
312
+ "x-epilot-agent": {
313
+ "expose": false
314
+ },
315
+ "x-portal-preview-read": true,
301
316
  "summary": "revokeToken",
302
317
  "description": "Revokes all of the access tokens for the given Refresh Token.",
303
318
  "tags": [
@@ -398,6 +413,7 @@
398
413
  "/v2/portal/config": {
399
414
  "get": {
400
415
  "operationId": "getPortalConfig",
416
+ "deprecated": true,
401
417
  "summary": "getPortalConfig",
402
418
  "description": "Retrieves the portal configuration.",
403
419
  "tags": [
@@ -444,6 +460,7 @@
444
460
  },
445
461
  "delete": {
446
462
  "operationId": "deletePortal",
463
+ "deprecated": true,
447
464
  "summary": "deletePortal",
448
465
  "description": "Deletes the portal.",
449
466
  "tags": [
@@ -484,6 +501,7 @@
484
501
  "/v1/portal/exports": {
485
502
  "post": {
486
503
  "operationId": "createExport",
504
+ "x-portal-preview-read": true,
487
505
  "summary": "createExport",
488
506
  "description": "Request an asynchronous CSV export of the portal user's entities for one schema. Columns come from the request Returns a job id to poll.\n",
489
507
  "tags": [
@@ -667,6 +685,7 @@
667
685
  "/v2/portal/extensions": {
668
686
  "get": {
669
687
  "operationId": "getPortalExtensions",
688
+ "deprecated": true,
670
689
  "summary": "getPortalExtensions",
671
690
  "description": "Retrieves the installed portal extensions.",
672
691
  "tags": [
@@ -717,6 +736,7 @@
717
736
  "/v2/portal/public/extensions": {
718
737
  "get": {
719
738
  "operationId": "getPublicPortalExtensionDetails",
739
+ "deprecated": true,
720
740
  "summary": "getPublicPortalExtensionDetails",
721
741
  "description": "Get public extension details shown to end customers and configuring users.",
722
742
  "tags": [
@@ -1074,6 +1094,7 @@
1074
1094
  "/v2/portal/visualization:export": {
1075
1095
  "post": {
1076
1096
  "operationId": "prepareVisualizationExport",
1097
+ "x-portal-preview-read": true,
1077
1098
  "summary": "prepareVisualizationExport",
1078
1099
  "description": "Asks an installed App to prepare a downloadable export of a visualization (consumption chart, dynamic tariff chart, etc.). The export is produced by the third-party App via a configured portal extension hook of type `dataExport` — this endpoint does not generate the file itself, it forwards the request to the configured hook and returns the descriptor the App provides (typically a `download_url`).\n",
1079
1100
  "tags": [
@@ -1652,6 +1673,7 @@
1652
1673
  "/v2/portal/external-links": {
1653
1674
  "get": {
1654
1675
  "operationId": "getExternalLinks",
1676
+ "deprecated": true,
1655
1677
  "summary": "getExternalLinks",
1656
1678
  "description": "Retrieves the portal configuration external links.",
1657
1679
  "tags": [
@@ -1986,6 +2008,7 @@
1986
2008
  "/v2/portal/public/portal/config": {
1987
2009
  "get": {
1988
2010
  "operationId": "getPublicPortalConfig",
2011
+ "deprecated": true,
1989
2012
  "summary": "getPublicPortalConfig",
1990
2013
  "description": "Retrieves the public portal configuration.",
1991
2014
  "tags": [
@@ -2033,6 +2056,7 @@
2033
2056
  "/v2/portal/org/portal/config": {
2034
2057
  "get": {
2035
2058
  "operationId": "getOrgPortalConfig",
2059
+ "deprecated": true,
2036
2060
  "summary": "getOrgPortalConfig",
2037
2061
  "description": "Retrieves the portal configuration for the organization.",
2038
2062
  "tags": [
@@ -2314,6 +2338,7 @@
2314
2338
  "/v2/portal/email-templates": {
2315
2339
  "post": {
2316
2340
  "operationId": "upsertEmailTemplates",
2341
+ "deprecated": true,
2317
2342
  "summary": "upsertEmailTemplates",
2318
2343
  "description": "Upserts the email templates of a portal",
2319
2344
  "tags": [
@@ -2385,6 +2410,7 @@
2385
2410
  },
2386
2411
  "get": {
2387
2412
  "operationId": "getEmailTemplates",
2413
+ "deprecated": true,
2388
2414
  "summary": "getEmailTemplates",
2389
2415
  "description": "Retrieves the email templates of a portal",
2390
2416
  "tags": [
@@ -2433,7 +2459,7 @@
2433
2459
  "post": {
2434
2460
  "operationId": "migrateEmailTemplateReferences",
2435
2461
  "summary": "migrateEmailTemplateReferences",
2436
- "description": "Walk every email-template config row in the caller's org and re-point any\nfield on `email_templates` that currently references `source_template_id`\nat `destination_template_id`. Intended to be called from the email-template\nmigration flow when a duplicated template is refined.\n\nOnly v3-shaped rows are migrated (those carrying `portal_sk_v3`). Returns\nthe portal IDs that were rewritten and any whose update failed.\n",
2462
+ "description": "Walk every email-template config row in the caller's org and re-point any\nfield on `email_templates` that currently references `source_template_id`\nat `destination_template_id`. Intended to be called from the email-template\nmigration flow when a duplicated template is refined.\n\nRows are keyed by portal id (`portal_sk_v3`); a row without one is skipped.\nReturns the portal IDs that were rewritten and any whose update failed.\n",
2437
2463
  "tags": [
2438
2464
  "ECP Admin"
2439
2465
  ],
@@ -3586,6 +3612,7 @@
3586
3612
  "/v2/portal/validate/caa-records": {
3587
3613
  "post": {
3588
3614
  "operationId": "validateCaaRecords",
3615
+ "deprecated": true,
3589
3616
  "summary": "validateCaaRecords",
3590
3617
  "description": "Validates the CAA records of a portal",
3591
3618
  "tags": [
@@ -3763,7 +3790,7 @@
3763
3790
  "patch": {
3764
3791
  "operationId": "updateContact",
3765
3792
  "summary": "updateContact",
3766
- "description": "Updates the contact details.",
3793
+ "description": "Updates the portal user's contact. Only the attributes in `PortalContactUpdate` can be changed;\nother attributes are ignored. Returns 403 when the body has none of them.\n",
3767
3794
  "deprecated": true,
3768
3795
  "tags": [
3769
3796
  "ECP"
@@ -3777,7 +3804,7 @@
3777
3804
  "content": {
3778
3805
  "application/json": {
3779
3806
  "schema": {
3780
- "$ref": "#/components/schemas/Entity"
3807
+ "$ref": "#/components/schemas/PortalContactUpdate"
3781
3808
  }
3782
3809
  }
3783
3810
  }
@@ -3870,6 +3897,7 @@
3870
3897
  "/v2/portal/public/contact/exists": {
3871
3898
  "post": {
3872
3899
  "operationId": "checkContactExists",
3900
+ "deprecated": true,
3873
3901
  "summary": "checkContactExists",
3874
3902
  "description": "True if contact with given identifiers exists.",
3875
3903
  "security": [],
@@ -4011,17 +4039,6 @@
4011
4039
  "NOT_FOUND"
4012
4040
  ],
4013
4041
  "description": "Present only when exists is false. NOT_FOUND means the given identifiers did\nnot match any contact (definitive - the client should not retry). TIMEOUT\nmeans the contact was not found within the processing window but may still be\ningesting; the client may retry (ideally with trigger_identifiers_check=false).\n"
4014
- },
4015
- "correlation_id": {
4016
- "type": "string",
4017
- "minLength": 1,
4018
- "maxLength": 255,
4019
- "pattern": "^[A-Za-z0-9_-]+$",
4020
- "description": "Correlation id of the triggered ERP sync. Present only for portals waiting on\nthe Integration Toolkit (`wait_for_toolkit`). Send it back with\n`trigger_identifiers_check: false` to keep waiting on the same correlation.\n"
4021
- },
4022
- "message": {
4023
- "type": "string",
4024
- "description": "Human-readable reason the identifiers did not resolve, when the portal knows\none. Set when the ERP finished sending for this correlation without the\nentities the portal needs (e.g. \"account data arrived, no contract\"), so\nsupport can tell a silent timeout from an incomplete delivery.\n"
4025
4042
  }
4026
4043
  }
4027
4044
  }
@@ -4034,9 +4051,6 @@
4034
4051
  "404": {
4035
4052
  "$ref": "#/components/responses/NotFound"
4036
4053
  },
4037
- "409": {
4038
- "$ref": "#/components/responses/ContactAmbiguousMatch"
4039
- },
4040
4054
  "429": {
4041
4055
  "$ref": "#/components/responses/TooManyRequests"
4042
4056
  },
@@ -4088,7 +4102,7 @@
4088
4102
  },
4089
4103
  "responses": {
4090
4104
  "200": {
4091
- "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. The one\naddition is `correlation_id` on a TIMEOUT from a portal waiting on the Integration\nToolkit, which a retry needs; what the ERP did or did not send is never part of it.\nThat includes an ERP delivery of several matching contacts: unlike\n`checkContactExistsV3`, which answers it with 409 `AMBIGUOUS_MATCH`, this endpoint\nanswers it as `NOT_FOUND`, because the 409 would confirm that the ERP knows the\nidentifiers.\n",
4105
+ "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",
4092
4106
  "content": {
4093
4107
  "application/json": {
4094
4108
  "schema": {
@@ -4248,11 +4262,11 @@
4248
4262
  }
4249
4263
  }
4250
4264
  },
4251
- "/v2/portal/user": {
4265
+ "/v2/portal/copilot/access": {
4252
4266
  "get": {
4253
- "operationId": "getPortalUser",
4254
- "summary": "getPortalUser",
4255
- "description": "Get the portal user details",
4267
+ "operationId": "getPortalCopilotAccess",
4268
+ "summary": "getPortalCopilotAccess",
4269
+ "description": "Whether the signed-in portal user may use the portal chat. False when the chat is off for the portal, when the user does not match its target visibility, or when targets cannot be matched.",
4256
4270
  "tags": [
4257
4271
  "ECP"
4258
4272
  ],
@@ -4263,14 +4277,17 @@
4263
4277
  ],
4264
4278
  "responses": {
4265
4279
  "200": {
4266
- "description": "Portal user returned successfully.",
4280
+ "description": "Portal chat access of the portal user.",
4267
4281
  "content": {
4268
4282
  "application/json": {
4269
4283
  "schema": {
4270
4284
  "type": "object",
4285
+ "required": [
4286
+ "allowed"
4287
+ ],
4271
4288
  "properties": {
4272
- "data": {
4273
- "$ref": "#/components/schemas/PortalUser"
4289
+ "allowed": {
4290
+ "type": "boolean"
4274
4291
  }
4275
4292
  }
4276
4293
  }
@@ -4284,11 +4301,13 @@
4284
4301
  "$ref": "#/components/responses/InternalServerError"
4285
4302
  }
4286
4303
  }
4287
- },
4288
- "patch": {
4289
- "operationId": "updatePortalUser",
4290
- "summary": "updatePortalUser",
4291
- "description": "Update the portal user details",
4304
+ }
4305
+ },
4306
+ "/v2/portal/user": {
4307
+ "get": {
4308
+ "operationId": "getPortalUser",
4309
+ "summary": "getPortalUser",
4310
+ "description": "Get the portal user details",
4292
4311
  "tags": [
4293
4312
  "ECP"
4294
4313
  ],
@@ -4297,18 +4316,9 @@
4297
4316
  "PortalAuth": []
4298
4317
  }
4299
4318
  ],
4300
- "requestBody": {
4301
- "content": {
4302
- "application/json": {
4303
- "schema": {
4304
- "$ref": "#/components/schemas/Entity"
4305
- }
4306
- }
4307
- }
4308
- },
4309
4319
  "responses": {
4310
4320
  "200": {
4311
- "description": "The portal user updated successfully.",
4321
+ "description": "Portal user returned successfully.",
4312
4322
  "content": {
4313
4323
  "application/json": {
4314
4324
  "schema": {
@@ -4375,6 +4385,9 @@
4375
4385
  "/v2/portal/user/update/email": {
4376
4386
  "put": {
4377
4387
  "operationId": "updatePortalUserEmail",
4388
+ "x-epilot-agent": {
4389
+ "expose": false
4390
+ },
4378
4391
  "summary": "updatePortalUserEmail",
4379
4392
  "description": "Update portal user email",
4380
4393
  "tags": [
@@ -4443,6 +4456,9 @@
4443
4456
  "/v2/portal/user/change/password": {
4444
4457
  "put": {
4445
4458
  "operationId": "changePortalUserPassword",
4459
+ "x-epilot-agent": {
4460
+ "expose": false
4461
+ },
4446
4462
  "summary": "changePortalUserPassword",
4447
4463
  "description": "Hand over a password change to the third-party system configured via the `changePassword` portal extension hook.\nOnly available when such a hook is configured for the portal; the built-in password change flow does not use this endpoint.\n",
4448
4464
  "tags": [
@@ -4501,45 +4517,80 @@
4501
4517
  }
4502
4518
  }
4503
4519
  },
4504
- "/v2/portal/user/resend/confirmation-email/{id}": {
4505
- "post": {
4506
- "operationId": "resendConfirmationEmail",
4507
- "summary": "resendConfirmationEmail",
4508
- "description": "Resend confirmation email",
4520
+ "/v2/portal/user/notification-preferences": {
4521
+ "get": {
4522
+ "operationId": "getPortalUserNotificationPreferences",
4523
+ "summary": "getPortalUserNotificationPreferences",
4524
+ "description": "Returns the notification preferences of the authenticated portal user. Notifications follow an opt-out model: a preference that was never stored on the portal user defaults to enabled (true). Responds 404 while the engagement-center v2 feature is not enabled for the organization.",
4509
4525
  "tags": [
4510
- "ECP Admin"
4526
+ "ECP"
4511
4527
  ],
4512
4528
  "security": [
4513
4529
  {
4514
- "EpilotAuth": []
4530
+ "PortalAuth": []
4515
4531
  }
4516
4532
  ],
4517
- "parameters": [
4533
+ "responses": {
4534
+ "200": {
4535
+ "description": "The resolved notification preferences of the authenticated portal user.",
4536
+ "content": {
4537
+ "application/json": {
4538
+ "schema": {
4539
+ "$ref": "#/components/schemas/PortalUserNotificationPreferences"
4540
+ }
4541
+ }
4542
+ }
4543
+ },
4544
+ "401": {
4545
+ "$ref": "#/components/responses/Unauthorized"
4546
+ },
4547
+ "403": {
4548
+ "$ref": "#/components/responses/Forbidden"
4549
+ },
4550
+ "404": {
4551
+ "$ref": "#/components/responses/NotFound"
4552
+ },
4553
+ "500": {
4554
+ "$ref": "#/components/responses/InternalServerError"
4555
+ }
4556
+ }
4557
+ },
4558
+ "put": {
4559
+ "operationId": "updatePortalUserNotificationPreferences",
4560
+ "summary": "updatePortalUserNotificationPreferences",
4561
+ "description": "Partially updates the notification preferences of the authenticated portal user. Only the channels present in the request body are changed; omitted channels keep their stored value (or the enabled-by-default behaviour when never set). Returns the resulting full preferences. Responds 404 while the engagement-center v2 feature is not enabled for the organization.",
4562
+ "tags": [
4563
+ "ECP"
4564
+ ],
4565
+ "security": [
4518
4566
  {
4519
- "in": "path",
4520
- "name": "id",
4521
- "schema": {
4522
- "$ref": "#/components/schemas/EntityId"
4523
- },
4524
- "required": true,
4525
- "description": "The ID of portal user id"
4567
+ "PortalAuth": []
4526
4568
  }
4527
4569
  ],
4570
+ "requestBody": {
4571
+ "required": true,
4572
+ "content": {
4573
+ "application/json": {
4574
+ "schema": {
4575
+ "type": "object",
4576
+ "description": "Channels to update. Omitted channels are left unchanged.",
4577
+ "properties": {
4578
+ "in_app": {
4579
+ "type": "boolean",
4580
+ "description": "Whether the portal user wants to receive in-app notifications."
4581
+ }
4582
+ }
4583
+ }
4584
+ }
4585
+ }
4586
+ },
4528
4587
  "responses": {
4529
4588
  "200": {
4530
- "description": "Confirmation email resent successfully for the portal user.",
4589
+ "description": "The resulting full notification preferences after the update.",
4531
4590
  "content": {
4532
4591
  "application/json": {
4533
4592
  "schema": {
4534
- "type": "object",
4535
- "properties": {
4536
- "message": {
4537
- "type": "string",
4538
- "enum": [
4539
- "Confirmation email sent successfully."
4540
- ]
4541
- }
4542
- }
4593
+ "$ref": "#/components/schemas/PortalUserNotificationPreferences"
4543
4594
  }
4544
4595
  }
4545
4596
  }
@@ -4550,6 +4601,9 @@
4550
4601
  "401": {
4551
4602
  "$ref": "#/components/responses/Unauthorized"
4552
4603
  },
4604
+ "403": {
4605
+ "$ref": "#/components/responses/Forbidden"
4606
+ },
4553
4607
  "404": {
4554
4608
  "$ref": "#/components/responses/NotFound"
4555
4609
  },
@@ -4559,49 +4613,31 @@
4559
4613
  }
4560
4614
  }
4561
4615
  },
4562
- "/v2/portal/users/by-related-entity": {
4616
+ "/v2/portal/user/passkeys": {
4563
4617
  "get": {
4564
- "operationId": "fetchPortalUsersByRelatedEntity",
4565
- "summary": "fetchPortalUsersByRelatedEntity",
4566
- "description": "Get all users for a given entity",
4618
+ "operationId": "listPortalUserPasskeys",
4619
+ "summary": "listPortalUserPasskeys",
4620
+ "description": "Lists the passkeys the logged-in portal user has added.",
4567
4621
  "tags": [
4568
- "ECP Admin"
4622
+ "ECP"
4569
4623
  ],
4570
4624
  "security": [
4571
4625
  {
4572
- "EpilotAuth": []
4573
- }
4574
- ],
4575
- "parameters": [
4576
- {
4577
- "in": "query",
4578
- "name": "entity_id",
4579
- "required": true,
4580
- "schema": {
4581
- "$ref": "#/components/schemas/EntityId"
4582
- }
4583
- },
4584
- {
4585
- "in": "query",
4586
- "name": "slug",
4587
- "required": true,
4588
- "schema": {
4589
- "$ref": "#/components/schemas/EntitySlug"
4590
- }
4626
+ "PortalAuth": []
4591
4627
  }
4592
4628
  ],
4593
4629
  "responses": {
4594
4630
  "200": {
4595
- "description": "Returns the portal users under the given entity.",
4631
+ "description": "The user's passkeys",
4596
4632
  "content": {
4597
4633
  "application/json": {
4598
4634
  "schema": {
4599
4635
  "type": "object",
4600
4636
  "properties": {
4601
- "portalUsers": {
4637
+ "results": {
4602
4638
  "type": "array",
4603
4639
  "items": {
4604
- "$ref": "#/components/schemas/PortalUser"
4640
+ "$ref": "#/components/schemas/PortalUserPasskey"
4605
4641
  }
4606
4642
  }
4607
4643
  }
@@ -4619,66 +4655,63 @@
4619
4655
  "$ref": "#/components/responses/InternalServerError"
4620
4656
  }
4621
4657
  }
4622
- }
4623
- },
4624
- "/v2/portal/user/confirm": {
4625
- "get": {
4626
- "operationId": "confirmUser",
4627
- "summary": "confirmUser",
4628
- "description": "Confirm a portal user",
4658
+ },
4659
+ "post": {
4660
+ "operationId": "addPortalUserPasskey",
4661
+ "summary": "addPortalUserPasskey",
4662
+ "description": "Stores a new passkey for the logged-in portal user.\nSend the browser's `navigator.credentials.create()` result for options from `createPortalUserPasskeyRegistrationOptions`.\nOnly available when `auth_settings.passkeys.enabled` is set for the portal.\n",
4629
4663
  "tags": [
4630
- "Public"
4664
+ "ECP"
4631
4665
  ],
4632
- "security": [],
4633
- "parameters": [
4634
- {
4635
- "in": "query",
4636
- "name": "confirmation_link_token",
4637
- "required": true,
4638
- "schema": {
4639
- "type": "string",
4640
- "description": "Confirmation link token"
4641
- }
4642
- },
4666
+ "security": [
4643
4667
  {
4644
- "in": "query",
4645
- "name": "use_redirect",
4646
- "required": false,
4647
- "schema": {
4648
- "type": "boolean",
4649
- "description": "Should the operation result in a 301 redirect",
4650
- "default": true
4651
- }
4668
+ "PortalAuth": []
4652
4669
  }
4653
4670
  ],
4671
+ "requestBody": {
4672
+ "required": true,
4673
+ "content": {
4674
+ "application/json": {
4675
+ "schema": {
4676
+ "type": "object",
4677
+ "required": [
4678
+ "registration_response"
4679
+ ],
4680
+ "properties": {
4681
+ "registration_response": {
4682
+ "type": "object",
4683
+ "additionalProperties": true,
4684
+ "description": "WebAuthn RegistrationResponseJSON from the browser"
4685
+ },
4686
+ "name": {
4687
+ "type": "string",
4688
+ "maxLength": 64,
4689
+ "description": "Label shown to the user, e.g. \"iPhone\""
4690
+ }
4691
+ }
4692
+ }
4693
+ }
4694
+ }
4695
+ },
4654
4696
  "responses": {
4655
- "200": {
4656
- "description": "Returned in case of successful user confirmation",
4697
+ "201": {
4698
+ "description": "The passkey was added",
4657
4699
  "content": {
4658
4700
  "application/json": {
4659
4701
  "schema": {
4660
- "type": "object",
4661
- "properties": {
4662
- "confirmed": {
4663
- "type": "boolean",
4664
- "description": "Is the user confirmed",
4665
- "example": true
4666
- },
4667
- "user_already_confirmed": {
4668
- "type": "boolean",
4669
- "description": "Is the user previously confirmed",
4670
- "example": true
4671
- }
4672
- }
4702
+ "$ref": "#/components/schemas/PortalUserPasskey"
4673
4703
  }
4674
4704
  }
4675
4705
  }
4676
4706
  },
4677
- "301": {
4678
- "description": "User is confirmed and redirected to the portal successfully."
4679
- },
4680
4707
  "400": {
4681
- "$ref": "#/components/responses/ConfirmUserInvalidRequest"
4708
+ "$ref": "#/components/responses/InvalidRequest"
4709
+ },
4710
+ "401": {
4711
+ "$ref": "#/components/responses/Unauthorized"
4712
+ },
4713
+ "403": {
4714
+ "$ref": "#/components/responses/Forbidden"
4682
4715
  },
4683
4716
  "500": {
4684
4717
  "$ref": "#/components/responses/InternalServerError"
@@ -4686,24 +4719,339 @@
4686
4719
  }
4687
4720
  }
4688
4721
  },
4689
- "/v2/portal/user/confirm/{id}": {
4690
- "get": {
4691
- "operationId": "confirmUserWithUserId",
4692
- "summary": "confirmUserWithUserId",
4693
- "description": "Confirm a portal user",
4722
+ "/v2/portal/user/passkeys/registration-options": {
4723
+ "post": {
4724
+ "operationId": "createPortalUserPasskeyRegistrationOptions",
4725
+ "summary": "createPortalUserPasskeyRegistrationOptions",
4726
+ "description": "Returns WebAuthn options for `navigator.credentials.create()` to add a passkey for the logged-in portal user.\nNeeds the code from `sendPortalUserPasskeyVerificationCode`, so a session alone can't add a passkey; the code is used up here.\nThe passkey is bound to the domain in the request's `Origin` header, which must be one of the portal's domains.\nOnly available when `auth_settings.passkeys.enabled` is set for the portal.\n",
4694
4727
  "tags": [
4695
- "Public"
4728
+ "ECP"
4696
4729
  ],
4697
- "security": [],
4698
- "parameters": [
4730
+ "security": [
4699
4731
  {
4700
- "in": "query",
4701
- "name": "origin",
4702
- "required": true,
4703
- "schema": {
4704
- "$ref": "#/components/schemas/Origin"
4705
- }
4706
- },
4732
+ "PortalAuth": []
4733
+ }
4734
+ ],
4735
+ "requestBody": {
4736
+ "required": true,
4737
+ "content": {
4738
+ "application/json": {
4739
+ "schema": {
4740
+ "type": "object",
4741
+ "required": [
4742
+ "verification_code"
4743
+ ],
4744
+ "properties": {
4745
+ "verification_code": {
4746
+ "type": "string",
4747
+ "description": "The 6-digit code sent to the user's email address"
4748
+ }
4749
+ }
4750
+ }
4751
+ }
4752
+ }
4753
+ },
4754
+ "responses": {
4755
+ "200": {
4756
+ "description": "WebAuthn PublicKeyCredentialCreationOptionsJSON, valid for 5 minutes",
4757
+ "content": {
4758
+ "application/json": {
4759
+ "schema": {
4760
+ "type": "object",
4761
+ "additionalProperties": true
4762
+ }
4763
+ }
4764
+ }
4765
+ },
4766
+ "400": {
4767
+ "$ref": "#/components/responses/InvalidRequest"
4768
+ },
4769
+ "401": {
4770
+ "$ref": "#/components/responses/Unauthorized"
4771
+ },
4772
+ "403": {
4773
+ "$ref": "#/components/responses/Forbidden"
4774
+ },
4775
+ "500": {
4776
+ "$ref": "#/components/responses/InternalServerError"
4777
+ }
4778
+ }
4779
+ }
4780
+ },
4781
+ "/v2/portal/user/passkeys/verification-code": {
4782
+ "post": {
4783
+ "operationId": "sendPortalUserPasskeyVerificationCode",
4784
+ "summary": "sendPortalUserPasskeyVerificationCode",
4785
+ "description": "Emails the logged-in portal user a 6-digit code needed to add a passkey (`createPortalUserPasskeyRegistrationOptions`).\nThe code is valid for 10 minutes and allows 5 attempts; a new code replaces the previous one and can be requested once a minute.\nOnly available when `auth_settings.passkeys.enabled` is set for the portal.\n",
4786
+ "tags": [
4787
+ "ECP"
4788
+ ],
4789
+ "security": [
4790
+ {
4791
+ "PortalAuth": []
4792
+ }
4793
+ ],
4794
+ "responses": {
4795
+ "204": {
4796
+ "description": "The code was sent"
4797
+ },
4798
+ "401": {
4799
+ "$ref": "#/components/responses/Unauthorized"
4800
+ },
4801
+ "403": {
4802
+ "$ref": "#/components/responses/Forbidden"
4803
+ },
4804
+ "429": {
4805
+ "$ref": "#/components/responses/TooManyRequests"
4806
+ },
4807
+ "500": {
4808
+ "$ref": "#/components/responses/InternalServerError"
4809
+ }
4810
+ }
4811
+ }
4812
+ },
4813
+ "/v2/portal/user/passkeys/{credential_id}": {
4814
+ "delete": {
4815
+ "operationId": "deletePortalUserPasskey",
4816
+ "summary": "deletePortalUserPasskey",
4817
+ "description": "Removes one of the logged-in portal user's passkeys. Works even when passkeys are turned off for the portal.",
4818
+ "tags": [
4819
+ "ECP"
4820
+ ],
4821
+ "security": [
4822
+ {
4823
+ "PortalAuth": []
4824
+ }
4825
+ ],
4826
+ "parameters": [
4827
+ {
4828
+ "name": "credential_id",
4829
+ "in": "path",
4830
+ "required": true,
4831
+ "schema": {
4832
+ "type": "string"
4833
+ }
4834
+ }
4835
+ ],
4836
+ "responses": {
4837
+ "204": {
4838
+ "description": "The passkey was removed"
4839
+ },
4840
+ "401": {
4841
+ "$ref": "#/components/responses/Unauthorized"
4842
+ },
4843
+ "404": {
4844
+ "$ref": "#/components/responses/NotFound"
4845
+ },
4846
+ "500": {
4847
+ "$ref": "#/components/responses/InternalServerError"
4848
+ }
4849
+ }
4850
+ }
4851
+ },
4852
+ "/v2/portal/user/resend/confirmation-email/{id}": {
4853
+ "post": {
4854
+ "operationId": "resendConfirmationEmail",
4855
+ "summary": "resendConfirmationEmail",
4856
+ "description": "Resend confirmation email",
4857
+ "tags": [
4858
+ "ECP Admin"
4859
+ ],
4860
+ "security": [
4861
+ {
4862
+ "EpilotAuth": []
4863
+ }
4864
+ ],
4865
+ "parameters": [
4866
+ {
4867
+ "in": "path",
4868
+ "name": "id",
4869
+ "schema": {
4870
+ "$ref": "#/components/schemas/EntityId"
4871
+ },
4872
+ "required": true,
4873
+ "description": "The ID of portal user id"
4874
+ }
4875
+ ],
4876
+ "responses": {
4877
+ "200": {
4878
+ "description": "Confirmation email resent successfully for the portal user.",
4879
+ "content": {
4880
+ "application/json": {
4881
+ "schema": {
4882
+ "type": "object",
4883
+ "properties": {
4884
+ "message": {
4885
+ "type": "string",
4886
+ "enum": [
4887
+ "Confirmation email sent successfully."
4888
+ ]
4889
+ }
4890
+ }
4891
+ }
4892
+ }
4893
+ }
4894
+ },
4895
+ "400": {
4896
+ "$ref": "#/components/responses/InvalidRequest"
4897
+ },
4898
+ "401": {
4899
+ "$ref": "#/components/responses/Unauthorized"
4900
+ },
4901
+ "404": {
4902
+ "$ref": "#/components/responses/NotFound"
4903
+ },
4904
+ "500": {
4905
+ "$ref": "#/components/responses/InternalServerError"
4906
+ }
4907
+ }
4908
+ }
4909
+ },
4910
+ "/v2/portal/users/by-related-entity": {
4911
+ "get": {
4912
+ "operationId": "fetchPortalUsersByRelatedEntity",
4913
+ "summary": "fetchPortalUsersByRelatedEntity",
4914
+ "description": "Get all users for a given entity",
4915
+ "tags": [
4916
+ "ECP Admin"
4917
+ ],
4918
+ "security": [
4919
+ {
4920
+ "EpilotAuth": []
4921
+ }
4922
+ ],
4923
+ "parameters": [
4924
+ {
4925
+ "in": "query",
4926
+ "name": "entity_id",
4927
+ "required": true,
4928
+ "schema": {
4929
+ "$ref": "#/components/schemas/EntityId"
4930
+ }
4931
+ },
4932
+ {
4933
+ "in": "query",
4934
+ "name": "slug",
4935
+ "required": true,
4936
+ "schema": {
4937
+ "$ref": "#/components/schemas/EntitySlug"
4938
+ }
4939
+ }
4940
+ ],
4941
+ "responses": {
4942
+ "200": {
4943
+ "description": "Returns the portal users under the given entity.",
4944
+ "content": {
4945
+ "application/json": {
4946
+ "schema": {
4947
+ "type": "object",
4948
+ "properties": {
4949
+ "portalUsers": {
4950
+ "type": "array",
4951
+ "items": {
4952
+ "$ref": "#/components/schemas/PortalUser"
4953
+ }
4954
+ }
4955
+ }
4956
+ }
4957
+ }
4958
+ }
4959
+ },
4960
+ "401": {
4961
+ "$ref": "#/components/responses/Unauthorized"
4962
+ },
4963
+ "403": {
4964
+ "$ref": "#/components/responses/Forbidden"
4965
+ },
4966
+ "500": {
4967
+ "$ref": "#/components/responses/InternalServerError"
4968
+ }
4969
+ }
4970
+ }
4971
+ },
4972
+ "/v2/portal/user/confirm": {
4973
+ "get": {
4974
+ "operationId": "confirmUser",
4975
+ "summary": "confirmUser",
4976
+ "description": "Confirm a portal user",
4977
+ "tags": [
4978
+ "Public"
4979
+ ],
4980
+ "security": [],
4981
+ "parameters": [
4982
+ {
4983
+ "in": "query",
4984
+ "name": "confirmation_link_token",
4985
+ "required": true,
4986
+ "schema": {
4987
+ "type": "string",
4988
+ "description": "Confirmation link token"
4989
+ }
4990
+ },
4991
+ {
4992
+ "in": "query",
4993
+ "name": "use_redirect",
4994
+ "required": false,
4995
+ "schema": {
4996
+ "type": "boolean",
4997
+ "description": "Should the operation result in a 301 redirect",
4998
+ "default": true
4999
+ }
5000
+ }
5001
+ ],
5002
+ "responses": {
5003
+ "200": {
5004
+ "description": "Returned in case of successful user confirmation",
5005
+ "content": {
5006
+ "application/json": {
5007
+ "schema": {
5008
+ "type": "object",
5009
+ "properties": {
5010
+ "confirmed": {
5011
+ "type": "boolean",
5012
+ "description": "Is the user confirmed",
5013
+ "example": true
5014
+ },
5015
+ "user_already_confirmed": {
5016
+ "type": "boolean",
5017
+ "description": "Is the user previously confirmed",
5018
+ "example": true
5019
+ }
5020
+ }
5021
+ }
5022
+ }
5023
+ }
5024
+ },
5025
+ "301": {
5026
+ "description": "User is confirmed and redirected to the portal successfully."
5027
+ },
5028
+ "400": {
5029
+ "$ref": "#/components/responses/ConfirmUserInvalidRequest"
5030
+ },
5031
+ "500": {
5032
+ "$ref": "#/components/responses/InternalServerError"
5033
+ }
5034
+ }
5035
+ }
5036
+ },
5037
+ "/v2/portal/user/confirm/{id}": {
5038
+ "get": {
5039
+ "operationId": "confirmUserWithUserId",
5040
+ "summary": "confirmUserWithUserId",
5041
+ "description": "Confirm a portal user",
5042
+ "tags": [
5043
+ "Public"
5044
+ ],
5045
+ "security": [],
5046
+ "parameters": [
5047
+ {
5048
+ "in": "query",
5049
+ "name": "origin",
5050
+ "required": true,
5051
+ "schema": {
5052
+ "$ref": "#/components/schemas/Origin"
5053
+ }
5054
+ },
4707
5055
  {
4708
5056
  "in": "path",
4709
5057
  "name": "id",
@@ -4737,6 +5085,7 @@
4737
5085
  "/v2/portal/public/user/exists": {
4738
5086
  "get": {
4739
5087
  "operationId": "userExists",
5088
+ "deprecated": true,
4740
5089
  "summary": "userExists",
4741
5090
  "description": "Checks whether a user exists in the portal",
4742
5091
  "tags": [
@@ -4975,6 +5324,7 @@
4975
5324
  "/v2/portal/configure-distribution": {
4976
5325
  "get": {
4977
5326
  "operationId": "configureDistribution",
5327
+ "deprecated": true,
4978
5328
  "summary": "configureDistribution",
4979
5329
  "description": "Configure the distribution for the portal's custom domain",
4980
5330
  "tags": [
@@ -5321,69 +5671,6 @@
5321
5671
  "$ref": "#/components/responses/InternalServerError"
5322
5672
  }
5323
5673
  }
5324
- },
5325
- "patch": {
5326
- "operationId": "updateOrder",
5327
- "summary": "updateOrder",
5328
- "description": "Update an order by id. Use getPortalUserEntity instead.",
5329
- "deprecated": true,
5330
- "tags": [
5331
- "ECP"
5332
- ],
5333
- "security": [
5334
- {
5335
- "PortalAuth": []
5336
- }
5337
- ],
5338
- "parameters": [
5339
- {
5340
- "in": "path",
5341
- "name": "id",
5342
- "schema": {
5343
- "$ref": "#/components/schemas/EntityId"
5344
- },
5345
- "required": true,
5346
- "description": "The ID of order"
5347
- }
5348
- ],
5349
- "requestBody": {
5350
- "content": {
5351
- "application/json": {
5352
- "schema": {
5353
- "$ref": "#/components/schemas/Entity"
5354
- }
5355
- }
5356
- }
5357
- },
5358
- "responses": {
5359
- "200": {
5360
- "description": "Updated the order details successfully.",
5361
- "content": {
5362
- "application/json": {
5363
- "schema": {
5364
- "type": "object",
5365
- "properties": {
5366
- "data": {
5367
- "$ref": "#/components/schemas/Order"
5368
- }
5369
- }
5370
- }
5371
- }
5372
- }
5373
- },
5374
- "401": {
5375
- "$ref": "#/components/responses/Unauthorized"
5376
- },
5377
- "403": {
5378
- "$ref": "#/components/responses/Forbidden"
5379
- },
5380
- "404": {
5381
- "$ref": "#/components/responses/NotFound"
5382
- },
5383
- "500": {
5384
- "$ref": "#/components/responses/InternalServerError"
5385
- }
5386
- }
5387
5674
  }
5388
5675
  },
5389
5676
  "/v2/portal/opportunity": {
@@ -5545,6 +5832,7 @@
5545
5832
  "/v2/portal/opportunities/search": {
5546
5833
  "post": {
5547
5834
  "operationId": "getSearchResultsForOpportunities",
5835
+ "x-portal-preview-read": true,
5548
5836
  "summary": "getSearchResultsForOpportunities",
5549
5837
  "description": "Get all opportunity with the given searched attributes. Use searchPortalUserEntities instead.",
5550
5838
  "deprecated": true,
@@ -5740,71 +6028,6 @@
5740
6028
  "$ref": "#/components/responses/InternalServerError"
5741
6029
  }
5742
6030
  }
5743
- },
5744
- "patch": {
5745
- "operationId": "updateOpportunity",
5746
- "summary": "updateOpportunity",
5747
- "description": "Update an opportunity by id.",
5748
- "deprecated": true,
5749
- "tags": [
5750
- "ECP"
5751
- ],
5752
- "security": [
5753
- {
5754
- "PortalAuth": []
5755
- }
5756
- ],
5757
- "parameters": [
5758
- {
5759
- "in": "path",
5760
- "name": "id",
5761
- "schema": {
5762
- "$ref": "#/components/schemas/EntityId"
5763
- },
5764
- "required": true,
5765
- "description": "The ID of opportunity"
5766
- }
5767
- ],
5768
- "requestBody": {
5769
- "description": "Requested opportunity body to update",
5770
- "required": true,
5771
- "content": {
5772
- "application/json": {
5773
- "schema": {
5774
- "$ref": "#/components/schemas/Entity"
5775
- }
5776
- }
5777
- }
5778
- },
5779
- "responses": {
5780
- "200": {
5781
- "description": "Updated the opportunity successfully.",
5782
- "content": {
5783
- "application/json": {
5784
- "schema": {
5785
- "type": "object",
5786
- "properties": {
5787
- "data": {
5788
- "$ref": "#/components/schemas/Opportunity"
5789
- }
5790
- }
5791
- }
5792
- }
5793
- }
5794
- },
5795
- "401": {
5796
- "$ref": "#/components/responses/Unauthorized"
5797
- },
5798
- "403": {
5799
- "$ref": "#/components/responses/Forbidden"
5800
- },
5801
- "404": {
5802
- "$ref": "#/components/responses/NotFound"
5803
- },
5804
- "500": {
5805
- "$ref": "#/components/responses/InternalServerError"
5806
- }
5807
- }
5808
6031
  }
5809
6032
  },
5810
6033
  "/v2/portal/request": {
@@ -6083,7 +6306,7 @@
6083
6306
  "patch": {
6084
6307
  "operationId": "updateContract",
6085
6308
  "summary": "updateContract",
6086
- "description": "Update a contract by id. Use getPortalUserEntity instead.",
6309
+ "description": "Updates a contract of the portal user. Only the attributes in `PortalContractUpdate` can be changed;\nother attributes are ignored. Returns 403 when the body has none of them.\n",
6087
6310
  "deprecated": true,
6088
6311
  "tags": [
6089
6312
  "ECP"
@@ -6105,12 +6328,12 @@
6105
6328
  }
6106
6329
  ],
6107
6330
  "requestBody": {
6108
- "description": "Requested contract body to update",
6331
+ "description": "The contract attributes to change",
6109
6332
  "required": true,
6110
6333
  "content": {
6111
6334
  "application/json": {
6112
6335
  "schema": {
6113
- "$ref": "#/components/schemas/Entity"
6336
+ "$ref": "#/components/schemas/PortalContractUpdate"
6114
6337
  }
6115
6338
  }
6116
6339
  }
@@ -6149,6 +6372,7 @@
6149
6372
  "/v2/portal/contract/{id}/resolve-templates": {
6150
6373
  "post": {
6151
6374
  "operationId": "getContractWithTemplates",
6375
+ "x-portal-preview-read": true,
6152
6376
  "summary": "getContractWithTemplates",
6153
6377
  "description": "Resolve Handlebars templates against a contract's related meters and return the contract with templates_output populated per meter. This is a read-only operation — it does not modify the contract. It uses POST only so the templates can be passed in the request body. Prefer getPortalUserEntity / getContract for reads that don't need templates.",
6154
6378
  "tags": [
@@ -6276,7 +6500,7 @@
6276
6500
  "post": {
6277
6501
  "operationId": "addContractByIdentifiers",
6278
6502
  "summary": "addContractByIdentifiers",
6279
- "description": "Self-assign contract(s) by pre-configured identifiers.\n\nWhen the portal is configured to wait on the Integration Toolkit\n(`extension_hooks.contractIdentification.wait_for_toolkit`) and the ERP has not finished\nsending within the request budget, the call fails with 404 and `reason: TIMEOUT`, carrying\nthe `correlation_id` of the triggered sync. Repeating the call with that `correlation_id`\nkeeps waiting on the same correlation without calling the third party again.\n",
6503
+ "description": "Self-assign contract(s) by pre-configured identifiers.",
6280
6504
  "tags": [
6281
6505
  "ECP"
6282
6506
  ],
@@ -6285,20 +6509,6 @@
6285
6509
  "PortalAuth": []
6286
6510
  }
6287
6511
  ],
6288
- "parameters": [
6289
- {
6290
- "in": "query",
6291
- "name": "correlation_id",
6292
- "required": false,
6293
- "schema": {
6294
- "type": "string",
6295
- "minLength": 1,
6296
- "maxLength": 255,
6297
- "pattern": "^[A-Za-z0-9_-]+$"
6298
- },
6299
- "description": "Correlation id of an already triggered ERP sync, as returned by a previous call that\nfailed with `reason: TIMEOUT`. Keeps waiting on the same correlation instead of calling\nthe contract identification hook again. Ignored unless the portal is configured with\n`extension_hooks.contractIdentification.wait_for_toolkit`. At most 255 letters, digits,\nhyphens and underscores: the toolkit's bound for a correlation id and the charset it\nenforces on its identifiers.\n"
6300
- }
6301
- ],
6302
6512
  "requestBody": {
6303
6513
  "description": "Identifiers to search by. Have to be pre-configured in Portal Configuration Permissions.",
6304
6514
  "required": true,
@@ -6360,9 +6570,6 @@
6360
6570
  "403": {
6361
6571
  "$ref": "#/components/responses/Forbidden"
6362
6572
  },
6363
- "404": {
6364
- "$ref": "#/components/responses/ContractAssignmentNotFound"
6365
- },
6366
6573
  "409": {
6367
6574
  "$ref": "#/components/responses/ContractAssignmentConflict"
6368
6575
  },
@@ -6787,71 +6994,12 @@
6787
6994
  "$ref": "#/components/responses/InternalServerError"
6788
6995
  }
6789
6996
  }
6790
- },
6791
- "delete": {
6792
- "operationId": "deleteEntityFile",
6793
- "summary": "deleteEntityFile",
6794
- "description": "Delete files from an entity",
6795
- "tags": [
6796
- "ECP"
6797
- ],
6798
- "security": [
6799
- {
6800
- "PortalAuth": []
6801
- }
6802
- ],
6803
- "requestBody": {
6804
- "description": "Delete file",
6805
- "required": true,
6806
- "content": {
6807
- "application/json": {
6808
- "schema": {
6809
- "$ref": "#/components/schemas/DeleteEntityFile"
6810
- }
6811
- }
6812
- }
6813
- },
6814
- "responses": {
6815
- "202": {
6816
- "description": "The files are removed from the requested entity successfully.",
6817
- "content": {
6818
- "application/json": {
6819
- "schema": {
6820
- "type": "object",
6821
- "properties": {
6822
- "deletedFiles": {
6823
- "type": "array",
6824
- "items": {
6825
- "$ref": "#/components/schemas/EntityId",
6826
- "description": "File ID"
6827
- }
6828
- }
6829
- }
6830
- }
6831
- }
6832
- }
6833
- },
6834
- "400": {
6835
- "$ref": "#/components/responses/InvalidRequest"
6836
- },
6837
- "401": {
6838
- "$ref": "#/components/responses/Unauthorized"
6839
- },
6840
- "403": {
6841
- "$ref": "#/components/responses/Forbidden"
6842
- },
6843
- "404": {
6844
- "$ref": "#/components/responses/NotFound"
6845
- },
6846
- "500": {
6847
- "$ref": "#/components/responses/InternalServerError"
6848
- }
6849
- }
6850
6997
  }
6851
6998
  },
6852
6999
  "/v2/portal/portal/files": {
6853
7000
  "post": {
6854
7001
  "operationId": "savePortalFiles",
7002
+ "deprecated": true,
6855
7003
  "summary": "savePortalFiles",
6856
7004
  "description": "Add files to portal",
6857
7005
  "tags": [
@@ -7508,8 +7656,11 @@
7508
7656
  "/v2/portal/admin:login-as-user": {
7509
7657
  "post": {
7510
7658
  "operationId": "loginToPortalAsUser",
7659
+ "x-epilot-agent": {
7660
+ "expose": false
7661
+ },
7511
7662
  "summary": "loginToPortalAsUser",
7512
- "description": "Generate a token to log in to a portal impersonating a users.\n\nToken is valid for 5 minutes.\n\nSupports three identification methods (at least one required):\n1. Using portal_id (takes precedence)\n2. Using domain\n3. Using origin (legacy)\n",
7663
+ "description": "Generate a token to log in to a portal impersonating a users.\n\nToken is valid for 5 minutes.\n\nSupports three identification methods (at least one required):\n1. Using portal_id (takes precedence)\n2. Using domain\n3. Using origin (legacy)\n\nA partner user can log in as a portal user the vendor shared with their organization by sending\n`x-epilot-org-id: <vendor org id>` with a `portal_id`. This needs `portal:login_as_user` on that portal in the\npartner role and the portal user shared with the partner organization.\n",
7513
7664
  "tags": [
7514
7665
  "ECP Admin"
7515
7666
  ],
@@ -7558,6 +7709,11 @@
7558
7709
  "login_as_token": {
7559
7710
  "type": "string",
7560
7711
  "description": "A generated login_as_token to log in to a portal impersonating a user."
7712
+ },
7713
+ "portal_url": {
7714
+ "type": "string",
7715
+ "description": "URL of the portal on its epilot domain, to open `/login-as?token=<login_as_token>` on.",
7716
+ "example": "https://my-portal.ecp.epilot.io"
7561
7717
  }
7562
7718
  }
7563
7719
  }
@@ -7570,6 +7726,7 @@
7570
7726
  "/v2/portal/entity/{schema}/access": {
7571
7727
  "post": {
7572
7728
  "operationId": "triggerEntityAccessEvent",
7729
+ "deprecated": true,
7573
7730
  "summary": "triggerEntityAccessEvent",
7574
7731
  "description": "Trigger entity access event for a portal user",
7575
7732
  "tags": [
@@ -7706,6 +7863,7 @@
7706
7863
  "/v2/portal/entity:get": {
7707
7864
  "post": {
7708
7865
  "operationId": "getPortalUserEntity",
7866
+ "x-portal-preview-read": true,
7709
7867
  "summary": "getPortalUserEntity",
7710
7868
  "description": "Get a single entity for a portal user",
7711
7869
  "tags": [
@@ -7755,6 +7913,7 @@
7755
7913
  "/v2/portal/entity:search": {
7756
7914
  "post": {
7757
7915
  "operationId": "searchPortalUserEntities",
7916
+ "x-portal-preview-read": true,
7758
7917
  "summary": "searchPortalUserEntities",
7759
7918
  "description": "Search all entities of a portal user",
7760
7919
  "x-contact-identification-token": true,
@@ -8206,7 +8365,7 @@
8206
8365
  "get": {
8207
8366
  "operationId": "getOutstandingTasks",
8208
8367
  "summary": "Get outstanding workflow tasks for the portal user",
8209
- "description": "Returns all outstanding workflow journey tasks for the authenticated portal user, across their opportunity, order and contract entities. Each task is an active workflow step that exposes a journey the customer still needs to fill out.",
8368
+ "description": "Returns all outstanding workflow journey tasks for the authenticated portal user, across their opportunity, order and contract entities. Each task is an active workflow step that exposes a journey the customer still needs to fill out. A step whose start date still lies in the future is left out until that date.",
8210
8369
  "tags": [
8211
8370
  "ECP"
8212
8371
  ],
@@ -8326,6 +8485,7 @@
8326
8485
  "/v2/portal/entities/workflows/linearized/batch": {
8327
8486
  "post": {
8328
8487
  "operationId": "getEntityPortalWorkflowsBatch",
8488
+ "x-portal-preview-read": true,
8329
8489
  "summary": "getEntityPortalWorkflowsBatch",
8330
8490
  "description": "Batch variant of `getEntityPortalWorkflows`: returns portal-relevant workflows for\nup to 50 entities — schemas may differ — in a single call, each already linearized\nby the Workflows API into a flat, ordered list of portal-visible tasks.\n\nIntended for portal list views (e.g. an entity list where every card shows its\nworkflow progress). Entities the portal user cannot access are omitted from the\nresponse; an entry with an empty task list means the entity has no portal-relevant\nworkflow.\n",
8331
8491
  "tags": [
@@ -8636,6 +8796,7 @@
8636
8796
  "/v2/portal/metering/readings": {
8637
8797
  "post": {
8638
8798
  "operationId": "getMeterReadings",
8799
+ "x-portal-preview-read": true,
8639
8800
  "summary": "getMeterReadings",
8640
8801
  "x-contact-identification-token": true,
8641
8802
  "description": "Fetches meter readings for a counter and optionally resolves Handlebars\ntemplate strings against each reading object using @epilot/variables.\n",
@@ -8696,6 +8857,11 @@
8696
8857
  },
8697
8858
  "description": "DEPRECATED — client-supplied template map resolved against the counter entity. Use `templates_ref` instead; same deprecation rules as `templates`."
8698
8859
  },
8860
+ "include_counter": {
8861
+ "type": "boolean",
8862
+ "default": false,
8863
+ "description": "When true, the response includes `counter` with the counter's unit, OBIS number, direction and tariff type. Access is checked by metering-api on the counter's parent meter."
8864
+ },
8699
8865
  "templates_ref": {
8700
8866
  "$ref": "#/components/schemas/TemplatesRef",
8701
8867
  "description": "Reference to the meter reading block whose admin-configured content provides both the per-reading templates (content_top_name, main_content_name, content_bottom_name) and the counter templates (counter_title_name, counter_subtitle_name), derived server-side."
@@ -8737,6 +8903,9 @@
8737
8903
  "type": "string"
8738
8904
  },
8739
8905
  "description": "Resolved counter templates."
8906
+ },
8907
+ "counter": {
8908
+ "$ref": "#/components/schemas/MeterReadingCounterDetails"
8740
8909
  }
8741
8910
  }
8742
8911
  }
@@ -8761,6 +8930,10 @@
8761
8930
  "/v2/portal/public/sso/providers/{provider_slug}": {
8762
8931
  "get": {
8763
8932
  "operationId": "getSSOProvider",
8933
+ "x-epilot-agent": {
8934
+ "expose": false
8935
+ },
8936
+ "deprecated": true,
8764
8937
  "summary": "getSSOProvider",
8765
8938
  "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\nUse this endpoint at SSO initiation time (i.e. when the end user clicks \"Sign in with X\")\nto obtain the resolved OIDC settings needed to construct the authorization URL.\nThe web `client_secret` is intentionally never returned — it is used server-side by\nthe SSO callback to exchange the authorization code for tokens.\n\nSupports three identification methods:\n1. `org_id` + `origin`\n2. `org_id` + `portal_id`\n3. `domain`\n",
8766
8939
  "security": [],
@@ -8845,6 +9018,9 @@
8845
9018
  "/v3/portal/public/sso/providers/{provider_slug}": {
8846
9019
  "get": {
8847
9020
  "operationId": "getPublicSSOProviderV3",
9021
+ "x-epilot-agent": {
9022
+ "expose": false
9023
+ },
8848
9024
  "summary": "getPublicSSOProviderV3",
8849
9025
  "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",
8850
9026
  "security": [],
@@ -8907,6 +9083,10 @@
8907
9083
  "/v2/portal/public/sso/login": {
8908
9084
  "post": {
8909
9085
  "operationId": "ssoLogin",
9086
+ "x-epilot-agent": {
9087
+ "expose": false
9088
+ },
9089
+ "deprecated": true,
8910
9090
  "summary": "ssoLogin",
8911
9091
  "description": "Initiate login using external SSO identity.\n\nVerifies the user with the issuer and matches the identity to an epilot user (or creates a new user).\n\nReturns parameters to be used with CUSTOM_AUTH flow against Cognito\n",
8912
9092
  "security": [
@@ -8983,6 +9163,16 @@
8983
9163
  }
8984
9164
  }
8985
9165
  }
9166
+ },
9167
+ "400": {
9168
+ "description": "SSO login or token exchange failed",
9169
+ "content": {
9170
+ "application/json": {
9171
+ "schema": {
9172
+ "$ref": "#/components/schemas/SSOErrorResp"
9173
+ }
9174
+ }
9175
+ }
8986
9176
  }
8987
9177
  }
8988
9178
  }
@@ -8990,6 +9180,9 @@
8990
9180
  "/v3/portal/public/sso/login": {
8991
9181
  "post": {
8992
9182
  "operationId": "ssoLoginV3",
9183
+ "x-epilot-agent": {
9184
+ "expose": false
9185
+ },
8993
9186
  "summary": "ssoLoginV3",
8994
9187
  "description": "Initiate login using external SSO identity.\n\nVerifies the user with the issuer and matches the identity to an epilot user (or creates a new user).\n\nReturns parameters to be used with CUSTOM_AUTH flow against Cognito.\n\nSupports two identification methods:\n1. Using org_id + portal_id\n2. Using domain\n",
8995
9188
  "security": [
@@ -9082,7 +9275,14 @@
9082
9275
  }
9083
9276
  },
9084
9277
  "400": {
9085
- "$ref": "#/components/responses/InvalidRequest"
9278
+ "description": "SSO login or token exchange failed",
9279
+ "content": {
9280
+ "application/json": {
9281
+ "schema": {
9282
+ "$ref": "#/components/schemas/SSOErrorResp"
9283
+ }
9284
+ }
9285
+ }
9086
9286
  }
9087
9287
  }
9088
9288
  }
@@ -9090,6 +9290,9 @@
9090
9290
  "/v2/portal/public/sso/redirect": {
9091
9291
  "post": {
9092
9292
  "operationId": "ssoRedirect",
9293
+ "x-epilot-agent": {
9294
+ "expose": false
9295
+ },
9093
9296
  "summary": "ssoRedirect",
9094
9297
  "description": "Handles the redirect from the external SSO provider. Validates the authorization `code` and `state` received from the provider.\nRedirects the user to the provided `web_uri` with the validated credentials.\n",
9095
9298
  "security": [],
@@ -9154,6 +9357,9 @@
9154
9357
  "/v2/portal/public/sso/callback": {
9155
9358
  "post": {
9156
9359
  "operationId": "ssoCallback",
9360
+ "x-epilot-agent": {
9361
+ "expose": false
9362
+ },
9157
9363
  "summary": "ssoCallback",
9158
9364
  "description": "Handles the callback from the external SSO provider, validates the authorization `code`\nand generates a external provider token to be used with the CUSTOM_AUTH flow against Cognito.\n",
9159
9365
  "security": [],
@@ -9192,6 +9398,16 @@
9192
9398
  }
9193
9399
  }
9194
9400
  },
9401
+ "400": {
9402
+ "description": "SSO login or token exchange failed",
9403
+ "content": {
9404
+ "application/json": {
9405
+ "schema": {
9406
+ "$ref": "#/components/schemas/SSOErrorResp"
9407
+ }
9408
+ }
9409
+ }
9410
+ },
9195
9411
  "500": {
9196
9412
  "$ref": "#/components/responses/InternalServerError"
9197
9413
  }
@@ -9328,6 +9544,118 @@
9328
9544
  }
9329
9545
  }
9330
9546
  },
9547
+ "/v2/portal/pages/{id}/detach": {
9548
+ "parameters": [
9549
+ {
9550
+ "in": "path",
9551
+ "name": "id",
9552
+ "required": true,
9553
+ "schema": {
9554
+ "type": "string",
9555
+ "format": "uuid",
9556
+ "example": "5da0a718-c822-403d-9f5d-20d4584e0528"
9557
+ }
9558
+ }
9559
+ ],
9560
+ "post": {
9561
+ "operationId": "detachPortalPage",
9562
+ "summary": "detachPortalPage",
9563
+ "description": "Detach a blueprint-managed page from its blueprint. The page and all of its blocks become local (editable, no longer updated by the blueprint) and the page's lineage is tombstoned on the portal so future syncs skip it. One-way. Idempotent for pages that are not managed.\n",
9564
+ "tags": [
9565
+ "ECP Admin"
9566
+ ],
9567
+ "security": [
9568
+ {
9569
+ "EpilotAuth": []
9570
+ }
9571
+ ],
9572
+ "responses": {
9573
+ "200": {
9574
+ "description": "The detached page.",
9575
+ "content": {
9576
+ "application/json": {
9577
+ "schema": {
9578
+ "$ref": "#/components/schemas/Page"
9579
+ }
9580
+ }
9581
+ }
9582
+ },
9583
+ "401": {
9584
+ "$ref": "#/components/responses/Unauthorized"
9585
+ },
9586
+ "403": {
9587
+ "$ref": "#/components/responses/Forbidden"
9588
+ },
9589
+ "404": {
9590
+ "$ref": "#/components/responses/NotFound"
9591
+ },
9592
+ "500": {
9593
+ "$ref": "#/components/responses/InternalServerError"
9594
+ }
9595
+ }
9596
+ }
9597
+ },
9598
+ "/v2/portal/pages/{id}/blocks/{block_id}/detach": {
9599
+ "parameters": [
9600
+ {
9601
+ "in": "path",
9602
+ "name": "id",
9603
+ "required": true,
9604
+ "schema": {
9605
+ "type": "string",
9606
+ "format": "uuid",
9607
+ "example": "5da0a718-c822-403d-9f5d-20d4584e0528"
9608
+ }
9609
+ },
9610
+ {
9611
+ "in": "path",
9612
+ "name": "block_id",
9613
+ "required": true,
9614
+ "schema": {
9615
+ "type": "string",
9616
+ "format": "uuid",
9617
+ "example": "c495fef9-eeca-4019-a989-8390dcd9825b"
9618
+ }
9619
+ }
9620
+ ],
9621
+ "post": {
9622
+ "operationId": "detachPortalPageBlock",
9623
+ "summary": "detachPortalPageBlock",
9624
+ "description": "Detach a blueprint-managed block. The block becomes the customer's and its lineage is tombstoned on the page so future syncs skip it. A managed page stays managed and keeps receiving blueprint updates for everything else. One-way. Idempotent for blocks that are not managed.\n",
9625
+ "tags": [
9626
+ "ECP Admin"
9627
+ ],
9628
+ "security": [
9629
+ {
9630
+ "EpilotAuth": []
9631
+ }
9632
+ ],
9633
+ "responses": {
9634
+ "200": {
9635
+ "description": "The detached block.",
9636
+ "content": {
9637
+ "application/json": {
9638
+ "schema": {
9639
+ "$ref": "#/components/schemas/Block"
9640
+ }
9641
+ }
9642
+ }
9643
+ },
9644
+ "401": {
9645
+ "$ref": "#/components/responses/Unauthorized"
9646
+ },
9647
+ "403": {
9648
+ "$ref": "#/components/responses/Forbidden"
9649
+ },
9650
+ "404": {
9651
+ "$ref": "#/components/responses/NotFound"
9652
+ },
9653
+ "500": {
9654
+ "$ref": "#/components/responses/InternalServerError"
9655
+ }
9656
+ }
9657
+ }
9658
+ },
9331
9659
  "/v2/portal/pages": {
9332
9660
  "parameters": [
9333
9661
  {
@@ -9540,6 +9868,7 @@
9540
9868
  "/v2/portal/pages/interpolate": {
9541
9869
  "post": {
9542
9870
  "operationId": "interpolatePortalPages",
9871
+ "x-portal-preview-read": true,
9543
9872
  "summary": "interpolatePortalPages",
9544
9873
  "description": "Interpolate template variables in portal pages without reading from the database. Accepts pages in the request body and returns them with templates resolved. Portal Builder preview only: requires a 360 (epilot) token or a `portal_preview` token; plain portal user tokens get 403.",
9545
9874
  "tags": [
@@ -10539,6 +10868,9 @@
10539
10868
  "/v2/portal/public/m-login/deregister/{client_id}/{user_id}": {
10540
10869
  "delete": {
10541
10870
  "operationId": "deRegisterMLoginUser",
10871
+ "x-epilot-agent": {
10872
+ "expose": false
10873
+ },
10542
10874
  "summary": "deRegisterMLoginUser",
10543
10875
  "description": "Deregisters a user from the M Login client",
10544
10876
  "tags": [
@@ -10581,6 +10913,9 @@
10581
10913
  "/v2/portal/public/m-login/notify-interest-change/{client_id}/{user_id}": {
10582
10914
  "post": {
10583
10915
  "operationId": "notifyMLoginInterestChange",
10916
+ "x-epilot-agent": {
10917
+ "expose": false
10918
+ },
10584
10919
  "summary": "notifyMLoginInterestChange",
10585
10920
  "description": "Notifies the interest change of a user in the M Login client",
10586
10921
  "tags": [
@@ -10808,7 +11143,7 @@
10808
11143
  ],
10809
11144
  "default": "id"
10810
11145
  },
10811
- "description": "Determines how pages are matched for upsert operations:\n- `id` (default): Match pages by their ID. Use this when page IDs are stable and known upfront.\n- `slug`: Match pages by their slug. When a request page has the same slug as an existing page, the existing page ID is adopted. Use this when page ids are unknown or when source page IDs differ from destination page IDs.\n"
11146
+ "description": "Determines how pages are matched for upsert operations:\n- `id` (default): Match pages by their ID. Use this when page IDs are stable and known upfront.\n- `slug`: Match pages by their slug. When a request page has the same slug as an existing page, the existing page ID is adopted. Use this when page ids are unknown or when source page IDs differ from destination page IDs. A page may also bring its `_lineage_id` (the blueprint source page id); it is set once, on pages that have none.\n"
10812
11147
  }
10813
11148
  ],
10814
11149
  "requestBody": {
@@ -11184,6 +11519,71 @@
11184
11519
  }
11185
11520
  }
11186
11521
  },
11522
+ "/v3/portal/config/{portal_id}/revisions/{revision_id}:preview": {
11523
+ "post": {
11524
+ "operationId": "createPortalRevisionPreview",
11525
+ "summary": "createPortalRevisionPreview",
11526
+ "description": "Mints a short-lived preview token for one revision and returns a link that opens the revision inside the real end-customer portal, on the portal's live domain, without publishing it.\n\nThe portal sends the token back in the `x-portal-preview-token` header. Public and portal-user config, page, widget and extension reads that honour it answer with the revision's config and pages instead of the live ones and add `preview_revision` to config responses. Server-owned fields (ids, domains, Cognito pool identifiers, extension secrets) always come from the live portal. Nothing is written: the live portal, its pages and which revision is published stay as they are.\n\nRequires view permission on the portal. Returns `404` when portal versioning or revision preview is not enabled for the organization, or when the revision does not exist.\n",
11527
+ "tags": [
11528
+ "ECP Admin"
11529
+ ],
11530
+ "parameters": [
11531
+ {
11532
+ "in": "path",
11533
+ "name": "portal_id",
11534
+ "required": true,
11535
+ "schema": {
11536
+ "type": "string",
11537
+ "format": "uuid",
11538
+ "example": "5da0a718-c822-403d-9f5d-20d4584e0528"
11539
+ },
11540
+ "description": "Portal ID (readonly UUID generated on portal creation)"
11541
+ },
11542
+ {
11543
+ "in": "path",
11544
+ "name": "revision_id",
11545
+ "required": true,
11546
+ "schema": {
11547
+ "type": "string",
11548
+ "example": "2026-08-25T14:03:11.482Z-a7f3c1d9"
11549
+ },
11550
+ "description": "Revision ID. Contains `:` characters — percent-encode it."
11551
+ }
11552
+ ],
11553
+ "security": [
11554
+ {
11555
+ "EpilotAuth": []
11556
+ }
11557
+ ],
11558
+ "responses": {
11559
+ "200": {
11560
+ "description": "Preview token minted successfully.",
11561
+ "content": {
11562
+ "application/json": {
11563
+ "schema": {
11564
+ "$ref": "#/components/schemas/PortalRevisionPreview"
11565
+ }
11566
+ }
11567
+ }
11568
+ },
11569
+ "400": {
11570
+ "$ref": "#/components/responses/InvalidRequest"
11571
+ },
11572
+ "401": {
11573
+ "$ref": "#/components/responses/Unauthorized"
11574
+ },
11575
+ "403": {
11576
+ "$ref": "#/components/responses/Forbidden"
11577
+ },
11578
+ "404": {
11579
+ "$ref": "#/components/responses/NotFound"
11580
+ },
11581
+ "500": {
11582
+ "$ref": "#/components/responses/InternalServerError"
11583
+ }
11584
+ }
11585
+ }
11586
+ },
11187
11587
  "/v3/portal/config/{portal_id}/publish": {
11188
11588
  "post": {
11189
11589
  "operationId": "publishPortalRevision",
@@ -11441,6 +11841,16 @@
11441
11841
  "404": {
11442
11842
  "$ref": "#/components/responses/NotFound"
11443
11843
  },
11844
+ "409": {
11845
+ "description": "One of the portals (or one of their pages) is blueprint-managed; swaps are refused for them.",
11846
+ "content": {
11847
+ "application/json": {
11848
+ "schema": {
11849
+ "$ref": "#/components/schemas/ErrorResp"
11850
+ }
11851
+ }
11852
+ }
11853
+ },
11444
11854
  "500": {
11445
11855
  "$ref": "#/components/responses/InternalServerError"
11446
11856
  }
@@ -12198,146 +12608,50 @@
12198
12608
  "schema": {
12199
12609
  "oneOf": [
12200
12610
  {
12201
- "$ref": "#/components/schemas/ErrorResp"
12202
- },
12203
- {
12204
- "$ref": "#/components/schemas/FailedRuleErrorResp"
12205
- }
12206
- ]
12207
- }
12208
- }
12209
- }
12210
- },
12211
- "Conflict": {
12212
- "description": "The request conflicts with the current state of the target resource.",
12213
- "content": {
12214
- "application/json": {
12215
- "schema": {
12216
- "$ref": "#/components/schemas/ErrorResp"
12217
- }
12218
- }
12219
- }
12220
- },
12221
- "ContractAssignmentConflict": {
12222
- "description": "Contract was found but is not assignable in its current state.",
12223
- "content": {
12224
- "application/json": {
12225
- "schema": {
12226
- "allOf": [
12227
- {
12228
- "$ref": "#/components/schemas/ErrorResp"
12229
- },
12230
- {
12231
- "properties": {
12232
- "status": {
12233
- "type": "integer",
12234
- "description": "The HTTP status code, repeated in the body.",
12235
- "example": 409
12236
- },
12237
- "error": {
12238
- "type": "string",
12239
- "description": "Human-readable error message. For support, not for display to end customers."
12240
- },
12241
- "reason": {
12242
- "type": "string",
12243
- "description": "Reason why the contract is not assignable.\n- `DRAFT`: the contract is still a draft.\n- `MULTIPLE`: multiple contracts were found and the business logic does not allow it.\n- `SELF_ASSIGNMENT_DISABLED`: the contract already has a contact assigned and\n self-assignment is disabled.\n- `AMBIGUOUS_MATCH`: the portal waited on the Integration Toolkit and the ERP\n delivered entities it cannot tell apart (several matching the selector, or\n contracts naming different customers). Nothing was assigned, and retrying\n will not help.\n",
12244
- "enum": [
12245
- "DRAFT",
12246
- "MULTIPLE",
12247
- "SELF_ASSIGNMENT_DISABLED",
12248
- "AMBIGUOUS_MATCH"
12249
- ]
12250
- },
12251
- "correlation_id": {
12252
- "type": "string",
12253
- "minLength": 1,
12254
- "maxLength": 255,
12255
- "pattern": "^[A-Za-z0-9_-]+$",
12256
- "description": "Correlation id of the ERP sync that was waited on. Present only with `AMBIGUOUS_MATCH`."
12257
- }
12258
- },
12259
- "required": [
12260
- "reason"
12261
- ]
12262
- }
12263
- ]
12264
- }
12265
- }
12266
- }
12267
- },
12268
- "ContractAssignmentNotFound": {
12269
- "description": "No contract could be assigned. Without a `reason`, the identifiers matched nothing. With\n`reason`, the portal waited on the Integration Toolkit\n(`extension_hooks.contractIdentification.wait_for_toolkit`):\n- `TIMEOUT`: the ERP has not finished sending yet. Repeat the request with the\n `correlation_id` query parameter set to the `correlation_id` of this body to keep waiting\n on the same ERP sync without calling the third party again.\n- `INCOMPLETE`: the ERP finished sending without the data needed. Definitive; `error` names\n what was missing, for support, not for display to end customers.\n",
12270
- "content": {
12271
- "application/json": {
12272
- "schema": {
12273
- "type": "object",
12274
- "properties": {
12275
- "status": {
12276
- "type": "integer",
12277
- "description": "The HTTP status code, repeated in the body.",
12278
- "example": 404
12279
- },
12280
- "error": {
12281
- "type": "string",
12282
- "description": "Human-readable error message. For support, not for display to end customers."
12283
- },
12284
- "reason": {
12285
- "type": "string",
12286
- "enum": [
12287
- "TIMEOUT",
12288
- "INCOMPLETE"
12289
- ]
12611
+ "$ref": "#/components/schemas/ErrorResp"
12290
12612
  },
12291
- "correlation_id": {
12292
- "type": "string",
12293
- "minLength": 1,
12294
- "maxLength": 255,
12295
- "pattern": "^[A-Za-z0-9_-]+$",
12296
- "description": "Correlation id of the ERP sync that was waited on. Present with `TIMEOUT` and `INCOMPLETE`."
12613
+ {
12614
+ "$ref": "#/components/schemas/FailedRuleErrorResp"
12297
12615
  }
12298
- },
12299
- "required": [
12300
- "status",
12301
- "error"
12302
12616
  ]
12303
12617
  }
12304
12618
  }
12305
12619
  }
12306
12620
  },
12307
- "ContactAmbiguousMatch": {
12308
- "description": "The portal waited on the Integration Toolkit\n(`extension_hooks.registrationIdentifiersCheck.wait_for_toolkit`) and the ERP delivered\nseveral contacts matching the identifiers. Nothing was linked; retrying will not help.\n",
12621
+ "Conflict": {
12622
+ "description": "The request conflicts with the current state of the target resource.",
12309
12623
  "content": {
12310
12624
  "application/json": {
12311
12625
  "schema": {
12312
- "type": "object",
12313
- "properties": {
12314
- "status": {
12315
- "type": "integer",
12316
- "description": "The HTTP status code, repeated in the body.",
12317
- "example": 409
12318
- },
12319
- "error": {
12320
- "type": "string",
12321
- "description": "Human-readable error message. For support, not for display to end customers."
12626
+ "$ref": "#/components/schemas/ErrorResp"
12627
+ }
12628
+ }
12629
+ }
12630
+ },
12631
+ "ContractAssignmentConflict": {
12632
+ "description": "Contract was found but is not assignable in its current state.",
12633
+ "content": {
12634
+ "application/json": {
12635
+ "schema": {
12636
+ "allOf": [
12637
+ {
12638
+ "$ref": "#/components/schemas/ErrorResp"
12322
12639
  },
12323
- "reason": {
12324
- "type": "string",
12325
- "enum": [
12326
- "AMBIGUOUS_MATCH"
12640
+ {
12641
+ "properties": {
12642
+ "reason": {
12643
+ "type": "string",
12644
+ "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.",
12645
+ "enum": [
12646
+ "DRAFT",
12647
+ "MULTIPLE"
12648
+ ]
12649
+ }
12650
+ },
12651
+ "required": [
12652
+ "reason"
12327
12653
  ]
12328
- },
12329
- "correlation_id": {
12330
- "type": "string",
12331
- "minLength": 1,
12332
- "maxLength": 255,
12333
- "pattern": "^[A-Za-z0-9_-]+$",
12334
- "description": "Correlation id of the ERP sync that was waited on."
12335
12654
  }
12336
- },
12337
- "required": [
12338
- "status",
12339
- "error",
12340
- "reason"
12341
12655
  ]
12342
12656
  }
12343
12657
  }
@@ -12526,6 +12840,72 @@
12526
12840
  }
12527
12841
  },
12528
12842
  "schemas": {
12843
+ "PortalUserPasskey": {
12844
+ "type": "object",
12845
+ "required": [
12846
+ "credential_id",
12847
+ "rp_id",
12848
+ "created_at"
12849
+ ],
12850
+ "properties": {
12851
+ "credential_id": {
12852
+ "type": "string",
12853
+ "description": "WebAuthn credential ID (base64url)"
12854
+ },
12855
+ "name": {
12856
+ "type": "string"
12857
+ },
12858
+ "rp_id": {
12859
+ "type": "string",
12860
+ "description": "Domain the passkey works on",
12861
+ "example": "kundenportal.example.de"
12862
+ },
12863
+ "device_type": {
12864
+ "type": "string",
12865
+ "enum": [
12866
+ "singleDevice",
12867
+ "multiDevice"
12868
+ ]
12869
+ },
12870
+ "backed_up": {
12871
+ "type": "boolean",
12872
+ "description": "Whether the passkey is synced, e.g. via iCloud Keychain or Google Password Manager"
12873
+ },
12874
+ "created_at": {
12875
+ "type": "string",
12876
+ "format": "date-time"
12877
+ },
12878
+ "last_used_at": {
12879
+ "type": "string",
12880
+ "format": "date-time"
12881
+ }
12882
+ }
12883
+ },
12884
+ "MeterReadingCounterDetails": {
12885
+ "type": "object",
12886
+ "description": "Display details of the counter the readings belong to. Only returned when `include_counter` is true and the counter could be resolved.",
12887
+ "properties": {
12888
+ "unit": {
12889
+ "type": "string",
12890
+ "nullable": true,
12891
+ "example": "kwh"
12892
+ },
12893
+ "obis_number": {
12894
+ "type": "string",
12895
+ "nullable": true,
12896
+ "example": "1-1:1.8.1"
12897
+ },
12898
+ "direction": {
12899
+ "type": "string",
12900
+ "nullable": true,
12901
+ "example": "feed-in"
12902
+ },
12903
+ "tariff_type": {
12904
+ "type": "string",
12905
+ "nullable": true
12906
+ }
12907
+ }
12908
+ },
12529
12909
  "PortalNotification": {
12530
12910
  "type": "object",
12531
12911
  "description": "A 360 notification addressed to a portal user.",
@@ -12589,6 +12969,20 @@
12589
12969
  }
12590
12970
  }
12591
12971
  },
12972
+ "PortalUserNotificationPreferences": {
12973
+ "type": "object",
12974
+ "description": "Resolved notification preferences of a portal user. Notifications are opt-out: a channel that was never stored on the portal user resolves to enabled (true).",
12975
+ "required": [
12976
+ "in_app"
12977
+ ],
12978
+ "properties": {
12979
+ "in_app": {
12980
+ "type": "boolean",
12981
+ "description": "Whether the portal user receives in-app notifications.",
12982
+ "example": true
12983
+ }
12984
+ }
12985
+ },
12592
12986
  "MobileBuildStatus": {
12593
12987
  "type": "object",
12594
12988
  "description": "Latest build/upload status for a platform (system-written).",
@@ -12948,6 +13342,92 @@
12948
13342
  }
12949
13343
  }
12950
13344
  },
13345
+ "SSOErrorResp": {
13346
+ "description": "Error response of the public SSO endpoints. Like every error response the message is in\n`error`; `reason` carries a stable machine-readable cause when one is known (e.g.\n`MISSING_EMAIL_CLAIM`, `INVALID_PROVIDER`, `SELF_REGISTRATION_NOT_ALLOWED`,\n`TOKEN_EXCHANGE_FAILED`). `debug` is only present while SSO debug mode is active for the\nresolved provider (`ProviderConfig.debug_mode_until`).\n",
13347
+ "type": "object",
13348
+ "properties": {
13349
+ "status": {
13350
+ "type": "integer",
13351
+ "example": 400
13352
+ },
13353
+ "error": {
13354
+ "type": "string",
13355
+ "description": "Error message"
13356
+ },
13357
+ "reason": {
13358
+ "type": "string",
13359
+ "description": "Machine-readable cause, when the API can name one."
13360
+ },
13361
+ "debug": {
13362
+ "$ref": "#/components/schemas/SSODebugInfo"
13363
+ }
13364
+ }
13365
+ },
13366
+ "SSODebugInfo": {
13367
+ "type": "object",
13368
+ "description": "Technical details of a failed SSO login or token exchange, returned only while SSO debug\nmode is active for the provider. Never carries secrets, authorization codes, PKCE\nverifiers, tokens or claim values.\n",
13369
+ "properties": {
13370
+ "stage": {
13371
+ "type": "string",
13372
+ "enum": [
13373
+ "exchange",
13374
+ "login"
13375
+ ],
13376
+ "description": "Which step failed — the backend token exchange (`ssoCallback`) or the login (`ssoLogin`)."
13377
+ },
13378
+ "reason": {
13379
+ "type": "string",
13380
+ "description": "Stable reason code, e.g. `TOKEN_EXCHANGE_FAILED`, `MISSING_EMAIL_CLAIM`, `INVALID_PROVIDER`.",
13381
+ "example": "TOKEN_EXCHANGE_FAILED"
13382
+ },
13383
+ "message": {
13384
+ "type": "string",
13385
+ "description": "The API's error message."
13386
+ },
13387
+ "upstream_status": {
13388
+ "type": "integer",
13389
+ "description": "HTTP status the IdP's token endpoint answered with.",
13390
+ "example": 401
13391
+ },
13392
+ "oauth_error": {
13393
+ "type": "string",
13394
+ "description": "The IdP's RFC 6749 §5.2 `error`.",
13395
+ "example": "invalid_client"
13396
+ },
13397
+ "oauth_error_description": {
13398
+ "type": "string",
13399
+ "description": "The IdP's RFC 6749 §5.2 `error_description` (e.g. the full `AADSTS…` text)."
13400
+ },
13401
+ "token_endpoint_host": {
13402
+ "type": "string",
13403
+ "example": "login.microsoftonline.com"
13404
+ },
13405
+ "token_endpoint_path": {
13406
+ "type": "string",
13407
+ "description": "Path of the configured token endpoint, without query.",
13408
+ "example": "/contoso.onmicrosoft.com/oauth2/v2.0/token"
13409
+ },
13410
+ "redirect_uri": {
13411
+ "type": "string",
13412
+ "description": "The redirect URI sent in the token exchange."
13413
+ },
13414
+ "received_claims": {
13415
+ "type": "array",
13416
+ "description": "Names (never values) of the claims received from the IdP.",
13417
+ "items": {
13418
+ "type": "string"
13419
+ }
13420
+ },
13421
+ "request_id": {
13422
+ "type": "string",
13423
+ "description": "API request id, for support."
13424
+ },
13425
+ "hint": {
13426
+ "type": "string",
13427
+ "description": "English hint for well-known IdP errors."
13428
+ }
13429
+ }
13430
+ },
12951
13431
  "FailedRuleErrorResp": {
12952
13432
  "type": "object",
12953
13433
  "properties": {
@@ -13044,33 +13524,6 @@
13044
13524
  }
13045
13525
  }
13046
13526
  },
13047
- "DeleteEntityFile": {
13048
- "type": "object",
13049
- "required": [
13050
- "file_entity_ids",
13051
- "entity_type",
13052
- "entity_id"
13053
- ],
13054
- "properties": {
13055
- "entity_id": {
13056
- "$ref": "#/components/schemas/EntityId"
13057
- },
13058
- "entity_type": {
13059
- "type": "string",
13060
- "example": "order",
13061
- "description": "Entity type"
13062
- },
13063
- "file_entity_ids": {
13064
- "type": "array",
13065
- "items": {
13066
- "$ref": "#/components/schemas/EntityId"
13067
- },
13068
- "minItems": 1,
13069
- "uniqueItems": true,
13070
- "description": "Array of file entity IDs"
13071
- }
13072
- }
13073
- },
13074
13527
  "SaveEntityFile": {
13075
13528
  "type": "object",
13076
13529
  "required": [
@@ -13483,6 +13936,17 @@
13483
13936
  }
13484
13937
  }
13485
13938
  },
13939
+ "passkeys": {
13940
+ "type": "object",
13941
+ "description": "Opt-in, set by the configuring user. When enabled, a second factor is enforced after the password and portal users can add passkeys (fingerprint, face or device PIN) as one variant of it, next to the email code of advanced_mfa. Users with a passkey use it by default; with advanced_mfa off, the passkey is the only second factor once a user has added one. On passwordless portals a passkey can replace the email code. Passkeys are bound to the domain they were added on. Default off; turning it off again restores the previous behaviour and keeps registered passkeys.\n",
13942
+ "properties": {
13943
+ "enabled": {
13944
+ "type": "boolean",
13945
+ "description": "Enforce a second factor and allow passkeys as one variant of it",
13946
+ "default": false
13947
+ }
13948
+ }
13949
+ },
13486
13950
  "entry_point": {
13487
13951
  "type": "string",
13488
13952
  "enum": [
@@ -13515,6 +13979,11 @@
13515
13979
  "example": "6bsd0jkgoie74k2i8mrhc1vest",
13516
13980
  "description": "Cognito user pool client ID"
13517
13981
  },
13982
+ "cognito_mobile_client_id": {
13983
+ "type": "string",
13984
+ "example": "3k9fj2l1m0n8b7v6c5x4z3a2s1",
13985
+ "description": "Cognito user pool client ID used by the native portal apps (longer refresh token validity)"
13986
+ },
13518
13987
  "cognito_user_pool_arn": {
13519
13988
  "type": "string",
13520
13989
  "example": "arn:aws:cognito-idp:us-east-1:123412341234:userpool/us-east-1_123412341",
@@ -13718,6 +14187,15 @@
13718
14187
  }
13719
14188
  ]
13720
14189
  },
14190
+ "self_assignment_mode": {
14191
+ "type": "string",
14192
+ "enum": [
14193
+ "contracts",
14194
+ "customer_to_portal_user"
14195
+ ],
14196
+ "default": "contracts",
14197
+ "description": "What the built-in self-assignment (without a contractIdentification hook) assigns once the `contract_identifiers` match a Contract:\n - `contracts`: the user's Contact is added as a customer of the Contract.\n - `customer_to_portal_user`: the Contract's customers (Contacts and Accounts) are added to the Portal User's `represents`. The Portal User sees all their data, including Contracts, transitively. Mirrors the `contact_to_portal_user` assignment mode of the contractIdentification hook.\n"
14198
+ },
13721
14199
  "contract_selector_config": {
13722
14200
  "type": "object",
13723
14201
  "description": "Configuration for contract selector in the portal",
@@ -13903,7 +14381,8 @@
13903
14381
  },
13904
14382
  "is_v3_item": {
13905
14383
  "type": "boolean",
13906
- "description": "Whether this is a v3 portal configuration"
14384
+ "deprecated": true,
14385
+ "description": "Always true. Legacy v2 storage was migrated; field kept for compatibility and will be removed."
13907
14386
  },
13908
14387
  "published_revision_id": {
13909
14388
  "type": "string",
@@ -13945,6 +14424,25 @@
13945
14424
  "engagement_center_enabled": {
13946
14425
  "type": "boolean",
13947
14426
  "description": "Master toggle for the portal's engagement center (in-app notifications including the notification triggers above). Off when absent — portal users only see the engagement center and receive trigger notifications after an admin enables it."
14427
+ },
14428
+ "engagement_center_task_templates": {
14429
+ "$ref": "#/components/schemas/EngagementCenterTaskTemplates"
14430
+ }
14431
+ }
14432
+ },
14433
+ "EngagementCenterTaskTemplates": {
14434
+ "type": "object",
14435
+ "description": "Handlebars templates rendering each outstanding task (to-do) row in the portal's engagement center. Resolved server-side per task against the task's entity (same variable syntax as page block templates, e.g. `{{order.order_number}}`, custom and environment variables) plus `task.*` (the workflow step, e.g. `{{task.name}}`), `workflow.*` (the execution, e.g. `{{workflow.name}}`) and `entity.*` (the task's entity regardless of its schema, e.g. `{{entity._title}}`). A template that is absent or blank keeps the portal's default rendering for that line. Admin/builder-only — never exposed via the public portal config.",
14436
+ "properties": {
14437
+ "title": {
14438
+ "type": "string",
14439
+ "description": "Template for the row title. Default rendering is the step name.",
14440
+ "example": "{{task.name}}"
14441
+ },
14442
+ "subtitle": {
14443
+ "type": "string",
14444
+ "description": "Template for the row's context line. Default rendering is the workflow name and the entity title.",
14445
+ "example": "{{workflow.name}} · {{entity._title}}"
13948
14446
  }
13949
14447
  }
13950
14448
  },
@@ -13956,8 +14454,9 @@
13956
14454
  "properties": {
13957
14455
  "trigger_type": {
13958
14456
  "type": "string",
13959
- "description": "The 360 event that fires this notification trigger.",
14457
+ "description": "The 360 event that fires this notification trigger. `entity_added` is the unified trigger written by the Portal Builder: it fires for both the entity-created and the entity-assigned event of its `entity_schema`. `entity_created` / `entity_assigned` are the legacy per-event triggers and keep working; when a portal has both a legacy row and an `entity_added` row for the same schema, the legacy row wins for its event.",
13960
14458
  "enum": [
14459
+ "entity_added",
13961
14460
  "entity_created",
13962
14461
  "entity_assigned",
13963
14462
  "workflow_step_overdue"
@@ -13965,7 +14464,7 @@
13965
14464
  },
13966
14465
  "entity_schema": {
13967
14466
  "type": "string",
13968
- "description": "For `entity_created` / `entity_assigned` triggers, the entity schema slug (e.g. `opportunity`, `order`) whose creation or assignment fires this trigger. Ignored for other trigger types.",
14467
+ "description": "For `entity_added` / `entity_created` / `entity_assigned` triggers, the entity schema slug (e.g. `opportunity`, `order`) whose creation or assignment fires this trigger. Ignored for other trigger types.",
13969
14468
  "example": "opportunity"
13970
14469
  },
13971
14470
  "enabled": {
@@ -13977,6 +14476,13 @@
13977
14476
  "type": "string",
13978
14477
  "format": "uuid",
13979
14478
  "description": "Entity id of the notification_template to render for this trigger."
14479
+ },
14480
+ "target_ids": {
14481
+ "type": "array",
14482
+ "description": "Targeting segment ids (targets) that narrow this trigger's recipients: the notification is only created for portal users whose contact matches at least one of the targets. Absent or empty means no narrowing — every portal user linked to the entity's contacts is notified.",
14483
+ "items": {
14484
+ "type": "string"
14485
+ }
13980
14486
  }
13981
14487
  }
13982
14488
  },
@@ -14008,6 +14514,34 @@
14008
14514
  "example": 12345,
14009
14515
  "description": "ID of the organization"
14010
14516
  },
14517
+ "_manifest": {
14518
+ "type": "array",
14519
+ "readOnly": true,
14520
+ "description": "Blueprint instance ids managing this portal. Server-managed: ignored (stripped) on every write from a user.\n",
14521
+ "items": {
14522
+ "type": "string"
14523
+ }
14524
+ },
14525
+ "_lineage_id": {
14526
+ "type": "string",
14527
+ "description": "Id of this portal in the blueprint source. Set once: a blueprint install or update (portal create, `page_upsert_mode=slug`, or a revision with `revision_source` `blueprint_sync`) may give a portal the lineage it does not have yet, unless another portal of the organization holds it. A portal created as a copy next to the organization's live portal of its origin (downgraded to `ADDITIONAL_PORTAL`) never takes it. After that it never changes; every other write keeps the stored value. Locks nothing on its own.\n"
14528
+ },
14529
+ "detached_lineage_ids": {
14530
+ "type": "array",
14531
+ "readOnly": true,
14532
+ "description": "Lineage ids of pages detached from their blueprint; a blueprint sync leaves them alone. Server-managed.\n",
14533
+ "items": {
14534
+ "type": "string"
14535
+ }
14536
+ },
14537
+ "held_back": {
14538
+ "type": "array",
14539
+ "readOnly": true,
14540
+ "description": "Only present on write responses: blueprint-managed pages/blocks the request tried to change. Their stored version was kept and the rest of the save succeeded.\n",
14541
+ "items": {
14542
+ "$ref": "#/components/schemas/HeldBackNode"
14543
+ }
14544
+ },
14011
14545
  "org_settings": {
14012
14546
  "type": "object",
14013
14547
  "description": "Organization settings",
@@ -14053,6 +14587,15 @@
14053
14587
  "items": {
14054
14588
  "$ref": "#/components/schemas/ProviderPublicConfig"
14055
14589
  }
14590
+ },
14591
+ "preview_revision": {
14592
+ "readOnly": true,
14593
+ "description": "Present only when the request carried a valid `x-portal-preview-token`: the config is this revision's, not the live one.\n",
14594
+ "allOf": [
14595
+ {
14596
+ "$ref": "#/components/schemas/PreviewRevision"
14597
+ }
14598
+ ]
14056
14599
  }
14057
14600
  }
14058
14601
  }
@@ -14070,7 +14613,8 @@
14070
14613
  },
14071
14614
  "is_v3_item": {
14072
14615
  "type": "boolean",
14073
- "description": "Indicates whether the widget configuration is stored as a V3 portal-scoped item"
14616
+ "deprecated": true,
14617
+ "description": "Always true. Legacy v2 storage was migrated; field kept for compatibility and will be removed."
14074
14618
  },
14075
14619
  "widgets": {
14076
14620
  "type": "array",
@@ -14456,14 +15000,6 @@
14456
15000
  "type": "boolean",
14457
15001
  "default": true,
14458
15002
  "description": "Whether to (re)trigger the registration identifiers check hook, which issues a request\nto the connected ERP to (re)sync the contact, in addition to waiting for the entity to\narrive. Defaults to true to preserve existing behaviour. Set to false on retry attempts\nto only poll for an already-triggered sync to land, without issuing another upstream\nrequest to the ERP.\n"
14459
- },
14460
- "correlation_id": {
14461
- "type": "string",
14462
- "minLength": 1,
14463
- "maxLength": 255,
14464
- "pattern": "^[A-Za-z0-9_-]+$",
14465
- "description": "Correlation id of an already triggered ERP sync, as returned by a previous call that\nanswered `reason: TIMEOUT`. Sent together with `trigger_identifiers_check: false` on a\nretry so the portal keeps waiting on the same correlation instead of starting over.\nOnly used by portals configured with\n`extension_hooks.registrationIdentifiersCheck.wait_for_toolkit`; ignored otherwise.\nAt most 255 letters, digits, hyphens and underscores: the toolkit's bound for a\ncorrelation id and the charset it enforces on its identifiers.\n",
14466
- "example": "0199c0d5-6f1c-7a3b-9e21-9a9ce1d1b2c3"
14467
15003
  }
14468
15004
  }
14469
15005
  },
@@ -14535,6 +15071,11 @@
14535
15071
  "description": "Whether the identified contact already has a portal user on this portal, so the caller\ncan skip offering account creation. Present only on a match; omitted when it could not\nbe determined.\n",
14536
15072
  "example": false
14537
15073
  },
15074
+ "portal_user_email": {
15075
+ "type": "string",
15076
+ "description": "The e-mail the identified contact's portal user on this portal logs in with. It can\ndiffer from the contact's own e-mail. Present only when `has_portal_user` is true.\n",
15077
+ "example": "john.doe@example.com"
15078
+ },
14538
15079
  "reason": {
14539
15080
  "type": "string",
14540
15081
  "enum": [
@@ -14542,13 +15083,6 @@
14542
15083
  "NOT_FOUND"
14543
15084
  ],
14544
15085
  "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"
14545
- },
14546
- "correlation_id": {
14547
- "type": "string",
14548
- "minLength": 1,
14549
- "maxLength": 255,
14550
- "pattern": "^[A-Za-z0-9_-]+$",
14551
- "description": "Correlation id of the triggered ERP sync. Present only with `reason: TIMEOUT`, for\nportals waiting on the Integration Toolkit (`wait_for_toolkit`). Send it back with\n`trigger_identifiers_check: false` to keep waiting on the same correlation.\n"
14552
15086
  }
14553
15087
  }
14554
15088
  },
@@ -15338,6 +15872,47 @@
15338
15872
  }
15339
15873
  ]
15340
15874
  },
15875
+ "PortalContactUpdate": {
15876
+ "type": "object",
15877
+ "description": "Contact attributes a portal user can change. Other attributes are ignored.\nValue types follow the organization's contact schema.\n",
15878
+ "additionalProperties": true,
15879
+ "properties": {
15880
+ "language": {
15881
+ "type": "string",
15882
+ "example": "de"
15883
+ },
15884
+ "first_name": {},
15885
+ "last_name": {},
15886
+ "salutation": {},
15887
+ "phone": {},
15888
+ "phone_number": {},
15889
+ "mobile": {},
15890
+ "street": {},
15891
+ "street_number": {},
15892
+ "postal_code": {},
15893
+ "city": {},
15894
+ "country": {},
15895
+ "preferred_language": {},
15896
+ "communication_preferences": {}
15897
+ }
15898
+ },
15899
+ "PortalContractUpdate": {
15900
+ "type": "object",
15901
+ "description": "Contract attributes a portal user can change. Other attributes are ignored.",
15902
+ "additionalProperties": true,
15903
+ "properties": {
15904
+ "installment_amount": {
15905
+ "type": "number",
15906
+ "description": "Installment amount in cents",
15907
+ "example": 8500
15908
+ },
15909
+ "installment_amount_decimal": {
15910
+ "type": "string",
15911
+ "description": "Installment amount as a decimal string",
15912
+ "example": "85.00"
15913
+ }
15914
+ }
15915
+ },
15341
15916
  "Contact": {
15342
15917
  "description": "The mapped contact of the portal user",
15343
15918
  "allOf": [
@@ -16342,6 +16917,39 @@
16342
16917
  "type": "string"
16343
16918
  }
16344
16919
  }
16920
+ },
16921
+ "link_hint": {
16922
+ "type": "object",
16923
+ "description": "Tells journey-api which route to link; omitted for a portal without an id",
16924
+ "required": [
16925
+ "route",
16926
+ "params"
16927
+ ],
16928
+ "properties": {
16929
+ "route": {
16930
+ "type": "string",
16931
+ "enum": [
16932
+ "portal-settings"
16933
+ ]
16934
+ },
16935
+ "params": {
16936
+ "type": "object",
16937
+ "required": [
16938
+ "portal_id"
16939
+ ],
16940
+ "properties": {
16941
+ "portal_id": {
16942
+ "type": "string"
16943
+ },
16944
+ "settings": {
16945
+ "type": "string",
16946
+ "enum": [
16947
+ "journey-actions"
16948
+ ]
16949
+ }
16950
+ }
16951
+ }
16952
+ }
16345
16953
  }
16346
16954
  }
16347
16955
  }
@@ -16472,6 +17080,14 @@
16472
17080
  "complete_task_automatically": {
16473
17081
  "type": "boolean",
16474
17082
  "description": "Whether submitting the journey from the portal should auto-complete the task. When false, an internal user completes it."
17083
+ },
17084
+ "title": {
17085
+ "type": "string",
17086
+ "description": "Row title rendered from the portal's `engagement_center_task_templates.title`. Absent when the portal has no title template or it rendered blank for this task; the client then shows `step_name`."
17087
+ },
17088
+ "subtitle": {
17089
+ "type": "string",
17090
+ "description": "Row context line rendered from the portal's `engagement_center_task_templates.subtitle`. Absent when the portal has no subtitle template or it rendered blank for this task; the client then shows the workflow name and entity title."
16475
17091
  }
16476
17092
  }
16477
17093
  },
@@ -16752,7 +17368,12 @@
16752
17368
  },
16753
17369
  "is_active": {
16754
17370
  "type": "boolean",
16755
- "description": "True when the task has been reached in the execution — all predecessor tasks on its\npath are completed/skipped — so the portal user can act on it (e.g. start its journey).\nTasks that are only part of the projected future path are returned with is_active false.\n"
17371
+ "description": "True when the task has been reached in the execution — all predecessor tasks on its\npath are completed/skipped — so the portal user can act on it (e.g. start its journey).\nTasks that are only part of the projected future path are returned with is_active false.\nThe start date is not taken into account: an active task can still be upcoming (see start_date).\n"
17372
+ },
17373
+ "start_date": {
17374
+ "type": "string",
17375
+ "format": "date-time",
17376
+ "description": "Instant before which nobody may work on the task. While it lies in the future, a task\nthat is not done yet is upcoming: the Workflows API refuses to complete it, so the\nportal shows it as upcoming instead of offering its journey. Neither status nor\nis_active reflects it; compare it with the current time. Absent when the task has no\nstart date or its relative start date has not been resolved yet.\n"
16756
17377
  },
16757
17378
  "ecp": {
16758
17379
  "$ref": "#/components/schemas/PortalTaskConfig"
@@ -17329,8 +17950,25 @@
17329
17950
  }
17330
17951
  },
17331
17952
  "sort": {
17332
- "type": "string",
17333
- "example": "_created_at:desc"
17953
+ "description": "Sort order of the results: an `attribute:order` pair, or a list of Elasticsearch sort fields that are\nused as they are, e.g. the sort order of a target as returned by the Targeting API.\nWithout a sort, the sort order of the first requested target that defines one applies.\n",
17954
+ "oneOf": [
17955
+ {
17956
+ "type": "string",
17957
+ "example": "_created_at:desc"
17958
+ },
17959
+ {
17960
+ "type": "array",
17961
+ "description": "At most 20 sort fields.",
17962
+ "items": {
17963
+ "type": "string",
17964
+ "pattern": "^[^\\s:,]+:(asc|desc)$"
17965
+ },
17966
+ "example": [
17967
+ "last_name.keyword:desc",
17968
+ "_id:desc"
17969
+ ]
17970
+ }
17971
+ ]
17334
17972
  },
17335
17973
  "from": {
17336
17974
  "type": "integer",
@@ -17627,36 +18265,6 @@
17627
18265
  "hook_id": {
17628
18266
  "type": "string",
17629
18267
  "description": "The ID of the selected hook."
17630
- },
17631
- "wait_for_toolkit": {
17632
- "type": "boolean",
17633
- "default": false,
17634
- "description": "Opt in to waiting on the Integration Toolkit instead of polling entity search for the\ndata the ERP pushes after the hook returns. When true, the portal hands the hook a\ncorrelation id (`{{CorrelationId}}`) and then asks the toolkit whether the entities it\nneeds have landed under that correlation. Off by default: a portal that does not set it\nkeeps polling entity search exactly as before.\n\nOnly effective for hooks whose partner pushes through the Integration Toolkit and\nechoes the correlation id. A hook that answers with an epilot contact id is fetched\ndirectly, as today, whatever this flag says.\n"
17635
- },
17636
- "wait_for_entities": {
17637
- "type": "object",
17638
- "description": "The entities the portal waits for, keyed by entity slug. Configured explicitly per\nhook: nothing is derived from the hook type or from the portal's identifier\nconfiguration, and a hook that configures no requirement for the entity the portal\nlinks does not wait at all - it keeps polling entity search exactly as before.\n\nA selector may only name an attribute that is literally a member of that entity\ntarget's configured `unique_ids` in the inbound use case, because the toolkit reports\nexactly those and nothing else. A selector on any other attribute can never match. A\ntoolkit that validates selectors rejects such a wait (400) with the slug, the key and\nthe keys it accepts; the portal logs that at error level with the hook it came from\nand polls entity search for the request, so the customer is unaffected and never told.\n\nThe slug the portal resolves its link target from is fixed by the hook, and a\nrequirement for it must be configured for the wait to happen at all:\n`registrationIdentifiersCheck` resolves the contact, every `contractIdentification`\nassignment mode resolves the contract (in the contact modes the linked contact is the\ndelivered contract's `customer`). Every other configured slug is evidence that the push\ncarried what it promised, and is never linked.\n\nIgnored unless `wait_for_toolkit` is true.\n\nWorked examples, verified against the live integration configuration. Each block is the\n`wait_for_entities` of one hook selection, and each names the slug that hook links.\n\nRatingen / Wilken add-contract hook (`contractIdentification`, `contact_to_portal_user`).\nNo value the user types is a unique id anywhere in the org - every one is a Wilken\nsurrogate `external_id` - so the contact cannot be selected by attribute at all. Wait on\nthe contract and link its `customer`; the identity-keyed contact is evidence that the\nbusiness partner landed, and is never linked.\n\n```yaml\ncontract: {}\ncontact:\n key: external_id # the PROFILE contact's unique id is external_id\n from_caller_contact: external_id\n```\n\nWEMAG registration hook (`registrationIdentifiersCheck`). The contact's unique ids are\n`customer_pin` and `reenumber`; `meter.external_id` and `meter.meter_number` are both\nthe `ZNR` the user types.\n\n```yaml\ncontact:\n key: reenumber\n from_identifier: contact.complete_customer_number\n split_on: \"-\" # \"0002-<customer_pin>-<reenumber>\"\n part: 2\nmeter:\n key: external_id\n from_identifier: meter.meter_number\n```\n\nWEMAG add-contract hook (`contractIdentification`, `contact_to_portal_user`). The\ncontract's unique ids are `billing_account_number` (the `reenumber`, the same third\npart of `complete_customer_number`) and `customer_number` (the PIN). Wait on the\ncontract of the typed Rechnungseinheit and link its `customer`.\n\n```yaml\ncontract:\n key: billing_account_number\n from_identifier: contact.complete_customer_number\n split_on: \"-\" # \"0002-<customer_pin>-<reenumber>\"\n part: 2\nmeter:\n key: external_id\n from_identifier: meter.meter_number\n```\n\nNever `complete_customer_number` or `customer_number` on a WEMAG contact - neither is a\nunique id there - and never `customer_pin` alone on a contact or `customer_number` alone\non a contract, both of which are the PIN and ambiguous, because one person has one\ncontact and one contract per Rechnungseinheit. Never a key-less `contract` either: WEMAG's\n`EKM Anlagedaten` writes contracts of its own, keyed on `contract_number`.\n",
17639
- "additionalProperties": {
17640
- "$ref": "#/components/schemas/WaitForEntityRule"
17641
- },
17642
- "example": {
17643
- "contract": {},
17644
- "contact": {
17645
- "key": "external_id",
17646
- "from_caller_contact": "external_id"
17647
- }
17648
- }
17649
- },
17650
- "wait_for_use_cases": {
17651
- "type": "array",
17652
- "description": "Inbound use case slugs the portal additionally waits for. A requirement is met once at\nleast one event of that use case slug has finished processing, whether or not it\nproduced an entity, so it covers pushes whose entity set varies and pushes whose\noutcome is a file or a relation rather than an entity.\n\nEntity requirements stay primary - the portal links by entity id, so it always waits\nfor the entity it links. Use case slugs are an additional bar: when both are\nconfigured, both must be met. Empty by default. Ignored unless `wait_for_toolkit` is\ntrue.\n",
17653
- "items": {
17654
- "type": "string"
17655
- },
17656
- "default": [],
17657
- "example": [
17658
- "wilken_contract_changed"
17659
- ]
17660
18268
  }
17661
18269
  },
17662
18270
  "required": [
@@ -17665,39 +18273,6 @@
17665
18273
  "hook_id"
17666
18274
  ]
17667
18275
  },
17668
- "WaitForEntityRule": {
17669
- "type": "object",
17670
- "description": "One requirement of a hook's correlation wait, for the entity slug it is configured under.\n\nWithout `key` the requirement is satisfied by any entity of that slug. That is safe for\nevidence, and for a linked slug only where the inbound use case writes exactly one entity\nof it per push - Wilken's `BusinessPartnerChanged` always writes two contacts, and WEMAG's\n`DocumentChange` / `D3Metadata` write contacts under the same unique-id keys a registration\npush uses - so a second match answers 409 rather than linking a guess.\n\nWith `key`, the value comes from exactly one declared source, `from_identifier` or\n`from_caller_contact`. A rule whose source has no value produces no wait at all, so the\nhook keeps polling rather than waiting on a requirement that cannot be met.\n",
17671
- "properties": {
17672
- "key": {
17673
- "type": "string",
17674
- "description": "Unique-id attribute the selector matches on. Must be a member of that entity target's\nconfigured `unique_ids` in the inbound use case; the toolkit reports nothing else.\n",
17675
- "example": "reenumber"
17676
- },
17677
- "from_identifier": {
17678
- "type": "string",
17679
- "description": "Identifier the user submitted to take the value from, as `<schema>.<name>` - the same\nschema and name the portal's `registration_identifiers` / `contract_identifiers` use.\nMutually exclusive with `from_caller_contact`.\n",
17680
- "example": "contact.complete_customer_number"
17681
- },
17682
- "from_caller_contact": {
17683
- "type": "string",
17684
- "description": "Attribute of the calling portal user's own contact to take the value from. Only\navailable to hooks called by a signed-in user. Mutually exclusive with\n`from_identifier`.\n",
17685
- "example": "external_id"
17686
- },
17687
- "split_on": {
17688
- "type": "string",
17689
- "description": "Separator the source value is split on before `part` is taken, for identifiers that\ncarry a unique id inside a larger value the user types. WEMAG's\n`complete_customer_number` is `\"0002-<customer_pin>-<reenumber>\"`, so splitting on `-`\nand taking part 2 yields the contact's `reenumber` unique id.\n",
17690
- "example": "-"
17691
- },
17692
- "part": {
17693
- "type": "integer",
17694
- "minimum": 0,
17695
- "default": 0,
17696
- "description": "Zero-based index of the part to use after splitting on `split_on`.",
17697
- "example": 2
17698
- }
17699
- }
17700
- },
17701
18276
  "PublicExtensionCapabilities": {
17702
18277
  "type": "object",
17703
18278
  "properties": {
@@ -18436,10 +19011,6 @@
18436
19011
  "type": "string",
18437
19012
  "deprecated": true,
18438
19013
  "description": "Deprecated. Use `resolved.result` instead. Contact ID usually retrieved from the response body, e.g. `{{CallResponse.data.contact_id}}`. If no result is passed and the request suceeds, we attempt to resolve the Contact ID automatically. Supports variable interpolation."
18439
- },
18440
- "result_correlation_id": {
18441
- "type": "string",
18442
- "description": "Correlation id to wait for inbound ERP events under, resolved from the third-party\nresponse, e.g. `{{CallResponse.data.acknowledgeId}}`. Only read when the portal is\nconfigured to wait on the Integration Toolkit\n(`extension_hooks.<hook>.wait_for_toolkit`).\n\nThere is no default: unless a hook declares this, the portal waits on the id it\nminted and handed the hook as `{{CorrelationId}}`, whatever the response carries. A\npartner that pushes under that id leaves this unset; only a partner that pushes\nunder an id of its own declares where the response carries it. A declared id that\nis absent from the response falls back to the minted one; one that is not a valid\ncorrelation id (at most 255 letters, digits, `-` and `_`) is not waited on, and\nthe portal polls entity search instead.\n"
18443
19014
  }
18444
19015
  },
18445
19016
  "required": [
@@ -18454,10 +19025,6 @@
18454
19025
  "type": "string",
18455
19026
  "description": "Contact ID usually retrieved from the response body, e.g. `{{CallResponse.data.contact_id}}`. If no result is passed and the request suceeds, we attempt to resolve the Contact ID automatically. Supports variable interpolation. Supersedes the deprecated `call.result`."
18456
19027
  },
18457
- "result_correlation_id": {
18458
- "type": "string",
18459
- "description": "Correlation id to wait for inbound ERP events under, resolved from the third-party\nresponse, e.g. `{{CallResponse.data.acknowledgeId}}`. Supersedes\n`call.result_correlation_id`. No default; see `call.result_correlation_id`.\n"
18460
- },
18461
19028
  "error_message_path": {
18462
19029
  "type": "string",
18463
19030
  "description": "Optional path to a human-readable error message in the third-party response body, used when the call fails (non-2xx status).\nIf specified and the path resolves to a string, that message is forwarded to the end user instead of a generic error.\n",
@@ -18529,10 +19096,6 @@
18529
19096
  "type": "string",
18530
19097
  "deprecated": true,
18531
19098
  "description": "Deprecated. Use `resolved.result` instead. Contact ID usually retrieved from the response body, e.g. `{{CallResponse.data.contact_id}}`. If no result is passed and the request suceeds, we attempt to resolve the Contact ID automatically. Supports variable interpolation."
18532
- },
18533
- "result_correlation_id": {
18534
- "type": "string",
18535
- "description": "Correlation id to wait for inbound ERP events under, resolved from the third-party\nresponse, e.g. `{{CallResponse.data.acknowledgeId}}`. Only read when the portal is\nconfigured to wait on the Integration Toolkit\n(`extension_hooks.<hook>.wait_for_toolkit`).\n\nThere is no default: unless a hook declares this, the portal waits on the id it\nminted and handed the hook as `{{CorrelationId}}`, whatever the response carries. A\npartner that pushes under that id leaves this unset; only a partner that pushes\nunder an id of its own declares where the response carries it. A declared id that\nis absent from the response falls back to the minted one; one that is not a valid\ncorrelation id (at most 255 letters, digits, `-` and `_`) is not waited on, and\nthe portal polls entity search instead.\n"
18536
19099
  }
18537
19100
  },
18538
19101
  "required": [
@@ -18547,10 +19110,6 @@
18547
19110
  "type": "string",
18548
19111
  "description": "Contract or Contact ID usually retrieved from the response body, e.g. `{{CallResponse.data.contact_id}}`. If no result is passed and the request suceeds, we attempt to resolve the Contact ID automatically. Supports variable interpolation. Supersedes the deprecated `call.result`."
18549
19112
  },
18550
- "result_correlation_id": {
18551
- "type": "string",
18552
- "description": "Correlation id to wait for inbound ERP events under, resolved from the third-party\nresponse, e.g. `{{CallResponse.data.acknowledgeId}}`. Supersedes\n`call.result_correlation_id`. No default; see `call.result_correlation_id`.\n"
18553
- },
18554
19113
  "error_message_path": {
18555
19114
  "type": "string",
18556
19115
  "description": "Optional path to a human-readable error message in the third-party response body, used when the call fails (non-2xx status).\nIf specified and the path resolves to a string, that message is forwarded to the end user instead of a generic error.\n",
@@ -19801,6 +20360,13 @@
19801
20360
  "type": "boolean",
19802
20361
  "description": "Allow the resolved `client_secret` to be returned through the\npublic single-provider endpoint at SSO initiation. Only set this\nfor OIDC flows that require a public client secret in the\nbrowser (e.g. some PKCE-less authorization-code variants). When\nunset (default), the secret is kept server-side and only used at\nthe token exchange.\n",
19803
20362
  "example": false
20363
+ },
20364
+ "debug_mode_until": {
20365
+ "type": "string",
20366
+ "format": "date-time",
20367
+ "nullable": true,
20368
+ "description": "Temporarily enables SSO debug mode for this provider. While the current time is\nbefore this timestamp, failed SSO logins and token exchanges answer with a `debug`\nobject (`SSODebugInfo`) next to the error, so admins can see the real cause (e.g. the\nIdP's `AADSTS…` error) on the portal login page.\n\nCapped at 24 hours from the time of the write (plus 5 minutes clock-skew tolerance);\na later value is rejected with 400. `null` or absent turns debug mode off, and an\nexpired value is simply inactive. Never exposed publicly — public reads only carry\nthe derived `debug_mode` flag.\n",
20369
+ "example": "2026-10-30T12:00:00.000Z"
19804
20370
  }
19805
20371
  },
19806
20372
  "required": [
@@ -19822,6 +20388,11 @@
19822
20388
  },
19823
20389
  "mobile_oidc_config": {
19824
20390
  "$ref": "#/components/schemas/MoblieOIDCConfig"
20391
+ },
20392
+ "debug_mode": {
20393
+ "type": "boolean",
20394
+ "description": "Derived from `ProviderConfig.debug_mode_until`: `true` only while SSO debug mode is\nactive for this provider. Clients may use it to show technical error details on\nfailed SSO logins. Absent or `false` when debug mode is off.\n",
20395
+ "example": false
19825
20396
  }
19826
20397
  },
19827
20398
  "required": [
@@ -19841,6 +20412,11 @@
19841
20412
  },
19842
20413
  "oidc_config": {
19843
20414
  "$ref": "#/components/schemas/OIDCProviderConfig"
20415
+ },
20416
+ "debug_mode": {
20417
+ "type": "boolean",
20418
+ "description": "Derived from `ProviderConfig.debug_mode_until`: `true` only while SSO debug mode is\nactive for this provider. Clients may use it to show technical error details on\nfailed SSO logins. Absent or `false` when debug mode is off.\n",
20419
+ "example": false
19844
20420
  }
19845
20421
  },
19846
20422
  "required": [
@@ -20313,6 +20889,31 @@
20313
20889
  "description": "The type of the block. eg; tabs, tab, group, attribute",
20314
20890
  "example": "tab"
20315
20891
  },
20892
+ "HeldBackNode": {
20893
+ "type": "object",
20894
+ "description": "A blueprint-managed page or block a write tried to change; the stored version was kept.",
20895
+ "required": [
20896
+ "page_id",
20897
+ "reason"
20898
+ ],
20899
+ "properties": {
20900
+ "page_id": {
20901
+ "type": "string",
20902
+ "example": "5da0a718-c822-403d-9f5d-20d4584e0528"
20903
+ },
20904
+ "block_id": {
20905
+ "type": "string",
20906
+ "description": "Set when a single block (not the whole page) was held back",
20907
+ "example": "c495fef9-eeca-4019-a989-8390dcd9825b"
20908
+ },
20909
+ "reason": {
20910
+ "type": "string",
20911
+ "enum": [
20912
+ "BLUEPRINT_MANAGED"
20913
+ ]
20914
+ }
20915
+ }
20916
+ },
20316
20917
  "BlockRequest": {
20317
20918
  "type": "object",
20318
20919
  "additionalProperties": true,
@@ -20359,6 +20960,24 @@
20359
20960
  "description": "The id of the block",
20360
20961
  "format": "uuid",
20361
20962
  "example": "c495fef9-eeca-4019-a989-8390dcd9825b"
20963
+ },
20964
+ "_manifest": {
20965
+ "type": "array",
20966
+ "readOnly": true,
20967
+ "description": "Blueprint instance ids managing this block. Non-empty means the block is blueprint-managed and read-only for users until it is detached. Server-managed: ignored (stripped) on every write from a user.\n",
20968
+ "items": {
20969
+ "type": "string"
20970
+ }
20971
+ },
20972
+ "_lineage_id": {
20973
+ "type": "string",
20974
+ "readOnly": true,
20975
+ "description": "Stable id of this block in the blueprint source. Server-managed."
20976
+ },
20977
+ "_level": {
20978
+ "type": "string",
20979
+ "readOnly": true,
20980
+ "description": "Reserved for author-defined block levels (e.g. `dummy`, `strict`). Not interpreted yet. Server-managed: ignored (stripped) on every write from a user.\n"
20362
20981
  }
20363
20982
  }
20364
20983
  }
@@ -20546,6 +21165,26 @@
20546
21165
  "old-dashboard",
20547
21166
  "home"
20548
21167
  ]
21168
+ },
21169
+ "_manifest": {
21170
+ "type": "array",
21171
+ "readOnly": true,
21172
+ "description": "Blueprint instance ids managing this page. Non-empty means the page is blueprint-managed: its page-level fields and its blueprint-managed blocks are read-only for users (single-target writes answer 409 `BLUEPRINT_MANAGED`, bulk saves keep the stored version), while detached and customer-added blocks on it stay editable and the page keeps receiving blueprint updates. Server-managed: ignored (stripped) on every write from a user.\n",
21173
+ "items": {
21174
+ "type": "string"
21175
+ }
21176
+ },
21177
+ "_lineage_id": {
21178
+ "type": "string",
21179
+ "description": "Stable id of this page in the blueprint source. Set once: a blueprint install or update (`page_upsert_mode=slug`, portal create, or a revision with `revision_source` `blueprint_sync`) may give a page the lineage it does not have yet, unless another page of the portal holds it. After that it never changes; every other write keeps the stored value. Locks nothing on its own.\n"
21180
+ },
21181
+ "detached_lineage_ids": {
21182
+ "type": "array",
21183
+ "readOnly": true,
21184
+ "description": "Lineage ids of blocks on this page that were detached from their blueprint; a blueprint sync leaves them alone. Server-managed.\n",
21185
+ "items": {
21186
+ "type": "string"
21187
+ }
20549
21188
  }
20550
21189
  }
20551
21190
  }
@@ -20667,6 +21306,17 @@
20667
21306
  }
20668
21307
  }
20669
21308
  },
21309
+ "passkeys": {
21310
+ "type": "object",
21311
+ "description": "Opt-in, set by the configuring user. When enabled, a second factor is enforced after the password and portal users can add passkeys (fingerprint, face or device PIN) as one variant of it, next to the email code of advanced_mfa. Users with a passkey use it by default; with advanced_mfa off, the passkey is the only second factor once a user has added one. On passwordless portals a passkey can replace the email code. Passkeys are bound to the domain they were added on. Default off; turning it off again restores the previous behaviour and keeps registered passkeys.\n",
21312
+ "properties": {
21313
+ "enabled": {
21314
+ "type": "boolean",
21315
+ "description": "Enforce a second factor and allow passkeys as one variant of it",
21316
+ "default": false
21317
+ }
21318
+ }
21319
+ },
20670
21320
  "entry_point": {
20671
21321
  "type": "string",
20672
21322
  "enum": [
@@ -20699,6 +21349,11 @@
20699
21349
  "example": "6bsd0jkgoie74k2i8mrhc1vest",
20700
21350
  "description": "Cognito user pool client ID"
20701
21351
  },
21352
+ "cognito_mobile_client_id": {
21353
+ "type": "string",
21354
+ "example": "3k9fj2l1m0n8b7v6c5x4z3a2s1",
21355
+ "description": "Cognito user pool client ID used by the native portal apps (longer refresh token validity)"
21356
+ },
20702
21357
  "cognito_user_pool_arn": {
20703
21358
  "type": "string",
20704
21359
  "example": "arn:aws:cognito-idp:us-east-1:123412341234:userpool/us-east-1_123412341",
@@ -20902,6 +21557,15 @@
20902
21557
  }
20903
21558
  ]
20904
21559
  },
21560
+ "self_assignment_mode": {
21561
+ "type": "string",
21562
+ "enum": [
21563
+ "contracts",
21564
+ "customer_to_portal_user"
21565
+ ],
21566
+ "default": "contracts",
21567
+ "description": "What the built-in self-assignment (without a contractIdentification hook) assigns once the `contract_identifiers` match a Contract:\n - `contracts`: the user's Contact is added as a customer of the Contract.\n - `customer_to_portal_user`: the Contract's customers (Contacts and Accounts) are added to the Portal User's `represents`. The Portal User sees all their data, including Contracts, transitively. Mirrors the `contact_to_portal_user` assignment mode of the contractIdentification hook.\n"
21568
+ },
20905
21569
  "contract_selector_config": {
20906
21570
  "type": "object",
20907
21571
  "description": "Configuration for contract selector in the portal",
@@ -21087,7 +21751,8 @@
21087
21751
  },
21088
21752
  "is_v3_item": {
21089
21753
  "type": "boolean",
21090
- "description": "Whether this is a v3 portal configuration"
21754
+ "deprecated": true,
21755
+ "description": "Always true. Legacy v2 storage was migrated; field kept for compatibility and will be removed."
21091
21756
  },
21092
21757
  "published_revision_id": {
21093
21758
  "type": "string",
@@ -21345,6 +22010,68 @@
21345
22010
  "portal_copilot_agent_id": {
21346
22011
  "type": "string",
21347
22012
  "description": "ID of the Agent Builder portal agent this portal's copilot chat runs, set by Agent Builder. When unset, or when the agent no longer exists in the portal's organization, the chat runs the default portal copilot."
22013
+ },
22014
+ "portal_copilot_visibility": {
22015
+ "$ref": "#/components/schemas/PortalCopilotVisibility"
22016
+ }
22017
+ }
22018
+ },
22019
+ "PortalCopilotVisibility": {
22020
+ "type": "object",
22021
+ "description": "Limits portal chat to portal users matching targets, with the same rules as page and block visibility. Without targets every signed-in portal user can use the chat.",
22022
+ "properties": {
22023
+ "visible_for_targets": {
22024
+ "type": "array",
22025
+ "items": {
22026
+ "type": "string"
22027
+ },
22028
+ "description": "Target IDs that must match for the portal user to use the chat"
22029
+ },
22030
+ "visible_for_operator": {
22031
+ "type": "string",
22032
+ "enum": [
22033
+ "and",
22034
+ "or"
22035
+ ],
22036
+ "description": "Operator for visible_for_targets matching"
22037
+ },
22038
+ "hidden_for_targets": {
22039
+ "type": "array",
22040
+ "items": {
22041
+ "type": "string"
22042
+ },
22043
+ "description": "Target IDs that if matched deny the portal user the chat"
22044
+ },
22045
+ "hidden_for_operator": {
22046
+ "type": "string",
22047
+ "enum": [
22048
+ "and",
22049
+ "or"
22050
+ ],
22051
+ "description": "Operator for hidden_for_targets matching"
22052
+ }
22053
+ }
22054
+ },
22055
+ "PortalUsingDesign": {
22056
+ "type": "object",
22057
+ "description": "A portal whose design_id is the design asked about.",
22058
+ "required": [
22059
+ "portal_id"
22060
+ ],
22061
+ "properties": {
22062
+ "portal_id": {
22063
+ "$ref": "#/components/schemas/PortalId"
22064
+ },
22065
+ "name": {
22066
+ "type": "string",
22067
+ "example": "Kundenportal"
22068
+ },
22069
+ "origin": {
22070
+ "$ref": "#/components/schemas/Origin"
22071
+ },
22072
+ "design_id": {
22073
+ "type": "string",
22074
+ "example": "9ba94f20-b872-11ed-b30a-b3e3bf2dc8ab"
21348
22075
  }
21349
22076
  }
21350
22077
  },
@@ -21537,6 +22264,69 @@
21537
22264
  "design_id": {
21538
22265
  "type": "string",
21539
22266
  "description": "Design of the portal, used to preload it from the served document"
22267
+ },
22268
+ "design": {
22269
+ "type": "object",
22270
+ "nullable": true,
22271
+ "description": "Portal design background and accent for the served document. `null` when the portal has no design or it could not be read.\n",
22272
+ "required": [
22273
+ "background",
22274
+ "accent",
22275
+ "last_modified_at"
22276
+ ],
22277
+ "properties": {
22278
+ "background": {
22279
+ "type": "object",
22280
+ "nullable": true,
22281
+ "description": "Spark background as hex colours; `null` when the design sets neither",
22282
+ "required": [
22283
+ "light",
22284
+ "dark"
22285
+ ],
22286
+ "properties": {
22287
+ "light": {
22288
+ "type": "string",
22289
+ "nullable": true,
22290
+ "pattern": "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$",
22291
+ "example": "#f5f5f5"
22292
+ },
22293
+ "dark": {
22294
+ "type": "string",
22295
+ "nullable": true,
22296
+ "pattern": "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$",
22297
+ "example": "#101010"
22298
+ }
22299
+ }
22300
+ },
22301
+ "accent": {
22302
+ "type": "object",
22303
+ "nullable": true,
22304
+ "description": "Spark accent as hex colours, falling back to the classic palette primary; dark falls back to light. `null` when the design sets neither\n",
22305
+ "required": [
22306
+ "light",
22307
+ "dark"
22308
+ ],
22309
+ "properties": {
22310
+ "light": {
22311
+ "type": "string",
22312
+ "nullable": true,
22313
+ "pattern": "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$",
22314
+ "example": "#005eb4"
22315
+ },
22316
+ "dark": {
22317
+ "type": "string",
22318
+ "nullable": true,
22319
+ "pattern": "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$",
22320
+ "example": "#4a9eff"
22321
+ }
22322
+ }
22323
+ },
22324
+ "last_modified_at": {
22325
+ "type": "string",
22326
+ "nullable": true,
22327
+ "description": "When the design was last saved"
22328
+ }
22329
+ }
21540
22330
  }
21541
22331
  }
21542
22332
  },
@@ -21776,6 +22566,51 @@
21776
22566
  }
21777
22567
  ]
21778
22568
  },
22569
+ "PortalRevisionPreview": {
22570
+ "type": "object",
22571
+ "required": [
22572
+ "preview_token",
22573
+ "expires_at",
22574
+ "url"
22575
+ ],
22576
+ "properties": {
22577
+ "preview_token": {
22578
+ "type": "string",
22579
+ "description": "Short-lived token that makes portal reads return this revision. Send it in the `x-portal-preview-token` header.\n"
22580
+ },
22581
+ "expires_at": {
22582
+ "type": "string",
22583
+ "format": "date-time",
22584
+ "description": "When the preview token stops being honoured."
22585
+ },
22586
+ "url": {
22587
+ "type": "string",
22588
+ "description": "Link that opens the revision in the portal on its live domain.",
22589
+ "example": "https://portal.example.com/?preview_token=eyJhbGciOiJIUzI1NiIs..."
22590
+ }
22591
+ }
22592
+ },
22593
+ "PreviewRevision": {
22594
+ "type": "object",
22595
+ "description": "The revision a config response shows instead of the live portal config.",
22596
+ "required": [
22597
+ "revision_id",
22598
+ "created_at"
22599
+ ],
22600
+ "properties": {
22601
+ "revision_id": {
22602
+ "type": "string",
22603
+ "example": "2026-08-25T14:03:11.482Z-a7f3c1d9"
22604
+ },
22605
+ "name": {
22606
+ "type": "string"
22607
+ },
22608
+ "created_at": {
22609
+ "type": "string",
22610
+ "format": "date-time"
22611
+ }
22612
+ }
22613
+ },
21779
22614
  "PortalRevision": {
21780
22615
  "allOf": [
21781
22616
  {
@@ -21846,6 +22681,13 @@
21846
22681
  "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",
21847
22682
  "items": {
21848
22683
  "type": "string"
22684
+ }
22685
+ },
22686
+ "held_back": {
22687
+ "type": "array",
22688
+ "description": "Only present when non-empty: blueprint-managed live pages/blocks this publish could not replace because the publisher is not the owning blueprint. Their live version stays as it is; the rest of the revision was published. Detach a node to make it publishable.\n",
22689
+ "items": {
22690
+ "$ref": "#/components/schemas/HeldBackNode"
21849
22691
  },
21850
22692
  "example": [
21851
22693
  "allowed_entities_change"