{
  "openapi": "3.0.3",
  "info": {
    "title": "Portal API",
    "description": "Backend for epilot portals - End Customer Portal & Installer Portal\n",
    "version": "1.0.0"
  },
  "tags": [
    {
      "name": "ECP",
      "description": "APIs defined for a portal user"
    },
    {
      "name": "ECP Admin",
      "description": "APIs defined for a ECP Admin"
    },
    {
      "name": "Public",
      "description": "Public APIs"
    },
    {
      "name": "portal_user_schema",
      "x-displayName": "Portal User",
      "description": "<SchemaDefinition schemaRef=\"#/components/schemas/PortalUser\" />\n"
    }
  ],
  "x-tagGroups": [
    {
      "name": "APIs",
      "tags": [
        "ECP",
        "ECP Admin",
        "Public"
      ]
    },
    {
      "name": "Schemas",
      "tags": [
        "portal_user_schema"
      ]
    }
  ],
  "security": [
    {
      "EpilotAuth": []
    },
    {
      "PortalAuth": []
    },
    {
      "EitherAuth": []
    }
  ],
  "paths": {
    "/v2/portal/portal": {
      "post": {
        "operationId": "upsertPortal",
        "summary": "upsertPortal",
        "description": "Upserts the settings for a portal of an organization.",
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "tags": [
          "ECP Admin"
        ],
        "parameters": [
          {
            "in": "query",
            "name": "origin",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/Origin"
            },
            "description": "Origin of the portal"
          },
          {
            "in": "query",
            "name": "portal_id",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Portal ID"
          }
        ],
        "requestBody": {
          "description": "Portal payload",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpsertPortalConfig"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Portal updated successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PortalConfig"
                }
              }
            }
          },
          "201": {
            "description": "Portal created successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PortalConfig"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/public/user": {
      "post": {
        "operationId": "createUser",
        "summary": "createUser",
        "description": "Registers a portal user",
        "deprecated": true,
        "security": [],
        "tags": [
          "Public"
        ],
        "parameters": [
          {
            "in": "query",
            "name": "origin",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/Origin"
            },
            "description": "Origin of the portal"
          }
        ],
        "requestBody": {
          "description": "Portal user payload",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateUserRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "User created successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "message",
                    "response"
                  ],
                  "properties": {
                    "message": {
                      "type": "string",
                      "enum": [
                        "User created successfully"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v3/portal/public/user": {
      "post": {
        "operationId": "createUserV3",
        "summary": "createUserV3",
        "description": "Registers a portal user.\nSupports two identification methods:\n1. Using portal_id\n2. Using domain\n",
        "security": [],
        "tags": [
          "Public"
        ],
        "parameters": [
          {
            "in": "query",
            "name": "portal_id",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/PortalId"
            },
            "description": "Portal ID (required if domain is not provided)"
          },
          {
            "in": "query",
            "name": "domain",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Portal domain for identification (alternative to portal_id)"
          }
        ],
        "requestBody": {
          "description": "Portal user payload",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateUserRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "User created successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "message",
                    "response"
                  ],
                  "properties": {
                    "message": {
                      "type": "string",
                      "enum": [
                        "User created successfully"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/token/validate": {
      "post": {
        "operationId": "validateToken",
        "summary": "validateToken",
        "description": "Validates Portal Token is valid. Pass the token via Authorization Header.",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "x-contact-identification-token": true,
        "responses": {
          "204": {
            "description": "Tokens is valid for the given organization."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/token/revoke": {
      "post": {
        "operationId": "revokeToken",
        "summary": "revokeToken",
        "description": "Revokes all of the access tokens for the given Refresh Token.",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "requestBody": {
          "description": "Token payload",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "refresh_token"
                ],
                "properties": {
                  "refresh_token": {
                    "type": "string",
                    "description": "Refresh Token to be revoked",
                    "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Access Tokens revoked successfully for the given Refresh Token.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "enum": [
                        "Token revoked successfully"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/public/config": {
      "get": {
        "operationId": "getPortalConfigByDomain",
        "summary": "getPortalConfigByDomain",
        "description": "Retrieves the portal configuration by domain.",
        "tags": [
          "Public"
        ],
        "security": [],
        "parameters": [
          {
            "in": "query",
            "name": "domain",
            "required": true,
            "schema": {
              "type": "string",
              "example": "example.com",
              "description": "Domain of the portal"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Portal config retrieved successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PortalConfig"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/config": {
      "get": {
        "operationId": "getPortalConfig",
        "summary": "getPortalConfig",
        "description": "Retrieves the portal configuration.",
        "tags": [
          "ECP Admin",
          "ECP"
        ],
        "parameters": [
          {
            "in": "query",
            "name": "origin",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/Origin"
            },
            "description": "Origin of the portal"
          }
        ],
        "security": [
          {
            "EitherAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Portal config retrieved successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PortalConfig"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      },
      "delete": {
        "operationId": "deletePortal",
        "summary": "deletePortal",
        "description": "Deletes the portal.",
        "tags": [
          "ECP Admin"
        ],
        "parameters": [
          {
            "in": "query",
            "name": "origin",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/Origin"
            },
            "description": "Origin of the portal"
          }
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "responses": {
          "204": {
            "description": "Portal deleted successfully."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v1/portal/exports": {
      "post": {
        "operationId": "createExport",
        "summary": "createExport",
        "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",
        "tags": [
          "ECP"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "schema",
                  "columns"
                ],
                "properties": {
                  "schema": {
                    "type": "string",
                    "description": "Entity schema to export (must be an allowed portal entity)."
                  },
                  "search": {
                    "type": "object",
                    "description": "Search parameters, same shape the entity list queries with.",
                    "properties": {
                      "q": {
                        "type": "string"
                      },
                      "q_fields": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      },
                      "filters": {
                        "type": "array",
                        "items": {
                          "type": "object"
                        }
                      },
                      "filters_context": {
                        "type": "array",
                        "items": {
                          "type": "object"
                        }
                      },
                      "sort": {}
                    }
                  },
                  "columns": {
                    "type": "array",
                    "description": "Ordered column definitions; order is the CSV column order.",
                    "items": {
                      "$ref": "#/components/schemas/PortalDataExportColumn"
                    }
                  },
                  "expand_over": {
                    "type": "string"
                  },
                  "language": {
                    "type": "string",
                    "example": "de"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "responses": {
          "202": {
            "description": "Export job accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobId": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "queued",
                        "running",
                        "ready",
                        "failed"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Missing or invalid export columns."
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v1/portal/exports/{jobId}": {
      "get": {
        "operationId": "getExport",
        "summary": "getExport",
        "description": "Get the status of an export job, including the download URL once ready.",
        "tags": [
          "ECP"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "jobId",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The export job id returned by createExport."
          }
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Export job status.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobId": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "queued",
                        "running",
                        "ready",
                        "failed",
                        "expired"
                      ]
                    },
                    "downloadUrl": {
                      "type": "string"
                    },
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Export job not found."
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/extensions": {
      "get": {
        "operationId": "getPortalExtensions",
        "summary": "getPortalExtensions",
        "description": "Retrieves the installed portal extensions.",
        "tags": [
          "ECP Admin"
        ],
        "parameters": [
          {
            "in": "query",
            "name": "origin",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/Origin"
            },
            "description": "Origin of the portal"
          }
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Portal extensions retrieved successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Extension"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/public/extensions": {
      "get": {
        "operationId": "getPublicPortalExtensionDetails",
        "summary": "getPublicPortalExtensionDetails",
        "description": "Get public extension details shown to end customers and configuring users.",
        "tags": [
          "Public"
        ],
        "security": [],
        "parameters": [
          {
            "in": "query",
            "name": "org_id",
            "required": true,
            "schema": {
              "type": "string",
              "example": 12324,
              "description": "Organization ID"
            }
          },
          {
            "in": "query",
            "name": "origin",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/Origin"
            },
            "description": "Origin of the portal"
          }
        ],
        "responses": {
          "200": {
            "description": "Public details of extensions.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicExtensionCapabilities"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v3/portal/extensions": {
      "get": {
        "operationId": "getPortalExtensionsV3",
        "summary": "getPortalExtensionsV3",
        "description": "Retrieves the installed portal extensions.",
        "tags": [
          "ECP Admin"
        ],
        "parameters": [
          {
            "in": "query",
            "name": "portal_id",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/PortalId"
            },
            "description": "Portal ID"
          }
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Portal extensions retrieved successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Extension"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v3/portal/public/extensions": {
      "get": {
        "operationId": "getPublicPortalExtensionDetailsV3",
        "summary": "getPublicPortalExtensionDetailsV3",
        "description": "Get public extension details shown to end customers and configuring users.\nSupports two identification methods:\n1. Using org_id + portal_id\n2. Using domain\n",
        "tags": [
          "Public"
        ],
        "security": [],
        "parameters": [
          {
            "in": "query",
            "name": "org_id",
            "required": false,
            "schema": {
              "type": "string",
              "example": 12324
            },
            "description": "Organization ID (required if domain is not provided)"
          },
          {
            "in": "query",
            "name": "portal_id",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/PortalId"
            },
            "description": "Portal ID (required if domain is not provided)"
          },
          {
            "in": "query",
            "name": "domain",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Portal domain for identification (alternative to org_id + portal_id)"
          }
        ],
        "responses": {
          "200": {
            "description": "Public details of extensions.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicExtensionCapabilities"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/consumption": {
      "get": {
        "operationId": "getConsumption",
        "summary": "getConsumption",
        "description": "Get energy consumption data between a given time period.",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "app_id",
            "schema": {
              "type": "string"
            },
            "required": false,
            "description": "App ID for consumption data."
          },
          {
            "in": "query",
            "name": "extensionId",
            "schema": {
              "type": "string"
            },
            "required": true,
            "description": "Extension ID for consumption data."
          },
          {
            "in": "query",
            "name": "hookId",
            "schema": {
              "type": "string"
            },
            "required": false,
            "description": "Optional Hook ID. If omitted, the only matching hook on the extension is used; if the extension has multiple hooks of the relevant type, this becomes required."
          },
          {
            "in": "query",
            "name": "meter_id",
            "schema": {
              "type": "string"
            },
            "required": false,
            "deprecated": true,
            "description": "Meter ID for consumption data. Deprecated - use context_entities instead."
          },
          {
            "in": "query",
            "name": "from",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "required": true,
            "description": "Start date for consumption data (ISO 8601 format)."
          },
          {
            "in": "query",
            "name": "to",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "required": true,
            "description": "End date for consumption data (ISO 8601 format)."
          },
          {
            "in": "query",
            "name": "interval",
            "schema": {
              "type": "string",
              "enum": [
                "PT15M",
                "PT1H",
                "P1D",
                "P1M",
                "P1Y",
                "custom"
              ]
            },
            "required": true,
            "description": "Interval between consumption data points (e.g., PT15M for 15 minutes, PT1H for hourly). Not all intervals have to be supported. `custom` is period-based retrieval for sources that advertise it in their `visualizationMetadata.intervals`: the App returns one record per period it has data for within `from`..`to`, each carrying `period`, and the portal renders every record as its own bar.\n"
          },
          {
            "in": "query",
            "name": "context_entities",
            "required": false,
            "description": "Additional entities to include in the context for variable interpolation in the hook.",
            "schema": {
              "$ref": "#/components/schemas/ContextEntities"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Consumption data returned successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "consumptions": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "timestamp": {
                            "type": "string",
                            "format": "date-time",
                            "description": "ISO 8601 timestamp of the consumption record."
                          },
                          "value": {
                            "type": "number",
                            "description": "The consumption value."
                          },
                          "type": {
                            "type": "string",
                            "description": "Optional type of the consumption, such as 'nt' (night time) and 'ht' (high time) or 'feed-in' and 'feed-out'. Can be any string.",
                            "example": "nt"
                          },
                          "unit": {
                            "type": "string",
                            "description": "Optional unit of the consumption value. Defaults to unit present on the relevant Meter Counter.",
                            "example": "kWh"
                          },
                          "label": {
                            "type": "object",
                            "additionalProperties": {
                              "type": "string"
                            },
                            "description": "Optional localized label for this individual value, keyed by ISO 3166-1 alpha-2 language code (same shape as `VisualizationTypeOption.label`). When present, the portal renders it as the data point label instead of the default timestamp-derived label (e.g. to name billing periods or tariff windows).\n",
                            "example": {
                              "en": "Billing period 1",
                              "de": "Abrechnungszeitraum 1"
                            }
                          },
                          "period": {
                            "type": "object",
                            "description": "The date range this value covers. Required for period-based sources (`interval=custom`), whose records don't sit on a fixed time grid: the portal renders one bar per record, orders them by `period.from`, and — unless `label` is set — labels each bar with the formatted `from` - `to` range. Ignored for interval-based retrieval.\n",
                            "properties": {
                              "from": {
                                "type": "string",
                                "format": "date-time",
                                "description": "Start of the covered period."
                              },
                              "to": {
                                "type": "string",
                                "format": "date-time",
                                "description": "End of the covered period. Shown as given in the fallback label, so pass the date the period visibly ends on (consecutive periods may share this boundary).\n"
                              }
                            },
                            "required": [
                              "from",
                              "to"
                            ],
                            "example": {
                              "from": "2024-01-03T00:00:00.000Z",
                              "to": "2025-01-05T00:00:00.000Z"
                            }
                          }
                        },
                        "required": [
                          "value",
                          "timestamp"
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/visualization:export": {
      "post": {
        "operationId": "prepareVisualizationExport",
        "summary": "prepareVisualizationExport",
        "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",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "app_id": {
                    "type": "string",
                    "description": "App ID providing the dataExport hook."
                  },
                  "extension_id": {
                    "type": "string",
                    "description": "Extension ID providing the dataExport hook."
                  },
                  "hook_id": {
                    "type": "string",
                    "description": "Optional Hook ID. If omitted, the only `dataExport` hook on the extension is used; if the extension has multiple `dataExport` hooks, this becomes required."
                  },
                  "from": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Optional start date for the export window (ISO 8601 format)."
                  },
                  "to": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Optional end date for the export window (ISO 8601 format)."
                  },
                  "context_entities": {
                    "$ref": "#/components/schemas/ContextEntities"
                  }
                },
                "required": [
                  "app_id",
                  "extension_id"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Export descriptor returned by the App.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "download_url": {
                      "type": "string",
                      "description": "URL the client can use to download the exported file. May be a pre-signed or short-lived URL."
                    },
                    "filename": {
                      "type": "string",
                      "description": "Suggested filename for the exported file."
                    },
                    "content_type": {
                      "type": "string",
                      "description": "MIME type of the exported file.",
                      "example": "text/csv"
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "Optional expiration timestamp for the download URL."
                    }
                  },
                  "required": [
                    "download_url"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/visualization/metadata": {
      "get": {
        "operationId": "getVisualizationMetadata",
        "summary": "getVisualizationMetadata",
        "description": "Returns runtime metadata describing how a visualization (consumption / price / cost chart) should be rendered for a given portal context (meter, contract, etc). Resolves the extension's `visualizationMetadata` hook implicitly from `app_id` + `extensionId` and invokes it. Supplies the response as a structured payload that the portal uses to configure type/aggregation options, supported intervals, and the available data range.\n",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "app_id",
            "schema": {
              "type": "string"
            },
            "required": true,
            "description": "App ID providing the visualizationMetadata hook."
          },
          {
            "in": "query",
            "name": "extensionId",
            "schema": {
              "type": "string"
            },
            "required": true,
            "description": "Extension ID providing the visualizationMetadata hook."
          },
          {
            "in": "query",
            "name": "context_entities",
            "required": false,
            "description": "Entities to include in the context for variable interpolation in the hook (typically the meter and any other entities that scope the visualization).",
            "schema": {
              "$ref": "#/components/schemas/ContextEntities"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Visualization metadata returned successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VisualizationMetadata"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/costs": {
      "get": {
        "operationId": "getCosts",
        "summary": "getCosts",
        "description": "Get energy cost data between a given time period.",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "app_id",
            "schema": {
              "type": "string"
            },
            "required": false,
            "description": "App ID for consumption data."
          },
          {
            "in": "query",
            "name": "extensionId",
            "schema": {
              "type": "string"
            },
            "required": true,
            "description": "Extension ID for cost data."
          },
          {
            "in": "query",
            "name": "hookId",
            "schema": {
              "type": "string"
            },
            "required": false,
            "description": "Optional Hook ID. If omitted, the only matching hook on the extension is used; if the extension has multiple hooks of the relevant type, this becomes required."
          },
          {
            "in": "query",
            "name": "meter_id",
            "schema": {
              "type": "string"
            },
            "required": false,
            "deprecated": true,
            "description": "Meter ID for cost data. Deprecated - use context_entities instead."
          },
          {
            "in": "query",
            "name": "from",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "required": true,
            "description": "Start date for cost data (ISO 8601 format)."
          },
          {
            "in": "query",
            "name": "to",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "required": true,
            "description": "End date for cost data (ISO 8601 format)."
          },
          {
            "in": "query",
            "name": "interval",
            "schema": {
              "type": "string",
              "enum": [
                "PT15M",
                "PT1H",
                "P1D",
                "P1M",
                "P1Y"
              ]
            },
            "required": true,
            "description": "Interval between cost data points (e.g., PT15M for 15 minutes, PT1H for hourly). Not all intervals have to be supported."
          },
          {
            "in": "query",
            "name": "context_entities",
            "required": false,
            "description": "Additional entities to include in the context for variable interpolation in the hook.",
            "schema": {
              "$ref": "#/components/schemas/ContextEntities"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Cost data returned successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "costs": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "timestamp": {
                            "type": "string",
                            "format": "date-time",
                            "description": "ISO 8601 timestamp of the cost record."
                          },
                          "unit_amount": {
                            "type": "integer",
                            "description": "Cost in cents, e.g. 1234 for 12,34 €.",
                            "example": 1234
                          },
                          "unit_amount_currency": {
                            "type": "string",
                            "description": "ISO 4217:2015 currency.",
                            "example": "EUR"
                          },
                          "unit_amount_decimal": {
                            "type": "string",
                            "description": "Cost in decimal format, e.g. \"12.34\".",
                            "example": "12.34"
                          },
                          "tax_behavior": {
                            "type": "string",
                            "description": "Is the tax (typically Value Added Tax) included in the amounts. Typically should NOT be included - exclusive of tax.",
                            "enum": [
                              "inclusive",
                              "exclusive"
                            ],
                            "example": "exclusive"
                          },
                          "tax_rate": {
                            "type": "integer",
                            "description": "Tax rate in percent, e.g. 19 for 19%.",
                            "example": 19
                          }
                        },
                        "required": [
                          "timestamp",
                          "unit_amount",
                          "unit_amount_currency",
                          "unit_amount_decimal",
                          "tax_behavior",
                          "tax_rate"
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/prices": {
      "get": {
        "operationId": "getPrices",
        "summary": "getPrices",
        "description": "Get energy prices data between a given time period.",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "app_id",
            "schema": {
              "type": "string"
            },
            "required": false,
            "description": "App ID for consumption data."
          },
          {
            "in": "query",
            "name": "extensionId",
            "schema": {
              "type": "string"
            },
            "required": true,
            "description": "Extension ID for price data."
          },
          {
            "in": "query",
            "name": "hookId",
            "schema": {
              "type": "string"
            },
            "required": false,
            "description": "Optional Hook ID. If omitted, the only matching hook on the extension is used; if the extension has multiple hooks of the relevant type, this becomes required."
          },
          {
            "in": "query",
            "name": "meter_id",
            "schema": {
              "type": "string"
            },
            "required": false,
            "deprecated": true,
            "description": "Meter ID for price data. Deprecated - use context_entities instead."
          },
          {
            "in": "query",
            "name": "from",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "required": true,
            "description": "Start date for price data (ISO 8601 format)."
          },
          {
            "in": "query",
            "name": "to",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "required": true,
            "description": "End date for price data (ISO 8601 format)."
          },
          {
            "in": "query",
            "name": "interval",
            "schema": {
              "type": "string",
              "enum": [
                "PT15M",
                "PT1H",
                "P1D",
                "P1M",
                "P1Y"
              ]
            },
            "required": true,
            "description": "Interval between price data points (e.g., PT15M for 15 minutes, PT1H for hourly). Not all intervals have to be supported."
          },
          {
            "in": "query",
            "name": "context_entities",
            "required": false,
            "description": "Additional entities to include in the context for variable interpolation in the hook.",
            "schema": {
              "$ref": "#/components/schemas/ContextEntities"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Price data returned successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "prices": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "timestamp": {
                            "type": "string",
                            "format": "date-time",
                            "description": "ISO 8601 timestamp of the price record."
                          },
                          "unit_amount": {
                            "type": "integer",
                            "description": "Cost in cents, e.g. 1234 for 12,34 €.",
                            "example": 1234
                          },
                          "unit_amount_currency": {
                            "type": "string",
                            "description": "ISO 4217:2015 currency.",
                            "example": "EUR"
                          },
                          "unit_amount_decimal": {
                            "type": "string",
                            "description": "Cost in decimal format, e.g. \"12.34\".",
                            "example": "12.34"
                          },
                          "components": {
                            "type": "object",
                            "description": "Optional price components.",
                            "properties": {
                              "auction_price_amount": {
                                "type": "integer",
                                "description": "Market price in cents, e.g. 1000 for 10,00 €.",
                                "example": 1000
                              },
                              "auction_price_amount_decimal": {
                                "type": "string",
                                "description": "Market price in decimal format, e.g. \"10.00\".",
                                "example": "10.00"
                              },
                              "taxes_levies_amount": {
                                "type": "integer",
                                "description": "Taxes/Levies other than tax specified on the price level in cents, e.g. 50 for 00,50 €.",
                                "example": 50
                              },
                              "taxes_levies_amount_decimal": {
                                "type": "string",
                                "description": "Taxes/Levies other than tax specified on the price level in decimal format, e.g. \"0.50\".",
                                "example": "0.50"
                              },
                              "source_fee_amount": {
                                "type": "integer",
                                "description": "Fee associated with the source, e.g. Green Energy Certificate fee in cents, e.g. 50 for 00,50 €.",
                                "example": 50
                              },
                              "source_fee_amount_decimal": {
                                "type": "string",
                                "description": "Fee associated with the source, e.g. Green Energy Certificate fee in decimal format, e.g. \"0.50\".",
                                "example": "0.50"
                              },
                              "grid_fee_amount": {
                                "type": "integer",
                                "description": "Fee associated with the transmission/distribution in cents, e.g. 100 for 1,00 €.",
                                "example": 100
                              },
                              "grid_fee_amount_decimal": {
                                "type": "string",
                                "description": "Fee associated with the transmission/distribution in decimal format, e.g. \"1.00\".",
                                "example": "1.00"
                              },
                              "margin_amount": {
                                "type": "integer",
                                "description": "Margin in cents, e.g. 34 for 0,34 €.",
                                "example": 34
                              },
                              "margin_amount_decimal": {
                                "type": "string",
                                "description": "Margin in decimal format, e.g. \"0.34\".",
                                "example": "0.34"
                              }
                            }
                          },
                          "tax_behavior": {
                            "type": "string",
                            "description": "Is the tax (typically Value Added Tax) included in the amounts. Typically should NOT be included - exclusive of tax.",
                            "enum": [
                              "inclusive",
                              "exclusive"
                            ],
                            "example": "exclusive"
                          },
                          "tax_rate": {
                            "type": "integer",
                            "description": "Tax rate in percent, e.g. 19 for 19%.",
                            "example": 19
                          }
                        },
                        "required": [
                          "timestamp",
                          "unit_amount",
                          "unit_amount_currency",
                          "unit_amount_decimal",
                          "tax_behavior",
                          "tax_rate"
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/external-links": {
      "get": {
        "operationId": "getExternalLinks",
        "summary": "getExternalLinks",
        "description": "Retrieves the portal configuration external links.",
        "tags": [
          "ECP Admin",
          "ECP"
        ],
        "parameters": [
          {
            "in": "query",
            "name": "origin",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/Origin"
            },
            "description": "Origin of the portal"
          },
          {
            "in": "query",
            "name": "contactId",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/EntityId"
            },
            "description": "Contact ID of the user"
          }
        ],
        "security": [
          {
            "EitherAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Portal config links retrieved successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ExternalLink"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/resolve:external-link/{id}": {
      "get": {
        "operationId": "getResolvedExternalLink",
        "summary": "getResolvedExternalLink",
        "deprecated": true,
        "description": "Retrieves a resolved portal external link. Use /v2/portal/resolve:seamless-link/{id} instead.",
        "tags": [
          "ECP Admin",
          "ECP"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/EntityId"
            },
            "description": "ID of the External Link"
          },
          {
            "in": "query",
            "name": "origin",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/Origin"
            },
            "description": "Origin of the portal"
          },
          {
            "in": "query",
            "name": "contactId",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/EntityId"
            },
            "description": "Contact ID of the user"
          }
        ],
        "security": [
          {
            "EitherAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Resolved portal config link retrieved successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalLink"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v3/portal/external-links": {
      "get": {
        "operationId": "getExternalLinksV3",
        "summary": "getExternalLinksV3",
        "description": "Retrieves the portal configuration external links.",
        "tags": [
          "ECP Admin",
          "ECP"
        ],
        "parameters": [
          {
            "in": "query",
            "name": "portal_id",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/PortalId"
            },
            "description": "PortalId of the portal"
          },
          {
            "in": "query",
            "name": "contactId",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/EntityId"
            },
            "description": "Contact ID of the user"
          }
        ],
        "security": [
          {
            "EitherAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Portal config links retrieved successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ExternalLink"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v3/portal/resolve:external-link/{id}": {
      "get": {
        "operationId": "getResolvedExternalLinkV3",
        "summary": "getResolvedExternalLinkV3",
        "deprecated": true,
        "description": "Retrieves a resolved portal external link. Use /v2/portal/resolve:seamless-link/{id} instead.",
        "tags": [
          "ECP Admin",
          "ECP"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/EntityId"
            },
            "description": "ID of the External Link"
          },
          {
            "in": "query",
            "name": "portal_id",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/PortalId"
            },
            "description": "PortalId of the portal"
          },
          {
            "in": "query",
            "name": "contactId",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/EntityId"
            },
            "description": "Contact ID of the user"
          }
        ],
        "security": [
          {
            "EitherAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Resolved portal config link retrieved successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalLink"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/resolve:seamless-link": {
      "get": {
        "operationId": "getResolvedSeamlessLink",
        "summary": "getResolvedSeamlessLink",
        "description": "Retrieves a resolved seamless portal link.",
        "tags": [
          "ECP"
        ],
        "parameters": [
          {
            "in": "query",
            "name": "app_id",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "ID of the App if the Portal Extension was installed from an App"
          },
          {
            "in": "query",
            "name": "extension_id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID of the Portal Extension"
          },
          {
            "in": "query",
            "name": "link_id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID of the Seamless Link"
          },
          {
            "in": "query",
            "name": "context_entities",
            "required": false,
            "description": "If the request is in a context of certain entities (i.e. the user in in a context of a specific contract), links can be customized for that. Portal User and Contact entities are automatically part of the context.",
            "schema": {
              "$ref": "#/components/schemas/ContextEntities"
            }
          }
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Resolved portal seamless link retrieved successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "link": {
                      "type": "string",
                      "description": "Resolved link"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/public/portal/config": {
      "get": {
        "operationId": "getPublicPortalConfig",
        "summary": "getPublicPortalConfig",
        "description": "Retrieves the public portal configuration.",
        "tags": [
          "Public"
        ],
        "security": [],
        "parameters": [
          {
            "in": "query",
            "name": "org_id",
            "required": true,
            "schema": {
              "type": "string",
              "example": 12324,
              "description": "Organization ID"
            }
          },
          {
            "in": "query",
            "name": "origin",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/Origin"
            },
            "description": "Origin of the portal"
          }
        ],
        "responses": {
          "200": {
            "description": "Portal config retrieved successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PortalConfig"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/org/portal/config": {
      "get": {
        "operationId": "getOrgPortalConfig",
        "summary": "getOrgPortalConfig",
        "description": "Retrieves the portal configuration for the organization.",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "origin",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/Origin"
            },
            "description": "Origin of the portal"
          }
        ],
        "responses": {
          "200": {
            "description": "Portal config retrieved successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/PortalConfig"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "certificate_details": {
                          "type": "object",
                          "properties": {
                            "status": {
                              "type": "string",
                              "description": "Status of the certificate",
                              "enum": [
                                "PENDING_VALIDATION",
                                "ISSUED",
                                "INACTIVE",
                                "EXPIRED",
                                "VALIDATION_TIMED_OUT",
                                "REVOKED",
                                "FAILED",
                                "PENDING_AUTO_RENEWAL"
                              ]
                            },
                            "failed_reason": {
                              "type": "string",
                              "description": "Reason for failed certificate",
                              "example": "CAA_ERROR"
                            }
                          }
                        },
                        "identity_providers": {
                          "type": "array",
                          "description": "SSO identity providers configured for the portal. Includes raw\n`oidc_config.client_secret` values so the admin UI can round-trip\nthem. Customers are encouraged to reference secrets via env\ntemplates (`{{ env.VAR }}`) rather than embed raw values.\n",
                          "items": {
                            "$ref": "#/components/schemas/ProviderConfig"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v3/portal/public/portal/config": {
      "get": {
        "operationId": "getPublicPortalConfigV3",
        "summary": "getPublicPortalConfigV3",
        "description": "Retrieves the public portal configuration.",
        "tags": [
          "Public"
        ],
        "security": [],
        "parameters": [
          {
            "in": "query",
            "name": "org_id",
            "required": true,
            "schema": {
              "type": "string",
              "example": 12324,
              "description": "Organization ID"
            }
          },
          {
            "in": "query",
            "name": "portal_id",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/PortalId"
            },
            "description": "PortalId of the portal"
          }
        ],
        "responses": {
          "200": {
            "description": "Portal config retrieved successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/PortalConfig"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "identity_providers": {
                          "type": "array",
                          "description": "SSO identity providers configured for the portal, reduced to the\nfields needed to render provider login buttons. Omitted when the\nportal has no providers.\n",
                          "items": {
                            "$ref": "#/components/schemas/PublicIdentityProvider"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v3/portal/org/portal/config": {
      "get": {
        "operationId": "getOrgPortalConfigV3",
        "summary": "getOrgPortalConfigV3",
        "description": "Retrieves the portal configuration for the organization.",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "portal_id",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/PortalId"
            },
            "description": "PortalId of the portal"
          }
        ],
        "responses": {
          "200": {
            "description": "Portal config retrieved successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/PortalConfig"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "certificate_details": {
                          "type": "object",
                          "properties": {
                            "status": {
                              "type": "string",
                              "description": "Status of the certificate",
                              "enum": [
                                "PENDING_VALIDATION",
                                "ISSUED",
                                "INACTIVE",
                                "EXPIRED",
                                "VALIDATION_TIMED_OUT",
                                "REVOKED",
                                "FAILED",
                                "PENDING_AUTO_RENEWAL"
                              ]
                            },
                            "failed_reason": {
                              "type": "string",
                              "description": "Reason for failed certificate",
                              "example": "CAA_ERROR"
                            }
                          }
                        },
                        "identity_providers": {
                          "type": "array",
                          "description": "SSO identity providers configured for the portal. Includes raw\n`oidc_config.client_secret` values so the admin UI can round-trip\nthem. Customers are encouraged to reference secrets via env\ntemplates (`{{ env.VAR }}`) rather than embed raw values.\n",
                          "items": {
                            "$ref": "#/components/schemas/ProviderConfig"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/configs": {
      "get": {
        "operationId": "getAllPortalConfigs",
        "summary": "getAllPortalConfigs",
        "description": "Retrieves all portal configurations.",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "All portal configs retrieved successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PortalConfig"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/email-templates": {
      "post": {
        "operationId": "upsertEmailTemplates",
        "summary": "upsertEmailTemplates",
        "description": "Upserts the email templates of a portal",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "origin",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/Origin"
            },
            "description": "Origin of the portal"
          }
        ],
        "requestBody": {
          "description": "Email templates payload",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmailTemplates"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Upserted email templates of the portal successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "message",
                    "emailTemplates"
                  ],
                  "properties": {
                    "message": {
                      "type": "string",
                      "enum": [
                        "Email Templates upserted successfully"
                      ]
                    },
                    "emailTemplates": {
                      "$ref": "#/components/schemas/EmailTemplates"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      },
      "get": {
        "operationId": "getEmailTemplates",
        "summary": "getEmailTemplates",
        "description": "Retrieves the email templates of a portal",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "origin",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/Origin"
            },
            "description": "Origin of the portal"
          }
        ],
        "responses": {
          "200": {
            "description": "Retrieved the email templates successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmailTemplates"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v3/portal/email-templates:migrate-references": {
      "post": {
        "operationId": "migrateEmailTemplateReferences",
        "summary": "migrateEmailTemplateReferences",
        "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",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "requestBody": {
          "description": "Source and destination template ids",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "source_template_id",
                  "destination_template_id"
                ],
                "properties": {
                  "source_template_id": {
                    "type": "string",
                    "description": "Template id currently referenced on portal rows"
                  },
                  "destination_template_id": {
                    "type": "string",
                    "description": "Template id to write in place of the source"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Migration completed (may have partial failures in failed_portal_ids).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "migrated_portal_count",
                    "migrated_portal_ids",
                    "failed_portal_ids"
                  ],
                  "properties": {
                    "migrated_portal_count": {
                      "type": "integer",
                      "example": 2
                    },
                    "migrated_portal_ids": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "failed_portal_ids": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v3/portal/email-templates:list-references": {
      "post": {
        "operationId": "listEmailTemplateReferences",
        "summary": "listEmailTemplateReferences",
        "description": "Read-only sibling of migrateEmailTemplateReferences. Lists every portal in\nthe caller's org whose `email_templates` config references `template_id`,\nwithout rewriting anything. Used by the email-template MFE to show which\nportals a template affects (in template settings and as a pre-migrate\npreview). Uses the same discovery as the migrate path.\n",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "requestBody": {
          "description": "Template id to look up references for",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "template_id"
                ],
                "properties": {
                  "template_id": {
                    "type": "string",
                    "description": "Email template id to find portal references for"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Portals referencing the template.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "portals"
                  ],
                  "properties": {
                    "portals": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id"
                        ],
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string",
                            "nullable": true,
                            "description": "Portal display name (or domain); falls back to id when unavailable"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v3/portal/email-templates/{portal_id}": {
      "post": {
        "operationId": "upsertEmailTemplatesByPortalId",
        "summary": "upsertEmailTemplatesByPortalId",
        "description": "Upserts the email templates of a portal by portal ID",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "portal_id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID of the portal"
          }
        ],
        "requestBody": {
          "description": "Email templates payload",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmailTemplates"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Upserted email templates of the portal successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "message",
                    "emailTemplates"
                  ],
                  "properties": {
                    "message": {
                      "type": "string",
                      "enum": [
                        "Email Templates upserted successfully"
                      ]
                    },
                    "emailTemplates": {
                      "$ref": "#/components/schemas/EmailTemplates"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      },
      "get": {
        "operationId": "getEmailTemplatesByPortalId",
        "summary": "getEmailTemplatesByPortalId",
        "description": "Retrieves the email templates of a portal by portal ID",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "portal_id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID of the portal"
          }
        ],
        "responses": {
          "200": {
            "description": "Retrieved the email templates successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmailTemplates"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/public-widgets": {
      "get": {
        "operationId": "getPublicPortalWidgets",
        "summary": "getPublicPortalWidgets",
        "description": "Retrieves the public widgets of a portal.",
        "deprecated": true,
        "tags": [
          "Public"
        ],
        "security": [],
        "parameters": [
          {
            "in": "query",
            "name": "org_id",
            "required": true,
            "schema": {
              "type": "string",
              "description": "Organization ID",
              "example": 123
            }
          },
          {
            "in": "query",
            "name": "origin",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/Origin"
            },
            "description": "Origin of the portal"
          }
        ],
        "responses": {
          "200": {
            "description": "Retrieved the portal public widgets successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UpsertPortalWidget"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v3/portal/public/widgets": {
      "get": {
        "operationId": "getPublicPortalWidgetsV3",
        "summary": "getPublicPortalWidgetsV3",
        "description": "Retrieves the public widgets of a portal.\nSupports two identification methods:\n1. Using org_id + portal_id\n2. Using domain\n",
        "tags": [
          "Public"
        ],
        "security": [],
        "parameters": [
          {
            "in": "query",
            "name": "org_id",
            "required": false,
            "schema": {
              "type": "string",
              "example": 12324
            },
            "description": "Organization ID (required if domain is not provided)"
          },
          {
            "in": "query",
            "name": "portal_id",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/PortalId"
            },
            "description": "Portal ID (required if domain is not provided)"
          },
          {
            "in": "query",
            "name": "domain",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Portal domain for identification (alternative to org_id + portal_id)"
          }
        ],
        "responses": {
          "200": {
            "description": "Retrieved the portal public widgets successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UpsertPortalWidget"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/widgets": {
      "post": {
        "operationId": "upsertPortalWidget",
        "summary": "upsertPortalWidget",
        "description": "Upsert widget for a portal of an organization.",
        "deprecated": true,
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "tags": [
          "ECP Admin"
        ],
        "parameters": [
          {
            "in": "query",
            "name": "origin",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/Origin"
            },
            "description": "Origin of the portal"
          }
        ],
        "requestBody": {
          "description": "Portal widgets payload",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpsertPortalWidget"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Portal widget configuration upserted successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UpsertPortalWidget"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      },
      "get": {
        "operationId": "getPortalWidgets",
        "summary": "getPortalWidgets",
        "description": "Retrieves the widgets of a portal.",
        "deprecated": true,
        "tags": [
          "ECP Admin",
          "ECP"
        ],
        "security": [
          {
            "EitherAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "origin",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/Origin"
            },
            "description": "Origin of the portal"
          },
          {
            "in": "query",
            "name": "contract_id",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/EntityId"
            },
            "description": "Contract context for widgets"
          }
        ],
        "responses": {
          "200": {
            "description": "Retrieved the portal widgets successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UpsertPortalWidget"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v3/portal/widgets": {
      "get": {
        "operationId": "getPortalWidgetsV3",
        "summary": "getPortalWidgetsV3",
        "description": "Retrieves the widgets of a portal by portal_id.",
        "tags": [
          "ECP Admin",
          "ECP"
        ],
        "security": [
          {
            "EitherAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "portal_id",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/PortalId"
            },
            "description": "PortalId of the portal"
          },
          {
            "in": "query",
            "name": "contract_id",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/EntityId"
            },
            "description": "Contract context for widgets"
          }
        ],
        "responses": {
          "200": {
            "description": "Retrieved the portal widgets successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UpsertPortalWidget"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      },
      "post": {
        "operationId": "upsertPortalWidgetV3",
        "summary": "upsertPortalWidgetV3",
        "description": "Upsert widget for a portal by portal_id.",
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "tags": [
          "ECP Admin"
        ],
        "parameters": [
          {
            "in": "query",
            "name": "portal_id",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/PortalId"
            },
            "description": "PortalId of the portal"
          }
        ],
        "requestBody": {
          "description": "Portal widgets payload",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpsertPortalWidget"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Portal widget configuration upserted successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UpsertPortalWidget"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v3/portal/portal/files": {
      "post": {
        "operationId": "savePortalFilesV3",
        "summary": "savePortalFilesV3",
        "description": "Add files to portal by portal_id",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "portal_id",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/PortalId"
            },
            "description": "PortalId of the portal"
          }
        ],
        "requestBody": {
          "description": "portal files request",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SavePortalFile"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The files have been saved to the portal successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "createdFiles": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/File"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/replace-ecp-template-variables": {
      "post": {
        "operationId": "replaceECPTemplateVariables",
        "summary": "replaceECPTemplateVariables",
        "description": "Replaces the template variables of a portal",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "requestBody": {
          "description": "ECPVariables payload",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "template_id": {
                    "$ref": "#/components/schemas/EntityId",
                    "description": "ID of the template"
                  },
                  "entity_context": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "object",
                      "properties": {
                        "_id": {
                          "$ref": "#/components/schemas/EntityId",
                          "description": "ID of the entity"
                        },
                        "is_main_entity": {
                          "type": "boolean"
                        },
                        "journey_registration_email": {
                          "type": "string",
                          "description": "The email address registered in the journey's Login & Registration block"
                        },
                        "journey_registration_portal_id": {
                          "type": "string",
                          "description": "The portal selected on the journey's Login & Registration block, used to target the registration link to that portal"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Replaced portal template variables successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "customerPortal": {
                      "type": "object",
                      "properties": {
                        "invitationLink": {
                          "type": "string",
                          "example": "https://end-customer-portal.ecp.dev.epilot.io/register?contactId=7aa44fb8-d60e-40cc-9a3a-ba09a1ff7f51&email=john@doe.com"
                        },
                        "newDocumentLink": {
                          "type": "string",
                          "example": "https://end-customer-portal.ecp.dev.epilot.io/requests/opportunities/b8fef220-abe0-4382-a704-26848f60977b"
                        },
                        "entityLink": {
                          "type": "string",
                          "example": "https://end-customer-portal.ecp.dev.epilot.io/requests/opportunities/b8fef220-abe0-4382-a704-26848f60977b"
                        },
                        "userEmailsOnEntity": {
                          "type": "array",
                          "example": [
                            "john@doe.com",
                            "mary@doe.com"
                          ],
                          "items": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "installerPortal": {
                      "type": "object",
                      "properties": {
                        "invitationLink": {
                          "type": "string",
                          "example": "https://installer-portal.ecp.dev.epilot.io/register?contactId=7aa44fb8-d60e-40cc-9a3a-ba09a1ff7f51&email=john@doe.com"
                        },
                        "newDocumentLink": {
                          "type": "string",
                          "example": "https://installer-portal.ecp.dev.epilot.io/requests/opportunities/b8fef220-abe0-4382-a704-26848f60977b"
                        },
                        "entityLink": {
                          "type": "string",
                          "example": "https://installer-portal.ecp.dev.epilot.io/requests/opportunities/b8fef220-abe0-4382-a704-26848f60977b"
                        },
                        "userEmailsOnEntity": {
                          "type": "array",
                          "example": [
                            "peter@doe.com",
                            "jane@doe.com"
                          ],
                          "items": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "portalUser": {
                      "type": "object",
                      "properties": {
                        "invitationLink": {
                          "type": "string",
                          "example": "https://employee-portal.ecp.dev.epilot.io/register?contactId=7aa44fb8-d60e-40cc-9a3a-ba09a1ff7f51&email=john@doe.com"
                        },
                        "newDocumentLink": {
                          "type": "string",
                          "example": "https://employee-portal.ecp.dev.epilot.io/requests/opportunities/b8fef220-abe0-4382-a704-26848f60977b"
                        },
                        "entityLink": {
                          "type": "string",
                          "example": "https://employee-portal.ecp.dev.epilot.io/requests/opportunities/b8fef220-abe0-4382-a704-26848f60977b"
                        },
                        "userEmailsOnEntity": {
                          "type": "array",
                          "example": [
                            "peter@doe.com",
                            "jane@doe.com"
                          ],
                          "items": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/org/settings": {
      "get": {
        "operationId": "getOrganizationSettings",
        "summary": "getOrganizationSettings",
        "description": "Retrieves the organization settings. This endpoint is deprecated, use org_settings from the portal config or getOrganizationSettingsByDomain instead.",
        "deprecated": true,
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Retrieved the settings for an organization successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/OrganizationSettings"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/schemas": {
      "get": {
        "operationId": "getSchemas",
        "summary": "getSchemas",
        "description": "Retrieves the schemas. Only schemas usable in the private part of the portal are returned.",
        "x-contact-identification-token": true,
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Retrieved schemas for an organization successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "schemas": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Schema"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/public/schemas": {
      "get": {
        "operationId": "getSchemasByDomain",
        "summary": "getSchemasByDomain",
        "description": "Retrieves schemas by domain. Only schemas and attributes used on public pages are returned.",
        "tags": [
          "Public"
        ],
        "security": [],
        "parameters": [
          {
            "in": "query",
            "name": "domain",
            "required": true,
            "schema": {
              "type": "string",
              "description": "Domain of the portal",
              "example": "ecp.dev.epilot.io"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Retrieved schemas for an organization successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "schemas": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Schema"
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v3/portal/public/schemas": {
      "get": {
        "operationId": "getPublicSchemasV3",
        "summary": "getPublicSchemasV3",
        "description": "Retrieves schemas by portal. Only schemas and attributes used on public pages are returned.\nSupports two identification methods:\n1. Using org_id + portal_id\n2. Using domain\n",
        "tags": [
          "Public"
        ],
        "security": [],
        "parameters": [
          {
            "in": "query",
            "name": "org_id",
            "required": false,
            "schema": {
              "type": "string",
              "example": 12324
            },
            "description": "Organization ID (required if domain is not provided)"
          },
          {
            "in": "query",
            "name": "portal_id",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/PortalId"
            },
            "description": "Portal ID (required if domain is not provided)"
          },
          {
            "in": "query",
            "name": "domain",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Portal domain for identification (alternative to org_id + portal_id)"
          }
        ],
        "responses": {
          "200": {
            "description": "Retrieved schemas for an organization successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "schemas": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Schema"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/public/org/settings": {
      "get": {
        "operationId": "getOrganizationSettingsByDomain",
        "summary": "getOrganizationSettingsByDomain",
        "description": "Retrieves organization settings by domain. Only public organization settings are returned.",
        "tags": [
          "Public"
        ],
        "security": [],
        "parameters": [
          {
            "in": "query",
            "name": "domain",
            "required": true,
            "schema": {
              "type": "string",
              "description": "Domain of the portal",
              "example": "ecp.dev.epilot.io"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Retrieved organization settings successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/OrganizationSettings"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/extra-permission-attributes": {
      "get": {
        "operationId": "extraPermissionAttributes",
        "summary": "extraPermissionAttributes",
        "description": "Retrieves the extra permission attributes.",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Retrieved extra permission attributes successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "contact": {
                          "$ref": "#/components/schemas/ExtraSchemaAttributes"
                        },
                        "contract": {
                          "$ref": "#/components/schemas/ExtraSchemaAttributes"
                        },
                        "order": {
                          "$ref": "#/components/schemas/ExtraSchemaAttributes"
                        },
                        "opportunity": {
                          "$ref": "#/components/schemas/ExtraSchemaAttributes"
                        },
                        "meter": {
                          "$ref": "#/components/schemas/ExtraSchemaAttributes"
                        },
                        "meter_counter": {
                          "$ref": "#/components/schemas/ExtraSchemaAttributes"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/validate/caa-records": {
      "post": {
        "operationId": "validateCaaRecords",
        "summary": "validateCaaRecords",
        "description": "Validates the CAA records of a portal",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "origin",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/Origin"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Validated CAA records successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "retry": {
                      "type": "boolean",
                      "description": "Whether to retry the validation to continue the domain setup"
                    },
                    "message": {
                      "type": "string",
                      "description": "Message of the validation"
                    },
                    "isDNSConfigured": {
                      "type": "boolean",
                      "description": "Whether the DNS is configured from the customer side"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v3/portal/validate/caa-records": {
      "post": {
        "operationId": "validateCaaRecordsV3",
        "summary": "validateCaaRecordsV3",
        "description": "Validates the CAA records of a portal",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "portal_id",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/PortalId"
            },
            "description": "PortalId of the portal"
          }
        ],
        "responses": {
          "200": {
            "description": "Validated CAA records successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "retry": {
                      "type": "boolean",
                      "description": "Whether to retry the validation to continue the domain setup"
                    },
                    "message": {
                      "type": "string",
                      "description": "Message of the validation"
                    },
                    "isDNSConfigured": {
                      "type": "boolean",
                      "description": "Whether the DNS is configured from the customer side"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/contact": {
      "get": {
        "operationId": "getContact",
        "summary": "getContact",
        "description": "Retrieves the contact of the logged in user.",
        "x-contact-identification-token": true,
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Retrieves the mapped contact of the logged in user successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "entity": {
                      "$ref": "#/components/schemas/Contact"
                    },
                    "files": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/File"
                      }
                    },
                    "relations": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EntityItem"
                      }
                    },
                    "journey_actions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/JourneyActions"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      },
      "patch": {
        "operationId": "updateContact",
        "summary": "updateContact",
        "description": "Updates the contact details.",
        "deprecated": true,
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Entity"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated the contact details successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Contact"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenByRule"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/ecp/contact": {
      "get": {
        "operationId": "getECPContact",
        "summary": "getECPContact",
        "description": "Get the Contact by id",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "id",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/EntityId"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The contact returned successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Contact"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/public/contact/exists": {
      "post": {
        "operationId": "checkContactExists",
        "summary": "checkContactExists",
        "description": "True if contact with given identifiers exists.",
        "security": [],
        "tags": [
          "Public"
        ],
        "parameters": [
          {
            "in": "query",
            "name": "origin",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/Origin"
            },
            "description": "Origin of the portal"
          }
        ],
        "requestBody": {
          "description": "Request payload",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactExistsRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Return result true|false if the contact with given identifiers exists.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "exists": {
                      "type": "boolean",
                      "description": "Whether the contact exists with the given identifier values",
                      "example": true
                    },
                    "contactId": {
                      "$ref": "#/components/schemas/EntityId",
                      "description": "ID of the contact if exists"
                    },
                    "reason": {
                      "type": "string",
                      "enum": [
                        "TIMEOUT",
                        "NOT_FOUND"
                      ],
                      "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"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v3/portal/public/contact/exists": {
      "post": {
        "operationId": "checkContactExistsV3",
        "summary": "checkContactExistsV3",
        "description": "True if contact with given identifiers exists.\nSupports two identification methods:\n1. Using portal_id\n2. Using domain\n",
        "security": [],
        "tags": [
          "Public"
        ],
        "parameters": [
          {
            "in": "query",
            "name": "portal_id",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/PortalId"
            },
            "description": "PortalId of the portal (required if domain is not provided)"
          },
          {
            "in": "query",
            "name": "domain",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Portal domain for identification (alternative to portal_id)"
          }
        ],
        "requestBody": {
          "description": "Request payload",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactExistsRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Return result true|false if the contact with given identifiers exists.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "exists": {
                      "type": "boolean",
                      "description": "Whether the contact exists with the given identifier values",
                      "example": true
                    },
                    "contactId": {
                      "$ref": "#/components/schemas/EntityId",
                      "description": "ID of the contact if exists"
                    },
                    "accountId": {
                      "$ref": "#/components/schemas/EntityId",
                      "description": "ID of the resolved account when the portal is configured for account-based registration"
                    },
                    "reason": {
                      "type": "string",
                      "enum": [
                        "TIMEOUT",
                        "NOT_FOUND"
                      ],
                      "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"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v3/portal/public/contact/identify": {
      "post": {
        "operationId": "identifyContact",
        "summary": "identifyContact",
        "description": "Identify a contact by the portal's configured registration identifiers and, on a match,\nissue a short-lived bearer token that acts as that contact.\n\nResolution is identical to `checkContactExistsV3`. The token does not grant a portal\nsession; it is accepted only on the operations listed in `allowed_operations`, and\nexpires at `expires_at`. Requires `surface_id` to name a surface of the portal whose\n`authentication` is `registration_identifiers`; returns 403 otherwise. The token is\nconfined to that surface's data access. Requests may be rate limited (429).\n",
        "security": [],
        "tags": [
          "Public"
        ],
        "parameters": [
          {
            "in": "query",
            "name": "portal_id",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/PortalId"
            },
            "description": "PortalId of the portal (required if domain is not provided)"
          },
          {
            "in": "query",
            "name": "domain",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Portal domain for identification (alternative to portal_id)"
          }
        ],
        "requestBody": {
          "description": "Request payload",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactIdentifyRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactIdentifyResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v3/portal/public/account/exists": {
      "post": {
        "operationId": "checkAccountExists",
        "summary": "checkAccountExists",
        "description": "True if account with given identifiers exists.\nSupports two identification methods:\n1. Using portal_id\n2. Using domain\n",
        "security": [],
        "tags": [
          "Public"
        ],
        "parameters": [
          {
            "in": "query",
            "name": "portal_id",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/PortalId"
            },
            "description": "PortalId of the portal (required if domain is not provided)"
          },
          {
            "in": "query",
            "name": "domain",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Portal domain for identification (alternative to portal_id)"
          }
        ],
        "requestBody": {
          "description": "Request payload",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AccountExistsRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Return result true|false if the account with given identifiers exists.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "exists": {
                      "type": "boolean",
                      "description": "Whether the account exists with the given identifier values",
                      "example": true
                    },
                    "accountId": {
                      "$ref": "#/components/schemas/EntityId",
                      "description": "ID of the account if exists"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/contact/valid/secondary/attributes": {
      "get": {
        "operationId": "getValidSecondaryAttributes",
        "summary": "getValidSecondaryAttributes",
        "description": "Get valid secondary attributes that are used while mapping a contact on registration",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Valid secondary attributes for the contact entity are returned successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string",
                            "description": "Name of the secondary attribute",
                            "example": "first_name"
                          },
                          "type": {
                            "type": "string",
                            "description": "Type of the secondary attribute",
                            "example": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/user": {
      "get": {
        "operationId": "getPortalUser",
        "summary": "getPortalUser",
        "description": "Get the portal user details",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Portal user returned successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/PortalUser"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      },
      "patch": {
        "operationId": "updatePortalUser",
        "summary": "updatePortalUser",
        "description": "Update the portal user details",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Entity"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The portal user updated successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/PortalUser"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      },
      "delete": {
        "operationId": "deletePortalUser",
        "summary": "deletePortalUser",
        "description": "Delete the portal user",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Portal user deleted successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "description": "`User Succesfully Deleted` when the user was deleted, or `Account deletion requested`\nwhen an asynchronous deleteAccount portal extension hook handed the deletion over to a third party.\n",
                      "example": "User Succesfully Deleted"
                    },
                    "data": {
                      "$ref": "#/components/schemas/EntityId"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/user/update/email": {
      "put": {
        "operationId": "updatePortalUserEmail",
        "summary": "updatePortalUserEmail",
        "description": "Update portal user email",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "requestBody": {
          "description": "Request payload",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "description": "New email address of the portal user",
                    "example": "john@doe.com"
                  },
                  "password": {
                    "type": "string",
                    "description": "Password of the portal user for confirmation.\nRequired unless a `changeEmail` portal extension hook with `require_password_confirmation` disabled is configured for the portal,\nor the portal user's identity is managed by an identity provider - an SSO login has no password to confirm.\n"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Portal user email updated successfully.\nThe user receives a confirmation email, unless a `changeEmail` portal extension hook with\n`change_mode` set to `synchronous` changed the login email right away.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "description": "`You will receive a confirmation mail soon on your updated email address.` for the built-in flow,\n`Your email change request has been received.` when a changeEmail portal extension hook handed\nthe change over to a third party, or `Your email has been changed.` when a synchronous changeEmail\nportal extension hook changed the login email immediately - the portal user has to sign in again\nwith the new email address in that case.\n",
                      "example": "You will receive a confirmation mail soon on your updated email address."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/user/change/password": {
      "put": {
        "operationId": "changePortalUserPassword",
        "summary": "changePortalUserPassword",
        "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",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "requestBody": {
          "description": "Request payload",
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "new_password": {
                    "type": "string",
                    "description": "New password chosen by the portal user.\nRequired when the configured `changePassword` hook has `require_new_password` enabled, ignored otherwise.\n"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The password change request was handed over to the third-party system.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "No changePassword portal extension hook is configured for this portal."
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/user/resend/confirmation-email/{id}": {
      "post": {
        "operationId": "resendConfirmationEmail",
        "summary": "resendConfirmationEmail",
        "description": "Resend confirmation email",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "$ref": "#/components/schemas/EntityId"
            },
            "required": true,
            "description": "The ID of portal user id"
          }
        ],
        "responses": {
          "200": {
            "description": "Confirmation email resent successfully for the portal user.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "enum": [
                        "Confirmation email sent successfully."
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/users/by-related-entity": {
      "get": {
        "operationId": "fetchPortalUsersByRelatedEntity",
        "summary": "fetchPortalUsersByRelatedEntity",
        "description": "Get all users for a given entity",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "entity_id",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/EntityId"
            }
          },
          {
            "in": "query",
            "name": "slug",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/EntitySlug"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Returns the portal users under the given entity.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "portalUsers": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PortalUser"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/user/confirm": {
      "get": {
        "operationId": "confirmUser",
        "summary": "confirmUser",
        "description": "Confirm a portal user",
        "tags": [
          "Public"
        ],
        "security": [],
        "parameters": [
          {
            "in": "query",
            "name": "confirmation_link_token",
            "required": true,
            "schema": {
              "type": "string",
              "description": "Confirmation link token"
            }
          },
          {
            "in": "query",
            "name": "use_redirect",
            "required": false,
            "schema": {
              "type": "boolean",
              "description": "Should the operation result in a 301 redirect",
              "default": true
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Returned in case of successful user confirmation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "confirmed": {
                      "type": "boolean",
                      "description": "Is the user confirmed",
                      "example": true
                    },
                    "user_already_confirmed": {
                      "type": "boolean",
                      "description": "Is the user previously confirmed",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "301": {
            "description": "User is confirmed and redirected to the portal successfully."
          },
          "400": {
            "$ref": "#/components/responses/ConfirmUserInvalidRequest"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/user/confirm/{id}": {
      "get": {
        "operationId": "confirmUserWithUserId",
        "summary": "confirmUserWithUserId",
        "description": "Confirm a portal user",
        "tags": [
          "Public"
        ],
        "security": [],
        "parameters": [
          {
            "in": "query",
            "name": "origin",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/Origin"
            }
          },
          {
            "in": "path",
            "name": "id",
            "schema": {
              "$ref": "#/components/schemas/EntityId"
            },
            "required": true,
            "description": "The ID of portal user id"
          },
          {
            "in": "query",
            "name": "org_id",
            "required": true,
            "schema": {
              "type": "string",
              "example": 123
            },
            "description": "Organization ID"
          }
        ],
        "responses": {
          "301": {
            "description": "User is confirmed and redirected to the portal successfully."
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/public/user/exists": {
      "get": {
        "operationId": "userExists",
        "summary": "userExists",
        "description": "Checks whether a user exists in the portal",
        "tags": [
          "Public"
        ],
        "security": [],
        "parameters": [
          {
            "in": "query",
            "name": "email",
            "required": true,
            "schema": {
              "type": "string",
              "description": "Email Address of the portal user",
              "example": "user@example.com"
            }
          },
          {
            "in": "query",
            "name": "org_id",
            "required": true,
            "schema": {
              "type": "string",
              "description": "Organization ID",
              "example": 123
            }
          },
          {
            "in": "query",
            "name": "origin",
            "required": false,
            "description": "Checkes if user exists in the given portal origin. If not provided, checks in all origins.",
            "schema": {
              "$ref": "#/components/schemas/Origin"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Returned whether the user exists in the portal or not successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "exists"
                  ],
                  "properties": {
                    "exists": {
                      "type": "boolean",
                      "description": "Whether the user exists in the portal",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v3/portal/public/user/exists": {
      "get": {
        "operationId": "userExistsV3",
        "summary": "userExistsV3",
        "description": "Checks whether a user exists in the portal.\nSupports two identification methods:\n1. Using org_id + portal_id\n2. Using domain\n",
        "tags": [
          "Public"
        ],
        "security": [],
        "parameters": [
          {
            "in": "query",
            "name": "email",
            "required": true,
            "schema": {
              "type": "string",
              "description": "Email Address of the portal user",
              "example": "user@example.com"
            }
          },
          {
            "in": "query",
            "name": "org_id",
            "required": false,
            "schema": {
              "type": "string",
              "description": "Organization ID (required if domain is not provided)",
              "example": 123
            }
          },
          {
            "in": "query",
            "name": "portal_id",
            "required": false,
            "description": "Checks if user exists in the given portal ID. If not provided, checks in all portals.",
            "schema": {
              "$ref": "#/components/schemas/PortalId"
            }
          },
          {
            "in": "query",
            "name": "domain",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Portal domain for identification (alternative to org_id + portal_id)"
          }
        ],
        "responses": {
          "200": {
            "description": "Returned whether the user exists in the portal or not successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "exists"
                  ],
                  "properties": {
                    "exists": {
                      "type": "boolean",
                      "description": "Whether the user exists in the portal",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/recipients-to-notify": {
      "post": {
        "operationId": "getRecipientsToNotifyOnAutomation",
        "summary": "getRecipientsToNotifyOnAutomation",
        "description": "Get recipients to notify on automation",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "emails",
                  "template_id",
                  "context_id"
                ],
                "properties": {
                  "emails": {
                    "type": "array",
                    "example": [
                      "john@doe.com"
                    ],
                    "description": "Emails array that are part of the automation",
                    "items": {
                      "type": "string"
                    }
                  },
                  "template_id": {
                    "$ref": "#/components/schemas/EntityId",
                    "description": "Email template ID that used on the automation"
                  },
                  "context_id": {
                    "$ref": "#/components/schemas/EntityId",
                    "description": "Entity ID that is part of the automation"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Returns the valid recipients to notify successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "emails"
                  ],
                  "properties": {
                    "recipients": {
                      "type": "array",
                      "description": "Filtered recipients to notify",
                      "items": {
                        "type": "object",
                        "required": [
                          "email",
                          "recipient_id"
                        ],
                        "properties": {
                          "email": {
                            "type": "string",
                            "description": "Email of the recipient",
                            "example": "john@doe.com"
                          },
                          "recipient_id": {
                            "$ref": "#/components/schemas/EntityId",
                            "description": "ID of the recipient"
                          }
                        }
                      }
                    },
                    "message": {
                      "type": "string",
                      "description": "Reason to not notify the user"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/configure-distribution": {
      "get": {
        "operationId": "configureDistribution",
        "summary": "configureDistribution",
        "description": "Configure the distribution for the portal's custom domain",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "origin",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/Origin"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The cloudfront distribution has been configure successfully for the custom domain.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "domainName": {
                      "type": "string",
                      "description": "The domain name of the configured distribution",
                      "example": "dsj8op4ha01jha23.cloudfront.net"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v3/portal/configure-distribution": {
      "get": {
        "operationId": "configureDistributionV3",
        "summary": "configureDistributionV3",
        "description": "Configure the distribution for the portal's custom domain",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "portal_id",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/PortalId"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The cloudfront distribution has been configure successfully for the custom domain.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "domainName": {
                      "type": "string",
                      "description": "The domain name of the configured distribution",
                      "example": "dsj8op4ha01jha23.cloudfront.net"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/order": {
      "get": {
        "operationId": "getAllOrders",
        "summary": "getAllOrders",
        "description": "Get all orders for the portal user. Use searchPortalUserEntities instead.",
        "deprecated": true,
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "from",
            "required": false,
            "schema": {
              "type": "number",
              "description": "Initial offset to set for the search results",
              "example": 0,
              "default": 0
            }
          },
          {
            "in": "query",
            "name": "size",
            "required": false,
            "schema": {
              "type": "number",
              "description": "Size of the search results",
              "example": 100,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The orders for the portal user returned successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/Order"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "journey_actions": {
                                "$ref": "#/components/schemas/JourneyActions"
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/order/{id}/acceptance": {
      "post": {
        "operationId": "postOrderAcceptance",
        "summary": "postOrderAcceptance",
        "description": "Accept/decline an offer by id",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "$ref": "#/components/schemas/EntityId"
            },
            "required": true,
            "description": "The ID of order"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AcceptanceDecision"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Order was accepted/declined successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Order"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/order/{id}": {
      "get": {
        "operationId": "getOrder",
        "summary": "getOrder",
        "description": "Get an order by id. Use getPortalUserEntity instead.",
        "deprecated": true,
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "$ref": "#/components/schemas/EntityId"
            },
            "required": true,
            "description": "The ID of order"
          }
        ],
        "responses": {
          "200": {
            "description": "The requested order has been retrieved successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "entity": {
                      "$ref": "#/components/schemas/Order"
                    },
                    "files": {
                      "type": "array",
                      "description": "The related files of the requested order",
                      "items": {
                        "$ref": "#/components/schemas/File"
                      }
                    },
                    "relations": {
                      "type": "array",
                      "description": "The related entities of the requested order",
                      "items": {
                        "$ref": "#/components/schemas/EntityItem"
                      }
                    },
                    "products": {
                      "type": "array",
                      "description": "The related products of the requested order",
                      "items": {
                        "$ref": "#/components/schemas/Product"
                      }
                    },
                    "crossSellableProducts": {
                      "type": "array",
                      "description": "The related cross sellable products of the requested order",
                      "items": {
                        "$ref": "#/components/schemas/Product"
                      }
                    },
                    "workflow": {
                      "type": "array",
                      "description": "The related workflows of the requested order",
                      "items": {
                        "$ref": "#/components/schemas/WorkflowExecution"
                      }
                    },
                    "journey_actions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/JourneyActions"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      },
      "patch": {
        "operationId": "updateOrder",
        "summary": "updateOrder",
        "description": "Update an order by id. Use getPortalUserEntity instead.",
        "deprecated": true,
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "$ref": "#/components/schemas/EntityId"
            },
            "required": true,
            "description": "The ID of order"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Entity"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated the order details successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Order"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/opportunity": {
      "get": {
        "operationId": "getAllOpportunities",
        "summary": "getAllOpportunities",
        "description": "Get all opportunities of a portal user. Use searchPortalUserEntities instead.",
        "deprecated": true,
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "from",
            "required": false,
            "schema": {
              "type": "number",
              "description": "Initial offset to set for the search results",
              "example": 0,
              "default": 0
            }
          },
          {
            "in": "query",
            "name": "size",
            "required": false,
            "schema": {
              "type": "number",
              "description": "Size of the search results",
              "example": 100,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The opportunities is retrieved successfully for the portal user.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/Opportunity"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "journey_actions": {
                                "$ref": "#/components/schemas/JourneyActions"
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/opportunities/searchable-attributes": {
      "get": {
        "operationId": "getSearchableAttributesForOpportunities",
        "summary": "getSearchableAttributesForOpportunities",
        "description": "Get all opportunity searchable attributes for a portal user. Use searchPortalUserEntities instead.",
        "deprecated": true,
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "from",
            "required": false,
            "schema": {
              "type": "number",
              "description": "Initial offset to set for the search results",
              "example": 0,
              "default": 0
            }
          },
          {
            "in": "query",
            "name": "size",
            "required": false,
            "schema": {
              "type": "number",
              "description": "Size of the search results",
              "example": 1000,
              "default": 1000
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The opportunities is retrieved successfully for the portal user.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Opportunity"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/opportunities/search": {
      "post": {
        "operationId": "getSearchResultsForOpportunities",
        "summary": "getSearchResultsForOpportunities",
        "description": "Get all opportunity with the given searched attributes. Use searchPortalUserEntities instead.",
        "deprecated": true,
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "from",
            "required": false,
            "schema": {
              "type": "number",
              "description": "Initial offset to set for the search results",
              "example": 0,
              "default": 0
            }
          },
          {
            "in": "query",
            "name": "size",
            "required": false,
            "schema": {
              "type": "number",
              "description": "Size of the search results",
              "example": 1000,
              "default": 1000
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "addresses": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "customers": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "purposes": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/EntityId"
                    }
                  },
                  "workflows": {
                    "type": "array",
                    "items": {
                      "items": {
                        "$ref": "#/components/schemas/WorfklowIdentifier"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The opportunities is retrieved successfully for the portal user.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Opportunity"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/opportunities/{id}": {
      "get": {
        "operationId": "getOpportunity",
        "summary": "getOpportunity",
        "description": "Get an opportunity by id. Use getPortalUserEntity instead.",
        "deprecated": true,
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "$ref": "#/components/schemas/EntityId"
            },
            "required": true,
            "description": "The ID of opportunity"
          }
        ],
        "responses": {
          "200": {
            "description": "The returned opportunities",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "entity": {
                      "$ref": "#/components/schemas/Opportunity"
                    },
                    "orders": {
                      "type": "array",
                      "description": "The related orders of the requested opportunity",
                      "items": {
                        "$ref": "#/components/schemas/Order"
                      }
                    },
                    "files": {
                      "type": "array",
                      "description": "The related files of the requested opportunity",
                      "items": {
                        "$ref": "#/components/schemas/File"
                      }
                    },
                    "relations": {
                      "type": "array",
                      "description": "The related entities of the requested opportunity",
                      "items": {
                        "$ref": "#/components/schemas/EntityItem"
                      }
                    },
                    "workflow": {
                      "type": "array",
                      "description": "The related workflows of the requested opportunity",
                      "items": {
                        "$ref": "#/components/schemas/WorkflowExecution"
                      }
                    },
                    "journey_actions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/JourneyActions"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      },
      "patch": {
        "operationId": "updateOpportunity",
        "summary": "updateOpportunity",
        "description": "Update an opportunity by id.",
        "deprecated": true,
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "$ref": "#/components/schemas/EntityId"
            },
            "required": true,
            "description": "The ID of opportunity"
          }
        ],
        "requestBody": {
          "description": "Requested opportunity body to update",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Entity"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated the opportunity successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Opportunity"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/request": {
      "get": {
        "operationId": "getAllRequests",
        "summary": "getAllRequests",
        "description": "Get all opportunities & orders of a portal user. Use searchPortalUserEntities instead.",
        "deprecated": true,
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "from",
            "required": false,
            "schema": {
              "type": "number",
              "description": "Initial offset to set for the search results",
              "example": 0,
              "default": 0
            }
          },
          {
            "in": "query",
            "name": "size",
            "required": false,
            "schema": {
              "type": "number",
              "description": "Size of the search results",
              "example": 100,
              "default": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The opportunities is retrieved successfully for the portal user.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "hits": {
                      "type": "number",
                      "description": "Total number of hits",
                      "example": 1
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "type": "object",
                            "oneOf": [
                              {
                                "$ref": "#/components/schemas/Opportunity"
                              },
                              {
                                "$ref": "#/components/schemas/Order"
                              }
                            ]
                          },
                          {
                            "type": "object",
                            "properties": {
                              "journey_actions": {
                                "$ref": "#/components/schemas/JourneyActions"
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/contract": {
      "get": {
        "operationId": "getAllContracts",
        "summary": "getAllContracts",
        "description": "Get all contracts for a portal user. Use searchPortalUserEntities instead.",
        "deprecated": true,
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "from",
            "required": false,
            "schema": {
              "type": "number",
              "description": "Initial offset to set for the search results",
              "example": 0,
              "default": 0
            }
          },
          {
            "in": "query",
            "name": "size",
            "required": false,
            "schema": {
              "type": "number",
              "description": "Size of the search results",
              "example": 100,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Contracts have been retrieved successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/Contract"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "journey_actions": {
                                "$ref": "#/components/schemas/JourneyActions"
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/contract/{id}": {
      "get": {
        "operationId": "getContract",
        "summary": "getContract",
        "description": "Get a contract by id. Use getPortalUserEntity instead.",
        "deprecated": true,
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "$ref": "#/components/schemas/EntityId"
            },
            "required": true,
            "description": "The ID of the contract"
          }
        ],
        "responses": {
          "200": {
            "description": "The requested contract returned successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "entity": {
                      "$ref": "#/components/schemas/Contract"
                    },
                    "orders": {
                      "type": "array",
                      "description": "The related orders of the requested contract",
                      "items": {
                        "$ref": "#/components/schemas/Order"
                      }
                    },
                    "meters": {
                      "type": "array",
                      "description": "The related meters of the requested contract",
                      "items": {
                        "$ref": "#/components/schemas/Meter"
                      }
                    },
                    "files": {
                      "type": "array",
                      "description": "The related files of the requested contract",
                      "items": {
                        "$ref": "#/components/schemas/File"
                      }
                    },
                    "relations": {
                      "type": "array",
                      "description": "The related entities of the requested contract",
                      "items": {
                        "$ref": "#/components/schemas/EntityItem"
                      }
                    },
                    "workflow": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WorkflowExecution"
                      }
                    },
                    "journey_actions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/JourneyActions"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      },
      "patch": {
        "operationId": "updateContract",
        "summary": "updateContract",
        "description": "Update a contract by id. Use getPortalUserEntity instead.",
        "deprecated": true,
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "$ref": "#/components/schemas/EntityId"
            },
            "required": true,
            "description": "The ID of the contract"
          }
        ],
        "requestBody": {
          "description": "Requested contract body to update",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Entity"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated the contract successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Contract"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenByRule"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/contract/{id}/resolve-templates": {
      "post": {
        "operationId": "getContractWithTemplates",
        "summary": "getContractWithTemplates",
        "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.",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "$ref": "#/components/schemas/EntityId"
            },
            "required": true,
            "description": "The ID of the contract"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "templates": {
                    "type": "object",
                    "deprecated": true,
                    "additionalProperties": {
                      "type": "string"
                    },
                    "description": "DEPRECATED — client-supplied map of content field name to Handlebars template string, resolved per related meter and returned as templates_output on the meter. Use `templates_ref` instead; ignored when `templates_ref` is present and rejected once the org has the `portals-reject-client-templates` flag enabled."
                  },
                  "templates_ref": {
                    "$ref": "#/components/schemas/TemplatesRef",
                    "description": "Reference to the meter selector block whose admin-configured content provides the templates, derived server-side."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The requested contract returned successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "entity": {
                      "$ref": "#/components/schemas/Contract"
                    },
                    "orders": {
                      "type": "array",
                      "description": "The related orders of the requested contract",
                      "items": {
                        "$ref": "#/components/schemas/Order"
                      }
                    },
                    "meters": {
                      "type": "array",
                      "description": "The related meters of the requested contract",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/Meter"
                          },
                          {
                            "$ref": "#/components/schemas/EntityTemplates"
                          }
                        ]
                      }
                    },
                    "files": {
                      "type": "array",
                      "description": "The related files of the requested contract",
                      "items": {
                        "$ref": "#/components/schemas/File"
                      }
                    },
                    "relations": {
                      "type": "array",
                      "description": "The related entities of the requested contract",
                      "items": {
                        "$ref": "#/components/schemas/EntityItem"
                      }
                    },
                    "workflow": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WorkflowExecution"
                      }
                    },
                    "journey_actions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/JourneyActions"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/contract/by-identifiers": {
      "post": {
        "operationId": "addContractByIdentifiers",
        "summary": "addContractByIdentifiers",
        "description": "Self-assign contract(s) by pre-configured identifiers.",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "requestBody": {
          "description": "Identifiers to search by. Have to be pre-configured in Portal Configuration Permissions.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Identifier-value pairs per schema to identify the contract",
                "additionalProperties": {
                  "type": "object",
                  "additionalProperties": {
                    "type": "string"
                  }
                },
                "example": {
                  "contract": {
                    "contract_number": "123456"
                  },
                  "meter": {
                    "meter_number": "123456"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Contract(s) assigned successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EntityItem"
                      }
                    },
                    "hits": {
                      "type": "number",
                      "minimum": 0
                    }
                  },
                  "required": [
                    "hits"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/ContractAssignmentConflict"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/entity/identifiers/{slug}": {
      "get": {
        "operationId": "getEntityIdentifiers",
        "summary": "getEntityIdentifiers",
        "description": "Retrieve a list of entity identifiers used for entity search by portal users.",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "slug",
            "schema": {
              "$ref": "#/components/schemas/EntitySlug"
            },
            "required": true,
            "description": "The slug of an entity"
          }
        ],
        "responses": {
          "200": {
            "description": "The identifiers of the requested entity returned successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string",
                            "description": "The name of the identifier",
                            "example": "contract_number"
                          },
                          "type": {
                            "type": "string",
                            "description": "The type of the identifier",
                            "example": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/entity/{slug}/{id}/activity": {
      "get": {
        "operationId": "getEntityActivityFeed",
        "summary": "getEntityActivityFeed",
        "description": "Get activity feed for an entity\n",
        "tags": [
          "Activity"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "slug",
            "description": "Entity Type",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/EntitySlug"
            }
          },
          {
            "in": "path",
            "name": "id",
            "description": "Entity id",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/EntityId"
            }
          },
          {
            "in": "query",
            "name": "after",
            "description": "Get activities after this timestamp",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "in": "query",
            "name": "before",
            "description": "get activities before this timestamp",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "in": "query",
            "name": "from",
            "description": "start from page",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "in": "query",
            "name": "size",
            "description": "max number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 25
            }
          },
          {
            "in": "query",
            "name": "type",
            "description": "Filter by activity type",
            "schema": {
              "type": "string",
              "example": "SyncActivity"
            }
          },
          {
            "in": "query",
            "name": "include_relations",
            "description": "Include activities from related entities",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "total": {
                      "type": "integer",
                      "example": 1
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ActivityItem"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v2/portal/{slug}/{id}:validateRule": {
      "get": {
        "operationId": "validateCadenceEntityEditRules",
        "summary": "validateCadenceEntityEditRules",
        "description": "Validate if cadence rule is valid for an entity\n",
        "tags": [
          "ECP"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "slug",
            "description": "Entity Type",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/EntitySlug"
            }
          },
          {
            "in": "path",
            "name": "id",
            "description": "Entity id",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/EntityId"
            }
          },
          {
            "in": "query",
            "name": "attribute",
            "description": "Get activities after this timestamp",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "isBlockedByRules": {
                      "type": "boolean",
                      "example": false
                    },
                    "failedRule": {
                      "$ref": "#/components/schemas/EntityEditRule"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v2/portal/entities-by-payment/{id}": {
      "get": {
        "operationId": "searchPaymentRelationsInEntities",
        "summary": "searchPaymentRelationsInEntities",
        "description": "Search for entities that have the payment relation with the given payment id\n",
        "tags": [
          "ECP"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "description": "Entity id",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/EntityId"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Entities that have the payment relation.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "results": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EntityItem"
                      }
                    },
                    "hits": {
                      "type": "number",
                      "description": "Total number of files for entities found",
                      "example": 50
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v2/portal/entity/activity": {
      "put": {
        "operationId": "createCustomEntityActivity",
        "summary": "createCustomEntityActivity",
        "description": "Create a custom activity that can be displayed in activity feed of an entity.",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "entities",
            "description": "Comma-separated list of entities which the activity primarily concerns. Deprecated - ignored as the list of entities is automatically determined now.",
            "deprecated": true,
            "style": "form",
            "schema": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/EntityId"
              },
              "minItems": 1,
              "uniqueItems": true
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "enum": [
                      "PortalUserResetPassword"
                    ],
                    "description": "One of supported activity types"
                  }
                },
                "required": [
                  "type"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActivityItem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/entity/file": {
      "post": {
        "operationId": "saveEntityFile",
        "summary": "saveEntityFile",
        "description": "Add files to an entity",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "requestBody": {
          "description": "The request of an file entity",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SaveEntityFile"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The files have been saved to the entitiy successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "createdFiles": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/File"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      },
      "delete": {
        "operationId": "deleteEntityFile",
        "summary": "deleteEntityFile",
        "description": "Delete files from an entity",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "requestBody": {
          "description": "Delete file",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeleteEntityFile"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "The files are removed from the requested entity successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deletedFiles": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EntityId",
                        "description": "File ID"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/portal/files": {
      "post": {
        "operationId": "savePortalFiles",
        "summary": "savePortalFiles",
        "description": "Add files to portal",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "requestBody": {
          "description": "portal files request",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SavePortalFile"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The files have been saved to the portal successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "createdFiles": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/File"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/registration/identifiers": {
      "get": {
        "operationId": "getRegistrationIdentifiers",
        "summary": "getRegistrationIdentifiers",
        "description": "Get valid attributes from entities that can be used as identifier to map contact to user on registration",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "A dictionary of identifier attributes per entity schema are returned successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "array",
                        "items": {
                          "$ref": "#/components/schemas/IdentifierAttribute"
                        }
                      },
                      "example": {
                        "contact": [
                          {
                            "label": "First name",
                            "name": "first_name",
                            "type": "string"
                          }
                        ],
                        "contract": [
                          {
                            "label": "Contract number",
                            "name": "contract_number",
                            "type": "string"
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/user/files": {
      "get": {
        "operationId": "getAllFiles",
        "summary": "getAllFiles",
        "description": "Fetch all documents under the related entities of a contact. Use searchPortalUserEntities instead.",
        "deprecated": true,
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "from",
            "schema": {
              "type": "number",
              "description": "Initial offset to set for the search results",
              "example": 0
            },
            "required": true
          },
          {
            "in": "query",
            "name": "size",
            "schema": {
              "type": "number",
              "description": "Size of the search results",
              "example": 0
            },
            "required": true
          },
          {
            "in": "query",
            "name": "entity_ids",
            "description": "List of entity ids to filter the results",
            "schema": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/EntityId"
              },
              "minItems": 1,
              "uniqueItems": true,
              "example": [
                "4910096f-000a-4504-bf5a-d3774ec3032a",
                "7c9f8536-6266-42e8-a0de-c60b61aa81a7"
              ]
            }
          },
          {
            "in": "query",
            "name": "file_types",
            "description": "List of `File.type`'s to filter the results",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "minItems": 1,
              "uniqueItems": true,
              "example": [
                "document",
                "image"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The files have been fetched successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "results": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FileItem"
                      }
                    },
                    "hits": {
                      "type": "number",
                      "description": "Total number of files for pagination",
                      "example": 50
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/user/file/{id}": {
      "get": {
        "operationId": "getFileById",
        "summary": "getFileById",
        "description": "Fetch a document with ID. Use getPortalUserEntity instead.",
        "deprecated": true,
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "$ref": "#/components/schemas/EntityId"
            },
            "required": true,
            "description": "The Id of a file"
          }
        ],
        "responses": {
          "200": {
            "description": "The files have been fetched successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "file": {
                      "$ref": "#/components/schemas/FileItem"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/user/file/{id}/preview": {
      "get": {
        "operationId": "getFilePreview",
        "summary": "getFilePreview",
        "description": "resolves an in-portal preview for a file. Returns a Content-Disposition: inline URL for directly-previewable files (PDF, common image formats), or a document-api PDF conversion result for the Office formats it currently supports (docx/xls/xlsx/xlsm) -- NOT doc/csv/pptx, which document-api's conversion does not cover today. Returns { kind: \"unsupported\" } (no url) for anything else, so the caller can fall back to the existing download flow. An optional `download_url` is returned for all kinds — including \"unsupported\" — whenever the file entity exposes one, so the client can render a real anchor for its Download action; it is only a Content-Disposition: attachment URL for private files, and for other access levels it is the plain object URL, which may render inline. The previewability decision is made server-side from the file's own filename/mime_type — never trust a client-supplied kind for anything security-sensitive.",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "$ref": "#/components/schemas/EntityId"
            },
            "required": true,
            "description": "The Id of a file"
          }
        ],
        "responses": {
          "200": {
            "description": "The file preview was resolved (which may still indicate kind \"unsupported\" with no url).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FilePreviewResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/user/file/{id}/downloaded": {
      "post": {
        "operationId": "trackFileDownloaded",
        "summary": "trackFileDownloaded",
        "description": "Track that user has downloaded a file",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "$ref": "#/components/schemas/EntityId"
            },
            "required": true,
            "description": "The Id of a file"
          }
        ],
        "responses": {
          "200": {
            "description": "File download tracked successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "file": {
                      "$ref": "#/components/schemas/FileItem"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/user/files/count-by-entity": {
      "get": {
        "operationId": "getFilesCountByEntity",
        "summary": "getFilesCountByEntity",
        "description": "Fetch file counts for all ECP user related entities",
        "deprecated": true,
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The file counts have been fetched successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/EntityFileCount"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/billing/events": {
      "get": {
        "operationId": "getBillingEvents",
        "summary": "getBillingEvents",
        "description": "Fetch billing events for a portal user",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "from",
            "required": false,
            "schema": {
              "type": "number",
              "description": "Initial offset to set for the search results",
              "default": 0,
              "minimum": 0,
              "example": 0
            }
          },
          {
            "in": "query",
            "name": "size",
            "required": false,
            "schema": {
              "type": "number",
              "description": "Size of the search results.",
              "default": 100,
              "minimum": 0,
              "maximum": 1000,
              "example": 100
            }
          },
          {
            "name": "entity_id",
            "in": "query",
            "description": "Entity ID to filter billing events by",
            "required": false,
            "schema": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/EntityId"
              },
              "minItems": 1,
              "uniqueItems": true
            }
          },
          {
            "name": "event_type",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "installment",
                "reimbursement"
              ],
              "description": "Type of billing event to filter by"
            }
          },
          {
            "name": "paid",
            "in": "query",
            "schema": {
              "type": "boolean",
              "description": "Filter billing events by paid status"
            }
          },
          {
            "name": "date_after",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time",
              "description": "List billing events after this date"
            }
          },
          {
            "name": "date_before",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time",
              "description": "List billing events before this date"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "schema": {
              "type": "string",
              "description": "Key to sort by. Pass a comma-separated list to apply additional keys as tiebreakers, in order of precedence.",
              "example": "paid_date:desc,booking_date:desc"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List billing events for all contracts/orders of specific customer",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "results": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/BillingEvent"
                      }
                    },
                    "hits": {
                      "type": "number",
                      "description": "Total number of billing events for pagination",
                      "example": 50
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v2/portal/billing/customers/balance": {
      "get": {
        "operationId": "getCustomerBalance",
        "summary": "getCustomerBalance",
        "description": "Get total balance across all contracts and orders of a customer entity.",
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "tags": [
          "Balance"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Balance"
                }
              }
            }
          }
        }
      }
    },
    "/v2/portal/billing/accounts/{id}": {
      "parameters": [
        {
          "in": "path",
          "name": "id",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid",
            "example": "5da0a718-c822-403d-9f5d-20d4584e0528"
          }
        }
      ],
      "get": {
        "operationId": "getBillingAccount",
        "summary": "getBillingAccount",
        "description": "Get a billing account by id.",
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "tags": [
          "Billing Accounts",
          "ECP"
        ],
        "responses": {
          "200": {
            "description": "The billing account has been fetched successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "entity": {
                      "$ref": "#/components/schemas/BillingAccount"
                    },
                    "relations": {
                      "type": "array",
                      "description": "The related entities of the requested billing account",
                      "items": {
                        "$ref": "#/components/schemas/EntityItem"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/admin:login-as-user": {
      "post": {
        "operationId": "loginToPortalAsUser",
        "summary": "loginToPortalAsUser",
        "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",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "requestBody": {
          "description": "The request body to log in to a portal impersonating a user",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "description": "The email address of the user to log in as",
                    "example": "portal-customer@email.com"
                  },
                  "origin": {
                    "$ref": "#/components/schemas/Origin"
                  },
                  "portal_id": {
                    "type": "string",
                    "description": "The portal ID to look up the portal config. When provided, takes precedence over origin."
                  },
                  "domain": {
                    "type": "string",
                    "description": "Portal domain for identification. Alternative to portal_id or origin."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The token has been generated successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "login_as_token": {
                      "type": "string",
                      "description": "A generated login_as_token to log in to a portal impersonating a user."
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v2/portal/entity/{schema}/access": {
      "post": {
        "operationId": "triggerEntityAccessEvent",
        "summary": "triggerEntityAccessEvent",
        "description": "Trigger entity access event for a portal user",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "name": "schema",
            "in": "path",
            "required": true,
            "description": "Entity schema",
            "schema": {
              "type": "string",
              "example": "contract"
            }
          },
          {
            "name": "entity_id",
            "in": "query",
            "required": false,
            "description": "Entity ID",
            "schema": {
              "$ref": "#/components/schemas/EntityId"
            }
          },
          {
            "name": "origin",
            "in": "query",
            "required": true,
            "description": "Portal origin",
            "schema": {
              "$ref": "#/components/schemas/Origin",
              "description": "Origin of the portal"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The event has been triggered successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "eventId": {
                      "type": "string",
                      "description": "Event ID"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v3/portal/entity/{schema}/access": {
      "post": {
        "operationId": "triggerEntityAccessEventV3",
        "summary": "triggerEntityAccessEventV3",
        "description": "Trigger entity access event for a portal user.\nSupports two identification methods:\n1. Using portal_id\n2. Using domain\n",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "name": "schema",
            "in": "path",
            "required": true,
            "description": "Entity schema",
            "schema": {
              "type": "string",
              "example": "contract"
            }
          },
          {
            "name": "entity_id",
            "in": "query",
            "required": false,
            "description": "Entity ID",
            "schema": {
              "$ref": "#/components/schemas/EntityId"
            }
          },
          {
            "name": "portal_id",
            "in": "query",
            "required": false,
            "description": "Portal ID (required if domain is not provided)",
            "schema": {
              "$ref": "#/components/schemas/PortalId"
            }
          },
          {
            "name": "domain",
            "in": "query",
            "required": false,
            "description": "Portal domain for identification (alternative to portal_id)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The event has been triggered successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "eventId": {
                      "type": "string",
                      "description": "Event ID"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v2/portal/entity:get": {
      "post": {
        "operationId": "getPortalUserEntity",
        "summary": "getPortalUserEntity",
        "description": "Get a single entity for a portal user",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "x-contact-identification-token": true,
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EntityGetParams"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The entity has been retrieved successfully for the portal user.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EntityResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/entity:search": {
      "post": {
        "operationId": "searchPortalUserEntities",
        "summary": "searchPortalUserEntities",
        "description": "Search all entities of a portal user",
        "x-contact-identification-token": true,
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EntitySearchParams"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The entities have been retrieved successfully for the portal user.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/EntityResponseWithHits"
                    },
                    {
                      "$ref": "#/components/schemas/EntityResponseGroupedWithHits"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/entity/{slug}": {
      "post": {
        "operationId": "createPortalUserEntity",
        "summary": "createPortalUserEntity",
        "description": "**EXPERIMENTAL — do not rely on this endpoint.** It is unstable, currently limited to the `asset` schema, and may change or be removed without notice; third parties must not build on it yet.\nCreate a single entity on behalf of a portal user. The schema slug is passed in the path and must be one of the supported (experimental) schemas; field-level permissions are enforced by the caller's role grants. The request body is the entity to create (its attributes); the created entity is automatically related to the caller's contact.",
        "deprecated": true,
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "slug",
            "description": "Entity schema slug to create. Limited to the supported (experimental) schemas, currently `asset`.",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "asset"
              ],
              "example": "asset"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Entity"
              },
              "example": {
                "title": "PV Inverter",
                "manufacturer": "SMA",
                "external_id": "device-123"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The entity has been created successfully for the portal user.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EntityResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/entity/{slug}/{id}": {
      "patch": {
        "operationId": "patchPortalUserEntity",
        "summary": "patchPortalUserEntity",
        "description": "**EXPERIMENTAL — do not rely on this endpoint.** It is unstable, currently limited to the `asset` schema, and may change or be removed without notice; third parties must not build on it yet.\nPartially update a single entity on behalf of a portal user. The schema slug and entity id are passed in the path; the schema must be one of the supported (experimental) schemas. Field-level permissions are enforced by the caller's role grants (use null to clear a field, e.g. external_id). The target entity must already be owned by the caller's contact.",
        "deprecated": true,
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "slug",
            "description": "Entity schema slug to update. Limited to the supported (experimental) schemas, currently `asset`.",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "asset"
              ],
              "example": "asset"
            }
          },
          {
            "in": "path",
            "name": "id",
            "description": "ID of the entity to update. Must already be owned by the caller's contact.",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/EntityId"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Entity"
              },
              "example": {
                "external_id": null
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The entity has been updated successfully for the portal user.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EntityResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/can-trigger-portal-flow": {
      "post": {
        "operationId": "canTriggerPortalFlow",
        "summary": "canTriggerPortalFlow",
        "description": "Returns whether the user can trigger a portal flow",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "origin",
            "required": false,
            "deprecated": true,
            "schema": {
              "$ref": "#/components/schemas/Origin"
            },
            "description": "Origin of the portal"
          },
          {
            "in": "query",
            "name": "portal_id",
            "required": true,
            "schema": {
              "type": "string",
              "description": "Portal ID",
              "example": "123"
            }
          }
        ],
        "requestBody": {
          "description": "Request of trigger portal flow",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TriggerPortalFlow"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Can Trigger Portal Flow",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "can_trigger": {
                      "type": "boolean",
                      "description": "Whether the flow can be triggered",
                      "example": true
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v2/portal/automation-context": {
      "get": {
        "operationId": "getAutomationContext",
        "summary": "getAutomationContext",
        "description": "Retrieves the automation context.",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "activity_id",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/ActivityId"
            },
            "description": "Activity ID"
          },
          {
            "in": "query",
            "name": "type",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "file"
              ]
            },
            "description": "Type of the context to retrieve"
          }
        ],
        "responses": {
          "200": {
            "description": "Retrieved the automation context successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": {
                    "$ref": "#/components/schemas/Entity"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/workflow/{workflow_id}/{step_id}:markDone": {
      "put": {
        "operationId": "updateWorkflowStepAsDone",
        "summary": "updateWorkflowStepAsDone",
        "description": "Update a workflow step as done",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "workflow_id",
            "required": true,
            "schema": {
              "type": "string",
              "description": "ID of a workflow",
              "example": "0bjwcxc827t"
            }
          },
          {
            "in": "path",
            "name": "step_id",
            "required": true,
            "schema": {
              "type": "string",
              "description": "ID of a step",
              "example": "q1d6vcbsqvn"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Updated workflow step successfully",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorkflowStep"
                }
              }
            }
          }
        }
      }
    },
    "/v2/portal/entity/{slug}/{id}/workflows": {
      "get": {
        "operationId": "getEntityWorkflows",
        "summary": "getEntityWorkflows",
        "description": "Get all workflows associated with an entity (requires access to the entity)",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "slug",
            "required": true,
            "schema": {
              "type": "string",
              "description": "Schema slug of the entity",
              "example": "contact"
            }
          },
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "description": "ID of the entity",
              "example": "abc123"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of workflows for the entity",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "workflow_executions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WorkflowExecution"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/engagement/tasks": {
      "get": {
        "operationId": "getOutstandingTasks",
        "summary": "Get outstanding workflow tasks for the portal user",
        "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.",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "List of outstanding workflow tasks for the portal user",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "tasks",
                    "total"
                  ],
                  "properties": {
                    "tasks": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OutstandingTask"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "example": 0
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/entity/{slug}/{id}/workflows/linearized": {
      "get": {
        "operationId": "getEntityPortalWorkflows",
        "summary": "getEntityPortalWorkflows",
        "description": "Get all portal-relevant workflows associated with an entity (requires access to the entity),\nlinearized by the Workflows API into a flat, ordered list of portal-visible tasks.\n\nUnlike `getEntityWorkflows`, this endpoint returns a single uniform shape for both legacy\n(V1) workflow executions and graph-based (V2) flow executions — the execution graph is\nalready resolved server-side (active path, task reachability, chronological ordering), so\nclients can render a timeline without any graph traversal logic.\n\nTasks carry the config of both portal audiences (`ecp` and `installer`); clients filter\nper their configured audience, so one response serves every block on a page.\n",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "slug",
            "required": true,
            "schema": {
              "type": "string",
              "description": "Schema slug of the entity",
              "example": "opportunity"
            }
          },
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "description": "ID of the entity",
              "example": "abc123"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Portal-relevant workflows for the entity, each with linearized tasks",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "portal_workflows": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PortalWorkflow"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/entities/workflows/linearized/batch": {
      "post": {
        "operationId": "getEntityPortalWorkflowsBatch",
        "summary": "getEntityPortalWorkflowsBatch",
        "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",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "requestBody": {
          "description": "Entities to load portal workflows for",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "entities": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "description": "ID of the entity"
                        },
                        "slug": {
                          "type": "string",
                          "description": "Schema slug of the entity",
                          "example": "order"
                        }
                      },
                      "required": [
                        "id",
                        "slug"
                      ]
                    },
                    "minItems": 1,
                    "maxItems": 50
                  }
                },
                "required": [
                  "entities"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One entry per accessible entity, each with its linearized workflows",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "results": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EntityPortalWorkflows"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/metering/reading/photo": {
      "post": {
        "operationId": "uploadMeterReadingPhoto",
        "summary": "uploadMeterReadingPhoto",
        "description": "Uploads a Meter Reading photo and - if enabled - gives back data extracted from the photo.",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "requestBody": {
          "description": "Upload Meter reading photo payload.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MeterReadingPhoto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Meter reading photo saved successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/MeterReadingPhotoData"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/metering/reading": {
      "post": {
        "operationId": "createMeterReading",
        "summary": "createMeterReading",
        "description": "Inserts a new meter reading.",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "name": "override_plausibility",
            "in": "query",
            "required": false,
            "description": "Override plausibility check",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "requestBody": {
          "description": "Meter reading payload.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MeterReading"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Meter reading created successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/MeterReading"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequestCreateMeterReading"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/metering/reading/allowed-range/{meter_id}": {
      "get": {
        "operationId": "getAllowedMeterReadingRange",
        "summary": "getAllowedMeterReadingRange",
        "description": "Get allowed reading range for all counters of a meter from the configured\nthird-party plausibility check hook using 'range' mode. This endpoint requires\na plausibility check hook to be configured for the portal.\n",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "meter_id",
            "schema": {
              "type": "string"
            },
            "required": true,
            "description": "The ID of the meter."
          },
          {
            "in": "query",
            "name": "origin",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/Origin"
            },
            "description": "Origin of the portal"
          },
          {
            "in": "query",
            "name": "timestamp",
            "required": false,
            "description": "If not provided, the system will default to now.",
            "schema": {
              "type": "string",
              "example": "2022-10-01T10:10:00.000Z"
            }
          },
          {
            "in": "query",
            "name": "context_entities",
            "required": false,
            "description": "Additional entities to include in the context for variable interpolation in the hook.",
            "schema": {
              "$ref": "#/components/schemas/ContextEntities"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Allowed reading ranges for the meter.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "meter_counter_id": {
                            "type": "string",
                            "description": "The ID of the counter"
                          },
                          "min_value": {
                            "type": "number",
                            "description": "Minimum allowed reading value",
                            "nullable": true
                          },
                          "max_value": {
                            "type": "number",
                            "description": "Maximum allowed reading value",
                            "nullable": true
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/metering/readings": {
      "post": {
        "operationId": "getMeterReadings",
        "summary": "getMeterReadings",
        "x-contact-identification-token": true,
        "description": "Fetches meter readings for a counter and optionally resolves Handlebars\ntemplate strings against each reading object using @epilot/variables.\n",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "meter_id",
                  "counter_id"
                ],
                "properties": {
                  "meter_id": {
                    "type": "string"
                  },
                  "counter_id": {
                    "type": "string"
                  },
                  "sort": {
                    "type": "string",
                    "enum": [
                      "asc",
                      "desc"
                    ],
                    "default": "desc"
                  },
                  "from": {
                    "type": "integer",
                    "default": 0
                  },
                  "size": {
                    "type": "integer",
                    "default": 10
                  },
                  "templates": {
                    "type": "object",
                    "deprecated": true,
                    "additionalProperties": {
                      "type": "string"
                    },
                    "description": "DEPRECATED — client-supplied template map (key to Handlebars template string), resolved per reading. Use `templates_ref` instead; ignored when `templates_ref` is present and rejected once the org has the `portals-reject-client-templates` flag enabled."
                  },
                  "counter_templates": {
                    "type": "object",
                    "deprecated": true,
                    "additionalProperties": {
                      "type": "string"
                    },
                    "description": "DEPRECATED — client-supplied template map resolved against the counter entity. Use `templates_ref` instead; same deprecation rules as `templates`."
                  },
                  "templates_ref": {
                    "$ref": "#/components/schemas/TemplatesRef",
                    "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."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Meter readings with resolved templates",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "results": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": true,
                        "properties": {
                          "templates_output": {
                            "type": "object",
                            "additionalProperties": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    },
                    "hits": {
                      "type": "integer"
                    },
                    "counter_templates_output": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "string"
                      },
                      "description": "Resolved counter templates."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/public/sso/providers/{provider_slug}": {
      "get": {
        "operationId": "getSSOProvider",
        "summary": "getSSOProvider",
        "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",
        "security": [],
        "tags": [
          "Public"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "provider_slug",
            "required": true,
            "description": "Provider slug (organization-unique)",
            "schema": {
              "$ref": "#/components/schemas/ProviderSlug"
            }
          },
          {
            "in": "query",
            "name": "org_id",
            "required": false,
            "description": "epilot organization id (required with `origin` or `portal_id`)",
            "schema": {
              "type": "string",
              "example": 123
            }
          },
          {
            "in": "query",
            "name": "origin",
            "required": false,
            "description": "Origin of the Portal",
            "schema": {
              "type": "string",
              "enum": [
                "END_CUSTOMER_PORTAL",
                "INSTALLER_PORTAL"
              ]
            }
          },
          {
            "in": "query",
            "name": "portal_id",
            "required": false,
            "description": "ID of the Portal",
            "schema": {
              "$ref": "#/components/schemas/PortalId"
            }
          },
          {
            "in": "query",
            "name": "domain",
            "required": false,
            "description": "Portal domain for identification (alternative to org_id + origin/portal_id)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Resolved public SSO provider configuration. The handler strips `oidc_config.client_secret` from the response (kept server-side for the token exchange); the schema includes it for shape compatibility with the portal-config response.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProviderPublicConfig"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v3/portal/public/sso/providers/{provider_slug}": {
      "get": {
        "operationId": "getPublicSSOProviderV3",
        "summary": "getPublicSSOProviderV3",
        "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",
        "security": [],
        "tags": [
          "Public"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "provider_slug",
            "required": true,
            "description": "Provider slug (organization-unique)",
            "schema": {
              "$ref": "#/components/schemas/ProviderSlug"
            }
          },
          {
            "in": "query",
            "name": "org_id",
            "required": true,
            "description": "epilot organization id",
            "schema": {
              "type": "string",
              "example": 123
            }
          },
          {
            "in": "query",
            "name": "portal_id",
            "required": true,
            "description": "ID of the Portal",
            "schema": {
              "$ref": "#/components/schemas/PortalId"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Resolved public SSO provider configuration, reduced to the web OIDC flow. `mobile_oidc_config` is omitted; `oidc_config.client_secret` and the `metadata.test_auth_*` credentials are stripped (kept server-side for the token exchange).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProviderPublicConfigV3"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/public/sso/login": {
      "post": {
        "operationId": "ssoLogin",
        "summary": "ssoLogin",
        "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",
        "security": [
          {
            "ExternalOIDCAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "origin",
            "required": true,
            "description": "Origin of the Portal",
            "schema": {
              "$ref": "#/components/schemas/Origin"
            }
          },
          {
            "in": "query",
            "name": "org_id",
            "description": "epilot organization id",
            "required": true,
            "schema": {
              "type": "string",
              "example": 123
            }
          },
          {
            "in": "query",
            "name": "contact_id",
            "description": "contact id in the epilot system",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid",
              "example": "5da0a718-c822-403d-9f5d-20d4584e0528"
            }
          }
        ],
        "tags": [
          "Login"
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "provider_slug": {
                    "$ref": "#/components/schemas/ProviderSlug"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Cognito login parameters to use with the CUSTOM_AUTH_FLOW",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "token": {
                      "$ref": "#/components/schemas/SSOLoginToken"
                    },
                    "email": {
                      "type": "string",
                      "description": "The email address of the user to log in as",
                      "example": "portal-customer@email.com"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v3/portal/public/sso/login": {
      "post": {
        "operationId": "ssoLoginV3",
        "summary": "ssoLoginV3",
        "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",
        "security": [
          {
            "ExternalOIDCAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "portal_id",
            "required": false,
            "description": "ID of the Portal (required if domain is not provided)",
            "schema": {
              "$ref": "#/components/schemas/PortalId"
            }
          },
          {
            "in": "query",
            "name": "org_id",
            "description": "epilot organization id (required if domain is not provided)",
            "required": false,
            "schema": {
              "type": "string",
              "example": 123
            }
          },
          {
            "in": "query",
            "name": "domain",
            "description": "Portal domain for identification (alternative to org_id + portal_id)",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "contact_id",
            "description": "contact id in the epilot system",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid",
              "example": "5da0a718-c822-403d-9f5d-20d4584e0528"
            }
          }
        ],
        "tags": [
          "Login"
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "provider_slug": {
                    "$ref": "#/components/schemas/ProviderSlug"
                  },
                  "login_only": {
                    "type": "boolean",
                    "default": false,
                    "description": "Authenticate existing identities only. When no portal user and no contact matches the identity, the login fails with a 400 response carrying `reason: PORTAL_ACCOUNT_NOT_FOUND` instead of registering a new portal user. A matched contact is still provisioned a portal user; contacts and accounts are never created."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Cognito login parameters to use with the CUSTOM_AUTH_FLOW",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "token": {
                      "$ref": "#/components/schemas/SSOLoginToken"
                    },
                    "email": {
                      "type": "string",
                      "description": "The email address of the user to log in as",
                      "example": "portal-customer@email.com"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          }
        }
      }
    },
    "/v2/portal/public/sso/redirect": {
      "post": {
        "operationId": "ssoRedirect",
        "summary": "ssoRedirect",
        "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",
        "security": [],
        "tags": [
          "Public"
        ],
        "parameters": [
          {
            "in": "query",
            "name": "web_uri",
            "required": true,
            "description": "The URI to redirect to after the SSO login",
            "schema": {
              "type": "string",
              "example": "https://customer-portal.com"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "code": {
                    "type": "string",
                    "description": "The code received from the external SSO provider",
                    "example": "123456"
                  },
                  "state": {
                    "type": "string",
                    "description": "The state received from the external SSO provider",
                    "example": "123456"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "301": {
            "description": "Redirect to the login page with the code",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string",
                  "example": "https://customer-portal.com/login?code=123456&state=123456"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/public/sso/callback": {
      "post": {
        "operationId": "ssoCallback",
        "summary": "ssoCallback",
        "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",
        "security": [],
        "tags": [
          "Public"
        ],
        "parameters": [
          {
            "in": "query",
            "name": "domain",
            "schema": {
              "type": "string",
              "example": "customer-portal.epilot.io",
              "description": "The domain of the portal"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SSOCallbackRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The token to be used with the CUSTOM_AUTH flow against Cognito",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SSOCallbackResponse"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/pages/{id}": {
      "parameters": [
        {
          "in": "path",
          "name": "id",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid",
            "example": "5da0a718-c822-403d-9f5d-20d4584e0528"
          }
        }
      ],
      "get": {
        "operationId": "getPortalPage",
        "summary": "getPortalPage",
        "description": "Fetch a portal page by id",
        "tags": [
          "ECP",
          "ECP Admin"
        ],
        "security": [
          {
            "EitherAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The portal page has been fetched successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      },
      "put": {
        "operationId": "updatePortalPage",
        "summary": "updatePortalPage",
        "description": "Update a portal page by id",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "requestBody": {
          "description": "The portal page to update",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PageRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The portal page has been fetched successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      },
      "delete": {
        "operationId": "deletePortalPage",
        "summary": "deletePortalPage",
        "description": "Delete a portal page by id",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "responses": {
          "204": {
            "description": "The portal page has been fetched successfully."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/pages": {
      "parameters": [
        {
          "in": "query",
          "name": "domain",
          "required": true,
          "schema": {
            "type": "string",
            "example": "customer-portal.epilot.io",
            "description": "The domain of the portal"
          }
        }
      ],
      "get": {
        "operationId": "getPortalPages",
        "summary": "getPortalPages",
        "description": "Fetch all portal pages",
        "tags": [
          "ECP",
          "ECP Admin"
        ],
        "security": [
          {
            "EitherAuth": []
          }
        ],
        "x-contact-identification-token": true,
        "parameters": [
          {
            "in": "query",
            "name": "fields",
            "required": false,
            "description": "The fields to include in the response",
            "schema": {
              "type": "string",
              "example": "id,slug,description"
            }
          },
          {
            "in": "query",
            "name": "filter",
            "required": false,
            "description": "The filter to apply to the response",
            "schema": {
              "type": "string",
              "example": "slug=contracts"
            }
          },
          {
            "in": "query",
            "name": "contract_id",
            "required": false,
            "deprecated": true,
            "schema": {
              "$ref": "#/components/schemas/EntityId"
            },
            "description": "Contract context for blocks. Use context_entities instead."
          },
          {
            "in": "query",
            "name": "context_entities",
            "required": false,
            "description": "If the request is in a context of certain entities (i.e. the user in in a context of a specific contract), pages can be customized for that. Portal User and Contact entities are automatically part of the context.",
            "schema": {
              "$ref": "#/components/schemas/ContextEntities"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The portal pages have been fetched successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Page"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      },
      "post": {
        "operationId": "createPortalPage",
        "summary": "createPortalPage",
        "description": "Create a new portal page",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "requestBody": {
          "description": "The portal page to create",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PageRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The portal page has been created successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/public/pages": {
      "get": {
        "operationId": "getPublicPages",
        "summary": "getPublicPages",
        "description": "Fetch all public portal pages",
        "tags": [
          "Public"
        ],
        "security": [],
        "parameters": [
          {
            "in": "query",
            "name": "domain",
            "required": true,
            "schema": {
              "type": "string",
              "example": "customer-portal.epilot.io",
              "description": "The domain of the portal"
            }
          },
          {
            "in": "query",
            "name": "fields",
            "required": false,
            "description": "The fields to include in the response",
            "schema": {
              "type": "string",
              "example": "id,slug,description"
            }
          },
          {
            "in": "query",
            "name": "filter",
            "required": false,
            "description": "The filter to apply to the response",
            "schema": {
              "type": "string",
              "example": "schema=contract,slug=contracts"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The portal pages have been fetched successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Page"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/pages/interpolate": {
      "post": {
        "operationId": "interpolatePortalPages",
        "summary": "interpolatePortalPages",
        "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.",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EitherAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "pages"
                ],
                "properties": {
                  "pages": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/Page"
                    },
                    "description": "The pages to interpolate"
                  },
                  "context_entities": {
                    "$ref": "#/components/schemas/ContextEntities"
                  },
                  "evaluate_targeting": {
                    "type": "boolean",
                    "description": "When true, evaluates targeting conditions and annotates each page/block visibility with _targeting_status ('visible' | 'hidden' | 'no_targeting'). Blocks and pages are never removed — for preview/builder use only."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The portal pages have been interpolated successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Page"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/config/triggered-journeys/{trigger_name}": {
      "parameters": [
        {
          "in": "path",
          "name": "trigger_name",
          "required": true,
          "schema": {
            "type": "string",
            "enum": [
              "FIRST_LOGIN",
              "ACCEPT_ORDER",
              "DECLINE_ORDER"
            ]
          },
          "description": "The trigger event for the auto-triggered journey"
        }
      ],
      "get": {
        "operationId": "getTriggeredJourney",
        "summary": "getTriggeredJourney",
        "description": "Returns the auto-triggered journey configured for the given trigger\nwith handlebars templates in `context_params` already resolved.\nUses the caller's auth context (contact, portal user) plus any\n`context_entities` supplied at trigger time (e.g. the order being\naccepted) — same resolver as `getPortalPages`.\n",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "context_entities",
            "required": false,
            "description": "Additional entities to include in the resolution context (e.g. the order being accepted at trigger time). Portal User and Contact entities are automatically part of the context.",
            "schema": {
              "$ref": "#/components/schemas/ContextEntities"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The triggered journey has been resolved successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "trigger_name": {
                      "type": "string",
                      "enum": [
                        "FIRST_LOGIN",
                        "ACCEPT_ORDER",
                        "DECLINE_ORDER"
                      ]
                    },
                    "journey_id": {
                      "$ref": "#/components/schemas/EntityId"
                    },
                    "context_params": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "key": {
                            "type": "string"
                          },
                          "value": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/pages/default": {
      "get": {
        "operationId": "getDefaultPages",
        "summary": "getDefaultPages",
        "description": "Fetch all default portal pages",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The default portal pages have been fetched successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Page"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/pages/{id}/blocks": {
      "parameters": [
        {
          "in": "path",
          "name": "id",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid",
            "example": "5da0a718-c822-403d-9f5d-20d4584e0528"
          }
        }
      ],
      "post": {
        "operationId": "createPortalPageBlock",
        "summary": "createPortalPageBlock",
        "description": "Create a new portal page block",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "requestBody": {
          "description": "The portal page block to create",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BlockRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The portal page block has been created successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Block"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      },
      "get": {
        "operationId": "getPortalPageBlocks",
        "summary": "getPortalPageBlocks",
        "description": "Fetch all portal page blocks",
        "tags": [
          "ECP",
          "ECP Admin"
        ],
        "security": [
          {
            "EitherAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The portal page blocks have been fetched successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Block"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/pages/{id}/blocks/{block_id}": {
      "parameters": [
        {
          "in": "path",
          "name": "id",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid",
            "example": "5da0a718-c822-403d-9f5d-20d4584e0528"
          }
        },
        {
          "in": "path",
          "name": "block_id",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid",
            "example": "5da0a718-c822-403d-9f5d-20d4584e0528"
          }
        }
      ],
      "get": {
        "operationId": "getPortalPageBlock",
        "summary": "getPortalPageBlock",
        "description": "Fetch a portal page block by id",
        "tags": [
          "ECP",
          "ECP Admin"
        ],
        "security": [
          {
            "EitherAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The portal page block has been fetched successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Block"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      },
      "put": {
        "operationId": "updatePortalPageBlock",
        "summary": "updatePortalPageBlock",
        "description": "Update a portal page block by id",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "requestBody": {
          "description": "The portal page block to update",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BlockRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The portal page block has been fetched successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Block"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      },
      "delete": {
        "operationId": "deletePortalPageBlock",
        "summary": "deletePortalPageBlock",
        "description": "Delete a portal page block by id",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "responses": {
          "204": {
            "description": "The portal page block has been fetched successfully."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/public/user/entry-point": {
      "get": {
        "operationId": "getUserEntryPoint",
        "summary": "getUserEntryPoint",
        "description": "Get the entry point for the user",
        "tags": [
          "Public"
        ],
        "security": [],
        "parameters": [
          {
            "in": "query",
            "name": "email",
            "required": true,
            "schema": {
              "type": "string",
              "description": "Email Address of the portal user",
              "example": "user@example.com"
            }
          },
          {
            "in": "query",
            "name": "domain",
            "required": true,
            "schema": {
              "type": "string",
              "description": "Domain of the portal",
              "example": "customer-portal.epilot.io"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Returned the entry point for the user",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "entry_point",
                    "user_exists"
                  ],
                  "properties": {
                    "user_exists": {
                      "type": "boolean",
                      "description": "Whether the user exists in the portal",
                      "example": true
                    },
                    "entry_point": {
                      "type": "string",
                      "description": "The entry point for the user",
                      "enum": [
                        "PASSWORD",
                        "SSO"
                      ]
                    },
                    "preferred_sso_providers": {
                      "type": "array",
                      "description": "The SSO providers for the user",
                      "items": {
                        "$ref": "#/components/schemas/ProviderSlug"
                      }
                    },
                    "is_soft_deleted": {
                      "type": "boolean",
                      "description": "Whether the user is soft deleted",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/campaign/{campaign_id}/entity:status": {
      "put": {
        "operationId": "updateCampaignPortalBlockStatus",
        "summary": "updateCampaignPortalBlockStatus",
        "description": "Updates the status of a campaign portal block for multiple recipients.",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "campaign_id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID of the campaign"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "status",
                  "entity_refs"
                ],
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "seen",
                      "dismissed",
                      "clicked"
                    ],
                    "description": "The status to set for the campaign portal block"
                  },
                  "entity_refs": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "entity_id",
                        "entity_schema"
                      ],
                      "properties": {
                        "entity_id": {
                          "$ref": "#/components/schemas/EntityId"
                        },
                        "entity_schema": {
                          "type": "string",
                          "description": "Schema of the entity (e.g., contact, contract, opportunity, order, meter)"
                        }
                      }
                    },
                    "description": "Array of entity references with their schemas"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Campaign portal block status updated successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "updated",
                    "failed",
                    "total"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "description": "Whether the operation completed successfully",
                      "example": true
                    },
                    "updated": {
                      "type": "integer",
                      "description": "Number of entities successfully updated",
                      "example": 2
                    },
                    "failed": {
                      "type": "integer",
                      "description": "Number of entities that failed to update",
                      "example": 0
                    },
                    "total": {
                      "type": "integer",
                      "description": "Total number of entities processed",
                      "example": 2
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/notifications": {
      "get": {
        "operationId": "listPortalNotifications",
        "summary": "listPortalNotifications",
        "description": "Lists the 360 notifications addressed to the authenticated portal user, newest first. The organization and the portal user are derived from the authenticated session, so a user can only ever read their own notifications.",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Base64 encoded cursor returned by a previous call, used for pagination."
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "The maximum number of notifications to return."
          }
        ],
        "responses": {
          "200": {
            "description": "List of notifications for the authenticated portal user.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "cursor": {
                      "type": "string",
                      "description": "Base64 encoded cursor to fetch the next page. Absent when there are no more results."
                    },
                    "total_unread": {
                      "type": "integer",
                      "description": "Total number of unread notifications for the user."
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PortalNotification"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/notifications/unread-count": {
      "get": {
        "operationId": "getPortalNotificationsUnreadCount",
        "summary": "getPortalNotificationsUnreadCount",
        "description": "Returns the number of unread notifications for the authenticated portal user.",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Unread notification count.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "count": {
                      "type": "integer",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/notifications/read-all": {
      "put": {
        "operationId": "markAllPortalNotificationsRead",
        "summary": "markAllPortalNotificationsRead",
        "description": "Marks all notifications of the authenticated portal user as read.",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "responses": {
          "204": {
            "description": "All notifications marked as read."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/notifications/{id}/read": {
      "put": {
        "operationId": "markPortalNotificationRead",
        "summary": "markPortalNotificationRead",
        "description": "Marks a single notification of the authenticated portal user as read.",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "Numeric id of the notification to mark as read."
          }
        ],
        "responses": {
          "204": {
            "description": "Notification marked as read."
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/notifications/entity:status": {
      "put": {
        "operationId": "updateNotificationsStatus",
        "summary": "updateNotificationsStatus",
        "description": "Updates the statuses of multiple notifications at once.",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "notifications"
                ],
                "properties": {
                  "notifications": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "id",
                        "status"
                      ],
                      "properties": {
                        "id": {
                          "type": "string",
                          "description": "ID of the notification"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "read",
                            "important"
                          ],
                          "description": "The status to set for the notification"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Notifications status updated successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "description": "Message indicating the status of the notifications",
                      "example": "Notifications status updated successfully."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/public/m-login/deregister/{client_id}/{user_id}": {
      "delete": {
        "operationId": "deRegisterMLoginUser",
        "summary": "deRegisterMLoginUser",
        "description": "Deregisters a user from the M Login client",
        "tags": [
          "Public"
        ],
        "security": [],
        "parameters": [
          {
            "in": "path",
            "name": "client_id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Client ID"
          },
          {
            "in": "path",
            "name": "user_id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "User ID"
          }
        ],
        "responses": {
          "204": {
            "description": "User deregistered successfully"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/public/m-login/notify-interest-change/{client_id}/{user_id}": {
      "post": {
        "operationId": "notifyMLoginInterestChange",
        "summary": "notifyMLoginInterestChange",
        "description": "Notifies the interest change of a user in the M Login client",
        "tags": [
          "Public"
        ],
        "security": [],
        "parameters": [
          {
            "in": "path",
            "name": "client_id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Client ID"
          },
          {
            "in": "path",
            "name": "user_id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "User ID"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": true,
                "properties": {
                  "resource_id": {
                    "type": "string",
                    "description": "Resource ID"
                  },
                  "user_id": {
                    "type": "string",
                    "description": "Resource type"
                  },
                  "interest_tag": {
                    "type": "string",
                    "description": "Interest tag"
                  },
                  "action": {
                    "type": "string",
                    "description": "Action"
                  },
                  "resource": {
                    "type": "object",
                    "additionalProperties": true,
                    "properties": {
                      "resource_id": {
                        "type": "string",
                        "description": "Resource ID"
                      },
                      "description": {
                        "type": "string",
                        "description": "Description",
                        "nullable": true
                      },
                      "contact": {
                        "type": "string",
                        "description": "Contact"
                      },
                      "kind": {
                        "type": "string",
                        "description": "Kind"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Interest change acknowledged successfully"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v3/portal/config": {
      "post": {
        "operationId": "createPortalConfig",
        "summary": "createPortalConfig",
        "description": "Creates a new portal configuration.",
        "tags": [
          "ECP Admin"
        ],
        "requestBody": {
          "description": "Portal configuration payload",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpsertPortalConfigV3"
              }
            }
          }
        },
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "responses": {
          "201": {
            "description": "Portal config created successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PortalConfigV3"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v3/portal/config/{portal_id}": {
      "get": {
        "operationId": "getPortalConfigV3",
        "summary": "getPortalConfigV3",
        "description": "Retrieves a specific portal configuration by ID.",
        "tags": [
          "ECP Admin",
          "ECP"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "portal_id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "example": "5da0a718-c822-403d-9f5d-20d4584e0528"
            },
            "description": "Portal ID (readonly UUID generated on portal creation)"
          }
        ],
        "security": [
          {
            "EitherAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Portal config retrieved successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PortalConfigV3"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      },
      "put": {
        "operationId": "putPortalConfig",
        "summary": "putPortalConfig",
        "description": "Updates a specific portal configuration by ID.",
        "tags": [
          "ECP Admin"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "portal_id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "example": "5da0a718-c822-403d-9f5d-20d4584e0528"
            },
            "description": "Portal ID (readonly UUID generated on portal creation)"
          },
          {
            "in": "query",
            "name": "page_upsert_mode",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "id",
                "slug"
              ],
              "default": "id"
            },
            "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"
          }
        ],
        "requestBody": {
          "description": "Portal configuration payload",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PortalConfigV3"
              }
            }
          }
        },
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Portal config updated successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PortalConfigV3"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      },
      "delete": {
        "operationId": "deletePortalConfig",
        "summary": "deletePortalConfig",
        "description": "Deletes a specific portal configuration by ID.",
        "tags": [
          "ECP Admin"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "portal_id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "example": "5da0a718-c822-403d-9f5d-20d4584e0528"
            },
            "description": "Portal ID (readonly UUID generated on portal creation)"
          }
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "responses": {
          "204": {
            "description": "Portal config deleted successfully."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v3/portal/config/{portal_id}/revisions": {
      "post": {
        "operationId": "createPortalRevision",
        "summary": "createPortalRevision",
        "description": "Creates a new revision — a complete, immutable snapshot of the portal's configuration, pages and email templates. Nothing about the live portal changes; the snapshot only becomes live when it is published.\n\nThe payload must be COMPLETE. This endpoint does not merge against live or against the previous revision: publishing a revision deletes every live page the revision does not contain. The server validates structure only — `pages` present, each page carrying `id`, `slug`, `order` and `blocks`, and no two pages sharing an `id` or a `slug`. Semantic completeness of the config is a promise the caller makes, and a partial payload is honoured rather than rejected.\n\n`email_templates` is optional, and absence means \"keep the portal's current templates\", never \"no templates\". A present but partial map is taken verbatim.\n",
        "tags": [
          "ECP Admin"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "portal_id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "example": "5da0a718-c822-403d-9f5d-20d4584e0528"
            },
            "description": "Portal ID (readonly UUID generated on portal creation)"
          }
        ],
        "requestBody": {
          "description": "Complete portal configuration snapshot",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PortalRevisionRequest"
              }
            }
          }
        },
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "responses": {
          "201": {
            "description": "Revision created successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PortalRevisionCreated"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      },
      "get": {
        "operationId": "listPortalRevisions",
        "summary": "listPortalRevisions",
        "description": "Lists a portal's revision history, newest first. Metadata only — no config blob, no page bodies. `is_published` says whether that revision is the one currently live, which is a different question from `published_at`, which records the last time it was published.\n",
        "tags": [
          "ECP Admin"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "portal_id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "example": "5da0a718-c822-403d-9f5d-20d4584e0528"
            },
            "description": "Portal ID (readonly UUID generated on portal creation)"
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            },
            "description": "Maximum number of revisions to return"
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Opaque pagination cursor, taken from a previous response's `next_cursor`"
          }
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Revision history retrieved successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PortalRevisionList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v3/portal/config/{portal_id}/revisions/{revision_id}": {
      "get": {
        "operationId": "getPortalRevision",
        "summary": "getPortalRevision",
        "description": "Returns the full content of one revision: the snapshotted config, its pages (in the live `Page` shape), email templates and identity providers with secrets redacted.\n\nThis is a pure read. The server records nothing about it: no \"loaded\" marker, no audit entry, and no change to which revision is live. Revision content re-enters the system only as a new `POST .../revisions`.\n\nSecret-typed extension option values are removed from `config` entirely, not masked.\n\nReturns `409` when the revision exists but its stored config or email templates row cannot be read, rather than a partial snapshot. Saving again produces a complete revision.\n",
        "tags": [
          "ECP Admin"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "portal_id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "example": "5da0a718-c822-403d-9f5d-20d4584e0528"
            },
            "description": "Portal ID (readonly UUID generated on portal creation)"
          },
          {
            "in": "path",
            "name": "revision_id",
            "required": true,
            "schema": {
              "type": "string",
              "example": "2026-08-25T14:03:11.482Z-a7f3c1d9"
            },
            "description": "Revision ID. Contains `:` characters — percent-encode it."
          }
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Revision retrieved successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PortalRevision"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v3/portal/config/{portal_id}/publish": {
      "post": {
        "operationId": "publishPortalRevision",
        "summary": "publishPortalRevision",
        "description": "Makes one revision the portal's live configuration, atomically: either everything below takes effect or nothing does. Publish is a full-snapshot replace: it writes config, every page and email templates to match the revision exactly, and deletes every live page the revision does not contain.\n\nAccepts any valid `revision_id` for the portal, old or new. There is no separate rollback endpoint and none is needed.\n\nThe same atomic publish stamps `name`, `description` and `published_at` onto the revision being published — permanently, on that revision, and not on any later one that supersedes it as live. `name` is generated server-side when the request omits it, so every published revision carries one.\n\nConflict detection covers the live config only: publish is rejected with `409` when the live config changed after publish read it. Live pages, their redirect routes and the email templates are replaced outright — a concurrent edit to a live page is not detected and is overwritten. `409` is also returned when the revision's stored config or email templates cannot be read, or when two of its pages would resolve to the same slug. A domain the revision claims that another portal already owns is a `400`.\n",
        "tags": [
          "ECP Admin"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "portal_id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "example": "5da0a718-c822-403d-9f5d-20d4584e0528"
            },
            "description": "Portal ID (readonly UUID generated on portal creation)"
          }
        ],
        "requestBody": {
          "description": "The revision to publish",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublishRevisionRequest"
              }
            }
          }
        },
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Revision published successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublishResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/v3/portal/configs": {
      "get": {
        "operationId": "listAllPortalConfigs",
        "summary": "listAllPortalConfigs",
        "description": "Retrieves all portal configurations.",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "All portal configs retrieved successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PortalConfigV3"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v3/portal/config/swap": {
      "post": {
        "operationId": "swapPortalConfig",
        "summary": "swapPortalConfig",
        "description": "Swaps the portal configuration of two portals.",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "requestBody": {
          "description": "Source and target portal IDs",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "source_portal_id",
                  "target_portal_id"
                ],
                "properties": {
                  "source_portal_id": {
                    "$ref": "#/components/schemas/PortalId"
                  },
                  "target_portal_id": {
                    "$ref": "#/components/schemas/PortalId"
                  },
                  "items_to_swap": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/SwappableConfig"
                    },
                    "description": "Optional, opt-in configuration items to additionally swap on top of the always-swapped pages and functional config. Defaults to an empty list (nothing extra swapped). Domain and access/security settings can never be swapped."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Domain and users swapped successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Domain and users swapped successfully."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v3/portal/config/clone": {
      "post": {
        "operationId": "clonePortalConfig",
        "summary": "clonePortalConfig",
        "description": "Creates a new portal by cloning configuration and pages from an existing portal. The new portal gets its own domain, users, email templates, and authentication settings.",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "requestBody": {
          "description": "Source portal ID and optional name for the cloned portal",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "source_portal_id"
                ],
                "properties": {
                  "source_portal_id": {
                    "$ref": "#/components/schemas/PortalId"
                  },
                  "name": {
                    "type": "string",
                    "description": "Name for the cloned portal. Defaults to \"Copy of <source portal name>\"."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Portal cloned successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PortalConfigV3"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v3/portal/partner/invite": {
      "post": {
        "operationId": "invitePartner",
        "summary": "invitePartner",
        "description": "Invites a partner to a portal",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "requestBody": {
          "description": "Partner to invite",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "description": "Email address of the partner to invite"
                  },
                  "contact_data": {
                    "type": "object",
                    "description": "Additional contact entity fields to set when creating the contact for the invited user.\nThese are mapped directly to contact entity attributes (e.g. first_name, last_name, phone).\nValues can be strings or arrays of strings (for multiselect attributes).\n",
                    "additionalProperties": {
                      "oneOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      ]
                    }
                  },
                  "portal_user_data": {
                    "type": "object",
                    "description": "Additional portal user entity fields to set when creating the portal user for the invited user.\nThese are mapped directly to portal_user entity attributes.\nValues can be strings or arrays of strings (for multiselect attributes).\n",
                    "additionalProperties": {
                      "oneOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "User invited successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "User invited successfully"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v3/portal/partner/list": {
      "get": {
        "operationId": "listBusinessPartners",
        "summary": "listBusinessPartners",
        "description": "Lists all business partners linked to the businessaccount",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Business partners listed successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/BusinessPartnerItem"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v3/portal/partner/{partner_id}/resend-invitation": {
      "post": {
        "operationId": "resendPartnerInvitation",
        "summary": "resendPartnerInvitation",
        "description": "Resends an invitation email to a partner",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "name": "partner_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "description": "ID of the partner to resend invitation to"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Partner invitation resent successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Partner invitation resent successfully"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v3/portal/partner/{partner_id}/revoke": {
      "delete": {
        "operationId": "revokePartner",
        "summary": "revokePartner",
        "description": "Revokes a partner from a portal",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "name": "partner_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "description": "ID of the partner to revoke from the portal"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Partner revoked from portal successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Partner revoked from portal successfully"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v3/portal/partner/{partner_id}/disable": {
      "post": {
        "operationId": "disablePartner",
        "summary": "disablePartner",
        "description": "Disables a partner from a portal",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "name": "partner_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "description": "ID of the partner to disable from the portal"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Partner disabled from portal successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Partner disabled from portal successfully"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v3/portal/partner/{partner_id}/enable": {
      "post": {
        "operationId": "enablePartner",
        "summary": "enablePartner",
        "description": "Enables a partner from a portal",
        "tags": [
          "ECP"
        ],
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "parameters": [
          {
            "name": "partner_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "description": "ID of the partner to enable from the portal"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Partner enabled from portal successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Partner enabled from portal successfully"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v3/portal/verify-dns": {
      "post": {
        "operationId": "verifyDns",
        "summary": "verifyDns",
        "description": "Manually triggers DNS verification for a portal's domain setup. Runs the same verification logic as the scheduled processAllPendingNetworks lambda.",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "portal_id",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/PortalId"
            },
            "description": "PortalId of the portal"
          }
        ],
        "responses": {
          "200": {
            "description": "DNS verification result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "domain_status": {
                      "type": "string",
                      "description": "The status of the custom domain verification",
                      "enum": [
                        "PENDING",
                        "SUCCEED"
                      ]
                    },
                    "message": {
                      "type": "string",
                      "description": "A message describing the result"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v2/portal/proxy/execute": {
      "post": {
        "operationId": "portalProxyExecute",
        "summary": "portalProxyExecute",
        "description": "Execute an Integration Hub managed-call use case on behalf of a portal user.\nBridges PortalAuth to the Integration API by generating an internal token.\n",
        "security": [
          {
            "PortalAuth": []
          }
        ],
        "tags": [
          "ECP"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "integration_id",
                  "use_case_slug"
                ],
                "properties": {
                  "integration_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Integration ID containing the managed-call use case"
                  },
                  "use_case_slug": {
                    "type": "string",
                    "description": "Use case slug (acts as the RPC method name)"
                  },
                  "payload": {
                    "type": "object",
                    "description": "Input data for the managed-call operation",
                    "additionalProperties": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Managed-call execution result envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "additionalProperties": true,
                      "description": "Managed-call response payload. Shape is defined by the use\ncase's JSONata response_mapping; if no mapping is\nconfigured the raw external API response is returned.\n"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/v1/portal/mobile-config": {
      "get": {
        "operationId": "getMobileConfig",
        "summary": "getMobileConfig",
        "description": "Returns the portal's mobile app configuration. By default the response is build-ready (resolved): base info (display_name from the portal name, app_host from the domain, environment) and branding (logo from the portal images, colors from the design palette) are filled in. Pass raw=true to get only the stored mobile_config without resolution.",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "portal_id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Portal ID"
          },
          {
            "in": "query",
            "name": "raw",
            "required": false,
            "schema": {
              "type": "boolean"
            },
            "description": "Return only the stored mobile_config without resolving base info/branding."
          }
        ],
        "responses": {
          "200": {
            "description": "Mobile config retrieved successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MobileConfig"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      },
      "put": {
        "operationId": "putMobileConfig",
        "summary": "putMobileConfig",
        "description": "Merges the provided fields into the portal's mobile app configuration\n(deep merge). Only mobile_config is modified; all other portal settings\nare left untouched.\n",
        "tags": [
          "ECP Admin"
        ],
        "security": [
          {
            "EpilotAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "portal_id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Portal ID"
          }
        ],
        "requestBody": {
          "description": "Editable mobile fields to merge into the existing mobile_config. Only mobile-relevant settings + app branding are applied; other fields are ignored.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MobileConfigUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Mobile config updated successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MobileConfig"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    }
  },
  "components": {
    "responses": {
      "InvalidRequest": {
        "description": "The request could not be validated",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResp"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Could not authenticate the user",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResp"
            }
          }
        }
      },
      "Forbidden": {
        "description": "The user is not allowed to access this resource",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResp"
            }
          }
        }
      },
      "ForbiddenByRule": {
        "description": "The user is not allowed to access this resource",
        "content": {
          "application/json": {
            "schema": {
              "oneOf": [
                {
                  "$ref": "#/components/schemas/ErrorResp"
                },
                {
                  "$ref": "#/components/schemas/FailedRuleErrorResp"
                }
              ]
            }
          }
        }
      },
      "Conflict": {
        "description": "The request conflicts with the current state of the target resource.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResp"
            }
          }
        }
      },
      "ContractAssignmentConflict": {
        "description": "Contract was found but is not assignable in its current state.",
        "content": {
          "application/json": {
            "schema": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/ErrorResp"
                },
                {
                  "properties": {
                    "reason": {
                      "type": "string",
                      "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.",
                      "enum": [
                        "DRAFT",
                        "MULTIPLE"
                      ]
                    }
                  },
                  "required": [
                    "reason"
                  ]
                }
              ]
            }
          }
        }
      },
      "NotFound": {
        "description": "The specified resource was not found",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResp"
            }
          }
        }
      },
      "InternalServerError": {
        "description": "Internal Server Error",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResp"
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "The upstream service rate-limited the request",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResp"
            }
          }
        }
      },
      "BadGateway": {
        "description": "The upstream service failed to process the request",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResp"
            }
          }
        }
      },
      "ServiceUnavailable": {
        "description": "The requested feature is not configured",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResp"
            }
          }
        }
      },
      "GatewayTimeout": {
        "description": "The upstream service timed out",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResp"
            }
          }
        }
      },
      "ConfirmUserInvalidRequest": {
        "description": "The request could not be validated",
        "content": {
          "application/json": {
            "schema": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/ErrorResp"
                },
                {
                  "properties": {
                    "reason": {
                      "type": "string",
                      "enum": [
                        "invalid_token"
                      ]
                    }
                  },
                  "required": [
                    "reason"
                  ]
                }
              ]
            }
          }
        }
      },
      "InvalidRequestCreateMeterReading": {
        "description": "The request could not be validated",
        "content": {
          "application/json": {
            "schema": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/ErrorResp"
                },
                {
                  "properties": {
                    "reason": {
                      "type": "string",
                      "enum": [
                        "contract_period",
                        "no_counter",
                        "no_direction",
                        "timestamp_future",
                        "less_than_previous",
                        "greater_than_subsequent",
                        "meter_decommissioned",
                        "plausibility_check_failed"
                      ]
                    }
                  }
                },
                {
                  "oneOf": [
                    {
                      "properties": {
                        "reason": {
                          "enum": [
                            "plausibility_check_failed"
                          ]
                        },
                        "upper_limit": {
                          "type": "number"
                        },
                        "lower_limit": {
                          "type": "number"
                        }
                      },
                      "required": [
                        "upper_limit",
                        "lower_limit"
                      ]
                    },
                    {
                      "not": {
                        "properties": {
                          "reason": {
                            "enum": [
                              "plausibility_check_failed"
                            ]
                          }
                        }
                      }
                    }
                  ]
                }
              ]
            }
          }
        }
      }
    },
    "securitySchemes": {
      "PortalAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Portal Cognito Token"
      },
      "EpilotAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Epilot Bearer Token"
      },
      "EitherAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Portal or Epilot Bearer Token"
      },
      "AsOrganization": {
        "type": "apiKey",
        "in": "header",
        "name": "x-ivy-org-id",
        "description": "Set organization id as internal user"
      },
      "ExternalOIDCAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Authorization header with id token from external OIDC provider",
        "bearerFormat": "JWT"
      }
    },
    "schemas": {
      "PortalNotification": {
        "type": "object",
        "description": "A 360 notification addressed to a portal user.",
        "required": [
          "id",
          "read"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable string identifier of the notification (the numeric notification id as a string).",
            "example": "1234567890"
          },
          "notification_id": {
            "type": "number",
            "description": "Numeric id of the notification, used to mark it as read.",
            "example": 1234567890
          },
          "type": {
            "type": "string",
            "description": "Type of notification.",
            "example": "workflow_step_overdue"
          },
          "title": {
            "type": "object",
            "description": "Localized, already-rendered notification title.",
            "properties": {
              "en": {
                "type": "string"
              },
              "de": {
                "type": "string"
              }
            }
          },
          "message": {
            "type": "object",
            "description": "Localized, already-rendered notification message.",
            "properties": {
              "en": {
                "type": "string"
              },
              "de": {
                "type": "string"
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the notification was created."
          },
          "read": {
            "type": "boolean",
            "description": "Whether the notification has been read by the user.",
            "example": false
          },
          "redirect_url": {
            "type": "string",
            "description": "Optional URL the notification points to."
          }
        }
      },
      "MobileBuildStatus": {
        "type": "object",
        "description": "Latest build/upload status for a platform (system-written).",
        "additionalProperties": true,
        "properties": {
          "version": {
            "type": "string"
          },
          "build_number": {
            "type": "integer"
          },
          "track": {
            "type": "string",
            "description": "e.g. testflight | internal | beta"
          },
          "status": {
            "type": "string",
            "enum": [
              "building",
              "uploaded",
              "failed"
            ]
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "error": {
            "type": "string"
          }
        }
      },
      "MobileBranding": {
        "type": "object",
        "additionalProperties": true,
        "properties": {
          "app_icon": {
            "type": "string"
          },
          "splash": {
            "type": "string"
          },
          "splash_dark": {
            "type": "string"
          },
          "icon_background_color": {
            "type": "string"
          },
          "splash_background_color": {
            "type": "string"
          },
          "splash_background_color_dark": {
            "type": "string"
          }
        }
      },
      "MobileConfig": {
        "type": "object",
        "description": "Mobile app configuration for the portal. Stored inside the portal's config object. Identifiers/branding are non-secret; signing credentials live in a secure store, never here.",
        "additionalProperties": true,
        "properties": {
          "portal_id": {
            "type": "string",
            "description": "Portal id (response-only; ignored on write)."
          },
          "enabled": {
            "type": "boolean"
          },
          "display_name": {
            "type": "string",
            "description": "App display name compiled into the binary."
          },
          "app_host": {
            "type": "string",
            "description": "Host the mobile shell loads (defaults to the portal domain)."
          },
          "environment": {
            "type": "string",
            "enum": [
              "prod",
              "staging",
              "dev"
            ]
          },
          "branding": {
            "$ref": "#/components/schemas/MobileBranding"
          },
          "ios": {
            "type": "object",
            "additionalProperties": true,
            "properties": {
              "bundle_id": {
                "type": "string",
                "description": "iOS bundle id (matches the App Store Connect app)."
              },
              "team_id": {
                "type": "string",
                "description": "Apple Developer Team ID."
              },
              "credentials_status": {
                "type": "string",
                "enum": [
                  "not_configured",
                  "configured"
                ]
              },
              "app_store_id": {
                "type": "string",
                "description": "Numeric App Store id (system-written after first upload)."
              },
              "store_url": {
                "type": "string",
                "description": "System-written App Store URL."
              },
              "last_build": {
                "$ref": "#/components/schemas/MobileBuildStatus"
              }
            }
          },
          "android": {
            "type": "object",
            "additionalProperties": true,
            "properties": {
              "package_name": {
                "type": "string",
                "description": "Android package name (matches the Play Console app)."
              },
              "credentials_status": {
                "type": "string",
                "enum": [
                  "not_configured",
                  "configured"
                ]
              },
              "upload_key_status": {
                "type": "string",
                "description": "Play App Signing upload-key state.",
                "enum": [
                  "not_configured",
                  "generated",
                  "enrolled"
                ]
              },
              "store_url": {
                "type": "string",
                "description": "System-written Play Store URL."
              },
              "last_build": {
                "$ref": "#/components/schemas/MobileBuildStatus"
              }
            }
          },
          "ota": {
            "$ref": "#/components/schemas/MobileOtaConfig"
          }
        }
      },
      "MobileConfigUpdate": {
        "type": "object",
        "description": "Editable mobile fields for PUT. Only mobile-relevant settings + app branding can be changed. Portal-derived values (display_name, app_host), the portal logo, and system-written fields (credentials_status, app_store_id, last_build, …) are ignored if sent.",
        "additionalProperties": true,
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "ios": {
            "type": "object",
            "additionalProperties": true,
            "properties": {
              "bundle_id": {
                "type": "string"
              },
              "team_id": {
                "type": "string"
              },
              "store_url": {
                "type": "string"
              },
              "app_store_id": {
                "type": "string"
              }
            }
          },
          "android": {
            "type": "object",
            "additionalProperties": true,
            "properties": {
              "package_name": {
                "type": "string"
              },
              "store_url": {
                "type": "string"
              }
            }
          },
          "branding": {
            "$ref": "#/components/schemas/MobileBranding"
          },
          "ota": {
            "$ref": "#/components/schemas/MobileOtaConfig"
          }
        }
      },
      "MobileOtaConfig": {
        "type": "object",
        "description": "OTA (over-the-air) update settings for the portal's mobile app. Drives the OTA build pipeline and the per-portal manifest. channel / update_strategy / min_native_version are epilot-internal controls.",
        "additionalProperties": true,
        "properties": {
          "enabled": {
            "type": "boolean",
            "description": "Whether OTA updates are enabled for this portal."
          },
          "channel": {
            "type": "string",
            "enum": [
              "canary",
              "stable"
            ],
            "description": "Release channel this portal follows."
          },
          "auto_update": {
            "type": "boolean",
            "description": "Whether the app auto-updates or prompts the user."
          },
          "update_strategy": {
            "type": "string",
            "enum": [
              "next-launch",
              "immediate"
            ],
            "description": "When to apply a downloaded bundle."
          },
          "min_native_version": {
            "type": "string",
            "description": "Minimum native app version required to load OTA bundles."
          }
        }
      },
      "OtaPortal": {
        "type": "object",
        "description": "A portal that has mobile OTA updates enabled.",
        "required": [
          "domain",
          "channel",
          "autoUpdate",
          "updateStrategy"
        ],
        "properties": {
          "domain": {
            "type": "string",
            "description": "Portal hostname — the OTA manifest filename ({domain}.json).",
            "example": "kundenportal.twl.de"
          },
          "channel": {
            "type": "string",
            "enum": [
              "canary",
              "stable"
            ]
          },
          "autoUpdate": {
            "type": "boolean"
          },
          "updateStrategy": {
            "type": "string",
            "enum": [
              "next-launch",
              "immediate"
            ]
          },
          "minNativeVersion": {
            "type": "string",
            "example": "1.0.0"
          }
        }
      },
      "ContextEntity": {
        "type": "object",
        "description": "An entity reference for context-aware operations",
        "properties": {
          "entity_schema": {
            "type": "string",
            "description": "Entity schema",
            "example": "contract"
          },
          "entity_id": {
            "type": "string",
            "format": "uuid",
            "description": "Entity id",
            "example": "5da0a718-c822-403d-9f5d-20d4584e0528"
          }
        },
        "required": [
          "entity_id",
          "entity_schema"
        ]
      },
      "ContextEntities": {
        "type": "array",
        "description": "Additional entities to include in the context for variable interpolation. Portal User and Contact entities are automatically part of the context.",
        "items": {
          "$ref": "#/components/schemas/ContextEntity"
        },
        "example": [
          {
            "entity_id": "5da0a718-c822-403d-9f5d-20d4584e0528",
            "entity_schema": "contract"
          }
        ]
      },
      "ErrorResp": {
        "type": "object",
        "properties": {
          "message": {
            "type": "string",
            "description": "Error message"
          },
          "reason": {
            "type": "string",
            "description": "Machine-readable cause, when the API can name one. `PORTAL_USER_PENDING_ACTIVATION` means the portal user is authenticated but not mapped to a contact yet, so re-authenticating will not help."
          }
        }
      },
      "FailedRuleErrorResp": {
        "type": "object",
        "properties": {
          "message": {
            "type": "string",
            "description": "Error message"
          },
          "failed_rule": {
            "type": "object",
            "description": "Failed validation rule"
          }
        }
      },
      "EmailTemplates": {
        "type": "object",
        "description": "Email templates used for authentication and internal processes",
        "properties": {
          "confirmAccount": {
            "$ref": "#/components/schemas/EntityId",
            "nullable": true,
            "description": "ID of the confirmation email template upon registration"
          },
          "advancedAuth": {
            "$ref": "#/components/schemas/EntityId",
            "nullable": true,
            "description": "ID of the advanced Auth with login link and login code"
          },
          "advancedMFA": {
            "$ref": "#/components/schemas/EntityId",
            "nullable": true,
            "deprecated": true,
            "description": "ID of the advanced MFA with login link and login code"
          },
          "journeySignUp": {
            "$ref": "#/components/schemas/EntityId",
            "nullable": true,
            "description": "ID of the email template for signing up from Journeys"
          },
          "journeySignInOneTimePassword": {
            "$ref": "#/components/schemas/EntityId",
            "nullable": true,
            "description": "ID of the email template for OTP to sign in from Journeys"
          },
          "journeyLoginOTP": {
            "$ref": "#/components/schemas/EntityId",
            "nullable": true,
            "description": "ID of the email template for OTP to sign in from Journeys",
            "deprecated": true
          },
          "forgotPassword": {
            "$ref": "#/components/schemas/EntityId",
            "nullable": true,
            "description": "ID of the email template for forgot password"
          },
          "invitation": {
            "$ref": "#/components/schemas/EntityId",
            "nullable": true,
            "description": "ID of the email template for invitation when the user is just invited to register on the portal."
          },
          "partnerInvitation": {
            "$ref": "#/components/schemas/EntityId",
            "nullable": true,
            "description": "ID of the email template for invitation when a partner invites another partner within the portal."
          },
          "onNewQuote": {
            "$ref": "#/components/schemas/EntityId",
            "nullable": true,
            "description": "ID of the email template for new quote"
          },
          "onMapAPendingUser": {
            "$ref": "#/components/schemas/EntityId",
            "nullable": true,
            "description": "ID of the email template for mapping a pending portal user with a contact"
          },
          "onDocUpload": {
            "$ref": "#/components/schemas/EntityId",
            "nullable": true,
            "description": "ID of the email template for document upload"
          },
          "onWorkflowStepAssigned": {
            "$ref": "#/components/schemas/EntityId",
            "nullable": true,
            "description": "ID of the email template for workflow step assignment"
          },
          "confirmEmailUpdate": {
            "$ref": "#/components/schemas/EntityId",
            "nullable": true,
            "description": "ID of the email template for setting password while updating email"
          },
          "verifyCodeToSetPassword": {
            "$ref": "#/components/schemas/EntityId",
            "nullable": true,
            "description": "ID of the email template for setting password while verifying code"
          }
        }
      },
      "DeleteEntityFile": {
        "type": "object",
        "required": [
          "file_entity_ids",
          "entity_type",
          "entity_id"
        ],
        "properties": {
          "entity_id": {
            "$ref": "#/components/schemas/EntityId"
          },
          "entity_type": {
            "type": "string",
            "example": "order",
            "description": "Entity type"
          },
          "file_entity_ids": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EntityId"
            },
            "minItems": 1,
            "uniqueItems": true,
            "description": "Array of file entity IDs"
          }
        }
      },
      "SaveEntityFile": {
        "type": "object",
        "required": [
          "entity_id",
          "entity_type",
          "files"
        ],
        "properties": {
          "entity_id": {
            "$ref": "#/components/schemas/EntityId"
          },
          "entity_type": {
            "type": "string",
            "example": "order",
            "description": "Entity type"
          },
          "files": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "filename",
                "s3ref"
              ],
              "properties": {
                "filename": {
                  "type": "string",
                  "example": "document.pdf",
                  "description": "File name"
                },
                "access_control": {
                  "type": "string",
                  "default": "private",
                  "enum": [
                    "private",
                    "public-read"
                  ],
                  "description": "Access control level for the file. Deprecated - all files are private.",
                  "deprecated": true
                },
                "s3ref": {
                  "type": "object",
                  "required": [
                    "bucket",
                    "key"
                  ],
                  "properties": {
                    "bucket": {
                      "type": "string",
                      "example": 12345,
                      "description": "S3 bucket name"
                    },
                    "key": {
                      "type": "string",
                      "example": 12345,
                      "description": "S3 key"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "SavePortalFile": {
        "type": "object",
        "required": [
          "files",
          "origin"
        ],
        "properties": {
          "origin": {
            "$ref": "#/components/schemas/Origin",
            "description": "Origin of the portal"
          },
          "files": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "file_type"
              ],
              "properties": {
                "filename": {
                  "type": "string",
                  "example": 12345,
                  "description": "File name"
                },
                "file_type": {
                  "type": "string",
                  "example": "orderRightTeaser",
                  "description": "File type"
                },
                "_tags": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  },
                  "example": 12345,
                  "description": "Array of file tags"
                },
                "s3ref": {
                  "type": "object",
                  "required": [
                    "bucket",
                    "key"
                  ],
                  "properties": {
                    "bucket": {
                      "type": "string",
                      "example": 12345,
                      "description": "S3 bucket name"
                    },
                    "key": {
                      "type": "string",
                      "example": 12345,
                      "description": "S3 key"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "ExtraSchemaAttributes": {
        "type": "array",
        "items": {
          "type": "object",
          "required": [
            "name",
            "label",
            "group"
          ],
          "properties": {
            "name": {
              "type": "string",
              "description": "Attribute name"
            },
            "label": {
              "type": "string",
              "description": "Attribute label"
            },
            "group": {
              "type": "string",
              "description": "Attribute group"
            }
          }
        }
      },
      "Origin": {
        "type": "string",
        "description": "Origin of the portal"
      },
      "AllowedFileExtensions": {
        "type": "object",
        "description": "Allowed file extensions for upload",
        "properties": {
          "document": {
            "type": "array",
            "items": {
              "type": "string",
              "example": "pdf"
            }
          },
          "image": {
            "type": "array",
            "items": {
              "type": "string",
              "example": "jpg"
            }
          },
          "spreadsheet": {
            "type": "array",
            "items": {
              "type": "string",
              "example": "xls"
            }
          },
          "presentation": {
            "type": "array",
            "items": {
              "type": "string",
              "example": "ppt"
            }
          },
          "audioVideo": {
            "type": "array",
            "items": {
              "type": "string",
              "example": "mp4"
            }
          },
          "email": {
            "type": "array",
            "items": {
              "type": "string",
              "example": "eml"
            }
          },
          "archive": {
            "type": "array",
            "items": {
              "type": "string",
              "example": "zip"
            }
          },
          "cad": {
            "type": "array",
            "items": {
              "type": "string",
              "example": "cad"
            }
          },
          "calendar": {
            "type": "array",
            "items": {
              "type": "string",
              "example": "ics"
            }
          },
          "other": {
            "type": "array",
            "items": {
              "type": "string",
              "example": "txt"
            }
          }
        },
        "additionalProperties": false
      },
      "UpdateOnlyPortalConfigAttributes": {
        "type": "object",
        "properties": {
          "entity_actions": {
            "type": "array",
            "description": "Journey actions allowed on an entity by a portal user",
            "items": {
              "type": "object",
              "properties": {
                "journey_id": {
                  "$ref": "#/components/schemas/EntityId"
                },
                "slug": {
                  "$ref": "#/components/schemas/EntitySlug"
                },
                "action_Label": {
                  "type": "object",
                  "properties": {
                    "en": {
                      "type": "string"
                    },
                    "de": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "extensions": {
            "type": "array",
            "description": "Configured Portal extensions",
            "items": {
              "$ref": "#/components/schemas/ExtensionConfig"
            }
          },
          "extension_hooks": {
            "type": "object",
            "description": "Configured Portal extensions hooks",
            "additionalProperties": {
              "$ref": "#/components/schemas/ExtensionHookSelection"
            }
          },
          "default_user_to_notify": {
            "type": "object",
            "description": "Default 360 user to notify upon an internal notification",
            "properties": {
              "onPendingUser": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/AdminUser"
                },
                "description": "Default admin users for pending user notification to notify"
              }
            }
          },
          "identity_providers": {
            "type": "array",
            "description": "SSO identity providers for the portal. When sent on a portal save (PUT/POST),\nthe list is fully synced — incoming providers are upserted and any existing\nproviders not in the list are deleted. Omit the field to leave SSO\nconfiguration unchanged; send an empty array to remove all providers.\n\nSecrets: a provider sent without `oidc_config.client_secret` keeps the\nstored secret for the same slug; an explicit empty string clears it.\n`getPortalConfigV3` returns providers with raw secrets redacted;\n`{{ env.VAR }}` references pass through.\n",
            "items": {
              "$ref": "#/components/schemas/ProviderConfig"
            }
          }
        }
      },
      "CommonConfigAttributes": {
        "type": "object",
        "properties": {
          "mobile_config": {
            "description": "Mobile app configuration (top-level; moved out of the config blob).",
            "$ref": "#/components/schemas/MobileConfig"
          },
          "enabled": {
            "type": "boolean",
            "description": "Enable/Disable the portal access"
          },
          "name": {
            "type": "string",
            "description": "A short name to identify your portal",
            "example": "Installer Portal"
          },
          "domain": {
            "type": "string",
            "description": "The URL on which the portal is accessible",
            "example": "abc.com"
          },
          "is_epilot_domain": {
            "type": "boolean",
            "description": "Mark true if the domain is an Epilot domain"
          },
          "epilot_domain": {
            "type": "string",
            "description": "The URL on which the portal is accessible",
            "example": "example-portal-12345.ecp.epilot.cloud"
          },
          "domain_settings": {
            "$ref": "#/components/schemas/DomainSettings"
          },
          "design_id": {
            "$ref": "#/components/schemas/EntityId",
            "description": "ID of the design used to build the portal"
          },
          "allowed_portal_entities": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Allowed portal entities for the portal",
            "example": [
              "contact",
              "contract"
            ]
          },
          "self_registration_setting": {
            "type": "string",
            "enum": [
              "ALLOW_WITH_CONTACT_CREATION",
              "ALLOW_WITHOUT_CONTACT_CREATION",
              "DENY",
              "ALWAYS_CREATE_CONTACT",
              "DISALLOW_COMPLETELY",
              "BLOCK_IF_PORTAL_USER_EXISTS"
            ]
          },
          "self_registration_account_setting": {
            "type": "string",
            "description": "Controls behavior of self-registration when account is the registration\nentity. `BLOCK_IF_PORTAL_USER_EXISTS` matches an existing account and\nrejects the request when any portal user is already linked to that\naccount (no creation). Blocking can also be enabled on the other\nnon-create modes via `block_registration_if_portal_user_exists`.\n",
            "enum": [
              "ALLOW_WITH_CREATION",
              "DENY",
              "ALWAYS_CREATE",
              "BLOCK_IF_PORTAL_USER_EXISTS",
              "DISALLOW_COMPLETELY"
            ]
          },
          "block_registration_if_portal_user_exists": {
            "type": "boolean",
            "description": "Account-mode only. Reject registration when the resolved account already\nhas any portal user (any portal user whose mapped contact is linked to\nthe account).\n"
          },
          "self_registration_entity": {
            "type": "string",
            "description": "Entity type used as the primary identifier for self-registration",
            "enum": [
              "contact",
              "account"
            ]
          },
          "user_account_self_management": {
            "type": "boolean",
            "description": "Enable or disable user account self management",
            "example": false
          },
          "feature_settings": {
            "type": "object",
            "description": "Feature settings for the portal",
            "properties": {
              "start_page": {
                "type": "boolean",
                "description": "Start page feature flag"
              },
              "billing": {
                "type": "boolean",
                "description": "Billing feature flag"
              },
              "change_due_date": {
                "type": "boolean",
                "description": "Change due date feature flag"
              },
              "new_design": {
                "type": "boolean",
                "description": "Enable or disable the new design for the portal"
              },
              "mcp_enabled": {
                "type": "boolean",
                "description": "Enable the MCP (AI agent) connector channel for this portal"
              },
              "mcp_grant_version": {
                "type": "integer",
                "minimum": 0,
                "default": 0,
                "readOnly": true,
                "description": "Server-managed generation used to invalidate MCP grants after the connector is disabled or re-enabled"
              }
            }
          },
          "accessToken": {
            "type": "string",
            "description": "Access token for the portal"
          },
          "advanced_mfa": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "Advanced MFA feature flag"
              }
            }
          },
          "auth_settings": {
            "type": "object",
            "description": "Authentication settings for the portal",
            "properties": {
              "passwordless_login": {
                "type": "object",
                "properties": {
                  "enabled": {
                    "type": "boolean",
                    "description": "Passwordless login feature flag"
                  }
                }
              },
              "entry_point": {
                "type": "string",
                "enum": [
                  "PASSWORD",
                  "SSO"
                ]
              },
              "preferred_sso_providers": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ProviderSlug"
                }
              },
              "auto_redirect_to_sso": {
                "type": "boolean",
                "description": "Decide whether to automatically redirect to the provider page during login, which would completely bypass showing the portal authentication page."
              },
              "prevent_user_enumeration": {
                "type": "boolean",
                "description": "Opt-in. When true, suppresses responses that reveal whether a user exists for public, pre-authentication actions (the login entry-point check and self-registration), at the expense of some UX. Already-authenticated actions are unaffected. Default false.\n"
              }
            }
          },
          "cognito_details": {
            "type": "object",
            "description": "AWS Cognito Pool details for the portal",
            "properties": {
              "cognito_user_pool_client_id": {
                "type": "string",
                "example": "6bsd0jkgoie74k2i8mrhc1vest",
                "description": "Cognito user pool client ID"
              },
              "cognito_user_pool_arn": {
                "type": "string",
                "example": "arn:aws:cognito-idp:us-east-1:123412341234:userpool/us-east-1_123412341",
                "description": "Cognito user pool ARN"
              },
              "cognito_user_pool_id": {
                "type": "string",
                "example": "eu-central-1_CUEQRNbUb",
                "description": "Cognito user pool ID"
              },
              "timeouts": {
                "type": "object",
                "description": "Timeouts for the cognito tokens",
                "properties": {
                  "refresh_token": {
                    "type": "integer",
                    "example": 300,
                    "description": "Timeout for the refresh token"
                  },
                  "access_token": {
                    "type": "integer",
                    "example": 300,
                    "description": "Timeout for the access token"
                  },
                  "id_token": {
                    "type": "integer",
                    "example": 300,
                    "description": "Timeout for the id token"
                  }
                }
              },
              "advanced_authentication": {
                "type": "object",
                "description": "Advanced authentication settings for the portal",
                "properties": {
                  "user_activity_logging": {
                    "type": "boolean",
                    "example": true,
                    "description": "Enables detailed logging of user authentication attempts including risk assessments, IP addresses, user agents, and device information. These logs can be used for security analysis and monitoring."
                  },
                  "adaptive_authentication": {
                    "type": "boolean",
                    "example": true,
                    "description": "Automatically assesses risk for every authentication session. Based on risk ratings, can block authentication or require MFA for suspicious sign-in attempts. Helps protect user accounts from potential attacks by adapting security measures in real-time."
                  },
                  "compromised_credentials_detection": {
                    "type": "boolean",
                    "example": true,
                    "description": "Checks passwords against databases of leaked and commonly-guessed passwords during sign-up, sign-in, and password reset. Blocks or warns users when insecure passwords are detected, preventing unauthorized access from compromised credentials."
                  }
                }
              },
              "password_policy": {
                "type": "object",
                "description": "Password policy for the portal",
                "properties": {
                  "minimum_length": {
                    "type": "integer",
                    "example": 8,
                    "description": "Minimum password length"
                  },
                  "maximum_length": {
                    "type": "integer",
                    "example": 256,
                    "description": "Maximum password length"
                  },
                  "require_lowercase": {
                    "type": "boolean",
                    "example": true,
                    "description": "Require lowercase characters"
                  },
                  "require_uppercase": {
                    "type": "boolean",
                    "example": true,
                    "description": "Require uppercase characters"
                  },
                  "require_numbers": {
                    "type": "boolean",
                    "example": true,
                    "description": "Require numbers"
                  },
                  "require_symbols": {
                    "type": "boolean",
                    "example": true,
                    "description": "Require symbols"
                  },
                  "password_history_size": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 24,
                    "example": 3,
                    "description": "Number of previous passwords a user is prevented from reusing. Set to 0 to disable reuse prevention. Maps to Cognito's PasswordHistorySize and requires the user pool to be on the Essentials or Plus feature plan."
                  }
                }
              }
            }
          },
          "config": {
            "type": "string",
            "description": "Stringified object with configuration details"
          },
          "contact_identifiers": {
            "type": "array",
            "description": "Deprecated. Use registration_identifiers instead.",
            "deprecated": true,
            "items": {
              "type": "string"
            },
            "example": [
              "email",
              "last_name"
            ]
          },
          "approval_state_attributes": {
            "type": "object",
            "additionalProperties": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "example": {
              "contact": [
                "name",
                "address"
              ],
              "contract": [
                "installment_amount"
              ]
            }
          },
          "email_templates": {
            "$ref": "#/components/schemas/EmailTemplates"
          },
          "images": {
            "type": "object",
            "description": "Teaser & Banner Image web links",
            "properties": {
              "orderLeftTeaser": {
                "type": "string",
                "nullable": true,
                "example": "https://epilot-bucket.s3.eu-central-1.amazonaws.com/12344/6538fddb-f0e9-4f0f-af51-6e57891ff20a/order-left-teaser.jpeg",
                "description": "URL of the order left teaser image"
              },
              "orderRightTeaser": {
                "type": "string",
                "nullable": true,
                "example": "https://epilot-bucket.s3.eu-central-1.amazonaws.com/12344/6538fddb-f0e9-4f0f-af51-6e57891ff20a/order-right-teaser.jpeg",
                "description": "URL of the order right teaser image"
              },
              "welcomeBanner": {
                "type": "string",
                "nullable": true,
                "example": "https://epilot-bucket.s3.eu-central-1.amazonaws.com/12344/6538fddb-f0e9-4f0f-af51-6e57891ff20a/welcome-banner.jpeg",
                "description": "URL of the welcome banner image"
              }
            }
          },
          "entity_identifiers": {
            "type": "object",
            "description": "Identifiers used to identify an entity by a portal user. Deprecated. Use contract_identifiers instead.",
            "deprecated": true,
            "properties": {
              "type": {
                "type": "object",
                "properties": {
                  "isEnabled": {
                    "type": "boolean",
                    "description": "Enable/Disable the entity identifier"
                  },
                  "attributes": {
                    "type": "array",
                    "description": "Attributes used to identify an entity",
                    "items": {
                      "type": "string",
                      "example": "contract_number"
                    }
                  }
                }
              }
            }
          },
          "contract_identifiers": {
            "type": "array",
            "description": "Identifiers to identify a contract by a portal user.",
            "items": {
              "$ref": "#/components/schemas/ContractIdentifier"
            },
            "example": [
              {
                "name": "email",
                "schema": "contact"
              },
              {
                "name": "last_name",
                "schema": "contact"
              },
              {
                "name": "contract_number",
                "schema": "contract"
              }
            ]
          },
          "contract_selector_config": {
            "type": "object",
            "description": "Configuration for contract selector in the portal",
            "properties": {
              "show_inactive": {
                "type": "boolean",
                "description": "Whether to show inactive contracts in the selector"
              },
              "title_path": {
                "type": "string",
                "description": "Path to the property to use as the contract title"
              }
            }
          },
          "registration_identifiers": {
            "type": "array",
            "description": "Identifiers to identify a contact of a portal user during the registration.",
            "items": {
              "$ref": "#/components/schemas/ContractIdentifier"
            },
            "example": [
              {
                "name": "last_name",
                "schema": "contact"
              },
              {
                "name": "contract_number",
                "schema": "contract"
              }
            ]
          },
          "surfaces": {
            "type": "array",
            "description": "Surfaces this portal's data is reached from besides the portal UI itself (public journeys on the website, chat). Configured under Security > Surfaces. Each surface defines how a caller authenticates and what data access applies on it; the portal UI is the implicit default surface (login, default scope) and is not listed here. A surface with `authentication: registration_identifiers` is what makes `identifyContact` issue tokens for this portal: without one, `identifyContact` returns 403.\n",
            "items": {
              "$ref": "#/components/schemas/PortalSurface"
            }
          },
          "contact_identifiers_for_account": {
            "type": "array",
            "description": "Account-mode only. Identifiers on the contact entity of the primarily\nidentified account. Used to pick an existing related contact within the\nresolved account; if none matches, the values are written onto the new\ncontact that is created and linked to the account.\n",
            "items": {
              "$ref": "#/components/schemas/RegistrationIdentifier"
            },
            "example": [
              {
                "name": "first_name",
                "schema": "contact"
              },
              {
                "name": "last_name",
                "schema": "contact"
              }
            ]
          },
          "additional_contact_attributes": {
            "type": "array",
            "description": "Contact attributes collected from the user during self-registration that are\nwritten onto the newly created contact but are not used to identify an\nexisting one.\n",
            "items": {
              "$ref": "#/components/schemas/AdditionalContactAttribute"
            },
            "example": [
              {
                "name": "first_name",
                "required": true
              },
              {
                "name": "last_name",
                "required": true
              }
            ]
          },
          "triggered_journeys": {
            "type": "array",
            "description": "Journeys automatically opened on a portal user action",
            "items": {
              "type": "object",
              "properties": {
                "trigger_name": {
                  "type": "string",
                  "enum": [
                    "FIRST_LOGIN",
                    "ACCEPT_ORDER",
                    "DECLINE_ORDER"
                  ]
                },
                "journey_id": {
                  "$ref": "#/components/schemas/EntityId"
                },
                "context_params": {
                  "type": "array",
                  "description": "Context parameters forwarded to the journey when it is\nauto-triggered. Values may contain handlebars templates\nthat reference the available context (e.g.\n`{{contact._id}}`, `{{portal_user.email}}`,\n`{{order._id}}`) — these are resolved at trigger time by\n`GET /v2/portal/config/triggered-journeys/{trigger_name}`\nusing the caller's auth context plus runtime entities\nsupplied via the `context_entities` query param.\n",
                  "items": {
                    "type": "object",
                    "properties": {
                      "key": {
                        "type": "string"
                      },
                      "value": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "entity_edit_rules": {
            "type": "array",
            "description": "Rules for editing an entity by a portal user",
            "items": {
              "type": "object",
              "properties": {
                "slug": {
                  "$ref": "#/components/schemas/EntitySlug"
                },
                "attribute": {
                  "type": "string",
                  "example": "first_name"
                },
                "rule_type": {
                  "type": "string",
                  "enum": [
                    "cadence",
                    "relative_to_current_value",
                    "days_before_date",
                    "overdue_payments"
                  ]
                },
                "cadence_period_type": {
                  "type": "string",
                  "enum": [
                    "days",
                    "weeks",
                    "months"
                  ]
                },
                "cadence_period": {
                  "type": "number",
                  "example": 1
                },
                "changes_allowed": {
                  "type": "integer",
                  "example": 1
                },
                "grace_period": {
                  "type": "integer",
                  "example": 1
                },
                "allowed_increment": {
                  "type": "string",
                  "example": "10%"
                },
                "allowed_decrement": {
                  "type": "string",
                  "example": "10%"
                },
                "number_of_days_before_restriction": {
                  "type": "integer",
                  "example": 10
                }
              }
            }
          },
          "allowed_file_extensions": {
            "$ref": "#/components/schemas/AllowedFileExtensions"
          },
          "prevent_search_engine_indexing": {
            "type": "boolean",
            "description": "Prevent indexing by search engines"
          },
          "meter_reading_grace_period": {
            "type": "number",
            "description": "Grace period in days for meter readings"
          },
          "inactive_contract_cutoff_years": {
            "type": "number",
            "description": "Number of years to look back for showing inactive contracts in the portal"
          },
          "is_dummy": {
            "type": "boolean",
            "description": "Whether this is a dummy/test portal configuration"
          },
          "is_v3_item": {
            "type": "boolean",
            "description": "Whether this is a v3 portal configuration"
          },
          "published_revision_id": {
            "type": "string",
            "readOnly": true,
            "description": "The revision currently live on this portal. Absent until the first publish.",
            "example": "2026-08-25T14:03:11.482Z-a7f3c1d9"
          },
          "portal_id": {
            "$ref": "#/components/schemas/PortalId"
          },
          "portal_sk_v3": {
            "type": "string",
            "example": "PORTAL_CONFIG#453ad7bf-86d5-46c8-8252-bcc868df5e3c",
            "description": "Key of the portal config"
          },
          "origin": {
            "$ref": "#/components/schemas/Origin"
          },
          "pages": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/Page"
            }
          },
          "global_blocks": {
            "type": "object",
            "description": "Portal-level blocks shared across all pages (e.g. footer). Keyed by block id.",
            "additionalProperties": {
              "$ref": "#/components/schemas/Block"
            }
          },
          "notification_triggers": {
            "type": "array",
            "description": "Configures which 360 events generate an in-app notification for the portal user. Each enabled trigger renders the referenced notification template and creates a notification addressed to the portal user. Admin/builder-only — never exposed via the public portal config.",
            "items": {
              "$ref": "#/components/schemas/NotificationTriggerConfig"
            }
          },
          "engagement_center_enabled": {
            "type": "boolean",
            "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."
          }
        }
      },
      "NotificationTriggerConfig": {
        "type": "object",
        "required": [
          "trigger_type"
        ],
        "properties": {
          "trigger_type": {
            "type": "string",
            "description": "The 360 event that fires this notification trigger.",
            "enum": [
              "entity_created",
              "entity_assigned",
              "workflow_step_overdue"
            ]
          },
          "entity_schema": {
            "type": "string",
            "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.",
            "example": "opportunity"
          },
          "enabled": {
            "type": "boolean",
            "description": "Whether this trigger is active.",
            "default": true
          },
          "template_id": {
            "type": "string",
            "format": "uuid",
            "description": "Entity id of the notification_template to render for this trigger."
          }
        }
      },
      "UpsertPortalConfig": {
        "allOf": [
          {
            "$ref": "#/components/schemas/UpdateOnlyPortalConfigAttributes"
          },
          {
            "$ref": "#/components/schemas/CommonConfigAttributes"
          }
        ]
      },
      "PortalConfig": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CommonConfigAttributes"
          },
          {
            "properties": {
              "id": {
                "type": "string",
                "example": 12345,
                "description": "ID of the organization",
                "deprecated": true
              },
              "organization_id": {
                "type": "string",
                "example": 12345,
                "description": "ID of the organization"
              },
              "org_settings": {
                "type": "object",
                "description": "Organization settings",
                "properties": {
                  "canary": {
                    "type": "object",
                    "description": "Canary feature flag",
                    "properties": {
                      "enabled": {
                        "type": "boolean",
                        "description": "Enable/Disable the canary feature"
                      }
                    }
                  },
                  "notracking": {
                    "type": "object",
                    "description": "Disable Advanced Usage Metrics",
                    "properties": {
                      "enabled": {
                        "type": "boolean",
                        "description": "Disable browser-side scripts that track advanced usage metrics"
                      }
                    }
                  }
                }
              },
              "feature_flags": {
                "type": "object",
                "description": "Feature flags for the portal",
                "additionalProperties": {
                  "type": "boolean"
                }
              },
              "grants": {
                "type": "array",
                "description": "Permissions granted to a portal user while accessing entities",
                "items": {
                  "$ref": "#/components/schemas/Grant"
                }
              },
              "identity_providers": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ProviderPublicConfig"
                }
              }
            }
          }
        ]
      },
      "UpsertPortalWidget": {
        "type": "object",
        "required": [
          "widgets"
        ],
        "properties": {
          "portal_sk_v3": {
            "type": "string",
            "description": "V3 portal-scoped storage key for the widget configuration"
          },
          "is_v3_item": {
            "type": "boolean",
            "description": "Indicates whether the widget configuration is stored as a V3 portal-scoped item"
          },
          "widgets": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PortalWidget"
            }
          }
        }
      },
      "DomainSettings": {
        "type": "object",
        "description": "Domain settings for the portal",
        "properties": {
          "is_custom_domain_enabled": {
            "type": "boolean",
            "description": "Whether the custom domain is enabled"
          },
          "is_epilot_domain_enabled": {
            "type": "boolean",
            "description": "Whether the Epilot domain is enabled"
          },
          "is_redirection_enabled": {
            "type": "boolean",
            "description": "Whether the redirection is enabled"
          }
        }
      },
      "WidgetBase": {
        "type": "object",
        "required": [
          "id",
          "type",
          "listIndex"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "ACTION_WIDGET",
              "CONTENT_WIDGET",
              "ENTITY_WIDGET",
              "TEASER_WIDGET",
              "DOCUMENT_WIDGET",
              "PAYMENT_WIDGET",
              "METER_READING_WIDGET",
              "METER_CHART_WIDGET",
              "CAMPAIGN_WIDGET",
              "PRODUCT_RECOMMENDATIONS_WIDGET"
            ]
          },
          "listIndex": {
            "type": "integer",
            "description": "Index of the widget in the list, used for ordering (left or right)"
          },
          "headline": {
            "type": "object",
            "properties": {
              "en": {
                "type": "string"
              },
              "de": {
                "type": "string"
              }
            }
          },
          "subHeadline": {
            "type": "object",
            "properties": {
              "en": {
                "type": "string"
              },
              "de": {
                "type": "string"
              }
            }
          }
        }
      },
      "EntityWidget": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WidgetBase"
          },
          {
            "type": "object",
            "properties": {
              "schema": {
                "type": "string"
              }
            }
          }
        ]
      },
      "MeterReadingWidget": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WidgetBase"
          },
          {
            "type": "object",
            "properties": {
              "schema": {
                "type": "string"
              }
            }
          }
        ]
      },
      "MeterChartWidget": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WidgetBase"
          },
          {
            "type": "object",
            "properties": {
              "schema": {
                "type": "string"
              }
            }
          }
        ]
      },
      "WidgetAction": {
        "type": "object",
        "properties": {
          "_id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "link",
              "journey"
            ]
          },
          "label": {
            "type": "object",
            "properties": {
              "en": {
                "type": "string"
              },
              "de": {
                "type": "string"
              }
            }
          },
          "url": {
            "type": "string"
          },
          "rules": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "attribute": {
                  "type": "string"
                },
                "attribute_value": {
                  "type": "string"
                },
                "entity": {
                  "type": "string"
                }
              },
              "required": [
                "attribute",
                "attribute_value",
                "entity"
              ]
            }
          }
        },
        "required": [
          "type",
          "label",
          "url",
          "_id"
        ]
      },
      "ActionWidget": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WidgetBase"
          },
          {
            "type": "object",
            "properties": {
              "actions": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/WidgetAction"
                }
              }
            }
          }
        ]
      },
      "TeaserWidget": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WidgetBase"
          },
          {
            "type": "object",
            "properties": {
              "imageUrl": {
                "type": "string"
              },
              "button": {
                "type": "object",
                "properties": {
                  "label": {
                    "type": "object",
                    "properties": {
                      "en": {
                        "type": "string"
                      },
                      "de": {
                        "type": "string"
                      }
                    }
                  },
                  "url": {
                    "type": "string"
                  }
                }
              }
            }
          }
        ]
      },
      "ContentWidget": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WidgetBase"
          },
          {
            "type": "object",
            "properties": {
              "content": {
                "type": "string"
              }
            }
          }
        ]
      },
      "DocumentWidget": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WidgetBase"
          }
        ]
      },
      "PaymentWidget": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WidgetBase"
          }
        ]
      },
      "CampaignWidget": {
        "allOf": [
          {
            "$ref": "#/components/schemas/TeaserWidget"
          },
          {
            "type": "object",
            "properties": {
              "campaign_id": {
                "type": "string",
                "description": "ID of the campaign"
              }
            }
          }
        ]
      },
      "ProductRecommendationsWidget": {
        "allOf": [
          {
            "$ref": "#/components/schemas/TeaserWidget"
          },
          {
            "type": "object",
            "properties": {
              "campaign_id": {
                "type": "string",
                "description": "ID of the campaign"
              }
            }
          }
        ]
      },
      "PortalWidget": {
        "anyOf": [
          {
            "$ref": "#/components/schemas/EntityWidget"
          },
          {
            "$ref": "#/components/schemas/ContentWidget"
          },
          {
            "$ref": "#/components/schemas/ActionWidget"
          },
          {
            "$ref": "#/components/schemas/TeaserWidget"
          },
          {
            "$ref": "#/components/schemas/DocumentWidget"
          },
          {
            "$ref": "#/components/schemas/PaymentWidget"
          },
          {
            "$ref": "#/components/schemas/MeterReadingWidget"
          },
          {
            "$ref": "#/components/schemas/MeterChartWidget"
          },
          {
            "$ref": "#/components/schemas/CampaignWidget"
          },
          {
            "$ref": "#/components/schemas/ProductRecommendationsWidget"
          }
        ]
      },
      "ContactCountRequest": {
        "type": "object",
        "required": [
          "contactIdentifiers",
          "orgId"
        ],
        "properties": {
          "orgId": {
            "type": "string",
            "example": 728,
            "description": "ID of the organization"
          },
          "contactIdentifiers": {
            "type": "object",
            "description": "Identifiers to identify a contact",
            "additionalProperties": {
              "type": "string"
            }
          }
        }
      },
      "ContactExistsRequest": {
        "type": "object",
        "required": [
          "registration_identifiers",
          "org_id"
        ],
        "properties": {
          "org_id": {
            "type": "string",
            "example": 728,
            "description": "ID of the organization"
          },
          "registration_identifiers": {
            "type": "object",
            "description": "Identifier-value pairs per schema to identify a contact of a portal user during the resgistration",
            "additionalProperties": {
              "type": "object",
              "additionalProperties": {
                "type": "string"
              }
            },
            "example": {
              "contact": {
                "email": "john.doe@example.com"
              },
              "contract": {
                "contract_number": "123456"
              }
            }
          },
          "trigger_identifiers_check": {
            "type": "boolean",
            "default": true,
            "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"
          }
        }
      },
      "ContactIdentifyRequest": {
        "description": "ContactExistsRequest plus the surface the token is requested for.\n",
        "allOf": [
          {
            "$ref": "#/components/schemas/ContactExistsRequest"
          },
          {
            "type": "object",
            "required": [
              "surface_id"
            ],
            "properties": {
              "surface_id": {
                "type": "string",
                "description": "Id of the portal surface (see `surfaces` on the portal config) this token is\nfor. The surface must exist and use `authentication: registration_identifiers`;\notherwise the call returns 403. The issued token is bound to this surface and\nconfined to its data access settings.\n",
                "example": "website-journeys"
              }
            }
          }
        ]
      },
      "ContactIdentifyResponse": {
        "type": "object",
        "properties": {
          "contact_id": {
            "$ref": "#/components/schemas/EntityId",
            "description": "ID of the identified contact. Present only on a match."
          },
          "account_id": {
            "$ref": "#/components/schemas/EntityId",
            "description": "ID of the resolved account when the portal is configured for account-based\nregistration. Present only on a match.\n"
          },
          "token": {
            "type": "string",
            "description": "One-time bearer token scoped to the identified contact, to be sent as\n`Authorization: Bearer <token>` against the portal APIs. Present only on a match.\n",
            "example": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
          },
          "token_type": {
            "type": "string",
            "enum": [
              "contact_identification"
            ],
            "description": "Type of the issued token, matching its own `token_type` claim and the token type in access-token-api. Present only on a match."
          },
          "surface_id": {
            "type": "string",
            "description": "The surface the token is bound to; echoes the request. Present only on a match.",
            "example": "website-journeys"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the issued token stops being accepted. Present only on a match.",
            "example": "2026-08-11T10:35:00.000Z"
          },
          "allowed_operations": {
            "type": "array",
            "description": "The operationIds the issued token may call. Every other operation rejects the token\nwith 401, including read operations. Echoed so a caller does not have to infer the\nsurface from this specification, and so a change to the allowlist is visible at\nruntime. Present only on a match.\n",
            "items": {
              "type": "string"
            },
            "example": []
          },
          "reason": {
            "type": "string",
            "enum": [
              "TIMEOUT",
              "NOT_FOUND"
            ],
            "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"
          }
        }
      },
      "AccountExistsRequest": {
        "type": "object",
        "required": [
          "registration_identifiers",
          "org_id"
        ],
        "properties": {
          "org_id": {
            "type": "string",
            "example": 728,
            "description": "ID of the organization"
          },
          "registration_identifiers": {
            "type": "object",
            "description": "Identifier-value pairs per schema to identify an account during portal user registration",
            "additionalProperties": {
              "type": "object",
              "additionalProperties": {
                "type": "string"
              }
            },
            "example": {
              "account": {
                "customer_number": "ACC-123456"
              },
              "contract": {
                "contract_number": "123456"
              }
            }
          }
        }
      },
      "UserRequest": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "example": "testemail921@yopmail.com",
            "description": "User's email address"
          },
          "first_name": {
            "type": "string",
            "example": "John",
            "description": "First Name of the portal user"
          },
          "last_name": {
            "type": "string",
            "example": "Doe",
            "description": "Last Name of the portal user"
          },
          "contactId": {
            "$ref": "#/components/schemas/EntityId",
            "description": "ID of the contact"
          }
        }
      },
      "CreateUserRequest": {
        "required": [
          "email",
          "orgId",
          "password"
        ],
        "allOf": [
          {
            "$ref": "#/components/schemas/UserRequest"
          },
          {
            "properties": {
              "orgId": {
                "type": "string",
                "example": 728,
                "description": "ID of the organization"
              },
              "password": {
                "type": "string",
                "example": "124n$aAJs*d41h4",
                "description": "User's password"
              },
              "contactIdentifiers": {
                "type": "object",
                "description": "Deprecated. Use registration_identifiers instead.",
                "deprecated": true,
                "additionalProperties": {
                  "type": "string"
                }
              },
              "registration_identifiers": {
                "type": "object",
                "description": "Identifier-value pairs per schema to identify a contact of a portal user during the resgistration",
                "additionalProperties": {
                  "type": "object",
                  "additionalProperties": {
                    "type": "string"
                  }
                },
                "example": {
                  "contact": {
                    "email": "john.doe@example.com"
                  },
                  "contract": {
                    "contract_number": "123456"
                  }
                }
              },
              "contact_identifiers_for_account": {
                "type": "object",
                "description": "Account-mode only. Values for the contact identifiers configured\nunder `contact_identifiers_for_account` on the portal. Used to\nfind an existing related contact within the resolved account, or\nwritten onto the new contact if none matches.\n",
                "additionalProperties": {
                  "type": "string"
                },
                "example": {
                  "first_name": "John",
                  "last_name": "Doe"
                }
              },
              "additional_contact_attributes": {
                "type": "object",
                "description": "Values for the contact attributes configured under\n`additional_contact_attributes` on the portal. These are written\nonto the newly created contact but are not used to identify an\nexisting one.\n",
                "additionalProperties": {
                  "type": "string"
                },
                "example": {
                  "first_name": "John",
                  "last_name": "Doe"
                }
              },
              "account_id": {
                "type": "string",
                "description": "ID of the account"
              }
            }
          }
        ]
      },
      "OrganizationSettings": {
        "type": "object",
        "properties": {
          "automation_entity_mapping": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "Enable/Disable automation entity mapping"
              }
            }
          },
          "automation_preview": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "Enable/Disable automation preview"
              }
            }
          },
          "central_inbox_preview_setting": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "Enable/Disable central inbox preview setting"
              }
            }
          },
          "contracts_preview_setting": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "Enable/Disable contracts preview setting"
              }
            }
          },
          "disable_ivy": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "Enable/Disable Ivy"
              }
            }
          },
          "double_opt_in": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "Enable/Disable double opt-in"
              }
            }
          },
          "ecommerce_catalog_preview": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "Enable/Disable ecommerce catalog preview"
              }
            }
          },
          "ecommerce_opportunities_preview": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "Enable/Disable ecommerce opportunities preview"
              }
            }
          },
          "ecommerce_preview": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "Enable/Disable ecommerce preview"
              }
            }
          },
          "end_customer_portal": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "Enable/Disable end customer portal"
              }
            }
          },
          "installer_portal": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "Enable/Disable installer portal"
              }
            }
          },
          "entity_schema_builder": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "Enable/Disable entity schema builder"
              }
            }
          },
          "logic_editor_preview": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "Enable/Disable logic editor preview"
              }
            }
          },
          "new_navigation": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "Enable/Disable new navigation"
              }
            }
          },
          "partnering": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "Enable/Disable partnering"
              }
            }
          },
          "product-availability": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "Enable/Disable product availability"
              }
            }
          },
          "sso": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "Enable/Disable single sign-on (SSO)"
              }
            }
          },
          "submission_preview": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "Enable/Disable submission preview"
              }
            }
          },
          "user_roles_preview": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "Enable/Disable user roles preview"
              }
            }
          }
        }
      },
      "AuthConfig": {
        "type": "object",
        "properties": {
          "user_pool_id": {
            "type": "string",
            "example": "eu-central-1_CUEQRNbUb",
            "description": "AWS Cognito User Pool ID"
          },
          "user_pool_client_id": {
            "type": "string",
            "example": "6bsd0jkgoie74k2i8mrhc1vest",
            "description": "AWS Cognito User Pool Client ID"
          },
          "user_pool_identity_pool_id": {
            "type": "string",
            "example": "eu-central-1:a63af1f7-ab86-4ab5-a0eb-f461cb37c2b1",
            "description": "AWS Cognito User Pool Identity Pool ID"
          },
          "portal_id": {
            "type": "string",
            "example": "7h2hwdj7hhjsdcjkq03eidna3ep",
            "description": "Portal ID"
          }
        },
        "required": [
          "user_pool_id",
          "user_pool_client_id",
          "portal_id"
        ]
      },
      "Exists": {
        "type": "object",
        "required": [
          "exists"
        ],
        "properties": {
          "exists": {
            "type": "boolean",
            "description": "Indicate whether the item exists"
          },
          "active": {
            "type": "boolean",
            "description": "Indicate whether the item is active"
          }
        },
        "example": {
          "exists": true,
          "active": false
        }
      },
      "EntitySlug": {
        "description": "URL-friendly identifier for the entity schema",
        "type": "string",
        "example": "contact"
      },
      "EntitySlugConfig": {
        "description": "Per-slug search configuration with scoped targets and templates",
        "type": "object",
        "required": [
          "slug"
        ],
        "properties": {
          "slug": {
            "$ref": "#/components/schemas/EntitySlug"
          },
          "targets": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          },
          "templates": {
            "type": "object",
            "deprecated": true,
            "description": "DEPRECATED — client-supplied Handlebars templates. Use `templates_ref` instead so templates are derived server-side from admin-authored portal configuration. Ignored when `templates_ref` is present; rejected once the org has the `portals-reject-client-templates` flag enabled.",
            "additionalProperties": {
              "type": "string"
            }
          },
          "templates_ref": {
            "type": "object",
            "description": "Reference to admin-authored portal configuration (a page block or a global search configuration item) from which the API derives Handlebars templates server-side. This replaces client-supplied template strings so portal users can never submit arbitrary templates for resolution. When both a reference and raw `templates` are provided, the reference wins and the raw templates are ignored.",
            "properties": {
              "page_id": {
                "type": "string",
                "description": "ID of the portal page to derive templates from. When given without `block_id`, templates are derived from every block of the page and returned resolved as a nested map keyed by block id (the entity detail page contract)."
              },
              "block_id": {
                "type": "string",
                "description": "ID of a block within the page. Templates are derived from the block's content according to its block type (e.g. meter_selector, meter_reading, entity_list)."
              },
              "config_id": {
                "type": "string",
                "description": "For blocks carrying a per-schema configuration array (entity_list), the id of the configuration item to derive templates from. Requires `page_id` and `block_id`."
              },
              "global_search_config_id": {
                "type": "string",
                "description": "ID of the portal's `global_search` configuration item to derive search result templates and the group title template from. Mutually exclusive with `page_id`."
              }
            }
          }
        }
      },
      "TemplatesRef": {
        "type": "object",
        "description": "Reference to admin-authored portal configuration (a page block or a global search configuration item) from which the API derives Handlebars templates server-side. This replaces client-supplied template strings so portal users can never submit arbitrary templates for resolution. When both a reference and raw `templates` are provided, the reference wins and the raw templates are ignored.",
        "properties": {
          "page_id": {
            "type": "string",
            "description": "ID of the portal page to derive templates from. When given without `block_id`, templates are derived from every block of the page and returned resolved as a nested map keyed by block id (the entity detail page contract)."
          },
          "block_id": {
            "type": "string",
            "description": "ID of a block within the page. Templates are derived from the block's content according to its block type (e.g. meter_selector, meter_reading, entity_list)."
          },
          "config_id": {
            "type": "string",
            "description": "For blocks carrying a per-schema configuration array (entity_list), the id of the configuration item to derive templates from. Requires `page_id` and `block_id`."
          },
          "global_search_config_id": {
            "type": "string",
            "description": "ID of the portal's `global_search` configuration item to derive search result templates and the group title template from. Mutually exclusive with `page_id`."
          }
        }
      },
      "EntityId": {
        "type": "string",
        "format": "uuid",
        "description": "Entity ID",
        "example": "5da0a718-c822-403d-9f5d-20d4584e0528"
      },
      "BaseEntity": {
        "type": "object",
        "properties": {
          "_id": {
            "$ref": "#/components/schemas/EntityId",
            "description": "Entity ID",
            "example": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
          },
          "_title": {
            "type": "string",
            "description": "Title of the entity",
            "example": "Example Entity"
          },
          "_org": {
            "type": "string",
            "description": "Organization ID the entity belongs to",
            "example": "123"
          },
          "_tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Array of entity tags",
            "example": [
              "example",
              "mock"
            ]
          },
          "_created_at": {
            "type": "string",
            "format": "date-time",
            "description": "Creation timestamp of the entity",
            "example": "2021-02-09T12:41:43.662Z"
          },
          "_updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "Last update timestamp of the entity",
            "example": "2021-02-09T12:41:43.662Z"
          }
        },
        "required": [
          "_id",
          "_title",
          "_org",
          "_created_at",
          "_updated_at"
        ]
      },
      "Schema": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "description": "Slug of the schema",
            "example": "contact"
          }
        },
        "additionalProperties": true
      },
      "Entity": {
        "type": "object",
        "additionalProperties": true,
        "properties": {
          "templates_output_highlighted": {
            "type": "object",
            "readOnly": true,
            "description": "Template outputs with search highlight <em> tags merged in. Only present when highlight is requested and matches exist.",
            "additionalProperties": {
              "oneOf": [
                {
                  "type": "string"
                },
                {
                  "type": "object",
                  "additionalProperties": {
                    "type": "string"
                  }
                }
              ]
            }
          },
          "search_snippets": {
            "type": "array",
            "readOnly": true,
            "description": "Highlighted fragments for search matches on fields not shown in template content. Max 1 entry.",
            "items": {
              "type": "object",
              "properties": {
                "field": {
                  "type": "string",
                  "description": "Human-readable field label"
                },
                "fragment": {
                  "type": "string",
                  "description": "Highlighted text fragment with <em> tags"
                }
              }
            }
          }
        }
      },
      "EntityTemplates": {
        "type": "object",
        "properties": {
          "templates_output": {
            "type": "object",
            "description": "Resolved template strings corresponding to the templates parameter. Supports both string values and nested objects of strings.",
            "additionalProperties": {
              "oneOf": [
                {
                  "type": "string"
                },
                {
                  "type": "object",
                  "additionalProperties": {
                    "type": "string"
                  }
                }
              ]
            },
            "example": {
              "content_top_name": "Customer #123456",
              "main_content_name": "Orange Flexible A2 (654321)",
              "content_bottom_name": "Porscheplatz 1, 70435 Stuttgart, Germany",
              "nested_content": {
                "title": "Orange Flexible A2",
                "subtitle": "654321"
              }
            }
          }
        }
      },
      "EntityItem": {
        "allOf": [
          {
            "$ref": "#/components/schemas/BaseEntity"
          },
          {
            "$ref": "#/components/schemas/Entity"
          },
          {
            "$ref": "#/components/schemas/EntityTemplates"
          },
          {
            "type": "object",
            "required": [
              "_schema"
            ],
            "properties": {
              "_schema": {
                "$ref": "#/components/schemas/EntitySlug",
                "description": "Entity schema identifier"
              }
            }
          }
        ]
      },
      "EntityResponse": {
        "type": "object",
        "description": "Response for entity get request",
        "properties": {
          "result": {
            "$ref": "#/components/schemas/EntityItem"
          }
        }
      },
      "EntityResponseWithHits": {
        "type": "object",
        "description": "Response for entity search requests",
        "properties": {
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EntityItem"
            }
          },
          "pagination": {
            "type": "object",
            "description": "Pagination metadata",
            "properties": {
              "from": {
                "type": "number",
                "description": "Starting offset for the current page",
                "example": 0
              },
              "size": {
                "type": "number",
                "description": "Number of entities per page",
                "example": 10
              },
              "total": {
                "type": "number",
                "description": "Total number of entities available",
                "example": 50
              },
              "has_more": {
                "type": "boolean",
                "description": "Whether there are more entities available beyond the current page"
              }
            }
          },
          "hits": {
            "type": "number",
            "description": "Number of entities returned in this response",
            "example": 10
          },
          "includes": {
            "$ref": "#/components/schemas/SearchIncludes"
          }
        }
      },
      "EntityResponseGroupedWithHits": {
        "type": "object",
        "description": "Response for entity search requests, but with groupings",
        "properties": {
          "groups": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "group": {
                  "type": "string",
                  "description": "Group title",
                  "example": "active"
                },
                "group_title": {
                  "type": "string",
                  "description": "Resolved group title from variable",
                  "example": "Account #987654321"
                },
                "count": {
                  "type": "number",
                  "description": "Total number of entities in this group",
                  "example": 10
                },
                "results": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/EntityItem"
                  }
                },
                "pagination": {
                  "type": "object",
                  "description": "Pagination metadata for entities within this group",
                  "properties": {
                    "from": {
                      "type": "number",
                      "description": "Starting offset for entities in this group",
                      "example": 0
                    },
                    "size": {
                      "type": "number",
                      "description": "Number of entities returned for this group",
                      "example": 5
                    },
                    "total": {
                      "type": "number",
                      "description": "Total number of entities available in this group",
                      "example": 10
                    },
                    "has_more": {
                      "type": "boolean",
                      "description": "Whether there are more entities available in this group beyond the current page"
                    }
                  }
                }
              }
            }
          },
          "groups_pagination": {
            "type": "object",
            "description": "Group pagination metadata",
            "properties": {
              "has_more": {
                "type": "boolean",
                "description": "Whether there are more groups available"
              },
              "after_key": {
                "type": "object",
                "description": "Composite aggregation key for the next page",
                "additionalProperties": {
                  "type": "string"
                }
              }
            }
          },
          "hits": {
            "type": "number",
            "description": "Number of groups returned in this response",
            "example": 5
          },
          "includes": {
            "$ref": "#/components/schemas/SearchIncludes"
          }
        }
      },
      "SearchIncludes": {
        "type": "object",
        "description": "Side-loaded enrichment data requested via the `include` parameter.",
        "properties": {
          "active_workflow": {
            "type": "object",
            "description": "The active workflow execution for each entity, keyed by entity ID. Only entities with an active workflow are present.",
            "additionalProperties": {
              "type": "object"
            }
          }
        }
      },
      "PortalUser": {
        "description": "The portal user entity",
        "allOf": [
          {
            "$ref": "#/components/schemas/BaseEntity"
          },
          {
            "$ref": "#/components/schemas/Entity"
          },
          {
            "type": "object",
            "required": [
              "_schema"
            ],
            "properties": {
              "_schema": {
                "type": "string",
                "enum": [
                  "portal_user"
                ]
              }
            }
          }
        ]
      },
      "Contact": {
        "description": "The mapped contact of the portal user",
        "allOf": [
          {
            "$ref": "#/components/schemas/BaseEntity"
          },
          {
            "$ref": "#/components/schemas/Entity"
          },
          {
            "type": "object",
            "required": [
              "_schema"
            ],
            "properties": {
              "_schema": {
                "type": "string",
                "enum": [
                  "contact"
                ]
              }
            }
          }
        ]
      },
      "WorfklowIdentifier": {
        "description": "Workflow identifier object",
        "allOf": [
          {
            "type": "object",
            "properties": {
              "defition_id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            }
          }
        ]
      },
      "Meter": {
        "description": "The meter entity",
        "allOf": [
          {
            "$ref": "#/components/schemas/BaseEntity"
          },
          {
            "$ref": "#/components/schemas/Entity"
          },
          {
            "type": "object",
            "required": [
              "_schema"
            ],
            "properties": {
              "_schema": {
                "type": "string",
                "enum": [
                  "meter"
                ]
              }
            }
          }
        ]
      },
      "Order": {
        "description": "The order entity",
        "allOf": [
          {
            "$ref": "#/components/schemas/BaseEntity"
          },
          {
            "$ref": "#/components/schemas/Entity"
          },
          {
            "type": "object",
            "required": [
              "_schema"
            ],
            "properties": {
              "_schema": {
                "type": "string",
                "enum": [
                  "order"
                ]
              }
            }
          }
        ]
      },
      "Opportunity": {
        "description": "The opportunity entity",
        "allOf": [
          {
            "$ref": "#/components/schemas/BaseEntity"
          },
          {
            "$ref": "#/components/schemas/Entity"
          },
          {
            "type": "object",
            "required": [
              "_schema"
            ],
            "properties": {
              "_schema": {
                "type": "string",
                "enum": [
                  "opportunity"
                ]
              }
            }
          }
        ]
      },
      "Contract": {
        "description": "The contract entity",
        "allOf": [
          {
            "$ref": "#/components/schemas/BaseEntity"
          },
          {
            "$ref": "#/components/schemas/Entity"
          },
          {
            "type": "object",
            "properties": {
              "contract_name": {
                "type": "string",
                "description": "The name of the contract.",
                "example": "Grid Contract"
              },
              "contract_number": {
                "type": "string",
                "description": "The unique identifier of the contract.",
                "example": "12345"
              },
              "status": {
                "type": "string",
                "enum": [
                  "draft",
                  "in_approval_process",
                  "approved",
                  "active",
                  "deactivated",
                  "revoked",
                  "terminated",
                  "expired"
                ],
                "default": "draft",
                "description": "The status of the contract.",
                "example": "approved"
              },
              "description": {
                "type": "string",
                "description": "A brief description of the contract.",
                "example": "This contract is for the supply of widgets."
              },
              "account_number": {
                "type": "string",
                "description": "The account number associated with the contract.",
                "example": "67890"
              },
              "branch": {
                "type": "string",
                "enum": [
                  "power",
                  "gas",
                  "water",
                  "waste_water",
                  "district_heating"
                ],
                "description": "The branch associated with the contract.",
                "example": "power"
              },
              "billing_address": {
                "type": "string",
                "description": "The billing address associated with the contract.",
                "example": "123 Main St, Anytown"
              },
              "delivery_address": {
                "type": "string",
                "description": "The delivery address associated with the contract.",
                "example": "456 Elm St, Anytown"
              },
              "additional_addresses": {
                "type": "string",
                "description": "Any additional addresses associated with the contract.",
                "example": "789 Oak St, Anytown"
              },
              "termination_date": {
                "type": "string",
                "description": "The date on which the contract was terminated.",
                "example": "2022-01-01"
              },
              "termination_reason": {
                "type": "string",
                "description": "The reason for the termination of the contract.",
                "example": "Non-payment"
              },
              "billing_period": {
                "type": "string",
                "enum": [
                  "weekly",
                  "monthly",
                  "every_quarter",
                  "every_6_months",
                  "yearly"
                ],
                "default": "weekly",
                "description": "The billing period associated with the contract.",
                "example": "monthly"
              },
              "billing_duration_amount": {
                "type": "number",
                "minimum": 0,
                "description": "The duration of the billing period.",
                "example": 30
              },
              "renewal_duration_amount": {
                "type": "number",
                "minimum": 0,
                "description": "The duration of the renewal period.",
                "example": 365
              },
              "renewal_duration_unit": {
                "type": "string",
                "enum": [
                  "weeks",
                  "months",
                  "years"
                ],
                "default": "months",
                "description": "The unit of time for the renewal period.",
                "example": "years"
              },
              "notice_time_amount": {
                "type": "number",
                "minimum": 0,
                "description": "The amount of notice required for termination of the contract.",
                "example": 30
              },
              "notice_time_unit": {
                "type": "string",
                "enum": [
                  "weeks",
                  "months",
                  "years"
                ],
                "default": "months",
                "description": "The unit of time for the notice period.",
                "example": "months"
              },
              "start_date": {
                "type": "string",
                "description": "The start date of the contract.",
                "example": "2021-01-01"
              },
              "billing_due_day": {
                "type": "integer",
                "description": "Defines the day of the month in which the installments are due.",
                "minimum": 1,
                "maximum": 31,
                "example": 2
              },
              "installment_amount": {
                "type": "integer",
                "description": "Set amount for installments in cents. (precision 2)",
                "example": 10050
              },
              "balance": {
                "type": "integer",
                "description": "Current balance of the contract in cents. (precision 2)",
                "example": 8990
              },
              "balance_currency": {
                "$ref": "#/components/schemas/Currency"
              }
            }
          }
        ]
      },
      "File": {
        "description": "The file entity",
        "allOf": [
          {
            "$ref": "#/components/schemas/BaseEntity"
          },
          {
            "$ref": "#/components/schemas/Entity"
          },
          {
            "type": "object",
            "required": [
              "_schema"
            ],
            "properties": {
              "_schema": {
                "type": "string",
                "enum": [
                  "file"
                ]
              }
            }
          }
        ]
      },
      "Product": {
        "description": "The product entity",
        "allOf": [
          {
            "$ref": "#/components/schemas/BaseEntity"
          },
          {
            "$ref": "#/components/schemas/Entity"
          },
          {
            "type": "object",
            "required": [
              "_schema"
            ],
            "properties": {
              "_schema": {
                "type": "string",
                "enum": [
                  "product"
                ]
              }
            }
          }
        ]
      },
      "ActivityId": {
        "type": "string",
        "format": "ulid",
        "description": "See https://github.com/ulid/spec",
        "example": "01F130Q52Q6MWSNS8N2AVXV4JN"
      },
      "ActivityCallerContext": {
        "type": "object",
        "additionalProperties": true,
        "properties": {
          "PortalAuth": {
            "type": "object",
            "properties": {
              "token": {
                "type": "object",
                "properties": {
                  "sub": {
                    "type": "string",
                    "example": "8cc73157-3dc4-47f3-b163-d3a5039bba72"
                  },
                  "email": {
                    "type": "string",
                    "example": "john@doe.com"
                  },
                  "cognito:username": {
                    "type": "string",
                    "example": "john@doe.com"
                  },
                  "custom:portal_user_id": {
                    "$ref": "#/components/schemas/EntityId"
                  },
                  "custom:contact_entity_id": {
                    "$ref": "#/components/schemas/EntityId"
                  }
                },
                "example": {
                  "cognito:username": "john@doe.com",
                  "custom:contact_entity_id": "7579d22f-9400-41d1-b460-04730239ee91",
                  "custom:org_id": "123456",
                  "custom:origin": "END_CUSTOMER_PORTAL",
                  "custom:portal_user_id": "06c78f9d-af75-4483-893d-a3fad524400f",
                  "email": "john@doe.com",
                  "email_verified": true,
                  "exp": 1694693219,
                  "iat": 1694689619,
                  "sub": "8cc73157-3dc4-47f3-b163-d3a5039bba72"
                }
              }
            }
          }
        }
      },
      "Activity": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "example": "MyCustomActivity"
          },
          "title": {
            "type": "string",
            "description": "Title for activity. Supports handlebars syntax.",
            "example": "My custom activity"
          },
          "message": {
            "type": "string",
            "description": "Message for activity. Supports handlebars syntax.",
            "example": "{{caller}} did something with {{entity payload.entity.id}}."
          },
          "payload": {
            "type": "object",
            "properties": {},
            "additionalProperties": true,
            "example": {
              "entity": {
                "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                "schema": "contact"
              }
            }
          }
        },
        "required": [
          "type",
          "title",
          "message"
        ]
      },
      "EntityEditRule": {
        "type": "object",
        "properties": {
          "slug": {
            "$ref": "#/components/schemas/EntitySlug"
          },
          "attribute": {
            "type": "string"
          },
          "rule_type": {
            "type": "string"
          },
          "cadence_period_type": {
            "type": "string"
          },
          "changes_allowed": {
            "type": "number"
          },
          "cadence_period": {
            "type": "number"
          },
          "allowed_decrement": {
            "type": "string"
          },
          "allowed_increment": {
            "type": "string"
          },
          "number_of_days_before_restriction": {
            "type": "number"
          },
          "grace_period": {
            "type": "number"
          }
        }
      },
      "ActivityItem": {
        "allOf": [
          {
            "type": "object",
            "properties": {
              "_id": {
                "$ref": "#/components/schemas/ActivityId"
              },
              "timestamp": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          {
            "$ref": "#/components/schemas/Activity"
          },
          {
            "type": "object",
            "properties": {
              "payload": {
                "type": "object",
                "properties": {
                  "entity": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "$ref": "#/components/schemas/EntityId"
                      },
                      "schema": {
                        "type": "string"
                      }
                    }
                  },
                  "caller": {
                    "$ref": "#/components/schemas/ActivityCallerContext"
                  }
                }
              }
            }
          }
        ]
      },
      "FileItem": {
        "allOf": [
          {
            "$ref": "#/components/schemas/File"
          },
          {
            "type": "object",
            "properties": {
              "filename": {
                "type": "string",
                "example": "document.pdf"
              },
              "access_control": {
                "type": "string",
                "default": "private",
                "enum": [
                  "private",
                  "public-read"
                ]
              },
              "file_date": {
                "type": "string",
                "format": "date-time",
                "description": "The date used for sorting the file",
                "example": "2021-02-09T12:41:43.662Z"
              },
              "public_url": {
                "description": "Direct URL for file (public only if file access control is public-read)",
                "type": "string",
                "format": "url",
                "example": "https://epilot-files-prod.s3.eu-central-1.amazonaws.com/123/4d689aeb-1497-4410-a9fe-b36ca9ac4389/document.pdf"
              },
              "type": {
                "description": "Human readable type for file",
                "type": "string",
                "enum": [
                  "document",
                  "document_template",
                  "text",
                  "image",
                  "video",
                  "audio",
                  "spreadsheet",
                  "presentation",
                  "font",
                  "archive",
                  "application",
                  "unknown"
                ]
              },
              "mime_type": {
                "type": "string",
                "description": "MIME type of the file",
                "example": "application/pdf"
              },
              "_relations": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "entity_id": {
                      "$ref": "#/components/schemas/EntityId",
                      "description": "The ID of the parent entity"
                    },
                    "_schema": {
                      "$ref": "#/components/schemas/EntitySlug"
                    },
                    "_title": {
                      "type": "string",
                      "description": "The title of the parent entity",
                      "example": "Opportunity ABC"
                    }
                  }
                }
              },
              "is_new": {
                "type": "boolean",
                "description": "Indicate whether the user has not seen/downloaded the file before"
              },
              "custom_download_url_auth": {
                "type": "string",
                "enum": [
                  "token",
                  "presigned"
                ],
                "description": "Authorization mode of an externally hosted file's download url. `presigned` (the default) means `public_url` is HMAC-signed and needs no auth header; `token` means it authorizes the caller's bearer token instead and must be fetched with an `Authorization` header. Because `presigned` is the default, the absence of this property does NOT prove the file is stored in epilot — an external file may omit it."
              }
            }
          }
        ]
      },
      "FilePreviewResult": {
        "type": "object",
        "required": [
          "kind"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "description": "What kind of preview (if any) is available. \"pdf\" covers both native PDFs and Office documents that were converted to PDF server-side. \"unsupported\" means no preview is available and the caller should fall back to download.",
            "enum": [
              "pdf",
              "image",
              "unsupported"
            ]
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "A Content-Disposition: inline URL to render in a preview viewer. Present only when kind is \"pdf\" or \"image\". Short-lived — do not cache across dialog opens."
          },
          "requires_auth": {
            "type": "boolean",
            "description": "When true, `url` only answers to the portal user's bearer token: fetch the bytes with an `Authorization: Bearer <token>` header and render them from an object URL. Only ever set for URLs on an allow-listed ERP file-proxy host."
          },
          "download_url": {
            "type": "string",
            "format": "uri",
            "description": "Optional download URL for the file, returned for any kind — including \"unsupported\" — whenever the file entity exposes one. It is only a Content-Disposition: attachment URL for files with access_control \"private\", where it is a short-lived signed URL; for any other access level it is the file entity's plain public/CDN object URL, which carries no attachment disposition and may render inline in the browser. Absent when the file entity exposes no usable URL, so clients must handle it being missing."
          }
        }
      },
      "EntityFileCount": {
        "type": "object",
        "required": [
          "entity_id",
          "file_count",
          "_schema"
        ],
        "properties": {
          "entity_id": {
            "$ref": "#/components/schemas/EntityId",
            "description": "The ID of the parent entity"
          },
          "_schema": {
            "$ref": "#/components/schemas/EntitySlug"
          },
          "_title": {
            "type": "string",
            "description": "The title of the parent entity",
            "example": "Opportunity ABC"
          },
          "file_count": {
            "type": "integer",
            "description": "Number of files associated with the entity and shared with portal user",
            "example": 2
          }
        }
      },
      "AdminUser": {
        "type": "object",
        "additionalProperties": true,
        "properties": {
          "type": {
            "type": "string",
            "example": "user",
            "nullable": true
          },
          "user_id": {
            "type": "string",
            "example": "123456"
          },
          "display_name": {
            "type": "string",
            "example": "John",
            "nullable": true
          },
          "image_uri": {
            "type": "object",
            "nullable": true,
            "properties": {
              "original": {
                "type": "string",
                "example": "https://fuafjvoHKsu.cloudimg.io/v7/e-mage-sam-bucket-dev.s3.eu-central-1.amazonaws.com/files/fuafjvoHKsudhfagweucjasdvga/original",
                "nullable": true
              },
              "thumbnail_32": {
                "type": "string",
                "example": "https://fuafjvoHKsu.cloudimg.io/v7/e-mage-sam-bucket-dev.s3.eu-central-1.amazonaws.com/files/fuafjvoHKsudhfagweucjasdvga/original?w=32&h=32",
                "nullable": true
              },
              "thumbnail_64": {
                "type": "string",
                "example": "https://fuafjvoHKsu.cloudimg.io/v7/e-mage-sam-bucket-dev.s3.eu-central-1.amazonaws.com/files/fuafjvoHKsudhfagweucjasdvga/original?w=64&h=64",
                "nullable": true
              },
              "key": {
                "type": "string",
                "example": "fuafjvoHKsudhfagweucjasdvga",
                "nullable": true
              }
            }
          },
          "org_id": {
            "type": "string",
            "example": "123"
          },
          "email": {
            "type": "string",
            "example": "j.doe@epilot.cloud",
            "nullable": true
          },
          "phone": {
            "type": "string",
            "example": "12345 67890",
            "nullable": true
          }
        }
      },
      "PortalSurface": {
        "type": "object",
        "description": "One surface a portal's data is reached from (see `surfaces` on the portal config).\n\nA surface names how callers authenticate on it and what they may reach once they\nhave. Data access is always a subset of the portal's own: a schema not in\n`allowed_portal_entities` cannot be opened up by a surface, and the portal's\ncontact-relation rules still apply underneath the surface's own.\n\nTokens minted for a surface (currently: contact identification tokens for\n`registration_identifiers` surfaces) carry the surface id. The surface's data\naccess is resolved from the portal config on every request, not baked into the\ntoken, so tightening a surface applies to tokens already in circulation.\n",
        "required": [
          "id",
          "name",
          "authentication"
        ],
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64,
            "pattern": "^[a-z0-9][a-z0-9_-]*$",
            "description": "Stable identifier, unique within the portal. Referenced by `identifyContact` and carried in issued tokens.",
            "example": "website-journeys"
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120,
            "example": "Website journeys"
          },
          "description": {
            "type": "string",
            "maxLength": 500,
            "description": "Free text for the configuring user, e.g. where this surface is embedded."
          },
          "authentication": {
            "type": "string",
            "enum": [
              "login",
              "registration_identifiers"
            ],
            "description": "How a caller on this surface proves who they are.\n\n- `login`: a logged-in portal user token (the portal's own authentication).\n- `registration_identifiers`: the caller supplies the portal's\n  `registration_identifiers` to `identifyContact` and receives a short-lived\n  contact identification token. Anyone who knows or guesses those values can\n  use this surface, so its data access should be as narrow as the use case allows.\n\n`anonymous` (no proof of identity at all) is planned and not accepted yet.\n"
          },
          "token_ttl_seconds": {
            "type": "integer",
            "format": "int32",
            "default": 300,
            "minimum": 60,
            "maximum": 900,
            "description": "Lifetime of tokens minted for this surface. Only used with `registration_identifiers`."
          },
          "data_access": {
            "$ref": "#/components/schemas/PortalSurfaceDataAccess"
          }
        }
      },
      "PortalSurfaceDataAccess": {
        "type": "object",
        "description": "What a caller on a surface may reach. Every property is optional; omitting all of them is the portal's default scope.",
        "properties": {
          "entities": {
            "type": "array",
            "description": "Schemas reachable on this surface, each with the targets that always apply to\nit. Must be a subset of the portal's `allowed_portal_entities`; anything else\nis ignored. Empty or omitted means the surface reaches nothing - data access is\nopted into per schema.\n",
            "items": {
              "$ref": "#/components/schemas/PortalSurfaceEntityAccess"
            }
          },
          "role_id": {
            "type": "string",
            "description": "360 role whose grants apply on this surface (`<org_id>:<slug>`), giving\nvertical permissions - which attributes and actions are permitted. Omitted\nmeans the portal's default role.\n",
            "example": "728:public_journeys_readonly"
          }
        }
      },
      "PortalSurfaceEntityAccess": {
        "type": "object",
        "description": "One schema this surface reaches, with the targets that always apply to it.",
        "properties": {
          "schema": {
            "type": "string",
            "description": "Schema slug, from the portal's `allowed_portal_entities`.",
            "example": "contract"
          },
          "target_ids": {
            "type": "array",
            "description": "Targets (see the Targeting API) whose filters always apply to reads of this\nschema on this surface, giving horizontal permissions - which rows are\nreachable. They are applied in addition to any targets the caller passes. A\ntarget matches one schema, which is why they are configured per schema here.\n",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "schema"
        ]
      },
      "Grant": {
        "type": "object",
        "properties": {
          "action": {
            "type": "string",
            "example": "entity-read",
            "description": "Action for granting permission"
          },
          "resource": {
            "type": "string",
            "example": "entity:123:contact:f7c22299-ca72-4bca-8538-0a88eeefc947",
            "description": "Resource for granting permission"
          },
          "effect": {
            "type": "string",
            "default": "allow",
            "enum": [
              "allow",
              "deny"
            ],
            "description": "Effect of the permission"
          }
        },
        "required": [
          "action"
        ]
      },
      "ActionLabel": {
        "type": "object",
        "properties": {
          "en": {
            "type": "string",
            "nullable": true
          },
          "de": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "Rule": {
        "type": "object",
        "properties": {
          "entity": {
            "type": "string",
            "nullable": true
          },
          "attribute": {
            "type": "string",
            "nullable": true
          },
          "attribute_value": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "JourneyActions": {
        "type": "object",
        "properties": {
          "journey_id": {
            "type": "string",
            "nullable": true
          },
          "action_label": {
            "type": "object",
            "$ref": "#/components/schemas/ActionLabel",
            "nullable": true
          },
          "slug": {
            "type": "string",
            "nullable": true
          },
          "rules": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Rule"
            },
            "nullable": true
          }
        }
      },
      "ExternalLink": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique identifier for the external link"
          },
          "label": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "type": {
            "type": "string",
            "enum": [
              "link",
              "journey",
              "seamless"
            ]
          },
          "link": {
            "type": "string",
            "description": "The URL of the external link"
          },
          "rules": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "attribute": {
            "type": "string",
            "description": "Attribute associated with the link"
          },
          "entity": {
            "type": "string",
            "description": "Entity associated with the link"
          },
          "attribute_value": {
            "type": "string",
            "description": "Attribute value for the link"
          },
          "icon": {
            "type": "object",
            "description": "Configuration of the icon for the external link",
            "properties": {
              "name": {
                "type": "string",
                "description": "The name of the icon"
              },
              "color": {
                "type": "string",
                "description": "The color of the icon"
              },
              "size": {
                "type": "integer",
                "description": "Size of the icon in pixels"
              }
            }
          },
          "extension_link_id": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Seamless link identifier in a form of [extensionId, linkId]"
          }
        },
        "required": [
          "id",
          "label",
          "type",
          "link"
        ]
      },
      "OutstandingTask": {
        "type": "object",
        "required": [
          "entity_id",
          "entity_schema",
          "entity_title",
          "workflow_id",
          "workflow_name",
          "step_id",
          "step_name",
          "journey_id"
        ],
        "properties": {
          "entity_id": {
            "type": "string",
            "description": "ID of the entity the task belongs to"
          },
          "entity_schema": {
            "type": "string",
            "description": "Schema slug of the entity (opportunity, order or contract)"
          },
          "entity_title": {
            "type": "string",
            "description": "Title (_title) of the entity"
          },
          "workflow_id": {
            "type": "string",
            "description": "Workflow execution id"
          },
          "workflow_name": {
            "type": "string",
            "description": "Workflow execution / template name"
          },
          "step_id": {
            "type": "string",
            "description": "Task (step) id within the workflow execution"
          },
          "step_name": {
            "type": "string",
            "description": "Task (step) name"
          },
          "journey_id": {
            "type": "string",
            "description": "Journey id the customer needs to fill out"
          },
          "complete_task_automatically": {
            "type": "boolean",
            "description": "Whether submitting the journey from the portal should auto-complete the task. When false, an internal user completes it."
          }
        }
      },
      "WorkflowExecution": {
        "type": "object",
        "properties": {},
        "additionalProperties": true,
        "example": {
          "id": "8gja72h6kas6h",
          "name": "Lead Qualification",
          "trigger": "MANUAL",
          "status": "STARTED",
          "creationTime": "2021-04-27T12:01:13.000Z",
          "lastUpdateTime": "2021-04-27T12:01:13.000Z",
          "dueDate": "2021-04-27T12:01:13.000Z",
          "assignedTo": [
            "252",
            "29052"
          ],
          "flow": [
            {
              "id": "sectionId1",
              "name": "Initial Information Gathering",
              "steps": [
                {
                  "id": "sada5641f3a21",
                  "name": "Call client and confirm address and product",
                  "status": "ASSIGNED",
                  "assignedTo": [
                    "11"
                  ]
                },
                {
                  "id": "sada5641f3a22",
                  "name": "Check product availability",
                  "status": "UNASSIGNED"
                },
                {
                  "id": "sada5641f3a23",
                  "name": "Send email confirming contact with the client",
                  "status": "SKIPPED"
                }
              ]
            },
            {
              "id": "firstLevelStepId1",
              "name": "Print and send catalog",
              "status": "SKIPPED",
              "dueDate": "2023-01-15T20:00:00"
            }
          ]
        }
      },
      "WorkflowStep": {
        "type": "object",
        "properties": {},
        "additionalProperties": true,
        "example": {
          "startedTime": "2024-01-12T13:29:55.942Z",
          "requirements": [],
          "created": "2023-10-20T17:41:10.256Z",
          "executionType": "MANUAL",
          "assignedToInProgress": "-",
          "sectionId": "lzxsw2sblj7",
          "type": "STEP",
          "entityRefId": "q1d6vcbsqvn",
          "assignedTo": [
            "10014532"
          ],
          "lastUpdated": "2024-01-13T05:18:43.838Z",
          "ecp": {},
          "userIds": [],
          "name": "Hinterlege den vereinbarten LIC Termin",
          "id": "q1d6vcbsqvn",
          "definitionId": "9UjHKq",
          "status": "COMPLETED",
          "manuallyCreated": false,
          "enabled": true,
          "completedTime": "2024-01-13T05:18:43.827Z"
        }
      },
      "PortalWorkflow": {
        "type": "object",
        "description": "A portal-facing projection of a workflow execution (V1 or V2), with the execution\ngraph already linearized by the Workflows API into a flat, ordered list of\nportal-visible tasks.\n",
        "properties": {
          "id": {
            "type": "string",
            "description": "Id of the workflow / flow execution"
          },
          "definition_id": {
            "type": "string",
            "description": "Id of the workflow definition / flow template this execution was created from"
          },
          "name": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "STARTED",
              "DONE",
              "CLOSED"
            ]
          },
          "version": {
            "type": "integer",
            "description": "2 = legacy V1 workflow execution (linear model), 3 = V2 flow execution (graph model)",
            "enum": [
              2,
              3
            ]
          },
          "created_at": {
            "type": "string",
            "description": "Creation timestamp, doubles as started time"
          },
          "updated_at": {
            "type": "string",
            "description": "Last update timestamp"
          },
          "completed_at": {
            "type": "string",
            "description": "Timestamp when the execution was completed, if it is"
          },
          "due_date": {
            "type": "string"
          },
          "assigned_to": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "contexts": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "entity_id": {
                  "type": "string"
                },
                "entity_schema": {
                  "type": "string"
                },
                "is_primary": {
                  "type": "boolean"
                }
              }
            }
          },
          "is_path_complete": {
            "type": "boolean",
            "description": "True when the linearized path reached the end of the execution graph. False when an\nunevaluated decision task was encountered, meaning additional tasks may appear once\nthe decision is resolved (the returned tasks are still the guaranteed active path).\nAlways true for V1 executions.\n"
          },
          "tasks": {
            "type": "array",
            "description": "Portal-visible tasks in linear (timeline) order",
            "items": {
              "$ref": "#/components/schemas/PortalWorkflowTask"
            }
          },
          "stages": {
            "type": "array",
            "description": "Customer-facing stages of the execution in strict order, each with a\nprogress status derived by the Workflows API. Present only for V2 flow\nexecutions whose template defines stages and whose boundaries resolve\ncleanly; omitted otherwise, so consumers must fall back to the flat\ntask timeline.\n",
            "items": {
              "$ref": "#/components/schemas/PortalWorkflowStage"
            }
          }
        },
        "required": [
          "id",
          "name",
          "status",
          "version",
          "is_path_complete",
          "tasks"
        ]
      },
      "PortalWorkflowStage": {
        "type": "object",
        "description": "A customer-facing stage of a flow execution, with a progress status derived\nfrom the execution's tasks by the Workflows API. Stages form a strict total\norder; the array order is the stage order.\n",
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable unique identifier for the stage"
          },
          "name": {
            "type": "string",
            "description": "User-facing stage title"
          },
          "description": {
            "type": "string",
            "description": "Customer-facing description of what happens in this stage"
          },
          "status": {
            "type": "string",
            "enum": [
              "COMPLETED",
              "IN_PROGRESS",
              "UPCOMING"
            ],
            "description": "Derived progress status:\n- COMPLETED: every task of the stage is done, and the flow has moved past it\n- IN_PROGRESS: the flow's current work is inside this stage\n- UPCOMING: the flow has not reached this stage yet\n"
          },
          "completed_at": {
            "type": "string",
            "description": "Latest completion timestamp among the stage's tasks; set only when the\nstage is COMPLETED and at least one of its tasks recorded one\n"
          }
        },
        "required": [
          "id",
          "name",
          "status"
        ]
      },
      "PortalWorkflowTask": {
        "type": "object",
        "description": "A single portal-visible task of a linearized workflow execution",
        "properties": {
          "id": {
            "type": "string",
            "description": "Id of the underlying task (V2) or step (V1)"
          },
          "name": {
            "type": "string",
            "description": "Internal task/step name (fallback label when the portal config has none)"
          },
          "order": {
            "type": "integer",
            "description": "Zero-based position of the task in the linearized timeline"
          },
          "status": {
            "type": "string",
            "enum": [
              "COMPLETED",
              "SKIPPED",
              "IN_PROGRESS",
              "PENDING"
            ],
            "description": "Normalized task status:\n- COMPLETED / SKIPPED: the task is done (skipped tasks count as done for progress)\n- IN_PROGRESS: the task has been reached and work has started\n- PENDING: the task has not been completed yet\n"
          },
          "is_active": {
            "type": "boolean",
            "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"
          },
          "ecp": {
            "$ref": "#/components/schemas/PortalTaskConfig"
          },
          "installer": {
            "$ref": "#/components/schemas/PortalTaskConfig"
          },
          "journey": {
            "type": "object",
            "description": "Journey linked to the task, if any",
            "properties": {
              "id": {
                "type": "string"
              },
              "journeyId": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "complete_task_automatically": {
                "type": "boolean",
                "description": "If true, the task is auto completed when the journey is completed"
              }
            }
          },
          "assigned_to": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "phase_id": {
            "type": "string",
            "description": "Id of the phase the underlying task belongs to, if any (V2 only)"
          },
          "phase_name": {
            "type": "string",
            "description": "Name of the phase the underlying task belongs to, if any (V2 only)"
          },
          "stage_id": {
            "type": "string",
            "description": "Id of the stage the underlying task belongs to, when the execution\ncarries stages (derived by the Workflows API at read time)\n"
          },
          "completed_at": {
            "type": "string",
            "description": "Timestamp when the task was completed or skipped"
          },
          "updated_at": {
            "type": "string",
            "description": "Last update timestamp of the underlying task/step"
          }
        },
        "required": [
          "id",
          "name",
          "order",
          "status",
          "is_active"
        ]
      },
      "EntityPortalWorkflows": {
        "type": "object",
        "description": "Linearized portal workflows of a single entity",
        "properties": {
          "entity_id": {
            "type": "string"
          },
          "portal_workflows": {
            "type": "array",
            "description": "Empty when the entity has no portal-relevant workflow",
            "items": {
              "$ref": "#/components/schemas/PortalWorkflow"
            }
          }
        },
        "required": [
          "entity_id",
          "portal_workflows"
        ]
      },
      "PortalTaskConfig": {
        "type": "object",
        "description": "Portal-specific (ECP / installer) display config of a workflow task",
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "label": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "journey": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "journeyId": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "complete_task_automatically": {
                "type": "boolean"
              }
            }
          }
        }
      },
      "BaseBillingEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/BaseEntity"
          },
          {
            "$ref": "#/components/schemas/Entity"
          }
        ],
        "description": "A base billing event to be inherited by all billing events.",
        "type": "object",
        "required": [
          "type",
          "contract"
        ],
        "additionalProperties": true,
        "properties": {
          "billing_amount": {
            "type": "integer",
            "description": "Amount to be paid in cents i.e. precision 2",
            "example": 10050
          },
          "billing_amount_decimal": {
            "type": "string",
            "description": "Amount to be paid in cents in decimal string representation",
            "example": "100.50"
          },
          "billing_currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "external_id": {
            "type": "string",
            "description": "Unique identifier for event, used to reference the event to a 3rd party resource such as a SAP Installment.",
            "example": "d4fb2a4e-3f74-4fc4-8fba-6fdaaaa3b08e"
          },
          "contract": {
            "type": "object",
            "properties": {
              "$relation": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "entity_id": {
                      "type": "string",
                      "description": "Entity ID for the related contract.",
                      "example": "f589786b-3024-43cd-9cb3-5a3c953f2896"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "InstallmentEvent": {
        "description": "An entity that describes an installment billing event.",
        "allOf": [
          {
            "$ref": "#/components/schemas/BaseBillingEvent"
          },
          {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "installment"
                ],
                "description": "Type of the billing event.",
                "example": "installment"
              },
              "due_date": {
                "type": "string",
                "format": "date",
                "description": "Date on which the installment is due."
              },
              "paid_date": {
                "type": "string",
                "format": "date",
                "description": "Date on which the installment is paid by the customer."
              }
            },
            "required": [
              "due_date"
            ]
          }
        ],
        "properties": {
          "billing_amount_decimal": {
            "type": "string",
            "description": "Amount to be paid in cents in decimal string representation",
            "example": "100.50"
          }
        }
      },
      "ReimbursementEvent": {
        "description": "An entity that describes a reimbursement billing event.",
        "allOf": [
          {
            "$ref": "#/components/schemas/BaseBillingEvent"
          },
          {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "reimbursement"
                ],
                "description": "Type of the billing event.",
                "example": "reimbursement"
              },
              "due_date": {
                "type": "string",
                "format": "date",
                "description": "Date on which the installment is due."
              },
              "paid_date": {
                "type": "string",
                "format": "date",
                "description": "Date on which the customer is reimbursed."
              }
            }
          }
        ]
      },
      "BillingEvent": {
        "description": "An entity that describes a billing event such as a future installment or a reimbursement back to the customer.",
        "oneOf": [
          {
            "$ref": "#/components/schemas/InstallmentEvent"
          },
          {
            "$ref": "#/components/schemas/ReimbursementEvent"
          }
        ],
        "properties": {
          "billing_amount_decimal": {
            "type": "string",
            "description": "Amount to be paid in cents in decimal string representation",
            "example": "100.50"
          }
        }
      },
      "BillingAccount": {
        "allOf": [
          {
            "$ref": "#/components/schemas/BaseEntity"
          },
          {
            "$ref": "#/components/schemas/Entity"
          }
        ],
        "description": "A billing account",
        "type": "object",
        "properties": {
          "billing_account_number": {
            "type": "string",
            "description": "Billing account number"
          },
          "balance": {
            "type": "number",
            "description": "Balance of the billing account"
          },
          "balance_decimal": {
            "type": "string",
            "description": "Balance of the billing account in decimal string representation"
          },
          "balance_currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "billing_contact": {
            "type": "object",
            "properties": {
              "$relation": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "entity_id": {
                      "type": "string",
                      "description": "Entity ID for the related billing contact.",
                      "example": "f589786b-3024-43cd-9cb3-5a3c953f2896"
                    }
                  }
                }
              }
            }
          },
          "billing_address": {
            "type": "object",
            "properties": {
              "$relation_ref": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "entity_id": {
                      "type": "string",
                      "description": "Entity ID for the related billing address.",
                      "example": "f589786b-3024-43cd-9cb3-5a3c953f2896"
                    },
                    "path": {
                      "type": "string",
                      "description": "Path to the related billing address.",
                      "example": "address"
                    },
                    "_id": {
                      "type": "string",
                      "description": "ID of the related billing address.",
                      "example": "f589786b-3024-43cd-9cb3-5a3c953f2896"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "Balance": {
        "type": "object",
        "properties": {
          "balance": {
            "type": "integer",
            "description": "Current balance of the customer in cents. (precision 2)",
            "example": 8990
          },
          "balance_decimal": {
            "type": "number",
            "description": "Current balance of the customer in decimal string representation.",
            "example": "89.90"
          },
          "balance_currency": {
            "$ref": "#/components/schemas/Currency"
          }
        }
      },
      "Currency": {
        "type": "string",
        "description": "Currency code in ISO 4217 format",
        "example": "EUR"
      },
      "EntityGetParams": {
        "type": "object",
        "properties": {
          "slug": {
            "$ref": "#/components/schemas/EntitySlug",
            "description": "Single entity schema slug"
          },
          "entity_id": {
            "type": "string",
            "format": "uuid",
            "description": "Optional entity ID to filter by. If provided, creates a filter for _id.",
            "example": "3ec28ab5-8598-41ef-9486-b57fca1d5e2a"
          },
          "hydrate": {
            "type": "boolean",
            "description": "When true, enables entity hydration to resolve nested $relation & $relation_ref references in-place.",
            "default": false
          },
          "fields": {
            "type": "array",
            "description": "List of entity fields to include in search results",
            "items": {
              "type": "string"
            },
            "example": [
              "_id",
              "_title"
            ]
          },
          "templates": {
            "type": "object",
            "deprecated": true,
            "description": "DEPRECATED — client-supplied template strings to parse and return as synthetic fields. Supports both string values and nested objects of strings. Use `templates_ref` instead so templates are derived server-side from admin-authored portal configuration; ignored when `templates_ref` is present and rejected once the org has the `portals-reject-client-templates` flag enabled.",
            "additionalProperties": {
              "oneOf": [
                {
                  "type": "string"
                },
                {
                  "type": "object",
                  "additionalProperties": {
                    "type": "string"
                  }
                }
              ]
            },
            "example": {
              "content_top_name": "Customer #{{contract.customer_number}}",
              "main_content_name": "{{contract.contract_name}} ({{contract.contract_number}})",
              "content_bottom_name": "{{custom_contract_delivery_address}}",
              "nested_content": {
                "title": "{{contract.contract_name}}",
                "subtitle": "{{contract.contract_number}}"
              }
            }
          },
          "templates_ref": {
            "$ref": "#/components/schemas/TemplatesRef",
            "description": "Reference to the portal page (or a single block within it) whose admin-configured content provides the templates, derived server-side. With only `page_id`, the derived templates are a nested map keyed by block id, mirroring the entity detail page contract."
          },
          "filters": {
            "type": "array",
            "description": "Additional filters to apply to the search query",
            "items": {
              "type": "object",
              "description": "Elasticsearch filter object"
            },
            "example": [
              {
                "term": {
                  "status.keyword": "active"
                }
              },
              {
                "range": {
                  "_created_at": {
                    "gte": "2023-01-01"
                  }
                }
              }
            ]
          },
          "filters_context": {
            "type": "array",
            "description": "Context-based filters for entity relations.",
            "items": {
              "type": "object",
              "additionalProperties": {
                "oneOf": [
                  {
                    "type": "boolean"
                  },
                  {
                    "type": "string"
                  }
                ]
              }
            },
            "example": [
              {
                "portal_user": true
              },
              {
                "contact": true
              },
              {
                "contract": "3ec28ab5-8598-41ef-9486-b57fca1d5e2a"
              }
            ]
          },
          "targets": {
            "type": "array",
            "description": "Filters from these targets will be applied to the search query.",
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "example": [
              "3ec28ab5-8598-41ef-9486-b57fca1d5e2a"
            ]
          }
        },
        "required": [
          "slug"
        ]
      },
      "EntitySearchParams": {
        "type": "object",
        "properties": {
          "slug": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/EntitySlug"
              },
              {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/EntitySlug"
                },
                "minItems": 1
              },
              {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/EntitySlugConfig"
                },
                "minItems": 1
              }
            ],
            "description": "Entity slug, array of slugs, or array of per-slug configurations"
          },
          "q": {
            "type": "string",
            "description": "Free-text keyword search. This is plain text, **not** a query language: punctuation separates words rather than carrying any special meaning, and every word has to match. Use `q_fields` to restrict which fields are searched, and `filters`/`targets` for structured filtering. Overly long input is trimmed, and input with no words in it is ignored — the remaining parameters still apply.",
            "example": "contract"
          },
          "q_fields": {
            "type": "array",
            "description": "List of fields that can be searched",
            "items": {
              "type": "string"
            },
            "example": [
              "_title",
              "customer._title",
              "customer.first_name",
              "customer.last_name"
            ]
          },
          "group": {
            "type": "string",
            "description": "Field to group results by",
            "example": "customer._title"
          },
          "group_title": {
            "type": "string",
            "deprecated": true,
            "description": "DEPRECATED — client-supplied Handlebars template for the group title. Use `templates_ref` (global_search_config_id) instead; overridden when `templates_ref` derives a group title and rejected once the org has the `portals-reject-client-templates` flag enabled.",
            "example": "{{customer[Primary].first_name}} {{customer[Primary].last_name}}"
          },
          "group_size": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1000,
            "default": 100,
            "description": "Number of groups to return"
          },
          "group_sort": {
            "type": "string",
            "description": "Sort order for groups",
            "enum": [
              "asc",
              "desc"
            ],
            "default": "asc",
            "example": "desc"
          },
          "group_after_key": {
            "type": "object",
            "description": "Composite aggregation key for group pagination",
            "additionalProperties": {
              "oneOf": [
                {
                  "type": "string"
                },
                {
                  "type": "number"
                }
              ]
            }
          },
          "sort": {
            "type": "string",
            "example": "_created_at:desc"
          },
          "from": {
            "type": "integer",
            "minimum": 0,
            "default": 0
          },
          "size": {
            "type": "integer",
            "minimum": 0,
            "default": 100,
            "description": "Max search size is 1000 with higher values defaulting to 1000"
          },
          "hydrate": {
            "type": "boolean",
            "description": "When true, enables entity hydration to resolve nested $relation & $relation_ref references in-place.",
            "default": false
          },
          "fields": {
            "type": "array",
            "description": "List of entity fields to include in search results",
            "items": {
              "type": "string"
            },
            "example": [
              "_id",
              "_title"
            ]
          },
          "templates": {
            "type": "object",
            "deprecated": true,
            "description": "DEPRECATED — client-supplied template strings to parse and return as synthetic fields. Use `templates_ref` instead; ignored when `templates_ref` is present and rejected once the org has the `portals-reject-client-templates` flag enabled.",
            "additionalProperties": {
              "type": "string"
            },
            "example": {
              "content_top_name": "Customer #{{contract.customer_number}}",
              "main_content_name": "{{contract.contract_name}} ({{contract.contract_number}})",
              "content_bottom_name": "{{custom_contract_delivery_address}}"
            }
          },
          "templates_ref": {
            "$ref": "#/components/schemas/TemplatesRef",
            "description": "Reference to admin-authored configuration providing the templates, derived server-side. For the global search block pass `global_search_config_id`; the derived templates also supply the group title template (overriding `group_title`)."
          },
          "filters": {
            "type": "array",
            "description": "Additional filters to apply to the search query",
            "items": {
              "type": "object",
              "description": "Elasticsearch filter object"
            },
            "example": [
              {
                "term": {
                  "status.keyword": "active"
                }
              },
              {
                "range": {
                  "_created_at": {
                    "gte": "2023-01-01"
                  }
                }
              }
            ]
          },
          "filters_context": {
            "type": "array",
            "description": "Context-based filters for entity relations.",
            "items": {
              "type": "object",
              "additionalProperties": {
                "oneOf": [
                  {
                    "type": "boolean"
                  },
                  {
                    "type": "string"
                  }
                ]
              }
            },
            "example": [
              {
                "portal_user": true
              },
              {
                "contact": true
              },
              {
                "contract": "3ec28ab5-8598-41ef-9486-b57fca1d5e2a"
              }
            ]
          },
          "targets": {
            "type": "array",
            "description": "Filters from these targets will be applied to the search query.",
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "example": [
              "3ec28ab5-8598-41ef-9486-b57fca1d5e2a"
            ]
          },
          "include": {
            "type": "array",
            "description": "Optional enrichment data to side-load alongside results under the `includes` response key.",
            "items": {
              "type": "string",
              "enum": [
                "active_workflow"
              ]
            },
            "example": [
              "active_workflow"
            ]
          },
          "highlight": {
            "type": "object",
            "description": "Elasticsearch highlight configuration passed through to the Entity API. When provided, matching results include a `highlight` object with field names as keys and arrays of highlighted fragments as values."
          }
        },
        "required": [
          "slug"
        ]
      },
      "IdentifierAttribute": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string",
            "description": "Label attribute"
          },
          "name": {
            "type": "string",
            "description": "Name of the attribute"
          },
          "type": {
            "type": "string",
            "description": "Type of the secondary attribute"
          }
        }
      },
      "RegistrationIdentifier": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Name of the identifier/attribute"
          },
          "schema": {
            "$ref": "#/components/schemas/EntitySlug"
          }
        }
      },
      "AdditionalContactAttribute": {
        "type": "object",
        "description": "A contact attribute that is collected from the user during self-registration\nand written onto the newly created contact, but is NOT used to identify\nan existing contact.\n",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Name of the contact attribute"
          },
          "required": {
            "type": "boolean",
            "description": "Whether the user must provide a value for this attribute",
            "default": false
          }
        }
      },
      "ContractIdentifier": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Name of the identifier/attribute"
          },
          "schema": {
            "$ref": "#/components/schemas/EntitySlug"
          }
        }
      },
      "AcceptanceDecision": {
        "type": "object",
        "properties": {
          "decision": {
            "type": "string",
            "enum": [
              "accept",
              "decline"
            ],
            "description": "Acceptance decision"
          }
        },
        "required": [
          "decision"
        ],
        "additionalProperties": false
      },
      "TriggerPortalFlow": {
        "type": "object",
        "properties": {
          "activity_id": {
            "type": "string",
            "description": "Id of the activity",
            "example": "01F130Q52Q6MWSNS8N2AVXV4JN"
          },
          "ecp_config": {
            "type": "object",
            "properties": {
              "file_config": {
                "type": "object",
                "properties": {
                  "shared_with_end_customer": {
                    "type": "boolean",
                    "description": "Indicate whether the file is shared with the end customer"
                  },
                  "_tags": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Array of entity tags",
                    "example": [
                      "example",
                      "mock"
                    ]
                  }
                }
              }
            }
          }
        }
      },
      "ExtensionConfig": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Name of the extension"
          },
          "status": {
            "type": "string",
            "enum": [
              "installed",
              "enabled"
            ],
            "description": "Status of the extension",
            "default": "installed"
          },
          "options": {
            "type": "object",
            "description": "Extension option values.",
            "additionalProperties": {
              "type": "string"
            },
            "default": {}
          }
        },
        "required": [
          "id",
          "status"
        ]
      },
      "ExtensionHookSelection": {
        "type": "object",
        "nullable": true,
        "properties": {
          "app_id": {
            "type": "string",
            "description": "The ID of the selected app."
          },
          "extension_id": {
            "type": "string",
            "description": "The ID of the selected extension."
          },
          "hook_id": {
            "type": "string",
            "description": "The ID of the selected hook."
          }
        },
        "required": [
          "app_id",
          "extension_id",
          "hook_id"
        ]
      },
      "PublicExtensionCapabilities": {
        "type": "object",
        "properties": {
          "consumptionDataRetrieval": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DataRetrievalItem"
            }
          },
          "dataExport": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DataRetrievalItem"
            }
          },
          "priceDataRetrieval": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DataRetrievalItem"
            }
          },
          "costDataRetrieval": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DataRetrievalItem"
            }
          },
          "contractIdentification": {
            "type": "object",
            "properties": {
              "app": {
                "$ref": "#/components/schemas/PublicAppDetails"
              },
              "extension": {
                "$ref": "#/components/schemas/PublicExtensionDetails"
              },
              "hook": {
                "$ref": "#/components/schemas/PublicContractIdentificationDetails"
              }
            }
          },
          "meterReadingPlausibilityCheck": {
            "type": "object",
            "properties": {
              "extension": {
                "$ref": "#/components/schemas/PublicExtensionDetails"
              },
              "hook": {
                "$ref": "#/components/schemas/PublicMeterReadingPlausibilityCheckDetails"
              }
            }
          },
          "changeEmail": {
            "type": "object",
            "properties": {
              "app": {
                "$ref": "#/components/schemas/PublicAppDetails"
              },
              "extension": {
                "$ref": "#/components/schemas/PublicExtensionDetails"
              },
              "hook": {
                "$ref": "#/components/schemas/PublicChangeEmailDetails"
              }
            }
          },
          "changePassword": {
            "type": "object",
            "properties": {
              "app": {
                "$ref": "#/components/schemas/PublicAppDetails"
              },
              "extension": {
                "$ref": "#/components/schemas/PublicExtensionDetails"
              },
              "hook": {
                "$ref": "#/components/schemas/PublicChangePasswordDetails"
              }
            }
          },
          "deleteAccount": {
            "type": "object",
            "properties": {
              "app": {
                "$ref": "#/components/schemas/PublicAppDetails"
              },
              "extension": {
                "$ref": "#/components/schemas/PublicExtensionDetails"
              },
              "hook": {
                "$ref": "#/components/schemas/PublicDeleteAccountDetails"
              }
            }
          }
        }
      },
      "PublicSelfManagementExplanation": {
        "type": "object",
        "properties": {
          "en": {
            "type": "string",
            "description": "Explanation of the functionality shown to the end user."
          }
        },
        "additionalProperties": {
          "type": "string",
          "description": "Explanation of the functionality in language denoted by ISO 3166-1 alpha-2 code."
        },
        "required": [
          "en"
        ],
        "description": "Explanation of the hook."
      },
      "PublicChangeEmailDetails": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Identifier of the hook."
          },
          "change_mode": {
            "type": "string",
            "enum": [
              "synchronous",
              "asynchronous"
            ],
            "description": "Whether the third-party system applies the email change immediately (`synchronous`) or takes it over entirely (`asynchronous`).\nIn synchronous mode the login email is changed as soon as the request succeeds and the portal user has to sign in again with the new address.\n",
            "default": "asynchronous"
          },
          "require_password_confirmation": {
            "type": "boolean",
            "description": "Whether the portal user must confirm their current password before the email change is handed over to the third-party system.\nNot required for portal users whose identity is managed by an identity provider - an SSO login has no password to confirm.\n",
            "default": true
          },
          "explanation": {
            "$ref": "#/components/schemas/PublicSelfManagementExplanation"
          }
        }
      },
      "PublicChangePasswordDetails": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Identifier of the hook."
          },
          "require_new_password": {
            "type": "boolean",
            "description": "Whether the portal user must provide a new password that is passed to the third-party system.",
            "default": false
          },
          "explanation": {
            "$ref": "#/components/schemas/PublicSelfManagementExplanation"
          }
        }
      },
      "PublicDeleteAccountDetails": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Identifier of the hook."
          },
          "explanation": {
            "$ref": "#/components/schemas/PublicSelfManagementExplanation"
          }
        }
      },
      "DataRetrievalItem": {
        "type": "object",
        "properties": {
          "app": {
            "$ref": "#/components/schemas/PublicAppDetails"
          },
          "extension": {
            "$ref": "#/components/schemas/PublicExtensionDetails"
          },
          "hook": {
            "$ref": "#/components/schemas/PublicDataRetrievalHookDetails"
          }
        }
      },
      "PublicAppDetails": {
        "type": "object",
        "properties": {
          "app_id": {
            "type": "string",
            "description": "Identifier of the app."
          },
          "name": {
            "type": "object",
            "properties": {
              "en": {
                "type": "string",
                "description": "Name of the app in English."
              }
            },
            "additionalProperties": {
              "type": "string",
              "description": "Name of the app in some other language denoted by ISO 3166-1 alpha-2 code."
            },
            "required": [
              "en"
            ]
          }
        }
      },
      "PublicExtensionDetails": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Identifier of the extension."
          },
          "name": {
            "type": "object",
            "properties": {
              "en": {
                "type": "string",
                "description": "Name of the extension in English."
              }
            },
            "additionalProperties": {
              "type": "string",
              "description": "Name of the extension in some other language denoted by ISO 3166-1 alpha-2 code."
            },
            "required": [
              "en"
            ]
          }
        }
      },
      "PublicDataRetrievalHookDetails": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Identifier of the hook."
          },
          "name": {
            "type": "object",
            "properties": {
              "en": {
                "type": "string",
                "description": "Name of the extension in English."
              }
            },
            "additionalProperties": {
              "type": "string",
              "description": "Name of the extension in some other language denoted by ISO 3166-1 alpha-2 code."
            },
            "required": [
              "en"
            ]
          },
          "intervals": {
            "type": "array",
            "deprecated": true,
            "items": {
              "type": "string"
            },
            "description": "Deprecated. Prefer fetching `intervals` from the `visualizationMetadata` endpoint\n(`GET /v2/portal/visualization/metadata`) so the supported intervals can vary per\nmeter/contract. Still emitted as a fallback for clients that have not migrated yet.\n"
          },
          "block_types": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Optional list of portal block types the hook supports. Empty/missing means the hook is usable on any export-capable block."
          }
        }
      },
      "VisualizationMetadata": {
        "type": "object",
        "description": "Runtime metadata describing how a visualization should be rendered for a given portal context. Returned by `GET /v2/portal/visualization/metadata`.\n",
        "properties": {
          "type_options": {
            "type": "array",
            "description": "Types advertised for the current context (e.g. `ht`/`nt`, `feed-in`/`feed-out`). The `id` matches the `type` field returned by the data hook.\n",
            "items": {
              "$ref": "#/components/schemas/VisualizationTypeOption"
            }
          },
          "intervals": {
            "type": "array",
            "description": "Intervals supported for the current context. If omitted, all intervals are assumed supported. `custom` marks a period-based consumption source: the portal requests the whole `data_range` once with `interval=custom` and renders one bar per returned record (see `period` on the consumption data point) instead of offering interval / date navigation or period comparison. When `custom` is present it takes precedence over any fixed intervals also listed.\n",
            "items": {
              "type": "string",
              "enum": [
                "PT15M",
                "PT1H",
                "P1D",
                "P1M",
                "P1Y",
                "custom"
              ]
            }
          },
          "data_range": {
            "$ref": "#/components/schemas/VisualizationDataRange"
          }
        }
      },
      "VisualizationTypeOption": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Identifier of the type. Matches the `type` field on the data hook response."
          },
          "label": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Localized label for the type, keyed by ISO 3166-1 alpha-2 language code."
          },
          "aggregation_group": {
            "type": "string",
            "description": "Optional grouping key. Types in the same `aggregation_group` are visually combined; types in different groups (or without a group) render separately. How they combine depends on each type's `statistical_method`:\n  - bar chart (`sum`): same-group types are stacked into a single bar (e.g. ht/nt\n    summed into total consumption); different-group types render side-by-side.\n  - line chart (`min` / `average` / `max`): same-group types are rendered as an\n    area chart; different-group types render as separate lines.\n"
          },
          "statistical_method": {
            "type": "string",
            "enum": [
              "sum",
              "average",
              "min",
              "max"
            ],
            "description": "Statistical method already applied to this type's data. Determines the chart shape used to render the type's values: `sum` is shown as a bar chart; `min`, `average`, and `max` are shown as a line chart. Each type advertises its own method, so a single visualization can mix bar-shaped types with line-shaped types. Defaults to `sum` when omitted.\n"
          },
          "unit": {
            "type": "string",
            "description": "Unit shared by all values of this type (e.g. \"kWh\")."
          },
          "color": {
            "type": "string",
            "enum": [
              "primary",
              "slate",
              "mauve",
              "orange",
              "red",
              "tomato",
              "amber",
              "green",
              "blue"
            ],
            "description": "Optional Spark color token used to render this type in the visualization. Maps onto the portal's Spark palette (`primary` is the org's primary brand color). When omitted the consumer falls back to its own per-type default.\n"
          },
          "precision": {
            "type": "integer",
            "minimum": 0,
            "maximum": 10,
            "description": "Optional number of decimal places to show when rendering values of this type in the visualization (axis labels, tooltips, summaries). When omitted the consumer falls back to its own default precision.\n"
          }
        },
        "required": [
          "id"
        ]
      },
      "VisualizationDataRange": {
        "type": "object",
        "description": "Earliest / latest timestamps for which data is available in the current context.",
        "properties": {
          "from": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601 timestamp of the earliest available data point."
          },
          "to": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601 timestamp of the latest available data point."
          }
        }
      },
      "PublicContractIdentificationDetails": {
        "type": "object",
        "properties": {
          "explanation": {
            "type": "object",
            "properties": {
              "en": {
                "type": "string",
                "description": "Explanation of the functionality shown to the end user.",
                "example": "This process will give you access to all Contracts kept"
              }
            },
            "additionalProperties": {
              "type": "string",
              "description": "Explanation of the functionality in language denoted by ISO 3166-1 alpha-2 code."
            },
            "required": [
              "en"
            ],
            "description": "Explanation of the hook."
          }
        }
      },
      "PublicMeterReadingPlausibilityCheckDetails": {
        "type": "object",
        "properties": {
          "plausibility_mode": {
            "type": "string",
            "enum": [
              "check",
              "range"
            ],
            "description": "Mode for plausibility check:\n- \"check\": Validates meter reading and returns valid: boolean (used during submission)\n- \"range\": Returns min/max allowed values for validation before submission\n"
          }
        }
      },
      "Extension": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Identifier of the extension. Should not change between updates."
          },
          "app_id": {
            "type": "string",
            "description": "Identifier of the app from which the extension was installed. Should not change between updates."
          },
          "app_name": {
            "type": "string",
            "description": "Name of the app from which the extension was installed. Should not change between updates."
          },
          "name": {
            "type": "object",
            "properties": {
              "en": {
                "type": "string",
                "description": "Name of the extension in English."
              }
            },
            "additionalProperties": {
              "type": "string",
              "description": "Name of the extension in some other language denoted by ISO 3166-1 alpha-2 code."
            },
            "required": [
              "en"
            ],
            "description": "Name of the extension."
          },
          "description": {
            "type": "object",
            "properties": {
              "en": {
                "type": "string",
                "description": "Name of the extension in English."
              }
            },
            "additionalProperties": {
              "type": "string",
              "description": "Name of the extension in some other language denoted by ISO 3166-1 alpha-2 code."
            },
            "required": [
              "en"
            ],
            "description": "Name of the extension."
          },
          "version": {
            "type": "string",
            "description": "Version of the extension."
          },
          "options": {
            "type": "array",
            "description": "Options available to the extension configurable by the portal administrator.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "description": "Identifier of the option. Should not change between updates."
                },
                "name": {
                  "type": "object",
                  "properties": {
                    "en": {
                      "type": "string",
                      "description": "Name of the option in English."
                    }
                  },
                  "additionalProperties": {
                    "type": "string",
                    "description": "Name of the option in some other language denoted by ISO 3166-1 alpha-2 code."
                  },
                  "required": [
                    "en"
                  ],
                  "description": "Name of the option."
                },
                "type": {
                  "type": "string",
                  "enum": [
                    "text",
                    "secret"
                  ],
                  "description": "Type of the option."
                },
                "description": {
                  "type": "object",
                  "properties": {
                    "en": {
                      "type": "string",
                      "description": "Description of the option in English."
                    }
                  },
                  "additionalProperties": {
                    "type": "string",
                    "description": "Description of the option in some other language denoted by ISO 3166-1 alpha-2 code."
                  },
                  "required": [
                    "en"
                  ],
                  "description": "Description of the option."
                },
                "default": {
                  "type": "string",
                  "description": "Default value of the option."
                },
                "required": {
                  "type": "boolean",
                  "description": "Indicate whether the option is required.",
                  "default": false
                }
              },
              "required": [
                "id",
                "name",
                "type"
              ]
            }
          },
          "links": {
            "type": "array",
            "description": "External links added to the portal.",
            "items": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/ExtensionSeamlessLink"
                }
              ]
            }
          },
          "hooks": {
            "type": "array",
            "description": "Hooks that influence the behavior of Portal.",
            "items": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/ExtensionHook"
                },
                {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ExtensionHookRegistrationIdentifiersCheck"
                    },
                    {
                      "$ref": "#/components/schemas/ExtensionHookContractIdentification"
                    },
                    {
                      "$ref": "#/components/schemas/ExtensionHookPriceDataRetrieval"
                    },
                    {
                      "$ref": "#/components/schemas/ExtensionHookConsumptionDataRetrieval"
                    },
                    {
                      "$ref": "#/components/schemas/ExtensionHookDataExport"
                    },
                    {
                      "$ref": "#/components/schemas/ExtensionHookCostDataRetrieval"
                    },
                    {
                      "$ref": "#/components/schemas/ExtensionHookMeterReadingPlausibilityCheck"
                    },
                    {
                      "$ref": "#/components/schemas/ExtensionHookVisualizationMetadata"
                    },
                    {
                      "$ref": "#/components/schemas/ExtensionHookChangeEmail"
                    },
                    {
                      "$ref": "#/components/schemas/ExtensionHookChangePassword"
                    },
                    {
                      "$ref": "#/components/schemas/ExtensionHookDeleteAccount"
                    }
                  ]
                }
              ]
            }
          }
        },
        "required": [
          "id",
          "name"
        ]
      },
      "ExtensionSeamlessLink": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Identifier of the link. Should not change between updates."
          },
          "name": {
            "type": "object",
            "properties": {
              "en": {
                "type": "string",
                "description": "Name of the extension in English."
              }
            },
            "additionalProperties": {
              "type": "string",
              "description": "Name of the extension in some other language denoted by ISO 3166-1 alpha-2 code."
            },
            "required": [
              "en"
            ],
            "description": "Name of the extension."
          },
          "description": {
            "type": "object",
            "properties": {
              "en": {
                "type": "string",
                "description": "Name of the extension in English."
              }
            },
            "additionalProperties": {
              "type": "string",
              "description": "Name of the extension in some other language denoted by ISO 3166-1 alpha-2 code."
            },
            "required": [
              "en"
            ],
            "description": "Name of the extension."
          },
          "type": {
            "type": "string",
            "enum": [
              "seamless"
            ]
          },
          "condition": {
            "type": "string",
            "description": "Controls whether the link should be shown. Supports variable interpolation.",
            "example": "{{Contact.customer_number | is_not_empty}}"
          },
          "auth": {
            "$ref": "#/components/schemas/ExtensionAuthBlock"
          },
          "redirect": {
            "type": "object",
            "properties": {
              "url": {
                "type": "string",
                "description": "URL to redirect to. Supports variable interpolation."
              },
              "params": {
                "type": "object",
                "description": "Parameters to append to the URL. Supports variable interpolation.",
                "additionalProperties": {
                  "type": "string"
                },
                "default": {}
              }
            }
          }
        },
        "required": [
          "id",
          "name",
          "type",
          "redirect"
        ]
      },
      "ExtensionHook": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Identifier of the hook. Should not change between updates."
          }
        }
      },
      "ExtensionHookRegistrationIdentifiersCheck": {
        "description": "Hook that replaces the built-in registration identifiers check. This hook makes a POST call whenever a user is trying to register to find the corresponding contact. The expected response to the call is:\n  - 200 with contact id if exactly one contact is found\n  - 404 if no contact is found or more than contact is found\n",
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "registrationIdentifiersCheck"
            ]
          },
          "auth": {
            "$ref": "#/components/schemas/ExtensionAuthBlock"
          },
          "call": {
            "type": "object",
            "properties": {
              "method": {
                "type": "string",
                "description": "HTTP method to use for the call",
                "default": "POST"
              },
              "url": {
                "type": "string",
                "description": "URL to call. Supports variable interpolation."
              },
              "params": {
                "type": "object",
                "description": "Parameters to append to the URL. Supports variable interpolation.",
                "additionalProperties": {
                  "type": "string"
                },
                "default": {}
              },
              "headers": {
                "type": "object",
                "description": "Headers to use. Supports variable interpolation.",
                "additionalProperties": {
                  "type": "string"
                },
                "default": {}
              },
              "body": {
                "type": "object",
                "description": "Optional JSON body to use for the call. Defaults to object with all configured identifiers grouped by entity, e.g. `{\"contract\": {\"contract_name\": \"Name\"}}`. Supports variable interpolation."
              },
              "result": {
                "type": "string",
                "deprecated": true,
                "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."
              }
            },
            "required": [
              "url",
              "headers"
            ]
          },
          "resolved": {
            "type": "object",
            "properties": {
              "result": {
                "type": "string",
                "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`."
              },
              "error_message_path": {
                "type": "string",
                "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",
                "example": "error.message"
              }
            }
          },
          "use_static_ips": {
            "type": "boolean",
            "deprecated": true,
            "description": "Deprecated. Prefer `secure_proxy` instead.\nIf true, requests are made from a set of static IP addresses and only allow connections to a set of allowed IP addresses. Get in touch with us to add your IP addresses.\n",
            "default": false
          },
          "secure_proxy": {
            "$ref": "#/components/schemas/SecureProxyConfig"
          }
        },
        "required": [
          "type",
          "call"
        ]
      },
      "ExtensionHookContractIdentification": {
        "description": "Hook that replaces the built-in Contract identification for self-assignment. This hook involves an HTTP request whenever a user is trying to self-assign Contract(s).\nThe expected response http status code to the call is:\n  - 200 if found\n  - 404 if not found\n\nThe following assignment modes are supported:\n  - `contracts`: We expect the response to contain Contract ids (customizable using `result` property).\n  - `contact_to_contracts`: We expect the response to contain a Contact id (customizable using `result` property) and we will assign the Contact as a Customer to the Contracts and (optionally) update the Contact attribute specified by `contact_relation_attribute` to add the matched Contact.\n  - `contact_to_portal_user`: We expect the response to contain a Contact id (customizable using `result` property) and we will assign the Contact to the Portal User. Portal User will be able to see all data including Contracts transitively.\n\nDefaults to `contact_to_contracts` for backwards compatibility. We recommend using `contact_to_portal_user` as it does not influence the data model of business entities.\n",
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "contractIdentification"
            ]
          },
          "auth": {
            "$ref": "#/components/schemas/ExtensionAuthBlock"
          },
          "call": {
            "type": "object",
            "properties": {
              "method": {
                "type": "string",
                "description": "HTTP method to use for the call",
                "default": "POST"
              },
              "url": {
                "type": "string",
                "description": "URL to call. Supports variable interpolation."
              },
              "params": {
                "type": "object",
                "description": "Parameters to append to the URL. Supports variable interpolation.",
                "additionalProperties": {
                  "type": "string"
                },
                "default": {}
              },
              "headers": {
                "type": "object",
                "description": "Headers to use. Supports variable interpolation.",
                "additionalProperties": {
                  "type": "string"
                },
                "default": {}
              },
              "body": {
                "type": "object",
                "description": "Optional JSON body to use for the call. Defaults to object with all configured identifiers grouped by entity, e.g. `{\"contract\": {\"contract_name\": \"Name\"}}`. Supports variable interpolation."
              },
              "result": {
                "type": "string",
                "deprecated": true,
                "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."
              }
            },
            "required": [
              "url",
              "headers"
            ]
          },
          "resolved": {
            "type": "object",
            "properties": {
              "result": {
                "type": "string",
                "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`."
              },
              "error_message_path": {
                "type": "string",
                "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",
                "example": "error.message"
              }
            }
          },
          "assignment_mode": {
            "type": "string",
            "enum": [
              "contracts",
              "contact_to_contracts",
              "contact_to_portal_user"
            ],
            "description": "Mode of contract assignment. See hook description for mode details.",
            "default": "contact_to_contracts"
          },
          "contact_relation_attribute": {
            "type": "string",
            "description": "Name of the Contact attribute to update with the matched Contact ID when using `contact_to_contracts` mode. Must be a Contact relation attribute supporting multiple entities.",
            "example": "represents_contact"
          },
          "explanation": {
            "type": "object",
            "properties": {
              "en": {
                "type": "string",
                "description": "Explanation of the functionality shown to the end user.",
                "example": "This process will give you access to the matching Contracts."
              }
            },
            "additionalProperties": {
              "type": "string",
              "description": "Explanation of the functionality in language denoted by ISO 3166-1 alpha-2 code."
            },
            "required": [
              "en"
            ],
            "description": "Explanation of the hook."
          },
          "use_static_ips": {
            "type": "boolean",
            "deprecated": true,
            "description": "Deprecated. Prefer `secure_proxy` instead.\nIf true, requests are made from a set of static IP addresses and only allow connections to a set of allowed IP addresses. Get in touch with us to add your IP addresses.\n",
            "default": false
          },
          "secure_proxy": {
            "$ref": "#/components/schemas/SecureProxyConfig"
          }
        },
        "required": [
          "type",
          "call"
        ]
      },
      "ExtensionHookMeterReadingPlausibilityCheck": {
        "description": "Hook that checks the plausibility of meter readings before they are saved. This hook makes a POST call whenever a user is trying to save a meter reading. The expected response to the call is:\n  - 200:\n    If meter reading is plausible, the response should contain:\n      - valid: true\n    If meter reading is not plausible, the response should contain:\n      - valid: false\n",
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "meterReadingPlausibilityCheck"
            ]
          },
          "plausibility_mode": {
            "type": "string",
            "enum": [
              "check",
              "range"
            ],
            "default": "check",
            "description": "Mode for plausibility check:\n- \"check\": Validates meter reading and returns valid: boolean (used during submission)\n- \"range\": Returns min/max allowed values for each counter for validation before submission\n"
          },
          "auth": {
            "$ref": "#/components/schemas/ExtensionAuthBlock"
          },
          "call": {
            "type": "object",
            "properties": {
              "url": {
                "type": "string",
                "description": "URL to call. Supports variable interpolation."
              },
              "body": {
                "type": "object",
                "description": "JSON body to use for the call. Supports variable interpolation.",
                "additionalProperties": {
                  "type": "string"
                },
                "default": {}
              },
              "headers": {
                "type": "object",
                "description": "Headers to use. Supports variable interpolation.",
                "additionalProperties": {
                  "type": "string"
                },
                "default": {}
              }
            },
            "required": [
              "url",
              "headers",
              "body"
            ]
          },
          "resolved": {
            "type": "object",
            "description": "Response to the call",
            "properties": {
              "data_path": {
                "type": "string",
                "description": "Optional path to an array in the response. If specified and the path points to an array,\nthe hook will map over each item using 'Item' variable for interpolation.\nRelevant only if plausibility_mode is \"range\".\n",
                "example": "data.results"
              },
              "dataPath": {
                "type": "string",
                "deprecated": true,
                "description": "Deprecated. Use `data_path` instead."
              },
              "counter_identifiers": {
                "description": "Counter identifier(s) used to match against the meter's counters.\nCan be a string (counter ID) or an object with counter properties.\nThe backend resolves this to meter_counter_id in the final response.\nRelevant only if plausibility_mode is \"range\".\n",
                "oneOf": [
                  {
                    "type": "string",
                    "example": "{{Item.counter_id}}"
                  },
                  {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    },
                    "example": {
                      "obis_code": "{{Item.obis}}"
                    }
                  }
                ]
              },
              "valid": {
                "type": "string",
                "description": "Indicate whether the meter reading is plausible. Relevant only if plausibility_mode is \"check\".",
                "example": "{{CallResponse.data.valid}}"
              },
              "upper_limit": {
                "type": "string",
                "description": "Upper allowed limit of the meter reading",
                "example": "{{CallResponse.data.upper_limit}}"
              },
              "lower_limit": {
                "type": "string",
                "description": "Lower allowed limit of the meter reading",
                "example": "{{CallResponse.data.lower_limit}}"
              },
              "error_message_path": {
                "type": "string",
                "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",
                "example": "error.message"
              }
            }
          },
          "use_static_ips": {
            "type": "boolean",
            "deprecated": true,
            "description": "Deprecated. Prefer `secure_proxy` instead.\nIf true, requests are made from a set of static IP addresses and only allow connections to a set of allowed IP addresses. Get in touch with us to add your IP addresses.\n",
            "default": false
          },
          "secure_proxy": {
            "$ref": "#/components/schemas/SecureProxyConfig"
          }
        },
        "required": [
          "type",
          "call",
          "resolved"
        ]
      },
      "ExtensionHookPriceDataRetrieval": {
        "description": "Hook that will allow using the specified source as data for price visualizations. This hook is triggered to fetch the data. Format of the request and response has to follow the following specification: TBD. The expected response to the call is:\n  - 200 with the time series data\n",
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "priceDataRetrieval"
            ]
          },
          "auth": {
            "$ref": "#/components/schemas/ExtensionAuthBlock"
          },
          "call": {
            "type": "object",
            "properties": {
              "method": {
                "type": "string",
                "description": "HTTP method to use for the call",
                "default": "GET"
              },
              "url": {
                "type": "string",
                "description": "URL to call. Supports variable interpolation."
              },
              "params": {
                "type": "object",
                "description": "Parameters to append to the URL. Supports variable interpolation.",
                "additionalProperties": {
                  "type": "string"
                },
                "default": {}
              },
              "headers": {
                "type": "object",
                "description": "Headers to use. Supports variable interpolation.",
                "additionalProperties": {
                  "type": "string"
                },
                "default": {}
              },
              "body": {
                "type": "object",
                "description": "Request body to send. Supports variable interpolation. Content format is determined by Content-Type header.",
                "additionalProperties": {
                  "type": "string"
                },
                "default": {}
              }
            },
            "required": [
              "url"
            ]
          },
          "resolved": {
            "type": "object",
            "properties": {
              "data_path": {
                "type": "string",
                "description": "Optional path to the data (array) in the response. If omitted, the data is assumed to be on the top level."
              },
              "dataPath": {
                "type": "string",
                "deprecated": true,
                "description": "Deprecated. Use `data_path` instead."
              },
              "error_message_path": {
                "type": "string",
                "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",
                "example": "error.message"
              }
            }
          },
          "use_static_ips": {
            "type": "boolean",
            "deprecated": true,
            "description": "Deprecated. Prefer `secure_proxy` instead.\nIf true, requests are made from a set of static IP addresses and only allow connections to a set of allowed IP addresses. Get in touch with us to add your IP addresses.\n",
            "default": false
          },
          "secure_proxy": {
            "$ref": "#/components/schemas/SecureProxyConfig"
          }
        },
        "required": [
          "type",
          "call"
        ]
      },
      "ExtensionHookConsumptionDataRetrieval": {
        "description": "Hook that will allow using the specified source as data for consumption visualizations. This hook is triggered to fetch the data. Format of the request and response has to follow the following specification: TBD. The expected response to the call is:\n  - 200 with the time series data\n",
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "consumptionDataRetrieval"
            ]
          },
          "auth": {
            "$ref": "#/components/schemas/ExtensionAuthBlock"
          },
          "call": {
            "type": "object",
            "properties": {
              "method": {
                "type": "string",
                "description": "HTTP method to use for the call",
                "default": "GET"
              },
              "url": {
                "type": "string",
                "description": "URL to call. Supports variable interpolation."
              },
              "params": {
                "type": "object",
                "description": "Parameters to append to the URL. Supports variable interpolation.",
                "additionalProperties": {
                  "type": "string"
                },
                "default": {}
              },
              "headers": {
                "type": "object",
                "description": "Headers to use. Supports variable interpolation.",
                "additionalProperties": {
                  "type": "string"
                },
                "default": {}
              },
              "body": {
                "type": "object",
                "description": "Request body to send. Supports variable interpolation. Content format is determined by Content-Type header.",
                "additionalProperties": {
                  "type": "string"
                },
                "default": {}
              }
            },
            "required": [
              "url"
            ]
          },
          "resolved": {
            "type": "object",
            "properties": {
              "data_path": {
                "type": "string",
                "description": "Optional path to the data (array) in the response. If omitted, the data is assumed to be on the top level."
              },
              "dataPath": {
                "type": "string",
                "deprecated": true,
                "description": "Deprecated. Use `data_path` instead."
              },
              "error_message_path": {
                "type": "string",
                "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",
                "example": "error.message"
              }
            }
          },
          "use_static_ips": {
            "type": "boolean",
            "deprecated": true,
            "description": "Deprecated. Prefer `secure_proxy` instead.\nIf true, requests are made from a set of static IP addresses and only allow connections to a set of allowed IP addresses. Get in touch with us to add your IP addresses.\n",
            "default": false
          },
          "secure_proxy": {
            "$ref": "#/components/schemas/SecureProxyConfig"
          }
        },
        "required": [
          "type",
          "call"
        ]
      },
      "ExtensionHookDataExport": {
        "description": "Generic data export hook. When configured on a visualization block, the portal delegates the export action (e.g. CSV/Excel/PDF download) to the configured external source instead of generating the file itself. Can be used by any block that supports export — consumption charts, dynamic tariff charts, etc. The expected response to the call is:\n  - 200 with a JSON body describing the exported file (download_url, optional filename, content_type, expires_at)\n",
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "dataExport"
            ]
          },
          "block_types": {
            "type": "array",
            "description": "Optional list of portal block types this hook supports. If omitted,\nthe hook is usable on any export-capable block. Allowed values match\nthe block type identifiers used by the portal builder\n(e.g. `consumption_visualization`, `dynamic_tariff`).\n",
            "items": {
              "type": "string"
            }
          },
          "auth": {
            "$ref": "#/components/schemas/ExtensionAuthBlock"
          },
          "call": {
            "type": "object",
            "properties": {
              "method": {
                "type": "string",
                "description": "HTTP method to use for the call",
                "default": "GET"
              },
              "url": {
                "type": "string",
                "description": "URL to call. Supports variable interpolation."
              },
              "params": {
                "type": "object",
                "description": "Parameters to append to the URL. Supports variable interpolation.",
                "additionalProperties": {
                  "type": "string"
                },
                "default": {}
              },
              "headers": {
                "type": "object",
                "description": "Headers to use. Supports variable interpolation.",
                "additionalProperties": {
                  "type": "string"
                },
                "default": {}
              },
              "body": {
                "type": "object",
                "description": "Request body to send. Supports variable interpolation. Content format is determined by Content-Type header.",
                "additionalProperties": {
                  "type": "string"
                },
                "default": {}
              }
            },
            "required": [
              "url"
            ]
          },
          "resolved": {
            "type": "object",
            "properties": {
              "error_message_path": {
                "type": "string",
                "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",
                "example": "error.message"
              }
            }
          },
          "use_static_ips": {
            "type": "boolean",
            "deprecated": true,
            "description": "Deprecated. Prefer `secure_proxy` instead.\nIf true, requests are made from a set of static IP addresses and only allow connections to a set of allowed IP addresses. Get in touch with us to add your IP addresses.\n",
            "default": false
          },
          "secure_proxy": {
            "$ref": "#/components/schemas/SecureProxyConfig"
          }
        },
        "required": [
          "type",
          "call"
        ]
      },
      "ExtensionHookVisualizationMetadata": {
        "description": "Hook that returns runtime metadata describing how a visualization should be rendered for a given portal context. Invoked by the portal before fetching data, with the same context the data hook receives.\n",
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "visualizationMetadata"
            ]
          },
          "auth": {
            "$ref": "#/components/schemas/ExtensionAuthBlock"
          },
          "call": {
            "type": "object",
            "properties": {
              "method": {
                "type": "string",
                "description": "HTTP method to use for the call",
                "default": "GET"
              },
              "url": {
                "type": "string",
                "description": "URL to call. Supports variable interpolation."
              },
              "params": {
                "type": "object",
                "description": "Parameters to append to the URL. Supports variable interpolation.",
                "additionalProperties": {
                  "type": "string"
                },
                "default": {}
              },
              "headers": {
                "type": "object",
                "description": "Headers to use. Supports variable interpolation.",
                "additionalProperties": {
                  "type": "string"
                },
                "default": {}
              },
              "body": {
                "type": "object",
                "description": "Request body to send. Supports variable interpolation. Content format is determined by Content-Type header.",
                "additionalProperties": {
                  "type": "string"
                },
                "default": {}
              }
            },
            "required": [
              "url"
            ]
          },
          "resolved": {
            "type": "object",
            "properties": {
              "data_path": {
                "type": "string",
                "description": "Optional path to the metadata object in the response. If omitted, the metadata is assumed to be on the top level."
              },
              "dataPath": {
                "type": "string",
                "deprecated": true,
                "description": "Deprecated. Use `data_path` instead."
              },
              "error_message_path": {
                "type": "string",
                "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",
                "example": "error.message"
              }
            }
          },
          "use_static_ips": {
            "type": "boolean",
            "deprecated": true,
            "description": "Deprecated. Prefer `secure_proxy` instead.\nIf true, requests are made from a set of static IP addresses and only allow connections to a set of allowed IP addresses. Get in touch with us to add your IP addresses.\n",
            "default": false
          },
          "secure_proxy": {
            "$ref": "#/components/schemas/SecureProxyConfig"
          }
        },
        "required": [
          "type",
          "call"
        ]
      },
      "ExtensionHookCostDataRetrieval": {
        "description": "Hook that will allow using the specified source as data for consumption visualizations. This hook is triggered to fetch the data. Format of the request and response has to follow the following specification: TBD. The expected response to the call is:\n  - 200 with the time series data\n",
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "costDataRetrieval"
            ]
          },
          "auth": {
            "$ref": "#/components/schemas/ExtensionAuthBlock"
          },
          "call": {
            "type": "object",
            "properties": {
              "method": {
                "type": "string",
                "description": "HTTP method to use for the call",
                "default": "GET"
              },
              "url": {
                "type": "string",
                "description": "URL to call. Supports variable interpolation."
              },
              "params": {
                "type": "object",
                "description": "Parameters to append to the URL. Supports variable interpolation.",
                "additionalProperties": {
                  "type": "string"
                },
                "default": {}
              },
              "headers": {
                "type": "object",
                "description": "Headers to use. Supports variable interpolation.",
                "additionalProperties": {
                  "type": "string"
                },
                "default": {}
              },
              "body": {
                "type": "object",
                "description": "Request body to send. Supports variable interpolation. Content format is determined by Content-Type header.",
                "additionalProperties": {
                  "type": "string"
                },
                "default": {}
              }
            },
            "required": [
              "url"
            ]
          },
          "resolved": {
            "type": "object",
            "properties": {
              "data_path": {
                "type": "string",
                "description": "Optional path to the data (array) in the response. If omitted, the data is assumed to be on the top level."
              },
              "dataPath": {
                "type": "string",
                "deprecated": true,
                "description": "Deprecated. Use `data_path` instead."
              },
              "error_message_path": {
                "type": "string",
                "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",
                "example": "error.message"
              }
            }
          },
          "use_static_ips": {
            "type": "boolean",
            "deprecated": true,
            "description": "Deprecated. Prefer `secure_proxy` instead.\nIf true, requests are made from a set of static IP addresses and only allow connections to a set of allowed IP addresses. Get in touch with us to add your IP addresses.\n",
            "default": false
          },
          "secure_proxy": {
            "$ref": "#/components/schemas/SecureProxyConfig"
          }
        },
        "required": [
          "type",
          "call"
        ]
      },
      "ExtensionHookChangeEmail": {
        "description": "Hook that replaces the built-in change email functionality for portal users. When configured, the portal does not run its own change email flow. Instead, this hook makes an HTTP call to the third-party system, which is expected to handle the email change.\nThe `change_mode` controls what the portal does after the call:\n  - `asynchronous`: The third-party system takes the email change over entirely (most likely by sending the user instructions to confirm the new email address). The portal does not change the login email itself.\n  - `synchronous`: The third-party system applies the email change immediately. The portal waits for a successful (2xx) response and then also changes the portal user's login email right away, without sending a confirmation email. The user has to sign in again with the new email address afterwards.\n\nThe expected response http status code to the call is:\n  - 2xx if the request was accepted\n  - non-2xx if the request failed (optionally with a human-readable message resolved via `resolved.error_message_path`)\n",
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "changeEmail"
            ]
          },
          "change_mode": {
            "type": "string",
            "enum": [
              "synchronous",
              "asynchronous"
            ],
            "description": "Controls how the email change is handled once the third-party system accepted it. `asynchronous` hands the email change over entirely to the third-party system and the portal does not change the login email itself. `synchronous` waits for the third-party system to respond and then changes the portal user's login email immediately, without a confirmation email.\n",
            "default": "asynchronous"
          },
          "require_password_confirmation": {
            "type": "boolean",
            "description": "Whether the portal user must confirm their current password before the change email request is handed over to the third-party system. When true, the portal collects and verifies the current password before calling the hook. Skipped for portal users whose identity is managed by an identity provider: an SSO login has no password to confirm. When no password is confirmed and `change_mode` is `synchronous`, the re-created login gets a random password and the portal user signs in through their identity provider, or sets a new password via the email code flow.\n",
            "default": true
          },
          "explanation": {
            "type": "object",
            "properties": {
              "en": {
                "type": "string",
                "description": "Explanation of the functionality shown to the end user.",
                "example": "You will receive an email with instructions to confirm your new email address."
              }
            },
            "additionalProperties": {
              "type": "string",
              "description": "Explanation of the functionality in language denoted by ISO 3166-1 alpha-2 code."
            },
            "required": [
              "en"
            ],
            "description": "Optional explanation shown to the user in the change email confirmation dialog."
          },
          "auth": {
            "$ref": "#/components/schemas/ExtensionAuthBlock"
          },
          "call": {
            "type": "object",
            "properties": {
              "method": {
                "type": "string",
                "description": "HTTP method to use for the call",
                "default": "POST"
              },
              "url": {
                "type": "string",
                "description": "URL to call. Supports variable interpolation."
              },
              "params": {
                "type": "object",
                "description": "Parameters to append to the URL. Supports variable interpolation.",
                "additionalProperties": {
                  "type": "string"
                },
                "default": {}
              },
              "headers": {
                "type": "object",
                "description": "Headers to use. Supports variable interpolation.",
                "additionalProperties": {
                  "type": "string"
                },
                "default": {}
              },
              "body": {
                "type": "object",
                "description": "Optional JSON body to use for the call. Defaults to an object with the requested new email and portal user context. The requested new email is available as `{{Input.new_email}}` and the current account email as `{{Input.old_email}}`. Supports variable interpolation."
              }
            },
            "required": [
              "url",
              "headers"
            ]
          },
          "resolved": {
            "type": "object",
            "properties": {
              "error_message_path": {
                "type": "string",
                "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",
                "example": "error.message"
              }
            }
          },
          "secure_proxy": {
            "$ref": "#/components/schemas/SecureProxyConfig"
          }
        },
        "required": [
          "type",
          "call"
        ]
      },
      "ExtensionHookChangePassword": {
        "description": "Hook that replaces the built-in change password functionality for portal users. When configured, the portal does not change the user's password itself. Instead, this hook makes an HTTP call to the third-party system, which is expected to handle the password change (most likely by sending the user instructions to complete the process).\nThe expected response http status code to the call is:\n  - 2xx if the request was accepted\n  - non-2xx if the request failed (optionally with a human-readable message resolved via `resolved.error_message_path`)\n",
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "changePassword"
            ]
          },
          "require_new_password": {
            "type": "boolean",
            "description": "Whether the portal user must provide a new password. When false, the portal only asks the user to confirm (showing the configured explanation) and no new password is collected; the third-party system is expected to handle the password change. When true, the portal collects a new password and passes it to the third-party system as `{{Input.new_password}}`.\n",
            "default": false
          },
          "explanation": {
            "type": "object",
            "properties": {
              "en": {
                "type": "string",
                "description": "Explanation of the functionality shown to the end user.",
                "example": "You will receive an email with instructions to reset your password."
              }
            },
            "additionalProperties": {
              "type": "string",
              "description": "Explanation of the functionality in language denoted by ISO 3166-1 alpha-2 code."
            },
            "required": [
              "en"
            ],
            "description": "Optional explanation shown to the user in the change password confirmation dialog."
          },
          "auth": {
            "$ref": "#/components/schemas/ExtensionAuthBlock"
          },
          "call": {
            "type": "object",
            "properties": {
              "method": {
                "type": "string",
                "description": "HTTP method to use for the call",
                "default": "POST"
              },
              "url": {
                "type": "string",
                "description": "URL to call. Supports variable interpolation."
              },
              "params": {
                "type": "object",
                "description": "Parameters to append to the URL. Supports variable interpolation.",
                "additionalProperties": {
                  "type": "string"
                },
                "default": {}
              },
              "headers": {
                "type": "object",
                "description": "Headers to use. Supports variable interpolation.",
                "additionalProperties": {
                  "type": "string"
                },
                "default": {}
              },
              "body": {
                "type": "object",
                "description": "Optional JSON body to use for the call. Defaults to an object with portal user context (and the new password as `{{Input.new_password}}` when `require_new_password` is true). Supports variable interpolation."
              }
            },
            "required": [
              "url",
              "headers"
            ]
          },
          "resolved": {
            "type": "object",
            "properties": {
              "error_message_path": {
                "type": "string",
                "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",
                "example": "error.message"
              }
            }
          },
          "secure_proxy": {
            "$ref": "#/components/schemas/SecureProxyConfig"
          }
        },
        "required": [
          "type",
          "call"
        ]
      },
      "ExtensionHookDeleteAccount": {
        "description": "Hook that replaces the built-in delete account functionality for portal users. When configured, the portal does not delete the user itself. Instead, this hook makes an HTTP call to the third-party system, which is expected to handle the deletion.\nThe `deletion_mode` controls what the portal does after the call:\n  - `synchronous`: The third-party system deletes the user immediately. The portal waits for a successful (2xx) response and then also deletes the epilot user.\n  - `asynchronous`: The third-party system handles deletion out-of-band. The portal does not delete anything immediately; cleanup is expected to happen later (e.g. via the user deletion API or webhooks).\n\nThe optional `delete_contact` additionally deletes the contact related to the portal user, once the portal user itself was deleted (`synchronous` mode only).\n\nThe expected response http status code to the call is:\n  - 2xx if the request was accepted\n  - non-2xx if the request failed (optionally with a human-readable message resolved via `resolved.error_message_path`)\n",
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "deleteAccount"
            ]
          },
          "deletion_mode": {
            "type": "string",
            "enum": [
              "synchronous",
              "asynchronous"
            ],
            "description": "Controls how the account deletion is handled. `synchronous` waits for the third-party system to respond and then also deletes the epilot user. `asynchronous` hands the deletion over entirely to the third-party system and the portal does not delete anything immediately.\n",
            "default": "synchronous"
          },
          "delete_contact": {
            "type": "string",
            "enum": [
              "none",
              "soft",
              "hard"
            ],
            "description": "Opt-in deletion of the contact related to the portal user, applied after the portal user itself was deleted. `none` (default) leaves the contact untouched. `soft` deletes the contact, so it can still be restored from the trash. `hard` permanently purges the contact. Only applied in `synchronous` deletion mode, as `asynchronous` mode hands the cleanup over to the third-party system.\n",
            "default": "none"
          },
          "explanation": {
            "type": "object",
            "properties": {
              "en": {
                "type": "string",
                "description": "Explanation of the functionality shown to the end user.",
                "example": "Your account deletion will be processed by our system. This may take a few days."
              }
            },
            "additionalProperties": {
              "type": "string",
              "description": "Explanation of the functionality in language denoted by ISO 3166-1 alpha-2 code."
            },
            "required": [
              "en"
            ],
            "description": "Optional explanation shown to the user in the delete account confirmation dialog."
          },
          "auth": {
            "$ref": "#/components/schemas/ExtensionAuthBlock"
          },
          "call": {
            "type": "object",
            "properties": {
              "method": {
                "type": "string",
                "description": "HTTP method to use for the call",
                "default": "POST"
              },
              "url": {
                "type": "string",
                "description": "URL to call. Supports variable interpolation."
              },
              "params": {
                "type": "object",
                "description": "Parameters to append to the URL. Supports variable interpolation.",
                "additionalProperties": {
                  "type": "string"
                },
                "default": {}
              },
              "headers": {
                "type": "object",
                "description": "Headers to use. Supports variable interpolation.",
                "additionalProperties": {
                  "type": "string"
                },
                "default": {}
              },
              "body": {
                "type": "object",
                "description": "Optional JSON body to use for the call. Defaults to an object with portal user context, e.g. `{\"portal_user_id\": \"...\", \"email\": \"...\"}`. Supports variable interpolation."
              }
            },
            "required": [
              "url",
              "headers"
            ]
          },
          "resolved": {
            "type": "object",
            "properties": {
              "error_message_path": {
                "type": "string",
                "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",
                "example": "error.message"
              }
            }
          },
          "secure_proxy": {
            "$ref": "#/components/schemas/SecureProxyConfig"
          }
        },
        "required": [
          "type",
          "call"
        ]
      },
      "SecureProxyConfig": {
        "type": "object",
        "description": "Configuration for routing requests through the ERP Integration secure proxy. Mutually exclusive with use_static_ips.",
        "properties": {
          "integration_id": {
            "type": "string",
            "format": "uuid",
            "description": "Integration ID that owns the secure_proxy use case."
          },
          "use_case_slug": {
            "type": "string",
            "description": "Use case slug for the secure proxy use case."
          }
        },
        "required": [
          "integration_id",
          "use_case_slug"
        ]
      },
      "ExtensionAuthBlock": {
        "type": "object",
        "properties": {
          "method": {
            "type": "string",
            "description": "HTTP method to use for authentication",
            "default": "GET"
          },
          "url": {
            "type": "string",
            "description": "URL to use for authentication. Supports variable interpolation."
          },
          "params": {
            "type": "object",
            "description": "Parameters to append to the URL. Supports variable interpolation.",
            "additionalProperties": {
              "type": "string"
            },
            "default": {}
          },
          "headers": {
            "type": "object",
            "description": "Headers to use for authentication. Supports variable interpolation.",
            "additionalProperties": {
              "type": "string"
            },
            "default": {}
          },
          "body": {
            "type": "object",
            "description": "JSON body to use for authentication. Supports variable interpolation. Content format is determined by Content-Type header.",
            "additionalProperties": {
              "type": "string"
            },
            "default": {}
          },
          "cache": {
            "type": "object",
            "properties": {
              "key": {
                "type": "string",
                "description": "Key to use to identify the auth response. Supports interpolation.",
                "example": "{{Options.api_key}}"
              },
              "ttl": {
                "type": "string",
                "description": "Time to live in seconds for the cache. Supports interpolation.",
                "example": "{{AuthResponse.data.expires_in}}"
              }
            },
            "required": [
              "key",
              "ttl"
            ]
          }
        },
        "required": [
          "url"
        ]
      },
      "Direction": {
        "type": "string",
        "enum": [
          "feed-in",
          "feed-out"
        ]
      },
      "TariffType": {
        "type": "string",
        "enum": [
          "ht",
          "nt"
        ]
      },
      "Source": {
        "type": "string",
        "enum": [
          "ECP",
          "ERP",
          "360",
          "journey-submission"
        ]
      },
      "Reason": {
        "type": "string",
        "nullable": true,
        "example": "Storing the feed-in record",
        "description": "The reason for recording the reading\nIf no reason is specified or left empty, the Epilot UI will show 'Regular' as the default display text\n",
        "enum": [
          "",
          "regular",
          "irregular",
          "last",
          "first",
          "meter_change",
          "contract_change",
          "meter_adjustment"
        ]
      },
      "ReadBy": {
        "type": "string",
        "nullable": true,
        "example": "John Doe",
        "description": "The person who recorded the reading"
      },
      "ReadingStatus": {
        "type": "string",
        "nullable": true,
        "enum": [
          "valid",
          "in-validation",
          "implausible",
          null,
          ""
        ]
      },
      "MeterReading": {
        "type": "object",
        "required": [
          "value",
          "meter_id",
          "source"
        ],
        "properties": {
          "value": {
            "type": "number",
            "example": 240,
            "description": "The reading value of the meter"
          },
          "read_by": {
            "$ref": "#/components/schemas/ReadBy"
          },
          "reason": {
            "$ref": "#/components/schemas/Reason"
          },
          "meter_id": {
            "$ref": "#/components/schemas/EntityId",
            "description": "The ID of the associated meter"
          },
          "counter_id": {
            "$ref": "#/components/schemas/EntityId",
            "description": "The ID of the associated meter counter"
          },
          "direction": {
            "$ref": "#/components/schemas/Direction",
            "description": "The direction of the reading (feed-in or feed-out)"
          },
          "timestamp": {
            "type": "string",
            "description": "If the value is not provided, the system will be set with the time the request is processed.",
            "example": "2022-10-10"
          },
          "source": {
            "$ref": "#/components/schemas/Source",
            "description": "The source of the reading"
          },
          "status": {
            "$ref": "#/components/schemas/ReadingStatus",
            "description": "The status of the reading"
          },
          "external_id": {
            "type": "string",
            "description": "The external ID of the reading"
          },
          "remark": {
            "type": "string",
            "nullable": true,
            "description": "A remark or comment for the reading",
            "example": "Customer reported unusual consumption"
          },
          "metadata": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Additional metadata for the reading",
            "example": {
              "registration_id": "1234567890",
              "business_unit": "ABC"
            }
          }
        }
      },
      "MeterReadingPhoto": {
        "type": "object",
        "required": [
          "filename",
          "mime_type",
          "contents",
          "meter_id"
        ],
        "properties": {
          "filename": {
            "type": "string",
            "example": "Reading 10.01.2025.jpg"
          },
          "mime_type": {
            "type": "string",
            "example": "image/jpeg"
          },
          "contents": {
            "type": "string",
            "description": "Base64 encoded image",
            "example": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBxMTEhUTExMWFhUXGBgYGBgYGBgYGBgYGBgYFxgYFxgYHSggGBolHRgXITEhJSkrLi4uFx8zODMtNygtLisBCgoKDg0OGhAQGy0lHyUt"
          },
          "meter_id": {
            "$ref": "#/components/schemas/EntityId",
            "description": "The ID of the associated Meter"
          }
        }
      },
      "MeterReadingPhotoData": {
        "type": "object",
        "required": [
          "filename",
          "s3ref"
        ],
        "properties": {
          "filename": {
            "type": "string",
            "example": "Reading 10.01.2025.jpg"
          },
          "s3ref": {
            "type": "object",
            "required": [
              "bucket",
              "key"
            ],
            "properties": {
              "bucket": {
                "type": "string",
                "example": "meter-readings",
                "description": "S3 bucket name"
              },
              "key": {
                "type": "string",
                "example": "uuid/reading-10.01.2025.jpg",
                "description": "S3 key"
              }
            }
          },
          "reading": {
            "type": "string",
            "description": "Recognized meter reading.",
            "example": "000123.45"
          },
          "sector": {
            "type": "string",
            "description": "Recognized sector of the meter.",
            "example": "water"
          },
          "meter_numbers": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Recognized list of (potential) meter numbers.",
            "example": [
              "00123456"
            ]
          },
          "file_id": {
            "type": "string",
            "description": "ID of the created file entity for the uploaded photo.",
            "example": "abc123def456"
          }
        }
      },
      "SSOLoginToken": {
        "type": "string"
      },
      "ProviderSlug": {
        "type": "string",
        "example": "office-365-login",
        "description": "URL-friendly slug to use as organization-unique identifier for Provider",
        "pattern": "[0-9a-z_-]+"
      },
      "ProviderDisplayName": {
        "type": "string",
        "example": "Office 365 Login",
        "description": "Human-readable display name for identity provider shown in login"
      },
      "ProviderConfig": {
        "type": "object",
        "description": "SSO identity provider configuration.\n\nEnv var interpolation: only string fields under `oidc_config` and\n`mobile_oidc_config` (incl. their nested `metadata`) are passed through\nLiquid templating, so they may contain `{{ env.VAR }}` placeholders that\nget resolved at runtime against the organization's environment.\n\nThe following fields are used as literal values and MUST NOT contain\ntemplate syntax: `slug`, `display_name`, `provider_type`, all keys and\nvalues under `attribute_mappings` (used as JSONPath-like accessors into\ntoken claims), and all keys and values under `entity_matching`.\n",
        "properties": {
          "slug": {
            "$ref": "#/components/schemas/ProviderSlug"
          },
          "display_name": {
            "$ref": "#/components/schemas/ProviderDisplayName"
          },
          "provider_type": {
            "type": "string",
            "enum": [
              "OIDC"
            ]
          },
          "attribute_mappings": {
            "$ref": "#/components/schemas/AttributeMappingConfig"
          },
          "entity_matching": {
            "$ref": "#/components/schemas/EntityMatchingConfig"
          },
          "oidc_config": {
            "$ref": "#/components/schemas/OIDCProviderConfig"
          },
          "mobile_oidc_config": {
            "$ref": "#/components/schemas/MoblieOIDCConfig"
          },
          "expose_client_secret": {
            "type": "boolean",
            "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",
            "example": false
          }
        },
        "required": [
          "display_name",
          "provider_type"
        ]
      },
      "ProviderPublicConfig": {
        "type": "object",
        "properties": {
          "slug": {
            "$ref": "#/components/schemas/ProviderSlug"
          },
          "display_name": {
            "$ref": "#/components/schemas/ProviderDisplayName"
          },
          "oidc_config": {
            "$ref": "#/components/schemas/OIDCProviderConfig"
          },
          "mobile_oidc_config": {
            "$ref": "#/components/schemas/MoblieOIDCConfig"
          }
        },
        "required": [
          "slug",
          "display_name"
        ]
      },
      "ProviderPublicConfigV3": {
        "type": "object",
        "description": "Web-only public provider configuration served by `getPublicSSOProviderV3`.\nUnlike `ProviderPublicConfig` there is no `mobile_oidc_config`, and\n`oidc_config` never carries `client_secret` or the `metadata.test_auth_*`\ncredentials — `oidc_config.has_client_secret` signals their presence so\nclients route the token exchange through the backend SSO callback.\n",
        "properties": {
          "slug": {
            "$ref": "#/components/schemas/ProviderSlug"
          },
          "display_name": {
            "$ref": "#/components/schemas/ProviderDisplayName"
          },
          "oidc_config": {
            "$ref": "#/components/schemas/OIDCProviderConfig"
          }
        },
        "required": [
          "slug",
          "display_name"
        ]
      },
      "PublicIdentityProvider": {
        "type": "object",
        "description": "Minimal public identity provider info — enough to render a provider login button.",
        "properties": {
          "slug": {
            "$ref": "#/components/schemas/ProviderSlug"
          },
          "display_name": {
            "$ref": "#/components/schemas/ProviderDisplayName"
          }
        },
        "required": [
          "slug",
          "display_name"
        ]
      },
      "AttributeMappingConfig": {
        "type": "object",
        "description": "Dictionary of epilot user attributes to claims",
        "properties": {
          "contact": {
            "additionalProperties": true,
            "type": "object",
            "required": [
              "email"
            ],
            "properties": {
              "email": {
                "type": "string",
                "example": "email"
              },
              "first_name": {
                "type": "string",
                "example": "first_name"
              },
              "last_name": {
                "type": "string",
                "example": "last_name"
              },
              "phone": {
                "type": "string",
                "example": "tel"
              },
              "preferred_language": {
                "type": "string",
                "example": "language"
              },
              "concession": {
                "type": "string",
                "example": "GPE",
                "description": "The concession of the user. For eg: GPE, EPE, WPE"
              },
              "installer_number": {
                "type": "string",
                "example": 123456,
                "description": "The installer number of the user"
              }
            }
          },
          "account": {
            "additionalProperties": true,
            "type": "object",
            "properties": {
              "name": {
                "type": "string",
                "example": "company_name"
              },
              "street": {
                "type": "string",
                "example": "street_name"
              },
              "house_number": {
                "type": "string",
                "example": "house_number"
              },
              "postal_code": {
                "type": "string",
                "example": "postal_code"
              },
              "city": {
                "type": "string",
                "example": "city"
              }
            }
          },
          "portal_user": {
            "additionalProperties": true,
            "type": "object",
            "properties": {
              "access_status": {
                "type": "string",
                "example": "access_status"
              },
              "expires_at": {
                "type": "string",
                "example": "expires_at"
              }
            }
          },
          "concession_attributes": {
            "type": "object",
            "properties": {
              "water": {
                "type": "string",
                "example": "water"
              },
              "energy": {
                "type": "string",
                "example": "energy"
              },
              "gas": {
                "type": "string",
                "example": "gas"
              }
            }
          }
        },
        "required": [
          "contact"
        ]
      },
      "EntityMatchingConfig": {
        "type": "object",
        "description": "Configuration for matching existing entities during SSO login using token claims",
        "properties": {
          "portal_user": {
            "type": "object",
            "description": "Mapping of portal_user attributes to token claim paths. All configured fields must match (AND logic).",
            "additionalProperties": {
              "type": "string"
            },
            "example": {
              "external_id": "sub"
            }
          },
          "contact": {
            "type": "object",
            "description": "Mapping of contact attributes to token claim paths. All configured fields must match (AND logic).",
            "additionalProperties": {
              "type": "string"
            },
            "example": {
              "customer_number": "customer_id",
              "external_id": "sub"
            }
          },
          "auto_create_cognito_user": {
            "type": "boolean",
            "description": "Whether to automatically create Cognito user when matched portal_user has no authentication data. Defaults to true.",
            "default": true
          }
        }
      },
      "OIDCProviderConfig": {
        "type": "object",
        "description": "OIDC provider configuration. Values are resolved at SSO invocation time\n(login / callback), so the fields below may reference org env vars via\nmustache-like templates, e.g. `{{ env.MY_PROVIDER_CLIENT_SECRET }}`.\n\nFields used to render the SSO buttons up-front (`ProviderConfig.slug`,\n`ProviderConfig.display_name`) are NOT interpolated and must be literal.\n",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "authorization_code",
              "implicit"
            ],
            "default": "implicit"
          },
          "oidc_issuer": {
            "type": "string",
            "description": "Issuing Authority URL. Supports env var interpolation, e.g. `{{ env.MY_ISSUER }}`.",
            "example": "https://login.microsoftonline.com/33d4f3e5-3df2-421e-b92e-a63cfa680a88/v2.0"
          },
          "redirect_uri": {
            "type": "string",
            "description": "Redirect URI for the OIDC flow. Supports env var interpolation.",
            "example": "https://customer-portal.com/login"
          },
          "client_id": {
            "type": "string",
            "description": "Supports env var interpolation, e.g. `{{ env.MY_CLIENT_ID }}`.",
            "example": "ab81daf8-8b1f-42d6-94ca-c51621054c75"
          },
          "client_secret": {
            "type": "string",
            "description": "Client Secret. Supports env var interpolation (incl. secrets), e.g.\n`{{ env.MY_CLIENT_SECRET }}`. Prefer storing the actual secret as an org\nenv var and referencing it here.\n",
            "example": "7BIUnn~6shh.7fNtXb..3k1Mp3s6k6WK3B"
          },
          "has_client_secret": {
            "type": "boolean",
            "description": "Whether the client secret is present",
            "example": true
          },
          "scope": {
            "type": "string",
            "description": "Space-separated list of OAuth 2.0 scopes to request from OpenID Connect",
            "example": "openid email"
          },
          "metadata": {
            "$ref": "#/components/schemas/OIDCProviderMetadata"
          },
          "prompt": {
            "type": "string",
            "enum": [
              "login",
              "select_account",
              "consent",
              "signup"
            ]
          }
        },
        "required": [
          "oidc_issuer",
          "client_id",
          "scope"
        ]
      },
      "MoblieOIDCConfig": {
        "type": "object",
        "description": "Mobile OIDC configuration. Values are resolved at SSO invocation time, so the\nfields below may reference org env vars via mustache-like templates, e.g.\n`{{ env.MOBILE_CLIENT_SECRET }}`.\n",
        "properties": {
          "client_id": {
            "type": "string",
            "description": "Client ID for the mobile app. Supports env var interpolation.",
            "example": 123456
          },
          "client_secret": {
            "type": "string",
            "description": "Client Secret for the mobile app. Supports env var interpolation (incl. secrets),\ne.g. `{{ env.MOBILE_CLIENT_SECRET }}`.\n",
            "example": 123456
          }
        }
      },
      "OIDCProviderMetadata": {
        "type": "object",
        "properties": {
          "response_modes_supported": {
            "type": "array",
            "description": "Response modes the provider accepts (e.g. `form_post` for Apple)",
            "items": {
              "type": "string",
              "enum": [
                "form_post",
                "fragment",
                "query"
              ]
            }
          },
          "authorization_endpoint": {
            "type": "string",
            "description": "URL of the authorization endpoint",
            "example": "https://www.facebook.com/v12.0/dialog/oauth"
          },
          "token_endpoint": {
            "type": "string",
            "description": "URL of the token endpoint",
            "example": "https://graph.facebook.com/v12.0/oauth/access_token"
          },
          "userinfo_endpoint": {
            "type": "string",
            "description": "URL of the userinfo endpoint",
            "example": "https://graph.facebook.com/me"
          },
          "logout_uri": {
            "type": "string",
            "description": "URL of the logout/end session endpoint",
            "example": "https://login.microsoftonline.com/common/oauth2/v2.0/logout"
          },
          "logout_redirect_uri": {
            "type": "string",
            "description": "URL to redirect to after logout completes",
            "example": "https://customer-portal.com/login"
          },
          "skip_login_as_logout": {
            "type": "boolean",
            "description": "When true, skip SSO logout redirect during \"login as\" flow. Use this for providers that cannot redirect back after logout and would break log in as.",
            "default": false
          },
          "mobile_redirect_uri": {
            "type": "string",
            "description": "URL of the mobile redirect URI",
            "example": "msauth.io.epilot.ecp://auth"
          },
          "test_auth_username": {
            "type": "string",
            "description": "The username for the test auth, only used for testing on auth code flow.\nSupports env var interpolation, e.g. `{{ env.MY_TEST_AUTH_USERNAME }}`.\n",
            "example": "test@epilot.io"
          },
          "test_auth_password": {
            "type": "string",
            "description": "The password for the test auth, only used for testing on auth code flow.\nSupports env var interpolation (incl. secrets), e.g. `{{ env.MY_TEST_AUTH_PASSWORD }}`.\n"
          }
        }
      },
      "SAMLProviderConfig": {
        "type": "object"
      },
      "SSOCallbackRequest": {
        "type": "object",
        "required": [
          "token_endpoint",
          "grant_type",
          "code",
          "client_id",
          "code_verifier"
        ],
        "properties": {
          "provider_slug": {
            "$ref": "#/components/schemas/ProviderSlug"
          },
          "token_endpoint": {
            "type": "string",
            "deprecated": true,
            "description": "Deprecated and ignored; the token endpoint is derived server-side from the provider config.",
            "example": "https://www.facebook.com/v12.0/dialog/oauth"
          },
          "grant_type": {
            "type": "string",
            "description": "The grant type",
            "example": "authorization_code"
          },
          "code": {
            "type": "string",
            "description": "The code received from the external SSO provider",
            "example": "123456"
          },
          "redirect_uri": {
            "type": "string",
            "description": "The redirect uri used in the authorization request. Optional: when omitted it is\nresolved server-side from the provider's configured `redirect_uri`, falling back to\nthe portal's own callback route (`https://<domain>/sso`).\n",
            "example": "https://customer-portal.com/sso"
          },
          "client_id": {
            "type": "string",
            "description": "The client id",
            "example": "123456"
          },
          "code_verifier": {
            "type": "string",
            "description": "The code verifier",
            "example": "123456"
          }
        }
      },
      "SSOCallbackResponse": {
        "type": "object",
        "required": [
          "access_token",
          "token_type",
          "expires_in",
          "refresh_token"
        ],
        "properties": {
          "access_token": {
            "type": "string",
            "description": "The access token",
            "example": "123456"
          },
          "token_type": {
            "type": "string",
            "description": "The token type",
            "example": "Bearer"
          },
          "expires_in": {
            "type": "number",
            "description": "The expires in",
            "example": 3600
          },
          "refresh_token": {
            "type": "string",
            "description": "The refresh token",
            "example": "123456"
          },
          "id_token": {
            "type": "string",
            "description": "The id token",
            "example": "123456"
          },
          "scope": {
            "type": "string",
            "description": "The scope of the access token",
            "example": "openid email"
          }
        }
      },
      "BlockProps": {
        "type": "object",
        "additionalProperties": true,
        "properties": {
          "visibility": {
            "type": "object",
            "description": "The conditions that need to be met for the block to be shown",
            "additionalProperties": true,
            "properties": {
              "is_hidden": {
                "type": "boolean",
                "description": "Block is fully hidden from portal users"
              },
              "hidden_on_mobile": {
                "type": "boolean",
                "description": "Block is hidden on mobile viewports"
              },
              "hidden_on_desktop": {
                "type": "boolean",
                "description": "Block is hidden on desktop viewports"
              },
              "hidden_in_app": {
                "type": "boolean",
                "description": "Block is hidden in the mobile app"
              },
              "start_date": {
                "type": "string",
                "format": "date-time",
                "description": "Schedule visibility start date. If only start_date is set, block is visible from this date onwards."
              },
              "end_date": {
                "type": "string",
                "format": "date-time",
                "description": "Schedule visibility end date. If only end_date is set, block is visible until this date."
              },
              "visible_for_targets": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Target IDs that must match for the block to be visible"
              },
              "visible_for_operator": {
                "type": "string",
                "enum": [
                  "and",
                  "or"
                ],
                "description": "Operator for visible_for_targets matching"
              },
              "hidden_for_targets": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Target IDs that if matched will hide the block"
              },
              "hidden_for_operator": {
                "type": "string",
                "enum": [
                  "and",
                  "or"
                ],
                "description": "Operator for hidden_for_targets matching"
              }
            }
          },
          "content": {
            "type": "object",
            "description": "The content of the block",
            "additionalProperties": true
          },
          "design": {
            "type": "object",
            "description": "The design of the block",
            "additionalProperties": true
          }
        }
      },
      "BlockType": {
        "type": "string",
        "description": "The type of the block. eg; tabs, tab, group, attribute",
        "example": "tab"
      },
      "BlockRequest": {
        "type": "object",
        "additionalProperties": true,
        "required": [
          "type",
          "order"
        ],
        "properties": {
          "props": {
            "$ref": "#/components/schemas/BlockProps"
          },
          "parentId": {
            "type": "string",
            "description": "The id of the parent block",
            "example": "c495fef9-eeca-4019-a989-8390dcd9825b"
          },
          "type": {
            "$ref": "#/components/schemas/BlockType"
          },
          "order": {
            "type": "number",
            "description": "The order of the block",
            "example": 1
          }
        }
      },
      "BlockId": {
        "type": "string",
        "description": "The id of the block",
        "format": "uuid",
        "example": "c495fef9-eeca-4019-a989-8390dcd9825b"
      },
      "Block": {
        "allOf": [
          {
            "$ref": "#/components/schemas/BlockRequest"
          },
          {
            "type": "object",
            "additionalProperties": true,
            "properties": {
              "id": {
                "type": "string",
                "description": "The id of the block",
                "format": "uuid",
                "example": "c495fef9-eeca-4019-a989-8390dcd9825b"
              }
            }
          }
        ]
      },
      "PageRequest": {
        "type": "object",
        "additionalProperties": true,
        "required": [
          "slug",
          "order"
        ],
        "properties": {
          "slug": {
            "type": "string",
            "description": "The slug of the page",
            "example": "dashboard"
          },
          "path": {
            "type": "string",
            "deprecated": true,
            "description": "The path of the page",
            "example": "/dashboard"
          },
          "schema": {
            "type": "array",
            "items": {
              "type": "string",
              "description": "The schema of the page"
            }
          },
          "visibility": {
            "type": "object",
            "description": "The conditions that need to be met for the page to be shown",
            "additionalProperties": true,
            "properties": {
              "is_hidden": {
                "type": "boolean",
                "description": "Page is fully hidden from portal users"
              },
              "hidden_in_app": {
                "type": "boolean",
                "description": "Page is hidden in the mobile app"
              },
              "start_date": {
                "type": "string",
                "format": "date-time",
                "description": "Schedule visibility start date. If only start_date is set, page is visible from this date onwards."
              },
              "end_date": {
                "type": "string",
                "format": "date-time",
                "description": "Schedule visibility end date. If only end_date is set, page is visible until this date."
              },
              "visible_for_targets": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Target IDs that must match for the page to be visible"
              },
              "visible_for_operator": {
                "type": "string",
                "enum": [
                  "and",
                  "or"
                ],
                "description": "Operator for visible_for_targets matching"
              },
              "hidden_for_targets": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Target IDs that if matched will hide the page"
              },
              "hidden_for_operator": {
                "type": "string",
                "enum": [
                  "and",
                  "or"
                ],
                "description": "Operator for hidden_for_targets matching"
              }
            }
          },
          "content": {
            "type": "object",
            "description": "The content of the page",
            "additionalProperties": true
          },
          "design": {
            "type": "object",
            "description": "The design of the page",
            "additionalProperties": true
          },
          "blocks": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/Block"
            }
          },
          "order": {
            "type": "number",
            "description": "The order of the block",
            "example": 1
          },
          "is_system": {
            "type": "boolean",
            "description": "Whether the page is a system page",
            "example": false
          },
          "is_detail": {
            "type": "boolean",
            "description": "Whether the page is a detail page",
            "example": false
          },
          "detail_schema": {
            "type": "string",
            "description": "The schema of the detail page",
            "example": "contact"
          },
          "show_in_navigation": {
            "type": "boolean",
            "description": "Whether the detail page should appear in the main navigation",
            "example": false
          },
          "is_public": {
            "type": "boolean",
            "description": "Whether the page is public",
            "example": true
          },
          "parentId": {
            "type": "string",
            "nullable": true,
            "description": "The id of the parent page",
            "example": "c495fef9-eeca-4019-a989-8390dcd9825b"
          },
          "is_entry_route": {
            "type": "boolean",
            "description": "Whether the page is the entry route",
            "example": false
          },
          "is_deleted": {
            "type": "boolean",
            "description": "Send the flag as true to delete the page",
            "example": false
          }
        }
      },
      "Page": {
        "type": "object",
        "allOf": [
          {
            "$ref": "#/components/schemas/PageRequest"
          },
          {
            "type": "object",
            "additionalProperties": true,
            "properties": {
              "id": {
                "type": "string",
                "description": "The id of the page",
                "format": "uuid",
                "example": "c495fef9-eeca-4019-a989-8390dcd9825b"
              },
              "last_modified_at": {
                "type": "string",
                "format": "date-time",
                "description": "Last modified timestamp of the Page",
                "example": "2021-02-09T12:41:43.662Z"
              },
              "portal_id": {
                "type": "string",
                "description": "The id of the portal",
                "example": "453ad7bf-86d5-46c8-8252-bcc868df5e3c"
              },
              "past_routes": {
                "type": "array",
                "description": "Slugs that previously belonged to this page. The portal redirects requests for these slugs to the current slug. Managed by the server: when a page's slug changes, the old slug is appended here, and when another page claims one of these slugs it is removed from this list.\n",
                "items": {
                  "type": "string"
                },
                "example": [
                  "old-dashboard",
                  "home"
                ]
              }
            }
          }
        ]
      },
      "CommonConfigAttributesV3": {
        "type": "object",
        "properties": {
          "mobile_config": {
            "description": "Mobile app configuration (top-level; moved out of the config blob).",
            "$ref": "#/components/schemas/MobileConfig"
          },
          "enabled": {
            "type": "boolean",
            "description": "Enable/Disable the portal access"
          },
          "name": {
            "type": "string",
            "description": "A short name to identify your portal",
            "example": "Installer Portal"
          },
          "domain": {
            "type": "string",
            "description": "The URL on which the portal is accessible",
            "example": "abc.com"
          },
          "is_epilot_domain": {
            "type": "boolean",
            "description": "Mark true if the domain is an Epilot domain"
          },
          "epilot_domain": {
            "type": "string",
            "description": "The Epilot domain on which the portal is accessible",
            "example": "example-portal-1.ecp.epilot.io"
          },
          "domain_settings": {
            "$ref": "#/components/schemas/DomainSettings"
          },
          "design_id": {
            "$ref": "#/components/schemas/EntityId",
            "description": "ID of the design used to build the portal"
          },
          "allowed_portal_entities": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Allowed portal entities for the portal",
            "example": [
              "contact",
              "contract"
            ]
          },
          "self_registration_setting": {
            "type": "string",
            "enum": [
              "ALLOW_WITH_CONTACT_CREATION",
              "ALLOW_WITHOUT_CONTACT_CREATION",
              "DENY",
              "ALWAYS_CREATE_CONTACT",
              "DISALLOW_COMPLETELY",
              "BLOCK_IF_PORTAL_USER_EXISTS"
            ]
          },
          "self_registration_account_setting": {
            "type": "string",
            "description": "Controls behavior of self-registration when account is the registration\nentity. `BLOCK_IF_PORTAL_USER_EXISTS` matches an existing account and\nrejects the request when any portal user is already linked to that\naccount (no creation). Blocking can also be enabled on the other\nnon-create modes via `block_registration_if_portal_user_exists`.\n",
            "enum": [
              "ALLOW_WITH_CREATION",
              "DENY",
              "ALWAYS_CREATE",
              "BLOCK_IF_PORTAL_USER_EXISTS",
              "DISALLOW_COMPLETELY"
            ]
          },
          "block_registration_if_portal_user_exists": {
            "type": "boolean",
            "description": "Account-mode only. Reject registration when the resolved account already\nhas any portal user (any portal user whose mapped contact is linked to\nthe account).\n"
          },
          "self_registration_entity": {
            "type": "string",
            "description": "Entity type used as the primary identifier for self-registration",
            "enum": [
              "contact",
              "account"
            ]
          },
          "user_account_self_management": {
            "type": "boolean",
            "description": "Enable or disable user account self management",
            "example": false
          },
          "feature_settings": {
            "type": "object",
            "description": "Feature settings for the portal",
            "properties": {
              "start_page": {
                "type": "boolean",
                "description": "Start page feature flag"
              },
              "billing": {
                "type": "boolean",
                "description": "Billing feature flag"
              },
              "change_due_date": {
                "type": "boolean",
                "description": "Change due date feature flag"
              },
              "new_design": {
                "type": "boolean",
                "description": "Enable or disable the new design for the portal"
              },
              "mcp_enabled": {
                "type": "boolean",
                "description": "Enable the MCP (AI agent) connector channel for this portal"
              },
              "mcp_grant_version": {
                "type": "integer",
                "minimum": 0,
                "default": 0,
                "readOnly": true,
                "description": "Server-managed generation used to invalidate MCP grants after the connector is disabled or re-enabled"
              }
            }
          },
          "accessToken": {
            "type": "string",
            "description": "Access token for the portal"
          },
          "advanced_mfa": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "Advanced MFA feature flag"
              }
            }
          },
          "auth_settings": {
            "type": "object",
            "description": "Authentication settings for the portal",
            "properties": {
              "passwordless_login": {
                "type": "object",
                "properties": {
                  "enabled": {
                    "type": "boolean",
                    "description": "Passwordless login feature flag"
                  }
                }
              },
              "entry_point": {
                "type": "string",
                "enum": [
                  "PASSWORD",
                  "SSO"
                ]
              },
              "preferred_sso_providers": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ProviderSlug"
                }
              },
              "auto_redirect_to_sso": {
                "type": "boolean",
                "description": "Decide whether to automatically redirect to the provider page during login, which would completely bypass showing the portal authentication page."
              },
              "prevent_user_enumeration": {
                "type": "boolean",
                "description": "Opt-in. When true, suppresses responses that reveal whether a user exists for public, pre-authentication actions (the login entry-point check and self-registration), at the expense of some UX. Already-authenticated actions are unaffected. Default false.\n"
              }
            }
          },
          "cognito_details": {
            "type": "object",
            "description": "AWS Cognito Pool details for the portal",
            "properties": {
              "cognito_user_pool_client_id": {
                "type": "string",
                "example": "6bsd0jkgoie74k2i8mrhc1vest",
                "description": "Cognito user pool client ID"
              },
              "cognito_user_pool_arn": {
                "type": "string",
                "example": "arn:aws:cognito-idp:us-east-1:123412341234:userpool/us-east-1_123412341",
                "description": "Cognito user pool ARN"
              },
              "cognito_user_pool_id": {
                "type": "string",
                "example": "eu-central-1_CUEQRNbUb",
                "description": "Cognito user pool ID"
              },
              "timeouts": {
                "type": "object",
                "description": "Timeouts for the cognito tokens",
                "properties": {
                  "refresh_token": {
                    "type": "integer",
                    "example": 300,
                    "description": "Timeout for the refresh token"
                  },
                  "access_token": {
                    "type": "integer",
                    "example": 300,
                    "description": "Timeout for the access token"
                  },
                  "id_token": {
                    "type": "integer",
                    "example": 300,
                    "description": "Timeout for the id token"
                  }
                }
              },
              "advanced_authentication": {
                "type": "object",
                "description": "Advanced authentication settings for the portal",
                "properties": {
                  "user_activity_logging": {
                    "type": "boolean",
                    "example": true,
                    "description": "Enables detailed logging of user authentication attempts including risk assessments, IP addresses, user agents, and device information. These logs can be used for security analysis and monitoring."
                  },
                  "adaptive_authentication": {
                    "type": "boolean",
                    "example": true,
                    "description": "Automatically assesses risk for every authentication session. Based on risk ratings, can block authentication or require MFA for suspicious sign-in attempts. Helps protect user accounts from potential attacks by adapting security measures in real-time."
                  },
                  "compromised_credentials_detection": {
                    "type": "boolean",
                    "example": true,
                    "description": "Checks passwords against databases of leaked and commonly-guessed passwords during sign-up, sign-in, and password reset. Blocks or warns users when insecure passwords are detected, preventing unauthorized access from compromised credentials."
                  }
                }
              },
              "password_policy": {
                "type": "object",
                "description": "Password policy for the portal",
                "properties": {
                  "minimum_length": {
                    "type": "integer",
                    "example": 8,
                    "description": "Minimum password length"
                  },
                  "maximum_length": {
                    "type": "integer",
                    "example": 256,
                    "description": "Maximum password length"
                  },
                  "require_lowercase": {
                    "type": "boolean",
                    "example": true,
                    "description": "Require lowercase characters"
                  },
                  "require_uppercase": {
                    "type": "boolean",
                    "example": true,
                    "description": "Require uppercase characters"
                  },
                  "require_numbers": {
                    "type": "boolean",
                    "example": true,
                    "description": "Require numbers"
                  },
                  "require_symbols": {
                    "type": "boolean",
                    "example": true,
                    "description": "Require symbols"
                  },
                  "password_history_size": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 24,
                    "example": 3,
                    "description": "Number of previous passwords a user is prevented from reusing. Set to 0 to disable reuse prevention. Maps to Cognito's PasswordHistorySize and requires the user pool to be on the Essentials or Plus feature plan."
                  }
                }
              }
            }
          },
          "config": {
            "type": "string",
            "description": "Stringified object with configuration details"
          },
          "contact_identifiers": {
            "type": "array",
            "description": "Deprecated. Use registration_identifiers instead.",
            "deprecated": true,
            "items": {
              "type": "string"
            },
            "example": [
              "email",
              "last_name"
            ]
          },
          "approval_state_attributes": {
            "type": "object",
            "additionalProperties": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "example": {
              "contact": [
                "name",
                "address"
              ],
              "contract": [
                "installment_amount"
              ]
            }
          },
          "email_templates": {
            "$ref": "#/components/schemas/EmailTemplates"
          },
          "images": {
            "type": "object",
            "description": "Teaser & Banner Image web links",
            "properties": {
              "orderLeftTeaser": {
                "type": "string",
                "nullable": true,
                "example": "https://epilot-bucket.s3.eu-central-1.amazonaws.com/12344/6538fddb-f0e9-4f0f-af51-6e57891ff20a/order-left-teaser.jpeg",
                "description": "URL of the order left teaser image"
              },
              "orderRightTeaser": {
                "type": "string",
                "nullable": true,
                "example": "https://epilot-bucket.s3.eu-central-1.amazonaws.com/12344/6538fddb-f0e9-4f0f-af51-6e57891ff20a/order-right-teaser.jpeg",
                "description": "URL of the order right teaser image"
              },
              "welcomeBanner": {
                "type": "string",
                "nullable": true,
                "example": "https://epilot-bucket.s3.eu-central-1.amazonaws.com/12344/6538fddb-f0e9-4f0f-af51-6e57891ff20a/welcome-banner.jpeg",
                "description": "URL of the welcome banner image"
              }
            }
          },
          "entity_identifiers": {
            "type": "object",
            "description": "Identifiers used to identify an entity by a portal user. Deprecated. Use contract_identifiers instead.",
            "deprecated": true,
            "properties": {
              "type": {
                "type": "object",
                "properties": {
                  "isEnabled": {
                    "type": "boolean",
                    "description": "Enable/Disable the entity identifier"
                  },
                  "attributes": {
                    "type": "array",
                    "description": "Attributes used to identify an entity",
                    "items": {
                      "type": "string",
                      "example": "contract_number"
                    }
                  }
                }
              }
            }
          },
          "contract_identifiers": {
            "type": "array",
            "description": "Identifiers to identify a contract by a portal user.",
            "items": {
              "$ref": "#/components/schemas/ContractIdentifier"
            },
            "example": [
              {
                "name": "email",
                "schema": "contact"
              },
              {
                "name": "last_name",
                "schema": "contact"
              },
              {
                "name": "contract_number",
                "schema": "contract"
              }
            ]
          },
          "contract_selector_config": {
            "type": "object",
            "description": "Configuration for contract selector in the portal",
            "properties": {
              "show_inactive": {
                "type": "boolean",
                "description": "Whether to show inactive contracts in the selector"
              },
              "title_path": {
                "type": "string",
                "description": "Path to the property to use as the contract title"
              }
            }
          },
          "registration_identifiers": {
            "type": "array",
            "description": "Identifiers to identify a contact of a portal user during the registration.",
            "items": {
              "$ref": "#/components/schemas/ContractIdentifier"
            },
            "example": [
              {
                "name": "last_name",
                "schema": "contact"
              },
              {
                "name": "contract_number",
                "schema": "contract"
              }
            ]
          },
          "surfaces": {
            "type": "array",
            "description": "Surfaces this portal's data is reached from besides the portal UI itself (public journeys on the website, chat). Configured under Security > Surfaces. Each surface defines how a caller authenticates and what data access applies on it; the portal UI is the implicit default surface (login, default scope) and is not listed here. A surface with `authentication: registration_identifiers` is what makes `identifyContact` issue tokens for this portal: without one, `identifyContact` returns 403.\n",
            "items": {
              "$ref": "#/components/schemas/PortalSurface"
            }
          },
          "contact_identifiers_for_account": {
            "type": "array",
            "description": "Account-mode only. Identifiers on the contact entity of the primarily\nidentified account. Used to pick an existing related contact within the\nresolved account; if none matches, the values are written onto the new\ncontact that is created and linked to the account.\n",
            "items": {
              "$ref": "#/components/schemas/RegistrationIdentifier"
            },
            "example": [
              {
                "name": "first_name",
                "schema": "contact"
              },
              {
                "name": "last_name",
                "schema": "contact"
              }
            ]
          },
          "additional_contact_attributes": {
            "type": "array",
            "description": "Contact attributes collected from the user during self-registration that are\nwritten onto the newly created contact but are not used to identify an\nexisting one.\n",
            "items": {
              "$ref": "#/components/schemas/AdditionalContactAttribute"
            },
            "example": [
              {
                "name": "first_name",
                "required": true
              },
              {
                "name": "last_name",
                "required": true
              }
            ]
          },
          "triggered_journeys": {
            "type": "array",
            "description": "Journeys automatically opened on a portal user action",
            "items": {
              "type": "object",
              "properties": {
                "trigger_name": {
                  "type": "string",
                  "enum": [
                    "FIRST_LOGIN",
                    "ACCEPT_ORDER",
                    "DECLINE_ORDER"
                  ]
                },
                "journey_id": {
                  "$ref": "#/components/schemas/EntityId"
                },
                "context_params": {
                  "type": "array",
                  "description": "Context parameters forwarded to the journey when it is\nauto-triggered. Values may contain handlebars templates\nthat reference the available context (e.g.\n`{{contact._id}}`, `{{portal_user.email}}`,\n`{{order._id}}`) — these are resolved at trigger time by\n`GET /v2/portal/config/triggered-journeys/{trigger_name}`\nusing the caller's auth context plus runtime entities\nsupplied via the `context_entities` query param.\n",
                  "items": {
                    "type": "object",
                    "properties": {
                      "key": {
                        "type": "string"
                      },
                      "value": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "entity_edit_rules": {
            "type": "array",
            "description": "Rules for editing an entity by a portal user",
            "items": {
              "type": "object",
              "properties": {
                "slug": {
                  "$ref": "#/components/schemas/EntitySlug"
                },
                "attribute": {
                  "type": "string",
                  "example": "first_name"
                },
                "rule_type": {
                  "type": "string",
                  "enum": [
                    "cadence",
                    "relative_to_current_value",
                    "days_before_date",
                    "overdue_payments"
                  ]
                },
                "cadence_period_type": {
                  "type": "string",
                  "enum": [
                    "days",
                    "weeks",
                    "months"
                  ]
                },
                "cadence_period": {
                  "type": "number",
                  "example": 1
                },
                "changes_allowed": {
                  "type": "integer",
                  "example": 1
                },
                "grace_period": {
                  "type": "integer",
                  "example": 1
                },
                "allowed_increment": {
                  "type": "string",
                  "example": "10%"
                },
                "allowed_decrement": {
                  "type": "string",
                  "example": "10%"
                },
                "number_of_days_before_restriction": {
                  "type": "integer",
                  "example": 10
                }
              }
            }
          },
          "allowed_file_extensions": {
            "$ref": "#/components/schemas/AllowedFileExtensions"
          },
          "prevent_search_engine_indexing": {
            "type": "boolean",
            "description": "Prevent indexing by search engines"
          },
          "meter_reading_grace_period": {
            "type": "number",
            "description": "Grace period in days for meter readings"
          },
          "inactive_contract_cutoff_years": {
            "type": "number",
            "description": "Number of years to look back for showing inactive contracts in the portal"
          },
          "is_dummy": {
            "type": "boolean",
            "description": "Whether this is a dummy/test portal configuration"
          },
          "is_v3_item": {
            "type": "boolean",
            "description": "Whether this is a v3 portal configuration"
          },
          "published_revision_id": {
            "type": "string",
            "readOnly": true,
            "description": "The revision currently live on this portal. Absent until the first publish.",
            "example": "2026-08-25T14:03:11.482Z-a7f3c1d9"
          },
          "portal_id": {
            "$ref": "#/components/schemas/PortalId"
          },
          "portal_sk_v3": {
            "type": "string",
            "example": "PORTAL_CONFIG#453ad7bf-86d5-46c8-8252-bcc868df5e3c",
            "description": "Key of the portal config"
          },
          "origin": {
            "$ref": "#/components/schemas/Origin"
          },
          "global_blocks": {
            "type": "object",
            "description": "Portal-level blocks shared across all pages (e.g. footer). Keyed by block id.",
            "additionalProperties": {
              "$ref": "#/components/schemas/Block"
            }
          }
        }
      },
      "PortalId": {
        "type": "string",
        "description": "ID of the portal",
        "example": "453ad7bf-86d5-46c8-8252-bcc868df5e3c"
      },
      "PortalDataExportColumn": {
        "type": "object",
        "description": "One column of the portal data export CSV.",
        "required": [
          "key",
          "header",
          "source"
        ],
        "properties": {
          "key": {
            "type": "string",
            "description": "Stable, immutable column id (snake_case). Append-only contract: never rename, reorder semantics, or remove an existing key; new columns are appended.\n",
            "example": "vertragsnummer"
          },
          "header": {
            "type": "object",
            "description": "Localized header label, keyed by language (de, en).",
            "additionalProperties": {
              "type": "string"
            },
            "example": {
              "de": "Vertragsnummer",
              "en": "Contract number"
            }
          },
          "source": {
            "type": "object",
            "description": "Where the value comes from: relation hops from the anchor entity plus the attribute to read on the resolved entity. An empty path means the anchor itself.\n",
            "required": [
              "attribute"
            ],
            "properties": {
              "path": {
                "type": "array",
                "description": "Relation hops from the anchor entity (max 3).",
                "items": {
                  "type": "string"
                },
                "example": [
                  "customer"
                ]
              },
              "attribute": {
                "type": "string",
                "description": "Attribute to read on the resolved entity.",
                "example": "customer_number"
              },
              "address_field": {
                "type": "string",
                "description": "Which part of an address-typed attribute to render.",
                "enum": [
                  "full",
                  "street",
                  "street_number",
                  "postal_code",
                  "city",
                  "additional_info",
                  "country"
                ]
              }
            }
          },
          "formatter": {
            "type": "string",
            "enum": [
              "text",
              "date",
              "money_cents",
              "enum",
              "address"
            ]
          },
          "enum_labels": {
            "type": "object",
            "description": "Localized value maps for the enum formatter, keyed by language then raw value.\n",
            "additionalProperties": {
              "type": "object",
              "additionalProperties": {
                "type": "string"
              }
            }
          }
        }
      },
      "UpsertPortalConfigV3": {
        "allOf": [
          {
            "$ref": "#/components/schemas/UpdateOnlyPortalConfigAttributes"
          },
          {
            "$ref": "#/components/schemas/CommonConfigAttributesV3"
          },
          {
            "properties": {
              "origin": {
                "$ref": "#/components/schemas/Origin"
              },
              "pages": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/PageRequest"
                }
              }
            }
          }
        ]
      },
      "PortalConfigV3": {
        "allOf": [
          {
            "$ref": "#/components/schemas/UpdateOnlyPortalConfigAttributes"
          },
          {
            "$ref": "#/components/schemas/CommonConfigAttributesV3"
          },
          {
            "properties": {
              "organization_id": {
                "type": "string",
                "example": 12345,
                "description": "ID of the organization"
              },
              "org_settings": {
                "type": "object",
                "description": "Organization settings",
                "properties": {
                  "canary": {
                    "type": "object",
                    "description": "Canary feature flag",
                    "properties": {
                      "enabled": {
                        "type": "boolean",
                        "description": "Enable/Disable the canary feature"
                      }
                    }
                  },
                  "notracking": {
                    "type": "object",
                    "description": "Disable Advanced Usage Metrics",
                    "properties": {
                      "enabled": {
                        "type": "boolean",
                        "description": "Disable browser-side scripts that track advanced usage metrics"
                      }
                    }
                  }
                }
              },
              "feature_flags": {
                "type": "object",
                "description": "Feature flags for the portal",
                "additionalProperties": {
                  "type": "boolean"
                }
              },
              "grants": {
                "type": "array",
                "description": "Permissions granted to a portal user while accessing entities",
                "items": {
                  "$ref": "#/components/schemas/Grant"
                }
              },
              "identity_providers": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ProviderPublicConfig"
                }
              },
              "pages": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Page"
                }
              }
            }
          }
        ]
      },
      "JuiceSettings": {
        "type": "object",
        "properties": {
          "is_dummy": {
            "type": "boolean",
            "description": "Whether the org is in dummy mode"
          },
          "is_canary": {
            "type": "boolean",
            "description": "Whether the org is in canary mode"
          },
          "is_legacy_design": {
            "type": "boolean",
            "description": "Whether the portal still runs the old design (the `new_design` feature setting is off). Legacy portals are always served the frozen legacy bundle, even when the org is in canary."
          },
          "redirect_to": {
            "type": "string",
            "description": "The URL to redirect to",
            "example": "https://example.com"
          }
        }
      },
      "RevisionPageRequest": {
        "type": "object",
        "additionalProperties": true,
        "description": "A page inside a revision snapshot. `additionalProperties` is true on purpose — a page carries fields this schema does not name and they must survive into the snapshot and back onto the live page when it is published.\n",
        "required": [
          "id",
          "slug",
          "order",
          "blocks"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Stable page identity. Required because a revision diff correlates pages by id, not by slug.\n`format: uuid` is enforced, so a non-UUID id is rejected as a `400`.\n",
            "example": "c495fef9-eeca-4019-a989-8390dcd9825b"
          },
          "slug": {
            "type": "string",
            "example": "dashboard"
          },
          "order": {
            "type": "number",
            "example": 0
          },
          "blocks": {
            "type": "object",
            "additionalProperties": true,
            "description": "Required on purpose. A live save preserves an omitted `blocks` from the stored page; publish replaces each live page wholesale with no such guard, so a revision snapshotted without blocks would wipe them on publish. `{}` is a valid value — the field simply has to be present.\n"
          }
        }
      },
      "PortalRevisionRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/UpdateOnlyPortalConfigAttributes"
          },
          {
            "$ref": "#/components/schemas/CommonConfigAttributesV3"
          },
          {
            "type": "object",
            "additionalProperties": true,
            "required": [
              "pages"
            ],
            "properties": {
              "email_templates": {
                "$ref": "#/components/schemas/EmailTemplates"
              },
              "identity_providers": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ProviderConfig"
                }
              },
              "based_on_revision_id": {
                "type": "string"
              },
              "pages": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/RevisionPageRequest"
                }
              }
            }
          }
        ]
      },
      "PublishRevisionRequest": {
        "type": "object",
        "required": [
          "revision_id"
        ],
        "properties": {
          "revision_id": {
            "type": "string",
            "example": "2026-08-25T14:03:11.482Z-a7f3c1d9"
          },
          "name": {
            "type": "string",
            "maxLength": 255,
            "description": "Label stamped onto the revision being published, permanently and atomically with the publish itself. Optional: when omitted the revision keeps whatever name it already has, possibly none; the server never generates one. An explicit name replaces any previous one. Clients derive a display label for unnamed revisions from `published_at` / `created_at`.\n",
            "example": "FAQ page launch"
          },
          "description": {
            "type": "string",
            "maxLength": 8000,
            "description": "Optional description stamped onto the revision at publish time"
          }
        }
      },
      "PortalRevisionSummary": {
        "type": "object",
        "required": [
          "revision_id",
          "created_at",
          "page_count",
          "is_published"
        ],
        "properties": {
          "revision_id": {
            "type": "string",
            "example": "2026-08-25T14:03:11.482Z-a7f3c1d9"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "created_by": {
            "type": "string",
            "description": "May be absent — an internal-auth caller carries no user id."
          },
          "name": {
            "type": "string",
            "description": "Set at publish time only, and only when the publish request carried one. A revision can have been published and still have no name; `published_at` is the signal that a revision was live at some point, not this field.\n",
            "example": "FAQ page launch"
          },
          "description": {
            "type": "string"
          },
          "page_count": {
            "type": "number"
          },
          "published_at": {
            "type": "string",
            "format": "date-time",
            "description": "The last time this revision was published. NOT the same question as `is_published`: a revision that was live yesterday still carries a `published_at`.\n"
          },
          "published_by": {
            "type": "string",
            "description": "Who performed the last publish of this revision."
          },
          "is_published": {
            "type": "boolean",
            "description": "Whether this revision is the one currently live."
          }
        }
      },
      "PortalRevisionList": {
        "type": "object",
        "required": [
          "results"
        ],
        "properties": {
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PortalRevisionSummary"
            }
          },
          "next_cursor": {
            "type": "string",
            "description": "Opaque cursor to pass back as `cursor` to fetch the next page. When `next_cursor` is absent, the client has reached the end of the dataset.\n"
          }
        }
      },
      "PortalRevisionCreated": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PortalRevisionSummary"
          },
          {
            "type": "object",
            "properties": {
              "identity_providers": {
                "type": "array",
                "description": "The SSO identity providers just captured into this revision, with `client_secret` REDACTED exactly as the admin portal-config GET redacts it. Returned only here — not on list items or elsewhere — because the caller who just submitted providers is the one reader who needs to see what got captured, e.g. to warn that the revision was saved without a client secret.\n",
                "items": {
                  "$ref": "#/components/schemas/ProviderConfig"
                }
              }
            }
          }
        ]
      },
      "RevisionPage": {
        "description": "A page from a revision snapshot. Same shape as a live `Page`, without the server-managed fields (`past_routes`, `_created_at`, `_updated_at`, `is_deleted`), which publish re-derives from the live portal.\n",
        "allOf": [
          {
            "$ref": "#/components/schemas/Page"
          },
          {
            "type": "object",
            "required": [
              "id",
              "blocks"
            ],
            "properties": {
              "org_id": {
                "type": "string"
              }
            }
          }
        ]
      },
      "PortalRevision": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PortalRevisionSummary"
          },
          {
            "type": "object",
            "required": [
              "config",
              "pages"
            ],
            "properties": {
              "config": {
                "type": "object",
                "additionalProperties": true,
                "description": "The snapshotted portal configuration. Secret-typed extension option values are removed entirely — not masked — before the response is assembled.\n"
              },
              "pages": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/RevisionPage"
                }
              },
              "identity_providers": {
                "type": "array",
                "description": "The SSO identity providers captured in this revision, with `client_secret` REDACTED exactly as the admin portal-config GET redacts it (no read path returns a stored secret).\nPublishing this revision replaces every live provider on this portal's origin, not just this portal's own, with this set. Identity providers key on `IDP#{origin}#{slug}`, a partition shared by every portal on that origin, so any other portal sharing it (an ADDITIONAL_PORTAL clone, most commonly) is affected too. A client can diff this against the live providers (by `slug`, and on `oidc_config.oidc_issuer` / `oidc_config.client_id` for a changed provider) to warn that a publish would also change SSO configuration, for this portal and any others sharing its origin.\nABSENT and `[]` are different answers. `[]` means the revision has no providers and publishing it removes the live ones; absent means the revision was saved before provider versioning, carries no SSO opinion, and publishing it changes no provider.\n",
                "items": {
                  "$ref": "#/components/schemas/ProviderConfig"
                }
              },
              "email_templates": {
                "$ref": "#/components/schemas/EmailTemplates"
              },
              "email_template_settings": {
                "type": "object",
                "additionalProperties": true,
                "description": "The portal's email template settings as they were live when the revision was saved. Read-only: this field cannot be set through the revision request and is always captured from live.\n"
              }
            }
          }
        ]
      },
      "PublishResult": {
        "type": "object",
        "required": [
          "revision_id",
          "published_at",
          "post_publish_warnings"
        ],
        "properties": {
          "revision_id": {
            "type": "string"
          },
          "published_at": {
            "type": "string",
            "format": "date-time"
          },
          "published_by": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "description": "The name now on the revision: the one sent in the request, or the one a previous publish set when this request omitted `name`. Absent when neither exists.\n",
            "example": "FAQ page launch"
          },
          "post_publish_warnings": {
            "type": "array",
            "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",
            "items": {
              "type": "string"
            },
            "example": [
              "allowed_entities_change"
            ]
          }
        }
      },
      "SwappableConfig": {
        "type": "string",
        "description": "Optional configuration item that a portal swap can additionally include. The swap always transfers the pages and the functional experience config that keep the portal working. These items are opt-in on top of that and are OFF by default. Domain and access/security settings (domain, cognito_details, auth_settings) can never be swapped and are therefore not part of this enum.",
        "enum": [
          "email_templates"
        ]
      },
      "PortalUserRegistrationStatus": {
        "type": "string",
        "enum": [
          "Registration Pending",
          "Confirmation Email Sent",
          "Registered",
          "Email Update In Progress"
        ]
      },
      "BusinessPartnerItem": {
        "type": "object",
        "properties": {
          "_id": {
            "$ref": "#/components/schemas/EntityId"
          },
          "has_portal_user": {
            "type": "boolean",
            "example": true
          },
          "registration_status": {
            "$ref": "#/components/schemas/PortalUserRegistrationStatus"
          },
          "email": {
            "type": "string",
            "example": "john.doe@example.com"
          },
          "_title": {
            "type": "string",
            "example": "John Doe"
          },
          "first_name": {
            "type": "string",
            "example": "John"
          },
          "last_name": {
            "type": "string",
            "example": "Doe"
          },
          "access_status": {
            "type": "boolean",
            "example": true
          }
        }
      },
      "SupportReportType": {
        "type": "string",
        "enum": [
          "bug",
          "feedback"
        ]
      },
      "SupportRequestAttachment": {
        "type": "object",
        "required": [
          "filename",
          "mime_type",
          "contents"
        ],
        "properties": {
          "filename": {
            "type": "string",
            "maxLength": 255,
            "example": "screenshot.png"
          },
          "mime_type": {
            "type": "string",
            "example": "image/png"
          },
          "contents": {
            "type": "string",
            "description": "Base64-encoded file, optionally as a data URL"
          }
        }
      },
      "CleverPvContext": {
        "type": "object",
        "properties": {
          "screen": {
            "type": "string",
            "maxLength": 256,
            "description": "Current Clever PV screen identifier"
          },
          "version": {
            "type": "string",
            "maxLength": 64,
            "description": "Clever PV application version"
          },
          "connection_state": {
            "type": "string",
            "maxLength": 64,
            "description": "Device connectivity at the time of the report"
          },
          "locale": {
            "type": "string",
            "maxLength": 32,
            "description": "App locale at the time of the report"
          },
          "timezone": {
            "type": "string",
            "maxLength": 64,
            "description": "App timezone at the time of the report"
          },
          "firmware_version": {
            "type": "string",
            "maxLength": 64,
            "description": "Device firmware version when known"
          },
          "build": {
            "type": "string",
            "maxLength": 64,
            "description": "App build identifier when known"
          },
          "trace_id": {
            "type": "string",
            "maxLength": 128,
            "description": "Client-generated correlation id for the report"
          },
          "device": {
            "type": "object",
            "additionalProperties": true,
            "description": "Optional device snapshot from the portal. Attached as a JSON file on\nthe original Zendesk comment. Must include id when present. Serialized\nJSON is limited to 256 KiB.\n"
          },
          "vendor": {
            "type": "object",
            "additionalProperties": true,
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            },
            "description": "Optional onboarding vendor id and name. Included in the Zendesk\ncomment. Not a portal entity.\n"
          }
        }
      },
      "CreateSupportRequest": {
        "type": "object",
        "required": [
          "report_type",
          "description",
          "submission_id"
        ],
        "properties": {
          "report_type": {
            "$ref": "#/components/schemas/SupportReportType"
          },
          "description": {
            "type": "string",
            "minLength": 1,
            "maxLength": 8000
          },
          "submission_id": {
            "type": "string",
            "minLength": 8,
            "maxLength": 128,
            "pattern": "^[A-Za-z0-9._-]+$",
            "description": "Client-generated id for correlating the request"
          },
          "site_id": {
            "$ref": "#/components/schemas/EntityId"
          },
          "device_id": {
            "$ref": "#/components/schemas/EntityId"
          },
          "clever_pv": {
            "$ref": "#/components/schemas/CleverPvContext"
          },
          "attachments": {
            "type": "array",
            "maxItems": 5,
            "items": {
              "$ref": "#/components/schemas/SupportRequestAttachment"
            }
          }
        }
      },
      "SupportRequestResult": {
        "type": "object",
        "required": [
          "reference",
          "status"
        ],
        "properties": {
          "reference": {
            "type": "string",
            "example": "12345"
          },
          "status": {
            "type": "string",
            "example": "new"
          }
        }
      }
    }
  },
  "servers": [
    {
      "url": "https://customer-portal-api.sls.epilot.io"
    }
  ]
}
