diff --git a/.changeset/document-client-templates-validate.md b/.changeset/document-client-templates-validate.md new file mode 100644 index 000000000..340b7b303 --- /dev/null +++ b/.changeset/document-client-templates-validate.md @@ -0,0 +1,6 @@ +--- +"@epilot/document-client": minor +"@epilot/sdk": minor +--- + +Regenerate the document client from the Document API spec as deployed to production. New operation `validateTemplate` (`POST /v2/templates:validate`): checks a docx/xlsx/pptx template for tag and delimiter errors and, when asked, returns a corrected copy as a presigned download together with the list of issues (`TemplateIssue`: id, location such as `Tabelle1!N4`, before/after text, confidence, fixable). New types `TemplateValidationRequest` (`template_document`, `fix`, `fix_level: safe | aggressive`) and `TemplateValidationResponse` (`valid`, `fixed`, `issues`, `unresolved_errors`, `fixed_document`). diff --git a/.changeset/slick-bobcats-vanish.md b/.changeset/slick-bobcats-vanish.md new file mode 100644 index 000000000..81b440a22 --- /dev/null +++ b/.changeset/slick-bobcats-vanish.md @@ -0,0 +1,5 @@ +--- +"@epilot/document-client": patch +--- + +document-api adds new template validation endpoint diff --git a/clients/chat-client/package.json b/clients/chat-client/package.json index b464f774c..c923465f8 100644 --- a/clients/chat-client/package.json +++ b/clients/chat-client/package.json @@ -1,6 +1,6 @@ { "name": "@epilot/chat-client", - "version": "0.1.0", + "version": "0.1.1", "description": "Client for the epilot Chat API", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/clients/chat-client/src/openapi.d.ts b/clients/chat-client/src/openapi.d.ts index 6b30662fb..5376bcc92 100644 --- a/clients/chat-client/src/openapi.d.ts +++ b/clients/chat-client/src/openapi.d.ts @@ -1365,7 +1365,7 @@ export interface OperationMethods { 'listChatWidgets'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * createChatWidget @@ -1373,7 +1373,7 @@ export interface OperationMethods { 'createChatWidget'( parameters?: Parameters | null, data?: Paths.CreateChatWidget.RequestBody, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * getChatWidget @@ -1381,7 +1381,7 @@ export interface OperationMethods { 'getChatWidget'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * updateChatWidget @@ -1389,7 +1389,7 @@ export interface OperationMethods { 'updateChatWidget'( parameters?: Parameters | null, data?: Paths.UpdateChatWidget.RequestBody, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * deleteChatWidget @@ -1397,7 +1397,7 @@ export interface OperationMethods { 'deleteChatWidget'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * getPublicChatWidget - Resolve visitor-facing configuration for an independent widget and its current agent assignment. The widget ID is not an organisation ID or a destination such as website or portal. @@ -1405,7 +1405,7 @@ export interface OperationMethods { 'getPublicChatWidget'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * createPublicChatGrant - Called by the host website with its widget_key. Checks the saved website origin allowlist and issues a single-use grant for the iframe to exchange at POST /v1/sessions within 60 seconds. This deadline does not limit the resulting session. @@ -1413,7 +1413,7 @@ export interface OperationMethods { 'createPublicChatGrant'( parameters?: Parameters | null, data?: Paths.CreatePublicChatGrant.RequestBody, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * createAnonymousChatSession - Exchanges an unexpired grant once for an independent 30-minute session. Expired or previously used grants return 401 INVALID_GRANT; the host must bootstrap again. @@ -1421,7 +1421,7 @@ export interface OperationMethods { 'createAnonymousChatSession'( parameters?: Parameters | null, data?: Paths.CreateAnonymousChatSession.RequestBody, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * sendAnonymousChatMessage - Creates a turn or replays its persisted result. Reuse request_id and the exact @@ -1429,12 +1429,12 @@ export interface OperationMethods { * TEMPORARILY_UNAVAILABLE means the request outcome could not be checked; retry the same request ID and payload. * A running request returns 409 IN_PROGRESS; a changed payload returns 409 REQUEST_CONFLICT. * A failed admitted request never starts another model execution when retried. - * + * */ 'sendAnonymousChatMessage'( parameters?: Parameters | null, data?: Paths.SendAnonymousChatMessage.RequestBody, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } @@ -1446,7 +1446,7 @@ export interface PathsDictionary { 'get'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * createChatWidget @@ -1454,7 +1454,7 @@ export interface PathsDictionary { 'post'( parameters?: Parameters | null, data?: Paths.CreateChatWidget.RequestBody, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/widgets/{widget_id}']: { @@ -1464,7 +1464,7 @@ export interface PathsDictionary { 'get'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * updateChatWidget @@ -1472,7 +1472,7 @@ export interface PathsDictionary { 'put'( parameters?: Parameters | null, data?: Paths.UpdateChatWidget.RequestBody, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * deleteChatWidget @@ -1480,7 +1480,7 @@ export interface PathsDictionary { 'delete'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/widgets/{widget_id}/configuration']: { @@ -1490,7 +1490,7 @@ export interface PathsDictionary { 'get'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/bootstrap']: { @@ -1500,7 +1500,7 @@ export interface PathsDictionary { 'post'( parameters?: Parameters | null, data?: Paths.CreatePublicChatGrant.RequestBody, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/sessions']: { @@ -1510,7 +1510,7 @@ export interface PathsDictionary { 'post'( parameters?: Parameters | null, data?: Paths.CreateAnonymousChatSession.RequestBody, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/messages']: { @@ -1520,12 +1520,12 @@ export interface PathsDictionary { * TEMPORARILY_UNAVAILABLE means the request outcome could not be checked; retry the same request ID and payload. * A running request returns 409 IN_PROGRESS; a changed payload returns 409 REQUEST_CONFLICT. * A failed admitted request never starts another model execution when retried. - * + * */ 'post'( parameters?: Parameters | null, data?: Paths.SendAnonymousChatMessage.RequestBody, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } } diff --git a/clients/configuration-hub-client/src/openapi.d.ts b/clients/configuration-hub-client/src/openapi.d.ts index 06bf5dfe0..defb78e64 100644 --- a/clients/configuration-hub-client/src/openapi.d.ts +++ b/clients/configuration-hub-client/src/openapi.d.ts @@ -1247,307 +1247,307 @@ declare namespace Paths { export interface OperationMethods { /** * listConfigTypes - listConfigTypes - * + * * Returns the static list of available configuration types with display metadata. * This is a cheap call — no fan-out to downstream APIs. Returns all known types * with labels and icons. The frontend should then call `listConfigs` separately * for each type it wants to load. - * + * */ 'listConfigTypes'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * listConfigs - listConfigs - * + * * List configs of a given type with pagination. Returns summary metadata only * (not full payloads). The frontend calls this per type folder when expanding. - * + * * Supports offset-based pagination via `from` and `size` parameters. - * + * */ 'listConfigs'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * getConfigDependencies - getConfigDependencies - * + * * Get configs that are referenced by the given config. * Used to render children when expanding a config node in the tree. - * + * * Resolves dependencies by fetching the config payload server-side and scanning * for references (UUIDs, source IDs, slug-based references). - * + * */ 'getConfigDependencies'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * getConfigUsedBy - getConfigUsedBy - * + * * Get configs that reference the given config (reverse dependencies). * Scans the indexed config items for references to this config's ID or aliases. - * + * */ 'getConfigUsedBy'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * getIndex - getIndex - * + * * Return the current index build state for the caller's organization. * Clients poll this to decide whether to show a "building" indicator * and when to refetch data. - * + * */ 'getIndex'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * compareConfigs - compareConfigs - * + * * Compare the caller org's configs of a single type against another * (source) org, side by side. Rows are paired via the lineage registry in * both sync directions (caller imported from source, or source imported * from caller); configs without a counterpart come back as `only_current` * or `only_source`. - * + * * `source_auth_token` must be a valid token for `source_org_id` — the * frontend mints one via the pipeline pairing, mirroring `createSyncJob`'s * `target_auth_token`. The token is verified against `source_org_id` * before any source-org data is read. - * + * * POST because the request carries a token; the operation reads only. - * + * */ 'compareConfigs'( parameters?: Parameters | null, data?: Paths.CompareConfigs.RequestBody, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * suggestMatches - suggestMatches - * + * * Run the sync-grade heuristic match (`lookupByHeuristic` — name / slug / * unique key) for a batch of source-org configs that have no lineage * entry, and return candidate counterparts in the caller's org. - * + * * Suggestions are ephemeral — nothing is persisted. The client offers * each candidate to the user, and a confirmed pair is written via * `confirmLineage`. Batches are capped at 25 ids; the client pages * through unmatched rows across successive calls. - * + * */ 'suggestMatches'( parameters?: Parameters | null, data?: Paths.SuggestMatches.RequestBody, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * confirmLineage - confirmLineage - * + * * Persist a lineage entry pairing a source-org config with a config in * the caller's org — used to confirm a heuristic suggestion from the * Compare view. Writes to the caller org's lineage partition (caller as * sync target), so subsequent syncs PATCH the confirmed target instead of * creating a duplicate. Conflicts (an existing entry pointing at a * different target) return 409 so the client can refresh. - * + * */ 'confirmLineage'( parameters?: Parameters | null, data?: Paths.ConfirmLineage.RequestBody, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * breakLineage - breakLineage - * + * * Delete a lineage entry from the caller org's partition (caller as sync * target), identified by `type` + `source_id`. Used to break a wrong or * stale match from the Compare view. Note: a future sync can re-match the * same pair heuristically — the entry is deleted, not blocklisted. To * break a match recorded in the OTHER direction, call this operation * authenticated as the other org (pipeline token). - * + * */ 'breakLineage'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * listSyncJobs - listSyncJobs - * + * * List sync jobs scoped to the caller's organization, paginated with an opaque * cursor. Defaults to most-recent first. - * + * */ 'listSyncJobs'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * createSyncJob - createSyncJob - * + * * Create a new cross-org sync job. The job is enqueued for asynchronous execution * by the worker Lambda; the response returns the persisted job header with status * `pending`. - * + * * See `docs/sync/INTERFACES.md` for the locked request/response contract. - * + * */ 'createSyncJob'( parameters?: Parameters | null, data?: Paths.CreateSyncJob.RequestBody, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * getSyncJob - getSyncJob - * + * * Fetch a single sync job by ID. Returns the job header, counts summary, * current phase pointer, and the latest activity events. Frontend polls this * endpoint with a ramping interval. - * + * */ 'getSyncJob'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * retrySyncJob - retrySyncJob - * + * * Retry the unresolved resources from a prior sync job: `failed` rows, plus * rows the original run left at `pending`/`in_progress` because it stopped * early. Creates a new job whose scope is that `(type, source_id)` set and * enqueues it for execution. Optionally accepts inline payload overrides. - * + * */ 'retrySyncJob'( parameters?: Parameters | null, data?: Paths.RetrySyncJob.RequestBody, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * cancelSyncJob - cancelSyncJob - * + * * Cancel a running sync job. Marks the job `cancelled` with a `finished_at` * so it stops being reported as in-flight, and the worker stops at its next * batch boundary — phases re-read the job status and abort rather than * overwrite a cancellation with their own outcome. - * + * * Resources already written to the target are NOT rolled back; cancelling * stops further work. Jobs already in a terminal state are rejected with * 409. - * + * */ 'cancelSyncJob'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * listSyncJobResources - listSyncJobResources - * + * * List the per-resource rows for a sync job. Supports filtering by status * (e.g. `failed`) and cursor pagination. Used by the failures table and the * dry-run plan view in the frontend. - * + * */ 'listSyncJobResources'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * listDeleteJobs - listDeleteJobs - * + * * List bulk-delete jobs scoped to the caller's organization, paginated * with an opaque cursor. Most-recent first. - * + * */ 'listDeleteJobs'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * createDeleteJob - createDeleteJob - * + * * Create a bulk-delete job for the caller's organization. The selected * resources are deleted asynchronously by a worker Lambda; the response * returns the persisted job header with status `pending`. - * + * * Raw delete — no dependency checks are performed. On completion the * worker auto-triggers an index rebuild so the config list reflects the * deletions. - * + * */ 'createDeleteJob'( parameters?: Parameters | null, data?: Paths.CreateDeleteJob.RequestBody, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * getDeleteJob - getDeleteJob - * + * * Fetch a single bulk-delete job by ID. Returns the job header and counts * summary. Frontend polls this endpoint while the job runs. - * + * */ 'getDeleteJob'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * listDeleteJobResources - listDeleteJobResources - * + * * List the per-resource rows for a delete job, cursor-paginated. Used by * the failures view in the frontend. - * + * */ 'listDeleteJobResources'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * rebuildIndex - rebuildIndex - * + * * Rebuild the configuration index for the caller's organization. * Fire-and-forget: invokes the async worker and returns immediately. * A new rebuild will cancel any in-flight build (see `build_token`). - * + * */ 'rebuildIndex'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * getConfigInventory - getConfigInventory - * + * * Returns a fresh inventory of an org's configuration resources — `{ type, id }` identities only, * no full payloads. Calls every adapter's `list()` live (bypasses the 7-day DynamoDB index). * Intended for snapshot-api to consume when taking a full-org snapshot. - * + * */ 'getConfigInventory'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } @@ -1555,341 +1555,341 @@ export interface PathsDictionary { ['/v1/configs/types']: { /** * listConfigTypes - listConfigTypes - * + * * Returns the static list of available configuration types with display metadata. * This is a cheap call — no fan-out to downstream APIs. Returns all known types * with labels and icons. The frontend should then call `listConfigs` separately * for each type it wants to load. - * + * */ 'get'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/configs/{type}']: { /** * listConfigs - listConfigs - * + * * List configs of a given type with pagination. Returns summary metadata only * (not full payloads). The frontend calls this per type folder when expanding. - * + * * Supports offset-based pagination via `from` and `size` parameters. - * + * */ 'get'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/configs/{type}/{id}/dependencies']: { /** * getConfigDependencies - getConfigDependencies - * + * * Get configs that are referenced by the given config. * Used to render children when expanding a config node in the tree. - * + * * Resolves dependencies by fetching the config payload server-side and scanning * for references (UUIDs, source IDs, slug-based references). - * + * */ 'get'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/configs/{type}/{id}/used_by']: { /** * getConfigUsedBy - getConfigUsedBy - * + * * Get configs that reference the given config (reverse dependencies). * Scans the indexed config items for references to this config's ID or aliases. - * + * */ 'get'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/configs/index']: { /** * getIndex - getIndex - * + * * Return the current index build state for the caller's organization. * Clients poll this to decide whether to show a "building" indicator * and when to refetch data. - * + * */ 'get'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/configs/compare']: { /** * compareConfigs - compareConfigs - * + * * Compare the caller org's configs of a single type against another * (source) org, side by side. Rows are paired via the lineage registry in * both sync directions (caller imported from source, or source imported * from caller); configs without a counterpart come back as `only_current` * or `only_source`. - * + * * `source_auth_token` must be a valid token for `source_org_id` — the * frontend mints one via the pipeline pairing, mirroring `createSyncJob`'s * `target_auth_token`. The token is verified against `source_org_id` * before any source-org data is read. - * + * * POST because the request carries a token; the operation reads only. - * + * */ 'post'( parameters?: Parameters | null, data?: Paths.CompareConfigs.RequestBody, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/configs/compare/suggestions']: { /** * suggestMatches - suggestMatches - * + * * Run the sync-grade heuristic match (`lookupByHeuristic` — name / slug / * unique key) for a batch of source-org configs that have no lineage * entry, and return candidate counterparts in the caller's org. - * + * * Suggestions are ephemeral — nothing is persisted. The client offers * each candidate to the user, and a confirmed pair is written via * `confirmLineage`. Batches are capped at 25 ids; the client pages * through unmatched rows across successive calls. - * + * */ 'post'( parameters?: Parameters | null, data?: Paths.SuggestMatches.RequestBody, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/configs/lineage']: { /** * confirmLineage - confirmLineage - * + * * Persist a lineage entry pairing a source-org config with a config in * the caller's org — used to confirm a heuristic suggestion from the * Compare view. Writes to the caller org's lineage partition (caller as * sync target), so subsequent syncs PATCH the confirmed target instead of * creating a duplicate. Conflicts (an existing entry pointing at a * different target) return 409 so the client can refresh. - * + * */ 'post'( parameters?: Parameters | null, data?: Paths.ConfirmLineage.RequestBody, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * breakLineage - breakLineage - * + * * Delete a lineage entry from the caller org's partition (caller as sync * target), identified by `type` + `source_id`. Used to break a wrong or * stale match from the Compare view. Note: a future sync can re-match the * same pair heuristically — the entry is deleted, not blocklisted. To * break a match recorded in the OTHER direction, call this operation * authenticated as the other org (pipeline token). - * + * */ 'delete'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/configs/sync-jobs']: { /** * createSyncJob - createSyncJob - * + * * Create a new cross-org sync job. The job is enqueued for asynchronous execution * by the worker Lambda; the response returns the persisted job header with status * `pending`. - * + * * See `docs/sync/INTERFACES.md` for the locked request/response contract. - * + * */ 'post'( parameters?: Parameters | null, data?: Paths.CreateSyncJob.RequestBody, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * listSyncJobs - listSyncJobs - * + * * List sync jobs scoped to the caller's organization, paginated with an opaque * cursor. Defaults to most-recent first. - * + * */ 'get'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/configs/sync-jobs/{id}']: { /** * getSyncJob - getSyncJob - * + * * Fetch a single sync job by ID. Returns the job header, counts summary, * current phase pointer, and the latest activity events. Frontend polls this * endpoint with a ramping interval. - * + * */ 'get'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/configs/sync-jobs/{id}/retry']: { /** * retrySyncJob - retrySyncJob - * + * * Retry the unresolved resources from a prior sync job: `failed` rows, plus * rows the original run left at `pending`/`in_progress` because it stopped * early. Creates a new job whose scope is that `(type, source_id)` set and * enqueues it for execution. Optionally accepts inline payload overrides. - * + * */ 'post'( parameters?: Parameters | null, data?: Paths.RetrySyncJob.RequestBody, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/configs/sync-jobs/{id}/cancel']: { /** * cancelSyncJob - cancelSyncJob - * + * * Cancel a running sync job. Marks the job `cancelled` with a `finished_at` * so it stops being reported as in-flight, and the worker stops at its next * batch boundary — phases re-read the job status and abort rather than * overwrite a cancellation with their own outcome. - * + * * Resources already written to the target are NOT rolled back; cancelling * stops further work. Jobs already in a terminal state are rejected with * 409. - * + * */ 'post'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/configs/sync-jobs/{id}/resources']: { /** * listSyncJobResources - listSyncJobResources - * + * * List the per-resource rows for a sync job. Supports filtering by status * (e.g. `failed`) and cursor pagination. Used by the failures table and the * dry-run plan view in the frontend. - * + * */ 'get'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/configs/delete-jobs']: { /** * createDeleteJob - createDeleteJob - * + * * Create a bulk-delete job for the caller's organization. The selected * resources are deleted asynchronously by a worker Lambda; the response * returns the persisted job header with status `pending`. - * + * * Raw delete — no dependency checks are performed. On completion the * worker auto-triggers an index rebuild so the config list reflects the * deletions. - * + * */ 'post'( parameters?: Parameters | null, data?: Paths.CreateDeleteJob.RequestBody, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * listDeleteJobs - listDeleteJobs - * + * * List bulk-delete jobs scoped to the caller's organization, paginated * with an opaque cursor. Most-recent first. - * + * */ 'get'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/configs/delete-jobs/{id}']: { /** * getDeleteJob - getDeleteJob - * + * * Fetch a single bulk-delete job by ID. Returns the job header and counts * summary. Frontend polls this endpoint while the job runs. - * + * */ 'get'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/configs/delete-jobs/{id}/resources']: { /** * listDeleteJobResources - listDeleteJobResources - * + * * List the per-resource rows for a delete job, cursor-paginated. Used by * the failures view in the frontend. - * + * */ 'get'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/configs/index:rebuild']: { /** * rebuildIndex - rebuildIndex - * + * * Rebuild the configuration index for the caller's organization. * Fire-and-forget: invokes the async worker and returns immediately. * A new rebuild will cancel any in-flight build (see `build_token`). - * + * */ 'post'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/configs/inventory']: { /** * getConfigInventory - getConfigInventory - * + * * Returns a fresh inventory of an org's configuration resources — `{ type, id }` identities only, * no full payloads. Calls every adapter's `list()` live (bypasses the 7-day DynamoDB index). * Intended for snapshot-api to consume when taking a full-org snapshot. - * + * */ 'get'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } } diff --git a/clients/document-client/package.json b/clients/document-client/package.json index 954680bbf..e9579a27a 100644 --- a/clients/document-client/package.json +++ b/clients/document-client/package.json @@ -1,6 +1,6 @@ { "name": "@epilot/document-client", - "version": "0.19.2", + "version": "0.20.0", "description": "API Client for epilot document API", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/clients/document-client/src/openapi-runtime.json b/clients/document-client/src/openapi-runtime.json index 3a665f583..707a416aa 100644 --- a/clients/document-client/src/openapi-runtime.json +++ b/clients/document-client/src/openapi-runtime.json @@ -9,6 +9,7 @@ "post": { "operationId": "getTemplateMeta", "requestBody": { + "required": true, "content": { "application/json": {} } @@ -34,6 +35,7 @@ } ], "requestBody": { + "required": true, "content": { "application/json": {} } @@ -45,6 +47,19 @@ "post": { "operationId": "convertDocument", "requestBody": { + "required": true, + "content": { + "application/json": {} + } + }, + "responses": {} + } + }, + "/v2/templates:validate": { + "post": { + "operationId": "validateTemplate", + "requestBody": { + "required": true, "content": { "application/json": {} } diff --git a/clients/document-client/src/openapi.d.ts b/clients/document-client/src/openapi.d.ts index 50d0857cc..7967d0cc2 100644 --- a/clients/document-client/src/openapi.d.ts +++ b/clients/document-client/src/openapi.d.ts @@ -1,5 +1,3 @@ -/* eslint-disable */ - import type { OpenAPIClient, Parameters, @@ -306,9 +304,10 @@ declare namespace Components { * - DOC_TO_PDF_CONVERT_ERROR - Error while converting the document to PDF. Normally related with a ConvertAPI failure. * - INTERNAL_ERROR - Internal error. Please contact support. * - INVALID_TEMPLATE_FORMAT - Invalid template format (only .docx is supported). This can happen due to a bad word file or an unsupported file extension. + * - TEMPLATE_NOT_FOUND - Template file was not found in S3. This indicates the template was likely deleted. * */ - export type ErrorCode = "PARSE_ERROR" | "DOC_TO_PDF_CONVERT_ERROR" | "INTERNAL_ERROR" | "INVALID_TEMPLATE_FORMAT"; + export type ErrorCode = "PARSE_ERROR" | "DOC_TO_PDF_CONVERT_ERROR" | "INTERNAL_ERROR" | "INVALID_TEMPLATE_FORMAT" | "TEMPLATE_NOT_FOUND"; export interface ErrorOutput { /** * Error message @@ -320,6 +319,7 @@ declare namespace Components { * - DOC_TO_PDF_CONVERT_ERROR - Error while converting the document to PDF. Normally related with a ConvertAPI failure. * - INTERNAL_ERROR - Internal error. Please contact support. * - INVALID_TEMPLATE_FORMAT - Invalid template format (only .docx is supported). This can happen due to a bad word file or an unsupported file extension. + * - TEMPLATE_NOT_FOUND - Template file was not found in S3. This indicates the template was likely deleted. * */ ErrorCode; @@ -398,6 +398,75 @@ declare namespace Components { */ key: string; } + /** + * A single template syntax problem, and the repair proposed for it + */ + export interface TemplateIssue { + /** + * Problem kind. Reuses docxtemplater's error vocabulary where one exists + * (`unopened_tag`, `unclosed_tag`, `duplicate_open_tag`, `duplicate_close_tag`), plus + * `malformed_tag` for single-brace placeholders and `typographic_characters` for + * editor-substituted characters inside an otherwise valid tag. + * + * example: + * unopened_tag + */ + id?: string; + /** + * Zip part the problem was found in + * example: + * xl/sharedStrings.xml + */ + file?: string; + /** + * Where the user can find it — a cell reference for spreadsheets, a part label + * (`Document body`, `Header 1`, `Slide 3`) otherwise. + * + * example: + * Tabelle1!N4 + */ + location?: string; + /** + * The offending text with surrounding context + * example: + * …Datum: {system.date}} Unterschrift… + */ + context?: string; + /** + * Why the template parser rejects it + * example: + * The tag is missing an opening brace. + */ + explanation?: string; + /** + * Whether the proposed repair was applied to the fixed copy + * example: + * true + */ + fixable?: boolean; + /** + * How sure we are that the repair is what the author meant + */ + confidence?: "high" | "medium" | "low"; + /** + * The repair rule that produced `after` + * example: + * balance_opening_delimiter + */ + rule?: string; + /** + * The text as it is in the template + * example: + * {system.date}} + */ + before?: string; + /** + * The text as it would be in the fixed copy + * example: + * {{system.date}} + */ + after?: string; + } /** * Template Settings for document generation */ @@ -469,10 +538,81 @@ declare namespace Components { /** * The file entity id, used when persisting a new template version with updated settings * example: - * 1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p + * 123e4567-e89b-12d3-a456-426614174000 */ file_entity_id?: string; // uuid } + export interface TemplateValidationRequest { + /** + * Input template document + */ + template_document: { + /** + * Document original filename, used to name the fixed copy + * example: + * Umzugsmeldung.xlsx + */ + filename?: string; + s3ref: S3Reference; + }; + /** + * Attempt to produce a hotfixed copy of the template. When false, the template is only + * inspected and no file is written. + * + */ + fix?: boolean; + /** + * How far the hotfix may go: + * - safe - only repairs where the author's intent is unambiguous from the syntax + * (unbalanced or duplicated braces, editor-substituted characters inside a tag). + * - aggressive - additionally promotes single-brace placeholders such as `{contact.name}` + * to `{{contact.name}}`. These are common in customer templates but indistinguishable + * from prose that uses braces, so they are reported with `confidence: low`. + * + */ + fix_level?: "safe" | "aggressive"; + } + export interface TemplateValidationResponse { + /** + * Whether the template compiled cleanly before any repair was attempted + * example: + * false + */ + valid?: boolean; + /** + * Whether a corrected copy of the template could be produced + * example: + * true + */ + fixed?: boolean; + /** + * Everything found in the template, whether or not it could be repaired + */ + issues?: /* A single template syntax problem, and the repair proposed for it */ TemplateIssue[]; + /** + * Parser errors that remain after the hotfix (or the original errors when nothing was + * fixed). Empty when the template is valid. + * + */ + unresolved_errors?: /* DocxTemplater error detail */ DocxTemplaterErrorDetail[]; + /** + * The corrected copy, for the user to review and accept. Absent when no repair was + * applied. The original template is left untouched. + * + */ + fixed_document?: { + s3ref?: S3Reference; + /** + * example: + * Umzugsmeldung (fixed).xlsx + */ + filename?: string; + /** + * Short-lived download link for the corrected copy + */ + preview_url?: string; // uri + }; + } } } declare namespace Paths { @@ -485,13 +625,33 @@ declare namespace Paths { namespace GenerateDocumentV2 { namespace Parameters { export type JobId = string; + /** + * - partial_generation: Generates a partial document for user validation before final generation + * - full_generation: Completes the entire document generation process in one step + * + */ export type Mode = "partial_generation" | "full_generation"; + /** + * - open: Preview URL opens the file directly in browser + * - download: Preview URL triggers a download of the file + * + */ export type PreviewMode = "open" | "download"; } export interface QueryParameters { job_id?: Parameters.JobId; - mode?: Parameters.Mode; - preview_mode?: Parameters.PreviewMode; + mode?: /** + * - partial_generation: Generates a partial document for user validation before final generation + * - full_generation: Completes the entire document generation process in one step + * + */ + Parameters.Mode; + preview_mode?: /** + * - open: Preview URL opens the file directly in browser + * - download: Preview URL triggers a download of the file + * + */ + Parameters.PreviewMode; } export type RequestBody = Components.Schemas.DocumentGenerationV2Request; namespace Responses { @@ -507,6 +667,15 @@ declare namespace Paths { export type $415 = Components.Schemas.ErrorOutput; } } + namespace ValidateTemplate { + export type RequestBody = Components.Schemas.TemplateValidationRequest; + namespace Responses { + export type $200 = Components.Schemas.TemplateValidationResponse; + export type $403 = Components.Schemas.ErrorOutput; + export type $413 = Components.Schemas.ErrorOutput; + export type $415 = Components.Schemas.ErrorOutput; + } + } } @@ -564,6 +733,33 @@ export interface OperationMethods { data?: Paths.ConvertDocument.RequestBody, config?: AxiosRequestConfig ): OperationResponse + /** + * validateTemplate - validateTemplate + * + * Validates a document template's variable syntax and, optionally, proposes a hotfixed copy of it. + * + * The endpoint compiles the template with docxtemplater's core parser (the xlsx module for + * spreadsheets), so a delimiter or tag error reported here is one generation would fail on. + * Failures specific to the image or HTML modules are not covered. Every problem it can repair + * unambiguously — a missing brace, an extra brace, a smart quote or a non-breaking space that + * Word substituted inside a tag — is applied to a **copy** of the template, which is uploaded + * and returned as `fixed_document`. + * + * The original template is never modified. Accepting the fix is an explicit, separate step: + * the caller shows the user `issues` (each with its `before`/`after`), lets them download + * `fixed_document.preview_url`, and only then replaces the template. + * + * Supported input document types: + * - .docx, .docm, .dotx + * - .xlsx, .xlsm + * - .pptx + * + */ + 'validateTemplate'( + parameters?: Parameters | null, + data?: Paths.ValidateTemplate.RequestBody, + config?: AxiosRequestConfig + ): OperationResponse } export interface PathsDictionary { @@ -626,6 +822,35 @@ export interface PathsDictionary { config?: AxiosRequestConfig ): OperationResponse } + ['/v2/templates:validate']: { + /** + * validateTemplate - validateTemplate + * + * Validates a document template's variable syntax and, optionally, proposes a hotfixed copy of it. + * + * The endpoint compiles the template with docxtemplater's core parser (the xlsx module for + * spreadsheets), so a delimiter or tag error reported here is one generation would fail on. + * Failures specific to the image or HTML modules are not covered. Every problem it can repair + * unambiguously — a missing brace, an extra brace, a smart quote or a non-breaking space that + * Word substituted inside a tag — is applied to a **copy** of the template, which is uploaded + * and returned as `fixed_document`. + * + * The original template is never modified. Accepting the fix is an explicit, separate step: + * the caller shows the user `issues` (each with its `before`/`after`), lets them download + * `fixed_document.preview_url`, and only then replaces the template. + * + * Supported input document types: + * - .docx, .docm, .dotx + * - .xlsx, .xlsm + * - .pptx + * + */ + 'post'( + parameters?: Parameters | null, + data?: Paths.ValidateTemplate.RequestBody, + config?: AxiosRequestConfig + ): OperationResponse + } } export type Client = OpenAPIClient @@ -646,4 +871,7 @@ export type InternalErrorDetails = Components.Schemas.InternalErrorDetails; export type InvalidCustomVariableErrorDetail = Components.Schemas.InvalidCustomVariableErrorDetail; export type InvalidCustomVariableErrorDetails = Components.Schemas.InvalidCustomVariableErrorDetails; export type S3Reference = Components.Schemas.S3Reference; +export type TemplateIssue = Components.Schemas.TemplateIssue; export type TemplateSettings = Components.Schemas.TemplateSettings; +export type TemplateValidationRequest = Components.Schemas.TemplateValidationRequest; +export type TemplateValidationResponse = Components.Schemas.TemplateValidationResponse; diff --git a/clients/document-client/src/openapi.json b/clients/document-client/src/openapi.json index 40d2cfb61..725a4f929 100644 --- a/clients/document-client/src/openapi.json +++ b/clients/document-client/src/openapi.json @@ -26,6 +26,7 @@ "Documents" ], "requestBody": { + "required": true, "content": { "application/json": { "schema": { @@ -102,6 +103,7 @@ "partial_generation", "full_generation" ], + "description": "- partial_generation: Generates a partial document for user validation before final generation\n- full_generation: Completes the entire document generation process in one step\n", "default": "full_generation" } }, @@ -115,6 +117,7 @@ "open", "download" ], + "description": "- open: Preview URL opens the file directly in browser\n- download: Preview URL triggers a download of the file\n", "default": "open" } } @@ -123,6 +126,7 @@ "Documents" ], "requestBody": { + "required": true, "content": { "application/json": { "schema": { @@ -154,6 +158,7 @@ "Documents" ], "requestBody": { + "required": true, "content": { "application/json": { "schema": { @@ -175,6 +180,68 @@ } } } + }, + "/v2/templates:validate": { + "post": { + "operationId": "validateTemplate", + "summary": "validateTemplate", + "description": "Validates a document template's variable syntax and, optionally, proposes a hotfixed copy of it.\n\nThe endpoint compiles the template with docxtemplater's core parser (the xlsx module for\nspreadsheets), so a delimiter or tag error reported here is one generation would fail on.\nFailures specific to the image or HTML modules are not covered. Every problem it can repair\nunambiguously — a missing brace, an extra brace, a smart quote or a non-breaking space that\nWord substituted inside a tag — is applied to a **copy** of the template, which is uploaded\nand returned as `fixed_document`.\n\nThe original template is never modified. Accepting the fix is an explicit, separate step:\nthe caller shows the user `issues` (each with its `before`/`after`), lets them download\n`fixed_document.preview_url`, and only then replaces the template.\n\nSupported input document types:\n- .docx, .docm, .dotx\n- .xlsx, .xlsm\n- .pptx\n", + "tags": [ + "Documents" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TemplateValidationRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Validation result, with the hotfixed template when one could be produced", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TemplateValidationResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "413": { + "description": "Template larger than the validator accepts (25 MB)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "415": { + "description": "Unsupported media type", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + } + } + } } }, "components": { @@ -330,15 +397,162 @@ } }, "ErrorCode": { - "description": "Error codes for document generation:\n- PARSE_ERROR - Error while parsing the document. Normally related with a bad template using the wrong DocxTemplater syntax.\n- DOC_TO_PDF_CONVERT_ERROR - Error while converting the document to PDF. Normally related with a ConvertAPI failure.\n- INTERNAL_ERROR - Internal error. Please contact support.\n- INVALID_TEMPLATE_FORMAT - Invalid template format (only .docx is supported). This can happen due to a bad word file or an unsupported file extension.\n", + "description": "Error codes for document generation:\n- PARSE_ERROR - Error while parsing the document. Normally related with a bad template using the wrong DocxTemplater syntax.\n- DOC_TO_PDF_CONVERT_ERROR - Error while converting the document to PDF. Normally related with a ConvertAPI failure.\n- INTERNAL_ERROR - Internal error. Please contact support.\n- INVALID_TEMPLATE_FORMAT - Invalid template format (only .docx is supported). This can happen due to a bad word file or an unsupported file extension.\n- TEMPLATE_NOT_FOUND - Template file was not found in S3. This indicates the template was likely deleted.\n", "type": "string", "enum": [ "PARSE_ERROR", "DOC_TO_PDF_CONVERT_ERROR", "INTERNAL_ERROR", - "INVALID_TEMPLATE_FORMAT" + "INVALID_TEMPLATE_FORMAT", + "TEMPLATE_NOT_FOUND" ] }, + "TemplateValidationRequest": { + "type": "object", + "required": [ + "template_document" + ], + "properties": { + "template_document": { + "description": "Input template document", + "type": "object", + "required": [ + "s3ref" + ], + "properties": { + "filename": { + "type": "string", + "description": "Document original filename, used to name the fixed copy", + "example": "Umzugsmeldung.xlsx" + }, + "s3ref": { + "$ref": "#/components/schemas/S3Reference" + } + } + }, + "fix": { + "description": "Attempt to produce a hotfixed copy of the template. When false, the template is only\ninspected and no file is written.\n", + "type": "boolean", + "default": true + }, + "fix_level": { + "description": "How far the hotfix may go:\n- safe - only repairs where the author's intent is unambiguous from the syntax\n (unbalanced or duplicated braces, editor-substituted characters inside a tag).\n- aggressive - additionally promotes single-brace placeholders such as `{contact.name}`\n to `{{contact.name}}`. These are common in customer templates but indistinguishable\n from prose that uses braces, so they are reported with `confidence: low`.\n", + "type": "string", + "enum": [ + "safe", + "aggressive" + ], + "default": "safe" + } + } + }, + "TemplateValidationResponse": { + "type": "object", + "properties": { + "valid": { + "description": "Whether the template compiled cleanly before any repair was attempted", + "type": "boolean", + "example": false + }, + "fixed": { + "description": "Whether a corrected copy of the template could be produced", + "type": "boolean", + "example": true + }, + "issues": { + "description": "Everything found in the template, whether or not it could be repaired", + "type": "array", + "items": { + "$ref": "#/components/schemas/TemplateIssue" + } + }, + "unresolved_errors": { + "description": "Parser errors that remain after the hotfix (or the original errors when nothing was\nfixed). Empty when the template is valid.\n", + "type": "array", + "items": { + "$ref": "#/components/schemas/DocxTemplaterErrorDetail" + } + }, + "fixed_document": { + "description": "The corrected copy, for the user to review and accept. Absent when no repair was\napplied. The original template is left untouched.\n", + "type": "object", + "properties": { + "s3ref": { + "$ref": "#/components/schemas/S3Reference" + }, + "filename": { + "type": "string", + "example": "Umzugsmeldung (fixed).xlsx" + }, + "preview_url": { + "description": "Short-lived download link for the corrected copy", + "type": "string", + "format": "uri" + } + } + } + } + }, + "TemplateIssue": { + "type": "object", + "description": "A single template syntax problem, and the repair proposed for it", + "properties": { + "id": { + "description": "Problem kind. Reuses docxtemplater's error vocabulary where one exists\n(`unopened_tag`, `unclosed_tag`, `duplicate_open_tag`, `duplicate_close_tag`), plus\n`malformed_tag` for single-brace placeholders and `typographic_characters` for\neditor-substituted characters inside an otherwise valid tag.\n", + "type": "string", + "example": "unopened_tag" + }, + "file": { + "description": "Zip part the problem was found in", + "type": "string", + "example": "xl/sharedStrings.xml" + }, + "location": { + "description": "Where the user can find it — a cell reference for spreadsheets, a part label\n(`Document body`, `Header 1`, `Slide 3`) otherwise.\n", + "type": "string", + "example": "Tabelle1!N4" + }, + "context": { + "description": "The offending text with surrounding context", + "type": "string", + "example": "…Datum: {system.date}} Unterschrift…" + }, + "explanation": { + "description": "Why the template parser rejects it", + "type": "string", + "example": "The tag is missing an opening brace." + }, + "fixable": { + "description": "Whether the proposed repair was applied to the fixed copy", + "type": "boolean", + "example": true + }, + "confidence": { + "description": "How sure we are that the repair is what the author meant", + "type": "string", + "enum": [ + "high", + "medium", + "low" + ] + }, + "rule": { + "description": "The repair rule that produced `after`", + "type": "string", + "example": "balance_opening_delimiter" + }, + "before": { + "description": "The text as it is in the template", + "type": "string", + "example": "{system.date}}" + }, + "after": { + "description": "The text as it would be in the fixed copy", + "type": "string", + "example": "{{system.date}}" + } + } + }, "TemplateSettings": { "type": "object", "description": "Template Settings for document generation", @@ -404,7 +618,7 @@ "description": "The file entity id, used when persisting a new template version with updated settings", "type": "string", "format": "uuid", - "example": "1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p" + "example": "123e4567-e89b-12d3-a456-426614174000" } } }, @@ -510,7 +724,7 @@ "user_id": { "type": "string", "description": "User Id for variable context", - "example": 100321 + "example": "100321" }, "language": { "type": "string", diff --git a/packages/cli/README.md b/packages/cli/README.md index 5e26458c1..49c2dfa08 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -29,7 +29,7 @@ npm install -g @epilot/cli ``` -epilot v0.1.112 — CLI for epilot APIs +epilot v0.1.168 — CLI for epilot APIs USAGE epilot [params...] [flags] @@ -71,6 +71,7 @@ APIs billing Billing API blueprint-manifest Blueprint Manifest API calendar Calendar API + chat epilot Chat API configuration-hub Configuration Hub API consent Consent API customer-portal Portal API @@ -328,6 +329,7 @@ Full documentation with sample calls and responses for all APIs: | Billing API | `epilot billing` | [docs](./docs/billing.md) | | Blueprint Manifest API | `epilot blueprint-manifest` | [docs](./docs/blueprint-manifest.md) | | Calendar API | `epilot calendar` | [docs](./docs/calendar.md) | +| epilot Chat API | `epilot chat` | [docs](./docs/chat.md) | | Configuration Hub API | `epilot configuration-hub` | [docs](./docs/configuration-hub.md) | | Consent API | `epilot consent` | [docs](./docs/consent.md) | | Portal API | `epilot customer-portal` | [docs](./docs/customer-portal.md) | diff --git a/packages/cli/definitions/access-token.json b/packages/cli/definitions/access-token.json index 0bf18366b..6bd0c3c9a 100644 --- a/packages/cli/definitions/access-token.json +++ b/packages/cli/definitions/access-token.json @@ -86,6 +86,20 @@ "739224:e5c1f9b1-e41d-421d-83c4-c5626e464430" ] } + }, + "contact_identification": { + "value": { + "name": "Contact Identification Token", + "token_type": "contact_identification", + "portal_id": "portal_abc123", + "contact_id": "5da0a718-c822-403d-9f5d-20d4584e0528", + "surface_id": "website-journeys", + "allowed_operations": [ + "getContact", + "getContracts" + ], + "expires_in": 300 + } } } } @@ -495,6 +509,98 @@ } } } + }, + "/v1/access-tokens/contact-identification/.well-known/jwks.json": { + "get": { + "operationId": "getContactIdentificationTokenJwks", + "summary": "getContactIdentificationTokenJwks", + "description": "Get jwks public key set to verify contact identification tokens generated by this API", + "tags": [ + "Public" + ], + "security": [], + "responses": { + "200": { + "description": "Set of jwks", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "keys": { + "type": "array", + "items": { + "type": "object", + "properties": { + "alg": { + "type": "string", + "example": "RS256" + }, + "e": { + "type": "string", + "example": "AQAB" + }, + "kid": { + "type": "string", + "example": "tXWU5mPMbRPczpbQwi6vbhLF4GgF3wlMDSyqo7pfeiw=" + }, + "kty": { + "type": "string", + "example": "RSA" + }, + "n": { + "type": "string", + "example": "h_QDoCjZ8W_trtYXaP7_S22wf5r5Wd9XBLED78oT44bJjQXn8ddcFV8Hik65_4IYXVX_hTTU4zpxe3H8vx2j7-Zz3O59mYMp5S0MzODNEdf5Y_2o19eis0brmAJniixsNlQ9LlYkdrVamrgaxHu3ZpP_99zkfFybYeuYoQNzb3PyrT8xVnz_USs_nlFMHpGUxvvz7gfKPqxcLvgLJr4cwI9yzaSY9CD4qW181QVcnL_WzpQ8xx6AuhhHZQ1l_3GG4InTk8ahE7U2ZHVu8RrX6d01pMgc3piEcet9RgFLnhbTg3YIiKGoAbN42wJn_x3lgIAC42T9mbmTsHyUdS6nUQ" + }, + "use": { + "type": "string", + "example": "sig" + } + } + } + } + } + } + } + } + } + } + } + }, + "/v1/access-tokens/contact-identification/.well-known/openid-configuration": { + "get": { + "operationId": "getContactIdentificationTokenOIDC", + "summary": "getContactIdentificationTokenOIDC", + "description": "OpenID Connect configuration for Access Token API as a contact identification identity provider\n\nNote: This API is not a fully compliant OAuth2.0 / OIDC identity provider, but this endpoint is useful to\nautomate the process of verifying JWT tokens.\n", + "tags": [ + "Public" + ], + "security": [], + "responses": { + "200": { + "description": "OpenID Configuration", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "issuer": { + "type": "string", + "format": "uri", + "example": "https://access-token.sls.epilot.io/v1/access-tokens/contact-identification" + }, + "jwks_uri": { + "type": "string", + "format": "uri", + "example": "https://access-token.sls.epilot.io/v1/access-tokens/contact-identification/.well-known/jwks.json" + } + } + } + } + } + } + } + } } }, "components": { @@ -535,7 +641,8 @@ "portal", "assume", "app", - "portal_preview" + "portal_preview", + "contact_identification" ], "description": "Access token type", "example": "api" @@ -552,6 +659,22 @@ "type": "string", "description": "Portal User ID for access token type \"portal_preview\"" }, + "ContactId": { + "type": "string", + "description": "Contact entity ID for access token type \"contact_identification\"", + "example": "5da0a718-c822-403d-9f5d-20d4584e0528" + }, + "AllowedOperations": { + "type": "array", + "description": "openapi operationIds the token may call. Enforced by the API that consumes the token, which must additionally deny any operation not on this list. Baked into the token at issue time so widening the consumer's own allowlist later cannot retroactively widen a token that is already in circulation.", + "items": { + "type": "string" + }, + "example": [ + "getContact", + "getContracts" + ] + }, "TokenParameters": { "anyOf": [ { @@ -571,6 +694,9 @@ }, { "$ref": "#/components/schemas/PortalPreviewTokenParameters" + }, + { + "$ref": "#/components/schemas/ContactIdentificationTokenParameters" } ] }, @@ -579,10 +705,10 @@ { "type": "integer", "format": "int32", - "description": "Expiration time in seconds", + "description": "Expiration time in seconds. api tokens may live up to 365 days (31536000); all other token types are capped at 7 days (604800) server-side.", "example": 3600, "minimum": 30, - "maximum": 604800 + "maximum": 31536000 }, { "type": "string", @@ -597,6 +723,12 @@ "description": "When true, the issued token may only perform read-only actions. Any action guarded by permissions that is not read-only (i.e. not a view/export/download action) is denied, regardless of the roles the token carries.", "example": true }, + "Anonymize": { + "type": "boolean", + "default": false, + "description": "Forces PII anonymization on all entity data returned to this token. Cannot be disabled by the token bearer. See entity-api anonymized responses.", + "example": true + }, "AccessTokenParameters": { "type": "object", "additionalProperties": false, @@ -618,6 +750,9 @@ }, "read_only": { "$ref": "#/components/schemas/ReadOnly" + }, + "anonymize": { + "$ref": "#/components/schemas/Anonymize" } }, "required": [ @@ -692,6 +827,9 @@ }, "read_only": { "$ref": "#/components/schemas/ReadOnly" + }, + "anonymize": { + "$ref": "#/components/schemas/Anonymize" } }, "required": [ @@ -719,6 +857,9 @@ }, "read_only": { "$ref": "#/components/schemas/ReadOnly" + }, + "anonymize": { + "$ref": "#/components/schemas/Anonymize" } }, "required": [ @@ -751,6 +892,58 @@ "portal_user_id" ] }, + "ContactIdentificationTokenParameters": { + "type": "object", + "additionalProperties": false, + "description": "A token that acts as one contact, issued after that contact was identified by a portal's registration identifiers rather than by logging in. Distinct from portal_preview, which impersonates an existing portal user for a 360 operator: this kind belongs to no user, is minted for an unauthenticated caller, and is therefore always short-lived and restricted to an explicit operation allowlist. Never stored, so it cannot be listed or revoked - keep expires_in short.", + "properties": { + "name": { + "$ref": "#/components/schemas/AccessTokenName" + }, + "token_type": { + "type": "string", + "enum": [ + "contact_identification" + ] + }, + "portal_id": { + "$ref": "#/components/schemas/PortalId" + }, + "contact_id": { + "$ref": "#/components/schemas/ContactId" + }, + "surface_id": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Portal surface the token is issued for (see `surfaces` on the portal config in customer-portal-api). Carried as the `custom:surface_id` claim; the consuming API resolves the surface's data access from the portal config on every request.", + "example": "website-journeys" + }, + "allowed_operations": { + "$ref": "#/components/schemas/AllowedOperations" + }, + "expires_in": { + "type": "integer", + "format": "int32", + "minimum": 60, + "maximum": 900, + "description": "Lifetime in seconds. Integer only for this token type (no \"5m\" strings), and capped server-side by CONTACT_IDENTIFICATION_TOKEN_MAX_EXPIRATION_SECONDS - the value originates from portal configuration, so it is not trusted as-is.", + "example": 300 + }, + "email": { + "type": "string", + "description": "Optional. Carried as the `email` claim when the consumer needs it (e.g. to attribute an activity). Omit to keep the contact's email out of the token." + } + }, + "required": [ + "name", + "portal_id", + "contact_id", + "surface_id", + "allowed_operations", + "expires_in" + ] + }, "AccessTokenItem": { "type": "object", "properties": { @@ -776,17 +969,32 @@ "portal_user_id": { "$ref": "#/components/schemas/PortalUserId" }, + "contact_id": { + "$ref": "#/components/schemas/ContactId" + }, + "allowed_operations": { + "$ref": "#/components/schemas/AllowedOperations" + }, "assignments": { "$ref": "#/components/schemas/Assignments" }, "read_only": { "$ref": "#/components/schemas/ReadOnly" }, + "anonymize": { + "$ref": "#/components/schemas/Anonymize" + }, "last_used": { "type": "string", "format": "date", "description": "Last date the token was used (YYYY-MM-DD format, 1 day accuracy)", "example": "2026-02-24" + }, + "expires_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the token expires and stops being accepted (only set for tokens created with expires_in)", + "example": "2026-03-01T12:00:00.000Z" } }, "required": [ diff --git a/packages/cli/definitions/ai-agents.json b/packages/cli/definitions/ai-agents.json index 3152f38bb..d47d96ae5 100644 --- a/packages/cli/definitions/ai-agents.json +++ b/packages/cli/definitions/ai-agents.json @@ -2,7 +2,7 @@ "openapi": "3.0.0", "info": { "version": "3.0.0", - "title": "AI Agents API - OpenAPI 3.0", + "title": "AI Agents API", "description": "API for configuring and invoking AI agents in epilot platform", "termsOfService": "https://epilot.cloud/agb", "contact": { @@ -29,6 +29,14 @@ { "name": "Agent Execution", "description": "Execute AI agents and manage executions" + }, + { + "name": "Chat", + "description": "Streaming chat with AI agents" + }, + { + "name": "Conversations", + "description": "Manage conversation history" } ], "paths": { @@ -37,7 +45,7 @@ "tags": [ "Agents Configuration" ], - "summary": "Create Agent definition", + "summary": "createAgent", "description": "Creates a new custom agent. System skills cannot be created via this endpoint.", "operationId": "createAgent", "security": [ @@ -82,7 +90,7 @@ "tags": [ "Agents Configuration" ], - "summary": "List all agent configurations", + "summary": "listAgents", "description": "Lists agents from both system skills and custom agents.\nUse query parameters to filter by source, availability, or entity schema.\n", "operationId": "listAgents", "parameters": [ @@ -140,7 +148,7 @@ "tags": [ "Agents Configuration" ], - "summary": "Get the agent configuration by ID", + "summary": "getAgentById", "description": "Retrieves an agent by ID. Supports both:\n- System skill IDs (prefixed): \"skill:email-categorizer\"\n- Custom agent IDs (UUID): \"0336a235-9417-4dd8-894c-fe81285bba75\"\n", "operationId": "getAgentById", "security": [ @@ -185,7 +193,7 @@ "tags": [ "Agents Configuration" ], - "summary": "Update the agent configuration by ID", + "summary": "updateAgentById", "description": "Updates a custom agent. System skills cannot be updated via this endpoint.", "operationId": "updateAgentById", "security": [ @@ -240,7 +248,7 @@ "tags": [ "Agents Configuration" ], - "summary": "Delete the agent configuration by ID", + "summary": "deleteAgentById", "description": "Deletes a custom agent. System skills cannot be deleted via this endpoint.", "security": [ { @@ -280,7 +288,7 @@ "tags": [ "Agent Execution" ], - "summary": "Execute an agent", + "summary": "executeAgent", "description": "Executes an agent (system skill or custom agent).\nSupports both:\n- System skill IDs (prefixed): \"skill:email-categorizer\"\n- Custom agent IDs (UUID): \"0336a235-9417-4dd8-894c-fe81285bba75\"\n", "operationId": "executeAgent", "security": [ @@ -342,12 +350,80 @@ } } }, + "/v1/agents/{agent_id}/execute/stream": { + "post": { + "tags": [ + "Agent Execution" + ], + "summary": "executeAgentStream", + "description": "Executes an agent with real-time streaming of tokens and tool events.\nReturns Server-Sent Events (SSE) stream with token-by-token output,\ntool call progress, and completion status.\n\nUnlike the async `/execute` endpoint which returns immediately with\nan execution_id for polling, this endpoint streams all events in real-time.\n\nSupports both:\n- System skill IDs (prefixed): \"skill:email-categorizer\"\n- Custom agent IDs (UUID): \"0336a235-9417-4dd8-894c-fe81285bba75\"\n", + "operationId": "executeAgentStream", + "security": [ + { + "EpilotAuth": [] + } + ], + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "$ref": "#/components/schemas/AgentId" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExecuteAgentRequest" + } + } + } + }, + "responses": { + "200": { + "description": "SSE stream of execution events", + "content": { + "text/event-stream": { + "schema": { + "$ref": "#/components/schemas/StreamEvent" + } + } + } + }, + "400": { + "description": "Error while executing the agent", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Agent not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, "/v1/executions": { "get": { "tags": [ "Agent Execution" ], - "summary": "List executions", + "summary": "listExecutions", + "description": "List executions", "operationId": "listExecutions", "security": [ { @@ -410,7 +486,8 @@ "tags": [ "Agent Execution" ], - "summary": "Get execution by ID", + "summary": "getExecution", + "description": "Get execution by ID", "operationId": "getExecution", "security": [ { @@ -455,7 +532,8 @@ "tags": [ "Agent Execution" ], - "summary": "Cancel execution", + "summary": "cancelExecution", + "description": "Cancel execution", "operationId": "cancelExecution", "security": [ { @@ -512,7 +590,7 @@ "tags": [ "Agent Execution" ], - "summary": "Get execution trace/iterations", + "summary": "getExecutionTrace", "description": "Returns the step-by-step reasoning and tool calls for ReAct mode executions. Returns empty iterations array for direct mode executions.", "operationId": "getExecutionTrace", "security": [ @@ -555,12 +633,136 @@ } } }, + "/v1/executions/{execution_id}/feedback": { + "get": { + "tags": [ + "Agent Execution" + ], + "summary": "Get execution feedback", + "description": "Returns the authenticated user's feedback for an execution.", + "operationId": "getExecutionFeedback", + "security": [ + { + "EpilotAuth": [] + } + ], + "parameters": [ + { + "name": "execution_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Feedback state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExecutionFeedbackResponse" + } + } + } + }, + "404": { + "description": "Execution not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "put": { + "tags": [ + "Agent Execution" + ], + "summary": "Submit execution feedback", + "description": "Upserts thumbs up/down feedback and mirrors it to the execution's Langfuse trace.", + "operationId": "putExecutionFeedback", + "security": [ + { + "EpilotAuth": [] + } + ], + "parameters": [ + { + "name": "execution_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PutExecutionFeedbackRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Feedback recorded", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExecutionFeedbackResponse" + } + } + } + }, + "400": { + "description": "Invalid feedback", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Execution not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Execution cannot be rated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, "/v1/executions/{execution_id}/approve": { "post": { "tags": [ "Agent Execution" ], - "summary": "Approve pending action", + "summary": "approveExecution", "description": "Approves a pending tool action when execution is in waiting_approval status", "operationId": "approveExecution", "security": [ @@ -627,7 +829,7 @@ "tags": [ "Agent Execution" ], - "summary": "Reject pending action", + "summary": "rejectExecution", "description": "Rejects a pending tool action when execution is in waiting_approval status", "operationId": "rejectExecution", "security": [ @@ -689,65 +891,411 @@ } } } - } - }, - "components": { - "securitySchemes": { - "EpilotAuth": { - "type": "http", - "scheme": "bearer", - "description": "Authorization header with epilot OAuth2 bearer token. Supports RFC8725 for JWT validation.", - "bearerFormat": "JWT" - } }, - "schemas": { - "AgentId": { - "type": "string", - "description": "Agent identifier. Can be either:\n- System skill ID (prefixed): \"skill:email-categorizer\", \"skill:email-labeler\"\n- Custom agent UUID: \"0336a235-9417-4dd8-894c-fe81285bba75\"\n", - "example": "skill:email-categorizer" - }, - "CreateAgentRequest": { - "type": "object", - "required": [ - "name", - "system_prompt", - "execution_pattern" + "/v1/executions/{execution_id}/stream": { + "get": { + "tags": [ + "Agent Execution" ], - "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 100, - "example": "Email Reply Generator" - }, - "description": { - "type": "string", - "maxLength": 1000 - }, - "category": { - "$ref": "#/components/schemas/SkillCategory" - }, - "icon": { - "type": "string", - "example": "mail-reply" - }, - "system_prompt": { - "type": "string", - "minLength": 1, - "description": "Core LLM instructions" + "summary": "streamExecution", + "description": "Reconnects to an execution's event stream after approval. Replays missed events from event log and continues streaming if execution is still running.", + "operationId": "streamExecution", + "security": [ + { + "EpilotAuth": [] + } + ], + "parameters": [ + { + "name": "execution_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } }, - "tools": { - "type": "array", - "items": { - "type": "string" + { + "name": "from_sequence", + "in": "query", + "schema": { + "type": "integer" }, - "description": "Tool IDs this agent can use", - "example": [ - "entity.search", - "message.draft" - ] + "description": "Resume from this event sequence number (for reconnection)" + } + ], + "responses": { + "200": { + "description": "SSE stream of execution events", + "content": { + "text/event-stream": { + "schema": { + "$ref": "#/components/schemas/StreamEvent" + } + } + } }, - "model_config": { + "404": { + "description": "Execution not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/chat": { + "post": { + "tags": [ + "Chat" + ], + "summary": "chat", + "description": "Initiates a streaming chat session with an AI agent. Supports server-side conversation memory via conversationId or client-provided history via clientHistory.", + "operationId": "chat", + "security": [ + { + "EpilotAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChatRequest" + } + } + } + }, + "responses": { + "200": { + "description": "SSE stream of chat events", + "content": { + "text/event-stream": { + "schema": { + "$ref": "#/components/schemas/StreamEvent" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Agent not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/conversations": { + "get": { + "tags": [ + "Conversations" + ], + "summary": "listConversations", + "description": "Lists conversations for the authenticated user, sorted by most recent.", + "operationId": "listConversations", + "security": [ + { + "EpilotAuth": [] + } + ], + "parameters": [ + { + "name": "agent_id", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Filter by agent ID" + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "default": 20 + }, + "description": "Maximum number of conversations to return" + }, + { + "name": "cursor", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Pagination cursor" + } + ], + "responses": { + "200": { + "description": "List of conversations", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListConversationsResponse" + } + } + } + }, + "400": { + "description": "Error fetching conversations", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/conversations/{conversation_id}": { + "get": { + "tags": [ + "Conversations" + ], + "summary": "getConversation", + "description": "Retrieves a conversation and its message history.", + "operationId": "getConversation", + "security": [ + { + "EpilotAuth": [] + } + ], + "parameters": [ + { + "name": "conversation_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "message_limit", + "in": "query", + "schema": { + "type": "integer", + "default": 100 + }, + "description": "Maximum number of messages to return" + } + ], + "responses": { + "200": { + "description": "Conversation with messages", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConversationWithMessages" + } + } + } + }, + "404": { + "description": "Conversation not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Conversations" + ], + "summary": "deleteConversation", + "description": "Deletes a conversation and all its messages.", + "operationId": "deleteConversation", + "security": [ + { + "EpilotAuth": [] + } + ], + "parameters": [ + { + "name": "conversation_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "Conversation deleted" + }, + "404": { + "description": "Conversation not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/conversations/{conversation_id}/feedback": { + "post": { + "tags": [ + "Conversations" + ], + "summary": "submitConversationFeedback", + "description": "Records a thumbs up/down (with optional comment) for the assistant turn identified by its Langfuse trace id. The rating is persisted on the message and mirrored to Langfuse as a trace score.\n", + "operationId": "submitConversationFeedback", + "security": [ + { + "EpilotAuth": [] + } + ], + "parameters": [ + { + "name": "conversation_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubmitFeedbackRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Feedback recorded", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "feedback": { + "$ref": "#/components/schemas/MessageFeedback" + } + } + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Conversation or message not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + } + }, + "components": { + "securitySchemes": { + "EpilotAuth": { + "type": "http", + "scheme": "bearer", + "description": "Authorization header with epilot OAuth2 bearer token. Supports RFC8725 for JWT validation.", + "bearerFormat": "JWT" + } + }, + "schemas": { + "AgentId": { + "type": "string", + "description": "Agent identifier. Can be either:\n- System skill ID (prefixed): \"skill:email-categorizer\", \"skill:email-labeler\"\n- Custom agent UUID: \"0336a235-9417-4dd8-894c-fe81285bba75\"\n", + "example": "skill:email-categorizer" + }, + "CreateAgentRequest": { + "type": "object", + "required": [ + "name", + "system_prompt", + "execution_pattern" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "example": "Email Reply Generator" + }, + "description": { + "type": "string", + "maxLength": 1000 + }, + "category": { + "$ref": "#/components/schemas/SkillCategory" + }, + "icon": { + "type": "string", + "example": "mail-reply" + }, + "system_prompt": { + "type": "string", + "minLength": 1, + "description": "Core LLM instructions" + }, + "tools": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Tool IDs this agent can use", + "example": [ + "entity.search", + "message.draft" + ] + }, + "model_config": { "$ref": "#/components/schemas/ModelConfig" }, "max_iterations": { @@ -946,6 +1494,20 @@ "reason": { "type": "string", "description": "Optional reason for approval" + }, + "approved_action_ids": { + "type": "array", + "items": { + "type": "string" + }, + "description": "For batch approval - list of action IDs to approve. If not provided, all actions are approved." + }, + "rejected_action_ids": { + "type": "array", + "items": { + "type": "string" + }, + "description": "For batch approval - list of action IDs to reject. Actions not in approved_action_ids are implicitly rejected." } } }, @@ -1098,6 +1660,98 @@ "type": "string", "format": "date-time", "nullable": true + }, + "iterations": { + "type": "array", + "description": "Slim, labels-only step-progress projection of the ReAct iterations (empty for direct mode). Exposes only index, tool, status, and timestamp — raw thought, action input, and observation are intentionally excluded.", + "items": { + "$ref": "#/components/schemas/ExecutionIterationProjection" + } + } + } + }, + "ExecutionIterationProjection": { + "type": "object", + "properties": { + "index": { + "type": "integer" + }, + "tool": { + "type": "string", + "nullable": true, + "description": "Tool id the iteration invoked, or null for a non-tool step" + }, + "status": { + "type": "string", + "enum": [ + "running", + "completed" + ] + }, + "timestamp": { + "type": "string", + "format": "date-time" + } + } + }, + "ExecutionFeedback": { + "type": "object", + "required": [ + "rating", + "user_id", + "submitted_at" + ], + "properties": { + "rating": { + "type": "string", + "enum": [ + "up", + "down" + ] + }, + "comment": { + "type": "string" + }, + "user_id": { + "type": "string" + }, + "submitted_at": { + "type": "string", + "format": "date-time" + } + } + }, + "ExecutionFeedbackResponse": { + "type": "object", + "required": [ + "feedback" + ], + "properties": { + "feedback": { + "allOf": [ + { + "$ref": "#/components/schemas/ExecutionFeedback" + } + ], + "nullable": true + } + } + }, + "PutExecutionFeedbackRequest": { + "type": "object", + "required": [ + "rating" + ], + "properties": { + "rating": { + "type": "string", + "enum": [ + "up", + "down" + ] + }, + "comment": { + "type": "string" } } }, @@ -1223,8 +1877,107 @@ "duration_ms": { "type": "integer" }, - "iteration_count": { + "iteration_count": { + "type": "integer" + } + } + }, + "AgentExecutionEventStatus": { + "type": "string", + "description": "Lifecycle status carried on an agent execution event.\n\nDeliberately narrower than `ExecutionStatus`, which is the stored status of\nthe execution row: `pending` and `running` both surface as `started`, and\n`cancelled` surfaces as `failed`. Consumers must not assume the two\nvocabularies are interchangeable.\n", + "enum": [ + "started", + "completed", + "failed", + "approval_required" + ] + }, + "AgentExecutionEvent": { + "type": "object", + "description": "Detail payload of the `agent-execution-status-update` event, published on\nthe AI Agents event bus with source `ai-agents`.\n\n`execution_context` and `input` are present on every status, so a consumer\ncan tell whether an execution is theirs, and what it belonged to, from the\nterminal event alone — without a secondary lookup.\n", + "required": [ + "executionId", + "agentId", + "orgId", + "execution_status" + ], + "properties": { + "executionId": { + "type": "string", + "description": "The agent execution this event reports on" + }, + "agentId": { + "$ref": "#/components/schemas/AgentId" + }, + "orgId": { + "type": "string" + }, + "userId": { + "type": "string" + }, + "execution_status": { + "$ref": "#/components/schemas/AgentExecutionEventStatus" + }, + "execution_context": { + "$ref": "#/components/schemas/ExecutionContext" + }, + "input": { + "type": "object", + "additionalProperties": true, + "description": "The execution input, as supplied by the caller of the execute endpoint" + }, + "output": { + "type": "object", + "additionalProperties": true, + "description": "Present on `completed` — the execution result" + }, + "error": { + "type": "object", + "description": "Present on `failed`", + "properties": { + "message": { + "type": "string" + }, + "code": { + "type": "string" + }, + "stack": { + "type": "string" + } + } + }, + "duration": { + "type": "integer", + "description": "Wall-clock duration in milliseconds, present on `completed`" + }, + "iterations": { + "type": "integer" + }, + "iteration": { "type": "integer" + }, + "message": { + "type": "string", + "description": "Present on `approval_required` — what is awaiting approval" + }, + "toolCalls": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "args": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "timestamp": { + "type": "string", + "format": "date-time" } } }, @@ -1286,7 +2039,8 @@ "apply", "send", "link", - "unlink" + "unlink", + "batch_approval" ], "description": "Type of action being previewed" }, @@ -1414,7 +2168,7 @@ "type": "string", "enum": [ "entity", - "email", + "message", "taxonomy", "rag", "workflow" @@ -1462,9 +2216,10 @@ "enum": [ "flows", "copilot", + "portals", "all" ], - "description": "Where the skill/agent is available:\n- flows: Available in workflow automations\n- copilot: Available as a sub-agent in copilot\n- all: Available everywhere\n" + "description": "Where the skill/agent is available:\n- flows: Available in workflow automations\n- copilot: Available as a sub-agent in copilot\n- portals: Available in end-user self-service portals\n- all: Available everywhere\n" }, "ExecutionContext": { "type": "string", @@ -1517,9 +2272,10 @@ "taxonomy", "taxonomy-classification", "shared-inbox", - "label" + "label", + "matching-criteria" ], - "description": "Base types:\n- text: Text input field\n- textarea: Multi-line text input field\n- number: Numeric input field\n- boolean: Toggle switch\n- select: Dropdown selection (requires enum array)\n\nCustom types (domain-specific):\n- entity-schema: Entity schema selector (fetches from Entity API)\n- entity-attribute: Entity attribute selector (requires dependsOn)\n- entity-id: Entity picker (search and select entities)\n- taxonomy: Taxonomy selector (fetches from Taxonomy API)\n- taxonomy-classification: Classification selector (requires dependsOn)\n- shared-inbox: Shared inbox selector (fetches from Email Settings API)\n" + "description": "Base types:\n- text: Text input field\n- textarea: Multi-line text input field\n- number: Numeric input field\n- boolean: Toggle switch\n- select: Dropdown selection (requires enum array)\n\nCustom types (domain-specific):\n- entity-schema: Entity schema selector (fetches from Entity API)\n- entity-attribute: Entity attribute selector (requires dependsOn)\n- entity-id: Entity picker (search and select entities)\n- taxonomy: Taxonomy selector (fetches from Taxonomy API)\n- taxonomy-classification: Classification selector (requires dependsOn)\n- shared-inbox: Shared inbox selector (fetches from Email Settings API)\n- matching-criteria: Criteria editor for mapping compared fields between two contexts or entities\n" }, "InputParameterDefinition": { "type": "object", @@ -1605,6 +2361,11 @@ "type": "string" }, "description": "Filter to specific attribute types (for entity-attribute)" + }, + "hidden": { + "type": "boolean", + "default": false, + "description": "Whether to hide the parameter from the UI" } } }, @@ -1689,6 +2450,18 @@ "type": "integer", "default": 4096, "maximum": 8192 + }, + "thinking": { + "type": "boolean", + "default": false, + "description": "Enable extended thinking/reasoning for the model" + }, + "thinking_budget": { + "type": "integer", + "default": 10000, + "minimum": 1000, + "maximum": 50000, + "description": "Token budget for extended thinking (only used when thinking is enabled)" } } }, @@ -1705,6 +2478,385 @@ "type": "object" } } + }, + "ChatRequest": { + "type": "object", + "required": [ + "agentId", + "message" + ], + "properties": { + "agentId": { + "type": "string", + "description": "Agent ID to chat with" + }, + "message": { + "type": "string", + "description": "User message" + }, + "conversationId": { + "type": "string", + "format": "uuid", + "description": "Conversation ID for server-side memory. If provided, loads history from DynamoDB." + }, + "clientHistory": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChatMessage" + }, + "description": "Client-provided message history (overrides server-side memory)" + }, + "context": { + "type": "object", + "properties": { + "entityId": { + "type": "string" + }, + "customData": { + "type": "object", + "additionalProperties": true + } + } + }, + "streaming": { + "$ref": "#/components/schemas/StreamingOptions" + } + } + }, + "ChatMessage": { + "type": "object", + "required": [ + "role", + "content" + ], + "properties": { + "role": { + "type": "string", + "enum": [ + "user", + "assistant", + "tool", + "system" + ] + }, + "content": { + "type": "string" + }, + "tool_calls": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "input": { + "type": "object" + }, + "output": { + "type": "string" + } + } + } + } + } + }, + "StreamingOptions": { + "type": "object", + "properties": { + "mode": { + "type": "string", + "enum": [ + "updates", + "messages" + ], + "default": "updates", + "description": "Stream mode - updates for step-based, messages for token-based" + }, + "streamTokens": { + "type": "boolean", + "default": false, + "description": "Enable token-level streaming" + }, + "includeMetadata": { + "type": "boolean", + "default": false, + "description": "Include metadata events" + } + } + }, + "StreamEvent": { + "type": "object", + "description": "Server-Sent Event for streaming responses", + "properties": { + "type": { + "type": "string", + "enum": [ + "token", + "agent_step", + "tool_call", + "tool_result", + "complete", + "error", + "metadata", + "needs_approval" + ] + }, + "content": { + "type": "string", + "description": "Token content (for token events)" + }, + "index": { + "type": "integer", + "description": "Token index (for token events)" + }, + "tool": { + "type": "string", + "description": "Tool name (for tool events)" + }, + "input": { + "type": "object", + "description": "Tool input (for tool_call events)" + }, + "output": { + "type": "string", + "description": "Tool output (for tool_result events)" + }, + "callId": { + "type": "string", + "description": "Tool call ID" + }, + "result": { + "$ref": "#/components/schemas/ExecutionResult" + }, + "error": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "conversationId": { + "type": "string", + "format": "uuid", + "description": "Conversation ID (included in complete events)" + } + } + }, + "ExecutionResult": { + "type": "object", + "properties": { + "response": { + "type": "string" + }, + "structured_output": { + "type": "object" + }, + "status": { + "type": "string", + "enum": [ + "completed", + "failed", + "max_iterations", + "rejected" + ] + }, + "metrics": { + "$ref": "#/components/schemas/ExecutionMetrics" + } + } + }, + "ConversationItem": { + "type": "object", + "properties": { + "conversation_id": { + "type": "string", + "format": "uuid" + }, + "user_id": { + "type": "string" + }, + "agent_id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "message_count": { + "type": "integer" + }, + "last_message": { + "type": "string", + "description": "Preview of the last message" + }, + "last_message_at": { + "type": "string", + "format": "date-time" + }, + "context": { + "type": "object", + "properties": { + "entityId": { + "type": "string" + }, + "customData": { + "type": "object" + } + } + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + } + }, + "MessageItem": { + "type": "object", + "properties": { + "conversation_id": { + "type": "string", + "format": "uuid" + }, + "role": { + "type": "string", + "enum": [ + "user", + "assistant", + "tool", + "system" + ] + }, + "content": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "tool_calls": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "input": { + "type": "object" + }, + "output": { + "type": "string" + } + } + } + }, + "token_count": { + "type": "integer" + }, + "trace_id": { + "type": "string", + "format": "uuid", + "description": "Langfuse trace id for the turn (assistant messages only)" + }, + "feedback": { + "$ref": "#/components/schemas/MessageFeedback" + } + } + }, + "MessageFeedback": { + "type": "object", + "required": [ + "rating", + "user_id", + "submitted_at" + ], + "properties": { + "rating": { + "type": "string", + "enum": [ + "up", + "down" + ] + }, + "comment": { + "type": "string" + }, + "user_id": { + "type": "string" + }, + "submitted_at": { + "type": "string", + "format": "date-time" + } + } + }, + "SubmitFeedbackRequest": { + "type": "object", + "required": [ + "trace_id", + "rating" + ], + "properties": { + "trace_id": { + "type": "string", + "format": "uuid", + "description": "Langfuse trace id of the assistant turn being rated" + }, + "rating": { + "type": "string", + "enum": [ + "up", + "down" + ] + }, + "comment": { + "type": "string", + "description": "Optional free-text feedback" + } + } + }, + "ListConversationsResponse": { + "type": "object", + "properties": { + "conversations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConversationItem" + } + }, + "next_cursor": { + "type": "string", + "nullable": true + } + } + }, + "ConversationWithMessages": { + "type": "object", + "properties": { + "conversation": { + "$ref": "#/components/schemas/ConversationItem" + }, + "messages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessageItem" + } + } + } } } } diff --git a/packages/cli/definitions/app.json b/packages/cli/definitions/app.json index e8b95fadd..a32a70134 100644 --- a/packages/cli/definitions/app.json +++ b/packages/cli/definitions/app.json @@ -511,6 +511,7 @@ } ], "requestBody": { + "required": true, "content": { "application/json": { "schema": { @@ -1150,6 +1151,9 @@ "description": "Target version to create" } ], + "requestBody": { + "$ref": "#/components/requestBodies/CloneVersionRequest" + }, "responses": { "201": { "description": "New version created successfully", @@ -1438,6 +1442,70 @@ } } }, + "/v1/app/{appId}/options/resolve": { + "post": { + "summary": "resolveOptions", + "description": "Resolve the effective app-level options of an installation, including decrypted sensitive values (secrets). This endpoint accepts epilot internal-auth tokens exclusively (internal service calls, e.g. automation-workers or the customer portal API) — no user token, API key, or app token can call it, regardless of roles. Every call is audit-logged with the caller identity.", + "operationId": "resolveOptions", + "tags": [ + "App Installation" + ], + "parameters": [ + { + "name": "appId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "keys": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Narrow the response to these option keys" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Effective options with resolved values", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Options" + } + } + } + } + } + } + }, + "403": { + "description": "Caller is not authenticated with an internal-auth token" + }, + "404": { + "description": "App installation not found" + } + } + } + }, "/v1/app-events": { "post": { "summary": "ingestEvent", @@ -1501,6 +1569,18 @@ "type": "string" }, "description": "Path to forward to the proxy target" + }, + { + "name": "query", + "in": "query", + "required": false, + "style": "form", + "explode": true, + "schema": { + "type": "object", + "additionalProperties": true + }, + "description": "Free-form query parameters, forwarded unchanged to the proxy target" } ], "get": { @@ -1565,6 +1645,105 @@ "description": "Bad gateway - proxy target unreachable or error" } } + }, + "put": { + "summary": "publicProxyPut", + "operationId": "publicProxyPut", + "description": "Forward a PUT request to a registered proxy target from a public-facing component", + "tags": [ + "App Proxy" + ], + "security": [ + { + "EpilotAuth": [] + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Proxied response from the target API" + }, + "403": { + "description": "Forbidden - app not installed or no public component uses this proxy" + }, + "404": { + "description": "Proxy target not found" + }, + "502": { + "description": "Bad gateway - proxy target unreachable or error" + } + } + }, + "patch": { + "summary": "publicProxyPatch", + "operationId": "publicProxyPatch", + "description": "Forward a PATCH request to a registered proxy target from a public-facing component", + "tags": [ + "App Proxy" + ], + "security": [ + { + "EpilotAuth": [] + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Proxied response from the target API" + }, + "403": { + "description": "Forbidden - app not installed or no public component uses this proxy" + }, + "404": { + "description": "Proxy target not found" + }, + "502": { + "description": "Bad gateway - proxy target unreachable or error" + } + } + }, + "delete": { + "summary": "publicProxyDelete", + "operationId": "publicProxyDelete", + "description": "Forward a DELETE request to a registered proxy target from a public-facing component", + "tags": [ + "App Proxy" + ], + "security": [ + { + "EpilotAuth": [] + } + ], + "responses": { + "200": { + "description": "Proxied response from the target API" + }, + "403": { + "description": "Forbidden - app not installed or no public component uses this proxy" + }, + "404": { + "description": "Proxy target not found" + }, + "502": { + "description": "Bad gateway - proxy target unreachable or error" + } + } } } }, @@ -1713,6 +1892,42 @@ }, "grants": { "$ref": "#/components/schemas/Grants" + }, + "functions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FunctionDefinition" + }, + "description": "Replaces the full set of server-side functions for this version" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Options" + }, + "description": "Replaces the full set of app-level option declarations for this version.\nOption keys must be unique app-wide; `secret`-touching options are always\nsensitive and must not declare `sensitive: false`.\n" + }, + "changelog": { + "type": "string", + "maxLength": 250, + "description": "Short description of what changed in this version" + } + } + } + } + } + }, + "CloneVersionRequest": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "changelog": { + "type": "string", + "maxLength": 250, + "description": "Short description of what the new version will change" } } } @@ -1760,6 +1975,7 @@ } }, "InstallRequest": { + "required": true, "content": { "application/json": { "schema": { @@ -1820,7 +2036,7 @@ }, "Options": { "type": "object", - "description": "Options for the component configuration", + "description": "An option declaration — a setting the installing org fills in. Declared at app level.", "required": [ "key", "type" @@ -1866,6 +2082,25 @@ "object" ], "description": "The type of this option. `object` declares a structured value whose fields are listed\nunder `fields`. Combine with `repeatable: true` to express a list of these objects.\n" + }, + "sensitive": { + "type": "boolean", + "description": "Write-only, server-side only value — like a sensitive environment variable\n(app-level options only). Sensitive values are never serialized in any response;\nthey are resolvable only via server-side channels (API proxy injection, function\nruns, the internal options/resolve endpoint). Non-sensitive values\nare readable wherever the app runs, including the end-customer browser (journey and\nportal runtime). Forced to true for `secret`-touching options. Default: false.\n" + }, + "configured": { + "type": "boolean", + "readOnly": true, + "description": "Serialized instead of `value` for sensitive options (secrets): true when a\nvalue is stored.\n" + }, + "lifted": { + "type": "boolean", + "readOnly": true, + "description": "Compatibility marker: this option is declared at app level and folded back\ninto the component's options so existing consumers (journey runtime, portal\nblocks, older services) keep working unchanged. New consumers should read\napp-level options from the installation's `options` array instead.\n" + }, + "value_updated_at": { + "type": "string", + "readOnly": true, + "description": "Timestamp of the last value change. Only returned for sensitive options\n(alongside `configured`) so installers can see when a secret was last rotated.\n" } } }, @@ -1930,7 +2165,7 @@ "properties": { "component_id": { "type": "string", - "description": "ID of the component these values are for" + "description": "ID of the component these values are for. Use the sentinel `$app` for\napp-level option values (options declared at the manifest top level).\n" }, "options": { "type": "array", @@ -1970,10 +2205,88 @@ "CUSTOM_CAPABILITY", "EXTERNAL_PRODUCT_CATALOG", "CUSTOM_PAGE", - "API_PROXY" + "API_PROXY", + "EXTERNAL_VALUES", + "APP_FUNCTION" ], "description": "Type of app component" }, + "FunctionDefinition": { + "type": "object", + "description": "A named server-side function belonging to the app. Runs in the epilot code-execution sandbox with an installation-scoped app token. Functions with a schedule are executed automatically once per installation.\n", + "required": [ + "name", + "type", + "code" + ], + "properties": { + "name": { + "type": "string", + "description": "Unique function name within the app", + "pattern": "^[a-z0-9][a-z0-9-]{0,63}$" + }, + "type": { + "type": "string", + "enum": [ + "workflow", + "scheduled" + ], + "description": "Where the function can be used. `workflow` functions are referenced by CUSTOM_FLOW_ACTION components (type `function`) and run with entity context. `scheduled` functions run automatically per installation on their cron schedule.\n" + }, + "label": { + "allOf": [ + { + "description": "Human-readable display name of the function, shown to installing organizations (e.g. in the scheduled-functions summary)\n" + }, + { + "$ref": "#/components/schemas/TranslatedString" + } + ] + }, + "description": { + "allOf": [ + { + "description": "Description of the function" + }, + { + "$ref": "#/components/schemas/TranslatedString" + } + ] + }, + "code": { + "type": "string", + "maxLength": 307200, + "description": "JavaScript code to execute. Must declare a top-level `async function handler(input, context)`. Maximum size: 300KB (hard limit). Security restrictions: dynamic code evaluation via `eval` or the `Function` constructor is not allowed.\n" + }, + "schedule": { + "type": "string", + "maxLength": 100, + "description": "Cron trigger for the function, executed once per installation. Standard 5-field cron expression (e.g. \"0 3 * * *\") or rate expression (e.g. \"rate(30 minutes)\"). Minimum interval: 15 minutes. Scheduled runs are limited to 60 seconds.\n", + "example": "rate(30 minutes)" + }, + "schedule_timezone": { + "type": "string", + "description": "IANA timezone the cron expression is evaluated in", + "default": "Europe/Berlin" + }, + "schedule_overlap": { + "type": "string", + "enum": [ + "skip" + ], + "default": "skip", + "description": "Behavior when the previous scheduled run is still active" + }, + "secrets": { + "type": "array", + "items": { + "type": "string" + }, + "deprecated": true, + "description": "DEPRECATED and ignored — functions receive the app's full effective option keyspace (sensitive values included) via input.app_options. Kept for manifest compatibility.\n" + } + } + }, "Author": { "type": "object", "required": [ @@ -2060,6 +2373,13 @@ }, "description": "List of options for the app component" }, + "uses_options": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Keys of app-level options this component uses. Narrows which options are\nfolded back into `options` for legacy consumers (e.g. which values a journey\nblock receives in its public args) — a scoping hint, never a security\nboundary. Stamped automatically by the component-options migration; may also\nbe declared by the app developer.\n" + }, "surfaces": { "type": "object" } @@ -2083,7 +2403,8 @@ "CUSTOM_CAPABILITY": "#/components/schemas/CustomCapabilityComponent", "EXTERNAL_PRODUCT_CATALOG": "#/components/schemas/ExternalProductCatalogComponent", "CUSTOM_PAGE": "#/components/schemas/CustomPageComponent", - "API_PROXY": "#/components/schemas/ApiProxyComponent" + "API_PROXY": "#/components/schemas/ApiProxyComponent", + "EXTERNAL_VALUES": "#/components/schemas/ExternalValuesComponent" } }, "oneOf": [ @@ -2113,6 +2434,9 @@ }, { "$ref": "#/components/schemas/ApiProxyComponent" + }, + { + "$ref": "#/components/schemas/ExternalValuesComponent" } ] } @@ -2136,6 +2460,25 @@ } } }, + "ExternalValuesComponent": { + "type": "object", + "description": "Exposes typed values resolved from an external system at runtime (e.g. a meter reading prediction). Consumers such as validation rules reference a hook and one of its results by id. Resolution is performed by the external-values-api on behalf of authenticated epilot 360 users and portal end customers.\n", + "required": [ + "component_type", + "configuration" + ], + "properties": { + "component_type": { + "type": "string", + "enum": [ + "EXTERNAL_VALUES" + ] + }, + "configuration": { + "$ref": "#/components/schemas/ExternalValuesConfig" + } + } + }, "CustomCapabilityComponent": { "type": "object", "required": [ @@ -2157,7 +2500,8 @@ "type": "string", "enum": [ "tab", - "group" + "group", + "widget" ] }, "allowed_schemas": { @@ -2386,7 +2730,7 @@ } ] }, - "SandboxCustomActionConfig": { + "FunctionRefCustomActionConfig": { "allOf": [ { "$ref": "#/components/schemas/BaseCustomActionConfig" @@ -2397,41 +2741,37 @@ "type": { "type": "string", "enum": [ - "sandbox" + "function" ] }, - "sandbox_settings": { - "type": "object", - "properties": { - "code": { - "type": "string", - "description": "JavaScript code to execute for the sandbox action. Maximum size: 300KB (hard limit). Code is stored as raw JavaScript and will be syntax-validated on save. Security restrictions: eval() and Function() constructor are not allowed.\n", - "maxLength": 307200 - } - } + "function_name": { + "type": "string", + "description": "Name of a `workflow`-type function of the same app version. The component provides the org-facing contract (name, options, config surface); the referenced function provides the code.\n" } }, "required": [ - "type" + "type", + "function_name" ] } ] }, "CustomFlowConfig": { "type": "object", + "description": "Configuration of a flow action component. `external_integration` calls an external HTTP endpoint; `function` runs a workflow-type app function in the epilot sandbox.\n", "oneOf": [ { "$ref": "#/components/schemas/ExternalIntegrationCustomActionConfig" }, { - "$ref": "#/components/schemas/SandboxCustomActionConfig" + "$ref": "#/components/schemas/FunctionRefCustomActionConfig" } ], "discriminator": { "propertyName": "type", "mapping": { "external_integration": "#/components/schemas/ExternalIntegrationCustomActionConfig", - "sandbox": "#/components/schemas/SandboxCustomActionConfig" + "function": "#/components/schemas/FunctionRefCustomActionConfig" } } }, @@ -2651,7 +2991,10 @@ "dataExport": "#/components/schemas/PortalExtensionHookDataExport", "costDataRetrieval": "#/components/schemas/PortalExtensionHookCostDataRetrieval", "meterReadingPlausibilityCheck": "#/components/schemas/PortalExtensionHookMeterReadingPlausibilityCheck", - "visualizationMetadata": "#/components/schemas/PortalExtensionHookVisualizationMetadata" + "visualizationMetadata": "#/components/schemas/PortalExtensionHookVisualizationMetadata", + "changeEmail": "#/components/schemas/PortalExtensionHookChangeEmail", + "changePassword": "#/components/schemas/PortalExtensionHookChangePassword", + "deleteAccount": "#/components/schemas/PortalExtensionHookDeleteAccount" } }, "oneOf": [ @@ -2678,6 +3021,15 @@ }, { "$ref": "#/components/schemas/PortalExtensionHookVisualizationMetadata" + }, + { + "$ref": "#/components/schemas/PortalExtensionHookChangeEmail" + }, + { + "$ref": "#/components/schemas/PortalExtensionHookChangePassword" + }, + { + "$ref": "#/components/schemas/PortalExtensionHookDeleteAccount" } ] } @@ -3067,14 +3419,16 @@ "intervals": { "type": "array", "deprecated": true, - "description": "Deprecated. Prefer declaring a sibling `visualizationMetadata` hook on the same extension and returning `intervals` from its response — that way the supported intervals can vary per meter/contract.\nIntervals supported by the API. If omitted, it is assumed that all intervals are supported.\n", + "description": "Deprecated. Prefer declaring a sibling `visualizationMetadata` hook on the same extension and returning `intervals` from its response — that way the supported intervals can vary per meter/contract.\nIntervals supported by the API. If omitted, it is assumed that all intervals are supported.\n`custom` marks a period-based consumption source: records carry the `period` they cover instead of sitting on a fixed grid, and the portal shows the whole data range as one bar per record.\n", "items": { "type": "string", "enum": [ "PT15M", "PT1H", "P1D", - "P1M" + "P1M", + "P1Y", + "custom" ] } }, @@ -3178,14 +3532,16 @@ "intervals": { "type": "array", "deprecated": true, - "description": "Deprecated. Prefer declaring a sibling `visualizationMetadata` hook on the same extension and returning `intervals` from its response — that way the supported intervals can vary per meter/contract.\nIntervals supported by the API. If omitted, it is assumed that all intervals are supported.\n", + "description": "Deprecated. Prefer declaring a sibling `visualizationMetadata` hook on the same extension and returning `intervals` from its response — that way the supported intervals can vary per meter/contract.\nIntervals supported by the API. If omitted, it is assumed that all intervals are supported.\n`custom` marks a period-based consumption source: records carry the `period` they cover instead of sitting on a fixed grid, and the portal shows the whole data range as one bar per record.\n", "items": { "type": "string", "enum": [ "PT15M", "PT1H", "P1D", - "P1M" + "P1M", + "P1Y", + "custom" ] } }, @@ -3364,7 +3720,7 @@ "additionalProperties": false }, "PortalExtensionHookVisualizationMetadata": { - "description": "Hook that returns runtime metadata describing how a visualization (consumption / price / cost chart) should be rendered for a given portal context (meter, contract, etc). It is invoked by the portal before fetching data, with the same context the data hook would receive, so that the discovery shape can vary per meter/contract. The expected response to the call is:\n - 200 with a JSON body of shape:\n {\n \"type_options\": [\n { \"id\": \"ht\", \"label\": { \"en\": \"High tariff\" }, \"aggregation_group\": \"consumption\", \"statistical_method\": \"sum\", \"unit\": \"kWh\", \"color\": \"primary\", \"precision\": 2 },\n ...\n ],\n \"intervals\": [\"PT15M\", \"PT1H\", \"P1D\", \"P1M\"],\n \"data_range\": { \"from\": \"2024-01-01T00:00:00Z\", \"to\": \"2026-05-01T00:00:00Z\" }\n }\n Each type option carries its own `statistical_method`, which describes the method already applied to that type's data and dictates the chart shape: `sum` is rendered as a bar chart; `min`, `average`, and `max` are rendered as a line chart. A single visualization can therefore mix bar-shaped types with line-shaped types. Defaults to `sum` when omitted.\n Each type option may also customize its rendering: `color` picks a Spark palette color (`primary`, `slate`, `mauve`, `orange`, `red`, `tomato`, `amber`, `green`, `blue`) used to draw the type's series; `precision` sets the number of decimal places to show for that type's values (axis labels, tooltips, summaries). Both are optional — the consumer falls back to its own defaults when they are omitted.\n `aggregation_group` controls how types within a group are visually combined (depends on the per-type `statistical_method`):\n - bar chart (`sum`): same-group types are stacked into a single bar (e.g. ht/nt summed into total consumption); different-group types render side-by-side.\n - line chart (`min` / `average` / `max`): same-group types are rendered as an area chart; different-group types render as separate lines.\n All fields are optional; the consumer falls back to its defaults for whatever the hook does not return.\nThe portal looks up this hook implicitly per extension (one `visualizationMetadata` hook per extension) — there is no need for a data-retrieval hook to reference it explicitly.\n", + "description": "Hook that returns runtime metadata describing how a visualization (consumption / price / cost chart) should be rendered for a given portal context (meter, contract, etc). It is invoked by the portal before fetching data, with the same context the data hook would receive, so that the discovery shape can vary per meter/contract. The expected response to the call is:\n - 200 with a JSON body of shape:\n {\n \"type_options\": [\n { \"id\": \"ht\", \"label\": { \"en\": \"High tariff\" }, \"aggregation_group\": \"consumption\", \"statistical_method\": \"sum\", \"unit\": \"kWh\", \"color\": \"primary\", \"precision\": 2 },\n ...\n ],\n \"intervals\": [\"PT15M\", \"PT1H\", \"P1D\", \"P1M\", \"P1Y\", \"custom\"],\n \"data_range\": { \"from\": \"2024-01-01T00:00:00Z\", \"to\": \"2026-05-01T00:00:00Z\" }\n }\n Each type option carries its own `statistical_method`, which describes the method already applied to that type's data and dictates the chart shape: `sum` is rendered as a bar chart; `min`, `average`, and `max` are rendered as a line chart. A single visualization can therefore mix bar-shaped types with line-shaped types. Defaults to `sum` when omitted.\n Each type option may also customize its rendering: `color` picks a Spark palette color (`primary`, `slate`, `mauve`, `orange`, `red`, `tomato`, `amber`, `green`, `blue`) used to draw the type's series; `precision` sets the number of decimal places to show for that type's values (axis labels, tooltips, summaries). Both are optional — the consumer falls back to its own defaults when they are omitted.\n `aggregation_group` controls how types within a group are visually combined (depends on the per-type `statistical_method`):\n - bar chart (`sum`): same-group types are stacked into a single bar (e.g. ht/nt summed into total consumption); different-group types render side-by-side.\n - line chart (`min` / `average` / `max`): same-group types are rendered as an area chart; different-group types render as separate lines.\n All fields are optional; the consumer falls back to its defaults for whatever the hook does not return.\nThe portal looks up this hook implicitly per extension (one `visualizationMetadata` hook per extension) — there is no need for a data-retrieval hook to reference it explicitly.\n", "type": "object", "properties": { "id": { @@ -3481,14 +3837,16 @@ "intervals": { "type": "array", "deprecated": true, - "description": "Deprecated. Prefer declaring a sibling `visualizationMetadata` hook on the same extension and returning `intervals` from its response — that way the supported intervals can vary per meter/contract.\nIntervals supported by the API. If omitted, it is assumed that all intervals are supported.\n", + "description": "Deprecated. Prefer declaring a sibling `visualizationMetadata` hook on the same extension and returning `intervals` from its response — that way the supported intervals can vary per meter/contract.\nIntervals supported by the API. If omitted, it is assumed that all intervals are supported.\n`custom` marks a period-based consumption source: records carry the `period` they cover instead of sitting on a fixed grid, and the portal shows the whole data range as one bar per record.\n", "items": { "type": "string", "enum": [ "PT15M", "PT1H", "P1D", - "P1M" + "P1M", + "P1Y", + "custom" ] } }, @@ -3571,6 +3929,344 @@ ], "additionalProperties": false }, + "PortalExtensionHookChangeEmail": { + "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": { + "id": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "Identifier of the hook. Should not change between updates." + }, + "name": { + "$ref": "#/components/schemas/TranslatedString" + }, + "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/PortalExtensionAuthBlock" + }, + "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, e.g. `{\"new_email\": \"...\", \"old_email\": \"...\", \"portal_user_id\": \"...\"}`. 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" + ], + "additionalProperties": false + }, + "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" + } + }, + "additionalProperties": false + }, + "secure_proxy": { + "$ref": "#/components/schemas/PortalExtensionSecureProxy" + } + }, + "required": [ + "id", + "type", + "call" + ], + "additionalProperties": false + }, + "PortalExtensionHookChangePassword": { + "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": { + "id": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "Identifier of the hook. Should not change between updates." + }, + "name": { + "$ref": "#/components/schemas/TranslatedString" + }, + "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/PortalExtensionAuthBlock" + }, + "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" + ], + "additionalProperties": false + }, + "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" + } + }, + "additionalProperties": false + }, + "secure_proxy": { + "$ref": "#/components/schemas/PortalExtensionSecureProxy" + } + }, + "required": [ + "id", + "type", + "call" + ], + "additionalProperties": false + }, + "PortalExtensionHookDeleteAccount": { + "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 corresponding epilot Cognito 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": { + "id": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "Identifier of the hook. Should not change between updates." + }, + "name": { + "$ref": "#/components/schemas/TranslatedString" + }, + "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/PortalExtensionAuthBlock" + }, + "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" + ], + "additionalProperties": false + }, + "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" + } + }, + "additionalProperties": false + }, + "secure_proxy": { + "$ref": "#/components/schemas/PortalExtensionSecureProxy" + } + }, + "required": [ + "id", + "type", + "call" + ], + "additionalProperties": false + }, "PortalExtensionSeamlessLink": { "type": "object", "properties": { @@ -4113,6 +4809,39 @@ } } }, + "InternalReview": { + "description": "Review entry including app ownership info, used by internal admin endpoints", + "allOf": [ + { + "$ref": "#/components/schemas/Review" + }, + { + "type": "object", + "properties": { + "app_id": { + "type": "string", + "description": "ID of the app configuration", + "readOnly": true + }, + "org_id": { + "type": "string", + "description": "Organization ID of the app owner", + "readOnly": true + }, + "reviewed_by": { + "type": "string", + "description": "Identity of the internal reviewer who approved the review", + "readOnly": true + }, + "reviewed_at": { + "type": "string", + "description": "Timestamp of the approval", + "readOnly": true + } + } + } + ] + }, "ConfigurationMetadata": { "description": "Basic metadata about your app configuration which does not get versioned", "type": "object", @@ -4258,6 +4987,22 @@ }, "minItems": 0 }, + "functions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FunctionDefinition" + }, + "minItems": 0, + "description": "Server-side functions of the app, including scheduled functions" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Options" + }, + "minItems": 0, + "description": "App-level option declarations (with sensitivity) of this version" + }, "visibility": { "type": "string", "enum": [ @@ -4358,8 +5103,7 @@ }, "target": { "type": "string", - "format": "uri", - "description": "Base URL of the target API. Must be HTTPS.", + "description": "Base URL of the target API. Must be HTTPS. May reference component options via {{option_key}} interpolation (resolved from decrypted values), e.g. \"https://{{cluster}}.example.com/api\" for a per-installation host. The fully-resolved URL is validated at request time (SSRF protection).", "pattern": "^https://" }, "auth_type": { @@ -4367,10 +5111,11 @@ "enum": [ "header", "bearer", + "basic", "oauth2", "none" ], - "description": "Authentication strategy" + "description": "Authentication strategy. 'basic' sends an HTTP Basic Authorization header built server-side from the 'username' and 'password' component options." }, "auth_header": { "type": "string", @@ -4381,6 +5126,207 @@ "format": "uri", "description": "OAuth2 token endpoint URL", "pattern": "^https://" + }, + "headers": { + "type": "object", + "description": "Additional request headers injected server-side on every proxied request. Values may reference component options via {{option_key}} interpolation (resolved from decrypted secrets), so credentials are never exposed to the client. Applied in addition to auth_type.", + "additionalProperties": { + "type": "string" + } + } + } + }, + "ExternalValuesConfig": { + "type": "object", + "description": "Configuration for an EXTERNAL_VALUES component", + "required": [ + "hooks" + ], + "properties": { + "hooks": { + "type": "array", + "description": "Hooks exposed by this component. Hook ids must be unique within the component.", + "items": { + "$ref": "#/components/schemas/ExternalValuesHook" + } + } + } + }, + "ExternalValuesHook": { + "type": "object", + "description": "One external call that returns one or more typed results. Interpolated templates (Liquid) have access to `Input`, `Context`, `Consumer`, `Options`, `Env` and — for `result` templates — `Response` and `AuthResponse`.\n", + "required": [ + "id", + "name", + "call", + "result" + ], + "properties": { + "id": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]{1,100}$", + "description": "Stable identifier; consumers (e.g. validation rules) reference it. Renaming breaks references." + }, + "name": { + "$ref": "#/components/schemas/TranslatedString" + }, + "description": { + "$ref": "#/components/schemas/TranslatedString" + }, + "auth_type": { + "type": "string", + "enum": [ + "none", + "header", + "bearer", + "hmac" + ], + "default": "none", + "description": "Static request authentication applied to the call.\n- `none`: no static authentication.\n- `header`: sends `auth_secret` in header `auth_header` (default `X-API-Key`).\n- `bearer`: sends `Authorization: Bearer `.\n- `hmac`: signs the request per Standard Webhooks (headers `webhook-id`,\n `webhook-timestamp`, `webhook-signature` = `v1,` over\n `..`) using `auth_secret` as a `whsec_`-prefixed\n base64 secret, exactly as epilot webhooks do.\n\n`auth_secret` is required when `auth_type` is `header`, `bearer` or `hmac`.\nCombine with `auth` (a pre-call that fetches a token) when the target needs both.\n" + }, + "auth_header": { + "type": "string", + "description": "Header name used when `auth_type` is `header`. Defaults to `X-API-Key`." + }, + "auth_secret": { + "type": "string", + "description": "Liquid template resolving to the secret, e.g. \"{{Options.api_key}}\". Must reference an option declared with `type: secret`; literals are rejected. Required when `auth_type` is `header`, `bearer` or `hmac`.", + "example": "{{Options.api_key}}" + }, + "auth": { + "$ref": "#/components/schemas/PortalExtensionAuthBlock" + }, + "call": { + "type": "object", + "required": [ + "url" + ], + "properties": { + "method": { + "type": "string", + "enum": [ + "GET", + "POST", + "PUT", + "PATCH" + ], + "default": "POST", + "description": "HTTP method to use for the call" + }, + "url": { + "type": "string", + "description": "URL to call. Supports variable interpolation." + }, + "headers": { + "type": "object", + "description": "Headers to send with the call. Supports variable interpolation.", + "additionalProperties": { + "type": "string" + } + }, + "params": { + "type": "object", + "description": "Query parameters to append to the URL. Supports variable interpolation.", + "additionalProperties": { + "type": "string" + } + }, + "body": { + "type": "object", + "description": "JSON body to send. Values support variable interpolation. When omitted, `{ Input, Context, Consumer }` is sent as-is.", + "additionalProperties": true + } + } + }, + "result": { + "type": "array", + "minItems": 1, + "description": "Typed results extracted from the response. Result ids must be unique within the hook.", + "items": { + "$ref": "#/components/schemas/ExternalValuesResult" + } + }, + "timeout_ms": { + "type": "integer", + "minimum": 500, + "maximum": 5000, + "default": 3000, + "description": "Timeout for the external call in milliseconds." + }, + "on_unavailable": { + "type": "string", + "enum": [ + "skip", + "block" + ], + "default": "skip", + "description": "Behaviour when the external system is unavailable (timeout, network or upstream error). `skip`: consumers ignore conditions depending on this hook. `block`: consumers treat the dependent action as not allowed." + }, + "cache": { + "type": "object", + "description": "Response caching. Off when omitted.", + "required": [ + "ttl_seconds" + ], + "properties": { + "ttl_seconds": { + "type": "integer", + "minimum": 0, + "maximum": 3600, + "description": "Time to live in seconds. 0 disables caching." + }, + "key": { + "type": "string", + "description": "Liquid template for the cache key. Defaults to a hash of org, app, hook, `Input` and `Context`." + } + } + }, + "secure_proxy": { + "$ref": "#/components/schemas/PortalExtensionSecureProxy" + } + } + }, + "ExternalValuesResult": { + "type": "object", + "description": "A single typed value extracted from the hook response. Exactly one of `template`, `path` or `jsonata` must be set.\n", + "required": [ + "id", + "type", + "name" + ], + "properties": { + "id": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]{1,100}$", + "description": "Stable identifier of the result within the hook. Renaming breaks references." + }, + "type": { + "type": "string", + "enum": [ + "number", + "text", + "date", + "boolean" + ], + "description": "Type the extracted value is coerced to." + }, + "name": { + "$ref": "#/components/schemas/TranslatedString" + }, + "template": { + "type": "string", + "description": "Liquid template over `Response` etc.; output coerced to `type`.", + "example": "{{Response.value}}" + }, + "path": { + "type": "string", + "description": "Dot path into the response body, e.g. \"data.limit\".", + "example": "data.limit" + }, + "jsonata": { + "type": "string", + "description": "JSONata expression over `{ Response, Input, Context, Options, Env }`.", + "example": "$sum(Response.items.amount)" } } }, @@ -4459,6 +5405,11 @@ "description": "Name of the app", "readOnly": true }, + "icon_url": { + "type": "string", + "description": "Public URL of the app logo, if one has been uploaded", + "readOnly": true + }, "option_values": { "type": "array", "items": { @@ -4475,6 +5426,22 @@ "minLength": 0, "readOnly": true }, + "functions": { + "description": "Server-side functions of the installed version, including scheduled functions", + "type": "array", + "items": { + "$ref": "#/components/schemas/FunctionDefinition" + }, + "readOnly": true + }, + "options": { + "description": "Effective app-level options of the installation: declared top-level options plus\ncomponent-level options lifted to the app keyspace, each with its configured\n`value`. Values of sensitive options (secrets) are never included — they carry\n`configured` and `value_updated_at` instead.\n", + "type": "array", + "items": { + "$ref": "#/components/schemas/Options" + }, + "readOnly": true + }, "installed_version": { "type": "string", "description": "Version of the app that is installed", @@ -4582,6 +5549,22 @@ }, "minItems": 0 }, + "functions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FunctionDefinition" + }, + "minItems": 0, + "description": "Server-side functions of the app, including scheduled functions" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Options" + }, + "minItems": 0, + "description": "App-level option declarations (with sensitivity) of this version" + }, "is_beta": { "type": "boolean", "description": "Flag to indicate if the app is in beta.", @@ -4832,6 +5815,11 @@ "correlation_id": { "type": "string", "description": "Filter by correlation ID for tracing" + }, + "search": { + "type": "string", + "maxLength": 200, + "description": "Case-insensitive substring match over event details and component id" } } }, @@ -4882,6 +5870,11 @@ "minimum": 1, "maximum": 1000, "default": 100 + }, + "before": { + "type": "string", + "format": "date-time", + "description": "Cursor: only return events strictly older than this timestamp. Use the timestamp of the last received event to fetch the next page; more efficient than increasing page/page_size.\n" } } }, diff --git a/packages/cli/definitions/automation.json b/packages/cli/definitions/automation.json index 511b1d18a..e181edcb2 100644 --- a/packages/cli/definitions/automation.json +++ b/packages/cli/definitions/automation.json @@ -79,6 +79,15 @@ "example": "wfABCDEFGH" } }, + { + "name": "trigger_event_name", + "in": "query", + "description": "Filter by Event Catalog event name. Returns only automations that have an event_catalog trigger for this event.", + "schema": { + "type": "string", + "example": "CustomerRequestSubmitted" + } + }, { "name": "include_flows", "in": "query", @@ -621,6 +630,42 @@ } } }, + "/v1/automation/executions:search": { + "post": { + "operationId": "searchExecutions", + "summary": "searchExecutions", + "description": "Search automation executions of an entity with cursor-based pagination.\nReturns pages of up to 100 executions, newest first.\nPrefer this over GET /v1/automation/executions, which returns the full\nexecution history in a single response.\n", + "tags": [ + "executions" + ], + "requestBody": { + "description": "Search parameters", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SearchExecutionsReq" + } + } + } + }, + "responses": { + "200": { + "description": "A page of automation executions", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SearchExecutionsResp" + } + } + } + }, + "403": { + "$ref": "#/components/responses/ForbiddenError" + } + } + } + }, "/v1/automation/executions/bulk-jobs": { "post": { "operationId": "bulkTriggerExecutions", @@ -1213,6 +1258,9 @@ }, { "$ref": "#/components/schemas/FlowsTrigger" + }, + { + "$ref": "#/components/schemas/EventCatalogTrigger" } ] }, @@ -1259,6 +1307,9 @@ }, { "$ref": "#/components/schemas/ReplyEmailAction" + }, + { + "$ref": "#/components/schemas/AssignEntityAction" } ] }, @@ -1305,6 +1356,9 @@ }, { "$ref": "#/components/schemas/ReplyEmailActionConfig" + }, + { + "$ref": "#/components/schemas/AssignEntityActionConfig" } ] }, @@ -1384,14 +1438,25 @@ }, "source_type": { "type": "string", - "description": "How source_path is interpreted. 'journey-multi-select' is the v1 source type (journey card block with multi-select). Future source types (e.g. 'previous-action-outputs', 'entity-relation') will be added here.\n", + "description": "How source_path is interpreted. 'journey-multi-select' is the v1 source type (journey card block with multi-select). 'journey-file-upload' iterates the files uploaded in a journey file upload block: source_path points at the submission's `_files.$relation` array, `filter_tags` narrows it down to the block's files and every item is treated as a `file` entity reference. Future source types (e.g. 'previous-action-outputs', 'entity-relation') will be added here.\n", "enum": [ "journey-multi-select", + "journey-file-upload", "previous-action-outputs", "entity-relation" ], "default": "journey-multi-select" }, + "filter_tags": { + "type": "array", + "description": "Only iterate items of the resolved array whose `_tags` contain every listed tag. Meant for relation arrays such as `submission._files.$relation`, where the journey tags each file relation with the upload block it came from.\n", + "items": { + "type": "string" + }, + "example": [ + "_hidden_ 2a4b1c3d-0000-4000-8000-000000000000 - Dokumente" + ] + }, "length": { "type": "integer", "description": "Maximum number of iterations. 0 / omitted = iterate the full resolved array.\n", @@ -1815,6 +1880,10 @@ "type": "string", "description": "Schema of target entity" }, + "use_uniqueness_criteria": { + "type": "boolean", + "description": "Resolve this action's target (contact/account) via the organization's global uniqueness criteria instead of per-action identifiers (target_unique / Unique switches). Forwarded to entity mapping by automation-workers.\n" + }, "target_unique": { "type": "array", "description": "Unique key for target entity (see upsertEntity of Entity API)", @@ -1854,12 +1923,172 @@ "items": { "type": "string" } + }, + "graph_context": { + "type": "array", + "description": "Multi-hop graph queries (executed via entity-api's `POST /v1/entity:graph`), each\nmerging every traversed node into the mapping's source context under its own node id,\nso `mapping_attributes` (e.g. `_copy` / `_template`) can reference it alongside the\nmain entity, e.g. `{ \"_copy\": \"order.line_items\" }`.\n\n`seed.entity_id` and any node `filter[].value` may contain `{{handlebars}}`\nplaceholders (e.g. `{{trigger.entity._id}}`) that are resolved at execution time\nagainst the mapping's source context. Resolution and graph execution happen in\nentity-mapping-api; this API only accepts, validates and passes through this field.\n", + "items": { + "$ref": "#/components/schemas/GraphContextEntry" + } } }, "required": [ "target_schema" ] }, + "GraphContextEntry": { + "type": "object", + "description": "One multi-hop graph query to run against entity-api's `POST /v1/entity:graph`. Every node\nin `graph.nodes` is merged into the mapping's source context under its own `id`, so\nlisting multiple nodes here costs one entity-api call, not one per node. A node's value\noverwrites any existing source-context key of the same name (the source entity, a 1-hop\nrelation, or another graph node).\n", + "required": [ + "seed", + "graph" + ], + "properties": { + "seed": { + "$ref": "#/components/schemas/GraphSeed" + }, + "graph": { + "$ref": "#/components/schemas/GraphDefinition" + } + } + }, + "GraphSeed": { + "type": "object", + "required": [ + "entity_id", + "node_id" + ], + "properties": { + "entity_id": { + "type": "string", + "format": "uuid" + }, + "node_id": { + "type": "string", + "description": "The node ID in the graph definition that corresponds to the seed entity", + "example": "contact" + } + } + }, + "GraphDefinition": { + "type": "object", + "required": [ + "nodes", + "edges" + ], + "properties": { + "nodes": { + "type": "array", + "description": "List of node definitions in the graph", + "items": { + "$ref": "#/components/schemas/GraphNode" + } + }, + "edges": { + "type": "array", + "description": "List of edge definitions connecting nodes", + "items": { + "$ref": "#/components/schemas/GraphEdge" + } + } + } + }, + "GraphNode": { + "type": "object", + "required": [ + "id", + "schema" + ], + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for this node in the graph definition", + "example": "contact" + }, + "schema": { + "type": "string", + "description": "Entity schema slug for this node", + "example": "contact" + }, + "cardinality": { + "type": "string", + "enum": [ + "one", + "many" + ], + "description": "\"one\": this node resolves to a single entity. \"many\" (default if unset): this node\nresolves to an array of entities. The seed node always returns a single entity\nregardless of this setting.\n", + "example": "one" + }, + "fields": { + "type": "array", + "description": "Optional entity fields to include in the hydrated response for this node.", + "items": { + "type": "string" + } + }, + "filter": { + "type": "array", + "description": "Narrows this node's traversal results to entities matching every filter (AND\nsemantics). Useful for disambiguating among multiple entities reachable via the same\ngraph edge.\n", + "items": { + "$ref": "#/components/schemas/GraphNodeFilter" + } + }, + "optional": { + "type": "boolean", + "description": "Only meaningful for a node expected to resolve to a single entity (the seed node, or\n\"cardinality: one\"). When true, finding zero matching entities is not an error - the\nmapping proceeds without this node's data instead of failing. Finding more than one\nmatch still fails regardless of this flag; ambiguity is never silently accepted.\n" + } + } + }, + "GraphNodeFilter": { + "type": "object", + "description": "Entities are included in this node's result only if `attribute` exactly equals the literal `value`.", + "required": [ + "attribute", + "value" + ], + "properties": { + "attribute": { + "type": "string", + "description": "Entity attribute name to match against.", + "example": "order_number" + }, + "value": { + "description": "Literal value the attribute must exactly equal for the entity to be included in this node's result. Supports `{{handlebars}}` placeholders resolved against sourceContext when given as a string.", + "oneOf": [ + { + "type": "string", + "nullable": true + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ], + "example": "OR-113" + } + } + }, + "GraphEdge": { + "type": "object", + "required": [ + "from", + "to" + ], + "properties": { + "from": { + "type": "string", + "description": "Source node ID", + "example": "contact" + }, + "to": { + "type": "string", + "description": "Target node ID", + "example": "billing_account" + } + } + }, "MappingConfigRef": { "type": "object", "properties": { @@ -2187,13 +2416,300 @@ }, "add": { "type": "array", - "description": "Assignees to add to the thread", + "description": "Assignees to add to the thread. For even_distribution this may contain group IDs (prefixed \"group_\") whose members are candidates.", + "items": { + "type": "string" + } + }, + "assignment_type": { + "type": "string", + "description": "How assignees in `add` are resolved.\n- direct: assign exactly the users in `add` (default).\n- even_distribution: treat `add` as a candidate pool (users and/or groups)\n and assign the least-loaded available agent.\n- sequential: treat `add` as a candidate pool and assign the next\n user in a stable round-robin rotation.\n", + "enum": [ + "direct", + "even_distribution", + "sequential" + ], + "default": "direct" + }, + "only_available_users": { + "type": "boolean", + "description": "Candidate condition for even_distribution: when true, skip users who\nare currently absent (out-of-office). Opt-in; defaults to false.\n", + "default": false + }, + "hold_until_available": { + "type": "boolean", + "description": "Sub-option of only_available_users: when a day off (weekend / whole\nday off with a known return) is what empties the candidate pool, hold\nthe thread and retry when the soonest candidate is back — bounded to\n7 retries over 7 days — before applying the fallback. Defaults to\ntrue; set false to skip the hold and fall straight through to the\nfallback. Out-of-office / vacation and non-availability reasons never\nhold regardless.\n", + "default": true + }, + "match_user_skills": { + "type": "boolean", + "description": "Candidate condition for even_distribution: when true, only assign to\nusers whose skills (tags) match a label on the message. Opt-in;\ndefaults to false.\n", + "default": false + }, + "required_skill_categories": { + "type": "array", + "description": "Candidate condition (with match_user_skills): label families\n(taxonomy slugs) that must match. A user qualifies only if it shares\na label with the message within each of these categories. Leave empty\nto match on any label (flat OR across all categories).\n", + "items": { + "type": "string" + } + }, + "skill_match_mode": { + "type": "string", + "description": "How strictly required skill categories are matched (applies with\nmatch_user_skills and required skill categories set).\n- require_all: hard match (default) — a user must match every required\n category the message has a label in.\n- prefer: soft match — prefer the best-matching users, relaxing to\n fewer categories only when no better match exists; a user matching\n no category is never eligible (the fallback then applies).\n", + "enum": [ + "require_all", + "prefer" + ], + "default": "require_all" + }, + "fallback": { + "type": "string", + "description": "What to do when smart assignment resolves no assignable candidate.\nApplies to even_distribution.\n- leave_unassigned: leave the thread unassigned (default).\n- assign_to_fallback: route to the users/groups in `fallback_assignees`.\n", + "enum": [ + "leave_unassigned", + "assign_to_fallback" + ], + "default": "leave_unassigned" + }, + "fallback_assignees": { + "type": "array", + "description": "Target user/group IDs (group IDs prefixed \"group_\") to assign when\n`fallback` is \"assign_to_fallback\".\n", + "items": { + "type": "string" + } + } + } + }, + "EntityAssignee": { + "type": "object", + "description": "A single assignee as stored in a user-relation attribute. Written through\nverbatim by the assign-entity worker. Note this object encoding differs\ndeliberately from AssignThreadConfig, which stores bare id strings —\neach matches what its own target accepts.\n", + "properties": { + "type": { + "type": "string", + "description": "Which kind of principal this assignee is. Required: it selects which\nof the identifier properties below applies, and forms part of the\nappend-mode de-duplication key.\nPartner types cannot participate in a rotation; they stay valid in\n`assignees` and `fallback_assignees`.\n", + "enum": [ + "user", + "partner_user", + "partner_organization", + "group" + ] + }, + "user_id": { + "type": "string", + "description": "Set for user and partner_user assignees." + }, + "group_id": { + "type": "string", + "description": "Set for group assignees." + }, + "org_id": { + "type": "string", + "description": "Organization the assignee belongs to." + }, + "partner_id": { + "type": "string", + "description": "Set for partner_user and partner_organization assignees." + }, + "display_name": { + "type": "string", + "description": "Label snapshotted at configuration time; may go stale after a rename." + }, + "email": { + "type": "string" + } + }, + "required": [ + "type" + ] + }, + "AssignEntityConfig": { + "type": "object", + "properties": { + "attribute": { + "type": "string", + "description": "Slug of the user-relation attribute on the triggering entity to write to." + }, + "assignees": { + "type": "array", + "description": "Assignees to write into `attribute`, in the order given.", + "items": { + "$ref": "#/components/schemas/EntityAssignee" + } + }, + "write_mode": { + "type": "string", + "description": "How `assignees` combine with the attribute's current value.\n- replace: overwrite the attribute with `assignees` (default).\n- append: union with the existing assignees, de-duplicated, existing\n entries first. Two assignees are duplicates when their `type` and\n `org_id` both match and the identifier for that type matches:\n `user_id` for user and partner_user, `group_id` for group,\n `partner_id` for partner_organization. Scoping by `org_id` keeps\n assignees from different organizations distinct even when their\n identifiers coincide.\nHonoured in `direct` only; `sequential` always forces `replace`.\n", + "enum": [ + "replace", + "append" + ], + "default": "replace" + }, + "assignment_type": { + "type": "string", + "description": "How assignees are resolved. Omitted means `direct`.\n- direct: assign exactly the assignees listed in `assignees`.\n- sequential: round-robin over the members of `candidate_group`,\n selecting one user per execution. `assignees` is ignored and\n `write_mode` is forced to `replace`.\n- even_distribution: like sequential, but the member with the fewest\n matching entities wins; ties are resolved by the rotation.\n `workload_filter` narrows what counts as load.\n", + "enum": [ + "direct", + "sequential", + "even_distribution" + ], + "default": "direct" + }, + "candidate_group": { + "allOf": [ + { + "$ref": "#/components/schemas/EntityAssignee" + } + ], + "description": "The group whose members form the rotation. Must be `type: group`;\nignored in `direct`.\n" + }, + "reset_interval": { + "type": "string", + "description": "When the rotation restarts at the first member instead of continuing\nwhere the last execution left off. `never` wraps around continuously.\nBoundaries are evaluated in UTC. Applies to `sequential` only.\n", + "enum": [ + "daily", + "weekly", + "monthly", + "never" + ], + "default": "never" + }, + "workload_filter": { + "type": "object", + "description": "Narrows what counts as workload for `even_distribution`: only entities\nwhose `attribute` (a status-type attribute on the target schema) holds\none of `values` are counted. Absent means every non-deleted entity\nholding the assignee counts. Ignored by the other assignment types.\n", + "properties": { + "attribute": { + "type": "string", + "description": "Name of a `status`-type attribute on the target schema." + }, + "values": { + "type": "array", + "description": "Attribute values that count as load.", + "items": { + "type": "string" + } + } + } + }, + "match_user_skills": { + "type": "boolean", + "description": "Candidate condition for `sequential` and `even_distribution`: when true,\nonly assign to users whose skills (tags) match a label on the entity\nbeing assigned. Opt-in; defaults to false. An entity carrying no\nnamespaced label matches nobody, so the fallback applies.\n", + "default": false + }, + "required_skill_categories": { + "type": "array", + "description": "Candidate condition (with match_user_skills): label families\n(taxonomy slugs) that must match. Only categories the entity actually\ncarries a label in are scored: a user qualifies if it shares a label with\nthe entity within each of those. A required category the entity has no\nlabel in is a free pass and gates nobody — so when the entity has no label\nin any required category, every candidate passes this filter. Note this\nis the opposite of an entity carrying no namespaced label at all, which\nmatches nobody — see `match_user_skills`. Leave empty to match on any\nlabel (flat OR across all categories).\n", "items": { "type": "string" } + }, + "skill_match_mode": { + "type": "string", + "description": "How strictly required skill categories are matched (applies with\nmatch_user_skills and required skill categories set).\n- require_all: hard match (default) — a user must match every required\n category the entity has a label in.\n- prefer: soft match — prefer the best-matching users, relaxing to\n fewer categories only when no better match exists; a user matching\n no category is never eligible (the fallback then applies).\n", + "enum": [ + "require_all", + "prefer" + ], + "default": "require_all" + }, + "fallback": { + "type": "string", + "description": "What to do when no assignable principal resolves, e.g. an empty or\nfully ineligible `candidate_group`.\n- leave_unassigned: write nothing at all, leaving the attribute as it\n was. Not the same as writing an empty array, which would clear an\n existing assignment.\n- assign_to_fallback: write `fallback_assignees` instead.\n", + "enum": [ + "leave_unassigned", + "assign_to_fallback" + ], + "default": "leave_unassigned" + }, + "fallback_assignees": { + "type": "array", + "description": "Assignees to write when `fallback` is `assign_to_fallback`; required\nin that case and ignored otherwise. Any `EntityAssignee` type is\nallowed, partner principals included.\n", + "items": { + "$ref": "#/components/schemas/EntityAssignee" + } + }, + "source": { + "type": "object", + "description": "Which entity to assign to. Omit for the triggering entity (the\ndefault, and the behaviour of every flow saved before this field\nexisted).\n- trigger: the entity that started the flow.\n- action: the entity produced by an earlier action, identified by\n that action's `flow_action_id`.\n", + "properties": { + "id": { + "type": "string", + "description": "The producing action's `flow_action_id` when origin is `action`, or the trigger id when origin is `trigger`. Note this is `flow_action_id` (stable across executions), not `id`.\n" + }, + "origin": { + "type": "string", + "enum": [ + "trigger", + "action" + ] + }, + "schema": { + "type": "string", + "description": "Entity schema recorded when the source was chosen, for display. The worker uses the schema on the resolved entity itself, since this value goes stale if the producing action's target changes.\n" + } + } } } }, + "AssignEntityActionConfig": { + "allOf": [ + { + "$ref": "#/components/schemas/AutomationActionConfig" + }, + { + "type": "object", + "properties": { + "type": { + "enum": [ + "assign-entity" + ] + }, + "config": { + "$ref": "#/components/schemas/AssignEntityConfig" + } + } + } + ], + "example": { + "id": "25jga0-gkasl26-0asg-908sgaj2", + "name": "Assign Entity", + "type": "assign-entity", + "config": { + "attribute": "assigned_to", + "write_mode": "replace", + "assignment_type": "direct", + "assignees": [ + { + "type": "user", + "user_id": "90503", + "org_id": "66", + "display_name": "Jane Doe", + "email": "jane.doe@example.com" + } + ] + } + } + }, + "AssignEntityAction": { + "allOf": [ + { + "$ref": "#/components/schemas/AutomationAction" + }, + { + "type": "object", + "properties": { + "type": { + "enum": [ + "assign-entity" + ] + }, + "config": { + "$ref": "#/components/schemas/AssignEntityConfig" + } + } + } + ] + }, "SendEmailActionConfig": { "allOf": [ { @@ -2405,10 +2921,7 @@ "language_code": { "type": "string", "description": "Language code for the email template", - "enum": [ - "de", - "en" - ] + "example": "de" }, "reply_mode": { "type": "string", @@ -2439,10 +2952,7 @@ }, "language_code": { "type": "string", - "enum": [ - "de", - "en" - ] + "example": "de" }, "notify_portal_user_only": { "type": "boolean", @@ -2459,6 +2969,15 @@ "description": "Pause automation execution after sending email to wait for a confirmation link to be clicked.\n\nThe email template should contain a confirmation link using the variable `{{confirmation_url}}`\n", "default": false }, + "wait_for_journey_submission": { + "type": "boolean", + "description": "Only relevant when this action runs from a workflow automation task. After the email is sent,\nthe workflow task waits for the journey referenced in the email template to be submitted and\nits submission automation to complete (AL-2521).\n\nThe email template should contain a journey link created with the generateJourneyLink variable.\n", + "default": false + }, + "journey_id": { + "type": "string", + "description": "Snapshot of the journey referenced by the selected email template's journey link; set by the\nbuilder UI. Display/validation only — correlation does not depend on it.\n" + }, "reply_to_sender": { "type": "boolean", "description": "When enabled, overrides the template's \"To\" field with the sender address of the triggering incoming email.\nThis is useful for auto-reply scenarios where you want to automatically respond to the person who sent the email.\nOnly works when the automation is triggered by a received email (received_email or new_email_thread triggers).\n", @@ -3236,10 +3755,12 @@ }, "originType": { "type": "string", + "description": "`event` reads the operand from the trigger's Event Catalog payload instead of an entity: `attribute` is then a dot path into the payload (e.g. `ticket.subject`). Only valid with `origin: trigger` on flows started by an EventCatalogTrigger.\n", "enum": [ "entity", "workflow", - "journey_block" + "journey_block", + "event" ] }, "schema": { @@ -3345,6 +3866,14 @@ "type": "string", "description": "Schedule Id which indicates the schedule of the actions inside the condition" }, + "evaluation_order": { + "type": "string", + "enum": [ + "AFTER_SCHEDULE", + "BEFORE_SCHEDULE" + ], + "description": "Determines how the condition and its schedule combine in time, when the condition block also has a schedule.\n\n- `AFTER_SCHEDULE` (default): the schedule is created first and the condition is evaluated when the schedule fires, against the data at that moment (\"wait, then check\").\n- `BEFORE_SCHEDULE`: the condition is evaluated at trigger time against the current data; only when it passes is the schedule created, and it is not re-evaluated when it fires (\"check now, then wait\"). A failing condition skips the block immediately.\n\nAn absent value is treated as `AFTER_SCHEDULE` for backwards compatibility." + }, "evaluationResult": { "type": "boolean", "description": "Result of the condition evaluation" @@ -3352,6 +3881,10 @@ "error_output": { "$ref": "#/components/schemas/ErrorOutput" }, + "allow_failure": { + "description": "Whether the execution should continue when processing the condition\nblock itself fails (e.g. its schedule cannot be computed). When true,\nthe block's actions are cancelled and the execution moves on to the\nnext action instead of stopping in a failed state.\n", + "type": "boolean" + }, "statements": { "type": "array", "items": { @@ -3517,12 +4050,18 @@ }, { "$ref": "#/components/schemas/TriggerEventMessaging" + }, + { + "$ref": "#/components/schemas/TriggerEventEventCatalog" } ] }, "workflow_context": { "$ref": "#/components/schemas/WorkflowExecutionContext" }, + "workflow_wait_context": { + "$ref": "#/components/schemas/WorkflowWaitContext" + }, "loops": { "type": "array", "description": "Loop scope definitions propagated from the flow onto the execution record. Each loop has an id and a source_path resolved against the trigger entity at execution time. Actions referencing a loop's id via their loop_id property run once per item in the resolved array.\n", @@ -3536,6 +4075,13 @@ "additionalProperties": { "$ref": "#/components/schemas/AutomationLoopState" } + }, + "chain": { + "type": "array", + "description": "Ordered automation flow ids in this execution's trigger chain (multi-automation loop prevention).", + "items": { + "type": "string" + } } }, "required": [ @@ -3583,6 +4129,13 @@ "_execution_chain": { "$ref": "#/components/schemas/ExecutionChain" }, + "_automation_chain": { + "type": "array", + "description": "[Internal] Ordered automation flow ids that led to the parent flow being started. The started execution appends its own flow id, so entity writes it performs carry the full chain and a loop running through a workflow can be detected. This is an internal property and should not be used by external consumers.\n", + "items": { + "type": "string" + } + }, "entity_contexts": { "type": "array", "description": "Additional entity contexts from the parent flow execution. Used when an automation is triggered from a workflow task to carry all flow contexts into the automation, not just the primary entity.\n", @@ -3600,6 +4153,31 @@ } } } + }, + "trigger_user_id": { + "type": "string", + "description": "Id of the user on whose behalf the workflow task triggered this automation — the caller of a user-initiated run, or the user whose task completion advanced the flow to the automation task. Action workers resolve it as the acting user for template variable resolution (user.*) and email thread assignment, matching the attribution of manually triggered executions. Absent when no human action led to the trigger (scheduler fires, journey-triggered flows).\n", + "example": "10006129" + } + } + }, + "WorkflowWaitContext": { + "type": "object", + "description": "Correlation stamped when the triggering submission entity carried workflow wait claims from a journey link (AL-2521). Consumed by svc-workflows to resume a task waiting on this journey submission.\n", + "required": [ + "workflow_execution_id", + "workflow_task_id" + ], + "properties": { + "workflow_execution_id": { + "type": "string" + }, + "workflow_task_id": { + "type": "string" + }, + "source": { + "type": "string", + "example": "journey_submission" } } }, @@ -3771,6 +4349,92 @@ "operation_type" ] }, + "TriggerEventEventCatalog": { + "type": "object", + "description": "Set on executions started by an Event Catalog event (see EventCatalogTrigger). The full event payload is not stored inline (it may be up to 256 KB) but by reference in `payload_ref`; automation workers hydrate it before every action.\n", + "properties": { + "type": { + "type": "string", + "enum": [ + "event_catalog" + ] + }, + "org_id": { + "type": "string", + "example": "123" + }, + "entity_id": { + "$ref": "#/components/schemas/EntityId" + }, + "entity_node_id": { + "type": "string", + "description": "Node id of the event's entity graph that was resolved to `entity_id`", + "example": "ticket" + }, + "event_id": { + "type": "string", + "description": "Event Catalog event id (ULID) of the published event", + "example": "01K3ZK8QZ7Y7Q2M8W1V3N4X5P6" + }, + "event_name": { + "type": "string", + "example": "CustomerRequestSubmitted" + }, + "event_version": { + "type": "string", + "description": "Version the payload was delivered in (the version pinned on the trigger)", + "example": "1.1" + }, + "published_version": { + "type": "string", + "description": "Version the event was originally published with, before downgrading to `event_version`", + "example": "1.2" + }, + "event_time": { + "type": "string", + "format": "date-time" + }, + "trigger_source_type": { + "type": "string", + "description": "How the event was triggered in the catalog (api | automation | operation | portal_user)", + "example": "operation" + }, + "trigger_source": { + "type": "string", + "description": "Opaque source reference from the catalog (e.g. activity id, automation execution id)" + }, + "payload_ref": { + "$ref": "#/components/schemas/TriggerEventPayloadRef" + } + }, + "required": [ + "type", + "org_id", + "entity_id", + "entity_node_id", + "event_id", + "event_name", + "event_version" + ] + }, + "TriggerEventPayloadRef": { + "type": "object", + "description": "S3 reference to the stored trigger event payload (`_downgrades` stripped, downgraded to the pinned version)", + "properties": { + "bucket": { + "type": "string", + "example": "automation-trigger-payloads-prod" + }, + "key": { + "type": "string", + "example": "trigger-payloads/123/6c8e5d9a-7f1b-4c2e-9d3a-0b1c2d3e4f5a.json" + } + }, + "required": [ + "bucket", + "key" + ] + }, "ApiCallerContext": { "type": "object", "additionalProperties": true, @@ -3866,6 +4530,56 @@ "results" ] }, + "SearchExecutionsReq": { + "type": "object", + "properties": { + "entity_id": { + "$ref": "#/components/schemas/EntityId" + }, + "include_flows": { + "description": "Include flow automations in the response", + "type": "boolean", + "default": false + }, + "size": { + "description": "Max number of executions to return per page", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 25 + }, + "cursor": { + "description": "Opaque cursor returned as next_cursor by a previous page.\nPass it to fetch the next page of results.\n", + "type": "string" + } + }, + "required": [ + "entity_id" + ] + }, + "SearchExecutionsResp": { + "type": "object", + "properties": { + "total": { + "description": "Number of executions in this page", + "type": "number" + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AutomationExecution" + } + }, + "next_cursor": { + "description": "Opaque cursor to fetch the next page of results.\nOnly present when more results are available.\n", + "type": "string" + } + }, + "required": [ + "total", + "results" + ] + }, "StartExecutionRequest": { "type": "object", "properties": { @@ -4632,7 +5346,12 @@ "updateEntity", "deleteEntity", "softDeleteEntity", - "restoreEntity" + "restoreEntity", + "relationsAdded", + "relationsRemoved", + "relationsSoftDeleted", + "relationsRestored", + "relationsDeleted" ] }, "EntitySearchFilter": { @@ -4838,6 +5557,76 @@ } ] }, + "EventCatalogTrigger": { + "description": "Starts the flow when an Event Catalog event is published for the organization. The execution runs in the context of one entity from the event's entity graph (`entity_node_id`), and the event payload is available to conditions and actions as the `event` variable context.\n", + "allOf": [ + { + "$ref": "#/components/schemas/AutomationTrigger" + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "event_catalog" + ] + }, + "configuration": { + "type": "object", + "properties": { + "event_name": { + "type": "string", + "description": "Name of the Event Catalog event that starts this flow", + "example": "CustomerRequestSubmitted" + }, + "event_version": { + "type": "string", + "description": "Event version (MAJOR.MINOR) this trigger is pinned to. Payloads published in a newer version are downgraded to this version before the flow runs, so field references in conditions and actions stay stable.\n", + "example": "1.1" + }, + "entity_node_id": { + "type": "string", + "description": "Id of the cardinality-one node in the event's entity graph whose entity becomes the execution's main entity (the entity actions operate on).\n", + "example": "ticket" + }, + "entity_schema": { + "type": "string", + "description": "Schema of the entity behind `entity_node_id`, denormalized from the event definition at configuration time", + "example": "ticket" + }, + "ignore_automation_triggered": { + "type": "boolean", + "default": true, + "description": "When true (default), events that were emitted by an automation (`_trigger_source_type: automation`) do not start this flow. This prevents automation → event → automation chains unless explicitly enabled.\n" + } + }, + "required": [ + "event_name", + "event_version", + "entity_node_id", + "entity_schema" + ] + } + }, + "required": [ + "type", + "configuration" + ] + } + ], + "example": { + "id": "12d4f45a-1883-4841-a94c-5928cb338a94", + "type": "event_catalog", + "configuration": { + "event_name": "CustomerRequestSubmitted", + "event_version": "1.1", + "entity_node_id": "ticket", + "entity_schema": "ticket", + "ignore_automation_triggered": true + } + } + }, "TriggerCondition": { "type": "object", "properties": { diff --git a/packages/cli/definitions/billing.json b/packages/cli/definitions/billing.json index 969848c6b..6ced06877 100644 --- a/packages/cli/definitions/billing.json +++ b/packages/cli/definitions/billing.json @@ -2,21 +2,58 @@ "openapi": "3.0.3", "info": { "title": "Billing API", - "description": "API to manage billing data for epilot contracts and orders", + "description": "API to manage billing data for epilot contracts and orders.\n\nThis API provides endpoints for managing financial transactions (Buchungssätze) related to\ncustomer contracts (Verträge) including installments (Abschlagszahlungen), payments (Zahlungseingänge),\nreimbursements (Rückerstattungen), and other billing events.\n\n## Key Concepts\n\n- **Billing Event (Buchungssatz)**: A single financial transaction entry in the billing ledger\n- **Contract (Vertrag)**: A customer agreement linked to billing events\n- **Balance (Kontostand)**: The current financial standing of a customer across all contracts\n", "version": "1.0.0" }, "tags": [ { "name": "Billing Events", - "description": "Manage installments, payments and reimbursements" + "description": "Manage billing events (Buchungssätze) such as installments (Abschlagszahlungen),\npayments (Zahlungseingänge), reimbursements (Rückerstattungen), dunning fees (Mahngebühren),\nand other financial transactions.\n\nBilling events follow double-entry accounting principles where each entry is either\na debit (Soll) or credit (Haben) transaction.\n" }, { "name": "Contracts", - "description": "Manage contracts within 360" + "description": "Manage contract entities (Verträge) within epilot 360.\n\nContracts represent customer agreements and serve as the parent entity for billing events.\nEach contract has associated billing settings such as billing period (Abrechnungszeitraum),\ninstallment amounts (Abschlagsbeträge), and balance tracking.\n" }, { "name": "Balance", - "description": "Get customer balance" + "description": "Retrieve customer balance information (Kontostand).\n\nThe balance represents the total financial standing of a customer across all their\ncontracts and orders, calculated from the sum of all billing events.\n" + }, + { + "name": "Pricing Information", + "description": "Read contract and billing account pricing information" + }, + { + "name": "Configuration History", + "description": "Read billing configuration change history" + }, + { + "name": "billing_event_schema", + "x-displayName": "Billing Event", + "description": "\n" + }, + { + "name": "contract_schema", + "x-displayName": "Contract", + "description": "\n" + } + ], + "x-tagGroups": [ + { + "name": "APIs", + "tags": [ + "Billing Events", + "Contracts", + "Balance", + "Pricing Information", + "Configuration History" + ] + }, + { + "name": "Schemas", + "tags": [ + "billing_event_schema", + "contract_schema" + ] } ], "security": [ @@ -29,29 +66,32 @@ "get": { "operationId": "getBillingEvents", "summary": "getBillingEvents", - "description": "Get and filter billing events such as installments and reimbursements.", + "description": "Retrieve and filter billing events (Buchungssätze) such as installments (Abschlagszahlungen),\npayments (Zahlungseingänge), and reimbursements (Rückerstattungen).\n\nResults are paginated and can be filtered by entity, contact, event type, or date range.\n", "tags": [ "Billing Events" ], "parameters": [ { - "in": "query", "name": "from", + "in": "query", "required": false, + "description": "Pagination offset - number of results to skip", "schema": { "type": "number", - "description": "Initial offset to set for the search results", + "minimum": 0, "example": 0, "default": 0 } }, { - "in": "query", "name": "size", + "in": "query", "required": false, + "description": "Maximum number of results to return per page", "schema": { "type": "number", - "description": "Size of the search results", + "minimum": 1, + "maximum": 1000, "example": 100, "default": 100 } @@ -59,87 +99,158 @@ { "name": "entity_id", "in": "query", - "description": "Entity ID to filter billing events by", "required": false, + "description": "Filter billing events by one or more entity IDs (e.g., contract or order IDs)", "schema": { "type": "array", "items": { - "type": "string" + "type": "string", + "example": "5da0a718-c822-403d-9f5d-20d4584e0528" } + }, + "example": [ + "5da0a718-c822-403d-9f5d-20d4584e0528" + ] + }, + { + "name": "contact_id", + "in": "query", + "required": false, + "description": "Filter billing events by customer contact ID (Kundennummer)", + "schema": { + "type": "string", + "example": "1e3f0d58-69d2-4dbb-9a43-3ee63d862e8e" } }, { "name": "event_type", "in": "query", + "description": "Filter by billing event type (Buchungsart):\n- `installment`: Abschlagszahlung (scheduled payment due)\n- `reimbursement`: Rückerstattung (refund to customer)\n", "schema": { "type": "string", "enum": [ "installment", "reimbursement" ], - "description": "Type of billing event to filter by" + "example": "installment" } }, { "name": "date_after", "in": "query", + "description": "Filter billing events with booking date (Buchungsdatum) after this timestamp", "schema": { "type": "string", "format": "date-time", - "description": "List billing events after this date" + "example": "2025-01-01T00:00:00Z" } }, { "name": "date_before", "in": "query", + "description": "Filter billing events with booking date (Buchungsdatum) before this timestamp", "schema": { "type": "string", "format": "date-time", - "description": "List billing events before this date" + "example": "2025-12-31T23:59:59Z" } } ], "responses": { "200": { - "description": "List of billing events", + "description": "Paginated list of billing events matching the filter criteria", "content": { "application/json": { "schema": { "type": "object", "properties": { "hits": { - "type": "integer" + "type": "integer", + "description": "Total number of billing events matching the query", + "example": 42 }, "results": { "type": "array", + "description": "List of billing events for the current page", "items": { "$ref": "#/components/schemas/BillingEvent" } } } + }, + "example": { + "hits": 42, + "results": [ + { + "_id": "5da0a718-c822-403d-9f5d-20d4584e0528", + "_schema": "billing_event", + "type": "installment", + "direction": "debit", + "billing_amount": 5000, + "billing_amount_decimal": "50.00", + "billing_currency": "EUR", + "booking_date": "2025-07-10", + "due_date": "2025-07-15", + "status": "open", + "contract": { + "$relation": [ + { + "entity_id": "f589786b-3024-43cd-9cb3-5a3c953f2896" + } + ] + } + } + ] } } } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" } } }, "post": { "operationId": "createBillingEvent", "summary": "createBillingEvent", - "description": "Create a new billing event.", + "description": "Create a new billing event (Buchungssatz) such as an installment (Abschlagszahlung),\npayment (Zahlungseingang), or reimbursement (Rückerstattung).\n\nThe billing event will be linked to a contract and will affect the customer's balance.\n", "tags": [ "Billing Events" ], "requestBody": { "required": true, + "description": "Billing event data to create", "content": { "application/json": { "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/BillingEvent" - } - ] + "$ref": "#/components/schemas/BillingEvent" + }, + "example": { + "type": "installment", + "direction": "debit", + "billing_amount": 5000, + "billing_amount_decimal": "50.00", + "billing_currency": "EUR", + "booking_date": "2025-07-10", + "due_date": "2025-07-15", + "status": "open", + "note": "Monthly installment for July", + "contract": { + "$relation": [ + { + "entity_id": "f589786b-3024-43cd-9cb3-5a3c953f2896" + } + ] + } } } } @@ -150,14 +261,25 @@ "content": { "application/json": { "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/BillingEvent" - } - ] + "$ref": "#/components/schemas/BillingEvent" } } } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "422": { + "$ref": "#/components/responses/UnprocessableEntity" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" } } } @@ -166,7 +288,7 @@ "get": { "operationId": "getBillingEvent", "summary": "getBillingEvent", - "description": "Get a single billing event by ID.", + "description": "Retrieve a single billing event (Buchungssatz) by its unique ID.\n", "tags": [ "Billing Events" ], @@ -175,15 +297,16 @@ "name": "id", "in": "path", "required": true, - "description": "ID of the billing event to get", + "description": "Unique identifier of the billing event (Buchungssatz-ID)", "schema": { - "type": "string" + "type": "string", + "example": "5da0a718-c822-403d-9f5d-20d4584e0528" } } ], "responses": { "200": { - "description": "Billing Event", + "description": "Billing event details", "content": { "application/json": { "schema": { @@ -191,13 +314,25 @@ } } } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" } } }, "patch": { "operationId": "updateBillingEvent", "summary": "updateBillingEvent", - "description": "Update an existing billing event.", + "description": "Update an existing billing event (Buchungssatz).\n\nOnly the fields provided in the request body will be updated.\nCommon use cases include updating status from \"open\" to \"closed\" after payment,\nor correcting billing amounts.\n", "tags": [ "Billing Events" ], @@ -206,25 +341,32 @@ "name": "id", "in": "path", "required": true, - "description": "ID of the billing event to update", + "description": "Unique identifier of the billing event to update", "schema": { - "type": "string" + "type": "string", + "example": "5da0a718-c822-403d-9f5d-20d4584e0528" } } ], "requestBody": { "required": true, + "description": "Fields to update on the billing event", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BillingEvent" + "$ref": "#/components/schemas/BillingEventUpdate" + }, + "example": { + "status": "closed", + "paid_date": "2025-07-12T14:30:00Z", + "note": "Payment received via bank transfer" } } } }, "responses": { "200": { - "description": "Billing Event updated successfully", + "description": "Billing event updated successfully", "content": { "application/json": { "schema": { @@ -232,13 +374,31 @@ } } } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "422": { + "$ref": "#/components/responses/UnprocessableEntity" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" } } }, "delete": { "operationId": "deleteBillingEvent", "summary": "deleteBillingEvent", - "description": "Delete an existing billing event.", + "description": "Delete an existing billing event (Buchungssatz).\n\n**Warning**: Deleting billing events affects the customer's balance calculation.\nConsider using a correction event (Korrekturbuchung) instead for audit purposes.\n", "tags": [ "Billing Events" ], @@ -247,15 +407,28 @@ "name": "id", "in": "path", "required": true, - "description": "ID of the billing event to delete", + "description": "Unique identifier of the billing event to delete", "schema": { - "type": "string" + "type": "string", + "example": "5da0a718-c822-403d-9f5d-20d4584e0528" } } ], "responses": { "204": { - "description": "Billing Event deleted successfully" + "description": "Billing event deleted successfully" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" } } } @@ -264,7 +437,7 @@ "get": { "operationId": "getBillingEventByExternalId", "summary": "getBillingEventByExternalId", - "description": "Get a single billing event by External ID.", + "description": "Retrieve a billing event (Buchungssatz) by its external system identifier.\n\nThis endpoint is useful for integrations with external systems such as SAP FI/CA,\nERP systems, or payment processors that maintain their own reference IDs.\n", "tags": [ "Billing Events" ], @@ -273,15 +446,16 @@ "name": "external_id", "in": "path", "required": true, - "description": "ID of the billing event to get", + "description": "External system identifier for the billing event.\nFor example, a SAP document number or payment processor reference ID.\n", "schema": { - "type": "string" + "type": "string", + "example": "SAP-54321" } } ], "responses": { "200": { - "description": "Billing Event", + "description": "Billing event details", "content": { "application/json": { "schema": { @@ -289,6 +463,18 @@ } } } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" } } } @@ -297,16 +483,28 @@ "post": { "operationId": "createContractEntity", "summary": "createContractEntity", - "description": "Create a new contract entity.", + "description": "Create a new contract entity (Vertrag) for billing purposes.\n\nContracts serve as the parent entity for billing events and contain billing\nsettings such as installment amounts (Abschlagsbeträge), billing periods\n(Abrechnungszeiträume), and customer addresses.\n", "tags": [ "Contracts" ], "requestBody": { "required": true, + "description": "Contract data to create", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Contract" + }, + "example": { + "contract_name": "Stromvertrag Haushalt", + "contract_number": "STR-2025-001234", + "status": "active", + "branch": "power", + "billing_period": "monthly", + "installment_amount": 8500, + "billing_due_day": 15, + "start_date": "2025-01-01", + "billing_address": "Musterstraße 123, 50667 Köln" } } } @@ -321,6 +519,21 @@ } } } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "422": { + "$ref": "#/components/responses/UnprocessableEntity" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" } } } @@ -329,7 +542,7 @@ "patch": { "operationId": "updateContractEntity", "summary": "updateContractEntity", - "description": "Update an existing contract entity.", + "description": "Update an existing contract entity (Vertrag).\n\nOnly the fields provided in the request body will be updated.\nCommon use cases include updating billing settings, changing status,\nor recording termination details.\n", "tags": [ "Contracts" ], @@ -338,18 +551,25 @@ "name": "id", "in": "path", "required": true, - "description": "ID of the contract entity to update", + "description": "Unique identifier of the contract (Vertragsnummer)", "schema": { - "type": "string" + "type": "string", + "example": "f589786b-3024-43cd-9cb3-5a3c953f2896" } } ], "requestBody": { "required": true, + "description": "Fields to update on the contract", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Contract" + }, + "example": { + "status": "terminated", + "termination_date": "2025-06-30", + "termination_reason": "Kundenkündigung" } } } @@ -364,13 +584,31 @@ } } } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "422": { + "$ref": "#/components/responses/UnprocessableEntity" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" } } }, "delete": { "operationId": "deleteContractEntity", "summary": "deleteContractEntity", - "description": "Delete an existing contract entity.", + "description": "Delete an existing contract entity (Vertrag).\n\n**Warning**: Deleting a contract will affect all associated billing events.\nConsider setting the contract status to \"terminated\" instead for audit purposes.\n", "tags": [ "Contracts" ], @@ -379,19 +617,193 @@ "name": "id", "in": "path", "required": true, - "description": "ID of the contract entity to delete", + "description": "Unique identifier of the contract to delete", "schema": { - "type": "string" + "type": "string", + "example": "f589786b-3024-43cd-9cb3-5a3c953f2896" } } ], "responses": { "204": { - "description": "Contract deleted successfully", + "description": "Contract deleted successfully" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/v1/billing/contracts/{id}/pricing_information": { + "get": { + "operationId": "getContractPricingInformation", + "summary": "getContractPricingInformation", + "description": "Get current pricing information and recent configuration history for a Contract.", + "tags": [ + "Pricing Information" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "ID of the Contract entity", + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/HistoryChangeTypes" + }, + { + "$ref": "#/components/parameters/IncludeHistory" + } + ], + "responses": { + "200": { + "description": "Contract pricing information", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ContractItem" + "$ref": "#/components/schemas/ContractPricingInformation" + } + } + } + } + } + } + }, + "/v1/billing/billing_accounts/{id}/pricing_information": { + "get": { + "operationId": "getBillingAccountPricingInformation", + "summary": "getBillingAccountPricingInformation", + "description": "Get current pricing information for the active Contracts linked to a Billing Account.", + "tags": [ + "Pricing Information" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "ID of the Billing Account entity", + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/HistoryChangeTypes" + }, + { + "$ref": "#/components/parameters/IncludeHistory" + } + ], + "responses": { + "200": { + "description": "Billing Account pricing information", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BillingAccountPricingInformation" + } + } + } + } + } + } + }, + "/v1/billing/contracts/{id}/configuration_history": { + "get": { + "operationId": "getContractConfigurationHistory", + "summary": "getContractConfigurationHistory", + "description": "Get billing configuration history for a Contract.", + "tags": [ + "Configuration History" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "ID of the Contract entity", + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/ConfigurationHistoryChangeType" + }, + { + "$ref": "#/components/parameters/HistoryChangeTypes" + }, + { + "$ref": "#/components/parameters/From" + }, + { + "$ref": "#/components/parameters/Size" + } + ], + "responses": { + "200": { + "description": "Contract configuration history", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConfigurationHistoryResponse" + } + } + } + } + } + } + }, + "/v1/billing/billing_accounts/{id}/configuration_history": { + "get": { + "operationId": "getBillingAccountConfigurationHistory", + "summary": "getBillingAccountConfigurationHistory", + "description": "Get merged billing configuration history for active Contracts linked to a Billing Account.", + "tags": [ + "Configuration History" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "ID of the Billing Account entity", + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/ConfigurationHistoryChangeType" + }, + { + "$ref": "#/components/parameters/HistoryChangeTypes" + }, + { + "$ref": "#/components/parameters/From" + }, + { + "$ref": "#/components/parameters/Size" + } + ], + "responses": { + "200": { + "description": "Billing Account configuration history", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConfigurationHistoryResponse" } } } @@ -403,7 +815,7 @@ "get": { "operationId": "getCustomerBalance", "summary": "getCustomerBalance", - "description": "Get total balance across all contracts and orders of a customer entity.", + "description": "Retrieve the total balance (Kontostand) across all contracts and orders for a customer.\n\nThe balance is calculated from the sum of all billing events (Buchungssätze) associated\nwith the customer's contracts. A positive balance indicates the customer owes money;\na negative balance indicates a credit (Guthaben) in the customer's favor.\n", "tags": [ "Balance" ], @@ -411,7 +823,7 @@ { "name": "id", "in": "path", - "description": "Customer entity ID (contact or account)", + "description": "Customer entity ID. This can be either a contact ID (Kontakt-ID) or\nan account ID (Kundenkonto-ID).\n", "required": true, "schema": { "type": "string", @@ -421,20 +833,97 @@ ], "responses": { "200": { - "description": "OK", + "description": "Customer balance information", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Balance" + }, + "example": { + "balance": 8990, + "balance_decimal": "89.90", + "balance_currency": "EUR" } } } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" } } } } }, "components": { + "parameters": { + "From": { + "name": "from", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "description": "Initial offset for paginated results." + }, + "Size": { + "name": "size", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 25 + }, + "description": "Maximum number of results to return." + }, + "ConfigurationHistoryChangeType": { + "name": "change_type", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "installment_amount_changed", + "contract_pricing_changed" + ], + "default": "installment_amount_changed" + }, + "description": "Billing configuration change type to return." + }, + "HistoryChangeTypes": { + "name": "history_change_types", + "in": "query", + "required": false, + "schema": { + "type": "string", + "pattern": "^(installment_amount_changed|contract_pricing_changed)(,(installment_amount_changed|contract_pricing_changed))*$", + "example": "installment_amount_changed,contract_pricing_changed" + }, + "description": "Comma-separated billing configuration change types to return. When omitted, installment history is returned by default." + }, + "IncludeHistory": { + "name": "include_history", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": true + }, + "description": "Include recent configuration history in the pricing information response. Set to false when using the dedicated configuration history endpoint." + } + }, "securitySchemes": { "EpilotAuth": { "type": "http", @@ -443,108 +932,458 @@ "bearerFormat": "JWT" } }, - "schemas": { - "BaseEntity": { - "type": "object", - "additionalProperties": true, - "properties": { - "_id": { - "$ref": "#/components/schemas/EntityId" - }, - "_title": { - "type": "string", - "description": "Title of entity" - }, - "_org": { - "type": "string", - "description": "Organization Id the entity belongs to" - }, - "_schema": { - "$ref": "#/components/schemas/EntitySlug" - }, - "_tags": { - "type": "array", + "responses": { + "BadRequest": { + "description": "Bad Request - The request was malformed or contains invalid parameters.\nCheck the request body and query parameters for syntax errors.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "example": { + "error": "Bad Request", + "message": "Invalid date format for 'due_date'. Expected ISO 8601 date format (YYYY-MM-DD)." + } + } + } + }, + "Unauthorized": { + "description": "Unauthorized - Authentication credentials are missing or invalid.\nEnsure a valid Bearer token is included in the Authorization header.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "example": { + "error": "Unauthorized", + "message": "Missing or invalid authentication token." + } + } + } + }, + "Forbidden": { + "description": "Forbidden - The authenticated user does not have permission to perform this action.\nContact your organization administrator to request access.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "example": { + "error": "Forbidden", + "message": "You do not have permission to access this resource." + } + } + } + }, + "NotFound": { + "description": "Not Found - The requested resource does not exist or has been deleted.\nVerify the ID is correct and the resource exists in your organization.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "example": { + "error": "Not Found", + "message": "Billing event with ID '5da0a718-c822-403d-9f5d-20d4584e0528' not found." + } + } + } + }, + "UnprocessableEntity": { + "description": "Unprocessable Entity - The request was well-formed but contains semantic errors.\nThis typically indicates validation failures such as missing required fields\nor invalid field values.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "example": { + "error": "Unprocessable Entity", + "message": "Validation failed: 'billing_amount' must be a positive integer." + } + } + } + }, + "InternalServerError": { + "description": "Internal Server Error - An unexpected error occurred on the server.\nPlease try again later or contact support if the issue persists.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "example": { + "error": "Internal Server Error", + "message": "An unexpected error occurred. Please try again later." + } + } + } + } + }, + "schemas": { + "Error": { + "type": "object", + "description": "Standard error response format", + "properties": { + "error": { + "type": "string", + "description": "Error type or HTTP status text", + "example": "Bad Request" + }, + "message": { + "type": "string", + "description": "Human-readable error description", + "example": "Invalid request parameters." + } + }, + "required": [ + "error", + "message" + ] + }, + "BaseEntity": { + "type": "object", + "description": "Base schema for all epilot entities with common system fields", + "additionalProperties": true, + "properties": { + "_id": { + "$ref": "#/components/schemas/EntityId" + }, + "_title": { + "type": "string", + "description": "Display title of the entity (Anzeigetitel)", + "example": "Abschlagszahlung Juli 2025" + }, + "_org": { + "type": "string", + "description": "Organization ID the entity belongs to (Organisations-ID)", + "example": "123456" + }, + "_schema": { + "$ref": "#/components/schemas/EntitySlug" + }, + "_tags": { + "type": "array", + "description": "Tags for categorization and filtering (Schlagwörter)", "items": { "type": "string" - } + }, + "example": [ + "billing", + "energy" + ] }, "_created_at": { - "type": "string" + "type": "string", + "format": "date-time", + "description": "Timestamp when the entity was created (Erstellungszeitpunkt)", + "example": "2025-06-15T10:30:00Z" }, "_updated_at": { - "type": "string" + "type": "string", + "format": "date-time", + "description": "Timestamp when the entity was last updated (Aktualisierungszeitpunkt)", + "example": "2025-06-15T14:45:00Z" } } }, "EntityId": { "type": "string", - "description": "Entity ID", + "description": "Unique entity identifier (UUID format)", "example": "5da0a718-c822-403d-9f5d-20d4584e0528" }, "EntitySlug": { - "description": "URL-friendly identifier for the entity schema", "type": "string", - "enum": [ - "contact", - "contract", - "file", - "order", - "opportunity", - "product", - "price", - "meter", - "meter_counter", - "billing_event" - ], - "example": "contact" + "description": "URL-friendly identifier for the entity schema (Schema-Slug)", + "example": "billing_event" }, "EntityRelationItem": { "type": "object", + "description": "Reference to a related entity", "properties": { "entity_id": { "type": "string", - "description": "Entity ID for the related contract.", + "description": "Entity ID for the related entity (e.g., contract or order)", "example": "f589786b-3024-43cd-9cb3-5a3c953f2896" } } }, "BaseBillingEvent": { + "description": "Represents a single financial transaction entry (Buchungssatz) in the billing ledger.\nEach entry is either a debit or a credit, following double-entry accounting principles.\nCommon types include Abschlagszahlung (installment), Zahlungseingang (payment), Rückerstattung (reimbursement), etc.\n", "allOf": [ { "$ref": "#/components/schemas/BaseEntity" + }, + { + "type": "object", + "required": [ + "type", + "billing_amount", + "billing_amount_decimal", + "billing_currency", + "booking_date", + "contract" + ], + "properties": { + "type": { + "type": "string", + "description": "The classification of the billing transaction.\nThis field is used to group financial events for reporting or reconciliation.\nCommon examples:\n - installment (Abschlagszahlung)\n - payment (Zahlungseingang)\n - reimbursement (Rückerstattung)\n - dunning_fee (Mahngebühr)\n - chargeback (Lastschrift-Rückgabe)\n - final_bill (Endabrechnung)\n - bonus (Gutschrift)\n - correction (Korrekturbuchung)\n", + "example": "installment" + }, + "direction": { + "type": "string", + "enum": [ + "debit", + "credit" + ], + "description": "The accounting direction of the transaction:\n- debit (Soll): increases the customer’s liability (e.g. invoice issued)\n- credit (Haben): reduces the liability (e.g. payment received)\n\nAutomatically inferred based on event type if not specified.\n", + "example": "debit" + }, + "billing_amount": { + "type": "integer", + "description": "Betrag in Cent (net or gross depending on context)", + "example": 10000 + }, + "billing_amount_decimal": { + "type": "string", + "description": "Decimal representation of billing_amount for display or reporting", + "example": "100.00" + }, + "billing_currency": { + "$ref": "#/components/schemas/Currency" + }, + "external_id": { + "type": "string", + "description": "External system identifier (e.g. SAP FI/CA document number, Zahlungsavis ID)", + "example": "SAP-54321" + }, + "contract": { + "type": "object", + "description": "Link to the associated Vertragskonto or contract object", + "properties": { + "$relation": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EntityRelationItem" + } + } + } + }, + "booking_date": { + "type": "string", + "format": "date", + "description": "General ledger booking date (Buchungsdatum) for the billing event.\nUsed to determine when the entry was accounted for in the system.\nApplies to all event types (debits and credits).\nCan be used together with due_date or paid_date\n", + "example": "2025-06-15" + }, + "due_date": { + "type": "string", + "format": "date", + "description": "Due date of the invoice or charge (Fälligkeitsdatum)", + "example": "2025-06-30" + }, + "paid_date": { + "type": "string", + "format": "date-time", + "description": "Booking date (Zahlungseingang, Wertstellung)", + "example": "2025-06-15T10:00:00Z" + }, + "status": { + "type": "string", + "description": "Status of the billing event. Defaults to closed.", + "enum": [ + "closed", + "open" + ] + }, + "related_event": { + "type": "string", + "description": "Optional reference to a previous event e.g. a chargeback or a correction to a previous installment", + "example": "d4fb2a4e-3f74-4fc4-8fba-6fdaaaa3b08e", + "properties": { + "$relation": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EntityRelationItem" + } + } + } + }, + "external_link": { + "type": "object", + "description": "Optional link to an external resource e.g. an invoice in a customer portal", + "properties": { + "href": { + "type": "string", + "format": "uri", + "description": "URL of the external resource", + "example": "https://billing.example.com/invoices/12345" + }, + "title": { + "type": "string", + "description": "Title of the external resource", + "example": "Invoice 12345" + } + }, + "required": [ + "url" + ] + }, + "attachments": { + "type": "object", + "description": "Optional reference to e.g. an invoice file associated with the billing event", + "properties": { + "$relation": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EntityRelationItem" + } + } + } + }, + "note": { + "type": "string", + "description": "Öffentliche Notiz sichtbar für den Kunden, z.B. auf der Rechnung oder im Kundenportal", + "example": "Teilzahlung für Abschlag Juni" + }, + "internal_note": { + "type": "string", + "description": "Interne Notiz, nur sichtbar für Sachbearbeiter (nicht für Kunden sichtbar)", + "example": "Rückmeldung von SAP: Betrag aus Zahlungsavis 2025-06-14 übernommen" + } + } } - ], - "description": "A base billing event to be inherited by all billing events.", + ] + }, + "BillingEvent": { + "description": "Collection of supported billing event types (Buchungsarten).\nEach type represents a different kind of financial transaction\nthat affects the customer's balance.\n", + "oneOf": [ + { + "$ref": "#/components/schemas/InstallmentEvent" + }, + { + "$ref": "#/components/schemas/PaymentEvent" + }, + { + "$ref": "#/components/schemas/ReimbursementEvent" + }, + { + "$ref": "#/components/schemas/DunningFeeEvent" + }, + { + "$ref": "#/components/schemas/InvoiceEvent" + }, + { + "$ref": "#/components/schemas/FinalBillEvent" + }, + { + "$ref": "#/components/schemas/BonusEvent" + }, + { + "$ref": "#/components/schemas/CorrectionEvent" + }, + { + "$ref": "#/components/schemas/CustomEvent" + } + ] + }, + "BillingEventUpdate": { + "description": "Fields to update on an existing billing event.", "type": "object", - "required": [ - "type", - "contract", - "billing_amount", - "billing_amount_decimal", - "billing_currency" - ], - "additionalProperties": true, + "minProperties": 1, "properties": { + "type": { + "type": "string", + "description": "Updated billing event type.", + "example": "installment" + }, + "direction": { + "type": "string", + "enum": [ + "debit", + "credit" + ], + "description": "Updated accounting direction.", + "example": "debit" + }, "billing_amount": { "type": "integer", - "description": "Amount to be paid in cents i.e. precision 2", - "example": 10050 + "description": "Updated amount in cents.", + "example": 10000 }, "billing_amount_decimal": { "type": "string", - "description": "Amount to be paid in cents in decimal string representation", - "example": "100.50" + "description": "Updated decimal representation of billing_amount.", + "example": "100.00" }, "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" + "description": "Updated external system identifier.", + "example": "SAP-54321" }, "contract": { "type": "object", + "description": "Updated link to the associated contract object.", + "properties": { + "$relation": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EntityRelationItem" + } + } + } + }, + "booking_date": { + "type": "string", + "format": "date", + "description": "Updated general ledger booking date.", + "example": "2025-06-15" + }, + "due_date": { + "type": "string", + "format": "date", + "description": "Updated due date.", + "example": "2025-06-30" + }, + "paid_date": { + "type": "string", + "format": "date-time", + "description": "Updated payment date.", + "example": "2025-06-15T10:00:00Z" + }, + "status": { + "type": "string", + "enum": [ + "closed", + "open" + ], + "description": "Updated billing event status." + }, + "related_event": { + "type": "string", + "description": "Updated reference to a previous event.", + "example": "d4fb2a4e-3f74-4fc4-8fba-6fdaaaa3b08e" + }, + "external_link": { + "type": "object", + "description": "Updated link to an external resource.", + "properties": { + "href": { + "type": "string", + "format": "uri", + "description": "URL of the external resource", + "example": "https://billing.example.com/invoices/12345" + }, + "title": { + "type": "string", + "description": "Title of the external resource", + "example": "Invoice 12345" + } + } + }, + "attachments": { + "type": "object", + "description": "Updated invoice file relations associated with the billing event.", "properties": { "$relation": { "type": "array", @@ -553,11 +1392,21 @@ } } } + }, + "note": { + "type": "string", + "description": "Updated public note visible to the customer.", + "example": "Teilzahlung für Abschlag Juni" + }, + "internal_note": { + "type": "string", + "description": "Updated internal note visible to agents only.", + "example": "Rückmeldung von SAP: Betrag aus Zahlungsavis 2025-06-14 übernommen" } } }, "InstallmentEvent": { - "description": "An entity that describes an installment billing event.", + "description": "Installment billing event (Abschlagszahlung).\nRepresents a scheduled partial payment that the customer owes,\ntypically billed monthly for utilities like electricity or gas.\n", "allOf": [ { "$ref": "#/components/schemas/BaseBillingEvent" @@ -570,28 +1419,28 @@ "enum": [ "installment" ], - "description": "Type of the billing event.", - "example": "installment" - }, - "due_date": { - "type": "string", - "format": "date-time", - "description": "Date on which the installment is due." - }, - "paid_date": { - "type": "string", - "format": "date-time", - "description": "Date on which the installment is paid by the customer." + "description": "Event type identifier" } }, + "example": { + "type": "installment", + "direction": "debit", + "note": "July power & gas installment payment", + "status": "open", + "booking_date": "2025-07-10", + "due_date": "2025-07-10", + "billing_amount": 5000, + "billing_amount_decimal": "50.00", + "billing_currency": "EUR" + }, "required": [ "due_date" ] } ] }, - "ReimbursementEvent": { - "description": "An entity that describes a reimbursement billing event.", + "PaymentEvent": { + "description": "Payment received event (Zahlungseingang).\nRepresents money received from the customer, reducing their balance.\nThis is a credit transaction.\n", "allOf": [ { "$ref": "#/components/schemas/BaseBillingEvent" @@ -602,74 +1451,287 @@ "type": { "type": "string", "enum": [ - "reimbursement" + "payment" ], - "description": "Type of the billing event.", - "example": "reimbursement" - }, - "paid_date": { - "type": "string", - "format": "date-time", - "description": "Date on which the customer is reimbursed." + "description": "Event type identifier" } + }, + "example": { + "type": "payment", + "direction": "credit", + "note": "Payment reference 001234567", + "status": "closed", + "booking_date": "2025-06-15", + "due_date": "2025-06-15", + "paid_date": "2025-06-09T10:00:00Z", + "billing_amount": 5000, + "billing_amount_decimal": "50.00", + "billing_currency": "EUR" } } ] }, - "BillingEvent": { - "description": "An entity that describes a billing event such as a future installment or a reimbursement back to the customer.", - "oneOf": [ + "ReimbursementEvent": { + "description": "Reimbursement event (Rückerstattung).\nRepresents a refund to the customer, typically after overpayment\nor billing correction. This is a credit transaction.\n", + "allOf": [ { - "$ref": "#/components/schemas/InstallmentEvent" + "$ref": "#/components/schemas/BaseBillingEvent" }, { - "$ref": "#/components/schemas/ReimbursementEvent" + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "reimbursement" + ], + "description": "Event type identifier" + } + }, + "example": { + "type": "reimbursement", + "direction": "credit", + "billing_amount": 10000, + "billing_amount_decimal": "100.00", + "billing_currency": "EUR", + "note": "Refund due to meter correction", + "booking_date": "2025-06-16", + "paid_date": "2025-06-18T14:00:00Z" + } } ] }, - "Contract": { + "DunningFeeEvent": { + "description": "Dunning fee event (Mahngebühr).\nRepresents a late payment fee charged to the customer\nafter a payment reminder has been sent. This is a debit transaction.\n", "allOf": [ { - "$ref": "#/components/schemas/BaseEntity" + "$ref": "#/components/schemas/BaseBillingEvent" }, { "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": { "type": "string", "enum": [ - "draft", - "in_approval_process", - "approved", - "active", - "deactivated", - "revoked", - "terminated", - "expired" + "dunning_fee" ], - "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." + "description": "Event type identifier" + } + }, + "example": { + "type": "dunning_fee", + "direction": "debit", + "note": "Late fee for April invoice", + "status": "open", + "booking_date": "2025-06-10", + "due_date": "2025-06-10", + "billing_amount": 1500, + "billing_amount_decimal": "15.00", + "billing_currency": "EUR" + } + } + ] + }, + "InvoiceEvent": { + "allOf": [ + { + "$ref": "#/components/schemas/BaseBillingEvent" + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "invoice" + ] + } + }, + "example": { + "type": "invoice", + "direction": "debit", + "billing_amount": 8500, + "billing_amount_decimal": "85.00", + "billing_currency": "EUR", + "note": "Einmalige Rechnung für Zusatzleistung", + "booking_date": "2025-06-25", + "due_date": "2025-07-01" + }, + "required": [ + "due_date" + ] + } + ] + }, + "FinalBillEvent": { + "description": "Final bill event (Endabrechnung/Schlussrechnung).\nRepresents the final settlement when a contract ends,\naccounting for actual consumption vs. paid installments.\nCan be either debit (customer owes more) or credit (customer overpaid).\n", + "allOf": [ + { + "$ref": "#/components/schemas/BaseBillingEvent" + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "final_bill" + ], + "description": "Event type identifier" + } + }, + "example": { + "type": "final_bill", + "direction": "debit", + "billing_amount": 12000, + "billing_amount_decimal": "120.00", + "billing_currency": "EUR", + "note": "Final invoice after contract termination", + "booking_date": "2025-06-30" + } + } + ] + }, + "BonusEvent": { + "description": "Bonus/credit event (Gutschrift/Bonus).\nRepresents a promotional credit or bonus applied to the customer's account,\nsuch as welcome bonuses or loyalty rewards. This is a credit transaction.\n", + "allOf": [ + { + "$ref": "#/components/schemas/BaseBillingEvent" + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "bonus" + ], + "description": "Event type identifier" + } + }, + "example": { + "type": "bonus", + "direction": "credit", + "billing_amount": 1500, + "billing_amount_decimal": "15.00", + "billing_currency": "EUR", + "note": "Welcome bonus", + "booking_date": "2025-06-15" + } + } + ] + }, + "CorrectionEvent": { + "description": "Correction event (Korrekturbuchung).\nRepresents an adjustment to a previous billing entry,\nsuch as correcting an overcharge or undercharge.\nCan be either debit or credit depending on the correction.\n", + "allOf": [ + { + "$ref": "#/components/schemas/BaseBillingEvent" + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "correction" + ], + "description": "Event type identifier" + } + }, + "example": { + "type": "correction", + "direction": "credit", + "billing_amount": 200, + "billing_amount_decimal": "2.00", + "billing_currency": "EUR", + "note": "Corrected previous overcharge", + "booking_date": "2025-06-22" + } + } + ] + }, + "CustomEvent": { + "description": "Custom billing event (Benutzerdefinierte Buchung).\nAllows for organization-specific billing event types not covered\nby the standard types. Use a descriptive type name.\n", + "allOf": [ + { + "$ref": "#/components/schemas/BaseBillingEvent" + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "not": { + "enum": [ + "installment", + "payment", + "reimbursement", + "dunning_fee", + "invoice", + "final_bill", + "bonus", + "correction" + ] + }, + "description": "Custom billing event type (Freitext).\nUse a descriptive identifier for your custom event type.\nExamples: grid_fee_adjustment, meter_rental, special_charge\n" + } + }, + "example": { + "type": "grid_fee_adjustment", + "direction": "debit", + "billing_amount": 3200, + "billing_amount_decimal": "32.00", + "billing_currency": "EUR", + "note": "Netznachberechnung für Mai", + "booking_date": "2025-06-11" + } + } + ] + }, + "Contract": { + "description": "Represents a customer contract (Vertrag) for billing purposes.\nContracts are the parent entities for billing events and contain\nbilling configuration such as installment amounts and billing cycles.\n", + "allOf": [ + { + "$ref": "#/components/schemas/BaseEntity" + }, + { + "type": "object", + "properties": { + "contract_name": { + "type": "string", + "description": "Display name of the contract (Vertragsname)", + "example": "Stromvertrag Haushalt" + }, + "contract_number": { + "type": "string", + "description": "Unique contract identifier/number (Vertragsnummer)", + "example": "STR-2025-001234" + }, + "status": { + "type": "string", + "enum": [ + "draft", + "in_approval_process", + "approved", + "active", + "deactivated", + "revoked", + "terminated", + "expired" + ], + "default": "draft", + "description": "Current status of the contract (Vertragsstatus):\n- draft (Entwurf): Contract is being prepared\n- in_approval_process (In Prüfung): Awaiting approval\n- approved (Genehmigt): Approved but not yet active\n- active (Aktiv): Contract is currently active\n- deactivated (Deaktiviert): Temporarily suspended\n- revoked (Widerrufen): Cancelled by customer within cooling-off period\n- terminated (Gekündigt): Contract has been terminated\n- expired (Abgelaufen): Contract term has ended\n", + "example": "active" + }, + "description": { + "type": "string", + "description": "Brief description of the contract terms (Vertragsbeschreibung)", + "example": "Haushaltsstrom-Tarif mit 24 Monaten Preisgarantie" }, "account_number": { "type": "string", - "description": "The account number associated with the contract.", - "example": "67890" + "description": "Customer account number (Kundennummer/Vertragskonto)", + "example": "KD-67890" }, "branch": { "type": "string", @@ -680,33 +1742,34 @@ "waste_water", "district_heating" ], - "description": "The branch associated with the contract.", + "description": "Utility branch/commodity type (Sparte):\n- power (Strom)\n- gas (Gas)\n- water (Wasser)\n- waste_water (Abwasser)\n- district_heating (Fernwärme)\n", "example": "power" }, "billing_address": { "type": "string", - "description": "The billing address associated with the contract.", - "example": "123 Main St, Anytown" + "description": "Billing/invoice address (Rechnungsadresse)", + "example": "Musterstraße 123, 50667 Köln" }, "delivery_address": { "type": "string", - "description": "The delivery address associated with the contract.", - "example": "456 Elm St, Anytown" + "description": "Delivery/supply point address (Lieferadresse/Verbrauchsstelle)", + "example": "Musterstraße 123, 50667 Köln" }, "additional_addresses": { "type": "string", - "description": "Any additional addresses associated with the contract.", - "example": "789 Oak St, Anytown" + "description": "Additional addresses associated with the contract (Weitere Adressen)", + "example": "Postfach 456, 50668 Köln" }, "termination_date": { "type": "string", - "description": "The date on which the contract was terminated.", - "example": "2022-01-01" + "format": "date", + "description": "Date when the contract was/will be terminated (Kündigungsdatum)", + "example": "2025-12-31" }, "termination_reason": { "type": "string", - "description": "The reason for the termination of the contract.", - "example": "Non-payment" + "description": "Reason for contract termination (Kündigungsgrund)", + "example": "Kundenkündigung" }, "billing_period": { "type": "string", @@ -718,20 +1781,20 @@ "yearly" ], "default": "weekly", - "description": "The billing period associated with the contract.", + "description": "Billing cycle frequency (Abrechnungszeitraum):\n- weekly (Wöchentlich)\n- monthly (Monatlich)\n- every_quarter (Vierteljährlich)\n- every_6_months (Halbjährlich)\n- yearly (Jährlich)\n", "example": "monthly" }, "billing_duration_amount": { "type": "number", "minimum": 0, - "description": "The duration of the billing period.", + "description": "Duration amount for billing period calculation (Abrechnungsdauer)", "example": 30 }, "renewal_duration_amount": { "type": "number", "minimum": 0, - "description": "The duration of the renewal period.", - "example": 365 + "description": "Duration of automatic contract renewal (Verlängerungsdauer)", + "example": 12 }, "renewal_duration_unit": { "type": "string", @@ -741,8 +1804,8 @@ "years" ], "default": "months", - "description": "The unit of time for the renewal period.", - "example": "years" + "description": "Unit for renewal duration (Verlängerungseinheit)", + "example": "months" }, "notice_time_amount": { "type": "number", @@ -758,29 +1821,30 @@ "years" ], "default": "months", - "description": "The unit of time for the notice period.", + "description": "Unit for notice period (Kündigungsfrist-Einheit)", "example": "months" }, "start_date": { "type": "string", - "description": "The start date of the contract.", - "example": "2021-01-01" + "format": "date", + "description": "Contract start date (Vertragsbeginn)", + "example": "2025-01-01" }, "billing_due_day": { "type": "integer", - "description": "Defines the day of the month in which the installments are due.", - "minimum": 1, + "description": "Day of the month when installments are due (Fälligkeitstag). 0 means no fixed billing day is configured.", + "minimum": 0, "maximum": 31, - "example": 2 + "example": 15 }, "installment_amount": { "type": "integer", - "description": "Set amount for installments in cents. (precision 2)", - "example": 10050 + "description": "Fixed installment amount in cents (Abschlagsbetrag in Cent).\nUses integer representation with 2 decimal precision.\nExample: 10050 = 100.50 EUR\n", + "example": 8500 }, "balance": { "type": "integer", - "description": "Current balance of the contract in cents. (precision 2)", + "description": "Current contract balance in cents (Kontostand in Cent).\nPositive value = customer owes money (Forderung).\nNegative value = customer has credit (Guthaben).\n", "example": 8990 }, "balance_currency": { @@ -791,6 +1855,7 @@ ] }, "ContractItem": { + "description": "Contract entity with all required system fields populated", "allOf": [ { "$ref": "#/components/schemas/Contract" @@ -804,17 +1869,509 @@ "_updated_at" ] }, + "InstallmentAmountValue": { + "type": "object", + "properties": { + "amount": { + "type": "integer", + "description": "Amount in cents when available or derivable.", + "example": 10050 + }, + "amount_decimal": { + "type": "string", + "description": "Decimal amount string when available or derivable.", + "example": "100.50" + }, + "currency": { + "$ref": "#/components/schemas/Currency" + } + } + }, + "PriceContext": { + "type": "object", + "properties": { + "price_id": { + "type": "string" + }, + "price_title": { + "type": "string" + }, + "tariff_type": { + "type": "string", + "description": "Semantic tariff register for the price component, such as HT or NT." + }, + "pricing_model": { + "type": "string" + }, + "unit_amount_gross_decimal": { + "type": "string" + }, + "unit_amount_net_decimal": { + "type": "string" + }, + "before_discount_unit_amount_gross_decimal": { + "type": "string" + }, + "before_discount_unit_amount_net_decimal": { + "type": "string" + }, + "unit_discount_amount_decimal": { + "type": "string" + }, + "unit_discount_amount_net_decimal": { + "type": "string" + }, + "currency": { + "$ref": "#/components/schemas/Currency" + }, + "billing_period": { + "type": "string" + }, + "unit": { + "type": "string" + }, + "has_discount": { + "type": "boolean" + }, + "is_dynamic_tariff": { + "type": "boolean" + }, + "dynamic_tariff": { + "$ref": "#/components/schemas/DynamicTariffPriceContext" + } + } + }, + "DynamicTariffPriceContext": { + "type": "object", + "properties": { + "mode": { + "type": "string" + }, + "interval": { + "type": "string" + }, + "average_price_decimal": { + "type": "string" + }, + "markup_amount_decimal": { + "type": "string" + }, + "markup_amount_net_decimal": { + "type": "string" + }, + "markup_amount_gross_decimal": { + "type": "string" + }, + "market_price_decimal": { + "type": "string" + }, + "market_price_currency": { + "$ref": "#/components/schemas/Currency" + }, + "market": { + "type": "string" + }, + "bidding_zone": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + } + }, + "ConfigurationHistoryContext": { + "type": "object", + "properties": { + "base_price": { + "$ref": "#/components/schemas/PriceContext" + }, + "base_prices": { + "type": "array", + "description": "Canonically ordered base-price components. The singular base_price remains for compatibility.", + "items": { + "$ref": "#/components/schemas/PriceContext" + } + }, + "working_price": { + "$ref": "#/components/schemas/PriceContext" + }, + "working_prices": { + "type": "array", + "description": "Canonically ordered working-price components, including separate HT and NT prices when present. The singular working_price remains for compatibility.", + "items": { + "$ref": "#/components/schemas/PriceContext" + } + } + } + }, + "ContractTariffContext": { + "type": "object", + "properties": { + "product_id": { + "type": "string" + }, + "product_title": { + "type": "string" + } + } + }, + "ContractPricingContext": { + "allOf": [ + { + "$ref": "#/components/schemas/ConfigurationHistoryContext" + }, + { + "type": "object", + "properties": { + "tariff": { + "$ref": "#/components/schemas/ContractTariffContext" + }, + "tariffs": { + "type": "array", + "description": "Canonically ordered tariff products for composite Contracts. The singular tariff is populated when exactly one tariff product exists.", + "items": { + "$ref": "#/components/schemas/ContractTariffContext" + } + } + } + } + ] + }, + "ConfigurationHistoryRowBase": { + "type": "object", + "required": [ + "event_id", + "org_id", + "entity_type", + "entity_id", + "change_type", + "schema_version", + "changed_at", + "created_at", + "source" + ], + "properties": { + "event_id": { + "type": "string" + }, + "org_id": { + "type": "string" + }, + "entity_type": { + "type": "string", + "enum": [ + "contract", + "billing_account" + ] + }, + "entity_id": { + "type": "string" + }, + "schema_version": { + "type": "integer", + "example": 1 + }, + "effective_at": { + "type": "string", + "format": "date-time" + }, + "changed_at": { + "type": "string", + "format": "date-time" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "source": { + "type": "string", + "enum": [ + "portal", + "epilot", + "erp", + "system", + "api", + "external", + "journey", + "automation", + "unknown" + ] + }, + "source_label": { + "type": "string" + }, + "source_system": { + "type": "string" + }, + "source_reference": { + "type": "string" + } + } + }, + "InstallmentAmountChangedHistoryRow": { + "allOf": [ + { + "$ref": "#/components/schemas/ConfigurationHistoryRowBase" + }, + { + "type": "object", + "required": [ + "change_type", + "new_value" + ], + "properties": { + "change_type": { + "type": "string", + "enum": [ + "installment_amount_changed" + ] + }, + "previous_value": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/InstallmentAmountValue" + } + ] + }, + "new_value": { + "$ref": "#/components/schemas/InstallmentAmountValue" + }, + "context": { + "$ref": "#/components/schemas/ConfigurationHistoryContext" + } + } + } + ] + }, + "ContractPricingChangedHistoryRow": { + "allOf": [ + { + "$ref": "#/components/schemas/ConfigurationHistoryRowBase" + }, + { + "type": "object", + "required": [ + "change_type", + "previous_context", + "new_context", + "changed_fields" + ], + "properties": { + "change_type": { + "type": "string", + "enum": [ + "contract_pricing_changed" + ] + }, + "previous_context": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/ContractPricingContext" + } + ] + }, + "new_context": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/ContractPricingContext" + } + ] + }, + "changed_fields": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "tariff", + "base_price", + "working_price", + "discount", + "dynamic_tariff_configuration" + ] + } + }, + "installment_amount": { + "description": "Installment amount effective when the pricing change was recorded.", + "allOf": [ + { + "$ref": "#/components/schemas/InstallmentAmountValue" + } + ] + } + } + } + ] + }, + "ConfigurationHistoryRow": { + "oneOf": [ + { + "$ref": "#/components/schemas/InstallmentAmountChangedHistoryRow" + }, + { + "$ref": "#/components/schemas/ContractPricingChangedHistoryRow" + } + ], + "discriminator": { + "propertyName": "change_type", + "mapping": { + "installment_amount_changed": "#/components/schemas/InstallmentAmountChangedHistoryRow", + "contract_pricing_changed": "#/components/schemas/ContractPricingChangedHistoryRow" + } + } + }, + "ConfigurationHistoryResponse": { + "type": "object", + "required": [ + "history", + "total" + ], + "properties": { + "history": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConfigurationHistoryRow" + } + }, + "total": { + "type": "integer" + } + } + }, + "PricingInformationBalance": { + "type": "object", + "properties": { + "amount": { + "type": "integer", + "example": 8990 + }, + "amount_decimal": { + "type": "string", + "example": "89.90" + }, + "currency": { + "$ref": "#/components/schemas/Currency" + } + } + }, + "ContractPricingSchedule": { + "type": "object", + "required": [ + "inferred" + ], + "properties": { + "billing_due_day": { + "type": "integer", + "description": "Day of the month when installments are due. 0 means no fixed billing day is configured.", + "minimum": 0, + "maximum": 31 + }, + "billing_period": { + "type": "string", + "enum": [ + "weekly", + "monthly", + "every_quarter", + "every_6_months", + "yearly" + ] + }, + "installments_per_year": { + "type": "integer" + }, + "inferred": { + "type": "boolean" + } + } + }, + "ContractPricingInformation": { + "type": "object", + "required": [ + "entity_type", + "entity_id", + "history" + ], + "properties": { + "entity_type": { + "type": "string", + "enum": [ + "contract" + ] + }, + "entity_id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "current_installment_amount": { + "$ref": "#/components/schemas/InstallmentAmountValue" + }, + "context": { + "$ref": "#/components/schemas/ConfigurationHistoryContext" + }, + "balance": { + "$ref": "#/components/schemas/PricingInformationBalance" + }, + "schedule": { + "$ref": "#/components/schemas/ContractPricingSchedule" + }, + "pending_installment_change": { + "type": "boolean" + }, + "history": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConfigurationHistoryRow" + } + } + } + }, + "BillingAccountPricingInformation": { + "type": "object", + "required": [ + "entity_type", + "entity_id", + "contracts" + ], + "properties": { + "entity_type": { + "type": "string", + "enum": [ + "billing_account" + ] + }, + "entity_id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "balance": { + "$ref": "#/components/schemas/PricingInformationBalance" + }, + "contracts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ContractPricingInformation" + } + } + } + }, "Balance": { "type": "object", + "description": "Customer balance summary (Kontostandübersicht).\nRepresents the aggregated balance across all contracts and orders for a customer.\n", "properties": { "balance": { "type": "integer", - "description": "Current balance of the customer in cents. (precision 2)", + "description": "Total customer balance in cents (Gesamtkontostand in Cent).\nPositive = customer owes money (Offener Betrag).\nNegative = customer has credit (Guthaben).\n", "example": 8990 }, "balance_decimal": { "type": "string", - "description": "Current balance of the customer in decimal string representation.", + "description": "Balance as decimal string for display (Kontostand als Dezimalzahl).\nFormatted with 2 decimal places.\n", "example": "89.90" }, "balance_currency": { @@ -824,7 +2381,7 @@ }, "Currency": { "type": "string", - "description": "Currency code in ISO 4217 format", + "description": "Currency code in ISO 4217 format (Währungscode).\nCommon values: EUR (Euro), CHF (Swiss Franc)\n", "example": "EUR" } } diff --git a/packages/cli/definitions/blueprint-manifest.json b/packages/cli/definitions/blueprint-manifest.json index baf517146..2a35d8e95 100644 --- a/packages/cli/definitions/blueprint-manifest.json +++ b/packages/cli/definitions/blueprint-manifest.json @@ -2,7 +2,7 @@ "openapi": "3.0.2", "info": { "title": "Blueprint Manifest API", - "version": "4.7.0", + "version": "4.8.0", "description": "Service to create and install Blueprint Manifest files" }, "tags": [ @@ -823,7 +823,7 @@ "post": { "operationId": "preInstallBlueprint", "summary": "preInstallBlueprint", - "description": "Pre-install a Blueprint based on a blueprint file", + "description": "Pre-install a Blueprint based on a blueprint file. Format-agnostic: the engine is detected from the uploaded archive, so this endpoint accepts both Terraform exports and signed V3 packages. The returned preview's `sync_engine` says which install endpoint to call next. An archive that is neither a Terraform export nor a validly signed V3 package is rejected with 400.", "tags": [ "Blueprints" ], @@ -862,6 +862,9 @@ } } } + }, + "400": { + "description": "Invalid request body, or an archive that no install engine accepts" } } } @@ -904,6 +907,7 @@ "/v2/blueprint-manifest/blueprint:install": { "post": { "operationId": "installBlueprint", + "deprecated": true, "summary": "installBlueprint", "description": "Kick off a new blueprint installation job. Returns 202 Accepted with Location header pointing to the job resource\n", "tags": [ @@ -1104,9 +1108,228 @@ } } }, + "/v2/blueprint-manifest/blueprints/{blueprint_id}/notes": { + "post": { + "operationId": "addBlueprintNote", + "summary": "addBlueprintNote", + "description": "Append an internal note to a blueprint. `id`, `created_at` and `created_by`\nare stamped server-side from the caller, so notes cannot be backdated or\nattributed to someone else. Existing notes are never modified.\n", + "tags": [ + "Blueprints" + ], + "parameters": [ + { + "in": "path", + "required": true, + "name": "blueprint_id", + "schema": { + "$ref": "#/components/schemas/BlueprintID" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "text" + ], + "properties": { + "text": { + "type": "string", + "description": "Plain-text note body. Must not be blank.", + "minLength": 1 + } + } + } + } + } + }, + "responses": { + "201": { + "description": "The created note", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlueprintNote" + } + } + } + }, + "400": { + "description": "Blank note text", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/v2/blueprint-manifest/blueprints/{blueprint_id}/notes/{note_id}": { + "patch": { + "operationId": "updateBlueprintNote", + "summary": "updateBlueprintNote", + "description": "Rewrite the text of an existing internal note. The note keeps its position in\nthe list along with `created_at` and `created_by`, so an edit cannot reassign\nauthorship; `updated_at` is stamped server-side.\n", + "tags": [ + "Blueprints" + ], + "parameters": [ + { + "in": "path", + "required": true, + "name": "blueprint_id", + "schema": { + "$ref": "#/components/schemas/BlueprintID" + } + }, + { + "in": "path", + "required": true, + "name": "note_id", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "text" + ], + "properties": { + "text": { + "type": "string", + "description": "Plain-text note body. Must not be blank.", + "minLength": 1 + } + } + } + } + } + }, + "responses": { + "200": { + "description": "The updated note", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlueprintNote" + } + } + } + }, + "400": { + "description": "Blank note text", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + } + }, + "404": { + "description": "Blueprint or note not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + }, + "delete": { + "operationId": "deleteBlueprintNote", + "summary": "deleteBlueprintNote", + "description": "Remove a single internal note from a blueprint.", + "tags": [ + "Blueprints" + ], + "parameters": [ + { + "in": "path", + "required": true, + "name": "blueprint_id", + "schema": { + "$ref": "#/components/schemas/BlueprintID" + } + }, + { + "in": "path", + "required": true, + "name": "note_id", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The blueprint's remaining notes", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "notes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BlueprintNote" + } + } + } + } + } + } + }, + "404": { + "description": "Blueprint or note not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + }, "/v2/blueprint-manifest/blueprints/{blueprint_id}/validate": { "post": { "operationId": "validateBlueprint", + "deprecated": true, "summary": "validateBlueprint", "description": "Start a blueprint validation job. Validates Terraform for the blueprint (all types).\nReturns 202 Accepted with job_id. Poll GET /jobs/{job_id} for status, valid, and errors.\n", "tags": [ @@ -1204,6 +1427,22 @@ "destination_auth_token": { "type": "string", "description": "Auth token with access to the destination org. Required for cross-org verification when the caller token only has access to the source org. If not provided, the caller's bearer token is used for both orgs." + }, + "installation_job_id": { + "allOf": [ + { + "$ref": "#/components/schemas/BlueprintJobID" + } + ], + "description": "Optional install job this verification is checking. If omitted, the latest destination blueprint installation job is used when available." + }, + "sync_engine": { + "type": "string", + "enum": [ + "terraform", + "v3" + ], + "description": "Optional install engine hint. Usually inferred from installation_job_id." } } } @@ -1243,6 +1482,7 @@ "/v2/blueprint-manifest/blueprints/{blueprint_id}/patches:detect": { "post": { "operationId": "detectPatchChanges", + "deprecated": true, "summary": "detectPatchChanges", "description": "Detect changes between the current state of a blueprint's resources and its tfstate baseline.\nReturns field-level diffs for resources that have been modified since the blueprint was last installed/exported.\n", "tags": [ @@ -1302,6 +1542,7 @@ "/v2/blueprint-manifest/blueprints/{blueprint_id}/patches": { "post": { "operationId": "createPatch", + "deprecated": true, "summary": "createPatch", "description": "Create a new patch for a blueprint.", "tags": [ @@ -1375,6 +1616,7 @@ }, "get": { "operationId": "listPatches", + "deprecated": true, "summary": "listPatches", "description": "List all patches for a blueprint.", "tags": [ @@ -1418,6 +1660,7 @@ "/v2/blueprint-manifest/blueprints/{blueprint_id}/patches/{patch_id}": { "get": { "operationId": "getPatch", + "deprecated": true, "summary": "getPatch", "description": "Get a patch by ID, including per-org execution results.", "tags": [ @@ -1461,6 +1704,7 @@ "/v2/blueprint-manifest/blueprints/{blueprint_id}/patches/{patch_id}:apply": { "post": { "operationId": "applyPatch", + "deprecated": true, "summary": "applyPatch", "description": "Apply a patch to a single destination org.", "tags": [ @@ -1534,6 +1778,7 @@ "/v2/blueprint-manifest/blueprints/{blueprint_id}/patches/{patch_id}/orgs/{org_id}:retry": { "post": { "operationId": "retryPatchOrg", + "deprecated": true, "summary": "retryPatchOrg", "description": "Retry a failed patch execution for a specific org.", "tags": [ @@ -1602,6 +1847,7 @@ "/v2/blueprint-manifest/blueprints/{blueprint_id}:export": { "post": { "operationId": "exportBlueprint", + "deprecated": true, "summary": "exportBlueprint", "description": "Kick off a new blueprint export job. Returns 202 Accepted with Location header pointing to the job resource.\n", "tags": [ @@ -1924,6 +2170,28 @@ "items": { "$ref": "#/components/schemas/BlueprintResource" } + }, + "skipped": { + "type": "array", + "description": "Resources that were dropped during enrichment instead of being added", + "items": { + "$ref": "#/components/schemas/SkippedBlueprintResource" + } + }, + "errors": { + "type": "array", + "description": "Non-fatal dependency extraction failures encountered while enriching, deduplicated and capped. The listed resources were still added; some of their dependencies may be missing.", + "items": { + "$ref": "#/components/schemas/FormattedError" + } + }, + "total_errors": { + "type": "integer", + "description": "Total distinct dependency errors, before the cap applied to `errors`" + }, + "errors_truncated": { + "type": "boolean", + "description": "Whether `errors` omits some of the distinct errors counted by `total_errors`" } } } @@ -1949,6 +2217,21 @@ "schema": { "$ref": "#/components/schemas/BlueprintID" } + }, + { + "in": "query", + "required": false, + "name": "trigger", + "description": "What initiated the sync. Automated triggers (`pre_sync`,\n`post_revert`) are side effects of an operation the activity\nfeed already shows on its own row, so they are excluded from\nthe audit log — same policy as validate/verify.\n", + "schema": { + "type": "string", + "enum": [ + "manual", + "pre_sync", + "post_revert" + ], + "default": "manual" + } } ], "responses": { @@ -2030,6 +2313,28 @@ "items": { "$ref": "#/components/schemas/BlueprintResource" } + }, + "skipped": { + "type": "array", + "description": "Resources that were dropped during enrichment instead of being added", + "items": { + "$ref": "#/components/schemas/SkippedBlueprintResource" + } + }, + "errors": { + "type": "array", + "description": "Non-fatal dependency extraction failures encountered while enriching, deduplicated and capped. The listed resources were still added; some of their dependencies may be missing.", + "items": { + "$ref": "#/components/schemas/FormattedError" + } + }, + "total_errors": { + "type": "integer", + "description": "Total distinct dependency errors, before the cap applied to `errors`" + }, + "errors_truncated": { + "type": "boolean", + "description": "Whether `errors` omits some of the distinct errors counted by `total_errors`" } } } @@ -2245,7 +2550,7 @@ "/v2/blueprint-manifest/jobs": { "get": { "operationId": "listBlueprintJobs", - "summary": "List Blueprint Jobs", + "summary": "listBlueprintJobs", "description": "List all blueprint jobs", "tags": [ "Jobs" @@ -2279,8 +2584,8 @@ "/v2/blueprint-manifest/jobs/{job_id}": { "get": { "operationId": "getBlueprintJob", - "summary": "Get Job", - "description": "Poll current state of a job.", + "summary": "getBlueprintJob", + "description": "Poll the current state of a job. Serves both Terraform (v2) and V3-engine jobs —\ncheck `sync_engine` (`terraform` | `v3`) to tell them apart. V3 jobs additionally\nexpose live `resource_progress[]`. V3 single-install and bulk-install child jobs\nare polled here.\n", "tags": [ "Jobs" ], @@ -2311,8 +2616,8 @@ "/v2/blueprint-manifest/jobs/{job_id}:continue": { "post": { "operationId": "continueInstallationJob", - "summary": "Continue Installation Job", - "description": "Continue an installation job if it is waiting for user action.", + "summary": "continueInstallationJob", + "description": "Resume an installation job that is paused at `status: \"WAITING_USER_ACTION\"` after\nplanning. Works for both Terraform and V3 jobs. Not needed for V3 installs created\nwith `auto_apply: true` (including all bulk-install child jobs), which apply\nwithout pausing.\n\nAlso resumes a V3 install paused at `status: \"REAUTH_REQUIRED\"`, where the\ncaller's credentials could not safely cover another run. The job keeps its\n`job_id`, so resuming continues from the operation-log checkpoint rather than\nre-applying completed work. The request's bearer token becomes the destination\ncredential; for a cross-org install whose SOURCE credential also expired, send a\nfreshly scoped source token as `source_auth_token` — the destination bearer is not\na valid substitute for it.\n", "tags": [ "Jobs" ], @@ -2331,7 +2636,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BlueprintInstallationJobOptions" + "$ref": "#/components/schemas/ContinueInstallationJobRequest" } } } @@ -2350,11 +2655,11 @@ } } }, - "/v2/blueprint-manifest/jobs/{job_id}:cancel": { + "/v2/blueprint-manifest/jobs/{job_id}:retry": { "post": { - "operationId": "cancelBlueprintJob", - "summary": "Cancel Blueprint Job", - "description": "Cancel a blueprint job if it is still running.", + "operationId": "retryInstallationJob", + "summary": "retryInstallationJob", + "description": "Retry a finished V3 installation job whose status is `FAILED` or\n`PARTIAL_SUCCESS`. Starts a fresh install job (new `job_id`) with the same\nsource/destination and `auto_apply: true` — no `:continue` call is needed.\nThe plan phase re-resolves every resource: already-synced resources no-op,\nso effectively only the failed resources are re-applied.\n\nMust be called by a user of the destination org (where the job lives); the\ncaller's bearer token is used as the destination token. The plan reuses the\nmanifest persisted by the original install, so no source-org access is\nrequired. The original job's `options` (ignored resources, sync_notes) are\nreused when present; older jobs without persisted options fall back to the\ndestination blueprint's `ignored_resource_addresses`.\n", "tags": [ "Jobs" ], @@ -2369,17 +2674,71 @@ } ], "responses": { - "200": { - "description": "Blueprint job cancelled", - "content": { - "application/json": { + "202": { + "description": "Retry install job started", + "headers": { + "Location": { + "description": "Path to poll the resulting job: GET /v2/blueprint-manifest/jobs/{job_id}", "schema": { - "$ref": "#/components/schemas/BlueprintJob" + "type": "string" } } - } - } - } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "job_id": { + "$ref": "#/components/schemas/BlueprintJobID" + }, + "destination_blueprint_id": { + "$ref": "#/components/schemas/BlueprintID" + } + } + } + } + } + }, + "400": { + "description": "Job is not a retryable V3 installation job" + }, + "404": { + "description": "Job not found" + } + } + } + }, + "/v2/blueprint-manifest/jobs/{job_id}:cancel": { + "post": { + "operationId": "cancelBlueprintJob", + "summary": "cancelBlueprintJob", + "description": "Cancel a blueprint job if it is still running.", + "tags": [ + "Jobs" + ], + "parameters": [ + { + "in": "path", + "required": true, + "name": "job_id", + "schema": { + "$ref": "#/components/schemas/BlueprintJobID" + } + } + ], + "responses": { + "200": { + "description": "Blueprint job cancelled", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlueprintJob" + } + } + } + } + } } }, "/v1/blueprints/{blueprint_id}/marketplace-listing": { @@ -2861,11 +3220,132 @@ } } }, + "/v3/blueprint-manifest/blueprints/{blueprint_id}:publish": { + "post": { + "operationId": "publishBlueprintV3", + "summary": "publishBlueprintV3", + "description": "Starts an asynchronous V3 publication. The result is a signed, portable package; poll the existing blueprint job endpoint for completion.\n\nBy default this only builds the package and stores it privately for download. Creating or\noverwriting the public marketplace listing is opt-in via `publish_to_marketplace`.\n", + "tags": [ + "Blueprints" + ], + "parameters": [ + { + "in": "path", + "required": true, + "name": "blueprint_id", + "schema": { + "$ref": "#/components/schemas/BlueprintID" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "publish_to_marketplace": { + "type": "boolean", + "default": false, + "description": "When true, upload the package with public access and create or overwrite the\nblueprint's Webflow CMS listing. Leave unset for a download-only package build.\n" + } + } + } + } + } + }, + "responses": { + "202": { + "description": "Blueprint export job started", + "headers": { + "Location": { + "schema": { + "type": "string" + } + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "job_id" + ], + "properties": { + "job_id": { + "$ref": "#/components/schemas/BlueprintJobID" + } + } + } + } + } + } + } + } + }, + "/v3/blueprint-manifest/blueprints:pre-install": { + "post": { + "operationId": "preInstallBlueprintV3", + "summary": "preInstallBlueprintV3", + "description": "Validates a signed V3 package and returns the destination-specific resource plan used by the install UI.", + "tags": [ + "Blueprints" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "blueprint_file" + ], + "properties": { + "blueprint_file": { + "type": "string", + "description": "S3 key of a V3 blueprint package previously uploaded through uploadManifest" + }, + "source_blueprint_type": { + "type": "string", + "enum": [ + "marketplace" + ] + }, + "slug": { + "type": "string", + "description": "Marketplace slug to preserve across updates" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Destination-specific V3 blueprint preview", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlueprintPreview" + } + } + } + }, + "400": { + "description": "Invalid request or unsupported V3 package" + }, + "404": { + "description": "Blueprint package not found" + } + } + } + }, "/v3/blueprint-manifest/blueprint:install": { "post": { "operationId": "installBlueprintV3", - "summary": "Install Blueprint V3", - "description": "Install a blueprint using the V3 engine (direct API calls, no Terraform).\nCreates resources in topological order with global ID replacement.\nSupports checkpoint-based resume on failure.\n", + "summary": "installBlueprintV3", + "description": "Install a blueprint into a single destination org using the V3 engine (direct API\ncalls, no Terraform). Creates resources in topological order with global ID\nreplacement and supports checkpoint-based resume on failure.\n\n**Lifecycle (how to drive an install to completion):**\n1. `POST /v3/blueprint-manifest/blueprint:install` returns `{ job_id }` (202).\n2. Poll the job with `GET /v2/blueprint-manifest/jobs/{job_id}` — V3 jobs are\n served by the same v2 jobs endpoints as Terraform jobs and are identified by\n `sync_engine: \"v3\"`. Watch `status` and `resource_progress[]`.\n3. If `auto_apply` is `false` (default), the job pauses at\n `status: \"WAITING_USER_ACTION\"` after planning. Resume it with\n `POST /v2/blueprint-manifest/jobs/{job_id}:continue`.\n4. If `auto_apply` is `true`, the engine applies automatically after plan +\n snapshot — no `:continue` call is needed. This is what the bulk-install worker\n uses; to install into many orgs at once prefer `POST .../bulk-installs`.\n\nFor cross-org installs, pass `destination_auth_token` (the destination org's\ntoken); reads use the caller's bearer token, writes use that token.\n", "tags": [ "Blueprints" ], @@ -2911,12 +3391,66 @@ "type": "string", "description": "Auth token for the destination org. Required for cross-org installs where the caller's token belongs to the source org. Defaults to the caller's bearer token." }, + "source_blueprint_type": { + "type": "string", + "enum": [ + "marketplace" + ] + }, "options": { "$ref": "#/components/schemas/BlueprintInstallationJobOptions" }, "slug": { "type": "string", "description": "Slug for marketplace blueprint consistency" + }, + "auto_apply": { + "type": "boolean", + "description": "When `true`, the install skips the manual plan-approval step and applies\nstraight after plan + snapshot succeed (no `:continue` call needed).\nDefaults to `false`. Used internally by the bulk-install worker.\n", + "default": false + }, + "session_credentials": { + "type": "object", + "description": "Lets the install refresh the caller's session on its own, so a run that\noutlives the ~60 minute token lifetime continues without the user present.\nHeld server-side for the job's lifetime, referenced from the state machine\nrather than embedded in it, and deleted when the job finishes.\n\nOptional. Without it an install that outruns its token pauses at\n`status: \"REAUTH_REQUIRED\"` and waits to be resumed through\n`POST /v2/blueprint-manifest/jobs/{job_id}:continue`.\n\nBounded by the refresh token itself (18 hours), not unlimited.\n", + "required": [ + "refresh_token", + "client_id" + ], + "properties": { + "refresh_token": { + "type": "string", + "format": "password", + "description": "Cognito refresh token for the caller, scoped to the source org." + }, + "client_id": { + "type": "string", + "description": "The Cognito app client the refresh token was issued for. Required because\nepilot provisions one user pool and app client per organization at\nruntime, so the service cannot derive it.\n" + }, + "pipeline_id": { + "type": "string", + "description": "Cross-org installs only. The destination credential is not a Cognito\nsession — it is minted from the refreshed source session through\nsandbox-api, so it is re-derived per continuation instead of stored.\n" + } + } + } + } + }, + "examples": { + "sameOrgPlanThenContinue": { + "summary": "Same-org install, manual approval (default)", + "value": { + "source_org_id": "source-org-id", + "source_blueprint_id": "11111111-1111-1111-1111-111111111111", + "destination_org_id": "source-org-id" + } + }, + "crossOrgAutoApply": { + "summary": "Cross-org install, applied automatically", + "value": { + "source_org_id": "source-org-id", + "source_blueprint_id": "11111111-1111-1111-1111-111111111111", + "destination_org_id": "dest-org-id", + "destination_auth_token": "destination-org-token", + "auto_apply": true } } } @@ -2928,7 +3462,7 @@ "description": "V3 install job started", "headers": { "Location": { - "description": "URL to poll the job", + "description": "Path to poll the resulting job: GET /v2/blueprint-manifest/jobs/{job_id}", "schema": { "type": "string" } @@ -2971,8 +3505,8 @@ "/v3/blueprint-manifest/blueprints/{blueprint_id}/deployments/{job_id}:restore": { "post": { "operationId": "restoreBlueprintDeploymentV3", - "summary": "Restore a specific deployment by job_id", - "description": "Roll a deployment back to its pre-install state. Two phases:\n\n 1. Upsert — re-applies the captured payloads via snapshot-api's\n `:restore` (server-side; runs config-engine.apply with captured\n target ids pre-seeded). Skipped for pure-create deployments\n whose snapshot was empty.\n 2. Delete sweep — for lineage rows of this blueprint instance not\n present in the snapshot's captured set, deletes the live\n resource via the type's adapter. Co-ownership / drift /\n no-delete-capability skip the entry with the corresponding\n reason.\n\nResolves `(blueprint_id, job_id)` to the entry in\n`Blueprint.deployments[]` and reads its `snapshot_id` and\n`destination_blueprint_id` — the caller never needs to handle\nsnapshot ids directly.\n\nAsync — returns 202 with a job id. Poll the job to track progress.\nThe per-instance lock (`installation_status === 'IN_PROGRESS'`)\nrejects concurrent installs or restores with 409.\n", + "summary": "restoreBlueprintDeploymentV3", + "description": "Roll a deployment back to its pre-install state. Two phases:\n\n 1. Upsert — re-applies the captured payloads via snapshot-api's\n `:restore` (server-side; runs config-engine.apply with captured\n target ids pre-seeded). Skipped for pure-create deployments\n whose snapshot was empty.\n 2. Delete sweep — for lineage rows of this blueprint instance not\n present in the snapshot's captured set, deletes the live\n resource via the type's adapter. Co-ownership / drift /\n no-delete-capability skip the entry with the corresponding\n reason.\n\nResolves `(blueprint_id, job_id)` to the entry in\n`Blueprint.deployments[]` and reads its `snapshot_id` and\n`destination_blueprint_id` — the caller never needs to handle\nsnapshot ids directly.\n\nAsync — returns 202 with a job id. Poll the job to track progress.\nThe restore holds its own per-instance lock (`active_restore_job_id`),\nseparate from `installation_status`. Starting a restore is rejected with\n409 when another restore already holds that lock, or when an install is\nrunning (`installation_status === 'IN_PROGRESS'`). Keeping the two locks\ndistinct keeps installs and reverts decoupled: an in-flight or failed\nrestore never overwrites `installation_status`, which keeps reflecting\nthe actual install outcome (and drives the install badge in the\nBlueprints overview) throughout the revert.\n", "tags": [ "Blueprints" ], @@ -3039,7 +3573,7 @@ "/v3/blueprint-manifest/blueprints/{blueprint_id}/deployments/{job_id}/restore-preview": { "get": { "operationId": "getRestorePreview", - "summary": "Predicted outcome of reverting a deployment", + "summary": "getRestorePreview", "description": "Computes what would happen if the user triggered a restore on this\ndeployment, without performing any writes. The forecast uses the\nsnapshot's captured resources (when present) plus the current lineage\nstate plus per-adapter gates (co-ownership, no-delete-capability,\nheuristic-match, drift when wired).\n\nIdempotent and side-effect free. Safe to call repeatedly. The result\nmay shift between calls if operators edit destination resources or\nanother blueprint adopts a shared resource in the meantime.\n", "tags": [ "Blueprints" @@ -3080,136 +3614,202 @@ } } }, - "/v3/blueprint-manifest/blueprints/{blueprint_id}/lineage": { - "get": { - "operationId": "getBlueprintLineageV3", - "summary": "Get Blueprint Lineage V3", - "description": "Returns the lineage registry entries for a blueprint's resources in the current org.\nShows the mapping between source lineage IDs and target resource IDs.\n", + "/v3/blueprint-manifest/blueprints/{blueprint_id}/deployments/{job_id}:health-check": { + "post": { + "operationId": "triggerDeploymentHealthCheckV3", + "summary": "triggerDeploymentHealthCheckV3", + "description": "Starts a read-only health scan of the resources this deployment's\nblueprint instance tracks in the destination org (see\ndocs/rfcs/RFC-org-health-check.md, Phase 0). Checks:\n\n - live readability — every lineage-tracked resource is read back\n via its adapter; tracked-but-unreadable resources are flagged.\n - referential integrity — destination payloads containing\n unreplaced source-org ids, or references to tracked resources\n that are not readable.\n - install completeness — install operations whose final attempt\n ended `failed` or `skipped`, with their rejection reasons.\n\nAsync — returns 202 immediately with a `running` report stub. Poll\nthe health-report endpoint until `status` is `completed` or\n`failed`. Never mutates any resource.\n", "tags": [ "Blueprints" ], "parameters": [ { "in": "path", - "required": true, "name": "blueprint_id", + "required": true, "schema": { "$ref": "#/components/schemas/BlueprintID" } + }, + { + "in": "path", + "name": "job_id", + "required": true, + "description": "The install job whose deployment is being checked.", + "schema": { + "$ref": "#/components/schemas/BlueprintJobID" + } } ], - "responses": { - "200": { - "description": "Lineage entries for the blueprint", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "total": { - "type": "integer" - }, - "entries": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LineageEntry" - } - } + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "source_org_id": { + "type": "string", + "description": "Source/sandbox org to deep-compare entity mappings against." + }, + "source_auth_token": { + "type": "string", + "description": "Read token for the source org. Never persisted; used only for this scan." } } } } } - } - } - }, - "/v1/blueprint-manifest/uniqueness-criteria": { - "get": { - "operationId": "listUniquenessCriteria", - "summary": "listUniquenessCriteria", - "description": "List all custom uniqueness criteria configured for the caller's organization.\nThese overrides are applied during install (V2 and V3) when matching incoming\nresources against existing ones in the destination org, replacing the default\nper-resource-type field set with the caller's chosen fields (AND-combined).\n", - "tags": [ - "Uniqueness Criteria" - ], + }, "responses": { - "200": { - "description": "All configured criteria for the org", + "202": { + "description": "Health check started", + "headers": { + "Location": { + "description": "URL to poll the health report", + "schema": { + "type": "string" + } + } + }, "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/UniquenessCriteria" - } - } - } + "$ref": "#/components/schemas/DeploymentHealthReport" } } } + }, + "404": { + "description": "Blueprint not found, or no deployment with the given job_id on this blueprint." } } } }, - "/v1/blueprint-manifest/uniqueness-criteria/{resource_type}": { - "parameters": [ - { - "in": "path", - "required": true, - "name": "resource_type", - "schema": { - "$ref": "#/components/schemas/UniquenessCriteriaResourceType" - } - } - ], + "/v3/blueprint-manifest/blueprints/{blueprint_id}/deployments/{job_id}/health-report": { "get": { - "operationId": "getUniquenessCriteria", - "summary": "getUniquenessCriteria", - "description": "Get the configured uniqueness criteria for a specific resource type, if any.", + "operationId": "getDeploymentHealthReportV3", + "summary": "getDeploymentHealthReportV3", + "description": "Returns the most recent health report produced for this deployment\nby the `:health-check` endpoint. Idempotent and side-effect free.\n", "tags": [ - "Uniqueness Criteria" + "Blueprints" + ], + "parameters": [ + { + "in": "path", + "name": "blueprint_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/BlueprintID" + } + }, + { + "in": "path", + "name": "job_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/BlueprintJobID" + } + } ], "responses": { "200": { - "description": "Configured criteria for the resource type", + "description": "Latest health report", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UniquenessCriteria" + "$ref": "#/components/schemas/DeploymentHealthReport" } } } }, "404": { - "description": "No custom criteria configured (defaults will be used)" + "description": "No health report exists for this deployment yet." } } - }, - "put": { - "operationId": "putUniquenessCriteria", - "summary": "putUniquenessCriteria", - "description": "Set or replace the uniqueness criteria for a resource type. The provided fields\nmust be valid attributes on the resource's schema (the UI typically loads the\nschema to populate options). All listed fields are AND-combined during matching.\n", + } + }, + "/v3/blueprint-manifest/blueprints/{blueprint_id}/lineage": { + "get": { + "operationId": "getBlueprintLineageV3", + "summary": "getBlueprintLineageV3", + "description": "Returns the lineage registry entries for a blueprint's resources in the current org.\nShows the mapping between source lineage IDs and target resource IDs.\n", "tags": [ - "Uniqueness Criteria" + "Blueprints" + ], + "parameters": [ + { + "in": "path", + "required": true, + "name": "blueprint_id", + "schema": { + "$ref": "#/components/schemas/BlueprintID" + } + } + ], + "responses": { + "200": { + "description": "Lineage entries for the blueprint", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "total": { + "type": "integer" + }, + "entries": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LineageEntry" + } + } + } + } + } + } + } + } + } + }, + "/v3/blueprint-manifest/bulk-installs": { + "post": { + "operationId": "createBulkInstallV3", + "summary": "createBulkInstallV3", + "description": "Install one source blueprint into many destination organizations in a single\nrequest. The server schedules child V3 installs with `auto_apply=true` and caps\nactive installs at `max_concurrency`. Per-target failures are isolated and\nretryable; they do not stop the remaining targets.\n\nEach target carries its own write-only `destination_auth_token` (org-scoped).\nTokens are passed to the worker via Step Functions input only — they are never\npersisted in DynamoDB nor returned by any endpoint.\n", + "tags": [ + "Blueprints" ], "requestBody": { "required": true, "content": { "application/json": { "schema": { - "type": "object", - "required": [ - "fields" - ], - "properties": { - "fields": { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } + "$ref": "#/components/schemas/BulkInstallCreateRequest" + }, + "examples": { + "twoTargets": { + "summary": "Install one blueprint into two orgs", + "value": { + "source_org_id": "source-org-id", + "source_blueprint_id": "11111111-1111-1111-1111-111111111111", + "max_concurrency": 2, + "options": { + "resources_to_ignore": [ + "epilot-product_product.foo" + ] + }, + "targets": [ + { + "destination_org_id": "dest-org-1", + "destination_auth_token": "token-for-org-1" + }, + { + "destination_org_id": "dest-org-2", + "destination_blueprint_id": "22222222-2222-2222-2222-222222222222", + "destination_auth_token": "token-for-org-2" + } + ] } } } @@ -3217,95 +3817,408 @@ } }, "responses": { - "200": { - "description": "Stored criteria", + "202": { + "description": "Bulk install accepted and scheduled", + "headers": { + "Location": { + "description": "URL to poll the bulk install", + "schema": { + "type": "string" + } + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UniquenessCriteria" + "$ref": "#/components/schemas/BulkInstall" } } } }, "400": { - "description": "Invalid resource type or field list" + "description": "Validation error — missing `source_blueprint_id`, empty/oversized `targets`\n(1–100), duplicate `destination_org_id`, missing per-target\n`destination_auth_token`, or `max_concurrency` out of range (1–5).\n", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + } } } - }, - "delete": { - "operationId": "deleteUniquenessCriteria", - "summary": "deleteUniquenessCriteria", - "description": "Remove the custom criteria for a resource type, reverting to the default fields.", + } + }, + "/v3/blueprint-manifest/bulk-installs/{bulk_job_id}": { + "get": { + "operationId": "getBulkInstallV3", + "summary": "getBulkInstallV3", + "description": "Returns the bulk install parent with aggregate status and counts. Scoped by the\ncaller org as `source_org_id`. Target rows are not included — use the targets\nendpoint to page through them.\n", "tags": [ - "Uniqueness Criteria" + "Blueprints" + ], + "parameters": [ + { + "in": "path", + "name": "bulk_job_id", + "required": true, + "schema": { + "type": "string" + } + } ], "responses": { - "204": { - "description": "Criteria deleted (defaults will be used)" + "200": { + "description": "Bulk install parent", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkInstall" + } + } + } + }, + "404": { + "description": "Bulk install not found for the caller org" } } } - } - }, - "components": { - "schemas": { - "UniquenessCriteriaResourceType": { - "type": "string", - "description": "Resource type for which custom uniqueness criteria can be configured.", - "enum": [ - "emailtemplate", - "product", - "price", - "tax", - "coupon", - "product_recommendation", - "file", - "document_template", - "schema", - "taxonomy", - "notification_template", - "family", - "permission", - "journey" - ] - }, - "UniquenessCriteria": { - "type": "object", - "required": [ - "org_id", - "resource_type", - "fields", - "updated_at" + }, + "/v3/blueprint-manifest/bulk-installs/{bulk_job_id}/targets": { + "get": { + "operationId": "listBulkInstallTargetsV3", + "summary": "listBulkInstallTargetsV3", + "description": "Pages through the bulk install's target rows. Each row hydrates its latest child\ninstall job (`job_ids.at(-1)`) with the standard V3 job shape (`events[]`,\n`resource_progress[]`) so callers can inspect per-resource progress and errors.\n", + "tags": [ + "Blueprints" ], - "properties": { - "org_id": { - "type": "string" + "parameters": [ + { + "in": "path", + "name": "bulk_job_id", + "required": true, + "schema": { + "type": "string" + } }, - "resource_type": { - "$ref": "#/components/schemas/UniquenessCriteriaResourceType" + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 25, + "default": 10 + } }, - "fields": { - "type": "array", - "minItems": 1, - "items": { + { + "in": "query", + "name": "cursor", + "required": false, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "A page of target rows", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkInstallTargetList" + } + } + } }, - "updated_at": { - "type": "string", - "format": "date-time" - }, - "updated_by": { - "type": "string" + "404": { + "description": "Bulk install not found for the caller org" } } - }, - "LineageEntry": { - "type": "object", - "properties": { - "lineage_id": { - "type": "string", - "description": "Immutable resource identity across orgs" + } + }, + "/v3/blueprint-manifest/bulk-installs/{bulk_job_id}/targets/{destination_org_id}:retry": { + "post": { + "operationId": "retryBulkInstallTargetV3", + "summary": "retryBulkInstallTargetV3", + "description": "Retries a single failed target. Allowed only for `FAILED` and `PARTIAL_SUCCESS`\ntargets. Starts a new child install with `auto_apply=true`, appends its job id to\n`job_ids`, and reuses the same target row. Only the destination auth token may be\nsupplied for the new attempt; source/destination identifiers are immutable.\n", + "tags": [ + "Blueprints" + ], + "parameters": [ + { + "in": "path", + "name": "bulk_job_id", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "destination_org_id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "destination_auth_token" + ], + "properties": { + "destination_auth_token": { + "type": "string", + "description": "Write-only auth token for the destination org used for the new attempt." + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Updated target row with hydrated latest child job", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkInstallTarget" + } + } + } + }, + "400": { + "description": "Target is not in a retryable (`FAILED`/`PARTIAL_SUCCESS`) state." + }, + "404": { + "description": "Bulk install or target not found for the caller org." + }, + "409": { + "description": "The target was concurrently claimed by another retry (already in progress)." + } + } + } + }, + "/v1/blueprint-manifest/uniqueness-criteria": { + "get": { + "operationId": "listUniquenessCriteria", + "summary": "listUniquenessCriteria", + "description": "List all custom uniqueness criteria configured for the caller's organization.\nThese overrides are applied during install (V2 and V3) when matching incoming\nresources against existing ones in the destination org, replacing the default\nper-resource-type field set with the caller's chosen fields (AND-combined).\n", + "tags": [ + "Uniqueness Criteria" + ], + "responses": { + "200": { + "description": "All configured criteria for the org", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UniquenessCriteria" + } + }, + "defaults": { + "type": "object", + "description": "Built-in default uniqueness fields per resource type, as used by\nthe install engines when no custom criteria are configured.\n", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "readonly_types": { + "type": "array", + "description": "Resource types whose criteria are fixed by epilot and cannot be\ncustomized (writes are rejected). Shown read-only in the UI.\n", + "items": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "/v1/blueprint-manifest/uniqueness-criteria/{resource_type}": { + "parameters": [ + { + "in": "path", + "required": true, + "name": "resource_type", + "schema": { + "$ref": "#/components/schemas/UniquenessCriteriaResourceType" + } + } + ], + "get": { + "operationId": "getUniquenessCriteria", + "summary": "getUniquenessCriteria", + "description": "Get the configured uniqueness criteria for a specific resource type, if any.", + "tags": [ + "Uniqueness Criteria" + ], + "responses": { + "200": { + "description": "Configured criteria for the resource type", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UniquenessCriteria" + } + } + } + }, + "404": { + "description": "No custom criteria configured (defaults will be used)" + } + } + }, + "put": { + "operationId": "putUniquenessCriteria", + "summary": "putUniquenessCriteria", + "description": "Set or replace the uniqueness criteria for a resource type. The provided fields\nmust be valid attributes on the resource's schema (the UI typically loads the\nschema to populate options). All listed fields are AND-combined during matching.\n", + "tags": [ + "Uniqueness Criteria" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "fields" + ], + "properties": { + "fields": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + }, + "propagated_to": { + "type": "array", + "description": "Org IDs this rule was also applied to (the UI's \"Also apply to\"\nselection). Stored so the selection survives reloads; each target\norg still holds its own criteria row.\n", + "items": { + "type": "string" + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Stored criteria", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UniquenessCriteria" + } + } + } + }, + "400": { + "description": "Invalid resource type or field list" + } + } + }, + "delete": { + "operationId": "deleteUniquenessCriteria", + "summary": "deleteUniquenessCriteria", + "description": "Remove the custom criteria for a resource type, reverting to the default fields.", + "tags": [ + "Uniqueness Criteria" + ], + "responses": { + "204": { + "description": "Criteria deleted (defaults will be used)" + } + } + } + } + }, + "components": { + "schemas": { + "UniquenessCriteriaResourceType": { + "type": "string", + "description": "Resource type for which custom uniqueness criteria can be configured.", + "enum": [ + "emailtemplate", + "product", + "price", + "tax", + "coupon", + "product_recommendation", + "file", + "document_template", + "notification_template", + "journey" + ] + }, + "UniquenessCriteria": { + "type": "object", + "required": [ + "org_id", + "resource_type", + "fields", + "updated_at" + ], + "properties": { + "org_id": { + "type": "string" + }, + "resource_type": { + "$ref": "#/components/schemas/UniquenessCriteriaResourceType" + }, + "fields": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + }, + "propagated_to": { + "type": "array", + "items": { + "type": "string" + } + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "updated_by": { + "type": "string" + } + } + }, + "LineageEntry": { + "type": "object", + "properties": { + "lineage_id": { + "type": "string", + "description": "Immutable resource identity across orgs" }, "target_id": { "type": "string", @@ -3504,17 +4417,50 @@ } ] }, - "BlueprintID": { - "type": "string", - "description": "ID of a blueprint", - "example": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341" - }, - "BlueprintResourceID": { - "type": "string", - "description": "ID of a blueprint resource", - "example": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341" - }, - "CommonBlueprintFields": { + "BlueprintNote": { + "type": "object", + "description": "A single internal note on a blueprint.", + "required": [ + "id", + "text", + "created_at" + ], + "properties": { + "id": { + "type": "string", + "description": "Server-generated note id.", + "example": "3f1c9b0e-2f3a-4a1f-9a3e-6f2b8c7d1e40" + }, + "text": { + "type": "string", + "description": "Plain-text note body.", + "example": "Adjusted the meter-reading journey for the §14a rollout." + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "Set the first time the note text is edited." + }, + "created_by": { + "$ref": "#/components/schemas/CallerIdentity" + } + } + }, + "BlueprintID": { + "type": "string", + "description": "ID of a blueprint", + "example": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341" + }, + "BlueprintResourceID": { + "type": "string", + "description": "ID of a blueprint resource", + "example": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341" + }, + "CommonBlueprintFields": { "type": "object", "properties": { "id": { @@ -3541,6 +4487,13 @@ } } }, + "notes": { + "type": "array", + "description": "Internal collaboration notes, oldest first. Each entry is stamped with\nits author and creation time server-side; an edit rewrites only `text`\nand stamps `updated_at`, so the history of who noted what stays intact.\nWritten via `addBlueprintNote` / `updateBlueprintNote` /\n`deleteBlueprintNote`, not by `updateBlueprint`.\n\nAvailable on every blueprint including marketplace ones (whose\n`description` is read-only), and never included in the published\nmarketplace package — see `buildMetadata` in\n`services/blueprint-v3/published-blueprint.ts`. Carried to a\ndestination org only when an install/sync passes\n`options.sync_notes: true`.\n", + "items": { + "$ref": "#/components/schemas/BlueprintNote" + } + }, "version": { "type": "string" }, @@ -3569,6 +4522,14 @@ "type": "string", "format": "date-time" }, + "performed_by": { + "allOf": [ + { + "$ref": "#/components/schemas/CallerIdentity" + } + ], + "description": "Who actually ran this synchronization — the caller of the install job.\nAbsent on deployments recorded before this field existed; consumers\nshould fall back to the blueprint's `updated_by` for those.\n" + }, "note": { "type": "string", "description": "User-provided note about this synchronization" @@ -3708,6 +4669,17 @@ "FAILED" ] }, + "active_restore_job_id": { + "type": "string", + "nullable": true, + "description": "ID of the restore (revert) job currently holding a lock on this\nblueprint instance's resources, or null/absent when no revert is\nrunning. Deliberately separate from `installation_status`: installs\nand reverts stay decoupled, so a revert must not overwrite install\nhealth — `installation_status` keeps reflecting the actual install\noutcome (and drives the install badge in the Blueprints overview)\nfor the duration of the revert.\n" + }, + "active_restore_started_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "ISO timestamp of when the current restore lock\n(`active_restore_job_id`) was acquired, or null/absent when no revert\nis running. Written and cleared alongside `active_restore_job_id`.\nLets the lock self-expire: a lock older than the staleness threshold\n(whose job never completed the sweep, e.g. the state machine was\naborted) is treated as stale so a new revert can proceed instead of\nbeing blocked forever.\n" + }, "created_at": { "type": "string", "format": "date-time" @@ -3830,6 +4802,31 @@ } } }, + "SkippedBlueprintResource": { + "type": "object", + "description": "A resource that was requested (or discovered as a dependency) but was not\nadded to the Blueprint. Reasons are stable machine-readable codes.\n", + "required": [ + "id", + "type", + "reason" + ], + "properties": { + "id": { + "$ref": "#/components/schemas/BlueprintResourceID" + }, + "type": { + "$ref": "#/components/schemas/ResourceNodeType" + }, + "reason": { + "type": "string", + "enum": [ + "not_found", + "source_validation_failed", + "enrichment_failed" + ] + } + } + }, "BlueprintResource": { "type": "object", "properties": { @@ -3892,7 +4889,8 @@ "internal-update", "no-op", "delete", - "ignored" + "ignored", + "error" ] } }, @@ -3945,6 +4943,14 @@ "file" ] }, + "sync_engine": { + "type": "string", + "description": "Engine that must install this preview, detected from the uploaded archive's own format (never from a feature flag or request parameter). `v3` is only reported for a correctly shaped and validly signed V3 package. Clients must install a `v3` preview through `POST /v3/blueprint-manifest/blueprint:install` and a `terraform` preview through `POST /v2/blueprint-manifest/blueprints:install`. Absent only on previews created before format detection shipped; treat an absent value as `terraform`.", + "enum": [ + "terraform", + "v3" + ] + }, "blueprint_file_s3_key": { "type": "string", "description": "S3 key of the blueprint zip file" @@ -4355,11 +5361,15 @@ "$ref": "#/components/schemas/V3ResourceProgressEntry" } }, + "options": { + "$ref": "#/components/schemas/BlueprintInstallationJobOptions" + }, "status": { "type": "string", "enum": [ "IN_PROGRESS", "WAITING_USER_ACTION", + "REAUTH_REQUIRED", "CANCELED", "SUCCESS", "PARTIAL_SUCCESS", @@ -4370,162 +5380,743 @@ } ] }, + "BulkInstallStatus": { + "type": "string", + "description": "Aggregate status for a bulk install or one of its targets.\n- `QUEUED`: not started yet\n- `IN_PROGRESS`: at least one target queued/in-progress, not all done\n- `SUCCESS`: all targets succeeded\n- `PARTIAL_SUCCESS`: all targets terminal with a mix of success/partial/failure\n- `FAILED`: all targets terminal and none succeeded or partially succeeded\n", + "enum": [ + "QUEUED", + "IN_PROGRESS", + "SUCCESS", + "PARTIAL_SUCCESS", + "FAILED" + ] + }, + "BulkInstallCounts": { + "type": "object", + "description": "Tally of target rows by status. Recomputed from target rows on each transition.", + "properties": { + "queued": { + "type": "integer" + }, + "in_progress": { + "type": "integer" + }, + "success": { + "type": "integer" + }, + "partial_success": { + "type": "integer" + }, + "failed": { + "type": "integer" + } + } + }, + "BulkInstall": { + "type": "object", + "description": "Bulk install parent. Never carries target auth tokens.", + "properties": { + "bulk_job_id": { + "type": "string" + }, + "source_org_id": { + "type": "string" + }, + "source_blueprint_id": { + "$ref": "#/components/schemas/BlueprintID" + }, + "status": { + "$ref": "#/components/schemas/BulkInstallStatus" + }, + "target_count": { + "type": "integer" + }, + "max_concurrency": { + "type": "integer" + }, + "counts": { + "$ref": "#/components/schemas/BulkInstallCounts" + }, + "slug": { + "type": "string" + }, + "options": { + "$ref": "#/components/schemas/BlueprintInstallationJobOptions" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + } + }, + "BulkInstallTarget": { + "type": "object", + "description": "A single destination of a bulk install. `job` is the hydrated latest child\ninstall job derived from `job_ids.at(-1)`. Auth tokens are never stored or returned.\n", + "properties": { + "bulk_job_id": { + "type": "string" + }, + "destination_org_id": { + "type": "string" + }, + "destination_blueprint_id": { + "$ref": "#/components/schemas/BlueprintID" + }, + "status": { + "$ref": "#/components/schemas/BulkInstallStatus" + }, + "job_ids": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BlueprintJobID" + } + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "job": { + "allOf": [ + { + "$ref": "#/components/schemas/BlueprintInstallationJob" + } + ], + "nullable": true, + "description": "The hydrated latest child install job (`job_ids.at(-1)`), when present." + } + } + }, + "BulkInstallTargetList": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BulkInstallTarget" + } + }, + "next_cursor": { + "type": "string", + "description": "Opaque cursor for the next page. Absent on the last page." + } + } + }, + "BulkInstallTargetInput": { + "type": "object", + "required": [ + "destination_org_id", + "destination_auth_token" + ], + "properties": { + "destination_org_id": { + "type": "string" + }, + "destination_blueprint_id": { + "$ref": "#/components/schemas/BlueprintID" + }, + "destination_auth_token": { + "type": "string", + "description": "Write-only org-scoped token for the destination org. Never persisted or returned." + } + } + }, + "BulkInstallCreateRequest": { + "type": "object", + "required": [ + "source_blueprint_id", + "targets" + ], + "properties": { + "source_org_id": { + "type": "string", + "description": "The org that owns the source blueprint. Optional; defaults to the caller org and,\nif provided, must equal it — source reads use the caller's token, and the bulk job\nis owned/polled by the caller org. A different value is rejected with 400.\n" + }, + "source_blueprint_id": { + "$ref": "#/components/schemas/BlueprintID" + }, + "max_concurrency": { + "type": "integer", + "minimum": 1, + "maximum": 5, + "default": 2, + "description": "Maximum number of concurrently active child installs." + }, + "slug": { + "type": "string" + }, + "options": { + "$ref": "#/components/schemas/BlueprintInstallationJobOptions" + }, + "targets": { + "type": "array", + "minItems": 1, + "maxItems": 100, + "items": { + "$ref": "#/components/schemas/BulkInstallTargetInput" + } + } + } + }, "BlueprintRestoreJob": { "allOf": [ { "$ref": "#/components/schemas/CommonBlueprintJobFields" }, - { + { + "type": "object", + "properties": { + "job_type": { + "type": "string", + "enum": [ + "restore" + ] + }, + "destination_blueprint_id": { + "$ref": "#/components/schemas/BlueprintID" + }, + "destination_org_id": { + "type": "string" + }, + "install_job_id": { + "type": "string", + "nullable": true, + "description": "The install job whose deployment is being reverted. Maps back\nto the entry in `Blueprint.deployments[]`.\n" + }, + "snapshot_id": { + "type": "string", + "nullable": true, + "description": "The snapshot driving Phase 1 of the restore. Null for sweep-only\nrestores (pure-create deployments with no captured manifest).\n" + }, + "sync_engine": { + "type": "string", + "enum": [ + "v3" + ] + }, + "status": { + "type": "string", + "enum": [ + "IN_PROGRESS", + "SUCCESS", + "PARTIAL_SUCCESS", + "FAILED" + ] + }, + "restore_result": { + "nullable": true, + "description": "Absent while the job is still IN_PROGRESS.", + "allOf": [ + { + "$ref": "#/components/schemas/RestoreOutcome" + } + ] + } + } + } + ] + }, + "V3ResourceProgressEntry": { + "type": "object", + "required": [ + "lineage_id", + "type", + "address", + "status" + ], + "properties": { + "lineage_id": { + "type": "string" + }, + "type": { + "type": "string" + }, + "address": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "pending", + "in_progress", + "done", + "failed", + "skipped" + ] + }, + "target_id": { + "type": "string" + }, + "error_message": { + "type": "string" + }, + "error_code": { + "type": "string", + "description": "Stable machine code for a failed resource, when config-engine threw a\ntyped error. Keys a translation in the Blueprints UI; `error_message`\nis the English fallback when absent or unrecognised.\n" + }, + "error_data": { + "type": "object", + "additionalProperties": true, + "description": "Interpolation values for the translated `error_code` message." + } + } + }, + "RestoreOutcomeItem": { + "type": "object", + "required": [ + "lineage_id", + "type", + "action" + ], + "properties": { + "lineage_id": { + "type": "string" + }, + "type": { + "type": "string" + }, + "name": { + "type": "string", + "nullable": true + }, + "target_id": { + "type": "string", + "nullable": true + }, + "action": { + "type": "string", + "description": "On `restore-preview`: the action the restore would take.\nOn `restore_result`: the action that was applied.\n`failed` only appears on `restore_result`.\n", + "enum": [ + "restore", + "delete", + "skip", + "failed" + ] + }, + "reason": { + "type": "string", + "nullable": true, + "description": "Only set when `action == skip`.", + "enum": [ + "modified", + "delete_unsupported", + "heuristic_match", + "co_owned", + "dependency_protected", + "already_deleted", + "non_revertible" + ] + }, + "last_synced_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Only set when `reason == modified`. From the lineage row's last install write." + }, + "current_updated_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Only set when `reason == modified`. From the destination resource's current state." + }, + "error_message": { + "type": "string", + "nullable": true, + "description": "Only set when `action == failed`." + }, + "is_hidden": { + "type": "boolean", + "description": "Mirrors the install manifest's `is_hidden` for this resource —\nhelper resources (entity mappings, datasources, flow-template\nautomations) the UI hides on every other resources view. Absent\non rows from legacy installs without a persisted manifest.\n" + }, + "co_owned_by": { + "type": "array", + "description": "Only set when `reason == co_owned`. The other live blueprint\ninstances that still own this resource's lineage row — the\n\"another sync\" the skip refers to.\n", + "items": { + "type": "object", + "required": [ + "blueprint_id" + ], + "properties": { + "blueprint_id": { + "type": "string" + }, + "title": { + "type": "string", + "nullable": true + } + } + } + }, + "protected_by": { + "type": "array", + "description": "Only set when `reason == dependency_protected`. The surviving\nresource(s) whose 'hard' reference to this one is why it wasn't\ndeleted (e.g. a skipped Journey still triggering this Automation).\n", + "items": { + "type": "object", + "required": [ + "lineage_id", + "type" + ], + "properties": { + "lineage_id": { + "type": "string" + }, + "type": { + "type": "string" + }, + "target_id": { + "type": "string", + "nullable": true + } + } + } + } + } + }, + "HealthFinding": { + "type": "object", + "required": [ + "check_id", + "code", + "severity", + "resource_type", + "lineage_id", + "message" + ], + "properties": { + "check_id": { + "type": "string", + "enum": [ + "live_readability", + "referential_integrity", + "install_completeness", + "schema_consistency", + "mapping_integrity", + "execution_readiness", + "catalog_hygiene", + "spec_conformance" + ] + }, + "code": { + "type": "string", + "enum": [ + "missing_in_destination", + "fetch_error", + "no_readback", + "unreplaced_source_reference", + "broken_internal_reference", + "dropped_at_install", + "orphaned_group_reference", + "uuid_group_label", + "unreachable_attribute_purpose", + "duplicate_attribute_name", + "duplicate_headline", + "broken_mapping_reference", + "stale_mapping_version", + "mapping_slot_mismatch", + "mapping_parity_mismatch", + "stale_lineage", + "broken_trigger_reference", + "broken_action_reference", + "broken_workflow_step", + "invalid_mapping_target", + "invalid_mapping_source", + "invalid_mapping_version", + "unknown_mapping_attribute", + "workflow_edge_limit", + "dead_purpose_reference", + "broken_portal_reference", + "orphaned_portal_block", + "broken_closing_reason", + "broken_journey_settings", + "unresolved_assignee", + "current_write_invalid", + "duplicate_live_resource", + "missing_file_content", + "file_etag_mismatch", + "incomplete_webhook", + "broken_template_reference", + "orphaned_price", + "duplicate_price", + "inactive_price_in_use", + "cross_org_reference" + ] + }, + "severity": { + "type": "string", + "enum": [ + "error", + "warning", + "info" + ] + }, + "resource_type": { + "type": "string" + }, + "lineage_id": { + "type": "string" + }, + "target_id": { + "type": "string" + }, + "resource_name": { + "type": "string", + "description": "Human-readable name of the affected resource" + }, + "message": { + "type": "string" + }, + "verdict": { + "type": "string", + "description": "Mechanical root-cause classification and suggested fix, when determinable" + }, + "evidence": { "type": "object", + "description": "Check-specific evidence, e.g. the JSON path and referenced id of a broken reference.", "properties": { - "job_type": { - "type": "string", - "enum": [ - "restore" - ] + "path": { + "type": "string" }, - "destination_blueprint_id": { - "$ref": "#/components/schemas/BlueprintID" + "referenced_id": { + "type": "string" }, - "destination_org_id": { + "referenced_type": { "type": "string" }, - "install_job_id": { + "referenced_name": { "type": "string", - "nullable": true, - "description": "The install job whose deployment is being reverted. Maps back\nto the entry in `Blueprint.deployments[]`.\n" + "description": "Human-readable name or logical slug of the referenced source resource" }, - "snapshot_id": { + "referenced_lineage_id": { "type": "string", - "nullable": true, - "description": "The snapshot driving Phase 1 of the restore. Null for sweep-only\nrestores (pure-create deployments with no captured manifest).\n" + "description": "Logical source lineage identity of the referenced resource" }, - "sync_engine": { + "expected_target_id": { "type": "string", - "enum": [ - "v3" - ] + "description": "Destination id that should have replaced the source reference" }, - "status": { + "reference_kind": { "type": "string", - "enum": [ - "IN_PROGRESS", - "SUCCESS", - "PARTIAL_SUCCESS", - "FAILED" - ] + "description": "Human-readable description of the exact field carrying the reference" }, - "restore_result": { - "nullable": true, - "description": "Absent while the job is still IN_PROGRESS.", - "allOf": [ - { - "$ref": "#/components/schemas/RestoreOutcome" - } - ] + "operation": { + "type": "string" + }, + "rejection_reason": { + "type": "string" + }, + "error_message": { + "type": "string" + }, + "response_status": { + "type": "integer" } } } - ] + } }, - "V3ResourceProgressEntry": { + "HealthCheckCoverage": { "type": "object", "required": [ - "lineage_id", - "type", - "address", - "status" + "check_id", + "status", + "scope", + "resources_considered", + "details" ], "properties": { - "lineage_id": { - "type": "string" - }, - "type": { - "type": "string" - }, - "address": { - "type": "string" - }, - "name": { - "type": "string" + "check_id": { + "type": "string", + "enum": [ + "live_readability", + "referential_integrity", + "install_completeness", + "schema_consistency", + "mapping_integrity", + "execution_readiness", + "spec_conformance" + ] }, "status": { "type": "string", "enum": [ - "pending", - "in_progress", - "done", - "failed", - "skipped" + "completed", + "partial", + "skipped", + "not_applicable" ] }, - "target_id": { - "type": "string" + "scope": { + "type": "string", + "enum": [ + "tracked_resources", + "org_wide", + "source_comparison" + ] }, - "error_message": { - "type": "string" + "resources_considered": { + "type": "integer" + }, + "details": { + "type": "array", + "items": { + "type": "string" + } } } }, - "RestoreOutcomeItem": { + "HealthResourceTypeCoverage": { "type": "object", "required": [ - "lineage_id", - "type", - "action" + "resource_type", + "tracked", + "readable_by_lineage", + "missing_by_lineage", + "read_errors", + "unchecked", + "specialized_checks", + "known_blind_spots" ], "properties": { - "lineage_id": { + "resource_type": { "type": "string" }, - "type": { - "type": "string" + "tracked": { + "type": "integer" }, - "name": { - "type": "string", - "nullable": true + "readable_by_lineage": { + "type": "integer" }, - "target_id": { - "type": "string", - "nullable": true + "missing_by_lineage": { + "type": "integer" }, - "action": { + "read_errors": { + "type": "integer" + }, + "unchecked": { + "type": "integer" + }, + "specialized_checks": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "live_readability", + "referential_integrity", + "install_completeness", + "schema_consistency", + "mapping_integrity", + "execution_readiness", + "spec_conformance" + ] + } + }, + "known_blind_spots": { + "type": "array", + "description": "Type-specific contracts the current scanner explicitly does not claim to validate.", + "items": { + "type": "string" + } + } + } + }, + "HealthScanCoverage": { + "type": "object", + "required": [ + "checks", + "resource_types" + ], + "properties": { + "checks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HealthCheckCoverage" + } + }, + "resource_types": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HealthResourceTypeCoverage" + } + } + } + }, + "DeploymentHealthReport": { + "type": "object", + "required": [ + "status", + "job_id", + "blueprint_instance_id", + "destination_org_id", + "generated_at" + ], + "properties": { + "status": { "type": "string", - "description": "On `restore-preview`: the action the restore would take.\nOn `restore_result`: the action that was applied.\n`failed` only appears on `restore_result`.\n", "enum": [ - "restore", - "delete", - "skip", + "running", + "completed", "failed" ] }, - "reason": { - "type": "string", - "nullable": true, - "description": "Only set when `action == skip`.", - "enum": [ - "modified", - "co_owned", - "delete_unsupported", - "heuristic_match" - ] + "job_id": { + "$ref": "#/components/schemas/BlueprintJobID" }, - "last_synced_at": { + "blueprint_instance_id": { + "$ref": "#/components/schemas/BlueprintID" + }, + "destination_org_id": { + "type": "string" + }, + "generated_at": { "type": "string", - "format": "date-time", - "nullable": true, - "description": "Only set when `reason == modified`. From the lineage row's last install write." + "format": "date-time" }, - "current_updated_at": { + "html_url": { "type": "string", - "format": "date-time", - "nullable": true, - "description": "Only set when `reason == modified`. From the destination resource's current state." + "description": "Presigned link to the rendered HTML report (present once the check completed)" }, - "error_message": { + "summary": { + "type": "object", + "properties": { + "resources_scanned": { + "type": "integer" + }, + "unchecked": { + "type": "integer" + }, + "errors": { + "type": "integer" + }, + "warnings": { + "type": "integer" + }, + "infos": { + "type": "integer" + } + } + }, + "findings": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HealthFinding" + } + }, + "coverage": { + "$ref": "#/components/schemas/HealthScanCoverage" + }, + "error": { "type": "string", - "nullable": true, - "description": "Only set when `action == failed`." + "description": "Failure reason when status is `failed`." } } }, @@ -4542,6 +6133,10 @@ "items": { "$ref": "#/components/schemas/RestoreOutcomeItem" } + }, + "has_effective_changes": { + "type": "boolean", + "description": "`true` iff at least one entry in `resources` has an effective\naction (`action` is `restore` or `delete`). `false` when every\nresource would be skipped (all `skip` / `failed`). Consumers\ncan gate the \"Revert sync\" confirm button on this — if false,\nexecuting the revert is a no-op.\n" } } }, @@ -4674,6 +6269,22 @@ "destination_blueprint_id": { "$ref": "#/components/schemas/BlueprintID" }, + "installation_job_id": { + "allOf": [ + { + "$ref": "#/components/schemas/BlueprintJobID" + } + ], + "description": "Install job this verification is checking, when known." + }, + "sync_engine": { + "type": "string", + "enum": [ + "terraform", + "v3" + ], + "description": "Install engine used by the linked installation job, when known." + }, "status": { "type": "string", "enum": [ @@ -4731,6 +6342,16 @@ "destination_blueprint_id": { "$ref": "#/components/schemas/BlueprintID" }, + "installation_job_id": { + "$ref": "#/components/schemas/BlueprintJobID" + }, + "sync_engine": { + "type": "string", + "enum": [ + "terraform", + "v3" + ] + }, "summary": { "$ref": "#/components/schemas/VerificationSummary" } @@ -4768,9 +6389,15 @@ "source_resource_id": { "type": "string" }, + "source_resource_address": { + "type": "string" + }, "destination_resource_id": { "type": "string" }, + "destination_resource_address": { + "type": "string" + }, "status": { "type": "string", "enum": [ @@ -4780,6 +6407,32 @@ "fetch_error" ] }, + "failure_context": { + "type": "string", + "enum": [ + "depends_on_failed_resource", + "may_be_caused_by_failed_dependency" + ], + "description": "Explains whether this result is likely downstream of another failed resource." + }, + "failed_dependency_resource_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "failed_dependency_resource_names": { + "type": "array", + "items": { + "type": "string" + } + }, + "failed_dependency_addresses": { + "type": "array", + "items": { + "type": "string" + } + }, "field_diffs": { "type": "array", "items": { @@ -4860,9 +6513,31 @@ "items": { "type": "string" } + }, + "sync_notes": { + "description": "When `true`, the source blueprint's `notes` overwrite the destination\nblueprint's `notes`. Defaults to `false`, which leaves the\ndestination's notes untouched.\n", + "type": "boolean", + "default": false } } }, + "ContinueInstallationJobRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/BlueprintInstallationJobOptions" + }, + { + "type": "object", + "properties": { + "source_auth_token": { + "description": "A freshly issued bearer for the SOURCE organization, used only when resuming a\njob paused at `REAUTH_REQUIRED` whose source credential also expired. Applies to\ncross-org installs: apply-time reads such as the authenticated File API fallback\nfor private source files run against the source org, so the destination bearer\ncannot stand in for them. Omit it to keep the source token the job already has.\n", + "type": "string", + "format": "password" + } + } + } + ] + }, "ManifestID": { "type": "string", "description": "ID of an imported / installed manifest", @@ -4935,6 +6610,7 @@ "PENDING", "STARTED", "WAITING_USER_ACTION", + "REAUTH_REQUIRED", "CANCELED", "IN_PROGRESS", "SUCCESS", @@ -4968,6 +6644,7 @@ "webhook", "integration", "dashboard", + "insight", "custom_variable", "usergroup", "saved_view", diff --git a/packages/cli/definitions/calendar.json b/packages/cli/definitions/calendar.json index 3107b7468..39e6720b7 100644 --- a/packages/cli/definitions/calendar.json +++ b/packages/cli/definitions/calendar.json @@ -3,16 +3,24 @@ "info": { "title": "Calendar API", "version": "0.1.0", - "description": "epilot's calendar API.\n\nExposes calendars and events as first-class epilot resources.\nToday, data is sourced from external providers (Microsoft 365, Google Calendar) synced into epilot.\nIn the future, epilot will also host native calendars created directly in the platform.\n\nThe provider is an implementation detail surfaced only through each resource's `source` field.\nConsumers interact with calendars and events through the same API regardless of origin." + "description": "epilot's calendar API.\n\nExposes calendars and events as first-class epilot resources." }, "tags": [ + { + "name": "Absence", + "description": "User absence from calendar events and absence adjustments" + }, { "name": "Calendars", "description": "User and organization calendars" }, { - "name": "Events", + "name": "Calendar Events", "description": "Calendar events" + }, + { + "name": "Working Hours", + "description": "Recurring weekly working hours of users" } ], "servers": [ @@ -29,94 +37,326 @@ } ], "paths": { - "/v1/calendar": { + "/v1/calendar/absence/users": { "get": { - "operationId": "listCalendars", - "summary": "listCalendars", - "description": "List calendars visible to the caller.", + "operationId": "listUsersAbsence", + "summary": "listUsersAbsence", + "description": "List organization users with known absence metadata in the requested time window.", "tags": [ - "Calendars" + "Absence" ], "parameters": [ { "in": "query", - "name": "size", + "name": "from", + "description": "Start of the time window (inclusive). Maximum window: 31 days.", + "schema": { + "type": "string", + "format": "date-time", + "description": "Start of the time window (inclusive). Maximum window: 31 days." + }, + "required": true + }, + { + "in": "query", + "name": "to", + "description": "End of the time window (exclusive). Must be after from.", + "schema": { + "type": "string", + "format": "date-time", + "description": "End of the time window (exclusive). Must be after from." + }, + "required": true + }, + { + "in": "query", + "name": "include_busy", + "description": "Include busy inputs in addition to out-of-office absence. Defaults to false.", + "schema": { + "type": "boolean", + "description": "Include busy inputs in addition to out-of-office absence. Defaults to false.", + "default": false + } + }, + { + "in": "query", + "name": "working_hours_granularity", + "description": "At what granularity working hours contribute to absence. \"time\" (default): all time outside a user's working windows counts as absent. \"day\": only full days with no working windows count as absent (whole-day granularity, used by thread assignment). Users without a working-hours record never produce working-hours intervals.", + "schema": { + "type": "string", + "enum": [ + "time", + "day" + ], + "description": "At what granularity working hours contribute to absence. \"time\" (default): all time outside a user's working windows counts as absent. \"day\": only full days with no working windows count as absent (whole-day granularity, used by thread assignment). Users without a working-hours record never produce working-hours intervals.", + "default": "time" + } + }, + { + "in": "query", + "name": "query", + "description": "Optional user directory search query.", + "schema": { + "type": "string", + "description": "Optional user directory search query." + } + }, + { + "in": "query", + "name": "limit", + "description": "Maximum users to return.", "schema": { "type": "integer", "minimum": 1, - "maximum": 250 + "maximum": 100, + "description": "Maximum users to return." } }, { "in": "query", - "name": "cursor", - "description": "Opaque cursor from a previous response", + "name": "offset", + "description": "User directory offset.", "schema": { - "type": "string", - "description": "Opaque cursor from a previous response" + "type": "integer", + "minimum": 0, + "description": "User directory offset." } } ], "responses": { "200": { - "description": "List of calendars", + "description": "Organization users absence", "content": { "application/json": { "schema": { "type": "object", "properties": { + "from": { + "type": "string", + "format": "date-time" + }, + "to": { + "type": "string", + "format": "date-time" + }, + "hits": { + "type": "integer", + "minimum": 0 + }, "results": { "type": "array", "items": { - "$ref": "#/components/schemas/Calendar" + "type": "object", + "properties": { + "absent": { + "type": "boolean" + }, + "absence_intervals": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AbsenceInterval" + } + }, + "user_id": { + "type": "string" + }, + "display_name": { + "type": "string", + "nullable": true + }, + "email": { + "type": "string", + "nullable": true, + "format": "email" + }, + "status": { + "type": "string", + "nullable": true + } + }, + "required": [ + "absent", + "absence_intervals", + "user_id" + ] } - }, - "next_cursor": { - "type": "string", - "nullable": true } }, "required": [ + "from", + "to", + "hits", "results" ] } } } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } } } } }, - "/v1/calendar/{calendar_id}": { - "get": { - "operationId": "getCalendar", - "summary": "getCalendar", - "description": "Get a single calendar by its epilot ID.", + "/v1/calendar/absence:search": { + "post": { + "operationId": "searchAbsence", + "summary": "searchAbsence", + "description": "Search known absence for candidate users in the requested time window.", "tags": [ - "Calendars" + "Absence" ], - "parameters": [ - { - "in": "path", - "name": "calendar_id", - "schema": { - "type": "string" - }, - "required": true + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SearchAbsenceBody" + } + } + } + }, + "responses": { + "200": { + "description": "Users absence", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "from": { + "type": "string", + "format": "date-time" + }, + "to": { + "type": "string", + "format": "date-time" + }, + "users": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "absent": { + "type": "boolean" + }, + "absence_intervals": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AbsenceInterval" + } + } + }, + "required": [ + "absent", + "absence_intervals" + ] + } + } + }, + "required": [ + "from", + "to", + "users" + ] + } + } + } + }, + "400": { + "description": "Invalid request body", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } } + } + } + }, + "/v1/calendar/absence:search-now": { + "post": { + "operationId": "searchNowAbsence", + "summary": "searchNowAbsence", + "description": "Search known absence for candidate users at the current server time.", + "tags": [ + "Absence" ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SearchNowAbsenceBody" + } + } + } + }, "responses": { "200": { - "description": "Calendar", + "description": "Users current absence", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Calendar" + "type": "object", + "properties": { + "from": { + "type": "string", + "format": "date-time" + }, + "to": { + "type": "string", + "format": "date-time" + }, + "users": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "absent": { + "type": "boolean" + }, + "absence_intervals": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AbsenceInterval" + } + }, + "absent_until": { + "type": "string", + "nullable": true, + "format": "date-time", + "description": "End of the currently active absence interval, or null when the user is not absent." + } + }, + "required": [ + "absent", + "absence_intervals", + "absent_until" + ] + } + } + }, + "required": [ + "from", + "to", + "users" + ] } } } }, - "404": { - "description": "Resource not found", + "400": { + "description": "Invalid request body", "content": { "application/json": { "schema": { @@ -128,160 +368,1935 @@ } } }, - "/v1/calendar/events": { + "/v1/calendar/absence/users/{user_id}/adjustments": { "get": { - "operationId": "listEvents", - "summary": "listEvents", - "description": "List events in a time window. Recurring events are returned as expanded occurrences – each instance appears as its own event.", + "operationId": "listAbsenceAdjustments", + "summary": "listAbsenceAdjustments", + "description": "List absence adjustments for a user in a time window.", "tags": [ - "Events" + "Absence" ], "parameters": [ { - "in": "query", - "name": "from", - "description": "Start of the time window (inclusive)", + "in": "path", + "name": "user_id", "schema": { "type": "string", - "format": "date-time", - "description": "Start of the time window (inclusive)" + "minLength": 1 }, "required": true }, { "in": "query", - "name": "to", - "description": "End of the time window (exclusive)", + "name": "from", + "description": "Start of the time window (inclusive). Maximum window: 90 days.", "schema": { "type": "string", "format": "date-time", - "description": "End of the time window (exclusive)" + "description": "Start of the time window (inclusive). Maximum window: 90 days." }, "required": true }, { "in": "query", - "name": "calendar_id", - "description": "Filter to a single calendar. Omit to query across all accessible calendars.", + "name": "to", + "description": "End of the time window (exclusive). Must be after from.", "schema": { "type": "string", - "description": "Filter to a single calendar. Omit to query across all accessible calendars." - } - }, - { - "in": "query", - "name": "size", - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 250 - } - }, - { - "in": "query", - "name": "cursor", - "schema": { - "type": "string" - } + "format": "date-time", + "description": "End of the time window (exclusive). Must be after from." + }, + "required": true } ], "responses": { "200": { - "description": "List of events", + "description": "Absence adjustments", "content": { "application/json": { "schema": { "type": "object", "properties": { + "from": { + "type": "string", + "format": "date-time" + }, + "to": { + "type": "string", + "format": "date-time" + }, "results": { "type": "array", "items": { - "$ref": "#/components/schemas/CalendarEvent" + "$ref": "#/components/schemas/AbsenceAdjustment" } - }, - "next_cursor": { - "type": "string", - "nullable": true } }, "required": [ + "from", + "to", "results" ] } } } }, - "400": { - "description": "Invalid request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - } - } - }, - "/v1/calendar/events/{event_id}": { - "get": { - "operationId": "getEvent", - "summary": "getEvent", - "description": "Get a single event by its epilot ID.", - "tags": [ - "Events" - ], - "parameters": [ - { - "in": "path", - "name": "event_id", - "schema": { - "type": "string" + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "operationId": "createAbsenceAdjustment", + "summary": "createAbsenceAdjustment", + "description": "Create a time-bound absence adjustment for a user in the caller organization.", + "tags": [ + "Absence" + ], + "parameters": [ + { + "in": "path", + "name": "user_id", + "schema": { + "type": "string", + "minLength": 1 + }, + "required": true + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateAbsenceAdjustmentBody" + } + } + } + }, + "responses": { + "201": { + "description": "Created absence adjustment", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AbsenceAdjustment" + } + } + } + }, + "400": { + "description": "Invalid request body", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/calendar/absence/users/{user_id}/adjustments/{adjustment_id}": { + "get": { + "operationId": "getAbsenceAdjustment", + "summary": "getAbsenceAdjustment", + "description": "Get an absence adjustment by ID.", + "tags": [ + "Absence" + ], + "parameters": [ + { + "in": "path", + "name": "user_id", + "schema": { + "type": "string", + "minLength": 1 + }, + "required": true + }, + { + "in": "path", + "name": "adjustment_id", + "schema": { + "type": "string", + "minLength": 1 + }, + "required": true + } + ], + "responses": { + "200": { + "description": "Absence adjustment", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AbsenceAdjustment" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "patch": { + "operationId": "patchAbsenceAdjustment", + "summary": "patchAbsenceAdjustment", + "description": "Update an absence adjustment in the caller organization.", + "tags": [ + "Absence" + ], + "parameters": [ + { + "in": "path", + "name": "user_id", + "schema": { + "type": "string", + "minLength": 1 + }, + "required": true + }, + { + "in": "path", + "name": "adjustment_id", + "schema": { + "type": "string", + "minLength": 1 + }, + "required": true + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PatchAbsenceAdjustmentBody" + } + } + } + }, + "responses": { + "200": { + "description": "Updated absence adjustment", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AbsenceAdjustment" + } + } + } + }, + "400": { + "description": "Invalid request body", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteAbsenceAdjustment", + "summary": "deleteAbsenceAdjustment", + "description": "Delete an absence adjustment.", + "tags": [ + "Absence" + ], + "parameters": [ + { + "in": "path", + "name": "user_id", + "schema": { + "type": "string", + "minLength": 1 + }, + "required": true + }, + { + "in": "path", + "name": "adjustment_id", + "schema": { + "type": "string", + "minLength": 1 + }, + "required": true + } + ], + "responses": { + "204": { + "description": "Absence adjustment deleted" + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/calendar/absence/users/{user_id}": { + "get": { + "operationId": "getUserAbsence", + "summary": "getUserAbsence", + "description": "Get known absence for a user in a time window. absent=false means no known absence, not guaranteed availability.", + "tags": [ + "Absence" + ], + "parameters": [ + { + "in": "path", + "name": "user_id", + "schema": { + "type": "string", + "minLength": 1 + }, + "required": true + }, + { + "in": "query", + "name": "from", + "description": "Start of the time window (inclusive). Maximum window: 31 days.", + "schema": { + "type": "string", + "format": "date-time", + "description": "Start of the time window (inclusive). Maximum window: 31 days." + }, + "required": true + }, + { + "in": "query", + "name": "to", + "description": "End of the time window (exclusive). Must be after from.", + "schema": { + "type": "string", + "format": "date-time", + "description": "End of the time window (exclusive). Must be after from." + }, + "required": true + }, + { + "in": "query", + "name": "include_busy", + "description": "Include busy inputs in addition to out-of-office absence. Defaults to false.", + "schema": { + "type": "boolean", + "description": "Include busy inputs in addition to out-of-office absence. Defaults to false.", + "default": false + } + }, + { + "in": "query", + "name": "working_hours_granularity", + "description": "At what granularity working hours contribute to absence. \"time\" (default): all time outside a user's working windows counts as absent. \"day\": only full days with no working windows count as absent (whole-day granularity, used by thread assignment). Users without a working-hours record never produce working-hours intervals.", + "schema": { + "type": "string", + "enum": [ + "time", + "day" + ], + "description": "At what granularity working hours contribute to absence. \"time\" (default): all time outside a user's working windows counts as absent. \"day\": only full days with no working windows count as absent (whole-day granularity, used by thread assignment). Users without a working-hours record never produce working-hours intervals.", + "default": "time" + } + } + ], + "responses": { + "200": { + "description": "User absence", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "absent": { + "type": "boolean" + }, + "absence_intervals": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AbsenceInterval" + } + }, + "from": { + "type": "string", + "format": "date-time" + }, + "to": { + "type": "string", + "format": "date-time" + }, + "user_id": { + "type": "string" + }, + "external_calendars": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ExternalCalendar" + }, + "description": "The user's connected external calendars (Outlook/Google), if any." + } + }, + "required": [ + "absent", + "absence_intervals", + "from", + "to", + "user_id" + ] + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/calendar/working-hours/users/{user_id}": { + "get": { + "operationId": "getWorkingHours", + "summary": "getWorkingHours", + "description": "Get the recurring weekly working hours of a user. 404 means no record exists and the user is treated as always available.", + "tags": [ + "Working Hours" + ], + "parameters": [ + { + "in": "path", + "name": "user_id", + "schema": { + "type": "string", + "minLength": 1 + }, + "required": true + } + ], + "responses": { + "200": { + "description": "Working hours", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkingHours" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "put": { + "operationId": "putWorkingHours", + "summary": "putWorkingHours", + "description": "Create or fully replace the working hours of a user in the caller organization. This is a full replace, not a merge.", + "tags": [ + "Working Hours" + ], + "parameters": [ + { + "in": "path", + "name": "user_id", + "schema": { + "type": "string", + "minLength": 1 + }, + "required": true + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpsertWorkingHoursBody" + } + } + } + }, + "responses": { + "200": { + "description": "Stored working hours", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkingHours" + } + } + } + }, + "400": { + "description": "Invalid request body", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteWorkingHours", + "summary": "deleteWorkingHours", + "description": "Delete the working hours of a user. The user is then treated as always available again.", + "tags": [ + "Working Hours" + ], + "parameters": [ + { + "in": "path", + "name": "user_id", + "schema": { + "type": "string", + "minLength": 1 + }, + "required": true + } + ], + "responses": { + "204": { + "description": "Working hours deleted" + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/calendar": { + "get": { + "operationId": "listCalendars", + "summary": "listCalendars", + "description": "List calendars visible to the caller.", + "tags": [ + "Calendars" + ], + "parameters": [ + { + "in": "query", + "name": "size", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 250 + } + }, + { + "in": "query", + "name": "cursor", + "description": "Opaque cursor from a previous response", + "schema": { + "type": "string", + "description": "Opaque cursor from a previous response" + } + } + ], + "responses": { + "200": { + "description": "List of calendars", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Calendar" + } + }, + "next_cursor": { + "type": "string", + "nullable": true + } + }, + "required": [ + "results" + ] + } + } + } + } + } + }, + "post": { + "operationId": "createCalendar", + "summary": "createCalendar", + "description": "Create a native epilot calendar.", + "tags": [ + "Calendars" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CalendarCreateBody" + } + } + } + }, + "responses": { + "201": { + "description": "Calendar created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Calendar" + } + } + } + }, + "400": { + "description": "Invalid request body", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/calendar/sources/outlook": { + "post": { + "operationId": "addOutlookCalendar", + "summary": "addOutlookCalendar", + "description": "Registers one of the caller's Outlook calendars as an epilot calendar.\n\nThe returned calendar resource carries `source.provider=outlook` and the\nprovider calendar id, so subsequent reads through `GET /v1/calendar` /\n`GET /v1/calendar/events` route through the sync provider.", + "tags": [ + "Calendars" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddOutlookCalendarRequest" + } + } + } + }, + "responses": { + "201": { + "description": "Calendar created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Calendar" + } + } + } + }, + "400": { + "description": "Invalid request body", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Caller has not connected their personal Outlook calendar yet", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/calendar/sources/outlook/available": { + "get": { + "operationId": "listOutlookCalendars", + "summary": "listOutlookCalendars", + "description": "Lists the calling user's Outlook calendars available to import as epilot calendars.\n\nRequires the caller to have a personal Outlook calendar connection (created via the\nOAuth flow on email-settings: `POST /v2/outlook/connect { \"calendar\": true }`).", + "tags": [ + "Calendars" + ], + "responses": { + "200": { + "description": "List of available Outlook calendars", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "calendars": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AvailableOutlookCalendar" + } + } + }, + "required": [ + "calendars" + ] + } + } + } + }, + "404": { + "description": "Caller has not connected their personal Outlook calendar yet", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/calendar/sources/outlook/{calendar_id}": { + "delete": { + "operationId": "deleteOutlookCalendar", + "summary": "deleteOutlookCalendar", + "description": "Disconnects a previously registered Outlook calendar.\n\nDeletes the Microsoft Graph subscription, removes the backing `calendar`\nentity, and drops the local subscription record. Idempotent: a 404 from\nGraph (subscription already gone) is treated as success.", + "tags": [ + "Calendars" + ], + "parameters": [ + { + "in": "path", + "name": "calendar_id", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "204": { + "description": "Outlook calendar disconnected" + }, + "404": { + "description": "No Outlook calendar registration found for this id", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "502": { + "description": "Microsoft Graph subscription deletion failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/calendar/outlook/webhook": { + "post": { + "operationId": "outlookWebhook", + "summary": "outlookWebhook", + "description": "Public Microsoft Graph webhook receiver for per-user Outlook calendar\nsubscriptions. Unauthenticated by design (API Gateway `Authorizer: NONE`):\nGraph calls it with no epilot token.\n\nHandles both the subscription-validation handshake (echoes the\n`validationToken` query param as `text/plain`) and change notifications.\nEach notification is trusted only after its HMAC-signed `clientState` is\nverified and matched against the stored subscription – identity is never\nread from the request body.", + "tags": [ + "Calendars" + ], + "security": [], + "parameters": [ + { + "in": "query", + "name": "validationToken", + "description": "Set by Graph on the subscription-validation handshake; echoed back verbatim.", + "schema": { + "type": "string", + "description": "Set by Graph on the subscription-validation handshake; echoed back verbatim." + } + } + ], + "responses": { + "200": { + "description": "Validation handshake echo (text/plain)", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + }, + "202": { + "description": "Notification batch accepted" + } + } + } + }, + "/v1/calendar/{calendar_id}": { + "get": { + "operationId": "getCalendar", + "summary": "getCalendar", + "description": "Get a single calendar by its epilot ID.", + "tags": [ + "Calendars" + ], + "parameters": [ + { + "in": "path", + "name": "calendar_id", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "Calendar", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Calendar" + } + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "patch": { + "operationId": "updateCalendar", + "summary": "updateCalendar", + "description": "Update local calendar details. Changes to synced calendars do not modify the provider calendar.", + "tags": [ + "Calendars" + ], + "parameters": [ + { + "in": "path", + "name": "calendar_id", + "schema": { + "type": "string" + }, + "required": true + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CalendarPatchBody" + } + } + } + }, + "responses": { + "200": { + "description": "Calendar updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Calendar" + } + } + } + }, + "400": { + "description": "Invalid request body", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteCalendar", + "summary": "deleteCalendar", + "description": "Delete a native epilot calendar or disconnect a synced calendar, including its locally stored events.", + "tags": [ + "Calendars" + ], + "parameters": [ + { + "in": "path", + "name": "calendar_id", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "204": { + "description": "Calendar deleted or disconnected" + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "The epilot default calendar cannot be deleted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/calendar/events": { + "get": { + "operationId": "listEvents", + "summary": "listEvents", + "description": "List events in a time window. Recurring events are returned as expanded occurrences – each instance appears as its own event.", + "tags": [ + "Calendar Events" + ], + "parameters": [ + { + "in": "query", + "name": "from", + "description": "Start of the time window (inclusive)", + "schema": { + "type": "string", + "format": "date-time", + "description": "Start of the time window (inclusive)" + }, + "required": true + }, + { + "in": "query", + "name": "to", + "description": "End of the time window (exclusive)", + "schema": { + "type": "string", + "format": "date-time", + "description": "End of the time window (exclusive)" + }, + "required": true + }, + { + "in": "query", + "name": "calendar_id", + "description": "Filter to a single calendar. Omit to query across all accessible calendars.", + "schema": { + "type": "string", + "description": "Filter to a single calendar. Omit to query across all accessible calendars." + } + }, + { + "in": "query", + "name": "size", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 250 + } + }, + { + "in": "query", + "name": "cursor", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "List of events", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CalendarEvent" + } + }, + "next_cursor": { + "type": "string", + "nullable": true + } + }, + "required": [ + "results" + ] + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "operationId": "createEvent", + "summary": "createEvent", + "description": "Create a native epilot calendar event. Omit `calendar_id` to use the caller’s epilot default calendar.", + "tags": [ + "Calendar Events" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CalendarEventCreateBody" + } + } + } + }, + "responses": { + "201": { + "description": "Event created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CalendarEvent" + } + } + } + }, + "400": { + "description": "Invalid request body", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Calendar is read-only", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Calendar not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/calendar/events/{event_id}": { + "get": { + "operationId": "getEvent", + "summary": "getEvent", + "description": "Get a single event by its epilot ID.", + "tags": [ + "Calendar Events" + ], + "parameters": [ + { + "in": "path", + "name": "event_id", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "Event", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CalendarEvent" + } + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "patch": { + "operationId": "updateEvent", + "summary": "updateEvent", + "description": "Update a native epilot calendar event.", + "tags": [ + "Calendar Events" + ], + "parameters": [ + { + "in": "path", + "name": "event_id", + "schema": { + "type": "string" + }, + "required": true + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CalendarEventPatchBody" + } + } + } + }, + "responses": { + "200": { + "description": "Event updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CalendarEvent" + } + } + } + }, + "400": { + "description": "Invalid request body", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Event or calendar is read-only", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteEvent", + "summary": "deleteEvent", + "description": "Delete a native epilot calendar event.", + "tags": [ + "Calendar Events" + ], + "parameters": [ + { + "in": "path", + "name": "event_id", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "204": { + "description": "Event deleted" + }, + "403": { + "description": "Event or calendar is read-only", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/calendar/events/{event_id}/share": { + "post": { + "operationId": "shareEvent", + "summary": "shareEvent", + "description": "Share a calendar event with another user of the same organization, view-only. Owner-only: recipients of a share cannot re-share. Sharing an already-shared event is a no-op.", + "tags": [ + "Calendar Events" + ], + "parameters": [ + { + "in": "path", + "name": "event_id", + "schema": { + "type": "string" + }, + "required": true + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ShareEventBody" + } + } + } + }, + "responses": { + "204": { + "description": "Event shared" + }, + "400": { + "description": "Invalid request body", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Caller is not the event owner", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Event or recipient not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/calendar/events/{event_id}/share/{user_id}": { + "delete": { + "operationId": "unshareEvent", + "summary": "unshareEvent", + "description": "Revoke a per-event share. The recipient loses access immediately. Owner-only.", + "tags": [ + "Calendar Events" + ], + "parameters": [ + { + "in": "path", + "name": "event_id", + "schema": { + "type": "string" + }, + "required": true + }, + { + "in": "path", + "name": "user_id", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "204": { + "description": "Share revoked (or was not present)" + }, + "403": { + "description": "Caller is not the event owner", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Event not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + } + }, + "components": { + "securitySchemes": { + "EpilotAuth": { + "type": "http", + "scheme": "bearer", + "description": "Authorization header with epilot OAuth2 bearer token", + "bearerFormat": "JWT" + }, + "EpilotOrg": { + "type": "apiKey", + "in": "header", + "name": "x-epilot-org-id" + } + }, + "schemas": { + "AbsenceInterval": { + "type": "object", + "properties": { + "from": { + "type": "string", + "format": "date-time", + "description": "Absence interval start clipped to the requested window." + }, + "to": { + "type": "string", + "format": "date-time", + "description": "Absence interval end clipped to the requested window." + }, + "original_from": { + "type": "string", + "format": "date-time", + "description": "Original absence interval start before clipping." + }, + "original_to": { + "type": "string", + "format": "date-time", + "description": "Original absence interval end before clipping." + }, + "absent": { + "type": "boolean", + "description": "Effective absence state for this interval." + }, + "source": { + "$ref": "#/components/schemas/AbsenceIntervalSource" + }, + "calendar_event_id": { + "type": "string", + "description": "calendar_event ID for calendar-derived intervals." + }, + "absence_adjustment_id": { + "type": "string", + "description": "absence adjustment ID for manual intervals." + }, + "reason": { + "type": "string", + "nullable": true + } + }, + "required": [ + "from", + "to", + "original_from", + "original_to", + "absent", + "source" + ] + }, + "AbsenceIntervalSource": { + "type": "string", + "enum": [ + "calendar_event", + "absence_adjustment", + "working_hours" + ] + }, + "Error": { + "type": "object", + "properties": { + "status": { + "type": "integer" + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": {} + } + ] + } + }, + "required": [ + "status", + "error" + ] + }, + "SearchAbsenceBody": { + "type": "object", + "properties": { + "from": { + "type": "string", + "format": "date-time", + "description": "Start of the time window (inclusive). Maximum window: 31 days." + }, + "to": { + "type": "string", + "format": "date-time", + "description": "End of the time window (exclusive). Must be after from." + }, + "user_ids": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "minItems": 1, + "maxItems": 300, + "description": "Candidate users to check for absence inside the caller organization." + }, + "include_busy": { + "type": "boolean", + "description": "Include busy inputs in addition to out-of-office absence. Defaults to false.", + "default": false + }, + "working_hours_granularity": { + "type": "string", + "enum": [ + "time", + "day" + ], + "description": "At what granularity working hours contribute to absence. \"time\" (default): all time outside a user's working windows counts as absent. \"day\": only full days with no working windows count as absent (whole-day granularity, used by thread assignment). Users without a working-hours record never produce working-hours intervals.", + "default": "time" + } + }, + "required": [ + "from", + "to", + "user_ids" + ] + }, + "SearchNowAbsenceBody": { + "type": "object", + "properties": { + "user_ids": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "minItems": 1, + "maxItems": 300, + "description": "Candidate users to check for absence at the current server time inside the caller organization." + }, + "include_busy": { + "type": "boolean", + "description": "Include busy inputs in addition to out-of-office absence. Defaults to false.", + "default": false + }, + "working_hours_granularity": { + "type": "string", + "enum": [ + "time", + "day" + ], + "description": "At what granularity working hours contribute to absence. \"time\" (default): all time outside a user's working windows counts as absent. \"day\": only full days with no working windows count as absent (whole-day granularity, used by thread assignment). Users without a working-hours record never produce working-hours intervals.", + "default": "time" + } + }, + "required": [ + "user_ids" + ] + }, + "AbsenceAdjustment": { + "type": "object", + "properties": { + "from": { + "type": "string", + "format": "date-time", + "description": "Adjustment start. Maximum adjustment duration: 7 days." + }, + "to": { + "type": "string", + "format": "date-time", + "description": "Adjustment end. Must be after from." + }, + "absent": { + "type": "boolean", + "description": "Manual override state: true means absent, false means not absent." + }, + "status": { + "$ref": "#/components/schemas/AbsenceStatus" + }, + "type": { + "$ref": "#/components/schemas/AbsenceType" + }, + "reason": { + "type": "string", + "nullable": true, + "maxLength": 500 + }, + "adjustment_id": { + "type": "string" + }, + "user_id": { + "type": "string" + }, + "created_by": { + "type": "string" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "from", + "to", + "absent", + "status", + "adjustment_id", + "user_id", + "created_by", + "created_at", + "updated_at" + ] + }, + "AbsenceStatus": { + "type": "string", + "enum": [ + "oof", + "busy" + ], + "description": "Calendar status targeted by the absence adjustment." + }, + "AbsenceType": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "description": "Optional producer-defined reference stored with an absence adjustment." + }, + "CreateAbsenceAdjustmentBody": { + "type": "object", + "properties": { + "from": { + "type": "string", + "format": "date-time", + "description": "Adjustment start. Maximum adjustment duration: 7 days." + }, + "to": { + "type": "string", + "format": "date-time", + "description": "Adjustment end. Must be after from." + }, + "absent": { + "type": "boolean", + "description": "Manual override state: true means absent, false means not absent." + }, + "status": { + "$ref": "#/components/schemas/AbsenceStatus" + }, + "type": { + "$ref": "#/components/schemas/AbsenceType" + }, + "reason": { + "type": "string", + "nullable": true, + "maxLength": 500 + } + }, + "required": [ + "from", + "to", + "absent", + "status" + ] + }, + "PatchAbsenceAdjustmentBody": { + "type": "object", + "properties": { + "from": { + "type": "string", + "format": "date-time", + "description": "Adjustment start. Maximum adjustment duration: 7 days." + }, + "to": { + "type": "string", + "format": "date-time", + "description": "Adjustment end. Must be after from." + }, + "absent": { + "type": "boolean", + "description": "Manual override state: true means absent, false means not absent." + }, + "status": { + "$ref": "#/components/schemas/AbsenceStatus" + }, + "type": { + "$ref": "#/components/schemas/AbsenceType" + }, + "reason": { + "type": "string", + "nullable": true, + "maxLength": 500 + } + } + }, + "ExternalCalendar": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "enum": [ + "outlook", + "google" + ], + "description": "External calendar provider." + }, + "last_synced_at": { + "type": "string", + "nullable": true, + "format": "date-time", + "description": "Last successful sync of this provider calendar, if any." + } + }, + "required": [ + "provider", + "last_synced_at" + ] + }, + "WorkingHours": { + "type": "object", + "properties": { + "monday": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TimeWindow" + }, + "maxItems": 6, + "description": "Working windows for a day, sorted and non-overlapping. An empty array means a day off." + }, + "tuesday": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TimeWindow" + }, + "maxItems": 6, + "description": "Working windows for a day, sorted and non-overlapping. An empty array means a day off." + }, + "wednesday": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TimeWindow" + }, + "maxItems": 6, + "description": "Working windows for a day, sorted and non-overlapping. An empty array means a day off." + }, + "thursday": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TimeWindow" + }, + "maxItems": 6, + "description": "Working windows for a day, sorted and non-overlapping. An empty array means a day off." + }, + "friday": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TimeWindow" + }, + "maxItems": 6, + "description": "Working windows for a day, sorted and non-overlapping. An empty array means a day off." + }, + "saturday": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TimeWindow" + }, + "maxItems": 6, + "description": "Working windows for a day, sorted and non-overlapping. An empty array means a day off." + }, + "sunday": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TimeWindow" + }, + "maxItems": 6, + "description": "Working windows for a day, sorted and non-overlapping. An empty array means a day off." + }, + "timezone": { + "type": "string", + "description": "IANA timezone the working windows are expressed in." + }, + "user_id": { + "type": "string" + }, + "updated_by": { + "type": "string" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday", + "timezone", + "user_id", + "updated_by", + "created_at", + "updated_at" + ], + "description": "Recurring weekly working hours of a user. The absence of a record means the user is treated as always available." + }, + "UpsertWorkingHoursBody": { + "type": "object", + "properties": { + "monday": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TimeWindow" + }, + "maxItems": 6, + "description": "Working windows for a day, sorted and non-overlapping. An empty array means a day off." + }, + "tuesday": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TimeWindow" + }, + "maxItems": 6, + "description": "Working windows for a day, sorted and non-overlapping. An empty array means a day off." + }, + "wednesday": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TimeWindow" + }, + "maxItems": 6, + "description": "Working windows for a day, sorted and non-overlapping. An empty array means a day off." + }, + "thursday": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TimeWindow" + }, + "maxItems": 6, + "description": "Working windows for a day, sorted and non-overlapping. An empty array means a day off." + }, + "friday": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TimeWindow" + }, + "maxItems": 6, + "description": "Working windows for a day, sorted and non-overlapping. An empty array means a day off." + }, + "saturday": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TimeWindow" + }, + "maxItems": 6, + "description": "Working windows for a day, sorted and non-overlapping. An empty array means a day off." + }, + "sunday": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TimeWindow" }, - "required": true + "maxItems": 6, + "description": "Working windows for a day, sorted and non-overlapping. An empty array means a day off." + }, + "timezone": { + "type": "string", + "description": "IANA timezone the working windows are expressed in. Defaults to Europe/Berlin.", + "default": "Europe/Berlin" } + }, + "required": [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday" ], - "responses": { - "200": { - "description": "Event", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CalendarEvent" - } - } - } + "description": "Full replacement of the working-hours record. All weekdays are required; an empty array means a day off. Users without a working-hours record are treated as always available." + }, + "TimeWindow": { + "type": "object", + "properties": { + "start": { + "type": "string", + "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$", + "description": "24h wall-clock time (\"HH:mm\") in the timezone of the working-hours record." }, - "404": { - "description": "Resource not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } + "end": { + "type": "string", + "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$", + "description": "24h wall-clock time (\"HH:mm\") in the timezone of the working-hours record." } - } - } - } - }, - "components": { - "securitySchemes": { - "EpilotAuth": { - "type": "http", - "scheme": "bearer", - "description": "Authorization header with epilot OAuth2 bearer token", - "bearerFormat": "JWT" + }, + "required": [ + "start", + "end" + ], + "description": "A wall-clock working window within a single day." }, - "EpilotOrg": { - "type": "apiKey", - "in": "header", - "name": "x-epilot-org-id" - } - }, - "schemas": { "Calendar": { "type": "object", "properties": { @@ -304,7 +2319,8 @@ "description": "Computed display title" }, "name": { - "type": "string" + "type": "string", + "minLength": 1 }, "description": { "type": "string", @@ -317,11 +2333,15 @@ }, "is_default": { "type": "boolean", - "description": "True if this is the user's primary calendar" + "description": "True if the source provider marks this calendar as default" + }, + "is_epilot_default": { + "type": "boolean", + "description": "True for the epilot default calendar" }, "read_only": { "type": "boolean", - "description": "True if the caller cannot modify events in this calendar" + "description": "True if the caller cannot create, update, or delete events in this calendar" }, "owner_email": { "type": "string", @@ -346,6 +2366,7 @@ "_org", "name", "is_default", + "is_epilot_default", "read_only", "source" ] @@ -388,20 +2409,93 @@ ], "description": "Null for native epilot calendars" }, - "Error": { + "CalendarCreateBody": { "type": "object", "properties": { - "message": { - "type": "string" + "name": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string", + "nullable": true + }, + "color": { + "type": "string", + "nullable": true, + "description": "Hex color (e.g. \"#3b82f6\") or provider-defined named color" + } + }, + "required": [ + "name" + ] + }, + "AddOutlookCalendarRequest": { + "type": "object", + "properties": { + "provider_calendar_id": { + "type": "string", + "minLength": 1 + }, + "name": { + "type": "string", + "minLength": 1, + "description": "Optional override; defaults to the provider calendar name." + } + }, + "required": [ + "provider_calendar_id" + ] + }, + "AvailableOutlookCalendar": { + "type": "object", + "properties": { + "provider_calendar_id": { + "type": "string", + "description": "Microsoft Graph calendar id (immutable, e.g. base64-encoded)" }, - "code": { + "name": { "type": "string" + }, + "color": { + "type": "string", + "nullable": true + }, + "is_default": { + "type": "boolean" + }, + "can_edit": { + "type": "boolean" + }, + "owner": { + "type": "string", + "nullable": true, + "description": "Owner email address on the Microsoft side" } }, "required": [ - "message" + "provider_calendar_id", + "name" ] }, + "CalendarPatchBody": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string", + "nullable": true + }, + "color": { + "type": "string", + "nullable": true, + "description": "Hex color (e.g. \"#3b82f6\") or provider-defined named color" + } + } + }, "CalendarEvent": { "type": "object", "properties": { @@ -432,6 +2526,7 @@ }, "calendar_id": { "type": "string", + "minLength": 1, "description": "epilot calendar this event belongs to" }, "event_type": { @@ -455,6 +2550,7 @@ }, "timezone": { "type": "string", + "minLength": 1, "description": "IANA timezone of the original event (for display)" }, "is_all_day": { @@ -472,7 +2568,12 @@ "description": "Convenience flag, true when status is busy/oof/tentative" }, "is_cancelled": { - "type": "boolean" + "type": "boolean", + "description": "Whether the event was cancelled but still exists" + }, + "is_draft": { + "type": "boolean", + "description": "Whether the event is saved as a draft" }, "sensitivity": { "$ref": "#/components/schemas/Sensitivity" @@ -511,6 +2612,11 @@ }, "description": "Null when sensitivity is private or confidential" }, + "metadata": { + "type": "object", + "nullable": true, + "additionalProperties": {} + }, "is_recurring": { "type": "boolean" }, @@ -544,6 +2650,7 @@ "status", "busy", "is_cancelled", + "is_draft", "sensitivity", "importance", "is_online_meeting", @@ -669,6 +2776,128 @@ "required": [ "type" ] + }, + "CalendarEventCreateBody": { + "type": "object", + "properties": { + "calendar_id": { + "type": "string", + "minLength": 1, + "description": "Owned calendar ID. Omit this property to use the epilot default calendar." + }, + "description": { + "type": "string", + "nullable": true, + "description": "Preview of the event body, truncated to 255 chars", + "maxLength": 255 + }, + "start_time": { + "type": "string", + "format": "date-time", + "description": "Event start in UTC" + }, + "end_time": { + "type": "string", + "format": "date-time", + "description": "Event end in UTC" + }, + "timezone": { + "type": "string", + "minLength": 1, + "description": "IANA timezone of the original event (for display)" + }, + "is_all_day": { + "type": "boolean" + }, + "location": { + "type": "string", + "nullable": true + }, + "status": { + "$ref": "#/components/schemas/EventStatus" + }, + "sensitivity": { + "$ref": "#/components/schemas/Sensitivity" + }, + "metadata": { + "type": "object", + "nullable": true, + "additionalProperties": {} + }, + "_title": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "start_time", + "end_time", + "timezone", + "is_all_day", + "status", + "sensitivity", + "_title" + ] + }, + "CalendarEventPatchBody": { + "type": "object", + "properties": { + "description": { + "type": "string", + "nullable": true, + "description": "Preview of the event body, truncated to 255 chars", + "maxLength": 255 + }, + "start_time": { + "type": "string", + "format": "date-time", + "description": "Event start in UTC" + }, + "end_time": { + "type": "string", + "format": "date-time", + "description": "Event end in UTC" + }, + "timezone": { + "type": "string", + "minLength": 1, + "description": "IANA timezone of the original event (for display)" + }, + "is_all_day": { + "type": "boolean" + }, + "location": { + "type": "string", + "nullable": true + }, + "status": { + "$ref": "#/components/schemas/EventStatus" + }, + "is_cancelled": { + "type": "boolean", + "description": "Whether the event was cancelled but still exists" + }, + "sensitivity": { + "$ref": "#/components/schemas/Sensitivity" + }, + "_title": { + "type": "string", + "minLength": 1 + } + } + }, + "ShareEventBody": { + "type": "object", + "properties": { + "user_id": { + "type": "string", + "minLength": 1, + "description": "epilot user id (same organization) to grant view-only access to this event" + } + }, + "required": [ + "user_id" + ] } } } diff --git a/packages/cli/definitions/chat.json b/packages/cli/definitions/chat.json new file mode 100644 index 000000000..7be35da83 --- /dev/null +++ b/packages/cli/definitions/chat.json @@ -0,0 +1,2909 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "epilot Chat API", + "version": "0.1.0", + "description": "Widget management and anonymous browser chat." + }, + "servers": [ + { + "url": "https://chat.sls.epilot.io" + } + ], + "security": [ + { + "EpilotAuth": [] + } + ], + "paths": { + "/v1/widgets": { + "get": { + "tags": [ + "Chat widgets" + ], + "operationId": "listChatWidgets", + "security": [ + { + "EpilotAuth": [] + } + ], + "responses": { + "200": { + "description": "Widget response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "widgets" + ], + "properties": { + "widgets": { + "type": "array", + "items": { + "type": "object", + "required": [ + "widget_id", + "org_id", + "agent_id", + "binding_id", + "version", + "name", + "website_chat", + "website_chat_embed", + "created_at", + "updated_at" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "agent_id": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "website_chat": { + "type": "object", + "additionalProperties": false, + "required": [ + "allowed_origins", + "organisation_name", + "default_locale" + ], + "properties": { + "allowed_origins": { + "type": "array", + "maxItems": 50, + "uniqueItems": true, + "description": "Exact HTTPS origins. Empty means the widget cannot start sessions.", + "items": { + "type": "string", + "maxLength": 2048 + } + }, + "organisation_name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "default_locale": { + "type": "string", + "enum": [ + "en", + "de" + ] + }, + "design_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Design Builder configuration for the widget only." + }, + "authentication": { + "type": "object", + "additionalProperties": false, + "properties": { + "email_code": { + "type": "object", + "additionalProperties": false, + "properties": { + "email_template_id": { + "type": "string", + "format": "uuid", + "nullable": true + } + } + } + }, + "nullable": true, + "description": "Email Builder template selection. Does not enable verification by itself.", + "required": [ + "email_code" + ] + } + } + }, + "widget_id": { + "type": "string", + "format": "uuid", + "readOnly": true, + "description": "Stable public embed identifier, independent of the assigned agent. Not a credential." + }, + "org_id": { + "type": "string", + "readOnly": true + }, + "binding_id": { + "type": "string", + "format": "uuid", + "readOnly": true, + "description": "Server-owned session binding; changes on reassignment or allowed-origin changes." + }, + "version": { + "type": "integer", + "minimum": 1, + "readOnly": true + }, + "created_at": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "updated_at": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "website_chat_embed": { + "type": "object", + "readOnly": true, + "required": [ + "script_url", + "chat_api_origin", + "demo_url" + ], + "properties": { + "script_url": { + "type": "string", + "format": "uri" + }, + "chat_api_origin": { + "type": "string", + "format": "uri" + }, + "demo_url": { + "type": "string", + "format": "uri" + } + } + } + } + } + }, + "next_cursor": { + "type": "string" + } + } + } + } + } + }, + "400": { + "description": "Invalid widget settings", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object" + } + } + } + } + } + }, + "404": { + "description": "Widget not found in this organisation", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object" + } + } + } + } + } + }, + "409": { + "description": "Widget or assigned agent changed; reload before retrying", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object" + } + } + } + } + } + } + }, + "parameters": [ + { + "in": "query", + "name": "cursor", + "schema": { + "type": "string", + "maxLength": 1024 + } + } + ] + }, + "post": { + "tags": [ + "Chat widgets" + ], + "operationId": "createChatWidget", + "security": [ + { + "EpilotAuth": [] + } + ], + "responses": { + "201": { + "description": "Widget response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "widget_id", + "org_id", + "agent_id", + "binding_id", + "version", + "name", + "website_chat", + "website_chat_embed", + "created_at", + "updated_at" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "agent_id": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "website_chat": { + "type": "object", + "additionalProperties": false, + "required": [ + "allowed_origins", + "organisation_name", + "default_locale" + ], + "properties": { + "allowed_origins": { + "type": "array", + "maxItems": 50, + "uniqueItems": true, + "description": "Exact HTTPS origins. Empty means the widget cannot start sessions.", + "items": { + "type": "string", + "maxLength": 2048 + } + }, + "organisation_name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "default_locale": { + "type": "string", + "enum": [ + "en", + "de" + ] + }, + "design_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Design Builder configuration for the widget only." + }, + "authentication": { + "type": "object", + "additionalProperties": false, + "properties": { + "email_code": { + "type": "object", + "additionalProperties": false, + "properties": { + "email_template_id": { + "type": "string", + "format": "uuid", + "nullable": true + } + } + } + }, + "nullable": true, + "description": "Email Builder template selection. Does not enable verification by itself.", + "required": [ + "email_code" + ] + } + } + }, + "widget_id": { + "type": "string", + "format": "uuid", + "readOnly": true, + "description": "Stable public embed identifier, independent of the assigned agent. Not a credential." + }, + "org_id": { + "type": "string", + "readOnly": true + }, + "binding_id": { + "type": "string", + "format": "uuid", + "readOnly": true, + "description": "Server-owned session binding; changes on reassignment or allowed-origin changes." + }, + "version": { + "type": "integer", + "minimum": 1, + "readOnly": true + }, + "created_at": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "updated_at": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "website_chat_embed": { + "type": "object", + "readOnly": true, + "required": [ + "script_url", + "chat_api_origin", + "demo_url" + ], + "properties": { + "script_url": { + "type": "string", + "format": "uri" + }, + "chat_api_origin": { + "type": "string", + "format": "uri" + }, + "demo_url": { + "type": "string", + "format": "uri" + } + } + } + } + } + } + } + }, + "400": { + "description": "Invalid widget settings", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object" + } + } + } + } + } + }, + "404": { + "description": "Widget not found in this organisation", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object" + } + } + } + } + } + }, + "409": { + "description": "Widget or assigned agent changed; reload before retrying", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object" + } + } + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "agent_id", + "website_chat" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "agent_id": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "website_chat": { + "type": "object", + "additionalProperties": false, + "required": [ + "allowed_origins", + "organisation_name", + "default_locale" + ], + "properties": { + "allowed_origins": { + "type": "array", + "maxItems": 50, + "uniqueItems": true, + "description": "Exact HTTPS origins. Empty means the widget cannot start sessions.", + "items": { + "type": "string", + "maxLength": 2048 + } + }, + "organisation_name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "default_locale": { + "type": "string", + "enum": [ + "en", + "de" + ] + }, + "design_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Design Builder configuration for the widget only." + }, + "authentication": { + "type": "object", + "additionalProperties": false, + "properties": { + "email_code": { + "type": "object", + "additionalProperties": false, + "properties": { + "email_template_id": { + "type": "string", + "format": "uuid", + "nullable": true + } + } + } + }, + "nullable": true, + "description": "Email Builder template selection. Does not enable verification by itself.", + "required": [ + "email_code" + ] + } + } + } + } + } + } + } + } + } + }, + "/v1/widgets/{widget_id}": { + "parameters": [ + { + "in": "path", + "name": "widget_id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "get": { + "tags": [ + "Chat widgets" + ], + "operationId": "getChatWidget", + "security": [ + { + "EpilotAuth": [] + } + ], + "responses": { + "200": { + "description": "Widget response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "widget_id", + "org_id", + "agent_id", + "binding_id", + "version", + "name", + "website_chat", + "website_chat_embed", + "created_at", + "updated_at" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "agent_id": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "website_chat": { + "type": "object", + "additionalProperties": false, + "required": [ + "allowed_origins", + "organisation_name", + "default_locale" + ], + "properties": { + "allowed_origins": { + "type": "array", + "maxItems": 50, + "uniqueItems": true, + "description": "Exact HTTPS origins. Empty means the widget cannot start sessions.", + "items": { + "type": "string", + "maxLength": 2048 + } + }, + "organisation_name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "default_locale": { + "type": "string", + "enum": [ + "en", + "de" + ] + }, + "design_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Design Builder configuration for the widget only." + }, + "authentication": { + "type": "object", + "additionalProperties": false, + "properties": { + "email_code": { + "type": "object", + "additionalProperties": false, + "properties": { + "email_template_id": { + "type": "string", + "format": "uuid", + "nullable": true + } + } + } + }, + "nullable": true, + "description": "Email Builder template selection. Does not enable verification by itself.", + "required": [ + "email_code" + ] + } + } + }, + "widget_id": { + "type": "string", + "format": "uuid", + "readOnly": true, + "description": "Stable public embed identifier, independent of the assigned agent. Not a credential." + }, + "org_id": { + "type": "string", + "readOnly": true + }, + "binding_id": { + "type": "string", + "format": "uuid", + "readOnly": true, + "description": "Server-owned session binding; changes on reassignment or allowed-origin changes." + }, + "version": { + "type": "integer", + "minimum": 1, + "readOnly": true + }, + "created_at": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "updated_at": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "website_chat_embed": { + "type": "object", + "readOnly": true, + "required": [ + "script_url", + "chat_api_origin", + "demo_url" + ], + "properties": { + "script_url": { + "type": "string", + "format": "uri" + }, + "chat_api_origin": { + "type": "string", + "format": "uri" + }, + "demo_url": { + "type": "string", + "format": "uri" + } + } + } + } + } + } + } + }, + "400": { + "description": "Invalid widget settings", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object" + } + } + } + } + } + }, + "404": { + "description": "Widget not found in this organisation", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object" + } + } + } + } + } + }, + "409": { + "description": "Widget or assigned agent changed; reload before retrying", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object" + } + } + } + } + } + } + } + }, + "put": { + "tags": [ + "Chat widgets" + ], + "operationId": "updateChatWidget", + "security": [ + { + "EpilotAuth": [] + } + ], + "responses": { + "200": { + "description": "Widget response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "widget_id", + "org_id", + "agent_id", + "binding_id", + "version", + "name", + "website_chat", + "website_chat_embed", + "created_at", + "updated_at" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "agent_id": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "website_chat": { + "type": "object", + "additionalProperties": false, + "required": [ + "allowed_origins", + "organisation_name", + "default_locale" + ], + "properties": { + "allowed_origins": { + "type": "array", + "maxItems": 50, + "uniqueItems": true, + "description": "Exact HTTPS origins. Empty means the widget cannot start sessions.", + "items": { + "type": "string", + "maxLength": 2048 + } + }, + "organisation_name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "default_locale": { + "type": "string", + "enum": [ + "en", + "de" + ] + }, + "design_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Design Builder configuration for the widget only." + }, + "authentication": { + "type": "object", + "additionalProperties": false, + "properties": { + "email_code": { + "type": "object", + "additionalProperties": false, + "properties": { + "email_template_id": { + "type": "string", + "format": "uuid", + "nullable": true + } + } + } + }, + "nullable": true, + "description": "Email Builder template selection. Does not enable verification by itself.", + "required": [ + "email_code" + ] + } + } + }, + "widget_id": { + "type": "string", + "format": "uuid", + "readOnly": true, + "description": "Stable public embed identifier, independent of the assigned agent. Not a credential." + }, + "org_id": { + "type": "string", + "readOnly": true + }, + "binding_id": { + "type": "string", + "format": "uuid", + "readOnly": true, + "description": "Server-owned session binding; changes on reassignment or allowed-origin changes." + }, + "version": { + "type": "integer", + "minimum": 1, + "readOnly": true + }, + "created_at": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "updated_at": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "website_chat_embed": { + "type": "object", + "readOnly": true, + "required": [ + "script_url", + "chat_api_origin", + "demo_url" + ], + "properties": { + "script_url": { + "type": "string", + "format": "uri" + }, + "chat_api_origin": { + "type": "string", + "format": "uri" + }, + "demo_url": { + "type": "string", + "format": "uri" + } + } + } + } + } + } + } + }, + "400": { + "description": "Invalid widget settings", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object" + } + } + } + } + } + }, + "404": { + "description": "Widget not found in this organisation", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object" + } + } + } + } + } + }, + "409": { + "description": "Widget or assigned agent changed; reload before retrying", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object" + } + } + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "version" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "agent_id": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "website_chat": { + "type": "object", + "additionalProperties": false, + "required": [ + "allowed_origins", + "organisation_name", + "default_locale" + ], + "properties": { + "allowed_origins": { + "type": "array", + "maxItems": 50, + "uniqueItems": true, + "description": "Exact HTTPS origins. Empty means the widget cannot start sessions.", + "items": { + "type": "string", + "maxLength": 2048 + } + }, + "organisation_name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "default_locale": { + "type": "string", + "enum": [ + "en", + "de" + ] + }, + "design_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Design Builder configuration for the widget only." + }, + "authentication": { + "type": "object", + "additionalProperties": false, + "properties": { + "email_code": { + "type": "object", + "additionalProperties": false, + "properties": { + "email_template_id": { + "type": "string", + "format": "uuid", + "nullable": true + } + } + } + }, + "nullable": true, + "description": "Email Builder template selection. Does not enable verification by itself.", + "required": [ + "email_code" + ] + } + } + }, + "version": { + "type": "integer", + "minimum": 1, + "description": "Version read by the editor; stale writes return 409." + } + } + } + } + } + } + }, + "delete": { + "tags": [ + "Chat widgets" + ], + "operationId": "deleteChatWidget", + "security": [ + { + "EpilotAuth": [] + } + ], + "responses": { + "204": { + "description": "Widget deleted" + }, + "400": { + "description": "Invalid widget settings", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object" + } + } + } + } + } + }, + "404": { + "description": "Widget not found in this organisation", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object" + } + } + } + } + } + }, + "409": { + "description": "Widget or assigned agent changed; reload before retrying", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object" + } + } + } + } + } + } + }, + "parameters": [ + { + "in": "query", + "name": "version", + "required": true, + "schema": { + "type": "integer", + "minimum": 1 + } + } + ] + } + }, + "/v1/widgets/{widget_id}/configuration": { + "get": { + "operationId": "getPublicChatWidget", + "description": "Resolve visitor-facing configuration for an independent widget and its current agent assignment. The widget ID is not an organisation ID or a destination such as website or portal.", + "parameters": [ + { + "in": "path", + "name": "widget_id", + "description": "Public widget ID, also used as widget_key in bootstrap and data-epilot-chat in the embed.", + "example": "550e8400-e29b-41d4-a716-446655440000", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]{1,100}$" + } + } + ], + "responses": { + "200": { + "description": "Public presentation only", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "key", + "organisationName", + "assistantName", + "defaultLocale", + "locales" + ], + "properties": { + "key": { + "type": "string" + }, + "organisationName": { + "type": "string" + }, + "assistantName": { + "type": "string" + }, + "defaultLocale": { + "type": "string", + "enum": [ + "en", + "de" + ] + }, + "design": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "last_modified_at": { + "type": "string", + "maxLength": 200 + }, + "style": { + "type": "object", + "additionalProperties": false, + "properties": { + "palette": { + "type": "object", + "additionalProperties": false, + "properties": { + "primary": { + "type": "string", + "maxLength": 200 + }, + "background": { + "type": "string", + "maxLength": 200 + } + } + }, + "typography": { + "type": "object", + "additionalProperties": false, + "properties": { + "font": { + "type": "object", + "additionalProperties": false, + "properties": { + "font_family": { + "type": "string", + "maxLength": 200 + } + } + } + } + }, + "shape": { + "type": "object", + "additionalProperties": false, + "properties": { + "border_radius": { + "type": "number", + "minimum": 0, + "maximum": 100 + } + } + } + } + }, + "spark_theme": { + "type": "object", + "additionalProperties": false, + "properties": { + "accentColor": { + "type": "string", + "maxLength": 200 + }, + "backgroundColor": { + "type": "string", + "maxLength": 200 + }, + "fontBody": { + "type": "string", + "maxLength": 200 + }, + "fontHeading": { + "type": "string", + "maxLength": 200 + }, + "radius": { + "type": "string", + "maxLength": 200 + }, + "scaling": { + "type": "string", + "maxLength": 200 + }, + "spacing": { + "type": "string", + "maxLength": 200 + }, + "appearance": { + "type": "string", + "maxLength": 200 + }, + "neutralColor": { + "type": "string", + "maxLength": 200 + }, + "styleVariant": { + "type": "string", + "maxLength": 200 + }, + "labelPosition": { + "type": "string", + "maxLength": 200 + }, + "inputStyle": { + "type": "string", + "maxLength": 200 + }, + "inputColor": { + "type": "string", + "maxLength": 200 + }, + "cardVariant": { + "type": "string", + "maxLength": 200 + }, + "cardColor": { + "type": "string", + "maxLength": 200 + }, + "highContrast": { + "type": "boolean" + } + } + } + }, + "required": [ + "id" + ] + }, + "locales": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "en", + "de" + ] + } + } + } + } + } + } + }, + "default": { + "description": "400 invalid input, 401 expired capability, 403 origin denied, 404 unpublished widget, 409 conflict, 413 too large, 429 rate limit, or 503 unavailable.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "code" + ], + "properties": { + "code": { + "type": "string", + "enum": [ + "INVALID_REQUEST", + "ORIGIN_DENIED", + "NOT_FOUND", + "INVALID_GRANT", + "SESSION_EXPIRED", + "RATE_LIMITED", + "SESSION_LIMIT", + "USAGE_LIMIT", + "REQUEST_CONFLICT", + "IN_PROGRESS", + "UNAVAILABLE", + "TEMPORARILY_UNAVAILABLE" + ] + } + } + } + } + } + } + }, + "security": [] + } + }, + "/v1/bootstrap": { + "post": { + "operationId": "createPublicChatGrant", + "description": "Called by the host website with its widget_key. Checks the saved website origin allowlist and issues a single-use grant for the iframe to exchange at POST /v1/sessions within 60 seconds. This deadline does not limit the resulting session.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "widget_key" + ], + "properties": { + "widget_key": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]{1,100}$" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Single-use handoff grant, valid for 60 seconds. If unused at expiry, request a fresh grant. Contains no account identity.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "grant", + "expires_in", + "widget_origin" + ], + "properties": { + "grant": { + "type": "string" + }, + "expires_in": { + "type": "integer", + "example": 60 + }, + "widget_origin": { + "type": "string", + "format": "uri" + } + } + } + } + } + }, + "default": { + "description": "400 invalid input, 401 expired capability, 403 origin denied, 404 unpublished widget, 409 conflict, 413 too large, 429 rate limit, or 503 unavailable.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "code" + ], + "properties": { + "code": { + "type": "string", + "enum": [ + "INVALID_REQUEST", + "ORIGIN_DENIED", + "NOT_FOUND", + "INVALID_GRANT", + "SESSION_EXPIRED", + "RATE_LIMITED", + "SESSION_LIMIT", + "USAGE_LIMIT", + "REQUEST_CONFLICT", + "IN_PROGRESS", + "UNAVAILABLE", + "TEMPORARILY_UNAVAILABLE" + ] + } + } + } + } + } + } + }, + "security": [] + } + }, + "/v1/sessions": { + "post": { + "operationId": "createAnonymousChatSession", + "description": "Exchanges an unexpired grant once for an independent 30-minute session. Expired or previously used grants return 401 INVALID_GRANT; the host must bootstrap again.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "grant" + ], + "properties": { + "grant": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{43}$" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "An isolated anonymous conversation", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "token", + "conversation_id", + "expires_at", + "widget" + ], + "properties": { + "token": { + "type": "string" + }, + "conversation_id": { + "type": "string", + "format": "uuid" + }, + "expires_at": { + "type": "integer", + "description": "Unix seconds" + }, + "widget": { + "type": "object", + "required": [ + "key", + "organisationName", + "assistantName", + "defaultLocale", + "locales" + ], + "properties": { + "key": { + "type": "string" + }, + "organisationName": { + "type": "string" + }, + "assistantName": { + "type": "string" + }, + "defaultLocale": { + "type": "string", + "enum": [ + "en", + "de" + ] + }, + "design": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "last_modified_at": { + "type": "string", + "maxLength": 200 + }, + "style": { + "type": "object", + "additionalProperties": false, + "properties": { + "palette": { + "type": "object", + "additionalProperties": false, + "properties": { + "primary": { + "type": "string", + "maxLength": 200 + }, + "background": { + "type": "string", + "maxLength": 200 + } + } + }, + "typography": { + "type": "object", + "additionalProperties": false, + "properties": { + "font": { + "type": "object", + "additionalProperties": false, + "properties": { + "font_family": { + "type": "string", + "maxLength": 200 + } + } + } + } + }, + "shape": { + "type": "object", + "additionalProperties": false, + "properties": { + "border_radius": { + "type": "number", + "minimum": 0, + "maximum": 100 + } + } + } + } + }, + "spark_theme": { + "type": "object", + "additionalProperties": false, + "properties": { + "accentColor": { + "type": "string", + "maxLength": 200 + }, + "backgroundColor": { + "type": "string", + "maxLength": 200 + }, + "fontBody": { + "type": "string", + "maxLength": 200 + }, + "fontHeading": { + "type": "string", + "maxLength": 200 + }, + "radius": { + "type": "string", + "maxLength": 200 + }, + "scaling": { + "type": "string", + "maxLength": 200 + }, + "spacing": { + "type": "string", + "maxLength": 200 + }, + "appearance": { + "type": "string", + "maxLength": 200 + }, + "neutralColor": { + "type": "string", + "maxLength": 200 + }, + "styleVariant": { + "type": "string", + "maxLength": 200 + }, + "labelPosition": { + "type": "string", + "maxLength": 200 + }, + "inputStyle": { + "type": "string", + "maxLength": 200 + }, + "inputColor": { + "type": "string", + "maxLength": 200 + }, + "cardVariant": { + "type": "string", + "maxLength": 200 + }, + "cardColor": { + "type": "string", + "maxLength": 200 + }, + "highContrast": { + "type": "boolean" + } + } + } + }, + "required": [ + "id" + ] + }, + "locales": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "en", + "de" + ] + } + } + } + } + } + } + } + } + }, + "default": { + "description": "400 invalid input, 401 expired capability, 403 origin denied, 404 unpublished widget, 409 conflict, 413 too large, 429 rate limit, or 503 unavailable.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "code" + ], + "properties": { + "code": { + "type": "string", + "enum": [ + "INVALID_REQUEST", + "ORIGIN_DENIED", + "NOT_FOUND", + "INVALID_GRANT", + "SESSION_EXPIRED", + "RATE_LIMITED", + "SESSION_LIMIT", + "USAGE_LIMIT", + "REQUEST_CONFLICT", + "IN_PROGRESS", + "UNAVAILABLE", + "TEMPORARILY_UNAVAILABLE" + ] + } + } + } + } + } + } + }, + "security": [] + } + }, + "/v1/messages": { + "post": { + "operationId": "sendAnonymousChatMessage", + "security": [ + { + "AnonymousSession": [] + } + ], + "description": "Creates a turn or replays its persisted result. Reuse request_id and the exact\npayload for a retry. History, org, agent, tools and account identity cannot be supplied.\nTEMPORARILY_UNAVAILABLE means the request outcome could not be checked; retry the same request ID and payload.\nA running request returns 409 IN_PROGRESS; a changed payload returns 409 REQUEST_CONFLICT.\nA failed admitted request never starts another model execution when retried.\n", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "request_id", + "message", + "locale", + "simple_language" + ], + "properties": { + "request_id": { + "type": "string", + "format": "uuid", + "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$" + }, + "message": { + "type": "string", + "minLength": 1, + "maxLength": 4000, + "pattern": "\\S" + }, + "locale": { + "type": "string", + "enum": [ + "en", + "de" + ] + }, + "simple_language": { + "type": "boolean" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "SSE data frames whose JSON payload follows the PublicEvent component schema.\nOnly delta, complete and error events are public. Completion follows persistence.\n", + "content": { + "text/event-stream": { + "schema": { + "type": "string" + }, + "example": "data: {\"type\":\"delta\",\"text\":\"Hello\",\"request_id\":\"bd73e89b-01a5-4e8c-b12b-2b1a743e2ae0\"}\n\ndata: {\"type\":\"complete\",\"text\":\"Hello\",\"request_id\":\"bd73e89b-01a5-4e8c-b12b-2b1a743e2ae0\"}\n\n" + } + } + }, + "default": { + "description": "400 invalid input, 401 expired capability, 403 origin denied, 404 unpublished widget, 409 conflict, 413 too large, 429 rate limit, or 503 unavailable.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "code" + ], + "properties": { + "code": { + "type": "string", + "enum": [ + "INVALID_REQUEST", + "ORIGIN_DENIED", + "NOT_FOUND", + "INVALID_GRANT", + "SESSION_EXPIRED", + "RATE_LIMITED", + "SESSION_LIMIT", + "USAGE_LIMIT", + "REQUEST_CONFLICT", + "IN_PROGRESS", + "UNAVAILABLE", + "TEMPORARILY_UNAVAILABLE" + ] + } + } + } + } + } + } + } + } + } + }, + "components": { + "securitySchemes": { + "EpilotAuth": { + "type": "http", + "scheme": "bearer" + }, + "AnonymousSession": { + "type": "http", + "scheme": "bearer", + "description": "Opaque, expiring anonymous session token; never an epilot user access token." + } + }, + "schemas": { + "ListChatWidgetsResponse": { + "type": "object", + "required": [ + "widgets" + ], + "properties": { + "widgets": { + "type": "array", + "items": { + "type": "object", + "required": [ + "widget_id", + "org_id", + "agent_id", + "binding_id", + "version", + "name", + "website_chat", + "website_chat_embed", + "created_at", + "updated_at" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "agent_id": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "website_chat": { + "type": "object", + "additionalProperties": false, + "required": [ + "allowed_origins", + "organisation_name", + "default_locale" + ], + "properties": { + "allowed_origins": { + "type": "array", + "maxItems": 50, + "uniqueItems": true, + "description": "Exact HTTPS origins. Empty means the widget cannot start sessions.", + "items": { + "type": "string", + "maxLength": 2048 + } + }, + "organisation_name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "default_locale": { + "type": "string", + "enum": [ + "en", + "de" + ] + }, + "design_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Design Builder configuration for the widget only." + }, + "authentication": { + "type": "object", + "additionalProperties": false, + "properties": { + "email_code": { + "type": "object", + "additionalProperties": false, + "properties": { + "email_template_id": { + "type": "string", + "format": "uuid", + "nullable": true + } + } + } + }, + "nullable": true, + "description": "Email Builder template selection. Does not enable verification by itself.", + "required": [ + "email_code" + ] + } + } + }, + "widget_id": { + "type": "string", + "format": "uuid", + "readOnly": true, + "description": "Stable public embed identifier, independent of the assigned agent. Not a credential." + }, + "org_id": { + "type": "string", + "readOnly": true + }, + "binding_id": { + "type": "string", + "format": "uuid", + "readOnly": true, + "description": "Server-owned session binding; changes on reassignment or allowed-origin changes." + }, + "version": { + "type": "integer", + "minimum": 1, + "readOnly": true + }, + "created_at": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "updated_at": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "website_chat_embed": { + "type": "object", + "readOnly": true, + "required": [ + "script_url", + "chat_api_origin", + "demo_url" + ], + "properties": { + "script_url": { + "type": "string", + "format": "uri" + }, + "chat_api_origin": { + "type": "string", + "format": "uri" + }, + "demo_url": { + "type": "string", + "format": "uri" + } + } + } + } + } + }, + "next_cursor": { + "type": "string" + } + } + }, + "ChatWidget": { + "type": "object", + "required": [ + "widget_id", + "org_id", + "agent_id", + "binding_id", + "version", + "name", + "website_chat", + "website_chat_embed", + "created_at", + "updated_at" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "agent_id": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "website_chat": { + "type": "object", + "additionalProperties": false, + "required": [ + "allowed_origins", + "organisation_name", + "default_locale" + ], + "properties": { + "allowed_origins": { + "type": "array", + "maxItems": 50, + "uniqueItems": true, + "description": "Exact HTTPS origins. Empty means the widget cannot start sessions.", + "items": { + "type": "string", + "maxLength": 2048 + } + }, + "organisation_name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "default_locale": { + "type": "string", + "enum": [ + "en", + "de" + ] + }, + "design_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Design Builder configuration for the widget only." + }, + "authentication": { + "type": "object", + "additionalProperties": false, + "properties": { + "email_code": { + "type": "object", + "additionalProperties": false, + "properties": { + "email_template_id": { + "type": "string", + "format": "uuid", + "nullable": true + } + } + } + }, + "nullable": true, + "description": "Email Builder template selection. Does not enable verification by itself.", + "required": [ + "email_code" + ] + } + } + }, + "widget_id": { + "type": "string", + "format": "uuid", + "readOnly": true, + "description": "Stable public embed identifier, independent of the assigned agent. Not a credential." + }, + "org_id": { + "type": "string", + "readOnly": true + }, + "binding_id": { + "type": "string", + "format": "uuid", + "readOnly": true, + "description": "Server-owned session binding; changes on reassignment or allowed-origin changes." + }, + "version": { + "type": "integer", + "minimum": 1, + "readOnly": true + }, + "created_at": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "updated_at": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "website_chat_embed": { + "type": "object", + "readOnly": true, + "required": [ + "script_url", + "chat_api_origin", + "demo_url" + ], + "properties": { + "script_url": { + "type": "string", + "format": "uri" + }, + "chat_api_origin": { + "type": "string", + "format": "uri" + }, + "demo_url": { + "type": "string", + "format": "uri" + } + } + } + } + }, + "WebsiteChatSettings": { + "type": "object", + "additionalProperties": false, + "required": [ + "allowed_origins", + "organisation_name", + "default_locale" + ], + "properties": { + "allowed_origins": { + "type": "array", + "maxItems": 50, + "uniqueItems": true, + "description": "Exact HTTPS origins. Empty means the widget cannot start sessions.", + "items": { + "type": "string", + "maxLength": 2048 + } + }, + "organisation_name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "default_locale": { + "type": "string", + "enum": [ + "en", + "de" + ] + }, + "design_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Design Builder configuration for the widget only." + }, + "authentication": { + "type": "object", + "additionalProperties": false, + "properties": { + "email_code": { + "type": "object", + "additionalProperties": false, + "properties": { + "email_template_id": { + "type": "string", + "format": "uuid", + "nullable": true + } + } + } + }, + "nullable": true, + "description": "Email Builder template selection. Does not enable verification by itself.", + "required": [ + "email_code" + ] + } + } + }, + "Error": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object" + } + } + }, + "CreateChatWidgetRequest": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "agent_id", + "website_chat" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "agent_id": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "website_chat": { + "type": "object", + "additionalProperties": false, + "required": [ + "allowed_origins", + "organisation_name", + "default_locale" + ], + "properties": { + "allowed_origins": { + "type": "array", + "maxItems": 50, + "uniqueItems": true, + "description": "Exact HTTPS origins. Empty means the widget cannot start sessions.", + "items": { + "type": "string", + "maxLength": 2048 + } + }, + "organisation_name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "default_locale": { + "type": "string", + "enum": [ + "en", + "de" + ] + }, + "design_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Design Builder configuration for the widget only." + }, + "authentication": { + "type": "object", + "additionalProperties": false, + "properties": { + "email_code": { + "type": "object", + "additionalProperties": false, + "properties": { + "email_template_id": { + "type": "string", + "format": "uuid", + "nullable": true + } + } + } + }, + "nullable": true, + "description": "Email Builder template selection. Does not enable verification by itself.", + "required": [ + "email_code" + ] + } + } + } + } + }, + "UpdateChatWidgetRequest": { + "type": "object", + "additionalProperties": false, + "required": [ + "version" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "agent_id": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "website_chat": { + "type": "object", + "additionalProperties": false, + "required": [ + "allowed_origins", + "organisation_name", + "default_locale" + ], + "properties": { + "allowed_origins": { + "type": "array", + "maxItems": 50, + "uniqueItems": true, + "description": "Exact HTTPS origins. Empty means the widget cannot start sessions.", + "items": { + "type": "string", + "maxLength": 2048 + } + }, + "organisation_name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "default_locale": { + "type": "string", + "enum": [ + "en", + "de" + ] + }, + "design_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Design Builder configuration for the widget only." + }, + "authentication": { + "type": "object", + "additionalProperties": false, + "properties": { + "email_code": { + "type": "object", + "additionalProperties": false, + "properties": { + "email_template_id": { + "type": "string", + "format": "uuid", + "nullable": true + } + } + } + }, + "nullable": true, + "description": "Email Builder template selection. Does not enable verification by itself.", + "required": [ + "email_code" + ] + } + } + }, + "version": { + "type": "integer", + "minimum": 1, + "description": "Version read by the editor; stale writes return 409." + } + } + }, + "Widget": { + "type": "object", + "required": [ + "key", + "organisationName", + "assistantName", + "defaultLocale", + "locales" + ], + "properties": { + "key": { + "type": "string" + }, + "organisationName": { + "type": "string" + }, + "assistantName": { + "type": "string" + }, + "defaultLocale": { + "type": "string", + "enum": [ + "en", + "de" + ] + }, + "design": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "last_modified_at": { + "type": "string", + "maxLength": 200 + }, + "style": { + "type": "object", + "additionalProperties": false, + "properties": { + "palette": { + "type": "object", + "additionalProperties": false, + "properties": { + "primary": { + "type": "string", + "maxLength": 200 + }, + "background": { + "type": "string", + "maxLength": 200 + } + } + }, + "typography": { + "type": "object", + "additionalProperties": false, + "properties": { + "font": { + "type": "object", + "additionalProperties": false, + "properties": { + "font_family": { + "type": "string", + "maxLength": 200 + } + } + } + } + }, + "shape": { + "type": "object", + "additionalProperties": false, + "properties": { + "border_radius": { + "type": "number", + "minimum": 0, + "maximum": 100 + } + } + } + } + }, + "spark_theme": { + "type": "object", + "additionalProperties": false, + "properties": { + "accentColor": { + "type": "string", + "maxLength": 200 + }, + "backgroundColor": { + "type": "string", + "maxLength": 200 + }, + "fontBody": { + "type": "string", + "maxLength": 200 + }, + "fontHeading": { + "type": "string", + "maxLength": 200 + }, + "radius": { + "type": "string", + "maxLength": 200 + }, + "scaling": { + "type": "string", + "maxLength": 200 + }, + "spacing": { + "type": "string", + "maxLength": 200 + }, + "appearance": { + "type": "string", + "maxLength": 200 + }, + "neutralColor": { + "type": "string", + "maxLength": 200 + }, + "styleVariant": { + "type": "string", + "maxLength": 200 + }, + "labelPosition": { + "type": "string", + "maxLength": 200 + }, + "inputStyle": { + "type": "string", + "maxLength": 200 + }, + "inputColor": { + "type": "string", + "maxLength": 200 + }, + "cardVariant": { + "type": "string", + "maxLength": 200 + }, + "cardColor": { + "type": "string", + "maxLength": 200 + }, + "highContrast": { + "type": "boolean" + } + } + } + }, + "required": [ + "id" + ] + }, + "locales": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "en", + "de" + ] + } + } + } + }, + "WidgetDesign": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "last_modified_at": { + "type": "string", + "maxLength": 200 + }, + "style": { + "type": "object", + "additionalProperties": false, + "properties": { + "palette": { + "type": "object", + "additionalProperties": false, + "properties": { + "primary": { + "type": "string", + "maxLength": 200 + }, + "background": { + "type": "string", + "maxLength": 200 + } + } + }, + "typography": { + "type": "object", + "additionalProperties": false, + "properties": { + "font": { + "type": "object", + "additionalProperties": false, + "properties": { + "font_family": { + "type": "string", + "maxLength": 200 + } + } + } + } + }, + "shape": { + "type": "object", + "additionalProperties": false, + "properties": { + "border_radius": { + "type": "number", + "minimum": 0, + "maximum": 100 + } + } + } + } + }, + "spark_theme": { + "type": "object", + "additionalProperties": false, + "properties": { + "accentColor": { + "type": "string", + "maxLength": 200 + }, + "backgroundColor": { + "type": "string", + "maxLength": 200 + }, + "fontBody": { + "type": "string", + "maxLength": 200 + }, + "fontHeading": { + "type": "string", + "maxLength": 200 + }, + "radius": { + "type": "string", + "maxLength": 200 + }, + "scaling": { + "type": "string", + "maxLength": 200 + }, + "spacing": { + "type": "string", + "maxLength": 200 + }, + "appearance": { + "type": "string", + "maxLength": 200 + }, + "neutralColor": { + "type": "string", + "maxLength": 200 + }, + "styleVariant": { + "type": "string", + "maxLength": 200 + }, + "labelPosition": { + "type": "string", + "maxLength": 200 + }, + "inputStyle": { + "type": "string", + "maxLength": 200 + }, + "inputColor": { + "type": "string", + "maxLength": 200 + }, + "cardVariant": { + "type": "string", + "maxLength": 200 + }, + "cardColor": { + "type": "string", + "maxLength": 200 + }, + "highContrast": { + "type": "boolean" + } + } + } + }, + "required": [ + "id" + ] + }, + "PublicEvent": { + "description": "JSON payload of one SSE data frame from sendAnonymousChatMessage.", + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "text", + "request_id" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "delta" + ] + }, + "text": { + "type": "string", + "maxLength": 12000 + }, + "request_id": { + "type": "string", + "format": "uuid" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "text", + "request_id" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "complete" + ] + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 12000 + }, + "request_id": { + "type": "string", + "format": "uuid" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "code", + "request_id" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "error" + ] + }, + "code": { + "type": "string", + "enum": [ + "UNAVAILABLE" + ] + }, + "request_id": { + "type": "string", + "format": "uuid" + } + } + } + ] + }, + "PublicChatError": { + "type": "object", + "required": [ + "code" + ], + "properties": { + "code": { + "type": "string", + "enum": [ + "INVALID_REQUEST", + "ORIGIN_DENIED", + "NOT_FOUND", + "INVALID_GRANT", + "SESSION_EXPIRED", + "RATE_LIMITED", + "SESSION_LIMIT", + "USAGE_LIMIT", + "REQUEST_CONFLICT", + "IN_PROGRESS", + "UNAVAILABLE", + "TEMPORARILY_UNAVAILABLE" + ] + } + } + } + }, + "responses": { + "Error": { + "description": "400 invalid input, 401 expired capability, 403 origin denied, 404 unpublished widget, 409 conflict, 413 too large, 429 rate limit, or 503 unavailable.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "code" + ], + "properties": { + "code": { + "type": "string", + "enum": [ + "INVALID_REQUEST", + "ORIGIN_DENIED", + "NOT_FOUND", + "INVALID_GRANT", + "SESSION_EXPIRED", + "RATE_LIMITED", + "SESSION_LIMIT", + "USAGE_LIMIT", + "REQUEST_CONFLICT", + "IN_PROGRESS", + "UNAVAILABLE", + "TEMPORARILY_UNAVAILABLE" + ] + } + } + } + } + } + } + } + } +} diff --git a/packages/cli/definitions/customer-portal.json b/packages/cli/definitions/customer-portal.json index f860f363d..b4af504e3 100644 --- a/packages/cli/definitions/customer-portal.json +++ b/packages/cli/definitions/customer-portal.json @@ -2,7 +2,7 @@ "openapi": "3.0.3", "info": { "title": "Portal API", - "description": "Backend for epilot portals - End Customer Portal & Installer Portal", + "description": "Backend for epilot portals - End Customer Portal & Installer Portal\n", "version": "1.0.0" }, "tags": [ @@ -281,6 +281,7 @@ "PortalAuth": [] } ], + "x-contact-identification-token": true, "responses": { "204": { "description": "Tokens is valid for the given organization." @@ -480,6 +481,189 @@ } } }, + "/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", @@ -698,7 +882,7 @@ "/v2/portal/consumption": { "get": { "operationId": "getConsumption", - "summary": "Get Consumption", + "summary": "getConsumption", "description": "Get energy consumption data between a given time period.", "tags": [ "ECP" @@ -775,11 +959,13 @@ "PT15M", "PT1H", "P1D", - "P1M" + "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." + "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", @@ -822,6 +1008,41 @@ "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": [ @@ -853,7 +1074,7 @@ "/v2/portal/visualization:export": { "post": { "operationId": "prepareVisualizationExport", - "summary": "Prepare Visualization Export", + "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" @@ -957,7 +1178,7 @@ "/v2/portal/visualization/metadata": { "get": { "operationId": "getVisualizationMetadata", - "summary": "Get Visualization Metadata", + "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" @@ -1025,7 +1246,7 @@ "/v2/portal/costs": { "get": { "operationId": "getCosts", - "summary": "Get Costs", + "summary": "getCosts", "description": "Get energy cost data between a given time period.", "tags": [ "ECP" @@ -1102,7 +1323,8 @@ "PT15M", "PT1H", "P1D", - "P1M" + "P1M", + "P1Y" ] }, "required": true, @@ -1199,7 +1421,7 @@ "/v2/portal/prices": { "get": { "operationId": "getPrices", - "summary": "Get Prices", + "summary": "getPrices", "description": "Get energy prices data between a given time period.", "tags": [ "ECP" @@ -1276,7 +1498,8 @@ "PT15M", "PT1H", "P1D", - "P1M" + "P1M", + "P1Y" ] }, "required": true, @@ -1930,7 +2153,23 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PortalConfig" + "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" + } + } + } + } + ] } } } @@ -2190,11 +2429,11 @@ } } }, - "/v3/portal/email-templates/{portal_id}": { + "/v3/portal/email-templates:migrate-references": { "post": { - "operationId": "upsertEmailTemplatesByPortalId", - "summary": "upsertEmailTemplatesByPortalId", - "description": "Upserts the email templates of a portal by portal ID", + "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" ], @@ -2203,48 +2442,59 @@ "EpilotAuth": [] } ], - "parameters": [ - { - "in": "path", - "name": "portal_id", - "required": true, - "schema": { - "type": "string" - }, - "description": "ID of the portal" - } - ], "requestBody": { - "description": "Email templates payload", + "description": "Source and destination template ids", "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmailTemplates" + "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": "Upserted email templates of the portal successfully.", + "description": "Migration completed (may have partial failures in failed_portal_ids).", "content": { "application/json": { "schema": { "type": "object", "required": [ - "message", - "emailTemplates" + "migrated_portal_count", + "migrated_portal_ids", + "failed_portal_ids" ], "properties": { - "message": { - "type": "string", - "enum": [ - "Email Templates upserted successfully" - ] + "migrated_portal_count": { + "type": "integer", + "example": 2 }, - "emailTemplates": { - "$ref": "#/components/schemas/EmailTemplates" + "migrated_portal_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "failed_portal_ids": { + "type": "array", + "items": { + "type": "string" + } } } } @@ -2261,11 +2511,13 @@ "$ref": "#/components/responses/InternalServerError" } } - }, - "get": { - "operationId": "getEmailTemplatesByPortalId", - "summary": "getEmailTemplatesByPortalId", - "description": "Retrieves the email templates of a portal by portal ID", + } + }, + "/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" ], @@ -2274,30 +2526,181 @@ "EpilotAuth": [] } ], - "parameters": [ - { - "in": "path", - "name": "portal_id", - "required": true, - "schema": { - "type": "string" - }, - "description": "ID of the portal" + "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": "Retrieved the email templates successfully.", + "description": "Portals referencing the template.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmailTemplates" - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" + "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" @@ -2765,6 +3168,14 @@ }, "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" } } } @@ -2921,6 +3332,7 @@ "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" ], @@ -3291,6 +3703,7 @@ "operationId": "getContact", "summary": "getContact", "description": "Retrieves the contact of the logged in user.", + "x-contact-identification-token": true, "tags": [ "ECP" ], @@ -3501,6 +3914,14 @@ "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" } } } @@ -3513,6 +3934,9 @@ "404": { "$ref": "#/components/responses/NotFound" }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + }, "500": { "$ref": "#/components/responses/InternalServerError" } @@ -3579,6 +4003,14 @@ "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" } } } @@ -3591,6 +4023,78 @@ "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" } @@ -3665,6 +4169,9 @@ "404": { "$ref": "#/components/responses/NotFound" }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + }, "500": { "$ref": "#/components/responses/InternalServerError" } @@ -3831,9 +4338,8 @@ "properties": { "message": { "type": "string", - "enum": [ - "User Succesfully Deleted" - ] + "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" @@ -3873,8 +4379,7 @@ "schema": { "type": "object", "required": [ - "email", - "password" + "email" ], "properties": { "email": { @@ -3884,7 +4389,7 @@ }, "password": { "type": "string", - "description": "Password of the portal user for confirmation" + "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" } } } @@ -3893,7 +4398,7 @@ }, "responses": { "200": { - "description": "Portal user email updated successfully and will receive a confirmation email soon.", + "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": { @@ -3901,9 +4406,8 @@ "properties": { "message": { "type": "string", - "enum": [ - "You will receive a confirmation mail soon on your updated email address." - ] + "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." } } } @@ -3922,28 +4426,89 @@ } } }, - "/v2/portal/user/resend/confirmation-email/{id}": { - "post": { - "operationId": "resendConfirmationEmail", - "summary": "resendConfirmationEmail", - "description": "Resend confirmation email", + "/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 Admin" + "ECP" ], "security": [ { - "EpilotAuth": [] + "PortalAuth": [] } ], - "parameters": [ - { - "in": "path", - "name": "id", - "schema": { - "$ref": "#/components/schemas/EntityId" - }, - "required": true, - "description": "The ID of portal user id" + "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": { @@ -5567,6 +6132,132 @@ } } }, + "/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", @@ -6416,6 +7107,56 @@ } } }, + "/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", @@ -6474,7 +7215,7 @@ "/v2/portal/user/files/count-by-entity": { "get": { "operationId": "getFilesCountByEntity", - "summary": "getFileCountByEntity", + "summary": "getFilesCountByEntity", "description": "Fetch file counts for all ECP user related entities", "deprecated": true, "tags": [ @@ -6610,8 +7351,8 @@ "in": "query", "schema": { "type": "string", - "description": "Key to sort by", - "example": "due_date:asc" + "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" } } ], @@ -6944,6 +7685,7 @@ "PortalAuth": [] } ], + "x-contact-identification-token": true, "requestBody": { "content": { "application/json": { @@ -6984,6 +7726,7 @@ "operationId": "searchPortalUserEntities", "summary": "searchPortalUserEntities", "description": "Search all entities of a portal user", + "x-contact-identification-token": true, "tags": [ "ECP" ], @@ -7034,71 +7777,221 @@ } } }, - "/v2/portal/can-trigger-portal-flow": { + "/v2/portal/entity/{slug}": { "post": { - "operationId": "canTriggerPortalFlow", - "summary": "canTriggerPortalFlow", - "description": "Returns whether the user can trigger a portal flow", + "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 Admin" + "ECP" ], "security": [ { - "EpilotAuth": [] + "PortalAuth": [] } ], "parameters": [ { - "in": "query", - "name": "origin", - "required": false, - "deprecated": true, - "schema": { - "$ref": "#/components/schemas/Origin" - }, - "description": "Origin of the portal" - }, - { - "in": "query", - "name": "portal_id", + "in": "path", + "name": "slug", + "description": "Entity schema slug to create. Limited to the supported (experimental) schemas, currently `asset`.", "required": true, "schema": { "type": "string", - "description": "Portal ID", - "example": "123" + "enum": [ + "asset" + ], + "example": "asset" } } ], "requestBody": { - "description": "Request of trigger portal flow", "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TriggerPortalFlow" + "$ref": "#/components/schemas/Entity" + }, + "example": { + "title": "PV Inverter", + "manufacturer": "SMA", + "external_id": "device-123" } } } }, "responses": { - "200": { - "description": "Can Trigger Portal Flow", + "201": { + "description": "The entity has been created successfully for the portal user.", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "can_trigger": { - "type": "boolean", - "description": "Whether the flow can be triggered", - "example": true - } - } + "$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": { @@ -7212,7 +8105,7 @@ "/v2/portal/entity/{slug}/{id}/workflows": { "get": { "operationId": "getEntityWorkflows", - "summary": "Get workflows for an entity", + "summary": "getEntityWorkflows", "description": "Get all workflows associated with an entity (requires access to the entity)", "tags": [ "ECP" @@ -7278,11 +8171,11 @@ } } }, - "/v2/portal/metering/reading/photo": { - "post": { - "operationId": "uploadMeterReadingPhoto", - "summary": "Upload Meter Reading Photo", - "description": "Uploads a Meter Reading photo and - if enabled - gives back data extracted from the photo.", + "/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" ], @@ -7291,36 +8184,33 @@ "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.", + "description": "List of outstanding workflow tasks for the portal user", "content": { "application/json": { "schema": { "type": "object", + "required": [ + "tasks", + "total" + ], "properties": { - "data": { - "$ref": "#/components/schemas/MeterReadingPhotoData" + "tasks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OutstandingTask" + } + }, + "total": { + "type": "integer", + "example": 0 } } } } } }, - "400": { - "$ref": "#/components/responses/InvalidRequest" - }, "401": { "$ref": "#/components/responses/Unauthorized" }, @@ -7333,11 +8223,11 @@ } } }, - "/v2/portal/metering/reading": { - "post": { - "operationId": "createMeterReading", - "summary": "Create Meter Reading", - "description": "Inserts a new meter reading.", + "/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" ], @@ -7348,62 +8238,65 @@ ], "parameters": [ { - "name": "override_plausibility", - "in": "query", - "required": false, - "description": "Override plausibility check", + "in": "path", + "name": "slug", + "required": true, "schema": { - "type": "boolean", - "default": false + "type": "string", + "description": "Schema slug of the entity", + "example": "opportunity" } - } - ], - "requestBody": { - "description": "Meter reading payload.", - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MeterReading" - } + }, + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string", + "description": "ID of the entity", + "example": "abc123" } } - }, + ], "responses": { "200": { - "description": "Meter reading created successfully.", + "description": "Portal-relevant workflows for the entity, each with linearized tasks", "content": { "application/json": { "schema": { "type": "object", "properties": { - "data": { - "$ref": "#/components/schemas/MeterReading" + "portal_workflows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PortalWorkflow" + } } } } } } }, - "400": { - "$ref": "#/components/responses/InvalidRequestCreateMeterReading" - }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, + "404": { + "$ref": "#/components/responses/NotFound" + }, "500": { "$ref": "#/components/responses/InternalServerError" } } } }, - "/v2/portal/metering/reading/allowed-range/{meter_id}": { - "get": { - "operationId": "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", + "/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" ], @@ -7412,26 +8305,234 @@ "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" - }, - { + "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, @@ -7504,7 +8605,8 @@ "/v2/portal/metering/readings": { "post": { "operationId": "getMeterReadings", - "summary": "Get meter readings with optional template resolution", + "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" @@ -7549,17 +8651,23 @@ }, "templates": { "type": "object", + "deprecated": true, "additionalProperties": { "type": "string" }, - "description": "Template map (key to Handlebars template string). Each template is resolved per reading." + "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": "Template map resolved against the counter entity." + "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." } } } @@ -7703,31 +8811,30 @@ } } }, - "/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": [] - } + "/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": "query", - "name": "origin", + "in": "path", + "name": "provider_slug", "required": true, - "description": "Origin of the Portal", + "description": "Provider slug (organization-unique)", "schema": { - "$ref": "#/components/schemas/Origin" + "$ref": "#/components/schemas/ProviderSlug" } }, { "in": "query", "name": "org_id", - "description": "epilot organization id", "required": true, + "description": "epilot organization id", "schema": { "type": "string", "example": 123 @@ -7735,24 +8842,87 @@ }, { "in": "query", - "name": "contact_id", - "description": "contact id in the epilot system", - "required": false, + "name": "portal_id", + "required": true, + "description": "ID of the Portal", "schema": { - "type": "string", - "format": "uuid", - "example": "5da0a718-c822-403d-9f5d-20d4584e0528" + "$ref": "#/components/schemas/PortalId" } } ], - "tags": [ - "Login" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", + "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" @@ -7848,6 +9018,11 @@ "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." } } } @@ -7874,6 +9049,9 @@ } } } + }, + "400": { + "$ref": "#/components/responses/InvalidRequest" } } } @@ -8145,6 +9323,7 @@ "EitherAuth": [] } ], + "x-contact-identification-token": true, "parameters": [ { "in": "query", @@ -8331,7 +9510,7 @@ "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.", + "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" ], @@ -8849,7 +10028,7 @@ "/v2/portal/campaign/{campaign_id}/entity:status": { "put": { "operationId": "updateCampaignPortalBlockStatus", - "summary": "Update Campaign Portal Block Status", + "summary": "updateCampaignPortalBlockStatus", "description": "Updates the status of a campaign portal block for multiple recipients.", "tags": [ "ECP" @@ -8972,6 +10151,192 @@ } } }, + "/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", @@ -9319,6 +10684,20 @@ "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": { @@ -9409,17 +10788,300 @@ } } }, - "/v3/portal/configs": { - "get": { - "operationId": "listAllPortalConfigs", - "summary": "listAllPortalConfigs", - "description": "Retrieves all portal configurations.", + "/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" ], - "security": [ + "parameters": [ { - "EpilotAuth": [] + "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": { @@ -9489,7 +11151,7 @@ "items": { "$ref": "#/components/schemas/SwappableConfig" }, - "description": "Items to swap" + "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." } } } @@ -9624,13 +11286,6 @@ "type": "string", "description": "Email address of the partner to invite" }, - "represents_contact_list": { - "type": "array", - "items": { - "$ref": "#/components/schemas/EntityId", - "description": "List of contact IDs representing the contacts / business partners the invited user should be added to.\nWhen omitted, the user is automatically added to all contacts / business partners linked to the account.\n" - } - }, "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", @@ -10127,15 +11782,136 @@ } } } - } - }, - "components": { - "responses": { - "InvalidRequest": { - "description": "The request could not be validated", - "content": { - "application/json": { - "schema": { + }, + "/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" } } @@ -10237,6 +12013,46 @@ } } }, + "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": { @@ -10360,86 +12176,429 @@ } }, "schemas": { - "ContextEntity": { + "PortalNotification": { "type": "object", - "description": "An entity reference for context-aware operations", + "description": "A 360 notification addressed to a portal user.", + "required": [ + "id", + "read" + ], "properties": { - "entity_schema": { + "id": { "type": "string", - "description": "Entity schema", - "example": "contract" + "description": "Stable string identifier of the notification (the numeric notification id as a string).", + "example": "1234567890" }, - "entity_id": { + "notification_id": { + "type": "number", + "description": "Numeric id of the notification, used to mark it as read.", + "example": 1234567890 + }, + "type": { "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": { + "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", - "description": "Error message" + "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." } } }, - "FailedRuleErrorResp": { + "MobileBuildStatus": { "type": "object", + "description": "Latest build/upload status for a platform (system-written).", + "additionalProperties": true, "properties": { - "message": { + "version": { + "type": "string" + }, + "build_number": { + "type": "integer" + }, + "track": { "type": "string", - "description": "Error message" + "description": "e.g. testflight | internal | beta" }, - "failed_rule": { - "type": "object", - "description": "Failed validation rule" + "status": { + "type": "string", + "enum": [ + "building", + "uploaded", + "failed" + ] + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "error": { + "type": "string" } } }, - "EmailTemplates": { + "MobileBranding": { "type": "object", - "description": "Email templates used for authentication and internal processes", + "additionalProperties": true, "properties": { - "confirmAccount": { - "$ref": "#/components/schemas/EntityId", - "nullable": true, - "description": "ID of the confirmation email template upon registration" + "app_icon": { + "type": "string" }, - "advancedAuth": { - "$ref": "#/components/schemas/EntityId", - "nullable": true, - "description": "ID of the advanced Auth with login link and login code" + "splash": { + "type": "string" }, - "advancedMFA": { - "$ref": "#/components/schemas/EntityId", - "nullable": true, - "deprecated": true, - "description": "ID of the advanced MFA with login link and login code" + "splash_dark": { + "type": "string" }, - "journeySignUp": { - "$ref": "#/components/schemas/EntityId", - "nullable": true, - "description": "ID of the email template for signing up from Journeys" + "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", @@ -10815,7 +12974,7 @@ }, "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\nEach provider is persisted verbatim — `oidc_config.client_secret` is stored\nas sent. Customers are encouraged to reference an org env secret via\n`{{ env.VAR }}` rather than embed raw values.\n", + "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" } @@ -10825,6 +12984,10 @@ "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" @@ -10924,6 +13087,17 @@ "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" } } }, @@ -10969,6 +13143,10 @@ "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" } } }, @@ -11066,6 +13244,13 @@ "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." } } } @@ -11208,6 +13393,13 @@ } ] }, + "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", @@ -11357,6 +13549,12 @@ "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" }, @@ -11380,16 +13578,59 @@ "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." } } }, - "UpsertPortalConfig": { - "allOf": [ - { - "$ref": "#/components/schemas/UpdateOnlyPortalConfigAttributes" + "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" + ] }, - { - "$ref": "#/components/schemas/CommonConfigAttributes" + "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" } ] }, @@ -11854,6 +14095,84 @@ "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 ` 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" } } }, @@ -12241,9 +14560,55 @@ }, "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`." } } }, @@ -13165,11 +15530,50 @@ "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 ` 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": [ @@ -13257,6 +15661,94 @@ } } }, + "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 (`:`), 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": { @@ -13415,6 +15907,57 @@ "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": {}, @@ -13493,61 +16036,336 @@ "completedTime": "2024-01-13T05:18:43.827Z" } }, - "BaseBillingEvent": { - "allOf": [ - { - "$ref": "#/components/schemas/BaseEntity" - }, - { - "$ref": "#/components/schemas/Entity" - } - ], - "description": "A base billing event to be inherited by all billing events.", + "PortalWorkflow": { "type": "object", - "required": [ - "type", - "contract" - ], - "additionalProperties": true, + "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": { - "billing_amount": { + "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": "Amount to be paid in cents i.e. precision 2", - "example": 10050 + "description": "2 = legacy V1 workflow execution (linear model), 3 = V2 flow execution (graph model)", + "enum": [ + 2, + 3 + ] }, - "billing_amount_decimal": { + "created_at": { "type": "string", - "description": "Amount to be paid in cents in decimal string representation", - "example": "100.50" + "description": "Creation timestamp, doubles as started time" }, - "billing_currency": { - "$ref": "#/components/schemas/Currency" + "updated_at": { + "type": "string", + "description": "Last update timestamp" }, - "external_id": { + "completed_at": { "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" + "description": "Timestamp when the execution was completed, if it is" }, - "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" - } - } - } - } + "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": [ @@ -13767,7 +16585,8 @@ }, "templates": { "type": "object", - "description": "Template strings to parse and return as synthetic fields. Supports both string values and nested objects of strings.", + "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": [ { @@ -13791,6 +16610,10 @@ } } }, + "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", @@ -13884,7 +16707,7 @@ }, "q": { "type": "string", - "description": "Keyword search query", + "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": { @@ -13907,7 +16730,8 @@ }, "group_title": { "type": "string", - "description": "Template for group title using variables", + "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": { @@ -13974,7 +16798,8 @@ }, "templates": { "type": "object", - "description": "Template strings to parse and return as synthetic fields", + "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" }, @@ -13984,6 +16809,10 @@ "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", @@ -14282,24 +17111,138 @@ "$ref": "#/components/schemas/PublicMeterReadingPlausibilityCheckDetails" } } - } - } - }, - "DataRetrievalItem": { - "type": "object", - "properties": { - "app": { - "$ref": "#/components/schemas/PublicAppDetails" }, - "extension": { - "$ref": "#/components/schemas/PublicExtensionDetails" + "changeEmail": { + "type": "object", + "properties": { + "app": { + "$ref": "#/components/schemas/PublicAppDetails" + }, + "extension": { + "$ref": "#/components/schemas/PublicExtensionDetails" + }, + "hook": { + "$ref": "#/components/schemas/PublicChangeEmailDetails" + } + } }, - "hook": { - "$ref": "#/components/schemas/PublicDataRetrievalHookDetails" + "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" + } + } } } }, - "PublicAppDetails": { + "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": { @@ -14402,14 +17345,16 @@ }, "intervals": { "type": "array", - "description": "Intervals supported for the current context. If omitted, all intervals are assumed supported.", + "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" + "P1M", + "P1Y", + "custom" ] } }, @@ -14694,6 +17639,15 @@ }, { "$ref": "#/components/schemas/ExtensionHookVisualizationMetadata" + }, + { + "$ref": "#/components/schemas/ExtensionHookChangeEmail" + }, + { + "$ref": "#/components/schemas/ExtensionHookChangePassword" + }, + { + "$ref": "#/components/schemas/ExtensionHookDeleteAccount" } ] } @@ -15558,87 +18512,389 @@ "call" ] }, - "SecureProxyConfig": { + "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", - "description": "Configuration for routing requests through the ERP Integration secure proxy. Mutually exclusive with use_static_ips.", "properties": { - "integration_id": { + "type": { "type": "string", - "format": "uuid", - "description": "Integration ID that owns the secure_proxy use case." + "enum": [ + "changeEmail" + ] }, - "use_case_slug": { + "change_mode": { "type": "string", - "description": "Use case slug for the secure proxy use case." + "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": [ - "integration_id", - "use_case_slug" + "type", + "call" ] }, - "ExtensionAuthBlock": { + "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": { - "method": { + "type": { "type": "string", - "description": "HTTP method to use for authentication", - "default": "GET" + "enum": [ + "changePassword" + ] }, - "url": { - "type": "string", - "description": "URL to use for authentication. Supports variable interpolation." + "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 }, - "params": { + "explanation": { "type": "object", - "description": "Parameters to append to the URL. Supports variable interpolation.", - "additionalProperties": { - "type": "string" + "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." + } }, - "default": {} - }, - "headers": { - "type": "object", - "description": "Headers to use for authentication. Supports variable interpolation.", "additionalProperties": { - "type": "string" + "type": "string", + "description": "Explanation of the functionality in language denoted by ISO 3166-1 alpha-2 code." }, - "default": {} + "required": [ + "en" + ], + "description": "Optional explanation shown to the user in the change password confirmation dialog." }, - "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": {} + "auth": { + "$ref": "#/components/schemas/ExtensionAuthBlock" }, - "cache": { + "call": { "type": "object", "properties": { - "key": { + "method": { "type": "string", - "description": "Key to use to identify the auth response. Supports interpolation.", - "example": "{{Options.api_key}}" + "description": "HTTP method to use for the call", + "default": "POST" }, - "ttl": { + "url": { "type": "string", - "description": "Time to live in seconds for the cache. Supports interpolation.", - "example": "{{AuthResponse.data.expires_in}}" + "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": [ - "key", - "ttl" + "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": [ - "url" + "type", + "call" ] }, - "Direction": { - "type": "string", + "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" @@ -15852,7 +19108,7 @@ "type": "string", "example": "office-365-login", "description": "URL-friendly slug to use as organization-unique identifier for Provider", - "pattern": "[0-9a-z-]+" + "pattern": "[0-9a-z_-]+" }, "ProviderDisplayName": { "type": "string", @@ -15886,6 +19142,11 @@ }, "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": [ @@ -15914,6 +19175,41 @@ "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", @@ -16198,7 +19494,6 @@ "token_endpoint", "grant_type", "code", - "redirect_uri", "client_id", "code_verifier" ], @@ -16208,7 +19503,8 @@ }, "token_endpoint": { "type": "string", - "description": "URL of the authorization endpoint", + "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": { @@ -16223,8 +19519,8 @@ }, "redirect_uri": { "type": "string", - "description": "The redirect uri", - "example": "https://customer-portal.com/login" + "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:///sso`).\n", + "example": "https://customer-portal.com/sso" }, "client_id": { "type": "string", @@ -16604,6 +19900,10 @@ "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" @@ -16703,6 +20003,17 @@ "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" } } }, @@ -16748,6 +20059,10 @@ "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" } } }, @@ -16845,6 +20160,13 @@ "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." } } } @@ -16987,6 +20309,13 @@ } ] }, + "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", @@ -17136,6 +20465,12 @@ "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" }, @@ -17161,26 +20496,110 @@ "description": "ID of the portal", "example": "453ad7bf-86d5-46c8-8252-bcc868df5e3c" }, - "UpsertPortalConfigV3": { - "allOf": [ - { - "$ref": "#/components/schemas/UpdateOnlyPortalConfigAttributes" + "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" }, - { - "$ref": "#/components/schemas/CommonConfigAttributesV3" + "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": { - "origin": { - "$ref": "#/components/schemas/Origin" - }, - "pages": { + "path": { "type": "array", + "description": "Relation hops from the anchor entity (max 3).", "items": { - "$ref": "#/components/schemas/PageRequest" - } - } - } + "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" + } + } + } } ] }, @@ -17266,6 +20685,10 @@ "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", @@ -17273,12 +20696,283 @@ } } }, + "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": [ - "all", - "domain", - "users", "email_templates" ] }, @@ -17325,6 +21019,157 @@ "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" + } + } } } }, diff --git a/packages/cli/definitions/dashboard.json b/packages/cli/definitions/dashboard.json index 464b92306..1009548f3 100644 --- a/packages/cli/definitions/dashboard.json +++ b/packages/cli/definitions/dashboard.json @@ -17,6 +17,9 @@ { "name": "Dashboards" }, + { + "name": "Insights" + }, { "name": "Visualisations" }, @@ -33,6 +36,53 @@ "tags": [ "Dashboards" ], + "parameters": [ + { + "$ref": "#/components/parameters/Search" + }, + { + "$ref": "#/components/parameters/CreatedBy" + }, + { + "$ref": "#/components/parameters/CreatedAfter" + }, + { + "$ref": "#/components/parameters/CreatedBefore" + }, + { + "$ref": "#/components/parameters/UpdatedAfter" + }, + { + "$ref": "#/components/parameters/UpdatedBefore" + }, + { + "$ref": "#/components/parameters/SharedWith" + }, + { + "$ref": "#/components/parameters/Owner" + }, + { + "$ref": "#/components/parameters/AccessibleTo" + }, + { + "$ref": "#/components/parameters/Favorite" + }, + { + "$ref": "#/components/parameters/FavoritesFirst" + }, + { + "$ref": "#/components/parameters/Sort" + }, + { + "$ref": "#/components/parameters/Order" + }, + { + "$ref": "#/components/parameters/Limit" + }, + { + "$ref": "#/components/parameters/Offset" + } + ], "responses": { "200": { "description": "List of dashboards available to the user", @@ -46,8 +96,15 @@ "items": { "$ref": "#/components/schemas/Dashboard" } + }, + "pagination": { + "$ref": "#/components/schemas/Pagination" } - } + }, + "required": [ + "results", + "pagination" + ] } } } @@ -85,6 +142,81 @@ } } }, + "/v1/dashboard/dashboards/favorites": { + "get": { + "operationId": "listFavoriteDashboardIds", + "summary": "listFavoriteDashboardIds", + "description": "Returns the current user's favorited dashboard ids, with no dashboard metadata. Lets a\nclient decide whether to default to a favorites-only view without first fetching the full\ndashboards list.\n", + "tags": [ + "Dashboards" + ], + "responses": { + "200": { + "description": "The current user's favorited dashboard ids", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "dashboard_ids": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DashboardID" + } + }, + "total": { + "type": "integer" + } + }, + "required": [ + "dashboard_ids", + "total" + ] + } + } + } + } + } + } + }, + "/v1/dashboard/dashboards/{id}/favorite": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "$ref": "#/components/schemas/DashboardID" + } + } + ], + "put": { + "operationId": "favoriteDashboard", + "summary": "favoriteDashboard", + "description": "Marks the dashboard as favorited by the current user. Idempotent — favoriting an\nalready-favorited dashboard is not an error. Requires only view-level access to the\ndashboard (unlike the edit-level check on `PATCH .../dashboards/{id}`).\n", + "tags": [ + "Dashboards" + ], + "responses": { + "204": { + "description": "The dashboard is now favorited by the current user" + } + } + }, + "delete": { + "operationId": "unfavoriteDashboard", + "summary": "unfavoriteDashboard", + "description": "Removes the current user's favorite for the dashboard. Idempotent — unfavoriting a\ndashboard that was not favorited is not an error, and neither is unfavoriting one that no\nlonger exists or is no longer shared with you. Requires no access to the dashboard, so that\na stale favorite always remains removable.\n", + "tags": [ + "Dashboards" + ], + "responses": { + "204": { + "description": "The dashboard is no longer favorited by the current user" + } + } + } + }, "/v1/dashboard/dashboards/{id}": { "parameters": [ { @@ -146,6 +278,36 @@ } } }, + "patch": { + "operationId": "patchDashboard", + "summary": "patchDashboard", + "description": "Partially update a dashboard by ID. Update content (title/tiles) and/or manage sharing\n(owners, shared_with, org_access) — only owners may change sharing.\n`owners` and `shared_with` replace the whole array (send the complete list); omit them to leave\nsharing unchanged, or send `shared_with: []` to revoke all per-user grants.\n", + "tags": [ + "Dashboards" + ], + "requestBody": { + "description": "The dashboard fields to update", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DashboardPatch" + } + } + } + }, + "responses": { + "200": { + "description": "The updated dashboard", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Dashboard" + } + } + } + } + } + }, "delete": { "operationId": "deleteDashboard", "summary": "deleteDashboard", @@ -167,6 +329,265 @@ } } }, + "/v1/dashboard/insights": { + "get": { + "operationId": "listInsights", + "summary": "listInsights", + "description": "List insights (saved charts) available to the user", + "tags": [ + "Insights" + ], + "parameters": [ + { + "$ref": "#/components/parameters/Search" + }, + { + "$ref": "#/components/parameters/VisualisationIdFilter" + }, + { + "$ref": "#/components/parameters/TagsFilter" + }, + { + "$ref": "#/components/parameters/TagsMatch" + }, + { + "$ref": "#/components/parameters/CreatedBy" + }, + { + "$ref": "#/components/parameters/CreatedAfter" + }, + { + "$ref": "#/components/parameters/CreatedBefore" + }, + { + "$ref": "#/components/parameters/UpdatedAfter" + }, + { + "$ref": "#/components/parameters/UpdatedBefore" + }, + { + "$ref": "#/components/parameters/SharedWith" + }, + { + "$ref": "#/components/parameters/Owner" + }, + { + "$ref": "#/components/parameters/AccessibleTo" + }, + { + "$ref": "#/components/parameters/Sort" + }, + { + "$ref": "#/components/parameters/Order" + }, + { + "$ref": "#/components/parameters/Limit" + }, + { + "$ref": "#/components/parameters/Offset" + } + ], + "responses": { + "200": { + "description": "List of insights available to the user", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Insight" + } + }, + "pagination": { + "$ref": "#/components/schemas/Pagination" + } + }, + "required": [ + "results", + "pagination" + ] + } + } + } + } + } + }, + "post": { + "operationId": "createInsight", + "summary": "createInsight", + "description": "Create a new insight (saved chart). The caller becomes the owner.", + "tags": [ + "Insights" + ], + "requestBody": { + "description": "The insight to create", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Insight" + } + } + } + }, + "responses": { + "201": { + "description": "The created insight", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Insight" + } + } + } + } + } + } + }, + "/v1/dashboard/insights/tags": { + "get": { + "operationId": "listInsightTags", + "summary": "listInsightTags", + "description": "List the distinct tags used by insights in the organization (for filter facets)", + "tags": [ + "Insights" + ], + "responses": { + "200": { + "description": "Distinct tags used by insights in the organization", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "/v1/dashboard/insights/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "$ref": "#/components/schemas/InsightID" + } + } + ], + "get": { + "operationId": "getInsight", + "summary": "getInsight", + "description": "Get insight by ID", + "tags": [ + "Insights" + ], + "responses": { + "200": { + "description": "The insight with the passed ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Insight" + } + } + } + } + } + }, + "put": { + "operationId": "putInsight", + "summary": "putInsight", + "description": "Replace an insight's content by ID. Sharing is managed via patchInsight.", + "tags": [ + "Insights" + ], + "requestBody": { + "description": "The updated insight", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Insight" + } + } + } + }, + "responses": { + "200": { + "description": "The insight with the passed ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Insight" + } + } + } + } + } + }, + "patch": { + "operationId": "patchInsight", + "summary": "patchInsight", + "description": "Partially update an insight by ID. Update content (name/description/visualisation/tags) and/or\nmanage sharing (owners, shared_with, org_access) — only owners may change sharing.\n`owners` and `shared_with` replace the whole array (send the complete list); omit them to leave\nsharing unchanged, or send `shared_with: []` to revoke all per-user grants.\n", + "tags": [ + "Insights" + ], + "requestBody": { + "description": "The insight fields to update", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InsightPatch" + } + } + } + }, + "responses": { + "200": { + "description": "The updated insight", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Insight" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteInsight", + "summary": "deleteInsight", + "description": "Delete an insight by ID. Only owners may delete.", + "tags": [ + "Insights" + ], + "responses": { + "200": { + "description": "The deleted insight", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Insight" + } + } + } + } + } + } + }, "/v1/dashboard/visualisations": { "get": { "operationId": "listAvailableVisualisations", @@ -251,72 +672,313 @@ "example": "3fa85f64-5717-4562-b3fc-2c963f66afa6" }, "Dashboard": { + "description": "A dashboard configuration. A dashboard is a canvas whose tiles arrange and\nposition insights (saved charts). Tiles may reference an insight by `insight_id`\nor, for backwards compatibility, embed a visualisation inline.\n", + "allOf": [ + { + "$ref": "#/components/schemas/AccessControl" + }, + { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/DashboardID" + }, + "title": { + "type": "string", + "example": "Employee Dashboard" + }, + "tiles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DashboardTile" + }, + "example": [ + { + "id": "e4af1297-1fd6-440f-9846-f475f580d40f", + "coordinates": {}, + "insight_id": "8d2e1c7a-3b4f-4a2e-9c1d-2f3a4b5c6d7e" + } + ] + }, + "favorited": { + "description": "Whether the requesting user has favorited this dashboard.", + "type": "boolean", + "readOnly": true, + "example": false + } + }, + "required": [ + "title", + "tiles" + ] + } + ] + }, + "DashboardTileID": { + "description": "Unique identifier for a tile in a dashboard", + "type": "string", + "format": "uuid", + "example": "e4af1297-1fd6-440f-9846-f475f580d40f" + }, + "SharePermission": { + "description": "Permission level granted to a user (or the whole organization) on a shared resource.\n`view` allows read-only access; `edit` additionally allows updating the content.\nFull control (delete and managing sharing) is reserved for owners.\n", + "type": "string", + "enum": [ + "view", + "edit" + ], + "example": "view" + }, + "ShareGrant": { + "description": "Grants a single user a permission level on a resource", "type": "object", - "description": "Adashboard configuration with tiles", "properties": { - "id": { - "$ref": "#/components/schemas/DashboardID" + "user_id": { + "description": "The id of the user the resource is shared with", + "type": "string", + "example": "10598" }, - "title": { + "permission": { + "$ref": "#/components/schemas/SharePermission" + } + }, + "required": [ + "user_id", + "permission" + ] + }, + "OrgAccess": { + "description": "Optional organization-wide grant. When set, every user in the resource's organization\nis granted this permission level. `null` (or omitted) means the resource is not shared\norg-wide.\n", + "anyOf": [ + { + "type": "string", + "enum": [ + "view", + "edit" + ] + }, + { + "enum": [ + null + ] + } + ], + "example": "view" + }, + "AccessControl": { + "description": "Ownership and sharing metadata common to dashboards and insights. `created_*`/`updated_*`\nfields are managed by the server. `owners`, `shared_with` and `org_access` describe who\nmay access the resource. New resources are private to their creator until shared.\n", + "type": "object", + "properties": { + "created_by": { + "description": "Id of the user who created the resource", + "type": "string", + "readOnly": true, + "example": "10598" + }, + "created_at": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "updated_by": { + "description": "Id of the user who last updated the resource", "type": "string", - "example": "Employee Dashboard" + "readOnly": true, + "example": "10598" }, - "tiles": { + "updated_at": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "owner_org_id": { + "description": "Id of the organisation that owns the resource. Set at creation time and immutable.", + "type": "string", + "readOnly": true, + "example": "739224" + }, + "owners": { + "description": "User ids with full control over the resource (view, edit, delete and manage sharing).\nThe creator is always an owner. There must always be at least one owner.\n", "type": "array", "items": { - "$ref": "#/components/schemas/DashboardTile" + "type": "string" }, "example": [ - { - "id": "e4af1297-1fd6-440f-9846-f475f580d40f", - "coordinates": {}, - "visualisation": "timechart", - "visualisation_config": { - "query": { - "dataset": "entity_operations", - "measure": "count_operations", - "filters": [ - { - "operation": [ - "createEntity" - ] - }, - { - "entity_schema": [ - "opportunity" - ] - } - ], - "dimensions": [ - { - "time_with_granularity": "month" - }, - { - "entity_attribute": "source.title" - } - ] - }, - "options": { - "type": "line" - } + "10598" + ] + }, + "shared_with": { + "description": "Per-user sharing grants", + "type": "array", + "items": { + "$ref": "#/components/schemas/ShareGrant" + } + }, + "org_access": { + "$ref": "#/components/schemas/OrgAccess" + } + } + }, + "AccessControlUpdate": { + "description": "Sharing fields that can be mutated via PATCH (owners only). `owners` and `shared_with` are\nfull replacements of their arrays, not merges — omit a field to leave it unchanged.\n", + "type": "object", + "properties": { + "owners": { + "description": "Replaces the entire list of owner user ids. Must keep at least one owner (else 400).\nOmit to leave owners unchanged.\n", + "type": "array", + "items": { + "type": "string" + } + }, + "shared_with": { + "description": "Replaces the entire list of per-user grants — any existing grant not included is removed.\nOmit to leave sharing unchanged; send an empty array to revoke all per-user grants.\n", + "type": "array", + "items": { + "$ref": "#/components/schemas/ShareGrant" + } + }, + "org_access": { + "$ref": "#/components/schemas/OrgAccess" + } + } + }, + "DashboardPatch": { + "description": "Fields that can be partially updated on a dashboard", + "allOf": [ + { + "$ref": "#/components/schemas/AccessControlUpdate" + }, + { + "type": "object", + "properties": { + "title": { + "type": "string", + "example": "Employee Dashboard" + }, + "tiles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DashboardTile" } } - ] + } } - }, - "required": [ - "title", - "tiles" ] }, - "DashboardTileID": { - "description": "Unique identifier for a tile in a dashboard", + "InsightID": { + "description": "Unique identifier for an insight (a saved chart / visualisation)", "type": "string", "format": "uuid", - "example": "e4af1297-1fd6-440f-9846-f475f580d40f" + "example": "8d2e1c7a-3b4f-4a2e-9c1d-2f3a4b5c6d7e" + }, + "Insight": { + "description": "An insight is a saved, reusable chart definition (a visualisation plus its query/options).\nInsights exist independently of dashboards: they can be created, shared and edited on their\nown, and referenced by one or more dashboard tiles via `insight_id`.\n", + "allOf": [ + { + "$ref": "#/components/schemas/AccessControl" + }, + { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/InsightID" + }, + "name": { + "description": "Human readable name of the insight", + "type": "string", + "example": "Opportunities created by journeys every month" + }, + "description": { + "type": "string", + "example": "Monthly count of opportunities grouped by source journey" + }, + "visualisation_id": { + "$ref": "#/components/schemas/VisualisationId" + }, + "visualisation_config": { + "$ref": "#/components/schemas/VisualisationConfig" + }, + "tags": { + "$ref": "#/components/schemas/Tags" + } + }, + "required": [ + "name", + "visualisation_id" + ] + } + ] + }, + "InsightPatch": { + "description": "Fields that can be partially updated on an insight", + "allOf": [ + { + "$ref": "#/components/schemas/AccessControlUpdate" + }, + { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "visualisation_id": { + "$ref": "#/components/schemas/VisualisationId" + }, + "visualisation_config": { + "$ref": "#/components/schemas/VisualisationConfig" + }, + "tags": { + "$ref": "#/components/schemas/Tags" + } + } + } + ] + }, + "Tags": { + "description": "Free-form labels for grouping and filtering insights", + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "revenue", + "marketing" + ] + }, + "Pagination": { + "description": "Offset-based pagination metadata for list responses", + "type": "object", + "properties": { + "total": { + "description": "Total number of results matching the query (ignoring limit/offset)", + "type": "integer" + }, + "limit": { + "description": "Page size used for this response", + "type": "integer" + }, + "offset": { + "description": "Number of results skipped before this page", + "type": "integer" + }, + "has_more": { + "description": "Whether more results exist beyond this page", + "type": "boolean" + } + }, + "required": [ + "total", + "limit", + "offset", + "has_more" + ] }, "DashboardTile": { "type": "object", + "description": "A positioned tile on a dashboard canvas. A tile references a saved insight via\n`insight_id`. Inline `visualisation_id`/`visualisation_config` remain supported for\nbackwards compatibility (ad-hoc tiles that are not backed by a shared insight).\n", "properties": { "id": { "$ref": "#/components/schemas/DashboardTileID" @@ -328,6 +990,10 @@ "type": "string", "example": "Number of opportunities created by journeys every month" }, + "insight_id": { + "description": "Reference to a saved insight rendered by this tile", + "$ref": "#/components/schemas/InsightID" + }, "visualisation_id": { "$ref": "#/components/schemas/VisualisationId" }, @@ -461,6 +1127,211 @@ } } }, + "parameters": { + "Search": { + "name": "q", + "in": "query", + "required": false, + "description": "Free-text search over name/title and description (case-insensitive substring).", + "schema": { + "type": "string" + } + }, + "VisualisationIdFilter": { + "name": "visualisation_id", + "in": "query", + "required": false, + "description": "Filter insights by one or more visualisation ids.", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VisualisationId" + } + } + }, + "TagsFilter": { + "name": "tags", + "in": "query", + "required": false, + "description": "Filter by one or more tags (see tags_match for any/all semantics).", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "TagsMatch": { + "name": "tags_match", + "in": "query", + "required": false, + "description": "Whether a result must match any (default) or all of the given tags.", + "schema": { + "type": "string", + "enum": [ + "any", + "all" + ], + "default": "any" + } + }, + "CreatedBy": { + "name": "created_by", + "in": "query", + "required": false, + "description": "Filter by the id of the creating user.", + "schema": { + "type": "string" + } + }, + "CreatedAfter": { + "name": "created_after", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } + }, + "CreatedBefore": { + "name": "created_before", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } + }, + "UpdatedAfter": { + "name": "updated_after", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } + }, + "UpdatedBefore": { + "name": "updated_before", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } + }, + "SharedWith": { + "name": "shared_with", + "in": "query", + "required": false, + "description": "Filter to resources shared with any of the given user ids.", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "Owner": { + "name": "owner", + "in": "query", + "required": false, + "description": "Filter to resources owned by any of the given user ids.", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "AccessibleTo": { + "name": "accessible_to", + "in": "query", + "required": false, + "description": "Filter to resources the given user id may view (owner, shared, org-wide or legacy).", + "schema": { + "type": "string" + } + }, + "Favorite": { + "name": "favorite", + "in": "query", + "required": false, + "description": "Dashboards only. Filter to only (`true`) or exclude (`false`) the requesting user's\nfavorited dashboards. A user with zero favorites and `favorite=true` gets an empty page,\nnot the unfiltered list.\n", + "schema": { + "type": "boolean" + } + }, + "FavoritesFirst": { + "name": "favorites_first", + "in": "query", + "required": false, + "description": "Dashboards only. No filtering — sorts the requesting user's favorited dashboards first,\npreserving the existing secondary ordering (`sort`/`order`) otherwise.\n", + "schema": { + "type": "boolean" + } + }, + "Sort": { + "name": "sort", + "in": "query", + "required": false, + "description": "Field to sort by. `name` and `title` are aliases for the resource's display name and are normalised per resource: dashboards sort by `title` and insights by `name`, whichever of the two values is sent. String sorts are case-insensitive.", + "schema": { + "type": "string", + "enum": [ + "created_at", + "updated_at", + "name", + "title" + ], + "default": "updated_at" + } + }, + "Order": { + "name": "order", + "in": "query", + "required": false, + "description": "Sort direction. Defaults to ascending (preserving the pre-migration order).", + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "default": "asc" + } + }, + "Limit": { + "name": "limit", + "in": "query", + "required": false, + "description": "Maximum results to return (max 200). Omit to return all matching results (pagination is opt-in).", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 200 + } + }, + "Offset": { + "name": "offset", + "in": "query", + "required": false, + "description": "Number of results to skip, for pagination. Use with limit (page N = offset N*limit).", + "schema": { + "type": "integer", + "minimum": 0, + "default": 0 + } + } + }, "securitySchemes": { "EpilotAuth": { "type": "http", diff --git a/packages/cli/definitions/deduplication.json b/packages/cli/definitions/deduplication.json index 469d61cec..56dcc1a3c 100644 --- a/packages/cli/definitions/deduplication.json +++ b/packages/cli/definitions/deduplication.json @@ -102,6 +102,222 @@ } } } + }, + "/v1/detect-duplicates": { + "post": { + "operationId": "detectDuplicates", + "summary": "detectDuplicates", + "description": "Detects potential duplicate entities for the given entity using the schema's prioritized uniqueness rules. Returns matches with a confidence score.", + "tags": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DetectDuplicatesRequestBody" + } + } + } + }, + "responses": { + "200": { + "description": "Potential duplicates ranked by rule priority", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DetectDuplicatesResponse" + } + } + } + } + } + } + }, + "/v1/duplicates/dismiss": { + "post": { + "operationId": "dismissDuplicates", + "summary": "dismissDuplicates", + "description": "Confirms entities as NOT duplicates: clears the internal duplicate-detection flags (_matching_entities) on each given entity, so they stop appearing as open duplicate sets. The records themselves are not modified otherwise.", + "tags": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DismissDuplicatesRequestBody" + } + } + } + }, + "responses": { + "200": { + "description": "Flags cleared", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DismissDuplicatesResponse" + } + } + } + } + } + } + }, + "/v1/uniqueness-criteria": { + "get": { + "operationId": "listUniquenessCriteria", + "summary": "listUniquenessCriteria", + "description": "Lists UniquenessCriteria for the requesting organization. Optionally filtered by schema.", + "tags": [], + "parameters": [ + { + "name": "schema", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Filter results to a specific entity schema." + } + ], + "responses": { + "200": { + "description": "Array of UniquenessCriteria records", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UniquenessCriteriaListResponse" + } + } + } + } + } + }, + "post": { + "operationId": "createUniquenessCriteria", + "summary": "createUniquenessCriteria", + "description": "Creates a new UniquenessCriteria record.", + "tags": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UniquenessCriteriaCreateBody" + } + } + } + }, + "responses": { + "201": { + "description": "Created record", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UniquenessCriteria" + } + } + } + } + } + } + }, + "/v1/uniqueness-criteria/{schema}": { + "get": { + "operationId": "getUniquenessCriteria", + "summary": "getUniquenessCriteria", + "description": "Fetch a single UniquenessCriteria record.", + "tags": [], + "parameters": [ + { + "name": "schema", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Entity schema slug." + } + ], + "responses": { + "200": { + "description": "The criteria record", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UniquenessCriteria" + } + } + } + }, + "404": { + "description": "Record not found" + } + } + }, + "put": { + "operationId": "updateUniquenessCriteria", + "summary": "updateUniquenessCriteria", + "description": "Replace the matchRules on an existing UniquenessCriteria record.", + "tags": [], + "parameters": [ + { + "name": "schema", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Entity schema slug." + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UniquenessCriteriaUpdateBody" + } + } + } + }, + "responses": { + "200": { + "description": "Updated record", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UniquenessCriteria" + } + } + } + }, + "404": { + "description": "Record not found" + } + } + }, + "delete": { + "operationId": "deleteUniquenessCriteria", + "summary": "deleteUniquenessCriteria", + "description": "Delete a UniquenessCriteria record.", + "tags": [], + "parameters": [ + { + "name": "schema", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Entity schema slug." + } + ], + "responses": { + "204": { + "description": "Record deleted" + }, + "404": { + "description": "Record not found" + } + } + } } }, "components": { @@ -120,26 +336,41 @@ "type": "object", "properties": { "toKeep": { - "type": "string" + "type": "string", + "minLength": 1, + "description": "Entity id to keep and merge the duplicates into" }, "toDelete": { "type": "array", "items": { - "type": "string" - } + "type": "string", + "minLength": 1 + }, + "minItems": 1, + "description": "Non-empty list of duplicate entity ids to merge into toKeep and delete" } }, "required": [ "toKeep", "toDelete" ] - } + }, + "minItems": 1 }, "DeduplicateRequestResponse": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Entity" - } + "type": "object", + "properties": { + "deduplicatedEntities": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Entity" + }, + "description": "The merged survivor entity of each processed set" + } + }, + "required": [ + "deduplicatedEntities" + ] }, "Entity": { "type": "object", @@ -303,6 +534,243 @@ "updatedAt" ], "description": "Represents an async deduplication job" + }, + "DetectDuplicatesRequestBody": { + "type": "object", + "properties": { + "schema": { + "type": "string", + "minLength": 1, + "description": "Entity schema to search (e.g. 'contact'). Must have UniquenessCriteria configured for the calling org." + }, + "entity": { + "type": "object", + "additionalProperties": {}, + "description": "The entity to look up potential duplicates for. Attribute values are extracted from this entity per the schema's match rules; system fields such as '_id' are supported. A rule is evaluated when at least one of its attributes resolves to a non-empty value on this entity; attributes without a value require candidates to also lack a value for them." + } + }, + "required": [ + "schema", + "entity" + ] + }, + "DetectDuplicatesResponse": { + "type": "object", + "properties": { + "matches": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DetectedDuplicateMatch" + } + } + }, + "required": [ + "matches" + ] + }, + "DetectedDuplicateMatch": { + "type": "object", + "properties": { + "entity": { + "$ref": "#/components/schemas/Entity" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score for the match, between 0 and 1." + }, + "matched_attributes": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "description": "Names of the attributes that matched on the rule that produced this hit." + } + }, + "required": [ + "entity", + "confidence", + "matched_attributes" + ] + }, + "DismissDuplicatesRequestBody": { + "type": "object", + "properties": { + "schema": { + "type": "string", + "minLength": 1 + }, + "entityIds": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "minItems": 1, + "maxItems": 100, + "description": "Entities whose duplicate flags (_matching_entities) are cleared — the records are confirmed as NOT duplicates and stay separate" + } + }, + "required": [ + "schema", + "entityIds" + ] + }, + "DismissDuplicatesResponse": { + "type": "object", + "properties": { + "dismissed": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Entity ids whose duplicate flags were cleared" + } + }, + "required": [ + "dismissed" + ] + }, + "UniquenessCriteriaListResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UniquenessCriteria" + } + } + }, + "required": [ + "items" + ] + }, + "UniquenessCriteria": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid", + "description": "Unique identifier of the criteria record." + }, + "orgId": { + "type": "string", + "description": "Organization the criteria belong to." + }, + "schema": { + "type": "string", + "minLength": 1, + "description": "Entity schema these criteria apply to (e.g. 'contact')." + }, + "matchRules": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MatchRule" + }, + "minItems": 1, + "description": "Ordered list of match rules. Order is the evaluation priority." + }, + "createdAt": { + "type": "string", + "description": "ISO 8601 timestamp of record creation.", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "description": "ISO 8601 timestamp of last update.", + "format": "date-time" + } + }, + "required": [ + "id", + "orgId", + "schema", + "matchRules", + "createdAt", + "updatedAt" + ], + "description": "Defines what makes an entity of a given schema unique within an organization." + }, + "MatchRule": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 100, + "description": "Optional human-readable label for the rule. Purely descriptive; not used during matching." + }, + "attributes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MatchAttribute" + }, + "minItems": 1, + "description": "Attributes that must all match for this rule to fire. Attributes without a value on the source entity must also have no value on a matching candidate." + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence assigned to matches produced by this rule, between 0 and 1." + } + }, + "required": [ + "attributes", + "confidence" + ], + "description": "One way to identify the same entity. Evaluated in order; first rule whose attributes are all available and that returns hits wins." + }, + "MatchAttribute": { + "type": "object", + "properties": { + "attribute": { + "type": "string", + "minLength": 1, + "description": "Name of the entity attribute to match on. Its query path is resolved from the entity schema at query time." + } + }, + "required": [ + "attribute" + ], + "description": "One attribute participating in a match rule. Wrapped as an object so per-attribute options can be added later." + }, + "UniquenessCriteriaCreateBody": { + "type": "object", + "properties": { + "schema": { + "type": "string", + "minLength": 1, + "description": "Entity schema these criteria apply to." + }, + "matchRules": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MatchRule" + }, + "minItems": 1 + } + }, + "required": [ + "schema", + "matchRules" + ] + }, + "UniquenessCriteriaUpdateBody": { + "type": "object", + "properties": { + "matchRules": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MatchRule" + }, + "minItems": 1 + } + }, + "required": [ + "matchRules" + ] } } }, diff --git a/packages/cli/definitions/design.json b/packages/cli/definitions/design.json index a09099d7b..2e4c25182 100644 --- a/packages/cli/definitions/design.json +++ b/packages/cli/definitions/design.json @@ -302,6 +302,71 @@ } } }, + "/v1/designs/{designId}/duplicate": { + "post": { + "operationId": "duplicateDesign", + "summary": "duplicateDesign", + "description": "Duplicate an existing design owned by the user organization. The copy carries over the style, custom CSS and name (marked as a copy) but does NOT carry over journey/portal relations, so the new design starts with 0 consumers, exactly like a freshly created design.", + "tags": [ + "design-builder" + ], + "parameters": [ + { + "in": "path", + "name": "designId", + "required": true, + "schema": { + "$ref": "#/components/schemas/DesignId" + } + } + ], + "responses": { + "201": { + "description": "Success - design duplicated with success.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddDesignRes" + } + } + } + }, + "400": { + "description": "Validation Errors", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + } + }, + "401": { + "description": "Authentication Errors", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + } + }, + "404": { + "description": "No design found" + }, + "500": { + "description": "Other errors", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + } + } + } + } + }, "/v1/designs/{designId}/parse": { "get": { "operationId": "getThemeFromDesign", diff --git a/packages/cli/definitions/document.json b/packages/cli/definitions/document.json index 40d2cfb61..725a4f929 100644 --- a/packages/cli/definitions/document.json +++ b/packages/cli/definitions/document.json @@ -26,6 +26,7 @@ "Documents" ], "requestBody": { + "required": true, "content": { "application/json": { "schema": { @@ -102,6 +103,7 @@ "partial_generation", "full_generation" ], + "description": "- partial_generation: Generates a partial document for user validation before final generation\n- full_generation: Completes the entire document generation process in one step\n", "default": "full_generation" } }, @@ -115,6 +117,7 @@ "open", "download" ], + "description": "- open: Preview URL opens the file directly in browser\n- download: Preview URL triggers a download of the file\n", "default": "open" } } @@ -123,6 +126,7 @@ "Documents" ], "requestBody": { + "required": true, "content": { "application/json": { "schema": { @@ -154,6 +158,7 @@ "Documents" ], "requestBody": { + "required": true, "content": { "application/json": { "schema": { @@ -175,6 +180,68 @@ } } } + }, + "/v2/templates:validate": { + "post": { + "operationId": "validateTemplate", + "summary": "validateTemplate", + "description": "Validates a document template's variable syntax and, optionally, proposes a hotfixed copy of it.\n\nThe endpoint compiles the template with docxtemplater's core parser (the xlsx module for\nspreadsheets), so a delimiter or tag error reported here is one generation would fail on.\nFailures specific to the image or HTML modules are not covered. Every problem it can repair\nunambiguously — a missing brace, an extra brace, a smart quote or a non-breaking space that\nWord substituted inside a tag — is applied to a **copy** of the template, which is uploaded\nand returned as `fixed_document`.\n\nThe original template is never modified. Accepting the fix is an explicit, separate step:\nthe caller shows the user `issues` (each with its `before`/`after`), lets them download\n`fixed_document.preview_url`, and only then replaces the template.\n\nSupported input document types:\n- .docx, .docm, .dotx\n- .xlsx, .xlsm\n- .pptx\n", + "tags": [ + "Documents" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TemplateValidationRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Validation result, with the hotfixed template when one could be produced", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TemplateValidationResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "413": { + "description": "Template larger than the validator accepts (25 MB)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "415": { + "description": "Unsupported media type", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + } + } + } } }, "components": { @@ -330,15 +397,162 @@ } }, "ErrorCode": { - "description": "Error codes for document generation:\n- PARSE_ERROR - Error while parsing the document. Normally related with a bad template using the wrong DocxTemplater syntax.\n- DOC_TO_PDF_CONVERT_ERROR - Error while converting the document to PDF. Normally related with a ConvertAPI failure.\n- INTERNAL_ERROR - Internal error. Please contact support.\n- INVALID_TEMPLATE_FORMAT - Invalid template format (only .docx is supported). This can happen due to a bad word file or an unsupported file extension.\n", + "description": "Error codes for document generation:\n- PARSE_ERROR - Error while parsing the document. Normally related with a bad template using the wrong DocxTemplater syntax.\n- DOC_TO_PDF_CONVERT_ERROR - Error while converting the document to PDF. Normally related with a ConvertAPI failure.\n- INTERNAL_ERROR - Internal error. Please contact support.\n- INVALID_TEMPLATE_FORMAT - Invalid template format (only .docx is supported). This can happen due to a bad word file or an unsupported file extension.\n- TEMPLATE_NOT_FOUND - Template file was not found in S3. This indicates the template was likely deleted.\n", "type": "string", "enum": [ "PARSE_ERROR", "DOC_TO_PDF_CONVERT_ERROR", "INTERNAL_ERROR", - "INVALID_TEMPLATE_FORMAT" + "INVALID_TEMPLATE_FORMAT", + "TEMPLATE_NOT_FOUND" ] }, + "TemplateValidationRequest": { + "type": "object", + "required": [ + "template_document" + ], + "properties": { + "template_document": { + "description": "Input template document", + "type": "object", + "required": [ + "s3ref" + ], + "properties": { + "filename": { + "type": "string", + "description": "Document original filename, used to name the fixed copy", + "example": "Umzugsmeldung.xlsx" + }, + "s3ref": { + "$ref": "#/components/schemas/S3Reference" + } + } + }, + "fix": { + "description": "Attempt to produce a hotfixed copy of the template. When false, the template is only\ninspected and no file is written.\n", + "type": "boolean", + "default": true + }, + "fix_level": { + "description": "How far the hotfix may go:\n- safe - only repairs where the author's intent is unambiguous from the syntax\n (unbalanced or duplicated braces, editor-substituted characters inside a tag).\n- aggressive - additionally promotes single-brace placeholders such as `{contact.name}`\n to `{{contact.name}}`. These are common in customer templates but indistinguishable\n from prose that uses braces, so they are reported with `confidence: low`.\n", + "type": "string", + "enum": [ + "safe", + "aggressive" + ], + "default": "safe" + } + } + }, + "TemplateValidationResponse": { + "type": "object", + "properties": { + "valid": { + "description": "Whether the template compiled cleanly before any repair was attempted", + "type": "boolean", + "example": false + }, + "fixed": { + "description": "Whether a corrected copy of the template could be produced", + "type": "boolean", + "example": true + }, + "issues": { + "description": "Everything found in the template, whether or not it could be repaired", + "type": "array", + "items": { + "$ref": "#/components/schemas/TemplateIssue" + } + }, + "unresolved_errors": { + "description": "Parser errors that remain after the hotfix (or the original errors when nothing was\nfixed). Empty when the template is valid.\n", + "type": "array", + "items": { + "$ref": "#/components/schemas/DocxTemplaterErrorDetail" + } + }, + "fixed_document": { + "description": "The corrected copy, for the user to review and accept. Absent when no repair was\napplied. The original template is left untouched.\n", + "type": "object", + "properties": { + "s3ref": { + "$ref": "#/components/schemas/S3Reference" + }, + "filename": { + "type": "string", + "example": "Umzugsmeldung (fixed).xlsx" + }, + "preview_url": { + "description": "Short-lived download link for the corrected copy", + "type": "string", + "format": "uri" + } + } + } + } + }, + "TemplateIssue": { + "type": "object", + "description": "A single template syntax problem, and the repair proposed for it", + "properties": { + "id": { + "description": "Problem kind. Reuses docxtemplater's error vocabulary where one exists\n(`unopened_tag`, `unclosed_tag`, `duplicate_open_tag`, `duplicate_close_tag`), plus\n`malformed_tag` for single-brace placeholders and `typographic_characters` for\neditor-substituted characters inside an otherwise valid tag.\n", + "type": "string", + "example": "unopened_tag" + }, + "file": { + "description": "Zip part the problem was found in", + "type": "string", + "example": "xl/sharedStrings.xml" + }, + "location": { + "description": "Where the user can find it — a cell reference for spreadsheets, a part label\n(`Document body`, `Header 1`, `Slide 3`) otherwise.\n", + "type": "string", + "example": "Tabelle1!N4" + }, + "context": { + "description": "The offending text with surrounding context", + "type": "string", + "example": "…Datum: {system.date}} Unterschrift…" + }, + "explanation": { + "description": "Why the template parser rejects it", + "type": "string", + "example": "The tag is missing an opening brace." + }, + "fixable": { + "description": "Whether the proposed repair was applied to the fixed copy", + "type": "boolean", + "example": true + }, + "confidence": { + "description": "How sure we are that the repair is what the author meant", + "type": "string", + "enum": [ + "high", + "medium", + "low" + ] + }, + "rule": { + "description": "The repair rule that produced `after`", + "type": "string", + "example": "balance_opening_delimiter" + }, + "before": { + "description": "The text as it is in the template", + "type": "string", + "example": "{system.date}}" + }, + "after": { + "description": "The text as it would be in the fixed copy", + "type": "string", + "example": "{{system.date}}" + } + } + }, "TemplateSettings": { "type": "object", "description": "Template Settings for document generation", @@ -404,7 +618,7 @@ "description": "The file entity id, used when persisting a new template version with updated settings", "type": "string", "format": "uuid", - "example": "1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p" + "example": "123e4567-e89b-12d3-a456-426614174000" } } }, @@ -510,7 +724,7 @@ "user_id": { "type": "string", "description": "User Id for variable context", - "example": 100321 + "example": "100321" }, "language": { "type": "string", diff --git a/packages/cli/definitions/email-settings.json b/packages/cli/definitions/email-settings.json index 850e1e28b..b043c78a6 100644 --- a/packages/cli/definitions/email-settings.json +++ b/packages/cli/definitions/email-settings.json @@ -26,6 +26,10 @@ "name": "O365 Outlook Connection", "description": "Microsoft Office 365 Outlook integration endpoints. These endpoints enable organizations to:\n- Initiate OAuth connections with Microsoft 365\n- Connect Outlook shared mailboxes\n- Manage connection status and tokens\n- Disconnect integrations when needed\n\nThe integration supports multiple Azure AD tenants and handles token refresh automatically.\n" }, + { + "name": "Custom SMTP", + "description": "Custom SMTP connection management. Lets organizations configure their own outbound\nSMTP server (e.g. Mailgun, SendGrid, a self-hosted Postfix) for sending emails\nfrom epilot via their existing provider. Credentials are encrypted at rest using\na customer-managed KMS key. Only outbound sending is supported — replies do not\ncome back into epilot.\n" + }, { "name": "Channels", "description": "Channel integration endpoints. Channels represent external communication providers\n(e.g., Microsoft Teams, WhatsApp) that can be connected to an epilot organization.\n\nAll channel endpoints live under `/v2/channels/{channel}`.\n" @@ -610,6 +614,81 @@ } } }, + "/v2/outlook/calendar/admin-consent-status": { + "get": { + "operationId": "getCalendarAdminConsentStatus", + "summary": "getCalendarAdminConsentStatus", + "description": "Reports whether the caller's organization can connect personal Outlook\ncalendars.\n\nConnecting a personal calendar requires the tenant admin to have\nconsented to epilot's Microsoft 365 app. That consent is tenant-wide,\none-time, and granted for the full scope set (mail + calendar), so the\norg-level Outlook connection set up during onboarding is the gate.\n\nWhen consent is missing the response includes `admin_consent_url` for\nthe admin to open, and the UI should ask the user to involve their IT\ninstead of starting an OAuth flow that would fail.\n", + "tags": [ + "O365 Outlook Connection" + ], + "responses": { + "200": { + "description": "Admin consent status for the organization", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CalendarAdminConsentStatus" + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal server error" + } + } + } + }, + "/v2/outlook/calendar/me": { + "get": { + "operationId": "getMyCalendarConnection", + "summary": "getMyCalendarConnection", + "description": "Returns the calling user's personal Outlook calendar connection,\nor null when the user hasn't connected yet.\n\nPer-user calendar connections are distinct from the org-level\nshared-mailbox connection (`/v2/outlook/connection/status`): each\nuser connects their own delegated token because reading user A's\ncalendar requires user A's consent.\n", + "tags": [ + "O365 Outlook Connection" + ], + "responses": { + "200": { + "description": "Calendar connection (or null if absent)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MyCalendarConnectionResponse" + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal server error" + } + } + }, + "delete": { + "operationId": "disconnectMyCalendar", + "summary": "disconnectMyCalendar", + "description": "Removes the calling user's personal calendar connection.", + "tags": [ + "O365 Outlook Connection" + ], + "responses": { + "204": { + "description": "Disconnected" + }, + "404": { + "description": "No connection to disconnect" + }, + "500": { + "description": "Internal server error" + } + } + } + }, "/v2/outlook/connection/disconnect": { "post": { "operationId": "disconnectOutlook", @@ -817,6 +896,28 @@ "description": "Shared inbox ID to associate with the mailbox. Defaults to the default shared inbox.", "default": "default" }, + "name": { + "type": "string", + "description": "Sender name to use for the Outlook mailbox email address." + }, + "user_ids": { + "type": "array", + "items": { + "type": "string" + }, + "description": "User IDs for which this email address should be available by default." + }, + "group_ids": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Group IDs for which this email address should be available by default." + }, + "default_signature_id": { + "type": "string", + "description": "Default signature ID to use for this Outlook mailbox email address." + }, "mailboxSyncTimeframe": { "type": "string", "enum": [ @@ -973,7 +1074,11 @@ ], "properties": { "timeframe": { - "$ref": "#/components/schemas/MailboxSyncTimeframePeriods", + "allOf": [ + { + "$ref": "#/components/schemas/MailboxSyncTimeframePeriods" + } + ], "description": "Sync period:\n- 5m: last 5 minutes (quick sync)\n- 1w: 1 week\n- 2w: 2 weeks\n- 1m: 1 month\n" } } @@ -1160,40 +1265,452 @@ "summary": "getConnectedOutlookEmails", "description": "Returns all Outlook email addresses connected to the organization.\n", "tags": [ - "O365 Outlook Connection" + "O365 Outlook Connection" + ], + "responses": { + "200": { + "description": "Outlook emails retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "outlook_emails", + "count" + ], + "properties": { + "outlook_emails": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConnectedOutlookEmail" + } + }, + "count": { + "type": "integer", + "description": "Number of Outlook emails" + } + } + } + } + } + }, + "400": { + "description": "Bad request - missing organization ID" + }, + "500": { + "description": "Internal server error" + } + } + } + }, + "/v2/smtp/connections": { + "get": { + "operationId": "listSmtpConnections", + "summary": "listSmtpConnections", + "description": "Returns all custom SMTP connections configured for the organization.\nPasswords are never returned.\n", + "tags": [ + "Custom SMTP" + ], + "responses": { + "200": { + "description": "SMTP connections listed", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "connections", + "has_connections" + ], + "properties": { + "connections": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SmtpConnection" + } + }, + "has_connections": { + "type": "boolean" + } + } + } + } + } + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + }, + "post": { + "operationId": "createSmtpConnection", + "summary": "createSmtpConnection", + "description": "Creates a new custom SMTP connection. Runs a live verify against the SMTP server\nbefore persisting; on failure the request is rejected and nothing is saved. Pass\n`skip_test=true` to store the configuration untested instead.\n", + "tags": [ + "Custom SMTP" + ], + "parameters": [ + { + "name": "skip_test", + "in": "query", + "required": false, + "description": "Save without running the live verify. Use when the server is not reachable from\nepilot yet, or when a referenced `{{ env.* }}` secret has not been set. The\nconnection is stored untested — `last_test_status` is left unset rather than\nclaimed as `ok` — and env references are not resolved either, since an\nunresolvable one is expected in this case.\n", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSmtpConnectionRequest" + } + } + } + }, + "responses": { + "201": { + "description": "SMTP connection created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SmtpConnection" + } + } + } + }, + "400": { + "description": "Validation error, SMTP test failed, or refused for security reasons\n(private IP, plaintext on non-localhost host, invalid TLS).\n" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "409": { + "$ref": "#/components/responses/Conflict" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/v2/smtp/connections/{connectionId}": { + "parameters": [ + { + "name": "connectionId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "get": { + "operationId": "getSmtpConnection", + "summary": "getSmtpConnection", + "description": "Returns a single custom SMTP connection by id. The password is never returned.", + "tags": [ + "Custom SMTP" + ], + "responses": { + "200": { + "description": "SMTP connection retrieved", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SmtpConnection" + } + } + } + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + }, + "put": { + "operationId": "updateSmtpConnection", + "summary": "updateSmtpConnection", + "description": "Partial update; omitted fields keep their existing values. The merged\nconfiguration is verified against the SMTP server before persisting. Pass\n`skip_test=true` to store it untested instead, which also clears any previous\ntest result — it described a configuration that no longer applies.\n", + "tags": [ + "Custom SMTP" + ], + "parameters": [ + { + "name": "skip_test", + "in": "query", + "required": false, + "description": "Save without running the live verify. Use when the server is not reachable from\nepilot yet, or when a referenced `{{ env.* }}` secret has not been set. The\nconnection is stored untested — `last_test_status` is left unset rather than\nclaimed as `ok` — and env references are not resolved either, since an\nunresolvable one is expected in this case.\n", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateSmtpConnectionRequest" + } + } + } + }, + "responses": { + "200": { + "description": "SMTP connection updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SmtpConnection" + } + } + } + }, + "400": { + "description": "Validation error or SMTP test failed" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + } + } + }, + "delete": { + "operationId": "deleteSmtpConnection", + "summary": "deleteSmtpConnection", + "description": "Deletes a custom SMTP connection. Messages already sent are unaffected.", + "tags": [ + "Custom SMTP" + ], + "responses": { + "200": { + "description": "SMTP connection deleted", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "connection_id": { + "type": "string" + } + } + } + } + } + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + } + }, + "/v2/smtp/connections/{connectionId}/test": { + "parameters": [ + { + "name": "connectionId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "post": { + "operationId": "testSmtpConnection", + "summary": "testSmtpConnection", + "description": "Re-runs a live SMTP verify against the saved configuration (EHLO + AUTH + NOOP + QUIT)\nand updates `last_test_status` / `last_tested_at` on the connection.\n", + "tags": [ + "Custom SMTP" + ], + "responses": { + "200": { + "description": "SMTP test executed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SmtpTestResult" + } + } + } + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + } + }, + "/v2/smtp/senders": { + "get": { + "operationId": "listSmtpSenders", + "summary": "listSmtpSenders", + "description": "Returns every address registered to send through a custom SMTP connection.", + "tags": [ + "Custom SMTP" + ], + "responses": { + "200": { + "description": "SMTP senders listed", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "senders" + ], + "properties": { + "senders": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SmtpSender" + } + } + } + } + } + } + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + }, + "post": { + "operationId": "connectSmtpSender", + "summary": "connectSmtpSender", + "description": "Registers an address as a sender on a custom SMTP connection:\n 1. Creates the email address so it can be picked in the composer\n 2. Binds it to the connection, so outgoing mail from it is routed there\nThe SMTP server still has the final say on which From addresses it accepts.\n", + "tags": [ + "Custom SMTP" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConnectSmtpSenderRequest" + } + } + } + }, + "responses": { + "201": { + "description": "Sender connected", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "email_address", + "sender" + ], + "properties": { + "email_address": { + "$ref": "#/components/schemas/EmailAddressResponse" + }, + "sender": { + "$ref": "#/components/schemas/SmtpSender" + } + } + } + } + } + }, + "400": { + "description": "Validation error" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "description": "The referenced SMTP connection does not exist" + }, + "409": { + "description": "The address is already registered, here or in another organization" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/v2/smtp/senders/{email}": { + "parameters": [ + { + "name": "email", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "email" + } + } + ], + "delete": { + "operationId": "disconnectSmtpSender", + "summary": "disconnectSmtpSender", + "description": "Removes a sender address: deletes the email address and its binding to the SMTP\nconnection. The connection itself and messages already sent are unaffected.\n", + "tags": [ + "Custom SMTP" ], "responses": { "200": { - "description": "Outlook emails retrieved successfully", + "description": "Sender disconnected", "content": { "application/json": { "schema": { "type": "object", "required": [ - "outlook_emails", - "count" + "success", + "email" ], "properties": { - "outlook_emails": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ConnectedOutlookEmail" - } + "success": { + "type": "boolean" }, - "count": { - "type": "integer", - "description": "Number of Outlook emails" + "email": { + "type": "string", + "format": "email" } } } } } }, - "400": { - "description": "Bad request - missing organization ID" + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" }, "500": { - "description": "Internal server error" + "$ref": "#/components/responses/InternalServerError" } } } @@ -1287,6 +1804,14 @@ "schema": { "type": "string" } + }, + { + "name": "clientdata", + "in": "query", + "required": false, + "schema": { + "type": "string" + } } ], "responses": { @@ -2078,6 +2603,12 @@ "calendar": { "type": "boolean", "description": "Request 'calendar' specific scopes." + }, + "return_to": { + "type": "string", + "maxLength": 512, + "pattern": "^/[^/\\\\].*$", + "description": "Optional same-origin frontend path to redirect to after the OAuth\nround-trip. Must start with a single \"/\" — protocol-relative\nand absolute URLs are rejected.\n" } } }, @@ -2644,6 +3175,89 @@ } } }, + "CalendarAdminConsentStatus": { + "type": "object", + "required": [ + "admin_consented" + ], + "properties": { + "admin_consented": { + "type": "boolean", + "description": "True when the tenant admin has consented to epilot's Microsoft 365\napp, so users in this organization can connect personal calendars.\n" + }, + "admin_consent_url": { + "type": "string", + "format": "uri", + "description": "Microsoft admin-consent URL for an administrator to open. Present\nonly when `admin_consented` is false.\n" + } + } + }, + "MyCalendarConnectionResponse": { + "type": "object", + "description": "Per-user calendar connection metadata, or null when the caller has\nnot connected their personal calendar yet.\n", + "properties": { + "connection": { + "allOf": [ + { + "$ref": "#/components/schemas/CalendarConnectionInfo" + } + ], + "nullable": true + } + } + }, + "CalendarConnectionInfo": { + "type": "object", + "required": [ + "user_id", + "tenant_id", + "status" + ], + "properties": { + "user_id": { + "type": "string", + "description": "Azure AD Object ID of the connected user" + }, + "tenant_id": { + "type": "string", + "description": "Microsoft Azure AD tenant ID" + }, + "status": { + "type": "string", + "enum": [ + "connected", + "expired" + ], + "description": "Token health for this calendar connection" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Granted permission scopes on the user's token" + }, + "connected_by_display_name": { + "type": "string" + }, + "connected_by_email": { + "type": "string", + "format": "email" + }, + "connected_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "expires_at": { + "type": "string", + "format": "date-time" + } + } + }, "OutlookConnectionStatus": { "type": "object", "required": [ @@ -2726,6 +3340,269 @@ } } }, + "SmtpConnection": { + "type": "object", + "required": [ + "connection_id", + "smtp_host", + "smtp_port", + "smtp_secure", + "smtp_username" + ], + "properties": { + "connection_id": { + "type": "string", + "description": "Stable identifier for this SMTP connection" + }, + "smtp_host": { + "type": "string", + "description": "Hostname of the SMTP server (e.g. smtp.mailgun.org)" + }, + "smtp_port": { + "type": "string", + "description": "SMTP port (typically 587 for STARTTLS or 465 for TLS). A string, because it\nmay hold a `{{ env.your_key }}` reference rather than a literal port.\n" + }, + "smtp_secure": { + "type": "string", + "enum": [ + "tls", + "starttls" + ], + "description": "Connection security mode. Plaintext SMTP is not offered: it would put the\nSMTP password on the wire in the clear.\n- tls: Implicit TLS (port 465)\n- starttls: STARTTLS upgrade (port 587)\n" + }, + "smtp_username": { + "type": "string", + "description": "SMTP auth username" + }, + "smtp_password": { + "type": "string", + "description": "Only returned when the stored value is exactly one `{{ env.your_key }}`\nreference, which names a secret rather than being one — that lets the\nsettings UI edit the reference without dropping it. A literal password, or a\npartly templated one, is omitted entirely: submit an update without this\nfield to keep the stored value.\n" + }, + "connected_by_display_name": { + "type": "string" + }, + "connected_by_email": { + "type": "string", + "format": "email" + }, + "connected_by_user_id": { + "type": "string" + }, + "connected_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "last_tested_at": { + "type": "string", + "format": "date-time" + }, + "last_test_status": { + "type": "string", + "enum": [ + "ok", + "auth_failed", + "tls_failed", + "host_unreachable", + "timeout", + "secret_missing", + "unknown" + ] + }, + "last_test_error": { + "type": "string", + "description": "Latest test error message, if any" + } + } + }, + "CreateSmtpConnectionRequest": { + "type": "object", + "required": [ + "smtp_host", + "smtp_port", + "smtp_secure", + "smtp_username", + "smtp_password" + ], + "properties": { + "smtp_host": { + "type": "string" + }, + "smtp_port": { + "description": "A port between 1 and 65535, or a `{{ env.your_key }}` template resolving to\none. An integer is still accepted, so callers predating the template support\nkeep working.\n", + "oneOf": [ + { + "type": "string" + }, + { + "type": "integer", + "minimum": 1, + "maximum": 65535 + } + ] + }, + "smtp_secure": { + "type": "string", + "enum": [ + "tls", + "starttls" + ] + }, + "smtp_username": { + "type": "string" + }, + "smtp_password": { + "type": "string", + "description": "May contain `{{ env.your_key }}` templates referencing the organization's\nenvironment variables (including secrets), resolved at send time. Stored as\nentered, so the settings UI edits the template rather than a resolved value.\n", + "example": "{{ env.smtp_password }}" + } + } + }, + "UpdateSmtpConnectionRequest": { + "type": "object", + "description": "Partial update; omitted fields keep their existing values.", + "properties": { + "smtp_host": { + "type": "string" + }, + "smtp_port": { + "description": "A port between 1 and 65535, or a `{{ env.your_key }}` template resolving to\none. An integer is still accepted, so callers predating the template support\nkeep working.\n", + "oneOf": [ + { + "type": "string" + }, + { + "type": "integer", + "minimum": 1, + "maximum": 65535 + } + ] + }, + "smtp_secure": { + "type": "string", + "enum": [ + "tls", + "starttls" + ] + }, + "smtp_username": { + "type": "string" + }, + "smtp_password": { + "type": "string" + } + } + }, + "SmtpSender": { + "type": "object", + "description": "An address allowed to send through a custom SMTP connection. Mirrors an Outlook\nmailbox: the address, its display name and its assignees live on the shared\nemail-address record, while this row binds it to a set of SMTP credentials.\n", + "required": [ + "email", + "connection_id" + ], + "properties": { + "email": { + "type": "string", + "format": "email", + "description": "The sender address. Stored lower-cased." + }, + "connection_id": { + "type": "string", + "description": "SMTP connection this address sends through" + }, + "reply_to_email": { + "type": "string", + "description": "Default Reply-To for messages from this address. Custom SMTP is send-only, so\npoint this at a mailbox someone actually monitors. May contain a\n`{{ env.your_key }}` template, so this is not constrained to `format: email`.\nA per-message Reply-To still wins.\n" + }, + "connected_at": { + "type": "string", + "format": "date-time" + }, + "connected_by_user_id": { + "type": "string" + } + } + }, + "ConnectSmtpSenderRequest": { + "type": "object", + "required": [ + "email", + "connection_id" + ], + "properties": { + "email": { + "type": "string", + "format": "email", + "description": "Address to send from. Must be one the SMTP server accepts as a From." + }, + "connection_id": { + "type": "string", + "description": "SMTP connection to send through" + }, + "name": { + "type": "string", + "description": "Sender name shown on outgoing messages." + }, + "reply_to_email": { + "type": "string", + "description": "Email address, or a `{{ env.your_key }}` template resolving to one." + }, + "shared_inbox_id": { + "type": "string", + "description": "Shared inbox to associate with the address. Defaults to the default shared inbox.", + "default": "default" + }, + "user_ids": { + "type": "array", + "items": { + "type": "string" + }, + "description": "User IDs for which this address should be available by default." + }, + "group_ids": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Group IDs for which this address should be available by default." + }, + "default_signature_id": { + "type": "string", + "description": "Default signature ID to use for this address." + } + } + }, + "SmtpTestResult": { + "type": "object", + "required": [ + "status", + "tested_at" + ], + "properties": { + "status": { + "type": "string", + "enum": [ + "ok", + "auth_failed", + "tls_failed", + "host_unreachable", + "timeout", + "secret_missing", + "unknown" + ] + }, + "error": { + "type": "string" + }, + "tested_at": { + "type": "string", + "format": "date-time" + } + } + }, "SignatureSetting": { "type": "string", "description": "Setting that allows to add a signature.", diff --git a/packages/cli/definitions/entity-mapping.json b/packages/cli/definitions/entity-mapping.json index 3f96c23c5..c56334355 100644 --- a/packages/cli/definitions/entity-mapping.json +++ b/packages/cli/definitions/entity-mapping.json @@ -763,6 +763,10 @@ "$ref": "#/components/schemas/EntityRef", "description": "A reference (id and schema) to the entity to be used as source." }, + "loop_ref": { + "$ref": "#/components/schemas/EntityRef", + "description": "Reference to the current iteration's entity when running inside a flow loop.\nWhen set, its fields and relations are merged into the source context;\non schema conflicts the loop entity wins, so paths like `._id` resolve\nto the iteration entity rather than to a same-schema relation of the source.\n" + }, "targets": { "type": "array", "items": { @@ -960,6 +964,11 @@ "type": "string" } }, + "use_uniqueness_criteria": { + "type": "boolean", + "default": false, + "description": "Execution wire flag set per automation by automation-workers: resolve the target entity via the organization's global uniqueness criteria (deduplication-api) instead of target_unique. Never persisted in stored mapping configs.\n" + }, "loop_config": { "type": "object", "description": "contains config in case of running in loop mode", @@ -968,10 +977,28 @@ "type": "string", "description": "path to the array from the entity payload" }, + "source_type": { + "type": "string", + "description": "Kind of journey block the loop iterates. 'journey-multi-select' (default) is a cards block with multi selection. 'journey-file-upload' is a file upload block: source_path points at the submission's `_files.$relation` array and `filter_tags` narrows it down to the block's files. Forwarded to the flow-level loop by the Automation Hub.\n", + "enum": [ + "journey-multi-select", + "journey-file-upload" + ], + "default": "journey-multi-select" + }, + "filter_tags": { + "type": "array", + "description": "Only iterate items of the array whose `_tags` contain every listed tag. Meant for relation arrays such as `submission._files.$relation`, where the journey tags each file relation with the upload block it came from. `##LOOP_INDEX##` keeps referring to the item's index in the unfiltered array.\n", + "items": { + "type": "string" + }, + "example": [ + "_hidden_ 2a4b1c3d-0000-4000-8000-000000000000 - Dokumente" + ] + }, "length": { "type": "number", - "description": "a hard limit of how many times the loop is allowed to run.", - "default": "the length of the array" + "description": "a hard limit of how many times the loop is allowed to run." } } }, @@ -1023,6 +1050,13 @@ "items": { "type": "string" } + }, + "graph_context": { + "type": "array", + "description": "Multi-hop entity graph lookups resolved before any mapping_attribute is evaluated. Each\nentry queries entity-api's `POST /v1/entity:graph` and merges every node's result into\nsourceContext under its own `graph.nodes[].id`, so mapping_attributes can `_copy`/`_template`\nfrom `.`. A graph node's value overwrites any existing sourceContext key of\nthe same name (a source entity field, a 1-hop relation, or another graph node).\n", + "items": { + "$ref": "#/components/schemas/GraphContextEntry" + } } }, "required": [ @@ -1093,6 +1127,13 @@ "description": "Include all relation tags (labels) present on the main entity relation", "default": false }, + "target_entity_tags": { + "type": "array", + "description": "Tags to add to the matched target entity's _tags array during mapping. Useful for assigning file collections to file entities.", + "items": { + "type": "string" + } + }, "override_with_source_filter": { "type": "boolean", "description": "Whether to override the relation source_filter with the specified one", @@ -1152,6 +1193,161 @@ "mode" ] }, + "GraphContextEntry": { + "type": "object", + "description": "One multi-hop graph lookup against entity-api's `POST /v1/entity:graph`, resolved during\ngraph_context enrichment (before mapping_attributes are evaluated). Every node in `graph.nodes`\nis merged into sourceContext under its own `id`, so listing several nodes here costs one\nentity-api call, not one per node.\n\nIf a node's `cardinality` is \"one\" (or it is the seed node), exactly one entity must be found\nfor it: zero or multiple matches fail the mapping execution instead of silently mapping\nmissing/wrong data. If \"many\" (the default), it resolves to an array - possibly empty - with no\nsuch failure. Each node's cardinality is validated independently.\n\n`seed.entity_id` and any `graph.nodes[].filter[].value` may contain `{{handlebars}}` placeholders\n(e.g. `{{contract._id}}`, `{{contract.origin_order}}`), resolved against the in-progress\nsourceContext (the source entity, its 1-hop relations, and any custom variables already\nresolved) before the graph query is sent.\n", + "properties": { + "seed": { + "$ref": "#/components/schemas/GraphSeed" + }, + "graph": { + "$ref": "#/components/schemas/GraphDefinition" + } + }, + "required": [ + "seed", + "graph" + ] + }, + "GraphSeed": { + "description": "Mirrors entity-api's GraphSeed (see entity-api openapi.yml) - the entity the graph traversal starts from.", + "type": "object", + "properties": { + "entity_id": { + "type": "string", + "description": "The id of the seed entity. Supports `{{handlebars}}` placeholders resolved against sourceContext.", + "example": "{{contract._id}}" + }, + "node_id": { + "type": "string", + "description": "The node ID in `graph.nodes` that corresponds to the seed entity.", + "example": "contact" + } + }, + "required": [ + "entity_id", + "node_id" + ] + }, + "GraphDefinition": { + "description": "Mirrors entity-api's GraphDefinition (see entity-api openapi.yml) - the shape of the graph to traverse.", + "type": "object", + "properties": { + "nodes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GraphNode" + } + }, + "edges": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GraphEdge" + } + } + }, + "required": [ + "nodes", + "edges" + ] + }, + "GraphNode": { + "description": "Mirrors entity-api's GraphNode (see entity-api openapi.yml).", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for this node in the graph definition.", + "example": "contact" + }, + "schema": { + "type": "string", + "description": "Entity schema slug for this node.", + "example": "contact" + }, + "cardinality": { + "type": "string", + "enum": [ + "one", + "many" + ], + "description": "\"one\": this node resolves to a single entity. \"many\" (default if unset): this node\nresolves to an array of entities.\n" + }, + "fields": { + "type": "array", + "description": "Optional entity fields to include in the hydrated response for this node.", + "items": { + "type": "string" + } + }, + "filter": { + "type": "array", + "description": "Narrows this node's traversal results to entities matching every filter (AND semantics).\nUseful for disambiguating among multiple entities reachable via the same graph edge.\n", + "items": { + "$ref": "#/components/schemas/GraphNodeFilter" + } + }, + "optional": { + "type": "boolean", + "description": "Only meaningful for a node expected to resolve to a single entity (the seed node, or\n\"cardinality: one\"). When true, finding zero matching entities is not an error - the\nmapping proceeds without this node's data instead of failing. Finding more than one\nmatch still fails regardless of this flag; ambiguity is never silently accepted.\n" + } + }, + "required": [ + "id", + "schema" + ] + }, + "GraphNodeFilter": { + "description": "Mirrors entity-api's GraphNodeFilter (see entity-api openapi.yml).", + "type": "object", + "properties": { + "attribute": { + "type": "string", + "description": "Entity attribute name to match against.", + "example": "order_number" + }, + "value": { + "description": "Literal value the attribute must exactly equal. Supports `{{handlebars}}` placeholders\nresolved against sourceContext when given as a string.\n", + "oneOf": [ + { + "type": "string", + "nullable": true + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ], + "example": "{{contract.origin_order}}" + } + }, + "required": [ + "attribute", + "value" + ] + }, + "GraphEdge": { + "description": "Mirrors entity-api's GraphEdge (see entity-api openapi.yml).", + "type": "object", + "properties": { + "from": { + "type": "string", + "description": "Source node ID.", + "example": "contact" + }, + "to": { + "type": "string", + "description": "Target node ID.", + "example": "order" + } + }, + "required": [ + "from", + "to" + ] + }, "MappingAttributeV2": { "type": "object", "properties": { @@ -1256,6 +1452,20 @@ "_random": { "description": "Generate random ids / numbers", "$ref": "#/components/schemas/RandomOperation" + }, + "_each": { + "description": "Iterate over a source array. Use with _as and _map.\nThe value is a path to resolve from the source entity context.\nExample: \"submission.meterReadings\"\n", + "type": "string", + "example": "submission.meterReadings" + }, + "_as": { + "description": "Name for the current iteration item in _each.\nAccessed as $ in _copy paths within _map.\nExample: \"reading\" (accessed as $reading)\n", + "type": "string", + "example": "reading" + }, + "_map": { + "description": "Operation to evaluate per _each iteration item.\nCan be any OperationNode, typically an object with _copy\nreferences to the $<_as name> alias.\n", + "$ref": "#/components/schemas/OperationNode" } }, "additionalProperties": true diff --git a/packages/cli/definitions/entity.json b/packages/cli/definitions/entity.json index d5ad201ec..c93d1e142 100644 --- a/packages/cli/definitions/entity.json +++ b/packages/cli/definitions/entity.json @@ -2,7 +2,7 @@ "openapi": "3.0.2", "info": { "title": "Entity API", - "version": "2.9.0", + "version": "2.10.0", "description": "Flexible data layer for epilot Entities.\n\nUse this API configure and access your business objects like Contacts, Opportunities and Products.\n\n[Feature Documentation](https://docs.epilot.io/docs/entities/flexible-entities)\n" }, "tags": [ @@ -75,7 +75,8 @@ { "in": "query", "name": "latest", - "description": "When true, return the latest version instead of the frozen version for frozen schemas.", + "deprecated": true, + "description": "No longer has any effect: schema freezing is retired and the latest version is always returned.\n", "schema": { "type": "boolean", "default": false @@ -161,7 +162,8 @@ { "in": "query", "name": "latest", - "description": "When true, return the latest version instead of the frozen version for frozen schemas.", + "deprecated": true, + "description": "No longer has any effect: schema freezing is retired and the latest version is always returned.\n", "schema": { "type": "boolean", "default": false @@ -223,7 +225,7 @@ "get": { "operationId": "getSchema", "summary": "getSchema", - "description": "By default gets the current version of the Schema (frozen version if frozen, otherwise latest).\nPass ?latest=true to get the latest version when the schema is frozen.\nPass ?id= to get a specific version by ID.\n", + "description": "Gets the latest version of the Schema.\nPass ?id= to get a specific version by ID.\n", "tags": [ "Schemas" ], @@ -241,7 +243,8 @@ { "in": "query", "name": "latest", - "description": "When true, return the latest version instead of the frozen version for frozen schemas.", + "deprecated": true, + "description": "No longer has any effect: schema freezing is retired and the latest version is always returned.\n", "schema": { "type": "boolean", "default": false @@ -267,7 +270,7 @@ "put": { "operationId": "putSchema", "summary": "putSchema", - "description": "Create or update a schema with a new version.\nWhen the schema is frozen, writes update the latest version without affecting the frozen version.\n", + "description": "Create or update a schema with a new version.\n", "tags": [ "Schemas" ], @@ -792,7 +795,8 @@ "type": "boolean" }, "frozen_version": { - "description": "The version ID that is currently frozen, if any", + "deprecated": true, + "description": "No longer returned: schema freezing is retired.\n", "allOf": [ { "$ref": "#/components/schemas/SchemaId" @@ -815,7 +819,8 @@ "post": { "operationId": "freezeSchema", "summary": "freezeSchema", - "description": "Freeze a schema at its current version, or at a specific version.\nWhen frozen, getSchema returns the frozen version by default.\nNew edits via putSchema update the latest version without affecting the frozen version.\n", + "deprecated": true, + "description": "Deprecated no-op: schema freezing is retired and every read returns the latest version.\nReturns the latest version of the schema unchanged. This endpoint will be removed.\n", "tags": [ "Schemas" ], @@ -864,7 +869,8 @@ "post": { "operationId": "unfreezeSchema", "summary": "unfreezeSchema", - "description": "Unfreeze a schema. Promotes the latest version to the current version for all users.\n", + "deprecated": true, + "description": "Deprecated: schema freezing is retired. Cleans up a leftover frozen pointer by promoting the\nlatest version to the current version, and succeeds when there is nothing to clean up.\nThis endpoint will be removed.\n", "tags": [ "Schemas" ], @@ -1419,6 +1425,9 @@ { "$ref": "#/components/parameters/HydrateEntitiesQueryParam" }, + { + "$ref": "#/components/parameters/AnonymizeQueryParam" + }, { "in": "query", "name": "fields", @@ -1568,6 +1577,9 @@ }, { "$ref": "#/components/parameters/HydrateEntitiesQueryParam" + }, + { + "$ref": "#/components/parameters/AnonymizeQueryParam" } ], "responses": { @@ -1776,6 +1788,9 @@ "Entities" ], "parameters": [ + { + "$ref": "#/components/parameters/AnonymizeQueryParam" + }, { "name": "input", "in": "query", @@ -1944,6 +1959,9 @@ "Activity" ], "parameters": [ + { + "$ref": "#/components/parameters/AnonymizeQueryParam" + }, { "$ref": "#/components/parameters/ActivityIdPathParam" }, @@ -2140,6 +2158,9 @@ "Entities" ], "parameters": [ + { + "$ref": "#/components/parameters/AnonymizeQueryParam" + }, { "$ref": "#/components/parameters/EntitySlugPathParam" }, @@ -2173,6 +2194,9 @@ "Activity" ], "parameters": [ + { + "$ref": "#/components/parameters/AnonymizeQueryParam" + }, { "$ref": "#/components/parameters/EntitySlugPathParam" }, @@ -2325,6 +2349,9 @@ { "$ref": "#/components/parameters/HydrateEntitiesQueryParam" }, + { + "$ref": "#/components/parameters/AnonymizeQueryParam" + }, { "$ref": "#/components/parameters/IncludeReverseQueryParam" }, @@ -2551,6 +2578,9 @@ { "$ref": "#/components/parameters/HydrateEntitiesQueryParam" }, + { + "$ref": "#/components/parameters/AnonymizeQueryParam" + }, { "in": "query", "name": "query", @@ -2620,6 +2650,9 @@ { "$ref": "#/components/parameters/HydrateEntitiesQueryParam" }, + { + "$ref": "#/components/parameters/AnonymizeQueryParam" + }, { "$ref": "#/components/parameters/IncludeReverseDeprecatedQueryParam" }, @@ -2940,6 +2973,69 @@ } } }, + "/v1/entity:abortImport": { + "post": { + "operationId": "abortEntityImport", + "summary": "Abort a running entity import", + "description": "Asks a running import to stop. Rows already imported are **kept** - this is a stop, not a\nrollback - and the job still produces its result reports for everything that landed before\nthe stop, then reaches the terminal status `CANCELLED`.\n\nThe stop is cooperative: the worker notices the request at its next batch boundary, so the\njob typically reaches `CANCELLED` within seconds. Poll `POST /v1/entity:import` with the\n`job_id` as usual to observe it.\n\nIdempotent - aborting an already-aborted or already-finished job is not an error.\n", + "tags": [ + "Import-Export" + ], + "parameters": [ + { + "in": "query", + "name": "job_id", + "required": true, + "description": "The ID of the import job to stop.", + "schema": { + "$ref": "#/components/schemas/ExportJobId" + } + }, + { + "in": "query", + "name": "schema", + "required": true, + "description": "Slug of the schema being imported, used to authorize the caller.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "202": { + "description": "The stop was requested. The job is still running until the worker picks the request up.\n", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "The job status at the time of the request." + }, + "jobPollId": { + "type": "string" + } + } + } + } + } + }, + "400": { + "description": "Bad Request" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "No such import job" + }, + "429": { + "$ref": "#/components/responses/TooManyRequestsError" + } + } + } + }, "/v1/entity/views": { "get": { "operationId": "listSavedViews", @@ -2990,6 +3086,15 @@ "maximum": 250 } }, + { + "in": "query", + "name": "q", + "description": "Free-text search on the saved view name (case-insensitive)", + "required": false, + "schema": { + "type": "string" + } + }, { "in": "query", "name": "fields", @@ -3158,7 +3263,10 @@ ], "responses": { "200": { - "description": "List of all available saved views" + "description": "The saved view was deleted" + }, + "404": { + "$ref": "#/components/responses/NotFoundError" }, "429": { "$ref": "#/components/responses/TooManyRequestsError" @@ -4967,6 +5075,63 @@ "example": true } } + }, + "grid_layout": { + "type": "object", + "deprecated": true, + "description": "Superseded by `widget_widths`. A whole-grid layout preset that assigned each widget a width by its POSITION, so a widget that rendered nothing shifted every width after it. Neither the entity app nor the entity builder reads it any more; values stored before it was replaced are left in place rather than migrated.", + "properties": { + "id": { + "type": "string", + "description": "Identifier of the chosen grid-layout preset." + }, + "columns": { + "type": "number", + "description": "Number of columns in the widget grid." + }, + "cells": { + "type": "array", + "description": "Per-cell column span, positionally paired with the widgets in order.", + "items": { + "type": "number" + } + } + } + }, + "widget_order": { + "type": "array", + "description": "Ordered list of widget ids controlling the entity-details widget-grid order. Each id is a capability widget's `component` (or `summary` for the synthesized summary card); widgets absent from the list keep their natural order at the end.", + "items": { + "type": "string" + } + }, + "widget_visibility": { + "type": "object", + "description": "Per-widget on/off state for the entity-details widget grid, keyed by widget id (a capability widget's `component`, or `summary` for the synthesized summary card). Holds only the admin's deviations from each widget's declared default: a widget absent from this map falls back to its ui_hook `default_visible`, and then to visible. Do not seed this map from migrations — doing so would freeze today's defaults into the schema.", + "additionalProperties": { + "type": "boolean" + }, + "example": { + "next_best_action": true, + "address_map": false + } + }, + "widget_widths": { + "type": "object", + "description": "Per-widget width in the entity-details widget grid, keyed by widget id (a capability widget's `component`, or `summary` for the synthesized summary card). Spans a 12-column grid: `one_third_width` = 4, `half_width` = 6, `two_third_width` = 8, `full_width` = 12. Holds only the admin's deviations from each widget's declared default: a widget absent from this map falls back to its ui_hook `default_width`, and then to `full_width`. Do not seed this map from migrations — doing so would freeze today's defaults into the schema. Replaces the positional `grid_layout` preset.", + "additionalProperties": { + "type": "string", + "enum": [ + "one_third_width", + "half_width", + "two_third_width", + "full_width" + ] + }, + "example": { + "address_map": "full_width", + "recent_communications": "one_third_width" + } } } }, @@ -5094,6 +5259,14 @@ "items": { "$ref": "#/components/schemas/GroupHeadline" } + }, + "conditions": { + "type": "array", + "nullable": true, + "description": "Dimensions that conditional variants of this entity type are keyed by.\n\nAbsent or `null` means entities of this type are not conditional. Attributes whose\nvalues a variant may override are marked with `overridable_attribute`.\n", + "items": { + "$ref": "#/components/schemas/ConditionDefinition" + } } }, "required": [ @@ -5133,11 +5306,13 @@ } }, "frozen": { - "description": "Indicates this schema is currently frozen. Present when the returned version is the frozen version.", + "deprecated": true, + "description": "No longer returned: schema freezing is retired.\n", "type": "boolean" }, "latest": { - "description": "Indicates this is the latest version of the schema. Both frozen and latest can be true if no changes were made since freezing.", + "deprecated": true, + "description": "No longer returned: every read returns the latest version.\n", "type": "boolean" }, "_summary": { @@ -5523,6 +5698,11 @@ "type": "boolean", "default": false }, + "overridable_attribute": { + "type": "boolean", + "default": false, + "description": "Allow conditional variants of the entity to override this attribute's value.\nOnly meaningful on schemas that declare `conditions`. Unflagged attributes stay\nfixed on the base entity.\n\nAccepted on scalar attributes and on relations — a variant overrides a relation by\nreplacing its whole `$relation` list (e.g. a product's prices). Rejected on system\n(`_`-prefixed), `readonly`, `hidden`, `computed` and `repeatable` attributes.\n" + }, "default_value": {}, "group": { "description": "Which group the attribute should appear in. Accepts group ID or group name", @@ -5549,6 +5729,14 @@ "type": "string", "description": "Defines the conditional rendering expression for showing this field.\nWhen a valid expression is parsed, their evaluation defines the visibility of this attribute.\nNote: Empty or invalid expression have no effect on the field visibility.\n" }, + "data_classification": { + "type": "string", + "enum": [ + "public", + "pii" + ], + "description": "Data classification of the attribute, used by anonymized responses (`?anonymize=true` or tokens minted with `anonymize: true`).\n\n- `pii`: the attribute value is always anonymized in anonymized responses (use to opt in free-text fields containing personal data)\n- `public`: the attribute value is never anonymized (use to opt out fields matched by built-in defaults, e.g. non-personal identifiers)\n\nWhen unset, built-in defaults apply based on the attribute type (email, phone, address, payment) and a curated list of well-known PII fields.\n" + }, "_purpose": { "type": "array", "items": { @@ -7115,12 +7303,184 @@ } ] }, + "BaseConditionDefinition": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid", + "description": "Stable identity of the condition, generated by the client when the condition is\ncreated. It is what makes a rename distinguishable from a remove + add, so it must\nbe round-tripped unchanged for the lifetime of the condition.\n", + "example": "d5839b94-ba20-4225-a78e-76951d352bd6" + }, + "name": { + "type": "string", + "pattern": "^[a-z0-9_]+$", + "description": "The key variant values are stored under. Unique within the schema and immutable\nonce the condition has been saved.\n", + "example": "delivery_area" + }, + "label": { + "type": "string", + "description": "Display label. Always editable.", + "example": "Delivery Area" + } + }, + "required": [ + "id", + "name", + "label" + ] + }, + "ScalarConditionDefinition": { + "allOf": [ + { + "$ref": "#/components/schemas/BaseConditionDefinition" + }, + { + "type": "object", + "description": "A condition whose value is a plain scalar, with no further configuration.", + "properties": { + "type": { + "type": "string", + "description": "Immutable once the condition has been saved.", + "enum": [ + "string", + "number", + "date", + "daterange", + "boolean" + ] + } + }, + "required": [ + "type" + ] + } + ] + }, + "SelectConditionDefinition": { + "allOf": [ + { + "$ref": "#/components/schemas/BaseConditionDefinition" + }, + { + "type": "object", + "description": "A condition whose value is one of a fixed set of options.", + "properties": { + "type": { + "type": "string", + "description": "Immutable once the condition has been saved.", + "enum": [ + "select" + ] + }, + "options": { + "type": "array", + "description": "Selectable options, in the same shape a `select` attribute's `options` take: a bare\nvalue or a `{ value, title }` object. May be empty while the condition is being\nconfigured.\n", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "title": { + "type": "string" + } + }, + "required": [ + "value" + ] + } + ] + }, + "example": [ + "12_months", + { + "value": "24_months", + "title": "24 months" + } + ] + } + }, + "required": [ + "type" + ] + } + ] + }, + "LocationConditionDefinition": { + "allOf": [ + { + "$ref": "#/components/schemas/BaseConditionDefinition" + }, + { + "type": "object", + "description": "A condition whose value identifies a place.", + "properties": { + "type": { + "type": "string", + "description": "Immutable once the condition has been saved.", + "enum": [ + "location" + ] + }, + "format": { + "type": "string", + "description": "The shape of the location value. Immutable once the condition has been saved.", + "enum": [ + "zipcode", + "zipcode_town" + ] + } + }, + "required": [ + "type" + ] + } + ] + }, + "ConditionDefinition": { + "description": "One dimension that conditional variants of an entity type are keyed by. `type`\ndiscriminates the variants: only a `select` condition carries `options`, and only a\n`location` condition carries `format`.\n", + "oneOf": [ + { + "$ref": "#/components/schemas/ScalarConditionDefinition" + }, + { + "$ref": "#/components/schemas/SelectConditionDefinition" + }, + { + "$ref": "#/components/schemas/LocationConditionDefinition" + } + ] + }, "EntitySlug": { "description": "URL-friendly identifier for the entity schema", "type": "string", "pattern": "^[a-zA-Z0-9_-]+$", "example": "contact" }, + "AdjustInstallmentCapabilityConfig": { + "description": "Configuration for adjusting a contract installment through a Journey.", + "type": "object", + "additionalProperties": false, + "example": { + "journey_id": "adjust-installment-flow" + }, + "required": [ + "journey_id" + ], + "properties": { + "journey_id": { + "type": "string", + "minLength": 1, + "description": "ID of the Flow Journey to open when adjusting the installment." + } + } + }, "EntityCapability": { "description": "Capabilities the Entity has. Turn features on/off for entities.", "type": "object", @@ -7146,6 +7506,11 @@ "$ref": "#/components/schemas/Attribute" } }, + "overridable_attribute": { + "type": "boolean", + "default": false, + "description": "Allow conditional variants of the entity to override the values managed by this\ncapability. Only meaningful on schemas that declare `conditions` and on capabilities\nthat render an attribute group (`EntityAttributes:Group` ui hook), e.g. pricing tiers.\nAttributes carried by the capability opt in individually via their own\n`overridable_attribute` flag.\n" + }, "_purpose": { "type": "array", "items": { @@ -7167,6 +7532,9 @@ "description": "ID of the app if the capability is provided by an app", "example": "123e4567-e89b-12d3-a456-426614174000" }, + "adjust_installment": { + "$ref": "#/components/schemas/AdjustInstallmentCapabilityConfig" + }, "ui_config": { "type": "object", "properties": { @@ -7250,6 +7618,27 @@ "required": [ "action" ] + }, + "default_visible": { + "type": "boolean", + "description": "Whether this widget is switched on by default for entity types that have the capability. Only meaningful on the `EntityDetailsV2:Widget` hook. Defaults to true when omitted. An admin's explicit choice in the entity builder is stored in the schema's `ui_config.widget_visibility` and wins over this.", + "example": false + }, + "default_width": { + "type": "string", + "enum": [ + "one_third_width", + "half_width", + "two_third_width", + "full_width" + ], + "description": "How wide this widget is by default in the entity-details widget grid. Only meaningful on the `EntityDetailsV2:Widget` and `EntityDetailsV2:Header` hooks. Defaults to `full_width` when omitted — deliberately unlike the attribute layout default of `half_width`, because a full-width widget can never leave a hole in a row. An admin's explicit choice in the entity builder is stored in the schema's `ui_config.widget_widths` and wins over this.", + "example": "half_width" + }, + "pricing_tier_setting": { + "type": "string", + "description": "The pricing-tier settings key an organisation must have enabled to use this widget. Omit for widgets available to everyone. Resolved against `GET /v2/pricing-tiers/me` as `override_settings[key].enabled ?? settings[key].enabled`. Note this is commercial packaging enforced in the UI, not a security control — the key must also be added to the pricing tier settings list in epilot-admin-portal, or it can never be switched on for a tier.", + "example": "entity_address_maps" } }, "required": [ @@ -8090,6 +8479,7 @@ "from": {}, "size": {}, "hydrate": {}, + "anonymize": {}, "fields": {}, "aggs": {}, "include_deleted": {}, @@ -8174,6 +8564,7 @@ "from": {}, "size": {}, "hydrate": {}, + "anonymize": {}, "fields": {}, "aggs": {}, "include_deleted": {}, @@ -8398,6 +8789,11 @@ "description": "When true, enables entity hydration to resolve nested $relation & $relation_ref references in-place.", "default": false }, + "anonymize": { + "type": "boolean", + "description": "When true, anonymizes PII in the response: identifiers are replaced with deterministic pseudonyms,\naddresses are generalized and well-known free-text fields are redacted.\nAnonymization is forced (regardless of this parameter) when the access token was created with `anonymize: true`.\n", + "default": false + }, "fields": { "$ref": "#/components/schemas/FieldsParam" }, @@ -8626,6 +9022,39 @@ "description": "Optional array of field names to include in the hydrated entity response for this node.\nWhen specified, only the requested fields plus required internal fields (_id, _schema, _org) will be returned.\nOnly applies when hydrate=true.\n" } ] + }, + "filter": { + "type": "array", + "description": "Narrows this node's traversal results to entities matching every filter (AND semantics). Useful for\ndisambiguating among multiple entities reachable via the same graph edge.\n", + "items": { + "$ref": "#/components/schemas/GraphNodeFilter" + } + } + } + }, + "GraphNodeFilter": { + "type": "object", + "description": "Entities are included in this node's result only if `attribute` exactly equals the literal `value`.", + "required": [ + "attribute", + "value" + ], + "properties": { + "attribute": { + "type": "string", + "description": "Entity attribute name to match against. Must be a plain attribute name, not an Elasticsearch field path.", + "example": "order_number" + }, + "value": { + "allOf": [ + { + "$ref": "#/components/schemas/SearchFilterValue" + }, + { + "description": "Literal value the attribute must exactly equal for the entity to be included in this node's result." + } + ], + "example": "OR-113" } } }, @@ -9030,6 +9459,9 @@ } } }, + "source_context": { + "$ref": "#/components/schemas/EntityOperationSourceContext" + }, "_workflow_origin": { "type": "object", "description": "Internal property for workflow origin tracking and infinite loop prevention.\nPopulated when an entity update originates from a workflow execution.\nThis allows downstream automation services to prevent circular triggering.\n", @@ -9043,6 +9475,13 @@ "description": "The flow template ID - used to detect and prevent circular triggering" } } + }, + "_automation_chain": { + "type": "array", + "description": "Ordered automation flow ids that caused this change; used to break multi-automation trigger loops.", + "items": { + "type": "string" + } } }, "required": [ @@ -9051,6 +9490,59 @@ "operation" ] }, + "EntityOperationSourceContext": { + "type": "object", + "description": "Billing and audit source context for the operation event.", + "properties": { + "source": { + "type": "string", + "description": "Normalized source bucket for the operation.", + "enum": [ + "portal", + "epilot", + "erp", + "system", + "api", + "external", + "journey", + "automation", + "unknown" + ] + }, + "source_label": { + "type": "string", + "description": "Original free-form source label from the caller or changeset, when available." + }, + "source_system": { + "type": "string", + "description": "Optional upstream system label, for example an ERP system name." + }, + "source_reference": { + "type": "string", + "description": "Optional upstream change reference." + }, + "actor_type": { + "type": "string", + "enum": [ + "user", + "portal_user", + "api_client", + "automation", + "system" + ] + }, + "actor_id": { + "type": "string" + }, + "effective_at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "source" + ] + }, "BaseActivityItem": { "allOf": [ { @@ -9192,6 +9684,11 @@ "user_id": { "type": "string", "example": "10598" + }, + "org_id": { + "type": "string", + "description": "The organization ID of the user who created the view. Used for partner access control.", + "example": "739224" } } }, @@ -9372,7 +9869,8 @@ "message", "portal_user", "request", - "comment" + "comment", + "user" ] }, "TaxonomySearchIncludeArchivedParam": { @@ -10052,6 +10550,16 @@ } }, "parameters": { + "AnonymizeQueryParam": { + "name": "anonymize", + "description": "When true, anonymizes PII in the response: identifiers (names, emails, phone numbers, IBANs) are replaced\nwith deterministic pseudonyms (stable within an org), addresses are generalized to postal code / city / country,\nand well-known free-text fields (e.g. note content) are redacted.\n\nUseful for AI agents and data analysis use cases that must not access personal data.\n\nAnonymization is forced (regardless of this parameter) when the access token was created with `anonymize: true`.\n", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false + } + }, "EntityIdPathParam": { "name": "id", "description": "Entity id", diff --git a/packages/cli/definitions/environments.json b/packages/cli/definitions/environments.json index 85120c98e..70f323376 100644 --- a/packages/cli/definitions/environments.json +++ b/packages/cli/definitions/environments.json @@ -20,7 +20,7 @@ "/v1/environments": { "get": { "operationId": "listEnvironmentVariables", - "summary": "List environment variables", + "summary": "listEnvironmentVariables", "description": "List all environment variables for the organization. Returns metadata only, no secret values.", "tags": [ "environments" @@ -49,8 +49,8 @@ }, "post": { "operationId": "createEnvironmentVariable", - "summary": "Create environment variable", - "description": "Create a new environment variable or secret for the organization.", + "summary": "createEnvironmentVariable", + "description": "Create a new environment variable or secret for the organization. If `group` is provided and the group does not yet exist, it is created automatically.", "tags": [ "environments" ], @@ -96,7 +96,7 @@ "/v1/environments/groups": { "get": { "operationId": "listEnvironmentGroups", - "summary": "List environment groups", + "summary": "listEnvironmentGroups", "description": "List all environment groups for the organization.", "tags": [ "environments" @@ -138,6 +138,7 @@ "put": { "operationId": "putEnvironmentGroup", "summary": "putEnvironmentGroup", + "description": "Create or update an environment group by name. Acts as an upsert — creates the group if it does not exist.", "tags": [ "environments" ], @@ -185,7 +186,7 @@ }, "delete": { "operationId": "deleteEnvironmentGroup", - "summary": "Delete an environment group", + "summary": "deleteEnvironmentGroup", "description": "Deletes a group. Variables assigned to this group become ungrouped.", "tags": [ "environments" @@ -224,8 +225,8 @@ ], "get": { "operationId": "getEnvironmentVariable", - "summary": "Get environment variable", - "description": "Get an environment variable by key. Returns value only for String type, omitted for SecretString.", + "summary": "getEnvironmentVariable", + "description": "Get an environment variable by key. Returns value for non-secret types, omitted for SecretString.", "tags": [ "environments" ], @@ -256,8 +257,8 @@ }, "put": { "operationId": "updateEnvironmentVariable", - "summary": "Update environment variable", - "description": "Create or update an environment variable. Acts as an upsert — creates the variable if it does not exist.", + "summary": "updateEnvironmentVariable", + "description": "Create or update an environment variable. Acts as an upsert — creates the variable if it does not exist. If `group` is provided and the group does not yet exist, it is created automatically.", "tags": [ "environments" ], @@ -301,6 +302,9 @@ "403": { "description": "Forbidden" }, + "409": { + "description": "Cannot change the type of a variable that currently holds a value" + }, "500": { "description": "Internal server error" } @@ -308,7 +312,7 @@ }, "delete": { "operationId": "deleteEnvironmentVariable", - "summary": "Delete environment variable", + "summary": "deleteEnvironmentVariable", "description": "Delete an environment variable by key.", "tags": [ "environments" @@ -344,9 +348,414 @@ "schemas": { "EnvironmentValueType": { "type": "string", + "description": "The structure a variable's value holds. `SecretString` is encrypted at rest and\nits value is never returned. `Text`, `Number`, `Boolean`, `Map`, `JSON` and `Link`\nmay be served to browser-facing consumers; `String` and `SecretString` may not. A\n`List` inherits this from its element type: a `List` is served only when its\n`itemType` is itself client-safe, so a `List` or `List` is not.\n", "enum": [ "String", - "SecretString" + "SecretString", + "Text", + "Number", + "Boolean", + "Map", + "JSON", + "Link", + "List" + ] + }, + "StringTranslations": { + "description": "A string translated per language. Keys are language codes (e.g. `de`,\n`en-US`), matching the hyphen-only BCP-47 form epilot's i18n stack uses\neverywhere else: `^[a-z]{2,3}(-[A-Za-z0-9]+)*$`. The server enforces\nthat with LANGUAGE_KEY_PATTERN in src/core/value-types.ts — the two are\nnot otherwise linked. The pattern is documented rather than declared\nbecause `propertyNames` is JSON Schema / OAS 3.1 and this document is\n3.0.3, where it fails `spectral lint` (oas3-schema).\n", + "type": "object", + "minProperties": 1, + "additionalProperties": { + "type": "string", + "minLength": 1 + } + }, + "MapEntry": { + "description": "One entry of a Map. `key` is the token a journey submits; `value` is\nwhat the customer reads — either one string, or one string per\nlanguage. Every entry of a Map must agree on which of the two it uses.\n", + "type": "object", + "required": [ + "key", + "value" + ], + "additionalProperties": false, + "properties": { + "key": { + "type": "string", + "minLength": 1 + }, + "value": { + "oneOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "$ref": "#/components/schemas/StringTranslations" + } + ] + } + } + }, + "MapValue": { + "type": "object", + "required": [ + "options" + ], + "additionalProperties": false, + "properties": { + "fallbackLanguage": { + "type": "string", + "minLength": 2, + "default": "de" + }, + "options": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/components/schemas/MapEntry" + } + } + } + }, + "JsonValue": { + "description": "Arbitrary JSON object, e.g. a flat key/value map used by integrations for\nenum translation: {\"Mr.\": 1, \"Ms. / Mrs.\": 2}. Max 32 KB serialised.\n", + "type": "object", + "additionalProperties": true + }, + "LinkValue": { + "description": "One URL with a label and description a customer reads. `label` and\n`description` are each either a plain string or one string per language;\nthe two fields decide that independently — a plain string means \"the\nsame in every language\". A translated field must carry the fallback\nlanguage.\n\nThe URL must be absolute and `http`/`https` only. This value is served\nto browser-facing consumers, so `javascript:` and `data:` URLs are\nrejected at write time.\n\nWritten flat rather than composed from a shared field set: `allOf` plus\n`additionalProperties: false` is rejected by most validators, because\neach branch sees the sibling's properties as unknown. The composition\nlives in zod (LinkFieldsSchema) — see src/core/value-types.ts.\n\nEach translated value of `label` and `description` is also limited to\nthe same maximum as the plain-string form above — 255 characters for\n`label`, 1024 for `description`. The limit is enforced by the server\neven though the shared `StringTranslations` schema referenced below\ndoes not itself declare it.\n", + "type": "object", + "required": [ + "url", + "label" + ], + "additionalProperties": false, + "properties": { + "url": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + }, + "label": { + "oneOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + { + "$ref": "#/components/schemas/StringTranslations" + } + ] + }, + "description": { + "oneOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 1024 + }, + { + "$ref": "#/components/schemas/StringTranslations" + } + ] + }, + "fallbackLanguage": { + "type": "string", + "minLength": 2, + "default": "de" + } + } + }, + "LinkFields": { + "description": "The fields of a link, without a fallback language — the shape a `List`\nitem carries. Inside a list the fallback belongs to the wrapper, which\nowns the language tabs for every row.\n\nRestated rather than composed with `LinkValue`: `allOf` plus\n`additionalProperties: false` is rejected by most validators, because\neach branch sees the sibling's properties as unknown. zod composes\nproperly — see LinkFieldsSchema in src/core/value-types.ts.\n", + "type": "object", + "required": [ + "url", + "label" + ], + "additionalProperties": false, + "properties": { + "url": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + }, + "label": { + "oneOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + { + "$ref": "#/components/schemas/StringTranslations" + } + ] + }, + "description": { + "oneOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 1024 + }, + { + "$ref": "#/components/schemas/StringTranslations" + } + ] + } + } + }, + "ListItemType": { + "description": "The element type a `List` holds — every value type except the\ncontainers. `Map` is already a keyed collection and a list of lists has\nno consumer. A list is exactly as client-safe and exactly as secret as\nits element type: a list of `SecretString` is encrypted per item and its\nvalue is never returned.\n", + "type": "string", + "enum": [ + "String", + "SecretString", + "Text", + "Number", + "Boolean", + "JSON", + "Link" + ] + }, + "ListOfText": { + "type": "object", + "required": [ + "itemType", + "items" + ], + "additionalProperties": false, + "properties": { + "itemType": { + "type": "string", + "enum": [ + "Text" + ] + }, + "items": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 32768 + } + } + } + }, + "ListOfString": { + "type": "object", + "required": [ + "itemType", + "items" + ], + "additionalProperties": false, + "properties": { + "itemType": { + "type": "string", + "enum": [ + "String" + ] + }, + "items": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + } + } + }, + "ListOfSecretString": { + "description": "Write-only in effect: the items are encrypted per item at rest and the\nwhole value is omitted from every read response, exactly as a\n`SecretString` variable's value is. Read `item_type` to learn what a\nlist holds when its value is withheld.\n", + "type": "object", + "required": [ + "itemType", + "items" + ], + "additionalProperties": false, + "properties": { + "itemType": { + "type": "string", + "enum": [ + "SecretString" + ] + }, + "items": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1, + "description": "Must be at most 4096 bytes when UTF-8 encoded — the same limit KMS\nenforces on a symmetric Encrypt call. This is a byte bound, not a\ncharacter bound: a string with multi-byte characters can be well\nunder 4096 characters long and still be rejected.\n" + } + } + } + }, + "ListOfNumber": { + "type": "object", + "required": [ + "itemType", + "items" + ], + "additionalProperties": false, + "properties": { + "itemType": { + "type": "string", + "enum": [ + "Number" + ] + }, + "items": { + "type": "array", + "minItems": 1, + "items": { + "type": "number" + } + } + } + }, + "ListOfBoolean": { + "type": "object", + "required": [ + "itemType", + "items" + ], + "additionalProperties": false, + "properties": { + "itemType": { + "type": "string", + "enum": [ + "Boolean" + ] + }, + "items": { + "type": "array", + "minItems": 1, + "items": { + "type": "boolean" + } + } + } + }, + "ListOfJson": { + "type": "object", + "required": [ + "itemType", + "items" + ], + "additionalProperties": false, + "properties": { + "itemType": { + "type": "string", + "enum": [ + "JSON" + ] + }, + "items": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "ListOfLink": { + "description": "A list of links. `fallbackLanguage` applies to every item's translated\n`label` and `description`; items may mix plain and translated fields\nfreely, since a plain string is a complete answer for any language.\n", + "type": "object", + "required": [ + "itemType", + "items" + ], + "additionalProperties": false, + "properties": { + "itemType": { + "type": "string", + "enum": [ + "Link" + ] + }, + "fallbackLanguage": { + "type": "string", + "minLength": 2, + "default": "de" + }, + "items": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/components/schemas/LinkFields" + } + } + } + }, + "ListValue": { + "description": "An ordered collection of one declared element type. Items round-trip in\nthe order written; nothing sorts them. Holds at most 100 items.\n\nThe whole value is limited to 32768 characters when serialised — the\nserver enforces this, and it is not expressible per-property here.\n", + "oneOf": [ + { + "$ref": "#/components/schemas/ListOfText" + }, + { + "$ref": "#/components/schemas/ListOfNumber" + }, + { + "$ref": "#/components/schemas/ListOfBoolean" + }, + { + "$ref": "#/components/schemas/ListOfJson" + }, + { + "$ref": "#/components/schemas/ListOfLink" + }, + { + "$ref": "#/components/schemas/ListOfString" + }, + { + "$ref": "#/components/schemas/ListOfSecretString" + } + ], + "discriminator": { + "propertyName": "itemType", + "mapping": { + "Text": "#/components/schemas/ListOfText", + "Number": "#/components/schemas/ListOfNumber", + "Boolean": "#/components/schemas/ListOfBoolean", + "JSON": "#/components/schemas/ListOfJson", + "Link": "#/components/schemas/ListOfLink", + "String": "#/components/schemas/ListOfString", + "SecretString": "#/components/schemas/ListOfSecretString" + } + } + }, + "EnvironmentValue": { + "description": "A variable's value. The JSON type corresponds to the variable's `type`:\n`String`, `SecretString` and `Text` are strings, `Number` is a number,\n`Boolean` is a boolean, and `Map`, `JSON`, `Link` and `List` are objects.\nNumbers are IEEE 754 doubles; integers above 2^53 may lose precision on\nround-trip.\n", + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "$ref": "#/components/schemas/MapValue" + }, + { + "$ref": "#/components/schemas/JsonValue" + }, + { + "$ref": "#/components/schemas/LinkValue" + }, + { + "$ref": "#/components/schemas/ListValue" + } ] }, "EnvironmentVariable": { @@ -365,6 +774,14 @@ "type": { "$ref": "#/components/schemas/EnvironmentValueType" }, + "item_type": { + "allOf": [ + { + "$ref": "#/components/schemas/ListItemType" + } + ], + "description": "Present only for a `List` variable that holds a value, derived from\nthat value at read time and never stored. Returned even when `value`\nitself is withheld, which is the case for a list of `SecretString` —\nit is the only way a client learns what such a list holds.\n" + }, "description": { "type": "string" }, @@ -373,8 +790,16 @@ "description": "Optional group name for organising variables in the UI" }, "value": { - "type": "string", - "description": "Value is returned for String type, omitted for SecretString" + "allOf": [ + { + "$ref": "#/components/schemas/EnvironmentValue" + } + ], + "description": "Returned for non-secret types, omitted for SecretString and for a\nList of SecretString. Also omitted when\nthe variable has been created without a value — for example by a blueprint\ninstall, which syncs a variable's key and type but never its value.\n" + }, + "protected": { + "type": "boolean", + "description": "Whether the variable is protected from editing" }, "created_at": { "type": "string", @@ -401,6 +826,14 @@ "type": { "$ref": "#/components/schemas/EnvironmentValueType" }, + "item_type": { + "allOf": [ + { + "$ref": "#/components/schemas/ListItemType" + } + ], + "description": "Present only for a `List` variable that holds a value, derived from\nthat value at read time and never stored. Returned even when `value`\nitself is withheld, which is the case for a list of `SecretString` —\nit is the only way a client learns what such a list holds.\n" + }, "description": { "type": "string" }, @@ -409,8 +842,16 @@ "description": "Optional group name for organising variables in the UI" }, "value": { - "type": "string", - "description": "Value is returned for String type, omitted for SecretString" + "allOf": [ + { + "$ref": "#/components/schemas/EnvironmentValue" + } + ], + "description": "Returned for non-secret types, omitted for SecretString and for a\nList of SecretString. Also omitted when\nthe variable has been created without a value — for example by a blueprint\ninstall, which syncs a variable's key and type but never its value.\n" + }, + "protected": { + "type": "boolean", + "description": "Whether the variable is protected from editing" }, "created_at": { "type": "string", @@ -457,7 +898,11 @@ "type": "string" }, "value": { - "type": "string" + "$ref": "#/components/schemas/EnvironmentValue" + }, + "protected": { + "type": "boolean", + "description": "Whether the variable is protected from editing" } } }, @@ -473,13 +918,17 @@ ] }, "value": { - "type": "string" + "$ref": "#/components/schemas/EnvironmentValue" }, "description": { "type": "string" }, "group": { "type": "string" + }, + "protected": { + "type": "boolean", + "description": "Whether the variable is protected from editing" } } }, diff --git a/packages/cli/definitions/event-catalog.json b/packages/cli/definitions/event-catalog.json index 7f05180fe..1a445b998 100644 --- a/packages/cli/definitions/event-catalog.json +++ b/packages/cli/definitions/event-catalog.json @@ -48,6 +48,39 @@ } } } + }, + "post": { + "operationId": "createCustomEvent", + "summary": "createCustomEvent", + "description": "Reserve an org-scoped custom event name and persist its immutable v1.0 draft definition. Custom events are always projected from an entity graph: entity_graph is required and, in guided mapping mode, every schema field needs a graph_source expression. Caller-supplied trigger fields are ignored for custom events.", + "tags": [ + "Event Catalog" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateCustomEventPayload" + } + } + } + }, + "responses": { + "201": { + "description": "Custom event draft created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EventConfig" + } + } + } + }, + "409": { + "description": "Name is reserved by a built-in or existing org event" + } + } } }, "/v1/events/{event_name}": { @@ -125,6 +158,179 @@ "description": "Event not found" } } + }, + "put": { + "operationId": "replaceCustomEventDraft", + "summary": "replaceCustomEventDraft", + "description": "Replace the complete v1.0 definition of an org-scoped custom event while it is still an\nunpublished draft. Drafts have no consumers, so their definition is not yet immutable;\nthe event name is the identity and cannot change. Publication remains a separate action.\n", + "tags": [ + "Event Catalog" + ], + "parameters": [ + { + "name": "event_name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateCustomEventPayload" + } + } + } + }, + "responses": { + "200": { + "description": "Updated draft event configuration", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EventConfig" + } + } + } + }, + "400": { + "description": "Definition is invalid or the body event_name differs from the path" + }, + "404": { + "description": "Custom event not found" + }, + "409": { + "description": "Event is built-in or no longer an unpublished draft" + }, + "422": { + "description": "Definition could not be stored" + } + } + }, + "delete": { + "operationId": "deprecateCustomEvent", + "summary": "deprecateCustomEvent", + "description": "Soft-deprecate an org-scoped custom event. Definitions and v1.0 history remain readable.", + "tags": [ + "Event Catalog" + ], + "parameters": [ + { + "name": "event_name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Custom event deprecated" + }, + "404": { + "description": "Event not found" + }, + "409": { + "description": "Built-ins cannot be deprecated or lifecycle cannot transition" + } + } + } + }, + "/v1/events/{event_name}:preview": { + "post": { + "operationId": "previewCustomEvent", + "summary": "previewCustomEvent", + "description": "Assemble and fully validate a persisted custom-event draft without publishing it.", + "tags": [ + "Event Catalog" + ], + "parameters": [ + { + "name": "event_name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TriggerEventPayload" + } + } + } + }, + "responses": { + "200": { + "description": "Deterministic preview result", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PreviewEventResponse" + } + } + } + }, + "400": { + "description": "Projection or schema validation failed" + }, + "404": { + "description": "Custom event not found" + } + } + } + }, + "/v1/events/{event_name}:publish": { + "post": { + "operationId": "publishCustomEventDefinition", + "summary": "publishCustomEventDefinition", + "description": "Conditionally activate an immutable custom-event v1.0 definition.", + "tags": [ + "Event Catalog" + ], + "parameters": [ + { + "name": "event_name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublishCustomEventPayload" + } + } + } + }, + "responses": { + "200": { + "description": "Published event configuration", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EventConfig" + } + } + } + }, + "409": { + "description": "Definition is not a draft or base publication choice is invalid" + } + } } }, "/v1/events/{event_name}/json_schema": { @@ -437,7 +643,7 @@ "post": { "operationId": "triggerEvent", "summary": "triggerEvent", - "description": "Explicitly trigger an event by providing input field values and an optional entity seed\nfor graph hydration. The event must be enabled for the organization.\n\n- For events with an entity_graph, a seed (entity_id + node_id) is required\n- For events without an entity_graph, only fields are needed\n- Entity operation context fields (operation, trigger_entity, activity_id, activity_type)\n are not included when triggering via API\n", + "description": "Explicitly trigger an event by providing input field values and an optional entity seed\nfor graph hydration. The event must be enabled for the organization.\n\n- For events with an entity_graph, a seed (entity_id + node_id) is required\n- For events without an entity_graph, only fields are needed\n- Entity operation context fields (operation, trigger_entity, activity_id, activity_type)\n are not included when triggering via API\n- Events marked `automation_trigger_only` require `_trigger_source_type: automation` and a\n stable execution/action ID in `_trigger_source`; retries with that ID return the same\n logical event, including while durable delivery is pending\n", "tags": [ "Event Catalog" ], @@ -464,7 +670,7 @@ }, "responses": { "200": { - "description": "Event triggered successfully", + "description": "Event published successfully", "content": { "application/json": { "schema": { @@ -481,6 +687,33 @@ }, "404": { "description": "Event not found" + }, + "409": { + "description": "The Automation trigger source was already used with different trigger input, including a different entity seed" + }, + "425": { + "description": "The hydrated entity graph is not ready yet, another request currently owns the\nidempotency lease, or a durably queued event (including an already-requested redrive)\nis awaiting publication. Automation must retry with the same trigger source and may\nadvance only after a 200 response.\n", + "headers": { + "Retry-After": { + "description": "Seconds before the Automation action should retry", + "schema": { + "type": "integer", + "minimum": 1 + } + } + } + }, + "503": { + "description": "Durable delivery exhausted its bounded automatic redrives. The same logical event is\nretained and no new event is created. An API retry with the same trigger source\nrequests a redrive of the existing Standard workflow from its failed task checkpoint\nand receives this retryable response. Subsequent retries receive 425 while that\nredrive is pending or running.\n", + "headers": { + "Retry-After": { + "description": "Seconds before the Automation action should retry", + "schema": { + "type": "integer", + "minimum": 1 + } + } + } } } } @@ -593,6 +826,54 @@ "description": "Whether this event can be explicitly triggered by automations.\nWhen true, the event will appear in the automation builder as a\n\"Trigger Event\" action option.\nDefaults to false if not specified.\n", "default": false, "example": true + }, + "api_trigger": { + "type": "boolean", + "default": true, + "description": "Allows explicit API-source triggering, not HTTP transport. Automation also uses HTTP. Automation-only events always disable this source." + }, + "automation_trigger_only": { + "type": "boolean", + "description": "Whether explicit triggering is restricted to Automation. When true, callers must pass\n`_trigger_source_type: automation` and a stable `_trigger_source`. The trigger uses\nstrict entity readiness validation and the durable Automation outbox.\nRequires `automation_trigger: true` and cannot be combined with `entity_operation`.\n", + "default": false, + "example": true + }, + "automation_trigger_seed_node": { + "type": "string", + "description": "Required entity-graph seed node for an Automation-only trigger. When configured, an\nexplicit trigger using any other node is rejected before hydration.\n", + "example": "ticket" + }, + "event_origin": { + "type": "string", + "readOnly": true, + "enum": [ + "builtin", + "custom" + ], + "description": "Definition ownership. Built-ins always win a name collision." + }, + "mapping": { + "$ref": "#/components/schemas/EventMapping" + }, + "lineage": { + "$ref": "#/components/schemas/CustomEventLineage" + }, + "success_criteria": { + "type": "array", + "description": "Org-defined success criteria for this event: the entity attributes that an\norganization considers must be captured for an event change request to be\ntreated as complete (e.g. for telephony / self-service flows).\n\nAdvisory metadata — event-catalog does NOT require an org to define any and\ndoes NOT enforce them when an event is triggered or published. The org may\ndefine none (empty array or omitted). When provided, each entry is validated\nfor well-formedness on write (see SuccessCriterion).\n", + "items": { + "$ref": "#/components/schemas/SuccessCriterion" + }, + "example": [ + { + "entity_schema": "contract", + "attribute": "installment_amount" + }, + { + "entity_schema": "billing_account", + "attribute": "due_date" + } + ] } } }, @@ -612,17 +893,203 @@ } ] }, - "UpdateEventPayload": { - "description": "Payload for updating an event configuration.\nAccepts the same fields as EventConfig (all optional for PATCH).\nCurrently `enabled` and `auto_trigger` fields are processed.\n", - "allOf": [ - { - "$ref": "#/components/schemas/EventConfigBase" + "CreateCustomEventPayload": { + "type": "object", + "description": "Complete immutable custom-event v1.0 definition projected from a required entity graph. Publication is a separate conditional action.", + "properties": { + "event_name": { + "type": "string", + "pattern": "^[A-Z][A-Za-z0-9]{2,79}$" }, - { - "type": "object" + "event_title": { + "type": "string", + "minLength": 1, + "maxLength": 160 + }, + "event_description": { + "type": "string", + "maxLength": 2000 + }, + "event_tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "schema_fields": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/CustomSchemaField" + } + }, + "entity_graph": { + "$ref": "#/components/schemas/GraphDefinition" + }, + "entity_operation": { + "$ref": "#/components/schemas/EntityOperationTrigger" + }, + "automation_trigger": { + "type": "boolean", + "default": true + }, + "api_trigger": { + "type": "boolean", + "default": true, + "description": "Allows API-source triggering independently of Automation (which also uses HTTP). Must be false for Automation-only events." + }, + "automation_trigger_only": { + "type": "boolean", + "default": false, + "description": "Restricts triggering to Automation with durable delivery and strict readiness. Requires automation_trigger true, api_trigger false, and no entity_operation." + }, + "automation_trigger_seed_node": { + "type": "string", + "minLength": 1, + "description": "Required cardinality-one graph seed for an Automation-only event. Inherited restrictions cannot be changed." + }, + "mapping": { + "$ref": "#/components/schemas/EventMapping" + }, + "lineage": { + "$ref": "#/components/schemas/CustomEventLineage" + }, + "example": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "event_name", + "event_title", + "schema_fields", + "entity_graph" + ] + }, + "EventMapping": { + "type": "object", + "description": "Guided mappings use schema_fields graph_source expressions; raw mode evaluates one JSONata object transform.", + "properties": { + "mode": { + "type": "string", + "enum": [ + "guided", + "jsonata" + ], + "default": "guided" + }, + "jsonata": { + "type": "string", + "minLength": 1, + "maxLength": 20000 } + }, + "required": [ + "mode" ] }, + "CustomEventLineage": { + "type": "object", + "description": "Optional catalog lineage to a separately named base event. Built-in inheritance is validated against this exact registered version; its trigger restrictions cannot be removed or replaced. It does not replace the base event.", + "properties": { + "base_event_name": { + "type": "string" + }, + "base_event_version": { + "type": "string" + } + }, + "required": [ + "base_event_name", + "base_event_version" + ] + }, + "PurposeFilterSnapshot": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "display_name": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "id", + "display_name" + ] + }, + "PublishCustomEventPayload": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "auto_trigger": { + "type": "boolean", + "default": true + }, + "base_auto_trigger_enabled": { + "type": "boolean", + "description": "Optional publication choice. False disables only the named lineage base event's org auto-trigger overlay." + } + } + }, + "ValidationIssue": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "path", + "message" + ] + }, + "PreviewEventResponse": { + "type": "object", + "properties": { + "payload": { + "type": "object", + "additionalProperties": true + }, + "errors": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationIssue" + } + } + }, + "required": [ + "payload", + "errors" + ] + }, + "UpdateEventPayload": { + "type": "object", + "description": "Mutable org activation overlay. Immutable event definition fields are not accepted.", + "properties": { + "enabled": { + "type": "boolean" + }, + "auto_trigger": { + "type": "boolean" + }, + "success_criteria": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SuccessCriterion" + } + } + }, + "additionalProperties": false + }, "PrimitiveField": { "type": "object", "description": "A primitive JSON Schema field definition", @@ -642,7 +1109,7 @@ }, "graph_source": { "type": "string", - "description": "Optional JSONata expression to extract the field value from the hydrated entity graph.\n\nThe expression has access to all hydrated graph nodes by their node ID.\nIf not specified, the field value must be provided as input when triggering the event.\n\nExamples:\n - \"ticket.meter_reading_value\" (simple path)\n - \"contact.email[0].email\" (nested/array access)\n - \"ticket.reading_timestamp ?? $now()\" (with fallback)\n - \"$number(meter_counter.reading_value)\" (type coercion)\n", + "description": "JSONata expression to extract the field value from the hydrated entity graph.\n\nThe expression has access to all hydrated graph nodes by their node ID.\nOptional for built-in events, where an unset value must be provided as input when\ntriggering the event. Required for custom events in guided mapping mode.\n\nExamples:\n - \"ticket.meter_reading_value\" (simple path)\n - \"contact.email[0].email\" (nested/array access)\n - \"ticket.reading_timestamp ?? $now()\" (with fallback)\n - \"$number(meter_counter.reading_value)\" (type coercion)\n", "example": "ticket.meter_reading_value" } }, @@ -683,6 +1150,17 @@ "items" ] }, + "CustomSchemaField": { + "description": "Custom v1 fields support graph-projected JSON Schema values and context entities; attachment semantics are built-in-only.", + "oneOf": [ + { + "$ref": "#/components/schemas/PrimitiveField" + }, + { + "$ref": "#/components/schemas/ContextEntity" + } + ] + }, "SchemaField": { "oneOf": [ { @@ -696,6 +1174,26 @@ } ] }, + "SuccessCriterion": { + "type": "object", + "description": "A single org-defined success criterion: an entity attribute that must be captured\nfor this event's change request to be considered complete.\n\nIdentity is the entity schema plus the attribute name — mirroring the\nEntityOperationTrigger `schema`/`attribute` vocabulary. On write (PATCH), both\n`attribute` and `entity_schema` are required and `entity_schema` must match the\n`schema` of a node in the event's `entity_graph` (else the request is rejected) —\nthis prevents unsatisfiable criteria. The criteria themselves are advisory and are\nnever enforced when an event is triggered or published.\n", + "properties": { + "entity_schema": { + "type": "string", + "description": "Entity schema slug the attribute belongs to (matches a node schema in the event's entity_graph).", + "example": "contract" + }, + "attribute": { + "type": "string", + "description": "Attribute name on the entity schema.", + "example": "installment_amount" + } + }, + "required": [ + "entity_schema", + "attribute" + ] + }, "CommonEventMetadata": { "type": "object", "description": "Common metadata fields present in all event payloads", @@ -878,6 +1376,24 @@ ] } }, + "InlineDowngradeStep": { + "type": "object", + "description": "One step of an event's inline `_downgrades` chain. Maps the current-version payload to the previous version via a JSONata expression. Stamped by Event Catalog at publish time; executed by consumers during walk-back, never by EC itself.", + "properties": { + "to": { + "type": "string", + "description": "Version label this step downgrades to (the previous version)." + }, + "jsonata": { + "type": "string", + "description": "JSONata expression mapping the current-shape payload to the previous-shape payload." + } + }, + "required": [ + "to", + "jsonata" + ] + }, "Event": { "type": "object", "description": "An event instance in the event history", @@ -920,6 +1436,20 @@ "_ack_id": { "type": "string", "description": "Unique acknowledgment tracking ID for the event.\nUsed to track event delivery and processing status.\n" + }, + "_downgrades": { + "type": "array", + "description": "Inline downgrade chain stamped by Event Catalog at publish time, ordered newest-to-oldest. Present ONLY on multi-version events. Internal versioning transport: consumers (e.g. svc-webhooks) walk the payload back to a pinned version using these JSONata steps, then strip the field before delivery -- end customers never receive it.", + "items": { + "$ref": "#/components/schemas/InlineDowngradeStep" + } + }, + "_automation_chain": { + "type": "array", + "description": "Ordered automation flow ids that caused this event (at most 100), propagated verbatim from the trigger input or the originating entity operation. Internal loop-prevention transport for automation-api; svc-webhooks strips it before delivery.", + "items": { + "type": "string" + } } }, "required": [ @@ -1126,7 +1656,7 @@ }, "attribute": { "type": "array", - "description": "Optional list of entity attributes to track for changes.\nIf specified, the event only triggers when these attributes are affected.\n- On createEntity: attribute must be defined in the entity payload\n- On updateEntity: attribute must be in diff.added, diff.updated, or diff.deleted\nIf not specified, all changes to matching entities will trigger the event.\n", + "description": "Optional list of entity attributes to track for changes.\nIf specified, the event only triggers when these attributes are affected.\n- On createEntity: attribute must be defined in the entity payload\n- On updateEntity: attribute must be in diff.added, diff.updated, or diff.deleted\nIf not specified, all changes to matching entities will trigger the event.\nDerived built-in triggers retain every inherited attribute; additions are OR alternatives.\nAn inherited unfiltered trigger cannot be narrowed by adding attribute filters.\n", "items": { "type": "string" }, @@ -1138,7 +1668,7 @@ }, "purpose": { "type": "array", - "description": "Optional list of purpose names to filter by.\nThe entity must have at least one matching purpose in its _purpose array.\nPurpose names are matched against the taxonomy classification names (e.g., \"Kündigung\", \"Umzug/Auszug\").\nIf not specified, the event triggers regardless of entity purpose.\n", + "description": "Optional list of purpose names to filter by.\nThe entity must have at least one matching purpose in its _purpose array.\nPurpose names are matched against the taxonomy classification names (e.g., \"Kündigung\", \"Umzug/Auszug\").\nFor custom events, names are allowed only when inherited unchanged from the actual\npinned built-in lineage version. New purposes must use stable purpose_filters IDs.\nNames and stable IDs are OR alternatives; operation/schema/attribute conditions still apply.\nIf neither purpose nor purpose_filters is specified, the event triggers regardless of purpose.\n", "items": { "type": "string" }, @@ -1146,6 +1676,13 @@ "Kündigung", "Umzug/Auszug" ] + }, + "purpose_filters": { + "type": "array", + "description": "Stable purpose IDs plus immutable display-name snapshots for custom events. Additive to inherited purpose names (OR). Inherited unfiltered triggers cannot be narrowed.", + "items": { + "$ref": "#/components/schemas/PurposeFilterSnapshot" + } } }, "required": [ @@ -1238,6 +1775,14 @@ "TriggerEventPayload": { "type": "object", "description": "Payload for explicitly triggering an event via API", + "example": { + "seed": { + "entity_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "node_id": "ticket" + }, + "_trigger_source_type": "automation", + "_trigger_source": "execution-id/action-id" + }, "properties": { "seed": { "type": "object", @@ -1260,7 +1805,7 @@ }, "fields": { "type": "object", - "description": "Input field values for the event. Keys must match the event's\nschema_fields definitions. Values are validated against each\nfield's JSON Schema.\n", + "description": "Input field values for built-in events. Keys must match the event's\nschema_fields definitions. Values are validated against each\nfield's JSON Schema. Ignored for custom events, whose fields are\nalways projected from the entity graph.\n", "additionalProperties": true }, "skip_hydration": { @@ -1276,7 +1821,14 @@ }, "_trigger_source": { "type": "string", - "description": "Identifier of the specific trigger source.\nExamples: user ID, automation execution ID, activity ID, portal user email\nDefaults to the calling user ID if not specified.\n" + "description": "Identifier of the specific trigger source.\nExamples: user ID, automation execution ID, activity ID, portal user email\nDefaults to the calling user ID if not specified.\nRequired for events marked `automation_trigger_only` and must remain stable across\naction retries.\n" + }, + "_automation_chain": { + "type": "array", + "description": "Ordered automation flow ids that caused this trigger (at most 100); propagated verbatim onto the published event for automation loop prevention.", + "items": { + "type": "string" + } } } }, @@ -1286,7 +1838,7 @@ "properties": { "success": { "type": "boolean", - "description": "Whether the event was triggered successfully" + "description": "Whether the event was published successfully" }, "event_id": { "type": "string", @@ -1294,7 +1846,7 @@ }, "event_bridge_event_id": { "type": "string", - "description": "EventBridge event ID from publishing" + "description": "EventBridge event ID when synchronous delivery or a completed retry provides it" } }, "required": [ diff --git a/packages/cli/definitions/file.json b/packages/cli/definitions/file.json index 34d654638..486cb4604 100644 --- a/packages/cli/definitions/file.json +++ b/packages/cli/definitions/file.json @@ -2,7 +2,7 @@ "openapi": "3.0.3", "info": { "title": "File API", - "version": "1.10.0", + "version": "1.13.0", "description": "The File API enables you to upload, store, manage, and share files within the epilot platform.\n\n## Key Features\n- **Upload files** to temporary storage and save them permanently as File entities\n- **Generate previews** (thumbnails) for images and documents\n- **Create public links** to share private files externally\n- **Organize files** into collections for better management\n- **Version control** with automatic file versioning on updates\n\n## File Upload Workflow\n1. Call `uploadFileV2` to get a pre-signed S3 URL\n2. Upload your file directly to S3 using the pre-signed URL (PUT request)\n3. Call `saveFileV2` with the S3 reference to create a permanent File entity\n\n## Changelog\nView API Changelog\n" }, "tags": [ @@ -124,6 +124,9 @@ "401": { "$ref": "#/components/responses/UnauthorizedError" }, + "422": { + "$ref": "#/components/responses/UnprocessableEntityError" + }, "500": { "$ref": "#/components/responses/InternalServerError" } @@ -281,6 +284,9 @@ "404": { "$ref": "#/components/responses/NotFoundError" }, + "422": { + "$ref": "#/components/responses/UnprocessableEntityError" + }, "500": { "$ref": "#/components/responses/InternalServerError" } @@ -366,6 +372,9 @@ "401": { "$ref": "#/components/responses/UnauthorizedError" }, + "422": { + "$ref": "#/components/responses/UnprocessableEntityError" + }, "500": { "$ref": "#/components/responses/InternalServerError" } @@ -431,6 +440,9 @@ "401": { "$ref": "#/components/responses/UnauthorizedError" }, + "422": { + "$ref": "#/components/responses/UnprocessableEntityError" + }, "500": { "$ref": "#/components/responses/InternalServerError" } @@ -557,7 +569,7 @@ "get": { "operationId": "downloadFile", "summary": "downloadFile", - "description": "Generate a pre-signed download URL for a file.\n\nThe returned URL is valid for a limited time (typically 15 minutes) and can be used to download the file directly.\n", + "description": "Generate a pre-signed download URL for a file.\n\nThe returned URL is valid for a limited time (typically 15 minutes) and can be used to download the file directly.\n\n**Download attribution (`x-track-*`):** any query parameter whose name starts with\n`x-track-` is treated as opaque tracking metadata and signed into the returned URL, so it\nshows up in the S3 access logs / CloudTrail data events when the link is actually opened.\nThis is a generic, service-agnostic channel — e.g. the customer portal sends\n`x-track-portal-id` and `x-track-contact-id`. Only the `x-track-` namespace is forwarded;\nthe values cannot affect how S3 resolves or serves the object. Parameter names must match\n`^x-track-[a-z0-9](?:[a-z0-9_-]{0,38}[a-z0-9])?$`, values are capped at 256 characters, and\nat most 10 such parameters are honoured (see the `x-track` parameter below).\n", "tags": [ "File" ], @@ -588,6 +600,20 @@ "type": "boolean", "default": true } + }, + { + "name": "x-track", + "in": "query", + "required": false, + "style": "form", + "explode": true, + "description": "Download-attribution channel. Provide any number of query parameters whose names\nstart with `x-track-` (e.g. `x-track-portal-id`, `x-track-contact-id`); each is signed\ninto the pre-signed URL verbatim so the download is recorded in the S3 access logs /\nCloudTrail data events. Only the `x-track-` namespace is forwarded — values cannot\naffect how S3 resolves or serves the object. Parameter names must match\n`^x-track-[a-z0-9](?:[a-z0-9_-]{0,38}[a-z0-9])?$`, values are capped at 256 characters,\nand at most 10 such parameters are honoured.\n", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } } ], "responses": { @@ -640,11 +666,11 @@ "name": "s3_key", "in": "query", "required": true, - "description": "The S3 object key", + "description": "The S3 object key, as returned in `s3ref.key` of the file entity.\n\nNote: object keys store the filename segment percent-encoded. For example, a file named\n`Straße 1.pdf` is stored under the key `.../Stra%C3%9Fe%201.pdf`, and that is the value\n`s3ref.key` contains. Pass it exactly as returned by the API and make sure your HTTP client\nURL-encodes the query parameter value, so a literal `%` arrives encoded as `%25`.\n", "schema": { "type": "string" }, - "example": "123/4d689aeb-1497-4410-a9fe-b36ca9ac4389/document.pdf" + "example": "123/4d689aeb-1497-4410-a9fe-b36ca9ac4389/Stra%C3%9Fe%201.pdf" }, { "name": "s3_bucket", @@ -691,6 +717,9 @@ "401": { "$ref": "#/components/responses/UnauthorizedError" }, + "403": { + "$ref": "#/components/responses/ForbiddenError" + }, "404": { "$ref": "#/components/responses/NotFoundError" }, @@ -875,11 +904,11 @@ } } }, - "/v1/files/{id}/summary:generate": { - "post": { - "operationId": "generateFileSummary", - "summary": "generateFileSummary", - "description": "Request AI generation of preview and short summaries for a file entity.", + "/v1/files/{id}/summary": { + "get": { + "operationId": "getFileSummary", + "summary": "getFileSummary", + "description": "Get summary text for a file entity together with the current summary job status when available.", "tags": [ "File" ], @@ -895,12 +924,12 @@ } ], "responses": { - "202": { - "description": "Summary generation accepted", + "200": { + "description": "Current file summary state", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FileSummaryGenerationStatus" + "$ref": "#/components/schemas/FileSummary" } } } @@ -917,22 +946,19 @@ "404": { "$ref": "#/components/responses/NotFoundError" }, - "409": { - "$ref": "#/components/responses/ConflictError" - }, "500": { "$ref": "#/components/responses/InternalServerError" } } } }, - "/v1/files/{id}/preview": { + "/v1/files/{id}/summary/feedback": { "get": { - "operationId": "previewFile", - "summary": "previewFile", - "description": "Generate a thumbnail preview for a file entity.\n\nSupported file types include images (PNG, JPEG, GIF, WebP), PDFs, and common document formats.\nThe preview is returned as an image (PNG or JPEG).\n\n**Tip:** Use with CookieAuth to embed previews directly in `` tags.\n", + "operationId": "getFileSummaryFeedback", + "summary": "Get file summary feedback", + "description": "Get the authenticated user's feedback for the current generated file summary.", "tags": [ - "Preview" + "File" ], "parameters": [ { @@ -943,56 +969,28 @@ "schema": { "$ref": "#/components/schemas/FileEntityId" } - }, - { - "name": "version", - "in": "query", - "description": "Index of the file version to preview (0 = latest)", - "schema": { - "type": "integer", - "default": 0 - } - }, - { - "name": "w", - "in": "query", - "description": "Desired width in pixels (maintains aspect ratio if only width is specified)", - "schema": { - "type": "integer" - }, - "example": 200 - }, - { - "name": "h", - "in": "query", - "description": "Desired height in pixels (maintains aspect ratio if only height is specified)", - "schema": { - "type": "integer" - }, - "example": 200 } ], "responses": { "200": { - "description": "Generated thumbnail image", + "description": "Current feedback state", "content": { - "image/png": { - "schema": { - "type": "string", - "format": "binary" - } - }, - "image/jpeg": { + "application/json": { "schema": { - "type": "string", - "format": "binary" + "$ref": "#/components/schemas/FileSummaryFeedbackResponse" } } } }, + "400": { + "$ref": "#/components/responses/BadRequestError" + }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, + "403": { + "$ref": "#/components/responses/ForbiddenError" + }, "404": { "$ref": "#/components/responses/NotFoundError" }, @@ -1000,67 +998,42 @@ "$ref": "#/components/responses/InternalServerError" } } - } - }, - "/v1/files:previewS3": { - "post": { - "operationId": "previewS3File", - "summary": "previewS3File", - "description": "Generate a thumbnail preview from an S3 reference.\n\nUse this endpoint when you have the S3 bucket and key but not the file entity ID.\n", + }, + "put": { + "operationId": "putFileSummaryFeedback", + "summary": "Submit file summary feedback", + "description": "Upsert thumbs up/down feedback for the current generated file summary.", "tags": [ - "Preview" + "File" ], "parameters": [ { - "name": "w", - "in": "query", - "description": "Desired width in pixels", - "schema": { - "type": "integer" - }, - "example": 200 - }, - { - "name": "h", - "in": "query", - "description": "Desired height in pixels", + "name": "id", + "in": "path", + "required": true, + "description": "The UUID of the file entity", "schema": { - "type": "integer" - }, - "example": 200 + "$ref": "#/components/schemas/FileEntityId" + } } ], "requestBody": { + "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/S3Ref" - }, - "examples": { - "Preview from S3": { - "value": { - "bucket": "epilot-prod-user-content", - "key": "123/4d689aeb-1497-4410-a9fe-b36ca9ac4389/image.png" - } - } + "$ref": "#/components/schemas/PutFileSummaryFeedbackRequest" } } } }, "responses": { "200": { - "description": "Generated thumbnail image", + "description": "Feedback recorded", "content": { - "image/png": { - "schema": { - "type": "string", - "format": "binary" - } - }, - "image/jpeg": { + "application/json": { "schema": { - "type": "string", - "format": "binary" + "$ref": "#/components/schemas/FileSummaryFeedbackResponse" } } } @@ -1071,75 +1044,95 @@ "401": { "$ref": "#/components/responses/UnauthorizedError" }, + "403": { + "$ref": "#/components/responses/ForbiddenError" + }, "404": { "$ref": "#/components/responses/NotFoundError" }, + "409": { + "$ref": "#/components/responses/ConflictError" + }, "500": { "$ref": "#/components/responses/InternalServerError" } } - }, - "get": { - "operationId": "previewS3FileGet", - "summary": "previewS3FileGet", - "description": "Get a thumbnail preview from an S3 reference using query parameters.\n\nThis GET variant is useful for embedding previews directly in `` tags.\n", + } + }, + "/v1/files/{id}/summary-jobs": { + "post": { + "operationId": "createFileSummaryJob", + "summary": "createFileSummaryJob", + "description": "Create or return the current AI summary job for a file entity.", "tags": [ - "Preview" + "File" ], "parameters": [ { - "name": "key", - "in": "query", - "description": "The S3 object key", + "name": "id", + "in": "path", "required": true, + "description": "The UUID of the file entity", "schema": { - "type": "string" - }, - "example": "123/4d689aeb-1497-4410-a9fe-b36ca9ac4389/image.png" + "$ref": "#/components/schemas/FileEntityId" + } + } + ], + "responses": { + "202": { + "description": "Summary job accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileSummaryJob" + } + } + } }, - { - "name": "bucket", - "in": "query", - "description": "The S3 bucket name", - "required": true, - "schema": { - "type": "string" - }, - "example": "epilot-prod-user-content" + "400": { + "$ref": "#/components/responses/BadRequestError" }, - { - "name": "w", - "in": "query", - "description": "Desired width in pixels", - "schema": { - "type": "integer" - }, - "example": 200 + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "403": { + "$ref": "#/components/responses/ForbiddenError" + }, + "404": { + "$ref": "#/components/responses/NotFoundError" }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/v1/files/{id}/summary-jobs/current": { + "get": { + "operationId": "getCurrentFileSummaryJob", + "summary": "getCurrentFileSummaryJob", + "description": "Get the latest AI summary job for the file entity's current source.", + "tags": [ + "File" + ], + "parameters": [ { - "name": "h", - "in": "query", - "description": "Desired height in pixels", + "name": "id", + "in": "path", + "required": true, + "description": "The UUID of the file entity", "schema": { - "type": "integer" - }, - "example": 200 + "$ref": "#/components/schemas/FileEntityId" + } } ], "responses": { "200": { - "description": "Generated thumbnail image", + "description": "Current summary job", "content": { - "image/png": { - "schema": { - "type": "string", - "format": "binary" - } - }, - "image/jpeg": { + "application/json": { "schema": { - "type": "string", - "format": "binary" + "$ref": "#/components/schemas/FileSummaryJob" } } } @@ -1150,6 +1143,9 @@ "401": { "$ref": "#/components/responses/UnauthorizedError" }, + "403": { + "$ref": "#/components/responses/ForbiddenError" + }, "404": { "$ref": "#/components/responses/NotFoundError" }, @@ -1159,124 +1155,54 @@ } } }, - "/v1/files/public/{id}/preview": { + "/v1/files/{id}/summary-jobs/{job_id}": { "get": { - "operationId": "previewPublicFile", - "summary": "previewPublicFile", - "description": "Generate a thumbnail preview for a public file entity.\n\n**No authentication required.** This endpoint only works for files with `access_control: public-read`.\n", - "security": [], + "operationId": "getFileSummaryJob", + "summary": "getFileSummaryJob", + "description": "Get an AI summary job by id.", "tags": [ - "Preview" + "File" ], "parameters": [ { "name": "id", "in": "path", "required": true, - "description": "The UUID of the public file entity", + "description": "The UUID of the file entity", "schema": { "$ref": "#/components/schemas/FileEntityId" } }, { - "name": "version", - "in": "query", - "description": "Index of the file version to preview (0 = latest)", + "name": "job_id", + "in": "path", + "required": true, + "description": "The UUID of the summary job", "schema": { - "type": "integer", - "default": 0 + "type": "string", + "format": "uuid" } - }, - { - "name": "w", - "in": "query", - "description": "Desired width in pixels", - "schema": { - "type": "integer" - }, - "example": 200 - }, - { - "name": "h", - "in": "query", - "description": "Desired height in pixels", - "schema": { - "type": "integer" - }, - "example": 200 - }, - { - "name": "org_id", - "in": "query", - "description": "Organization ID that owns the file", - "schema": { - "type": "string" - }, - "example": "123" } ], "responses": { "200": { - "description": "Generated thumbnail image for a public file", - "headers": { - "ETag": { - "description": "Identifier of the previewed file version, for conditional requests", - "schema": { - "type": "string" - } - }, - "Cache-Control": { - "description": "Caching policy for the preview", - "schema": { - "type": "string" - } - } - }, + "description": "Summary job", "content": { - "image/png": { - "schema": { - "type": "string", - "format": "binary" - } - }, - "image/jpeg": { + "application/json": { "schema": { - "type": "string", - "format": "binary" + "$ref": "#/components/schemas/FileSummaryJob" } } } }, - "304": { - "description": "The preview is unchanged since the version identified by the `If-None-Match`\nrequest header. No body is returned; the cached copy should be reused.\n", - "headers": { - "ETag": { - "description": "Identifier of the current file version's preview", - "schema": { - "type": "string" - } - }, - "Cache-Control": { - "description": "Caching policy for the preview", - "schema": { - "type": "string" - } - } - } + "400": { + "$ref": "#/components/responses/BadRequestError" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" }, "403": { - "description": "File is not public", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorObject" - }, - "example": { - "status": 403, - "error": "File is not publicly accessible" - } - } - } + "$ref": "#/components/responses/ForbiddenError" }, "404": { "$ref": "#/components/responses/NotFoundError" @@ -1287,78 +1213,87 @@ } } }, - "/v1/files/session": { - "get": { - "operationId": "getSession", - "summary": "getSession", - "description": "Start a browser session by converting a Bearer token into a server-side cookie.\n\n**Use case:** After calling this endpoint, you can use preview URLs directly in `` tags\nwithout needing to set the Authorization header manually.\n\n**Example flow:**\n1. Call this endpoint with your Bearer token: `GET /v1/files/session` with `Authorization: Bearer `\n2. The server sets an HTTP-only cookie named `token`\n3. Use preview URLs directly: ``\n", + "/v1/files/{id}/summary:generate": { + "post": { + "operationId": "generateFileSummary", + "summary": "generateFileSummary", + "description": "Compatibility alias for creating or returning the current AI summary job for a file entity.", "tags": [ - "Session" + "File" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The UUID of the file entity", + "schema": { + "$ref": "#/components/schemas/FileEntityId" + } + } ], "responses": { - "200": { - "description": "Session started successfully. A session cookie has been set.", - "headers": { - "Set-Cookie": { - "description": "HTTP-only session cookie containing the authentication token", + "202": { + "description": "Summary generation accepted", + "content": { + "application/json": { "schema": { - "type": "string", - "example": "token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...; HttpOnly; Secure; Path=/" + "$ref": "#/components/schemas/FileSummaryJob" } } } }, + "400": { + "$ref": "#/components/responses/BadRequestError" + }, "401": { "$ref": "#/components/responses/UnauthorizedError" - } - } - }, - "delete": { - "operationId": "deleteSession", - "summary": "deleteSession", - "description": "End a browser session by deleting the token cookie.\n\nCall this endpoint to log out and clear the session cookie.\n", - "tags": [ - "Session" - ], - "responses": { - "200": { - "description": "Session deleted successfully. The session cookie has been cleared." + }, + "403": { + "$ref": "#/components/responses/ForbiddenError" + }, + "404": { + "$ref": "#/components/responses/NotFoundError" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" } } } }, - "/v1/files/{id}/public/links": { - "post": { - "operationId": "generatePublicLink", - "summary": "generatePublicLink", - "description": "Generate a public link to share a private file externally.\n\nThe generated link:\n- Is permanent until explicitly revoked\n- Includes the filename for user-friendly URLs\n- Does not require authentication to access\n- Redirects to a signed download URL when accessed\n\n**Use case:** Share invoices, contracts, or documents with external parties who don't have epilot accounts.\n", + "/v1/files/{id}/text": { + "get": { + "operationId": "getFileText", + "summary": "getFileText", + "description": "Get the plain-text representation of a file entity. Returns status `not_ready` while text is being prepared and `unsupported` when text is unavailable for the file or organization.", "tags": [ - "Public Links" + "File" ], "parameters": [ { "name": "id", "in": "path", "required": true, - "description": "The UUID of the file entity to share", + "description": "The UUID of the file entity", "schema": { "$ref": "#/components/schemas/FileEntityId" } } ], "responses": { - "201": { - "description": "Public link generated successfully", + "200": { + "description": "Text state for the file entity", "content": { "application/json": { "schema": { - "type": "string", - "description": "The public URL that can be shared externally" - }, - "example": "https://file.sls.epilot.io/v1/files/public/links/3ef5c6d9-818d-45e6-8efb-b1de59079a1c/invoice-2023-12.pdf" + "$ref": "#/components/schemas/FileText" + } } } }, + "400": { + "$ref": "#/components/responses/BadRequestError" + }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, @@ -1372,205 +1307,193 @@ "$ref": "#/components/responses/InternalServerError" } } - }, - "get": { - "operationId": "listPublicLinksForFile", - "summary": "listPublicLinksForFile", - "description": "Fetches all public links previously generated for a file", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "description": "The UUID of the file entity", - "schema": { - "type": "string" - }, - "example": "13d22918-36bd-4227-9ad4-2cb978788c8d" - } - ], + } + }, + "/v1/files/validations": { + "post": { + "operationId": "validateFile", + "summary": "validateFile", + "description": "Validate an uploaded file against one or more document validation rules\n(validation-rules-api rules with `input_type: document`).\n\nThe file is referenced either by its file entity id or, for an upload that has not been\nsaved as an entity yet (e.g. a journey upload in the temporary prefix), by its S3 reference.\nThe service collects an observation of the file - deterministic facts such as the sniffed\nmime type, size, page count, image resolution, blank and password-protection detection -\nand evaluates every rule against it with the shared validation rules engine.\n\nThe response carries one result per rule with a per-condition outcome (`pass`, `fail`,\n`uncertain`, `skipped`), plus the observation the results were derived from. `status` is\n`completed` for checks that finish synchronously; `pending` is reserved for check levels\nthat need asynchronous content extraction.\n", "tags": [ - "Public Links" + "File" ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileValidationRequest" + } + } + } + }, "responses": { "200": { - "description": "Public links retrieved successfully", + "description": "The validation result", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PublicLink" - } - } - } - }, - "example": { - "results": [ - { - "id": "3ef5c6d9-818d-45e6-8efb-b1de59079a1c", - "link": "https://file.sls.epilot.io/v1/files/public/links/3ef5c6d9-818d-45e6-8efb-b1de59079a1c/invoice.pdf", - "last_accessed_at": "2024-01-15T10:30:00Z" - } - ] + "$ref": "#/components/schemas/FileValidation" } } } }, - "501": { - "description": "Not implemented", + "400": { + "$ref": "#/components/responses/BadRequestError" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "403": { + "$ref": "#/components/responses/ForbiddenError" + }, + "404": { + "$ref": "#/components/responses/NotFoundError" + }, + "413": { + "description": "The file is too large to validate", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorObject" + } + } + } + }, + "422": { + "description": "The file could not be read", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorObject" - }, - "example": { - "status": 501, - "error": "Not Implemented" } } } + }, + "500": { + "$ref": "#/components/responses/InternalServerError" } } } }, - "/v1/files/public/links/{id}/{filename}": { + "/v1/files/{id}/preview": { "get": { - "operationId": "accessPublicLink", - "summary": "accessPublicLink", - "security": [], + "operationId": "previewFile", + "summary": "previewFile", + "description": "Generate a thumbnail preview for a file entity.\n\nSupported file types include images (PNG, JPEG, GIF, WebP), PDFs, and common document formats.\nThe preview is returned as an image (PNG or JPEG).\n\n**Tip:** Use with CookieAuth to embed previews directly in `` tags.\n", "tags": [ - "Public Links" + "Preview" ], - "description": "Access a file via its public link.\n\n**No authentication required.** This endpoint redirects to a signed S3 URL for downloading the file.\n\nThe filename in the URL is for user-friendliness and SEO; the actual file is identified by the link ID.\n", "parameters": [ { "name": "id", "in": "path", "required": true, - "description": "The UUID of the public link (not the file entity ID)", + "description": "The UUID of the file entity", "schema": { - "type": "string" - }, - "example": "13d22918-36bd-4227-9ad4-2cb978788c8d" + "$ref": "#/components/schemas/FileEntityId" + } }, { - "name": "filename", - "in": "path", - "required": true, - "description": "The filename (for user-friendly URLs)", + "name": "version", + "in": "query", + "description": "Index of the file version to preview (0 = latest)", "schema": { - "type": "string" + "type": "integer", + "default": 0 + } + }, + { + "name": "w", + "in": "query", + "description": "Desired width in pixels (maintains aspect ratio if only width is specified)", + "schema": { + "type": "integer" }, - "example": "invoice-2023-12.pdf" + "example": 200 }, { - "name": "hash", + "name": "h", "in": "query", - "required": false, - "description": "Optional cache-busting hash to force re-download", + "description": "Desired height in pixels (maintains aspect ratio if only height is specified)", "schema": { - "type": "string" + "type": "integer" }, - "example": "abc123" + "example": 200 } ], "responses": { - "302": { - "description": "Redirect to a signed S3 URL for downloading the file", - "headers": { - "Location": { - "description": "The signed S3 URL to download the file", + "200": { + "description": "Generated thumbnail image", + "content": { + "image/png": { "schema": { "type": "string", - "format": "uri" - }, - "example": "https://epilot-prod-user-content.s3.eu-central-1.amazonaws.com/123/document.pdf?X-Amz-..." - } - } - }, - "404": { - "description": "Public link not found or has been revoked", - "content": { - "application/json": { + "format": "binary" + } + }, + "image/jpeg": { "schema": { - "$ref": "#/components/schemas/ErrorObject" - }, - "example": { - "status": 404, - "error": "Public link not found" + "type": "string", + "format": "binary" } } } }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "403": { + "$ref": "#/components/responses/ForbiddenError" + }, + "404": { + "$ref": "#/components/responses/NotFoundError" + }, "500": { "$ref": "#/components/responses/InternalServerError" } } } }, - "/v1/files/public/links/{id}": { - "delete": { - "operationId": "revokePublicLink", - "summary": "revokePublicLink", - "description": "Revokes a given public link by ID", + "/v1/files:previewS3": { + "post": { + "operationId": "previewS3File", + "summary": "previewS3File", + "description": "Generate a thumbnail preview from an S3 reference.\n\nUse this endpoint when you have the S3 bucket and key but not the file entity ID.\n", + "tags": [ + "Preview" + ], "parameters": [ { - "name": "id", - "in": "path", - "required": true, - "description": "The UUID of the public link to revoke", + "name": "w", + "in": "query", + "description": "Desired width in pixels", "schema": { - "type": "string" + "type": "integer" }, - "example": "13d22918-36bd-4227-9ad4-2cb978788c8d" - } - ], - "tags": [ - "Public Links" - ], - "responses": { - "204": { - "description": "Public link revoked successfully" + "example": 200 }, - "501": { - "description": "Not implemented", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorObject" - }, - "example": { - "status": 501, - "error": "Not Implemented" - } - } - } + { + "name": "h", + "in": "query", + "description": "Desired height in pixels", + "schema": { + "type": "integer" + }, + "example": 200 } - } - } - }, - "/v1/files/download:verify": { - "post": { - "operationId": "verifyCustomDownloadUrl", - "summary": "verifyCustomDownloadUrl", - "description": "Verify that a custom download URL is valid and has not expired.\n\nUse this endpoint to validate custom download URLs before redirecting users.\nCustom download URLs include a signature and expiration time for security.\n", - "tags": [ - "File" ], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/VerifyCustomDownloadUrlPayload" + "$ref": "#/components/schemas/S3Ref" }, "examples": { - "Verify URL": { + "Preview from S3": { "value": { - "custom_download_url": "https://some-api-url.com?file_id=123&expires_at=1699273500029&signature=abcdefg" + "bucket": "epilot-prod-user-content", + "key": "123/4d689aeb-1497-4410-a9fe-b36ca9ac4389/image.png" } } } @@ -1579,29 +1502,18 @@ }, "responses": { "200": { - "description": "Verification result", + "description": "Generated thumbnail image", "content": { - "application/json": { + "image/png": { "schema": { - "type": "object", - "properties": { - "valid": { - "type": "boolean", - "description": "Whether the URL is valid and not expired" - } - } - }, - "examples": { - "Valid URL": { - "value": { - "valid": true - } - }, - "Invalid URL": { - "value": { - "valid": false - } - } + "type": "string", + "format": "binary" + } + }, + "image/jpeg": { + "schema": { + "type": "string", + "format": "binary" } } } @@ -1612,82 +1524,75 @@ "401": { "$ref": "#/components/responses/UnauthorizedError" }, + "404": { + "$ref": "#/components/responses/NotFoundError" + }, "500": { "$ref": "#/components/responses/InternalServerError" } } - } - }, - "/v1/files/public/upload": { - "post": { - "operationId": "uploadFilePublic", - "summary": "uploadFilePublic", - "security": [], - "description": "Create a pre-signed S3 URL for uploading a file without authentication.\n\n**No authentication required.** This endpoint is intended for public-facing forms and journeys\nwhere end-users need to upload files without logging in.\n\nThe uploaded file is stored temporarily (one week). Use `saveFileV2` with proper authentication\nto store the file permanently.\n\n**Security note:** Files uploaded via this endpoint are temporary and require authenticated\naccess to be saved permanently.\n", + }, + "get": { + "operationId": "previewS3FileGet", + "summary": "previewS3FileGet", + "description": "Get a thumbnail preview from an S3 reference using query parameters.\n\nThis GET variant is useful for embedding previews directly in `` tags.\n", "tags": [ - "File" + "Preview" ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UploadFilePayload" - }, - "examples": { - "Upload an image": { - "description": "Upload an image file", - "value": { - "filename": "image.png", - "mime_type": "image/png" - } - }, - "Upload a document": { - "description": "Upload a PDF document", - "value": { - "filename": "document.pdf", - "mime_type": "application/pdf" - } - } - } - } + "parameters": [ + { + "name": "key", + "in": "query", + "description": "The S3 object key", + "required": true, + "schema": { + "type": "string" + }, + "example": "123/4d689aeb-1497-4410-a9fe-b36ca9ac4389/image.png" + }, + { + "name": "bucket", + "in": "query", + "description": "The S3 bucket name", + "required": true, + "schema": { + "type": "string" + }, + "example": "epilot-prod-user-content" + }, + { + "name": "w", + "in": "query", + "description": "Desired width in pixels", + "schema": { + "type": "integer" + }, + "example": 200 + }, + { + "name": "h", + "in": "query", + "description": "Desired height in pixels", + "schema": { + "type": "integer" + }, + "example": 200 } - }, + ], "responses": { - "201": { - "description": "Pre-signed URL for uploading the file", + "200": { + "description": "Generated thumbnail image", "content": { - "application/json": { + "image/png": { "schema": { - "type": "object", - "properties": { - "s3ref": { - "allOf": [ - { - "$ref": "#/components/schemas/S3Ref" - }, - { - "description": "S3 reference to use when saving the file permanently" - }, - { - "example": { - "bucket": "epilot-prod-user-content", - "key": "123/temp/4d689aeb-1497-4410-a9fe-b36ca9ac4389/document.pdf" - } - } - ] - }, - "upload_url": { - "type": "string", - "format": "url", - "description": "Pre-signed URL for uploading the file via PUT request", - "example": "https://epilot-prod-user-content.s3.eu-central-1.amazonaws.com/123/temp/4d689aeb-1497-4410-a9fe-b36ca9ac4389/document.pdf?AWSParams=123" - }, - "error": { - "type": "string", - "description": "Error message if the upload preparation failed", - "example": "File entity not found" - } - } + "type": "string", + "format": "binary" + } + }, + "image/jpeg": { + "schema": { + "type": "string", + "format": "binary" } } } @@ -1695,127 +1600,160 @@ "400": { "$ref": "#/components/responses/BadRequestError" }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "$ref": "#/components/responses/NotFoundError" + }, "500": { "$ref": "#/components/responses/InternalServerError" } } } }, - "/v1/{slug}/collections": { + "/v1/files/public/{id}/preview": { "get": { - "operationId": "getUserSchemaFileCollections", - "summary": "getUserSchemaFileCollections", - "description": "Get all file collections for the current user within a specific schema.\n\nCollections help organize files into logical groups (e.g., \"Contracts\", \"Invoices\").\nUser collections are private to the creating user.\n", + "operationId": "previewPublicFile", + "summary": "previewPublicFile", + "description": "Generate a thumbnail preview for a public file entity.\n\n**No authentication required.** This endpoint only works for files with `access_control: public-read`.\n", + "security": [], "tags": [ - "File Collections" + "Preview" ], "parameters": [ { - "name": "slug", + "name": "id", "in": "path", "required": true, - "description": "The entity schema slug (e.g., order, opportunity, contact)", + "description": "The UUID of the public file entity", + "schema": { + "$ref": "#/components/schemas/FileEntityId" + } + }, + { + "name": "version", + "in": "query", + "description": "Index of the file version to preview (0 = latest)", + "schema": { + "type": "integer", + "default": 0 + } + }, + { + "name": "w", + "in": "query", + "description": "Desired width in pixels", + "schema": { + "type": "integer" + }, + "example": 200 + }, + { + "name": "h", + "in": "query", + "description": "Desired height in pixels", + "schema": { + "type": "integer" + }, + "example": 200 + }, + { + "name": "org_id", + "in": "query", + "description": "Organization ID that owns the file", "schema": { "type": "string" }, - "example": "opportunity" + "example": "123" } ], "responses": { "200": { - "description": "List of collections for the user and schema", + "description": "Generated thumbnail image for a public file", + "headers": { + "ETag": { + "description": "Identifier of the previewed file version, for conditional requests", + "schema": { + "type": "string" + } + }, + "Cache-Control": { + "description": "Caching policy for the preview", + "schema": { + "type": "string" + } + } + }, "content": { - "application/json": { + "image/png": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FileCollectionItem" - } - }, - "example": [ - { - "slug": "_system_files_collection_3fa85f64-5717-4562-b3fc-2c963f66afa6_10234:documents", - "name": "Documents", - "id": "ef7d985c-2385-44f4-9c71-ae06a52264f8", - "parents": [], - "starred": false, - "order": 0, - "created_at": "2024-01-01T12:00:00Z", - "updated_at": "2024-01-02T12:00:00Z" - } - ] + "type": "string", + "format": "binary" + } + }, + "image/jpeg": { + "schema": { + "type": "string", + "format": "binary" + } } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" + "204": { + "description": "No preview could be generated and no redirect target is available\n(the file has no public URL). No body is returned.\n" }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - } - }, - "post": { - "operationId": "createUserSchemaFileCollection", - "summary": "createUserSchemaFileCollection", - "description": "Create a new file collection for the current user within a specific schema.\n\nThe collection will be private to the creating user and associated with the specified schema.\n", - "tags": [ - "File Collections" - ], - "parameters": [ - { - "name": "slug", - "in": "path", - "required": true, - "description": "The entity schema slug (e.g., order, opportunity, contact)", - "schema": { - "type": "string" - }, - "example": "opportunity" - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FileCollectionCreateRequest" + "302": { + "description": "The preview could not be optimized (e.g. image dimensions above the input\npixel limit, an oversized file, or an unsupported/undecodable format).\nSince the file is public, the client is redirected to the original file,\nwhich the browser can render directly.\n", + "headers": { + "Location": { + "description": "URL of the original public file", + "schema": { + "type": "string", + "format": "uri" + } }, - "examples": { - "Create collection": { - "value": { - "name": "Contracts", - "starred": false - } - }, - "Create nested collection": { - "value": { - "name": "2024 Contracts", - "parents": [ - "_system_files_collection_3fa85f64-5717-4562-b3fc-2c963f66afa6_10234:contracts" - ] - } + "Cache-Control": { + "description": "Caching policy for the redirect", + "schema": { + "type": "string" } } } - } - }, - "responses": { - "201": { - "description": "File collection created successfully", + }, + "304": { + "description": "The preview is unchanged since the version identified by the `If-None-Match`\nrequest header. No body is returned; the cached copy should be reused.\n", + "headers": { + "ETag": { + "description": "Identifier of the current file version's preview", + "schema": { + "type": "string" + } + }, + "Cache-Control": { + "description": "Caching policy for the preview", + "schema": { + "type": "string" + } + } + } + }, + "403": { + "description": "File is not public", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FileCollectionItem" + "$ref": "#/components/schemas/ErrorObject" + }, + "example": { + "status": 403, + "error": "File is not publicly accessible" } } } }, - "400": { - "$ref": "#/components/responses/BadRequestError" - }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" + "404": { + "$ref": "#/components/responses/NotFoundError" }, "500": { "$ref": "#/components/responses/InternalServerError" @@ -1823,119 +1761,84 @@ } } }, - "/v1/{slug}/collections/{collectionSlug}": { - "put": { - "operationId": "updateUserSchemaFileCollection", - "summary": "updateUserSchemaFileCollection", - "description": "Update an existing file collection.\n\nYou can update the name, parent relationships, starred status, and enabled locations/purposes.\n", + "/v1/files/session": { + "get": { + "operationId": "getSession", + "summary": "getSession", + "description": "Start a browser session by converting a Bearer token into a server-side cookie.\n\n**Use case:** After calling this endpoint, you can use preview URLs directly in `` tags\nwithout needing to set the Authorization header manually.\n\n**Example flow:**\n1. Call this endpoint with your Bearer token: `GET /v1/files/session` with `Authorization: Bearer `\n2. The server sets an HTTP-only cookie named `token`\n3. Use preview URLs directly: ``\n", "tags": [ - "File Collections" + "Session" ], - "parameters": [ - { - "name": "slug", - "in": "path", - "required": true, - "description": "The entity schema slug (e.g., order, opportunity)", - "schema": { - "type": "string" - }, - "example": "opportunity" - }, - { - "name": "collectionSlug", - "in": "path", - "required": true, - "description": "The collection slug identifier", - "schema": { - "type": "string" - }, - "example": "documents" - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FileCollectionAttributes" - }, - "examples": { - "Rename collection": { - "value": { - "name": "Important Documents" - } - }, - "Star collection": { - "value": { - "starred": true - } - } - } - } - } - }, "responses": { "200": { - "description": "File collection updated successfully", - "content": { - "application/json": { + "description": "Session started successfully. A session cookie has been set.", + "headers": { + "Set-Cookie": { + "description": "HTTP-only session cookie containing the authentication token", "schema": { - "$ref": "#/components/schemas/FileCollectionItem" + "type": "string", + "example": "token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...; HttpOnly; Secure; Path=/" } } } }, - "400": { - "$ref": "#/components/responses/BadRequestError" - }, "401": { "$ref": "#/components/responses/UnauthorizedError" - }, - "404": { - "$ref": "#/components/responses/NotFoundError" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" } } }, "delete": { - "operationId": "deleteUserSchemaFileCollection", - "summary": "deleteUserSchemaFileCollection", - "description": "Delete a file collection.\n\n**Note:** Deleting a collection does not delete the files within it.\nFiles will remain but will no longer be associated with this collection.\n", + "operationId": "deleteSession", + "summary": "deleteSession", + "description": "End a browser session by deleting the token cookie.\n\nCall this endpoint to log out and clear the session cookie.\n", "tags": [ - "File Collections" + "Session" + ], + "responses": { + "200": { + "description": "Session deleted successfully. The session cookie has been cleared." + } + } + } + }, + "/v1/files/{id}/public/links": { + "post": { + "operationId": "generatePublicLink", + "summary": "generatePublicLink", + "description": "Generate a public link to share a private file externally.\n\nThe generated link:\n- Is permanent until explicitly revoked\n- Includes the filename for user-friendly URLs\n- Does not require authentication to access\n- Redirects to a signed download URL when accessed\n\n**Use case:** Share invoices, contracts, or documents with external parties who don't have epilot accounts.\n", + "tags": [ + "Public Links" ], "parameters": [ { - "name": "slug", - "in": "path", - "required": true, - "description": "The entity schema slug (e.g., order, opportunity)", - "schema": { - "type": "string" - }, - "example": "opportunity" - }, - { - "name": "collectionSlug", + "name": "id", "in": "path", "required": true, - "description": "The collection slug identifier", + "description": "The UUID of the file entity to share", "schema": { - "type": "string" - }, - "example": "documents" + "$ref": "#/components/schemas/FileEntityId" + } } ], "responses": { - "200": { - "description": "File collection deleted successfully" + "201": { + "description": "Public link generated successfully", + "content": { + "application/json": { + "schema": { + "type": "string", + "description": "The public URL that can be shared externally" + }, + "example": "https://file.sls.epilot.io/v1/files/public/links/3ef5c6d9-818d-45e6-8efb-b1de59079a1c/invoice-2023-12.pdf" + } + } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, + "403": { + "$ref": "#/components/responses/ForbiddenError" + }, "404": { "$ref": "#/components/responses/NotFoundError" }, @@ -1943,64 +1846,128 @@ "$ref": "#/components/responses/InternalServerError" } } - } - }, - "/v1/entity/{id}/collections/{collectionSlug}/files": { + }, "get": { - "operationId": "getFilesInCollection", - "summary": "getFilesInCollection", - "description": "Get all files within a specific collection for an entity.\n\nThe schema is automatically derived from the entity. This endpoint requires\nview permission on the parent entity to access its files.\n", - "tags": [ - "File Collections" - ], + "operationId": "listPublicLinksForFile", + "summary": "listPublicLinksForFile", + "description": "Fetches all public links previously generated for a file", "parameters": [ { - "$ref": "#/components/parameters/EntityIdPathParam" - }, - { - "name": "collectionSlug", + "name": "id", "in": "path", "required": true, - "description": "The collection slug identifier", + "description": "The UUID of the file entity", "schema": { "type": "string" }, - "example": "documents" + "example": "13d22918-36bd-4227-9ad4-2cb978788c8d" } ], + "tags": [ + "Public Links" + ], "responses": { "200": { - "description": "List of files in the collection", + "description": "Public links retrieved successfully", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FileEntity" + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PublicLink" + } + } } + }, + "example": { + "results": [ + { + "id": "3ef5c6d9-818d-45e6-8efb-b1de59079a1c", + "link": "https://file.sls.epilot.io/v1/files/public/links/3ef5c6d9-818d-45e6-8efb-b1de59079a1c/invoice.pdf", + "last_accessed_at": "2024-01-15T10:30:00Z" + } + ] } } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, - "403": { - "description": "Insufficient permissions to view the entity's files", + "501": { + "description": "Not implemented", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorObject" }, "example": { - "status": 403, - "error": "User must have permission to view this entity to access its files" + "status": 501, + "error": "Not Implemented" } } } + } + } + } + }, + "/v1/files/public/links/{id}/{filename}": { + "get": { + "operationId": "accessPublicLink", + "summary": "accessPublicLink", + "security": [], + "tags": [ + "Public Links" + ], + "description": "Access a file via its public link.\n\n**No authentication required.** This endpoint redirects to a signed S3 URL for downloading the file.\n\nThe filename in the URL is for user-friendliness and SEO; the actual file is identified by the link ID.\n", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The UUID of the public link (not the file entity ID)", + "schema": { + "type": "string" + }, + "example": "13d22918-36bd-4227-9ad4-2cb978788c8d" + }, + { + "name": "filename", + "in": "path", + "required": true, + "description": "The filename (for user-friendly URLs)", + "schema": { + "type": "string" + }, + "example": "invoice-2023-12.pdf" + }, + { + "name": "hash", + "in": "query", + "required": false, + "description": "Optional cache-busting hash to force re-download", + "schema": { + "type": "string" + }, + "example": "abc123" + } + ], + "responses": { + "302": { + "description": "Redirect to a signed S3 URL for downloading the file", + "headers": { + "Location": { + "description": "The signed S3 URL to download the file", + "schema": { + "type": "string", + "format": "uri" + }, + "example": "https://epilot-prod-user-content.s3.eu-central-1.amazonaws.com/123/document.pdf?X-Amz-..." + } + } }, "404": { - "description": "Entity or collection not found", + "description": "Public link not found or has been revoked", "content": { "application/json": { "schema": { @@ -2008,7 +1975,7 @@ }, "example": { "status": 404, - "error": "Entity not found" + "error": "Public link not found" } } } @@ -2019,29 +1986,221 @@ } } }, - "/v1/collections/{schemaSlug}": { - "get": { - "operationId": "getGlobalFileCollections", - "summary": "getGlobalFileCollections", - "description": "Get all global file collections for a specific schema.\n\nGlobal collections are shared across all users in the organization for the specified schema.\nUnlike user collections, these are visible to everyone with access to entities of that schema.\n", - "tags": [ - "File Collections" - ], + "/v1/files/public/links/{id}": { + "delete": { + "operationId": "revokePublicLink", + "summary": "revokePublicLink", + "description": "Revokes a given public link by ID", "parameters": [ { - "name": "schemaSlug", + "name": "id", "in": "path", "required": true, - "description": "The entity schema slug (e.g., order, opportunity, contact)", + "description": "The UUID of the public link to revoke", "schema": { "type": "string" }, - "example": "order" + "example": "13d22918-36bd-4227-9ad4-2cb978788c8d" } ], + "tags": [ + "Public Links" + ], "responses": { - "200": { - "description": "List of global collections for the schema", + "204": { + "description": "Public link revoked successfully" + }, + "501": { + "description": "Not implemented", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorObject" + }, + "example": { + "status": 501, + "error": "Not Implemented" + } + } + } + } + } + } + }, + "/v1/files/download:verify": { + "post": { + "operationId": "verifyCustomDownloadUrl", + "summary": "verifyCustomDownloadUrl", + "description": "Verify that a custom download URL is valid and has not expired.\n\nUse this endpoint to validate custom download URLs before redirecting users.\nCustom download URLs include a signature and expiration time for security.\n", + "tags": [ + "File" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyCustomDownloadUrlPayload" + }, + "examples": { + "Verify URL": { + "value": { + "custom_download_url": "https://some-api-url.com?file_id=123&expires_at=1699273500029&signature=abcdefg" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Verification result", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "valid": { + "type": "boolean", + "description": "Whether the URL is valid and not expired" + } + } + }, + "examples": { + "Valid URL": { + "value": { + "valid": true + } + }, + "Invalid URL": { + "value": { + "valid": false + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequestError" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/v1/files/public/upload": { + "post": { + "operationId": "uploadFilePublic", + "summary": "uploadFilePublic", + "security": [], + "description": "Create a pre-signed S3 URL for uploading a file without authentication.\n\n**No authentication required.** This endpoint is intended for public-facing forms and journeys\nwhere end-users need to upload files without logging in.\n\nThe uploaded file is stored temporarily (one week). Use `saveFileV2` with proper authentication\nto store the file permanently.\n\n**Security note:** Files uploaded via this endpoint are temporary and require authenticated\naccess to be saved permanently.\n", + "tags": [ + "File" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UploadFilePayload" + }, + "examples": { + "Upload an image": { + "description": "Upload an image file", + "value": { + "filename": "image.png", + "mime_type": "image/png" + } + }, + "Upload a document": { + "description": "Upload a PDF document", + "value": { + "filename": "document.pdf", + "mime_type": "application/pdf" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Pre-signed URL for uploading the file", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "s3ref": { + "allOf": [ + { + "$ref": "#/components/schemas/S3Ref" + }, + { + "description": "S3 reference to use when saving the file permanently" + }, + { + "example": { + "bucket": "epilot-prod-user-content", + "key": "123/temp/4d689aeb-1497-4410-a9fe-b36ca9ac4389/document.pdf" + } + } + ] + }, + "upload_url": { + "type": "string", + "format": "url", + "description": "Pre-signed URL for uploading the file via PUT request", + "example": "https://epilot-prod-user-content.s3.eu-central-1.amazonaws.com/123/temp/4d689aeb-1497-4410-a9fe-b36ca9ac4389/document.pdf?AWSParams=123" + }, + "error": { + "type": "string", + "description": "Error message if the upload preparation failed", + "example": "File entity not found" + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequestError" + }, + "422": { + "$ref": "#/components/responses/UnprocessableEntityError" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/v1/{slug}/collections": { + "get": { + "operationId": "getUserSchemaFileCollections", + "summary": "getUserSchemaFileCollections", + "description": "Get all file collections for the current user within a specific schema.\n\nCollections help organize files into logical groups (e.g., \"Contracts\", \"Invoices\").\nUser collections are private to the creating user.\n", + "tags": [ + "File Collections" + ], + "parameters": [ + { + "name": "slug", + "in": "path", + "required": true, + "description": "The entity schema slug (e.g., order, opportunity, contact)", + "schema": { + "type": "string" + }, + "example": "opportunity" + } + ], + "responses": { + "200": { + "description": "List of collections for the user and schema", "content": { "application/json": { "schema": { @@ -2052,208 +2211,1061 @@ }, "example": [ { - "slug": "_system_files_collection_schema_order:templates", - "name": "Templates", - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "slug": "_system_files_collection_3fa85f64-5717-4562-b3fc-2c963f66afa6_10234:documents", + "name": "Documents", + "id": "ef7d985c-2385-44f4-9c71-ae06a52264f8", "parents": [], "starred": false, "order": 0, "created_at": "2024-01-01T12:00:00Z", - "updated_at": "2024-01-01T12:00:00Z" + "updated_at": "2024-01-02T12:00:00Z" } ] } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + }, + "post": { + "operationId": "createUserSchemaFileCollection", + "summary": "createUserSchemaFileCollection", + "description": "Create a new file collection for the current user within a specific schema.\n\nThe collection will be private to the creating user and associated with the specified schema.\n", + "tags": [ + "File Collections" + ], + "parameters": [ + { + "name": "slug", + "in": "path", + "required": true, + "description": "The entity schema slug (e.g., order, opportunity, contact)", + "schema": { + "type": "string" + }, + "example": "opportunity" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileCollectionCreateRequest" + }, + "examples": { + "Create collection": { + "value": { + "name": "Contracts", + "starred": false + } + }, + "Create nested collection": { + "value": { + "name": "2024 Contracts", + "parents": [ + "_system_files_collection_3fa85f64-5717-4562-b3fc-2c963f66afa6_10234:contracts" + ] + } + } + } + } + } + }, + "responses": { + "201": { + "description": "File collection created successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileCollectionItem" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequestError" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/v1/{slug}/collections/{collectionSlug}": { + "put": { + "operationId": "updateUserSchemaFileCollection", + "summary": "updateUserSchemaFileCollection", + "description": "Update an existing file collection.\n\nYou can update the name, parent relationships, starred status, and enabled locations/purposes.\n", + "tags": [ + "File Collections" + ], + "parameters": [ + { + "name": "slug", + "in": "path", + "required": true, + "description": "The entity schema slug (e.g., order, opportunity)", + "schema": { + "type": "string" + }, + "example": "opportunity" + }, + { + "name": "collectionSlug", + "in": "path", + "required": true, + "description": "The collection slug identifier", + "schema": { + "type": "string" + }, + "example": "documents" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileCollectionAttributes" + }, + "examples": { + "Rename collection": { + "value": { + "name": "Important Documents" + } + }, + "Star collection": { + "value": { + "starred": true + } + } + } + } + } + }, + "responses": { + "200": { + "description": "File collection updated successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileCollectionItem" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequestError" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "$ref": "#/components/responses/NotFoundError" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + }, + "delete": { + "operationId": "deleteUserSchemaFileCollection", + "summary": "deleteUserSchemaFileCollection", + "description": "Delete a file collection.\n\n**Note:** Deleting a collection does not delete the files within it.\nFiles will remain but will no longer be associated with this collection.\n", + "tags": [ + "File Collections" + ], + "parameters": [ + { + "name": "slug", + "in": "path", + "required": true, + "description": "The entity schema slug (e.g., order, opportunity)", + "schema": { + "type": "string" + }, + "example": "opportunity" + }, + { + "name": "collectionSlug", + "in": "path", + "required": true, + "description": "The collection slug identifier", + "schema": { + "type": "string" + }, + "example": "documents" + } + ], + "responses": { + "200": { + "description": "File collection deleted successfully" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "$ref": "#/components/responses/NotFoundError" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/v1/entity/{id}/collections/{collectionSlug}/files": { + "get": { + "operationId": "getFilesInCollection", + "summary": "getFilesInCollection", + "description": "Get all files within a specific collection for an entity.\n\nThe schema is automatically derived from the entity. This endpoint requires\nview permission on the parent entity to access its files.\n", + "tags": [ + "File Collections" + ], + "parameters": [ + { + "$ref": "#/components/parameters/EntityIdPathParam" + }, + { + "name": "collectionSlug", + "in": "path", + "required": true, + "description": "The collection slug identifier", + "schema": { + "type": "string" + }, + "example": "documents" + } + ], + "responses": { + "200": { + "description": "List of files in the collection", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileEntity" + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "403": { + "description": "Insufficient permissions to view the entity's files", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorObject" + }, + "example": { + "status": 403, + "error": "User must have permission to view this entity to access its files" + } + } + } + }, + "404": { + "description": "Entity or collection not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorObject" + }, + "example": { + "status": 404, + "error": "Entity not found" + } + } + } + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/v1/collections/{schemaSlug}": { + "get": { + "operationId": "getGlobalFileCollections", + "summary": "getGlobalFileCollections", + "description": "Get all global file collections for a specific schema.\n\nGlobal collections are shared across all users in the organization for the specified schema.\nUnlike user collections, these are visible to everyone with access to entities of that schema.\n", + "tags": [ + "File Collections" + ], + "parameters": [ + { + "name": "schemaSlug", + "in": "path", + "required": true, + "description": "The entity schema slug (e.g., order, opportunity, contact)", + "schema": { + "type": "string" + }, + "example": "order" + } + ], + "responses": { + "200": { + "description": "List of global collections for the schema", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileCollectionItem" + } + }, + "example": [ + { + "slug": "_system_files_collection_schema_order:templates", + "name": "Templates", + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "parents": [], + "starred": false, + "order": 0, + "created_at": "2024-01-01T12:00:00Z", + "updated_at": "2024-01-01T12:00:00Z" + } + ] + } + } + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + } + }, + "components": { + "securitySchemes": { + "EpilotAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT", + "description": "Bearer token authentication using epilot OAuth2 JWT tokens.\n\n**When to use:** Server-to-server integrations, API clients, and programmatic access.\n\n**How to obtain a token:**\n1. Use the epilot Auth API to authenticate\n2. Include the token in the `Authorization` header: `Authorization: Bearer `\n\n**Example:**\n```\nAuthorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...\n```\n\n**Token contents:** The JWT contains user identity, organization ID, and permissions.\n" + }, + "CookieAuth": { + "type": "apiKey", + "in": "cookie", + "name": "token", + "description": "Cookie-based session authentication for browser applications.\n\n**When to use:** Browser-based applications that need to:\n- Embed file previews directly in `` tags\n- Download files without JavaScript token handling\n- Access files from HTML elements that cannot set custom headers\n\n**How to establish a session:**\n1. Obtain a Bearer token via EpilotAuth\n2. Call `GET /v1/files/session` with the Bearer token\n3. The server sets an HTTP-only cookie named `token`\n4. Subsequent requests automatically include the cookie\n\n**Security note:** The cookie is HTTP-only and secure, protecting against XSS attacks.\n" + } + }, + "schemas": { + "EntityId": { + "type": "string", + "example": "ef7d985c-2385-44f4-9c71-ae06a52264f8" + }, + "EntitySlug": { + "description": "URL-friendly identifier for the entity schema", + "type": "string", + "example": "contact" + }, + "ActivityId": { + "type": "string", + "format": "ulid", + "description": "See https://github.com/ulid/spec", + "example": "01F130Q52Q6MWSNS8N2AVXV4JN" + }, + "FileEntityId": { + "oneOf": [ + { + "type": "string", + "maxLength": 0, + "description": "Empty string (used when file ID not yet assigned)" + }, + { + "type": "string", + "format": "uuid", + "description": "Valid UUID v4" + } + ], + "example": "ef7d985c-2385-44f4-9c71-ae06a52264f8" + }, + "FileAttributes": { + "type": "object", + "properties": { + "_tags": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "tag1", + "tag2" + ] + }, + "_purpose": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "8d396871-95a0-4c9d-bb4d-9eda9c35776c", + "da7cdf9a-01be-40c9-a29c-9a8f9f0de6f8" + ] + }, + "_manifest": { + "type": "array", + "description": "Manifest ID used to create/update the entity", + "items": { + "type": "string", + "format": "uuid", + "example": "123e4567-e89b-12d3-a456-426614174000" + } + }, + "filename": { + "type": "string", + "example": "document.pdf" + }, + "type": { + "$ref": "#/components/schemas/FileType" + }, + "mime_type": { + "type": "string", + "description": "MIME type of the file", + "example": "application/pdf" + }, + "size_bytes": { + "type": "integer", + "minimum": 0, + "description": "File size in bytes", + "example": 1234, + "readOnly": true + }, + "readable_size": { + "type": "string", + "example": "1.2 MB", + "description": "Human readable file size", + "readOnly": true + }, + "etag": { + "type": "string", + "description": "Content hash (S3 ETag) of the latest file version. For single-part uploads this is the hex MD5 of the content and is stable across orgs, so identical content yields an identical etag. Used to detect real content changes without re-downloading the bytes.", + "example": "9bb58f26192e4ba00f01e2e7b136bbd8", + "readOnly": true + }, + "access_control": { + "type": "string", + "default": "private", + "enum": [ + "private", + "public-read" + ] + }, + "public_url": { + "description": "Direct URL for file (public only if file access control is public-read)", + "type": "string", + "format": "url", + "example": "https://epilot-prod-user-content.s3.eu-central-1.amazonaws.com/123/4d689aeb-1497-4410-a9fe-b36ca9ac4389/document.pdf", + "readOnly": true + }, + "custom_download_url": { + "$ref": "#/components/schemas/CustomDownloadUrl" + }, + "custom_download_url_auth": { + "$ref": "#/components/schemas/CustomDownloadUrlAuth" + }, + "preview_summary_de": { + "type": "string", + "description": "Compact German summary for hover and list preview surfaces.", + "readOnly": true + }, + "short_summary_de": { + "type": "string", + "description": "Short German paragraph summary for file preview surfaces.", + "readOnly": true + }, + "preview_summary_en": { + "type": "string", + "description": "Compact English summary for hover and list preview surfaces.", + "readOnly": true + }, + "short_summary_en": { + "type": "string", + "description": "Short English paragraph summary for file preview surfaces.", + "readOnly": true + }, + "file_summary_execution_id": { + "type": "string", + "format": "uuid", + "description": "AI execution that produced the current summary. Used to correlate user feedback.", + "readOnly": true + } + } + }, + "FileType": { + "type": "string", + "enum": [ + "document", + "document_template", + "text", + "image", + "video", + "audio", + "spreadsheet", + "presentation", + "font", + "archive", + "application", + "unknown" + ] + }, + "FileSummaryJobStatus": { + "type": "string", + "description": "Current state of a file summary job.", + "enum": [ + "queued", + "waiting_for_extraction", + "processing", + "completed", + "failed", + "unsupported", + "stale" + ], + "readOnly": true + }, + "FileSummaryJob": { + "type": "object", + "properties": { + "job_id": { + "type": "string", + "format": "uuid", + "description": "File summary job ID." + }, + "file_id": { + "$ref": "#/components/schemas/FileEntityId" + }, + "status": { + "$ref": "#/components/schemas/FileSummaryJobStatus" + }, + "error": { + "type": "string", + "description": "Human-readable failure or unsupported reason when available." + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "completed_at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "job_id", + "file_id", + "status", + "created_at", + "updated_at" + ] + }, + "FileSummary": { + "type": "object", + "properties": { + "status": { + "$ref": "#/components/schemas/FileSummaryJobStatus" + }, + "job_id": { + "type": "string", + "format": "uuid", + "description": "Current summary job ID when available." + }, + "feedback_available": { + "type": "boolean", + "description": "Whether the current summary can receive user feedback." + }, + "preview_summary_de": { + "type": "string", + "nullable": true, + "description": "Compact German summary for hover and list preview surfaces." + }, + "short_summary_de": { + "type": "string", + "nullable": true, + "description": "Short German paragraph summary for file preview surfaces." + }, + "preview_summary_en": { + "type": "string", + "nullable": true, + "description": "Compact English summary for hover and list preview surfaces." + }, + "short_summary_en": { + "type": "string", + "nullable": true, + "description": "Short English paragraph summary for file preview surfaces." + } + } + }, + "FileSummaryFeedback": { + "type": "object", + "required": [ + "rating", + "user_id", + "submitted_at" + ], + "properties": { + "rating": { + "type": "string", + "enum": [ + "up", + "down" + ] + }, + "comment": { + "type": "string" + }, + "user_id": { + "type": "string" + }, + "submitted_at": { + "type": "string", + "format": "date-time" + } + } + }, + "FileSummaryFeedbackResponse": { + "type": "object", + "required": [ + "feedback" + ], + "properties": { + "feedback": { + "allOf": [ + { + "$ref": "#/components/schemas/FileSummaryFeedback" + } + ], + "nullable": true + } + } + }, + "PutFileSummaryFeedbackRequest": { + "type": "object", + "required": [ + "rating" + ], + "properties": { + "rating": { + "type": "string", + "enum": [ + "up", + "down" + ] + }, + "comment": { + "type": "string" + } + } + }, + "FileTextStatus": { + "type": "string", + "description": "Availability of the plain-text representation for a file entity.", + "enum": [ + "ready", + "not_ready", + "unsupported" + ], + "readOnly": true + }, + "FileValidationRequest": { + "type": "object", + "required": [ + "file", + "rule_ids" + ], + "additionalProperties": false, + "properties": { + "file": { + "$ref": "#/components/schemas/FileValidationFileReference" + }, + "rule_ids": { + "type": "array", + "description": "Document validation rules to evaluate. Rules of another input type are rejected.", + "minItems": 1, + "maxItems": 10, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1 + } + } + } + }, + "FileValidationFileReference": { + "description": "The file to validate - a saved file entity, or an uploaded object not saved as an entity yet.", + "oneOf": [ + { + "type": "object", + "required": [ + "entity_id" + ], + "additionalProperties": false, + "properties": { + "entity_id": { + "$ref": "#/components/schemas/FileEntityId" + } + } + }, + { + "type": "object", + "required": [ + "s3ref" + ], + "additionalProperties": false, + "properties": { + "s3ref": { + "$ref": "#/components/schemas/S3Ref" + } + } + } + ] + }, + "FileValidation": { + "type": "object", + "required": [ + "id", + "status", + "file", + "observation", + "results", + "created_at" + ], + "properties": { + "id": { + "type": "string", + "description": "Identifier of this validation run." + }, + "status": { + "type": "string", + "enum": [ + "completed", + "pending", + "failed", + "skipped" + ], + "description": "`completed` when every rule was evaluated. `pending` is reserved for check levels that\nneed asynchronous content extraction; `failed` when the observation could not be produced;\n`skipped` when document validation is not enabled for the organisation - the file was not\nread and `results` is empty, so callers treat the file as accepted.\n" + }, + "file": { + "$ref": "#/components/schemas/FileValidationFile" + }, + "observation": { + "$ref": "#/components/schemas/FileValidationObservation" + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileValidationRuleResult" + } + }, + "created_at": { + "type": "string", + "format": "date-time" + } + } + }, + "FileValidationFile": { + "type": "object", + "description": "The file that was validated, with the identity facts collected from its content.", + "properties": { + "entity_id": { + "$ref": "#/components/schemas/FileEntityId" + }, + "s3ref": { + "$ref": "#/components/schemas/S3Ref" + }, + "filename": { + "type": "string" + }, + "mime_type": { + "type": "string", + "description": "Mime type detected from the file content, falling back to the declared type." + }, + "size_bytes": { + "type": "integer" + } + } + }, + "FileValidationObservation": { + "type": "object", + "description": "Everything observed about the file, as passed to the validation rules engine.", + "required": [ + "facts" + ], + "properties": { + "facts": { + "$ref": "#/components/schemas/FileValidationFacts" + }, + "judgements": { + "type": "object", + "description": "Content judgements keyed by condition id, produced by the document requirements\ncheck for `meetsCriteria` conditions at the `standard` and `advanced` levels.\nAbsent when no content check ran (basic level, unsupported file, or the check\ndid not finish within its budget).\n", + "additionalProperties": { + "$ref": "#/components/schemas/FileValidationJudgement" + } + }, + "document_type": { + "type": "object", + "description": "Document type detected by the content check, when it ran.", + "properties": { + "slug": { + "type": "string" + }, + "confidence": { + "type": "number" + } + } + } + } + }, + "FileValidationJudgement": { + "type": "object", + "required": [ + "result" + ], + "properties": { + "result": { + "type": "string", + "enum": [ + "pass", + "fail", + "uncertain" + ] + }, + "confidence": { + "type": "number" }, - "500": { - "$ref": "#/components/responses/InternalServerError" + "evidence": { + "type": "string" + }, + "explanation": { + "type": "string" } } - } - } - }, - "components": { - "securitySchemes": { - "EpilotAuth": { - "type": "http", - "scheme": "bearer", - "bearerFormat": "JWT", - "description": "Bearer token authentication using epilot OAuth2 JWT tokens.\n\n**When to use:** Server-to-server integrations, API clients, and programmatic access.\n\n**How to obtain a token:**\n1. Use the epilot Auth API to authenticate\n2. Include the token in the `Authorization` header: `Authorization: Bearer `\n\n**Example:**\n```\nAuthorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...\n```\n\n**Token contents:** The JWT contains user identity, organization ID, and permissions.\n" - }, - "CookieAuth": { - "type": "apiKey", - "in": "cookie", - "name": "token", - "description": "Cookie-based session authentication for browser applications.\n\n**When to use:** Browser-based applications that need to:\n- Embed file previews directly in `` tags\n- Download files without JavaScript token handling\n- Access files from HTML elements that cannot set custom headers\n\n**How to establish a session:**\n1. Obtain a Bearer token via EpilotAuth\n2. Call `GET /v1/files/session` with the Bearer token\n3. The server sets an HTTP-only cookie named `token`\n4. Subsequent requests automatically include the cookie\n\n**Security note:** The cookie is HTTP-only and secure, protecting against XSS attacks.\n" - } - }, - "schemas": { - "EntityId": { - "type": "string", - "example": "ef7d985c-2385-44f4-9c71-ae06a52264f8" - }, - "EntitySlug": { - "description": "URL-friendly identifier for the entity schema", - "type": "string", - "example": "contact" }, - "ActivityId": { - "type": "string", - "format": "ulid", - "description": "See https://github.com/ulid/spec", - "example": "01F130Q52Q6MWSNS8N2AVXV4JN" - }, - "FileEntityId": { - "oneOf": [ - { - "type": "string", - "maxLength": 0, - "description": "Empty string (used when file ID not yet assigned)" + "FileValidationFacts": { + "type": "object", + "description": "Deterministic properties of the file. A property that is absent could not be determined\n(for example the resolution of an image the service cannot decode); conditions that need it\nare skipped rather than failed.\n", + "properties": { + "mime_type": { + "type": "string" }, - { - "type": "string", - "format": "uuid", - "description": "Valid UUID v4" + "size_bytes": { + "type": "integer" + }, + "page_count": { + "type": "integer", + "description": "Pages of a PDF; 1 for images." + }, + "longer_edge_px": { + "type": "integer", + "description": "Longer edge of an image in pixels." + }, + "is_blank": { + "type": "boolean" + }, + "is_password_protected": { + "type": "boolean" } - ], - "example": "ef7d985c-2385-44f4-9c71-ae06a52264f8" + } }, - "FileAttributes": { + "FileValidationRuleResult": { "type": "object", + "required": [ + "rule_id", + "level", + "is_valid", + "conditions" + ], "properties": { - "_tags": { + "rule_id": { + "type": "string" + }, + "rule_title": { + "type": "string" + }, + "level": { + "type": "string", + "enum": [ + "basic", + "standard", + "advanced" + ], + "description": "The rule's check level." + }, + "is_valid": { + "type": "boolean", + "description": "True when every blocking condition passed or was skipped." + }, + "failed_parts": { "type": "array", + "description": "Error messages of blocking conditions that failed.", "items": { "type": "string" - }, - "example": [ - "tag1", - "tag2" - ] + } }, - "_purpose": { + "warnings": { "type": "array", + "description": "Error messages of advisory conditions that failed and of uncertain judgements.", "items": { "type": "string" - }, - "example": [ - "8d396871-95a0-4c9d-bb4d-9eda9c35776c", - "da7cdf9a-01be-40c9-a29c-9a8f9f0de6f8" - ] + } }, - "_manifest": { + "conditions": { "type": "array", - "description": "Manifest ID used to create/update the entity", "items": { - "type": "string", - "format": "uuid", - "example": "123e4567-e89b-12d3-a456-426614174000" + "$ref": "#/components/schemas/FileValidationConditionResult" } + } + } + }, + "FileValidationConditionResult": { + "type": "object", + "required": [ + "id", + "operator", + "outcome", + "blocking", + "message" + ], + "properties": { + "id": { + "type": "string" }, - "filename": { + "operator": { + "type": "string" + }, + "outcome": { "type": "string", - "example": "document.pdf" + "enum": [ + "pass", + "fail", + "uncertain", + "skipped" + ] }, - "type": { - "$ref": "#/components/schemas/FileType" + "blocking": { + "type": "boolean", + "description": "True when this outcome makes the file invalid." }, - "mime_type": { + "message": { "type": "string", - "description": "MIME type of the file", - "example": "application/pdf" + "description": "The condition's configured error message." }, - "size_bytes": { - "type": "integer", - "minimum": 0, - "description": "File size in bytes", - "example": 1234, - "readOnly": true + "evidence": { + "type": "string" }, - "readable_size": { - "type": "string", - "example": "1.2 MB", - "description": "Human readable file size", - "readOnly": true + "explanation": { + "type": "string" + } + } + }, + "FileText": { + "type": "object", + "properties": { + "status": { + "$ref": "#/components/schemas/FileTextStatus" }, - "access_control": { - "type": "string", - "default": "private", - "enum": [ - "private", - "public-read" - ] + "text": { + "type": "string" }, - "public_url": { - "description": "Direct URL for file (public only if file access control is public-read)", - "type": "string", - "format": "url", - "example": "https://epilot-prod-user-content.s3.eu-central-1.amazonaws.com/123/4d689aeb-1497-4410-a9fe-b36ca9ac4389/document.pdf", - "readOnly": true + "truncated": { + "type": "boolean" }, - "custom_download_url": { - "$ref": "#/components/schemas/CustomDownloadUrl" + "total_chars": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "status" + ], + "oneOf": [ + { + "$ref": "#/components/schemas/FileTextReady" }, - "preview_summary_de": { - "type": "string", - "description": "Compact German summary for hover and list preview surfaces.", - "readOnly": true + { + "$ref": "#/components/schemas/FileTextNotReady" }, - "short_summary_de": { + { + "$ref": "#/components/schemas/FileTextUnsupported" + } + ] + }, + "FileTextReady": { + "type": "object", + "additionalProperties": false, + "properties": { + "status": { "type": "string", - "description": "Short German paragraph summary for file preview surfaces.", - "readOnly": true + "enum": [ + "ready" + ] }, - "preview_summary_en": { + "text": { "type": "string", - "description": "Compact English summary for hover and list preview surfaces.", - "readOnly": true + "description": "Plain-text representation, trimmed and truncated to a server-side limit of 50000 characters." }, - "short_summary_en": { - "type": "string", - "description": "Short English paragraph summary for file preview surfaces.", - "readOnly": true + "truncated": { + "type": "boolean", + "description": "True when the returned text was cut off by the character limit." }, - "summary_status": { - "$ref": "#/components/schemas/FileSummaryStatus" + "total_chars": { + "type": "integer", + "minimum": 0, + "description": "Length of the full text before truncation." } - } - }, - "FileType": { - "type": "string", - "enum": [ - "document", - "document_template", + }, + "required": [ + "status", "text", - "image", - "video", - "audio", - "spreadsheet", - "presentation", - "font", - "archive", - "application", - "unknown" + "truncated", + "total_chars" ] }, - "FileSummaryStatus": { - "type": "string", - "description": "Current AI summary generation state for the file.", - "enum": [ - "processing", - "completed", - "failed", - "unsupported" - ], - "readOnly": true + "FileTextNotReady": { + "type": "object", + "additionalProperties": false, + "properties": { + "status": { + "type": "string", + "enum": [ + "not_ready" + ] + } + }, + "required": [ + "status" + ] }, - "FileSummaryGenerationStatus": { + "FileTextUnsupported": { "type": "object", + "additionalProperties": false, "properties": { "status": { - "$ref": "#/components/schemas/FileSummaryStatus" + "type": "string", + "enum": [ + "unsupported" + ] } }, "required": [ @@ -2266,6 +3278,16 @@ "format": "uri", "example": "https://some-api-url.com/download?file_id=123" }, + "CustomDownloadUrlAuth": { + "description": "Authorization mode for the custom_download_url. `presigned` (the default) uses an HMAC-signed URL; `token` authorizes the download via the caller's bearer token, matched against the exact stored custom_download_url. In token mode the File API returns the unsigned custom_download_url (no expires_at/signature query params) so the stored url matches exactly.", + "type": "string", + "enum": [ + "presigned", + "token" + ], + "default": "presigned", + "example": "token" + }, "FileEntity": { "allOf": [ { @@ -2437,6 +3459,9 @@ "properties": { "custom_download_url": { "$ref": "#/components/schemas/CustomDownloadUrl" + }, + "custom_download_url_auth": { + "$ref": "#/components/schemas/CustomDownloadUrlAuth" } } } @@ -2818,6 +3843,11 @@ "mime_type": { "type": "string", "example": "image/jpeg" + }, + "etag": { + "type": "string", + "description": "Content hash (S3 ETag) of this file version", + "example": "9bb58f26192e4ba00f01e2e7b136bbd8" } } }, @@ -3138,6 +4168,26 @@ } } }, + "UnprocessableEntityError": { + "description": "The request was well-formed but the file's type or content is not acceptable", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ErrorObject" + }, + { + "example": { + "status": 422, + "error": "File type not allowed" + } + } + ] + } + } + } + }, "UnauthorizedError": { "description": "Authentication required or invalid credentials", "content": { diff --git a/packages/cli/definitions/integration-toolkit.json b/packages/cli/definitions/integration-toolkit.json index e73f11e39..e424db823 100644 --- a/packages/cli/definitions/integration-toolkit.json +++ b/packages/cli/definitions/integration-toolkit.json @@ -2,7 +2,7 @@ "openapi": "3.0.3", "info": { "title": "Integration Toolkit API", - "version": "1.2.0", + "version": "1.33.0", "description": "API for integrating with external systems in a standardised way." }, "tags": [ @@ -10,6 +10,10 @@ "name": "erp", "description": "ERP integration endpoints" }, + { + "name": "erp-imports", + "description": "ERP pricing file import endpoints" + }, { "name": "trigger", "description": "Endpoints to trigger ERP related actions" @@ -279,6 +283,9 @@ "401": { "$ref": "#/components/responses/Unauthorized" }, + "404": { + "$ref": "#/components/responses/NotFound" + }, "500": { "$ref": "#/components/responses/InternalServerError" } @@ -332,6 +339,9 @@ "401": { "$ref": "#/components/responses/Unauthorized" }, + "404": { + "$ref": "#/components/responses/NotFound" + }, "500": { "$ref": "#/components/responses/InternalServerError" } @@ -609,6 +619,167 @@ } } }, + "/v1/erp/updates/direct_simulation": { + "post": { + "operationId": "simulateDirect", + "summary": "simulateDirect", + "description": "Dry run for direct-mode payloads: validates a `DirectPayload` against a `direct: true`\nuse case configuration and previews the internal entity and meter reading updates the\npipeline would apply — without persisting anything.\n\nUnlike the ingest endpoint, validation does not stop at the first problem: ALL issues\nacross all operations are collected into `errors`, each carrying the index of the\noperation it refers to. When the payload is valid, the response additionally contains\nthe translated update previews plus non-blocking `warnings` (for example unique-id\nfields that are not defined in the target entity schema, or a failed server-side\nunique-id type derivation).\n\nUse this endpoint while designing a direct integration, before sending events to\n`POST /v3/erp/updates/events`. See documentation at /docs/DIRECT_MODE.md.\n", + "tags": [ + "erp" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DirectSimulationRequest" + }, + "examples": { + "entity_upsert": { + "summary": "Entity upsert (open mode)", + "value": { + "event_configuration": { + "direct": true + }, + "payload": { + "version": "1", + "operations": [ + { + "entity_slug": "contact", + "unique_ids": { + "customer_number": "CUST-12345" + }, + "attributes": { + "first_name": "Anna", + "last_name": "Schmidt", + "email": "anna.schmidt@example.com" + } + } + ] + } + } + }, + "allowlist_with_relation": { + "summary": "Allowlisted entities with a relation", + "value": { + "event_configuration": { + "direct": true, + "entities": [ + { + "entity_schema": "contract", + "unique_ids": [ + "contract_number" + ] + }, + { + "entity_schema": "contact", + "unique_ids": [ + "customer_number" + ] + } + ] + }, + "payload": { + "version": "1", + "operations": [ + { + "entity_slug": "contract", + "unique_ids": { + "contract_number": "CTR-001" + }, + "attributes": { + "display_name": "Premium Contract", + "customer": { + "$relation": { + "_set": [ + { + "schema": "contact", + "unique_ids": { + "customer_number": "CUST-12345" + }, + "tags": [ + "primary" + ] + } + ] + } + } + } + } + ] + } + } + }, + "meter_reading": { + "summary": "Meter reading", + "value": { + "event_configuration": { + "direct": true + }, + "payload": { + "version": "1", + "operations": [ + { + "type": "meter_reading", + "meter": { + "unique_ids": { + "meter_number": "M-001" + } + }, + "counter": { + "unique_ids": { + "obis_number": "1-0:1.8.0" + } + }, + "reading_matching": "strict-date", + "attributes": { + "external_id": "R-001", + "timestamp": "2026-08-24T06:00:00Z", + "source": "ERP", + "value": 12345.6 + } + } + ] + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Simulation executed - `valid` reports the verdict, `errors` lists every violation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DirectSimulationResponse" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "422": { + "description": "Unprocessable entity - simulation could not be executed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBase" + } + } + } + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, "/v1/erp/updates/mapping_simulation": { "post": { "operationId": "simulateMapping", @@ -1618,29 +1789,41 @@ } } }, - "/v2/integrations": { + "/v1/integrations/{integrationId}/documentation": { "get": { - "operationId": "listIntegrationsV2", - "summary": "listIntegrationsV2", - "description": "Retrieve all integrations with embedded use cases for the authenticated organization", + "operationId": "listDocumentationPages", + "summary": "listDocumentationPages", + "description": "Retrieve all documentation pages of an integration, without their markdown content.\nAn integration has at most one general page plus at most one page per use case.\nThe page id is 'general' for the integration-wide page, otherwise the use case ID.\n", "tags": [ "integrations" ], + "parameters": [ + { + "name": "integrationId", + "in": "path", + "required": true, + "description": "The integration ID", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], "responses": { "200": { - "description": "Successfully retrieved integrations with use cases", + "description": "Successfully retrieved documentation pages", "content": { "application/json": { "schema": { "type": "object", "required": [ - "integrations" + "pages" ], "properties": { - "integrations": { + "pages": { "type": "array", "items": { - "$ref": "#/components/schemas/IntegrationWithUseCases" + "$ref": "#/components/schemas/DocumentationPageSummary" } } } @@ -1651,44 +1834,8 @@ "401": { "$ref": "#/components/responses/Unauthorized" }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - } - }, - "post": { - "operationId": "createIntegrationV2", - "summary": "createIntegrationV2", - "description": "Create a new integration with embedded use cases.\n", - "tags": [ - "integrations" - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpsertIntegrationWithUseCasesRequest" - } - } - } - }, - "responses": { - "201": { - "description": "Integration created successfully with use cases", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/IntegrationWithUseCases" - } - } - } - }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, - "401": { - "$ref": "#/components/responses/Unauthorized" + "404": { + "description": "Integration not found" }, "500": { "$ref": "#/components/responses/InternalServerError" @@ -1696,11 +1843,11 @@ } } }, - "/v2/integrations/{integrationId}": { + "/v1/integrations/{integrationId}/documentation/{docId}": { "get": { - "operationId": "getIntegrationV2", - "summary": "getIntegrationV2", - "description": "Retrieve a specific integration with all its embedded use cases", + "operationId": "getDocumentationPage", + "summary": "getDocumentationPage", + "description": "Retrieve a single documentation page including its markdown content", "tags": [ "integrations" ], @@ -1714,15 +1861,25 @@ "type": "string", "format": "uuid" } + }, + { + "name": "docId", + "in": "path", + "required": true, + "description": "'general' for the integration-wide page, otherwise a use case ID", + "schema": { + "type": "string", + "pattern": "^(general|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$" + } } ], "responses": { "200": { - "description": "Successfully retrieved integration with use cases", + "description": "Successfully retrieved documentation page", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IntegrationWithUseCases" + "$ref": "#/components/schemas/DocumentationPage" } } } @@ -1731,7 +1888,7 @@ "$ref": "#/components/responses/Unauthorized" }, "404": { - "description": "Integration not found" + "description": "Documentation page not found" }, "500": { "$ref": "#/components/responses/InternalServerError" @@ -1739,9 +1896,9 @@ } }, "put": { - "operationId": "updateIntegrationV2", - "summary": "updateIntegrationV2", - "description": "Update an existing integration with embedded use cases.\nThe integration must already exist.\nUse cases are updated declaratively:\n- Use cases in the request with matching IDs are updated\n- Use cases in the request without matching IDs are created\n- Existing use cases not in the request are deleted\n", + "operationId": "upsertDocumentationPage", + "summary": "upsertDocumentationPage", + "description": "Create or update the documentation page identified by docId.\nUpsert semantics enforce the invariant of one general page per\nintegration and one page per use case. For use case pages the\nuse case must exist.\n", "tags": [ "integrations" ], @@ -1750,11 +1907,21 @@ "name": "integrationId", "in": "path", "required": true, - "description": "The integration ID (client-provided)", + "description": "The integration ID", "schema": { "type": "string", "format": "uuid" } + }, + { + "name": "docId", + "in": "path", + "required": true, + "description": "'general' for the integration-wide page, otherwise a use case ID", + "schema": { + "type": "string", + "pattern": "^(general|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$" + } } ], "requestBody": { @@ -1762,18 +1929,18 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpsertIntegrationWithUseCasesRequest" + "$ref": "#/components/schemas/UpsertDocumentationPageRequest" } } } }, "responses": { "200": { - "description": "Integration updated successfully with use cases", + "description": "Documentation page created or updated successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IntegrationWithUseCases" + "$ref": "#/components/schemas/DocumentationPage" } } } @@ -1784,8 +1951,11 @@ "401": { "$ref": "#/components/responses/Unauthorized" }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, "404": { - "description": "Integration not found" + "description": "Integration or use case not found" }, "500": { "$ref": "#/components/responses/InternalServerError" @@ -1793,9 +1963,9 @@ } }, "delete": { - "operationId": "deleteIntegrationV2", - "summary": "deleteIntegrationV2", - "description": "Delete an integration and all its use cases", + "operationId": "deleteDocumentationPage", + "summary": "deleteDocumentationPage", + "description": "Delete a documentation page", "tags": [ "integrations" ], @@ -1809,11 +1979,21 @@ "type": "string", "format": "uuid" } + }, + { + "name": "docId", + "in": "path", + "required": true, + "description": "'general' for the integration-wide page, otherwise a use case ID", + "schema": { + "type": "string", + "pattern": "^(general|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$" + } } ], "responses": { "200": { - "description": "Integration deleted successfully", + "description": "Documentation page deleted successfully", "content": { "application/json": { "schema": { @@ -1830,8 +2010,11 @@ "401": { "$ref": "#/components/responses/Unauthorized" }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, "404": { - "description": "Integration not found" + "description": "Documentation page not found" }, "500": { "$ref": "#/components/responses/InternalServerError" @@ -1839,119 +2022,68 @@ } } }, - "/v2/integrations/{integrationId}/use-cases/{useCaseId}/secure-proxy-whitelist": { + "/v2/integrations": { "get": { - "operationId": "getSecureProxyWhitelist", - "summary": "Get secure_proxy whitelist (admin portal only)", - "description": "Returns the current allowed_domains, allowed_ips, and vpc_mode for a secure_proxy use case.\nStaff-only — gated by internal-auth issuer AND admin-portal Cognito user pool membership.\nRejects Login-As tokens.\n", + "operationId": "listIntegrationsV2", + "summary": "listIntegrationsV2", + "description": "Retrieve all integrations with embedded use cases for the authenticated organization", "tags": [ - "integrations", - "proxy" - ], - "security": [ - { - "EpilotAuth": [] - } - ], - "parameters": [ - { - "name": "integrationId", - "in": "path", - "required": true, - "description": "The integration ID", - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "useCaseId", - "in": "path", - "required": true, - "description": "The use case ID", - "schema": { - "type": "string", - "format": "uuid" - } - } + "integrations" ], "responses": { "200": { - "description": "Current whitelist", + "description": "Successfully retrieved integrations with use cases", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SecureProxyWhitelist" + "type": "object", + "required": [ + "integrations" + ], + "properties": { + "integrations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IntegrationWithUseCases" + } + } + } } } } }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, "401": { "$ref": "#/components/responses/Unauthorized" }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "404": { - "description": "Use case not found or not of type secure_proxy" + "500": { + "$ref": "#/components/responses/InternalServerError" } } }, - "put": { - "operationId": "updateSecureProxyWhitelist", - "summary": "Update secure_proxy whitelist (admin portal only)", - "description": "Replaces allowed_domains and/or allowed_ips on a secure_proxy use case.\nAt least one of the two fields is required. Validation mirrors the CLI's\n`validateDomainPatterns` / `validateCidrs`. Writes a USECASE_HISTORY row\nwith the admin user's email as `changed_by`.\n\nUpdate semantics per field:\n - **omitted** — field is not modified; the stored value is preserved.\n - **non-empty array** — the stored value is replaced with the supplied list.\n - **empty array (`[]`)** — the list is cleared (stored as `[]`). This is\n the canonical way to remove all entries. `null` is not accepted.\n\nStaff-only — same auth gates as GET.\n", + "post": { + "operationId": "createIntegrationV2", + "summary": "createIntegrationV2", + "description": "Create a new integration with embedded use cases.\n", "tags": [ - "integrations", - "proxy" - ], - "security": [ - { - "EpilotAuth": [] - } - ], - "parameters": [ - { - "name": "integrationId", - "in": "path", - "required": true, - "description": "The integration ID", - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "useCaseId", - "in": "path", - "required": true, - "description": "The use case ID", - "schema": { - "type": "string", - "format": "uuid" - } - } + "integrations" ], "requestBody": { "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SecureProxyWhitelistUpdate" + "$ref": "#/components/schemas/UpsertIntegrationWithUseCasesRequest" } } } }, "responses": { - "200": { - "description": "Updated whitelist (same shape as GET)", + "201": { + "description": "Integration created successfully with use cases", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SecureProxyWhitelist" + "$ref": "#/components/schemas/IntegrationWithUseCases" } } } @@ -1962,28 +2094,19 @@ "401": { "$ref": "#/components/responses/Unauthorized" }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "404": { - "description": "Use case not found or not of type secure_proxy" + "500": { + "$ref": "#/components/responses/InternalServerError" } } } }, - "/v2/integrations/{integrationId}/use-cases/{useCaseId}/secure-proxy-whitelist/history": { + "/v2/integrations/{integrationId}": { "get": { - "operationId": "listSecureProxyWhitelistHistory", - "summary": "List secure_proxy whitelist change history (admin portal only)", - "description": "Returns the most recent USECASE_HISTORY entries for a secure_proxy use case,\nin reverse chronological order (newest first). Each entry includes the\nactor email (`changed_by`), the ISO-8601 timestamp (`history_created_at`),\nthe `change_description` (free-text action), and the full `configuration`\nsnapshot — from which UI-08 computes a before/after diff between consecutive\nentries.\n\nStaff-only — gated by internal-auth issuer AND admin-portal Cognito user pool\nmembership. Rejects Login-As tokens (same auth gate as the GET / PUT\nsecure-proxy-whitelist operations).\n\nThin wrapper over the service-layer `listUseCaseHistory` that powers\n`GET /v1/integrations/{integrationId}/use-cases/{useCaseId}/history` — the\nseparate path exists because the /v1 variant is tenant-gated and admin-portal\ninternal-auth tokens do not carry tenant permissions.\n", + "operationId": "getIntegrationV2", + "summary": "getIntegrationV2", + "description": "Retrieve a specific integration with all its embedded use cases", "tags": [ - "integrations", - "proxy" - ], - "security": [ - { - "EpilotAuth": [] - } + "integrations" ], "parameters": [ { @@ -1995,76 +2118,34 @@ "type": "string", "format": "uuid" } - }, - { - "name": "useCaseId", - "in": "path", - "required": true, - "description": "The use case ID (must be of type secure_proxy)", - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "limit", - "in": "query", - "required": false, - "description": "Maximum number of history entries to return. Default 10, max 50.\nCapped at the service-layer page size (20) so `limit > 20` is silently\nclamped to 20. UI-08 requests 5-10 for the panel view.\n", - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 50, - "default": 10 - } } ], "responses": { "200": { - "description": "Successfully retrieved secure_proxy history", + "description": "Successfully retrieved integration with use cases", "content": { "application/json": { "schema": { - "type": "object", - "required": [ - "history" - ], - "properties": { - "history": { - "type": "array", - "description": "Entries are `SecureProxyUseCaseHistoryEntry` in reverse\nchronological order (newest first). Entries older than the\nrequested `limit` are not returned.\n", - "items": { - "$ref": "#/components/schemas/SecureProxyUseCaseHistoryEntry" - } - } - } + "$ref": "#/components/schemas/IntegrationWithUseCases" } } } }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, "401": { "$ref": "#/components/responses/Unauthorized" }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, "404": { - "description": "Use case not found or not of type secure_proxy" + "description": "Integration not found" }, "500": { "$ref": "#/components/responses/InternalServerError" } } - } - }, - "/v1/integrations/{integrationId}/app-mapping": { + }, "put": { - "operationId": "setIntegrationAppMapping", - "summary": "setIntegrationAppMapping", - "description": "Creates or updates a mapping from an app/component to an integration.\nThis allows ERP updates sent via app_id and component_id to be associated\nwith a specific integration configuration.\n", + "operationId": "updateIntegrationV2", + "summary": "updateIntegrationV2", + "description": "Update an existing integration with embedded use cases.\nThe integration must already exist.\nUse cases are updated declaratively:\n- Use cases in the request with matching IDs are updated\n- Use cases in the request without matching IDs are created\n- Existing use cases not in the request are deleted\n", "tags": [ "integrations" ], @@ -2073,7 +2154,7 @@ "name": "integrationId", "in": "path", "required": true, - "description": "The integration ID to map to", + "description": "The integration ID (client-provided)", "schema": { "type": "string", "format": "uuid" @@ -2085,18 +2166,18 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SetIntegrationAppMappingRequest" + "$ref": "#/components/schemas/UpsertIntegrationWithUseCasesRequest" } } } }, "responses": { "200": { - "description": "App mapping set successfully", + "description": "Integration updated successfully with use cases", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IntegrationAppMapping" + "$ref": "#/components/schemas/IntegrationWithUseCases" } } } @@ -2110,25 +2191,15 @@ "404": { "description": "Integration not found" }, - "409": { - "description": "Mapping already exists (use overwrite=true to replace)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponseBase" - } - } - } - }, "500": { "$ref": "#/components/responses/InternalServerError" } } }, "delete": { - "operationId": "deleteIntegrationAppMapping", - "summary": "deleteIntegrationAppMapping", - "description": "Removes a mapping from an app/component to an integration.\n", + "operationId": "deleteIntegrationV2", + "summary": "deleteIntegrationV2", + "description": "Delete an integration and all its use cases", "tags": [ "integrations" ], @@ -2137,26 +2208,16 @@ "name": "integrationId", "in": "path", "required": true, - "description": "The integration ID (used for authorization, must match the mapping)", + "description": "The integration ID", "schema": { "type": "string", "format": "uuid" } } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DeleteIntegrationAppMappingRequest" - } - } - } - }, "responses": { "200": { - "description": "App mapping deleted successfully", + "description": "Integration deleted successfully", "content": { "application/json": { "schema": { @@ -2170,14 +2231,11 @@ } } }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { - "description": "App mapping not found" + "description": "Integration not found" }, "500": { "$ref": "#/components/responses/InternalServerError" @@ -2185,15 +2243,19 @@ } } }, - "/v1/integrations/{integrationId}/monitoring/inbound-events": { - "post": { - "operationId": "queryInboundMonitoringEvents", - "summary": "queryInboundMonitoringEvents", - "description": "Query inbound monitoring events for a specific integration.\nReturns detailed information about inbound sync events from ERP systems,\nincluding success rates, error breakdowns, and processing metrics.\n", + "/v2/integrations/{integrationId}/notifications/history": { + "get": { + "operationId": "listNotificationHistory", + "summary": "listNotificationHistory", + "description": "Returns the cursor-paginated, newest-first notification history for an\nintegration (every real notification decision — both fired and suppressed).\nRequires the `integration:view` permission on the integration's organization.\n", "tags": [ - "monitoring", "integrations" ], + "security": [ + { + "EpilotAuth": [] + } + ], "parameters": [ { "name": "integrationId", @@ -2204,21 +2266,47 @@ "type": "string", "format": "uuid" } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/QueryInboundMonitoringEventsRequest" - } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "description": "Opaque base64 pagination cursor returned as `next_cursor` by a prior page.", + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "description": "Requested number of items to return. Values above 100 are accepted and clamped server-side to 100 (the enforcement point is the handler, not this schema), so a large value never 400s at the contract layer.", + "schema": { + "type": "integer", + "minimum": 1, + "default": 50 + } + }, + { + "name": "type", + "in": "query", + "required": false, + "description": "Optional notification type filter (e.g. `critical_error`, `error_threshold`).", + "schema": { + "type": "string" } } - }, + ], "responses": { "200": { - "$ref": "#/components/responses/QueryInboundMonitoringEventsResponse" + "description": "Cursor-paginated notification history (newest first)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotificationHistoryResponse" + } + } + } }, "400": { "$ref": "#/components/responses/BadRequest" @@ -2226,6 +2314,9 @@ "401": { "$ref": "#/components/responses/Unauthorized" }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, "404": { "$ref": "#/components/responses/NotFound" }, @@ -2235,15 +2326,19 @@ } } }, - "/v1/integrations/{integrationId}/monitoring/stats": { + "/v2/integrations/{integrationId}/notifications/test": { "post": { - "operationId": "getMonitoringStats", - "summary": "getMonitoringStats", - "description": "Get aggregated statistics for both inbound and outbound monitoring events for a specific integration.\nReturns summary metrics for inbound (ERP sync) and outbound (webhook delivery) events,\nincluding success/error counts and optional breakdowns.\n", + "operationId": "testSendNotification", + "summary": "testSendNotification", + "description": "Renders and sends ONE representative notification of the requested kind/type to\nthe CALLING USER ONLY (never any other recipient), so an operator can preview how\na notification looks. A test send does NOT write to the notification history.\nRequires the `integration:manage` permission on the integration's organization.\n", "tags": [ - "monitoring", "integrations" ], + "security": [ + { + "EpilotAuth": [] + } + ], "parameters": [ { "name": "integrationId", @@ -2261,14 +2356,21 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetMonitoringStatsRequest" + "$ref": "#/components/schemas/TestNotificationRequest" } } } }, "responses": { - "200": { - "$ref": "#/components/responses/GetMonitoringStatsResponse" + "202": { + "description": "Test notification accepted for delivery to the calling user (sent is true)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TestNotificationResponse" + } + } + } }, "400": { "$ref": "#/components/responses/BadRequest" @@ -2276,24 +2378,41 @@ "401": { "$ref": "#/components/responses/Unauthorized" }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, "404": { "$ref": "#/components/responses/NotFound" }, "500": { "$ref": "#/components/responses/InternalServerError" + }, + "502": { + "description": "The downstream notification service rejected or failed the test send.\nThe body uses TestNotificationResponse with sent set to false so the\noperator sees the real (failed) delivery outcome.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TestNotificationResponse" + } + } + } } } } }, - "/v1/integrations/{integrationId}/monitoring/timeseries": { - "post": { - "operationId": "getMonitoringTimeSeries", - "summary": "getMonitoringTimeSeries", - "description": "Get time-series aggregated event counts for monitoring charts.\nReturns pre-bucketed counts at configurable intervals for both inbound and outbound events.\nMaximum of 200 buckets per request. Returns 400 if the time range and interval would exceed this limit.\n", + "/v2/integrations/{integrationId}/notifications/status": { + "get": { + "operationId": "getNotificationStatus", + "summary": "getNotificationStatus", + "description": "Returns the live per-rule alert state and (for 'auto' rules) the current\nhour-of-week baseline band for an integration's notification monitoring.\nReflects the latest 5-minute sweep — near-real-time, not live.\nRequires the `integration:view` permission on the integration's organization.\n", "tags": [ - "monitoring", "integrations" ], + "security": [ + { + "EpilotAuth": [] + } + ], "parameters": [ { "name": "integrationId", @@ -2304,71 +2423,40 @@ "type": "string", "format": "uuid" } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GetMonitoringTimeSeriesRequest" - } - } - } - }, - "responses": { - "200": { - "$ref": "#/components/responses/GetMonitoringTimeSeriesResponse" - }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, - "401": { - "$ref": "#/components/responses/Unauthorized" }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - } - } - }, - "/v1/integrations/{integrationId}/outbound-status": { - "get": { - "operationId": "getOutboundStatus", - "summary": "getOutboundStatus", - "description": "Get the status of all outbound use cases for a specific integration.\nReturns conflict information when events or webhooks are disabled but the use case is enabled.\n", - "tags": [ - "integrations" - ], - "parameters": [ { - "name": "integrationId", - "in": "path", - "required": true, - "description": "The integration ID", + "name": "include", + "in": "query", + "required": false, + "description": "Add `baseline_series` to also return all 168 hour-of-week buckets per\n'auto' rule (heavier; omit for just the current-bucket markers).\n", "schema": { "type": "string", - "format": "uuid" + "enum": [ + "baseline_series" + ] } } ], "responses": { "200": { - "description": "Successfully retrieved outbound status", + "description": "Live notification status", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OutboundStatusResponse" + "$ref": "#/components/schemas/NotificationStatusResponse" } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, "404": { "$ref": "#/components/responses/NotFound" }, @@ -2378,13 +2466,14 @@ } } }, - "/v1/integrations/{integrationId}/outbound/messages/poll": { - "post": { - "operationId": "pollOutboundMessages", - "summary": "pollOutboundMessages", - "description": "Poll outbound messages for an integration's poll-mode use cases.\nTakes a lease on the head-of-line batch of the integration's FIFO stream:\nthe returned messages stay invisible to subsequent polls until they are\nacknowledged or the visibility timeout elapses. POST because taking a\nlease mutates server state — auto-retrying middleware must not burn\nleases. One in-flight batch per stream: while a lease is active (or\nanother poll wins the race), the response is an empty batch\n(`messages: []`), not an error. Requires the `integration:consume` grant.\n", + "/v2/integrations/{integrationId}/use-cases/{useCaseId}/secure-proxy-whitelist": { + "get": { + "operationId": "getSecureProxyWhitelist", + "summary": "getSecureProxyWhitelist", + "description": "Returns the current allowed_domains, allowed_ips, and vpc_mode for a secure_proxy use case.\nStaff-only — gated by internal-auth issuer AND admin-portal Cognito user pool membership.\nRejects Login-As tokens.\n", "tags": [ - "integrations" + "integrations", + "proxy" ], "security": [ { @@ -2401,25 +2490,25 @@ "type": "string", "format": "uuid" } - } - ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PollOutboundMessagesRequest" - } + }, + { + "name": "useCaseId", + "in": "path", + "required": true, + "description": "The use case ID", + "schema": { + "type": "string", + "format": "uuid" } } - }, + ], "responses": { "200": { - "description": "Leased batch of outbound messages (possibly empty)", + "description": "Current whitelist", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PollOutboundMessagesResponse" + "$ref": "#/components/schemas/SecureProxyWhitelist" } } } @@ -2434,21 +2523,17 @@ "$ref": "#/components/responses/Forbidden" }, "404": { - "$ref": "#/components/responses/NotFound" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" + "description": "Use case not found or not of type secure_proxy" } } - } - }, - "/v1/integrations/{integrationId}/outbound/messages/ack": { - "post": { - "operationId": "ackOutboundMessages", - "summary": "ackOutboundMessages", - "description": "Acknowledge polled outbound messages. Acks are validated against the\nactive lease and committed as a prefix-contiguous cursor advance:\nmessages must be acknowledged in stream order. Out-of-order acks past\nthe first gap are rejected per id (`out_of_order`), acks with an\noutdated lease token are rejected (`stale_lease`), and unknown ids are\nrejected (`not_found`). Acknowledged messages are never delivered\nagain. Requires the `integration:consume` grant.\n", + }, + "put": { + "operationId": "updateSecureProxyWhitelist", + "summary": "updateSecureProxyWhitelist", + "description": "Replaces allowed_domains and/or allowed_ips on a secure_proxy use case.\nAt least one of the two fields is required. Validation mirrors the CLI's\n`validateDomainPatterns` / `validateCidrs`. Writes a USECASE_HISTORY row\nwith the admin user's email as `changed_by`.\n\nUpdate semantics per field:\n - **omitted** — field is not modified; the stored value is preserved.\n - **non-empty array** — the stored value is replaced with the supplied list.\n - **empty array (`[]`)** — the list is cleared (stored as `[]`). This is\n the canonical way to remove all entries. `null` is not accepted.\n\nStaff-only — same auth gates as GET.\n", "tags": [ - "integrations" + "integrations", + "proxy" ], "security": [ { @@ -2465,6 +2550,16 @@ "type": "string", "format": "uuid" } + }, + { + "name": "useCaseId", + "in": "path", + "required": true, + "description": "The use case ID", + "schema": { + "type": "string", + "format": "uuid" + } } ], "requestBody": { @@ -2472,18 +2567,18 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AckOutboundMessagesRequest" + "$ref": "#/components/schemas/SecureProxyWhitelistUpdate" } } } }, "responses": { "200": { - "description": "Per-id acknowledgement results", + "description": "Updated whitelist (same shape as GET)", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AckOutboundMessagesResponse" + "$ref": "#/components/schemas/SecureProxyWhitelist" } } } @@ -2498,21 +2593,19 @@ "$ref": "#/components/responses/Forbidden" }, "404": { - "$ref": "#/components/responses/NotFound" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" + "description": "Use case not found or not of type secure_proxy" } } } }, - "/v1/integrations/{integrationId}/outbound/messages/dlq": { + "/v2/integrations/{integrationId}/use-cases/{useCaseId}/secure-proxy-whitelist/history": { "get": { - "operationId": "listOutboundDlqMessages", - "summary": "listOutboundDlqMessages", - "description": "List an integration's dead-lettered outbound queue messages\n(poison_policy enforcement and operator skips move messages here).\nOperator endpoint — requires the `integration:manage` grant. Message\npayloads are NOT included in listings: entries are keyed by the\nopaque message id and carry delivery metadata only. Paginated via an\nopaque `next_token`.\n", + "operationId": "listSecureProxyWhitelistHistory", + "summary": "listSecureProxyWhitelistHistory", + "description": "Returns the most recent USECASE_HISTORY entries for a secure_proxy use case,\nin reverse chronological order (newest first). Each entry includes the\nactor email (`changed_by`), the ISO-8601 timestamp (`history_created_at`),\nthe `change_description` (free-text action), and the full `configuration`\nsnapshot — from which UI-08 computes a before/after diff between consecutive\nentries.\n\nStaff-only — gated by internal-auth issuer AND admin-portal Cognito user pool\nmembership. Rejects Login-As tokens (same auth gate as the GET / PUT\nsecure-proxy-whitelist operations).\n\nThin wrapper over the service-layer `listUseCaseHistory` that powers\n`GET /v1/integrations/{integrationId}/use-cases/{useCaseId}/history` — the\nseparate path exists because the /v1 variant is tenant-gated and admin-portal\ninternal-auth tokens do not carry tenant permissions.\n", "tags": [ - "integrations" + "integrations", + "proxy" ], "security": [ { @@ -2531,34 +2624,47 @@ } }, { - "name": "limit", - "in": "query", - "required": false, - "description": "Maximum number of DLQ entries to return", + "name": "useCaseId", + "in": "path", + "required": true, + "description": "The use case ID (must be of type secure_proxy)", "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 25 + "type": "string", + "format": "uuid" } }, { - "name": "next_token", + "name": "limit", "in": "query", "required": false, - "description": "Opaque pagination token from a previous response", + "description": "Maximum number of history entries to return. Default 10, max 50.\nCapped at the service-layer page size (20) so `limit > 20` is silently\nclamped to 20. UI-08 requests 5-10 for the panel view.\n", "schema": { - "type": "string" + "type": "integer", + "minimum": 1, + "maximum": 50, + "default": 10 } } ], "responses": { "200": { - "description": "Page of dead-lettered messages", + "description": "Successfully retrieved secure_proxy history", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OutboundDlqListResponse" + "type": "object", + "required": [ + "history" + ], + "properties": { + "history": { + "type": "array", + "description": "Entries are `SecureProxyUseCaseHistoryEntry` in reverse\nchronological order (newest first). Entries older than the\nrequested `limit` are not returned.\n", + "items": { + "$ref": "#/components/schemas/SecureProxyUseCaseHistoryEntry" + } + } + } } } } @@ -2573,7 +2679,7 @@ "$ref": "#/components/responses/Forbidden" }, "404": { - "$ref": "#/components/responses/NotFound" + "description": "Use case not found or not of type secure_proxy" }, "500": { "$ref": "#/components/responses/InternalServerError" @@ -2581,25 +2687,20 @@ } } }, - "/v1/integrations/{integrationId}/outbound/messages/dlq/redrive": { - "post": { - "operationId": "redriveOutboundDlqMessages", - "summary": "redriveOutboundDlqMessages", - "description": "Redrive selected dead-lettered messages back into the live stream.\nOperator endpoint — requires the `integration:manage` grant.\nA redriven message is re-enqueued at the tail with a new id and\nsequence — it is delivered out of its original per-entity order (the\nstream has moved on); this is inherent to redrive and matches SQS DLQ\nsemantics. The redriven copy starts with zero delivery attempts and a\nfresh retention window; the original DLQ entry is removed. Per-id\nresults report `redriven` or `not_found` (unknown ids, or entries\nconcurrently redriven/expired).\n", + "/v1/integrations/{integrationId}/app-mapping": { + "put": { + "operationId": "setIntegrationAppMapping", + "summary": "setIntegrationAppMapping", + "description": "Creates or updates a mapping from an app/component to an integration.\nThis allows ERP updates sent via app_id and component_id to be associated\nwith a specific integration configuration.\n", "tags": [ "integrations" ], - "security": [ - { - "EpilotAuth": [] - } - ], "parameters": [ { "name": "integrationId", "in": "path", "required": true, - "description": "The integration ID", + "description": "The integration ID to map to", "schema": { "type": "string", "format": "uuid" @@ -2611,18 +2712,18 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RedriveOutboundDlqRequest" + "$ref": "#/components/schemas/SetIntegrationAppMappingRequest" } } } }, "responses": { "200": { - "description": "Per-id redrive results", + "description": "App mapping set successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RedriveOutboundDlqResponse" + "$ref": "#/components/schemas/IntegrationAppMapping" } } } @@ -2633,37 +2734,37 @@ "401": { "$ref": "#/components/responses/Unauthorized" }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, "404": { - "$ref": "#/components/responses/NotFound" + "description": "Integration not found" + }, + "409": { + "description": "Mapping already exists (use overwrite=true to replace)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBase" + } + } + } }, "500": { "$ref": "#/components/responses/InternalServerError" } } - } - }, - "/v1/integrations/{integrationId}/outbound/messages/unblock": { - "post": { - "operationId": "unblockOutboundStream", - "summary": "unblockOutboundStream", - "description": "Unblock an integration's outbound stream halted by the `block`\npoison policy: skips (dead-letters) the current blocked head message,\nemitting MSG_DEAD_LETTERED and letting the next message become the\nhead. Operator endpoint — requires the `integration:manage` grant.\nReturns `unblocked: false` as a no-op when the stream is not\ncurrently blocked (or the state moved concurrently) — safe to retry.\n", + }, + "delete": { + "operationId": "deleteIntegrationAppMapping", + "summary": "deleteIntegrationAppMapping", + "description": "Removes a mapping from an app/component to an integration.\n", "tags": [ "integrations" ], - "security": [ - { - "EpilotAuth": [] - } - ], "parameters": [ { "name": "integrationId", "in": "path", "required": true, - "description": "The integration ID", + "description": "The integration ID (used for authorization, must match the mapping)", "schema": { "type": "string", "format": "uuid" @@ -2671,22 +2772,27 @@ } ], "requestBody": { - "required": false, + "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UnblockOutboundStreamRequest" + "$ref": "#/components/schemas/DeleteIntegrationAppMappingRequest" } } } }, "responses": { "200": { - "description": "Unblock outcome", + "description": "App mapping deleted successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UnblockOutboundStreamResponse" + "type": "object", + "properties": { + "message": { + "type": "string" + } + } } } } @@ -2697,11 +2803,8 @@ "401": { "$ref": "#/components/responses/Unauthorized" }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, "404": { - "$ref": "#/components/responses/NotFound" + "description": "App mapping not found" }, "500": { "$ref": "#/components/responses/InternalServerError" @@ -2709,11 +2812,12 @@ } } }, - "/v1/integrations/{integrationId}/monitoring/access-logs": { + "/v1/integrations/{integrationId}/monitoring/inbound-events": { "post": { - "operationId": "queryAccessLogs", - "summary": "queryAccessLogs", - "description": "Query API access logs for a specific integration's organization.\nReturns access token usage analytics filtered by user_id (access token).\nSupports infinite scroll pagination with cursor-based navigation.\n", + "deprecated": true, + "operationId": "queryInboundMonitoringEvents", + "summary": "queryInboundMonitoringEvents", + "description": "**Deprecated and no longer implemented.** The `erp_monitoring` table this\nendpoint read has been retired in favour of the unified `erp_monitoring_v2`\ntable. The endpoint is kept only so existing clients do not 404; it always\nresponds `200` with an empty result and never reads any data.\n\nUse `POST /v2/integrations/{integrationId}/monitoring/events` instead,\nfiltered to `use_case_type: inbound`.\n", "tags": [ "monitoring", "integrations" @@ -2723,7 +2827,7 @@ "name": "integrationId", "in": "path", "required": true, - "description": "The integration ID (used for tenant authorization)", + "description": "The integration ID", "schema": { "type": "string", "format": "uuid" @@ -2735,14 +2839,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/QueryAccessLogsRequest" + "$ref": "#/components/schemas/QueryInboundMonitoringEventsRequest" } } } }, "responses": { "200": { - "$ref": "#/components/responses/QueryAccessLogsResponse" + "$ref": "#/components/responses/QueryInboundMonitoringEventsResponse" }, "400": { "$ref": "#/components/responses/BadRequest" @@ -2759,11 +2863,12 @@ } } }, - "/v1/integrations/{integrationId}/monitoring/outbound-events": { + "/v1/integrations/{integrationId}/monitoring/stats": { "post": { - "operationId": "queryOutboundMonitoringEvents", - "summary": "queryOutboundMonitoringEvents", - "description": "Query outbound monitoring events for a specific integration.\nReturns detailed information about outbound event deliveries,\nfiltered by event_name (event_catalog_event) linked to the integration's outbound use cases.\n", + "deprecated": true, + "operationId": "getMonitoringStats", + "summary": "getMonitoringStats", + "description": "**Deprecated and no longer implemented.** The `erp_monitoring` and\n`webhook_events` tables this endpoint aggregated have been retired in favour\nof the unified `erp_monitoring_v2` table. The endpoint is kept only so\nexisting clients do not 404; it always responds `200` with zeroed counters\nand never reads any data.\n\nUse `POST /v2/integrations/{integrationId}/monitoring/stats` instead.\n", "tags": [ "monitoring", "integrations" @@ -2785,14 +2890,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/QueryOutboundMonitoringEventsRequest" + "$ref": "#/components/schemas/GetMonitoringStatsRequest" } } } }, "responses": { "200": { - "$ref": "#/components/responses/QueryOutboundMonitoringEventsResponse" + "$ref": "#/components/responses/GetMonitoringStatsResponse" }, "400": { "$ref": "#/components/responses/BadRequest" @@ -2809,11 +2914,12 @@ } } }, - "/v2/integrations/{integrationId}/monitoring/events": { + "/v1/integrations/{integrationId}/monitoring/timeseries": { "post": { - "operationId": "queryMonitoringEventsV2", - "summary": "queryMonitoringEventsV2", - "description": "Query monitoring events from the unified erp_monitoring_v2 table.\nReturns all event types (inbound, outbound, file_proxy, etc.) in a single list.\nReplaces the separate v1 inbound-events and outbound-events endpoints.\n", + "deprecated": true, + "operationId": "getMonitoringTimeSeries", + "summary": "getMonitoringTimeSeries", + "description": "**Deprecated and no longer implemented.** The `erp_monitoring` and\n`webhook_events` tables this endpoint bucketed have been retired in favour\nof the unified `erp_monitoring_v2` table. The endpoint is kept only so\nexisting clients do not 404; it always responds `200` with an empty bucket\nlist and never reads any data.\n\nUse `POST /v2/integrations/{integrationId}/monitoring/time-series` instead.\n", "tags": [ "monitoring", "integrations" @@ -2835,14 +2941,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/QueryMonitoringEventsV2Request" + "$ref": "#/components/schemas/GetMonitoringTimeSeriesRequest" } } } }, "responses": { "200": { - "$ref": "#/components/responses/QueryMonitoringEventsV2Response" + "$ref": "#/components/responses/GetMonitoringTimeSeriesResponse" }, "400": { "$ref": "#/components/responses/BadRequest" @@ -2859,13 +2965,12 @@ } } }, - "/v2/integrations/{integrationId}/monitoring/stats": { - "post": { - "operationId": "getMonitoringStatsV2", - "summary": "getMonitoringStatsV2", - "description": "Get aggregated statistics from the unified erp_monitoring_v2 table.\nReturns combined metrics for all event types with optional breakdowns.\n", + "/v1/integrations/{integrationId}/outbound-status": { + "get": { + "operationId": "getOutboundStatus", + "summary": "getOutboundStatus", + "description": "Get the status of all outbound use cases for a specific integration.\nReturns conflict information when events or webhooks are disabled but the use case is enabled.\n", "tags": [ - "monitoring", "integrations" ], "parameters": [ @@ -2880,22 +2985,16 @@ } } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GetMonitoringStatsV2Request" - } - } - } - }, "responses": { "200": { - "$ref": "#/components/responses/GetMonitoringStatsV2Response" - }, - "400": { - "$ref": "#/components/responses/BadRequest" + "description": "Successfully retrieved outbound status", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutboundStatusResponse" + } + } + } }, "401": { "$ref": "#/components/responses/Unauthorized" @@ -2909,40 +3008,46 @@ } } }, - "/v2/integrations/{integrationId}/monitoring/time-series": { - "post": { - "operationId": "getMonitoringTimeSeriesV2", - "summary": "getMonitoringTimeSeriesV2", - "description": "Get time-series aggregated event counts from the unified erp_monitoring_v2 table.\nReturns bucketed counts for chart rendering.\n", + "/v1/integrations/entities/{entityId}/sync-status": { + "get": { + "operationId": "getEntitySyncStatus", + "summary": "getEntitySyncStatus", + "description": "Get the inbound ERP sync status of an entity: when each integration last\nsynchronized (checked) the entity against the ERP, and when it last\nactually changed it. `last_synced_at` also advances on no-op checks —\nevents that were received and evaluated but changed nothing — which by\ndesign leave no trace on the entity itself (no activity feed entry, no\n`_updated_at` bump). Use it to tell whether an entity is up to date with\nthe ERP. Returns an empty list for entities no inbound use case has\nprocessed. Org-scoped via the caller's token.\n\nDeprecated alias: `GET /v1/entities/{entityId}/sync-status` is still\nserved (rewritten to this path in the API handler) but will be removed —\nmigrate callers to this path.\n", "tags": [ - "monitoring", "integrations" ], "parameters": [ { - "name": "integrationId", + "name": "entityId", "in": "path", "required": true, - "description": "The integration ID", + "description": "The entity ID", "schema": { "type": "string", "format": "uuid" } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GetMonitoringTimeSeriesV2Request" - } + }, + { + "name": "integration_id", + "in": "query", + "required": false, + "description": "Narrow the result to a single integration", + "schema": { + "type": "string", + "format": "uuid" } } - }, + ], "responses": { "200": { - "$ref": "#/components/responses/GetMonitoringTimeSeriesV2Response" + "description": "Successfully retrieved entity sync status", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EntitySyncStatusResponse" + } + } + } }, "400": { "$ref": "#/components/responses/BadRequest" @@ -2950,24 +3055,25 @@ "401": { "$ref": "#/components/responses/Unauthorized" }, - "404": { - "$ref": "#/components/responses/NotFound" - }, "500": { "$ref": "#/components/responses/InternalServerError" } } } }, - "/v2/integrations/{integrationId}/monitoring/events/{eventId}/associated": { - "get": { - "operationId": "getAssociatedMonitoringEvents", - "summary": "getAssociatedMonitoringEvents", - "description": "Returns all monitoring events sharing the same event_id, ordered chronologically.\nAlso includes the original inbound event payload from erp_incoming_events if available.\nUsed to display a full event trace/timeline.\n", + "/v1/integrations/{integrationId}/outbound/messages/poll": { + "post": { + "operationId": "pollOutboundMessages", + "summary": "pollOutboundMessages", + "description": "Poll outbound messages for an integration's poll-mode use cases.\nTakes a lease on the head-of-line batch of the integration's FIFO stream:\nthe returned messages stay invisible to subsequent polls until they are\nacknowledged or the visibility timeout elapses. POST because taking a\nlease mutates server state — auto-retrying middleware must not burn\nleases. One in-flight batch per stream: while a lease is active (or\nanother poll wins the race), the response is an empty batch\n(`messages: []`), not an error. Requires the `integration:consume` grant.\n", "tags": [ - "monitoring", "integrations" ], + "security": [ + { + "EpilotAuth": [] + } + ], "parameters": [ { "name": "integrationId", @@ -2978,107 +3084,89 @@ "type": "string", "format": "uuid" } - }, - { - "name": "eventId", - "in": "path", - "required": true, - "description": "The event ID to get associated events for", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "$ref": "#/components/responses/GetAssociatedMonitoringEventsResponse" - }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" } - } - } - }, - "/v1/integrations/secure-proxies": { - "get": { - "operationId": "listSecureProxies", - "summary": "List all secure proxy use cases", - "description": "Lists all secure_proxy use cases across all integrations for the authenticated organization.\nReturns minimal data suitable for dropdowns and selection UIs.\n", - "tags": [ - "integrations" ], - "security": [ - { - "EpilotAuth": [] + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PollOutboundMessagesRequest" + } + } } - ], + }, "responses": { "200": { - "description": "Successfully retrieved secure proxy use cases", + "description": "Leased batch of outbound messages (possibly empty)", "content": { "application/json": { "schema": { - "type": "object", - "required": [ - "secure_proxies" - ], - "properties": { - "secure_proxies": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SecureProxySummary" - } - } - } + "$ref": "#/components/schemas/PollOutboundMessagesResponse" } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, "500": { "$ref": "#/components/responses/InternalServerError" } } } }, - "/v1/secure-proxy": { + "/v1/integrations/{integrationId}/outbound/messages/ack": { "post": { - "operationId": "secureProxy", - "summary": "Proxy HTTP request through secure VPC", - "description": "Routes an HTTP request through a VPC with either static IP egress or VPN secure link access.\nThe VPC mode is determined by the referenced secure_proxy use case configuration.\nFor secure_link mode, the target URL must match the use case's allowed_domains whitelist.\n", + "operationId": "ackOutboundMessages", + "summary": "ackOutboundMessages", + "description": "Acknowledge polled outbound messages. Acks are validated against the\nactive lease and committed as a prefix-contiguous cursor advance:\nmessages must be acknowledged in stream order. Out-of-order acks past\nthe first gap are rejected per id (`out_of_order`), acks with an\noutdated lease token are rejected (`stale_lease`), and unknown ids are\nrejected (`not_found`). Acknowledged messages are never delivered\nagain. Requires the `integration:consume` grant.\n", "tags": [ - "proxy" + "integrations" ], "security": [ { "EpilotAuth": [] } ], + "parameters": [ + { + "name": "integrationId", + "in": "path", + "required": true, + "description": "The integration ID", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], "requestBody": { "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SecureProxyRequest" + "$ref": "#/components/schemas/AckOutboundMessagesRequest" } } } }, "responses": { "200": { - "description": "Proxied response from the target URL", + "description": "Per-id acknowledgement results", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SecureProxyResponse" + "$ref": "#/components/schemas/AckOutboundMessagesResponse" } } } @@ -3090,55 +3178,99 @@ "$ref": "#/components/responses/Unauthorized" }, "403": { - "description": "Forbidden - insufficient permissions or target domain not whitelisted", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponseBase" - } - } - } + "$ref": "#/components/responses/Forbidden" }, - "502": { - "description": "Upstream error from the proxied target", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponseBase" - } - } + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/v1/integrations/{integrationId}/outbound/messages/dlq": { + "get": { + "operationId": "listOutboundDlqMessages", + "summary": "listOutboundDlqMessages", + "description": "List an integration's dead-lettered outbound queue messages\n(poison_policy enforcement and operator skips move messages here).\nOperator endpoint — requires the `integration:manage` grant. Message\npayloads are NOT included in listings: entries are keyed by the\nopaque message id and carry delivery metadata only. Paginated via an\nopaque `next_token`.\n", + "tags": [ + "integrations" + ], + "security": [ + { + "EpilotAuth": [] + } + ], + "parameters": [ + { + "name": "integrationId", + "in": "path", + "required": true, + "description": "The integration ID", + "schema": { + "type": "string", + "format": "uuid" } }, - "503": { - "description": "VPC proxy not available for the requested mode", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponseBase" - } - } + { + "name": "limit", + "in": "query", + "required": false, + "description": "Maximum number of DLQ entries to return", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 25 } }, - "504": { - "description": "Upstream timeout", + { + "name": "next_token", + "in": "query", + "required": false, + "description": "Opaque pagination token from a previous response", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Page of dead-lettered messages", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponseBase" + "$ref": "#/components/schemas/OutboundDlqListResponse" } } } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" } } } }, - "/v1/managed-call/{slug}/execute": { + "/v1/integrations/{integrationId}/outbound/messages/dlq/redrive": { "post": { - "operationId": "managedCallExecute", - "summary": "Execute a managed call operation", - "description": "Execute a managed call operation synchronously. The slug in the path acts as the RPC method name.\nCalls an external partner API with JSONata mapping on both request and response.\n", + "operationId": "redriveOutboundDlqMessages", + "summary": "redriveOutboundDlqMessages", + "description": "Redrive selected dead-lettered messages back into the live stream.\nOperator endpoint — requires the `integration:manage` grant.\nA redriven message is re-enqueued at the tail with a new id and\nsequence — it is delivered out of its original per-entity order (the\nstream has moved on); this is inherent to redrive and matches SQS DLQ\nsemantics. The redriven copy starts with zero delivery attempts and a\nfresh retention window; the original DLQ entry is removed. Per-id\nresults report `redriven` or `not_found` (unknown ids, or entries\nconcurrently redriven/expired).\n", "tags": [ - "managed-call" + "integrations" ], "security": [ { @@ -3147,14 +3279,14 @@ ], "parameters": [ { - "name": "slug", + "name": "integrationId", "in": "path", "required": true, + "description": "The integration ID", "schema": { "type": "string", - "pattern": "^[a-z0-9][a-z0-9_-]*$" - }, - "description": "Use case slug (acts as the RPC method name)" + "format": "uuid" + } } ], "requestBody": { @@ -3162,39 +3294,24 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ManagedCallExecuteRequest" + "$ref": "#/components/schemas/RedriveOutboundDlqRequest" } } } }, "responses": { "200": { - "description": "Managed call executed successfully.\nThe response body is the JSONata-mapped result (or raw external API response if no mapping).\nIf inbound routing is configured, check X-Inbound-Event-Id header for tracking.\n", - "headers": { - "X-Inbound-Event-Id": { - "description": "Event ID for tracking inbound pipeline processing (only present when inbound routing is configured)", - "schema": { - "type": "string" - } - } - }, + "description": "Per-id redrive results", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ManagedCallExecuteResponse" + "$ref": "#/components/schemas/RedriveOutboundDlqResponse" } } } }, "400": { - "description": "Bad request (e.g., invalid URL blocked by SSRF protection)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ManagedCallErrorResponse" - } - } - } + "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" @@ -3207,25 +3324,15 @@ }, "500": { "$ref": "#/components/responses/InternalServerError" - }, - "502": { - "description": "External API error (upstream service returned an error)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ManagedCallErrorResponse" - } - } - } } } } }, - "/v1/integrations/{integrationId}/generate-types-preview": { + "/v1/integrations/{integrationId}/outbound/messages/unblock": { "post": { - "operationId": "generateTypesPreview", - "summary": "Preview scaffolded types for a connector integration", - "description": "Analyses the JSONata mappings of all managed-call use cases in the integration and returns scaffolded type descriptors. The frontend uses these to show the type editor modal where developers fill in leaf types.\n", + "operationId": "unblockOutboundStream", + "summary": "unblockOutboundStream", + "description": "Unblock an integration's outbound stream halted by the `block`\npoison policy: skips (dead-letters) the current blocked head message,\nemitting MSG_DEAD_LETTERED and letting the next message become the\nhead. Operator endpoint — requires the `integration:manage` grant.\nReturns `unblocked: false` as a no-op when the stream is not\ncurrently blocked (or the state moved concurrently) — safe to retry.\n", "tags": [ "integrations" ], @@ -3239,19 +3346,30 @@ "name": "integrationId", "in": "path", "required": true, + "description": "The integration ID", "schema": { "type": "string", "format": "uuid" } } ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnblockOutboundStreamRequest" + } + } + } + }, "responses": { "200": { - "description": "Scaffolded type preview", + "description": "Unblock outcome", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GenerateTypesPreviewResponse" + "$ref": "#/components/schemas/UnblockOutboundStreamResponse" } } } @@ -3259,6 +3377,9 @@ "400": { "$ref": "#/components/responses/BadRequest" }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, "403": { "$ref": "#/components/responses/Forbidden" }, @@ -3271,24 +3392,21 @@ } } }, - "/v1/integrations/{integrationId}/generate-types": { + "/v1/integrations/{integrationId}/monitoring/access-logs": { "post": { - "operationId": "generateTypes", - "summary": "Generate a TypeScript npm package for a connector integration", - "description": "Generates a complete TypeScript npm package with typed interfaces for all managed-call use cases. This is a stateless operation that does not persist any changes. Use the commit-types endpoint to lock configurations after review.\n", + "operationId": "queryAccessLogs", + "summary": "queryAccessLogs", + "description": "Query API access logs for a specific integration's organization.\nReturns access token usage analytics filtered by user_id (access token).\nSupports infinite scroll pagination with cursor-based navigation.\n", "tags": [ + "monitoring", "integrations" ], - "security": [ - { - "EpilotAuth": [] - } - ], "parameters": [ { "name": "integrationId", "in": "path", "required": true, + "description": "The integration ID (used for tenant authorization)", "schema": { "type": "string", "format": "uuid" @@ -3300,27 +3418,20 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GenerateTypesRequest" + "$ref": "#/components/schemas/QueryAccessLogsRequest" } } } }, "responses": { "200": { - "description": "Generated package", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GenerateTypesResponse" - } - } - } + "$ref": "#/components/responses/QueryAccessLogsResponse" }, "400": { "$ref": "#/components/responses/BadRequest" }, - "403": { - "$ref": "#/components/responses/Forbidden" + "401": { + "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" @@ -3331,24 +3442,22 @@ } } }, - "/v1/integrations/{integrationId}/commit-types": { + "/v1/integrations/{integrationId}/monitoring/outbound-events": { "post": { - "operationId": "commitTypes", - "summary": "Commit generated types and lock use case configurations", - "description": "Commits the generated types by locking use case configurations and updating version tracking. Should be called after the user reviews and downloads the generated package.\n", + "deprecated": true, + "operationId": "queryOutboundMonitoringEvents", + "summary": "queryOutboundMonitoringEvents", + "description": "**Deprecated and no longer implemented.** The `webhook_events` table this\nendpoint read has been retired in favour of the unified `erp_monitoring_v2`\ntable. The endpoint is kept only so existing clients do not 404; it always\nresponds `200` with an empty result and never reads any data.\n\nUse `POST /v2/integrations/{integrationId}/monitoring/events` instead,\nfiltered to `use_case_type: outbound`.\n", "tags": [ + "monitoring", "integrations" ], - "security": [ - { - "EpilotAuth": [] - } - ], "parameters": [ { "name": "integrationId", "in": "path", "required": true, + "description": "The integration ID", "schema": { "type": "string", "format": "uuid" @@ -3360,27 +3469,20 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CommitTypesRequest" + "$ref": "#/components/schemas/QueryOutboundMonitoringEventsRequest" } } } }, "responses": { "200": { - "description": "Types committed successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CommitTypesResponse" - } - } - } + "$ref": "#/components/responses/QueryOutboundMonitoringEventsResponse" }, "400": { "$ref": "#/components/responses/BadRequest" }, - "403": { - "$ref": "#/components/responses/Forbidden" + "401": { + "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" @@ -3390,37 +3492,1945 @@ } } } - } - }, - "components": { - "securitySchemes": { - "EpilotAuth": { - "type": "http", - "scheme": "bearer", - "description": "Authorization header with epilot OAuth2 bearer token", - "bearerFormat": "JWT" - } }, - "schemas": { - "ErrorResponseBase": { - "type": "object", - "properties": { - "code": { - "type": "string", - "description": "Computer-readable error code" - }, - "message": { - "type": "string", - "description": "Error message" - } - } - }, - "ErpEvent": { - "type": "object", - "required": [ - "event_type", - "object_type", - "timestamp", + "/v2/integrations/{integrationId}/monitoring/events": { + "post": { + "operationId": "queryMonitoringEventsV2", + "summary": "queryMonitoringEventsV2", + "description": "Query monitoring events from the unified erp_monitoring_v2 table.\nReturns all event types (inbound, outbound, file_proxy, etc.) in a single list.\nReplaces the separate v1 inbound-events and outbound-events endpoints.\n", + "tags": [ + "monitoring", + "integrations" + ], + "parameters": [ + { + "name": "integrationId", + "in": "path", + "required": true, + "description": "The integration ID", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueryMonitoringEventsV2Request" + } + } + } + }, + "responses": { + "200": { + "$ref": "#/components/responses/QueryMonitoringEventsV2Response" + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/v2/integrations/{integrationId}/monitoring/stats": { + "post": { + "operationId": "getMonitoringStatsV2", + "summary": "getMonitoringStatsV2", + "description": "Get aggregated statistics from the unified erp_monitoring_v2 table.\nReturns combined metrics for all event types with optional breakdowns.\n", + "tags": [ + "monitoring", + "integrations" + ], + "parameters": [ + { + "name": "integrationId", + "in": "path", + "required": true, + "description": "The integration ID", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetMonitoringStatsV2Request" + } + } + } + }, + "responses": { + "200": { + "$ref": "#/components/responses/GetMonitoringStatsV2Response" + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/v2/integrations/{integrationId}/monitoring/time-series": { + "post": { + "operationId": "getMonitoringTimeSeriesV2", + "summary": "getMonitoringTimeSeriesV2", + "description": "Get time-series aggregated event counts from the unified erp_monitoring_v2 table.\nReturns bucketed counts for chart rendering.\n", + "tags": [ + "monitoring", + "integrations" + ], + "parameters": [ + { + "name": "integrationId", + "in": "path", + "required": true, + "description": "The integration ID", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetMonitoringTimeSeriesV2Request" + } + } + } + }, + "responses": { + "200": { + "$ref": "#/components/responses/GetMonitoringTimeSeriesV2Response" + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/v2/integrations/{integrationId}/monitoring/events/{eventId}/associated": { + "get": { + "operationId": "getAssociatedMonitoringEvents", + "summary": "getAssociatedMonitoringEvents", + "description": "Returns all monitoring events sharing the same event_id, ordered chronologically.\nAlso includes the original inbound event payload from erp_incoming_events if available.\nUsed to display a full event trace/timeline.\n", + "tags": [ + "monitoring", + "integrations" + ], + "parameters": [ + { + "name": "integrationId", + "in": "path", + "required": true, + "description": "The integration ID", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "eventId", + "in": "path", + "required": true, + "description": "The event ID to get associated events for", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/GetAssociatedMonitoringEventsResponse" + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/v2/integrations/{integrationId}/monitoring/external-events": { + "post": { + "operationId": "ingestExternalMonitoringEvents", + "summary": "ingestExternalMonitoringEvents", + "description": "Ingest monitoring spans produced by an EXTERNAL system (e.g. an integration\nmiddleware), so the Integration Hub is the central monitoring point and the\ncross-system event trace spans both the external system and epilot's own\nprocessing.\n\nEach span is validated, assigned a server-controlled `EXTERNAL_*` taxonomy code\nderived from its `level` (the client never supplies a code — this marks\nprovenance and prevents spoofing an epilot code), has its `use_case_slug`\nresolved against the integration's configured use cases, and is republished onto\nthe same monitoring event bus as epilot's own spans — so external spans are\nfirst-class to the trace view, stats, alerting and digests.\n\nSpans link to the epilot trace via `correlation_id` (the trace id): the middleware\nmust stamp the same `correlation_id` here and on the event it forwards to the\ninbound endpoint. Invalid spans in a batch are reported per-item and do not fail\nthe whole batch.\n", + "tags": [ + "monitoring", + "integrations" + ], + "parameters": [ + { + "name": "integrationId", + "in": "path", + "required": true, + "description": "The integration ID", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IngestExternalMonitoringEventsRequest" + } + } + } + }, + "responses": { + "202": { + "description": "Batch accepted (some spans may have been rejected — see counts).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IngestExternalMonitoringEventsResponse" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/v2/integrations/{integrationId}/monitoring/traces/{correlationId}": { + "get": { + "operationId": "getMonitoringTraceByCorrelation", + "summary": "getMonitoringTraceByCorrelation", + "description": "Returns the cross-system event trace for a `correlation_id`: every monitoring\nspan sharing it — external spans (middleware) plus epilot's own processing spans\n— ordered chronologically, with a rolled-up status and the epilot inbound event\n\"head\" attached. This is the correlation-grouped counterpart to\n`…/events/{eventId}/associated` (which groups a single epilot event's fan-out by\nevent_id). Origin is distinguished by the `EXTERNAL_*` code prefix on external\nspans. Freshness is near-real-time (spans land as they are produced/ingested).\n", + "tags": [ + "monitoring", + "integrations" + ], + "parameters": [ + { + "name": "integrationId", + "in": "path", + "required": true, + "description": "The integration ID", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "correlationId", + "in": "path", + "required": true, + "description": "The trace id (correlation_id) shared across systems", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The assembled cross-system trace.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MonitoringTraceResponse" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/v1/integrations/secure-proxies": { + "get": { + "operationId": "listSecureProxies", + "summary": "listSecureProxies", + "description": "Lists all secure_proxy use cases across all integrations for the authenticated organization.\nReturns minimal data suitable for dropdowns and selection UIs.\n", + "tags": [ + "integrations" + ], + "security": [ + { + "EpilotAuth": [] + } + ], + "responses": { + "200": { + "description": "Successfully retrieved secure proxy use cases", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "secure_proxies" + ], + "properties": { + "secure_proxies": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SecureProxySummary" + } + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/v1/secure-proxy": { + "post": { + "operationId": "secureProxy", + "summary": "secureProxy", + "description": "Routes an HTTP request through a VPC with either static IP egress or VPN secure link access.\nThe VPC mode is determined by the referenced secure_proxy use case configuration.\nFor secure_link mode, the target URL must match the use case's allowed_domains whitelist.\n", + "tags": [ + "proxy" + ], + "security": [ + { + "EpilotAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SecureProxyRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Proxied response from the target URL", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SecureProxyResponse" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "description": "Forbidden - insufficient permissions or target domain not whitelisted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBase" + } + } + } + }, + "502": { + "description": "Upstream failure: epilot could not obtain an HTTP response from the proxied target\n(e.g. TLS handshake failure, connection refused/reset, DNS failure or timeout).\nThe body's `code`/`reason` identify the underlying cause, distinguishing a\nremote-side problem from an epilot-side one. When the target *does* answer with\nits own status, that status and body are passed through unchanged via\n`SecureProxyResponse` instead.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SecureProxyUpstreamError" + } + } + } + }, + "503": { + "description": "VPC proxy not available for the requested mode", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBase" + } + } + } + }, + "504": { + "description": "Upstream timeout", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBase" + } + } + } + } + } + } + }, + "/v1/managed-call/{slug}/execute": { + "post": { + "operationId": "managedCallExecute", + "summary": "managedCallExecute", + "description": "Execute a managed call operation synchronously. The slug in the path acts as the RPC method name.\nCalls an external partner API with JSONata mapping on both request and response.\n", + "tags": [ + "managed-call" + ], + "security": [ + { + "EpilotAuth": [] + } + ], + "parameters": [ + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9_-]*$" + }, + "description": "Use case slug (acts as the RPC method name)" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagedCallExecuteRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Managed call executed successfully.\nThe response body is the JSONata-mapped result (or raw external API response if no mapping).\nIf inbound routing is configured, check X-Inbound-Event-Id header for tracking.\n", + "headers": { + "X-Inbound-Event-Id": { + "description": "Event ID for tracking inbound pipeline processing (only present when inbound routing is configured)", + "schema": { + "type": "string" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagedCallExecuteResponse" + } + } + } + }, + "400": { + "description": "Bad request (e.g., invalid URL blocked by SSRF protection)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagedCallErrorResponse" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + }, + "502": { + "description": "External API error (upstream service returned an error)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagedCallErrorResponse" + } + } + } + } + } + } + }, + "/v1/integrations/{integrationId}/generate-types-preview": { + "post": { + "operationId": "generateTypesPreview", + "summary": "generateTypesPreview", + "description": "Analyses the JSONata mappings of all managed-call use cases in the integration and returns scaffolded type descriptors. The frontend uses these to show the type editor modal where developers fill in leaf types.\n", + "tags": [ + "integrations" + ], + "security": [ + { + "EpilotAuth": [] + } + ], + "parameters": [ + { + "name": "integrationId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Scaffolded type preview", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenerateTypesPreviewResponse" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/v1/integrations/{integrationId}/generate-types": { + "post": { + "operationId": "generateTypes", + "summary": "generateTypes", + "description": "Generates a complete TypeScript npm package with typed interfaces for all managed-call use cases. This is a stateless operation that does not persist any changes. Use the commit-types endpoint to lock configurations after review.\n", + "tags": [ + "integrations" + ], + "security": [ + { + "EpilotAuth": [] + } + ], + "parameters": [ + { + "name": "integrationId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenerateTypesRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Generated package", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenerateTypesResponse" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/v1/integrations/{integrationId}/commit-types": { + "post": { + "operationId": "commitTypes", + "summary": "commitTypes", + "description": "Commits the generated types by locking use case configurations and updating version tracking. Should be called after the user reviews and downloads the generated package.\n", + "tags": [ + "integrations" + ], + "security": [ + { + "EpilotAuth": [] + } + ], + "parameters": [ + { + "name": "integrationId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CommitTypesRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Types committed successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CommitTypesResponse" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/v2/erp/imports": { + "post": { + "operationId": "createErpImport", + "summary": "createErpImport", + "description": "Register an already-uploaded file (S3 ref) as a pricing-file import job. Returns the job and a file preview. Nothing runs yet: no use case is chosen and no validation starts here. Optionally rank candidates with POST /v2/erp/imports/{importId}:suggest-use-cases, then start validation with POST /v2/erp/imports/{importId}:validate.\nPass `import_id` to repoint an existing import at a different file instead, keeping its id and its place in the history — allowed while the import has written nothing.", + "tags": [ + "erp-imports" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateErpImportRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The named import now points at this file. Returned only when `import_id` was supplied — no `Location`, because nothing was created.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateErpImportResponse" + } + } + } + }, + "201": { + "description": "Import job created. Returned only when `import_id` was omitted.", + "headers": { + "Location": { + "description": "URL of the created job (`/v2/erp/imports/{importId}`).", + "schema": { + "type": "string" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateErpImportResponse" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + }, + "get": { + "operationId": "listErpImports", + "summary": "listErpImports", + "description": "List recent pricing-file import jobs for the org, newest first.\n\nPass `next_cursor` back as `cursor` for the next page. A page can be\nshorter than `limit` and still have more behind it, so stop on\n`next_cursor: null`.\n", + "tags": [ + "erp-imports" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "description": "Page size. Values above 100 are clamped to 100.", + "schema": { + "type": "integer", + "minimum": 1, + "default": 50 + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "description": "Opaque cursor from a prior page's `next_cursor`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Import jobs", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErpImportList" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/v2/erp/imports/{importId}": { + "get": { + "operationId": "getErpImport", + "summary": "getErpImport", + "description": "Get a pricing-file import job (status, counts, result links).", + "tags": [ + "erp-imports" + ], + "parameters": [ + { + "name": "importId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The import job", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErpImportJob" + } + } + } + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + }, + "delete": { + "operationId": "deleteErpImport", + "summary": "deleteErpImport", + "description": "Remove an import and the file it owns. Allowed from any status: an import whose run is still in flight is stopped by the deletion, and rows it already wrote stay written.", + "tags": [ + "erp-imports" + ], + "parameters": [ + { + "name": "importId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "The import and its file were removed" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/v2/erp/imports/{importId}:validate": { + "post": { + "operationId": "validateErpImport", + "summary": "validateErpImport", + "description": "Choose the use case to read this file with, and start the validate phase.\nCallable from PENDING, READY and FAILED — so a wrong choice is corrected by calling this again with a different use case, rather than re-uploading. Each call replaces any previous verdict. Any other status returns 409.", + "tags": [ + "erp-imports" + ], + "parameters": [ + { + "name": "importId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidateErpImportRequest" + } + } + } + }, + "responses": { + "202": { + "description": "Validation started", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErpImportJob" + } + } + } + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + }, + "422": { + "description": "The check could not be run. `code` is the translation key.\n\n- `USE_CASE_NOT_USABLE` — missing, not inbound, disabled, or no mapping\n configuration.\n- `SCHEMA_NOT_AUTHORITATIVE` — an entity schema could not be read. Retryable.\n\nA mapping that CAN be judged and is found wanting is not a 422: the job is\nrecorded FAILED with `error.code = VALIDATION_BLOCKED` and the detail in\n`validation.issues`.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBase" + } + } + } + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/v2/erp/imports/{importId}:suggest-use-cases": { + "post": { + "operationId": "suggestErpImportUseCases", + "summary": "suggestErpImportUseCases", + "description": "Rank the org's inbound use cases against this file's columns — the input to the ranked picker (\"matches 6 of your 7 columns\"). Optional: skip this and call `:validate` directly when the use case is already known. Reads only the file's first row, not its data.\nEvery eligible use case is returned, including ones matching nothing: the \"nothing fits, pick anyway\" view needs the full list. Highest match first.\nCSV only — an xlsx import fails with 400 rather than silently returning zero matches, which would look identical to \"we checked and nothing matches\".", + "tags": [ + "erp-imports" + ], + "parameters": [ + { + "name": "importId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Ranked use-case suggestions", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuggestErpImportUseCasesResponse" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/v2/erp/imports/{importId}:execute": { + "post": { + "operationId": "executeErpImport", + "summary": "executeErpImport", + "description": "Confirm and run the write phase of a validated import. Only a READY job may be executed; any other status returns 409.\nThe verdict is re-checked against live inputs first: if the mapping or an entity schema changed since the check, this returns 409 `VALIDATION_STALE` and leaves the verdict untouched for `:validate` to re-form.", + "tags": [ + "erp-imports" + ], + "parameters": [ + { + "name": "importId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExecuteErpImportRequest" + } + } + } + }, + "responses": { + "202": { + "description": "Execution started", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErpImportJob" + } + } + } + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "description": "The job cannot be executed as it stands. `code` is the translation key.\n\n- `IMPORT_NOT_READY` — the job is not READY, or a concurrent confirm won.\n- `VALIDATION_STALE` — the inputs the verdict was formed against have changed;\n re-run `:validate`.\n\nNeither changes the job's status or its verdict.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBase" + } + } + } + }, + "422": { + "description": "The write could not be started. `code` is the translation key.\n\n- `WARNINGS_NOT_ACKNOWLEDGED` — the verdict carries warnings and `ack_warnings`\n was not true.\n- `USE_CASE_NOT_USABLE` — the use case went missing, disabled, or lost its\n mapping configuration after the check.\n- `SCHEMA_NOT_AUTHORITATIVE` — an entity schema could not be read. Retryable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBase" + } + } + } + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/v2/erp/imports/{importId}:abort": { + "post": { + "operationId": "abortErpImport", + "summary": "abortErpImport", + "description": "Ask a running import to stop. Valid while the job is VALIDATING or PROCESSING; any other status returns 409.\nThe stop is cooperative: the job passes through CANCELLING and typically reaches CANCELLED within seconds.\nRows already published to the platform are **kept**: this is a stop, not a rollback. Aborting during VALIDATING has published nothing, so it is always clean; aborting during PROCESSING leaves a partial import, and the rows that landed are visible in monitoring under the job's correlation_id.", + "tags": [ + "erp-imports" + ], + "parameters": [ + { + "name": "importId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "202": { + "description": "The stop was recorded. The job is still finishing up — poll GET /v2/erp/imports/{importId} to observe CANCELLED.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErpImportJob" + } + } + } + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + } + }, + "components": { + "securitySchemes": { + "EpilotAuth": { + "type": "http", + "scheme": "bearer", + "description": "Authorization header with epilot OAuth2 bearer token", + "bearerFormat": "JWT" + } + }, + "schemas": { + "S3Reference": { + "type": "object", + "required": [ + "bucket", + "key" + ], + "properties": { + "bucket": { + "type": "string" + }, + "key": { + "type": "string" + } + } + }, + "CreateErpImportRequest": { + "type": "object", + "description": "Register an already-uploaded file as an import. The use case is chosen later, via `:validate` — upload and interpretation are separate decisions.", + "required": [ + "s3_reference" + ], + "additionalProperties": false, + "properties": { + "s3_reference": { + "$ref": "#/components/schemas/S3Reference" + }, + "include_preview": { + "type": "boolean", + "default": false, + "description": "Return a sample of the file's first rows in `preview`. Only controls whether the sample comes BACK — the head is read either way, because that read is how an unreadable file gets refused." + }, + "import_id": { + "type": "string", + "description": "Repoint an EXISTING import at this file instead of registering a new one — same row, same id, so iterating on which file to import leaves one entry in the history rather than one per attempt.\nRepointing is allowed exactly while the import has written nothing: from PENDING and READY, and from FAILED when `error.code` is one of `VALIDATION_BLOCKED`, `VALIDATE_TIMEOUT` or `FILE_FORMAT_UNSUPPORTED`. Anything else returns 409 with code `IMPORT_NOT_REPLACEABLE`. The repoint clears any verdict the import had.\nAn id that names nothing returns 404. It never falls back to registering a new import: a stale id must fail loudly rather than quietly produce a second one." + } + } + }, + "ValidateErpImportRequest": { + "type": "object", + "required": [ + "integration_id", + "use_case_slug" + ], + "additionalProperties": false, + "properties": { + "integration_id": { + "type": "string" + }, + "use_case_slug": { + "type": "string", + "description": "An inbound use case's slug, unique within its integration." + } + } + }, + "ErpImportUseCaseSuggestion": { + "type": "object", + "required": [ + "integration_id", + "integration_name", + "use_case_slug", + "use_case_name", + "matched_columns", + "entity_types" + ], + "properties": { + "integration_id": { + "type": "string" + }, + "integration_name": { + "type": "string" + }, + "use_case_slug": { + "type": "string" + }, + "use_case_name": { + "type": "string" + }, + "entity_types": { + "type": "integer", + "description": "How many distinct entity schemas this mapping uses." + }, + "matched_columns": { + "type": "integer", + "description": "How many of the FILE's columns this use case reads, by exact trimmed name." + } + } + }, + "SuggestErpImportUseCasesResponse": { + "type": "object", + "required": [ + "file_columns", + "suggestions" + ], + "properties": { + "file_columns": { + "type": "integer", + "description": "Columns in the file. Present even when `suggestions` is empty." + }, + "suggestions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ErpImportUseCaseSuggestion" + } + } + } + }, + "ExecuteErpImportRequest": { + "type": "object", + "description": "Confirmation options. Required only when the verdict carries warnings — a clean import needs no body at all.", + "additionalProperties": false, + "properties": { + "ack_warnings": { + "type": "boolean", + "description": "The caller has seen `validation.issues` and accepts them. Required when `validation.warnings` is greater than 0; ignored otherwise." + } + } + }, + "ErpImportIssue": { + "type": "object", + "description": "A problem found during validation, scoped to the file as a whole rather than to\nindividual rows.\n\n`code` is the translation key and the other fields are its parameters — there is\ndeliberately no message to display. Each code appears at most once, with\neverything it has to say aggregated into that one entry.\n\nThe conditional codes describe a file that cannot be written as variants:\n\n- `TIER_ROWS_NOT_GROUPED` — rows of one variant are spread through the file;\n `columns` names what to group by.\n- `TIER_BANDS_CONFLICT` — two rows of one variant claim the same band.\n- `CONDITION_VALUE_MISSING` — a condition column is blank in every row.\n- `VARIANT_VALUE_CONFLICT` — rows of one variant disagree on a value that is not\n the one being folded; the first row's value is what would be written.\n- `ATTRIBUTE_NOT_OVERRIDABLE` — the schema does not let a variant override this\n attribute, so the mapped value would be discarded.\n- `ATTRIBUTE_NOT_IN_SCHEMA` — the schema does not have the attribute at all.\n\nThe rest reject the mapping against the entity schemas, before the file is read, and\nname the thing at fault in `subject`: `IS_CONDITIONAL_NOT_CONSTANT`,\n`SCHEMA_NOT_CONDITIONABLE`, `SCHEMA_NOT_FOUND`, `SCHEMA_DECLARES_NO_CONDITIONS`,\n`GROUPING_KEY_NOT_A_COLUMN`, `GROUPING_KEY_IS_FOLD_COLUMN`.", + "required": [ + "code", + "severity" + ], + "properties": { + "code": { + "type": "string", + "description": "Enum of possible issue codes.\n", + "enum": [ + "UNIQUE_ID_COLUMN_MISSING", + "MAPPED_COLUMN_MISSING", + "MALFORMED_ROW", + "INVALID_ENCODING", + "EMPTY_FILE", + "TOO_MANY_ROWS", + "BLANK_ROWS_SKIPPED", + "TIER_ROWS_NOT_GROUPED", + "TIER_BANDS_CONFLICT", + "CONDITION_VALUE_MISSING", + "VARIANT_VALUE_CONFLICT", + "ATTRIBUTE_NOT_OVERRIDABLE", + "ATTRIBUTE_NOT_IN_SCHEMA", + "IS_CONDITIONAL_NOT_CONSTANT", + "SCHEMA_NOT_CONDITIONABLE", + "SCHEMA_NOT_FOUND", + "SCHEMA_DECLARES_NO_CONDITIONS", + "GROUPING_KEY_NOT_A_COLUMN", + "GROUPING_KEY_IS_FOLD_COLUMN" + ] + }, + "severity": { + "type": "string", + "enum": [ + "warning", + "blocking" + ] + }, + "columns": { + "type": "array", + "description": "The columns this issue is about, at most one entry per column per entity.\nOn UNIQUE_ID_COLUMN_MISSING the file has NONE of these. Do NOT tell the user that adding one of them is enough: a unique id may read several columns through a JSONata expression, and whether it combines them (`A & B`, both needed) or falls back between them (`A ? A : B`, either will do) is not knowable here.", + "items": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "description": "The column name, spelled as the mapping reads it." + }, + "entity": { + "type": "string", + "description": "Slug of the entity this column helps identify. UNIQUE_ID_COLUMN_MISSING only." + } + } + } + }, + "subject": { + "type": "string", + "description": "What the issue is about — an attribute, an entity type slug, or an \"attribute reads column\" pair — to substitute into the client's copy for the code. Independent of `columns`; a code can carry both." + }, + "row": { + "type": "integer", + "description": "The offending data row, 1-based as the user counts rows. MALFORMED_ROW only." + } + } + }, + "ErpImportValidation": { + "type": "object", + "description": "Validate-phase summary: what the file will create, and whether it may be confirmed. Absent until the validate phase completes. No per-row detail is kept — a rejected file is corrected and imported again.", + "required": [ + "blocking", + "warnings", + "entities" + ], + "properties": { + "total_rows": { + "type": "integer", + "description": "Data rows the import will act on. Rows with no value in any column are dropped before they are counted, and reported as BLANK_ROWS_SKIPPED — so this can be lower than the line count of the file.\nABSENT when the verdict was reached without reading the file, as a mapping refused against the entity schemas alone is. Do not default it to zero." + }, + "blocking": { + "type": "integer", + "description": "Blocking problems found, counting per-row ones that are not listed in `issues`." + }, + "warnings": { + "type": "integer" + }, + "entities": { + "type": "object", + "additionalProperties": { + "type": "integer" + }, + "description": "Distinct entities the file expresses, keyed by entity slug." + }, + "entity_details": { + "type": "array", + "description": "What this import will write, one entry per entity instance, capped at 20 in first-appearance order. `entities` stays exact regardless.", + "items": { + "$ref": "#/components/schemas/ErpImportEntityDetail" + } + }, + "entity_details_truncated": { + "type": "boolean", + "description": "More entities exist than `entity_details` lists." + }, + "issues": { + "type": "array", + "description": "Whole-file issues, at most one per `code`. Do not expect the length to match blocking + warnings: those also count per-row problems, which are recorded for support but never listed here. Warnings here are what `ack_warnings` on `:execute` acknowledges.", + "items": { + "$ref": "#/components/schemas/ErpImportIssue" + } + } + } + }, + "ErpImportEntityDetail": { + "type": "object", + "description": "One entity instance the import will write, attributed to the mapping target that produces it.", + "required": [ + "mapping_index", + "entity_schema", + "business_key" + ], + "properties": { + "mapping_index": { + "type": "integer", + "description": "Which `config.entities[]` target produced this entity — the join key back to the mapping." + }, + "entity_schema": { + "type": "string" + }, + "business_key": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "The entity's identity: its `unique_ids` as the mapping resolved them, keyed by attribute name in the order the target declares them. Values are strings, as they arrive from CSV." + }, + "variants": { + "type": "integer", + "description": "Distinct condition tuples. Absent when the entity is not conditional." + }, + "versions": { + "type": "integer", + "description": "Version writes — one per variant per effective date. Absent when the entity is not conditional." + } + } + }, + "ErpImportProgress": { + "type": "object", + "description": "How far the currently running phase has got. Written at every batch boundary, so it advances during long runs rather than only at the end.\n`total_rows` is ABSENT during the validate phase until the file has been read to the end — there is deliberately no counting pass, since that would be a second unbounded read of the whole file. Render an indeterminate indicator while it is missing: dividing by a missing total yields a determinate bar pinned at 0%, which reads as a hung import.", + "required": [ + "processed_rows" + ], + "properties": { + "processed_rows": { + "type": "integer", + "description": "Rows fully processed. An exact \"the first N rows are done\" watermark, not an estimate — it only advances once a batch has been completely handled." + }, + "total_rows": { + "type": "integer", + "description": "Rows in the file. Known only once a phase has read to EOF; the execute phase has it from the start, because validate recorded it first." + } + } + }, + "ErpImportError": { + "type": "object", + "description": "Why the import failed — present if and only if status = FAILED. `code` is the translation key; for VALIDATION_BLOCKED the specifics are in `validation`.", + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Enum of possible error codes.\n", + "enum": [ + "VALIDATION_BLOCKED", + "FILE_FORMAT_UNSUPPORTED", + "FILE_UNAVAILABLE", + "VALIDATE_TIMEOUT", + "IMPORT_TIMEOUT", + "USE_CASE_NOT_USABLE", + "IMPORT_NO_PROGRESS", + "TIER_ROWS_NOT_GROUPED", + "INTERNAL_ERROR" + ] + }, + "message": { + "type": "string", + "description": "One English sentence, derived from `code` so the two always agree. A fallback for a client that has no translation for this code — prefer translating `code`, and never parse this. It deliberately does NOT restate `validation.issues`." + } + } + }, + "ErpImportFilePreview": { + "type": "object", + "description": "Sample of the file's first rows, using the same parser as `:validate`. Registration refuses a file it cannot read, so a created job always includes this.", + "required": [ + "columns", + "rows" + ], + "properties": { + "columns": { + "type": "array", + "description": "Effective column names (trimmed, duplicates collapsed).", + "items": { + "type": "string" + } + }, + "rows": { + "type": "array", + "description": "Up to the first 3 data rows, aligned to `columns`.", + "items": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "CreateErpImportResponse": { + "type": "object", + "required": [ + "job" + ], + "properties": { + "job": { + "$ref": "#/components/schemas/ErpImportJob" + }, + "preview": { + "$ref": "#/components/schemas/ErpImportFilePreview" + } + } + }, + "ErpImportJob": { + "type": "object", + "required": [ + "import_id", + "org_id", + "format", + "status", + "s3_input_ref", + "created_at", + "updated_at" + ], + "properties": { + "import_id": { + "type": "string", + "description": "`imp_{ULID}` — time-ordered, also used as the job's correlation_id." + }, + "org_id": { + "type": "string" + }, + "created_by": { + "type": "string" + }, + "integration_id": { + "type": "string", + "description": "The integration owning the chosen use case. Absent until the first `:validate`." + }, + "use_case_slug": { + "type": "string", + "description": "The inbound use case whose mapping drives both phases. Absent until the first `:validate`, and MUTABLE — re-validating with a different use case replaces it." + }, + "format": { + "type": "string", + "enum": [ + "csv", + "xlsx" + ] + }, + "status": { + "type": "string", + "description": "PENDING → VALIDATING → READY → PROCESSING → IMPORTED, with FAILED reachable from any working status, and CANCELLING → CANCELLED reachable from VALIDATING or PROCESSING via :abort. Only IMPORTED and CANCELLED are terminal. READY and FAILED both accept a further `:validate`, which is how a wrong use case is corrected without re-uploading the file.\nIMPORTED means every row was handed to the platform, not that the platform finished — per-row outcomes live in monitoring, filtered by correlation_id. A file that fails validation is FAILED with error.code = VALIDATION_BLOCKED.\nREADY is legitimately idle for as long as the user takes to confirm, so it carries no running work and never goes stale.\nCANCELLING is transient and cooperative: the abort has been recorded but the worker only notices at its next batch boundary. Rows already published stay published — a stop is not a rollback.", + "enum": [ + "PENDING", + "VALIDATING", + "READY", + "PROCESSING", + "IMPORTED", + "FAILED", + "CANCELLING", + "CANCELLED" + ] + }, + "s3_input_ref": { + "$ref": "#/components/schemas/S3Reference" + }, + "size_bytes": { + "type": "integer", + "format": "int64", + "description": "Size of the uploaded file, recorded at registration. Present on every job registered from version 1.17.0 onwards; absent on older rows, which were written before it was captured." + }, + "column_count": { + "type": "integer", + "description": "How many effective columns the file's header yielded — the length of the `preview.columns` returned at registration, kept so a job loaded later can still describe its file. The preview ROWS are deliberately not stored: they are a sample for the person about to choose a mapping, not job state. Present on every job registered from version 1.17.0 onwards." + }, + "validation": { + "$ref": "#/components/schemas/ErpImportValidation" + }, + "progress": { + "$ref": "#/components/schemas/ErpImportProgress" + }, + "error": { + "$ref": "#/components/schemas/ErpImportError" + }, + "correlation_id": { + "type": "string", + "description": "Scopes this run in monitoring. Always equal to `import_id`." + }, + "activity_id": { + "type": "string" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + } + }, + "ErpImportList": { + "type": "object", + "required": [ + "results" + ], + "properties": { + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ErpImportJob" + } + }, + "next_cursor": { + "type": "string", + "nullable": true, + "description": "Cursor for the next page, or null when there are no more rows." + } + } + }, + "NotificationHistoryItem": { + "type": "object", + "description": "A single notification-history row (one real notification decision).", + "required": [ + "id", + "type", + "severity", + "title", + "occurred_at", + "notified", + "recipients", + "context", + "created_at" + ], + "properties": { + "id": { + "type": "string", + "description": "Stable history row id (ULID)." + }, + "type": { + "type": "string", + "description": "The notification/rule type (e.g. critical_error, error_threshold, integration_digest)." + }, + "state_transition": { + "type": "string", + "nullable": true, + "description": "The state transition that produced this decision (e.g. OK->ALERTING), when applicable." + }, + "severity": { + "type": "string", + "enum": [ + "error", + "warning", + "info" + ], + "description": "Severity of the decision." + }, + "title": { + "type": "string", + "description": "Human-readable title at decision time." + }, + "occurred_at": { + "type": "string", + "format": "date-time", + "description": "When the decision occurred (newest-first ordering key)." + }, + "notified": { + "type": "boolean", + "description": "Whether the notification was actually sent (true) or suppressed (false)." + }, + "suppressed_reason": { + "type": "string", + "nullable": true, + "enum": [ + "muted", + "debounced", + "recipient_opt_out" + ], + "description": "Why the notification was suppressed (only set when notified is false)." + }, + "recipients": { + "type": "array", + "description": "epilot user ids the notification was (or would have been) delivered to.", + "items": { + "type": "string" + } + }, + "context": { + "type": "object", + "additionalProperties": true, + "description": "Type-specific context captured at decision time." + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "When the history row was written." + } + } + }, + "NotificationHistoryResponse": { + "type": "object", + "required": [ + "history" + ], + "properties": { + "history": { + "type": "array", + "description": "Notification-history rows, newest first.", + "items": { + "$ref": "#/components/schemas/NotificationHistoryItem" + } + }, + "next_cursor": { + "type": "string", + "nullable": true, + "description": "Opaque base64 cursor for the next page, or null when there are no more rows." + } + } + }, + "TestNotificationRequest": { + "type": "object", + "required": [ + "kind" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "alert", + "digest" + ], + "description": "The kind of notification to render and send." + }, + "type": { + "type": "string", + "description": "The alert type to render when kind=alert (e.g. critical_error, error_threshold)." + }, + "channels": { + "type": "array", + "description": "Delivery channels to use; defaults to the integration's configured channels.", + "items": { + "type": "string", + "enum": [ + "email", + "in_app" + ] + } + } + } + }, + "TestNotificationResponse": { + "type": "object", + "required": [ + "sent", + "recipient", + "channels" + ], + "properties": { + "sent": { + "type": "boolean", + "description": "Whether the test notification was accepted for delivery." + }, + "recipient": { + "type": "string", + "description": "The calling user id the test was sent to (the only recipient)." + }, + "channels": { + "type": "array", + "description": "The channels the test was delivered on.", + "items": { + "type": "string" + } + }, + "notification_id": { + "type": "string", + "nullable": true, + "description": "The svc-notification-api message id, or null when not returned." + } + } + }, + "NotificationStatusResponse": { + "type": "object", + "required": [ + "health", + "rules" + ], + "properties": { + "health": { + "type": "string", + "enum": [ + "healthy", + "alerting", + "muted" + ], + "description": "Rolled-up live status: `muted` when muteUntil is in the future; else `alerting` if any rule is currently ALERTING; else `healthy`.\n" + }, + "evaluated_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Most recent per-rule evaluation instant (max lastEvaluatedAt), or null when no rule has been evaluated. Updates on the 5-minute sweep tick.\n" + }, + "rules": { + "type": "array", + "description": "Per-rule status, one entry per configured rule.", + "items": { + "$ref": "#/components/schemas/NotificationRuleStatus" + } + } + } + }, + "NotificationRuleStatus": { + "type": "object", + "required": [ + "rule_id", + "state" + ], + "properties": { + "rule_id": { + "type": "string", + "description": "The rule's stable id (matches the configured rule id)." + }, + "state": { + "type": "string", + "enum": [ + "ok", + "alerting", + "recovered" + ], + "description": "The rule's live AlertState (defaults to `ok` when never evaluated)." + }, + "last_fired_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "ISO instant the rule last entered ALERTING." + }, + "last_cleared_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "ISO instant the rule last cleared back to OK." + }, + "last_evaluation": { + "type": "object", + "nullable": true, + "description": "The most recent evaluation of this rule, or null when it has never been evaluated. Answers \"why isn't this rule firing?\" for a rule that neither fires nor transitions — which otherwise leaves no trace at all.", + "required": [ + "evaluated_at", + "observed" + ], + "properties": { + "evaluated_at": { + "type": "string", + "format": "date-time", + "description": "ISO instant of the evaluation. Updates on every sweep tick." + }, + "observed": { + "type": "number", + "description": "The measure seen (error/warning count, success rate, or event total)." + }, + "threshold": { + "type": "number", + "nullable": true, + "description": "What it was compared against; null for a rule type with no threshold." + }, + "suppressed_reason": { + "type": "string", + "enum": [ + "sample_size" + ], + "description": "Present ONLY when the rule could not be evaluated. `sample_size` means the success-rate minimum-sample guard was not met, so the rule is neither healthy nor alerting — it is not evaluating at all. Absent when the evaluation ran normally." + } + } + }, + "baseline": { + "nullable": true, + "description": "Present only for enabled 'auto'-threshold rules; null otherwise.", + "allOf": [ + { + "$ref": "#/components/schemas/RuleBaselineStatus" + } + ] + } + } + }, + "RuleBaselineStatus": { + "type": "object", + "required": [ + "is_mature" + ], + "properties": { + "is_mature": { + "type": "boolean", + "description": "False during cold start; the sweeper uses the static fallbackThreshold until the baseline's history span is mature.\n" + }, + "computed_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "ISO instant the baseline was last computed." + }, + "median": { + "type": "number", + "nullable": true, + "description": "Typical in-scope event volume for the current hour-of-week bucket." + }, + "mad": { + "type": "number", + "nullable": true, + "description": "Median absolute deviation for the current hour-of-week bucket." + }, + "upper": { + "type": "number", + "nullable": true, + "description": "Dynamic alert threshold (median + k·MAD, k by sensitivity) for the current hour-of-week, or null when the bucket is uncovered.\n" + }, + "buckets": { + "type": "array", + "nullable": true, + "description": "Full 168-bucket series; only present when ?include=baseline_series.", + "items": { + "$ref": "#/components/schemas/RuleBaselineBucket" + } + } + } + }, + "RuleBaselineBucket": { + "type": "object", + "required": [ + "dow", + "hour", + "median", + "mad" + ], + "properties": { + "dow": { + "type": "integer", + "minimum": 1, + "maximum": 7, + "description": "Day of week, 1=Monday … 7=Sunday." + }, + "hour": { + "type": "integer", + "minimum": 0, + "maximum": 23, + "description": "Hour of day, 0 … 23 (UTC)." + }, + "median": { + "type": "number" + }, + "mad": { + "type": "number" + } + } + }, + "ErrorResponseBase": { + "type": "object", + "properties": { + "code": { + "type": "string", + "description": "Computer-readable error code" + }, + "message": { + "type": "string", + "description": "Error message" + } + } + }, + "ErpEvent": { + "type": "object", + "required": [ + "event_type", + "object_type", + "timestamp", "format", "payload" ], @@ -3476,6 +5486,11 @@ "description": "Optional unique identifier for idempotency - prevents duplicate processing of the same event within 24 hours in context of the same integration. Must contain only alphanumeric characters, hyphens, and underscores.\n", "example": "evt-2025-05-01-12345-create-bp" }, + "correlation_id": { + "type": "string", + "description": "Optional per-event trace id for cross-system tracing (unique per business operation). Overrides the request-level meta.correlation_id for THIS event. When absent, the event inherits the request-level correlation_id; when both are absent, epilot mints its own event_id and the trace is epilot-only. Orthogonal to deduplication_id (idempotency).\n", + "example": "bp-8f3a2c-7d4e-4b1a-9c2f-1e6d5a4b3c21" + }, "use_case_id": { "type": "string", "nullable": true, @@ -3564,7 +5579,7 @@ "additionalProperties": true } ], - "description": "The object data payload - can be either a serialized string or a direct JSON object", + "description": "The object data payload - can be either a serialized string or a direct JSON object.\n\nFor events routed to a mapped (default) use case this is the raw ERP object; the use\ncase's mapping configuration transforms it into entity updates.\n\nFor events routed to a use case whose configuration has `direct: true`, the payload\nMUST be a `DirectPayload` (as an object, or a JSON string): a versioned envelope of\npre-mapped entity and meter reading operations that skip the mapping engine entirely.\nDirect use cases accept JSON only - `format: \"xml\"` events are rejected.\n", "example": "{\"id\":\"BP10001\",\"name\":\"Acme Corporation\",\"type\":\"organization\",\"tax_id\":\"DE123456789\",\"status\":\"active\"}" }, "use_case_slug": { @@ -3582,6 +5597,19 @@ "maxLength": 255, "description": "Optional unique identifier for idempotency - prevents duplicate processing of the same event within 24 hours in context of the same integration. Must contain only alphanumeric characters, hyphens, and underscores.\n", "example": "evt-2025-05-01-12345-bp" + }, + "group_id": { + "type": "string", + "description": "Optional per-event ordering group. Overrides the request-level `group_id` for THIS event; when absent, the event inherits the request-level `group_id` (or the default strict per-integration ordering). Use it when a single batch carries events for unrelated business objects that may be processed in parallel. See the request-level `group_id` for semantics.\n", + "maxLength": 128, + "example": "customer-42" + }, + "correlation_id": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "description": "Optional per-event trace id. Overrides the request-level `correlation_id` for THIS event, so a batch carrying several distinct business operations keeps its traces segregated in monitoring instead of merged under one request-level id. When absent, the event inherits the request-level `correlation_id`.\n", + "example": "customer-42-sync-2026-08-26" } } }, @@ -3603,7 +5631,7 @@ }, "group_id": { "type": "string", - "description": "Controls ordering and parallelism for this request's events.\n\nBy default, all events for a given `integration_id` are processed\n**strictly in order, one at a time**. For high-volume integrations this\ncan become a throughput bottleneck.\n\nSet `group_id` to opt into **parallel processing**:\n- Events sharing the same `group_id` are processed in the order received.\n- Events with different `group_id` values are processed in parallel.\n\nTypical usage is to derive `group_id` from a logical partition key in\nyour payload — for example the customer ID, contract ID, or meter ID —\nso updates to the same business object remain ordered while unrelated\nobjects are processed concurrently.\n\nNotes:\n- Up to 20 groups per integration are processed concurrently. Using\n more distinct values than that yields no additional parallelism.\n- Omit this field if strict per-integration ordering is required.\n", + "description": "Controls ordering and parallelism for this request's events.\n\nBy default, all events for a given `integration_id` are processed\n**strictly in order, one at a time**. For high-volume integrations this\ncan become a throughput bottleneck.\n\nSet `group_id` to opt into **parallel processing**:\n- Events sharing the same `group_id` are processed in the order received.\n- Events with different `group_id` values are processed in parallel.\n\nTypical usage is to derive `group_id` from a logical partition key in\nyour payload — for example the customer ID, contract ID, or meter ID —\nso updates to the same business object remain ordered while unrelated\nobjects are processed concurrently.\n\nNotes:\n- Individual events may set their own `group_id`, which overrides this\n request-level value for that event only.\n- Up to 20 groups per integration are processed concurrently. Using\n more distinct values than that yields no additional parallelism.\n- Omit this field if strict per-integration ordering is required.\n", "maxLength": 128, "example": "customer-42" }, @@ -3742,6 +5770,13 @@ }, "description": "Configuration defining environment variables needed by this integration. Values are stored in the Environments API." }, + "maps": { + "type": "array", + "description": "Re-usable key/value maps (e.g. salutation codes) declared by this integration (max 50). Each map is synced to the Environments API as a non-secret variable of type `JSON` and is available in JSONata mappings as `$env.`, together with the `$mapValue(map, key, default)` and `$mapKey(map, value, default)` helpers.\n", + "items": { + "$ref": "#/components/schemas/IntegrationMap" + } + }, "settings": { "$ref": "#/components/schemas/IntegrationSettings" }, @@ -3957,6 +5992,38 @@ "UpdateIntegrationRequest": { "$ref": "#/components/schemas/IntegrationEditableFields" }, + "IntegrationMap": { + "type": "object", + "description": "A flat key/value map stored as an Environments API variable of type `JSON`. Values must be strings, numbers, booleans or null; the serialised map must not exceed 32 KB.\n", + "required": [ + "key", + "label", + "value" + ], + "properties": { + "key": { + "type": "string", + "description": "Environment variable key the map is synced to (`$env.` in JSONata).", + "pattern": "^[a-z0-9][a-z0-9_.\\-]{0,127}$" + }, + "label": { + "type": "string", + "description": "Display label for the map in the UI", + "minLength": 1, + "maxLength": 255 + }, + "description": { + "type": "string", + "description": "Help text shown for the map", + "maxLength": 1000 + }, + "value": { + "type": "object", + "description": "The map itself, e.g. `{\"Mr.\": 1, \"Ms. / Mrs.\": 2}`", + "additionalProperties": true + } + } + }, "EnvironmentFieldConfig": { "type": "object", "required": [ @@ -4007,6 +6074,9 @@ "properties": { "autoRefresh": { "$ref": "#/components/schemas/AutoRefreshSettings" + }, + "notifications": { + "$ref": "#/components/schemas/IntegrationNotificationConfig" } } }, @@ -4026,6 +6096,231 @@ } } }, + "IntegrationNotificationConfig": { + "type": "object", + "description": "Integration monitoring notification configuration. Rides Integration.settings.notifications (camelCase) and surfaces on both v1 and v2 GET/PUT. Unknown keys are stripped server-side to stay forward-compatible with deferred (V2) rule types.", + "required": [ + "enabled", + "recipients", + "defaultChannels", + "rules", + "digest" + ], + "properties": { + "enabled": { + "type": "boolean", + "description": "Master switch for this integration's notifications." + }, + "recipients": { + "type": "array", + "description": "epilot user ids notified for this integration. Same-org membership and per-user notification preferences are enforced at send time (Phases 3–5), not at config-write time.", + "items": { + "$ref": "#/components/schemas/NotificationRecipient" + } + }, + "defaultChannels": { + "$ref": "#/components/schemas/NotificationChannelSet" + }, + "monitoredUseCases": { + "type": "array", + "description": "Integration-level use-case include-filter; absent/empty means all use cases.", + "items": { + "type": "string" + } + }, + "monitoredCodes": { + "type": "array", + "description": "Integration-level code scope; absent/empty resolves to ['_error_']. Accepts concrete monitoring error codes or group sentinels (_error_, _warning_, _success_, _info_, _any_, _parent_).", + "items": { + "type": "string" + } + }, + "rules": { + "type": "array", + "description": "Enabled triggers and their params. A type MAY repeat; capped at 20 rules (enforced at the write boundary).", + "items": { + "$ref": "#/components/schemas/NotificationRule" + } + }, + "digest": { + "$ref": "#/components/schemas/NotificationDigestConfig" + }, + "muteUntil": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "ISO instant; snooze all non-digest alerts until this time. `null` means not muted." + } + } + }, + "NotificationRecipient": { + "type": "object", + "description": "A configured recipient. Only the epilot user_id is stored.", + "required": [ + "user_id" + ], + "properties": { + "user_id": { + "type": "string", + "description": "epilot user id. Same-org membership is enforced at send time (Phases 3–5), which re-validates each recipient against the integration's org before fanning out — it is not enforced at config-write time." + } + } + }, + "NotificationChannelSet": { + "type": "object", + "description": "Delivery channel toggles. New channels added in svc-notification-api inherit here.", + "required": [ + "email", + "in_app" + ], + "properties": { + "email": { + "type": "boolean" + }, + "in_app": { + "type": "boolean" + } + } + }, + "NotificationRule": { + "type": "object", + "description": "A single notification rule. Only the params relevant to a given type are set. The id is optional on write: the server mints a ULID when omitted and preserves a supplied id verbatim (a stable id keeps a rule's AlertState across config saves).", + "required": [ + "type", + "enabled" + ], + "properties": { + "id": { + "type": "string", + "description": "Stable AlertState + baseline key. Optional on write — the server mints a ULID when omitted; a supplied id is preserved verbatim." + }, + "name": { + "type": "string", + "description": "Optional human label disambiguating two rules of the same type." + }, + "type": { + "type": "string", + "description": "Rule trigger type. These are the only supported types; each is produced by a real alerter.", + "enum": [ + "critical_error", + "error_threshold", + "warning_threshold", + "success_rate_drop", + "recovery", + "silence" + ] + }, + "enabled": { + "type": "boolean" + }, + "channels": { + "$ref": "#/components/schemas/NotificationChannelSet" + }, + "codes": { + "type": "array", + "description": "Per-rule code scope. Event-matching rules default to ['_parent_']; silence defaults to ['_any_']. success_rate_drop and recovery take no codes.", + "items": { + "type": "string" + } + }, + "threshold": { + "description": "Count or percentage; 'auto' selects anomaly-baseline mode.", + "oneOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "auto" + ] + } + ] + }, + "sensitivity": { + "type": "string", + "description": "Band width for 'auto' mode.", + "enum": [ + "low", + "medium", + "high" + ] + }, + "fallbackThreshold": { + "type": "number", + "description": "Static value used while the 'auto' baseline is immature (cold start)." + }, + "window": { + "type": "string", + "description": "Evaluation window, e.g. '15m', '1h', '24h'." + }, + "minSampleSize": { + "type": "integer", + "description": "success_rate_drop minimum sample size guard." + }, + "quietPeriod": { + "type": "string", + "description": "silence quiet period, e.g. '12h'." + } + } + }, + "NotificationDigestConfig": { + "type": "object", + "description": "Digest schedule and content configuration.", + "required": [ + "enabled", + "frequency", + "timeOfDay", + "timezone", + "channels", + "includeHealthy", + "skipIfEmpty" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "frequency": { + "type": "string", + "enum": [ + "daily", + "weekly" + ] + }, + "dayOfWeek": { + "type": "integer", + "description": "Weekly only. 0 = Sunday … 6 = Saturday.", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6 + ] + }, + "timeOfDay": { + "type": "string", + "description": "HH:mm" + }, + "timezone": { + "type": "string", + "description": "IANA timezone, e.g. 'Europe/Berlin'." + }, + "channels": { + "$ref": "#/components/schemas/NotificationChannelSet" + }, + "includeHealthy": { + "type": "boolean", + "description": "List all integrations vs. only ones with issues." + }, + "skipIfEmpty": { + "type": "boolean", + "description": "Suppress the digest when nothing happened." + } + } + }, "SetIntegrationAppMappingRequest": { "type": "object", "required": [ @@ -4132,6 +6427,10 @@ "type": "object", "description": "Configuration for inbound use cases (ERP to epilot)", "properties": { + "direct": { + "type": "boolean", + "description": "Enables direct mode for this use case. When `true`, every event routed to this use\ncase must carry a `DirectPayload` - pre-mapped entity and meter reading operations -\ninstead of raw ERP data, and the mapping engine is skipped entirely. Everything else\nin the inbound pipeline (deduplication, ordering, create-vs-update resolution,\nrelation resolution, monitoring) behaves exactly as in mapped mode.\n\nIn direct mode, `entities` doubles as an optional allowlist: when non-empty, each\noperation's `entity_slug` must match an entry's `entity_schema`, and when that entry\ndeclares `unique_ids`, the operation's unique-id keys must be exactly that set (or\njust `_id`). Entries need only `entity_schema` and `unique_ids` - `fields` is not\nrequired and is ignored. `meter_readings` is ignored in direct mode.\n\nFlag changes take up to 5 minutes to propagate (configuration cache).\n" + }, "entities": { "type": "array", "description": "Array of entity configurations for this event", @@ -4161,6 +6460,11 @@ "description": "The Event Catalog event name that triggers this outbound flow", "example": "contract.created" }, + "event_filter": { + "type": "string", + "description": "JSONata boolean predicate over the hydrated event payload. The use case handles the\nevent only when this evaluates truthy; when absent it handles every event of its name,\nwhich is the behaviour of every pre-existing configuration.\n\nThis is where event scoping belongs — narrowing to certain ticket purposes, contract\ntypes or channels. Evaluation input is the full hydrated event, so relation nodes such\nas `ticket` and `contact` are populated. An expression that throws is treated as no\nmatch and logged, so one malformed filter cannot block the other use cases subscribed\nto the same event.\n", + "example": "$count(ticket._purpose[$ in ['bae4b4d1-d728-49ef-92ff-54486dd301b4']]) > 0" + }, "mappings": { "type": "array", "minItems": 1, @@ -4168,6 +6472,15 @@ "$ref": "#/components/schemas/OutboundMapping" }, "description": "List of mappings that transform and deliver the event" + }, + "ack_tracking": { + "type": "string", + "enum": [ + "on", + "off" + ], + "default": "on", + "description": "Whether this use case participates in the acknowledgement protocol.\n\n`on` (the default, and the behaviour of every pre-existing use case) records an\nACK_PENDING on each event and expects the consumer to confirm receipt via\n`POST /v1/erp/tracking/acknowledgement`; unconfirmed events raise ACK_TIMEOUT after\nthe timeout window.\n\n`off` opts the use case out entirely: no tracking row, no ACK_PENDING, no\nACK_TIMEOUT. Set it for consumers that never acknowledge — otherwise every event\nproduces a guaranteed timeout warning — and for deliveries that already keep their\nown durable per-item record, such as `file_proxy`.\n\nThe tracking row is per-event, not per-use-case, so it is suppressed only when EVERY\nenabled use case matching the event has opted out.\n" } } }, @@ -4175,8 +6488,7 @@ "type": "object", "required": [ "entity_schema", - "unique_ids", - "fields" + "unique_ids" ], "properties": { "entity_schema": { @@ -4217,17 +6529,108 @@ "default": "upsert", "description": "Operation mode for entity mapping:\n- 'upsert': Create or update the entity (default)\n- 'delete': Soft delete the entity (marks as deleted)\n- 'purge': Hard delete the entity (permanent removal)\n- 'upsert-prune-scope-purge': Upsert entities from array, then purge entities in scope that weren't upserted\n- 'upsert-prune-scope-delete': Upsert entities from array, then soft delete entities in scope that weren't upserted\n" }, - "scope": { - "$ref": "#/components/schemas/PruneScopeConfig" + "scope": { + "$ref": "#/components/schemas/PruneScopeConfig" + }, + "fields": { + "type": "array", + "description": "Field mapping definitions. Required for mapped-mode entity configurations; may be\nomitted for allowlist entries of a `direct: true` use case, where no mapping runs.\n", + "items": { + "$ref": "#/components/schemas/IntegrationEntityField" + } + }, + "conditional": { + "$ref": "#/components/schemas/IntegrationEntityConditional" + } + } + }, + "IntegrationEntityConditional": { + "type": "object", + "description": "Conditional Pricing extras for this target, used when it writes a conditional entity (a Product, Price or Coupon carrying context-dependent variants). The conditions themselves are declared on the entity schema, not here.", + "required": [ + "folds" + ], + "properties": { + "folds": { + "type": "array", + "minItems": 1, + "maxItems": 1, + "description": "Row folds for this target. Exactly one is supported; the array is the shape for later, not a capability claim.", + "items": { + "$ref": "#/components/schemas/IntegrationEntityFold" + } + } + } + }, + "IntegrationEntityFold": { + "type": "object", + "description": "Collapse the rows of one variant-version into a single array attribute, ordered by a column. A commodity file carries one row per consumption band, while a variant-version holds exactly one `tiers` array.", + "required": [ + "attribute", + "sort_by", + "item" + ], + "properties": { + "attribute": { + "type": "string", + "description": "The array attribute this fold builds, e.g. `tiers`." + }, + "sort_by": { + "allOf": [ + { + "$ref": "#/components/schemas/MappedFieldValue" + } + ], + "description": "Orders the group's rows. Coerced to a number, so `10000` sorts after `5000` rather than before it as strings." + }, + "item": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/MappedFieldValue" + }, + "description": "Attribute name to field value, shaping one array item from one row. `jsonataExpression` is often needed here: German ERP exports carry decimal commas (`31,25`) that `Number()` cannot parse." + } + } + }, + "MappedFieldValue": { + "description": "One mapped value. Exactly one of field, constant or jsonataExpression must be set — the same three forms `IntegrationEntityField` accepts.", + "oneOf": [ + { + "type": "object", + "required": [ + "field" + ], + "properties": { + "field": { + "type": "string", + "description": "Source column name, or a JSONPath expression when it starts with $" + } + } + }, + { + "type": "object", + "required": [ + "constant" + ], + "properties": { + "constant": { + "description": "Constant value, of any type" + } + } }, - "fields": { - "type": "array", - "description": "Field mapping definitions", - "items": { - "$ref": "#/components/schemas/IntegrationEntityField" + { + "type": "object", + "required": [ + "jsonataExpression" + ], + "properties": { + "jsonataExpression": { + "type": "string", + "description": "JSONata expression evaluated against the row" + } } } - } + ] }, "IntegrationMeterReading": { "type": "object", @@ -4888,7 +7291,11 @@ "change_description": { "type": "string", "maxLength": 2000, - "description": "Description of the last change made to this use case" + "description": "Reason given for the update that produced the current version. Absent when the update supplied none — it is not inherited from the previous version.\n" + }, + "changed_by": { + "type": "string", + "description": "User ID recorded for the update that produced the current version. Absent when the update had no user context, e.g. an automated sync.\n" }, "created_at": { "type": "string", @@ -5392,13 +7799,240 @@ } ] }, - "UpdateSecureProxyUseCaseRequest": { + "UpdateSecureProxyUseCaseRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/UpdateUseCaseRequestBase" + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "secure_proxy" + ], + "description": "Use case type" + }, + "configuration": { + "$ref": "#/components/schemas/SecureProxyUseCaseConfiguration" + } + } + } + ] + }, + "UseCaseHistoryEntry": { + "oneOf": [ + { + "$ref": "#/components/schemas/InboundUseCaseHistoryEntry" + }, + { + "$ref": "#/components/schemas/OutboundUseCaseHistoryEntry" + }, + { + "$ref": "#/components/schemas/FileProxyUseCaseHistoryEntry" + }, + { + "$ref": "#/components/schemas/ManagedCallUseCaseHistoryEntry" + }, + { + "$ref": "#/components/schemas/SecureProxyUseCaseHistoryEntry" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "inbound": "#/components/schemas/InboundUseCaseHistoryEntry", + "outbound": "#/components/schemas/OutboundUseCaseHistoryEntry", + "file_proxy": "#/components/schemas/FileProxyUseCaseHistoryEntry", + "managed_call": "#/components/schemas/ManagedCallUseCaseHistoryEntry", + "secure_proxy": "#/components/schemas/SecureProxyUseCaseHistoryEntry" + } + } + }, + "UseCaseHistoryEntryBase": { + "type": "object", + "required": [ + "id", + "useCaseId", + "integrationId", + "name", + "type", + "enabled", + "created_at", + "updated_at", + "history_created_at" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid", + "description": "Unique identifier for this history entry" + }, + "useCaseId": { + "type": "string", + "format": "uuid", + "description": "Reference to the parent use case" + }, + "integrationId": { + "type": "string", + "format": "uuid", + "description": "Parent integration ID" + }, + "name": { + "type": "string", + "description": "Use case name at this point in history" + }, + "slug": { + "type": "string", + "description": "Use case slug at this point in history" + }, + "enabled": { + "type": "boolean", + "description": "Whether the use case was enabled at this point in history" + }, + "change_description": { + "type": "string", + "description": "Description of the change that was made at this point in history" + }, + "changed_by": { + "type": "string", + "description": "User ID of the user who made the change that produced this history entry" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "ISO-8601 timestamp when the use case was originally created" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "ISO-8601 timestamp of this historical snapshot (before the update)" + }, + "history_created_at": { + "type": "string", + "format": "date-time", + "description": "ISO-8601 timestamp when this history entry was created" + } + } + }, + "InboundUseCaseHistoryEntry": { + "allOf": [ + { + "$ref": "#/components/schemas/UseCaseHistoryEntryBase" + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "inbound" + ], + "description": "Use case type" + }, + "configuration": { + "$ref": "#/components/schemas/InboundIntegrationEventConfiguration" + } + } + } + ] + }, + "OutboundUseCaseHistoryEntry": { + "allOf": [ + { + "$ref": "#/components/schemas/UseCaseHistoryEntryBase" + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "outbound" + ], + "description": "Use case type" + }, + "configuration": { + "$ref": "#/components/schemas/OutboundIntegrationEventConfiguration" + } + } + } + ] + }, + "FileProxyUseCaseHistoryEntry": { + "allOf": [ + { + "$ref": "#/components/schemas/UseCaseHistoryEntryBase" + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "file_proxy" + ], + "description": "Use case type" + }, + "configuration": { + "$ref": "#/components/schemas/FileProxyUseCaseConfiguration" + } + } + } + ] + }, + "ManagedCallUseCaseHistoryEntry": { + "allOf": [ + { + "$ref": "#/components/schemas/UseCaseHistoryEntryBase" + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "managed_call" + ], + "description": "Use case type for managed API calls" + }, + "configuration": { + "$ref": "#/components/schemas/ManagedCallOperationConfig" + }, + "type_annotations": { + "$ref": "#/components/schemas/TypeAnnotations" + }, + "types_locked": { + "type": "boolean", + "description": "Whether types have been generated for this use case" + } + } + } + ] + }, + "SecureProxyUseCaseHistoryEntry": { "allOf": [ { - "$ref": "#/components/schemas/UpdateUseCaseRequestBase" + "$ref": "#/components/schemas/UseCaseHistoryEntryBase" }, { "type": "object", + "required": [ + "type" + ], "properties": { "type": { "type": "string", @@ -5414,1192 +8048,1672 @@ } ] }, - "UseCaseHistoryEntry": { - "oneOf": [ - { - "$ref": "#/components/schemas/InboundUseCaseHistoryEntry" + "SecureProxyUseCaseConfiguration": { + "type": "object", + "required": [ + "vpc_mode" + ], + "description": "Configuration for secure_proxy use cases. Defines how to route requests through a secure VPC.\n", + "properties": { + "vpc_mode": { + "type": "string", + "enum": [ + "static_ip", + "secure_link" + ], + "readOnly": true, + "description": "VPC routing mode. Read-only after creation.\n- static_ip: Routes through a VPC with static outbound IP (NAT Gateway) for IP-allowlisted external APIs.\n- secure_link: Routes through a VPN VPC for accessing private customer networks.\n" }, - { - "$ref": "#/components/schemas/OutboundUseCaseHistoryEntry" + "allowed_domains": { + "type": "array", + "items": { + "type": "string" + }, + "readOnly": true, + "description": "Domain whitelist for secure_link mode. Admin-only — can only be modified directly in DynamoDB via admin script.\nSupports exact match (e.g., \"api.wemag.com\") and wildcard prefix (e.g., \"*.wemag.com\").\n" }, - { - "$ref": "#/components/schemas/FileProxyUseCaseHistoryEntry" + "allowed_ips": { + "type": "array", + "items": { + "type": "string" + }, + "readOnly": true, + "description": "IP allowlist (CIDR notation) for secure_link mode. Admin-only — can only be modified directly in DynamoDB via admin script.\nRequired for secure_link mode. All DNS-resolved IPs must match at least one range.\nExample: [\"10.0.1.0/24\", \"192.168.1.0/24\"]\n" + } + } + }, + "SecureProxyWhitelist": { + "type": "object", + "required": [ + "vpc_mode", + "allowed_domains", + "allowed_ips" + ], + "description": "Current whitelist state for a secure_proxy use case. vpc_mode is read-only\ncontext so the UI can show the user what mode the pool is in.\n", + "properties": { + "vpc_mode": { + "type": "string", + "nullable": true, + "enum": [ + "static_ip", + "secure_link" + ], + "description": "VPC routing mode (read-only). `null` if not yet set." }, - { - "$ref": "#/components/schemas/ManagedCallUseCaseHistoryEntry" + "allowed_domains": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Exact domain (\"api.example.com\") or wildcard prefix (\"*.example.com\").\nWildcards must have at least 2 labels in the suffix.\n" }, - { - "$ref": "#/components/schemas/SecureProxyUseCaseHistoryEntry" + "allowed_ips": { + "type": "array", + "items": { + "type": "string" + }, + "description": "CIDR-notation IP ranges, e.g. \"10.0.0.0/24\"." } - ], - "discriminator": { - "propertyName": "type", - "mapping": { - "inbound": "#/components/schemas/InboundUseCaseHistoryEntry", - "outbound": "#/components/schemas/OutboundUseCaseHistoryEntry", - "file_proxy": "#/components/schemas/FileProxyUseCaseHistoryEntry", - "managed_call": "#/components/schemas/ManagedCallUseCaseHistoryEntry", - "secure_proxy": "#/components/schemas/SecureProxyUseCaseHistoryEntry" + } + }, + "SecureProxyWhitelistUpdate": { + "type": "object", + "description": "Partial update for a secure_proxy whitelist. At least one of\n`allowed_domains` or `allowed_ips` must be provided.\n\nPer-field semantics:\n - **omitted** — the field is not modified; the stored value is preserved.\n - **non-empty array** — the stored value is replaced with the supplied list.\n - **empty array (`[]`)** — the list is cleared (stored as `[]`). This is the\n canonical way to remove all entries from a list. `null` is not accepted.\n", + "minProperties": 1, + "properties": { + "allowed_domains": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Exact domain (\"api.example.com\") or wildcard prefix (\"*.example.com\").\nWildcards must have at least 2 labels in the suffix.\nPass `[]` to clear all allowed domains.\n" + }, + "allowed_ips": { + "type": "array", + "items": { + "type": "string" + }, + "description": "CIDR-notation IP ranges, e.g. \"10.0.0.0/24\".\nPass `[]` to clear all allowed IPs.\n" } } }, - "UseCaseHistoryEntryBase": { + "SecureProxySummary": { "type": "object", "required": [ "id", - "useCaseId", - "integrationId", "name", - "type", "enabled", - "created_at", - "updated_at", - "history_created_at" + "vpc_mode", + "integration_id", + "integration_name" ], "properties": { "id": { "type": "string", - "format": "uuid", - "description": "Unique identifier for this history entry" + "format": "uuid" }, - "useCaseId": { + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "vpc_mode": { + "type": "string", + "enum": [ + "static_ip", + "secure_link" + ] + }, + "allowed_domains": { + "type": "array", + "items": { + "type": "string" + } + }, + "allowed_ips": { + "type": "array", + "items": { + "type": "string" + } + }, + "integration_id": { + "type": "string", + "format": "uuid" + }, + "integration_name": { + "type": "string" + } + } + }, + "SecureProxyRequest": { + "type": "object", + "required": [ + "url", + "method", + "integration_id" + ], + "properties": { + "integration_id": { "type": "string", "format": "uuid", - "description": "Reference to the parent use case" + "description": "Integration ID that owns the secure_proxy use case" }, - "integrationId": { + "use_case_id": { "type": "string", "format": "uuid", - "description": "Parent integration ID" + "description": "Use case ID (provide either use_case_id or use_case_slug)" }, - "name": { + "use_case_slug": { "type": "string", - "description": "Use case name at this point in history" + "description": "Use case slug (provide either use_case_id or use_case_slug)" }, - "slug": { + "url": { "type": "string", - "description": "Use case slug at this point in history" + "format": "uri", + "description": "Target URL to proxy the request to" }, - "enabled": { - "type": "boolean", - "description": "Whether the use case was enabled at this point in history" + "method": { + "type": "string", + "enum": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "description": "HTTP method" }, - "change_description": { + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Request headers to forward" + }, + "body": { + "description": "Request body (string or JSON)" + }, + "response_type": { "type": "string", - "description": "Description of the change that was made at this point in history" + "enum": [ + "json", + "binary" + ], + "default": "json", + "description": "Expected response type" + } + } + }, + "SecureProxyResponse": { + "type": "object", + "properties": { + "status_code": { + "type": "integer", + "description": "HTTP status code from the upstream response" }, - "changed_by": { + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Response headers from upstream" + }, + "body": { + "description": "Response body from upstream. When `status_code` is 502 and the target never produced an HTTP response (TLS/connection/DNS failure or timeout), this is a `SecureProxyUpstreamError` describing the underlying cause." + } + } + }, + "SecureProxyUpstreamError": { + "type": "object", + "description": "Error payload returned when epilot could not obtain an HTTP response from the proxied target. The failure is epilot-generated (HTTP 502) but the cause is usually remote-side; `code`/`reason` make that attributable.", + "required": [ + "message" + ], + "properties": { + "message": { "type": "string", - "description": "User ID of the user who made the change that produced this history entry" + "description": "Short error category.", + "enum": [ + "Upstream network error", + "Upstream error" + ] + }, + "code": { + "type": "string", + "description": "Underlying Node.js/axios error code when available (e.g. `UNABLE_TO_VERIFY_LEAF_SIGNATURE`, `ECONNREFUSED`, `ETIMEDOUT`, `ENOTFOUND`)." + }, + "reason": { + "type": "string", + "description": "Human-readable explanation, present only for well-known codes (TLS/certificate, DNS and connection failures)." + } + } + }, + "ManagedCallOperationConfig": { + "type": "object", + "required": [ + "operation" + ], + "description": "Configuration for managed_call use cases. Defines a single API operation with JSONata mapping.", + "properties": { + "operation": { + "$ref": "#/components/schemas/ManagedCallOperation" }, - "created_at": { + "request_mapping": { "type": "string", - "format": "date-time", - "description": "ISO-8601 timestamp when the use case was originally created" + "description": "JSONata expression for outbound body transformation" }, - "updated_at": { + "response_mapping": { "type": "string", - "format": "date-time", - "description": "ISO-8601 timestamp of this historical snapshot (before the update)" + "description": "JSONata expression for inbound response transformation" }, - "history_created_at": { + "inbound_use_case_slug": { "type": "string", - "format": "date-time", - "description": "ISO-8601 timestamp when this history entry was created" + "description": "Slug of the inbound use case to route responses to for async entity processing.\nWhen set, the managed call response is queued to the inbound pipeline and processed\nusing the referenced inbound use case's mapping configuration.\n" } } }, - "InboundUseCaseHistoryEntry": { - "allOf": [ - { - "$ref": "#/components/schemas/UseCaseHistoryEntryBase" + "ManagedCallOperation": { + "type": "object", + "required": [ + "method", + "path" + ], + "description": "HTTP operation configuration for managed calls", + "properties": { + "method": { + "type": "string", + "enum": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ] }, - { + "path": { + "type": "string", + "description": "URL path template with {{variable}} interpolation" + }, + "headers": { "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "inbound" - ], - "description": "Use case type" - }, - "configuration": { - "$ref": "#/components/schemas/InboundIntegrationEventConfiguration" - } + "description": "Custom HTTP headers for the request. Values support {{variable}} interpolation from the request payload and {{env.VAR}} references for environment variables.", + "additionalProperties": { + "type": "string" } - } - ] - }, - "OutboundUseCaseHistoryEntry": { - "allOf": [ - { - "$ref": "#/components/schemas/UseCaseHistoryEntryBase" }, - { + "query_params": { "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "outbound" - ], - "description": "Use case type" - }, - "configuration": { - "$ref": "#/components/schemas/OutboundIntegrationEventConfiguration" - } + "description": "Query parameters for the request. Values support {{variable}} interpolation from the request payload.", + "additionalProperties": { + "type": "string" } } - ] + } }, - "FileProxyUseCaseHistoryEntry": { - "allOf": [ - { - "$ref": "#/components/schemas/UseCaseHistoryEntryBase" + "ManagedCallExecuteRequest": { + "type": "object", + "required": [ + "integration_id" + ], + "properties": { + "integration_id": { + "type": "string", + "format": "uuid", + "description": "Integration ID" }, - { + "payload": { "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "file_proxy" - ], - "description": "Use case type" - }, - "configuration": { - "$ref": "#/components/schemas/FileProxyUseCaseConfiguration" - } - } + "additionalProperties": true, + "description": "Request payload for the managed call operation" + }, + "correlation_id": { + "type": "string", + "description": "Correlation ID for tracing related events (auto-generated if not provided)" } - ] + } }, - "ManagedCallUseCaseHistoryEntry": { - "allOf": [ - { - "$ref": "#/components/schemas/UseCaseHistoryEntryBase" - }, - { + "ManagedCallExecuteResponse": { + "description": "The response from a managed call execution.\nOn success, returns the JSONata-mapped response data directly (no wrapper).\nThe shape is entirely defined by your response_mapping JSONata expression.\nIf no response_mapping is configured, returns the raw external API response.\nCheck the X-Inbound-Event-Id header for inbound pipeline tracking when inbound routing is configured.\n", + "additionalProperties": true + }, + "ManagedCallErrorResponse": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { "type": "object", "required": [ - "type" + "code", + "message" ], "properties": { - "type": { + "code": { "type": "string", - "enum": [ - "managed_call" - ], - "description": "Use case type for managed API calls" - }, - "configuration": { - "$ref": "#/components/schemas/ManagedCallOperationConfig" + "description": "Error code (e.g., EXTERNAL_API_ERROR, SSRF_BLOCKED, MANAGED_CALL_EXECUTION_ERROR)" }, - "type_annotations": { - "$ref": "#/components/schemas/TypeAnnotations" + "message": { + "type": "string", + "description": "Human-readable error message" }, - "types_locked": { - "type": "boolean", - "description": "Whether types have been generated for this use case" + "details": { + "type": "object", + "additionalProperties": true, + "description": "Additional error details (e.g., status code and body for external API errors)" } } } - ] + } }, - "SecureProxyUseCaseHistoryEntry": { - "allOf": [ - { - "$ref": "#/components/schemas/UseCaseHistoryEntryBase" + "TypeAnnotations": { + "type": "object", + "description": "Developer-provided type annotations for a use case's request and response fields", + "properties": { + "request": { + "type": "object", + "description": "Type annotations for request fields, keyed by dot-path (e.g., \"vendors[].id\" -> \"string\")", + "additionalProperties": { + "type": "string" + } }, - { + "response": { "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "secure_proxy" - ], - "description": "Use case type" - }, - "configuration": { - "$ref": "#/components/schemas/SecureProxyUseCaseConfiguration" - } + "description": "Type annotations for response fields", + "additionalProperties": { + "type": "string" } } - ] + } }, - "SecureProxyUseCaseConfiguration": { + "TypeDescriptor": { "type": "object", + "description": "Describes the inferred type shape of a JSONata expression", "required": [ - "vpc_mode" + "kind" ], - "description": "Configuration for secure_proxy use cases. Defines how to route requests through a secure VPC.\n", "properties": { - "vpc_mode": { + "kind": { "type": "string", "enum": [ - "static_ip", - "secure_link" - ], - "readOnly": true, - "description": "VPC routing mode. Read-only after creation.\n- static_ip: Routes through a VPC with static outbound IP (NAT Gateway) for IP-allowlisted external APIs.\n- secure_link: Routes through a VPN VPC for accessing private customer networks.\n" + "object", + "array", + "string", + "number", + "boolean", + "null", + "unknown", + "union" + ] }, - "allowed_domains": { - "type": "array", - "items": { - "type": "string" - }, - "readOnly": true, - "description": "Domain whitelist for secure_link mode. Admin-only — can only be modified directly in DynamoDB via admin script.\nSupports exact match (e.g., \"api.wemag.com\") and wildcard prefix (e.g., \"*.wemag.com\").\n" + "properties": { + "type": "object", + "description": "For kind=object, the properties and their type descriptors", + "additionalProperties": { + "$ref": "#/components/schemas/TypeDescriptor" + } + }, + "items": { + "description": "For kind=array, the type of array items", + "allOf": [ + { + "$ref": "#/components/schemas/TypeDescriptor" + } + ] }, - "allowed_ips": { + "source": { + "type": "string", + "description": "For kind=unknown, the JSONata path that produced this value" + }, + "variants": { "type": "array", + "description": "For kind=union, the variant types", "items": { - "type": "string" - }, - "readOnly": true, - "description": "IP allowlist (CIDR notation) for secure_link mode. Admin-only — can only be modified directly in DynamoDB via admin script.\nRequired for secure_link mode. All DNS-resolved IPs must match at least one range.\nExample: [\"10.0.1.0/24\", \"192.168.1.0/24\"]\n" + "$ref": "#/components/schemas/TypeDescriptor" + } } } }, - "SecureProxyWhitelist": { + "UseCaseTypePreview": { "type": "object", + "description": "Scaffolded type descriptors for a single use case", "required": [ - "vpc_mode", - "allowed_domains", - "allowed_ips" + "slug", + "request_shape", + "response_shape" ], - "description": "Current whitelist state for a secure_proxy use case. vpc_mode is read-only\ncontext so the UI can show the user what mode the pool is in.\n", "properties": { - "vpc_mode": { - "type": "string", - "nullable": true, - "enum": [ - "static_ip", - "secure_link" - ], - "description": "VPC routing mode (read-only). `null` if not yet set." + "slug": { + "type": "string" }, - "allowed_domains": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Exact domain (\"api.example.com\") or wildcard prefix (\"*.example.com\").\nWildcards must have at least 2 labels in the suffix.\n" + "name": { + "type": "string" }, - "allowed_ips": { - "type": "array", - "items": { - "type": "string" - }, - "description": "CIDR-notation IP ranges, e.g. \"10.0.0.0/24\"." + "request_shape": { + "$ref": "#/components/schemas/TypeDescriptor" + }, + "response_shape": { + "$ref": "#/components/schemas/TypeDescriptor" + }, + "existing_annotations": { + "$ref": "#/components/schemas/TypeAnnotations" } } }, - "SecureProxyWhitelistUpdate": { + "GenerateTypesPreviewResponse": { "type": "object", - "description": "Partial update for a secure_proxy whitelist. At least one of\n`allowed_domains` or `allowed_ips` must be provided.\n\nPer-field semantics:\n - **omitted** — the field is not modified; the stored value is preserved.\n - **non-empty array** — the stored value is replaced with the supplied list.\n - **empty array (`[]`)** — the list is cleared (stored as `[]`). This is the\n canonical way to remove all entries from a list. `null` is not accepted.\n", - "minProperties": 1, + "required": [ + "use_cases" + ], "properties": { - "allowed_domains": { + "integration_name": { + "type": "string" + }, + "use_cases": { "type": "array", "items": { - "type": "string" - }, - "description": "Exact domain (\"api.example.com\") or wildcard prefix (\"*.example.com\").\nWildcards must have at least 2 labels in the suffix.\nPass `[]` to clear all allowed domains.\n" + "$ref": "#/components/schemas/UseCaseTypePreview" + } }, - "allowed_ips": { + "previous_version": { + "type": "string", + "description": "Previously generated version, if any" + }, + "suggested_version": { + "type": "string", + "description": "Suggested next version based on detected changes" + }, + "suggested_bump": { + "type": "string", + "enum": [ + "major", + "minor" + ] + }, + "detected_changes": { "type": "array", "items": { - "type": "string" - }, - "description": "CIDR-notation IP ranges, e.g. \"10.0.0.0/24\".\nPass `[]` to clear all allowed IPs.\n" + "type": "object", + "required": [ + "slug", + "field", + "type", + "description" + ], + "properties": { + "slug": { + "type": "string" + }, + "field": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "breaking", + "non-breaking" + ] + }, + "description": { + "type": "string" + } + } + } } } }, - "SecureProxySummary": { + "GenerateTypesRequest": { "type": "object", "required": [ - "id", - "name", - "enabled", - "vpc_mode", - "integration_id", - "integration_name" + "package_name", + "version" ], "properties": { - "id": { + "package_name": { "type": "string", - "format": "uuid" + "description": "npm package name", + "example": "@epilot/hems-cleverpv" }, - "name": { - "type": "string" + "version": { + "type": "string", + "description": "Package version", + "example": "1.0.0" }, - "slug": { + "description": { "type": "string" }, - "enabled": { - "type": "boolean" + "domain_package": { + "type": "string", + "description": "Domain standard package to extend", + "example": "@epilot/hems" }, - "vpc_mode": { + "domain_map_name": { "type": "string", - "enum": [ - "static_ip", - "secure_link" - ] + "description": "Name of the domain UseCaseMap to extend", + "example": "HemsUseCaseMap" }, - "allowed_domains": { - "type": "array", - "items": { + "annotations": { + "type": "object", + "description": "Type annotations per use case slug", + "additionalProperties": { + "$ref": "#/components/schemas/TypeAnnotations" + } + } + } + }, + "GenerateTypesResponse": { + "type": "object", + "required": [ + "package_name", + "version", + "files" + ], + "properties": { + "package_name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "files": { + "type": "object", + "description": "Generated file contents keyed by file path", + "additionalProperties": { "type": "string" } }, - "allowed_ips": { + "warnings": { "type": "array", "items": { "type": "string" } - }, - "integration_id": { - "type": "string", - "format": "uuid" - }, - "integration_name": { - "type": "string" } } }, - "SecureProxyRequest": { + "CommitTypesRequest": { "type": "object", "required": [ - "url", - "method", - "integration_id" + "package_name", + "version" ], "properties": { - "integration_id": { - "type": "string", - "format": "uuid", - "description": "Integration ID that owns the secure_proxy use case" - }, - "use_case_id": { - "type": "string", - "format": "uuid", - "description": "Use case ID (provide either use_case_id or use_case_slug)" - }, - "use_case_slug": { - "type": "string", - "description": "Use case slug (provide either use_case_id or use_case_slug)" - }, - "url": { + "package_name": { "type": "string", - "format": "uri", - "description": "Target URL to proxy the request to" + "description": "npm package name", + "example": "@epilot/hems-cleverpv" }, - "method": { + "version": { "type": "string", - "enum": [ - "GET", - "POST", - "PUT", - "PATCH", - "DELETE" - ], - "description": "HTTP method" + "description": "Package version", + "example": "1.0.0" }, - "headers": { + "annotations": { "type": "object", + "description": "Type annotations per use case slug", "additionalProperties": { - "type": "string" - }, - "description": "Request headers to forward" - }, - "body": { - "description": "Request body (string or JSON)" - }, - "response_type": { - "type": "string", - "enum": [ - "json", - "binary" - ], - "default": "json", - "description": "Expected response type" + "$ref": "#/components/schemas/TypeAnnotations" + } } } }, - "SecureProxyResponse": { + "CommitTypesResponse": { "type": "object", + "required": [ + "committed" + ], "properties": { - "status_code": { - "type": "integer", - "description": "HTTP status code from the upstream response" + "committed": { + "type": "boolean" }, - "headers": { - "type": "object", - "additionalProperties": { + "warnings": { + "type": "array", + "items": { "type": "string" - }, - "description": "Response headers from upstream" - }, - "body": { - "description": "Response body from upstream" + } } } }, - "ManagedCallOperationConfig": { + "FileProxyUseCaseConfiguration": { "type": "object", "required": [ - "operation" + "steps" ], - "description": "Configuration for managed_call use cases. Defines a single API operation with JSONata mapping.", + "description": "Configuration for file_proxy use cases. Defines how to authenticate and move files\nbetween epilot and an external document system, in either direction (see `direction`).\n\n**Download** (`direction: download`, the default) fetches a file from the external system\nand serves it to a browser. The download URL always requires `orgId`, `integrationId`, and\neither `useCaseSlug` (recommended) or `useCaseId` (legacy UUID) as query parameters.\nThe `orgId` is included in the signed URL to establish organization context without\nrequiring authentication. Additional use-case-specific parameters are declared in the\n`params` array. `response` is REQUIRED for download use cases.\n\n**Upload** (`direction: upload`) pushes epilot files to the external system. It is not\nreachable over the download endpoint; an outbound use case points at it via a `file_proxy`\ndelivery, and this configuration owns everything about what gets sent: `fan_out` decides\nhow many deliveries one event produces, and every step builds its own request body with\n`body_jsonata` — or leaves it empty to send the delivery's files unchanged.\n`upload` is REQUIRED and `response` MUST be omitted.\n\nTwo expression languages, split by what they produce. **Handlebars composes strings**:\n`url` and `headers` on upload, plus a form-encoded, XML or plain-text `body` on\ndownload. **JSONata produces data**: `body_jsonata` and the per-step `enabled`.\nJSONata omits keys whose value is undefined, so an optional field needs no conditional\nguard — it is simply absent from the serialized body.\n\nHandlebars templates are rendered EXACTLY ONCE, against a single context holding `env`,\n`file_data`, `steps` and `auth_token`. Writing\n`{{ env.some_var }}` resolves it. The legacy `\\{{ env.some_var }}` escape belongs to the\ntwo-pass renderer and is NOT rewritten here — it renders as the literal text\n`{{ env.some_var }}`, which is rejected (see below) rather than shipped.\n\nTwo guards run on every rendered upload template, because single-pass rendering fails\nquietly by default. Both are terminal, and each names what to fix:\n\n- **residual `{{` after rendering** — a configuration still carrying the `\\{{` escape.\n Rewrite it without the backslash.\n- **a referenced `env` key absent from the environment** — checked BEFORE the URL is\n parsed, because an empty value in host position turns\n `https://{{env.host}}/document/import` into `https:///document/import`, whose host\n then parses as `document`. Provision the environment variable. A key that exists and\n is legitimately empty is fine; only absence fails.\n\nOpenAPI 3.0 cannot express this conditional requiredness, so it is enforced by the\nserver-side validator, which returns an explicit message naming the offending field.\n", "properties": { - "operation": { - "$ref": "#/components/schemas/ManagedCallOperation" - }, - "request_mapping": { + "direction": { "type": "string", - "description": "JSONata expression for outbound body transformation" + "enum": [ + "download", + "upload" + ], + "default": "download", + "description": "Direction of file travel. `download` (default) pulls a file from the external system\ninto epilot; `upload` pushes an epilot file out to the external system. Omitted means\n`download`, so every pre-existing configuration keeps its exact meaning.\n\nNote this is the direction of the FILE, not the epilot use-case type — an `upload`\nfile_proxy use case is still a `file_proxy` use case, never an `outbound` one.\n" }, - "response_mapping": { - "type": "string", - "description": "JSONata expression for inbound response transformation" + "upload": { + "$ref": "#/components/schemas/FileProxyUploadConfig" }, - "inbound_use_case_slug": { + "fan_out": { + "$ref": "#/components/schemas/FileProxyFanOutConfig" + }, + "secure_proxy": { + "description": "Optional secure proxy attachment for routing all outbound file proxy requests.\nOnly `use_case_slug` is supported and the referenced secure_proxy use case\nmust belong to the same integration.\n", + "allOf": [ + { + "$ref": "#/components/schemas/FileProxySecureProxyAttachment" + } + ] + }, + "auth": { + "$ref": "#/components/schemas/FileProxyAuth" + }, + "params": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileProxyParam" + }, + "description": "Download-only. Additional use-case-specific parameters expected in the download URL\nquery string (beyond the required orgId, integrationId, and useCaseSlug or useCaseId).\nRejected when `direction` is `upload`.\n" + }, + "allowed_origins": { + "type": "array", + "items": { + "type": "string", + "format": "uri", + "pattern": "^https?://" + }, + "description": "Download-only. Additional origins permitted to call /download for this use case\n(CORS, exact match). Portal origins are always allowed. Rejected when `direction`\nis `upload`.\n" + }, + "steps": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/components/schemas/FileProxyStep" + }, + "description": "Ordered list of HTTP steps to execute. For `download` these retrieve the file; for\n`upload` they deliver it, each assembling its own request body — `body_jsonata` for\nJSON, `body` for anything else.\n" + }, + "response": { + "$ref": "#/components/schemas/FileProxyResponseConfig" + }, + "prevent_indirect_serving": { + "type": "boolean", + "default": false, + "description": "Download-only; rejected when `direction` is `upload`.\nWhen `true`, this use case is served via the streaming endpoint: mapped file URLs\nare built as `/stream/download`, files of any size are streamed inline over HTTP\nresponse streaming, and buffered `/download` requests for oversize files are\n307-redirected to `/stream`. Files never transit epilot's temporary S3 storage on\nthe streaming path. Defaults to `false` (small files are served directly and large\nfiles are transparently served via a temporary S3 redirect).\n" + } + } + }, + "FileProxySecureProxyAttachment": { + "type": "object", + "required": [ + "use_case_slug" + ], + "additionalProperties": false, + "properties": { + "use_case_slug": { "type": "string", - "description": "Slug of the inbound use case to route responses to for async entity processing.\nWhen set, the managed call response is queued to the inbound pipeline and processed\nusing the referenced inbound use case's mapping configuration.\n" + "description": "Slug of the secure_proxy use case in the same integration." } } }, - "ManagedCallOperation": { + "FileProxyAuth": { "type": "object", "required": [ - "method", - "path" + "type", + "token_url", + "client_id", + "client_secret" ], - "description": "HTTP operation configuration for managed calls", "properties": { - "method": { + "type": { "type": "string", "enum": [ - "GET", - "POST", - "PUT", - "PATCH", - "DELETE" - ] + "oauth2_client_credentials", + "oauth2_password" + ], + "description": "Authentication type" }, - "path": { + "token_url": { "type": "string", - "description": "URL path template with {{variable}} interpolation" + "description": "Handlebars template for the OAuth2 token endpoint URL" + }, + "client_id": { + "type": "string", + "description": "Handlebars template for the OAuth2 client ID" + }, + "client_secret": { + "type": "string", + "description": "Handlebars template for the OAuth2 client secret" + }, + "scope": { + "type": "string", + "description": "Optional OAuth2 scope" + }, + "audience": { + "type": "string", + "description": "Handlebars template for the OAuth2 audience parameter" + }, + "resource": { + "type": "string", + "description": "Handlebars template for the OAuth2 resource parameter" + }, + "username": { + "type": "string", + "description": "Handlebars template for the OAuth2 resource owner username. Required when type is oauth2_password." + }, + "password": { + "type": "string", + "description": "Handlebars template for the OAuth2 resource owner password. Required when type is oauth2_password." + }, + "body_params": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Additional key-value pairs to include in the token request body. Values support Handlebars templates." }, "headers": { "type": "object", - "description": "Custom HTTP headers for the request. Values support {{variable}} interpolation from the request payload and {{env.VAR}} references for environment variables.", "additionalProperties": { "type": "string" - } + }, + "description": "Additional headers to include in the token request. Values support Handlebars templates." }, "query_params": { "type": "object", - "description": "Query parameters for the request. Values support {{variable}} interpolation from the request payload.", "additionalProperties": { "type": "string" - } + }, + "description": "Additional query parameters to append to the token URL. Values support Handlebars templates." } } }, - "ManagedCallExecuteRequest": { + "FileProxyParam": { "type": "object", "required": [ - "integration_id" + "name", + "required" ], "properties": { - "integration_id": { + "name": { "type": "string", - "format": "uuid", - "description": "Integration ID" + "description": "Parameter name as it appears in the query string" }, - "payload": { - "type": "object", - "additionalProperties": true, - "description": "Request payload for the managed call operation" + "required": { + "type": "boolean", + "description": "Whether this parameter is required" }, - "correlation_id": { + "description": { "type": "string", - "description": "Correlation ID for tracing related events (auto-generated if not provided)" + "description": "Human-readable description of the parameter" } } }, - "ManagedCallExecuteResponse": { - "description": "The response from a managed call execution.\nOn success, returns the JSONata-mapped response data directly (no wrapper).\nThe shape is entirely defined by your response_mapping JSONata expression.\nIf no response_mapping is configured, returns the raw external API response.\nCheck the X-Inbound-Event-Id header for inbound pipeline tracking when inbound routing is configured.\n", - "additionalProperties": true - }, - "ManagedCallErrorResponse": { + "FileProxyStep": { "type": "object", "required": [ - "error" + "url", + "method", + "response_type" ], "properties": { - "error": { - "type": "object", - "required": [ - "code", - "message" - ], - "properties": { - "code": { - "type": "string", - "description": "Error code (e.g., EXTERNAL_API_ERROR, SSRF_BLOCKED, MANAGED_CALL_EXECUTION_ERROR)" - }, - "message": { - "type": "string", - "description": "Human-readable error message" - }, - "details": { - "type": "object", - "additionalProperties": true, - "description": "Additional error details (e.g., status code and body for external API errors)" - } - } - } - } - }, - "TypeAnnotations": { - "type": "object", - "description": "Developer-provided type annotations for a use case's request and response fields", - "properties": { - "request": { - "type": "object", - "description": "Type annotations for request fields, keyed by dot-path (e.g., \"vendors[].id\" -> \"string\")", - "additionalProperties": { - "type": "string" - } + "url": { + "type": "string", + "description": "Handlebars template for the request URL" }, - "response": { - "type": "object", - "description": "Type annotations for response fields", - "additionalProperties": { - "type": "string" - } - } - } - }, - "TypeDescriptor": { - "type": "object", - "description": "Describes the inferred type shape of a JSONata expression", - "required": [ - "kind" - ], - "properties": { - "kind": { + "method": { "type": "string", "enum": [ - "object", - "array", - "string", - "number", - "boolean", - "null", - "unknown", - "union" - ] + "GET", + "POST", + "PUT", + "PATCH" + ], + "description": "HTTP method" }, - "properties": { + "headers": { "type": "object", - "description": "For kind=object, the properties and their type descriptors", "additionalProperties": { - "$ref": "#/components/schemas/TypeDescriptor" - } + "type": "string" + }, + "description": "Handlebars templates for request headers" }, - "items": { - "description": "For kind=array, the type of array items", - "allOf": [ - { - "$ref": "#/components/schemas/TypeDescriptor" - } - ] + "body_jsonata": { + "type": "string", + "description": "Upload-only. JSONata producing the request body as DATA; the result is serialized to\nJSON and sent. Use this for every JSON body — it cannot emit malformed JSON, and it\nomits a key whose value is undefined instead of sending it empty, which is what makes\noptional fields work without a conditional guard.\n\nThe evaluation root is the hydrated event, so `contact.customer_pin` and\n`ticket._purpose` are reachable directly, unprefixed. **Everything else is a\n`$`-prefixed binding**: `$file_data` (the files this delivery carries — see\n`FileProxyFanOutConfig` for what it holds in each mode), `$ack_id`, `$env`,\n`$steps` (results of the steps already executed, each\n`{statusCode, headers, body}`), `$germanDate(iso)` and `$now()`.\nWriting `file_data[0].filename` instead of `$file_data[0].filename` yields\nnothing — it reads a field named `file_data` on the event, which does not exist.\n\nLeave this empty to send the delivery's files exactly as they are: the single\nattachment object when fanning out, the whole array when not. No mapping is\nneeded for the common case.\n\nMust evaluate to an object or an array. Anything else fails the delivery terminally\nwith `MAPPING_EXPRESSION_FAILED`, naming the step.\n\n**Only `undefined` omits a key.** `null`, `\"\"`, `false` and `0` are values and are\nall sent. Two traps follow from that, and one idiom avoids both:\n\n- `x ? $string(x)` omits the key when `x` is `0` or `\"\"`, because JSONata reads both\n as false. A meter number of `\"0\"` would silently vanish.\n- `: undefined` is not a literal — JSONata has no `undefined` keyword, so it is a\n path lookup that happens to find nothing. Against an event that really does carry\n a field named `undefined`, it returns that field's value.\n\nWrite the **two-arm ternary with no else branch**, which omits the key when the test\nis false: `{ \"pin\": $exists(contact.pin) ? $string(contact.pin) }`.\n\nEvery `$` binding is checked at save time against the known set, so a mistyped\nbinding (`$lookup.documentType` for `$file_data[0].filename`) is rejected rather\nthan silently producing a missing key.\n\nNot accepted together with `body`, which the upload direction rejects outright.\n", + "example": "{ \"documentType\": \"Zählerstand\", \"fileName\": $file_data[0].filename, \"fileData\": $file_data[0].base64, \"pin\": $exists(contact.customer_pin) ? $string(contact.customer_pin) }" }, - "source": { + "enabled": { "type": "string", - "description": "For kind=unknown, the JSONata path that produced this value" + "description": "Upload-only, OPTIONAL. JSONata returning a boolean, deciding whether this step\nruns at all. Absent means it runs.\n\nA false result is a BREAK: this step is skipped and so is every step after it,\nand the delivery is recorded as `skipped` rather than delivered or failed. It is\nacknowledged and never retried, and a `STEP_DISABLED` monitoring event is emitted\nat level `info` — a disabled step is the configuration working, not a fault.\n\nReads the same bindings a body does, `$steps` included, so it can branch on what\nan earlier step returned. This is how a delivery is filtered out: with one\ndelivery per attachment, a false result on the first step drops that file.\n\nAn expression that throws, or returns a non-boolean, is a terminal\n`MAPPING_EXPRESSION_FAILED` instead — a broken predicate must not read as a\ndeliberate skip.\n", + "example": "$file_data[0].mime_type = \"application/pdf\"" }, - "variants": { - "type": "array", - "description": "For kind=union, the variant types", - "items": { - "$ref": "#/components/schemas/TypeDescriptor" - } + "body": { + "type": "string", + "description": "Handlebars template for the request body (write methods only), for bodies that are\nNOT JSON — form-encoded, XML, plain text. For a JSON body use `body_jsonata`\ninstead; it cannot produce malformed JSON.\n\n**Download-only.** The upload direction rejects it: use `body_jsonata`, or leave\nthat empty to send the files unchanged.\n\nRendered once, against the context described on `FileProxyUseCaseConfiguration`:\n`{{ params.* }}`, `{{ env.* }}`, `{{ steps.N.body }}`, `{{ auth_token }}`.\n\nIf a JSON body is written here anyway, **route every user-controlled value through\nthe `json` helper**: the template engine does not escape, so\n`\"name\":\"{{ params.documentName }}\"` produces invalid JSON the moment a value\ncontains a quote. Write `\"name\": {{json params.documentName}}` instead — the helper\nemits the surrounding quotes itself and renders absent values as `null`.\n\nMutually exclusive with `body_jsonata`, which is upload-only.\n" + }, + "response_type": { + "type": "string", + "enum": [ + "json", + "binary" + ], + "description": "Expected response type" } } }, - "UseCaseTypePreview": { + "FileProxyFanOutConfig": { "type": "object", - "description": "Scaffolded type descriptors for a single use case", "required": [ - "slug", - "request_shape", - "response_shape" + "enabled" ], + "description": "Whether one event produces one delivery per file, or a single delivery carrying all of\nthem. The split is always over the event's `event_attachments` — there is no expression\nto write, because an upload only ever runs on events that declare that field.\n\nEach resulting delivery is fully independent: its own idempotency record, its own retry\nschedule, its own monitoring events. A four-file event can therefore end up three-of-four\ndelivered, which is the honest state to report.\n\nThe split is evaluated ONCE, when the event is enqueued, so item indices — and therefore\nidempotency keys — stay stable across retries.\n\nEither way, expressions read the same `$file_data` binding, so no expression needs to\nknow which mode it runs in. An event carrying no attachments is recorded as `skipped`.\n", "properties": { - "slug": { - "type": "string" - }, - "name": { - "type": "string" - }, - "request_shape": { - "$ref": "#/components/schemas/TypeDescriptor" + "enabled": { + "type": "boolean", + "description": "When true, one delivery per attachment, and `$file_data` holds exactly that one\nfile. When false (or absent), the event produces exactly one delivery and\n`$file_data` holds every attachment on the event.\n\nAn empty `event_attachments` produces no deliveries and one info-level\n`FAN_OUT_EMPTY`, which is the normal outcome for a catch-all subscription seeing an\nevent with nothing to send.\n" + } + } + }, + "FileProxyUploadConfig": { + "type": "object", + "description": "Upload-side settings for a file_proxy use case with `direction: upload`.\nThe surrounding file_proxy configuration owns WHAT and HOW to send: `fan_out`,\nauth, and the steps with their `body_jsonata`. This nested object governs\nupload-specific limits and how the final external response is judged. The\noutbound mapping remains a pure pointer to the recipe (see `FileProxyDeliveryConfig`).\n", + "properties": { + "max_total_bytes": { + "type": "integer", + "description": "Ceiling for ALL of a delivery's files together, in bytes. Capped by the platform\nlimit, which it may lower but never raise.\n\nOnly reachable with `fan_out.enabled: false`, where one delivery carries every\nattachment and the worker holds them all resident at once. base64 inflates each\nby about a third, so the combined figure is what matters rather than any single\nfile's size.\n", + "example": 26214400 }, - "response_shape": { - "$ref": "#/components/schemas/TypeDescriptor" + "max_file_bytes": { + "type": "integer", + "minimum": 1, + "maximum": 104857600, + "description": "Per-file ceiling for this use case, in bytes. Files above it fail terminally with\n`FILE_TOO_LARGE` before any bytes are fetched. Defaults to — and is clamped by — the\nplatform ceiling of 100 MiB (104857600).\n" }, - "existing_annotations": { - "$ref": "#/components/schemas/TypeAnnotations" + "max_delivery_attempts": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 8, + "description": "Maximum delivery attempts per item before the delivery is marked failed. Attempts\nare spaced by an exponential, jittered backoff. The default 8 attempts schedule at\nmost 7 delays, totaling about 7 hours 40 minutes before jitter, so a normal ERP\nmaintenance window does not immediately exhaust them.\n\nLives here rather than on the outbound delivery because how hard to retry is a\nproperty of the transport — the same judgement as `max_file_bytes` — and the outbound\ndelivery is a pure pointer.\n" } } }, - "GenerateTypesPreviewResponse": { + "FileProxyResponseConfig": { "type": "object", + "description": "How to extract the file from the step results. REQUIRED when `direction` is `download`;\nrejected when `direction` is `upload` (an upload has no file to extract).\n", "required": [ - "use_cases" + "body", + "encoding" ], "properties": { - "integration_name": { - "type": "string" - }, - "use_cases": { - "type": "array", - "items": { - "$ref": "#/components/schemas/UseCaseTypePreview" - } - }, - "previous_version": { + "body": { "type": "string", - "description": "Previously generated version, if any" + "description": "JSONata expression to extract file content from step results" }, - "suggested_version": { + "encoding": { "type": "string", - "description": "Suggested next version based on detected changes" + "enum": [ + "base64", + "binary" + ], + "description": "Encoding of the extracted body" }, - "suggested_bump": { + "filename": { "type": "string", - "enum": [ - "major", - "minor" - ] + "description": "JSONata expression to extract the filename" }, - "detected_changes": { - "type": "array", - "items": { - "type": "object", - "required": [ - "slug", - "field", - "type", - "description" - ], - "properties": { - "slug": { - "type": "string" - }, - "field": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "breaking", - "non-breaking" - ] - }, - "description": { - "type": "string" - } - } - } + "content_type": { + "type": "string", + "description": "JSONata expression to extract the content type" } } }, - "GenerateTypesRequest": { + "MappingSimulationRequest": { "type": "object", "required": [ - "package_name", - "version" + "mapping_configuration", + "object_type", + "format", + "payload" ], "properties": { - "package_name": { - "type": "string", - "description": "npm package name", - "example": "@epilot/hems-cleverpv" + "mapping_configuration": { + "oneOf": [ + { + "$ref": "#/components/schemas/IntegrationConfigurationV1" + }, + { + "$ref": "#/components/schemas/IntegrationConfigurationV2" + } + ] }, - "version": { + "object_type": { "type": "string", - "description": "Package version", - "example": "1.0.0" - }, - "description": { - "type": "string" + "description": "Type of the object/event being mapped.\nFor v1.0: must match a key in mapping_configuration.mapping.objects\nFor v2.0: must match a key in mapping_configuration.mapping.events\n" }, - "domain_package": { + "format": { "type": "string", - "description": "Domain standard package to extend", - "example": "@epilot/hems" + "enum": [ + "json", + "xml" + ], + "default": "json", + "description": "Format of the payload data" }, - "domain_map_name": { + "payload": { + "oneOf": [ + { + "type": "string", + "description": "The serialized object data payload (JSON, XML, etc.) as a string" + }, + { + "type": "object", + "description": "Direct JSON object (will be automatically serialized)", + "additionalProperties": true + } + ], + "description": "The object data payload - can be either a serialized string or a direct JSON object" + } + } + }, + "MappingSimulationV2Request": { + "type": "object", + "description": "Request for v2 mapping simulation. Uses the same configuration format stored in integration use case resources,\nmaking it easier to test configurations before saving them.\n", + "required": [ + "event_configuration", + "format", + "payload" + ], + "properties": { + "event_configuration": { + "$ref": "#/components/schemas/InboundIntegrationEventConfiguration" + }, + "format": { "type": "string", - "description": "Name of the domain UseCaseMap to extend", - "example": "HemsUseCaseMap" + "enum": [ + "json", + "xml" + ], + "default": "json", + "description": "Format of the payload data" }, - "annotations": { - "type": "object", - "description": "Type annotations per use case slug", - "additionalProperties": { - "$ref": "#/components/schemas/TypeAnnotations" - } + "payload": { + "oneOf": [ + { + "type": "string", + "description": "The serialized event data payload (JSON, XML, etc.) as a string" + }, + { + "type": "object", + "description": "Direct JSON object (will be automatically serialized)", + "additionalProperties": true + } + ], + "description": "The event data payload - can be either a serialized string or a direct JSON object" } } }, - "GenerateTypesResponse": { + "MappingSimulationResponse": { "type": "object", "required": [ - "package_name", - "version", - "files" + "entity_updates" ], "properties": { - "package_name": { - "type": "string" + "entity_updates": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EntityUpdate" + } }, - "version": { - "type": "string" + "meter_readings_updates": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MeterReadingUpdate" + } }, - "files": { - "type": "object", - "description": "Generated file contents keyed by file path", - "additionalProperties": { - "type": "string" + "prune_scope_updates": { + "type": "array", + "description": "Resolved prune scopes produced by entity mappings using `upsert-prune-scope-purge` / `upsert-prune-scope-delete`. Each entry describes the entities that would be deleted at runtime because they exist in scope but were not part of this payload.", + "items": { + "$ref": "#/components/schemas/EntityPruneScopeUpdate" + } + }, + "meter_readings_prune_scope_updates": { + "type": "array", + "description": "Resolved prune scopes produced by meter reading mappings using `upsert-prune-scope`. Each entry describes the readings that would be deleted at runtime for a meter (+ counter) because they were not part of this payload.", + "items": { + "$ref": "#/components/schemas/MeterReadingPruneScopeUpdate" } }, "warnings": { "type": "array", + "description": "Validation warnings about the configuration (e.g., unique_ids referencing non-indexed fields)", "items": { - "type": "string" + "$ref": "#/components/schemas/MappingSimulationWarning" } } } }, - "CommitTypesRequest": { + "MappingSimulationWarning": { "type": "object", "required": [ - "package_name", - "version" + "entity_schema", + "field", + "message" ], "properties": { - "package_name": { + "entity_schema": { "type": "string", - "description": "npm package name", - "example": "@epilot/hems-cleverpv" + "description": "The entity schema slug where the issue was found" }, - "version": { + "field": { "type": "string", - "description": "Package version", - "example": "1.0.0" + "description": "The field name that caused the warning" }, - "annotations": { - "type": "object", - "description": "Type annotations per use case slug", - "additionalProperties": { - "$ref": "#/components/schemas/TypeAnnotations" - } + "message": { + "type": "string", + "description": "Description of the validation issue" } } }, - "CommitTypesResponse": { + "DirectPayload": { "type": "object", + "description": "The payload of an ERP event routed to a `direct: true` use case: a versioned envelope of\npre-mapped operations that skip the mapping engine. Sent as the `payload` of an\n`ErpEventV3` - either as a JSON object or as a JSON string (`format` must be `json`;\nXML is rejected for direct use cases).\n\nThe schema is strict: unknown keys on the envelope or on any operation are rejected with\nan error naming the offending path. Attribute values, by contrast, pass through to the\nentity verbatim (except `$relation` / `$relation_ref` envelopes, which are validated and\nresolved).\n\nOne event lands as one queue message, so the whole serialized event (including this\npayload and its internal envelope) must stay within the 1 MiB message budget. Identical\nconsecutive payloads within 5 minutes can be deduplicated by the queue - use the event's\n`deduplication_id` deliberately when re-sending identical data.\n", "required": [ - "committed" + "version", + "operations" ], "properties": { - "committed": { - "type": "boolean" + "version": { + "type": "string", + "description": "Version of the direct payload contract. Currently only `\"1\"` is supported; an\nunsupported version is rejected with `DIRECT_VERSION_UNSUPPORTED`. Deliberately not\nan enum: the constraint is enforced by the handler so the dry-run endpoint returns\na structured `valid: false` verdict instead of a schema-level 400.\n" }, - "warnings": { + "operations": { "type": "array", + "description": "The operations to apply, in order. At most 100 operations per event (enforced by\nthe handler, with a structured error naming the limit). Each item is either an\nentity operation (`type` omitted or `\"entity\"`) or a meter reading operation\n(`type: \"meter_reading\"`).\n", "items": { - "type": "string" + "anyOf": [ + { + "$ref": "#/components/schemas/DirectEntityOperation" + }, + { + "$ref": "#/components/schemas/DirectMeterReadingOperation" + } + ] } } } }, - "FileProxyUseCaseConfiguration": { + "DirectEntityOperation": { "type": "object", + "description": "A single pre-mapped entity operation: upserts, soft-deletes, or purges one entity,\nidentified by its unique ids. Produces the same internal update as a mapped-mode entity\nconfiguration, so create-vs-update resolution, relation resolution, ordering, and\nmonitoring behave identically.\n", "required": [ - "steps", - "response" + "entity_slug", + "unique_ids" ], - "description": "Configuration for file_proxy use cases. Defines how to authenticate and fetch files from external document systems.\n\nThe file proxy download URL always requires `orgId`, `integrationId`, and either `useCaseSlug` (recommended) or `useCaseId` (legacy UUID) as query parameters.\nThe `orgId` is included in the signed URL to establish organization context without requiring authentication.\nAdditional use-case-specific parameters are declared in the `params` array.\n", + "additionalProperties": false, "properties": { - "secure_proxy": { - "description": "Optional secure proxy attachment for routing all outbound file proxy requests.\nOnly `use_case_slug` is supported and the referenced secure_proxy use case\nmust belong to the same integration.\n", - "allOf": [ - { - "$ref": "#/components/schemas/FileProxySecureProxyAttachment" - } - ] + "type": { + "type": "string", + "enum": [ + "entity" + ], + "description": "Operation discriminator. Optional - an operation without `type` is an entity operation." }, - "auth": { - "$ref": "#/components/schemas/FileProxyAuth" + "entity_slug": { + "type": "string", + "minLength": 1, + "description": "Target entity schema slug (e.g. `contact`, `contract`).", + "example": "contract" }, - "params": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FileProxyParam" + "unique_ids": { + "type": "object", + "minProperties": 1, + "additionalProperties": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ] }, - "description": "Additional use-case-specific parameters expected in the download URL query string (beyond the required orgId, integrationId, and useCaseSlug or useCaseId)" + "description": "Attribute values that uniquely identify this entity, as `{ attribute: value }`.\nValues may be strings or numbers (numbers are coerced to strings); values that are\nempty after trimming are rejected. Use `_id` as the sole key to reference an entity\ndirectly by its epilot id.\n\nUnlike mapped mode, the unique-id ATTRIBUTE NAMES are given directly - there is no\nfield-to-attribute mapping step.\n", + "example": { + "contract_number": "CTR-001" + } }, - "allowed_origins": { - "type": "array", - "items": { + "unique_id_types": { + "type": "object", + "additionalProperties": { "type": "string", - "format": "uri", - "pattern": "^https?://" - }, - "description": "Additional origins permitted to call /download for this use case (CORS, exact match). Portal origins are always allowed." - }, - "steps": { - "type": "array", - "minItems": 1, - "items": { - "$ref": "#/components/schemas/FileProxyStep" + "enum": [ + "email", + "phone" + ] }, - "description": "Ordered list of HTTP steps to execute to retrieve the file" + "description": "Optional per-field type hints for repeatable unique-id fields (`email` / `phone`),\nneeded so lookups search the repeatable storage format correctly. When omitted, the\nserver derives the types from the entity schema; explicit values always win over\nderivation.\n" }, - "response": { - "$ref": "#/components/schemas/FileProxyResponseConfig" - } - } - }, - "FileProxySecureProxyAttachment": { - "type": "object", - "required": [ - "use_case_slug" - ], - "additionalProperties": false, - "properties": { - "use_case_slug": { + "mode": { "type": "string", - "description": "Slug of the secure_proxy use case in the same integration." + "enum": [ + "upsert", + "delete", + "purge" + ], + "default": "upsert", + "description": "Operation mode: `upsert` creates or updates the entity (default), `delete` soft\ndeletes it, `purge` removes it permanently. For `delete` / `purge`, `attributes`\nmay be omitted - only `unique_ids` are used to locate the entity.\n" + }, + "attributes": { + "type": "object", + "description": "Attribute values to write, keyed by attribute name. Required when `mode` is `upsert`\n(an empty object is allowed); optional for `delete` / `purge`.\n\nValues are written verbatim - the server does not validate them against the entity\nschema (attributes not defined in the schema are stored but not indexed; the\nsimulation endpoint surfaces warnings for unique-id fields missing from the schema).\nTwo envelope forms receive special treatment: `$relation` values\n(`DirectRelationValue`) and `$relation_ref` values (`DirectRelationRefValue`) are\nvalidated and resolved to entity relations.\n", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/components/schemas/DirectRelationValue" + }, + { + "$ref": "#/components/schemas/DirectRelationRefValue" + }, + { + "description": "Any other JSON value - written to the entity attribute verbatim." + } + ] + } } } }, - "FileProxyAuth": { + "DirectMeterReadingOperation": { "type": "object", + "description": "A single pre-mapped meter reading operation: upserts or deletes one reading on a meter\n(and optionally a specific counter), identified by their unique ids. Produces the same\ninternal update as a mapped-mode meter reading configuration.\n", "required": [ "type", - "token_url", - "client_id", - "client_secret" + "meter", + "attributes" ], + "additionalProperties": false, "properties": { "type": { "type": "string", "enum": [ - "oauth2_client_credentials", - "oauth2_password" + "meter_reading" ], - "description": "Authentication type" - }, - "token_url": { - "type": "string", - "description": "Handlebars template for the OAuth2 token endpoint URL" - }, - "client_id": { - "type": "string", - "description": "Handlebars template for the OAuth2 client ID" + "description": "Operation discriminator. Required for meter reading operations." }, - "client_secret": { - "type": "string", - "description": "Handlebars template for the OAuth2 client secret" + "meter": { + "type": "object", + "required": [ + "unique_ids" + ], + "additionalProperties": false, + "description": "Reference to the meter this reading belongs to.", + "properties": { + "unique_ids": { + "type": "object", + "minProperties": 1, + "additionalProperties": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ] + }, + "description": "Attribute values that uniquely identify the meter, as `{ attribute: value }`.", + "example": { + "meter_number": "M-001" + } + } + } }, - "scope": { - "type": "string", - "description": "Optional OAuth2 scope" + "counter": { + "type": "object", + "required": [ + "unique_ids" + ], + "additionalProperties": false, + "description": "Optional reference to the meter counter (register) the reading belongs to. Required\nwhen `reading_matching` is `strict-date`.\n", + "properties": { + "unique_ids": { + "type": "object", + "minProperties": 1, + "additionalProperties": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ] + }, + "description": "Attribute values that uniquely identify the counter, as `{ attribute: value }`.", + "example": { + "obis_number": "1-0:1.8.0" + } + } + } }, - "audience": { + "mode": { "type": "string", - "description": "Handlebars template for the OAuth2 audience parameter" + "enum": [ + "upsert", + "delete" + ], + "default": "upsert", + "description": "Operation mode: `upsert` creates or updates the reading (default), `delete` removes it." }, - "resource": { + "reading_matching": { "type": "string", - "description": "Handlebars template for the OAuth2 resource parameter" + "enum": [ + "external_id", + "strict-date" + ], + "description": "Strategy for matching this reading against existing readings. `external_id` (the\ndefault) matches by the reading's `external_id`; `strict-date` matches by meter +\ncounter + direction + date and REQUIRES `counter`. Useful when readings originate in\nthe portal and are echoed back by the ERP with truncated timestamps.\n" }, - "username": { + "attributes": { + "type": "object", + "required": [ + "external_id", + "timestamp", + "source", + "value" + ], + "description": "The reading data. Additional keys (e.g. `direction`, `reason`, `read_by`, `status`,\n`metadata`) pass through to the metering service verbatim.\n", + "properties": { + "external_id": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ], + "description": "External identifier of the reading (numbers are coerced to strings)." + }, + "timestamp": { + "type": "string", + "description": "When the reading was taken. ISO 8601 - either `YYYY-MM-DD` or\n`YYYY-MM-DDTHH:mm:ss` (optional fractional seconds and `Z` / `±HH:mm` offset).\nOther formats (e.g. `DD.MM.YYYY`, epoch numbers) are rejected.\n", + "example": "2026-08-24T06:00:00Z" + }, + "source": { + "type": "string", + "enum": [ + "ECP", + "ERP", + "360", + "journey-submission" + ], + "description": "Origin system of the reading." + }, + "value": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "string" + } + ], + "description": "The reading value - a number, or a numeric string (coerced to a number)." + } + } + } + } + }, + "DirectRelationValue": { + "type": "object", + "description": "An entity attribute value that establishes relations to other entities. Written on any\nattribute inside a direct entity operation's `attributes`.\n\nTwo forms are accepted: a bare array of items (shorthand for `_set`, i.e. replace), or\nan object with EXACTLY ONE of the operations `_set` (replace), `_append` (merge,\ndeduplicated by entity id), `_append_all` (merge without deduplication). No other\noperation keys are accepted.\n", + "required": [ + "$relation" + ], + "additionalProperties": false, + "properties": { + "$relation": { + "oneOf": [ + { + "type": "array", + "description": "Shorthand for `_set` - replaces the attribute's relations with these items.", + "items": { + "$ref": "#/components/schemas/DirectRelationItem" + } + }, + { + "type": "object", + "description": "Exactly one of `_set`, `_append`, `_append_all` must be given.", + "additionalProperties": false, + "properties": { + "_set": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DirectRelationItem" + } + }, + "_append": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DirectRelationItem" + } + }, + "_append_all": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DirectRelationItem" + } + } + } + } + ] + } + } + }, + "DirectRelationItem": { + "type": "object", + "description": "One relation target: either an already-resolved reference (`entity_id`), or a lookup by\nunique ids (`schema` + `unique_ids`, resolved by the pipeline like any other unique-id\nlookup, including stub creation and retry when the target does not exist yet). The two\nforms are mutually exclusive. Use `_id` as the sole `unique_ids` key for a direct-id\nlookup.\n", + "additionalProperties": false, + "properties": { + "entity_id": { "type": "string", - "description": "Handlebars template for the OAuth2 resource owner username. Required when type is oauth2_password." + "description": "Epilot entity id of the relation target. Must not be combined with `schema` / `unique_ids`." }, - "password": { + "schema": { "type": "string", - "description": "Handlebars template for the OAuth2 resource owner password. Required when type is oauth2_password." + "description": "Entity schema slug of the relation target. Required when `entity_id` is not given.", + "example": "contact" }, - "body_params": { + "unique_ids": { "type": "object", + "minProperties": 1, "additionalProperties": { - "type": "string" + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ] }, - "description": "Additional key-value pairs to include in the token request body. Values support Handlebars templates." + "description": "Attribute values that uniquely identify the target. Required when `entity_id` is not given.", + "example": { + "customer_number": "CUST-12345" + } }, - "headers": { + "unique_id_types": { "type": "object", "additionalProperties": { - "type": "string" + "type": "string", + "enum": [ + "email", + "phone" + ] }, - "description": "Additional headers to include in the token request. Values support Handlebars templates." + "description": "Optional per-field type hints for repeatable unique-id fields, as on the entity operation." }, - "query_params": { - "type": "object", - "additionalProperties": { + "tags": { + "type": "array", + "items": { "type": "string" }, - "description": "Additional query parameters to append to the token URL. Values support Handlebars templates." + "description": "Optional relation tags (e.g. `primary`, `billing`)." } } }, - "FileProxyParam": { + "DirectRelationRefValue": { "type": "object", + "description": "An entity attribute value that references an ITEM INSIDE a repeatable attribute of\nanother entity - for example one address out of a contact's `address` list. The target\nentity is looked up by unique ids, then the item at `path` matching `value` is\nreferenced (creating it when missing).\n\nThe same operation envelope as `$relation` applies: a bare array is shorthand for\n`_set`, otherwise exactly one of `_set`, `_append`, `_append_all`.\n", "required": [ - "name", - "required" + "$relation_ref" ], + "additionalProperties": false, "properties": { - "name": { - "type": "string", - "description": "Parameter name as it appears in the query string" - }, - "required": { - "type": "boolean", - "description": "Whether this parameter is required" - }, - "description": { - "type": "string", - "description": "Human-readable description of the parameter" + "$relation_ref": { + "oneOf": [ + { + "type": "array", + "description": "Shorthand for `_set`.", + "items": { + "$ref": "#/components/schemas/DirectRelationRefItem" + } + }, + { + "type": "object", + "description": "Exactly one of `_set`, `_append`, `_append_all` must be given.", + "additionalProperties": false, + "properties": { + "_set": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DirectRelationRefItem" + } + }, + "_append": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DirectRelationRefItem" + } + }, + "_append_all": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DirectRelationRefItem" + } + } + } + } + ] } } }, - "FileProxyStep": { + "DirectRelationRefItem": { "type": "object", + "description": "One relation-ref target - the entity to look up, and the repeatable item to reference on it.", "required": [ - "url", - "method", - "response_type" + "schema", + "unique_ids", + "path", + "value" ], + "additionalProperties": false, "properties": { - "url": { - "type": "string", - "description": "Handlebars template for the request URL" - }, - "method": { + "schema": { "type": "string", - "enum": [ - "GET", - "POST" - ], - "description": "HTTP method" + "minLength": 1, + "description": "Entity schema slug of the target entity.", + "example": "contact" }, - "headers": { + "unique_ids": { "type": "object", + "minProperties": 1, "additionalProperties": { - "type": "string" + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ] }, - "description": "Handlebars templates for request headers" - }, - "body": { - "type": "string", - "description": "Handlebars template for the request body (POST only)" - }, - "response_type": { - "type": "string", - "enum": [ - "json", - "binary" - ], - "description": "Expected response type" - } - } - }, - "FileProxyResponseConfig": { - "type": "object", - "required": [ - "body", - "encoding" - ], - "properties": { - "body": { - "type": "string", - "description": "JSONata expression to extract file content from step results" + "description": "Attribute values that uniquely identify the target entity." }, - "encoding": { - "type": "string", - "enum": [ - "base64", - "binary" - ], - "description": "Encoding of the extracted body" + "unique_id_types": { + "type": "object", + "additionalProperties": { + "type": "string", + "enum": [ + "email", + "phone" + ] + }, + "description": "Optional per-field type hints for repeatable unique-id fields." }, - "filename": { + "path": { "type": "string", - "description": "JSONata expression to extract the filename" + "minLength": 1, + "description": "The attribute on the TARGET entity that holds the repeatable array (e.g. `address`)." }, - "content_type": { - "type": "string", - "description": "JSONata expression to extract the content type" + "value": { + "description": "The item to match (or create) at `path` - e.g. an address object.", + "example": { + "street": "Main Street", + "city": "Berlin" + } } } }, - "MappingSimulationRequest": { + "DirectSimulationRequest": { "type": "object", + "description": "Request for a direct-mode dry run: the `direct: true` use case configuration to test\nagainst, and the payload to validate and translate.\n", "required": [ - "mapping_configuration", - "object_type", - "format", + "event_configuration", "payload" ], "properties": { - "mapping_configuration": { - "oneOf": [ - { - "$ref": "#/components/schemas/IntegrationConfigurationV1" - }, + "event_configuration": { + "allOf": [ { - "$ref": "#/components/schemas/IntegrationConfigurationV2" + "$ref": "#/components/schemas/InboundIntegrationEventConfiguration" } - ] - }, - "object_type": { - "type": "string", - "description": "Type of the object/event being mapped.\nFor v1.0: must match a key in mapping_configuration.mapping.objects\nFor v2.0: must match a key in mapping_configuration.mapping.events\n" - }, - "format": { - "type": "string", - "enum": [ - "json", - "xml" ], - "default": "json", - "description": "Format of the payload data" + "description": "The direct use case configuration to simulate against. `direct` must be `true`;\n`entities` acts as the optional allowlist exactly as at runtime. This is the same\nconfiguration format stored on the inbound use case resource, so a configuration can\nbe tested before saving it.\n" }, "payload": { "oneOf": [ { "type": "string", - "description": "The serialized object data payload (JSON, XML, etc.) as a string" + "description": "The direct payload as a JSON string (as it would be sent in the event's `payload`)." }, { - "type": "object", - "description": "Direct JSON object (will be automatically serialized)", - "additionalProperties": true + "$ref": "#/components/schemas/DirectPayload" } ], - "description": "The object data payload - can be either a serialized string or a direct JSON object" + "description": "The direct payload to validate - a `DirectPayload` object, or its JSON string form." } } }, - "MappingSimulationV2Request": { + "DirectSimulationResponse": { "type": "object", - "description": "Request for v2 mapping simulation. Uses the same configuration format stored in integration use case resources,\nmaking it easier to test configurations before saving them.\n", + "description": "Result of a direct-mode dry run. `valid: false` responses list EVERY violation found\nacross all operations in `errors`. `valid: true` responses preview the internal updates\nthe pipeline would apply, plus non-blocking `warnings`.\n", "required": [ - "event_configuration", - "format", - "payload" + "valid", + "errors" ], "properties": { - "event_configuration": { - "$ref": "#/components/schemas/InboundIntegrationEventConfiguration" + "valid": { + "type": "boolean", + "description": "Whether the payload would be accepted by the ingest endpoint." }, - "format": { - "type": "string", - "enum": [ - "json", - "xml" - ], - "default": "json", - "description": "Format of the payload data" + "errors": { + "type": "array", + "description": "All validation errors found. Empty when `valid` is `true`.", + "items": { + "$ref": "#/components/schemas/DirectSimulationError" + } }, - "payload": { - "oneOf": [ - { - "type": "string", - "description": "The serialized event data payload (JSON, XML, etc.) as a string" - }, - { - "type": "object", - "description": "Direct JSON object (will be automatically serialized)", - "additionalProperties": true - } - ], - "description": "The event data payload - can be either a serialized string or a direct JSON object" - } - } - }, - "MappingSimulationResponse": { - "type": "object", - "required": [ - "entity_updates" - ], - "properties": { - "entity_updates": { + "warnings": { "type": "array", + "description": "Non-blocking findings - e.g. unique-id fields not defined in the target entity\nschema (lookups would never match), or a failed server-side unique-id type\nderivation.\n", "items": { - "$ref": "#/components/schemas/EntityUpdate" + "$ref": "#/components/schemas/MappingSimulationWarning" } }, - "meter_readings_updates": { + "entity_updates": { "type": "array", + "description": "The translated internal entity updates, exactly as the pipeline would process them.\nOnly present when `valid` is `true`.\n", "items": { - "$ref": "#/components/schemas/MeterReadingUpdate" + "allOf": [ + { + "$ref": "#/components/schemas/EntityUpdate" + }, + { + "type": "object", + "properties": { + "unique_identifiers_metadata": { + "type": "object", + "description": "Per-field metadata for repeatable unique-id fields - explicit\n`unique_id_types` merged with server-side schema derivation. Absent when\nno field resolved to a repeatable type.\n", + "additionalProperties": { + "type": "object", + "properties": { + "fieldType": { + "type": "string", + "enum": [ + "email", + "phone" + ] + }, + "index": { + "type": "integer" + } + } + } + } + } + } + ] } }, - "warnings": { + "meter_reading_updates": { "type": "array", - "description": "Validation warnings about the configuration (e.g., unique_ids referencing non-indexed fields)", + "description": "The translated internal meter reading updates. Only present when `valid` is `true`.\n", "items": { - "$ref": "#/components/schemas/MappingSimulationWarning" + "allOf": [ + { + "$ref": "#/components/schemas/MeterReadingUpdate" + }, + { + "type": "object", + "properties": { + "_config": { + "type": "object", + "description": "Per-reading pipeline configuration carried by the operation.", + "properties": { + "reading_matching": { + "type": "string", + "enum": [ + "external_id", + "strict-date" + ] + } + } + } + } + } + ] } } } }, - "MappingSimulationWarning": { + "DirectSimulationError": { "type": "object", + "description": "One validation error found during a direct-mode dry run.", "required": [ - "entity_schema", - "field", + "code", "message" ], "properties": { - "entity_schema": { - "type": "string", - "description": "The entity schema slug where the issue was found" - }, - "field": { + "code": { "type": "string", - "description": "The field name that caused the warning" + "enum": [ + "DIRECT_PAYLOAD_INVALID", + "DIRECT_VERSION_UNSUPPORTED", + "DIRECT_ENTITY_NOT_ALLOWED" + ], + "description": "Error class, matching the monitoring codes the ingest pipeline emits:\n`DIRECT_PAYLOAD_INVALID` (schema violation, JSON parse failure, or XML format),\n`DIRECT_VERSION_UNSUPPORTED` (unknown payload `version`),\n`DIRECT_ENTITY_NOT_ALLOWED` (entity or unique-id keys not permitted by the use\ncase's allowlist).\n" }, "message": { "type": "string", - "description": "Description of the validation issue" + "description": "Human-readable description, including the path of the offending element." + }, + "operation_index": { + "type": "integer", + "description": "Zero-based index of the operation the error refers to, when attributable to one." } } }, @@ -6608,7 +9722,8 @@ "required": [ "entity_slug", "unique_identifiers", - "attributes" + "attributes", + "mode" ], "properties": { "entity_slug": { @@ -6624,6 +9739,38 @@ "type": "object", "description": "Mapped attribute values", "additionalProperties": true + }, + "pricing": { + "type": "object", + "description": "Present when the entity mapping has a `pricing` block. Echoes the pricing configuration and the data extracted for it, so mapping authors can see what would be sent to the pricing service.", + "required": [ + "config", + "data" + ], + "properties": { + "config": { + "type": "object", + "additionalProperties": true, + "description": "The pricing configuration from the entity mapping" + }, + "data": { + "type": "array", + "description": "The pricing input data extracted from the payload", + "items": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "mode": { + "type": "string", + "enum": [ + "upsert", + "delete", + "purge" + ], + "description": "Effective operation mode applied to this entity at runtime. `upsert-prune-scope-purge` / `upsert-prune-scope-delete` configurations report `upsert` here, because the individual entities in the payload are upserted — the destructive part of those modes is reported separately in `prune_scope_updates`. For `delete` / `purge`, `attributes` are still mapped and returned but ignored at runtime: only `unique_identifiers` are used to locate the entity to remove." } } }, @@ -6631,7 +9778,8 @@ "type": "object", "required": [ "meter", - "attributes" + "attributes", + "mode" ], "properties": { "meter": { @@ -6661,6 +9809,121 @@ "type": "object", "additionalProperties": true, "description": "Meter reading attributes. Required: external_id, timestamp, source, value. `timestamp` must be ISO 8601 — either `YYYY-MM-DD` or `YYYY-MM-DDTHH:mm:ss` (with optional fractional seconds and optional `Z` / `±HH:mm` timezone offset); non-ISO formats (e.g. `DD.MM.YYYY` or epoch numbers) are rejected and must be converted upstream via a `jsonataExpression` (e.g. `$fromMillis(...)`). Date-only values are normalized to midnight UTC and offset-less date-times are anchored to UTC before being forwarded to the metering API. `source` must be one of: ECP, ERP, 360, journey-submission. `reason` (optional) must be one of: regular, irregular, last, first, meter_change, contract_change, meter_adjustment (or empty/null)." + }, + "mode": { + "type": "string", + "enum": [ + "upsert", + "delete" + ], + "description": "Effective operation mode applied to this reading at runtime. `upsert-prune-scope` configurations report `upsert` here — their destructive part is reported separately in `meter_readings_prune_scope_updates`." + } + } + }, + "EntityPruneScopeUpdate": { + "type": "object", + "description": "A resolved entity prune scope. At runtime, entities of `entity_slug` found within `scope` whose unique identifiers are not listed in `keep_unique_ids` are removed using `deletion_mode`.", + "required": [ + "entity_slug", + "scope", + "keep_unique_ids", + "deletion_mode" + ], + "properties": { + "entity_slug": { + "type": "string", + "description": "The entity type that would be pruned" + }, + "scope": { + "type": "object", + "description": "The scope configuration resolved against the payload", + "required": [ + "scope_mode" + ], + "properties": { + "scope_mode": { + "type": "string", + "enum": [ + "relations", + "query" + ], + "description": "How the entities in scope are found" + }, + "schema": { + "type": "string", + "description": "Schema of the related entity defining the scope (`relations` scope_mode only)" + }, + "unique_ids": { + "type": "object", + "additionalProperties": true, + "description": "Resolved unique identifiers of the related scope entity (`relations` scope_mode only)" + }, + "query": { + "type": "object", + "additionalProperties": true, + "description": "Resolved query parameters used to find entities in scope (`query` scope_mode only)" + } + } + }, + "keep_unique_ids": { + "type": "array", + "description": "Unique identifiers of the entities upserted by this payload. Everything else found in scope would be deleted.", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "deletion_mode": { + "type": "string", + "enum": [ + "delete", + "purge" + ], + "description": "`delete` soft deletes the pruned entities, `purge` removes them permanently" + } + } + }, + "MeterReadingPruneScopeUpdate": { + "type": "object", + "description": "A resolved meter reading prune scope. At runtime, readings of the given meter (+ counter) whose external ids are not listed in `keep_external_ids` are deleted.", + "required": [ + "meter", + "keep_external_ids" + ], + "properties": { + "meter": { + "type": "object", + "required": [ + "$entity_unique_ids" + ], + "properties": { + "$entity_unique_ids": { + "type": "object", + "additionalProperties": true, + "description": "Unique identifiers for the meter" + } + } + }, + "meter_counter": { + "type": "object", + "properties": { + "$entity_unique_ids": { + "type": "object", + "additionalProperties": true, + "description": "Unique identifiers for the meter counter" + } + } + }, + "keep_external_ids": { + "type": "array", + "description": "External ids of the readings present in this payload. All other readings in scope would be deleted. An empty array means every reading in scope would be deleted.", + "items": { + "type": "string" + } + }, + "source": { + "type": "string", + "description": "When set, only readings with this source are eligible for pruning (e.g. `ERP`)" } } }, @@ -6796,12 +10059,12 @@ "enabled", "delivery" ], - "description": "A mapping that delivers an event to an external system — either pushed to a webhook (with a JSONata payload transformation) or made available on the pull-based poll queue (raw event payload, no transformation)", + "description": "A mapping that delivers an event to an external system by one of three mechanisms — pushed to a webhook (with a JSONata payload transformation), made available on the pull-based poll queue (raw event payload, no transformation), or handed to a file_proxy use case that uploads files to an external document system (a pointer only; the referenced use case owns the payload and the fan-out)", "properties": { "id": { "type": "string", "format": "uuid", - "description": "Unique identifier for this mapping" + "description": "Unique identifier for this mapping; generated by the API when omitted" }, "name": { "type": "string", @@ -6810,7 +10073,7 @@ }, "jsonata_expression": { "type": "string", - "description": "JSONata expression to transform the event payload. Required for webhook delivery; ignored for poll delivery.", + "description": "JSONata expression to transform the event payload. Required for webhook delivery, ignored for poll delivery, and rejected for file_proxy delivery — a file_proxy payload is built by the referenced use case's steps, so accepting an expression here would silently do nothing.", "example": "{ \"id\": entity._id, \"customer\": entity.customer_name }" }, "enabled": { @@ -6834,20 +10097,24 @@ } }, "DeliveryConfig": { - "description": "Configuration for how the event should be delivered. webhook = push delivery via svc-webhooks (JSONata-transformed payload); poll = pull-based queue delivery where the consumer fetches items via the poll API (raw event payload)", + "description": "Configuration for how the event should be delivered. webhook = push delivery via svc-webhooks (JSONata-transformed payload); poll = pull-based queue delivery where the consumer fetches items via the poll API (raw event payload); file_proxy = one push per event attachment to an external document system, through a file_proxy use case (JSONata-transformed payload carrying the file bytes)", "oneOf": [ { "$ref": "#/components/schemas/WebhookDeliveryConfig" }, { "$ref": "#/components/schemas/PollDeliveryConfig" + }, + { + "$ref": "#/components/schemas/FileProxyDeliveryConfig" } ], "discriminator": { "propertyName": "type", "mapping": { "webhook": "#/components/schemas/WebhookDeliveryConfig", - "poll": "#/components/schemas/PollDeliveryConfig" + "poll": "#/components/schemas/PollDeliveryConfig", + "file_proxy": "#/components/schemas/FileProxyDeliveryConfig" } } }, @@ -6908,7 +10175,7 @@ "block" ], "default": "dead_letter", - "description": "What happens when an item exhausts max_delivery_attempts: dead_letter routes the exhausted item to the dead-letter queue; block halts the queue until operator/consumer action. Enforcement lands with the queue consumer (Phase 10) — this field defines the contract." + "description": "What happens when an item exhausts max_delivery_attempts: dead_letter routes the exhausted item to the dead-letter queue and advances past it so the stream keeps flowing; block halts the queue at that item until operator/consumer action removes it." }, "max_delivery_attempts": { "type": "integer", @@ -6919,6 +10186,120 @@ } } }, + "FileProxyDeliveryConfig": { + "type": "object", + "required": [ + "type", + "use_case_slug" + ], + "description": "Push delivery to an external document system through a `file_proxy` use case.\n\nA pure pointer, deliberately. The outbound use case decides WHEN to deliver — its event\nname plus `event_filter` — and the referenced `file_proxy` use case decides WHAT and HOW:\nwhich items to fan out over (`fan_out`), what values to build (each\nstep's `body_jsonata`), and the steps that send them. Keeping every file concern on the\nfile_proxy side is what lets one upload recipe be shared by several event subscriptions\nwithout duplicating any of it.\n\n`jsonata_expression` on the mapping is NOT used for this delivery type and is rejected.\n\nThe slug is resolved at delivery time, not at save time, so use cases can be saved in any\norder; a dangling reference surfaces as a `USE_CASE_NOT_FOUND` monitoring event naming\nthe referencing mapping.\n", + "properties": { + "type": { + "type": "string", + "enum": [ + "file_proxy" + ], + "description": "Delivery mechanism type" + }, + "use_case_slug": { + "type": "string", + "description": "Slug of a `file_proxy` use case with `direction: upload` in the SAME integration.\n", + "example": "wemag_d3_document_upload" + } + } + }, + "EntitySyncStatusResponse": { + "type": "object", + "required": [ + "entity_id", + "sync_states" + ], + "properties": { + "entity_id": { + "type": "string", + "format": "uuid", + "description": "The entity the sync states belong to" + }, + "sync_states": { + "type": "array", + "description": "One entry per integration that has processed an inbound event for the entity", + "items": { + "$ref": "#/components/schemas/EntitySyncState" + } + } + } + }, + "EntitySyncState": { + "type": "object", + "description": "Inbound sync state of one entity for one integration. Carries two\nindependent groups: the entity-level fields (`last_synced_at`, …) and,\non meter/meter_counter entities, the readings-level fields\n(`readings_last_synced_at`, …). Either group can be present alone — a\nmeter whose readings sync via the ERP but whose entity was never\nitself inbound-mapped carries only the readings group.\n", + "required": [ + "entity_id", + "integration_id", + "entity_slug" + ], + "properties": { + "entity_id": { + "type": "string", + "format": "uuid", + "description": "The entity ID" + }, + "integration_id": { + "type": "string", + "format": "uuid", + "description": "The integration that synchronized the entity" + }, + "entity_slug": { + "type": "string", + "description": "The entity schema slug", + "example": "contract" + }, + "use_case_id": { + "type": "string", + "description": "The inbound use case that last processed the entity" + }, + "last_synced_at": { + "type": "string", + "format": "date-time", + "description": "Last time an inbound ERP event for this entity was processed —\nincluding no-op checks that changed nothing on the entity.\n" + }, + "last_changed_at": { + "type": "string", + "format": "date-time", + "description": "Last time an inbound ERP event actually changed the entity (create/patch/delete)" + }, + "last_operation": { + "type": "string", + "enum": [ + "create", + "patch", + "delete", + "no-op" + ], + "description": "Outcome of the most recent inbound sync check" + }, + "last_event_id": { + "type": "string", + "description": "The event ID of the most recent inbound sync check" + }, + "readings_last_synced_at": { + "type": "string", + "format": "date-time", + "description": "Last time meter readings for this meter/meter_counter entity were\nreceived and processed from the ERP. Only present on meter and\nmeter_counter entities. The metering path cannot detect no-ops,\nso there is no separate readings changed-at.\n" + }, + "readings_last_operation": { + "type": "string", + "enum": [ + "upsert", + "delete" + ], + "description": "Outcome of the most recent inbound readings sync" + }, + "readings_last_event_id": { + "type": "string", + "description": "The event ID of the most recent inbound readings sync" + } + } + }, "OutboundStatusResponse": { "type": "object", "required": [ @@ -6960,35 +10341,89 @@ "description": "The Event Catalog event name that triggers this outbound flow", "example": "contract.created" }, - "eventEnabled": { + "eventEnabled": { + "type": "boolean", + "nullable": true, + "description": "Whether the event is enabled in Event Catalog. Null if the API is unreachable." + }, + "webhooks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WebhookStatus" + } + }, + "status": { + "type": "string", + "enum": [ + "ok", + "conflict", + "disabled" + ], + "description": "Overall status of the use case:\n- 'ok': Use case is enabled and all dependencies are properly configured\n- 'conflict': Use case has configuration issues (disabled events/webhooks while enabled)\n- 'disabled': Use case is disabled\n" + }, + "conflicts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OutboundConflict" + }, + "description": "List of detected conflicts, if any" + }, + "poll": { + "$ref": "#/components/schemas/OutboundPollStatus" + }, + "file_proxy": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OutboundFileProxyTargetStatus" + }, + "description": "One entry per file_proxy mapping on this use case. Present only on use cases with a\nfile_proxy delivery mapping.\n" + } + } + }, + "OutboundFileProxyTargetStatus": { + "type": "object", + "description": "Resolution state of one file_proxy mapping's referenced upload use case.\n\nBecause the transport lives on a separate use case, edits to the endpoint or credentials\ndo not appear in THIS use case's history. `target_updated_at` is the hook that lets an\noperator notice a target changed underneath a delivery that started failing.\n", + "required": [ + "mapping_id", + "use_case_slug", + "resolved" + ], + "properties": { + "mapping_id": { + "type": "string", + "description": "The mapping holding this file_proxy delivery" + }, + "use_case_slug": { + "type": "string", + "description": "Slug of the referenced file_proxy use case" + }, + "resolved": { + "type": "boolean", + "description": "Whether the slug currently resolves to an enabled `file_proxy` use case with\n`direction: upload` in this integration. References are resolved at delivery time,\nso `false` is a live warning rather than a save-time error.\n" + }, + "target_use_case_id": { + "type": "string", + "format": "uuid", + "description": "Id of the referenced use case — absent when unresolved" + }, + "target_enabled": { "type": "boolean", - "nullable": true, - "description": "Whether the event is enabled in Event Catalog. Null if the API is unreachable." + "description": "Whether the referenced use case is enabled — absent when unresolved" }, - "webhooks": { - "type": "array", - "items": { - "$ref": "#/components/schemas/WebhookStatus" - } + "target_updated_at": { + "type": "string", + "format": "date-time", + "description": "When the referenced use case was last modified — absent when unresolved. Surfaced\nbecause a change here is invisible in this use case's own history diff.\n" }, - "status": { + "unresolved_reason": { "type": "string", "enum": [ - "ok", - "conflict", + "not_found", + "wrong_type", + "wrong_direction", "disabled" ], - "description": "Overall status of the use case:\n- 'ok': Use case is enabled and all dependencies are properly configured\n- 'conflict': Use case has configuration issues (disabled events/webhooks while enabled)\n- 'disabled': Use case is disabled\n" - }, - "conflicts": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OutboundConflict" - }, - "description": "List of detected conflicts, if any" - }, - "poll": { - "$ref": "#/components/schemas/OutboundPollStatus" + "description": "Why the reference did not resolve — absent when `resolved` is true" } } }, @@ -8417,14 +11852,15 @@ "enum": [ "success", "error", - "skipped", - "warning" + "info", + "warning", + "skipped" ], - "description": "Filter by event level" + "description": "Filter by event level. `skipped` is accepted but can never match: the v2 pipeline normalises it to `info`, and the table's enum has no such member. It is retained here — and ONLY here, not on `MonitoringEventV2` — so that a client with a saved `level=skipped` filter keeps getting an empty 200 rather than a 400 from request validation. Use `info` instead." }, "code": { "type": "string", - "description": "Filter by taxonomy code (e.g. OAUTH2_TOKEN_FAILURE, HTTP_502)" + "description": "Filter by taxonomy code (e.g. OAUTH2_TOKEN_FAILURE, HTTP_502). Accepts any `MonitoringCode` value, or an `HTTP_{status}` family value." }, "event_id": { "type": "string", @@ -8470,6 +11906,246 @@ } } }, + "ExternalMonitoringSpan": { + "type": "object", + "properties": { + "correlation_id": { + "type": "string", + "description": "Trace id — links this span to the epilot trace. Unique per business operation. Required." + }, + "level": { + "type": "string", + "description": "Span outcome level — one of success | error | warning | info. Drives the server-assigned EXTERNAL_* code, coloring, and alerting. Required.\n" + }, + "use_case_slug": { + "type": "string", + "description": "Business use case slug (e.g. \"business_partner\"). Resolved server-side against the integration's configured use cases to a use_case_id/use_case_type; stored as the grouping dimension. Portable — no epilot-internal UUIDs required.\n" + }, + "occurred_at": { + "type": "string", + "format": "date-time", + "description": "External clock time the span occurred; stored as created_at." + }, + "message": { + "type": "string", + "description": "Human-readable line shown in the trace and event tables." + }, + "detail": { + "type": "object", + "additionalProperties": true, + "nullable": true, + "description": "Free-form context (step, http_status, reason, …). The place for external-system specificity — the taxonomy code stays clean (EXTERNAL_*).\n" + } + }, + "description": "A single monitoring span produced by an external system. `correlation_id`, `level`, `use_case_slug`, `occurred_at` and `message` are all required and `level` must be one of success|error|warning|info — but these are validated PER SPAN at ingest and reported in the per-item `results[]`, so one malformed span never fails the whole batch (the fields are intentionally not marked `required`/`enum` at the schema level to preserve that partial-acceptance behavior). The client does NOT send a `code` (server-assigned from `level`), a `source` (origin is the EXTERNAL_* code prefix), or span/event ids (the external system owns its own identity and dedup).\n" + }, + "IngestExternalMonitoringEventsRequest": { + "type": "object", + "required": [ + "events" + ], + "properties": { + "events": { + "type": "array", + "minItems": 1, + "maxItems": 100, + "items": { + "$ref": "#/components/schemas/ExternalMonitoringSpan" + }, + "description": "Batch of external monitoring spans (max 100 per request)." + } + } + }, + "IngestExternalMonitoringEventsResponse": { + "type": "object", + "required": [ + "accepted", + "rejected" + ], + "properties": { + "accepted": { + "type": "integer", + "description": "Number of spans accepted and republished onto the monitoring bus." + }, + "rejected": { + "type": "integer", + "description": "Number of spans rejected in validation. See `results` for reasons." + }, + "results": { + "type": "array", + "description": "Per-span results; present for rejections in a mixed batch.", + "items": { + "type": "object", + "required": [ + "index", + "status" + ], + "properties": { + "index": { + "type": "integer", + "description": "Zero-based index of the span in the request `events` array." + }, + "status": { + "type": "string", + "enum": [ + "accepted", + "rejected" + ] + }, + "reason": { + "type": "string", + "description": "Rejection reason (present when status is rejected)." + } + } + } + } + } + }, + "MonitoringTraceResponse": { + "type": "object", + "required": [ + "correlation_id", + "status", + "span_count", + "truncated", + "spans" + ], + "properties": { + "correlation_id": { + "type": "string", + "description": "The trace id these spans share." + }, + "status": { + "type": "string", + "enum": [ + "success", + "error", + "warning", + "info" + ], + "description": "Rolled-up status across all spans (error > warning > success > info)." + }, + "started_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "created_at of the earliest span (null when the trace is empty)." + }, + "ended_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "created_at of the latest returned span (null when the trace is empty)." + }, + "span_count": { + "type": "integer", + "description": "Number of spans RETURNED (== spans.length; capped)." + }, + "truncated": { + "type": "boolean", + "description": "True when the trace has more spans than the cap; the returned window is the earliest spans, so ended_at/status reflect only that window.\n" + }, + "spans": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MonitoringEventV2" + }, + "description": "All spans sharing the correlation_id, chronological. External-origin spans carry an EXTERNAL_* code; epilot spans carry an epilot code or empty (success).\n" + }, + "inbound_event": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "The epilot inbound event \"head\" (raw payload), when present." + } + } + }, + "MonitoringCode": { + "type": "string", + "description": "The monitoring code taxonomy — every code the Integration Toolkit itself emits,\nwith a fixed level (see the published code reference at\nhttps://docs.epilot.io/docs/integrations/integration-toolkit/monitoring/codes).\n\nThis schema exists so consumers can import the union as a type. It is\ndeliberately NOT applied to `MonitoringEventV2.code` or to the query filter:\nalongside these, the secure proxy emits an unbounded `HTTP_{status}` family for\nan upstream refusal, so constraining those fields to this enum would reject a\nlegitimate value — `code` stays a plain string there on purpose.\n\nKept in lock-step with the `MonitoringErrorCode` taxonomy in erp-utils by\n`monitoring-code-enum.test.ts`, which fails if the two diverge.\n", + "enum": [ + "ACK_CONFIRMED", + "ACK_PENDING", + "ACK_TIMEOUT", + "ATTACHMENT_NOT_FOUND", + "ATTRIBUTE_TYPE_MISMATCH", + "DEPRECATED_ENDPOINT", + "DIRECT_ENTITY_NOT_ALLOWED", + "DIRECT_PAYLOAD_INVALID", + "DIRECT_VERSION_UNSUPPORTED", + "DUPLICATE_EVENT", + "ENTITY_CREATED", + "ENTITY_DELETED", + "ENTITY_NO_OP", + "ENTITY_REFERENCE_NOT_FOUND", + "ENTITY_UPDATED", + "EVENT_NOT_CONFIGURED", + "EXTERNAL_API_ERROR", + "EXTERNAL_ERROR", + "EXTERNAL_INFO", + "EXTERNAL_SUCCESS", + "EXTERNAL_WARNING", + "FAN_OUT_EMPTY", + "FAN_OUT_INVALID_RESULT", + "FILE_EXTRACTION_FAILED", + "FILE_FETCH_FAILED", + "FILE_PROXY_OK", + "FILE_PROXY_UPLOADED", + "FILE_PROXY_UPLOAD_ENQUEUED", + "FILE_PROXY_UPLOAD_FAILED", + "FILE_PROXY_UPLOAD_RETRYING", + "FILE_TOO_LARGE", + "INTEGRATION_NOT_FOUND", + "INVALID_METER_READING_ATTRIBUTES", + "LOOKUP_UNMAPPED", + "MALFORMED_PAYLOAD", + "MAPPING_EXPRESSION_FAILED", + "METERING_API_ERROR", + "METER_READING_DELETED", + "METER_READING_GROUP_FAILED", + "METER_READING_GROUP_RETRYING", + "METER_READING_UPSERTED", + "MISSING_REQUIRED_PARAM", + "MISSING_UNIQUE_IDENTIFIERS", + "MSG_ACKED", + "MSG_DEAD_LETTERED", + "MSG_ENQUEUED", + "MSG_EXPIRED_UNPOLLED", + "MSG_HEAD_BLOCKED", + "OAUTH2_TOKEN_FAILURE", + "PAYLOAD_TOO_LARGE", + "PRUNE_SCOPE_COMPLETED", + "PRUNE_SCOPE_PARTIAL_FAILURE", + "RECURSION_DEPTH_EXCEEDED", + "RELATION_REF_ITEM_NOT_FOUND", + "RELATION_REF_VALUE_UNDEFINED", + "REQUIRED_PARAM_MISSING", + "SECURE_PROXY_DISABLED", + "SECURE_PROXY_DOMAIN_BLOCKED", + "SECURE_PROXY_DOMAIN_NOT_ALLOWED", + "SECURE_PROXY_ERROR", + "SECURE_PROXY_INVALID_CONFIG", + "SECURE_PROXY_INVALID_TYPE", + "SECURE_PROXY_INVALID_URL", + "SECURE_PROXY_IP_BLOCKED", + "SECURE_PROXY_IP_NOT_ALLOWED", + "SECURE_PROXY_NOT_FOUND", + "SECURE_PROXY_UNAVAILABLE", + "SIGNATURE_VERIFICATION_FAILED", + "SIGNATURE_VERIFICATION_UNAVAILABLE", + "SOFT_DELETED_ENTITY_MATCHED", + "STEP_DISABLED", + "TIMEOUT", + "UNIQUE_ID_MULTIPLE_MATCHES", + "UNIQUE_ID_NOT_IN_SCHEMA", + "UNKNOWN_ERROR", + "USE_CASE_DISABLED", + "USE_CASE_INVALID_TYPE", + "USE_CASE_MISSING_CONFIG", + "USE_CASE_NOT_FOUND", + "WEBHOOK_DELIVERED" + ] + }, "MonitoringEventV2": { "type": "object", "required": [ @@ -8524,14 +12200,14 @@ "enum": [ "success", "error", - "skipped", + "info", "warning" ], "description": "Event outcome level" }, "code": { "type": "string", - "description": "Taxonomy code (e.g. OAUTH2_TOKEN_FAILURE, HTTP_502). Empty for success." + "description": "Taxonomy code (e.g. OAUTH2_TOKEN_FAILURE, HTTP_502). Empty for success. One of `MonitoringCode`, or an `HTTP_{status}` family value from the secure proxy — which is why this is a string rather than that enum." }, "message": { "type": "string", @@ -8574,7 +12250,21 @@ "managed_call", "secure_proxy" ], - "description": "Filter stats by use case type" + "description": "Filter stats by a single use case type" + }, + "use_case_types": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "inbound", + "outbound", + "file_proxy", + "managed_call", + "secure_proxy" + ] + }, + "description": "Filter stats to this set of use case types (matches any). Takes precedence over `use_case_type` when both are present. Used by the notification producers to scope alerts/digests to the integration's `monitoredUseCases`." }, "group_by": { "type": "string", @@ -8605,7 +12295,8 @@ "success_count", "error_count", "warning_count", - "skipped_count" + "skipped_count", + "info_count" ], "properties": { "total_events": { @@ -8626,7 +12317,12 @@ }, "skipped_count": { "type": "integer", - "description": "Number of skipped events" + "deprecated": true, + "description": "Always 0. The v2 pipeline has no `skipped` level; v1 `skipped` is normalised to `info`. Retained for backward compatibility — use `info_count`." + }, + "info_count": { + "type": "integer", + "description": "Number of info-level events (ACK lifecycle, poll-queue MSG_*, duplicate events, fan-out anchors). Info events are counted in `total_events` but are excluded from `success_rate`." }, "ack_timeout_count": { "type": "integer", @@ -8712,6 +12408,7 @@ "error_count", "warning_count", "skipped_count", + "info_count", "total_count" ], "properties": { @@ -8744,7 +12441,12 @@ }, "skipped_count": { "type": "integer", - "description": "Number of skipped events in the breakdown item" + "deprecated": true, + "description": "Always 0. The v2 pipeline has no `skipped` level; v1 `skipped` is normalised to `info`. Retained for backward compatibility — use `info_count`." + }, + "info_count": { + "type": "integer", + "description": "Number of info-level events in the breakdown item" }, "total_count": { "type": "integer", @@ -8778,7 +12480,12 @@ }, "skipped_count": { "type": "integer", - "description": "Number of skipped events in the bucket" + "deprecated": true, + "description": "Always 0. The v2 pipeline has no `skipped` level; v1 `skipped` is normalised to `info`. Retained for backward compatibility — use `info_count`." + }, + "info_count": { + "type": "integer", + "description": "Number of info-level events in the bucket" }, "total_count": { "type": "integer", @@ -8792,6 +12499,110 @@ } } } + }, + "DocumentationPageSummary": { + "type": "object", + "description": "Documentation page metadata without the markdown content", + "required": [ + "id", + "integration_id", + "scope", + "title", + "created_at", + "updated_at" + ], + "properties": { + "id": { + "type": "string", + "description": "'general' for the integration-wide page, otherwise the use case ID", + "example": "general" + }, + "integration_id": { + "type": "string", + "format": "uuid", + "readOnly": true + }, + "scope": { + "type": "string", + "enum": [ + "integration", + "use_case" + ], + "description": "Whether the page documents the whole integration or a single use case" + }, + "use_case_id": { + "type": "string", + "format": "uuid", + "description": "The linked use case. Only set when scope is use_case." + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 150 + }, + "created_at": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "created_by": { + "type": "string", + "nullable": true, + "description": "User ID that created the page", + "readOnly": true + }, + "updated_at": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "updated_by": { + "type": "string", + "nullable": true, + "description": "User ID of the last edit", + "readOnly": true + } + } + }, + "DocumentationPage": { + "description": "A markdown documentation page of an integration", + "allOf": [ + { + "$ref": "#/components/schemas/DocumentationPageSummary" + }, + { + "type": "object", + "required": [ + "content" + ], + "properties": { + "content": { + "type": "string", + "maxLength": 100000, + "description": "Markdown source of the page" + } + } + } + ] + }, + "UpsertDocumentationPageRequest": { + "type": "object", + "required": [ + "title", + "content" + ], + "properties": { + "title": { + "type": "string", + "minLength": 1, + "maxLength": 150 + }, + "content": { + "type": "string", + "maxLength": 100000, + "description": "Markdown source of the page" + } + } } }, "responses": { @@ -8835,6 +12646,16 @@ } } }, + "Conflict": { + "description": "Conflict - the resource is not in a state that allows this operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBase" + } + } + } + }, "InternalServerError": { "description": "Internal Server Error", "content": { @@ -9014,7 +12835,7 @@ } }, "ReplayEventsResponse": { - "description": "Events replay initiated", + "description": "Events replay initiated. Always 200 — per-event outcomes are reported in `results`, so inspect it (or compare `replayed` against the number of requested ids) rather than treating the status code as success.\n", "content": { "application/json": { "schema": { @@ -9024,6 +12845,48 @@ "results" ], "properties": { + "replayed": { + "type": "integer", + "description": "Number of events actually queued for re-processing.", + "example": 2 + }, + "results": { + "type": "array", + "description": "One entry per requested event id, in request order.", + "items": { + "type": "object", + "required": [ + "event_id", + "status" + ], + "properties": { + "event_id": { + "type": "string", + "description": "The requested (source) event ID." + }, + "status": { + "type": "string", + "enum": [ + "success", + "queued", + "skipped", + "ignored", + "not_found", + "error" + ], + "description": "Outcome for this event. `success`/`queued` means it was enqueued for re-processing. `not_found` means no inbound event with that ID exists for the organization. `skipped` means it was deduplicated, `ignored` means no enabled use case matched it, and `error` means it could not be queued (see `message`).\n" + }, + "replay_event_id": { + "type": "string", + "description": "The new event ID assigned to the replayed event. Use it to follow the replay in monitoring.\n" + }, + "message": { + "type": "string", + "description": "Human-readable detail for this outcome." + } + } + } + }, "event_ids": { "type": "array", "items": { diff --git a/packages/cli/definitions/journey.json b/packages/cli/definitions/journey.json index 2777259d5..ebaf0b19e 100644 --- a/packages/cli/definitions/journey.json +++ b/packages/cli/definitions/journey.json @@ -3,7 +3,7 @@ "info": { "title": "Journey API", "description": "API to configure journeys", - "version": "1.4.2" + "version": "1.6.0" }, "security": [ { @@ -117,13 +117,23 @@ { "name": "version", "in": "query", - "description": "DynamoDB version to fetch. `0` (default) is the live row; positive integers are historical snapshots created on each save. Note: this is distinct from the `revisions` counter on the row body.\n", + "description": "Revision row to read. `0` (default) is the journey record, the published version every customer receives; a positive integer `n` reads revision row `n` (used by config-engine for history reads). Distinct from the `revisions` counter on the body, which is the legacy change counter of the published version. Versioning metadata is only available via `publish-state`.\n", "required": false, "schema": { "type": "integer", "default": 0 } }, + { + "name": "revision_id", + "in": "query", + "description": "Serve the named revision instead of the published version, so a save-and-continue session can stay on the revision it started on. Only revisions that have been published at least once are served on the public and portal routes; an unpublished revision is served to `EpilotAuth` callers with the edit permission and is otherwise unknown. Cannot be combined with the legacy `version` parameter.\n", + "required": false, + "schema": { + "type": "string", + "example": "42" + } + }, { "name": "source", "in": "query", @@ -149,7 +159,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Journey" + "allOf": [ + { + "$ref": "#/components/schemas/Journey" + }, + { + "$ref": "#/components/schemas/JourneyActivationGuarantee" + }, + { + "$ref": "#/components/schemas/JourneyServedRevision" + } + ] } } } @@ -200,6 +220,409 @@ } } }, + "/v1/journey/configuration/{id}/revisions": { + "post": { + "operationId": "createJourneyRevision", + "summary": "createJourneyRevision", + "description": "Creates a revision: an immutable copy of the complete journey configuration, identified by `revision_id`. Nothing customers receive changes; the revision only becomes the published version when it is published. The payload is complete and is not merged against the journey record or the previous revision. The entity datasource set of the revision is stored separately through the datasources API under the same `revision_id`.\n\nOnly available for organizations with journey versioning enabled; returns `404` otherwise.\n", + "tags": [ + "Journeys" + ], + "security": [ + { + "EpilotAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Journey ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "509cdffe-424f-457a-95c2-9708c304ce77" + } + } + ], + "requestBody": { + "description": "Complete journey configuration for the revision", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JourneyRevisionRequest" + } + } + } + }, + "responses": { + "201": { + "description": "Revision created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JourneyRevisionSummary" + } + } + } + }, + "400": { + "description": "Invalid payload" + }, + "403": { + "description": "No permission to edit the journey" + }, + "404": { + "description": "Journey versioning not enabled for the organization, or journey not found" + }, + "409": { + "description": "`parent_revision_id` is not the latest revision: the journey was saved since the editor loaded it. The body carries the current latest revision.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JourneyRevisionConflict" + } + } + } + } + } + }, + "get": { + "operationId": "listJourneyRevisions", + "summary": "listJourneyRevisions", + "description": "Lists the journey's revision history, newest first. Metadata only, no configuration payload. `is_published` says whether a revision is the published version right now, which is a different question from `published_at`, which records the last time it was published.\n\nOnly available for organizations with journey versioning enabled; returns `404` otherwise.\n", + "tags": [ + "Journeys" + ], + "security": [ + { + "EpilotAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Journey ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "509cdffe-424f-457a-95c2-9708c304ce77" + } + }, + { + "name": "limit", + "in": "query", + "description": "Maximum number of revisions to return", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 25 + } + }, + { + "name": "cursor", + "in": "query", + "description": "Opaque pagination cursor, taken from a previous response's `next_cursor`", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Revision history", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JourneyRevisionList" + } + } + } + }, + "400": { + "description": "Invalid query parameters" + }, + "403": { + "description": "No permission to edit the journey" + }, + "404": { + "description": "Journey versioning not enabled for the organization, or journey not found" + } + } + } + }, + "/v1/journey/configuration/{id}/revisions/{revision_id}": { + "get": { + "operationId": "getJourneyRevision", + "summary": "getJourneyRevision", + "description": "Returns one revision with its full configuration, in the shape the builder holds a journey in, so it can be loaded straight into the editor as unsaved changes. Reading a revision changes nothing; a client persists it, if at all, by posting it back to `POST .../revisions`.\n\nOnly available for organizations with journey versioning enabled; returns `404` otherwise.\n", + "tags": [ + "Journeys" + ], + "security": [ + { + "EpilotAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Journey ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "509cdffe-424f-457a-95c2-9708c304ce77" + } + }, + { + "name": "revision_id", + "in": "path", + "description": "Revision ID, as returned in a revision summary", + "required": true, + "schema": { + "type": "string", + "example": "42" + } + } + ], + "responses": { + "200": { + "description": "The revision with its configuration", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JourneyRevision" + } + } + } + }, + "403": { + "description": "No permission to edit the journey" + }, + "404": { + "description": "Journey versioning not enabled for the organization, or journey or revision not found" + } + } + } + }, + "/v1/journey/configuration/{id}/publish": { + "post": { + "operationId": "publishJourneyRevision", + "summary": "publishJourneyRevision", + "description": "Makes one revision the published version, the one customers receive, in a single transaction: the revision's configuration is copied onto the journey record, its datasource set onto the published datasources, and `name`, `description`, `published_at` and `published_by` are stamped onto the revision. Any revision can be published, old or new, so publishing an older revision is the rollback.\n\n`name` is generated server-side when omitted, so every published revision carries one; an omitted name never replaces a name a previous publish already set. `settings.isActive` is never changed by a publish.\n\nOnly available for organizations with journey versioning enabled; returns `404` otherwise.\n", + "tags": [ + "Journeys" + ], + "security": [ + { + "EpilotAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Journey ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "509cdffe-424f-457a-95c2-9708c304ce77" + } + } + ], + "requestBody": { + "description": "The revision to publish", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublishRevisionRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Revision published", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublishResult" + } + } + } + }, + "400": { + "description": "Invalid payload" + }, + "403": { + "description": "No permission to edit the journey" + }, + "404": { + "description": "Journey versioning not enabled for the organization, or journey or revision not found" + }, + "409": { + "description": "The journey record changed between read and publish, please try again" + } + } + } + }, + "/v1/journey/configuration/{id}/publish-state": { + "get": { + "operationId": "getJourneyPublishState", + "summary": "getJourneyPublishState", + "description": "Answers which revision is the published version without paging through the history. A journey that has not adopted versioning yet returns `200` with `published_revision_id: null`, not `404`.\n\nOnly available for organizations with journey versioning enabled; returns `404` otherwise.\n", + "tags": [ + "Journeys" + ], + "security": [ + { + "EpilotAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Journey ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "509cdffe-424f-457a-95c2-9708c304ce77" + } + } + ], + "responses": { + "200": { + "description": "Publish state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JourneyPublishState" + } + } + } + }, + "403": { + "description": "No permission to edit the journey" + }, + "404": { + "description": "Journey versioning not enabled for the organization, or journey not found" + } + } + } + }, + "/v1/journey/configuration/{id}/environment": { + "get": { + "operationId": "getJourneyEnvironment", + "summary": "getJourneyEnvironment", + "description": "Resolve the environment variables referenced by this journey. Only browser-safe value types are returned.", + "security": [ + { + "EpilotAuth": [] + } + ], + "tags": [ + "Journeys" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Journey ID bound to the supplied journey access token", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Resolved environment-backed datasources", + "headers": { + "Cache-Control": { + "schema": { + "type": "string" + }, + "description": "Always private, no-store" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JourneyEnvironmentResponse" + } + } + } + }, + "401": { + "description": "Journey access token missing" + }, + "403": { + "description": "Journey access token does not match the journey" + }, + "502": { + "description": "Environment resolver unavailable" + } + } + } + }, + "/v1/journey/environment-variables": { + "get": { + "operationId": "getJourneyEnvironmentVariables", + "summary": "getJourneyEnvironmentVariables", + "description": "List the organization's environment variables that a journey block may use as an options source. Only Map variables that currently hold a valid value are returned. Intended for the journey builder's authoring UI; requires a journey authoring token, not an environments token.", + "security": [ + { + "EpilotAuth": [] + } + ], + "tags": [ + "Journeys" + ], + "responses": { + "200": { + "description": "Map environment variables available as an options source", + "headers": { + "Cache-Control": { + "schema": { + "type": "string" + }, + "description": "Always private, no-store" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JourneyEnvironmentVariablesResponse" + } + } + } + }, + "401": { + "description": "Authorization token missing" + }, + "403": { + "description": "Not permitted to read this organization's journeys" + }, + "502": { + "description": "Environment resolver unavailable" + } + } + } + }, "/v1/journey/products/{id}": { "get": { "operationId": "getJourneyProducts", @@ -722,7 +1145,7 @@ { "name": "version", "in": "query", - "description": "DynamoDB version to fetch. `0` (default) is the live row; positive integers are historical snapshots created on each save. Note: this is distinct from the `revisions` counter on the row body.\n", + "description": "Revision row to read. `0` (default) is the journey record, the published version every customer receives; a positive integer `n` reads revision row `n` (used by config-engine for history reads). Distinct from the `revisions` counter on the body, which is the legacy change counter of the published version. Versioning metadata is only available via `publish-state`.\n", "required": false, "schema": { "type": "integer", @@ -736,7 +1159,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/JourneyCreationRequestV2" + "allOf": [ + { + "$ref": "#/components/schemas/JourneyCreationRequestV2" + }, + { + "$ref": "#/components/schemas/JourneyActivationGuarantee" + } + ] } } } @@ -927,17 +1357,248 @@ "type": "string", "example": "Unknown API Error" } - } + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "EnvironmentMapEntry": { + "type": "object", + "required": [ + "key", + "value" + ], + "properties": { + "key": { + "type": "string" + }, + "value": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + ] + } + } + }, + "EnvironmentMap": { + "type": "object", + "required": [ + "options" + ], + "properties": { + "fallbackLanguage": { + "type": "string", + "default": "de" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EnvironmentMapEntry" + } + } + } + }, + "EnvironmentLink": { + "description": "One link a customer clicks. `label` and `description` are each either a\nplain string or one string per language, deciding that independently.\nMirrors environments-api's `LinkFields`.\n", + "type": "object", + "required": [ + "url", + "label" + ], + "properties": { + "url": { + "type": "string" + }, + "label": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + ] + }, + "description": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + ] + } + } + }, + "EnvironmentLinkList": { + "description": "A list of links, served under this service's own `List` type\ntoken. environments-api types the variable `List` and declares the\nelement type inside the value; journey-api composes the two so a\nconsumer needs only one discriminant.\n", + "type": "object", + "required": [ + "itemType", + "items" + ], + "properties": { + "itemType": { + "type": "string", + "enum": [ + "Link" + ] + }, + "fallbackLanguage": { + "type": "string", + "default": "de" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EnvironmentLink" + } + } + } + }, + "JourneyEnvironmentResponse": { + "type": "object", + "required": [ + "items", + "errors" + ], + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "required": [ + "datasourceId", + "type", + "value" + ], + "properties": { + "datasourceId": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "Text", + "Number", + "Boolean", + "Map", + "List" + ] + }, + "value": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "$ref": "#/components/schemas/EnvironmentMap" + }, + { + "$ref": "#/components/schemas/EnvironmentLinkList" + } + ] + } + } + } + }, + "errors": { + "type": "array", + "items": { + "type": "object", + "required": [ + "datasourceId", + "code" + ], + "properties": { + "datasourceId": { + "type": "string" + }, + "code": { + "type": "string", + "enum": [ + "not_found", + "unsupported_type", + "not_set", + "invalid_value", + "incompatible_consumer" + ] + } + } + } + } + } + }, + "JourneyEnvironmentVariablesResponse": { + "type": "object", + "required": [ + "items" + ], + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "required": [ + "key", + "type", + "value" + ], + "properties": { + "key": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "Map", + "List" + ] + }, + "value": { + "oneOf": [ + { + "$ref": "#/components/schemas/EnvironmentMap" + }, + { + "$ref": "#/components/schemas/EnvironmentLinkList" + } + ] + }, + "description": { + "type": "string" } } } } } - } - } - }, - "components": { - "schemas": { + }, "GetJourneysResponse": { "type": "object" }, @@ -1064,6 +1725,13 @@ "type": "boolean", "nullable": true }, + "stepperType": { + "type": "string", + "enum": [ + "numbers", + "progress bar" + ] + }, "hideNextButton": { "type": "boolean", "nullable": true @@ -1296,6 +1964,12 @@ "shouldLoadEntity": { "type": "boolean", "description": "If type is not text, we can instruct the journey to fetch the entity id we receive as value" + }, + "description": { + "type": "string", + "nullable": true, + "maxLength": 500, + "description": "Human-readable note describing the parameter's purpose. Free text; may contain newlines." } }, "required": [ @@ -1336,11 +2010,7 @@ }, "lang": { "type": "string", - "enum": [ - "de", - "en", - "fr" - ] + "example": "de" }, "width": { "type": "string" @@ -1382,7 +2052,8 @@ "type": "string" }, "templateId": { - "type": "string" + "type": "string", + "nullable": true }, "entityId": { "type": "string", @@ -1391,6 +2062,10 @@ "mappingsAutomationId": { "type": "string" }, + "newMappings": { + "description": "When true, the journey is created without a mapping config or automation; mappings are managed as advanced mappings on a lazily created automation.", + "type": "boolean" + }, "targetedCustomer": { "type": "string" }, @@ -1481,6 +2156,19 @@ "PRIVATE" ] }, + "authGate": { + "type": "object", + "description": "Steps after this step require an authenticated session (auth gate)", + "required": [ + "stepId" + ], + "properties": { + "stepId": { + "type": "string", + "description": "The step containing the Login & Registration block" + } + } + }, "isPublished": { "type": "boolean" }, @@ -1493,7 +2181,7 @@ "savingProgress": { "type": "object", "properties": { - "savingMode": { + "mode": { "type": "string", "enum": [ "auto", @@ -1502,7 +2190,7 @@ "none" ] }, - "supportedVersion": { + "supportedRevision": { "type": "number" } } @@ -1591,6 +2279,13 @@ "type": "boolean", "nullable": true }, + "stepperType": { + "type": "string", + "enum": [ + "numbers", + "progress bar" + ] + }, "hideNextButton": { "type": "boolean", "nullable": true @@ -1823,6 +2518,12 @@ "shouldLoadEntity": { "type": "boolean", "description": "If type is not text, we can instruct the journey to fetch the entity id we receive as value" + }, + "description": { + "type": "string", + "nullable": true, + "maxLength": 500, + "description": "Human-readable note describing the parameter's purpose. Free text; may contain newlines." } }, "required": [ @@ -1863,11 +2564,7 @@ }, "lang": { "type": "string", - "enum": [ - "de", - "en", - "fr" - ] + "example": "de" }, "width": { "type": "string" @@ -1910,8 +2607,13 @@ "mappingsAutomationId": { "type": "string" }, + "newMappings": { + "description": "When true, the journey is created without a mapping config or automation; mappings are managed as advanced mappings on a lazily created automation.", + "type": "boolean" + }, "templateId": { - "type": "string" + "type": "string", + "nullable": true }, "targetedCustomer": { "type": "string" @@ -1992,12 +2694,46 @@ "PRIVATE" ] }, + "authGate": { + "type": "object", + "description": "Steps after this step require an authenticated session (auth gate)", + "required": [ + "stepId" + ], + "properties": { + "stepId": { + "type": "string", + "description": "The step containing the Login & Registration block" + } + } + }, "enableDarkMode": { "type": "boolean", "description": "If true, the journey shows an icon to toggle dark mode" }, + "useAustrianLabels": { + "type": "boolean", + "description": "If true, some journey input labels are in Austrian format" + }, "isActive": { "type": "boolean" + }, + "savingProgress": { + "type": "object", + "properties": { + "mode": { + "type": "string", + "enum": [ + "auto", + "local", + "remote", + "none" + ] + }, + "supportedRevision": { + "type": "number" + } + } } }, "additionalProperties": false @@ -2169,6 +2905,37 @@ } ] }, + "JourneyServedRevision": { + "description": "Which revision the runtime read served: the requested one, or the published version when no revision was requested. Absent while the journey has not adopted versioning and on legacy `version` reads.\n", + "type": "object", + "properties": { + "revision_id": { + "type": "string", + "example": "42" + } + } + }, + "JourneyActivationGuarantee": { + "description": "Journeys read from the API always carry an explicit `settings.isActive`. The flag remains optional in request bodies.\n", + "type": "object", + "required": [ + "settings" + ], + "properties": { + "settings": { + "type": "object", + "required": [ + "isActive" + ], + "properties": { + "isActive": { + "type": "boolean", + "description": "Whether the journey is active (accessible to end customers)" + } + } + } + } + }, "JourneyFeatureFlags": { "type": "object", "properties": { @@ -2191,10 +2958,12 @@ "type": "string" }, "version": { - "type": "number" + "type": "number", + "description": "Revision row number of this item. `0` is the journey record, the published version.\n" }, "revisions": { - "type": "number" + "type": "number", + "description": "Legacy change counter of the published version, kept for compatibility: bumped whenever the published version changes and used for optimistic concurrency on `PUT`, by submissions (`journey_context.journey_revision`) and by save-and-continue. Saving a revision never changes it. Versioning metadata is only available via `publish-state`.\n" } }, "required": [ @@ -2204,6 +2973,224 @@ "revisions" ] }, + "JourneyRevisionRequest": { + "description": "The complete journey configuration a revision is created from, same shape as the `PUT` body. Server-managed attributes of the journey record (`revisions`, `version`, publish metadata) are ignored.\n", + "allOf": [ + { + "$ref": "#/components/schemas/JourneyCreationRequest" + }, + { + "type": "object", + "properties": { + "parent_revision_id": { + "type": "string", + "description": "The revision the editor started from. When present and not the latest revision the save is rejected with `409`; absent skips the check.\n", + "example": "41" + }, + "based_on_revision_id": { + "type": "string", + "description": "Provenance, not configuration: the revision that was loaded into the editor before this save, when it was an older one.\n", + "example": "37" + }, + "mapping_config_version": { + "type": "number", + "description": "Version of the entity-mapping config stored alongside this revision. Publishing the revision points the journey's automation at it.\n", + "example": 12 + }, + "revision_name": { + "type": "string", + "description": "What this save changed, for the revision history to list. Distinct from `name`, which is the journey's own name. A publish replaces it with the version name.\n", + "example": "Changes to Steps and Logic" + } + } + } + ] + }, + "PublishRevisionRequest": { + "type": "object", + "required": [ + "revision_id" + ], + "properties": { + "revision_id": { + "type": "string", + "example": "42" + }, + "name": { + "type": "string", + "maxLength": 255, + "description": "Name stamped onto the revision being published. Optional: when omitted the server generates one, so every revision that has ever been published carries a name. An omitted name never replaces a name a previous publish already set; an explicit one does.\n", + "example": "Summer campaign" + }, + "description": { + "type": "string", + "maxLength": 8000, + "description": "Optional description stamped onto the revision at publish time" + } + } + }, + "JourneyRevisionSummary": { + "type": "object", + "required": [ + "revision_id", + "created_at", + "is_published" + ], + "properties": { + "revision_id": { + "type": "string", + "description": "Opaque identifier of the revision. Moves on every save.", + "example": "42" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "created_by": { + "type": "string", + "description": "User id of the author. Absent for revisions recorded from writes without a user, e.g. blueprint installs.\n" + }, + "name": { + "type": "string", + "description": "Set at publish time only, so present if and only if this revision has been published at least once.\n", + "example": "Summer campaign" + }, + "description": { + "type": "string" + }, + "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 the published version yesterday still carries a `published_at`.\n" + }, + "is_published": { + "type": "boolean", + "description": "Whether this revision is the published version right now." + }, + "mapping_config_version": { + "type": "number", + "description": "Version of the entity-mapping config stored alongside this revision.", + "example": 12 + } + } + }, + "JourneyRevisionList": { + "type": "object", + "required": [ + "results" + ], + "properties": { + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/JourneyRevisionSummary" + } + }, + "next_cursor": { + "type": "string", + "description": "Opaque cursor to pass back as `cursor` to fetch the next page. Absent when the client has reached the end of the history.\n" + } + } + }, + "JourneyRevision": { + "allOf": [ + { + "$ref": "#/components/schemas/JourneyRevisionSummary" + }, + { + "type": "object", + "required": [ + "configuration" + ], + "properties": { + "configuration": { + "description": "The journey configuration as it was saved in this revision, in the shape the builder holds a journey in.\n", + "allOf": [ + { + "$ref": "#/components/schemas/Journey" + } + ] + } + } + } + ] + }, + "JourneyRevisionConflict": { + "type": "object", + "required": [ + "message", + "latest_revision" + ], + "properties": { + "message": { + "type": "string", + "example": "The journey has been saved since your editor loaded it" + }, + "latest_revision": { + "$ref": "#/components/schemas/JourneyRevisionSummary" + } + } + }, + "JourneyPublishState": { + "type": "object", + "required": [ + "journey_id", + "published_revision_id" + ], + "properties": { + "journey_id": { + "type": "string", + "format": "uuid" + }, + "published_revision_id": { + "type": "string", + "nullable": true, + "description": "An explicit `null` means the journey has not adopted versioning yet: it exists, customers receive its journey record, and no revision is the published version.\n", + "example": "42" + }, + "published_at": { + "type": "string", + "format": "date-time" + }, + "published_by": { + "type": "string" + } + } + }, + "PublishResult": { + "type": "object", + "required": [ + "revision_id", + "name", + "published_at", + "post_publish_warnings" + ], + "properties": { + "revision_id": { + "type": "string", + "example": "42" + }, + "name": { + "type": "string", + "description": "The name now on the revision: the one sent in the request, the one a previous publish set when this request omitted `name`, or the server-generated default.\n", + "example": "Summer campaign" + }, + "published_at": { + "type": "string", + "format": "date-time" + }, + "published_by": { + "type": "string" + }, + "post_publish_warnings": { + "type": "array", + "description": "Names any best-effort side effect that failed after the publish itself succeeded, e.g. the journey entity sync, so a caller can say \"published, but the entity did not update\".\n", + "items": { + "type": "string" + } + } + } + }, "JourneyValidationResponse": { "type": "object", "properties": { @@ -2534,6 +3521,11 @@ } } }, + "context_entity_id": { + "description": "Entity id used to resolve entity and relational template variables", + "type": "string", + "format": "uuid" + }, "language": { "description": "Language code for the document", "type": "string", @@ -2583,13 +3575,22 @@ }, "ValidationRuleRef": { "type": "object", - "description": "References to validation rules organized by blocks and fields.\nMaps block IDs to either rule IDs (for block-level rules) or rule references (for field-level rules).\n", + "description": "References to validation rules organized by blocks and fields.\nMaps block IDs to either one or more ordered rule IDs (for block-level rules)\nor rule references (for field-level rules).\n", "additionalProperties": { "oneOf": [ { "type": "string", "description": "Rule ID for block-level validation" }, + { + "type": "array", + "description": "Ordered rule IDs for block-level validation", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string" + } + }, { "$ref": "#/components/schemas/RuleRef" } @@ -2597,23 +3598,45 @@ }, "example": { "block1": "rule123", - "block2": { - "field1": "rule456", - "field2": "rule789" + "block2": [ + "rule456", + "rule789" + ], + "block3": { + "field1": "rule101", + "field2": [ + "rule102", + "rule103" + ] } } }, "RuleRef": { "type": "object", - "description": "Field-level rule references within a block.\nMaps field names to rule IDs.\n", + "description": "Field-level rule references within a block.\nMaps field names to one or more ordered rule IDs.\n", "additionalProperties": { - "type": "string", - "description": "Rule ID for field-level validation" + "oneOf": [ + { + "type": "string", + "description": "Rule ID for field-level validation" + }, + { + "type": "array", + "description": "Ordered rule IDs for field-level validation", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string" + } + } + ] }, "example": { "firstName": "rule123", - "lastName": "rule456", - "email": "rule789" + "lastName": [ + "rule456", + "rule789" + ] } } }, diff --git a/packages/cli/definitions/message.json b/packages/cli/definitions/message.json index 20797c379..ff4361631 100644 --- a/packages/cli/definitions/message.json +++ b/packages/cli/definitions/message.json @@ -123,6 +123,34 @@ }, "403": { "description": "Forbidden" + }, + "409": { + "description": "Conflict - the draft was edited by someone else since it was loaded", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Human-readable conflict message", + "example": "This draft was edited by someone else. Please reload and try again." + }, + "current_message": { + "description": "The current server version of the message (includes updated_by / _updated_at)", + "allOf": [ + { + "$ref": "#/components/schemas/BaseEntity" + }, + { + "$ref": "#/components/schemas/Message" + } + ] + } + } + } + } + } } } } @@ -553,6 +581,44 @@ } } }, + "/v1/message/unread:counts": { + "post": { + "operationId": "getUnreadCounts", + "summary": "getUnreadCounts", + "description": "Unread counts for several named scopes in one request.\n\nA scope is a name plus the same parameters the thread list already takes (`q`, `inbox_id`),\nso a scope's count and the list beneath it are the same predicate and agree by construction.\nThe server adds only the read-state condition; it does not re-author the caller's view.\n\nThe `organization` scope is the exception and takes no `q`: it reuses the four canonical\ncentral-inbox queries, so its numbers match `getUnread` exactly.\n\nWhich buckets come back varies by scope type and actor. Every scope other than `organization`\nreturns `unread` alone. An `organization` scope returns all four (`unread`, `drafts`,\n`unassigned`, `spam`) for `actor: organization`, and `unread` and `drafts` only for\n`actor: user`. `getUnread` returns all four for both actors; this endpoint omits the two that\nno per-user surface renders, since each costs an aggregation.\n\nGated on the `message-unread-counts` flag, evaluated once per request against the calling\norg. With the flag off the response is `{ \"enabled\": false, \"counts\": {} }` and no\nElasticsearch query is issued.\n", + "tags": [ + "Messages" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnreadCountsPayload" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnreadCountsResult" + } + } + } + }, + "400": { + "description": "The request names more scopes than the cap allows, repeats a scope name, or omits both\n`q` and `view` on a scope type that needs a predicate. Over-cap requests are refused rather than truncated:\na silently dropped scope renders as a missing badge, which is indistinguishable from\nzero unread.\n" + }, + "403": { + "description": "Forbidden" + } + } + } + }, "/v2/message/messages/{id}/unread": { "post": { "operationId": "markUnreadMessageV2", @@ -762,6 +828,41 @@ } } }, + "/v2/message/threads:workload": { + "post": { + "operationId": "getAssigneeWorkload", + "summary": "getAssigneeWorkload", + "description": "Return the open-thread workload for a set of user ids.\n\nFor each requested user id, returns the number of *open* threads assigned\ndirectly to that user \u2014 matching what the user sees in their central-inbox\nopen view: in inbox, not trashed, not done, and excluding notification-only\nand spam threads.\n\nOnly threads assigned directly to a user are counted; threads assigned to a\ngroup the user belongs to are not. Intended for assignment load-balancing\n(e.g. even-distribution automations) that need a consistent, inbox-aligned\nworkload per user.\n", + "tags": [ + "Threads" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssigneeWorkloadParams" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssigneeWorkloadResults" + } + } + } + }, + "403": { + "description": "Forbidden" + } + } + } + }, "/v1/message/threads": { "put": { "operationId": "updateThread", @@ -2242,6 +2343,11 @@ "description": "User ID of user sends the message.", "example": "206801" }, + "updated_by": { + "type": "string", + "description": "User ID of the user who last updated (edited) the message. Set for drafts on each save.", + "example": "206801" + }, "subject": { "type": "string", "description": "Subject", @@ -2466,6 +2572,11 @@ "type": "boolean", "description": "If true then this attachment is sent via link. The link have to be inserted to email body by API caller. In this case, service doesn't process this attachment.\n", "default": false + }, + "save_to_entity": { + "type": "boolean", + "description": "If false, this file relation is kept on the message (e.g. for CID/inline rendering) but is NOT propagated to the entity the email is sent from. Used for inline images such as logos, signatures and social icons. Defaults to true.\n", + "default": true } } }, @@ -2481,6 +2592,11 @@ "description": "Whether the thread is marked as Done immediately after sending the message", "default": false }, + "mark_thread_as_read": { + "type": "boolean", + "description": "Whether to mark the thread as read by the sender's org/user after sending a reply.\nOnly applies when `parent_id` is set. When false, the thread's `org_read_message` and\n`user_read_message` arrays are left unchanged, so the thread remains unread for anyone\nwho had it unread before. Defaults to true for backward compatibility.\n", + "default": true + }, "thread": { "type": "object", "required": [ @@ -2563,131 +2679,498 @@ "type": "string", "description": "Template ID used for sending message.", "example": "3f34ce73-089c-4d45-a5ee-c161234e41c3" + }, + "source": { + "type": "string", + "description": "Identifies who triggered the send so server-side policy can branch on it.\nFor `manual` sends, the caller's curated `_tags` are trusted as-is and no\nserver-side enrichment from the referenced `template_id` (e.g. merging the\ntemplate's `email_tags`) is performed. Any non-manual value (or omitting the\nfield) opts into template-derived enrichment.\n", + "enum": [ + "manual", + "automation", + "workflow", + "partner", + "bulk", + "system" + ], + "example": "manual" } } }, - "SearchParamsV2": { + "ThreadView": { "type": "object", - "required": [ - "q" - ], + "additionalProperties": true, + "description": "A central-inbox view, described structurally so the server compiles the query for it. Both the\nthread list and the unread count for a view are compiled from the same description, so the two\ncannot disagree about what the view means.\n\nEvery field is optional and an omitted field adds no condition, so a view narrows the whole\nmailbox rather than being a template with required holes. Unknown fields are ignored.\n", "properties": { - "inbox_id": { - "oneOf": [ - { - "type": "string", - "description": "Inbox ID", - "example": "3f34ce73-089c-4d45-a5ee-c161234e41c3" - }, - { - "type": "array", - "items": { - "type": "string" - }, - "description": "Inbox IDs", - "example": [ - "3f34ce73-089c-4d45-a5ee-c161234e41c3", - "3f34ce73-089c-4d45-a5ee-c161234e41c4" - ] - } - ] + "folder": { + "type": "string", + "enum": [ + "inbox", + "favorite", + "sent", + "trash", + "spam", + "unassignable", + "draft" + ], + "description": "Which sidebar folder's membership predicate to apply." }, - "q": { - "description": "Lucene query syntax supported with ElasticSearch", + "mailbox": { "type": "string", - "example": "subject:\"Request for solar panel price\" AND _tags:INBOX" + "enum": [ + "organization", + "agent" + ], + "description": "Whose mailbox this is. `agent` scopes to threads assigned to the caller or their groups;\n`organization` scopes to the org and is the only mailbox that carries address filtering.\n" }, - "fields": { - "$ref": "#/components/schemas/FieldsParam" + "labels": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Saved-filter labels, ANDed. Matched exactly against the tag rather than against its\ntokens, so a label whose words overlap a folder tag no longer lands in that folder.\n" + }, + "purposes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Purpose ids on linked entities, ANDed." + }, + "filters": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "unread", + "resolved", + "trash" + ] + }, + "description": "State filters, independent of the folder. `resolved` and `trash` reach the Inbox folder\nonly and are mutually exclusive there; `unread` applies anywhere.\n" }, "from": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Sender addresses to filter on." + }, + "to": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Recipient addresses to filter on." + }, + "assigned_to": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Assignee user ids." + }, + "include_unassigned": { + "type": "boolean", + "description": "Whether threads with no assignee join the `assigned_to` set. A separate flag rather than a\nsentinel entry in that list, so `assigned_to` holds user ids and nothing else.\n" + }, + "date_from_days_ago": { "type": "integer", "minimum": 0, - "default": 0 + "description": "Lower bound of the date range, in days before now. Omitted means the epoch." }, - "size": { + "date_to_days_ago": { "type": "integer", - "minimum": 1, - "default": 10 + "minimum": 0, + "description": "Upper bound of the date range, in days before now. Omitted means now." }, - "hydrate": { - "type": "boolean", - "default": false + "email_filter": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Addresses the user selected in the address filter. Absent and empty differ, and the\ndifference is a real UI state: absent is \"not filtering by address\", empty is \"every\naddress deselected\", which matches nothing.\n\nDistinct from the permission restriction, which the server derives and a caller cannot\nauthor.\n" }, - "include_scores": { - "type": "boolean", - "default": false + "text": { + "type": "string", + "description": "The user's search string, as typed. Expanded across the searched fields server-side and\nnever interpreted as query syntax, so a typed operator or a stray bracket cannot\nre-associate the predicate around it.\n" }, - "sort": { - "type": "string" + "thread_ids": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Restrict the view to these threads. Exists because \"would this thread appear in the view\nthe user is looking at?\" is a real question the inbox asks when a new thread arrives, and\nit is a membership test against the view rather than a different view.\n" }, - "highlight": {} + "pinned_by": { + "type": "string", + "description": "Restrict the view to threads this user pinned. The pinned strip above the list is the same\nview with this one extra condition.\n" + } } }, - "SearchParams": { + "UnreadCountScope": { "type": "object", "required": [ - "q" + "name", + "type" ], "properties": { - "q": { - "description": "Lucene query syntax supported with ElasticSearch", + "name": { "type": "string", - "example": "subject:\"Request for solar panel price\" AND _tags:INBOX" - }, - "from": { - "type": "integer", - "minimum": 0, - "default": 0 + "description": "Caller-chosen key for this scope. Echoed back as the key in `counts`, so it is how the\ncaller matches a number to the sidebar row it belongs to. Must be unique within the\nrequest; duplicates are refused rather than silently collapsed.\n", + "example": "inbox-support" }, - "size": { - "type": "integer", - "minimum": 1, - "default": 10 + "type": { + "type": "string", + "enum": [ + "organization", + "shared_inbox", + "saved_view" + ], + "description": "Decides which buckets come back, and whether `q` is required. `organization` returns all\nfour buckets from the canonical central-inbox queries and takes no `q`. `shared_inbox`\nand `saved_view` return `unread` alone. A `saved_view` scope names its view with `view_id`;\na `shared_inbox` scope needs only its `inbox_id`, since the query follows from the type.\n\nA `shared_inbox` scope additionally requires `actor: organization` and is refused with a\n400 otherwise. A shared inbox is an organization-level construct \u2014 selecting one always\nswitches the mailbox to the organization \u2014 so it has no per-user read state and the\ncombination would compute a number no surface renders. `saved_view` accepts either actor,\nbecause a view's own configuration names its mailbox.\n" }, - "hydrate": { - "type": "boolean", - "default": false - } - } - }, - "SearchIDParams": { - "type": "object", - "properties": { "q": { - "description": "Lucene query syntax supported with ElasticSearch", "type": "string", - "example": "subject:\"Request for solar panel price\" AND _tags:INBOX" + "description": "The scope's query, in Lucene syntax, as passed to `threads:search` for the same scope. The\nserver adds the read-state condition and nothing else, so the count matches that list.\n\nAccepted for `shared_inbox` and `saved_view`; rejected for `organization`.\n\nSuperseded by server-side compilation. It remains accepted for callers whose counts are\nenabled while compilation is not, and is ignored when compilation is enabled. It will be\nremoved once compilation is enabled everywhere counts are.\n", + "example": "_tags.keyword:inbox AND !_tags.keyword:trash" + }, + "view_id": { + "type": "string", + "description": "The id of the saved view this scope counts. The server reads that view and compiles the same\nquery the thread list runs for it, so the count and the list cannot describe the view\ndifferently.\n\nRequired for `saved_view` scopes unless `q` is supplied instead; rejected for the other two\ntypes. A `shared_inbox` scope needs no predicate field at all, because its query follows from\nthe type and its `inbox_id`. An `organization` scope uses the canonical folder queries.\n\nThe view's own shared-inbox filter is read from the stored view, so `inbox_id` need not be\nsent alongside this.\n\nCompilation is enabled per organization by the `message-unread-unified-predicate` feature\nflag. While it is off, a scope supplying only a `view_id` has no query to run and its name is\nreturned in `omitted` rather than counted. A named view that this organization does not have,\nor whose stored configuration cannot be read, is omitted the same way.\n", + "example": "3f34ce73-089c-4d45-a5ee-c161234e41c3" + }, + "inbox_id": { + "description": "Shared inbox ids, resolved to bucket ids the same way `threads:search` resolves them.", + "oneOf": [ + { + "type": "string", + "example": "3f34ce73-089c-4d45-a5ee-c161234e41c3" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] } } }, - "ReadMessagePayload": { + "UnreadCountsPayload": { "type": "object", "required": [ + "actor", "scopes" ], "properties": { - "scopes": { - "description": "The scopes to be used when marking an item as read or unread. The read status will be synced for all provided scopes.", - "example": [ + "actor": { + "type": "string", + "enum": [ "organization", "user" ], + "description": "Which read state to count against \u2014 the org's or the calling user's. Same meaning as\n`getUnread`'s path parameter, and unrelated to a scope's `type`.\n" + }, + "email_filter": { "type": "array", - "default": [ - "organization", - "user" - ], "items": { - "$ref": "#/components/schemas/ReadingScope" + "type": "string" + }, + "description": "Restrict every scope to messages involving these addresses." + }, + "user_groups": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The caller's group ids, as `group_`, with the same meaning and constraints as on\n`threads:search`. Read only when a scope carries a `view`. Entries not matching\n`group_` are dropped. Not an authorization input.\n" + }, + "scopes": { + "type": "array", + "minItems": 1, + "maxItems": 25, + "items": { + "$ref": "#/components/schemas/UnreadCountScope" } } } }, - "ReadingScope": { - "description": "Who is marking an item as read or unread.", - "type": "string", - "enum": [ + "UnreadCountsResult": { + "type": "object", + "required": [ + "enabled", + "counts" + ], + "properties": { + "enabled": { + "type": "boolean", + "description": "False when the `message-unread-counts` flag is off for the calling org, in which case\n`counts` is empty and no Elasticsearch work was done. Callers render no badges rather\nthan rendering zeroes.\n", + "example": true + }, + "counts": { + "type": "object", + "description": "One entry per scope, keyed by the scope's `name`. A scope whose predicate could not be\nbuilt is **absent** rather than zero, because a zero badge is a claim about the mailbox\nand an absent one is a claim about the request.\n", + "additionalProperties": { + "$ref": "#/components/schemas/UnreadCountBuckets" + } + }, + "omitted": { + "type": "array", + "description": "Names of scopes that were accepted but could not be counted. Each appears here and is\nabsent from `counts`, so an omission is distinguishable from a mis-spelled scope name.\n\nCauses, not distinguishable from this field: a `shared_inbox` scope whose ids matched no\nbucket in the organization; a scope with no query to run because compilation is disabled for\nthe organization and no `q` was supplied; and a `view_id` naming a view this organization\ndoes not have or whose stored configuration cannot be read.\n", + "items": { + "type": "string" + } + } + } + }, + "UnreadCountBuckets": { + "type": "object", + "required": [ + "unread" + ], + "properties": { + "unread": { + "type": "number", + "example": 14 + }, + "drafts": { + "type": "number", + "description": "Organization scope only.", + "example": 12 + }, + "unassigned": { + "type": "number", + "description": "Organization scope only.", + "example": 1 + }, + "spam": { + "type": "number", + "description": "Organization scope only.", + "example": 3 + } + } + }, + "SearchParamsV2": { + "type": "object", + "properties": { + "inbox_id": { + "oneOf": [ + { + "type": "string", + "description": "Inbox ID", + "example": "3f34ce73-089c-4d45-a5ee-c161234e41c3" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "description": "Inbox IDs", + "example": [ + "3f34ce73-089c-4d45-a5ee-c161234e41c3", + "3f34ce73-089c-4d45-a5ee-c161234e41c4" + ] + } + ] + }, + "q": { + "description": "Lucene query syntax supported with ElasticSearch.\n\nSend this or `view`, not both. At least one is required; a request with neither is refused\nwith a 400. An empty string is accepted and returns no hits.\n", + "type": "string", + "example": "subject:\"Request for solar panel price\" AND _tags:INBOX" + }, + "view": { + "allOf": [ + { + "$ref": "#/components/schemas/ThreadView" + } + ], + "description": "A view for the server to compile into the query, instead of supplying `q`. When a view is\npresent and compilation is enabled for the calling organization, the compiled query runs and\n`q` is not consulted.\n\nRead by `threads:search` and `threads:searchIds` only. This schema is shared with\n`messages:search`, which compiles no view and ignores the field, so a request there must\nsupply `q`.\n\nCompilation is enabled per organization by the `message-unread-unified-predicate` feature\nflag. While it is off, `q` runs and a request supplying only a view returns no hits.\n" + }, + "user_groups": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The caller's group ids, as `group_`. Read only when a `view` is compiled, where they\ndetermine the agent mailbox's assignee condition and which shared inboxes, and therefore\nwhich addresses, are reachable. Required for those conditions to be correct, because group\nmembership is not present on the id token this service parses.\n\nEntries not matching `group_` are dropped.\n\nNot an authorization input, and not treated as one: naming groups the caller is not in\nwidens what the response includes, exactly as supplying a broader `q` does. Access control\nis enforced elsewhere.\n" + }, + "fields": { + "$ref": "#/components/schemas/FieldsParam" + }, + "from": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "size": { + "type": "integer", + "minimum": 1, + "default": 10 + }, + "hydrate": { + "type": "boolean", + "default": false + }, + "include_scores": { + "type": "boolean", + "default": false + }, + "sort": { + "type": "string" + }, + "highlight": {} + } + }, + "SearchParams": { + "type": "object", + "required": [ + "q" + ], + "properties": { + "q": { + "description": "Lucene query syntax supported with ElasticSearch", + "type": "string", + "example": "subject:\"Request for solar panel price\" AND _tags:INBOX" + }, + "from": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "size": { + "type": "integer", + "minimum": 1, + "default": 10 + }, + "hydrate": { + "type": "boolean", + "default": false + } + } + }, + "SearchIDParams": { + "type": "object", + "properties": { + "view": { + "allOf": [ + { + "$ref": "#/components/schemas/ThreadView" + } + ], + "description": "The view to compile, with the same meaning as on `threads:search`. Present here because\nthis endpoint returns the ordered id set *for that list*: if one compiled its view and the\nother ran an authored `q`, the two would disagree inside a single feature, which is the\ndrift this replaces.\n" + }, + "user_groups": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The caller's group ids, with the same meaning and caveats as on `threads:search`." + }, + "inbox_id": { + "oneOf": [ + { + "type": "string", + "description": "Inbox ID", + "example": "3f34ce73-089c-4d45-a5ee-c161234e41c3" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "description": "Inbox IDs", + "example": [ + "3f34ce73-089c-4d45-a5ee-c161234e41c3", + "3f34ce73-089c-4d45-a5ee-c161234e41c4" + ] + } + ] + }, + "q": { + "description": "Lucene query syntax supported with ElasticSearch", + "type": "string", + "example": "subject:\"Request for solar panel price\" AND _tags:INBOX" + } + } + }, + "AssigneeWorkloadParams": { + "type": "object", + "required": [ + "user_ids" + ], + "properties": { + "user_ids": { + "type": "array", + "description": "User ids to compute the open-thread workload for. At most 100 per request; larger requests are rejected with 422.", + "minItems": 1, + "items": { + "type": "string" + }, + "example": [ + "11000902", + "11001053" + ] + } + } + }, + "AssigneeWorkload": { + "type": "object", + "required": [ + "user_id", + "open_threads" + ], + "properties": { + "user_id": { + "type": "string", + "description": "The user id.", + "example": "11000902" + }, + "open_threads": { + "type": "number", + "description": "Number of open threads assigned directly to the user.", + "example": 2 + } + } + }, + "AssigneeWorkloadResults": { + "type": "object", + "required": [ + "workloads" + ], + "properties": { + "workloads": { + "type": "array", + "description": "Open-thread workload per requested user id, in request order.", + "items": { + "$ref": "#/components/schemas/AssigneeWorkload" + } + } + } + }, + "ReadMessagePayload": { + "type": "object", + "required": [ + "scopes" + ], + "properties": { + "scopes": { + "description": "The scopes to be used when marking an item as read or unread. The read status will be synced for all provided scopes.", + "example": [ + "organization", + "user" + ], + "type": "array", + "default": [ + "organization", + "user" + ], + "items": { + "$ref": "#/components/schemas/ReadingScope" + } + } + } + }, + "ReadingScope": { + "description": "Who is marking an item as read or unread.", + "type": "string", + "enum": [ "organization", "user" ] @@ -2758,6 +3241,254 @@ } } }, + "WorkflowStartedEvent": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "WORKFLOW_STARTED" + ] + }, + "workflow_id": { + "type": "string", + "description": "ID of the workflow/flow execution that was started" + }, + "workflow_name": { + "type": "string", + "description": "Name of the workflow that was started" + } + } + }, + "ThreadUserAssignedEvent": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "THREAD_USER_ASSIGNED" + ] + }, + "added": { + "type": "array", + "description": "User IDs assigned to the thread", + "items": { + "type": "string" + } + }, + "removed": { + "type": "array", + "description": "User IDs unassigned from the thread", + "items": { + "type": "string" + } + } + } + }, + "MessageLabelAddedEvent": { + "type": "object", + "required": [ + "type", + "label" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "MESSAGE_LABEL_ADDED" + ] + }, + "label": { + "type": "string", + "description": "The label that was added (raw tag slug, e.g. `sentiments:angry`)" + }, + "label_name": { + "type": "string", + "description": "Resolved taxonomy classification display name (e.g. `Ver\u00e4rgert`), when the label is a classification. Absent for free-form tags." + } + } + }, + "MessageLabelRemovedEvent": { + "type": "object", + "required": [ + "type", + "label" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "MESSAGE_LABEL_REMOVED" + ] + }, + "label": { + "type": "string", + "description": "The label that was removed (raw tag slug, e.g. `sentiments:angry`)" + }, + "label_name": { + "type": "string", + "description": "Resolved taxonomy classification display name (e.g. `Ver\u00e4rgert`), when the label is a classification. Absent for free-form tags." + } + } + }, + "MessageEntityLinkedEvent": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "MESSAGE_ENTITY_LINKED" + ] + }, + "entities": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TimelineLinkedEntity" + } + }, + "link_kind": { + "type": "string", + "enum": [ + "manual", + "auto" + ] + } + } + }, + "MessageEntityUnlinkedEvent": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "MESSAGE_ENTITY_UNLINKED" + ] + }, + "entities": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TimelineLinkedEntity" + } + }, + "link_kind": { + "type": "string", + "enum": [ + "manual", + "auto" + ] + } + } + }, + "MessageAutoReplySentEvent": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "MESSAGE_AUTO_REPLY_SENT" + ] + }, + "reply_message_id": { + "type": "string", + "description": "ID of the message that was sent as the automatic reply" + }, + "parent_message_id": { + "type": "string", + "description": "ID of the message the automatic reply responded to" + } + } + }, + "ThreadMovedToInboxEvent": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "THREAD_MOVED_TO_INBOX" + ] + }, + "target_inbox_id": { + "type": "string", + "description": "ID of the shared inbox the thread was moved to" + }, + "target_inbox_name": { + "type": "string", + "description": "Name of the shared inbox the thread was moved to" + } + } + }, + "ThreadTrashedEvent": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "THREAD_TRASHED" + ] + } + } + }, + "ThreadRestoredEvent": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "THREAD_RESTORED" + ] + } + } + }, + "TimelineLinkedEntity": { + "type": "object", + "required": [ + "entity_id" + ], + "properties": { + "entity_id": { + "type": "string" + }, + "schema": { + "type": "string", + "description": "Entity schema slug, e.g. \"opportunity\"" + } + } + }, + "TimelineActor": { + "type": "object", + "properties": { + "user_id": { + "type": "string" + }, + "email": { + "type": "string" + } + } + }, "TimelineEventData": { "type": "object", "discriminator": { @@ -2769,6 +3500,36 @@ }, { "$ref": "#/components/schemas/ThreadOpenEvent" + }, + { + "$ref": "#/components/schemas/WorkflowStartedEvent" + }, + { + "$ref": "#/components/schemas/ThreadUserAssignedEvent" + }, + { + "$ref": "#/components/schemas/MessageLabelAddedEvent" + }, + { + "$ref": "#/components/schemas/MessageLabelRemovedEvent" + }, + { + "$ref": "#/components/schemas/MessageEntityLinkedEvent" + }, + { + "$ref": "#/components/schemas/MessageEntityUnlinkedEvent" + }, + { + "$ref": "#/components/schemas/MessageAutoReplySentEvent" + }, + { + "$ref": "#/components/schemas/ThreadMovedToInboxEvent" + }, + { + "$ref": "#/components/schemas/ThreadTrashedEvent" + }, + { + "$ref": "#/components/schemas/ThreadRestoredEvent" } ] }, @@ -2779,13 +3540,47 @@ "data" ], "properties": { + "id": { + "type": "string", + "description": "Activity id (ActivityItem._id), for deep-linking to the item in the activity feed" + }, "data": { "$ref": "#/components/schemas/TimelineEventData" }, "timestamp": { "type": "string", "description": "Timestamp of the event", - "example": "2024-01-01T00:00:00Z" + "example": "2024-01-01T00:00:00.000Z" + }, + "message_id": { + "type": "string", + "description": "For message-level events, the message the activity is anchored to" + }, + "source": { + "type": "string", + "enum": [ + "user", + "automation", + "system" + ] + }, + "automated": { + "type": "boolean", + "description": "Whether the activity was performed automatically (automation/system)" + }, + "actor": { + "$ref": "#/components/schemas/TimelineActor" + }, + "automation": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + } } } }, diff --git a/packages/cli/definitions/metering.json b/packages/cli/definitions/metering.json index 11c8bdbbe..c3853fe2e 100644 --- a/packages/cli/definitions/metering.json +++ b/packages/cli/definitions/metering.json @@ -2,7 +2,7 @@ "openapi": "3.0.3", "info": { "title": "Metering API", - "version": "1.0.0", + "version": "1.2.0", "description": "The Metering API manages smart meter data, meter counters, and meter readings for epilot customers and administrators.\n\nIt supports two audiences:\n- **ECP (End Customer Portal)**: Portal users can view their meters, counters, and submit readings via the customer portal.\n- **ECP Admin**: Internal epilot users and ERP integrations can create, update, and bulk-manage meter readings.\n\nKey capabilities:\n- Retrieve meters and counters associated with a customer or contract\n- Submit individual or bulk meter readings (with optional validation skip)\n- Batch upsert/delete readings using the v2 endpoint\n- Query historical readings by date interval with cumulative or relative consumption modes\n- Retrieve allowed reading ranges to guide end customers entering readings\n" }, "tags": [ @@ -554,6 +554,9 @@ }, { "$ref": "#/components/parameters/DirectQueryParam" + }, + { + "$ref": "#/components/parameters/CreateTicketQueryParam" } ], "requestBody": { @@ -722,6 +725,9 @@ }, { "$ref": "#/components/parameters/DirectQueryParam" + }, + { + "$ref": "#/components/parameters/CreateTicketQueryParam" } ], "requestBody": { @@ -785,6 +791,101 @@ } } }, + "/v2/metering/readings/prune": { + "post": { + "operationId": "pruneMeterReadings", + "summary": "pruneMeterReadings", + "description": "Deletes every reading of a meter whose `external_id` is NOT in the provided keep list — in a single request.\n\nThe prune scope can optionally be narrowed to a single counter (`counter_id`) and/or a reading `source` (e.g. `ERP`).\nReplaces the client-side pattern of paginating the full reading history and issuing chunked batch deletes.\n\nReadings without an `external_id`:\n- when a `source` filter is provided, they are **deleted** — they cannot be referenced by any keep list\n- when no `source` filter is provided, they are **kept** (conservative default)\n\nDeletions reuse the same internal path as `batchWriteMeterReadings` with `operation: delete`: the same\nper-reading lifecycle events are emitted, and providing `activity_id` suppresses the per-reading delete\nactivities and attaches the given activity to the affected meter and counters instead. Delete operations\nnever create manual-intervention tickets; `create_ticket` is accepted for call-site parity with\n`batchWriteMeterReadings`.\n\n`keep_external_ids` is limited to 10000 entries.\n", + "tags": [ + "ECP Admin" + ], + "security": [ + { + "EpilotAuth": [] + } + ], + "parameters": [ + { + "in": "query", + "name": "async", + "description": "Don't wait for the deletions to become visible in GetReadings API. Useful for large prunes", + "required": false, + "schema": { + "type": "boolean", + "default": true + } + }, + { + "$ref": "#/components/parameters/ActivityIdQueryParam" + }, + { + "$ref": "#/components/parameters/CreateTicketQueryParam" + }, + { + "in": "query", + "name": "dry_run", + "description": "When true, computes and returns the deleted/kept counts without deleting anything.", + "required": false, + "schema": { + "type": "boolean", + "default": false + } + } + ], + "requestBody": { + "description": "Prune scope and keep list.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PruneMeterReadingsPayload" + } + } + } + }, + "responses": { + "200": { + "description": "Meter readings pruned successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "deleted_count": { + "type": "integer", + "description": "Number of readings deleted (or that would be deleted when `dry_run=true`).", + "example": 42 + }, + "kept_count": { + "type": "integer", + "description": "Number of readings in scope that were kept.", + "example": 12 + } + } + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/InvalidRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, "/v1/metering/reading/submission": { "post": { "operationId": "createMeterReadingFromSubmission", @@ -2347,6 +2448,38 @@ "propertyName": "operation" } }, + "PruneMeterReadingsPayload": { + "type": "object", + "required": [ + "meter_id", + "keep_external_ids" + ], + "properties": { + "meter_id": { + "$ref": "#/components/schemas/EntityId", + "description": "The ID of the meter whose readings are pruned" + }, + "counter_id": { + "$ref": "#/components/schemas/EntityId", + "description": "Optionally narrows the prune scope to a single counter of the meter" + }, + "source": { + "$ref": "#/components/schemas/Source", + "description": "Optionally only prune readings with this source (e.g. `ERP`)" + }, + "keep_external_ids": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Readings whose `external_id` is contained in this list are kept; every other reading in scope is deleted.\nAn empty array deletes all readings in scope (subject to the no-external-id rule described on the endpoint).\nLimited to 10000 entries.\n", + "example": [ + "erp-reading-1", + "erp-reading-2" + ] + } + } + }, "UpdateMeterReading": { "type": "object", "required": [ @@ -2969,6 +3102,16 @@ "$ref": "#/components/schemas/ActivityId" } }, + "CreateTicketQueryParam": { + "name": "create_ticket", + "in": "query", + "required": false, + "description": "Controls whether a manual-intervention (\"Übermittlung Zählerstand\") ticket is created for the\nwritten readings. Defaults to true, preserving portal/journey behaviour (readings with\nsource ECP or journey-submission mint a review ticket).\n\nSet to false for authoritative, system-driven writes — e.g. the ERP inbound integration\npipeline — whose readings arrive already confirmed and must not spawn a review ticket. This\nalso prevents the ticket-triggered `ServiceMeterReadingAdded` echo back to the ERP.\n", + "schema": { + "type": "boolean", + "default": true + } + }, "SkipValidationQueryParam": { "name": "skip_validation", "in": "query", @@ -2981,5 +3124,9 @@ } } }, - "servers": [] + "servers": [ + { + "url": "https://metering.sls.epilot.io" + } + ] } diff --git a/packages/cli/definitions/notification.json b/packages/cli/definitions/notification.json index 7180fd924..84e4fb334 100644 --- a/packages/cli/definitions/notification.json +++ b/packages/cli/definitions/notification.json @@ -1013,6 +1013,20 @@ "in_app": false } } + }, + "allowed_channels": { + "description": "Optional delivery-channel ceiling. When present, delivery is restricted to the intersection of this set and each recipient's own notification preferences — it can only NARROW delivery, never force a channel on. Absent means no restriction (recipient preferences and per-type defaults apply as before). An empty array suppresses all channels.", + "type": "array", + "items": { + "type": "string", + "enum": [ + "email", + "in_app" + ] + }, + "example": [ + "in_app" + ] } }, "required": [ diff --git a/packages/cli/definitions/permissions.json b/packages/cli/definitions/permissions.json index 7964337eb..7c42f50a2 100644 --- a/packages/cli/definitions/permissions.json +++ b/packages/cli/definitions/permissions.json @@ -928,15 +928,21 @@ { "$ref": "#/components/schemas/EqualsCondition" }, + { + "$ref": "#/components/schemas/NotEqualsCondition" + }, { "$ref": "#/components/schemas/EqualsCurrentUserCondition" + }, + { + "$ref": "#/components/schemas/NotEqualsCurrentUserCondition" } ] } ] }, "EqualsCurrentUserCondition": { - "description": "Check if any relation_user attribute on the entity contains the current user. When attribute is provided, only that specific attribute path is checked.", + "description": "Check if any relation_user attribute on the entity contains the current user. When attribute is provided, only that specific attribute path is checked. When attributes is provided, it takes precedence over attribute and the condition passes when the current user appears in ANY of the listed attribute paths.", "type": "object", "properties": { "attribute": { @@ -944,6 +950,18 @@ "description": "Optional JSON path to a specific user attribute. When omitted, all relation_user attributes on the entity are scanned.", "example": "assignee.*.user_id" }, + "attributes": { + "type": "array", + "description": "Optional list of JSON paths to user attributes. Takes precedence over attribute.", + "minItems": 1, + "items": { + "type": "string" + }, + "example": [ + "assignee.*.user_id", + "owner.*.user_id" + ] + }, "operation": { "type": "string", "enum": [ @@ -955,6 +973,38 @@ "operation" ] }, + "NotEqualsCurrentUserCondition": { + "description": "Check if the current user is absent from the relation_user attributes on the entity. When attribute is provided, only that specific attribute path is checked. When attributes is provided, it takes precedence over attribute and the condition passes only when the current user appears in NONE of the listed attribute paths.", + "type": "object", + "properties": { + "attribute": { + "type": "string", + "description": "Optional JSON path to a specific user attribute. When omitted, all relation_user attributes on the entity are scanned.", + "example": "assignee.*.user_id" + }, + "attributes": { + "type": "array", + "description": "Optional list of JSON paths to user attributes. Takes precedence over attribute.", + "minItems": 1, + "items": { + "type": "string" + }, + "example": [ + "assignee.*.user_id", + "owner.*.user_id" + ] + }, + "operation": { + "type": "string", + "enum": [ + "not_equals_current_user" + ] + } + }, + "required": [ + "operation" + ] + }, "EqualsCondition": { "description": "Check if attribute equals to any of the values", "type": "object", @@ -971,6 +1021,35 @@ }, "values": { "type": "array", + "minItems": 1, + "items": { + "example": "Qualification" + } + } + }, + "required": [ + "attribute", + "operation", + "values" + ] + }, + "NotEqualsCondition": { + "description": "Passes when the attribute does not equal any of the values. Records where the attribute is missing or empty pass the condition.", + "type": "object", + "properties": { + "attribute": { + "type": "string", + "example": "workflows.primary.task_name" + }, + "operation": { + "type": "string", + "enum": [ + "not_equals" + ] + }, + "values": { + "type": "array", + "minItems": 1, "items": { "example": "Qualification" } @@ -1511,5 +1590,9 @@ } } }, - "servers": [] + "servers": [ + { + "url": "https://permissions.sls.epilot.io" + } + ] } diff --git a/packages/cli/definitions/pricing.json b/packages/cli/definitions/pricing.json index b483273c5..09b19cc31 100644 --- a/packages/cli/definitions/pricing.json +++ b/packages/cli/definitions/pricing.json @@ -2,8 +2,8 @@ "openapi": "3.0.3", "info": { "title": "Pricing API", - "description": "The `pricing-api` hub sets the foundations for the following Pricing APIs:\n\n### Order API\nThis api enables the management of orders in epilot 360, providing features such as:\n - Automatic calculation of totals and price breakdowns for taxes on the Order entity\n - Product and pricing data validation\n\n### Shopping Cart API\nUsed to interact with a cart during a customer's checkout session, providing:\n - An unified data model to model a Shopping Cart\n - Product and pricing data validation\n - Checkout a cart into an order or quote\n\n### Catalog API\nProvides a way to query the entire catalog of products and prices.\n\n### Availability API\nProvides endpoints for querying products availability by a set of predefined dimensions.\n\n### Spot Market API\nProvides endpoints to fetch (historic) spot market price data.\n\n### External Integrations API\nProvides endpoints for external integrations. E.g. GetAG.\n\n### External Catalog API\nProvides endpoints for external catalog.\n\n### Product Recommendations API\nProvides endpoints for product recommendations.\n", - "version": "1.1.0", + "description": "The `pricing-api` hub sets the foundations for the following Pricing APIs:\n\n### Order API\nThis api enables the management of orders in epilot 360, providing features such as:\n - Automatic calculation of totals and price breakdowns for taxes on the Order entity\n - Product and pricing data validation\n\n### Shopping Cart API\nUsed to interact with a cart during a customer's checkout session, providing:\n - An unified data model to model a Shopping Cart\n - Product and pricing data validation\n - Checkout a cart into an order or quote\n\n### Catalog API\nProvides a way to query the entire catalog of products and prices.\n\n### Availability API\nProvides endpoints for querying products availability by a set of predefined dimensions.\n\n### Spot Market API\nProvides endpoints to fetch (historic) spot market price data.\n\n### External Integrations API\nProvides endpoints for external integrations. E.g. GetAG.\n\n### External Catalog API\nProvides endpoints for external catalog.\n\n### Product Recommendations API\nProvides endpoints for product recommendations.\n\n### Conditional Pricing API\nProvides endpoints for resolving conditional Products, Prices and Coupons, and for authoring the conditions they vary by.\n", + "version": "1.2.1", "termsOfService": "https://epilot.cloud/agb", "contact": { "name": "Support", @@ -48,6 +48,10 @@ "name": "Product Recommendations API", "description": "Provides endpoints for product recommendations.\n" }, + { + "name": "Conditional Pricing API", + "description": "Provides endpoints for resolving conditional Products, Prices and Coupons — the variant of an\nentity that applies to a given context — and for authoring the conditions they vary by.\n" + }, { "name": "order_schema", "x-displayName": "Order", @@ -96,7 +100,8 @@ "External Integrations API", "External Catalog API", "Product Recommendations API", - "Promo Codes API" + "Promo Codes API", + "Conditional Pricing API" ] }, { @@ -136,7 +141,7 @@ "/v1/pricing:compute": { "post": { "description": "Computes a set of pricing details that can be persisted on an entity with the pricing capability enabled, e.g: Orders or Contracts.", - "summary": "calculatePricingDetails", + "summary": "$calculatePricingDetails", "operationId": "$calculatePricingDetails", "tags": [ "Order API" @@ -876,7 +881,7 @@ "name": "id", "description": "Order entity ID", "schema": { - "type": "string" + "$ref": "#/components/schemas/EntityId" }, "required": true, "example": "9d4602d3-03be-4d85-86b2-f3c6555fc606" @@ -995,7 +1000,7 @@ "post": { "description": "Checkouts a cart and executes the specified checkout `mode` process.\n\nA Checkout implicitly finalizes the provided cart (if not transient from a fast-checkout) and behaves in one of the following modes:\n- `create_order` (**default**): the payment happens at a later date or managed by 3rd-party CRM (SAP)\n- `create_invoice`: the payment happens on the online checkout (paypal, stripe, adyen)\n- `create_quote`: the checkout represents a price quote request\n\nFast checkout is also supported, by passing the Cart contents directly.\nWhen a fast checkout is performed the cart is considered transient and there is no cart persistance.\n\nIf the checkout `mode` is omitted, the `mode` will default to `create_order`.\n", "operationId": "$checkoutCart", - "summary": "checkoutCart", + "summary": "$checkoutCart", "security": [ {}, { @@ -1064,7 +1069,7 @@ "post": { "description": "Provides a querying functionalities over products and prices of the Catalog for a given organization.", "operationId": "$searchCatalog", - "summary": "searchCatalog", + "summary": "$searchCatalog", "security": [ {}, { @@ -1131,7 +1136,7 @@ "post": { "description": "Provides a querying functionalities over products and prices of the Catalog for a given organization.", "operationId": "$privateSearchCatalog", - "summary": "privateSearchCatalog", + "summary": "$privateSearchCatalog", "tags": [ "Catalog API" ], @@ -1172,7 +1177,7 @@ "/v1/public/validate-promo-codes": { "post": { "description": "Validate a list of promo codes against a list of coupons", - "summary": "validatePromoCodes", + "summary": "$validatePromoCodes", "operationId": "$validatePromoCodes", "tags": [ "Promo Codes API" @@ -1288,7 +1293,7 @@ "post": { "description": "The availability check endpoint", "operationId": "$availabilityCheck", - "summary": "availabilityCheck", + "summary": "$availabilityCheck", "security": [ { "EpilotPublicAuth": [] @@ -1402,7 +1407,7 @@ "get": { "description": "Validates an availability file, it returns an array of errors if the file is invalid", "operationId": "$validateAvailabilityFile", - "summary": "validateAvailabilityFile", + "summary": "$validateAvailabilityFile", "tags": [ "Availability API" ], @@ -1483,7 +1488,7 @@ "get": { "description": "Get a series of historic energy prices for a given time period, market and bidding zone.", "operationId": "$historicMarketPrices", - "summary": "historicMarketPrices", + "summary": "$historicMarketPrices", "tags": [ "Spot Market API" ], @@ -1593,7 +1598,7 @@ "get": { "description": "Get the average energy prices for a given time period, market and bidding zone.", "operationId": "$averageMarketPrice", - "summary": "averageMarketPrice", + "summary": "$averageMarketPrice", "tags": [ "Spot Market API" ], @@ -1712,7 +1717,7 @@ "example": "external-catalog" } ], - "summary": "searchExternalProducts", + "summary": "$searchExternalProducts", "tags": [ "External Integrations API" ], @@ -1874,7 +1879,7 @@ "example": "external-catalog" } ], - "summary": "searchExternalProductRecommendations", + "summary": "$searchExternalProductRecommendations", "tags": [ "External Integrations API" ], @@ -2261,7 +2266,7 @@ "example": "getag" } ], - "summary": "searchProviders", + "summary": "$searchProviders", "tags": [ "External Integrations API" ], @@ -2413,7 +2418,7 @@ "example": "getag" } ], - "summary": "searchStreets", + "summary": "$searchStreets", "tags": [ "External Integrations API" ], @@ -2513,7 +2518,7 @@ "example": "getag" } ], - "summary": "calculatePricingDetails", + "summary": "$computePrice", "tags": [ "External Integrations API" ], @@ -2603,7 +2608,7 @@ "get": { "description": "Gets the credentials for a given integration / organization", "operationId": "$getCredentials", - "summary": "getCredentials", + "summary": "$getCredentials", "tags": [ "External Integrations API" ], @@ -2665,7 +2670,7 @@ "put": { "description": "Saves the credentials for a given integration / organization", "operationId": "$saveCredentials", - "summary": "saveCredentials", + "summary": "$saveCredentials", "tags": [ "External Integrations API" ], @@ -2719,7 +2724,7 @@ "delete": { "description": "Delete the credentials for a given integration / organization", "operationId": "$deleteCredentials", - "summary": "deleteCredentials", + "summary": "$deleteCredentials", "tags": [ "External Integrations API" ], @@ -2762,7 +2767,7 @@ "EpilotPublicAuth": [] } ], - "summary": "getExternalCatalogProducts", + "summary": "$getExternalCatalogProducts", "tags": [ "External Catalog API" ], @@ -2923,7 +2928,7 @@ "EpilotPublicAuth": [] } ], - "summary": "getExternalCatalogProductRecommendations", + "summary": "$getExternalCatalogProductRecommendations", "tags": [ "External Catalog API" ], @@ -3266,7 +3271,7 @@ "post": { "description": "Get a list of product recommendations based on the search parameters.", "operationId": "$productRecommendations", - "summary": "productRecommendations", + "summary": "$productRecommendations", "security": [ { "EpilotPublicAuth": [] @@ -3300,155 +3305,4667 @@ "22c134b7-8dd8-4872-a5c1-8fc6dd4ac2ee" ] } - }, - "Search product recommendations by id with location filter": { - "value": { - "product_recommendation_ids": [ - "22c134b7-8dd8-4872-a5c1-8fc6dd4ac2ee" + }, + "Search product recommendations by id with location filter": { + "value": { + "product_recommendation_ids": [ + "22c134b7-8dd8-4872-a5c1-8fc6dd4ac2ee" + ], + "filters": { + "location": { + "postal_code": "50670", + "city": "Köln" + } + } + } + }, + "Search product recommendations by source product and price": { + "value": { + "catalog_item": { + "product_id": "aa1ebf85-f36e-46ff-978c-ed948c646f12", + "price_id": "4ecf9a61-a974-4594-bae2-bcb798f54d76" + } + } + }, + "Search product recommendations by contract": { + "value": { + "contract_id": "98ffca40-46ab-4288-af10-a478893b2aa2" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "The product recommendations result", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProductRecommendationResponse" + }, + "examples": { + "Product recommendations": { + "value": { + "hits": 1, + "results": [ + { + "_title": "Tarifwechsel 2025", + "_created_at": "2025-01-29T15:46:41.014Z", + "_updated_at": "2025-01-29T15:46:41.014Z", + "name": "Tarifwechsel 2025", + "type": "change", + "source_product": { + "$relation": [ + { + "entity_id": "e2b12b54-d904-42b7-ac44-802d8ebaadf4" + } + ] + }, + "source_price": { + "$relation": [ + { + "entity_id": "255877f3-f9c2-483e-9c71-89088c142bfc" + } + ] + }, + "_schema": "product_recommendation", + "offers": [ + { + "target_id": "f9aa8757-505b-46bd-9310-605d64849565", + "items": [ + { + "product_id": "70ad8a08-46c2-473b-b1c5-8bccfb85e452", + "price_id": "26c9c39d-db4a-4132-af2e-86ab5c7b156c" + }, + { + "product_id": "dd97a2a4-e48b-43c5-8717-7ac563c67e05", + "price_id": "4ecf9a61-a974-4594-bae2-bcb798f54d76" + }, + { + "product_id": "aa1ebf85-f36e-46ff-978c-ed948c646f12", + "price_id": "4ecf9a61-a974-4594-bae2-bcb798f54d76" + } + ] + } + ], + "_id": "6d872912-ef8a-4815-b9db-01744c0828ac", + "_org": "739224" + } + ] + } + } + } + } + } + }, + "400": { + "description": "Invalid payload", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/conditional-pricing/{slug}/condition-sets": { + "get": { + "description": "Returns the condition sets built in for one conditional entity type: the situations a\nconditional Product, Price or Coupon is commonly varied by, ready to be copied into that\nschema's `conditions` array and extended or modified from there.\n\nWhich sets exist depends on the schema — an offer window is a Product's dimension, a delivery\narea is a Price's and a Coupon's — so only the sets built in for `slug` are returned.\n\nStatic, read-only reference data. The catalog is the same for every organization and is not\napplied to any schema by this endpoint — adding conditions to a schema stays an Entity API\nwrite.\n", + "operationId": "$getConditionSets", + "summary": "$getConditionSets", + "tags": [ + "Conditional Pricing API" + ], + "parameters": [ + { + "in": "path", + "name": "slug", + "description": "The conditional entity type whose built-in condition sets to return", + "schema": { + "$ref": "#/components/schemas/ConditionalEntitySlug" + }, + "required": true, + "example": "price" + } + ], + "responses": { + "200": { + "description": "The condition sets built in for this entity type", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionSetCatalog" + } + } + } + }, + "400": { + "description": "Invalid request, e.g. the slug names no conditional entity type", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/conditional-pricing:resolve": { + "post": { + "description": "Resolves which of a conditional entity's variants apply, and returns each one composed: the\nbase entity overlaid with the values of the version in effect at `as_of`.\n\nResolution is two selections in a fixed order — the variant, then the version by `as_of`. It\nis always scoped to one logical entity, so it stays a cheap, predictable lookup rather than\nan open search.\n\n**The variant is selected one of two ways, and the body says which.** A `context` describes a\nsituation and is matched against the conditions each variant pins. A `variant_id` names one\nvariant and skips matching entirely. Exactly one of the two: a body carrying both, or\nneither, is a `400`.\n\nMatching follows two rules worth knowing before assembling a context. A condition a variant\ndoes **not** pin matches any value, which is what lets a condition be added to a schema\nwithout breaking the variants that already exist. A condition **missing from `context`**,\nhowever, does not satisfy one a variant pinned: an incomplete integration resolves to\nnothing rather than silently matching another segment's variants.\n\nWhen nothing matches, the entity's `default` variant is returned if it has one. There is no\nimplicit fallback to the unmodified base entity: an empty `results` means nothing applies to\nthis situation, and the base entity's values are not an answer to it. A pin does not reach\nthat fallback at all: it asks for one variant by name, and answers with it or with a 404.\n\n`options.hydrate` returns the entities a relation attribute references in place of the\nreferences, on either branch.\n\nAvailability is a separate mechanism and is never consulted here.\n", + "operationId": "$resolveConditionalEntity", + "summary": "$resolveConditionalEntity", + "tags": [ + "Conditional Pricing API" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResolveConditionalEntityRequest" + }, + "examples": { + "Pin a variant at a recorded instant": { + "summary": "What an order shows — the numbers the customer agreed to", + "value": { + "schema": "price", + "entity_id": "price-sp26d1yo", + "variant_id": "var-46045", + "as_of": "2026-01-01T00:00:00Z" + } + }, + "Pin a variant as it stands now": { + "summary": "What a contract shows — what is billable today", + "value": { + "schema": "price", + "entity_id": "price-sp26d1yo", + "variant_id": "var-46045" + } + }, + "Match a context, hydrating relations": { + "summary": "A composite price and its components in one round trip", + "value": { + "schema": "price", + "entity_id": "price-composite-9f2", + "context": { + "postal_code": "46045" + }, + "options": { + "hydrate": true + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "The variants that apply, each composed with the version in effect. Empty when nothing\napplies and the entity has no `default` variant. With `resolve_one`, exactly one result;\nwith a pin, exactly one or a 404.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResolvedVariants" + }, + "examples": { + "A hydrated composite price": { + "summary": "`price_components` holds the component entities the variant's override names, fetched after composition", + "value": { + "results": [ + { + "_id": "price-composite-9f2", + "_variant_id": "var-46045", + "_version_valid_from": "2027-01-01T00:00:00.000Z", + "_conditions": { + "postal_code": "46045", + "default": false + }, + "_inert_overrides": [], + "_schema": "price", + "is_composite_price": true, + "price_components": [ + { + "_id": "price-base-fee-46045", + "_schema": "price", + "unit_amount": 1290, + "unit_amount_currency": "EUR" + }, + { + "_id": "price-kwh-46045", + "_schema": "price", + "unit_amount": 32, + "unit_amount_currency": "EUR" + } + ] + } + ] + } + }, + "A variant carrying overrides that did not apply": { + "summary": "`unit_amount_currency` reads as the entity's own value rather than the variant's, and `_inert_overrides` says why each stored override was passed over", + "value": { + "results": [ + { + "_id": "price-sp26d1yo", + "_variant_id": "var-46045", + "_version_valid_from": "2027-01-01T00:00:00.000Z", + "_conditions": { + "postal_code": "46045", + "default": false + }, + "_inert_overrides": [ + { + "attribute": "unit_amount_currency", + "reason": "ATTRIBUTE_NOT_OVERRIDABLE" + }, + { + "attribute": "legacy_surcharge", + "reason": "ATTRIBUTE_UNDECLARED" + } + ], + "_schema": "price", + "unit_amount": 2499, + "unit_amount_currency": "EUR" + } + ] + } + } + } + } + } + }, + "400": { + "description": "The context is not usable against this schema: it names an undefined condition\n(`CONDITION_UNDEFINED`), applies an operator the condition's type does not support\n(`OPERATOR_UNSUPPORTED`), carries a value malformed for its type (`CONTEXT_FORMAT_INVALID`),\nor selects more variants than one response may carry (`TOO_MANY_MATCHES`).\n\nOr the body did not pick a branch: it carries both `context` and `variant_id`, or\nneither, or sends `resolve_one` beside a pin. Those are request-validation failures, so\nthey carry a message and neither `code` nor `details` — testing `code` for absence is how\na client tells one from the four coded failures above.\n\nAlso refused here: an entity id belonging to another type than the slug names\n(`ENTITY_TYPE_MISMATCH`), and an entity that was never created as a conditional one\n(`ENTITY_NOT_CONDITIONAL`).\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + }, + "404": { + "description": "No such schema (`SCHEMA_NOT_FOUND`) or entity (`ENTITY_NOT_FOUND`).\n\nOn the pinned branch: the entity has no such variant, or the variant belongs to another\nentity (`VARIANT_NOT_FOUND`), or it has no version in effect at `as_of` because its first\none is later (`NO_ACTIVE_VERSION`, carrying the instant in `details.as_of`). Context\nmatching drops such a variant from the results instead — a set may lose a member where a\npin naming one cannot answer with silence. It is not `VERSION_NOT_FOUND`: a pin names no\n`valid_from`, and the variant's versions all exist — none is in effect yet.\n\nOn the context branch, with `resolve_one`: nothing applied to the context and the entity\nhas no `default` variant (`NO_MATCHES`). That says the addressing was right and nothing\nserves this situation; without `resolve_one` it is a `200` carrying an empty `results`.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + }, + "409": { + "description": "Several variants apply while a single result was requested (`AMBIGUOUS_RESOLUTION`); the\ncandidates are in `details`.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + }, + "501": { + "description": "A field published ahead of its behaviour was used: `variant_id`, or `options.hydrate` set\nto `true`. Declining it is how a deployed stage says the field exists and does not work\nyet, rather than quietly returning the `default` variant or unhydrated references.\n\nAnswered ahead of every other check, so a body that also names a schema that does not\nexist gets this rather than a `404`.\n\nRead `message`. The body is the shared `Error` shape rather than\n`ConditionalPricingError` — there is no code for \"not built yet\" — as it is on every 501\nthis API answers, so one branch covers all of them.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/conditional-pricing/{slug}/entities/{entity_id}/variants": { + "post": { + "description": "Creates one variant of a conditional entity, together with the first version carrying its\nvalues: a variant always has at least one version.\n\nThe body pins the situation the variant applies to. Pins are exact values only — predicates\nare a read-side concept and are rejected here — and are stored canonicalized for their\ncondition's type, so two spellings of one instant, or one town written two ways, are one\nvariant rather than two that no context can tell apart.\n\nThree write rules are worth knowing before the first call:\n\n- A variant must pin at least one condition or be marked `default`. A variant pinning nothing\n would be a universal wildcard matching every resolve, which is a far more dangerous thing\n than a fallback and far easier to create by accident.\n- `default` is a property of the variant, set by the `default` flag, and is never a value in\n `conditions` — not even `false`. A `default` variant cannot pin anything else, and an entity\n can have only one; a second is refused as `TUPLE_CONFLICT`. Any entity may have one;\n nothing is declared in the schema to allow it.\n- Condition values are immutable afterwards. A variant's identity is the situation it applies\n to, and orders and contracts pin it. **A condition added to a schema that already has\n variants is effectively one-way**: every existing variant is a wildcard on the new\n dimension, but the first variant that pins it is ambiguous against all of them, and\n retro-pinning the others is blocked by this same rule.\n\nAttribute values are applied only for attributes currently carrying `overridable_attribute`.\nMetadata and non-overridable fields present in the body are not applied rather than rejected,\nand every one but the metadata is named in the response's `warnings`, so a client working from\na slightly stale schema snapshot still succeeds and still learns which fields did not land.\nMetadata is never named, since a client echoing back a payload it read carries it in every\nbody.\n\n`variant_id` is always server-generated and returned, and is not accepted in the body — the\nrequest schema admits no such property. It is the durable key orders and contracts pin.\n", + "operationId": "$createConditionalVariant", + "summary": "$createConditionalVariant", + "tags": [ + "Conditional Pricing API" + ], + "parameters": [ + { + "in": "path", + "name": "slug", + "description": "The conditional entity type this variant belongs to", + "schema": { + "$ref": "#/components/schemas/ConditionalEntitySlug" + }, + "required": true, + "example": "price" + }, + { + "in": "path", + "name": "entity_id", + "description": "The conditional entity to add a variant to", + "schema": { + "type": "string" + }, + "required": true, + "example": "price-sp26d1yo" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateVariantRequest" + } + } + } + }, + "responses": { + "201": { + "description": "The variant, as created, together with its first version", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreatedVariant" + }, + "examples": { + "A body naming attributes this variant may not override": { + "summary": "The write succeeded and `values` holds what was stored, so the two attributes the warning names are absent from it", + "value": { + "variant_id": "var-46045", + "entity_id": "price-sp26d1yo", + "schema": "price", + "conditions": { + "postal_code": "46045", + "default": false + }, + "valid_from": "2027-01-01T00:00:00.000Z", + "values": { + "unit_amount": 2499 + }, + "_created_at": "2026-12-01T09:15:00.000Z", + "_updated_at": "2026-12-01T09:15:00.000Z", + "_revision": 1, + "warnings": [ + { + "code": "ATTRIBUTES_NOT_APPLIED", + "message": "The values sent for 2 attributes were not applied: unit_amount_currency, legacy_surcharge", + "details": { + "attributes": [ + { + "attribute": "unit_amount_currency", + "reason": "ATTRIBUTE_NOT_OVERRIDABLE" + }, + { + "attribute": "legacy_surcharge", + "reason": "ATTRIBUTE_UNDECLARED" + } + ] + } + } + ] + } + } + } + } + } + }, + "400": { + "description": "The variant cannot be created as described: it pins nothing and is not the default\n(`VARIANT_UNPINNED`), pins a condition the schema does not declare\n(`CONDITION_UNDEFINED`), pins a `select` value the condition's vocabulary does not admit\n(`CONDITION_VALUE_INVALID`), carries a value malformed for its condition's type\n(`PIN_FORMAT_INVALID`), or the entity already holds every variant it may hold\n(`VARIANT_LIMIT_REACHED`).\n\nSeveral refusals on this response carry no code, and testing `code` for absence is how\nthey are told from the five above: pinning the fallback marker directly under either of\nits names (`default` or `_default`), marking a variant `default` while it also pins a\nreal condition, pinning a condition whose declared type this deploy cannot read, and a\n`valid_from` this store cannot sort by. Each is an integration mistake rather than one\nbad row in a source file.\n\nAlso refused here: an entity id belonging to another type than the slug names\n(`ENTITY_TYPE_MISMATCH`), and an entity that was never created as a conditional one\n(`ENTITY_NOT_CONDITIONAL`).\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + }, + "404": { + "description": "No such schema (`SCHEMA_NOT_FOUND`), or no such entity under it (`ENTITY_NOT_FOUND`).\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + }, + "409": { + "description": "Another variant of this entity already pins this exact combination of condition values\n(`TUPLE_CONFLICT`, naming it in `details.conflicting_variant_id`) — which is also how a\nsecond `default` variant is refused — or the entity's items are being written\nconcurrently (`WRITE_CONFLICT`, retryable).\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + } + } + } + }, + "/v1/conditional-pricing/{slug}/entities/{entity_id}/variants:list": { + "post": { + "description": "Lists a conditional entity's variants and the conditions each one pins — the browse, filter\nand search read behind the Entity UI's variant screens.\n\nA `POST` because the condition filter is a structured object and needs a body; nothing is\nwritten. Every property in that body is optional, so `{}` is the whole of \"the first ten\nvariants, in `variant_id` order\" — but the body itself is required, so send `{}` rather than\nnothing at all.\n\nThree ways to narrow, and they combine. `conditions` filters on the pins themselves and takes\nthe same seven predicates a resolve context does; `search` is free text over pinned values;\n`sort` orders by one pin. **A variant matches the filter only where it pins the condition** —\nthe one place a filter and a resolve context differ: asking for the variants pinning postal\ncode 46045 does not return every variant that pins no postal code at all.\n\nRows report what is *stored*, not what resolves: no version data, no `_revision` and no\n`_inert_overrides`.\n\nPaging is by offset for the first pages and by an opaque `cursor` beyond them. `size` defaults\nto 10 and is clamped at 1000; a `from` past the offset window is refused rather than clamped,\nand the refusal names the cursor to continue with.\n\n**Published ahead of the behaviour.** No handler serves this yet, so a deployed stage answers\n`501`. The examples below are what a consumer builds against in the meantime.\n", + "operationId": "$listConditionalVariants", + "summary": "$listConditionalVariants", + "tags": [ + "Conditional Pricing API" + ], + "parameters": [ + { + "in": "path", + "name": "slug", + "description": "The conditional entity type the variants belong to", + "schema": { + "$ref": "#/components/schemas/ConditionalEntitySlug" + }, + "required": true, + "example": "price" + }, + { + "in": "path", + "name": "entity_id", + "description": "The conditional entity whose variants to list", + "schema": { + "type": "string" + }, + "required": true, + "example": "price-sp26d1yo" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListVariantsRequest" + }, + "examples": { + "The first page": { + "summary": "The smallest valid body — the first ten variants, in `variant_id` order", + "value": {} + }, + "Find the entity's fallback variant": { + "summary": "The variant served when nothing else applies — at most one per entity", + "value": { + "conditions": { + "default": true + } + } + }, + "Filter, search and sort together": { + "summary": "The variants pinning a consumption band in either segment, ordered by postal code", + "value": { + "conditions": { + "segment": { + "in": [ + "private", + "commercial" + ] + }, + "consumption": { + "lt": 5000 + } + }, + "search": "460", + "sort": "conditions.postal_code:asc", + "size": 25 + } + }, + "Continue past the offset window": { + "summary": "What a caller sends instead of the `from` that was refused", + "value": { + "cursor": "eyJmcm9tIjoyNSwibGlzdGluZyI6IjNmOWMxZTJhIn0", + "sort": "conditions.postal_code:asc", + "size": 25 + } + } + } + } + } + }, + "responses": { + "200": { + "description": "The page of matching variants, and how many match in total", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VariantList" + }, + "examples": { + "A page of a postal-code price": { + "summary": "Two of 8,128 matches, with the cursor that continues the listing", + "value": { + "hits": 8128, + "results": [ + { + "variant_id": "var-46045", + "entity_id": "price-sp26d1yo", + "schema": "price", + "conditions": { + "postal_code": "46045", + "default": false + } + }, + { + "variant_id": "var-50667", + "entity_id": "price-sp26d1yo", + "schema": "price", + "conditions": { + "postal_code": "50667", + "default": false + } + } + ], + "next": "eyJmcm9tIjoyNSwibGlzdGluZyI6IjNmOWMxZTJhIn0" + } + } + } + } + } + }, + "400": { + "description": "The listing cannot be served as described.\n\nThe filter is checked against the schema exactly as a resolve context is, by the same\ncode, so it reports the same three codes: a condition the schema does not declare\n(`CONDITION_UNDEFINED`), a predicate the condition's type does not support\n(`OPERATOR_UNSUPPORTED`), or a value malformed for its type (`CONTEXT_FORMAT_INVALID`).\n\nTwo paging refusals carry codes of their own, and the fix for each is a different request:\na `from` plus `size` reaching past the offset window (`OFFSET_WINDOW_EXCEEDED`, naming all\nthree numbers in `details`; page on with the last response's `next` instead), and a\n`cursor` that is malformed or was issued for a different listing (`CURSOR_INVALID`; start\nthe listing again without one).\n\nA `sort` naming something other than a `conditions.` of a sortable type carries a\nmessage and neither `code` nor `details`, as does a body this schema rejects outright.\nTesting `code` for absence is how those are told from the five above.\n\nAlso refused here: an entity id belonging to another type than the slug names\n(`ENTITY_TYPE_MISMATCH`), and an entity that was never created as a conditional one\n(`ENTITY_NOT_CONDITIONAL`).\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + }, + "404": { + "description": "No such schema (`SCHEMA_NOT_FOUND`), or no such entity under it (`ENTITY_NOT_FOUND`).\n\nAn entity that exists and has no variants is a `200` carrying an empty `results` and\n`hits: 0` — having none is an answer, not a missing resource.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + }, + "501": { + "description": "Published ahead of the behaviour. This operation is declared and not yet dispatched, so\nevery request to it is answered here until the listing behaviour lands — which is how a\ndeployed stage says \"this exists and does not work yet\" rather than answering with an\nempty page a client would read as an empty entity.\n\nRead `message`. The body is the shared `Error` shape, as it is on every 501 this API\nanswers, so one \"not built yet\" branch covers all of them.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/conditional-pricing/{slug}/entities/{entity_id}/variants:tree": { + "post": { + "description": "The variants list, each row carrying the version in effect at `as_of` — the Entity UI's main\nediting screen in one call rather than one call per row.\n\nEverything the variants list accepts, filtering, search, sort and paging alike, means the same\nhere. Three differences, all from the version lookup each row costs: the body takes an\n`as_of`, `size` is clamped at 100 rather than 1000, and a variant with no version to show is\nomitted from `results` (see `VariantTree`).\n\nEvery row carries a `version`, and a `status` saying which one it got. A variant always has at\nleast one version, so at any instant either a version is in effect (`active`) or every\nversion of that variant is still ahead of it (`scheduled`) — in which case `version` is that\nupcoming first one, which is what makes a staged variant visible on the screen rather than\nblank.\n\nThe version on a row carries no `_revision`. An editing screen re-reads the one version it is\nabout to write through that version's own `GET`, which is strongly consistent, and writes with\nthe revision it returns.\n\nThe base entity is not part of this response. The screen's standard-price row is an ordinary\nentity read, and a variant's full timeline is the versions list.\n\n**Published ahead of the behaviour.** No handler serves this yet, so a deployed stage answers\n`501`.\n", + "operationId": "$getConditionalVariantTree", + "summary": "$getConditionalVariantTree", + "tags": [ + "Conditional Pricing API" + ], + "parameters": [ + { + "in": "path", + "name": "slug", + "description": "The conditional entity type the variants belong to", + "schema": { + "$ref": "#/components/schemas/ConditionalEntitySlug" + }, + "required": true, + "example": "price" + }, + { + "in": "path", + "name": "entity_id", + "description": "The conditional entity whose variants to list", + "schema": { + "type": "string" + }, + "required": true, + "example": "price-sp26d1yo" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VariantTreeRequest" + }, + "examples": { + "The screen as it opens": { + "summary": "The first page as of now, ordered by postal code", + "value": { + "sort": "conditions.postal_code:asc", + "size": 25 + } + }, + "The screen at a future date": { + "summary": "What the table will look like once next year's versions take effect", + "value": { + "as_of": "2027-03-15T00:00:00Z", + "conditions": { + "postal_code": { + "in": [ + "46045", + "50667" + ] + } + }, + "size": 25 + } + } + } + } + } + }, + "responses": { + "200": { + "description": "The page of matching variants, each with the version its `status` names", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VariantTree" + }, + "examples": { + "One live row and one staged row": { + "summary": "A variant whose version is in effect, beside one whose first version is still ahead of `as_of`", + "value": { + "hits": 8128, + "results": [ + { + "variant_id": "var-46045", + "entity_id": "price-sp26d1yo", + "schema": "price", + "conditions": { + "postal_code": "46045", + "default": false + }, + "status": "active", + "version": { + "variant_id": "var-46045", + "entity_id": "price-sp26d1yo", + "schema": "price", + "conditions": { + "postal_code": "46045", + "default": false + }, + "valid_from": "2026-01-01T00:00:00.000Z", + "values": { + "unit_amount": 3261, + "unit_amount_decimal": "32.61" + }, + "_created_at": "2025-11-14T09:12:44.101Z", + "_updated_at": "2025-11-14T09:12:44.101Z" + } + }, + { + "variant_id": "var-50667", + "entity_id": "price-sp26d1yo", + "schema": "price", + "conditions": { + "postal_code": "50667", + "default": false + }, + "status": "scheduled", + "version": { + "variant_id": "var-50667", + "entity_id": "price-sp26d1yo", + "schema": "price", + "conditions": { + "postal_code": "50667", + "default": false + }, + "valid_from": "2027-01-01T00:00:00.000Z", + "values": { + "unit_amount": 3412, + "unit_amount_decimal": "34.12" + }, + "_created_at": "2026-08-02T16:40:03.882Z", + "_updated_at": "2026-08-02T16:40:03.882Z" + } + } + ], + "next": "eyJmcm9tIjoyNSwibGlzdGluZyI6IjNmOWMxZTJhIn0" + } + } + } + } + } + }, + "400": { + "description": "The listing cannot be served as described — the variants list's `400` word for word, plus\nan `as_of` that is not a timestamp this API can read.\n\nThe filter is checked against the schema exactly as a resolve context is, by the same\ncode, so it reports the same three codes: a condition the schema does not declare\n(`CONDITION_UNDEFINED`), a predicate the condition's type does not support\n(`OPERATOR_UNSUPPORTED`), or a value malformed for its type (`CONTEXT_FORMAT_INVALID`).\n\nTwo paging refusals carry codes of their own, and the fix for each is a different request:\na `from` plus `size` reaching past the offset window (`OFFSET_WINDOW_EXCEEDED`, naming all\nthree numbers in `details`; page on with the last response's `next` instead), and a\n`cursor` that is malformed or was issued for a different listing (`CURSOR_INVALID`; start\nthe listing again without one).\n\nA `sort` naming something other than a `conditions.` of a sortable type carries a\nmessage and neither `code` nor `details`, as does a body this schema rejects outright.\nTesting `code` for absence is how those are told from the five above.\n\nAlso refused here: an entity id belonging to another type than the slug names\n(`ENTITY_TYPE_MISMATCH`), and an entity that was never created as a conditional one\n(`ENTITY_NOT_CONDITIONAL`).\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + }, + "404": { + "description": "No such schema (`SCHEMA_NOT_FOUND`), or no such entity under it (`ENTITY_NOT_FOUND`).\n\nAn entity that exists and has no variants is a `200` carrying an empty `results` and\n`hits: 0` — having none is an answer, not a missing resource.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + }, + "501": { + "description": "Published ahead of the behaviour. This operation is declared and not yet dispatched, so\nevery request to it is answered here until the listing behaviour lands — which is how a\ndeployed stage says \"this exists and does not work yet\" rather than answering with an\nempty page a client would read as an empty entity.\n\nRead `message`. The body is the shared `Error` shape, as it is on every 501 this API\nanswers, so one \"not built yet\" branch covers all of them.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/conditional-pricing/{slug}/entities/{entity_id}/variants/{variant_id}": { + "get": { + "description": "Returns the version of this variant that is currently in effect — the one with the latest\n`valid_from` at or before now.\n\nThe \"open this variant\" read: no date arithmetic is asked of the caller, and what comes back\ncarries the `_revision` a write to that version has to be sent with, so an editing screen can\nload and save without working out which version it is looking at.\n\nWhat is returned is the version's own attribute overrides, not the base entity overlaid with\nthem. Composing the two is what `:resolve` answers.\n\nA variant staged ahead of its launch has versions but none of them in effect, and is reported\nas having none rather than as not existing — the two are fixed differently.\n", + "operationId": "$getActiveConditionalVariantVersion", + "summary": "$getActiveConditionalVariantVersion", + "tags": [ + "Conditional Pricing API" + ], + "parameters": [ + { + "in": "path", + "name": "slug", + "description": "The conditional entity type this variant belongs to", + "schema": { + "$ref": "#/components/schemas/ConditionalEntitySlug" + }, + "required": true, + "example": "price" + }, + { + "in": "path", + "name": "entity_id", + "description": "The conditional entity the variant belongs to", + "schema": { + "type": "string" + }, + "required": true, + "example": "price-sp26d1yo" + }, + { + "in": "path", + "name": "variant_id", + "description": "The variant whose timeline this call addresses", + "schema": { + "type": "string" + }, + "required": true, + "example": "var-46045" + } + ], + "responses": { + "200": { + "description": "The version, as stored", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VariantVersion" + } + } + } + }, + "400": { + "description": "Invalid request, e.g. the slug names no conditional entity type.\n\nAlso refused here: an entity id belonging to another type than the slug names\n(`ENTITY_TYPE_MISMATCH`), and an entity that was never created as a conditional one\n(`ENTITY_NOT_CONDITIONAL`).\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + }, + "404": { + "description": "No entity with that id (`ENTITY_NOT_FOUND`), no such variant under it\n(`VARIANT_NOT_FOUND`), or it has no version in effect at the instant addressed\n(`NO_ACTIVE_VERSION`). A variant whose versions are all still\nscheduled has none in effect, which is reported as such rather than as a missing\nvariant. Selecting the\nversion in effect and reading it are two reads, so a delete landing between them is\nanswered `VERSION_NOT_FOUND`.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + } + } + }, + "put": { + "description": "Replaces the values of the version currently in effect, wholesale.\n\nThe body is the complete set of attribute overrides: an attribute the variant may override and\nthat is absent from it stops being overridden. Attributes the variant may **not** override are\nnot applied where the body carries them, and their stored value is kept rather than dropped.\n\nEditing the version in effect is the ordinary way a live price is corrected, and warns about\nnothing: what changes is what that version *says*, not which version is in effect.\n\nNeither `valid_from` nor `conditions` can be changed here. Both are accepted when they match\nwhat is stored, so a client building its body from the version it loaded need not strip them\nout first, and both are refused when they name something else.\n", + "operationId": "$replaceActiveConditionalVariantVersion", + "summary": "$replaceActiveConditionalVariantVersion", + "tags": [ + "Conditional Pricing API" + ], + "parameters": [ + { + "in": "path", + "name": "slug", + "description": "The conditional entity type this variant belongs to", + "schema": { + "$ref": "#/components/schemas/ConditionalEntitySlug" + }, + "required": true, + "example": "price" + }, + { + "in": "path", + "name": "entity_id", + "description": "The conditional entity the variant belongs to", + "schema": { + "type": "string" + }, + "required": true, + "example": "price-sp26d1yo" + }, + { + "in": "path", + "name": "variant_id", + "description": "The variant whose timeline this call addresses", + "schema": { + "type": "string" + }, + "required": true, + "example": "var-46045" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReplaceVersionRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The version, as the write left it, together with anything the write moved", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WrittenVariantVersion" + } + } + } + }, + "400": { + "description": "The write cannot be applied as described: it would move the version it addresses to another\n`valid_from`, or change the conditions its variant is pinned to — both identity rather than\ncontent, and both fixed at creation. Also when `_revision` is missing or is not a revision\nmarker.\n\nAlso refused here: an entity id belonging to another type than the slug names\n(`ENTITY_TYPE_MISMATCH`), and an entity that was never created as a conditional one\n(`ENTITY_NOT_CONDITIONAL`).\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + }, + "404": { + "description": "No such schema (`SCHEMA_NOT_FOUND`), no entity with that id (`ENTITY_NOT_FOUND`), no such\nvariant under it (`VARIANT_NOT_FOUND`), or the variant has no version in effect at the\ninstant addressed (`NO_ACTIVE_VERSION`). A\nvariant whose versions are all still scheduled has none in effect, which is reported as\nsuch rather than as a missing variant. Selecting the\nversion in effect and reading it are two reads, so a delete landing between them is\nanswered `VERSION_NOT_FOUND`.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + }, + "409": { + "description": "The version has been written since `_revision` was read (`WRITE_CONFLICT`, retryable after\nre-reading the version).\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + } + } + }, + "patch": { + "description": "Changes only the fields it names on the version currently in effect.\n\nEverything the body does not mention is left as stored — the \"just nudge this number\" write. A\n`null` is a value like any other rather than a deletion; a client that wants an attribute to\nstop being overridden sends the complete snapshot without it through `PUT`.\n\nAttempting to change a pinned condition value is refused, as on every version write: a\nvariant's conditions are fixed at creation.\n", + "operationId": "$patchActiveConditionalVariantVersion", + "summary": "$patchActiveConditionalVariantVersion", + "tags": [ + "Conditional Pricing API" + ], + "parameters": [ + { + "in": "path", + "name": "slug", + "description": "The conditional entity type this variant belongs to", + "schema": { + "$ref": "#/components/schemas/ConditionalEntitySlug" + }, + "required": true, + "example": "price" + }, + { + "in": "path", + "name": "entity_id", + "description": "The conditional entity the variant belongs to", + "schema": { + "type": "string" + }, + "required": true, + "example": "price-sp26d1yo" + }, + { + "in": "path", + "name": "variant_id", + "description": "The variant whose timeline this call addresses", + "schema": { + "type": "string" + }, + "required": true, + "example": "var-46045" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PatchVersionRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The version, as the write left it, together with anything the write moved", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WrittenVariantVersion" + } + } + } + }, + "400": { + "description": "The write cannot be applied as described: it would move the version it addresses to another\n`valid_from`, or change the conditions its variant is pinned to — both identity rather than\ncontent, and both fixed at creation. Also when `_revision` is missing or is not a revision\nmarker.\n\nAlso refused here: an entity id belonging to another type than the slug names\n(`ENTITY_TYPE_MISMATCH`), and an entity that was never created as a conditional one\n(`ENTITY_NOT_CONDITIONAL`).\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + }, + "404": { + "description": "No such schema (`SCHEMA_NOT_FOUND`), no entity with that id (`ENTITY_NOT_FOUND`), no such\nvariant under it (`VARIANT_NOT_FOUND`), or the variant has no version in effect at the\ninstant addressed (`NO_ACTIVE_VERSION`). A\nvariant whose versions are all still scheduled has none in effect, which is reported as\nsuch rather than as a missing variant. Selecting the\nversion in effect and reading it are two reads, so a delete landing between them is\nanswered `VERSION_NOT_FOUND`.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + }, + "409": { + "description": "The version has been written since `_revision` was read (`WRITE_CONFLICT`, retryable after\nre-reading the version).\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + } + } + }, + "delete": { + "description": "Removes one variant of a conditional entity: the condition tuple it holds, its registration\nin the search index, and every version it accumulated.\n\nTwo phases. The first frees the tuple and deregisters the variant, and is what makes the\ncombination of condition values immediately reusable — the second removes the version rows in\nbatches afterwards. A response arrives only once both have finished for this request, but the\ntuple is reusable from the moment the first completes, whether or not the second did: a\nvariant with more versions than one transaction can carry is the ordinary case, not an edge\none. An interrupted delete is safe to send again; it picks up where it stopped.\n\nNothing is archived. A variant an order or contract pins stops resolving, and hydration drops\nthe reference leniently rather than failing the read.\n\nThis removes the **variant**, not one of its versions. To remove a single version, name it on\n`…/variants/{variant_id}/versions/{valid_from}` — including the one currently in effect, which\nhas no shorthand delete: a delete names the version it removes.\n", + "operationId": "$deleteConditionalVariant", + "summary": "$deleteConditionalVariant", + "tags": [ + "Conditional Pricing API" + ], + "parameters": [ + { + "in": "path", + "name": "slug", + "description": "The conditional entity type this variant belongs to", + "schema": { + "$ref": "#/components/schemas/ConditionalEntitySlug" + }, + "required": true, + "example": "price" + }, + { + "in": "path", + "name": "entity_id", + "description": "The conditional entity the variant belongs to", + "schema": { + "type": "string" + }, + "required": true, + "example": "price-sp26d1yo" + }, + { + "in": "path", + "name": "variant_id", + "description": "The variant to remove", + "schema": { + "type": "string" + }, + "required": true, + "example": "var-46045" + } + ], + "responses": { + "200": { + "description": "What the delete removed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedVariant" + } + } + } + }, + "400": { + "description": "Invalid request, e.g. the slug names no conditional entity type.\n\nAlso refused here: an entity id belonging to another type than the slug names\n(`ENTITY_TYPE_MISMATCH`), and an entity that was never created as a conditional one\n(`ENTITY_NOT_CONDITIONAL`).\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + }, + "404": { + "description": "No entity with that id (`ENTITY_NOT_FOUND`), or it has no such variant\n(`VARIANT_NOT_FOUND`).\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + }, + "409": { + "description": "The variant's items are being written concurrently (`WRITE_CONFLICT`, retryable).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + } + } + } + }, + "/v1/conditional-pricing/{slug}/entities/{entity_id}/variants/{variant_id}/versions": { + "get": { + "description": "Lists one variant's versions — its whole timeline, oldest first, which is what expanding a row\nof the tree loads.\n\nIts paging differs from the two variant reads: cursor paging only, no `from` and no `size`,\nand **no `hits`**.\n\nTwo paging facts a client gets wrong if it assumes otherwise. **A page may be shorter than\n`limit`, or empty, and still carry a `next`**, so a client pages until `next` is absent rather\nthan until a page looks short. And **a cursor belongs to one variant and one `order`**:\nreplaying one against another variant, or against the opposite order, is a `400` rather than a\nplausible-looking wrong page.\n\nVersions carry no `_revision` here. An editing screen re-reads the one version it is about to\nwrite through that version's own `GET`, which is strongly consistent, and writes with the\nrevision it returns.\n\n**Published ahead of the behaviour.** No handler serves this yet, so a deployed stage answers\n`501`.\n", + "operationId": "$listConditionalVariantVersions", + "summary": "$listConditionalVariantVersions", + "tags": [ + "Conditional Pricing API" + ], + "parameters": [ + { + "in": "path", + "name": "slug", + "description": "The conditional entity type this variant belongs to", + "schema": { + "$ref": "#/components/schemas/ConditionalEntitySlug" + }, + "required": true, + "example": "price" + }, + { + "in": "path", + "name": "entity_id", + "description": "The conditional entity the variant belongs to", + "schema": { + "type": "string" + }, + "required": true, + "example": "price-sp26d1yo" + }, + { + "in": "path", + "name": "variant_id", + "description": "The variant whose timeline to list", + "schema": { + "type": "string" + }, + "required": true, + "example": "var-46045" + }, + { + "in": "query", + "name": "limit", + "description": "Versions per page. Defaults to 100, which is also the maximum; a larger value is clamped\nsilently. A variant's timeline is usually short enough to fit one page.\n", + "schema": { + "type": "integer", + "minimum": 1, + "default": 100 + }, + "required": false, + "example": 100 + }, + { + "in": "query", + "name": "order", + "description": "Which end of the timeline to read from: `asc` oldest first, `desc` newest first. Defaults\nto `asc`.\n\nBaked into every cursor this read issues: a cursor resumes one direction, and replaying it\nagainst the other is a `400`.\n", + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "default": "asc" + }, + "required": false, + "example": "asc" + }, + { + "in": "query", + "name": "cursor", + "description": "Continue from a previous response's `next`. Opaque: it encodes the position and the order\nit was issued for, and nothing a client should read or construct.\n", + "schema": { + "type": "string" + }, + "required": false, + "example": "eyJzayI6IlYjcHJpY2Utc3AyNmQxeW8jdmFyLTQ2MDQ1IzIwMjYtMDEtMDFUMDA6MDA6MDAuMDAwWiIsIm9yZGVyIjoiYXNjIn0" + } + ], + "responses": { + "200": { + "description": "A page of the variant's timeline, and the cursor that continues it", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VariantVersionList" + }, + "examples": { + "A short page that is not the last one": { + "summary": "One version and a `next` — a page shorter than `limit` says nothing about whether the timeline has ended", + "value": { + "results": [ + { + "variant_id": "var-46045", + "entity_id": "price-sp26d1yo", + "schema": "price", + "conditions": { + "postal_code": "46045", + "default": false + }, + "valid_from": "2026-01-01T00:00:00.000Z", + "values": { + "unit_amount": 3261, + "unit_amount_decimal": "32.61" + }, + "_created_at": "2025-11-14T09:12:44.101Z", + "_updated_at": "2025-11-14T09:12:44.101Z" + } + ], + "next": "eyJzayI6IlYjcHJpY2Utc3AyNmQxeW8jdmFyLTQ2MDQ1IzIwMjYtMDEtMDFUMDA6MDA6MDAuMDAwWiIsIm9yZGVyIjoiYXNjIn0" + } + }, + "The last page": { + "summary": "No `next`, which is the only reliable end of the timeline", + "value": { + "results": [ + { + "variant_id": "var-46045", + "entity_id": "price-sp26d1yo", + "schema": "price", + "conditions": { + "postal_code": "46045", + "default": false + }, + "valid_from": "2027-01-01T00:00:00.000Z", + "values": { + "unit_amount": 3412, + "unit_amount_decimal": "34.12" + }, + "_created_at": "2026-08-02T16:40:03.882Z", + "_updated_at": "2026-08-02T16:40:03.882Z" + } + ] + } + } + } + } + } + }, + "400": { + "description": "A `cursor` that is malformed, belongs to another variant, or was issued for the opposite\norder is `CURSOR_INVALID`, with `details.reason` saying which. The fix is the same for all\nthree: read the timeline again without a cursor.\n\nA `limit` below 1 and an `order` that is neither `asc` nor `desc` carry a message and\nneither `code` nor `details`; testing `code` for absence is how they are told from the one\nabove.\n\nAlso refused here: an entity id belonging to another type than the slug names\n(`ENTITY_TYPE_MISMATCH`), and an entity that was never created as a conditional one\n(`ENTITY_NOT_CONDITIONAL`).\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + }, + "404": { + "description": "No such schema (`SCHEMA_NOT_FOUND`), no such entity under it (`ENTITY_NOT_FOUND`), or the\nentity has no such variant (`VARIANT_NOT_FOUND`) — which is also the answer for a variant\nbelonging to a *different* entity: a variant id alone addresses nothing.\n\nThe entity is established before the timeline is read, so a caller who mistyped the entity\nid is never sent to fix the variant id, and one who sent the wrong slug hears about the\nslug.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + }, + "501": { + "description": "Published ahead of the behaviour. This operation is declared and not yet dispatched, so\nevery request to it is answered here until the listing behaviour lands — which is how a\ndeployed stage says \"this exists and does not work yet\" rather than answering with an\nempty page a client would read as an empty entity.\n\nRead `message`. The body is the shared `Error` shape, as it is on every 501 this API\nanswers, so one \"not built yet\" branch covers all of them.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "description": "Appends a version to a variant: a new set of values taking effect at its own instant.\n\nThis is how a price changes. No version carries an end date and nothing is superseded\nexplicitly — the version in effect at an instant is simply the one with the latest `valid_from`\nat or before it, so appending a later version is the whole of \"this is the new price from then\non\". A version dated in the future is staged and excluded from resolution until its date.\n\n**A version is never refused for being late.** A `valid_from` in the past is written like any\nother and answered with warnings in `warnings` naming what it moved — what resolves now, what a\npast-dated read returns, or both. Correcting a price that took effect last week is ordinary\nwork.\n\nWhat is refused is appending at a `valid_from` the variant already has: that write means either\n\"replace it\" or \"and also this\", and only the caller knows which. The two operations both\nexist, on the dated version path.\n\nThe variant's `conditions` are its identity and are fixed at creation; they may be sent back\nunchanged but never changed.\n", + "operationId": "$appendConditionalVariantVersion", + "summary": "$appendConditionalVariantVersion", + "tags": [ + "Conditional Pricing API" + ], + "parameters": [ + { + "in": "path", + "name": "slug", + "description": "The conditional entity type this variant belongs to", + "schema": { + "$ref": "#/components/schemas/ConditionalEntitySlug" + }, + "required": true, + "example": "price" + }, + { + "in": "path", + "name": "entity_id", + "description": "The conditional entity the variant belongs to", + "schema": { + "type": "string" + }, + "required": true, + "example": "price-sp26d1yo" + }, + { + "in": "path", + "name": "variant_id", + "description": "The variant whose timeline this call addresses", + "schema": { + "type": "string" + }, + "required": true, + "example": "var-46045" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AppendVersionRequest" + } + } + } + }, + "responses": { + "201": { + "description": "The version, as appended, together with anything the write moved", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WrittenVariantVersion" + } + } + } + }, + "400": { + "description": "The version cannot be appended as described: the body would change the variant's conditions,\nor `valid_from` is not a timestamp this store can sort by.\n\nAlso refused here: an entity id belonging to another type than the slug names\n(`ENTITY_TYPE_MISMATCH`), and an entity that was never created as a conditional one\n(`ENTITY_NOT_CONDITIONAL`).\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + }, + "404": { + "description": "No such schema (`SCHEMA_NOT_FOUND`), no entity with that id (`ENTITY_NOT_FOUND`), or no\nsuch variant under it (`VARIANT_NOT_FOUND`).\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + }, + "409": { + "description": "The variant already has a version at that `valid_from` (`VERSION_CONFLICT`) — append means\nappend, never an implicit overwrite. Replace or patch that version instead.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + } + } + } + }, + "/v1/conditional-pricing/{slug}/entities/{entity_id}/variants/{variant_id}/versions/{valid_from}": { + "get": { + "description": "Returns one specific version of a variant, by the instant it takes effect — what a form editing\nthat version loads.\n\nExact, never nearest: an instant the variant has no version at is a not-found rather than the\nversion that would be in effect at it. That question is the shorthand read's, or `:resolve`'s.\n", + "operationId": "$getConditionalVariantVersion", + "summary": "$getConditionalVariantVersion", + "tags": [ + "Conditional Pricing API" + ], + "parameters": [ + { + "in": "path", + "name": "slug", + "description": "The conditional entity type this variant belongs to", + "schema": { + "$ref": "#/components/schemas/ConditionalEntitySlug" + }, + "required": true, + "example": "price" + }, + { + "in": "path", + "name": "entity_id", + "description": "The conditional entity the variant belongs to", + "schema": { + "type": "string" + }, + "required": true, + "example": "price-sp26d1yo" + }, + { + "in": "path", + "name": "variant_id", + "description": "The variant whose timeline this call addresses", + "schema": { + "type": "string" + }, + "required": true, + "example": "var-46045" + }, + { + "in": "path", + "name": "valid_from", + "description": "The version to address, by the instant it takes effect.\n\nAn RFC 3339 date (`2026-01-01`, read as midnight UTC) or date-time\n(`2026-01-01T00:00:00Z`), to at most millisecond precision. Written any accepted way: it is\ncanonicalized before it is matched, so the spelling a read returned and the spelling a\nhuman typed address the same version.\n", + "schema": { + "type": "string" + }, + "required": true, + "example": "2027-01-01T00:00:00.000Z" + } + ], + "responses": { + "200": { + "description": "The version, as stored", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VariantVersion" + } + } + } + }, + "400": { + "description": "Invalid request, e.g. a `valid_from` that is not a timestamp this store can sort by.\n\nAlso refused here: an entity id belonging to another type than the slug names\n(`ENTITY_TYPE_MISMATCH`), and an entity that was never created as a conditional one\n(`ENTITY_NOT_CONDITIONAL`).\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + }, + "404": { + "description": "No entity with that id (`ENTITY_NOT_FOUND`), no such variant under this schema\n(`VARIANT_NOT_FOUND`), or no version at that instant (`VERSION_NOT_FOUND`).\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + } + } + }, + "put": { + "description": "Replaces one version's values wholesale, addressed by its `valid_from`.\n\nEditable whatever its date, scheduled or past. Writing a superseded version is answered with a\nwarning naming what a past-dated read now returns; it is not refused.\n\nAttributes the variant may not override are not applied where the body carries them, and their\nstored value is preserved rather than dropped.\n", + "operationId": "$replaceConditionalVariantVersion", + "summary": "$replaceConditionalVariantVersion", + "tags": [ + "Conditional Pricing API" + ], + "parameters": [ + { + "in": "path", + "name": "slug", + "description": "The conditional entity type this variant belongs to", + "schema": { + "$ref": "#/components/schemas/ConditionalEntitySlug" + }, + "required": true, + "example": "price" + }, + { + "in": "path", + "name": "entity_id", + "description": "The conditional entity the variant belongs to", + "schema": { + "type": "string" + }, + "required": true, + "example": "price-sp26d1yo" + }, + { + "in": "path", + "name": "variant_id", + "description": "The variant whose timeline this call addresses", + "schema": { + "type": "string" + }, + "required": true, + "example": "var-46045" + }, + { + "in": "path", + "name": "valid_from", + "description": "The version to address, by the instant it takes effect.\n\nAn RFC 3339 date (`2026-01-01`, read as midnight UTC) or date-time\n(`2026-01-01T00:00:00Z`), to at most millisecond precision. Written any accepted way: it is\ncanonicalized before it is matched, so the spelling a read returned and the spelling a\nhuman typed address the same version.\n", + "schema": { + "type": "string" + }, + "required": true, + "example": "2027-01-01T00:00:00.000Z" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReplaceVersionRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The version, as the write left it, together with anything the write moved", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WrittenVariantVersion" + } + } + } + }, + "400": { + "description": "The write cannot be applied as described: it would move the version it addresses to another\n`valid_from`, or change the conditions its variant is pinned to — both identity rather than\ncontent, and both fixed at creation. Also when `_revision` is missing or is not a revision\nmarker.\n\nAlso refused here: an entity id belonging to another type than the slug names\n(`ENTITY_TYPE_MISMATCH`), and an entity that was never created as a conditional one\n(`ENTITY_NOT_CONDITIONAL`).\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + }, + "404": { + "description": "No such schema (`SCHEMA_NOT_FOUND`), no entity with that id (`ENTITY_NOT_FOUND`), no such\nvariant under it (`VARIANT_NOT_FOUND`), or no version at that instant\n(`VERSION_NOT_FOUND`).\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + }, + "409": { + "description": "The version has been written since `_revision` was read (`WRITE_CONFLICT`, retryable after\nre-reading the version).\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + } + } + }, + "patch": { + "description": "Changes only the fields it names on one version, addressed by its `valid_from`.\n\nEverything the body does not mention is left as stored. A partial update that tries to change a\npinned condition value is refused: condition values are immutable after a variant is created.\n", + "operationId": "$patchConditionalVariantVersion", + "summary": "$patchConditionalVariantVersion", + "tags": [ + "Conditional Pricing API" + ], + "parameters": [ + { + "in": "path", + "name": "slug", + "description": "The conditional entity type this variant belongs to", + "schema": { + "$ref": "#/components/schemas/ConditionalEntitySlug" + }, + "required": true, + "example": "price" + }, + { + "in": "path", + "name": "entity_id", + "description": "The conditional entity the variant belongs to", + "schema": { + "type": "string" + }, + "required": true, + "example": "price-sp26d1yo" + }, + { + "in": "path", + "name": "variant_id", + "description": "The variant whose timeline this call addresses", + "schema": { + "type": "string" + }, + "required": true, + "example": "var-46045" + }, + { + "in": "path", + "name": "valid_from", + "description": "The version to address, by the instant it takes effect.\n\nAn RFC 3339 date (`2026-01-01`, read as midnight UTC) or date-time\n(`2026-01-01T00:00:00Z`), to at most millisecond precision. Written any accepted way: it is\ncanonicalized before it is matched, so the spelling a read returned and the spelling a\nhuman typed address the same version.\n", + "schema": { + "type": "string" + }, + "required": true, + "example": "2027-01-01T00:00:00.000Z" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PatchVersionRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The version, as the write left it, together with anything the write moved", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WrittenVariantVersion" + } + } + } + }, + "400": { + "description": "The write cannot be applied as described: it would move the version it addresses to another\n`valid_from`, or change the conditions its variant is pinned to — both identity rather than\ncontent, and both fixed at creation. Also when `_revision` is missing or is not a revision\nmarker.\n\nAlso refused here: an entity id belonging to another type than the slug names\n(`ENTITY_TYPE_MISMATCH`), and an entity that was never created as a conditional one\n(`ENTITY_NOT_CONDITIONAL`).\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + }, + "404": { + "description": "No such schema (`SCHEMA_NOT_FOUND`), no entity with that id (`ENTITY_NOT_FOUND`), no such\nvariant under it (`VARIANT_NOT_FOUND`), or no version at that instant\n(`VERSION_NOT_FOUND`).\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + }, + "409": { + "description": "The version has been written since `_revision` was read (`WRITE_CONFLICT`, retryable after\nre-reading the version).\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + } + } + }, + "delete": { + "description": "Removes one version of a variant.\n\nWithdrawing a scheduled adjustment is what this is for, and deleting a future version warns\nabout nothing — nothing that has resolved, or could have resolved, changes. Deleting a version\nthat has taken effect is allowed too and answered with a warning: it changes what a past-dated\nread returns, and if it was the version in effect it changes what resolves now.\n\n**A variant's last remaining version cannot be deleted.** Such a variant would still hold its\ncondition tuple and still be selectable, and then resolve to nothing — which is a variant delete\nwearing a version delete's clothes. Delete the variant instead; that frees the tuple too.\n\nThe variant itself is untouched: it keeps its conditions, its tuple and its place in the index.\n", + "operationId": "$deleteConditionalVariantVersion", + "summary": "$deleteConditionalVariantVersion", + "tags": [ + "Conditional Pricing API" + ], + "parameters": [ + { + "in": "path", + "name": "slug", + "description": "The conditional entity type this variant belongs to", + "schema": { + "$ref": "#/components/schemas/ConditionalEntitySlug" + }, + "required": true, + "example": "price" + }, + { + "in": "path", + "name": "entity_id", + "description": "The conditional entity the variant belongs to", + "schema": { + "type": "string" + }, + "required": true, + "example": "price-sp26d1yo" + }, + { + "in": "path", + "name": "variant_id", + "description": "The variant whose timeline this call addresses", + "schema": { + "type": "string" + }, + "required": true, + "example": "var-46045" + }, + { + "in": "path", + "name": "valid_from", + "description": "The version to address, by the instant it takes effect.\n\nAn RFC 3339 date (`2026-01-01`, read as midnight UTC) or date-time\n(`2026-01-01T00:00:00Z`), to at most millisecond precision. Written any accepted way: it is\ncanonicalized before it is matched, so the spelling a read returned and the spelling a\nhuman typed address the same version.\n", + "schema": { + "type": "string" + }, + "required": true, + "example": "2027-01-01T00:00:00.000Z" + }, + { + "in": "query", + "name": "_revision", + "description": "The revision marker read from the version being deleted. The delete is refused if the\nversion has been written since.\n\nThe same marker the write bodies carry as `_revision`.\n", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 3 + } + ], + "responses": { + "200": { + "description": "The version removed, together with anything the delete moved", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedVariantVersion" + } + } + } + }, + "400": { + "description": "The version cannot be removed because it is the variant's only one\n(`LAST_VERSION_UNDELETABLE`) — such a variant would keep its condition tuple, stay\nselectable and resolve to nothing, so delete the variant instead, which frees the tuple\ntoo.\n\nThe refusals around it carry no code, and testing `code` for absence is how they are told\nfrom it: a missing or unreadable `_revision`, a `valid_from` this store cannot sort by,\nand an id this store cannot key by.\n\nAlso refused here: an entity id belonging to another type than the slug names\n(`ENTITY_TYPE_MISMATCH`), and an entity that was never created as a conditional one\n(`ENTITY_NOT_CONDITIONAL`).\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + }, + "404": { + "description": "No entity with that id (`ENTITY_NOT_FOUND`), no such variant under this schema\n(`VARIANT_NOT_FOUND`), or no version at that instant (`VERSION_NOT_FOUND`).\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + }, + "409": { + "description": "The version has been written since `_revision` was read (`WRITE_CONFLICT`, retryable after\nre-reading the version).\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + } + } + } + }, + "/v1/conditional-pricing/{slug}/variants:batchUpsert": { + "post": { + "description": "Writes up to 100 variants or versions in one call — the endpoint a bulk importer drives a\nrefresh cycle through, so hundreds of thousands of keys are a stream of calls rather than a\ncall per key.\n\n**One schema in the path, one entity per item.** A single call may name several entities, so\nit can refresh a whole tariff hierarchy — a composite price and its components together —\nand the entity id rides each item instead of the path.\n\n**An item addresses a condition tuple, never a `variant_id`.** An upsert creates a variant\nthat has no id yet. The id it created, or found, is on the result entry.\n\nEach item's outcome is derived from what is stored, with no mode for the caller to declare,\nin this order: an unknown tuple is `variant_created`, a known tuple with no version at the\nitem's `valid_from` is `version_created`, and an existing version at that exact instant is\n`updated` — or `skipped`, which is reserved for a write whose values are identical to what is\nstored, so re-running an unchanged import still reads as a no-op. `version_created` is\ndistinct from `variant_created` so an importer's counts can tell \"new postal codes appeared\"\nfrom \"existing variants got their scheduled adjustment\".\n\nAn item without `valid_from` is a current-state, last-write-wins write, and has no `skipped`\ndetection. A `valid_from` in the past changes nothing about the outcome — it is written like\nany other and answered with the timeline warnings on that item. An importer stamping one\n`valid_from` across a batch therefore sees backdate warnings on every item; omitting the\nfield is how it avoids them.\n\n**Items addressing the same variant apply in array order; items addressing different\nvariants are processed in parallel.** Here \"the same variant\" is the same `(entity_id,\ncondition tuple)`, so two items sharing a tuple and a `valid_from` apply in order and the\nlast one wins. There is no cross-item rollback.\n\n**This write is unguarded.** No `_revision` is accepted on an item or returned on an entry;\nan editing screen that needs a guard re-reads the one version it is about to write through\nits own `GET`.\n\nThree refusals a client would otherwise expect do not occur here. `VERSION_CONFLICT` never\ndoes: an existing `valid_from` is a replacement rather than a collision. A tuple-uniqueness\nguard lost to a concurrent writer is not reported as `TUPLE_CONFLICT` either — the item is\nre-read against current state and re-derived through the same outcome order above, which is\ncontent-aware. `WRITE_CONFLICT` marks the case that is genuinely worth retrying: transient\ncontention on one entity's rows.\n\n**Published ahead of the behaviour.** No handler serves this yet, so a deployed stage answers\n`501`. The examples below are what an importer builds against in the meantime.\n", + "operationId": "$batchUpsertConditionalVariants", + "summary": "$batchUpsertConditionalVariants", + "tags": [ + "Conditional Pricing API" + ], + "parameters": [ + { + "in": "path", + "name": "slug", + "description": "The conditional entity type every item in this call writes under", + "schema": { + "$ref": "#/components/schemas/ConditionalEntitySlug" + }, + "required": true, + "example": "price" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BatchUpsertVariantsRequest" + }, + "examples": { + "A monthly refresh cycle": { + "summary": "Five items across two entities — a new postal code, a scheduled adjustment, a correction, an unchanged row and one bad source row", + "value": { + "correlation_id": "tariff-refresh-2027-01", + "items": [ + { + "entity_id": "price-sp26d1yo", + "conditions": { + "postal_code": "46045" + }, + "valid_from": "2027-01-01T00:00:00Z", + "values": { + "unit_amount": 3261, + "unit_amount_decimal": "32.61" + } + }, + { + "entity_id": "price-sp26d1yo", + "conditions": { + "postal_code": "50667" + }, + "valid_from": "2027-01-01T00:00:00Z", + "values": { + "unit_amount": 3412, + "unit_amount_decimal": "34.12" + } + }, + { + "entity_id": "price-base-fee", + "conditions": { + "postal_code": "50667" + }, + "valid_from": "2027-01-01T00:00:00Z", + "values": { + "unit_amount": 1290, + "unit_amount_decimal": "12.90" + } + }, + { + "entity_id": "price-sp26d1yo", + "conditions": { + "postal_code": "80331" + }, + "valid_from": "2027-01-01T00:00:00Z", + "values": { + "unit_amount": 3120, + "unit_amount_decimal": "31.20", + "description": "Grundpreis 2027" + } + }, + { + "entity_id": "price-sp26d1yo", + "conditions": { + "postal_code": "10115", + "segment": "industrial" + }, + "valid_from": "2027-01-01T00:00:00Z", + "values": { + "unit_amount": 2980, + "unit_amount_decimal": "29.80" + } + } + ] + } + } + } + } + } + }, + "responses": { + "200": { + "description": "What every item did, in request order, and a count per outcome.\n\n`200` whatever the per-item outcomes: a batch that processed 100 items and failed 99 did\nits job, and `counts` says what happened.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BatchUpsertResult" + }, + "examples": { + "Every outcome once": { + "summary": "The five items above, in order — the created variant's id is the one an order pins, and item five failed on its own without touching the rest", + "value": { + "correlation_id": "tariff-refresh-2027-01", + "counts": { + "variant_created": 1, + "version_created": 1, + "updated": 1, + "skipped": 1, + "error": 1 + }, + "results": [ + { + "outcome": "variant_created", + "entity_id": "price-sp26d1yo", + "variant_id": "var-46045", + "valid_from": "2027-01-01T00:00:00.000Z", + "warnings": [ + { + "code": "VARIANT_COUNT_APPROACHING_CAP", + "message": "This entity holds 4998 of the 5000 variants it may hold", + "details": { + "variant_count": 4998, + "cap": 5000 + } + } + ] + }, + { + "outcome": "version_created", + "entity_id": "price-sp26d1yo", + "variant_id": "var-50667", + "valid_from": "2027-01-01T00:00:00.000Z", + "warnings": [] + }, + { + "outcome": "updated", + "entity_id": "price-base-fee", + "variant_id": "var-bf-50667", + "valid_from": "2027-01-01T00:00:00.000Z", + "warnings": [] + }, + { + "outcome": "skipped", + "entity_id": "price-sp26d1yo", + "variant_id": "var-80331", + "valid_from": "2027-01-01T00:00:00.000Z", + "warnings": [ + { + "code": "ATTRIBUTES_NOT_APPLIED", + "message": "The value sent for description was not applied", + "details": { + "attributes": [ + { + "attribute": "description", + "reason": "ATTRIBUTE_NOT_OVERRIDABLE" + } + ] + } + } + ] + }, + { + "outcome": "error", + "entity_id": "price-sp26d1yo", + "warnings": [], + "error": { + "message": "The value pinned for condition segment is not one of its declared options", + "code": "CONDITION_VALUE_INVALID", + "details": { + "condition_name": "segment", + "value": "industrial", + "options": [ + "private", + "commercial" + ] + } + } + } + ] + } + } + } + } + } + }, + "400": { + "description": "The envelope cannot be processed at all: more than 100 items, an empty `items`, a slug\nthat names no conditional entity type, or a body this schema rejects outright.\n\nAll of those are request-validation failures, so they carry a message and neither `code`\nnor `details`. Every failure of an individual item is on that item's result entry\ninstead, `ENTITY_NOT_FOUND` included — the entity id is on the item and not in the path,\nso one wrong id in a source file cannot fail the other 99 rows. `ENTITY_TYPE_MISMATCH`\nand `ENTITY_NOT_CONDITIONAL` are per item for the same reason: each item names its own\nentity, and each entity is checked against the slug and for `is_conditional` on its own.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + }, + "404": { + "description": "No such schema (`SCHEMA_NOT_FOUND`) — the one lookup the whole call depends on, since it\nis what the items are validated against.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + }, + "501": { + "description": "Published ahead of the behaviour. This operation is declared and not yet dispatched, so\nevery request to it is answered here until the batch write behaviour lands — which is how\na deployed stage says \"this exists and does not work yet\" rather than answering with\ncounts a client would read as a completed import.\n\nRead `message`. The body is the shared `Error` shape, as it is on every 501 this API\nanswers, so one \"not built yet\" branch covers all of them.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/conditional-pricing/{slug}/variants:batchDelete": { + "post": { + "description": "Removes up to 100 variants or versions in one call — the symmetric bulk withdrawal, so\nretiring a generation of variants, or a scheduled adjustment across many of them, is as\ncheap as creating it was.\n\nThe noun is `variants` on both batch endpoints, although an item carrying `valid_from`\nremoves one **version** rather than the variant: an item without it removes the whole variant\n— its condition tuple, its registration in the index, and every version it accumulated — and\nan item with it removes exactly that version, under the single-item rules. A future version\nand a superseded one are both deletable and both answered with the warnings that say what\nmoved; a variant's last remaining version is refused\n(`LAST_VERSION_UNDELETABLE`), because such a variant would still hold its tuple and still\nresolve to nothing. Delete the variant instead.\n\n**An item addresses its variant one of two ways, and never both**: by `variant_id`, or by the\ncondition tuple it pins. Use ids once the schema has drifted: a tuple naming a condition the\nschema no longer declares cannot be canonicalized, so it addresses nothing. An item naming\nboth fails validation and is an envelope `400`, not a per-item error: the request validator\nrejects the body before any item runs.\n\n**`entity_id` is required beside a `variant_id`, and is not redundant.** A variant id alone\naddresses nothing in this API.\n\n**Items addressing the same variant apply in array order; items addressing different variants\nare processed in parallel.** Because an item addresses its variant two ways, \"the same\nvariant\" is decided after addressing, in three steps: every condition tuple is resolved to a\nvariant id, items are grouped by that id, and each group is applied in array order. So one\ncall may hold an item naming `var-46045` and an item naming the tuple that variant pins, and\nthe guarantee holds across both. There is no cross-item rollback.\n\n**An item that addresses nothing is `skipped` — but only when the variant or the version is\nwhat is missing.** A missing *entity* is a per-item `ENTITY_NOT_FOUND`.\n\nAn interrupted call is safe to send again. A whole-variant delete frees the tuple in its\nfirst phase and removes the version rows afterwards, so a re-run picks up where it stopped\nand reports `skipped` for what has already gone. Nothing is archived: a variant an order or\ncontract pins stops resolving, and a pinned `:resolve` naming it answers `VARIANT_NOT_FOUND`.\n\n**Published ahead of the behaviour.** No handler serves this yet, so a deployed stage answers\n`501`.\n", + "operationId": "$batchDeleteConditionalVariants", + "summary": "$batchDeleteConditionalVariants", + "tags": [ + "Conditional Pricing API" + ], + "parameters": [ + { + "in": "path", + "name": "slug", + "description": "The conditional entity type every item in this call removes from", + "schema": { + "$ref": "#/components/schemas/ConditionalEntitySlug" + }, + "required": true, + "example": "price" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BatchDeleteVariantsRequest" + }, + "examples": { + "A cleanup pass, addressed both ways": { + "summary": "A version withdrawn by the tuple that pins it, a whole variant removed by id, and a version delete that would leave its variant with none", + "value": { + "correlation_id": "postal-code-cleanup-2026-09", + "items": [ + { + "entity_id": "price-sp26d1yo", + "conditions": { + "postal_code": "46045" + }, + "valid_from": "2026-01-01T00:00:00Z" + }, + { + "entity_id": "price-sp26d1yo", + "conditions": { + "postal_code": "99998" + } + }, + { + "entity_id": "price-sp26d1yo", + "variant_id": "var-80331", + "valid_from": "2026-01-01T00:00:00Z" + } + ] + } + } + } + } + } + }, + "responses": { + "200": { + "description": "What every item did, in request order, and a count per outcome.\n\n`200` whatever the per-item outcomes: the call did its job, and `counts` says what\nhappened.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BatchDeleteResult" + }, + "examples": { + "Every outcome once": { + "summary": "The three items above, in order — the second addressed a tuple no variant pins, so it names no variant to have skipped", + "value": { + "correlation_id": "postal-code-cleanup-2026-09", + "counts": { + "deleted": 1, + "skipped": 1, + "error": 1 + }, + "results": [ + { + "outcome": "deleted", + "entity_id": "price-sp26d1yo", + "variant_id": "var-46045", + "valid_from": "2026-01-01T00:00:00.000Z", + "warnings": [ + { + "code": "ACTIVE_VERSION_CHANGED", + "message": "The version in effect was removed, so what resolves now has changed", + "details": { + "valid_from": "2026-01-01T00:00:00.000Z", + "active_valid_from": "2026-01-01T00:00:00.000Z" + } + } + ] + }, + { + "outcome": "skipped", + "entity_id": "price-sp26d1yo", + "warnings": [] + }, + { + "outcome": "error", + "entity_id": "price-sp26d1yo", + "variant_id": "var-80331", + "valid_from": "2026-01-01T00:00:00.000Z", + "warnings": [], + "error": { + "message": "var-80331 has only this version, so removing it would leave the variant unresolvable", + "code": "LAST_VERSION_UNDELETABLE", + "details": { + "variant_id": "var-80331", + "valid_from": "2026-01-01T00:00:00.000Z" + } + } + } + ] + } + } + } + } + } + }, + "400": { + "description": "The envelope cannot be processed at all: more than 100 items, an empty `items`, a slug\nthat names no conditional entity type, an item naming both a `variant_id` and a condition\ntuple, or a body this schema rejects outright.\n\nAll of those are request-validation failures, so they carry a message and neither `code`\nnor `details`. Every failure of an individual item is on that item's result entry\ninstead, `ENTITY_NOT_FOUND` included, along with the per-entity `ENTITY_TYPE_MISMATCH`\nand `ENTITY_NOT_CONDITIONAL` — each item names its own entity, so each is checked on its\nown.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + }, + "404": { + "description": "No such schema (`SCHEMA_NOT_FOUND`) — the one lookup the whole call depends on.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConditionalPricingError" + } + } + } + }, + "501": { + "description": "Published ahead of the behaviour. This operation is declared and not yet dispatched, so\nevery request to it is answered here until the batch delete behaviour lands — which is\nhow a deployed stage says \"this exists and does not work yet\" rather than answering with\ncounts a client would read as a completed cleanup.\n\nRead `message`. The body is the shared `Error` shape, as it is on every 501 this API\nanswers, so one \"not built yet\" branch covers all of them.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + } + }, + "components": { + "securitySchemes": { + "EpilotAuth": { + "type": "http", + "scheme": "bearer", + "description": "Epilot Bearer Token" + }, + "EpilotPublicAuth": { + "type": "http", + "scheme": "bearer", + "description": "Epilot Public Access Bearer Token", + "bearerFormat": "JWT" + } + }, + "schemas": { + "IntegrationId": { + "type": "string", + "enum": [ + "getag", + "external-catalog" + ] + }, + "ConditionalEntitySlug": { + "type": "string", + "description": "Schema slug of an entity type that can be conditional — the `{slug}` of every\nconditional-pricing route.\n", + "enum": [ + "product", + "price", + "coupon" + ] + }, + "ConditionType": { + "type": "string", + "description": "The kind of value a condition holds, which decides how a variant's pinned value is matched\nagainst a resolve context.\n\n- `string`: an arbitrary string, matched exactly and case-sensitively\n- `number`: a numeric value\n- `date`: a single date\n- `daterange`: a window with a from and an until timestamp; both ends may be left open\n- `boolean`: a true/false value\n- `select`: one of the values declared in `options`, which is always a closed vocabulary\n- `location`: a geographic value, shaped by `format`\n\nThere is no condition type for the fallback variant. Being the entity's fallback is a\nproperty of the variant, set by the `default` flag on a variant write, and needs nothing\ndeclared in the schema.\n", + "enum": [ + "string", + "number", + "date", + "daterange", + "boolean", + "select", + "location" + ] + }, + "ConditionDefinition": { + "type": "object", + "description": "One condition dimension, in the shape a schema's `conditions` array holds it — copy it in\nverbatim.\n", + "required": [ + "id", + "name", + "label", + "type" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid", + "description": "Stable identity of the condition, round-tripped unchanged for the lifetime of the\ncondition: it is what tells a rename apart from a remove plus an add. The Entity API\nmints none of its own, so whoever creates a condition supplies one — a catalog condition\narrives with the identity the catalog gives it, the same in every org, and is copied into\nthe schema along with the rest of the object.\n", + "example": "d5839b94-ba20-4225-a78e-76951d352bd6" + }, + "name": { + "type": "string", + "description": "How variants and resolve contexts refer to this condition. Independent of attribute\nnames: a value needed as an attribute too is duplicated onto the variant.\n\n`default`, and any name beginning with `_`, are reserved for the server: a condition\ndeclared under one is ignored, since nothing could pin it and no context could address it.\n", + "example": "postal_code" + }, + "label": { + "type": "string", + "description": "Human-readable name of the condition.", + "example": "Postal Code" + }, + "type": { + "$ref": "#/components/schemas/ConditionType" + }, + "options": { + "type": "array", + "description": "The declared vocabulary of a `select` condition. Absent for every other type.\n\nThe same shape a `select` condition's `options` has on the Entity API, item for item: an\nentry is either the value itself or an object carrying that value and an optional display\n`title`. A `title` is never pinned by a variant and never matched — two entries differing\nonly in their title are one vocabulary entry.\n\nThe vocabulary is always closed: a condition carries no flag widening it, so a pinned\nvalue outside a declared vocabulary is rejected with `CONDITION_VALUE_INVALID`. A\nvocabulary that declares nothing is closed too — while `options` is absent or empty, or\nholds nothing this deploy can read, the condition admits no pin at all and the same code\nis returned with an empty `options`. It is *not* enforced on resolve — a vocabulary says\nwhat may be stored, not what may be asked for, so a context value outside it is a query\nthat simply matches nothing.\n", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "string" + }, + "title": { + "type": "string" + } + } + } + ] + }, + "example": [ + "private", + { + "value": "commercial", + "title": "Commercial customers" + } + ] + }, + "format": { + "type": "string", + "description": "The value shape of a `location` condition. Absent for every other type.", + "enum": [ + "zipcode", + "zipcode_town" + ] + } + } + }, + "ConditionSet": { + "type": "object", + "description": "A named bundle of condition definitions, built in for one entity type.", + "required": [ + "id", + "label", + "description", + "conditions" + ], + "properties": { + "id": { + "type": "string", + "description": "Identifies the set within this entity type's catalog.", + "example": "delivery_area" + }, + "label": { + "type": "string", + "description": "Human-readable name of the set.", + "example": "Delivery Area" + }, + "description": { + "type": "string", + "description": "What the set is for, and when to reach for it." + }, + "conditions": { + "type": "array", + "description": "The condition definitions to copy into the schema's own `conditions` array.", + "items": { + "$ref": "#/components/schemas/ConditionDefinition" + } + } + } + }, + "ConditionSetCatalog": { + "type": "object", + "required": [ + "results" + ], + "properties": { + "results": { + "type": "array", + "description": "The condition sets built in for the requested entity type, in the order they are offered.\n", + "items": { + "$ref": "#/components/schemas/ConditionSet" + } + } + } + }, + "ConditionalPricingErrorCode": { + "type": "string", + "description": "Machine-readable failure mode of a conditional-pricing operation, allowing clients\nto branch on the kind of failure instead of parsing the error message.\n\n- `SCHEMA_NOT_FOUND` (404): no conditional entity type by that slug\n- `ENTITY_NOT_FOUND` (404): the schema holds no entity with that id\n- `ENTITY_TYPE_MISMATCH` (400): that id belongs to an entity of another type than the slug named\n- `ENTITY_NOT_CONDITIONAL` (400): the entity is of the right type but was not created as a conditional one\n- `VARIANT_NOT_FOUND` (404): the entity has no such variant\n- `VERSION_NOT_FOUND` (404): the variant has no version at that `valid_from`\n- `NO_MATCHES` (404): nothing applied to the context and the entity has no `default` variant\n- `NO_ACTIVE_VERSION` (404): the variant has no version in effect at the instant asked about\n- `AMBIGUOUS_RESOLUTION` (409): several variants match the given context while a single result was requested\n- `TUPLE_CONFLICT` (409): the condition tuple is already claimed by another variant\n- `VERSION_CONFLICT` (409): a version already exists at the given `valid_from` on that variant\n- `CONDITION_UNDEFINED` (400): a resolve context, a listing filter or a variant's pins name a condition the entity's schema does not define\n- `OPERATOR_UNSUPPORTED` (400): the requested operator is not applicable to the condition's type\n- `CONTEXT_FORMAT_INVALID` (400): a resolve context or listing filter value is malformed for its condition type\n- `CONDITION_VALUE_INVALID` (400): a variant write pins a `select` value the condition's `options` do not admit, including every pin on a condition whose `options` are absent, empty or unreadable\n- `TOO_MANY_MATCHES` (400): a multi-match resolve exceeded its result cap\n- `WRITE_CONFLICT` (409): transient write contention, retryable unlike `TUPLE_CONFLICT`\n- `OFFSET_WINDOW_EXCEEDED` (400): a listing's `from` plus `size` reaches past the offset window the search index allows\n- `CURSOR_INVALID` (400): a paging cursor cannot be read, or does not belong to the read it was sent with\n- `VARIANT_LIMIT_REACHED` (400): the entity already holds every variant it may hold\n- `PIN_FORMAT_INVALID` (400): a variant pins a value malformed for its condition's type\n- `VARIANT_UNPINNED` (400): a variant write pins no condition and is not marked `default`, or a batch delete item addresses no variant\n- `LAST_VERSION_UNDELETABLE` (400): the delete would leave the variant with no version at all\n\nIn a batch, the last four are the refusals an importer branches on: `VARIANT_LIMIT_REACHED`\nmeans stop the import, `PIN_FORMAT_INVALID` means one bad row, `LAST_VERSION_UNDELETABLE`\nmeans delete the variant instead.\n\nNot every refusal has a code. These carry a message and neither `code` nor `details`, and\neach operation's `400` names its own: a write pinning the reserved marker (`default` or\n`_default`), a variant marked `default` that also pins a real condition, a pin on a condition\nwhose declared type this deploy cannot read, an id this store cannot key by, a `valid_from`\nthis store cannot sort by, and a version write or delete with no `_revision`. Testing `code`\nfor absence is how a client tells them from the coded failures.\n\nFour of the 404s say that something the request addressed does not exist, and are fixed by\ncorrecting an id or accepting the thing is gone. The other two say the opposite: everything\naddressed exists and there is still nothing to serve — no variant applies to this situation,\nor none of a variant's versions is in effect yet. Those are ordinary business outcomes, told\napart from a wrong id by their code.\n\n`ENTITY_TYPE_MISMATCH` and `ENTITY_NOT_CONDITIONAL` are `400`s, not 404s: the entity the\nrequest addressed **was** found, and the fix is the slug beside it. A slug that names no\nconditional entity type at all is a `400` too.\n\nEach code is emitted with the HTTP status shown above, and only with that status, and each\none is pinned by a member of `ConditionalPricingError` — which is where the structured data\nthat code carries is declared.\n", + "enum": [ + "SCHEMA_NOT_FOUND", + "ENTITY_NOT_FOUND", + "ENTITY_TYPE_MISMATCH", + "ENTITY_NOT_CONDITIONAL", + "VARIANT_NOT_FOUND", + "VERSION_NOT_FOUND", + "NO_MATCHES", + "NO_ACTIVE_VERSION", + "AMBIGUOUS_RESOLUTION", + "TUPLE_CONFLICT", + "VERSION_CONFLICT", + "CONDITION_UNDEFINED", + "OPERATOR_UNSUPPORTED", + "CONTEXT_FORMAT_INVALID", + "CONDITION_VALUE_INVALID", + "TOO_MANY_MATCHES", + "WRITE_CONFLICT", + "OFFSET_WINDOW_EXCEEDED", + "CURSOR_INVALID", + "VARIANT_LIMIT_REACHED", + "PIN_FORMAT_INVALID", + "VARIANT_UNPINNED", + "LAST_VERSION_UNDELETABLE" + ] + }, + "ResolveConditionalEntityRequest": { + "description": "A resolve names one conditional entity, then says which of its variants it means — one of two\nways, and never both. `context` describes a situation and asks which variants apply to it;\n`variant_id` names one variant and skips matching entirely.\n\nA body carrying both, or neither, is a validation `400`. Asking for the default variant\nwithout knowing its id is `context: {}`, which matches nothing and therefore falls back to it.\n\nEverything below the variant selection is the same on both branches, `as_of` included.\n", + "oneOf": [ + { + "$ref": "#/components/schemas/ResolveByContextRequest" + }, + { + "$ref": "#/components/schemas/ResolveByPinRequest" + } + ] + }, + "ResolveByContextRequest": { + "type": "object", + "additionalProperties": false, + "description": "Resolve by matching a situation: which of this entity's variants apply to `context`, each\ncomposed with the version in effect at `as_of`.\n", + "required": [ + "schema", + "entity_id", + "context" + ], + "properties": { + "schema": { + "$ref": "#/components/schemas/ConditionalEntitySlug" + }, + "entity_id": { + "type": "string", + "description": "The conditional entity to resolve. Resolution is always scoped to exactly one.", + "example": "price-sp26d1yo" + }, + "context": { + "$ref": "#/components/schemas/ResolveContext" + }, + "as_of": { + "type": "string", + "description": "The instant the version is selected at — the version with the latest `valid_from` at or\nbefore it. Defaults to now. A variant whose first version is later than this is\nscheduled rather than applicable, and is excluded from resolution entirely.\n\nThat exclusion belongs to context matching only: a set of results may quietly drop a\nmember, where a pin naming one variant cannot answer with silence and is told\n`NO_ACTIVE_VERSION` instead.\n\nAn RFC 3339 date (`2026-01-01`, read as midnight UTC) or date-time\n(`2026-01-01T00:00:00Z`), to at most millisecond precision.\n", + "example": "2027-03-15T00:00:00Z" + }, + "options": { + "$ref": "#/components/schemas/ResolveOptions" + } + } + }, + "ResolveByPinRequest": { + "type": "object", + "additionalProperties": false, + "description": "Resolve by naming a variant: compose this one, whatever a context would have matched. What an\norder needs to show the numbers a customer agreed to, and what a contract needs to show what\nis billable now — the two differ only in whether `as_of` is supplied.\n", + "required": [ + "schema", + "entity_id", + "variant_id" + ], + "properties": { + "schema": { + "$ref": "#/components/schemas/ConditionalEntitySlug" + }, + "entity_id": { + "type": "string", + "description": "The conditional entity to resolve. Resolution is always scoped to exactly one.", + "example": "price-sp26d1yo" + }, + "variant_id": { + "type": "string", + "description": "The variant to compose. Condition matching is skipped entirely: no `context` is read, the\n`default` fallback does not apply, and `results` carries exactly one entry — a pin asks\nfor one variant by name.\n\nA `variant_id` this entity has no variant under is `VARIANT_NOT_FOUND`, and so is one\nnaming a variant of a different entity: a variant id alone addresses nothing.\n`SCHEMA_NOT_FOUND` and `ENTITY_NOT_FOUND` are still answered ahead of both.\n\n**Published ahead of the behaviour.** Until the pinned path is built, a body carrying\nthis field is answered `501`, ahead of every check above — the field exists so consumers\ncan build against it, and declining it is how a deployed stage says so rather than\nquietly returning the `default` variant.\n", + "example": "var-46045" + }, + "as_of": { + "type": "string", + "description": "The instant the version is selected at — the version with the latest `valid_from` at or\nbefore it. Defaults to now. The same selector, by the same rule, as on a context resolve:\nhow the variant was chosen is orthogonal to which of its versions applies, so a caller\nreplaying a recorded resolution instant supplies it here.\n\nA pinned variant whose first version is later than this is `NO_ACTIVE_VERSION`, carrying\nthe instant in `details.as_of`, rather than being dropped the way context matching drops\na scheduled variant.\n\nAn RFC 3339 date (`2026-01-01`, read as midnight UTC) or date-time\n(`2026-01-01T00:00:00Z`), to at most millisecond precision.\n", + "example": "2027-03-15T00:00:00Z" + }, + "options": { + "$ref": "#/components/schemas/PinnedResolveOptions" + } + } + }, + "ResolveContext": { + "type": "object", + "additionalProperties": true, + "description": "The situation to resolve for: a flat map keyed by condition name, as the entity's schema\ndeclares them. A condition left out of the map is not a wildcard — it matches only variants\nthat leave that condition unpinned.\n\nEach value is either an exact value, typed by its condition, or a single-operator predicate\nobject:\n\n- `{ \"lt\": v }`, `{ \"lte\": v }`, `{ \"gt\": v }`, `{ \"gte\": v }` — order against a `number` or\n `date` condition.\n- `{ \"in\": [...] }` — membership, against a `string`, `select` or `number` condition.\n- `{ \"between\": \"2026-03-01\" }` — the explicit spelling of `daterange` containment; a plain\n date supplied for a `daterange` condition means the same thing.\n- `{ \"exists\": true }` — pinned to any value. `{ \"exists\": false }` says what leaving the key\n out says.\n\nAn `in` list carries at most 50,000 values; a longer one is `CONTEXT_FORMAT_INVALID`. To match\na condition whatever its value, send `{ \"exists\": true }` rather than enumerating its\nvocabulary.\n\nExact values are typed by their condition: a `string` or `select` matches exactly and\ncase-sensitively, with no trimming; a `location` of format `zipcode` is the postal code\nitself, and one of format `zipcode_town` an object carrying both, whose town is compared\ncase- and whitespace-insensitively while its postal code is not.\n\n`default`, and any name beginning with `_`, are reserved for the server and cannot be\nsupplied here.\n\nAn empty map is valid and means what it says: it supplies no value, so it matches no variant\nthat pins a condition, and the entity's `default` variant is what comes back. It is the only way to\nask for the default variant without knowing its id.\n", + "example": { + "postal_code": "46045", + "consumption": { + "lt": 5000 + } + } + }, + "ResolveOptions": { + "type": "object", + "additionalProperties": false, + "description": "The options a context resolve accepts. A pin takes `PinnedResolveOptions` instead.", + "properties": { + "resolve_one": { + "type": "boolean", + "default": false, + "description": "Ask for an unambiguous answer. Several applicable variants become `AMBIGUOUS_RESOLUTION`\nrather than a set, and nothing applicable becomes `NO_MATCHES` rather than an empty one.\nThe response shape does not change: `results` simply carries exactly one entry.\n" + }, + "hydrate": { + "type": "boolean", + "default": false, + "description": "Return the entities a relation attribute references in place of the references\nthemselves, one level deep, exactly as an entity read with hydration does.\n\nA fetch, not a second resolution: a referenced entity comes back as it is read, and one\nthat is itself conditional carries its own flag — acting on that is the consumer's\nchoice, and this API does not resolve it on their behalf.\n\nApplied after composition, so a relation attribute whose value this variant's version\nreplaced is hydrated too. That is what makes a composite price work: the override\nreferences different component *entities*, which exist only in the composed payload.\n\nA reference that cannot be fetched comes back exactly as entity hydration returns it —\nno drop, no failure, and no field reporting it. A resolved payload behaves as an entity\nof the same shape would, and the discriminators are the only difference.\n\nCosts one fetch per referenced entity per result, and carries no cap of its own: the\nper-attribute limits are entity hydration's, and the 100-result cap on the resolve itself\nis unchanged.\n\n**Published ahead of the behaviour.** Until hydration is built, `true` is answered `501`\nrather than served as unhydrated references; `false`, which asks for what this path\nalready does, resolves normally.\n" + } + } + }, + "PinnedResolveOptions": { + "type": "object", + "additionalProperties": false, + "description": "The options a pinned resolve accepts — `hydrate` and nothing else. `resolve_one` has nothing\nto change on this branch, where the answer is exactly one result or a 404, so a body sending\nit is a validation `400`. `hydrate` means what `ResolveOptions.hydrate` means.\n", + "properties": { + "hydrate": { + "type": "boolean", + "default": false, + "description": "Return the entities a relation attribute references in place of the references\nthemselves, one level deep, exactly as an entity read with hydration does.\n\nA fetch, not a second resolution: a referenced entity comes back as it is read, and one\nthat is itself conditional carries its own flag — acting on that is the consumer's\nchoice, and this API does not resolve it on their behalf.\n\nApplied after composition, so a relation attribute whose value this variant's version\nreplaced is hydrated too. That is what makes a composite price work: the override\nreferences different component *entities*, which exist only in the composed payload.\n\nA reference that cannot be fetched comes back exactly as entity hydration returns it —\nno drop, no failure, and no field reporting it. A resolved payload behaves as an entity\nof the same shape would, and the discriminators are the only difference.\n\nCosts one fetch per referenced entity per result, and carries no cap of its own: the\nper-attribute limits are entity hydration's, and the 100-result cap on the resolve itself\nis unchanged.\n\n**Published ahead of the behaviour.** Until hydration is built, `true` is answered `501`\nrather than served as unhydrated references; `false`, which asks for what this path\nalready does, resolves normally.\n" + } + } + }, + "ResolvedVariants": { + "type": "object", + "required": [ + "results" + ], + "properties": { + "results": { + "type": "array", + "description": "One composed payload per applicable variant, capped at 100 — a context selecting more\nthan that is answered with `TOO_MANY_MATCHES` instead. No dominance or specificity\nordering is applied between them.\n", + "items": { + "$ref": "#/components/schemas/ResolvedVariant" + } + } + } + }, + "ResolvedVariant": { + "type": "object", + "additionalProperties": true, + "description": "The entity as this variant leaves it — every attribute of a plain entity read, with the\napplicable version's overrides applied — plus the discriminators saying where the numbers\ncame from.\n\nWith `options.hydrate`, a relation attribute holds the entities it references rather than the\nreferences themselves. That changes what an attribute holds, not the payload's shape, so\nnothing is declared here for it.\n", + "required": [ + "_id", + "_variant_id", + "_version_valid_from", + "_conditions", + "_inert_overrides" + ], + "properties": { + "_id": { + "type": "string", + "description": "The logical entity's id — the same one a plain entity read returns. Resolution never\nmints a new identity; a variant is a set of values for *this* entity, not another one.\n", + "example": "price-sp26d1yo" + }, + "_variant_id": { + "type": "string", + "description": "The variant these values came from. Durable: this is what an order or a contract pins to\nread the same numbers back later.\n", + "example": "var-46045" + }, + "_version_valid_from": { + "type": "string", + "description": "The `valid_from` of the version applied for the requested `as_of`.", + "example": "2027-01-01T00:00:00.000Z" + }, + "_conditions": { + "allOf": [ + { + "$ref": "#/components/schemas/VariantConditions" + } + ], + "description": "The conditions this variant pins, plus the boolean `default` discriminator.\n" + }, + "_inert_overrides": { + "type": "array", + "description": "The variant's stored overrides this payload did not apply, and why. Always present, and\nempty in the ordinary case — a client reads its length rather than branching on its\nabsence, the same way it reads a write's `warnings`.\n\nComputed per read from the schema as it stands, never stored, so granting or withdrawing\n`overridable_attribute` changes what resolves — and this list — without any data being\nrewritten. A version read reports what is stored and carries no such list; this is the\nonly surface that honours the schema.\n", + "items": { + "$ref": "#/components/schemas/InertOverride" + } + } + } + }, + "CreateVariantRequest": { + "type": "object", + "additionalProperties": false, + "required": [ + "values" + ], + "properties": { + "conditions": { + "$ref": "#/components/schemas/PinnedConditions" + }, + "default": { + "type": "boolean", + "default": false, + "description": "Mark this variant as the entity's fallback: the one served when no other variant applies.\n\nA property of the variant, never an entry in `conditions`. A default variant cannot pin\nanything else, and an entity can have at most one; a second is refused as\n`TUPLE_CONFLICT`.\n\nAvailable to every conditional entity: nothing has to be declared in the schema first.\n" + }, + "valid_from": { + "type": "string", + "description": "When the first version takes effect. Defaults to now.\n\nAn RFC 3339 date (`2026-01-01`, read as midnight UTC) or date-time\n(`2026-01-01T00:00:00Z`), to at most millisecond precision.\n", + "example": "2027-01-01T00:00:00Z" + }, + "values": { + "$ref": "#/components/schemas/VariantValues" + } + } + }, + "VariantConditions": { + "type": "object", + "additionalProperties": true, + "required": [ + "default" + ], + "description": "A variant's pinned conditions as a reader sees them: the pins the schema declares, plus a\nboolean `default` saying whether this is the entity's fallback.\n\n`default` is always present and always a boolean, so a client can branch on \"did I get the\nfallback?\" without knowing how one is stored. The reserved condition a fallback is actually\npinned under never appears here.\n", + "properties": { + "default": { + "type": "boolean" + } + }, + "example": { + "postal_code": "46045", + "default": false + } + }, + "PinnedConditions": { + "type": "object", + "additionalProperties": true, + "description": "The situation this variant applies to: a flat map keyed by condition name, as the entity's\nschema declares them. A condition left out is a wildcard — the variant applies whatever the\ncontext says for it, which is what makes adding a condition to a schema non-breaking for the\nvariants that already exist.\n\nExact values only. A predicate is a read-side thing — a resolve context or a listing's\ncondition filter — and is never stored: what a variant applies to is one situation, not a\nrange of them.\n\nValues are typed by their condition and stored canonicalized for that type: a `date` becomes\nmillisecond-precision UTC, a `daterange` an object carrying `from` and `until` where an empty\nstring is an open end, a `location` of format `zipcode` the postal code itself and one of\nformat `zipcode_town` an object carrying both. A `select` value must be a string, and must\nbe one the condition's `options` declare, which is always a closed vocabulary.\n\n`default`, and any name beginning with `_`, are reserved for the server and cannot be pinned\nhere. Whether a variant is the entity's fallback is set through the request's `default` flag.\n", + "example": { + "postal_code": "46045" + } + }, + "VariantValues": { + "type": "object", + "additionalProperties": true, + "description": "The attribute values this version overrides on the base entity, keyed by attribute name.\n\nOnly attributes currently declaring `overridable_attribute` are applied. Metadata fields\n(anything underscore-prefixed), readonly attributes, hidden attributes, computed attributes,\nattributes of a type no variant may override and non-overridable attributes present here are\nnot applied rather than rejected, and every one but the metadata is named in the write's\n`warnings`, so a client working from a slightly stale schema snapshot still succeeds instead\nof failing on fields it could not have known to drop, and still learns which of them did not\nland. Metadata is never named, since a client echoing back a payload it read carries it in\nevery body. An attribute's `render_condition` says when to show it and has no\nbearing on whether a variant may override it.\n\nNot applied means *not updated*, never *removed*: a value already stored for an attribute that\nis not currently overridable is preserved, so removing and restoring the flag deactivates and\nthen reactivates the same override. An append seeds the attributes the variant may not\noverride from the version in effect at its own `valid_from`, so its stored values are not a\npure function of the body that wrote it; a variant's first version, and an append dated before\nthe variant's earliest version, inherit nothing.\n\nA composite price's `price_components` is an ordinary overridable relation attribute. A\ncomposite variant's override references different component *entities*, never a variant or a\nversion of one, and holds whatever a relation attribute ordinarily holds — this API defines no\nreference shape of its own.\n", + "example": { + "unit_amount": 2499, + "unit_amount_decimal": "24.99" + } + }, + "CreatedVariant": { + "type": "object", + "required": [ + "variant_id", + "entity_id", + "schema", + "conditions", + "valid_from", + "values", + "_created_at", + "_updated_at", + "_revision", + "warnings" + ], + "properties": { + "variant_id": { + "type": "string", + "description": "Server-generated, always, and never accepted from a client. This is the durable key orders\nand contracts pin.\n", + "example": "var-46045" + }, + "entity_id": { + "type": "string", + "example": "price-sp26d1yo" + }, + "schema": { + "$ref": "#/components/schemas/ConditionalEntitySlug" + }, + "conditions": { + "allOf": [ + { + "$ref": "#/components/schemas/VariantConditions" + } + ], + "description": "The situation this variant applies to, plus the boolean `default` discriminator — the\nsame shape `_conditions` has on a resolved payload.\n" + }, + "valid_from": { + "type": "string", + "description": "When the first version takes effect, canonicalized to millisecond-precision UTC.", + "example": "2027-01-01T00:00:00.000Z" + }, + "values": { + "$ref": "#/components/schemas/VariantValues" + }, + "_created_at": { + "type": "string", + "description": "When the first version was created.", + "readOnly": true + }, + "_updated_at": { + "type": "string", + "description": "When the first version was last written.", + "readOnly": true + }, + "_revision": { + "type": "number", + "description": "The revision a later write to this version must carry to be accepted.\n", + "readOnly": true + }, + "warnings": { + "type": "array", + "description": "Things worth knowing that did not stop the write. Empty in the ordinary case — a client\nreads its length rather than branching on its absence.\n", + "items": { + "$ref": "#/components/schemas/WriteWarning" + } + } + } + }, + "WriteWarning": { + "description": "Something worth knowing that did not stop a write.\n\nOne vocabulary for every write, so a client branches on what happened rather than on which\nendpoint it called. `code` and `message` are the only two fields every code shares; everything\nelse lives in a `details` object typed per code, so narrowing on `code` yields a payload the\nclient can read rather than an untyped bag. A write raises each code at most once, and in the\nordinary case raises none of them.\n", + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "description": "This entity is nearing the number of variants it may hold. Surfaced rather than rejected,\nso an importer finds out with a whole run's notice instead of discovering the limit\nhalfway through a refresh.\n", + "required": [ + "code", + "message", + "details" + ], + "properties": { + "code": { + "type": "string", + "enum": [ + "VARIANT_COUNT_APPROACHING_CAP" + ] + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "additionalProperties": false, + "required": [ + "variant_count", + "cap" + ], + "properties": { + "variant_count": { + "type": "number", + "description": "Variants this entity holds, including the one just written." + }, + "cap": { + "type": "number", + "description": "Variants this entity may hold. Configurable per deploy, the same value for every\norganization on it.\n" + } + } + } + } + }, + { + "type": "object", + "additionalProperties": false, + "description": "What resolves **now** changed, other than by a newer version taking effect: the version in\neffect was written behind, or removed.\n", + "required": [ + "code", + "message", + "details" + ], + "properties": { + "code": { + "type": "string", + "enum": [ + "ACTIVE_VERSION_CHANGED" + ] + }, + "message": { + "type": "string" + }, + "details": { + "$ref": "#/components/schemas/VersionMoved" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "description": "What a past-dated (`as_of`) read returns changed: the write landed on, or created, a\nversion dated in the past. The version in effect is one of those whenever its own date has\npassed, which is the ordinary case — it covers every instant from that date until now. A\nversion dated now or later covers no past instant and is not reported here.\n", + "required": [ + "code", + "message", + "details" + ], + "properties": { + "code": { + "type": "string", + "enum": [ + "SUPERSEDED_VERSION_WRITTEN" + ] + }, + "message": { + "type": "string" + }, + "details": { + "$ref": "#/components/schemas/VersionMoved" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "description": "Attributes named in the request body that the write did not store, whatever the reason.\nThe write itself succeeded: an attribute a variant may not override is left alone rather\nthan making the whole call fail, so a client working from a slightly stale schema snapshot\nstill succeeds instead of failing on fields it could not have known to drop.\n\nOne entry per attribute, each with its own reason, so a client that only cares about typos\nfilters the entries by `reason` rather than branching on a second code.\n", + "required": [ + "code", + "message", + "details" + ], + "properties": { + "code": { + "type": "string", + "enum": [ + "ATTRIBUTES_NOT_APPLIED" + ] + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "additionalProperties": false, + "required": [ + "attributes" + ], + "properties": { + "attributes": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/components/schemas/InertOverride" + } + } + } + } + } + } + ] + }, + "VersionMoved": { + "type": "object", + "additionalProperties": false, + "description": "Which version a write moved, and which one was in effect while it did.\n", + "required": [ + "valid_from" + ], + "properties": { + "valid_from": { + "type": "string", + "description": "The version this write created, changed or removed.", + "example": "2026-08-01T00:00:00.000Z" + }, + "active_valid_from": { + "type": "string", + "description": "The version in effect when the write landed, before it did. Absent when the variant had\nnone — every version of it still scheduled.\n\nMay lag the variant's timeline by milliseconds, so a version written moments earlier may\nnot be named here. Advisory, like the warning carrying it: nothing branches on it except a\nhuman reading the message.\n", + "example": "2026-01-01T00:00:00.000Z" + } + } + }, + "InertOverride": { + "type": "object", + "additionalProperties": false, + "description": "One override that did not apply, and why.\n\nThe same entry on both sides of the feature: a write reports the attributes in its body it did\nnot store, and a resolved payload reports the stored overrides composition did not apply. Those\nare the same fact observed at two moments, so a client learns one shape and reads it in both\nplaces.\n", + "required": [ + "attribute", + "reason" + ], + "properties": { + "attribute": { + "type": "string", + "description": "The attribute's name, as the request body or the stored version spells it.", + "example": "unit_amount" + }, + "reason": { + "$ref": "#/components/schemas/InertOverrideReason" + } + } + }, + "InertOverrideReason": { + "type": "string", + "description": "Why one override did not apply.\n\n- `ATTRIBUTE_NOT_OVERRIDABLE`: the entity's schema declares the attribute but has not granted\n it `overridable_attribute`. Granting the flag is an ordinary schema edit, which makes this\n the reason most often worth acting on.\n- `ATTRIBUTE_READONLY`: the attribute is declared readonly, and a readonly attribute cannot\n be granted the flag.\n- `ATTRIBUTE_HIDDEN`: the attribute is declared hidden, and a hidden attribute cannot be\n granted the flag.\n- `ATTRIBUTE_COMPUTED`: the attribute's value is derived (`type: computed` or\n `computed: true`) rather than stored, so an override would be recomputed away.\n- `ATTRIBUTE_UNDECLARED`: the entity's schema declares no attribute of that name. On a write\n that is usually a typo; on a resolved payload it is a stored override whose attribute has\n since left the schema — a stored value outlives the flag being withdrawn, so it can outlive\n its own attribute too. This API keeps no record of what a schema once declared, so it states\n only the observable fact and does not distinguish the two.\n- `TYPE_NOT_OVERRIDABLE`: the attribute's type is not one a variant may override, whatever\n the schema says about that particular attribute.\n- `CAPABILITY_NOT_OVERRIDABLE`: the attribute is contributed by a capability rather than\n declared on the entity's schema. Published for completeness and not emitted in this version,\n in which no capability attribute can be overridden at all.\n", + "enum": [ + "ATTRIBUTE_NOT_OVERRIDABLE", + "ATTRIBUTE_READONLY", + "ATTRIBUTE_HIDDEN", + "ATTRIBUTE_COMPUTED", + "ATTRIBUTE_UNDECLARED", + "TYPE_NOT_OVERRIDABLE", + "CAPABILITY_NOT_OVERRIDABLE" + ] + }, + "DeletedVariant": { + "type": "object", + "required": [ + "variant_id", + "entity_id", + "schema", + "tuple_released", + "versions_deleted" + ], + "properties": { + "variant_id": { + "type": "string", + "example": "var-46045" + }, + "entity_id": { + "type": "string", + "example": "price-sp26d1yo" + }, + "schema": { + "$ref": "#/components/schemas/ConditionalEntitySlug" + }, + "tuple_released": { + "type": "boolean", + "description": "Whether this call is the one that freed the variant's combination of condition values.\n`false` where an earlier, interrupted attempt had already freed it — the delete still\nsucceeded, and the combination was already reusable.\n" + }, + "versions_deleted": { + "type": "number", + "description": "Version rows this call removed." + } + } + }, + "VariantVersion": { + "type": "object", + "description": "One version of one variant: the attribute overrides it carries, the instant it takes effect,\nand the variant it belongs to.\n\nThese are the version's **own** overrides, not the base entity overlaid with them — this is\nwhat an editing screen loads and saves, and what it edits is the overrides. Composing them onto\nthe entity is what `:resolve` answers.\n", + "required": [ + "variant_id", + "entity_id", + "schema", + "conditions", + "valid_from", + "values", + "_created_at", + "_updated_at", + "_revision" + ], + "properties": { + "variant_id": { + "type": "string", + "example": "var-46045" + }, + "entity_id": { + "type": "string", + "example": "price-sp26d1yo" + }, + "schema": { + "$ref": "#/components/schemas/ConditionalEntitySlug" + }, + "conditions": { + "allOf": [ + { + "$ref": "#/components/schemas/VariantConditions" + } + ], + "description": "The situation the variant applies to, plus the boolean `default` discriminator. A property\nof the variant rather than of this version: every version of a variant carries the same\none, and no version write can change it.\n" + }, + "valid_from": { + "type": "string", + "description": "When this version takes effect, canonicalized to millisecond-precision UTC. A version's\nidentity within its variant — it never moves.\n", + "example": "2027-01-01T00:00:00.000Z" + }, + "values": { + "$ref": "#/components/schemas/VariantValues" + }, + "_created_at": { + "type": "string", + "description": "When this version was created.", + "readOnly": true + }, + "_updated_at": { + "type": "string", + "description": "When this version was last written.", + "readOnly": true + }, + "_revision": { + "type": "integer", + "description": "The revision a write to this version must carry to be accepted. Always current: every read\nthat returns one is strongly consistent, so it is never a marker a write would be refused\nfor having read too early.\n", + "readOnly": true, + "example": 3 + } + } + }, + "WrittenVariantVersion": { + "description": "A version as a write left it, together with anything the write moved.\n", + "allOf": [ + { + "$ref": "#/components/schemas/VariantVersion" + }, + { + "type": "object", + "required": [ + "warnings" + ], + "properties": { + "warnings": { + "type": "array", + "description": "What this write moved, and anything in the body it did not store. Empty in the\nordinary case — a client reads its length rather than branching on its absence.\n", + "items": { + "$ref": "#/components/schemas/WriteWarning" + } + } + } + } + ] + }, + "DeletedVariantVersion": { + "type": "object", + "required": [ + "variant_id", + "entity_id", + "schema", + "valid_from", + "warnings" + ], + "properties": { + "variant_id": { + "type": "string", + "example": "var-46045" + }, + "entity_id": { + "type": "string", + "example": "price-sp26d1yo" + }, + "schema": { + "$ref": "#/components/schemas/ConditionalEntitySlug" + }, + "valid_from": { + "type": "string", + "description": "The version removed, canonicalized to millisecond-precision UTC.", + "example": "2027-01-01T00:00:00.000Z" + }, + "warnings": { + "type": "array", + "description": "What the delete moved, if anything. Empty when a scheduled version was withdrawn — a\nclient reads its length rather than branching on its absence.\n", + "items": { + "$ref": "#/components/schemas/WriteWarning" + } + } + } + }, + "AppendVersionRequest": { + "type": "object", + "additionalProperties": false, + "required": [ + "values" + ], + "properties": { + "valid_from": { + "type": "string", + "description": "When this version takes effect. Defaults to now.\n\nAn RFC 3339 date (`2026-01-01`, read as midnight UTC) or date-time\n(`2026-01-01T00:00:00Z`), to at most millisecond precision.\n\nA date in the past is accepted and answered with warnings, never refused. A date the\nvariant already has a version at is refused as `VERSION_CONFLICT`.\n\n**Omit this to mean \"now\"** — that is the only spelling of now that is reliably silent. A\ntimestamp taken from the caller's own clock is already some milliseconds old when the\nserver judges it, which makes it a backdate, however small, and it is answered with the\nwarnings a backdate earns.\n", + "example": "2027-01-01T00:00:00Z" + }, + "values": { + "allOf": [ + { + "$ref": "#/components/schemas/VariantValues" + } + ], + "description": "The attribute overrides this version carries. An append seeds the attributes the variant\nmay not override from the version in effect at this version's own `valid_from` and then\napplies these values over them, so the stored values are not a pure function of this\nbody. An append dated before the variant's earliest version inherits nothing.\n" + }, + "conditions": { + "allOf": [ + { + "$ref": "#/components/schemas/PinnedConditions" + } + ], + "description": "Optional, and never applied: a variant's conditions are fixed when it is created. Accepted\nonly so that a client building its body from the version it loaded is not forced to strip\nthem out, and refused when they describe a different situation from the stored one.\n" + } + } + }, + "ReplaceVersionRequest": { + "type": "object", + "additionalProperties": false, + "required": [ + "values", + "_revision" + ], + "properties": { + "values": { + "allOf": [ + { + "$ref": "#/components/schemas/VariantValues" + } + ], + "description": "The complete set of attribute overrides this version carries. An overridable attribute\nabsent from here stops being overridden.\n\nAttributes the variant may not override are not applied where this carries them, and\ntheir **stored value is kept rather than dropped**.\n" + }, + "_revision": { + "type": "integer", + "minimum": 1, + "description": "The revision marker read from the version being written. The write is refused with\n`WRITE_CONFLICT` if the version has been written since.\n", + "example": 3 + }, + "valid_from": { + "type": "string", + "description": "Optional, and never applied. Accepted when it names the version being addressed — so a\nclient building its body from what it loaded need not strip it out — and refused when it\nnames another: a version's `valid_from` is its identity, and moving it is an append and a\ndelete rather than an edit.\n" + }, + "conditions": { + "allOf": [ + { + "$ref": "#/components/schemas/PinnedConditions" + } + ], + "description": "Optional, and never applied: a variant's conditions are fixed when it is created. Refused\nwhen they describe a different situation from the stored one.\n" + } + } + }, + "PatchVersionRequest": { + "type": "object", + "additionalProperties": false, + "required": [ + "values", + "_revision" + ], + "properties": { + "values": { + "allOf": [ + { + "$ref": "#/components/schemas/VariantValues" + } + ], + "description": "Only the attribute overrides to change. Everything not mentioned is left as stored.\n\n`null` is a value like any other here rather than a deletion; to stop overriding an\nattribute, send the complete snapshot without it through the replace operation.\n" + }, + "_revision": { + "type": "integer", + "minimum": 1, + "description": "The revision marker read from the version being written. The write is refused with\n`WRITE_CONFLICT` if the version has been written since.\n", + "example": 3 + }, + "valid_from": { + "type": "string", + "description": "Optional, never applied, and refused when it names a version other than the one addressed." + }, + "conditions": { + "allOf": [ + { + "$ref": "#/components/schemas/PinnedConditions" + } + ], + "description": "Optional, and never applied. A partial update that tries to change a pinned condition value\nis refused.\n" + } + } + }, + "ListVariantsRequest": { + "type": "object", + "additionalProperties": false, + "description": "How to narrow and page a variant listing. Every property is optional, so `{}` is a valid body\nand asks for the first ten variants of the entity in `variant_id` order — the body itself is\nrequired, and an omitted one is a request-validation `400` rather than an unnarrowed page.\n\n`conditions` and `search` narrow independently and a variant has to satisfy both.\n", + "properties": { + "conditions": { + "$ref": "#/components/schemas/VariantConditionFilter" + }, + "search": { + "type": "string", + "description": "Free text matched against the variant's pinned values — how someone finds one postal code\namong 800,000.\n\nMatches `string`, `select` and `number` pins only. A `location` pin is stored as an array\nof its format's parts and a `daterange` pin as an object carrying `from` and `until`, so\nneither is text a user could have typed.\n", + "example": "460" + }, + "sort": { + "type": "string", + "description": "`conditions.:asc` or `conditions.:desc`, for a `string`, `select`, `number` or\n`date` pin. Anything else — another field, or a pin of another type — is a `400`.\n\n**`variant_id:asc` is appended by the server**, always, so the order is total: many\nvariants can pin one postal code, and without a tiebreaker a cursor would repeat or skip\nrows between pages. Asking for no sort is `variant_id:asc` alone.\n", + "example": "conditions.postal_code:asc" + }, + "from": { + "type": "integer", + "minimum": 0, + "default": 0, + "description": "The offset to read from. Not read when a `cursor` is sent, which carries its own position.\n\nBounded by the search index's offset window, together with `size`: the window bounds the\nlast row a page may contain, so the final servable offset is the window minus the page\nsize. A page reaching past it is `OFFSET_WINDOW_EXCEEDED`, naming all three numbers,\nrather than a page clamped back inside it as entity listing does. The window is the\ndeploy's: read its size from the error, not from here.\n" + }, + "size": { + "type": "integer", + "minimum": 1, + "default": 10, + "description": "Rows per page. Clamped silently at 1000, as entity listing's is.\n" + }, + "cursor": { + "type": "string", + "description": "Continue from a previous response's `next`, which is where a caller goes when the offset\nwindow runs out. Opaque: it encodes the position and the listing it was issued for, and\nnothing a client should read or construct.\n\n`conditions`, `search` and `sort` must be the ones the cursor was issued with — a cursor\nresumes one listing, and cannot mean anything against a different one. A cursor that is\nmalformed, or does not match the listing it is sent with, is a `400`.\n", + "example": "eyJmcm9tIjoyNSwibGlzdGluZyI6IjNmOWMxZTJhIn0" + } + } + }, + "VariantTreeRequest": { + "type": "object", + "additionalProperties": false, + "description": "The variants list's request plus `as_of`, the instant each row's version is selected at.\n`size` is clamped at 100 here; every other shared property means what it means on the list.\n", + "properties": { + "conditions": { + "$ref": "#/components/schemas/VariantConditionFilter" + }, + "search": { + "type": "string", + "description": "Free text matched against the variant's pinned values — how someone finds one postal code\namong 800,000.\n\nMatches `string`, `select` and `number` pins only. A `location` pin is stored as an array\nof its format's parts and a `daterange` pin as an object carrying `from` and `until`, so\nneither is text a user could have typed.\n", + "example": "460" + }, + "sort": { + "type": "string", + "description": "`conditions.:asc` or `conditions.:desc`, for a `string`, `select`, `number` or\n`date` pin. Anything else — another field, or a pin of another type — is a `400`.\n\n**`variant_id:asc` is appended by the server**, always, so the order is total: many\nvariants can pin one postal code, and without a tiebreaker a cursor would repeat or skip\nrows between pages. Asking for no sort is `variant_id:asc` alone.\n", + "example": "conditions.postal_code:asc" + }, + "from": { + "type": "integer", + "minimum": 0, + "default": 0, + "description": "The offset to read from. Not read when a `cursor` is sent, which carries its own position.\n\nBounded by the search index's offset window, together with `size`: the window bounds the\nlast row a page may contain, so the final servable offset is the window minus the page\nsize. A page reaching past it is `OFFSET_WINDOW_EXCEEDED`, naming all three numbers,\nrather than a page clamped back inside it as entity listing does. The window is the\ndeploy's: read its size from the error, not from here.\n" + }, + "size": { + "type": "integer", + "minimum": 1, + "default": 10, + "description": "Rows per page. Clamped silently at 100, a tenth of the variants list's cap: every row here\ncosts its own version lookup.\n" + }, + "cursor": { + "type": "string", + "description": "Continue from a previous response's `next`, which is where a caller goes when the offset\nwindow runs out. Opaque: it encodes the position and the listing it was issued for, and\nnothing a client should read or construct.\n\n`conditions`, `search` and `sort` must be the ones the cursor was issued with — a cursor\nresumes one listing, and cannot mean anything against a different one. A cursor that is\nmalformed, or does not match the listing it is sent with, is a `400`.\n", + "example": "eyJmcm9tIjoyNSwibGlzdGluZyI6IjNmOWMxZTJhIn0" + }, + "as_of": { + "type": "string", + "description": "The instant each row's version is selected at — the version with the latest `valid_from`\nat or before it. Defaults to now. The same selector, by the same rule, as `:resolve`'s.\n\nA variant whose first version is later than this is not dropped the way context matching\ndrops it: it is a row with `status: scheduled` carrying that upcoming first version, which\nis what makes a staged price visible on the editing screen.\n\nAn RFC 3339 date (`2026-01-01`, read as midnight UTC) or date-time\n(`2026-01-01T00:00:00Z`), to at most millisecond precision.\n", + "example": "2027-03-15T00:00:00Z" + } + } + }, + "VariantConditionFilter": { + "type": "object", + "additionalProperties": true, + "description": "Which pins a variant must carry to be listed: a flat map keyed by condition name, as the\nentity's schema declares them. A condition left out of the map is not filtered on at all.\n\nEach value is either an exact value, typed by its condition, or a single-operator predicate\nobject — the same seven a resolve context accepts, because in both cases a predicate is\napplied to the variant's *pinned* value, so nothing about matching moves:\n\n- `{ \"lt\": v }`, `{ \"lte\": v }`, `{ \"gt\": v }`, `{ \"gte\": v }` — order against a `number` or\n `date` condition.\n- `{ \"in\": [...] }` — membership, against a `string`, `select` or `number` condition.\n- `{ \"between\": \"2026-03-01\" }` — the explicit spelling of `daterange` containment; a plain\n date supplied for a `daterange` condition means the same thing.\n- `{ \"exists\": true }` — pinned to any value. `{ \"exists\": false }` — the condition left\n unpinned.\n\nAn `in` list carries at most 50,000 values; a longer one is `CONTEXT_FORMAT_INVALID`. To\nfilter on a condition whatever its pinned value, send `{ \"exists\": true }` rather than\nenumerating its vocabulary.\n\n**A variant matches only where it pins the condition** — the one place a filter and a resolve\ncontext differ. On `:resolve` a condition a variant does not pin matches any value; here,\nasking for postal code 46045 does not return the variants that pin no postal code at all.\n`{ \"exists\": false }` is how those are asked for.\n\nValues are typed and canonicalized exactly as a resolve context's are, by the same code, so\none instant written two ways filters the same way either way. A condition the schema does not\ndeclare is `CONDITION_UNDEFINED`, a predicate its type does not support is\n`OPERATOR_UNSUPPORTED`, and a value malformed for its type is `CONTEXT_FORMAT_INVALID`.\n\n**`default` is accepted here**, as the exact boolean every row reports it as: `true` selects\nthe entity's fallback variant, `false` every variant that is not it.\n\nIt takes no predicate. `default` is not a condition and has no type, so ordering and\nmembership have nothing to apply to. It is also the one key the pinned-only rule above does\nnot describe literally: a variant that is not the fallback does not pin the marker to `false`,\nit does not pin it at all, so `false` selects the variants that leave it unpinned.\n\nNames beginning with `_` stay reserved for the server and cannot be filtered on — `_default`,\nthe marker a fallback is actually stored under, included. `default` is the spelling every read\nreports and the only one this accepts.\n", + "example": { + "postal_code": "46045", + "consumption": { + "lt": 5000 + } + } + }, + "VariantList": { + "type": "object", + "required": [ + "hits", + "results" + ], + "properties": { + "hits": { + "type": "integer", + "description": "How many variants match, exactly, at any depth — not how many this page carries. Exact,\nas entity listing's is.\n", + "example": 8128 + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VariantListRow" + } + }, + "next": { + "type": "string", + "description": "The cursor that continues this listing, absent on the last page. Send it back as `cursor`,\nwith the same filter, search and sort.\n", + "example": "eyJmcm9tIjoyNSwibGlzdGluZyI6IjNmOWMxZTJhIn0" + } + } + }, + "VariantListRow": { + "type": "object", + "description": "One variant as a listing reports it: which variant it is and what it pins.\n\nNo `_revision` — a write re-reads its version through that version's own `GET` — and no\n`_inert_overrides`, since a listing reports what is stored and only `:resolve` honours the\nschema.\n", + "required": [ + "variant_id", + "entity_id", + "schema", + "conditions" + ], + "properties": { + "variant_id": { + "type": "string", + "example": "var-46045" + }, + "entity_id": { + "type": "string", + "example": "price-sp26d1yo" + }, + "schema": { + "$ref": "#/components/schemas/ConditionalEntitySlug" + }, + "conditions": { + "allOf": [ + { + "$ref": "#/components/schemas/VariantConditions" + } + ], + "description": "The situation this variant applies to, plus the boolean `default` discriminator — the same\nshape a variant write returns.\n\nMay lag: a variant just created can be missing from a page, and one just deleted can still\nbe on it. The pins shown for a variant are never stale, since a variant's conditions are\nimmutable after creation.\n" + } + } + }, + "VariantTree": { + "type": "object", + "required": [ + "hits", + "results" + ], + "properties": { + "hits": { + "type": "integer", + "description": "How many variants match, exactly, at any depth — not how many this page carries. Exact,\nas entity listing's is.\n", + "example": 8128 + }, + "results": { + "type": "array", + "description": "One row per matching variant, in the requested order.\n\nA variant the index still holds but whose versions are already gone — a variant\nmid-delete — is **omitted** rather than returned without a `version`. So `results` can be\nshorter than `hits` implies, for the width of that lag and no longer. Paging still ends\nwhere `next` does.\n", + "items": { + "$ref": "#/components/schemas/VariantTreeRow" + } + }, + "next": { + "type": "string", + "description": "The cursor that continues this listing, absent on the last page. Send it back as `cursor`,\nwith the same filter, search and sort.\n\n`as_of` is free to change between pages. It selects which version each row shows and has\nno bearing on which variants match or on the order they come back in, so a screen whose\ndate picker moves mid-listing keeps paging rather than starting over.\n", + "example": "eyJmcm9tIjoyNSwibGlzdGluZyI6IjNmOWMxZTJhIn0" + } + } + }, + "VariantTreeRow": { + "type": "object", + "description": "A listing row plus the one version the tree view shows for it, and the status saying which\nversion that is.\n", + "required": [ + "variant_id", + "entity_id", + "schema", + "conditions", + "status", + "version" + ], + "properties": { + "variant_id": { + "type": "string", + "example": "var-46045" + }, + "entity_id": { + "type": "string", + "example": "price-sp26d1yo" + }, + "schema": { + "$ref": "#/components/schemas/ConditionalEntitySlug" + }, + "conditions": { + "allOf": [ + { + "$ref": "#/components/schemas/VariantConditions" + } + ], + "description": "The situation this variant applies to, plus the boolean `default` discriminator — the same\nshape a variant write returns.\n\nMay lag: a variant just created can be missing from a page, and one just deleted can still\nbe on it. The pins shown for a variant are never stale, since a variant's conditions are\nimmutable after creation.\n" + }, + "status": { + "$ref": "#/components/schemas/VariantTreeRowStatus" + }, + "version": { + "allOf": [ + { + "$ref": "#/components/schemas/VariantVersionSnapshot" + } + ], + "description": "The version this row shows: the one in effect at `as_of`, or — where every version of the\nvariant is still ahead of it — that upcoming first one. `status` says which of the two it\nis.\n\nAlways present. A variant always has at least one version, and the one case where a row\ncould have none — a variant whose delete has removed its versions but not yet its index\ndocument — is omitted from `results` instead, so a consumer never reads this field\ndefensively.\n" + } + } + }, + "VariantTreeRowStatus": { + "type": "string", + "description": "Whether a tree row's version is the one in effect at `as_of`, or one still ahead of it.\n\nExactly two values, and every row has one: a variant always has at least one version, so\neither a version is in effect at `as_of` or every version of that variant is still to come.\n\n- `active`: `version` is the version with the latest `valid_from` at or before `as_of` —\n the same version `active_valid_from` and `NO_ACTIVE_VERSION` speak of.\n- `scheduled`: the variant's first version is later than `as_of`, and `version` is that\n upcoming first version.\n", + "enum": [ + "active", + "scheduled" + ] + }, + "VariantVersionSnapshot": { + "type": "object", + "description": "One version of one variant as a listing reports it: `VariantVersion` without `_revision`.\n\nThe revision is missing on purpose. An editing screen re-reads the one version it is about to\nwrite through that version's own `GET`, which is strongly consistent, and writes with the\nrevision it gets back.\n\nEverything else is `VariantVersion` field for field, including the variant's `conditions`,\nwhich every version of a variant repeats.\n", + "required": [ + "variant_id", + "entity_id", + "schema", + "conditions", + "valid_from", + "values", + "_created_at", + "_updated_at" + ], + "properties": { + "variant_id": { + "type": "string", + "example": "var-46045" + }, + "entity_id": { + "type": "string", + "example": "price-sp26d1yo" + }, + "schema": { + "$ref": "#/components/schemas/ConditionalEntitySlug" + }, + "conditions": { + "allOf": [ + { + "$ref": "#/components/schemas/VariantConditions" + } + ], + "description": "The situation the variant applies to, plus the boolean `default` discriminator. A property\nof the variant rather than of this version: every version of a variant carries the same\none, and no version write can change it.\n" + }, + "valid_from": { + "type": "string", + "description": "When this version takes effect, canonicalized to millisecond-precision UTC. A version's\nidentity within its variant — it never moves.\n", + "example": "2027-01-01T00:00:00.000Z" + }, + "values": { + "$ref": "#/components/schemas/VariantValues" + }, + "_created_at": { + "type": "string", + "description": "When this version was created.", + "readOnly": true + }, + "_updated_at": { + "type": "string", + "description": "When this version was last written.", + "readOnly": true + } + } + }, + "VariantVersionList": { + "type": "object", + "required": [ + "results" + ], + "properties": { + "results": { + "type": "array", + "description": "A page of the variant's timeline, in the requested `order`.\n", + "items": { + "$ref": "#/components/schemas/VariantVersionSnapshot" + } + }, + "next": { + "type": "string", + "description": "The cursor that continues this timeline, absent only on the last page.\n\nThe only end-of-data signal: a page shorter than `limit`, or an empty one, can still carry\na cursor, so a client pages until this field is absent rather than until a page looks\nshort. Send it back as `cursor`, against the same variant and the same `order`.\n", + "example": "eyJzayI6IlYjcHJpY2Utc3AyNmQxeW8jdmFyLTQ2MDQ1IzIwMjYtMDEtMDFUMDA6MDA6MDAuMDAwWiIsIm9yZGVyIjoiYXNjIn0" + } + } + }, + "BatchUpsertVariantsRequest": { + "type": "object", + "additionalProperties": false, + "description": "A batch of variant writes under one schema, each item naming the entity it writes to.\n", + "required": [ + "items" + ], + "properties": { + "correlation_id": { + "type": "string", + "description": "An opaque string the caller uses to tie this response to the file and cycle that produced\nit. Echoed back verbatim, only when it was sent, and never interpreted.\n", + "example": "tariff-refresh-2027-01" + }, + "items": { + "type": "array", + "minItems": 1, + "maxItems": 100, + "description": "The writes to apply, in the order they should apply where two of them address the same\nvariant. At most 100 per call — a limit on one request, distinct from the per-entity\nvariant cap, which limits stored state.\n", + "items": { + "$ref": "#/components/schemas/BatchUpsertItem" + } + } + } + }, + "BatchUpsertItem": { + "type": "object", + "additionalProperties": false, + "description": "One variant write: the entity it belongs to, the situation it applies to, and the values it\ncarries — the single-item create's body plus `entity_id`. The two differ on `conditions`: on\na create an existing tuple is `TUPLE_CONFLICT`, and here it is a version appended to the\nvariant already holding it.\n\nThere is no `variant_id`. An upsert creates variants that have no id yet.\n", + "required": [ + "entity_id", + "values" + ], + "properties": { + "entity_id": { + "type": "string", + "description": "The conditional entity this item writes to. On the item rather than in the path, so one\ncall can refresh a whole tariff hierarchy — a composite price and its components\ntogether.\n", + "example": "price-sp26d1yo" + }, + "conditions": { + "$ref": "#/components/schemas/PinnedConditions" + }, + "default": { + "type": "boolean", + "default": false, + "description": "Mark this variant as the entity's fallback, exactly as a create does: a property of the\nvariant, never an entry in `conditions`. An item that pins nothing and is not the default\nis `VARIANT_UNPINNED` — the empty postal-code column in a source file.\n" + }, + "valid_from": { + "type": "string", + "description": "When the version this item writes takes effect. Omitted, it is a current-state,\nlast-write-wins write with no `skipped` detection.\n\nAn RFC 3339 date (`2026-01-01`, read as midnight UTC) or date-time\n(`2026-01-01T00:00:00Z`), to at most millisecond precision.\n\nA past instant is written like any other and answered with the timeline warnings on this\nitem, so an importer stamping one `valid_from` across a batch sees them on every item.\n", + "example": "2027-01-01T00:00:00Z" + }, + "values": { + "$ref": "#/components/schemas/VariantValues" + } + } + }, + "BatchDeleteVariantsRequest": { + "type": "object", + "additionalProperties": false, + "description": "A batch of variant and version deletes under one schema, each item naming the entity it\nremoves from.\n", + "required": [ + "items" + ], + "properties": { + "correlation_id": { + "type": "string", + "description": "An opaque string the caller uses to tie this response to the file and cycle that produced\nit. Echoed back verbatim, only when it was sent, and never interpreted.\n", + "example": "postal-code-cleanup-2026-09" + }, + "items": { + "type": "array", + "minItems": 1, + "maxItems": 100, + "description": "The deletes to apply, in the order they should apply where two of them address the same\nvariant — which is decided after every condition tuple has been resolved to a variant id,\nso the order holds across the two addressing forms. At most 100 per call.\n", + "items": { + "$ref": "#/components/schemas/BatchDeleteItem" + } + } + } + }, + "BatchDeleteItem": { + "description": "One delete: the variant, addressed by id or by the condition tuple it pins, and optionally\nthe one version of it to remove.\n\nExactly one of the two forms. An item carrying both a `variant_id` and `conditions` matches\nneither branch and is an envelope `400`, since the request validator rejects the body before\nany item runs.\n", + "oneOf": [ + { + "$ref": "#/components/schemas/BatchDeleteByVariantId" + }, + { + "$ref": "#/components/schemas/BatchDeleteByConditions" + } + ] + }, + "BatchDeleteByVariantId": { + "type": "object", + "additionalProperties": false, + "description": "A delete addressing its variant by id — the form a cleanup pass uses after the schema has\ndrifted, since a tuple naming a condition the schema no longer declares addresses nothing.\n", + "required": [ + "entity_id", + "variant_id" + ], + "properties": { + "entity_id": { + "type": "string", + "description": "The conditional entity the variant belongs to. **Required beside `variant_id`, and not\nredundant**: a variant id alone addresses nothing in this API.\n", + "example": "price-sp26d1yo" + }, + "variant_id": { + "type": "string", + "description": "The variant to remove, or whose version to remove.", + "example": "var-46045" + }, + "valid_from": { + "type": "string", + "description": "The one version to remove, by the instant it takes effect. Omitted, the whole variant\ngoes — its tuple, its index registration and every version it accumulated.\n\nAn RFC 3339 date or date-time, to at most millisecond precision, canonicalized before it\nis matched.\n", + "example": "2027-01-01T00:00:00Z" + } + } + }, + "BatchDeleteByConditions": { + "type": "object", + "additionalProperties": false, + "description": "A delete addressing its variant by the situation it applies to — the form an importer uses\nwhen it knows the source rows rather than the ids they produced.\n\n`conditions` is optional because the entity's fallback variant pins nothing: an item\naddressing it sends `default: true` and no `conditions`, exactly as a create marks one.\n\n**An item that addresses no variant is a per-item `VARIANT_UNPINNED`, not an envelope\n`400` and not a `skipped`.** Three shapes reach it: no `conditions` and no `default`,\n`conditions: {}`, and `default: false` alone — an empty postal-code column in a source row,\nserialized one way or another.\n\nTwo more shapes validate here and are refused per item rather than described by the schema:\nan item marking `default` while also pinning `conditions` — a fallback variant applies only\nwhen nothing else does, so it cannot also pin — and an item carrying `valid_from` with no\nvariant addressed at all. Both carry a message and no code, as the create path refuses the\nfirst today.\n", + "required": [ + "entity_id" + ], + "properties": { + "entity_id": { + "type": "string", + "description": "The conditional entity the variant belongs to. Required, as it is beside a `variant_id`.\n", + "example": "price-sp26d1yo" + }, + "conditions": { + "$ref": "#/components/schemas/PinnedConditions" + }, + "default": { + "type": "boolean", + "default": false, + "description": "Address the entity's fallback variant, the one it serves when nothing else applies. A\nproperty of the variant, as it is on a write, never an entry in `conditions`.\n" + }, + "valid_from": { + "type": "string", + "description": "The one version to remove, by the instant it takes effect. Omitted, the whole variant\ngoes.\n\nAn RFC 3339 date or date-time, to at most millisecond precision, canonicalized before it\nis matched.\n", + "example": "2027-01-01T00:00:00Z" + } + } + }, + "BatchUpsertResult": { + "type": "object", + "description": "What a batch upsert did: one entry per item, in request order, and a count per outcome.\n", + "required": [ + "counts", + "results" + ], + "properties": { + "correlation_id": { + "type": "string", + "description": "The `correlation_id` the request carried, echoed only when it was sent.", + "example": "tariff-refresh-2027-01" + }, + "counts": { + "$ref": "#/components/schemas/BatchUpsertCounts" + }, + "results": { + "type": "array", + "description": "One entry per item, **in request order** — position is what maps an outcome back to its\nsource row, and no entry carries an index of its own.\n", + "items": { + "$ref": "#/components/schemas/BatchUpsertResultEntry" + } + } + } + }, + "BatchDeleteResult": { + "type": "object", + "description": "What a batch delete did: one entry per item, in request order, and a count per outcome.\n", + "required": [ + "counts", + "results" + ], + "properties": { + "correlation_id": { + "type": "string", + "description": "The `correlation_id` the request carried, echoed only when it was sent.", + "example": "postal-code-cleanup-2026-09" + }, + "counts": { + "$ref": "#/components/schemas/BatchDeleteCounts" + }, + "results": { + "type": "array", + "description": "One entry per item, **in request order** — position is what maps an outcome back to its\nsource row.\n", + "items": { + "$ref": "#/components/schemas/BatchDeleteResultEntry" + } + } + } + }, + "BatchUpsertOutcome": { + "type": "string", + "description": "What one upsert item did, derived from what was stored rather than from a mode the caller\ndeclared.\n\n- `variant_created`: the condition tuple was unknown, so a variant and its first version were\n created. The entry's `variant_id` is the id an order or contract pins.\n- `version_created`: the tuple was known and had no version at the item's `valid_from`, so\n one was appended. The ordinary monthly-refresh case, and a separate value from\n `variant_created` so an importer's counts can tell \"new postal codes appeared\" from\n \"existing variants got their scheduled adjustment\".\n- `updated`: a version existed at that exact instant and was written in place.\n- `skipped`: reserved for a write whose values are identical to what is stored, so re-running\n an unchanged import reads as a no-op. An item without `valid_from` has no `skipped`\n detection at all.\n- `error`: this item alone failed, and the entry's `error` says why.\n", + "enum": [ + "variant_created", + "version_created", + "updated", + "skipped", + "error" + ] + }, + "BatchDeleteOutcome": { + "type": "string", + "description": "What one delete item did.\n\n- `deleted`: the variant, or the one version the item named, is gone.\n- `skipped`: the item addressed nothing — **the variant or the version**, never the entity. An\n entity that cannot answer the item is an `error` carrying `ENTITY_NOT_FOUND`,\n `ENTITY_TYPE_MISMATCH` or `ENTITY_NOT_CONDITIONAL`.\n- `error`: this item alone failed, and the entry's `error` says why.\n", + "enum": [ + "deleted", + "skipped", + "error" + ] + }, + "BatchUpsertCounts": { + "type": "object", + "additionalProperties": false, + "description": "How many items reached each outcome. Keyed by exactly the values of `BatchUpsertOutcome`, all\nof them present, so a logger reads a count without `?? 0`.\n\n**They sum to the length of `results`.** There is no `total`.\n", + "required": [ + "variant_created", + "version_created", + "updated", + "skipped", + "error" + ], + "properties": { + "variant_created": { + "type": "integer", + "example": 1 + }, + "version_created": { + "type": "integer", + "example": 1 + }, + "updated": { + "type": "integer", + "example": 1 + }, + "skipped": { + "type": "integer", + "example": 1 + }, + "error": { + "type": "integer", + "example": 1 + } + } + }, + "BatchDeleteCounts": { + "type": "object", + "additionalProperties": false, + "description": "How many items reached each outcome. Keyed by exactly the values of `BatchDeleteOutcome`, all\nof them present, and summing to the length of `results`. No `total`.\n", + "required": [ + "deleted", + "skipped", + "error" + ], + "properties": { + "deleted": { + "type": "integer", + "example": 1 + }, + "skipped": { + "type": "integer", + "example": 1 + }, + "error": { + "type": "integer", + "example": 1 + } + } + }, + "BatchUpsertResultEntry": { + "type": "object", + "additionalProperties": false, + "description": "What one upsert item did, and anything worth knowing about it.\n\n**It carries nothing else.** Position in `results` is the contract, so no entry carries an\nindex; nothing the caller sent is echoed back beyond `entity_id`; and there is no `_revision`\n— an editing screen re-reads the version it is about to write through its own `GET`.\n", + "required": [ + "outcome", + "entity_id", + "warnings" + ], + "properties": { + "outcome": { + "$ref": "#/components/schemas/BatchUpsertOutcome" + }, + "entity_id": { + "type": "string", + "description": "The entity this item wrote to, echoed from the item — present whatever happened.", + "example": "price-sp26d1yo" + }, + "variant_id": { + "type": "string", + "description": "The variant this item created or wrote to. Present on every outcome but `error`: for a\n`variant_created` item it is the id an importer needs to pin, and for the rest it is the\nvariant the item's condition tuple resolved to.\n", + "example": "var-46045" + }, + "valid_from": { + "type": "string", + "description": "The version this item wrote, canonicalized to millisecond-precision UTC. Present on every\noutcome but `error`, including for an item that sent none — the server stamps the instant\na current-state write takes effect, and this is where the caller reads it back.\n", + "example": "2027-01-01T00:00:00.000Z" + }, + "warnings": { + "type": "array", + "description": "Things worth knowing that did not stop this item's write. **Always present, and possibly\nempty** — on a `skipped` and an `error` entry too — so a client reads its length rather\nthan branching on its absence, as every other write in this document already asks.\n`skipped` describes what storage did; a warning describes what the request asked for, and\nthe two are not the same fact.\n\nEvery warning fires per item, with no batch-level suppression:\n`VARIANT_COUNT_APPROACHING_CAP` included, even where an entity past its threshold\nproduces it on all 100 entries. A logger dedupes by code.\n", + "items": { + "$ref": "#/components/schemas/WriteWarning" + } + }, + "error": { + "allOf": [ + { + "$ref": "#/components/schemas/ConditionalPricingError" + } + ], + "description": "Why this item failed, present only with `outcome: error`. The same typed shape a\nsingle-item write is refused with, so a per-item failure and a single-item failure are\nread by one client type.\n\nAn item carries the codes variant create raises — `VARIANT_UNPINNED`,\n`CONDITION_UNDEFINED`, `CONDITION_VALUE_INVALID`, `PIN_FORMAT_INVALID`,\n`VARIANT_LIMIT_REACHED`, `WRITE_CONFLICT` for transient contention, and the three the\naddressed entity answers with: `ENTITY_NOT_FOUND`, `ENTITY_TYPE_MISMATCH` and\n`ENTITY_NOT_CONDITIONAL` — less two. Those three are per item because each item names its\nown entity, while `SCHEMA_NOT_FOUND` is the envelope's, since the slug is in the path. `TUPLE_CONFLICT` never appears on an item, and neither does `VERSION_CONFLICT`: a\nguard failure on a brand-new tuple is re-read and re-derived, and an existing\n`valid_from` is a replacement.\n" + } + } + }, + "BatchDeleteResultEntry": { + "type": "object", + "additionalProperties": false, + "description": "What one delete item did, and anything worth knowing about it.\n\nThe same six properties as a batch upsert entry, and it carries nothing else.\n", + "required": [ + "outcome", + "entity_id", + "warnings" + ], + "properties": { + "outcome": { + "$ref": "#/components/schemas/BatchDeleteOutcome" + }, + "entity_id": { + "type": "string", + "description": "The entity this item removed from, echoed from the item — present whatever happened.", + "example": "price-sp26d1yo" + }, + "variant_id": { + "type": "string", + "description": "The variant this item removed, or whose version it removed. Present wherever it is known:\nalways for an item that named one, and for an item addressing a condition tuple only once\nthat tuple resolved. **A `skipped` entry for a tuple no variant pins therefore names no\nvariant.**\n", + "example": "var-46045" + }, + "valid_from": { + "type": "string", + "description": "The version this item removed, canonicalized to millisecond-precision UTC. Absent where\nthe item removed the whole variant, which is what distinguishes the two deletes this one\nendpoint performs.\n", + "example": "2027-01-01T00:00:00.000Z" + }, + "warnings": { + "type": "array", + "description": "Things worth knowing that did not stop this item's delete — chiefly which reads the\nremoval moved: `ACTIVE_VERSION_CHANGED` where what resolves now changed, and\n`SUPERSEDED_VERSION_WRITTEN` where a past-dated read did. Always present and possibly\nempty, on every outcome, as batch upsert's is.\n", + "items": { + "$ref": "#/components/schemas/WriteWarning" + } + }, + "error": { + "allOf": [ + { + "$ref": "#/components/schemas/ConditionalPricingError" + } + ], + "description": "Why this item failed, present only with `outcome: error`. The same typed shape a\nsingle-item delete is refused with.\n\n`LAST_VERSION_UNDELETABLE` is the refusal specific to this endpoint's dated form;\n`VARIANT_UNPINNED` is an item that addresses no variant — no `variant_id`, no\n`default`, and no or empty `conditions`; `ENTITY_NOT_FOUND`, `ENTITY_TYPE_MISMATCH` and\n`ENTITY_NOT_CONDITIONAL` are per item, since each item names its own entity;\n`WRITE_CONFLICT` is transient contention.\nA missing variant or version is not here at all — that is `skipped`.\n" + } + } + }, + "Error": { + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string", + "description": "Error message" + }, + "status": { + "type": "number", + "description": "The HTTP status code" + }, + "cause": { + "type": "string", + "description": "The cause of the error (visible for bad requests - http 400)" + } + } + }, + "ReportedError": { + "description": "The `error` field of an error response: the message, or — where the request itself failed\nvalidation before any handler ran — the validation errors themselves, which those 400s put\nhere in place of a string.\n\nA conditional-pricing operation answers a body its schema rejects with the list, and\neverything else it refuses with the message.\n", + "oneOf": [ + { + "type": "string", + "description": "The message, the same string as `message`.", + "example": "The conditions requested for variant var-46045 are already pinned" + }, + { + "type": "array", + "description": "One entry per validation failure, as the request validator reported it.", + "items": { + "type": "object", + "additionalProperties": true + } + } + ] + }, + "ConditionalPricingError": { + "description": "An error from a conditional-pricing operation, carrying a machine-readable `code` from the\nconditional-pricing vocabulary plus the structured data that code explains, so a client can\nbranch on the kind of failure rather than parse the message.\nReferenced only by the operations that emit these codes; every other operation\nkeeps the plain `Error` shape.\n\n`details` is typed per code. Narrow on `code` and the object under it declares exactly the\nfields that code sends — never a field it does not send, and nothing beyond the declaration —\nso the conflicting variant id, or the value and vocabulary behind a rejected pin, is read\ndirectly.\n\nNot every failure these operations raise is in the vocabulary. A request body that is simply\nmalformed earns a message and nothing to branch on, and is answered with neither `code` nor\n`details` — the last member of the union, so testing `code` for absence is how a client tells\none of those from the twenty-three coded failures.\n", + "allOf": [ + { + "$ref": "#/components/schemas/Error" + }, + { + "type": "object", + "properties": { + "error": { + "allOf": [ + { + "$ref": "#/components/schemas/ReportedError" + } + ], + "description": "What went wrong, in the field responses have always used and every caller to date\nreads. Carries the same string as `message` — which the shared `Error` schema\nrequires — except on a request-validation failure, which puts the list of validation\nerrors here instead.\n" + } + } + }, + { + "oneOf": [ + { + "type": "object", + "description": "No conditional entity type by that slug — the organization has no schema under it, or\nthe schema it has is not a conditional-pricing one.\n\nAddressed to the caller's own path parameter, and the same answer for every operation:\nnothing below a schema can be looked up until the schema itself is known.\n", + "required": [ + "code", + "details" + ], + "properties": { + "code": { + "type": "string", + "enum": [ + "SCHEMA_NOT_FOUND" + ] + }, + "details": { + "type": "object", + "additionalProperties": false, + "required": [ + "schema" + ], + "properties": { + "schema": { + "type": "string", + "description": "The entity type the request addressed.", + "example": "price" + } + } + } + } + }, + { + "type": "object", + "description": "The schema exists and holds no entity with that id.\n\nA wrong entity id is answered here, never as a variant that was never there. An id\nthat *does* exist, under another type, is not this code — the entity was found, and\n`ENTITY_TYPE_MISMATCH` is what says so.\n", + "required": [ + "code", + "details" + ], + "properties": { + "code": { + "type": "string", + "enum": [ + "ENTITY_NOT_FOUND" + ] + }, + "details": { + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "entity_id" + ], + "properties": { + "schema": { + "type": "string", + "description": "The entity type the request addressed.", + "example": "price" + }, + "entity_id": { + "type": "string", + "description": "The conditional entity the request addressed.", + "example": "price-sp26d1yo" + } + } + } + } + }, + { + "type": "object", + "description": "That entity id belongs to an entity of a different type than the `{slug}` segment\nnamed.\n\nA `400`, not a `404`: the entity **was** found. The fix is to correct the slug and\nsend the request again; the entity id and the variant id were right. `actual_schema`\nnames the type the id belongs to, and where that is a conditional entity type it is\nthe slug to send.\n", + "required": [ + "code", + "details" + ], + "properties": { + "code": { + "type": "string", + "enum": [ + "ENTITY_TYPE_MISMATCH" + ] + }, + "details": { + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "entity_id", + "actual_schema" + ], + "properties": { + "schema": { + "type": "string", + "description": "The entity type the request addressed.", + "example": "price" + }, + "entity_id": { + "type": "string", + "description": "The conditional entity the request addressed.", + "example": "price-sp26d1yo" + }, + "actual_schema": { + "type": "string", + "description": "The entity type that id belongs to. Where it is a conditional entity type, it\nis the slug to send instead.\n", + "example": "product" + } + } + } + } + }, + { + "type": "object", + "description": "The entity is of the type the slug named, and is not a conditional one.\n\nA Product, Price or Coupon carries variants only if it was created with\n`is_conditional` set, and that flag is fixed at creation. So this is not a refusal\nanother request can get past: the entity has no conditional capability to address,\nand one that needs it has to be created as such.\n\nA `400`, as `ENTITY_TYPE_MISMATCH` is: the entity was found. Reading and writing the\nentity itself are unaffected; it is these endpoints that do not apply to it.\n", + "required": [ + "code", + "details" + ], + "properties": { + "code": { + "type": "string", + "enum": [ + "ENTITY_NOT_CONDITIONAL" + ] + }, + "details": { + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "entity_id" + ], + "properties": { + "schema": { + "type": "string", + "description": "The entity type the request addressed.", + "example": "price" + }, + "entity_id": { + "type": "string", + "description": "The conditional entity the request addressed.", + "example": "price-sp26d1yo" + } + } + } + } + }, + { + "type": "object", + "description": "This entity has no such variant — nothing to read, write or delete, and nothing that\nwas ever there to have deleted.\n\nRaised only once the entity itself has been established, so it never stands in for a\nwrong entity id or a wrong slug.\n", + "required": [ + "code", + "details" + ], + "properties": { + "code": { + "type": "string", + "enum": [ + "VARIANT_NOT_FOUND" + ] + }, + "details": { + "type": "object", + "additionalProperties": false, + "required": [ + "entity_id", + "variant_id" + ], + "properties": { + "entity_id": { + "type": "string", + "description": "The conditional entity the request addressed.", + "example": "price-sp26d1yo" + }, + "variant_id": { + "type": "string", + "description": "The variant the request addressed.", + "example": "var-46045" + } + } + } + } + }, + { + "type": "object", + "description": "No version at that `valid_from` — never written, or deleted since.\n\nA version is addressed by the exact instant it takes effect from, not by the instant\na read happens to fall in, so this is not \"no version applies then\"; that case is\n`NO_ACTIVE_VERSION`.\n\nResolving a dated address tells an absent version from an absent variant, so a\nvariant that does not exist at all answers `VARIANT_NOT_FOUND` — the same answer the\npaths that name no date give.\n", + "required": [ + "code", + "details" + ], + "properties": { + "code": { + "type": "string", + "enum": [ + "VERSION_NOT_FOUND" + ] + }, + "details": { + "type": "object", + "additionalProperties": false, + "required": [ + "variant_id", + "valid_from" + ], + "properties": { + "variant_id": { + "type": "string", + "description": "The variant the request addressed.", + "example": "var-46045" + }, + "valid_from": { + "type": "string", + "description": "The version the request addressed, by the instant it takes effect from.", + "example": "2027-01-01T00:00:00.000Z" + } + } + } + } + }, + { + "type": "object", + "description": "Nothing applied to the given context, and the entity has no `default` variant to fall\nback to.\n\nEverything the request addressed exists: this is an answer about the organization's\nown data, not a defect to fix — \"we do not serve this situation\".\n\nOnly reachable with `resolve_one`. Without it the same situation is a `200` carrying\nan empty `results`, since a set of applicable variants can legitimately be empty; it\nis asking for exactly one answer that turns having none into a failure.\n", + "required": [ + "code", + "details" + ], + "properties": { + "code": { + "type": "string", + "enum": [ + "NO_MATCHES" + ] + }, + "details": { + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "entity_id" + ], + "properties": { + "schema": { + "type": "string", + "description": "The entity type the request addressed.", + "example": "price" + }, + "entity_id": { + "type": "string", + "description": "The conditional entity the resolve was scoped to.", + "example": "price-sp26d1yo" + } + } + } + } + }, + { + "type": "object", + "description": "The variant has no version in effect at the instant asked about — raised by the\nshorthand reads and writes that address \"the version in effect\" without naming a\ndate, and by a pinned `:resolve` whose variant has no version in effect at `as_of`.\n\nRelative to `as_of`, and not a claim that every version is scheduled: a variant that\nis live today has none in effect at an instant before its first `valid_from` either.\nThe ordinary case is a variant staged ahead of its launch, which is a variant waiting\nrather than a variant broken — address one of its versions by `valid_from` to read or\nedit it before it takes effect.\n", + "required": [ + "code", + "details" + ], + "properties": { + "code": { + "type": "string", + "enum": [ + "NO_ACTIVE_VERSION" + ] + }, + "details": { + "type": "object", + "additionalProperties": false, + "required": [ + "variant_id", + "as_of" + ], + "properties": { + "variant_id": { + "type": "string", + "description": "The variant the request addressed.", + "example": "var-46045" + }, + "as_of": { + "type": "string", + "description": "The instant a version in effect was asked for at.", + "example": "2026-06-01T00:00:00.000Z" + } + } + } + } + }, + { + "type": "object", + "description": "Several variants apply to the given context while a single result was requested.\n\nThe candidates are named, and none of them is served over the others: which is right\nis a question about the organization's own data.\n", + "required": [ + "code", + "details" + ], + "properties": { + "code": { + "type": "string", + "enum": [ + "AMBIGUOUS_RESOLUTION" + ] + }, + "details": { + "type": "object", + "additionalProperties": false, + "required": [ + "candidates" + ], + "properties": { + "candidates": { + "type": "array", + "minItems": 2, + "description": "Every variant that applied, each with the conditions it pins — which is what\nmakes the overlap actionable: two variants both apply because their pins do\nnot distinguish the context they were both asked about.\n\nBounded by the same cap `TOO_MANY_MATCHES` reports, which is checked first,\nso this list is never longer than one response may carry.\n", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "variant_id", + "conditions" + ], + "properties": { + "variant_id": { + "type": "string", + "description": "The candidate variant.", + "example": "var-46045" + }, + "conditions": { + "$ref": "#/components/schemas/VariantConditions" + } + } + } + } + } + } + } + }, + { + "type": "object", + "description": "The condition tuple this write claims is already held.\n\nPersistent, unlike `WRITE_CONFLICT`: the same request fails the same way until the\nholder changes, so a bulk importer can tell \"send this again\" from \"this combination\nis taken and always will be\".\n", + "required": [ + "code", + "details" + ], + "properties": { + "code": { + "type": "string", + "enum": [ + "TUPLE_CONFLICT" + ] + }, + "details": { + "type": "object", + "additionalProperties": false, + "required": [ + "variant_id" + ], + "properties": { + "variant_id": { + "type": "string", + "description": "The variant the write addressed.", + "example": "var-46045" + }, + "conflicting_variant_id": { + "type": "string", + "description": "The variant already holding the tuple, where the write read it back.\n", + "example": "var-50667" + } + } + } + } + }, + { + "type": "object", + "description": "A version already exists at the given `valid_from` on that variant.", + "required": [ + "code", + "details" + ], + "properties": { + "code": { + "type": "string", + "enum": [ + "VERSION_CONFLICT" + ] + }, + "details": { + "type": "object", + "additionalProperties": false, + "required": [ + "variant_id", + "valid_from" + ], + "properties": { + "variant_id": { + "type": "string", + "description": "The variant the write addressed.", + "example": "var-46045" + }, + "valid_from": { + "type": "string", + "description": "The instant already claimed by a version of that variant.", + "example": "2027-01-01T00:00:00.000Z" + } + } + } + } + }, + { + "type": "object", + "description": "A condition the entity's schema does not define, named by a resolve context, by a\nlisting's condition filter, or by a variant's pins.\n", + "required": [ + "code", + "details" + ], + "properties": { + "code": { + "type": "string", + "enum": [ + "CONDITION_UNDEFINED" + ] + }, + "details": { + "type": "object", + "additionalProperties": false, + "required": [ + "condition_name" + ], + "properties": { + "condition_name": { + "type": "string", + "description": "The condition named by the request and absent from the schema.", + "example": "postal_code" + } + } + } + } + }, + { + "type": "object", + "description": "The requested operator is not applicable to the condition's type.", + "required": [ + "code", + "details" + ], + "properties": { + "code": { + "type": "string", + "enum": [ + "OPERATOR_UNSUPPORTED" + ] + }, + "details": { + "type": "object", + "additionalProperties": false, + "required": [ + "condition_name", + "condition_type", + "operator" + ], + "properties": { + "condition_name": { + "type": "string", + "example": "postal_code" + }, + "condition_type": { + "type": "string", + "description": "The type the schema declares that condition with, which is what decides the\noperators it accepts.\n", + "example": "location" + }, + "operator": { + "type": "string", + "description": "The operator the context or filter asked for.", + "example": "between" + } + } + } + } + }, + { + "type": "object", + "description": "A resolve context or listing filter value that is malformed for its condition's type.\n\n`details` says what the type requires, never what arrived: a resolve context value is\nnot quoted back.\n", + "required": [ + "code", + "details" + ], + "properties": { + "code": { + "type": "string", + "enum": [ + "CONTEXT_FORMAT_INVALID" + ] + }, + "details": { + "type": "object", + "additionalProperties": false, + "required": [ + "condition_name", + "expected" + ], + "properties": { + "condition_name": { + "type": "string", + "example": "postal_code" + }, + "expected": { + "type": "string", + "description": "What a value for that condition has to be, in prose.", + "example": "a postal code" + } + } + } + } + }, + { + "type": "object", + "description": "A variant write pins a `select` value the condition's `options` do not admit — either\na value a declared vocabulary does not contain, or any value at all where the\ncondition declares no vocabulary for it to be in.\n\nOne of the two codes that report the submitted value back — `PIN_FORMAT_INVALID` is\nthe other. A resolve context value is never quoted back.\n", + "required": [ + "code", + "details" + ], + "properties": { + "code": { + "type": "string", + "enum": [ + "CONDITION_VALUE_INVALID" + ] + }, + "details": { + "type": "object", + "additionalProperties": false, + "required": [ + "condition_name", + "value", + "options" + ], + "properties": { + "condition_name": { + "type": "string", + "example": "segment" + }, + "value": { + "description": "The value the write pinned, as it arrived. Declared without a type: the\nvocabulary holds strings, so anything else is out of it by definition and is\nreported as sent.\n", + "example": "industrial" + }, + "options": { + "type": "array", + "description": "The vocabulary *as enforced* — after the entries this deploy cannot read have\nbeen dropped, so a tenant whose `options` holds a title-only entry is told\nwhat the API actually checked against rather than what they believe they\nwrote. Empty when the condition declares no vocabulary at all, which is\nitself the reason the pin was refused; the message says which of the two\n(unconfigured, or unreadable) applies.\n", + "items": { + "type": "string" + }, + "example": [ + "private", + "commercial" + ] + } + } + } + } + }, + { + "type": "object", + "description": "A multi-match resolve found more variants than one response may carry. Narrowing the\ncontext is the only fix; the matches are not reported.\n", + "required": [ + "code", + "details" + ], + "properties": { + "code": { + "type": "string", + "enum": [ + "TOO_MANY_MATCHES" + ] + }, + "details": { + "type": "object", + "additionalProperties": false, + "required": [ + "limit" + ], + "properties": { + "limit": { + "type": "number", + "description": "The most variants one resolve may compose.", + "example": 100 + } + } + } + } + }, + { + "type": "object", + "description": "Transient write contention — concurrent writers, or throughput pressure on the\nentity's own rows. Retryable, unlike `TUPLE_CONFLICT`.\n\nThe revisions are present where the contention was detected on a specific version: a\nwrite carrying `_revision` lost to another that landed first, and a client that read\nthe version again would see `current_revision`. A variant-level refusal carries the\nvariant alone.\n", + "required": [ + "code", + "details" + ], + "properties": { + "code": { + "type": "string", + "enum": [ + "WRITE_CONFLICT" + ] + }, + "details": { + "type": "object", + "additionalProperties": false, + "required": [ + "variant_id" + ], + "properties": { + "variant_id": { + "type": "string", + "description": "The variant the write addressed.", + "example": "var-46045" + }, + "valid_from": { + "type": "string", + "description": "The version the write addressed, where one was addressed.", + "example": "2027-01-01T00:00:00.000Z" + }, + "expected_revision": { + "type": "number", + "description": "The revision the write required the stored version to still be at.", + "example": 3 + }, + "current_revision": { + "type": "number", + "description": "The revision the version is actually at, where the failed write read it back.\nAbsent when it could not be.\n", + "example": 4 + } + } + } + } + }, + { + "type": "object", + "description": "A listing asked for a page reaching past the window the search index allows.\n\n**`from` plus `size`**, not `from` alone: the window bounds the last row a page may\ncontain, so the final servable offset is `window` minus the page size. All three\nnumbers are in `details`, because a refusal quoting only an offset below the window\nreads like a mistake.\n\nNot a page clamped back inside the window, as entity listing does. The fix is a\ndifferent request: page on with the last response's `next`.\n\nThe window belongs to the deploy's search index, so it is reported and never\npublished.\n\nRaised by the paginated variant reads, which answer `501` until their behaviour\nlands.\n", + "required": [ + "code", + "details" + ], + "properties": { + "code": { + "type": "string", + "enum": [ + "OFFSET_WINDOW_EXCEEDED" + ] + }, + "details": { + "type": "object", + "additionalProperties": false, + "required": [ + "from", + "size", + "window" + ], + "properties": { + "from": { + "type": "integer", + "description": "The offset the request asked for.", + "example": 24990 + }, + "size": { + "type": "integer", + "description": "The page size the request asked for, after clamping. Present because the two\ntogether are what exceeded the window — an offset inside it can still be\nrefused for the page it would have to read.\n", + "example": 25 + }, + "window": { + "type": "integer", + "description": "The last row this deploy's index will serve from an offset. Read it to size a\npage control, never to decide when to switch to the cursor — a caller can page\non with `next` from any page.\n", + "example": 25000 + } + } + } + } + }, + { + "type": "object", + "description": "A paging cursor could not be used for the read it arrived on.\n\nOne code for every way that happens, because the caller's fix is the same for all of\nthem: start the read again without a cursor. `details.reason` says which check failed,\nfor a human reading a log rather than for a client to branch on — a cursor this API\nminted and a caller stored can go stale, be truncated in transit, be replayed against\na different filter or sort, or be replayed against another variant or the opposite\norder on a versions read.\n\nRaised by the paginated variant and version reads, which answer `501` until their\nbehaviour lands.\n", + "required": [ + "code", + "details" + ], + "properties": { + "code": { + "type": "string", + "enum": [ + "CURSOR_INVALID" + ] + }, + "details": { + "type": "object", + "additionalProperties": false, + "required": [ + "reason" + ], + "properties": { + "reason": { + "type": "string", + "description": "Which check the cursor failed, in prose.", + "example": "The cursor was issued for a different sort order" + } + } + } + } + }, + { + "type": "object", + "description": "The entity already holds every variant it may hold.\n\nA hard refusal, unlike the `VARIANT_COUNT_APPROACHING_CAP` warning that precedes it,\ncarrying the warning's two keys. In a batch it means stop the import rather than fix\na row: every remaining item for that entity will be refused the same way.\n\nEmitted by `$createConditionalVariant`. The batch writes that will also raise it\nanswer `501` until their handlers land.\n", + "required": [ + "code", + "details" + ], + "properties": { + "code": { + "type": "string", + "enum": [ + "VARIANT_LIMIT_REACHED" + ] + }, + "details": { + "type": "object", + "additionalProperties": false, + "required": [ + "variant_count", + "cap" ], - "filters": { - "location": { - "postal_code": "50670", - "city": "Köln" + "properties": { + "variant_count": { + "type": "number", + "description": "Variants this entity already holds.", + "example": 5000 + }, + "cap": { + "type": "number", + "description": "Variants this entity may hold. Configurable per deploy, the same value for\nevery organization on it.\n", + "example": 5000 } } } - }, - "Search product recommendations by source product and price": { - "value": { - "catalog_item": { - "product_id": "aa1ebf85-f36e-46ff-978c-ed948c646f12", - "price_id": "4ecf9a61-a974-4594-bae2-bcb798f54d76" + } + }, + { + "type": "object", + "description": "A variant pins a value that is malformed for its condition's type.\n\nThe write-side mirror of `CONTEXT_FORMAT_INVALID`: a context value is *matched*, a\nvariant's value is *pinned*. Unlike that one it reports the value back, as\n`CONDITION_VALUE_INVALID` does.\n\nDistinct from a condition whose *declared type* this deploy cannot read at all: that\nis a schema problem rather than a bad row, every item naming the condition fails\nidentically, and it stays uncoded.\n\nIt carries `expected` as well as `condition_type`, and the two are not the same fact:\na `location` condition is `condition_type: location` whichever format it declares,\nand the two formats want different values — a postal code, or an object carrying a\npostal code and a town. The type is what a client branches on; `expected` is what\nsays what the value had to be.\n\nEmitted by `$createConditionalVariant`. The batch writes that will also raise it\nanswer `501` until their handlers land.\n", + "required": [ + "code", + "details" + ], + "properties": { + "code": { + "type": "string", + "enum": [ + "PIN_FORMAT_INVALID" + ] + }, + "details": { + "type": "object", + "additionalProperties": false, + "required": [ + "condition_name", + "condition_type", + "expected", + "value" + ], + "properties": { + "condition_name": { + "type": "string", + "example": "valid_period" + }, + "condition_type": { + "type": "string", + "description": "The type the schema declares that condition with, which is what decides the\nvalues it accepts.\n", + "example": "daterange" + }, + "expected": { + "type": "string", + "description": "What a pin for that condition has to be, in prose — the same field\n`CONTEXT_FORMAT_INVALID` carries, worded for the write side. It says what\n`condition_type` cannot: a `location` of format `zipcode` wants a postal code\nand one of format `zipcode_town` wants an object carrying both, and the type\nis `location` either way.\n", + "example": "an object carrying a from and an until date, either may be open" + }, + "value": { + "description": "The value the write pinned, as it arrived. Declared without a type, since\nwhat makes it invalid is that it is not of the condition's type.\n", + "example": "2027-01-01/2027-12-31" + } } } - }, - "Search product recommendations by contract": { - "value": { - "contract_id": "98ffca40-46ab-4288-af10-a478893b2aa2" + } + }, + { + "type": "object", + "description": "The write pins no condition and is not marked `default`.\n\nSuch a variant would be a universal wildcard matching every resolve, which is a far\nmore dangerous thing than a fallback and far easier to create by accident — an empty\npostal-code column in a source file produces exactly this.\n\nRaised on a delete too, where an item addresses no variant at all — no `variant_id`,\nno `default`, and either no `conditions` or an empty one: the item names no target.\nIt is an `error`, not a `skipped`.\n\nEmitted by `$createConditionalVariant`. Batch upsert and batch delete, which raise\nit too, answer `501` until their handlers land.\n", + "required": [ + "code", + "details" + ], + "properties": { + "code": { + "type": "string", + "enum": [ + "VARIANT_UNPINNED" + ] + }, + "details": { + "type": "object", + "additionalProperties": false, + "required": [ + "entity_id" + ], + "properties": { + "entity_id": { + "type": "string", + "description": "The conditional entity the item addressed.", + "example": "price-sp26d1yo" + } + } } } - } - } - } - }, - "responses": { - "200": { - "description": "The product recommendations result", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProductRecommendationResponse" - }, - "examples": { - "Product recommendations": { - "value": { - "hits": 1, - "results": [ - { - "_title": "Tarifwechsel 2025", - "_created_at": "2025-01-29T15:46:41.014Z", - "_updated_at": "2025-01-29T15:46:41.014Z", - "name": "Tarifwechsel 2025", - "type": "change", - "source_product": { - "$relation": [ - { - "entity_id": "e2b12b54-d904-42b7-ac44-802d8ebaadf4" - } - ] - }, - "source_price": { - "$relation": [ - { - "entity_id": "255877f3-f9c2-483e-9c71-89088c142bfc" - } - ] - }, - "_schema": "product_recommendation", - "offers": [ - { - "target_id": "f9aa8757-505b-46bd-9310-605d64849565", - "items": [ - { - "product_id": "70ad8a08-46c2-473b-b1c5-8bccfb85e452", - "price_id": "26c9c39d-db4a-4132-af2e-86ab5c7b156c" - }, - { - "product_id": "dd97a2a4-e48b-43c5-8717-7ac563c67e05", - "price_id": "4ecf9a61-a974-4594-bae2-bcb798f54d76" - }, - { - "product_id": "aa1ebf85-f36e-46ff-978c-ed948c646f12", - "price_id": "4ecf9a61-a974-4594-bae2-bcb798f54d76" - } - ] - } - ], - "_id": "6d872912-ef8a-4815-b9db-01744c0828ac", - "_org": "739224" - } - ] + }, + { + "type": "object", + "description": "The delete would leave the variant with no version at all.\n\nSuch a variant would still hold its condition tuple and still be selectable, and\nwould then resolve to nothing — a variant delete wearing a version delete's clothes.\nDelete the variant instead; that frees the tuple too.\n\nEmitted by `$deleteConditionalVariantVersion`. Batch delete, which raises it too,\nanswers `501` until its handler lands.\n", + "required": [ + "code", + "details" + ], + "properties": { + "code": { + "type": "string", + "enum": [ + "LAST_VERSION_UNDELETABLE" + ] + }, + "details": { + "type": "object", + "additionalProperties": false, + "required": [ + "variant_id", + "valid_from" + ], + "properties": { + "variant_id": { + "type": "string", + "description": "The variant whose last version the delete addressed.", + "example": "var-46045" + }, + "valid_from": { + "type": "string", + "description": "The version the delete addressed, by the instant it takes effect from.", + "example": "2027-01-01T00:00:00.000Z" + } } } } - } - } - }, - "400": { - "description": "Invalid payload", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" + }, + { + "type": "object", + "description": "A failure the vocabulary has no entry for: a malformed request body, a value that is\nnot readable as its condition's type, a rule refusing a write for a reason a client\ncannot branch on. The message says what to fix, and no code is sent.\n\nCarries what every response carries and nothing else — reading `code` on this member\nis how a client tells it from the coded members above.\n", + "additionalProperties": false, + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + }, + "status": { + "type": "number" + }, + "cause": { + "type": "string" + }, + "error": { + "$ref": "#/components/schemas/ReportedError" + } } } - } + ] } - } - } - } - }, - "components": { - "securitySchemes": { - "EpilotAuth": { - "type": "http", - "scheme": "bearer", - "description": "Epilot Bearer Token" - }, - "EpilotPublicAuth": { - "type": "http", - "scheme": "bearer", - "description": "Epilot Public Access Bearer Token", - "bearerFormat": "JWT" - } - }, - "schemas": { - "IntegrationId": { - "type": "string", - "enum": [ - "getag", - "external-catalog" ] }, - "Error": { - "required": [ - "message" - ], - "properties": { - "message": { - "type": "string", - "description": "Error message" - }, - "status": { - "type": "number", - "description": "The HTTP status code" - }, - "cause": { - "type": "string", - "description": "The cause of the error (visible for bad requests - http 400)" - } - } - }, "Product": { "type": "object", "description": "The product entity", @@ -3641,6 +8158,10 @@ } } }, + "is_conditional": { + "description": "The flag for entities whose values vary by context. Resolve the values that apply with\n`POST /v1/conditional-pricing:resolve`.\n", + "type": "boolean" + }, "_availability_files": { "type": "array", "description": "Stores references to the availability files that define where this product is available.\nThese files are used when interacting with products via epilot Journeys, thought the AvailabilityCheck block.\n", @@ -4799,6 +9320,10 @@ false ] }, + "is_conditional": { + "description": "The flag for entities whose values vary by context. Resolve the values that apply with\n`POST /v1/conditional-pricing:resolve`.\n", + "type": "boolean" + }, "pricing_model": { "type": "string", "description": "Describes how to compute the price per period. Either `per_unit`, `tiered_graduated` or `tiered_volume`.\n- `per_unit` indicates that the fixed amount (specified in unit_amount or unit_amount_decimal) will be charged per unit in quantity\n- `tiered_graduated` indicates that the unit pricing will be computed using tiers attribute. The customer pays the price per unit in every range their purchase rises through.\n- `tiered_volume` indicates that the unit pricing will be computed using tiers attribute. The customer pays the same unit price for all purchased units.\n- `tiered_flatfee` While similar to tiered_volume, tiered flat fee charges for the same price (flat) for the entire range instead using the unit price to multiply the quantity.\n - `dynamic_tariff` indicates that the price is dynamically dependend on the (quarter)-hourly spot market price.\n- `external_getag` indicates that the price is influenced by aquisition fees provided by GetAG.\n", @@ -5119,6 +9644,10 @@ true ] }, + "is_conditional": { + "description": "The flag for entities whose values vary by context. Resolve the values that apply with\n`POST /v1/conditional-pricing:resolve`.\n", + "type": "boolean" + }, "_created_at": { "description": "The price creation date", "type": "string" @@ -5569,6 +10098,9 @@ "description": "When true, enables entity hydration to resolve nested $relation references in-place.", "default": false }, + "fields": { + "$ref": "#/components/schemas/CatalogFieldsParam" + }, "availability": { "$ref": "#/components/schemas/AvailabilityFilters" } @@ -5583,6 +10115,17 @@ "q" ] }, + "CatalogFieldsParam": { + "type": "array", + "description": "List of entity fields to include or exclude from the results.\n", + "items": { + "type": "string" + }, + "example": [ + "!_files", + "!**.versions" + ] + }, "CatalogSearchResult": { "description": "The query result payload", "type": "object", @@ -6953,7 +11496,14 @@ "description": "The deprecated tax rate applied.\nThis field has been deprecated in favor of the new Tax Management. You should use the new tax fields pointing to a proper tax entity.\n" }, "tax": { - "$ref": "#/components/schemas/Tax" + "oneOf": [ + { + "$ref": "#/components/schemas/Tax" + }, + { + "$ref": "#/components/schemas/TaxItem" + } + ] } } }, @@ -7687,6 +12237,9 @@ "oneOf": [ { "$ref": "#/components/schemas/Tax" + }, + { + "$ref": "#/components/schemas/TaxItem" } ] } @@ -8429,6 +12982,37 @@ "_updated_at": "2021-02-09T12:41:43.662Z" } }, + "TaxItem": { + "type": "object", + "description": "A minimal, ad-hoc tax rate for line items with no backing tax entity\nin the catalog (e.g. a fully custom/composite price component built\nby a client with no product/price reference to resolve tax from).\nMirrors how PriceItem relates to Price: unlike Tax, this has no\nentity identity — it isn't persisted and can't be looked up by _id,\nso it can't be shared/reused across price items the way a catalog\nTax can.\n", + "additionalProperties": false, + "required": [ + "type", + "rate" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "VAT", + "GST", + "Custom" + ] + }, + "rate": { + "type": "number", + "nullable": true + }, + "description": { + "type": "string" + } + }, + "example": { + "rate": 19, + "type": "VAT", + "description": "Custom 19% VAT" + } + }, "TaxBreakdownInfo": { "type": "object", "properties": { @@ -8553,6 +13137,10 @@ "active": { "type": "boolean" }, + "is_conditional": { + "description": "The flag for entities whose values vary by context. Resolve the values that apply with\n`POST /v1/conditional-pricing:resolve`.\n", + "type": "boolean" + }, "requires_promo_code": { "type": "boolean", "description": "Whether the coupon requires a promo code to be applied" diff --git a/packages/cli/definitions/snapshot.json b/packages/cli/definitions/snapshot.json index fe20ec74c..6ca157288 100644 --- a/packages/cli/definitions/snapshot.json +++ b/packages/cli/definitions/snapshot.json @@ -27,6 +27,10 @@ { "name": "Snapshots", "description": "Snapshot CRUD and restore operations" + }, + { + "name": "ScheduledSnapshots", + "description": "Enrollment and configuration for scheduled org snapshots" } ], "paths": { @@ -103,6 +107,20 @@ "pattern": "^[^:]+:.+$" } } + }, + { + "in": "query", + "name": "trigger", + "description": "Filter to snapshots with a specific trigger. Uses the `byTrigger` GSI\nfor an efficient indexed query — no table scan. Only snapshots created\nafter the GSI was added carry this index entry; pre-existing rows will\nnot appear in trigger-filtered results.\n", + "schema": { + "type": "string", + "enum": [ + "manual", + "sync", + "blueprint_install", + "scheduled" + ] + } } ], "responses": { @@ -146,7 +164,7 @@ "post": { "operationId": "captureOrgSnapshot", "summary": "captureOrgSnapshot", - "description": "Snapshot the caller's whole organization now. Fetches a fresh inventory\nof the org's configuration resources from configuration-hub-api, persists\nit as an inventory artifact, and starts a `scope: \"org\"` chunked capture.\nAsync — returns immediately with a snapshot ID; client polls `getSnapshot`\nand watches `capture_summary` fill in until `create.status` moves from\n`in_progress` to `completed` or `failed`.\n\nSensitive types (`access_token`, `environment_variable`), types with no\nengine adapter, and any `excluded_types` are dropped from the capture and\nrecorded in the snapshot's coverage report.\n", + "description": "Snapshot the caller's whole organization now. Creates a `scope: \"org\"`\nsnapshot row and starts a chunked capture Step Function, then returns\nimmediately. The capture asynchronously fetches a fresh inventory of the\norg's configuration resources from configuration-hub-api, persists it as\nan inventory artifact, and captures each resource. Client polls\n`getSnapshot` and watches `capture_summary` fill in until `create.status`\nmoves from `in_progress` to `completed` or `failed`. An org with no\ncapturable resources finalizes as a completed 0-resource snapshot.\n\nSensitive types (`access_token`, `environment_variable`), types with no\nengine adapter, and any `excluded_types` are dropped from the capture and\nrecorded in the snapshot's coverage report.\n", "tags": [ "Snapshots" ], @@ -173,9 +191,6 @@ }, "401": { "$ref": "#/components/responses/Unauthorized" - }, - "422": { - "$ref": "#/components/responses/UnprocessableEntity" } } } @@ -369,6 +384,89 @@ } } }, + "/v1/org-snapshot-schedule": { + "get": { + "operationId": "getOrgSnapshotSchedule", + "summary": "getOrgSnapshotSchedule", + "description": "Return the scheduled-snapshot enrollment config for the caller's org.\nReturns 404 when the org has not yet enrolled.\n", + "tags": [ + "ScheduledSnapshots" + ], + "responses": { + "200": { + "description": "Org snapshot schedule config", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgSnapshotSchedule" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + }, + "put": { + "operationId": "putOrgSnapshotSchedule", + "summary": "putOrgSnapshotSchedule", + "description": "Create or update the scheduled-snapshot enrollment config for the\ncaller's org (upsert). The cron expression and retention window are\nvalidated server-side; invalid values are rejected with 400.\n\nDefaults applied when a field is omitted on first create:\n - `cron_expression`: `cron(0 2 * * ? *)` (daily at 02:00)\n - `timezone`: `Europe/Berlin`\n - `retention`: `{ value: 90, unit: \"days\" }`\n - `enabled`: `true`\n\n**Note:** this call persists the row only. EventBridge schedule\nmaterialization is performed by a subsequent reconcile step (Task 6).\nThe row is the authoritative source of truth for the UI.\n", + "tags": [ + "ScheduledSnapshots" + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PutOrgSnapshotScheduleRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Stored config (after upsert)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgSnapshotSchedule" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + } + } + }, + "delete": { + "operationId": "deleteOrgSnapshotSchedule", + "summary": "deleteOrgSnapshotSchedule", + "description": "Remove the scheduled-snapshot enrollment for the caller's org.\nReturns 404 when no schedule exists.\nThe corresponding EventBridge schedule is removed by a reconcile\nstep (Task 6).\n", + "tags": [ + "ScheduledSnapshots" + ], + "responses": { + "204": { + "description": "Deleted" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + } + }, "/v1/snapshots:list-dependencies": { "post": { "operationId": "listDependencies", @@ -475,16 +573,6 @@ } } } - }, - "UnprocessableEntity": { - "description": "Unprocessable entity", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/EmptyInventoryError" - } - } - } } }, "schemas": { @@ -503,38 +591,6 @@ } } }, - "EmptyInventoryError": { - "type": "object", - "required": [ - "message", - "skipped_types" - ], - "description": "Returned (422) when the org inventory contains no capturable resources\nafter filtering out sensitive, unsupported, and excluded types. The\n`skipped_types` array explains why every type was dropped.\n", - "properties": { - "message": { - "type": "string", - "example": "No capturable resources in the org inventory" - }, - "skipped_types": { - "type": "array", - "items": { - "type": "object", - "required": [ - "type", - "reason" - ], - "properties": { - "type": { - "type": "string" - }, - "reason": { - "type": "string" - } - } - } - } - } - }, "ResourceRef": { "type": "object", "required": [ @@ -562,7 +618,8 @@ "properties": { "lineage_id": { "type": "string", - "description": "Cross-service correlation key — matches the lineage row id in\nblueprint-manifest-api's lineage table for `blueprint_install`\nsnapshots. Same as `target_id` for snapshots whose capture\ndoesn't distinguish source vs destination identifiers.\n" + "deprecated": true, + "description": "Deprecated alias of `target_id`. Always equals `target_id` (the\nimplementation never distinguished them). Use `target_id`.\n" }, "target_id": { "type": "string", @@ -727,17 +784,14 @@ }, "RestoreSnapshotRequest": { "type": "object", - "description": "Both flags default to `false`, which restores every captured resource —\nConfig Hub's manual-restore semantics. blueprint-manifest-api sets\nboth `true` when reverting a blueprint install so user edits and\ncross-blueprint contributions survive. Each flag is independent so a\ncaller can preserve edits without preserving co-ownership (or vice\nversa). Skipped resources surface under `Operation.skipped`.\n", + "description": "Apply a captured snapshot to its source org. snapshot-api applies the\nmanifest verbatim minus any target ids the caller pre-decided to skip.\nDrift detection (skip modified-since-install) is the caller's\nresponsibility — blueprint-manifest-api owns that logic for blueprint\nrestores; Config Hub's manual restore just omits the field.\n", "properties": { - "preserve_modified": { - "type": "boolean", - "default": false, - "description": "When `true`, skip captured resources whose live destination payload\nhas diverged from the install-time fingerprint stored on lineage.\nSurfaces under `Operation.skipped` with `reason: 'modified'`.\n" - }, - "preserve_co_owned": { - "type": "boolean", - "default": false, - "description": "When `true`, skip captured resources whose lineage row carries\nanother blueprint instance's id (co-ownership ≥2). Surfaces under\n`Operation.skipped` with `reason: 'co_owned'`.\n" + "exclude_target_ids": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Target ids the caller has decided not to restore. snapshot-api\napplies the manifest minus these ids. Drops are silent — the\ncaller supplied the list and already knows.\n" } } }, @@ -891,57 +945,156 @@ "partial", "failed" ], - "description": "`partial` indicates the operation completed but skipped at least\none resource — see `skipped`. Only populated by restores triggered\nwith `mode: 'preserve_edits'`.\n" + "description": "`partial` indicates `engine.apply` reported a partial success\n(one or more resources failed individually) but the operation\nas a whole did not fail.\n" }, "error": { "type": "string" }, "triggered_by": { "$ref": "#/components/schemas/CallerIdentity" - }, - "skipped": { - "type": "array", - "description": "Per-resource skips, populated only for restores triggered with\n`mode: 'preserve_edits'`. Empty / absent for Config Hub's\ndefault overwrite-mode restores.\n", - "items": { - "$ref": "#/components/schemas/SkippedResource" - } } } }, - "SkippedResource": { + "CallerIdentity": { "type": "object", "required": [ - "lineage_id", - "reason" + "name" ], "properties": { - "lineage_id": { + "name": { "type": "string" }, - "reason": { + "user_id": { + "type": "string" + }, + "token_id": { + "type": "string" + } + } + }, + "RetentionConfig": { + "type": "object", + "required": [ + "value", + "unit" + ], + "description": "Flat retention window for a scheduled snapshot.\nConverted to a `ttl` epoch at capture time. Capped at ~24 months.\n", + "properties": { + "value": { + "type": "integer", + "minimum": 1, + "description": "Numeric quantity of retention (e.g. 90 for \"90 days\")." + }, + "unit": { "type": "string", "enum": [ - "modified", - "co_owned" - ], - "description": "- `modified` — current destination payload's fingerprint differs\n from the install-time fingerprint on the lineage row.\n- `co_owned` — lineage row has ≥2 distinct\n `blueprint_instance_ids`; restoring would unilaterally affect\n another blueprint instance's contribution.\n" + "days", + "weeks", + "months" + ] } } }, - "CallerIdentity": { + "PutOrgSnapshotScheduleRequest": { + "type": "object", + "description": "Body for `putOrgSnapshotSchedule`. All fields optional; unset fields\nreceive defaults on first create and are left unchanged on updates\n(except `updated_at`).\n", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Whether the schedule is active." + }, + "cron_expression": { + "type": "string", + "description": "6-field EventBridge cron expression, e.g. `cron(0 2 * * ? *)`.\nValidated server-side: minute + hour must be concrete single integers\n(no `*`, lists, ranges, or steps) to cap cadence at ≤ once/day.\nExactly one of day-of-month / day-of-week must be `?`.\n", + "example": "cron(0 2 * * ? *)" + }, + "timezone": { + "type": "string", + "description": "IANA timezone string passed to EventBridge `ScheduleExpressionTimezone`,\ne.g. `Europe/Berlin`. AWS handles DST natively.\n", + "example": "Europe/Berlin" + }, + "retention": { + "$ref": "#/components/schemas/RetentionConfig" + }, + "excluded_types": { + "type": "array", + "description": "Resource types to exclude from the scheduled capture, in addition\nto the always-excluded sensitive types (`access_token`,\n`environment_variable`).\n", + "items": { + "type": "string" + } + } + } + }, + "OrgSnapshotSchedule": { "type": "object", "required": [ - "name" + "org_id", + "enabled", + "cron_expression", + "timezone", + "retention", + "schedule_name", + "created_by", + "created_at", + "updated_at" ], + "description": "Enrollment record for a scheduled org snapshot. One row per org.\nThis table — not EventBridge — is the source of truth; the EventBridge\nschedule entry is the materialization of this row (reconciled on write\nby Task 6).\n", "properties": { - "name": { + "org_id": { "type": "string" }, - "user_id": { - "type": "string" + "enabled": { + "type": "boolean" }, - "token_id": { + "cron_expression": { + "type": "string", + "description": "Validated 6-field EventBridge cron. Default `cron(0 2 * * ? *)`.", + "example": "cron(0 2 * * ? *)" + }, + "timezone": { + "type": "string", + "description": "IANA timezone. Default `Europe/Berlin`." + }, + "retention": { + "$ref": "#/components/schemas/RetentionConfig" + }, + "excluded_types": { + "type": "array", + "items": { + "type": "string" + } + }, + "schedule_name": { + "type": "string", + "description": "Name of the EventBridge Scheduler entry this row owns.\nSet at enrollment time as `org-snapshot-{orgId}`.\n" + }, + "last_started_at": { + "type": "string", + "format": "date-time" + }, + "last_completed_at": { + "type": "string", + "format": "date-time" + }, + "last_status": { + "type": "string", + "enum": [ + "completed", + "partial", + "failed" + ] + }, + "created_by": { "type": "string" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" } } } diff --git a/packages/cli/definitions/targeting.json b/packages/cli/definitions/targeting.json index 8edf74caf..387725c82 100644 --- a/packages/cli/definitions/targeting.json +++ b/packages/cli/definitions/targeting.json @@ -37,7 +37,7 @@ "/v1/campaign/{campaign_id}/status": { "post": { "operationId": "changeCampaignStatus", - "summary": "Change the status of a campaign", + "summary": "changeCampaignStatus", "description": "Change the status of a campaign to a desired status.\n\nThe status can be one of the following: active, inactive.\n\nStatus transition is accompanied by side effects, e.g., automation execution.\n", "tags": [ "Campaign" @@ -69,7 +69,7 @@ "get": { "operationId": "getCampaignJobStatus", "description": "Get the status of a campaign's automation job", - "summary": "Get the status of a campaign's automation job", + "summary": "getCampaignJobStatus", "tags": [ "Campaign" ], @@ -99,7 +99,7 @@ "/v1/campaign/{campaign_id}/portals": { "get": { "operationId": "getCampaignPortals", - "summary": "Get portals usage info for a campaign", + "summary": "getCampaignPortals", "description": "Get the list of portals and its widgets where the campaign is used.\n", "tags": [ "Campaign" @@ -130,7 +130,7 @@ "/v1/campaign/{campaign_id}/automations:retrigger": { "post": { "operationId": "retriggerCampaignAutomations", - "summary": "Retrigger automations for campaign recipients", + "summary": "retriggerCampaignAutomations", "description": "Retrigger automation executions for specific campaign recipients that have failed.\n\nThis endpoint starts new automation executions for the specified recipients\nusing the campaign's associated automation flow. Only recipients with\nautomation_status 'failed' will be processed. Recipients with other statuses\n(success, pending, in_progress, cancelled) will be skipped to prevent\naccidentally retriggering successful or ongoing automations.\n", "tags": [ "Campaign Delivery" @@ -174,7 +174,7 @@ "/v1/campaign:setup": { "post": { "operationId": "setupCampaign", - "summary": "Set up a campaign with related entities and configurations", + "summary": "setupCampaign", "description": "Creates a `campaign` entity together with its related entities and configurations in a single call.\nUsed by the campaign wizard UI, but not restricted to it.\n", "tags": [ "Campaign" @@ -210,7 +210,7 @@ "/v1/campaign:match": { "post": { "operationId": "matchCampaigns", - "summary": "Match campaigns", + "summary": "matchCampaigns", "description": "Match campaigns based on target entities.\n\nThis endpoint returns the list of campaigns where the provided entities are part of the target.\n", "tags": [ "Campaign" @@ -243,10 +243,46 @@ } } }, + "/v1/campaign:discover": { + "post": { + "operationId": "discoverCampaigns", + "summary": "discoverCampaigns", + "description": "Given an entity, returns the Next Best Actions it should see on the Entity-UI channel.\n\nEnumerates the organization's **active** campaigns that carry a valid Entity-UI Next Best\nAction, live-matches each against the entity using the existing match engine, and returns\nthe matching NBAs priority-sorted (one per campaign).\n\nThis is a pure read: it writes nothing. An entity that matches no campaigns returns an\nempty list, not an error.\n", + "tags": [ + "Campaign" + ], + "security": [ + { + "EpilotAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DiscoverCampaignsParams" + } + } + } + }, + "responses": { + "200": { + "$ref": "#/components/responses/DiscoverCampaignsResponse" + }, + "400": { + "$ref": "#/components/responses/ClientErrorResponse" + }, + "500": { + "$ref": "#/components/responses/ServerErrorResponse" + } + } + } + }, "/v1/target:match": { "post": { "operationId": "matchTargets", - "summary": "Match targets", + "summary": "matchTargets", "description": "Find targets from the provided list that include the provide entities.\n", "tags": [ "Target" @@ -282,7 +318,7 @@ "/v1/target/queries": { "post": { "operationId": "getTargetQueries", - "summary": "Get target queries", + "summary": "getTargetQueries", "description": "Transform target filters into Lucene queries for the provided target IDs.\nReturns the transformed query string for each target along with any errors encountered.\n", "tags": [ "Target" @@ -318,7 +354,7 @@ "/v1/campaign/{campaign_id}/recipient": { "post": { "operationId": "createRecipient", - "summary": "Create a recipient associated with a campaign", + "summary": "createRecipient", "description": "Creates a new recipient associated with a campaign.", "tags": [ "Campaign Recipient" @@ -359,7 +395,7 @@ "/v1/campaign/{campaign_id}/recipient/{recipient_id}": { "patch": { "operationId": "updateRecipient", - "summary": "Update a recipient", + "summary": "updateRecipient", "description": "Updates a recipient's attributes.", "tags": [ "Campaign Recipient" @@ -403,7 +439,7 @@ "/v1/campaign/{campaign_id}/recipient/{recipient_id}/portal:status": { "patch": { "operationId": "updateRecipientPortalStatus", - "summary": "Update portal status for a campaign recipient", + "summary": "updateRecipientPortalStatus", "description": "Updates the portal status for a specific campaign recipient.\nThe portal_status_updated_at timestamp is automatically set when the status changes.\n\nStatus transition rules:\n- From 'sent': can change to 'seen', 'dismissed', or 'clicked'\n- From 'seen': can change to 'dismissed' or 'clicked'\n- From 'dismissed' or 'clicked': cannot be changed (final states)\n", "tags": [ "Campaign Recipient" @@ -450,10 +486,97 @@ } } }, + "/v1/campaign/{campaign_id}/recipient/{recipient_id}/entity_ui:status": { + "patch": { + "operationId": "updateRecipientEntityUiStatus", + "summary": "updateRecipientEntityUiStatus", + "description": "Records a Next Best Action interaction for a recipient on the Entity-UI channel.\n\nUnlike the portal channel, an NBA recipient is created lazily: the first `seen` creates\nthe recipient record (and requires `entity_schema`). `seen` is idempotent — re-viewing an\nNBA that is already seen/clicked/dismissed is a no-op success and never regresses the status.\n\nStatus transition rules:\n- `seen`: lazily creates the recipient; a no-op success if a status already exists\n- From `seen`: can change to `clicked` or `dismissed`\n- From `clicked`: can change to `dismissed`\n- From `dismissed`: cannot be changed via this operation — use `entity_ui:restore`\n\n`dismissed` and `clicked` require an existing recipient (404 otherwise, since an NBA is\nborn at `seen`) and reject invalid transitions (409).\n\nThe entity_ui_status_updated_at timestamp is automatically set when the status changes.\n", + "tags": [ + "Campaign Recipient" + ], + "security": [ + { + "EpilotAuth": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/CampaignIdPathParam" + }, + { + "$ref": "#/components/parameters/RecipientIdPathParam" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateEntityUiStatusRequest" + } + } + } + }, + "responses": { + "200": { + "$ref": "#/components/responses/RecipientResponse" + }, + "400": { + "$ref": "#/components/responses/ClientErrorResponse" + }, + "404": { + "$ref": "#/components/responses/ClientErrorResponse" + }, + "409": { + "$ref": "#/components/responses/ClientErrorResponse" + }, + "500": { + "$ref": "#/components/responses/ServerErrorResponse" + } + } + } + }, + "/v1/campaign/{campaign_id}/recipient/{recipient_id}/entity_ui:restore": { + "post": { + "operationId": "restoreRecipientEntityUiStatus", + "summary": "Undo a dismissal on the Entity-UI (Next Best Action) channel", + "description": "Reverses a dismissal so the Next Best Action becomes visible to the entity again. This is\nthe only way out of `dismissed` — `entity_ui:status` cannot leave that state.\n\nThe recipient's status is restored to whatever it was immediately before the dismissal\n(recorded in `entity_ui_status_before_dismiss`), so a `clicked` NBA that was dismissed\nreturns to `clicked` and does not lose its recorded click. Falls back to `seen` when no\nprevious status was recorded.\n\nScoped to the Entity-UI channel only: the recipient's portal and automation state is never\ntouched. Takes no request body — the recipient's current state fully determines the result.\n\nReturns 404 when the recipient has no Entity-UI status at all, and 409 when it has one but\nis not currently `dismissed` (there is nothing to undo).\n", + "tags": [ + "Campaign Recipient" + ], + "security": [ + { + "EpilotAuth": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/CampaignIdPathParam" + }, + { + "$ref": "#/components/parameters/RecipientIdPathParam" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/RecipientResponse" + }, + "404": { + "$ref": "#/components/responses/ClientErrorResponse" + }, + "409": { + "$ref": "#/components/responses/ClientErrorResponse" + }, + "500": { + "$ref": "#/components/responses/ServerErrorResponse" + } + } + } + }, "/v1/campaign/{campaign_id}/recipients": { "get": { "operationId": "getRecipients", - "summary": "Get campaign recipients", + "summary": "getRecipients", "description": "Get a paginated list of recipients for a campaign.", "tags": [ "Campaign Recipient" @@ -495,9 +618,14 @@ { "name": "automation_status", "in": "query", - "description": "Filter by automation status", + "description": "Filter by automation status. Repeat the parameter to match multiple\nstatuses in one request\n(e.g. automation_status=failed&automation_status=cancelled).\n", + "style": "form", + "explode": true, "schema": { - "$ref": "#/components/schemas/AutomationStatus" + "type": "array", + "items": { + "$ref": "#/components/schemas/AutomationStatus" + } } }, { @@ -507,6 +635,14 @@ "schema": { "$ref": "#/components/schemas/PortalStatus" } + }, + { + "name": "email_status", + "in": "query", + "description": "Filter by email delivery status (e.g. bounced, delivered). The response total reflects the count for the filtered status.", + "schema": { + "$ref": "#/components/schemas/EmailStatus" + } } ], "responses": { @@ -521,6 +657,37 @@ } } } + }, + "/v1/campaign/{campaign_id}/email-stats": { + "get": { + "operationId": "getEmailStats", + "summary": "getEmailStats", + "description": "Aggregate email delivery counts for a campaign, for the KPI summary on the campaign UI.\nCounts cover the email (automation) channel only; `total_emailed` is the number of\nrecipients with a recorded email status. `delivered` is derivable as\n`total_emailed - bounced - complained - failed`.\n", + "tags": [ + "Campaign Recipient" + ], + "security": [ + { + "EpilotAuth": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/CampaignIdPathParam" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/EmailStatsResponse" + }, + "400": { + "$ref": "#/components/responses/ClientErrorResponse" + }, + "500": { + "$ref": "#/components/responses/ServerErrorResponse" + } + } + } } }, "components": { @@ -575,6 +742,7 @@ "CAMPAIGN_NOT_FOUND", "CAMPAIGN_HAS_NO_TARGET", "CAMPAIGN_HAS_NO_DELIVERY_METHOD", + "INVALID_NEXT_BEST_ACTION", "CAMPAIGN_HAS_JOB_IN_PROGRESS", "CAMPAIGN_HAS_UNEXPECTED_STATUS", "JOB_TOKEN_MISSING", @@ -913,6 +1081,150 @@ ], "additionalProperties": false }, + "NextBestAction": { + "type": "object", + "description": "A Next Best Action configured on a campaign's Entity-UI channel.\nThis is the canonical NBA contract shared by discovery (this API), authoring, and rendering.\nNBA content is single-language in v1; text fields may contain `{{placeholders}}` resolved at render time.\n", + "properties": { + "category": { + "type": "string", + "maxLength": 30, + "description": "Light category label shown above the title. Free-form text." + }, + "icon": { + "type": "object", + "description": "Curated icon for the NBA.", + "properties": { + "name": { + "type": "string", + "description": "Icon name from \"@epilot360/icons\"." + }, + "color": { + "type": "string", + "description": "Optional icon color." + } + }, + "required": [ + "name" + ] + }, + "title": { + "type": "string", + "maxLength": 60, + "description": "Bold action title. Required. Supports `{{placeholders}}`." + }, + "body": { + "type": "string", + "maxLength": 140, + "description": "Optional description. Supports `{{placeholders}}` (incl. relative dates)." + }, + "priority": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ], + "default": "medium", + "description": "Display priority. NBAs are shown highest-priority first." + }, + "is_dismissable": { + "type": "boolean", + "default": true, + "description": "Whether the agent can dismiss the NBA." + }, + "cta": { + "type": "object", + "description": "The NBA's single call-to-action.", + "properties": { + "type": { + "type": "string", + "enum": [ + "journey", + "workflow", + "flow" + ] + }, + "target": { + "type": "string", + "description": "Journey id, workflow definition id, or flow template id, depending on `type`." + }, + "context_params": { + "type": "array", + "description": "Journey context parameters (journey CTA only). Maps the journey's declared\ncontext parameters so the journey knows which entity it is about. Discovery\nreturns them verbatim; they are passed to the journey when it launches.\n", + "items": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "key", + "value" + ] + } + } + }, + "required": [ + "type", + "target" + ] + } + }, + "required": [ + "title", + "cta" + ] + }, + "DiscoverCampaignsParams": { + "type": "object", + "properties": { + "entity_id": { + "$ref": "#/components/schemas/BaseUUID" + }, + "entity_schema": { + "type": "string", + "description": "The schema slug of the entity (e.g. \"contact\" or \"account\")." + } + }, + "required": [ + "entity_id", + "entity_schema" + ], + "additionalProperties": false + }, + "DiscoverResult": { + "type": "object", + "description": "One discovered Next Best Action, plus this entity's interaction state for it.", + "properties": { + "campaign_id": { + "$ref": "#/components/schemas/BaseUUID" + }, + "nba": { + "$ref": "#/components/schemas/NextBestAction" + }, + "status": { + "allOf": [ + { + "$ref": "#/components/schemas/EntityUiStatus" + } + ], + "description": "The recipient's current Entity-UI status for this campaign, present only when a\nrecipient record already exists (i.e. the entity has previously seen, clicked or\ndismissed this NBA). Absent when the entity has not yet interacted with it.\n\nIn `results` this is only ever `seen` or `clicked`; in `dismissed` it is always\n`dismissed`. Lets the client skip a redundant `seen` call for NBAs already seen.\n" + }, + "status_updated_at": { + "type": "string", + "format": "date-time", + "description": "When `status` was last written. Present whenever `status` is. Lets the client show how\nlong ago an NBA was dismissed.\n" + } + }, + "required": [ + "campaign_id", + "nba" + ] + }, "MatchTargetParams": { "type": "object", "properties": { @@ -1017,6 +1329,34 @@ "clicked" ] }, + "EntityUiStatus": { + "type": "string", + "description": "Lifecycle status of a Next Best Action on the Entity-UI channel. Unlike the portal\nchannel there is no `sent` state: an NBA recipient is born at `seen`, the moment the\naction is first rendered to an agent.\n", + "enum": [ + "seen", + "dismissed", + "clicked" + ] + }, + "EmailStatus": { + "type": "string", + "description": "Delivery status of the email a campaign's automation sends to a recipient. Set to `sent`\nonce the automation hands the email off, then updated asynchronously as SES notifications\narrive (`bounced` / `complained`; an SES Reject maps to `bounced`). `failed` is a send-time\nfailure (the automation execution could not send at all), distinct from an asynchronous\nbounce. `delivered` is reserved for when Delivery events are published.\n", + "enum": [ + "sent", + "delivered", + "bounced", + "complained", + "failed" + ] + }, + "EmailBounceType": { + "type": "string", + "description": "SES bounce classification: `permanent` (hard) or `transient` (soft).", + "enum": [ + "permanent", + "transient" + ] + }, "Recipient": { "type": "object", "properties": { @@ -1046,6 +1386,100 @@ "type": "object", "additionalProperties": true }, + "entity_ui_status": { + "$ref": "#/components/schemas/EntityUiStatus" + }, + "entity_ui_status_updated_at": { + "type": "string", + "format": "date-time" + }, + "entity_ui_status_before_dismiss": { + "allOf": [ + { + "$ref": "#/components/schemas/EntityUiStatus" + } + ], + "description": "The Entity-UI status the recipient held immediately before it was dismissed, so\n`entity_ui:restore` can put it back without losing a recorded click.\n\nServer-managed — never send this from a client; it is ignored on write. Meaningful\n**only while** `entity_ui_status` is `dismissed`: a restore intentionally leaves the\nvalue behind rather than clearing it, so a stale value after a restore is expected and\nmust not be read.\n" + }, + "message_entity_id": { + "type": "string", + "description": "The message entity id of the email sent to this recipient by the campaign's automation\n(not the SES/provider message id), used to correlate SES delivery notifications back to\nthe recipient. Server-managed.\n" + }, + "email_status": { + "$ref": "#/components/schemas/EmailStatus" + }, + "email_status_updated_at": { + "type": "string", + "format": "date-time" + }, + "email_bounce_type": { + "$ref": "#/components/schemas/EmailBounceType" + }, + "email_bounce_subtype": { + "type": "string", + "description": "SES bounce sub-type (e.g. `General`, `NoEmail`, `MailboxFull`)." + }, + "email_complaint_type": { + "type": "string", + "description": "SES complaint feedback type (e.g. `abuse`, `fraud`), set for complaints." + }, + "email_bounce_reason": { + "type": "string", + "description": "Human-readable failure reason — the SES bounce `diagnosticCode`, the complaint\nfeedback type, or a send-time error message.\n" + }, + "email_send_error": { + "type": "object", + "additionalProperties": true, + "description": "The raw SES notification (or send error) kept verbatim for a detail view." + }, + "email_unsubscribed_at": { + "type": "string", + "format": "date-time", + "description": "When the contact unsubscribed from email marketing (opted out). Server-managed." + }, + "email_open_count": { + "type": "integer", + "description": "Number of SES opens recorded for this recipient (only where open tracking is on)." + }, + "email_first_opened_at": { + "type": "string", + "format": "date-time", + "description": "When the recipient first opened the email." + }, + "email_last_opened_at": { + "type": "string", + "format": "date-time", + "description": "When the recipient most recently opened the email." + }, + "email_click_count": { + "type": "integer", + "description": "Number of SES link clicks recorded for this recipient (only where click tracking is on)." + }, + "email_first_clicked_at": { + "type": "string", + "format": "date-time", + "description": "When the recipient first clicked a link." + }, + "email_clicked_links": { + "type": "array", + "description": "Raw click events (URL + timestamp); aggregate per-URL on the client.", + "items": { + "type": "object", + "required": [ + "url", + "clicked_at" + ], + "properties": { + "url": { + "type": "string" + }, + "clicked_at": { + "type": "string", + "format": "date-time" + } + } + } + }, "updated_at": { "type": "string", "format": "date-time" @@ -1211,6 +1645,22 @@ "status" ] }, + "UpdateEntityUiStatusRequest": { + "type": "object", + "properties": { + "status": { + "$ref": "#/components/schemas/EntityUiStatus" + }, + "entity_schema": { + "type": "string", + "description": "Schema slug of the recipient entity (e.g. \"contact\"). Required when recording the\nfirst `seen`, which lazily creates the recipient record; ignored on later transitions.\n" + } + }, + "required": [ + "status" + ], + "additionalProperties": false + }, "SetupCampaignRequest": { "description": "Discriminated by `type`. Each campaign variant has its own request shape;\nnew variants are added by introducing a new schema and extending the `oneOf` list.\n", "oneOf": [ @@ -1525,6 +1975,81 @@ } } }, + "EmailStatsResponse": { + "description": "Aggregate email delivery counts for a campaign.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "total_emailed", + "sent", + "delivered", + "bounced", + "bounced_hard", + "bounced_soft", + "complained", + "failed", + "unsubscribed", + "opened", + "clicked", + "total_opens", + "total_clicks" + ], + "properties": { + "total_emailed": { + "type": "integer", + "description": "Recipients with a recorded email status (the KPI denominator)." + }, + "sent": { + "type": "integer" + }, + "delivered": { + "type": "integer", + "description": "Recipients whose SES Delivery event was recorded (needs Delivery publishing enabled)." + }, + "bounced": { + "type": "integer" + }, + "bounced_hard": { + "type": "integer", + "description": "Bounces classified permanent (bounced_hard + bounced_soft = bounced)." + }, + "bounced_soft": { + "type": "integer", + "description": "Bounces classified transient." + }, + "complained": { + "type": "integer" + }, + "failed": { + "type": "integer" + }, + "unsubscribed": { + "type": "integer", + "description": "Recipients who opted out of email marketing." + }, + "opened": { + "type": "integer", + "description": "Unique recipients with at least one open (only where open tracking is on)." + }, + "clicked": { + "type": "integer", + "description": "Unique recipients with at least one link click (only where click tracking is on)." + }, + "total_opens": { + "type": "integer", + "description": "Total opens across all recipients." + }, + "total_clicks": { + "type": "integer", + "description": "Total link clicks across all recipients." + } + } + } + } + } + }, "MatchCampaignsResponse": { "description": "List of campaigns where the target entities match the given entities.", "content": { @@ -1556,6 +2081,41 @@ } } }, + "DiscoverCampaignsResponse": { + "description": "The Next Best Actions the entity should see on the Entity-UI channel, priority-sorted.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "hits": { + "type": "number", + "description": "Number of matching NBAs. Counts `results` only — dismissed ones are excluded." + }, + "results": { + "type": "array", + "description": "Matching NBAs, sorted by priority (desc); one entry per campaign.", + "items": { + "$ref": "#/components/schemas/DiscoverResult" + } + }, + "dismissed": { + "type": "array", + "description": "NBAs this entity has dismissed that would otherwise be in `results` — same shape,\nalso priority-sorted. Returned so the client can show the agent what it hid and\noffer to restore it, without a second round-trip.\n\nOnly currently-relevant dismissals appear: each one is matched against its\ncampaign's target exactly like a visible NBA, so a dismissal is dropped once the\ncampaign ends or the entity stops matching. Capped, so this is not a complete\ndismissal history.\n", + "items": { + "$ref": "#/components/schemas/DiscoverResult" + } + } + }, + "required": [ + "hits", + "results", + "dismissed" + ] + } + } + } + }, "MatchTargetsResponse": { "description": "List of targets where the given entities are found.", "content": { diff --git a/packages/cli/definitions/user.json b/packages/cli/definitions/user.json index 8021221d9..4d7fc33be 100644 --- a/packages/cli/definitions/user.json +++ b/packages/cli/definitions/user.json @@ -1046,6 +1046,117 @@ } } }, + "/v2/users/public/resetPassword": { + "post": { + "operationId": "resetPassword", + "summary": "resetPassword", + "description": "Set a new password using a reset token from the password reset email.\nThe token is single-use and time-limited.\n", + "security": [], + "x-rate-limit": { + "limit": 10 + }, + "tags": [ + "User V2" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "email", + "token", + "password" + ], + "properties": { + "email": { + "type": "string", + "description": "Email address of the account", + "example": "test@example.com" + }, + "token": { + "type": "string", + "description": "Reset token from the password reset email" + }, + "password": { + "type": "string", + "description": "The new password" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Password reset successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "success" + ], + "properties": { + "success": { + "type": "boolean", + "example": true + } + } + } + } + } + }, + "400": { + "description": "Invalid or expired reset link, or password does not meet the requirements", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "error", + "message" + ], + "properties": { + "error": { + "type": "string", + "description": "Discriminator distinguishing token errors from policy errors", + "enum": [ + "INVALID_RESET_LINK", + "PASSWORD_POLICY" + ] + }, + "message": { + "type": "string", + "example": "Password does not meet the requirements" + }, + "violations": { + "type": "array", + "description": "Itemized password-policy violation codes (only present when error is PASSWORD_POLICY)", + "items": { + "type": "string", + "enum": [ + "too_short", + "missing_lowercase", + "missing_uppercase", + "missing_number", + "missing_symbol" + ] + }, + "example": [ + "too_short", + "missing_uppercase" + ] + } + } + } + } + } + } + } + } + }, "/v2/users/public/checkToken": { "get": { "operationId": "checkInviteToken", @@ -1998,6 +2109,19 @@ "format": "date-time", "readOnly": true }, + "password_reset_token_hash": { + "description": "Internal: sha256 hash of the active password-reset token. Server-set, never returned in responses.", + "type": "string", + "nullable": true, + "readOnly": true + }, + "password_reset_token_expires_at": { + "description": "Internal: expiry for the active password-reset token (ISO 8601). Server-set, never returned in responses.", + "type": "string", + "nullable": true, + "format": "date-time", + "readOnly": true + }, "department": { "type": "string", "description": "User's department", @@ -2130,6 +2254,17 @@ "deleted_opportunity": true } }, + "in_app_notification_setting": { + "type": "object", + "additionalProperties": true, + "description": "Per-notification-type in-app delivery preferences (notification type key -> enabled). Written by the my-account notification settings UI and honored by svc-notification-api at delivery time. Absent or partial keys fall back to each notification type's configured default. Mirrors email_notification_setting for the in-app channel.", + "example": { + "integration_critical_error": true, + "integration_error_threshold": true, + "assigned_opportunity": true, + "assigned_task": true + } + }, "properties": { "type": "array", "items": { @@ -2149,6 +2284,17 @@ "value" ] } + }, + "tags": { + "type": "array", + "description": "User tags/labels assigned for classification (e.g. label slugs)", + "nullable": true, + "items": { + "type": "string" + }, + "example": [ + "non-billable" + ] } } }, @@ -2297,6 +2443,10 @@ "passkey_enabled": { "type": "boolean", "description": "Whether passkey login is enabled for this organization" + }, + "passkeys_registered": { + "type": "boolean", + "description": "Whether the user has at least one passkey registered. Lets the login\nUI offer passkey authentication as an alternative to an MFA code.\nDiscloses nothing that :beginPasskeyAuthentication doesn't already\nreveal for a known email address.\n" } } }, diff --git a/packages/cli/definitions/validation-rules.json b/packages/cli/definitions/validation-rules.json index 95f49bb76..56949f420 100644 --- a/packages/cli/definitions/validation-rules.json +++ b/packages/cli/definitions/validation-rules.json @@ -2,12 +2,13 @@ "openapi": "3.0.3", "info": { "title": "Validation Rules API", - "version": "1.0.0" + "version": "1.0.0", + "description": "The Validation Rules API manages reusable input validation rules for epilot journeys and entity attributes.\n\nValidation rules define constraints for user input (e.g. regex patterns, numeric ranges, character counts) that can be applied to journey blocks or entity schema attributes.\n\nKey capabilities:\n- Define validation rules using regex patterns, character patterns, or numeric constraints\n- Apply rules to journey blocks or entity schema attributes via the `used_by` association\n- Manage the lifecycle of rules (create, read, update, delete)\n- Compose complex validation logic using AND/OR/NOT condition combinators\n\n## Rule schema versions\n\nThe `rule` property holds one of two shapes, identified by the document's `_schema_version`:\n- `v1`: a json-rules-engine condition tree (regex/pattern/numeric) with static comparison values only.\n- `v2`: a declarative rule with predefined comparison operators whose comparison values may be static,\n dynamic (a path into runtime context declared via `contexts`,\n e.g. `contract.installment_amount`) or relative dates. v2 shapes carry an `input_type` property.\n Two v2 shapes exist: `ComparisonRuleType` for number, date and text inputs, and `DocumentRuleType`\n for uploaded files, whose conditions check file properties and, at the higher check levels,\n the document's content.\nConverting a rule between schema versions is not supported.\n" }, "tags": [ { "name": "Validation Rules", - "description": "Endpoints for managing validation rules" + "description": "CRUD endpoints for managing validation rules within an organization.\nAll endpoints require an epilot bearer token and operate within the authenticated organization's scope.\nRules are identified by a unique `ruleId` and can be referenced by journey blocks or entity attributes via the `used_by` field.\n" } ], "security": [ @@ -19,8 +20,8 @@ "/v1/validation-rules": { "get": { "operationId": "getValidationRules", - "summary": "Get all validation rules by organization Id", - "description": "Gets all validation rules by organization Id", + "summary": "getValidationRules", + "description": "Returns all validation rules belonging to the authenticated user's organization.\n\nResults are returned as a flat list. Use this endpoint to list available rules when configuring journeys or entity schemas.\n", "tags": [ "Validation Rules" ], @@ -36,6 +37,32 @@ "application/json": { "schema": { "$ref": "#/components/schemas/GetValidationRulesResponse" + }, + "example": { + "results": [ + { + "_id": "rule-abc123", + "_organization_id": "728224", + "_schema_version": "1.0", + "title": "German postal code", + "created_at": "2024-01-10T08:00:00.000Z", + "updated_at": "2024-01-10T08:00:00.000Z", + "created_by": "user-1", + "updated_by": "user-1", + "rule": { + "type": "regex", + "conditions": { + "all": [ + { + "fact": "inputValue", + "operator": "regexMatch", + "value": "^[0-9]{5}$" + } + ] + } + } + } + ] } } } @@ -56,6 +83,38 @@ } } }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + }, + "403": { + "description": "Forbidden - Insufficient permissions", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Forbidden" + } + } + } + } + } + }, "500": { "description": "Internal Server Error", "content": { @@ -76,8 +135,8 @@ }, "post": { "operationId": "createValidationRule", - "summary": "Create Validation Rule", - "description": "Creates a new validation rule", + "summary": "createValidationRule", + "description": "Creates a new validation rule for the authenticated organization.", "tags": [ "Validation Rules" ], @@ -87,22 +146,136 @@ } ], "requestBody": { + "description": "Validation rule to create", + "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateValidationRuleRequest" + }, + "example": { + "title": "German postal code", + "rule": { + "type": "regex", + "conditions": { + "all": [ + { + "fact": "inputValue", + "operator": "regexMatch", + "value": "^[0-9]{5}$", + "params": { + "errorMessage": "Must be a 5-digit German postal code" + } + } + ] + } + } } } - }, - "description": "Payload" + } }, "responses": { "201": { - "description": "Success", + "description": "Validation rule created successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRule" + }, + "example": { + "_id": "rule-abc123", + "_organization_id": "728224", + "_schema_version": "1.0", + "title": "German postal code", + "created_at": "2024-01-10T08:00:00.000Z", + "updated_at": "2024-01-10T08:00:00.000Z", + "created_by": "user-1", + "updated_by": "user-1", + "rule": { + "type": "regex", + "conditions": { + "all": [ + { + "fact": "inputValue", + "operator": "regexMatch", + "value": "^[0-9]{5}$" + } + ] + } + } + } + } + } + }, + "400": { + "description": "Bad Request - Invalid request body. Invalid external or environment comparison values additionally\ncarry a `code`: `external_value_not_found` when the app is not installed or the hook / result does not\nexist, `external_value_incompatible` when the result type does not match the compared kind,\n`environment_variable_not_found` when the referenced environment variable does not exist in the\norganisation, `environment_variable_incompatible` when its type cannot be compared as required\n(`Number` for numbers, `Text` for text and dates, `Number` / `Text` / `Boolean` for `applies_when`;\n`String`, `SecretString`, `Map` and `JSON` never).\n", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Invalid request body" + }, + "code": { + "type": "string", + "enum": [ + "external_value_not_found", + "external_value_incompatible", + "environment_variable_not_found", + "environment_variable_incompatible" + ] + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + }, + "403": { + "description": "Forbidden - Insufficient permissions", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Forbidden" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Unknown API Error" + } + } } } } @@ -113,8 +286,8 @@ "/v1/validation-rules/{ruleId}": { "get": { "operationId": "getValidationRuleById", - "summary": "Get validation rule by ID", - "description": "Retrieves a specific validation rule by its ID", + "summary": "getValidationRuleById", + "description": "Retrieves a specific validation rule by its unique ID.", "tags": [ "Validation Rules" ], @@ -131,16 +304,71 @@ "schema": { "type": "string" }, - "description": "The unique identifier of the validation rule to retrieve." + "description": "The unique identifier of the validation rule to retrieve.", + "example": "rule-abc123" } ], "responses": { "200": { - "description": "Success", + "description": "Validation rule retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRule" + }, + "example": { + "_id": "rule-abc123", + "_organization_id": "728224", + "_schema_version": "1.0", + "title": "German postal code", + "created_at": "2024-01-10T08:00:00.000Z", + "updated_at": "2024-01-10T08:00:00.000Z", + "created_by": "user-1", + "updated_by": "user-1", + "rule": { + "type": "regex", + "conditions": { + "all": [ + { + "fact": "inputValue", + "operator": "regexMatch", + "value": "^[0-9]{5}$" + } + ] + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + }, + "403": { + "description": "Forbidden - Insufficient permissions", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Forbidden" + } + } } } } @@ -181,8 +409,8 @@ }, "patch": { "operationId": "updateValidationRule", - "summary": "Update Validation Rule (partial update)", - "description": "Updates an existing validation rule partially by ID", + "summary": "updateValidationRule", + "description": "Partially updates an existing validation rule by ID. Only the fields provided in the request body are updated.", "tags": [ "Validation Rules" ], @@ -199,7 +427,8 @@ "schema": { "type": "string" }, - "description": "The unique identifier of the validation rule to update." + "description": "The unique identifier of the validation rule to update.", + "example": "rule-abc123" } ], "requestBody": { @@ -207,6 +436,24 @@ "application/json": { "schema": { "$ref": "#/components/schemas/UpdateValidationRuleRequest" + }, + "example": { + "title": "Updated German postal code", + "rule": { + "type": "regex", + "conditions": { + "all": [ + { + "fact": "inputValue", + "operator": "regexMatch", + "value": "^[0-9]{5}$", + "params": { + "errorMessage": "Must be a valid 5-digit German postal code" + } + } + ] + } + } } } }, @@ -214,17 +461,39 @@ }, "responses": { "200": { - "description": "Success", + "description": "Validation rule updated successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRule" + }, + "example": { + "_id": "rule-abc123", + "_organization_id": "728224", + "_schema_version": "1.0", + "title": "Updated German postal code", + "created_at": "2024-01-10T08:00:00.000Z", + "updated_at": "2024-01-20T11:00:00.000Z", + "created_by": "user-1", + "updated_by": "user-2", + "rule": { + "type": "regex", + "conditions": { + "all": [ + { + "fact": "inputValue", + "operator": "regexMatch", + "value": "^[0-9]{5}$" + } + ] + } + } } } } }, "400": { - "description": "Bad Request - Invalid request body", + "description": "Bad Request - Invalid request body. Invalid external or environment comparison values additionally\ncarry a `code`: `external_value_not_found` when the app is not installed or the hook / result does not\nexist, `external_value_incompatible` when the result type does not match the compared kind,\n`environment_variable_not_found` when the referenced environment variable does not exist in the\norganisation, `environment_variable_incompatible` when its type cannot be compared as required\n(`Number` for numbers, `Text` for text and dates, `Number` / `Text` / `Boolean` for `applies_when`;\n`String`, `SecretString`, `Map` and `JSON` never).\n", "content": { "application/json": { "schema": { @@ -233,6 +502,63 @@ "message": { "type": "string", "example": "Invalid request body" + }, + "code": { + "type": "string", + "enum": [ + "external_value_not_found", + "external_value_incompatible", + "environment_variable_not_found", + "environment_variable_incompatible" + ] + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + }, + "403": { + "description": "Forbidden - Insufficient permissions", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Forbidden" + } + } + } + } + } + }, + "404": { + "description": "Validation rule not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Validation rule not found" } } } @@ -259,8 +585,8 @@ }, "delete": { "operationId": "deleteValidationRule", - "summary": "Delete Validation Rule", - "description": "Deletes a validation rule by ID", + "summary": "deleteValidationRule", + "description": "Permanently deletes a validation rule by ID. Any journeys or entity attributes referencing this rule should be updated before deletion.", "tags": [ "Validation Rules" ], @@ -277,13 +603,62 @@ "schema": { "type": "string" }, - "description": "The unique identifier of the validation rule to delete." + "description": "The unique identifier of the validation rule to delete.", + "example": "rule-abc123" } ], "responses": { "204": { "description": "Validation rule deleted successfully" }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + }, + "403": { + "description": "Forbidden - Insufficient permissions", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Forbidden" + } + } + } + } + } + }, + "404": { + "description": "Validation rule not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Validation rule not found" + } + } + } + } + } + }, "500": { "description": "Internal Server Error", "content": { @@ -306,8 +681,8 @@ "/v1/validation-rules/{ruleId}/used-by": { "post": { "operationId": "addUsedByReference", - "summary": "Add a reference to the usedBy array", - "description": "Adds a single reference to the usedBy array of a validation rule", + "summary": "addUsedByReference", + "description": "Adds a single `used_by` reference to an existing validation rule.\n\nUse this endpoint when attaching a validation rule to a journey block or entity attribute to track where the rule is applied.\n", "tags": [ "Validation Rules" ], @@ -324,7 +699,8 @@ "schema": { "type": "string" }, - "description": "The unique identifier of the validation rule to update." + "description": "The unique identifier of the validation rule to update.", + "example": "rule-abc123" } ], "requestBody": { @@ -332,10 +708,14 @@ "application/json": { "schema": { "$ref": "#/components/schemas/UsedBy" + }, + "example": { + "type": "journey", + "source_id": "journey-xyz789" } } }, - "description": "The usedBy reference to add" + "description": "The used_by reference to add" }, "responses": { "200": { @@ -344,6 +724,34 @@ "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRule" + }, + "example": { + "_id": "rule-abc123", + "_organization_id": "728224", + "_schema_version": "1.0", + "title": "German postal code", + "created_at": "2024-01-10T08:00:00.000Z", + "updated_at": "2024-01-20T11:00:00.000Z", + "created_by": "user-1", + "updated_by": "user-1", + "rule": { + "type": "regex", + "conditions": { + "all": [ + { + "fact": "inputValue", + "operator": "regexMatch", + "value": "^[0-9]{5}$" + } + ] + } + }, + "used_by": [ + { + "type": "journey", + "source_id": "journey-xyz789" + } + ] } } } @@ -364,6 +772,38 @@ } } }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + }, + "403": { + "description": "Forbidden - Insufficient permissions", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Forbidden" + } + } + } + } + } + }, "404": { "description": "Validation rule not found", "content": { @@ -400,8 +840,8 @@ }, "delete": { "operationId": "removeUsedByReference", - "summary": "Remove a reference from the usedBy array", - "description": "Removes a specific reference from the usedBy array of a validation rule", + "summary": "removeUsedByReference", + "description": "Removes a specific `used_by` reference from an existing validation rule.\n\nUse this endpoint when detaching a validation rule from a journey block or entity attribute.\n", "tags": [ "Validation Rules" ], @@ -418,7 +858,8 @@ "schema": { "type": "string" }, - "description": "The unique identifier of the validation rule to update." + "description": "The unique identifier of the validation rule to update.", + "example": "rule-abc123" } ], "requestBody": { @@ -426,10 +867,14 @@ "application/json": { "schema": { "$ref": "#/components/schemas/UsedBy" + }, + "example": { + "type": "journey", + "source_id": "journey-xyz789" } } }, - "description": "The usedBy reference to remove" + "description": "The used_by reference to remove" }, "responses": { "200": { @@ -438,6 +883,29 @@ "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRule" + }, + "example": { + "_id": "rule-abc123", + "_organization_id": "728224", + "_schema_version": "1.0", + "title": "German postal code", + "created_at": "2024-01-10T08:00:00.000Z", + "updated_at": "2024-01-20T11:00:00.000Z", + "created_by": "user-1", + "updated_by": "user-1", + "rule": { + "type": "regex", + "conditions": { + "all": [ + { + "fact": "inputValue", + "operator": "regexMatch", + "value": "^[0-9]{5}$" + } + ] + } + }, + "used_by": [] } } } @@ -458,6 +926,38 @@ } } }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + }, + "403": { + "description": "Forbidden - Insufficient permissions", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Forbidden" + } + } + } + } + } + }, "404": { "description": "Validation rule not found", "content": { @@ -512,9 +1012,11 @@ "schemas": { "GetValidationRulesResponse": { "type": "object", + "description": "Response envelope for listing all validation rules within an organization.", "properties": { "results": { "type": "array", + "description": "Flat list of all validation rules belonging to the authenticated organization.", "items": { "$ref": "#/components/schemas/ValidationRule" } @@ -572,8 +1074,21 @@ }, { "$ref": "#/components/schemas/NumericRuleType" + }, + { + "$ref": "#/components/schemas/ComparisonRuleType" + }, + { + "$ref": "#/components/schemas/DocumentRuleType" } ] + }, + "contexts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ContextRequirement" + }, + "description": "Declares the dynamic context a v2 rule needs at evaluation time.\n`context` condition values reference these sources by using the schema slug\nas the first segment of their `path`. Only applicable to v2 rules.\n" } } } @@ -633,6 +1148,7 @@ }, "UsedBy": { "type": "object", + "description": "Describes where and how a validation rule is applied. Used to track associations between rules and the journeys or entity schemas that reference them.", "required": [ "type" ], @@ -643,18 +1159,24 @@ "journey", "entity" ], - "description": "The context in which the rule is used (e.g., journey or entity)." + "description": "The context in which the rule is used - either a journey block or an entity schema attribute.", + "example": "journey" }, "schema_slug": { "type": "string", - "description": "Slug of the schema using this rule for entities." + "description": "Slug of the entity schema using this rule. Only applicable when `type` is `entity`.", + "example": "contact" }, "source_id": { "type": "string", - "description": "Source identifier for the usage context." + "description": "Unique identifier of the source (e.g. journey ID or entity attribute key) that references this rule.", + "example": "journey-xyz789" } }, - "description": "Describes where and how a validation rule is applied." + "example": { + "type": "journey", + "source_id": "journey-xyz789" + } }, "RegexRuleType": { "description": "Validation rule that uses a regular expression to validate input.", @@ -1480,6 +2002,635 @@ } } ] + }, + "ComparisonRuleType": { + "type": "object", + "description": "Declarative validation rule (schema version v2). Supports predefined comparison operators\nover number, date and text inputs, with static, dynamic (context path), relative-date and\nexternal (app-provided) comparison values.\n", + "required": [ + "input_type", + "conditions" + ], + "additionalProperties": false, + "properties": { + "input_type": { + "type": "string", + "enum": [ + "number", + "date", + "text" + ], + "description": "The kind of input value the rule validates. Determines which operators are allowed." + }, + "conditions": { + "type": "array", + "description": "The comparisons the input value must satisfy. All blocking conditions must pass\nfor the input to be valid; `allow_failure` conditions are advisory and excluded\nfrom the verdict. Must contain at least one condition (enforced at write time).\n", + "items": { + "$ref": "#/components/schemas/Condition" + } + } + } + }, + "Condition": { + "type": "object", + "description": "A single comparison the input value must satisfy.", + "required": [ + "id", + "operator", + "value", + "error_message" + ], + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "minLength": 1, + "maxLength": 50, + "description": "Stable identifier of the condition within the rule, used for editing and error reporting." + }, + "operator": { + "$ref": "#/components/schemas/Operator" + }, + "value": { + "$ref": "#/components/schemas/ConditionValue" + }, + "error_message": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Message shown to the end user when this condition fails." + }, + "applies_when": { + "$ref": "#/components/schemas/AppliesWhen" + }, + "allow_failure": { + "type": "boolean", + "default": false, + "description": "When true, the condition is advisory: it is always evaluated and reported\nwhen it fails, but it never takes part in the validity verdict.\nThe input is valid when every blocking (non-advisory) condition passes.\nA rule whose conditions are all advisory is always valid (warnings only).\n" + } + } + }, + "Operator": { + "type": "string", + "description": "Predefined comparison operator. Compatibility (enforced at write time):\n- number: equal, notEqual, greaterThan, greaterThanInclusive, lessThan, lessThanInclusive, between, regexMatch,\n maxDigits, maxDecimals\n- date: dateBefore, dateOnOrBefore, dateAfter, dateOnOrAfter, dateBetween, notInFuture, notInPast, regexMatch\n- text: equal, notEqual, contains, doesNotContain, startsWith, endsWith, regexMatch, lengthBetween,\n greaterThan, greaterThanInclusive, lessThan, lessThanInclusive, between, maxDigits, maxDecimals\nRange operators (between, dateBetween, lengthBetween) require a `range` value;\nunary operators (notInFuture, notInPast) require a `none` value; all others require a scalar value.\nregexMatch validates the raw input string's format and always takes a static string pattern.\nNumeric comparison operators on text rules parse the input as a number at evaluation time\n(free-text fields often hold numbers); unparsable input fails the condition.\nmaxDigits limits how many digits the written input may contain in total (grouping\nseparators, sign and the decimal separator are not counted); maxDecimals limits how many\ndigits may follow the decimal separator. Both take a non-negative integer comparison value\nand, like the other numeric operators, are also allowed on text rules.\n", + "enum": [ + "equal", + "notEqual", + "greaterThan", + "greaterThanInclusive", + "lessThan", + "lessThanInclusive", + "between", + "dateBefore", + "dateOnOrBefore", + "dateAfter", + "dateOnOrAfter", + "dateBetween", + "notInFuture", + "notInPast", + "contains", + "doesNotContain", + "startsWith", + "endsWith", + "regexMatch", + "lengthBetween", + "maxDigits", + "maxDecimals" + ] + }, + "ConditionValue": { + "description": "The comparison value of a condition - a scalar, a range of scalars, or nothing (unary operators).", + "oneOf": [ + { + "$ref": "#/components/schemas/StaticValue" + }, + { + "$ref": "#/components/schemas/ContextValue" + }, + { + "$ref": "#/components/schemas/RelativeDateValue" + }, + { + "$ref": "#/components/schemas/EnvironmentValue" + }, + { + "$ref": "#/components/schemas/ExternalValue" + }, + { + "$ref": "#/components/schemas/RangeValue" + }, + { + "$ref": "#/components/schemas/NoValue" + } + ] + }, + "ScalarValue": { + "description": "A single comparison value - static, resolved from context, a relative date, an organisation environment variable, or an external value.", + "oneOf": [ + { + "$ref": "#/components/schemas/StaticValue" + }, + { + "$ref": "#/components/schemas/ContextValue" + }, + { + "$ref": "#/components/schemas/RelativeDateValue" + }, + { + "$ref": "#/components/schemas/EnvironmentValue" + }, + { + "$ref": "#/components/schemas/ExternalValue" + } + ] + }, + "EnvironmentValue": { + "type": "object", + "description": "A comparison value resolved at evaluation time from an organisation environment variable\n(environments-api). The rule stores the key, never the value, so one change to the variable\nreaches every rule that references it and a blueprint install never overwrites the\norganisation's own value.\n\nOnly browser-safe variable types are allowed: `Number` for numeric comparisons, `Text` for\ntext comparisons and for dates (ISO 8601 string), `Boolean` for `applies_when` values.\n`String` and `SecretString` variables are rejected at write time. Write-time validation also\nchecks that the variable exists in the organisation; a variable that exists without a value\n(for example seeded by a blueprint install) is accepted and resolves as unavailable until set.\n", + "required": [ + "source", + "key" + ], + "additionalProperties": false, + "properties": { + "source": { + "type": "string", + "enum": [ + "environment" + ] + }, + "key": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9_.\\-]{0,127}$", + "description": "Environment variable key, e.g. `abschlag.max`." + }, + "adjust": { + "$ref": "#/components/schemas/ValueAdjustment" + } + } + }, + "ExternalValue": { + "type": "object", + "description": "A comparison value produced at evaluation time by an External Values hook of an installed\napp (component type `EXTERNAL_VALUES`). The rule stores the reference only; the value is\nresolved server-side by the external-values-api, which executes the hook's HTTP call with\nthe app's credentials and returns the typed result identified by `result_id`.\n\nWrite-time validation checks that the app is installed in the organisation, that the hook\nand result exist, and that the result's type is compatible with the compared kind\n(`number` results for numeric comparisons, `text` for text, `date` for date). External values\nare resolved only for authenticated consumers (epilot 360 users and portal end customers);\nin public journeys the dependent conditions are skipped.\n", + "required": [ + "source", + "app_id", + "hook_id", + "result_id" + ], + "additionalProperties": false, + "properties": { + "source": { + "type": "string", + "enum": [ + "external" + ] + }, + "app_id": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "ID of the installed app that provides the External Values component." + }, + "hook_id": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "ID of the hook within the app's External Values component." + }, + "result_id": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "ID of the result entry within the hook whose value is compared." + }, + "adjust": { + "$ref": "#/components/schemas/ValueAdjustment" + } + } + }, + "StaticValue": { + "type": "object", + "description": "A fixed comparison value.", + "required": [ + "source", + "data" + ], + "additionalProperties": false, + "properties": { + "source": { + "type": "string", + "enum": [ + "static" + ] + }, + "data": { + "description": "The literal value. Numbers for numeric comparisons, ISO 8601 date strings for date comparisons, strings for text comparisons.", + "oneOf": [ + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "boolean" + } + ] + } + } + }, + "ContextValue": { + "type": "object", + "description": "A dynamic comparison value resolved from runtime context, e.g. `contract.installment_amount`\nor `previous_reading.value`. The first path segment must match the `name` of a declared\ncontext requirement.\n", + "required": [ + "source", + "path" + ], + "additionalProperties": false, + "properties": { + "source": { + "type": "string", + "enum": [ + "context" + ] + }, + "path": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*(\\.[a-zA-Z0-9_]+)*$", + "description": "Dot-separated path into the resolved context." + }, + "adjust": { + "$ref": "#/components/schemas/ValueAdjustment" + } + } + }, + "ValueAdjustment": { + "type": "object", + "description": "Adjusts a context-resolved numeric value before comparison, e.g. \"context value plus 10 percent\".\nUsed to express tolerance bands such as \"at most 10% above the current instalment amount\".\n", + "required": [ + "type", + "value", + "direction" + ], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "enum": [ + "percent", + "absolute" + ] + }, + "value": { + "description": "The adjustment amount - a fixed number, or a context path resolving to a number\n(e.g. a per-contract percentage limit). Contextual adjustment values must not\ncarry a nested adjustment.\n", + "oneOf": [ + { + "type": "number", + "minimum": 0 + }, + { + "$ref": "#/components/schemas/ContextValue" + }, + { + "$ref": "#/components/schemas/EnvironmentValue" + } + ] + }, + "direction": { + "type": "string", + "enum": [ + "increase", + "decrease" + ] + }, + "rounding": { + "type": "string", + "enum": [ + "up", + "down" + ], + "description": "Rounds the adjusted result to a whole number - `up` (ceiling) or `down` (floor).\nOmitted means no rounding. Used for bounds like \"round up(current \u00d7 0.9) to whole euros\".\n" + } + } + }, + "RelativeDateValue": { + "type": "object", + "description": "A date relative to the evaluation moment, e.g. \"today minus 30 days\". Only valid for date rules.", + "required": [ + "source", + "offset", + "unit" + ], + "additionalProperties": false, + "properties": { + "source": { + "type": "string", + "enum": [ + "relative_date" + ] + }, + "offset": { + "type": "integer", + "minimum": -36500, + "maximum": 36500, + "description": "Offset from the anchor. Negative values are in the past, positive in the future, 0 is the anchor itself." + }, + "unit": { + "type": "string", + "enum": [ + "days", + "months", + "years" + ] + }, + "anchor": { + "type": "string", + "enum": [ + "today" + ], + "default": "today" + } + } + }, + "RangeValue": { + "type": "object", + "description": "A lower and upper bound for range operators (between, dateBetween, lengthBetween). Bounds are inclusive.", + "required": [ + "source", + "min", + "max" + ], + "additionalProperties": false, + "properties": { + "source": { + "type": "string", + "enum": [ + "range" + ] + }, + "min": { + "$ref": "#/components/schemas/ScalarValue" + }, + "max": { + "$ref": "#/components/schemas/ScalarValue" + } + } + }, + "NoValue": { + "type": "object", + "description": "No comparison value - used by unary operators such as notInFuture / notInPast.", + "required": [ + "source" + ], + "additionalProperties": false, + "properties": { + "source": { + "type": "string", + "enum": [ + "none" + ] + } + } + }, + "DocumentRuleType": { + "type": "object", + "description": "Declarative validation rule (schema version v2) for uploaded files. The rule declares how deep\nthe check goes (`check.level`) and the conditions the file must satisfy. Property conditions\n(file type, size, page count, image resolution, blank and password-protected detection) are\ndeterministic and available at every level; the `meetsCriteria` condition judges a free-text\nrequirement against the document's extracted content and is available from level `standard`.\n\nEvaluation semantics for content conditions differ from scalar rules: each condition resolves to\npass, fail or uncertain. A confident fail on a blocking condition fails the input; an uncertain\nresult is always reported as advisory and never fails the input, regardless of `allow_failure`.\nDocument rules carry no `contexts` yet.\n", + "required": [ + "input_type", + "check", + "conditions" + ], + "additionalProperties": false, + "properties": { + "input_type": { + "type": "string", + "enum": [ + "document" + ], + "description": "Identifies the rule as a document rule. Determines which operators are allowed." + }, + "check": { + "$ref": "#/components/schemas/DocumentCheck" + }, + "conditions": { + "type": "array", + "description": "The checks the uploaded file must satisfy. All blocking conditions must pass for the\nfile to be valid; `allow_failure` conditions are advisory. Must contain at least one\ncondition (enforced at write time).\n", + "items": { + "$ref": "#/components/schemas/DocumentCondition" + } + } + } + }, + "DocumentCheck": { + "type": "object", + "description": "How deep the document check goes. Levels are cumulative.", + "required": [ + "level" + ], + "additionalProperties": false, + "properties": { + "level": { + "type": "string", + "enum": [ + "basic", + "standard", + "advanced" + ], + "description": "- `basic`: file properties only (type, size, page count, resolution, blank, password). Instant, no extraction.\n- `standard`: basic checks plus a quick scan of the content. A few seconds.\n- `advanced`: basic checks plus a thorough reading of the fully extracted content. Roughly 5 to 10 seconds.\n" + } + } + }, + "DocumentCondition": { + "type": "object", + "description": "A single check the uploaded file must satisfy.", + "required": [ + "id", + "operator", + "value", + "error_message" + ], + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "minLength": 1, + "maxLength": 50, + "description": "Stable identifier of the condition within the rule, used for editing and error reporting." + }, + "operator": { + "$ref": "#/components/schemas/DocumentOperator" + }, + "value": { + "$ref": "#/components/schemas/DocumentConditionValue" + }, + "error_message": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Message shown to the end user when this condition fails." + }, + "allow_failure": { + "type": "boolean", + "default": false, + "description": "When true, the condition is advisory: a failure is reported but never takes part in the\nvalidity verdict. Recommended for `meetsCriteria` until the organisation has reviewed\nresults on real files.\n" + } + } + }, + "DocumentOperator": { + "type": "string", + "description": "Predefined document check. Value compatibility (enforced at write time):\n- fileTypeIn: `file_types` value listing the accepted file categories.\n- sizeBelow: static integer, maximum file size in bytes.\n- pageCountBetween: `range` of static non-negative integers (inclusive).\n- resolutionAtLeast / resolutionAtMost: static positive integer, the image's longer edge in pixels.\n- isNotBlank / isNotPasswordProtected: `none` value (unary).\n- meetsCriteria: `criteria` value with the free-text requirement. Requires check level `standard` or `advanced`.\n", + "enum": [ + "fileTypeIn", + "sizeBelow", + "pageCountBetween", + "resolutionAtLeast", + "resolutionAtMost", + "isNotBlank", + "isNotPasswordProtected", + "meetsCriteria" + ] + }, + "DocumentConditionValue": { + "description": "The value of a document condition - a static scalar, a range, nothing, accepted file categories, or a free-text criterion.", + "oneOf": [ + { + "$ref": "#/components/schemas/StaticValue" + }, + { + "$ref": "#/components/schemas/RangeValue" + }, + { + "$ref": "#/components/schemas/NoValue" + }, + { + "$ref": "#/components/schemas/FileTypesValue" + }, + { + "$ref": "#/components/schemas/CriteriaValue" + } + ] + }, + "FileTypesValue": { + "type": "object", + "description": "Accepted file categories; each category expands to a mime list at check time.", + "required": [ + "source", + "types" + ], + "additionalProperties": false, + "properties": { + "source": { + "type": "string", + "enum": [ + "file_types" + ] + }, + "types": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "image", + "pdf", + "document", + "spreadsheet" + ] + } + } + } + }, + "CriteriaValue": { + "type": "object", + "description": "A free-text requirement judged against the document's extracted content, e.g. \"The photo shows\nan electricity meter with the meter number and the current reading fully legible.\" One requirement\nper condition keeps results specific and lets each requirement be graded separately.\n", + "required": [ + "source", + "text" + ], + "additionalProperties": false, + "properties": { + "source": { + "type": "string", + "enum": [ + "criteria" + ] + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 500 + } + } + }, + "AppliesWhen": { + "type": "object", + "description": "Optional precondition on a condition: the condition only takes part in the\nvalidation when this comparison over context holds. Examples: apply the\ndual-tariff reference only when `contract.htnt` is not empty, or run a\nplausibility check only when a context value reaches a threshold.\n", + "required": [ + "path", + "operator" + ], + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*(\\.[a-zA-Z0-9_]+)*$", + "description": "Dot-separated context path whose first segment must match a declared context schema." + }, + "operator": { + "type": "string", + "enum": [ + "equal", + "notEqual", + "greaterThan", + "greaterThanInclusive", + "lessThan", + "lessThanInclusive", + "isEmpty", + "isNotEmpty" + ], + "description": "Comparison applied to the resolved context value. `isEmpty` / `isNotEmpty`\ntake no `value`; all other operators require one.\n" + }, + "value": { + "description": "Comparison value for binary operators: a static number, string or boolean, or an\norganisation environment variable (`Number`, `Text` or `Boolean`) so a precondition\ncan act as a per-organisation switch or threshold.\n", + "oneOf": [ + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "boolean" + }, + { + "$ref": "#/components/schemas/EnvironmentValue" + } + ] + } + } + }, + "ContextRequirement": { + "type": "object", + "description": "An entity context source the rule needs at evaluation time, referenced by `context`\nvalue paths via the schema slug as their first segment (e.g. `contract.installment_amount`).\nHow the source is resolved (which entity instance) is decided by the consuming surface,\nnot by the rule. Meter reading comparisons use the meter/meter_counter entity schemas\n(e.g. `meter_counter.current_consumption` for the previous reading value).\n", + "required": [ + "schema" + ], + "additionalProperties": false, + "properties": { + "schema": { + "type": "string", + "minLength": 1, + "description": "Entity schema slug.", + "example": "contract" + } + } } } }, diff --git a/packages/cli/definitions/webhooks.json b/packages/cli/definitions/webhooks.json index d06c862c5..a9940a109 100644 --- a/packages/cli/definitions/webhooks.json +++ b/packages/cli/definitions/webhooks.json @@ -763,6 +763,17 @@ "description": "Event id" } ], + "requestBody": { + "description": "Optional replay options", + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReplayRequest" + } + } + } + }, "responses": { "204": { "description": "Success - replay event" @@ -777,6 +788,16 @@ } } }, + "422": { + "description": "Re-apply requested but the original (pre-transform) input could not be reconstructed (e.g. dangling catalog reference past retention).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + } + }, "500": { "description": "Other errors", "content": { @@ -1073,14 +1094,15 @@ }, "WebhookCondition": { "type": "object", - "description": "A condition that must be met for the webhook to fire.", + "description": "A condition that must be met for the webhook to fire. A condition is EITHER a classic field + operation comparison OR a single jsonata_expression evaluated against the event payload — the two forms are mutually exclusive.", "properties": { "field": { "type": "string", - "description": "Dot-notation path to the field in the event payload (e.g. \"entity.status\", \"entity.line_items\")" + "description": "Dot-notation path to the field in the event payload (e.g. \"entity.status\", \"entity.line_items\"). Supports array indices in either spelling, which resolve identically: \"meter_readings.1.file_id\" or \"meter_readings[1].file_id\"." }, "operation": { "type": "string", + "description": "Comparison operator. The length_* operations compare the item count of the field value (an array yields its length, a missing or null field yields 0, and any single non-array value yields 1) against values[0], which must be a non-negative integer.", "enum": [ "equals", "not_equals", @@ -1095,7 +1117,13 @@ "greater_than_or_equals", "less_than_or_equals", "is_empty", - "is_not_empty" + "is_not_empty", + "length_equals", + "length_not_equals", + "length_greater_than", + "length_less_than", + "length_greater_than_or_equals", + "length_less_than_or_equals" ] }, "values": { @@ -1126,12 +1154,13 @@ "type": "boolean", "description": "When true, evaluates conditions per-item in repeatable array fields", "default": false + }, + "jsonata_expression": { + "type": "string", + "maxLength": 15000, + "description": "JSONata expression evaluated against the event payload; the condition passes when the result is truthy per JSONata $boolean semantics. Mutually exclusive with field/operation." } - }, - "required": [ - "field", - "operation" - ] + } }, "WebhookConditionGroup": { "type": "object", @@ -1477,7 +1506,8 @@ ], "properties": { "status_code": { - "type": "string" + "type": "string", + "description": "The HTTP status returned by the customer endpoint (or the upstream gateway). ABSENT for epilot-internal errors (code INTERNAL_ERROR / INTERNAL_RESPONSE_TOO_LARGE), which are not customer HTTP outcomes; PRESENT only for real HTTP/upstream results. When the response was delivered but too large to record, a recovered delivered status may still be present." }, "message": { "type": "string" @@ -1486,7 +1516,8 @@ "type": "object" }, "code": { - "type": "string" + "type": "string", + "description": "Machine-readable classification of the outcome. Known values: OAuthTokenExchangeError (OAuth token exchange / upstream gateway failure), DataLimitExceeded (legacy oversized-response marker), INTERNAL_RESPONSE_TOO_LARGE (request delivered but the response was too large for epilot to record), INTERNAL_ERROR (epilot-internal processing failure — not a customer HTTP outcome)." }, "status": { "type": "string", @@ -1657,7 +1688,8 @@ "type": "object", "properties": { "status_code": { - "type": "integer" + "type": "integer", + "description": "The HTTP status returned by the customer endpoint (or the upstream gateway). ABSENT for epilot-internal errors (code INTERNAL_ERROR / INTERNAL_RESPONSE_TOO_LARGE), which are not customer HTTP outcomes; PRESENT only for real HTTP/upstream results. When the response was delivered but too large to record, a recovered delivered status may still be present." }, "message": { "type": "string" @@ -1666,7 +1698,8 @@ "type": "object" }, "code": { - "type": "string" + "type": "string", + "description": "Machine-readable classification of the outcome. Known values: OAuthTokenExchangeError (OAuth token exchange / upstream gateway failure), DataLimitExceeded (legacy oversized-response marker), INTERNAL_RESPONSE_TOO_LARGE (request delivered but the response was too large for epilot to record), INTERNAL_ERROR (epilot-internal processing failure — not a customer HTTP outcome)." } } }, @@ -1694,9 +1727,26 @@ "type": "string", "description": "stringified payload of the webhook request" }, + "original_payload": { + "type": "string", + "description": "Stringified pre-transform original payload — the raw input captured before any JSONata transform was applied. Returned by getEventById for non-catalog events that have a locally stored original source (inline or claim-checked to S3, hydrated here). Omitted when no original source exists or when the payload would exceed the inline response size limit. Catalog-backed events (event_-prefixed) do not carry a local original — their pre-transform payload is sourced from the event catalog instead." + }, "retry_attempt": { "type": "integer", "description": "Number of automatic delivery retries that preceded this terminal outcome. 0 means the event was delivered (or finally failed) on the first attempt." + }, + "can_reapply_transform": { + "type": "boolean", + "description": "Whether this event can be replayed with the JSONata transform re-applied. True only when the webhook config has a non-empty jsonataExpression AND a usable original source exists (a catalog reference or a locally stored original payload). Computed without a synchronous catalog round-trip." + }, + "can_reapply_transform_reason": { + "type": "string", + "enum": [ + "available", + "no_transform_configured", + "no_original_source" + ], + "description": "Reason explaining the can_reapply_transform value." } }, "required": [ @@ -1766,6 +1816,16 @@ "message" ] }, + "ReplayRequest": { + "type": "object", + "properties": { + "reapply_transform": { + "type": "boolean", + "default": false, + "description": "When true, reconstruct the original (pre-transform) event input and re-run the full delivery pipeline (file-loading + JSONata). When false or omitted, resend the previously delivered (post-transform) payload verbatim (backward-compatible default)." + } + } + }, "BatchReplayRequest": { "type": "object", "properties": { @@ -1781,6 +1841,11 @@ "2f1b7cf8-ff55-4359-966f-e56f39a52c94", "48c984bf-466b-470b-b743-d07cea168243" ] + }, + "reapply_transform": { + "type": "boolean", + "default": false, + "description": "When true, reconstruct the original (pre-transform) input for each event and re-run the full delivery pipeline (file-loading + JSONata). When false or omitted, resend the previously delivered (post-transform) payload verbatim (backward-compatible default)." } }, "required": [ diff --git a/packages/cli/definitions/workflow-definition.json b/packages/cli/definitions/workflow-definition.json index e5db147d5..d1a16b39d 100644 --- a/packages/cli/definitions/workflow-definition.json +++ b/packages/cli/definitions/workflow-definition.json @@ -3,13 +3,27 @@ "info": { "title": "Workflows Definitions", "version": "1.1.0", - "description": "Service for Workflow Definitions for different processes inside of an Organization\n" + "description": "The Workflows Definitions API enables you to create, manage, and configure reusable workflow templates\nwithin your organization. Workflow definitions serve as blueprints that define the structure and behavior\nof business processes, which can then be instantiated as workflow executions.\n\n## Core Concepts\n\n### Workflow Definition (V1)\nA workflow definition is a template that describes a business process. It consists of:\n- **Sections**: Logical groupings (phases) that organize related steps together\n- **Steps**: Individual tasks or actions that need to be completed within a section\n- **Closing Reasons**: Predefined reasons that can be selected when closing/completing a workflow\n\n### Flow Template (V2)\nThe modern workflow model that provides advanced capabilities:\n- **Phases**: Named stages that group related tasks and track progress through the workflow\n- **Tasks**: Individual units of work that can be manual, automated, AI-powered, or decision points\n- **Edges**: Connections between tasks that define the flow sequence and support conditional branching\n- **Triggers**: Define how a workflow is started (manual, automation, journey submission)\n\n### Task Types (V2)\n- **MANUAL**: Tasks assigned to users that require human action to complete\n- **AUTOMATION**: Tasks that execute automated actions when reached\n- **DECISION**: Conditional branching points that evaluate conditions to determine the next path\n- **AI_AGENT**: Tasks that invoke AI agents to perform intelligent actions\n\n## API Versions\n- **V1 endpoints** (`/v1/workflows/...`): Legacy linear workflow model with sections and steps\n- **V2 endpoints** (`/v2/flows/...`): Modern graph-based flow model with phases, tasks, and edges\n" }, "servers": [ { "url": "https://workflows-definition.sls.epilot.io" } ], + "tags": [ + { + "name": "Workflows", + "description": "Manage V1 workflow definitions with sections and steps. These endpoints support the legacy\nlinear workflow model where definitions contain a sequential flow of sections and steps.\n" + }, + { + "name": "Flows V2", + "description": "Manage V2 flow templates with phases, tasks, and edges. The modern workflow model supports\nadvanced features including conditional branching, multiple trigger types, automation tasks,\nAI agent tasks, and flexible task dependencies.\n" + }, + { + "name": "Closing Reason", + "description": "Manage closing reasons that can be associated with workflows. Closing reasons provide\npredefined options for users to select when closing or completing a workflow execution,\nenabling better tracking and reporting of workflow outcomes.\n" + } + ], "security": [ { "BearerAuth": [] @@ -40,7 +54,7 @@ } }, "500": { - "description": "Other errors", + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -56,7 +70,7 @@ "get": { "operationId": "getDefinitions", "summary": "getDefinitions", - "description": "Retrieve all Workflow Definitions from an Organization", + "description": "Retrieve all V1 workflow definitions belonging to the authenticated organization.", "tags": [ "Workflows" ], @@ -135,7 +149,7 @@ } }, "500": { - "description": "Other errors", + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -149,7 +163,7 @@ "post": { "operationId": "createDefinition", "summary": "createDefinition", - "description": "Create a Workflow Definition.", + "description": "Create a new V1 workflow definition. The definition consists of sections and steps\nthat define the structure of the workflow. Once created, the definition can be used\nto start workflow executions.\n", "tags": [ "Workflows" ], @@ -387,17 +401,89 @@ "post": { "operationId": "createFlowTemplate", "summary": "createFlowTemplate", - "description": "Create a new Flow Template.", + "description": "Create a new Flow Template (V2 workflow definition).\n\nA flow template defines the structure of a workflow including its phases, tasks,\nedges (connections between tasks), and trigger configuration. Once created, the\ntemplate can be used to start workflow executions.\n", "tags": [ "Flows V2" ], + "parameters": [ + { + "$ref": "#/components/parameters/EnforceLimitsParam" + } + ], "requestBody": { - "description": "Flow Template payload", + "description": "Flow Template payload containing the workflow structure.\n\nRequired fields:\n- `name`: Display name for the workflow\n- `tasks`: Array of task definitions (manual, automation, decision, or AI agent tasks)\n- `edges`: Array of connections defining the flow between tasks\n", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateFlowTemplate" + }, + "example": { + "name": "Customer Onboarding", + "description": "Standard customer onboarding workflow with verification steps", + "version": "v3", + "enabled": true, + "trigger": { + "type": "manual", + "entity_schema": "contact" + }, + "phases": [ + { + "id": "phase_1", + "name": "Verification" + }, + { + "id": "phase_2", + "name": "Setup" + } + ], + "tasks": [ + { + "id": "task_1", + "name": "Verify customer identity", + "task_type": "MANUAL", + "phase_id": "phase_1", + "assigned_to": [ + "user_123" + ] + }, + { + "id": "task_2", + "name": "Send welcome email", + "task_type": "AUTOMATION", + "phase_id": "phase_2", + "automation_config": { + "flow_id": "automation_456" + }, + "trigger_mode": "automatic" + }, + { + "id": "task_3", + "name": "Schedule follow-up", + "task_type": "MANUAL", + "phase_id": "phase_2" + } + ], + "edges": [ + { + "id": "edge_1", + "from_id": "task_1", + "to_id": "task_2" + }, + { + "id": "edge_2", + "from_id": "task_2", + "to_id": "task_3" + } + ], + "closing_reasons": [ + { + "id": "reason_completed" + }, + { + "id": "reason_cancelled" + } + ] } } } @@ -512,7 +598,7 @@ "get": { "operationId": "getFlowTemplate", "summary": "getFlowTemplate", - "description": "Get specific FLow template for a customer", + "description": "Retrieve a specific flow template by its unique identifier.", "tags": [ "Flows V2" ], @@ -523,7 +609,8 @@ "schema": { "$ref": "#/components/schemas/FlowTemplateId" }, - "required": true + "required": true, + "description": "Unique identifier of the flow template to retrieve." } ], "responses": { @@ -593,11 +680,15 @@ "schema": { "$ref": "#/components/schemas/FlowTemplateId" }, - "required": true + "required": true, + "description": "Unique identifier of the flow template to update." + }, + { + "$ref": "#/components/parameters/EnforceLimitsParam" } ], "requestBody": { - "description": "Flow Template payload", + "description": "Updated flow template payload containing the full workflow definition.", "required": true, "content": { "application/json": { @@ -619,7 +710,7 @@ } }, "400": { - "description": "Validation Errors", + "description": "Validation errors - request body contains invalid data", "content": { "application/json": { "schema": { @@ -629,7 +720,7 @@ } }, "401": { - "description": "Authentication Errors", + "description": "Authentication failed - invalid or missing Bearer token", "content": { "application/json": { "schema": { @@ -638,6 +729,16 @@ } } }, + "404": { + "description": "Flow template not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DefinitionNotFoundResp" + } + } + } + }, "409": { "description": "Conflict - the workflow was modified by another user since it was last loaded", "content": { @@ -649,7 +750,7 @@ } }, "500": { - "description": "Other errors", + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -675,7 +776,7 @@ "$ref": "#/components/schemas/FlowTemplateId" }, "required": true, - "description": "Id of the flow template to de deleted." + "description": "Unique identifier of the flow template to be deleted." } ], "responses": { @@ -693,7 +794,24 @@ } }, "404": { - "description": "No definition found" + "description": "Flow template not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DefinitionNotFoundResp" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + } } } } @@ -702,7 +820,7 @@ "post": { "operationId": "duplicateFlowTemplate", "summary": "duplicateFlowTemplate", - "description": "Duplicate a Flow Template from an existing workflow.", + "description": "Create a copy of an existing flow template. The duplicated template will have a new\nunique identifier and can be modified independently of the original. This is useful\nfor creating variations of existing workflows without starting from scratch.\n", "tags": [ "Flows V2" ], @@ -713,7 +831,8 @@ "schema": { "$ref": "#/components/schemas/FlowTemplateId" }, - "required": true + "required": true, + "description": "Unique identifier of the flow template to duplicate." } ], "responses": { @@ -728,7 +847,7 @@ } }, "400": { - "description": "Validation Errors", + "description": "Validation errors - source template contains invalid data", "content": { "application/json": { "schema": { @@ -738,7 +857,7 @@ } }, "401": { - "description": "Authentication Errors", + "description": "Authentication failed - invalid or missing Bearer token", "content": { "application/json": { "schema": { @@ -747,8 +866,18 @@ } } }, + "404": { + "description": "Source flow template not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DefinitionNotFoundResp" + } + } + } + }, "500": { - "description": "Other errors", + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -1106,7 +1235,7 @@ "type": "string" }, "required": true, - "description": "Id of the definition to de deleted.", + "description": "Unique identifier of the workflow definition to be deleted.", "example": "CustomerRequest" } ], @@ -1143,7 +1272,7 @@ "type": "boolean" }, "required": false, - "description": "Filter Closing Reasons by status like active inactiv", + "description": "When set to true, includes inactive closing reasons in the response. By default, only active closing reasons are returned.", "example": true } ], @@ -1588,6 +1717,18 @@ } }, "components": { + "parameters": { + "EnforceLimitsParam": { + "in": "query", + "name": "enforce_limits", + "required": false, + "schema": { + "type": "boolean", + "default": false + }, + "description": "When true, size/count limit violations (max incoming edges per task, max branches, max entity-sync rules, ...) are enforced as 400 errors. Used by the flow-builder UI for interactive authoring. On update, pre-existing violations are tolerated as long as the request does not worsen them. When false or absent (default for API/system callers such as blueprints, configuration hub and snapshots), limit violations are accepted and returned as `limit_warnings`. Structural errors are always enforced regardless of this flag." + } + }, "securitySchemes": { "BearerAuth": { "type": "http", @@ -1598,6 +1739,7 @@ "schemas": { "FlowTemplateBase": { "type": "object", + "description": "Base schema for V2 flow templates. A flow template defines the structure of a workflow\nincluding phases, tasks, edges, triggers, and other configuration. Flow templates serve\nas blueprints that can be instantiated as flow executions.\n", "required": [ "name", "tasks", @@ -1605,16 +1747,22 @@ ], "properties": { "id": { - "type": "string" + "type": "string", + "description": "Unique identifier for the flow template, generated by the system." }, "org_id": { - "type": "string" + "type": "string", + "description": "Organization identifier that owns this flow template." }, "name": { - "type": "string" + "type": "string", + "description": "Display name for the workflow, shown to users in the UI.", + "minLength": 3 }, "description": { - "type": "string" + "type": "string", + "description": "Optional detailed description of the workflow's purpose and behavior.", + "maxLength": 5000 }, "trigger": { "$ref": "#/components/schemas/Trigger" @@ -1715,6 +1863,64 @@ "items": { "type": "string" } + }, + "linear": { + "type": "boolean", + "description": "When true, this is a linear flow: task enablement is computed at runtime from the graph — a task is enabled only when all of its direct predecessor tasks in its branch are done. This fully overrides any explicit per-task requirements." + }, + "limit_warnings": { + "type": "array", + "description": "Non-blocking warnings for configuration limits the flow exceeds. Returned on read (get-flow) and on create/update responses. Size/count limits are soft: a flow exceeding them is still accepted and stored, and the violations are reported here so the UI can surface an informational banner. The flow is still fully editable and usable.", + "items": { + "$ref": "#/components/schemas/FlowLimitWarning" + } + } + } + }, + "FlowLimitWarning": { + "type": "object", + "description": "A configuration limit that the flow currently exceeds. Returned by the get-flow endpoint and by create/update responses so the UI can show a non-blocking banner. Size/count limit violations never cause a 400 — only structural errors do. The flow remains fully usable; users are encouraged to bring values within the limits.", + "required": [ + "i18nKey", + "message", + "max", + "path" + ], + "properties": { + "i18nKey": { + "type": "string", + "description": "i18n key identifying the exceeded limit (e.g. errors.task_max_incoming_edges)." + }, + "message": { + "type": "string", + "description": "Human-readable description of the exceeded limit." + }, + "max": { + "type": "number", + "description": "The configured maximum for this limit." + }, + "current": { + "type": "number", + "description": "The actual value the flow currently has for this limit (e.g. the actual number of incoming connections)." + }, + "node_id": { + "type": "string", + "description": "ID of the task/node the limit applies to, when applicable." + }, + "task_name": { + "type": "string" + }, + "branch_name": { + "type": "string" + }, + "param_name": { + "type": "string", + "description": "Name of the AI-agent parameter the limit applies to, when applicable." + }, + "path": { + "type": "array", + "items": {}, + "description": "JSON path within the flow to the offending value." } } }, @@ -2159,7 +2365,8 @@ ] }, "duration": { - "type": "number" + "type": "number", + "minimum": 1 }, "unit": { "$ref": "#/components/schemas/TimeUnit" @@ -2182,7 +2389,8 @@ ] }, "duration": { - "type": "number" + "type": "number", + "minimum": 1 }, "unit": { "$ref": "#/components/schemas/TimeUnit" @@ -2273,8 +2481,7 @@ "type": "integer", "description": "Maximum number of iterations for the loop branch", "default": 3, - "minimum": 1, - "maximum": 100 + "minimum": 1 } }, "required": [ @@ -2473,7 +2680,6 @@ "branch_name": { "type": "string", "description": "The name of the branch", - "maxLength": 255, "example": "Branch 1" }, "logical_operator": { @@ -2597,6 +2803,22 @@ "deleted" ] }, + "attributes": { + "type": "array", + "maxItems": 10, + "items": { + "type": "string" + }, + "description": "Multi-attribute mode. When present and length > 1, the statement is\nevaluated against every listed attribute and combined via\n`attributes_match`. All listed attributes must share the same\n`attribute_type`. Mutually exclusive with `attribute_sub_field`,\n`date_offset`, and `attribute_operation`. When absent or length === 1,\nthe legacy `attribute` field is used.\n" + }, + "attributes_match": { + "type": "string", + "enum": [ + "any", + "all" + ], + "description": "Inner connector across `attributes`. `any` (default) means at least\none attribute must satisfy the operator; `all` means every attribute\nmust satisfy it. Ignored when `attributes` is absent or has length < 2.\n" + }, "attribute_sub_field": { "type": "string", "description": "For complex attribute types, specifies which sub-field to extract (e.g., \"address\", \"name\", \"email_type\")" @@ -2681,7 +2903,8 @@ "hours", "days", "weeks", - "months" + "months", + "years" ] }, "EnableRequirement": { @@ -2710,15 +2933,19 @@ }, "WorkflowDefinition": { "type": "object", + "description": "V1 workflow definition schema. Defines a linear workflow structure with sections\nand steps. Sections group related steps together, and steps represent individual\ntasks that need to be completed. This is the legacy workflow model; for new\nimplementations, consider using V2 flow templates instead.\n", "properties": { "id": { - "type": "string" + "type": "string", + "description": "Unique identifier for the workflow definition, generated by the system." }, "name": { - "type": "string" + "type": "string", + "description": "Display name for the workflow, shown to users in the UI." }, "description": { - "type": "string" + "type": "string", + "description": "Optional detailed description of the workflow's purpose and behavior." }, "creationTime": { "type": "string", @@ -3029,7 +3256,8 @@ "type": "boolean" }, "value": { - "type": "string" + "type": "string", + "maxLength": 10000 } } }, @@ -3045,18 +3273,24 @@ } }, "DefinitionNotFoundResp": { - "description": "Definition could be not found", + "type": "object", + "description": "Error response returned when a workflow definition or flow template is not found.", "properties": { "message": { - "type": "string" + "type": "string", + "description": "Human-readable message indicating the resource was not found.", + "example": "Definition with id 'abc123' not found" } } }, "ClosingReasonNotFoundResp": { - "description": "Closing reason could be not found", + "type": "object", + "description": "Error response returned when a closing reason is not found.", "properties": { "message": { - "type": "string" + "type": "string", + "description": "Human-readable message indicating the closing reason was not found.", + "example": "Closing reason with id 'xyz789' not found" } } }, @@ -3143,9 +3377,12 @@ }, "ErrorResp": { "type": "object", + "description": "Standard error response returned when an API request fails.", "properties": { "message": { - "type": "string" + "type": "string", + "description": "Human-readable error message describing what went wrong.", + "example": "Invalid request body: name is required" } } }, @@ -3296,11 +3533,12 @@ ] }, "DynamicDueDate": { - "description": "set a Duedate for a step then a specific", + "description": "Configuration for calculating a due date dynamically based on workflow events.\nThe due date is computed by adding a duration (numberOfUnits + timePeriod) to\na reference point defined by actionTypeCondition (e.g., when workflow started,\nwhen a step closed, or when a phase finished).\n", "type": "object", "properties": { "numberOfUnits": { - "type": "number" + "type": "number", + "description": "Number of time units to add to the reference point." }, "timePeriod": { "$ref": "#/components/schemas/TimeUnit" diff --git a/packages/cli/definitions/workflow.json b/packages/cli/definitions/workflow.json index 59531c481..edfb93a18 100644 --- a/packages/cli/definitions/workflow.json +++ b/packages/cli/definitions/workflow.json @@ -3,7 +3,7 @@ "info": { "title": "Workflows Executions", "version": "1.0.0", - "description": "Service for Workflow Executions which covers executions of processes defined in an Organization\n" + "description": "## Overview\nThe Workflows Executions API manages the runtime instances of workflow processes within an organization.\nWhile **Workflow Definitions** (managed by the Workflows Definitions API) serve as reusable templates\nthat define the structure, phases, and tasks of a process, **Workflow Executions** are the active\ninstances created from those definitions.\n\n## Key Concepts\n\n### Definitions vs Executions\n- **Definition (Template)**: A blueprint defining workflow structure, phases, tasks, conditions, and automation rules\n- **Execution (Instance)**: A running instance of a definition, tracking actual progress, assignees, and status\n\n### Execution Lifecycle\n1. **STARTED**: Execution is active and tasks can be worked on\n2. **DONE**: All required tasks are completed\n3. **CLOSED**: Execution is terminated (completed successfully or cancelled with closing reasons)\n\n### Task Types\n- **Manual Tasks**: Require human action to complete\n- **Automation Tasks**: Execute configured automations automatically\n- **Decision Tasks**: Evaluate conditions to determine the next path in the workflow\n- **AI Agent Tasks**: Execute AI-powered agents for intelligent task processing\n\n### Contexts\nExecutions are linked to entity contexts (e.g., contacts, opportunities) that provide the data\ncontext for the workflow and allow tracking which entities a workflow operates on.\n\n## API Versions\n- **V1 (`/v1/workflows/`)**: Legacy linear phase/section/step model (deprecated for new integrations)\n- **V2 (`/v2/flows/`)**: Current graph-based model with advanced features like conditional branching,\n loops, and scheduling. **Recommended for all new integrations.**\n" }, "servers": [ { @@ -13,16 +13,19 @@ "tags": [ { "name": "Workflow Executions", - "description": "Interact with workflow executions - start / close / retrieve one or all / search / delete" + "description": "Manage V1 workflow executions (legacy linear model). Operations include starting new executions\nfrom definitions, retrieving execution details, updating execution status and assignees,\nand deleting executions. Use the Flows V2 endpoints for new integrations.\n" }, { - "name": "Workflow Steps" + "name": "Workflow Steps", + "description": "Manage individual steps within V1 workflow executions (legacy). Steps represent discrete tasks\nthat can be assigned to users, have due dates, and track completion status. Use the Flows V2\ntask endpoints for new integrations.\n" }, { - "name": "Closing Reasons" + "name": "Closing Reasons", + "description": "Retrieve closing reasons configured for workflow executions. When a workflow is closed/cancelled,\nusers can select from predefined closing reasons to document why the workflow ended. Closing\nreasons are snapshots from the definition at execution creation time.\n" }, { - "name": "Flows V2" + "name": "Flows V2", + "description": "**Recommended for new integrations.** Manage V2 flow executions using the graph-based execution model.\nThis API version supports advanced features including:\n- Conditional branching with decision tasks\n- Automation tasks with configurable triggers\n- AI agent tasks for intelligent processing\n- Task scheduling (immediate, delayed, or relative to events)\n- Loop iterations for repeatable task sequences\n- Phase-based organization with progress tracking\n" } ], "security": [ @@ -90,24 +93,61 @@ "post": { "operationId": "createExecution", "summary": "createExecution", - "description": "Create a Workflow Execution. Start a new workflow execution, based on a workflow definition (template).", + "description": "Creates a new V1 Workflow Execution from a workflow definition (template).\n\n**Note:** This is the legacy V1 API. For new integrations, use `POST /v2/flows/executions` instead.\n\nThe workflow definition specifies the structure (sections and steps) of the workflow.\nWhen created, the execution instantiates all steps and begins tracking progress.\n", "tags": [ "Workflow Executions" ], "requestBody": { - "description": "Workflow Execution payload", + "description": "Workflow execution creation payload with definition ID and entity contexts", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WorkflowExecutionCreateReq" + }, + "examples": { + "basicCreate": { + "summary": "Create with single context", + "description": "Create an execution linked to an opportunity entity", + "value": { + "workflowId": "j3f23fh23uif98", + "trigger": "MANUAL", + "contexts": [ + { + "id": "3fa3fa86-0907-4642-a57e-0fe30a19874d", + "title": "New Opportunity", + "schema": "opportunity" + } + ] + } + }, + "automatedTrigger": { + "summary": "Create from automation", + "description": "Create an execution triggered automatically by an automation rule", + "value": { + "workflowId": "j3f23fh23uif98", + "trigger": "AUTOMATIC", + "contexts": [ + { + "id": "3fa3fa86-0907-4642-a57e-0fe30a19874d", + "title": "John Doe", + "schema": "contact" + }, + { + "id": "3a6d42fa-5070-4723-b90f-41ead4303e33", + "title": "Sales Opportunity", + "schema": "opportunity" + } + ] + } + } } } } }, "responses": { "201": { - "description": "Success - if the execution is created successfully", + "description": "Execution created successfully. Returns the complete workflow execution\nincluding all sections, steps, and initial status.\n", "content": { "application/json": { "schema": { @@ -117,31 +157,40 @@ } }, "400": { - "description": "Validation Errors", + "description": "Validation error. Common causes include:\n- Missing required workflowId\n- Invalid workflowId (definition not found)\n- Invalid context entity references\n", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResp" + }, + "example": { + "message": "workflowId is required" } } } }, "401": { - "description": "Authentication Errors", + "description": "Authentication failed. The Bearer token is missing, expired, or invalid.\n", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResp" + }, + "example": { + "message": "Unauthorized" } } } }, "500": { - "description": "Other errors", + "description": "Internal server error. An unexpected error occurred while processing the request.\n", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResp" + }, + "example": { + "message": "Internal server error" } } } @@ -153,7 +202,7 @@ "get": { "operationId": "getExecution", "summary": "getExecution", - "description": "Get a full workflow execution, included steps information, by execution id.", + "description": "Retrieves a complete V1 workflow execution by ID, including all steps information.\n\n**Note:** This is the legacy V1 API. For new integrations, use `GET /v2/flows/executions/{execution_id}` instead.\n", "tags": [ "Workflow Executions" ], @@ -356,7 +405,7 @@ "patch": { "operationId": "updateStep", "summary": "updateStep", - "description": "Patches various changes to a workflow execution step.", + "description": "Updates a workflow execution step with new values for status, assignees, due date, position, and more.\n\n**Note:** This is the legacy V1 API. For new integrations, use `PATCH /v2/flows/executions/{execution_id}/tasks/{task_id}` instead.\n\n**Common use cases:**\n- Mark a step as completed or skipped\n- Assign or reassign users to a step\n- Update step due dates (static or dynamic)\n- Reorder steps within a section\n", "tags": [ "Workflow Steps" ], @@ -368,7 +417,7 @@ "type": "string" }, "required": true, - "description": "Id of the execution", + "description": "Unique identifier of the workflow execution", "example": "wd56125gah" }, { @@ -378,12 +427,12 @@ "type": "string" }, "required": true, - "description": "Short uuid (length 6) to identify the Workflow Execution Step.", + "description": "Short unique identifier (typically 6 characters) of the step within the execution", "example": "7hj28a" } ], "requestBody": { - "description": "Workflow Execution Step payload", + "description": "Step update payload with fields to modify", "required": true, "content": { "application/json": { @@ -391,24 +440,57 @@ "$ref": "#/components/schemas/UpdateStepReq" }, "examples": { - "dynamicDueDate": { + "markCompleted": { + "summary": "Mark step as completed", + "description": "Complete a step, advancing the workflow", + "value": { + "status": "COMPLETED" + } + }, + "markInProgress": { + "summary": "Mark step as in progress", + "description": "Indicate that work has started on this step", + "value": { + "status": "IN_PROGRESS" + } + }, + "assignUsers": { + "summary": "Assign users to step", + "description": "Assign one or more users to the step", "value": { - "stepId": "string", "assignedTo": [ - "10010730" - ], + "10010730", + "10010731" + ] + } + }, + "setStaticDueDate": { + "summary": "Set static due date", + "description": "Set an explicit due date for the step", + "value": { + "dueDate": "2024-06-15T17:00:00.000Z" + } + }, + "setDynamicDueDate": { + "summary": "Set dynamic due date", + "description": "Configure due date relative to workflow start or another step", + "value": { "dynamicDueDate": { "numberOfUnits": 2, "timePeriod": "weeks", "actionTypeCondition": "STEP_CLOSED", - "stepId": "optional" - }, - "name": "Static Duedate", + "stepId": "abc123" + } + } + }, + "reorderStep": { + "summary": "Reorder step position", + "description": "Move a step to a different position within its section", + "value": { "position": { - "index": 0 - }, - "sectionId": "string", - "status": "string" + "index": 2, + "sectionId": "section_abc123" + } } } } @@ -417,7 +499,7 @@ }, "responses": { "200": { - "description": "Success - if the step is updated successfully", + "description": "Step updated successfully. Returns the updated step object with all current values.\n", "content": { "application/json": { "schema": { @@ -681,24 +763,80 @@ "post": { "operationId": "startFlowExecution", "summary": "startFlowExecution", - "description": "Starts a new Flow Execution based on a flow template.", + "description": "Starts a new Flow Execution based on a flow template (definition).\n\nThe flow template defines the structure of the workflow including phases, tasks, edges (transitions),\nand automation configurations. When started, the execution creates runtime instances of all tasks\nand begins processing from the initial task(s).\n\n**Required fields:**\n- `flow_template_id`: The ID of the flow template to instantiate\n- `contexts`: At least one entity context to link the execution to\n\n**Optional fields:**\n- `trigger`: Specifies how the execution was triggered (manual or automatic)\n- `purposes`: Filter which phases/tasks are included based on taxonomy purposes\n", "tags": [ "Flows V2" ], "requestBody": { - "description": "Flow Execution payload", + "description": "Flow Execution payload containing the template ID and entity contexts", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StartFlowReq" + }, + "examples": { + "basicStart": { + "summary": "Start with single entity context", + "description": "Start a flow execution linked to an opportunity entity", + "value": { + "flow_template_id": "tpl_abc123def456", + "contexts": [ + { + "entity_id": "3fa3fa86-0907-4642-a57e-0fe30a19874d", + "entity_schema": "opportunity", + "is_primary": true + } + ] + } + }, + "multipleContexts": { + "summary": "Start with multiple entity contexts", + "description": "Start a flow execution linked to both a contact and an opportunity", + "value": { + "flow_template_id": "tpl_abc123def456", + "trigger": { + "type": "MANUAL" + }, + "contexts": [ + { + "entity_id": "3fa3fa86-0907-4642-a57e-0fe30a19874d", + "entity_schema": "contact", + "is_primary": true + }, + { + "entity_id": "7bc8de90-1234-5678-90ab-cdef12345678", + "entity_schema": "opportunity", + "is_primary": false + } + ] + } + }, + "withPurposes": { + "summary": "Start with purpose filtering", + "description": "Start a flow execution with specific purposes to filter applicable phases", + "value": { + "flow_template_id": "tpl_abc123def456", + "contexts": [ + { + "entity_id": "3fa3fa86-0907-4642-a57e-0fe30a19874d", + "entity_schema": "opportunity", + "is_primary": true + } + ], + "purposes": [ + "sales", + "onboarding" + ] + } + } } } } }, "responses": { "201": { - "description": "Success - when the flow execution is started successfully", + "description": "Flow execution started successfully. Returns the complete execution object including\nall phases, tasks, edges, and initial status.\n", "content": { "application/json": { "schema": { @@ -708,27 +846,53 @@ } }, "400": { - "description": "Validation Errors", + "description": "Validation error. Common causes include:\n- Missing required fields (flow_template_id, contexts)\n- Invalid flow_template_id (template not found)\n- Invalid entity context (entity not found)\n- Empty contexts array\n", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResp" + }, + "example": { + "message": "flow_template_id is required" } } } }, "401": { - "description": "Unauthorized" + "description": "Authentication failed. The Bearer token is missing, expired, or invalid.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + }, + "example": { + "message": "Unauthorized" + } + } + } }, "403": { - "description": "Forbidden" + "description": "Access forbidden. The authenticated user does not have permission to start\nexecutions for this flow template or organization.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + }, + "example": { + "message": "Forbidden" + } + } + } }, "500": { - "description": "Other errors", + "description": "Internal server error. An unexpected error occurred while processing the request.\n", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResp" + }, + "example": { + "message": "Internal server error" } } } @@ -740,7 +904,7 @@ "get": { "operationId": "getFlowExecution", "summary": "getFlowExecution", - "description": "Get a full flow execution, included tasks, phases, edges & analytics.", + "description": "Retrieves a complete flow execution by ID, including all phases, tasks, edges, contexts, and analytics.\n\nThe response includes:\n- **Execution metadata**: ID, name, status, timestamps, assignees\n- **Phases**: Organizational groupings of tasks with progress tracking\n- **Tasks**: Individual work items with their status, assignees, and configurations\n- **Edges**: Connections between tasks defining the workflow graph\n- **Analytics**: Timing information (started, completed, closed timestamps)\n- **Contexts**: Linked entity references\n", "tags": [ "Flows V2" ], @@ -928,7 +1092,7 @@ "patch": { "operationId": "patchTask", "summary": "patchTask", - "description": "Changes various attributes of a flow task, like assignees, status, due date, etc.", + "description": "Updates attributes of a flow task including status, assignees, due date, and more.\n\n**Common use cases:**\n- Mark a task as completed or skipped\n- Assign or reassign users to a task\n- Update task due dates\n- Enable or disable a task\n- Revert execution to a previous task\n\n**Status transitions:**\n- `PENDING` -> `IN_PROGRESS`: User starts working on the task\n- `IN_PROGRESS` -> `COMPLETED`: User finishes the task\n- `PENDING` or `IN_PROGRESS` -> `SKIPPED`: Task is bypassed\n- `COMPLETED` or `SKIPPED` -> `PENDING`: Task is reopened (with revert_execution flag)\n\n**Reverting execution:**\nWhen updating a task that was already completed/skipped and comes before the current task,\nuse `revert_execution: true` to reset the flow back to that point. All subsequent tasks\nwill be reset to PENDING status.\n", "tags": [ "Flows V2" ], @@ -941,19 +1105,75 @@ } ], "requestBody": { - "description": "Patch Task Payload", + "description": "Task update payload with fields to modify", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PatchTaskReq" + }, + "examples": { + "markCompleted": { + "summary": "Mark task as completed", + "description": "Complete a task, advancing the workflow to the next task", + "value": { + "status": "COMPLETED" + } + }, + "markInProgress": { + "summary": "Mark task as in progress", + "description": "Indicate that work has started on this task", + "value": { + "status": "IN_PROGRESS" + } + }, + "skipTask": { + "summary": "Skip a task", + "description": "Bypass a task without completing it", + "value": { + "status": "SKIPPED" + } + }, + "assignUsers": { + "summary": "Assign users to task", + "description": "Assign one or more users to be responsible for this task", + "value": { + "assigned_to": [ + "10010730", + "10010731" + ] + } + }, + "updateDueDate": { + "summary": "Set task due date", + "description": "Set an explicit due date for the task", + "value": { + "due_date": "2024-06-15T17:00:00.000Z" + } + }, + "revertToPreviousTask": { + "summary": "Revert execution to previous task", + "description": "Reset workflow back to a previously completed task, clearing all subsequent progress", + "value": { + "status": "PENDING", + "revert_execution": true + } + }, + "decisionTaskBranch": { + "summary": "Select branch for decision task", + "description": "For manual decision tasks, specify which condition/branch to follow", + "value": { + "status": "COMPLETED", + "next_condition_id": "cond_branch_a" + } + } } } } }, "responses": { "200": { - "description": "Success - when task has been successfully updated", + "description": "Task updated successfully. Returns the updated task object with all current values.\nIf the status change triggers workflow advancement, the flow execution state is also updated.\n", "content": { "application/json": { "schema": { @@ -963,31 +1183,53 @@ } }, "400": { - "description": "Validation Errors", + "description": "Validation error. Common causes include:\n- Invalid status transition (e.g., COMPLETED to IN_PROGRESS without revert)\n- Invalid task_id\n- Invalid assigned_to user IDs\n- Invalid due_date format\n", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResp" + }, + "example": { + "message": "Invalid status transition from COMPLETED to IN_PROGRESS" } } } }, "401": { - "description": "Authentication Errors", + "description": "Authentication failed. The Bearer token is missing, expired, or invalid.\n", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResp" + }, + "example": { + "message": "Unauthorized" + } + } + } + }, + "404": { + "description": "Task or execution not found. The specified execution_id or task_id does not exist.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + }, + "example": { + "message": "Task not found" } } } }, "500": { - "description": "Other errors", + "description": "Internal server error. An unexpected error occurred while processing the request.\n", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResp" + }, + "example": { + "message": "Internal server error" } } } @@ -1317,6 +1559,86 @@ } } }, + "/v2/flows/executions/{execution_id}/tasks/{task_id}/reconcile-automation": { + "post": { + "operationId": "reconcileAutomationTask", + "summary": "reconcileAutomationTask", + "description": "Reconciles an automation task's status against its linked automation execution.\n\nLooks the task up by id (whether or not it is currently a `crt_task`),\nfetches its automation execution, and — if the automation has reached a\nterminal state (success/failed/skipped) — corrects the task's `status`,\n`automation_config` and `analytics` to match and persists them. If the\ntask is the current task and the flow is still active, the flow is\nadvanced; otherwise only the task record is corrected.\n\nUsed by the \"Refresh status\" action to recover a task left `IN_PROGRESS`\nafter a missed or clobbered completion event. No-op (returns the\nexecution unchanged) when the task is already terminal or the automation\nis still running.\n", + "tags": [ + "Flows V2" + ], + "parameters": [ + { + "$ref": "#/components/parameters/ExecutionIdParam" + }, + { + "$ref": "#/components/parameters/TaskIdParam" + } + ], + "responses": { + "200": { + "description": "Success - returns the (possibly updated) flow execution", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FlowExecution" + } + } + } + }, + "400": { + "description": "Validation Errors", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + } + }, + "401": { + "description": "Authentication Errors", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + } + }, + "500": { + "description": "Other errors", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + } + } + } + } + }, "/v2/flows/executions/{execution_id}/tasks/{task_id}/schedule/run-now": { "post": { "operationId": "runTaskScheduleNow", @@ -1468,8 +1790,8 @@ "type": "string" }, "required": true, - "description": "Id of the execution", - "example": "wd561" + "description": "Unique identifier for the flow execution. This ID is generated when the execution\nis created and remains constant throughout its lifecycle.\n", + "example": "exec_abc123def456" }, "TaskIdParam": { "in": "path", @@ -1478,8 +1800,8 @@ "type": "string" }, "required": true, - "description": "Id of the task", - "example": "7hj28a" + "description": "Unique identifier for a task within the execution. Each task has a unique ID\nthat distinguishes it from other tasks in the same execution.\n", + "example": "task_7hj28a" }, "PhaseIdParam": { "in": "path", @@ -1488,8 +1810,8 @@ "type": "string" }, "required": true, - "description": "Id of the phase", - "example": "9gjs2952j" + "description": "Unique identifier for a phase within the execution. Phases group related tasks\ntogether and track collective progress.\n", + "example": "phase_9gjs2952j" }, "ScheduleIdParam": { "in": "path", @@ -1498,7 +1820,7 @@ "type": "string" }, "required": true, - "description": "Id of the schedule", + "description": "Unique identifier for a scheduled task action. Schedules are created when tasks\nare configured with delayed or relative timing.\n", "example": "sch_8k2m9n4p" }, "SoftDeleteParam": { @@ -1509,7 +1831,7 @@ "default": false }, "required": false, - "description": "If true, the execution will NOT be deleted permanently, but rather kept for archive purpose.", + "description": "When true, the execution is marked as deleted but retained in storage for archival\nand audit purposes. When false (default), the execution is permanently removed.\nSoft-deleted executions do not appear in normal queries.\n", "example": true } }, @@ -2606,11 +2928,17 @@ }, "ErrorResp": { "type": "object", + "description": "Standard error response returned when an API request fails.\nContains a human-readable message describing the error.\n", "properties": { "message": { - "type": "string" + "type": "string", + "description": "Human-readable description of the error that occurred", + "example": "Validation failed: workflowId is required" } - } + }, + "required": [ + "message" + ] }, "ClosingReasonResp": { "type": "object", @@ -2944,19 +3272,23 @@ }, "StartFlowReq": { "type": "object", + "description": "Request payload for starting a new flow execution from a template.\n", "required": [ "flow_template_id", "contexts" ], "properties": { "flow_template_id": { - "type": "string" + "type": "string", + "description": "The unique identifier of the flow template (definition) to instantiate.\nThe template must exist and be accessible within the organization.\n", + "example": "tpl_abc123def456" }, "trigger": { "$ref": "#/components/schemas/FlowTrigger" }, "contexts": { "type": "array", + "description": "Entity references that this execution is linked to. At least one context\nis required. The primary context (is_primary: true) is used for condition\nevaluation and data mapping.\n", "minItems": 1, "items": { "$ref": "#/components/schemas/FlowContext" @@ -2967,7 +3299,7 @@ "items": { "type": "string" }, - "description": "An array of purposes to filter workflow phases." + "description": "Taxonomy purpose IDs to filter which phases and tasks are included in the execution.\nOnly phases/tasks tagged with matching purposes will be active. If empty or omitted,\nall phases and tasks from the template are included.\n" } } }, @@ -3101,6 +3433,11 @@ "properties": { "id": { "$ref": "#/components/schemas/TaskId" + }, + "crt_since": { + "type": "string", + "format": "date-time", + "description": "Timestamp when this task entered crt_tasks (i.e. became current). Used by the flow-healing-service as the authoritative gate for \"has this PENDING task been stuck long enough to heal?\". Using the task's own analytics.status_updated_at as the gate produced false positives because transitioning a task INTO crt_tasks does not change its status — so that timestamp can be hours old for a freshly-current task." } } } @@ -3170,6 +3507,10 @@ "singleClosingReasonSelection": { "type": "boolean", "description": "Indicates whether only a single closing reason can be selected when closing the flow execution" + }, + "linear": { + "type": "boolean", + "description": "Copied from the flow template at start. When true, task enablement is computed at runtime from the graph (a task is enabled only when all of its direct predecessor tasks in its branch are done) instead of from each task's explicit requirements." } } }, @@ -3410,6 +3751,14 @@ "schedule_id": { "type": "string", "description": "The id of the created schedule" + }, + "scheduled_at": { + "type": "string", + "description": "The resolved absolute timestamp (ISO 8601, UTC) at which the task is\narmed to run. Set by the backend when the schedule is armed and the\ntask transitions to SCHEDULED. Absent while the task is still\npending/unscheduled.\n\nNote: intentionally typed as plain `string` (not\n`format: date-time`). `schedule` is embedded in the AutomationTask /\nDecisionTask schemas; if a future request body ever accepts a task\n(or schedule) and `safeParse`s it, `format: date-time` would make\nopenapi-zod-client emit `z.string().datetime({ offset: true })` and\nreject any round-tripped value that is empty or tz-less — the exact\nmechanism behind the May 2026 due_date incident (513ed597 added the\nformat, ee574b43 activated it via an unrelated regen; see commit\n2c91ff35). This field is server-written via `toISOString()` so it is\nalways a valid UTC instant; the datetime validator adds no\nprotection, only latent risk.\n" + }, + "error_reason": { + "type": "string", + "description": "Set by the backend when scheduling this task FAILED (e.g. the\nreferenced date attribute is empty/unreadable, or the resolved fire\ntime is already in the past). A JSON string\n({ error_code, error_reason, error_info }) describing why, so the UI\ncan show a specific warning and ask the user to fix the date\nattribute and re-schedule. Cleared when the task is successfully\n(re-)armed. This is the decision-task counterpart of\nAutomationInfo.error_reason (decision tasks have no automation_config).\n" } } }, @@ -3467,6 +3816,14 @@ "schedule_id": { "type": "string", "description": "The id of the created schedule" + }, + "scheduled_at": { + "type": "string", + "description": "The resolved absolute timestamp (ISO 8601, UTC) at which the task is\narmed to run. Set by the backend when the schedule is armed and the\ntask transitions to SCHEDULED. Absent while the task is still\npending/unscheduled.\n\nNote: intentionally typed as plain `string` (not\n`format: date-time`). `schedule` is embedded in the AutomationTask /\nDecisionTask schemas; if a future request body ever accepts a task\n(or schedule) and `safeParse`s it, `format: date-time` would make\nopenapi-zod-client emit `z.string().datetime({ offset: true })` and\nreject any round-tripped value that is empty or tz-less — the exact\nmechanism behind the May 2026 due_date incident (513ed597 added the\nformat, ee574b43 activated it via an unrelated regen; see commit\n2c91ff35). This field is server-written via `toISOString()` so it is\nalways a valid UTC instant; the datetime validator adds no\nprotection, only latent risk.\n" + }, + "error_reason": { + "type": "string", + "description": "Set by the backend when scheduling this task FAILED (e.g. the\nreferenced date attribute is empty/unreadable, or the resolved fire\ntime is already in the past). A JSON string\n({ error_code, error_reason, error_info }) describing why, so the UI\ncan show a specific warning and ask the user to fix the date\nattribute and re-schedule. Cleared when the task is successfully\n(re-)armed. This is the decision-task counterpart of\nAutomationInfo.error_reason (decision tasks have no automation_config).\n" } }, "required": [ @@ -3576,6 +3933,10 @@ }, "error_reason": { "type": "string" + }, + "outcome": { + "type": "string", + "description": "Server-computed outcome read from the agent execution's structured_output (e.g. assigned, recommended, no_eligible_partner, missing_input). When the outcome means the work is not really done (no_eligible_partner, missing_input) the task is held as the current task instead of auto-completing, so the phase does not advance." } } }, @@ -3767,6 +4128,15 @@ }, "input_context": { "$ref": "#/components/schemas/AutomationInputContext" + }, + "heal_attempts": { + "type": "integer", + "description": "Internal — number of times flow-healing-service has attempted to re-trigger this task's lost automation dispatch. Used to cap retries and avoid an indefinite heal-on-every-read storm against a deterministically-failing automation flow." + }, + "last_heal_attempted_at": { + "type": "string", + "format": "date-time", + "description": "Internal — timestamp of the most recent heal attempt for this task. flow-healing-service uses this as a per-task debounce gate so the heal cannot fire more than once per HEAL_RETRY_COOLDOWN_MS regardless of how often the flow execution is read." } }, "required": [ @@ -3978,6 +4348,22 @@ "deleted" ] }, + "attributes": { + "type": "array", + "maxItems": 10, + "items": { + "type": "string" + }, + "description": "Multi-attribute mode. When present and length > 1, the statement is\nevaluated against every listed attribute and combined via\n`attributes_match`. All listed attributes must share the same\n`attribute_type`. Mutually exclusive with `attribute_sub_field`,\n`date_offset`, and `attribute_operation`. When absent or length === 1,\nthe legacy `attribute` field is used.\n" + }, + "attributes_match": { + "type": "string", + "enum": [ + "any", + "all" + ], + "description": "Inner connector across `attributes`. `any` (default) means at least\none attribute must satisfy the operator; `all` means every attribute\nmust satisfy it. Ignored when `attributes` is absent or has length < 2.\n" + }, "attribute_sub_field": { "type": "string", "description": "For complex attribute types, specifies which sub-field to extract (e.g., 'address', 'name', 'email_type')" @@ -4062,7 +4448,8 @@ "hours", "days", "weeks", - "months" + "months", + "years" ] }, "EnableRequirement": { @@ -4139,16 +4526,20 @@ }, "PatchTaskReq": { "type": "object", + "description": "Request payload for updating a task within a flow execution.\nAll fields are optional; only provided fields will be updated.\n", "properties": { "name": { - "type": "string" + "type": "string", + "description": "Display name of the task", + "example": "Review customer application" }, "status": { "$ref": "#/components/schemas/StepStatus" }, "due_date": { "type": "string", - "example": "2021-04-27T12:00:00.000Z" + "description": "Explicit due date for the task. Takes precedence over\ndue_date_config if both are provided.\n\nNote: intentionally typed as plain `string` (not\n`format: date-time`). For day/week/month-precision due\ndates the server stores a \"floating\" datetime without a\ntimezone designator (e.g. `2026-05-28T00:00:00.000`) so\nthat the UI can render it as a date in the user's local\ntimezone without shifting the displayed day. Tightening\nthis to `format: date-time` causes openapi-zod-client to\nemit `z.string().datetime({ offset: true })` in\n`validators-generated.ts`, which then trips\n`safeParse(body)` in `patch-task.ts` whenever the\nsidebar sends a stored task back with a tz-less\n`due_date`. See commit 4aca299c (Aug 2024) for the\noriginal date-only display rationale and the May 2026\nincident (513ed597 added the format, ee574b43\nunintentionally activated it via an unrelated regen) for\nthe history. Long-term, day-precision due dates should\nmigrate to a separate `format: date` field.\n", + "example": "2026-05-28T00:00:00.000" }, "due_date_config": { "$ref": "#/components/schemas/DueDateConfig" @@ -4158,7 +4549,7 @@ }, "enabled": { "type": "boolean", - "description": "flag for controlling enabled/disabled state of the task" + "description": "Controls whether the task is enabled (can be worked on) or disabled (grayed out).\nDisabled tasks cannot have their status changed until re-enabled.\n" }, "automation_config": { "$ref": "#/components/schemas/AutomationInfo" @@ -4178,11 +4569,17 @@ }, "next_condition_id": { "type": "string", - "description": "Condition to evaluate as true for a decision task with a manual trigger mode" + "description": "For decision tasks with manual trigger mode, specifies which condition/branch\nto follow when completing the task. The condition ID must match one of the\nconditions defined on the decision task.\n", + "example": "cond_branch_approved" }, "revert_execution": { "type": "boolean", - "description": "When patching an already completed/skipped task that comes before the current task, this flag controls whether to revert the execution:\n- `true`: The patched task becomes the current task AND all succeeding tasks are reset to PENDING (full revert)\n- `false` or omitted (undefined): The task is updated but the current task does not change and no downstream tasks are affected\nThis parameter is silently ignored when patching the current task or future tasks.\n", + "description": "Controls behavior when updating a task that was already completed/skipped and\ncomes before the current task in the workflow:\n- `true`: Reverts the execution - the patched task becomes the current task\n and all subsequent tasks are reset to PENDING status\n- `false` (default): Updates only this task without affecting workflow position\n or other tasks\n\n**Important:** This parameter is silently ignored when:\n- Patching the current task\n- Patching future tasks (tasks that haven't been reached yet)\n", + "default": false + }, + "completed_via_journey": { + "type": "boolean", + "description": "Request-only signal indicating the task is being completed as a\nresult of the user submitting the task's linked journey (journey\nauto-completion). When `true` and the task transitions to\n`COMPLETED`, the activity log records a journey-specific message\ninstead of the generic completion message. Not persisted on the task.\n", "default": false } } diff --git a/packages/cli/docs/access-token.md b/packages/cli/docs/access-token.md index e0da1807b..4a9071733 100644 --- a/packages/cli/docs/access-token.md +++ b/packages/cli/docs/access-token.md @@ -47,6 +47,8 @@ epilot access-token listAccessTokens - [`getPortalPreviewTokenJwks`](#getportalpreviewtokenjwks) — Get jwks public key set to verify portal preview tokens generated by this API - [`getPublicTokenOIDC`](#getpublictokenoidc) — OpenID Connect configuration for Access Token API a a public identity provider - [`getPortalPreviewTokenOIDC`](#getportalpreviewtokenoidc) — OpenID Connect configuration for Access Token API a a portal preview identity provider +- [`getContactIdentificationTokenJwks`](#getcontactidentificationtokenjwks) — Get jwks public key set to verify contact identification tokens generated by this API +- [`getContactIdentificationTokenOIDC`](#getcontactidentificationtokenoidc) — OpenID Connect configuration for Access Token API as a contact identification identity provider ### `listAccessTokens` @@ -58,7 +60,7 @@ Lists all Access Tokens for current user (by default excludes system generated t | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `token_type` | query | "api" \| "journey" \| "portal" \| "assume" \| "app" \| "portal_preview"[] | No | Filter by token types | +| `token_type` | query | "api" \| "journey" \| "portal" \| "assume" \| "app" \| "portal_preview" \| "contact_identification"[] | No | Filter by token types | **Sample Call** @@ -85,9 +87,13 @@ epilot access-token listAccessTokens --jsonata '$' "journey_id": "string", "portal_id": "string", "portal_user_id": "string", + "contact_id": "5da0a718-c822-403d-9f5d-20d4584e0528", + "allowed_operations": ["getContact", "getContracts"], "assignments": ["123:owner"], "read_only": true, - "last_used": "2026-02-24" + "anonymize": true, + "last_used": "2026-02-24", + "expires_at": "2026-03-01T12:00:00.000Z" } ] ``` @@ -106,9 +112,22 @@ epilot access-token listAccessTokens --jsonata '$' **Sample Call** +```bash +epilot access-token createAccessToken +``` + +With request body: + ```bash epilot access-token createAccessToken \ - -d '{"name":"Postman Access Token","token_type":"api","assignments":["123:owner"],"expires_in":3600,"read_only":true}' + -d '{ + "name": "Postman Access Token", + "token_type": "api", + "assignments": ["123:owner"], + "expires_in": 3600, + "read_only": true, + "anonymize": true +}' ``` Using stdin pipe: @@ -136,9 +155,13 @@ epilot access-token createAccessToken --jsonata '$' "journey_id": "string", "portal_id": "string", "portal_user_id": "string", + "contact_id": "5da0a718-c822-403d-9f5d-20d4584e0528", + "allowed_operations": ["getContact", "getContracts"], "assignments": ["123:owner"], "read_only": true, - "last_used": "2026-02-24" + "anonymize": true, + "last_used": "2026-02-24", + "expires_at": "2026-03-01T12:00:00.000Z" } ``` @@ -189,9 +212,13 @@ epilot access-token revokeAccessToken -p id=api_5ZugdRXasLfWBypHi93Fk --jsonata "journey_id": "string", "portal_id": "string", "portal_user_id": "string", + "contact_id": "5da0a718-c822-403d-9f5d-20d4584e0528", + "allowed_operations": ["getContact", "getContracts"], "assignments": ["123:owner"], "read_only": true, - "last_used": "2026-02-24" + "anonymize": true, + "last_used": "2026-02-24", + "expires_at": "2026-03-01T12:00:00.000Z" } ``` @@ -414,3 +441,75 @@ epilot access-token getPortalPreviewTokenOIDC --jsonata 'issuer' --- + +### `getContactIdentificationTokenJwks` + +Get jwks public key set to verify contact identification tokens generated by this API + +`GET /v1/access-tokens/contact-identification/.well-known/jwks.json` + +**Sample Call** + +```bash +epilot access-token getContactIdentificationTokenJwks +``` + +With JSONata filter: + +```bash +epilot access-token getContactIdentificationTokenJwks --jsonata 'keys' +``` + +
+Sample Response + +```json +{ + "keys": [ + { + "alg": "RS256", + "e": "AQAB", + "kid": "tXWU5mPMbRPczpbQwi6vbhLF4GgF3wlMDSyqo7pfeiw=", + "kty": "RSA", + "n": "h_QDoCjZ8W_trtYXaP7_S22wf5r5Wd9XBLED78oT44bJjQXn8ddcFV8Hik65_4IYXVX_hTTU4zpxe3H8vx2j7-Zz3O59mYMp5S0MzODNEdf5Y_2o19eis0brmAJniixsNlQ9LlYkdrVamrgaxHu3ZpP_99zkfFybYeuYoQNzb3PyrT8xVnz_USs_nlFMHpGUxvvz7gfKPqxcLvgLJr4cwI9yzaSY9CD4qW181QVcnL_WzpQ8xx6AuhhHZQ1l_3GG4InTk8ahE7U2ZHVu8RrX6d01pMgc3piEcet9RgFLnhbTg3YIiKGoAbN42wJn_x3lgIAC42T9mbmTsHyUdS6nUQ", + "use": "sig" + } + ] +} +``` + +
+ +--- + +### `getContactIdentificationTokenOIDC` + +OpenID Connect configuration for Access Token API as a contact identification identity provider + +`GET /v1/access-tokens/contact-identification/.well-known/openid-configuration` + +**Sample Call** + +```bash +epilot access-token getContactIdentificationTokenOIDC +``` + +With JSONata filter: + +```bash +epilot access-token getContactIdentificationTokenOIDC --jsonata 'issuer' +``` + +
+Sample Response + +```json +{ + "issuer": "https://access-token.sls.epilot.io/v1/access-tokens/contact-identification", + "jwks_uri": "https://access-token.sls.epilot.io/v1/access-tokens/contact-identification/.well-known/jwks.json" +} +``` + +
+ +--- diff --git a/packages/cli/docs/ai-agents.md b/packages/cli/docs/ai-agents.md index 784262249..1ef5f1d3b 100644 --- a/packages/cli/docs/ai-agents.md +++ b/packages/cli/docs/ai-agents.md @@ -1,4 +1,4 @@ -# AI Agents API - OpenAPI 3.0 +# AI Agents API - **Base URL:** `https://ai-agents.sls.epilot.io` - **API Docs:** [https://docs.epilot.io/api/ai-agents](https://docs.epilot.io/api/ai-agents) @@ -44,12 +44,25 @@ epilot ai-agents listAgents **Agent Execution** - [`executeAgent`](#executeagent) — Executes an agent (system skill or custom agent). -- [`listExecutions`](#listexecutions) — GET /v1/executions -- [`getExecution`](#getexecution) — GET /v1/executions/{execution_id} -- [`cancelExecution`](#cancelexecution) — DELETE /v1/executions/{execution_id} +- [`executeAgentStream`](#executeagentstream) — Executes an agent with real-time streaming of tokens and tool events. +- [`listExecutions`](#listexecutions) — List executions +- [`getExecution`](#getexecution) — Get execution by ID +- [`cancelExecution`](#cancelexecution) — Cancel execution - [`getExecutionTrace`](#getexecutiontrace) — Returns the step-by-step reasoning and tool calls for ReAct mode executions. Returns empty iterations array for direct m +- [`getExecutionFeedback`](#getexecutionfeedback) — Returns the authenticated user's feedback for an execution. +- [`putExecutionFeedback`](#putexecutionfeedback) — Upserts thumbs up/down feedback and mirrors it to the execution's Langfuse trace. - [`approveExecution`](#approveexecution) — Approves a pending tool action when execution is in waiting_approval status - [`rejectExecution`](#rejectexecution) — Rejects a pending tool action when execution is in waiting_approval status +- [`streamExecution`](#streamexecution) — Reconnects to an execution's event stream after approval. Replays missed events from event log and continues streaming i + +**Chat** +- [`chat`](#chat) — Initiates a streaming chat session with an AI agent. Supports server-side conversation memory via conversationId or clie + +**Conversations** +- [`listConversations`](#listconversations) — Lists conversations for the authenticated user, sorted by most recent. +- [`getConversation`](#getconversation) — Retrieves a conversation and its message history. +- [`deleteConversation`](#deleteconversation) — Deletes a conversation and all its messages. +- [`submitConversationFeedback`](#submitconversationfeedback) — Records a thumbs up/down (with optional comment) for the assistant turn identified by its Langfuse trace id. The rating ### `listAgents` @@ -62,7 +75,7 @@ Lists agents from both system skills and custom agents. | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | | `source` | query | "system" \| "custom" | No | Filter by agent source (system = pre-built skills, custom = user-created) | -| `availability` | query | "flows" \| "copilot" \| "all" | No | Filter by availability context (flows, copilot) | +| `availability` | query | "flows" \| "copilot" \| "portals" \| "all" | No | Filter by availability context (flows, copilot) | | `entity_schema` | query | string | No | Filter by allowed entity schema (e.g., "message" for email-related skills) | **Sample Call** @@ -98,7 +111,9 @@ epilot ai-agents listAgents --jsonata 'agents' "model_config": { "model_id": "anthropic.claude-3-5-sonnet-20241022-v2:0", "temperature": 0.7, - "max_tokens": 4096 + "max_tokens": 4096, + "thinking": false, + "thinking_budget": 10000 }, "max_iterations": 0, "execution_pattern": "direct", @@ -166,7 +181,9 @@ epilot ai-agents createAgent \ "model_config": { "model_id": "anthropic.claude-3-5-sonnet-20241022-v2:0", "temperature": 0.7, - "max_tokens": 4096 + "max_tokens": 4096, + "thinking": false, + "thinking_budget": 10000 }, "max_iterations": 10, "execution_pattern": "direct", @@ -226,7 +243,9 @@ epilot ai-agents createAgent --jsonata 'agent_id' "model_config": { "model_id": "anthropic.claude-3-5-sonnet-20241022-v2:0", "temperature": 0.7, - "max_tokens": 4096 + "max_tokens": 4096, + "thinking": false, + "thinking_budget": 10000 }, "max_iterations": 0, "execution_pattern": "direct", @@ -313,7 +332,9 @@ epilot ai-agents getAgentById -p agent_id=skill:email-categorizer --jsonata 'age "model_config": { "model_id": "anthropic.claude-3-5-sonnet-20241022-v2:0", "temperature": 0.7, - "max_tokens": 4096 + "max_tokens": 4096, + "thinking": false, + "thinking_budget": 10000 }, "max_iterations": 0, "execution_pattern": "direct", @@ -386,7 +407,9 @@ epilot ai-agents updateAgentById \ "model_config": { "model_id": "anthropic.claude-3-5-sonnet-20241022-v2:0", "temperature": 0.7, - "max_tokens": 4096 + "max_tokens": 4096, + "thinking": false, + "thinking_budget": 10000 }, "max_iterations": 1, "execution_pattern": "direct", @@ -452,7 +475,9 @@ epilot ai-agents updateAgentById -p agent_id=skill:email-categorizer --jsonata ' "model_config": { "model_id": "anthropic.claude-3-5-sonnet-20241022-v2:0", "temperature": 0.7, - "max_tokens": 4096 + "max_tokens": 4096, + "thinking": false, + "thinking_budget": 10000 }, "max_iterations": 0, "execution_pattern": "direct", @@ -634,7 +659,15 @@ epilot ai-agents executeAgent -p agent_id=skill:email-categorizer --jsonata 'exe "iteration_count": 0 }, "started_at": "1970-01-01T00:00:00.000Z", - "completed_at": "1970-01-01T00:00:00.000Z" + "completed_at": "1970-01-01T00:00:00.000Z", + "iterations": [ + { + "index": 0, + "tool": "string", + "status": "running", + "timestamp": "1970-01-01T00:00:00.000Z" + } + ] } ``` @@ -642,8 +675,79 @@ epilot ai-agents executeAgent -p agent_id=skill:email-categorizer --jsonata 'exe --- +### `executeAgentStream` + +Executes an agent with real-time streaming of tokens and tool events. + +`POST /v1/agents/{agent_id}/execute/stream` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `agent_id` | path | string | Yes | | + +**Request Body** + +**Sample Call** + +```bash +epilot ai-agents executeAgentStream \ + -p agent_id=skill:email-categorizer +``` + +With request body: + +```bash +epilot ai-agents executeAgentStream \ + -p agent_id=skill:email-categorizer \ + -d '{ + "input": { + "entity_id": "string", + "entity_schema": "string", + "workflow_id": "string", + "workflow_execution_id": "string", + "task_id": "string", + "custom_data": {}, + "flow_context": [ + { + "entity_id": "string", + "entity_schema": "string" + } + ] + }, + "parameters": {}, + "execution_mode_override": "automatic", + "execution_context": "flows", + "callback_url": "https://example.com/path", + "timeout_ms": 30000 +}' +``` + +Using positional args for path parameters: + +```bash +epilot ai-agents executeAgentStream skill:email-categorizer +``` + +Using stdin pipe: + +```bash +cat body.json | epilot ai-agents executeAgentStream -p agent_id=skill:email-categorizer +``` + +With JSONata filter: + +```bash +epilot ai-agents executeAgentStream -p agent_id=skill:email-categorizer --jsonata '$' +``` + +--- + ### `listExecutions` +List executions + `GET /v1/executions` **Parameters** @@ -687,7 +791,8 @@ epilot ai-agents listExecutions --jsonata 'executions' "pending_action": {}, "metrics": {}, "started_at": "1970-01-01T00:00:00.000Z", - "completed_at": "1970-01-01T00:00:00.000Z" + "completed_at": "1970-01-01T00:00:00.000Z", + "iterations": [] } ], "next_cursor": "string" @@ -700,6 +805,8 @@ epilot ai-agents listExecutions --jsonata 'executions' ### `getExecution` +Get execution by ID + `GET /v1/executions/{execution_id}` **Parameters** @@ -772,7 +879,15 @@ epilot ai-agents getExecution -p execution_id=123e4567-e89b-12d3-a456-4266141740 "iteration_count": 0 }, "started_at": "1970-01-01T00:00:00.000Z", - "completed_at": "1970-01-01T00:00:00.000Z" + "completed_at": "1970-01-01T00:00:00.000Z", + "iterations": [ + { + "index": 0, + "tool": "string", + "status": "running", + "timestamp": "1970-01-01T00:00:00.000Z" + } + ] } ``` @@ -782,6 +897,8 @@ epilot ai-agents getExecution -p execution_id=123e4567-e89b-12d3-a456-4266141740 ### `cancelExecution` +Cancel execution + `DELETE /v1/executions/{execution_id}` **Parameters** @@ -854,7 +971,15 @@ epilot ai-agents cancelExecution -p execution_id=123e4567-e89b-12d3-a456-4266141 "iteration_count": 0 }, "started_at": "1970-01-01T00:00:00.000Z", - "completed_at": "1970-01-01T00:00:00.000Z" + "completed_at": "1970-01-01T00:00:00.000Z", + "iterations": [ + { + "index": 0, + "tool": "string", + "status": "running", + "timestamp": "1970-01-01T00:00:00.000Z" + } + ] } ``` @@ -921,6 +1046,113 @@ epilot ai-agents getExecutionTrace -p execution_id=123e4567-e89b-12d3-a456-42661 --- +### `getExecutionFeedback` + +Returns the authenticated user's feedback for an execution. + +`GET /v1/executions/{execution_id}/feedback` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `execution_id` | path | string (uuid) | Yes | | + +**Sample Call** + +```bash +epilot ai-agents getExecutionFeedback \ + -p execution_id=123e4567-e89b-12d3-a456-426614174000 +``` + +Using positional args for path parameters: + +```bash +epilot ai-agents getExecutionFeedback 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot ai-agents getExecutionFeedback -p execution_id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'feedback' +``` + +
+Sample Response + +```json +{ + "feedback": { + "rating": "up", + "comment": "string", + "user_id": "string", + "submitted_at": "1970-01-01T00:00:00.000Z" + } +} +``` + +
+ +--- + +### `putExecutionFeedback` + +Upserts thumbs up/down feedback and mirrors it to the execution's Langfuse trace. + +`PUT /v1/executions/{execution_id}/feedback` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `execution_id` | path | string (uuid) | Yes | | + +**Request Body** (required) + +**Sample Call** + +```bash +epilot ai-agents putExecutionFeedback \ + -p execution_id=123e4567-e89b-12d3-a456-426614174000 \ + -d '{"rating":"up","comment":"string"}' +``` + +Using positional args for path parameters: + +```bash +epilot ai-agents putExecutionFeedback 123e4567-e89b-12d3-a456-426614174000 +``` + +Using stdin pipe: + +```bash +cat body.json | epilot ai-agents putExecutionFeedback -p execution_id=123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot ai-agents putExecutionFeedback -p execution_id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'feedback' +``` + +
+Sample Response + +```json +{ + "feedback": { + "rating": "up", + "comment": "string", + "user_id": "string", + "submitted_at": "1970-01-01T00:00:00.000Z" + } +} +``` + +
+ +--- + ### `approveExecution` Approves a pending tool action when execution is in waiting_approval status @@ -940,7 +1172,7 @@ Approves a pending tool action when execution is in waiting_approval status ```bash epilot ai-agents approveExecution \ -p execution_id=123e4567-e89b-12d3-a456-426614174000 \ - -d '{"reason":"string"}' + -d '{"reason":"string","approved_action_ids":["string"],"rejected_action_ids":["string"]}' ``` Using positional args for path parameters: @@ -1006,7 +1238,15 @@ epilot ai-agents approveExecution -p execution_id=123e4567-e89b-12d3-a456-426614 "iteration_count": 0 }, "started_at": "1970-01-01T00:00:00.000Z", - "completed_at": "1970-01-01T00:00:00.000Z" + "completed_at": "1970-01-01T00:00:00.000Z", + "iterations": [ + { + "index": 0, + "tool": "string", + "status": "running", + "timestamp": "1970-01-01T00:00:00.000Z" + } + ] } ``` @@ -1099,7 +1339,341 @@ epilot ai-agents rejectExecution -p execution_id=123e4567-e89b-12d3-a456-4266141 "iteration_count": 0 }, "started_at": "1970-01-01T00:00:00.000Z", - "completed_at": "1970-01-01T00:00:00.000Z" + "completed_at": "1970-01-01T00:00:00.000Z", + "iterations": [ + { + "index": 0, + "tool": "string", + "status": "running", + "timestamp": "1970-01-01T00:00:00.000Z" + } + ] +} +``` + + + +--- + +### `streamExecution` + +Reconnects to an execution's event stream after approval. Replays missed events from event log and continues streaming i + +`GET /v1/executions/{execution_id}/stream` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `execution_id` | path | string (uuid) | Yes | | +| `from_sequence` | query | number | No | Resume from this event sequence number (for reconnection) | + +**Sample Call** + +```bash +epilot ai-agents streamExecution \ + -p execution_id=123e4567-e89b-12d3-a456-426614174000 +``` + +Using positional args for path parameters: + +```bash +epilot ai-agents streamExecution 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot ai-agents streamExecution -p execution_id=123e4567-e89b-12d3-a456-426614174000 --jsonata '$' +``` + +--- + +### `chat` + +Initiates a streaming chat session with an AI agent. Supports server-side conversation memory via conversationId or clie + +`POST /v1/chat` + +**Request Body** (required) + +**Sample Call** + +```bash +epilot ai-agents chat +``` + +With request body: + +```bash +epilot ai-agents chat \ + -d '{ + "agentId": "string", + "message": "string", + "conversationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "clientHistory": [ + { + "role": "user", + "content": "string", + "tool_calls": [ + { + "id": "string", + "name": "string", + "input": {}, + "output": "string" + } + ] + } + ], + "context": { + "entityId": "string", + "customData": {} + }, + "streaming": { + "mode": "updates", + "streamTokens": false, + "includeMetadata": false + } +}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot ai-agents chat +``` + +With JSONata filter: + +```bash +epilot ai-agents chat --jsonata '$' +``` + +--- + +### `listConversations` + +Lists conversations for the authenticated user, sorted by most recent. + +`GET /v1/conversations` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `agent_id` | query | string | No | Filter by agent ID | +| `limit` | query | number | No | Maximum number of conversations to return | +| `cursor` | query | string | No | Pagination cursor | + +**Sample Call** + +```bash +epilot ai-agents listConversations +``` + +With JSONata filter: + +```bash +epilot ai-agents listConversations --jsonata 'conversations' +``` + +
+Sample Response + +```json +{ + "conversations": [ + { + "conversation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "user_id": "string", + "agent_id": "string", + "title": "string", + "message_count": 0, + "last_message": "string", + "last_message_at": "1970-01-01T00:00:00.000Z", + "context": { + "entityId": "string", + "customData": {} + }, + "created_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z" + } + ], + "next_cursor": "string" +} +``` + +
+ +--- + +### `getConversation` + +Retrieves a conversation and its message history. + +`GET /v1/conversations/{conversation_id}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `conversation_id` | path | string (uuid) | Yes | | +| `message_limit` | query | number | No | Maximum number of messages to return | + +**Sample Call** + +```bash +epilot ai-agents getConversation \ + -p conversation_id=123e4567-e89b-12d3-a456-426614174000 +``` + +Using positional args for path parameters: + +```bash +epilot ai-agents getConversation 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot ai-agents getConversation -p conversation_id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'conversation' +``` + +
+Sample Response + +```json +{ + "conversation": { + "conversation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "user_id": "string", + "agent_id": "string", + "title": "string", + "message_count": 0, + "last_message": "string", + "last_message_at": "1970-01-01T00:00:00.000Z", + "context": { + "entityId": "string", + "customData": {} + }, + "created_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z" + }, + "messages": [ + { + "conversation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "role": "user", + "content": "string", + "timestamp": "1970-01-01T00:00:00.000Z", + "tool_calls": [ + { + "id": "string", + "name": "string", + "input": {}, + "output": "string" + } + ], + "token_count": 0, + "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "feedback": { + "rating": "up", + "comment": "string", + "user_id": "string", + "submitted_at": "1970-01-01T00:00:00.000Z" + } + } + ] +} +``` + +
+ +--- + +### `deleteConversation` + +Deletes a conversation and all its messages. + +`DELETE /v1/conversations/{conversation_id}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `conversation_id` | path | string (uuid) | Yes | | + +**Sample Call** + +```bash +epilot ai-agents deleteConversation \ + -p conversation_id=123e4567-e89b-12d3-a456-426614174000 +``` + +Using positional args for path parameters: + +```bash +epilot ai-agents deleteConversation 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot ai-agents deleteConversation -p conversation_id=123e4567-e89b-12d3-a456-426614174000 --jsonata '$' +``` + +--- + +### `submitConversationFeedback` + +Records a thumbs up/down (with optional comment) for the assistant turn identified by its Langfuse trace id. The rating + +`POST /v1/conversations/{conversation_id}/feedback` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `conversation_id` | path | string (uuid) | Yes | | + +**Request Body** (required) + +**Sample Call** + +```bash +epilot ai-agents submitConversationFeedback \ + -p conversation_id=123e4567-e89b-12d3-a456-426614174000 \ + -d '{"trace_id":"3fa85f64-5717-4562-b3fc-2c963f66afa6","rating":"up","comment":"string"}' +``` + +Using positional args for path parameters: + +```bash +epilot ai-agents submitConversationFeedback 123e4567-e89b-12d3-a456-426614174000 +``` + +Using stdin pipe: + +```bash +cat body.json | epilot ai-agents submitConversationFeedback -p conversation_id=123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot ai-agents submitConversationFeedback -p conversation_id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'feedback' +``` + +
+Sample Response + +```json +{ + "feedback": { + "rating": "up", + "comment": "string", + "user_id": "string", + "submitted_at": "1970-01-01T00:00:00.000Z" + } } ``` diff --git a/packages/cli/docs/app.md b/packages/cli/docs/app.md index 78be4bb6b..56150f367 100644 --- a/packages/cli/docs/app.md +++ b/packages/cli/docs/app.md @@ -43,6 +43,7 @@ epilot app getPublicFacingComponent -p appId=123e4567-e89b-12d3-a456-42661417400 - [`patchInstallation`](#patchinstallation) — Patch an installed app by its ID. - [`uninstall`](#uninstall) — Uninstall an app by its ID. - [`promoteVersion`](#promoteversion) — Update an installed app to a new version +- [`resolveOptions`](#resolveoptions) — Resolve the effective app-level options of an installation, including decrypted sensitive values (secrets). This endpoin **App Configuration** - [`listConfigurations`](#listconfigurations) — List all app configuration metadata owned by an organization. To get full app configuration details, use the /v1/app-con @@ -74,6 +75,9 @@ epilot app getPublicFacingComponent -p appId=123e4567-e89b-12d3-a456-42661417400 **App Proxy** - [`publicProxyGet`](#publicproxyget) — Forward a GET request to a registered proxy target from a public-facing component (e.g. journey blocks) - [`publicProxyPost`](#publicproxypost) — Forward a POST request to a registered proxy target from a public-facing component (e.g. journey blocks) +- [`publicProxyPut`](#publicproxyput) — Forward a PUT request to a registered proxy target from a public-facing component +- [`publicProxyPatch`](#publicproxypatch) — Forward a PATCH request to a registered proxy target from a public-facing component +- [`publicProxyDelete`](#publicproxydelete) — Forward a DELETE request to a registered proxy target from a public-facing component ### `getPublicFacingComponent` @@ -403,6 +407,35 @@ epilot app getPublicConfiguration -p appId=123e4567-e89b-12d3-a456-426614174000 "configuration": {} } ], + "functions": [ + { + "name": "string", + "type": "workflow", + "label": {}, + "description": {}, + "code": "string", + "schedule": "rate(30 minutes)", + "schedule_timezone": "Europe/Berlin", + "schedule_overlap": "skip", + "secrets": ["string"] + } + ], + "options": [ + { + "key": "string", + "label": "string", + "required": true, + "description": "string", + "repeatable": true, + "fields": [], + "value": {}, + "type": "text", + "sensitive": true, + "configured": true, + "lifted": true, + "value_updated_at": "string" + } + ], "is_beta": true, "deprecated_at": "string", "version": "string", @@ -424,6 +457,8 @@ epilot app getPublicConfiguration -p appId=123e4567-e89b-12d3-a456-426614174000 "app_id": "string", "owner_org_id": "string", "components": [], + "functions": [], + "options": [], "visibility": "private", "public": false, "pending": false, @@ -522,6 +557,35 @@ epilot app getConfiguration -p appId=123e4567-e89b-12d3-a456-426614174000 --json "configuration": {} } ], + "functions": [ + { + "name": "string", + "type": "workflow", + "label": {}, + "description": {}, + "code": "string", + "schedule": "rate(30 minutes)", + "schedule_timezone": "Europe/Berlin", + "schedule_overlap": "skip", + "secrets": ["string"] + } + ], + "options": [ + { + "key": "string", + "label": "string", + "required": true, + "description": "string", + "repeatable": true, + "fields": [], + "value": {}, + "type": "text", + "sensitive": true, + "configured": true, + "lifted": true, + "value_updated_at": "string" + } + ], "visibility": "private", "public": false, "pending": false, @@ -693,7 +757,8 @@ epilot app queryEvents \ "source": ["CUSTOM_JOURNEY_BLOCK"], "component_id": ["string"], "event_type": ["ERROR"], - "correlation_id": "string" + "correlation_id": "string", + "search": "string" }, "aggregation": { "group_by": ["source"], @@ -701,7 +766,8 @@ epilot app queryEvents \ }, "pagination": { "page": 1, - "page_size": 100 + "page_size": 100, + "before": "1970-01-01T00:00:00.000Z" }, "sort": { "field": "timestamp", @@ -743,7 +809,8 @@ epilot app queryEvents -p appId=123e4567-e89b-12d3-a456-426614174000 --jsonata ' "source": ["CUSTOM_JOURNEY_BLOCK"], "component_id": ["string"], "event_type": ["ERROR"], - "correlation_id": "string" + "correlation_id": "string", + "search": "string" }, "aggregation": { "group_by": ["source"], @@ -751,7 +818,8 @@ epilot app queryEvents -p appId=123e4567-e89b-12d3-a456-426614174000 --jsonata ' }, "pagination": { "page": 1, - "page_size": 100 + "page_size": 100, + "before": "1970-01-01T00:00:00.000Z" }, "sort": { "field": "timestamp", @@ -864,7 +932,7 @@ Generate a presigned URL to upload a zip file with artifacts that will be unpack | ---- | -- | ---- | -------- | ----------- | | `appId` | path | string | Yes | ID of the app configuration | -**Request Body** +**Request Body** (required) **Sample Call** @@ -1045,25 +1113,9 @@ epilot app listVersions -p appId=123e4567-e89b-12d3-a456-426614174000 --jsonata { "app_id": "string", "owner_org_id": "string", - "components": [ - { - "component_type": "CUSTOM_JOURNEY_BLOCK", - "configuration": { - "override_dev_mode": { - "override_url": "http://localhost:3000" - }, - "component_url": "https://cdn.apps.com/123/v1.0.0/bundle.js", - "component_tag": "string", - "component_args": [ - { - "type": "text" - } - ], - "component_size": 0, - "component_mapping": {} - } - } - ], + "components": [], + "functions": [], + "options": [], "visibility": "private", "public": false, "pending": false, @@ -1072,28 +1124,9 @@ epilot app listVersions -p appId=123e4567-e89b-12d3-a456-426614174000 --jsonata "deprecated_at": "string", "changelog": "string", "review_status": "approved", - "role": { - "id": "string", - "grants": [ - { - "action": "string", - "resource": "string" - } - ] - }, - "blueprint_ref": { - "manifest_id": "string", - "job_id": "string", - "source_blueprint_file": "string" - }, - "version_audit": { - "created_at": "string", - "created_by": "string", - "updated_at": "string", - "updated_by": "string", - "versioned_at": "string", - "versioned_by": "string" - } + "role": {}, + "blueprint_ref": {}, + "version_audit": {} } ], "pagination": { @@ -1187,6 +1220,35 @@ epilot app getVersion -p appId=123e4567-e89b-12d3-a456-426614174000 -p version=e "configuration": {} } ], + "functions": [ + { + "name": "string", + "type": "workflow", + "label": {}, + "description": {}, + "code": "string", + "schedule": "rate(30 minutes)", + "schedule_timezone": "Europe/Berlin", + "schedule_overlap": "skip", + "secrets": ["string"] + } + ], + "options": [ + { + "key": "string", + "label": "string", + "required": true, + "description": "string", + "repeatable": true, + "fields": [], + "value": {}, + "type": "text", + "sensitive": true, + "configured": true, + "lifted": true, + "value_updated_at": "string" + } + ], "visibility": "private", "public": false, "pending": false, @@ -1238,11 +1300,72 @@ Patch an existing app version **Sample Call** +```bash +epilot app patchVersion \ + -p appId=123e4567-e89b-12d3-a456-426614174000 \ + -p version=example +``` + +With request body: + ```bash epilot app patchVersion \ -p appId=123e4567-e89b-12d3-a456-426614174000 \ -p version=example \ - -d '{"manifest_id":"string","role_id":"string","grants":[{"action":"string","resource":"string"}]}' + -d '{ + "manifest_id": "string", + "role_id": "string", + "grants": [ + { + "action": "string", + "resource": "string" + } + ], + "functions": [ + { + "name": "string", + "type": "workflow", + "label": { + "en": "string", + "de": "string" + }, + "description": { + "en": "string", + "de": "string" + }, + "code": "string", + "schedule": "rate(30 minutes)", + "schedule_timezone": "Europe/Berlin", + "schedule_overlap": "skip", + "secrets": ["string"] + } + ], + "options": [ + { + "key": "string", + "label": "string", + "required": true, + "description": "string", + "repeatable": true, + "fields": [ + { + "key": "string", + "label": "string", + "description": "string", + "required": true, + "type": "text" + } + ], + "value": {}, + "type": "text", + "sensitive": true, + "configured": true, + "lifted": true, + "value_updated_at": "string" + } + ], + "changelog": "string" +}' ``` Using positional args for path parameters: @@ -1634,13 +1757,16 @@ Clone an existing app version to create a new version | `sourceVersion` | path | string | Yes | Source version to clone from | | `targetVersion` | path | string | Yes | Target version to create | +**Request Body** + **Sample Call** ```bash epilot app cloneVersion \ -p appId=123e4567-e89b-12d3-a456-426614174000 \ -p sourceVersion=example \ - -p targetVersion=example + -p targetVersion=example \ + -d '{"changelog":"string"}' ``` Using positional args for path parameters: @@ -1649,6 +1775,12 @@ Using positional args for path parameters: epilot app cloneVersion 123e4567-e89b-12d3-a456-426614174000 example example ``` +Using stdin pipe: + +```bash +cat body.json | epilot app cloneVersion -p appId=123e4567-e89b-12d3-a456-426614174000 -p sourceVersion=example -p targetVersion=example +``` + With JSONata filter: ```bash @@ -1680,7 +1812,7 @@ Retrieve a list of installed apps for the organization. | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `componentType` | query | "CUSTOM_JOURNEY_BLOCK" \| "CUSTOM_PORTAL_BLOCK" \| "PORTAL_EXTENSION" \| "CUSTOM_FLOW_ACTION" \| "ERP_INFORM_TOOLKIT" \| "CUSTOM_CAPABILITY" \| "EXTERNAL_PRODUCT_CATALOG" \| "CUSTOM_PAGE" \| "API_PROXY" | No | Filter apps by specific component type | +| `componentType` | query | "CUSTOM_JOURNEY_BLOCK" \| "CUSTOM_PORTAL_BLOCK" \| "PORTAL_EXTENSION" \| "CUSTOM_FLOW_ACTION" \| "ERP_INFORM_TOOLKIT" \| "CUSTOM_CAPABILITY" \| "EXTERNAL_PRODUCT_CATALOG" \| "CUSTOM_PAGE" \| "API_PROXY" \| "EXTERNAL_VALUES" \| "APP_FUNCTION" | No | Filter apps by specific component type | | `enabled` | query | boolean | No | Filter apps by enabled status | | `page` | query | number | No | Page number for pagination | | `pageSize` | query | number | No | Number of items per page | @@ -1709,49 +1841,15 @@ epilot app listInstallations --jsonata 'apps' "owner_org_id": "string", "enabled": true, "name": "string", - "option_values": [ - { - "component_id": "string", - "options": [ - { - "key": "string", - "value": {} - } - ] - } - ], - "components": [ - { - "component_type": "CUSTOM_JOURNEY_BLOCK", - "configuration": { - "override_dev_mode": { - "override_url": "http://localhost:3000" - }, - "component_url": "https://cdn.apps.com/123/v1.0.0/bundle.js", - "component_tag": "string", - "component_args": [ - { - "type": "text" - } - ], - "component_size": 0, - "component_mapping": {} - } - } - ], + "icon_url": "string", + "option_values": [], + "components": [], + "functions": [], + "options": [], "installed_version": "string", "role": "string", - "blueprint_ref": { - "manifest_id": "string", - "job_id": "string", - "source_blueprint_file": "string" - }, - "installation_audit": { - "created_at": "string", - "created_by": "string", - "updated_at": "string", - "updated_by": "string" - }, + "blueprint_ref": {}, + "installation_audit": {}, "_manifest": ["123e4567-e89b-12d3-a456-426614174000"] } ], @@ -1808,34 +1906,46 @@ epilot app getInstallation -p appId=123e4567-e89b-12d3-a456-426614174000 --jsona "owner_org_id": "string", "enabled": true, "name": "string", + "icon_url": "string", "option_values": [ { "component_id": "string", - "options": [ - { - "key": "string", - "value": {} - } - ] + "options": [] } ], "components": [ { "component_type": "CUSTOM_JOURNEY_BLOCK", - "configuration": { - "override_dev_mode": { - "override_url": "http://localhost:3000" - }, - "component_url": "https://cdn.apps.com/123/v1.0.0/bundle.js", - "component_tag": "string", - "component_args": [ - { - "type": "text" - } - ], - "component_size": 0, - "component_mapping": {} - } + "configuration": {} + } + ], + "functions": [ + { + "name": "string", + "type": "workflow", + "label": {}, + "description": {}, + "code": "string", + "schedule": "rate(30 minutes)", + "schedule_timezone": "Europe/Berlin", + "schedule_overlap": "skip", + "secrets": ["string"] + } + ], + "options": [ + { + "key": "string", + "label": "string", + "required": true, + "description": "string", + "repeatable": true, + "fields": [], + "value": {}, + "type": "text", + "sensitive": true, + "configured": true, + "lifted": true, + "value_updated_at": "string" } ], "installed_version": "string", @@ -1871,7 +1981,7 @@ Upsert app installation by its ID. | ---- | -- | ---- | -------- | ----------- | | `appId` | path | string | Yes | | -**Request Body** +**Request Body** (required) **Sample Call** @@ -1930,34 +2040,46 @@ epilot app install -p appId=123e4567-e89b-12d3-a456-426614174000 --jsonata 'app_ "owner_org_id": "string", "enabled": true, "name": "string", + "icon_url": "string", "option_values": [ { "component_id": "string", - "options": [ - { - "key": "string", - "value": {} - } - ] + "options": [] } ], "components": [ { "component_type": "CUSTOM_JOURNEY_BLOCK", - "configuration": { - "override_dev_mode": { - "override_url": "http://localhost:3000" - }, - "component_url": "https://cdn.apps.com/123/v1.0.0/bundle.js", - "component_tag": "string", - "component_args": [ - { - "type": "text" - } - ], - "component_size": 0, - "component_mapping": {} - } + "configuration": {} + } + ], + "functions": [ + { + "name": "string", + "type": "workflow", + "label": {}, + "description": {}, + "code": "string", + "schedule": "rate(30 minutes)", + "schedule_timezone": "Europe/Berlin", + "schedule_overlap": "skip", + "secrets": ["string"] + } + ], + "options": [ + { + "key": "string", + "label": "string", + "required": true, + "description": "string", + "repeatable": true, + "fields": [], + "value": {}, + "type": "text", + "sensitive": true, + "configured": true, + "lifted": true, + "value_updated_at": "string" } ], "installed_version": "string", @@ -1993,7 +2115,7 @@ Patch an installed app by its ID. | ---- | -- | ---- | -------- | ----------- | | `appId` | path | string | Yes | | -**Request Body** +**Request Body** (required) **Sample Call** @@ -2120,34 +2242,46 @@ epilot app promoteVersion -p appId=123e4567-e89b-12d3-a456-426614174000 -p versi "owner_org_id": "string", "enabled": true, "name": "string", + "icon_url": "string", "option_values": [ { "component_id": "string", - "options": [ - { - "key": "string", - "value": {} - } - ] + "options": [] } ], "components": [ { "component_type": "CUSTOM_JOURNEY_BLOCK", - "configuration": { - "override_dev_mode": { - "override_url": "http://localhost:3000" - }, - "component_url": "https://cdn.apps.com/123/v1.0.0/bundle.js", - "component_tag": "string", - "component_args": [ - { - "type": "text" - } - ], - "component_size": 0, - "component_mapping": {} - } + "configuration": {} + } + ], + "functions": [ + { + "name": "string", + "type": "workflow", + "label": {}, + "description": {}, + "code": "string", + "schedule": "rate(30 minutes)", + "schedule_timezone": "Europe/Berlin", + "schedule_overlap": "skip", + "secrets": ["string"] + } + ], + "options": [ + { + "key": "string", + "label": "string", + "required": true, + "description": "string", + "repeatable": true, + "fields": [], + "value": {}, + "type": "text", + "sensitive": true, + "configured": true, + "lifted": true, + "value_updated_at": "string" } ], "installed_version": "string", @@ -2171,6 +2305,82 @@ epilot app promoteVersion -p appId=123e4567-e89b-12d3-a456-426614174000 -p versi --- +### `resolveOptions` + +Resolve the effective app-level options of an installation, including decrypted sensitive values (secrets). This endpoin + +`POST /v1/app/{appId}/options/resolve` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `appId` | path | string | Yes | | + +**Request Body** + +**Sample Call** + +```bash +epilot app resolveOptions \ + -p appId=123e4567-e89b-12d3-a456-426614174000 \ + -d '{"keys":["string"]}' +``` + +Using positional args for path parameters: + +```bash +epilot app resolveOptions 123e4567-e89b-12d3-a456-426614174000 +``` + +Using stdin pipe: + +```bash +cat body.json | epilot app resolveOptions -p appId=123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot app resolveOptions -p appId=123e4567-e89b-12d3-a456-426614174000 --jsonata 'options' +``` + +
+Sample Response + +```json +{ + "options": [ + { + "key": "string", + "label": "string", + "required": true, + "description": "string", + "repeatable": true, + "fields": [ + { + "key": "string", + "label": "string", + "description": "string", + "required": true, + "type": "text" + } + ], + "value": {}, + "type": "text", + "sensitive": true, + "configured": true, + "lifted": true, + "value_updated_at": "string" + } + ] +} +``` + +
+ +--- + ### `ingestEvent` Internal endpoint for services to submit app events for analytic purposes @@ -2234,6 +2444,7 @@ Forward a GET request to a registered proxy target from a public-facing componen | `appId` | path | string | Yes | ID of the installed app | | `proxyName` | path | string | Yes | Name of the proxy target as defined in the app manifest | | `path` | path | string | Yes | Path to forward to the proxy target | +| `query` | query | object | No | Free-form query parameters, forwarded unchanged to the proxy target | **Sample Call** @@ -2271,6 +2482,7 @@ Forward a POST request to a registered proxy target from a public-facing compone | `appId` | path | string | Yes | ID of the installed app | | `proxyName` | path | string | Yes | Name of the proxy target as defined in the app manifest | | `path` | path | string | Yes | Path to forward to the proxy target | +| `query` | query | object | No | Free-form query parameters, forwarded unchanged to the proxy target | **Request Body** @@ -2303,3 +2515,135 @@ epilot app publicProxyPost -p appId=123e4567-e89b-12d3-a456-426614174000 -p prox ``` --- + +### `publicProxyPut` + +Forward a PUT request to a registered proxy target from a public-facing component + +`PUT /v1/public/app/{appId}/proxy/{proxyName}/{path}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `appId` | path | string | Yes | ID of the installed app | +| `proxyName` | path | string | Yes | Name of the proxy target as defined in the app manifest | +| `path` | path | string | Yes | Path to forward to the proxy target | +| `query` | query | object | No | Free-form query parameters, forwarded unchanged to the proxy target | + +**Request Body** + +**Sample Call** + +```bash +epilot app publicProxyPut \ + -p appId=123e4567-e89b-12d3-a456-426614174000 \ + -p proxyName=example \ + -p path=example \ + -d '{}' +``` + +Using positional args for path parameters: + +```bash +epilot app publicProxyPut 123e4567-e89b-12d3-a456-426614174000 example example +``` + +Using stdin pipe: + +```bash +cat body.json | epilot app publicProxyPut -p appId=123e4567-e89b-12d3-a456-426614174000 -p proxyName=example -p path=example +``` + +With JSONata filter: + +```bash +epilot app publicProxyPut -p appId=123e4567-e89b-12d3-a456-426614174000 -p proxyName=example -p path=example --jsonata '$' +``` + +--- + +### `publicProxyPatch` + +Forward a PATCH request to a registered proxy target from a public-facing component + +`PATCH /v1/public/app/{appId}/proxy/{proxyName}/{path}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `appId` | path | string | Yes | ID of the installed app | +| `proxyName` | path | string | Yes | Name of the proxy target as defined in the app manifest | +| `path` | path | string | Yes | Path to forward to the proxy target | +| `query` | query | object | No | Free-form query parameters, forwarded unchanged to the proxy target | + +**Request Body** + +**Sample Call** + +```bash +epilot app publicProxyPatch \ + -p appId=123e4567-e89b-12d3-a456-426614174000 \ + -p proxyName=example \ + -p path=example \ + -d '{}' +``` + +Using positional args for path parameters: + +```bash +epilot app publicProxyPatch 123e4567-e89b-12d3-a456-426614174000 example example +``` + +Using stdin pipe: + +```bash +cat body.json | epilot app publicProxyPatch -p appId=123e4567-e89b-12d3-a456-426614174000 -p proxyName=example -p path=example +``` + +With JSONata filter: + +```bash +epilot app publicProxyPatch -p appId=123e4567-e89b-12d3-a456-426614174000 -p proxyName=example -p path=example --jsonata '$' +``` + +--- + +### `publicProxyDelete` + +Forward a DELETE request to a registered proxy target from a public-facing component + +`DELETE /v1/public/app/{appId}/proxy/{proxyName}/{path}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `appId` | path | string | Yes | ID of the installed app | +| `proxyName` | path | string | Yes | Name of the proxy target as defined in the app manifest | +| `path` | path | string | Yes | Path to forward to the proxy target | +| `query` | query | object | No | Free-form query parameters, forwarded unchanged to the proxy target | + +**Sample Call** + +```bash +epilot app publicProxyDelete \ + -p appId=123e4567-e89b-12d3-a456-426614174000 \ + -p proxyName=example \ + -p path=example +``` + +Using positional args for path parameters: + +```bash +epilot app publicProxyDelete 123e4567-e89b-12d3-a456-426614174000 example example +``` + +With JSONata filter: + +```bash +epilot app publicProxyDelete -p appId=123e4567-e89b-12d3-a456-426614174000 -p proxyName=example -p path=example --jsonata '$' +``` + +--- diff --git a/packages/cli/docs/automation.md b/packages/cli/docs/automation.md index 361f96d5b..5c513316a 100644 --- a/packages/cli/docs/automation.md +++ b/packages/cli/docs/automation.md @@ -46,6 +46,7 @@ epilot automation searchFlows **executions** - [`getExecutions`](#getexecutions) — List automation executions - [`startExecution`](#startexecution) — Start new automation execution +- [`searchExecutions`](#searchexecutions) — Search automation executions of an entity with cursor-based pagination. - [`getExecution`](#getexecution) — Get automation execution - [`cancelExecution`](#cancelexecution) — Cancel automation execution - [`retriggerAction`](#retriggeraction) — Retry a specific automation execution action which failed / is stuck. @@ -72,6 +73,7 @@ Search available automation flows | `from` | query | number | No | Pagination: starting for results | | `trigger_source_id` | query | string | No | Trigger source identifier | | `target_workflow` | query | string | No | Filter by target workflow ID. Returns only automations that have a trigger-workflow action targeting this workflow. | +| `trigger_event_name` | query | string | No | Filter by Event Catalog event name. Returns only automations that have an event_catalog trigger for this event. | | `include_flows` | query | boolean | No | Include flow automations in the response | **Sample Call** @@ -306,8 +308,10 @@ epilot automation getExecutions --jsonata 'results[0]' "version": 2, "trigger_event": {}, "workflow_context": {}, + "workflow_wait_context": {}, "loops": [], - "loop_state": {} + "loop_state": {}, + "chain": ["string"] } ] } @@ -347,13 +351,15 @@ epilot automation startExecution \ "parent_task_id": "string", "depth": 0 }, + "_automation_chain": ["string"], "entity_contexts": [ { "entity_id": "string", "entity_schema": "string", "is_primary": true } - ] + ], + "trigger_user_id": "10006129" }, "flow_execution_id": "string", "flow_automation_task_id": "string" @@ -400,8 +406,10 @@ epilot automation startExecution --jsonata 'id' { "id": "string", "schedule_id": "string", + "evaluation_order": "AFTER_SCHEDULE", "evaluationResult": true, "error_output": {}, + "allow_failure": true, "statements": [] } ], @@ -481,19 +489,96 @@ epilot automation startExecution --jsonata 'id' "parent_task_id": "string", "depth": 0 }, + "_automation_chain": ["string"], "entity_contexts": [ {} - ] + ], + "trigger_user_id": "10006129" + }, + "workflow_wait_context": { + "workflow_execution_id": "string", + "workflow_task_id": "string", + "source": "journey_submission" }, "loops": [ { "id": "loop_contracts", "source_path": "submission.steps[0]['Contracts']", "source_type": "journey-multi-select", + "filter_tags": ["_hidden_ 2a4b1c3d-0000-4000-8000-000000000000 - Dokumente"], "length": 0 } ], - "loop_state": {} + "loop_state": {}, + "chain": ["string"] +} +``` + +
+ +--- + +### `searchExecutions` + +Search automation executions of an entity with cursor-based pagination. + +`POST /v1/automation/executions:search` + +**Request Body** (required) + +**Sample Call** + +```bash +epilot automation searchExecutions \ + -d '{"entity_id":"e3d3ebac-baab-4395-abf4-50b5bf1f8b74","include_flows":false,"size":25,"cursor":"string"}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot automation searchExecutions +``` + +With JSONata filter: + +```bash +epilot automation searchExecutions --jsonata 'results[0]' +``` + +
+Sample Response + +```json +{ + "total": 0, + "results": [ + { + "id": "9baf184f-bc81-4128-bca3-d974c90a12c4", + "execution_status": "pending", + "entity_id": "e3d3ebac-baab-4395-abf4-50b5bf1f8b74", + "activity_id": "e3d3ebac-baab-4395-abf4-50b5bf1f8b74", + "entity_snapshot": {}, + "org_id": "e3d3ebac-baab-4395-abf4-50b5bf1f8b74", + "flow_id": "7791b04a-16d2-44a2-9af9-2d59c25c512f", + "flow_name": "Handle contact form", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z", + "current_action_id": "9ec3711b-db63-449c-b894-54d5bb622a8f", + "conditions": [], + "schedules": [], + "actions": [], + "resume_token": "eyJraWQiOiJrZXkifQ==", + "trigger_context": {}, + "version": 2, + "trigger_event": {}, + "workflow_context": {}, + "workflow_wait_context": {}, + "loops": [], + "loop_state": {}, + "chain": ["string"] + } + ], + "next_cursor": "string" } ``` @@ -781,8 +866,10 @@ epilot automation getExecution -p execution_id=9baf184f-bc81-4128-bca3-d974c90a1 { "id": "string", "schedule_id": "string", + "evaluation_order": "AFTER_SCHEDULE", "evaluationResult": true, "error_output": {}, + "allow_failure": true, "statements": [] } ], @@ -862,19 +949,28 @@ epilot automation getExecution -p execution_id=9baf184f-bc81-4128-bca3-d974c90a1 "parent_task_id": "string", "depth": 0 }, + "_automation_chain": ["string"], "entity_contexts": [ {} - ] + ], + "trigger_user_id": "10006129" + }, + "workflow_wait_context": { + "workflow_execution_id": "string", + "workflow_task_id": "string", + "source": "journey_submission" }, "loops": [ { "id": "loop_contracts", "source_path": "submission.steps[0]['Contracts']", "source_type": "journey-multi-select", + "filter_tags": ["_hidden_ 2a4b1c3d-0000-4000-8000-000000000000 - Dokumente"], "length": 0 } ], - "loop_state": {} + "loop_state": {}, + "chain": ["string"] } ``` @@ -941,8 +1037,10 @@ epilot automation cancelExecution -p execution_id=9baf184f-bc81-4128-bca3-d974c9 { "id": "string", "schedule_id": "string", + "evaluation_order": "AFTER_SCHEDULE", "evaluationResult": true, "error_output": {}, + "allow_failure": true, "statements": [] } ], @@ -1022,19 +1120,28 @@ epilot automation cancelExecution -p execution_id=9baf184f-bc81-4128-bca3-d974c9 "parent_task_id": "string", "depth": 0 }, + "_automation_chain": ["string"], "entity_contexts": [ {} - ] + ], + "trigger_user_id": "10006129" + }, + "workflow_wait_context": { + "workflow_execution_id": "string", + "workflow_task_id": "string", + "source": "journey_submission" }, "loops": [ { "id": "loop_contracts", "source_path": "submission.steps[0]['Contracts']", "source_type": "journey-multi-select", + "filter_tags": ["_hidden_ 2a4b1c3d-0000-4000-8000-000000000000 - Dokumente"], "length": 0 } ], - "loop_state": {} + "loop_state": {}, + "chain": ["string"] } ``` @@ -1164,12 +1271,20 @@ epilot automation resumeExecutionWithToken --jsonata 'execution' "workflow_exec_task_id": "string", "workflow_role": "trigger_workflow", "_execution_chain": {}, - "entity_contexts": [] + "_automation_chain": ["string"], + "entity_contexts": [], + "trigger_user_id": "10006129" + }, + "workflow_wait_context": { + "workflow_execution_id": "string", + "workflow_task_id": "string", + "source": "journey_submission" }, "loops": [ {} ], - "loop_state": {} + "loop_state": {}, + "chain": ["string"] }, "resumedAction": { "id": "9ec3711b-db63-449c-b894-54d5bb622a8f", @@ -1179,11 +1294,13 @@ epilot automation resumeExecutionWithToken --jsonata 'execution' "config": { "mapping_config": {}, "target_schema": "string", + "use_uniqueness_criteria": true, "target_unique": ["string"], "mapping_attributes": [], "relation_attributes": [], "linkback_relation_attribute": "mapped_entities", - "linkback_relation_tags": ["string"] + "linkback_relation_tags": ["string"], + "graph_context": [] }, "allow_failure": true, "created_automatically": true, diff --git a/packages/cli/docs/billing.md b/packages/cli/docs/billing.md index 0e1b8edef..881254fc1 100644 --- a/packages/cli/docs/billing.md +++ b/packages/cli/docs/billing.md @@ -3,7 +3,7 @@ - **Base URL:** `https://billing.sls.epilot.io` - **API Docs:** [https://docs.epilot.io/api/billing](https://docs.epilot.io/api/billing) -API to manage billing data for epilot contracts and orders +API to manage billing data for epilot contracts and orders. ## Quick Start @@ -36,24 +36,32 @@ epilot billing getBillingEvents ## Operations **Billing Events** -- [`getBillingEvents`](#getbillingevents) — Get and filter billing events such as installments and reimbursements. -- [`createBillingEvent`](#createbillingevent) — Create a new billing event. -- [`getBillingEvent`](#getbillingevent) — Get a single billing event by ID. -- [`updateBillingEvent`](#updatebillingevent) — Update an existing billing event. -- [`deleteBillingEvent`](#deletebillingevent) — Delete an existing billing event. -- [`getBillingEventByExternalId`](#getbillingeventbyexternalid) — Get a single billing event by External ID. +- [`getBillingEvents`](#getbillingevents) — Retrieve and filter billing events (Buchungssätze) such as installments (Abschlagszahlungen), +- [`createBillingEvent`](#createbillingevent) — Create a new billing event (Buchungssatz) such as an installment (Abschlagszahlung), +- [`getBillingEvent`](#getbillingevent) — Retrieve a single billing event (Buchungssatz) by its unique ID. +- [`updateBillingEvent`](#updatebillingevent) — Update an existing billing event (Buchungssatz). +- [`deleteBillingEvent`](#deletebillingevent) — Delete an existing billing event (Buchungssatz). +- [`getBillingEventByExternalId`](#getbillingeventbyexternalid) — Retrieve a billing event (Buchungssatz) by its external system identifier. **Contracts** -- [`createContractEntity`](#createcontractentity) — Create a new contract entity. -- [`updateContractEntity`](#updatecontractentity) — Update an existing contract entity. -- [`deleteContractEntity`](#deletecontractentity) — Delete an existing contract entity. +- [`createContractEntity`](#createcontractentity) — Create a new contract entity (Vertrag) for billing purposes. +- [`updateContractEntity`](#updatecontractentity) — Update an existing contract entity (Vertrag). +- [`deleteContractEntity`](#deletecontractentity) — Delete an existing contract entity (Vertrag). + +**Pricing Information** +- [`getContractPricingInformation`](#getcontractpricinginformation) — Get current pricing information and recent configuration history for a Contract. +- [`getBillingAccountPricingInformation`](#getbillingaccountpricinginformation) — Get current pricing information for the active Contracts linked to a Billing Account. + +**Configuration History** +- [`getContractConfigurationHistory`](#getcontractconfigurationhistory) — Get billing configuration history for a Contract. +- [`getBillingAccountConfigurationHistory`](#getbillingaccountconfigurationhistory) — Get merged billing configuration history for active Contracts linked to a Billing Account. **Balance** -- [`getCustomerBalance`](#getcustomerbalance) — Get total balance across all contracts and orders of a customer entity. +- [`getCustomerBalance`](#getcustomerbalance) — Retrieve the total balance (Kontostand) across all contracts and orders for a customer. ### `getBillingEvents` -Get and filter billing events such as installments and reimbursements. +Retrieve and filter billing events (Buchungssätze) such as installments (Abschlagszahlungen), `GET /v1/billing/events` @@ -61,12 +69,16 @@ Get and filter billing events such as installments and reimbursements. | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `from` | query | number | No | | -| `size` | query | number | No | | -| `entity_id` | query | string[] | No | Entity ID to filter billing events by | -| `event_type` | query | "installment" \| "reimbursement" | No | | -| `date_after` | query | string (date-time) | No | | -| `date_before` | query | string (date-time) | No | | +| `from` | query | number | No | Pagination offset - number of results to skip | +| `size` | query | number | No | Maximum number of results to return per page | +| `entity_id` | query | string[] | No | Filter billing events by one or more entity IDs (e.g., contract or order IDs) | +| `contact_id` | query | string | No | Filter billing events by customer contact ID (Kundennummer) | +| `event_type` | query | "installment" \| "reimbursement" | No | Filter by billing event type (Buchungsart): +- `installment`: Abschlagszahlung (scheduled payment due) +- `reimbursement`: Rückerstattung (refund to customer) + | +| `date_after` | query | string (date-time) | No | Filter billing events with booking date (Buchungsdatum) after this timestamp | +| `date_before` | query | string (date-time) | No | Filter billing events with booking date (Buchungsdatum) before this timestamp | **Sample Call** @@ -85,30 +97,18 @@ epilot billing getBillingEvents --jsonata 'results[0]' ```json { - "hits": 0, + "hits": 42, "results": [ { - "billing_amount": 10050, - "billing_amount_decimal": "100.50", - "billing_currency": "EUR", - "external_id": "d4fb2a4e-3f74-4fc4-8fba-6fdaaaa3b08e", - "contract": { - "$relation": [ - { - "entity_id": "f589786b-3024-43cd-9cb3-5a3c953f2896" - } - ] - }, - "_id": "5da0a718-c822-403d-9f5d-20d4584e0528", - "_title": "string", - "_org": "string", - "_schema": "contact", - "_tags": ["string"], - "_created_at": "string", - "_updated_at": "string", "type": "installment", - "due_date": "1970-01-01T00:00:00.000Z", - "paid_date": "1970-01-01T00:00:00.000Z" + "direction": "debit", + "note": "July power & gas installment payment", + "status": "open", + "booking_date": "2025-07-10", + "due_date": "2025-07-10", + "billing_amount": 5000, + "billing_amount_decimal": "50.00", + "billing_currency": "EUR" } ] } @@ -120,7 +120,7 @@ epilot billing getBillingEvents --jsonata 'results[0]' ### `createBillingEvent` -Create a new billing event. +Create a new billing event (Buchungssatz) such as an installment (Abschlagszahlung), `POST /v1/billing/events` @@ -137,27 +137,15 @@ With request body: ```bash epilot billing createBillingEvent \ -d '{ - "billing_amount": 10050, - "billing_amount_decimal": "100.50", - "billing_currency": "EUR", - "external_id": "d4fb2a4e-3f74-4fc4-8fba-6fdaaaa3b08e", - "contract": { - "$relation": [ - { - "entity_id": "f589786b-3024-43cd-9cb3-5a3c953f2896" - } - ] - }, - "_id": "5da0a718-c822-403d-9f5d-20d4584e0528", - "_title": "string", - "_org": "string", - "_schema": "contact", - "_tags": ["string"], - "_created_at": "string", - "_updated_at": "string", "type": "installment", - "due_date": "1970-01-01T00:00:00.000Z", - "paid_date": "1970-01-01T00:00:00.000Z" + "direction": "debit", + "note": "July power & gas installment payment", + "status": "open", + "booking_date": "2025-07-10", + "due_date": "2025-07-10", + "billing_amount": 5000, + "billing_amount_decimal": "50.00", + "billing_currency": "EUR" }' ``` @@ -178,27 +166,15 @@ epilot billing createBillingEvent --jsonata '$' ```json { - "billing_amount": 10050, - "billing_amount_decimal": "100.50", - "billing_currency": "EUR", - "external_id": "d4fb2a4e-3f74-4fc4-8fba-6fdaaaa3b08e", - "contract": { - "$relation": [ - { - "entity_id": "f589786b-3024-43cd-9cb3-5a3c953f2896" - } - ] - }, - "_id": "5da0a718-c822-403d-9f5d-20d4584e0528", - "_title": "string", - "_org": "string", - "_schema": "contact", - "_tags": ["string"], - "_created_at": "string", - "_updated_at": "string", "type": "installment", - "due_date": "1970-01-01T00:00:00.000Z", - "paid_date": "1970-01-01T00:00:00.000Z" + "direction": "debit", + "note": "July power & gas installment payment", + "status": "open", + "booking_date": "2025-07-10", + "due_date": "2025-07-10", + "billing_amount": 5000, + "billing_amount_decimal": "50.00", + "billing_currency": "EUR" } ``` @@ -208,7 +184,7 @@ epilot billing createBillingEvent --jsonata '$' ### `getBillingEvent` -Get a single billing event by ID. +Retrieve a single billing event (Buchungssatz) by its unique ID. `GET /v1/billing/events/{id}` @@ -216,25 +192,25 @@ Get a single billing event by ID. | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `id` | path | string | Yes | ID of the billing event to get | +| `id` | path | string | Yes | Unique identifier of the billing event (Buchungssatz-ID) | **Sample Call** ```bash epilot billing getBillingEvent \ - -p id=123e4567-e89b-12d3-a456-426614174000 + -p id=5da0a718-c822-403d-9f5d-20d4584e0528 ``` Using positional args for path parameters: ```bash -epilot billing getBillingEvent 123e4567-e89b-12d3-a456-426614174000 +epilot billing getBillingEvent 5da0a718-c822-403d-9f5d-20d4584e0528 ``` With JSONata filter: ```bash -epilot billing getBillingEvent -p id=123e4567-e89b-12d3-a456-426614174000 --jsonata '$' +epilot billing getBillingEvent -p id=5da0a718-c822-403d-9f5d-20d4584e0528 --jsonata '$' ```
@@ -242,27 +218,15 @@ epilot billing getBillingEvent -p id=123e4567-e89b-12d3-a456-426614174000 --json ```json { - "billing_amount": 10050, - "billing_amount_decimal": "100.50", - "billing_currency": "EUR", - "external_id": "d4fb2a4e-3f74-4fc4-8fba-6fdaaaa3b08e", - "contract": { - "$relation": [ - { - "entity_id": "f589786b-3024-43cd-9cb3-5a3c953f2896" - } - ] - }, - "_id": "5da0a718-c822-403d-9f5d-20d4584e0528", - "_title": "string", - "_org": "string", - "_schema": "contact", - "_tags": ["string"], - "_created_at": "string", - "_updated_at": "string", "type": "installment", - "due_date": "1970-01-01T00:00:00.000Z", - "paid_date": "1970-01-01T00:00:00.000Z" + "direction": "debit", + "note": "July power & gas installment payment", + "status": "open", + "booking_date": "2025-07-10", + "due_date": "2025-07-10", + "billing_amount": 5000, + "billing_amount_decimal": "50.00", + "billing_currency": "EUR" } ``` @@ -272,7 +236,7 @@ epilot billing getBillingEvent -p id=123e4567-e89b-12d3-a456-426614174000 --json ### `updateBillingEvent` -Update an existing billing event. +Update an existing billing event (Buchungssatz). `PATCH /v1/billing/events/{id}` @@ -280,7 +244,7 @@ Update an existing billing event. | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `id` | path | string | Yes | ID of the billing event to update | +| `id` | path | string | Yes | Unique identifier of the billing event to update | **Request Body** (required) @@ -288,19 +252,21 @@ Update an existing billing event. ```bash epilot billing updateBillingEvent \ - -p id=123e4567-e89b-12d3-a456-426614174000 + -p id=5da0a718-c822-403d-9f5d-20d4584e0528 ``` With request body: ```bash epilot billing updateBillingEvent \ - -p id=123e4567-e89b-12d3-a456-426614174000 \ + -p id=5da0a718-c822-403d-9f5d-20d4584e0528 \ -d '{ - "billing_amount": 10050, - "billing_amount_decimal": "100.50", + "type": "installment", + "direction": "debit", + "billing_amount": 10000, + "billing_amount_decimal": "100.00", "billing_currency": "EUR", - "external_id": "d4fb2a4e-3f74-4fc4-8fba-6fdaaaa3b08e", + "external_id": "SAP-54321", "contract": { "$relation": [ { @@ -308,35 +274,43 @@ epilot billing updateBillingEvent \ } ] }, - "_id": "5da0a718-c822-403d-9f5d-20d4584e0528", - "_title": "string", - "_org": "string", - "_schema": "contact", - "_tags": ["string"], - "_created_at": "string", - "_updated_at": "string", - "type": "installment", - "due_date": "1970-01-01T00:00:00.000Z", - "paid_date": "1970-01-01T00:00:00.000Z" + "booking_date": "2025-06-15", + "due_date": "2025-06-30", + "paid_date": "2025-06-15T10:00:00Z", + "status": "closed", + "related_event": "d4fb2a4e-3f74-4fc4-8fba-6fdaaaa3b08e", + "external_link": { + "href": "https://billing.example.com/invoices/12345", + "title": "Invoice 12345" + }, + "attachments": { + "$relation": [ + { + "entity_id": "f589786b-3024-43cd-9cb3-5a3c953f2896" + } + ] + }, + "note": "Teilzahlung für Abschlag Juni", + "internal_note": "Rückmeldung von SAP: Betrag aus Zahlungsavis 2025-06-14 übernommen" }' ``` Using positional args for path parameters: ```bash -epilot billing updateBillingEvent 123e4567-e89b-12d3-a456-426614174000 +epilot billing updateBillingEvent 5da0a718-c822-403d-9f5d-20d4584e0528 ``` Using stdin pipe: ```bash -cat body.json | epilot billing updateBillingEvent -p id=123e4567-e89b-12d3-a456-426614174000 +cat body.json | epilot billing updateBillingEvent -p id=5da0a718-c822-403d-9f5d-20d4584e0528 ``` With JSONata filter: ```bash -epilot billing updateBillingEvent -p id=123e4567-e89b-12d3-a456-426614174000 --jsonata '$' +epilot billing updateBillingEvent -p id=5da0a718-c822-403d-9f5d-20d4584e0528 --jsonata '$' ```
@@ -344,27 +318,15 @@ epilot billing updateBillingEvent -p id=123e4567-e89b-12d3-a456-426614174000 --j ```json { - "billing_amount": 10050, - "billing_amount_decimal": "100.50", - "billing_currency": "EUR", - "external_id": "d4fb2a4e-3f74-4fc4-8fba-6fdaaaa3b08e", - "contract": { - "$relation": [ - { - "entity_id": "f589786b-3024-43cd-9cb3-5a3c953f2896" - } - ] - }, - "_id": "5da0a718-c822-403d-9f5d-20d4584e0528", - "_title": "string", - "_org": "string", - "_schema": "contact", - "_tags": ["string"], - "_created_at": "string", - "_updated_at": "string", "type": "installment", - "due_date": "1970-01-01T00:00:00.000Z", - "paid_date": "1970-01-01T00:00:00.000Z" + "direction": "debit", + "note": "July power & gas installment payment", + "status": "open", + "booking_date": "2025-07-10", + "due_date": "2025-07-10", + "billing_amount": 5000, + "billing_amount_decimal": "50.00", + "billing_currency": "EUR" } ``` @@ -374,7 +336,7 @@ epilot billing updateBillingEvent -p id=123e4567-e89b-12d3-a456-426614174000 --j ### `deleteBillingEvent` -Delete an existing billing event. +Delete an existing billing event (Buchungssatz). `DELETE /v1/billing/events/{id}` @@ -382,32 +344,32 @@ Delete an existing billing event. | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `id` | path | string | Yes | ID of the billing event to delete | +| `id` | path | string | Yes | Unique identifier of the billing event to delete | **Sample Call** ```bash epilot billing deleteBillingEvent \ - -p id=123e4567-e89b-12d3-a456-426614174000 + -p id=5da0a718-c822-403d-9f5d-20d4584e0528 ``` Using positional args for path parameters: ```bash -epilot billing deleteBillingEvent 123e4567-e89b-12d3-a456-426614174000 +epilot billing deleteBillingEvent 5da0a718-c822-403d-9f5d-20d4584e0528 ``` With JSONata filter: ```bash -epilot billing deleteBillingEvent -p id=123e4567-e89b-12d3-a456-426614174000 --jsonata '$' +epilot billing deleteBillingEvent -p id=5da0a718-c822-403d-9f5d-20d4584e0528 --jsonata '$' ``` --- ### `getBillingEventByExternalId` -Get a single billing event by External ID. +Retrieve a billing event (Buchungssatz) by its external system identifier. `GET /v1/billing/external/{external_id}` @@ -415,25 +377,27 @@ Get a single billing event by External ID. | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `external_id` | path | string | Yes | ID of the billing event to get | +| `external_id` | path | string | Yes | External system identifier for the billing event. +For example, a SAP document number or payment processor reference ID. + | **Sample Call** ```bash epilot billing getBillingEventByExternalId \ - -p external_id=123e4567-e89b-12d3-a456-426614174000 + -p external_id=SAP-54321 ``` Using positional args for path parameters: ```bash -epilot billing getBillingEventByExternalId 123e4567-e89b-12d3-a456-426614174000 +epilot billing getBillingEventByExternalId SAP-54321 ``` With JSONata filter: ```bash -epilot billing getBillingEventByExternalId -p external_id=123e4567-e89b-12d3-a456-426614174000 --jsonata '$' +epilot billing getBillingEventByExternalId -p external_id=SAP-54321 --jsonata '$' ```
@@ -441,27 +405,15 @@ epilot billing getBillingEventByExternalId -p external_id=123e4567-e89b-12d3-a45 ```json { - "billing_amount": 10050, - "billing_amount_decimal": "100.50", - "billing_currency": "EUR", - "external_id": "d4fb2a4e-3f74-4fc4-8fba-6fdaaaa3b08e", - "contract": { - "$relation": [ - { - "entity_id": "f589786b-3024-43cd-9cb3-5a3c953f2896" - } - ] - }, - "_id": "5da0a718-c822-403d-9f5d-20d4584e0528", - "_title": "string", - "_org": "string", - "_schema": "contact", - "_tags": ["string"], - "_created_at": "string", - "_updated_at": "string", "type": "installment", - "due_date": "1970-01-01T00:00:00.000Z", - "paid_date": "1970-01-01T00:00:00.000Z" + "direction": "debit", + "note": "July power & gas installment payment", + "status": "open", + "booking_date": "2025-07-10", + "due_date": "2025-07-10", + "billing_amount": 5000, + "billing_amount_decimal": "50.00", + "billing_currency": "EUR" } ``` @@ -471,7 +423,7 @@ epilot billing getBillingEventByExternalId -p external_id=123e4567-e89b-12d3-a45 ### `createContractEntity` -Create a new contract entity. +Create a new contract entity (Vertrag) for billing purposes. `POST /v1/billing/contracts` @@ -489,32 +441,32 @@ With request body: epilot billing createContractEntity \ -d '{ "_id": "5da0a718-c822-403d-9f5d-20d4584e0528", - "_title": "string", - "_org": "string", - "_schema": "contact", - "_tags": ["string"], - "_created_at": "string", - "_updated_at": "string", - "contract_name": "Grid Contract", - "contract_number": "12345", - "status": "approved", - "description": "This contract is for the supply of widgets.", - "account_number": "67890", + "_title": "Abschlagszahlung Juli 2025", + "_org": "123456", + "_schema": "billing_event", + "_tags": ["billing", "energy"], + "_created_at": "2025-06-15T10:30:00Z", + "_updated_at": "2025-06-15T14:45:00Z", + "contract_name": "Stromvertrag Haushalt", + "contract_number": "STR-2025-001234", + "status": "active", + "description": "Haushaltsstrom-Tarif mit 24 Monaten Preisgarantie", + "account_number": "KD-67890", "branch": "power", - "billing_address": "123 Main St, Anytown", - "delivery_address": "456 Elm St, Anytown", - "additional_addresses": "789 Oak St, Anytown", - "termination_date": "2022-01-01", - "termination_reason": "Non-payment", + "billing_address": "Musterstraße 123, 50667 Köln", + "delivery_address": "Musterstraße 123, 50667 Köln", + "additional_addresses": "Postfach 456, 50668 Köln", + "termination_date": "2025-12-31", + "termination_reason": "Kundenkündigung", "billing_period": "monthly", "billing_duration_amount": 30, - "renewal_duration_amount": 365, - "renewal_duration_unit": "years", + "renewal_duration_amount": 12, + "renewal_duration_unit": "months", "notice_time_amount": 30, "notice_time_unit": "months", - "start_date": "2021-01-01", - "billing_due_day": 2, - "installment_amount": 10050, + "start_date": "2025-01-01", + "billing_due_day": 15, + "installment_amount": 8500, "balance": 8990, "balance_currency": "EUR" }' @@ -538,32 +490,32 @@ epilot billing createContractEntity --jsonata '$' ```json { "_id": "5da0a718-c822-403d-9f5d-20d4584e0528", - "_title": "string", - "_org": "string", - "_schema": "contact", - "_tags": ["string"], - "_created_at": "string", - "_updated_at": "string", - "contract_name": "Grid Contract", - "contract_number": "12345", - "status": "approved", - "description": "This contract is for the supply of widgets.", - "account_number": "67890", + "_title": "Abschlagszahlung Juli 2025", + "_org": "123456", + "_schema": "billing_event", + "_tags": ["billing", "energy"], + "_created_at": "2025-06-15T10:30:00Z", + "_updated_at": "2025-06-15T14:45:00Z", + "contract_name": "Stromvertrag Haushalt", + "contract_number": "STR-2025-001234", + "status": "active", + "description": "Haushaltsstrom-Tarif mit 24 Monaten Preisgarantie", + "account_number": "KD-67890", "branch": "power", - "billing_address": "123 Main St, Anytown", - "delivery_address": "456 Elm St, Anytown", - "additional_addresses": "789 Oak St, Anytown", - "termination_date": "2022-01-01", - "termination_reason": "Non-payment", + "billing_address": "Musterstraße 123, 50667 Köln", + "delivery_address": "Musterstraße 123, 50667 Köln", + "additional_addresses": "Postfach 456, 50668 Köln", + "termination_date": "2025-12-31", + "termination_reason": "Kundenkündigung", "billing_period": "monthly", "billing_duration_amount": 30, - "renewal_duration_amount": 365, - "renewal_duration_unit": "years", + "renewal_duration_amount": 12, + "renewal_duration_unit": "months", "notice_time_amount": 30, "notice_time_unit": "months", - "start_date": "2021-01-01", - "billing_due_day": 2, - "installment_amount": 10050, + "start_date": "2025-01-01", + "billing_due_day": 15, + "installment_amount": 8500, "balance": 8990, "balance_currency": "EUR" } @@ -575,7 +527,7 @@ epilot billing createContractEntity --jsonata '$' ### `updateContractEntity` -Update an existing contract entity. +Update an existing contract entity (Vertrag). `PATCH /v1/billing/contracts/{id}` @@ -583,7 +535,7 @@ Update an existing contract entity. | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `id` | path | string | Yes | ID of the contract entity to update | +| `id` | path | string | Yes | Unique identifier of the contract (Vertragsnummer) | **Request Body** (required) @@ -591,42 +543,42 @@ Update an existing contract entity. ```bash epilot billing updateContractEntity \ - -p id=123e4567-e89b-12d3-a456-426614174000 + -p id=f589786b-3024-43cd-9cb3-5a3c953f2896 ``` With request body: ```bash epilot billing updateContractEntity \ - -p id=123e4567-e89b-12d3-a456-426614174000 \ + -p id=f589786b-3024-43cd-9cb3-5a3c953f2896 \ -d '{ "_id": "5da0a718-c822-403d-9f5d-20d4584e0528", - "_title": "string", - "_org": "string", - "_schema": "contact", - "_tags": ["string"], - "_created_at": "string", - "_updated_at": "string", - "contract_name": "Grid Contract", - "contract_number": "12345", - "status": "approved", - "description": "This contract is for the supply of widgets.", - "account_number": "67890", + "_title": "Abschlagszahlung Juli 2025", + "_org": "123456", + "_schema": "billing_event", + "_tags": ["billing", "energy"], + "_created_at": "2025-06-15T10:30:00Z", + "_updated_at": "2025-06-15T14:45:00Z", + "contract_name": "Stromvertrag Haushalt", + "contract_number": "STR-2025-001234", + "status": "active", + "description": "Haushaltsstrom-Tarif mit 24 Monaten Preisgarantie", + "account_number": "KD-67890", "branch": "power", - "billing_address": "123 Main St, Anytown", - "delivery_address": "456 Elm St, Anytown", - "additional_addresses": "789 Oak St, Anytown", - "termination_date": "2022-01-01", - "termination_reason": "Non-payment", + "billing_address": "Musterstraße 123, 50667 Köln", + "delivery_address": "Musterstraße 123, 50667 Köln", + "additional_addresses": "Postfach 456, 50668 Köln", + "termination_date": "2025-12-31", + "termination_reason": "Kundenkündigung", "billing_period": "monthly", "billing_duration_amount": 30, - "renewal_duration_amount": 365, - "renewal_duration_unit": "years", + "renewal_duration_amount": 12, + "renewal_duration_unit": "months", "notice_time_amount": 30, "notice_time_unit": "months", - "start_date": "2021-01-01", - "billing_due_day": 2, - "installment_amount": 10050, + "start_date": "2025-01-01", + "billing_due_day": 15, + "installment_amount": 8500, "balance": 8990, "balance_currency": "EUR" }' @@ -635,19 +587,19 @@ epilot billing updateContractEntity \ Using positional args for path parameters: ```bash -epilot billing updateContractEntity 123e4567-e89b-12d3-a456-426614174000 +epilot billing updateContractEntity f589786b-3024-43cd-9cb3-5a3c953f2896 ``` Using stdin pipe: ```bash -cat body.json | epilot billing updateContractEntity -p id=123e4567-e89b-12d3-a456-426614174000 +cat body.json | epilot billing updateContractEntity -p id=f589786b-3024-43cd-9cb3-5a3c953f2896 ``` With JSONata filter: ```bash -epilot billing updateContractEntity -p id=123e4567-e89b-12d3-a456-426614174000 --jsonata '$' +epilot billing updateContractEntity -p id=f589786b-3024-43cd-9cb3-5a3c953f2896 --jsonata '$' ```
@@ -656,32 +608,32 @@ epilot billing updateContractEntity -p id=123e4567-e89b-12d3-a456-426614174000 - ```json { "_id": "5da0a718-c822-403d-9f5d-20d4584e0528", - "_title": "string", - "_org": "string", - "_schema": "contact", - "_tags": ["string"], - "_created_at": "string", - "_updated_at": "string", - "contract_name": "Grid Contract", - "contract_number": "12345", - "status": "approved", - "description": "This contract is for the supply of widgets.", - "account_number": "67890", + "_title": "Abschlagszahlung Juli 2025", + "_org": "123456", + "_schema": "billing_event", + "_tags": ["billing", "energy"], + "_created_at": "2025-06-15T10:30:00Z", + "_updated_at": "2025-06-15T14:45:00Z", + "contract_name": "Stromvertrag Haushalt", + "contract_number": "STR-2025-001234", + "status": "active", + "description": "Haushaltsstrom-Tarif mit 24 Monaten Preisgarantie", + "account_number": "KD-67890", "branch": "power", - "billing_address": "123 Main St, Anytown", - "delivery_address": "456 Elm St, Anytown", - "additional_addresses": "789 Oak St, Anytown", - "termination_date": "2022-01-01", - "termination_reason": "Non-payment", + "billing_address": "Musterstraße 123, 50667 Köln", + "delivery_address": "Musterstraße 123, 50667 Köln", + "additional_addresses": "Postfach 456, 50668 Köln", + "termination_date": "2025-12-31", + "termination_reason": "Kundenkündigung", "billing_period": "monthly", "billing_duration_amount": 30, - "renewal_duration_amount": 365, - "renewal_duration_unit": "years", + "renewal_duration_amount": 12, + "renewal_duration_unit": "months", "notice_time_amount": 30, "notice_time_unit": "months", - "start_date": "2021-01-01", - "billing_due_day": 2, - "installment_amount": 10050, + "start_date": "2025-01-01", + "billing_due_day": 15, + "installment_amount": 8500, "balance": 8990, "balance_currency": "EUR" } @@ -693,7 +645,7 @@ epilot billing updateContractEntity -p id=123e4567-e89b-12d3-a456-426614174000 - ### `deleteContractEntity` -Delete an existing contract entity. +Delete an existing contract entity (Vertrag). `DELETE /v1/billing/contracts/{id}` @@ -701,32 +653,363 @@ Delete an existing contract entity. | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `id` | path | string | Yes | ID of the contract entity to delete | +| `id` | path | string | Yes | Unique identifier of the contract to delete | **Sample Call** ```bash epilot billing deleteContractEntity \ + -p id=f589786b-3024-43cd-9cb3-5a3c953f2896 +``` + +Using positional args for path parameters: + +```bash +epilot billing deleteContractEntity f589786b-3024-43cd-9cb3-5a3c953f2896 +``` + +With JSONata filter: + +```bash +epilot billing deleteContractEntity -p id=f589786b-3024-43cd-9cb3-5a3c953f2896 --jsonata '$' +``` + +--- + +### `getContractPricingInformation` + +Get current pricing information and recent configuration history for a Contract. + +`GET /v1/billing/contracts/{id}/pricing_information` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `id` | path | string | Yes | ID of the Contract entity | +| `history_change_types` | query | string | No | Comma-separated billing configuration change types to return. When omitted, installment history is returned by default. | +| `include_history` | query | boolean | No | Include recent configuration history in the pricing information response. Set to false when using the dedicated configuration history endpoint. | + +**Sample Call** + +```bash +epilot billing getContractPricingInformation \ + -p id=123e4567-e89b-12d3-a456-426614174000 +``` + +Using positional args for path parameters: + +```bash +epilot billing getContractPricingInformation 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot billing getContractPricingInformation -p id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'entity_type' +``` + +
+Sample Response + +```json +{ + "entity_type": "contract", + "entity_id": "string", + "title": "string", + "current_installment_amount": { + "amount": 10050, + "amount_decimal": "100.50", + "currency": "EUR" + }, + "context": { + "base_price": { + "price_id": "string", + "price_title": "string", + "tariff_type": "string", + "pricing_model": "string", + "unit_amount_gross_decimal": "string", + "unit_amount_net_decimal": "string", + "before_discount_unit_amount_gross_decimal": "string", + "before_discount_unit_amount_net_decimal": "string", + "unit_discount_amount_decimal": "string", + "unit_discount_amount_net_decimal": "string", + "currency": "EUR", + "billing_period": "string", + "unit": "string", + "has_discount": true, + "is_dynamic_tariff": true, + "dynamic_tariff": {} + }, + "base_prices": [ + {} + ], + "working_price": { + "price_id": "string", + "price_title": "string", + "tariff_type": "string", + "pricing_model": "string", + "unit_amount_gross_decimal": "string", + "unit_amount_net_decimal": "string", + "before_discount_unit_amount_gross_decimal": "string", + "before_discount_unit_amount_net_decimal": "string", + "unit_discount_amount_decimal": "string", + "unit_discount_amount_net_decimal": "string", + "currency": "EUR", + "billing_period": "string", + "unit": "string", + "has_discount": true, + "is_dynamic_tariff": true, + "dynamic_tariff": {} + }, + "working_prices": [ + {} + ] + }, + "balance": { + "amount": 8990, + "amount_decimal": "89.90", + "currency": "EUR" + }, + "schedule": { + "billing_due_day": 0, + "billing_period": "weekly", + "installments_per_year": 0, + "inferred": true + }, + "pending_installment_change": true, + "history": [ + { + "event_id": "string", + "org_id": "string", + "entity_type": "contract", + "entity_id": "string", + "schema_version": 1, + "effective_at": "1970-01-01T00:00:00.000Z", + "changed_at": "1970-01-01T00:00:00.000Z", + "created_at": "1970-01-01T00:00:00.000Z", + "source": "portal", + "source_label": "string", + "source_system": "string", + "source_reference": "string", + "change_type": "installment_amount_changed", + "previous_value": {}, + "new_value": {}, + "context": {} + } + ] +} +``` + +
+ +--- + +### `getBillingAccountPricingInformation` + +Get current pricing information for the active Contracts linked to a Billing Account. + +`GET /v1/billing/billing_accounts/{id}/pricing_information` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `id` | path | string | Yes | ID of the Billing Account entity | +| `history_change_types` | query | string | No | Comma-separated billing configuration change types to return. When omitted, installment history is returned by default. | +| `include_history` | query | boolean | No | Include recent configuration history in the pricing information response. Set to false when using the dedicated configuration history endpoint. | + +**Sample Call** + +```bash +epilot billing getBillingAccountPricingInformation \ + -p id=123e4567-e89b-12d3-a456-426614174000 +``` + +Using positional args for path parameters: + +```bash +epilot billing getBillingAccountPricingInformation 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot billing getBillingAccountPricingInformation -p id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'entity_type' +``` + +
+Sample Response + +```json +{ + "entity_type": "billing_account", + "entity_id": "string", + "title": "string", + "balance": { + "amount": 8990, + "amount_decimal": "89.90", + "currency": "EUR" + }, + "contracts": [ + { + "entity_type": "contract", + "entity_id": "string", + "title": "string", + "current_installment_amount": {}, + "context": {}, + "balance": {}, + "schedule": {}, + "pending_installment_change": true, + "history": [] + } + ] +} +``` + +
+ +--- + +### `getContractConfigurationHistory` + +Get billing configuration history for a Contract. + +`GET /v1/billing/contracts/{id}/configuration_history` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `id` | path | string | Yes | ID of the Contract entity | +| `change_type` | query | "installment_amount_changed" \| "contract_pricing_changed" | No | Billing configuration change type to return. | +| `history_change_types` | query | string | No | Comma-separated billing configuration change types to return. When omitted, installment history is returned by default. | +| `from` | query | number | No | Initial offset for paginated results. | +| `size` | query | number | No | Maximum number of results to return. | + +**Sample Call** + +```bash +epilot billing getContractConfigurationHistory \ -p id=123e4567-e89b-12d3-a456-426614174000 ``` Using positional args for path parameters: ```bash -epilot billing deleteContractEntity 123e4567-e89b-12d3-a456-426614174000 +epilot billing getContractConfigurationHistory 123e4567-e89b-12d3-a456-426614174000 ``` With JSONata filter: ```bash -epilot billing deleteContractEntity -p id=123e4567-e89b-12d3-a456-426614174000 --jsonata '$' +epilot billing getContractConfigurationHistory -p id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'history' +``` + +
+Sample Response + +```json +{ + "history": [ + { + "event_id": "string", + "org_id": "string", + "entity_type": "contract", + "entity_id": "string", + "schema_version": 1, + "effective_at": "1970-01-01T00:00:00.000Z", + "changed_at": "1970-01-01T00:00:00.000Z", + "created_at": "1970-01-01T00:00:00.000Z", + "source": "portal", + "source_label": "string", + "source_system": "string", + "source_reference": "string", + "change_type": "installment_amount_changed", + "previous_value": {}, + "new_value": {}, + "context": {} + } + ], + "total": 0 +} ``` +
+ +--- + +### `getBillingAccountConfigurationHistory` + +Get merged billing configuration history for active Contracts linked to a Billing Account. + +`GET /v1/billing/billing_accounts/{id}/configuration_history` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `id` | path | string | Yes | ID of the Billing Account entity | +| `change_type` | query | "installment_amount_changed" \| "contract_pricing_changed" | No | Billing configuration change type to return. | +| `history_change_types` | query | string | No | Comma-separated billing configuration change types to return. When omitted, installment history is returned by default. | +| `from` | query | number | No | Initial offset for paginated results. | +| `size` | query | number | No | Maximum number of results to return. | + +**Sample Call** + +```bash +epilot billing getBillingAccountConfigurationHistory \ + -p id=123e4567-e89b-12d3-a456-426614174000 +``` + +Using positional args for path parameters: + +```bash +epilot billing getBillingAccountConfigurationHistory 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot billing getBillingAccountConfigurationHistory -p id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'history' +``` + +
+Sample Response + +```json +{ + "history": [ + { + "event_id": "string", + "org_id": "string", + "entity_type": "contract", + "entity_id": "string", + "schema_version": 1, + "effective_at": "1970-01-01T00:00:00.000Z", + "changed_at": "1970-01-01T00:00:00.000Z", + "created_at": "1970-01-01T00:00:00.000Z", + "source": "portal", + "source_label": "string", + "source_system": "string", + "source_reference": "string", + "change_type": "installment_amount_changed", + "previous_value": {}, + "new_value": {}, + "context": {} + } + ], + "total": 0 +} +``` + +
+ --- ### `getCustomerBalance` -Get total balance across all contracts and orders of a customer entity. +Retrieve the total balance (Kontostand) across all contracts and orders for a customer. `GET /v1/billing/customers/{id}/balance` @@ -734,7 +1017,9 @@ Get total balance across all contracts and orders of a customer entity. | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `id` | path | string | Yes | Customer entity ID (contact or account) | +| `id` | path | string | Yes | Customer entity ID. This can be either a contact ID (Kontakt-ID) or +an account ID (Kundenkonto-ID). + | **Sample Call** diff --git a/packages/cli/docs/blueprint-manifest.md b/packages/cli/docs/blueprint-manifest.md index 5bffbb029..ef41d02d0 100644 --- a/packages/cli/docs/blueprint-manifest.md +++ b/packages/cli/docs/blueprint-manifest.md @@ -42,15 +42,15 @@ epilot blueprint-manifest getJob -p job_id=4854bb2a-94f9-424d-a968-3fb17fb0bf89 - [`listBlueprints`](#listblueprints) — List Custom and Installed Blueprints - [`createBlueprint`](#createblueprint) — Create a Blueprint - [`listInstalledMarketplaceBlueprints`](#listinstalledmarketplaceblueprints) — List installed Marketplace Blueprints for the organization. -- [`preInstallBlueprint`](#preinstallblueprint) — Pre-install a Blueprint based on a blueprint file +- [`preInstallBlueprint`](#preinstallblueprint) — Pre-install a Blueprint based on a blueprint file. Format-agnostic: the engine is detected from the uploaded archive, so - [`getBlueprintPreview`](#getblueprintpreview) — Get Blueprint Preview by ID -- [`installBlueprint`](#installblueprint) — Kick off a new blueprint installation job. Returns 202 Accepted with Location header pointing to the job resource - [`getBlueprint`](#getblueprint) — Get Blueprint by ID - [`updateBlueprint`](#updateblueprint) — Update a Blueprint - [`deleteBlueprint`](#deleteblueprint) — Delete a Blueprint -- [`validateBlueprint`](#validateblueprint) — Start a blueprint validation job. Validates Terraform for the blueprint (all types). +- [`addBlueprintNote`](#addblueprintnote) — Append an internal note to a blueprint. `id`, `created_at` and `created_by` +- [`updateBlueprintNote`](#updateblueprintnote) — Rewrite the text of an existing internal note. The note keeps its position in +- [`deleteBlueprintNote`](#deleteblueprintnote) — Remove a single internal note from a blueprint. - [`verifyBlueprint`](#verifyblueprint) — Start a blueprint verification job. Compares resource configurations between a source org -- [`exportBlueprint`](#exportblueprint) — Kick off a new blueprint export job. Returns 202 Accepted with Location header pointing to the job resource. - [`listMarketplaceSlugs`](#listmarketplaceslugs) — List all available marketplace blueprint slugs from Webflow CMS. - [`publishBlueprint`](#publishblueprint) — Publish a blueprint to the marketplace. Exports the blueprint, uploads it to file-api with public access, and updates th - [`formatBlueprintDescription`](#formatblueprintdescription) — Format a blueprint description as markdown using AI. @@ -62,23 +62,24 @@ epilot blueprint-manifest getJob -p job_id=4854bb2a-94f9-424d-a968-3fb17fb0bf89 - [`bulkDeleteBlueprintResources`](#bulkdeleteblueprintresources) — Bulk delete resources in a Blueprint - [`updateBlueprintResource`](#updateblueprintresource) — Update a resource in a Blueprint - [`deleteBlueprintResource`](#deleteblueprintresource) — Delete a resource from a Blueprint -- [`installBlueprintV3`](#installblueprintv3) — Install a blueprint using the V3 engine (direct API calls, no Terraform). +- [`publishBlueprintV3`](#publishblueprintv3) — Starts an asynchronous V3 publication. The result is a signed, portable package; poll the existing blueprint job endpoin +- [`preInstallBlueprintV3`](#preinstallblueprintv3) — Validates a signed V3 package and returns the destination-specific resource plan used by the install UI. +- [`installBlueprintV3`](#installblueprintv3) — Install a blueprint into a single destination org using the V3 engine (direct API - [`restoreBlueprintDeploymentV3`](#restoreblueprintdeploymentv3) — Roll a deployment back to its pre-install state. Two phases: - [`getRestorePreview`](#getrestorepreview) — Computes what would happen if the user triggered a restore on this +- [`triggerDeploymentHealthCheckV3`](#triggerdeploymenthealthcheckv3) — Starts a read-only health scan of the resources this deployment's +- [`getDeploymentHealthReportV3`](#getdeploymenthealthreportv3) — Returns the most recent health report produced for this deployment - [`getBlueprintLineageV3`](#getblueprintlineagev3) — Returns the lineage registry entries for a blueprint's resources in the current org. - -**Patches** -- [`detectPatchChanges`](#detectpatchchanges) — Detect changes between the current state of a blueprint's resources and its tfstate baseline. -- [`listPatches`](#listpatches) — List all patches for a blueprint. -- [`createPatch`](#createpatch) — Create a new patch for a blueprint. -- [`getPatch`](#getpatch) — Get a patch by ID, including per-org execution results. -- [`applyPatch`](#applypatch) — Apply a patch to a single destination org. -- [`retryPatchOrg`](#retrypatchorg) — Retry a failed patch execution for a specific org. +- [`createBulkInstallV3`](#createbulkinstallv3) — Install one source blueprint into many destination organizations in a single +- [`getBulkInstallV3`](#getbulkinstallv3) — Returns the bulk install parent with aggregate status and counts. Scoped by the +- [`listBulkInstallTargetsV3`](#listbulkinstalltargetsv3) — Pages through the bulk install's target rows. Each row hydrates its latest child +- [`retryBulkInstallTargetV3`](#retrybulkinstalltargetv3) — Retries a single failed target. Allowed only for `FAILED` and `PARTIAL_SUCCESS` **Jobs** - [`listBlueprintJobs`](#listblueprintjobs) — List all blueprint jobs -- [`getBlueprintJob`](#getblueprintjob) — Poll current state of a job. -- [`continueInstallationJob`](#continueinstallationjob) — Continue an installation job if it is waiting for user action. +- [`getBlueprintJob`](#getblueprintjob) — Poll the current state of a job. Serves both Terraform (v2) and V3-engine jobs — +- [`continueInstallationJob`](#continueinstallationjob) — Resume an installation job that is paused at `status: "WAITING_USER_ACTION"` after +- [`retryInstallationJob`](#retryinstallationjob) — Retry a finished V3 installation job whose status is `FAILED` or - [`cancelBlueprintJob`](#cancelblueprintjob) — Cancel a blueprint job if it is still running. **Marketplace Listings** @@ -181,12 +182,15 @@ epilot blueprint-manifest listBlueprints --jsonata 'results[0]' "title": "string", "slug": "string", "description": {}, + "notes": [], "version": "string", "deployments": [], "is_verified": true, "latest_verification": {}, "ignored_resource_addresses": ["string"], "installation_status": "IN_PROGRESS", + "active_restore_job_id": "string", + "active_restore_started_at": "1970-01-01T00:00:00.000Z", "created_at": "1970-01-01T00:00:00.000Z", "updated_at": "1970-01-01T00:00:00.000Z", "created_by": {}, @@ -235,6 +239,15 @@ epilot blueprint-manifest createBlueprint \ "preinstall": "This is the content of the preinstall.md file which contains the blueprint description.\n", "postinstall": "This is the content of the postinstall.md file\n" }, + "notes": [ + { + "id": "3f1c9b0e-2f3a-4a1f-9a3e-6f2b8c7d1e40", + "text": "Adjusted the meter-reading journey for the §14a rollout.", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z", + "created_by": {} + } + ], "version": "string", "deployments": [ { @@ -244,6 +257,7 @@ epilot blueprint-manifest createBlueprint \ "destination_blueprint_id": "string", "job_id": "string", "triggered_at": "1970-01-01T00:00:00.000Z", + "performed_by": {}, "note": "string", "status": "IN_PROGRESS", "restore_details": {}, @@ -261,6 +275,8 @@ epilot blueprint-manifest createBlueprint \ "source_blueprint_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", "destination_org_id": "string", "destination_blueprint_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", + "installation_job_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", + "sync_engine": "terraform", "summary": { "total_resources": 0, "matched": 0, @@ -271,6 +287,8 @@ epilot blueprint-manifest createBlueprint \ }, "ignored_resource_addresses": ["string"], "installation_status": "IN_PROGRESS", + "active_restore_job_id": "string", + "active_restore_started_at": "1970-01-01T00:00:00.000Z", "created_at": "1970-01-01T00:00:00.000Z", "updated_at": "1970-01-01T00:00:00.000Z", "created_by": { @@ -340,6 +358,15 @@ epilot blueprint-manifest createBlueprint --jsonata '$' "preinstall": "This is the content of the preinstall.md file which contains the blueprint description.\n", "postinstall": "This is the content of the postinstall.md file\n" }, + "notes": [ + { + "id": "3f1c9b0e-2f3a-4a1f-9a3e-6f2b8c7d1e40", + "text": "Adjusted the meter-reading journey for the §14a rollout.", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z", + "created_by": {} + } + ], "version": "string", "deployments": [ { @@ -349,6 +376,7 @@ epilot blueprint-manifest createBlueprint --jsonata '$' "destination_blueprint_id": "string", "job_id": "string", "triggered_at": "1970-01-01T00:00:00.000Z", + "performed_by": {}, "note": "string", "status": "IN_PROGRESS", "restore_details": {}, @@ -366,6 +394,8 @@ epilot blueprint-manifest createBlueprint --jsonata '$' "source_blueprint_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", "destination_org_id": "string", "destination_blueprint_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", + "installation_job_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", + "sync_engine": "terraform", "summary": { "total_resources": 0, "matched": 0, @@ -376,6 +406,8 @@ epilot blueprint-manifest createBlueprint --jsonata '$' }, "ignored_resource_addresses": ["string"], "installation_status": "IN_PROGRESS", + "active_restore_job_id": "string", + "active_restore_started_at": "1970-01-01T00:00:00.000Z", "created_at": "1970-01-01T00:00:00.000Z", "updated_at": "1970-01-01T00:00:00.000Z", "created_by": { @@ -482,7 +514,7 @@ epilot blueprint-manifest listInstalledMarketplaceBlueprints --jsonata 'results[ ### `preInstallBlueprint` -Pre-install a Blueprint based on a blueprint file +Pre-install a Blueprint based on a blueprint file. Format-agnostic: the engine is detected from the uploaded archive, so `POST /v2/blueprint-manifest/blueprints:pre-install` @@ -521,6 +553,7 @@ epilot blueprint-manifest preInstallBlueprint --jsonata 'id' "version": "string", "slug": "string", "source_type": "marketplace", + "sync_engine": "terraform", "blueprint_file_s3_key": "string", "is_verified": true, "docs_url": "string", @@ -606,6 +639,7 @@ epilot blueprint-manifest getBlueprintPreview -p preview_id=123e4567-e89b-12d3-a "version": "string", "slug": "string", "source_type": "marketplace", + "sync_engine": "terraform", "blueprint_file_s3_key": "string", "is_verified": true, "docs_url": "string", @@ -646,56 +680,6 @@ epilot blueprint-manifest getBlueprintPreview -p preview_id=123e4567-e89b-12d3-a --- -### `installBlueprint` - -Kick off a new blueprint installation job. Returns 202 Accepted with Location header pointing to the job resource - -`POST /v2/blueprint-manifest/blueprint:install` - -**Request Body** (required) - -**Sample Call** - -```bash -epilot blueprint-manifest installBlueprint -``` - -With request body: - -```bash -epilot blueprint-manifest installBlueprint \ - -d '{ - "source_org_id": "string", - "source_blueprint_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", - "source_blueprint_file": "string", - "destination_org_id": "string", - "destination_blueprint_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", - "source_auth_token": "string", - "destination_auth_token": "string", - "options": { - "resources_to_ignore": ["string"] - }, - "mode": "simple", - "source_blueprint_type": "marketplace", - "slug": "string", - "auto_enable_features": true -}' -``` - -Using stdin pipe: - -```bash -cat body.json | epilot blueprint-manifest installBlueprint -``` - -With JSONata filter: - -```bash -epilot blueprint-manifest installBlueprint --jsonata '$' -``` - ---- - ### `getBlueprint` Get Blueprint by ID @@ -739,6 +723,15 @@ epilot blueprint-manifest getBlueprint -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1 "preinstall": "This is the content of the preinstall.md file which contains the blueprint description.\n", "postinstall": "This is the content of the postinstall.md file\n" }, + "notes": [ + { + "id": "3f1c9b0e-2f3a-4a1f-9a3e-6f2b8c7d1e40", + "text": "Adjusted the meter-reading journey for the §14a rollout.", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z", + "created_by": {} + } + ], "version": "string", "deployments": [ { @@ -748,6 +741,7 @@ epilot blueprint-manifest getBlueprint -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1 "destination_blueprint_id": "string", "job_id": "string", "triggered_at": "1970-01-01T00:00:00.000Z", + "performed_by": {}, "note": "string", "status": "IN_PROGRESS", "restore_details": {}, @@ -765,6 +759,8 @@ epilot blueprint-manifest getBlueprint -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1 "source_blueprint_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", "destination_org_id": "string", "destination_blueprint_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", + "installation_job_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", + "sync_engine": "terraform", "summary": { "total_resources": 0, "matched": 0, @@ -775,6 +771,8 @@ epilot blueprint-manifest getBlueprint -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1 }, "ignored_resource_addresses": ["string"], "installation_status": "IN_PROGRESS", + "active_restore_job_id": "string", + "active_restore_started_at": "1970-01-01T00:00:00.000Z", "created_at": "1970-01-01T00:00:00.000Z", "updated_at": "1970-01-01T00:00:00.000Z", "created_by": { @@ -858,6 +856,15 @@ epilot blueprint-manifest updateBlueprint \ "preinstall": "This is the content of the preinstall.md file which contains the blueprint description.\n", "postinstall": "This is the content of the postinstall.md file\n" }, + "notes": [ + { + "id": "3f1c9b0e-2f3a-4a1f-9a3e-6f2b8c7d1e40", + "text": "Adjusted the meter-reading journey for the §14a rollout.", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z", + "created_by": {} + } + ], "version": "string", "deployments": [ { @@ -867,6 +874,7 @@ epilot blueprint-manifest updateBlueprint \ "destination_blueprint_id": "string", "job_id": "string", "triggered_at": "1970-01-01T00:00:00.000Z", + "performed_by": {}, "note": "string", "status": "IN_PROGRESS", "restore_details": {}, @@ -884,6 +892,8 @@ epilot blueprint-manifest updateBlueprint \ "source_blueprint_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", "destination_org_id": "string", "destination_blueprint_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", + "installation_job_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", + "sync_engine": "terraform", "summary": { "total_resources": 0, "matched": 0, @@ -894,6 +904,8 @@ epilot blueprint-manifest updateBlueprint \ }, "ignored_resource_addresses": ["string"], "installation_status": "IN_PROGRESS", + "active_restore_job_id": "string", + "active_restore_started_at": "1970-01-01T00:00:00.000Z", "created_at": "1970-01-01T00:00:00.000Z", "updated_at": "1970-01-01T00:00:00.000Z", "created_by": { @@ -969,6 +981,15 @@ epilot blueprint-manifest updateBlueprint -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6 "preinstall": "This is the content of the preinstall.md file which contains the blueprint description.\n", "postinstall": "This is the content of the postinstall.md file\n" }, + "notes": [ + { + "id": "3f1c9b0e-2f3a-4a1f-9a3e-6f2b8c7d1e40", + "text": "Adjusted the meter-reading journey for the §14a rollout.", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z", + "created_by": {} + } + ], "version": "string", "deployments": [ { @@ -978,6 +999,7 @@ epilot blueprint-manifest updateBlueprint -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6 "destination_blueprint_id": "string", "job_id": "string", "triggered_at": "1970-01-01T00:00:00.000Z", + "performed_by": {}, "note": "string", "status": "IN_PROGRESS", "restore_details": {}, @@ -995,6 +1017,8 @@ epilot blueprint-manifest updateBlueprint -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6 "source_blueprint_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", "destination_org_id": "string", "destination_blueprint_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", + "installation_job_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", + "sync_engine": "terraform", "summary": { "total_resources": 0, "matched": 0, @@ -1005,6 +1029,8 @@ epilot blueprint-manifest updateBlueprint -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6 }, "ignored_resource_addresses": ["string"], "installation_status": "IN_PROGRESS", + "active_restore_job_id": "string", + "active_restore_started_at": "1970-01-01T00:00:00.000Z", "created_at": "1970-01-01T00:00:00.000Z", "updated_at": "1970-01-01T00:00:00.000Z", "created_by": { @@ -1097,6 +1123,15 @@ epilot blueprint-manifest deleteBlueprint -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6 "preinstall": "This is the content of the preinstall.md file which contains the blueprint description.\n", "postinstall": "This is the content of the postinstall.md file\n" }, + "notes": [ + { + "id": "3f1c9b0e-2f3a-4a1f-9a3e-6f2b8c7d1e40", + "text": "Adjusted the meter-reading journey for the §14a rollout.", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z", + "created_by": {} + } + ], "version": "string", "deployments": [ { @@ -1106,6 +1141,7 @@ epilot blueprint-manifest deleteBlueprint -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6 "destination_blueprint_id": "string", "job_id": "string", "triggered_at": "1970-01-01T00:00:00.000Z", + "performed_by": {}, "note": "string", "status": "IN_PROGRESS", "restore_details": {}, @@ -1123,6 +1159,8 @@ epilot blueprint-manifest deleteBlueprint -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6 "source_blueprint_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", "destination_org_id": "string", "destination_blueprint_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", + "installation_job_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", + "sync_engine": "terraform", "summary": { "total_resources": 0, "matched": 0, @@ -1133,6 +1171,8 @@ epilot blueprint-manifest deleteBlueprint -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6 }, "ignored_resource_addresses": ["string"], "installation_status": "IN_PROGRESS", + "active_restore_job_id": "string", + "active_restore_started_at": "1970-01-01T00:00:00.000Z", "created_at": "1970-01-01T00:00:00.000Z", "updated_at": "1970-01-01T00:00:00.000Z", "created_by": { @@ -1182,44 +1222,11 @@ epilot blueprint-manifest deleteBlueprint -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6 --- -### `validateBlueprint` - -Start a blueprint validation job. Validates Terraform for the blueprint (all types). - -`POST /v2/blueprint-manifest/blueprints/{blueprint_id}/validate` - -**Parameters** - -| Name | In | Type | Required | Description | -| ---- | -- | ---- | -------- | ----------- | -| `blueprint_id` | path | string | Yes | | - -**Sample Call** - -```bash -epilot blueprint-manifest validateBlueprint \ - -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 -``` - -Using positional args for path parameters: - -```bash -epilot blueprint-manifest validateBlueprint c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 -``` - -With JSONata filter: - -```bash -epilot blueprint-manifest validateBlueprint -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 --jsonata '$' -``` - ---- - -### `verifyBlueprint` +### `addBlueprintNote` -Start a blueprint verification job. Compares resource configurations between a source org +Append an internal note to a blueprint. `id`, `created_at` and `created_by` -`POST /v2/blueprint-manifest/blueprints/{blueprint_id}:verify` +`POST /v2/blueprint-manifest/blueprints/{blueprint_id}/notes` **Parameters** @@ -1232,83 +1239,91 @@ Start a blueprint verification job. Compares resource configurations between a s **Sample Call** ```bash -epilot blueprint-manifest verifyBlueprint \ - -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 -``` - -With request body: - -```bash -epilot blueprint-manifest verifyBlueprint \ +epilot blueprint-manifest addBlueprintNote \ -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 \ - -d '{ - "source_org_id": "string", - "source_blueprint_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", - "destination_org_id": "string", - "destination_blueprint_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", - "source_auth_token": "string", - "destination_auth_token": "string" -}' + -d '{"text":"string"}' ``` Using positional args for path parameters: ```bash -epilot blueprint-manifest verifyBlueprint c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 +epilot blueprint-manifest addBlueprintNote c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 ``` Using stdin pipe: ```bash -cat body.json | epilot blueprint-manifest verifyBlueprint -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 +cat body.json | epilot blueprint-manifest addBlueprintNote -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 ``` With JSONata filter: ```bash -epilot blueprint-manifest verifyBlueprint -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 --jsonata '$' +epilot blueprint-manifest addBlueprintNote -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 --jsonata 'id' +``` + +
+Sample Response + +```json +{ + "id": "3f1c9b0e-2f3a-4a1f-9a3e-6f2b8c7d1e40", + "text": "Adjusted the meter-reading journey for the §14a rollout.", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z", + "created_by": { + "name": "manifest@epilot.cloud", + "org_id": "911690", + "user_id": "11001045", + "token_id": "api_5ZugdRXasLfWBypHi93Fk" + } +} ``` +
+ --- -### `detectPatchChanges` +### `updateBlueprintNote` -Detect changes between the current state of a blueprint's resources and its tfstate baseline. +Rewrite the text of an existing internal note. The note keeps its position in -`POST /v2/blueprint-manifest/blueprints/{blueprint_id}/patches:detect` +`PATCH /v2/blueprint-manifest/blueprints/{blueprint_id}/notes/{note_id}` **Parameters** | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | | `blueprint_id` | path | string | Yes | | +| `note_id` | path | string | Yes | | -**Request Body** +**Request Body** (required) **Sample Call** ```bash -epilot blueprint-manifest detectPatchChanges \ +epilot blueprint-manifest updateBlueprintNote \ -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 \ - -d '{"source_org_id":"string","dest_org_id":"string","dest_blueprint_id":"string","rollout_id":"string"}' + -p note_id=123e4567-e89b-12d3-a456-426614174000 \ + -d '{"text":"string"}' ``` Using positional args for path parameters: ```bash -epilot blueprint-manifest detectPatchChanges c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 +epilot blueprint-manifest updateBlueprintNote c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 123e4567-e89b-12d3-a456-426614174000 ``` Using stdin pipe: ```bash -cat body.json | epilot blueprint-manifest detectPatchChanges -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 +cat body.json | epilot blueprint-manifest updateBlueprintNote -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 -p note_id=123e4567-e89b-12d3-a456-426614174000 ``` With JSONata filter: ```bash -epilot blueprint-manifest detectPatchChanges -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 --jsonata 'resources' +epilot blueprint-manifest updateBlueprintNote -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 -p note_id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'id' ```
@@ -1316,22 +1331,16 @@ epilot blueprint-manifest detectPatchChanges -p blueprint_id=c2d6cac8-bdd5-4ea2- ```json { - "resources": [ - { - "type": "string", - "source_id": "string", - "address": "string", - "name": "string", - "changes": [ - { - "path": "string", - "op": "changed", - "baseline_value": {}, - "current_value": {} - } - ] - } - ] + "id": "3f1c9b0e-2f3a-4a1f-9a3e-6f2b8c7d1e40", + "text": "Adjusted the meter-reading journey for the §14a rollout.", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z", + "created_by": { + "name": "manifest@epilot.cloud", + "org_id": "911690", + "user_id": "11001045", + "token_id": "api_5ZugdRXasLfWBypHi93Fk" + } } ``` @@ -1339,35 +1348,37 @@ epilot blueprint-manifest detectPatchChanges -p blueprint_id=c2d6cac8-bdd5-4ea2- --- -### `listPatches` +### `deleteBlueprintNote` -List all patches for a blueprint. +Remove a single internal note from a blueprint. -`GET /v2/blueprint-manifest/blueprints/{blueprint_id}/patches` +`DELETE /v2/blueprint-manifest/blueprints/{blueprint_id}/notes/{note_id}` **Parameters** | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | | `blueprint_id` | path | string | Yes | | +| `note_id` | path | string | Yes | | **Sample Call** ```bash -epilot blueprint-manifest listPatches \ - -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 +epilot blueprint-manifest deleteBlueprintNote \ + -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 \ + -p note_id=123e4567-e89b-12d3-a456-426614174000 ``` Using positional args for path parameters: ```bash -epilot blueprint-manifest listPatches c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 +epilot blueprint-manifest deleteBlueprintNote c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 123e4567-e89b-12d3-a456-426614174000 ``` With JSONata filter: ```bash -epilot blueprint-manifest listPatches -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 --jsonata 'results[0]' +epilot blueprint-manifest deleteBlueprintNote -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 -p note_id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'notes' ```
@@ -1375,37 +1386,18 @@ epilot blueprint-manifest listPatches -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1c ```json { - "total": 0, - "results": [ + "notes": [ { - "patch_id": "string", - "version": 0, - "blueprint_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", - "rollout_id": "string", - "source_org_id": "string", - "name": "string", - "description": "string", - "status": "draft", - "resources": [ - { - "type": "string", - "source_id": "string", - "address": "string", - "name": "string", - "changes": [ - { - "path": "string", - "op": "changed", - "baseline_value": {}, - "current_value": {} - } - ] - } - ], - "changelog": "string", - "created_by": "string", + "id": "3f1c9b0e-2f3a-4a1f-9a3e-6f2b8c7d1e40", + "text": "Adjusted the meter-reading journey for the §14a rollout.", "created_at": "1970-01-01T00:00:00.000Z", - "applied_at": "1970-01-01T00:00:00.000Z" + "updated_at": "1970-01-01T00:00:00.000Z", + "created_by": { + "name": "manifest@epilot.cloud", + "org_id": "911690", + "user_id": "11001045", + "token_id": "api_5ZugdRXasLfWBypHi93Fk" + } } ] } @@ -1415,11 +1407,11 @@ epilot blueprint-manifest listPatches -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1c --- -### `createPatch` +### `verifyBlueprint` -Create a new patch for a blueprint. +Start a blueprint verification job. Compares resource configurations between a source org -`POST /v2/blueprint-manifest/blueprints/{blueprint_id}/patches` +`POST /v2/blueprint-manifest/blueprints/{blueprint_id}:verify` **Parameters** @@ -1432,130 +1424,63 @@ Create a new patch for a blueprint. **Sample Call** ```bash -epilot blueprint-manifest createPatch \ +epilot blueprint-manifest verifyBlueprint \ -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 ``` With request body: ```bash -epilot blueprint-manifest createPatch \ +epilot blueprint-manifest verifyBlueprint \ -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 \ -d '{ - "blueprint_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", - "rollout_id": "string", "source_org_id": "string", - "name": "string", - "description": "string", - "resources": [ - { - "type": "string", - "source_id": "string", - "address": "string", - "name": "string", - "changes": [ - { - "path": "string", - "op": "changed", - "baseline_value": {}, - "current_value": {} - } - ] - } - ], - "changelog": "string" + "source_blueprint_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", + "destination_org_id": "string", + "destination_blueprint_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", + "source_auth_token": "string", + "destination_auth_token": "string", + "installation_job_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", + "sync_engine": "terraform" }' ``` Using positional args for path parameters: ```bash -epilot blueprint-manifest createPatch c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 +epilot blueprint-manifest verifyBlueprint c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 ``` Using stdin pipe: ```bash -cat body.json | epilot blueprint-manifest createPatch -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 +cat body.json | epilot blueprint-manifest verifyBlueprint -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 ``` With JSONata filter: ```bash -epilot blueprint-manifest createPatch -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 --jsonata 'patch_id' -``` - -
-Sample Response - -```json -{ - "patch_id": "string", - "version": 0, - "blueprint_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", - "rollout_id": "string", - "source_org_id": "string", - "name": "string", - "description": "string", - "status": "draft", - "resources": [ - { - "type": "string", - "source_id": "string", - "address": "string", - "name": "string", - "changes": [ - { - "path": "string", - "op": "changed", - "baseline_value": {}, - "current_value": {} - } - ] - } - ], - "changelog": "string", - "created_by": "string", - "created_at": "1970-01-01T00:00:00.000Z", - "applied_at": "1970-01-01T00:00:00.000Z" -} +epilot blueprint-manifest verifyBlueprint -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 --jsonata '$' ``` -
- --- -### `getPatch` - -Get a patch by ID, including per-org execution results. - -`GET /v2/blueprint-manifest/blueprints/{blueprint_id}/patches/{patch_id}` +### `listMarketplaceSlugs` -**Parameters** +List all available marketplace blueprint slugs from Webflow CMS. -| Name | In | Type | Required | Description | -| ---- | -- | ---- | -------- | ----------- | -| `blueprint_id` | path | string | Yes | | -| `patch_id` | path | string | Yes | | +`GET /v2/blueprint-manifest/marketplace/slugs` **Sample Call** ```bash -epilot blueprint-manifest getPatch \ - -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 \ - -p patch_id=123e4567-e89b-12d3-a456-426614174000 -``` - -Using positional args for path parameters: - -```bash -epilot blueprint-manifest getPatch c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 123e4567-e89b-12d3-a456-426614174000 +epilot blueprint-manifest listMarketplaceSlugs ``` With JSONata filter: ```bash -epilot blueprint-manifest getPatch -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 -p patch_id=123e4567-e89b-12d3-a456-426614174000 --jsonata '$' +epilot blueprint-manifest listMarketplaceSlugs --jsonata 'results[0]' ```
@@ -1563,286 +1488,7 @@ epilot blueprint-manifest getPatch -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdb ```json { - "patch_id": "string", - "version": 0, - "blueprint_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", - "rollout_id": "string", - "source_org_id": "string", - "name": "string", - "description": "string", - "status": "draft", - "resources": [ - { - "type": "string", - "source_id": "string", - "address": "string", - "name": "string", - "changes": [ - { - "path": "string", - "op": "changed", - "baseline_value": {}, - "current_value": {} - } - ] - } - ], - "changelog": "string", - "created_by": "string", - "created_at": "1970-01-01T00:00:00.000Z", - "applied_at": "1970-01-01T00:00:00.000Z", - "org_results": [ - { - "patch_id": "string", - "version": 0, - "org_id": "string", - "org_name": "string", - "dest_blueprint_id": "string", - "status": "pending", - "error": "string", - "applied_at": "1970-01-01T00:00:00.000Z", - "retries": 0, - "changes_applied": [ - { - "path": "string", - "op": "changed", - "baseline_value": {}, - "current_value": {} - } - ] - } - ] -} -``` - -
- ---- - -### `applyPatch` - -Apply a patch to a single destination org. - -`POST /v2/blueprint-manifest/blueprints/{blueprint_id}/patches/{patch_id}:apply` - -**Parameters** - -| Name | In | Type | Required | Description | -| ---- | -- | ---- | -------- | ----------- | -| `blueprint_id` | path | string | Yes | | -| `patch_id` | path | string | Yes | | - -**Request Body** (required) - -**Sample Call** - -```bash -epilot blueprint-manifest applyPatch \ - -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 \ - -p patch_id=123e4567-e89b-12d3-a456-426614174000 -``` - -With request body: - -```bash -epilot blueprint-manifest applyPatch \ - -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 \ - -p patch_id=123e4567-e89b-12d3-a456-426614174000 \ - -d '{ - "org_id": "string", - "org_name": "string", - "dest_blueprint_id": "string", - "dest_org_id": "string", - "destination_auth_token": "string" -}' -``` - -Using positional args for path parameters: - -```bash -epilot blueprint-manifest applyPatch c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 123e4567-e89b-12d3-a456-426614174000 -``` - -Using stdin pipe: - -```bash -cat body.json | epilot blueprint-manifest applyPatch -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 -p patch_id=123e4567-e89b-12d3-a456-426614174000 -``` - -With JSONata filter: - -```bash -epilot blueprint-manifest applyPatch -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 -p patch_id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'patch_id' -``` - -
-Sample Response - -```json -{ - "patch_id": "string", - "version": 0, - "org_id": "string", - "org_name": "string", - "dest_blueprint_id": "string", - "status": "pending", - "error": "string", - "applied_at": "1970-01-01T00:00:00.000Z", - "retries": 0, - "changes_applied": [ - { - "path": "string", - "op": "changed", - "baseline_value": {}, - "current_value": {} - } - ] -} -``` - -
- ---- - -### `retryPatchOrg` - -Retry a failed patch execution for a specific org. - -`POST /v2/blueprint-manifest/blueprints/{blueprint_id}/patches/{patch_id}/orgs/{org_id}:retry` - -**Parameters** - -| Name | In | Type | Required | Description | -| ---- | -- | ---- | -------- | ----------- | -| `blueprint_id` | path | string | Yes | | -| `patch_id` | path | string | Yes | | -| `org_id` | path | string | Yes | | - -**Request Body** - -**Sample Call** - -```bash -epilot blueprint-manifest retryPatchOrg \ - -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 \ - -p patch_id=123e4567-e89b-12d3-a456-426614174000 \ - -p org_id=123e4567-e89b-12d3-a456-426614174000 \ - -d '{"org_name":"string","dest_blueprint_id":"string","destination_auth_token":"string"}' -``` - -Using positional args for path parameters: - -```bash -epilot blueprint-manifest retryPatchOrg c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 123e4567-e89b-12d3-a456-426614174000 123e4567-e89b-12d3-a456-426614174000 -``` - -Using stdin pipe: - -```bash -cat body.json | epilot blueprint-manifest retryPatchOrg -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 -p patch_id=123e4567-e89b-12d3-a456-426614174000 -p org_id=123e4567-e89b-12d3-a456-426614174000 -``` - -With JSONata filter: - -```bash -epilot blueprint-manifest retryPatchOrg -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 -p patch_id=123e4567-e89b-12d3-a456-426614174000 -p org_id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'patch_id' -``` - -
-Sample Response - -```json -{ - "patch_id": "string", - "version": 0, - "org_id": "string", - "org_name": "string", - "dest_blueprint_id": "string", - "status": "pending", - "error": "string", - "applied_at": "1970-01-01T00:00:00.000Z", - "retries": 0, - "changes_applied": [ - { - "path": "string", - "op": "changed", - "baseline_value": {}, - "current_value": {} - } - ] -} -``` - -
- ---- - -### `exportBlueprint` - -Kick off a new blueprint export job. Returns 202 Accepted with Location header pointing to the job resource. - -`POST /v2/blueprint-manifest/blueprints/{blueprint_id}:export` - -**Parameters** - -| Name | In | Type | Required | Description | -| ---- | -- | ---- | -------- | ----------- | -| `blueprint_id` | path | string | Yes | | - -**Request Body** - -**Sample Call** - -```bash -epilot blueprint-manifest exportBlueprint \ - -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 \ - -d '{"destination_org_id":"string","destination_blueprint_id":"string","validate":true}' -``` - -Using positional args for path parameters: - -```bash -epilot blueprint-manifest exportBlueprint c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 -``` - -Using stdin pipe: - -```bash -cat body.json | epilot blueprint-manifest exportBlueprint -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 -``` - -With JSONata filter: - -```bash -epilot blueprint-manifest exportBlueprint -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 --jsonata '$' -``` - ---- - -### `listMarketplaceSlugs` - -List all available marketplace blueprint slugs from Webflow CMS. - -`GET /v2/blueprint-manifest/marketplace/slugs` - -**Sample Call** - -```bash -epilot blueprint-manifest listMarketplaceSlugs -``` - -With JSONata filter: - -```bash -epilot blueprint-manifest listMarketplaceSlugs --jsonata 'results[0]' -``` - -
-Sample Response - -```json -{ - "results": [ + "results": [ { "slug": "wallbox_b2c", "marketplace_slug": "wallbox-b2c", @@ -2082,7 +1728,34 @@ epilot blueprint-manifest addBlueprintResource -p blueprint_id=c2d6cac8-bdd5-4ea "impact_on_install": ["create"], "impact_on_install_reason": ["string"] } - ] + ], + "skipped": [ + { + "id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", + "type": "designbuilder", + "reason": "not_found" + } + ], + "errors": [ + { + "error": "string", + "code": "dependency_extraction", + "data": { + "formattedResource": { + "id": "string", + "name": "string", + "type": "string" + }, + "resource": "string", + "resourceDependency": "string", + "resources": ["string"], + "addresses": ["string"], + "originalError": "string" + } + } + ], + "total_errors": 0, + "errors_truncated": true } ``` @@ -2101,6 +1774,10 @@ Sync dependencies of all root resources in a Blueprint | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | | `blueprint_id` | path | string | Yes | | +| `trigger` | query | "manual" \| "pre_sync" \| "post_revert" | No | What initiated the sync. Automated triggers (`pre_sync`, +`post_revert`) are side effects of an operation the activity +feed already shows on its own row, so they are excluded from +the audit log — same | **Sample Call** @@ -2208,7 +1885,34 @@ epilot blueprint-manifest bulkAddBlueprintResources -p blueprint_id=c2d6cac8-bdd "impact_on_install": ["create"], "impact_on_install_reason": ["string"] } - ] + ], + "skipped": [ + { + "id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", + "type": "designbuilder", + "reason": "not_found" + } + ], + "errors": [ + { + "error": "string", + "code": "dependency_extraction", + "data": { + "formattedResource": { + "id": "string", + "name": "string", + "type": "string" + }, + "resource": "string", + "resourceDependency": "string", + "resources": ["string"], + "addresses": ["string"], + "originalError": "string" + } + } + ], + "total_errors": 0, + "errors_truncated": true } ``` @@ -2614,7 +2318,7 @@ epilot blueprint-manifest listBlueprintJobs --jsonata 'results[0]' ### `getBlueprintJob` -Poll current state of a job. +Poll the current state of a job. Serves both Terraform (v2) and V3-engine jobs — `GET /v2/blueprint-manifest/jobs/{job_id}` @@ -2702,7 +2406,7 @@ epilot blueprint-manifest getBlueprintJob -p job_id=c2d6cac8-bdd5-4ea2-8a6c-1cbd ### `continueInstallationJob` -Continue an installation job if it is waiting for user action. +Resume an installation job that is paused at `status: "WAITING_USER_ACTION"` after `POST /v2/blueprint-manifest/jobs/{job_id}:continue` @@ -2719,7 +2423,7 @@ Continue an installation job if it is waiting for user action. ```bash epilot blueprint-manifest continueInstallationJob \ -p job_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 \ - -d '{"resources_to_ignore":["string"]}' + -d '{"resources_to_ignore":["string"],"sync_notes":false,"source_auth_token":"string"}' ``` Using positional args for path parameters: @@ -2800,9 +2504,15 @@ epilot blueprint-manifest continueInstallationJob -p job_id=c2d6cac8-bdd5-4ea2-8 "name": "string", "status": "pending", "target_id": "string", - "error_message": "string" + "error_message": "string", + "error_code": "string", + "error_data": {} } ], + "options": { + "resources_to_ignore": ["string"], + "sync_notes": false + }, "status": "IN_PROGRESS" } ``` @@ -2811,6 +2521,39 @@ epilot blueprint-manifest continueInstallationJob -p job_id=c2d6cac8-bdd5-4ea2-8 --- +### `retryInstallationJob` + +Retry a finished V3 installation job whose status is `FAILED` or + +`POST /v2/blueprint-manifest/jobs/{job_id}:retry` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `job_id` | path | string | Yes | | + +**Sample Call** + +```bash +epilot blueprint-manifest retryInstallationJob \ + -p job_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 +``` + +Using positional args for path parameters: + +```bash +epilot blueprint-manifest retryInstallationJob c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 +``` + +With JSONata filter: + +```bash +epilot blueprint-manifest retryInstallationJob -p job_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 --jsonata '$' +``` + +--- + ### `cancelBlueprintJob` Cancel a blueprint job if it is still running. @@ -3679,13 +3422,137 @@ epilot blueprint-manifest publishMarketplaceListingVersion -p listing_id=123e456 --- -### `installBlueprintV3` +### `publishBlueprintV3` -Install a blueprint using the V3 engine (direct API calls, no Terraform). +Starts an asynchronous V3 publication. The result is a signed, portable package; poll the existing blueprint job endpoin -`POST /v3/blueprint-manifest/blueprint:install` +`POST /v3/blueprint-manifest/blueprints/{blueprint_id}:publish` -**Request Body** (required) +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `blueprint_id` | path | string | Yes | | + +**Request Body** + +**Sample Call** + +```bash +epilot blueprint-manifest publishBlueprintV3 \ + -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 \ + -d '{"publish_to_marketplace":false}' +``` + +Using positional args for path parameters: + +```bash +epilot blueprint-manifest publishBlueprintV3 c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 +``` + +Using stdin pipe: + +```bash +cat body.json | epilot blueprint-manifest publishBlueprintV3 -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 +``` + +With JSONata filter: + +```bash +epilot blueprint-manifest publishBlueprintV3 -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 --jsonata '$' +``` + +--- + +### `preInstallBlueprintV3` + +Validates a signed V3 package and returns the destination-specific resource plan used by the install UI. + +`POST /v3/blueprint-manifest/blueprints:pre-install` + +**Request Body** (required) + +**Sample Call** + +```bash +epilot blueprint-manifest preInstallBlueprintV3 \ + -d '{"blueprint_file":"string","source_blueprint_type":"marketplace","slug":"string"}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot blueprint-manifest preInstallBlueprintV3 +``` + +With JSONata filter: + +```bash +epilot blueprint-manifest preInstallBlueprintV3 --jsonata 'id' +``` + +
+Sample Response + +```json +{ + "id": "string", + "org_id": "string", + "title": "string", + "description": { + "preinstall": "string" + }, + "version": "string", + "slug": "string", + "source_type": "marketplace", + "sync_engine": "terraform", + "blueprint_file_s3_key": "string", + "is_verified": true, + "docs_url": "string", + "recommended_apps": ["string"], + "required_features": { + "enabled": ["string"], + "disabled": ["string"] + }, + "created_at": "1970-01-01T00:00:00.000Z", + "created_by": { + "name": "manifest@epilot.cloud", + "org_id": "911690", + "user_id": "11001045", + "token_id": "api_5ZugdRXasLfWBypHi93Fk" + }, + "is_updating": true, + "resources": [ + { + "id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", + "name": "string", + "type": "designbuilder", + "address": "string", + "is_root": true, + "is_ready": true, + "is_hidden": true, + "is_disabled": false, + "hard_dependencies": ["designbuilder"], + "parent_resource_ids": ["c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341"], + "depends_on_addresses": ["string"], + "impact_on_install": ["create"], + "impact_on_install_reason": ["string"] + } + ] +} +``` + +
+ +--- + +### `installBlueprintV3` + +Install a blueprint into a single destination org using the V3 engine (direct API + +`POST /v3/blueprint-manifest/blueprint:install` + +**Request Body** (required) **Sample Call** @@ -3792,9 +3659,176 @@ epilot blueprint-manifest getRestorePreview -p blueprint_id=c2d6cac8-bdd5-4ea2-8 "reason": "modified", "last_synced_at": "1970-01-01T00:00:00.000Z", "current_updated_at": "1970-01-01T00:00:00.000Z", - "error_message": "string" + "error_message": "string", + "is_hidden": true, + "co_owned_by": [ + { + "blueprint_id": "string", + "title": "string" + } + ], + "protected_by": [ + { + "lineage_id": "string", + "type": "string", + "target_id": "string" + } + ] } - ] + ], + "has_effective_changes": true +} +``` + +
+ +--- + +### `triggerDeploymentHealthCheckV3` + +Starts a read-only health scan of the resources this deployment's + +`POST /v3/blueprint-manifest/blueprints/{blueprint_id}/deployments/{job_id}:health-check` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `blueprint_id` | path | string | Yes | | +| `job_id` | path | string | Yes | The install job whose deployment is being checked. | + +**Request Body** + +**Sample Call** + +```bash +epilot blueprint-manifest triggerDeploymentHealthCheckV3 \ + -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 \ + -p job_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 \ + -d '{"source_org_id":"string","source_auth_token":"string"}' +``` + +Using positional args for path parameters: + +```bash +epilot blueprint-manifest triggerDeploymentHealthCheckV3 c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 +``` + +Using stdin pipe: + +```bash +cat body.json | epilot blueprint-manifest triggerDeploymentHealthCheckV3 -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 -p job_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 +``` + +With JSONata filter: + +```bash +epilot blueprint-manifest triggerDeploymentHealthCheckV3 -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 -p job_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 --jsonata '$' +``` + +--- + +### `getDeploymentHealthReportV3` + +Returns the most recent health report produced for this deployment + +`GET /v3/blueprint-manifest/blueprints/{blueprint_id}/deployments/{job_id}/health-report` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `blueprint_id` | path | string | Yes | | +| `job_id` | path | string | Yes | | + +**Sample Call** + +```bash +epilot blueprint-manifest getDeploymentHealthReportV3 \ + -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 \ + -p job_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 +``` + +Using positional args for path parameters: + +```bash +epilot blueprint-manifest getDeploymentHealthReportV3 c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 +``` + +With JSONata filter: + +```bash +epilot blueprint-manifest getDeploymentHealthReportV3 -p blueprint_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 -p job_id=c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341 --jsonata 'status' +``` + +
+Sample Response + +```json +{ + "status": "running", + "job_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", + "blueprint_instance_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", + "destination_org_id": "string", + "generated_at": "1970-01-01T00:00:00.000Z", + "html_url": "string", + "summary": { + "resources_scanned": 0, + "unchecked": 0, + "errors": 0, + "warnings": 0, + "infos": 0 + }, + "findings": [ + { + "check_id": "live_readability", + "code": "missing_in_destination", + "severity": "error", + "resource_type": "string", + "lineage_id": "string", + "target_id": "string", + "resource_name": "string", + "message": "string", + "verdict": "string", + "evidence": { + "path": "string", + "referenced_id": "string", + "referenced_type": "string", + "referenced_name": "string", + "referenced_lineage_id": "string", + "expected_target_id": "string", + "reference_kind": "string", + "operation": "string", + "rejection_reason": "string", + "error_message": "string", + "response_status": 0 + } + } + ], + "coverage": { + "checks": [ + { + "check_id": "live_readability", + "status": "completed", + "scope": "tracked_resources", + "resources_considered": 0, + "details": ["string"] + } + ], + "resource_types": [ + { + "resource_type": "string", + "tracked": 0, + "readable_by_lineage": 0, + "missing_by_lineage": 0, + "read_errors": 0, + "unchecked": 0, + "specialized_checks": ["live_readability"], + "known_blind_spots": ["string"] + } + ] + }, + "error": "string" } ``` @@ -3856,6 +3890,306 @@ epilot blueprint-manifest getBlueprintLineageV3 -p blueprint_id=c2d6cac8-bdd5-4e --- +### `createBulkInstallV3` + +Install one source blueprint into many destination organizations in a single + +`POST /v3/blueprint-manifest/bulk-installs` + +**Request Body** (required) + +**Sample Call** + +```bash +epilot blueprint-manifest createBulkInstallV3 +``` + +With request body: + +```bash +epilot blueprint-manifest createBulkInstallV3 \ + -d '{ + "source_org_id": "string", + "source_blueprint_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", + "max_concurrency": 2, + "slug": "string", + "options": { + "resources_to_ignore": ["string"], + "sync_notes": false + }, + "targets": [ + { + "destination_org_id": "string", + "destination_blueprint_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", + "destination_auth_token": "string" + } + ] +}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot blueprint-manifest createBulkInstallV3 +``` + +With JSONata filter: + +```bash +epilot blueprint-manifest createBulkInstallV3 --jsonata '$' +``` + +--- + +### `getBulkInstallV3` + +Returns the bulk install parent with aggregate status and counts. Scoped by the + +`GET /v3/blueprint-manifest/bulk-installs/{bulk_job_id}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `bulk_job_id` | path | string | Yes | | + +**Sample Call** + +```bash +epilot blueprint-manifest getBulkInstallV3 \ + -p bulk_job_id=123e4567-e89b-12d3-a456-426614174000 +``` + +Using positional args for path parameters: + +```bash +epilot blueprint-manifest getBulkInstallV3 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot blueprint-manifest getBulkInstallV3 -p bulk_job_id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'bulk_job_id' +``` + +
+Sample Response + +```json +{ + "bulk_job_id": "string", + "source_org_id": "string", + "source_blueprint_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", + "status": "QUEUED", + "target_count": 0, + "max_concurrency": 0, + "counts": { + "queued": 0, + "in_progress": 0, + "success": 0, + "partial_success": 0, + "failed": 0 + }, + "slug": "string", + "options": { + "resources_to_ignore": ["string"], + "sync_notes": false + }, + "created_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z" +} +``` + +
+ +--- + +### `listBulkInstallTargetsV3` + +Pages through the bulk install's target rows. Each row hydrates its latest child + +`GET /v3/blueprint-manifest/bulk-installs/{bulk_job_id}/targets` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `bulk_job_id` | path | string | Yes | | +| `limit` | query | number | No | | +| `cursor` | query | string | No | | + +**Sample Call** + +```bash +epilot blueprint-manifest listBulkInstallTargetsV3 \ + -p bulk_job_id=123e4567-e89b-12d3-a456-426614174000 +``` + +Using positional args for path parameters: + +```bash +epilot blueprint-manifest listBulkInstallTargetsV3 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot blueprint-manifest listBulkInstallTargetsV3 -p bulk_job_id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'results[0]' +``` + +
+Sample Response + +```json +{ + "results": [ + { + "bulk_job_id": "string", + "destination_org_id": "string", + "destination_blueprint_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", + "status": "QUEUED", + "job_ids": ["c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341"], + "created_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z", + "job": {} + } + ], + "next_cursor": "string" +} +``` + +
+ +--- + +### `retryBulkInstallTargetV3` + +Retries a single failed target. Allowed only for `FAILED` and `PARTIAL_SUCCESS` + +`POST /v3/blueprint-manifest/bulk-installs/{bulk_job_id}/targets/{destination_org_id}:retry` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `bulk_job_id` | path | string | Yes | | +| `destination_org_id` | path | string | Yes | | + +**Request Body** (required) + +**Sample Call** + +```bash +epilot blueprint-manifest retryBulkInstallTargetV3 \ + -p bulk_job_id=123e4567-e89b-12d3-a456-426614174000 \ + -p destination_org_id=123e4567-e89b-12d3-a456-426614174000 \ + -d '{"destination_auth_token":"string"}' +``` + +Using positional args for path parameters: + +```bash +epilot blueprint-manifest retryBulkInstallTargetV3 123e4567-e89b-12d3-a456-426614174000 123e4567-e89b-12d3-a456-426614174000 +``` + +Using stdin pipe: + +```bash +cat body.json | epilot blueprint-manifest retryBulkInstallTargetV3 -p bulk_job_id=123e4567-e89b-12d3-a456-426614174000 -p destination_org_id=123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot blueprint-manifest retryBulkInstallTargetV3 -p bulk_job_id=123e4567-e89b-12d3-a456-426614174000 -p destination_org_id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'bulk_job_id' +``` + +
+Sample Response + +```json +{ + "bulk_job_id": "string", + "destination_org_id": "string", + "destination_blueprint_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", + "status": "QUEUED", + "job_ids": ["c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341"], + "created_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z", + "job": { + "id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", + "events": [ + { + "timestamp": "1970-01-01T00:00:00.000Z", + "message": "string", + "errors": [ + { + "error": "string", + "code": "dependency_extraction", + "data": { + "formattedResource": { + "id": "string", + "name": "string", + "type": "string" + }, + "resource": "string", + "resourceDependency": "string", + "resources": ["string"], + "addresses": ["string"], + "originalError": "string" + } + } + ], + "level": "info", + "data": { + "installed_blueprint_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", + "export_job_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", + "resources": 0 + } + } + ], + "triggered_at": "1970-01-01T00:00:00.000Z", + "created_by": { + "name": "manifest@epilot.cloud", + "org_id": "911690", + "user_id": "11001045", + "token_id": "api_5ZugdRXasLfWBypHi93Fk" + }, + "job_type": "install", + "source_blueprint_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", + "source_blueprint_type": "custom", + "source_org_id": "string", + "source_blueprint_file": "string", + "destination_blueprint_id": "c2d6cac8-bdd5-4ea2-8a6c-1cbdbe77b341", + "destination_org_id": "string", + "slug": "string", + "sync_engine": "terraform", + "resource_progress": [ + { + "lineage_id": "string", + "type": "string", + "address": "string", + "name": "string", + "status": "pending", + "target_id": "string", + "error_message": "string", + "error_code": "string", + "error_data": {} + } + ], + "options": { + "resources_to_ignore": ["string"], + "sync_notes": false + }, + "status": "IN_PROGRESS" + } +} +``` + +
+ +--- + ### `listUniquenessCriteria` List all custom uniqueness criteria configured for the caller's organization. @@ -3884,10 +4218,13 @@ epilot blueprint-manifest listUniquenessCriteria --jsonata 'results[0]' "org_id": "string", "resource_type": "emailtemplate", "fields": ["string"], + "propagated_to": ["string"], "updated_at": "1970-01-01T00:00:00.000Z", "updated_by": "string" } - ] + ], + "defaults": {}, + "readonly_types": ["string"] } ``` @@ -3905,7 +4242,7 @@ Get the configured uniqueness criteria for a specific resource type, if any. | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `resource_type` | path | "emailtemplate" \| "product" \| "price" \| "tax" \| "coupon" \| "product_recommendation" \| "file" \| "document_template" \| "schema" \| "taxonomy" \| "notification_template" \| "family" \| "permission" \| "journey" | Yes | | +| `resource_type` | path | "emailtemplate" \| "product" \| "price" \| "tax" \| "coupon" \| "product_recommendation" \| "file" \| "document_template" \| "notification_template" \| "journey" | Yes | | **Sample Call** @@ -3934,6 +4271,7 @@ epilot blueprint-manifest getUniquenessCriteria -p resource_type=example --jsona "org_id": "string", "resource_type": "emailtemplate", "fields": ["string"], + "propagated_to": ["string"], "updated_at": "1970-01-01T00:00:00.000Z", "updated_by": "string" } @@ -3953,7 +4291,7 @@ Set or replace the uniqueness criteria for a resource type. The provided fields | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `resource_type` | path | "emailtemplate" \| "product" \| "price" \| "tax" \| "coupon" \| "product_recommendation" \| "file" \| "document_template" \| "schema" \| "taxonomy" \| "notification_template" \| "family" \| "permission" \| "journey" | Yes | | +| `resource_type` | path | "emailtemplate" \| "product" \| "price" \| "tax" \| "coupon" \| "product_recommendation" \| "file" \| "document_template" \| "notification_template" \| "journey" | Yes | | **Request Body** (required) @@ -3962,7 +4300,7 @@ Set or replace the uniqueness criteria for a resource type. The provided fields ```bash epilot blueprint-manifest putUniquenessCriteria \ -p resource_type=example \ - -d '{"fields":["string"]}' + -d '{"fields":["string"],"propagated_to":["string"]}' ``` Using positional args for path parameters: @@ -3991,6 +4329,7 @@ epilot blueprint-manifest putUniquenessCriteria -p resource_type=example --jsona "org_id": "string", "resource_type": "emailtemplate", "fields": ["string"], + "propagated_to": ["string"], "updated_at": "1970-01-01T00:00:00.000Z", "updated_by": "string" } @@ -4010,7 +4349,7 @@ Remove the custom criteria for a resource type, reverting to the default fields. | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `resource_type` | path | "emailtemplate" \| "product" \| "price" \| "tax" \| "coupon" \| "product_recommendation" \| "file" \| "document_template" \| "schema" \| "taxonomy" \| "notification_template" \| "family" \| "permission" \| "journey" | Yes | | +| `resource_type` | path | "emailtemplate" \| "product" \| "price" \| "tax" \| "coupon" \| "product_recommendation" \| "file" \| "document_template" \| "notification_template" \| "journey" | Yes | | **Sample Call** diff --git a/packages/cli/docs/calendar.md b/packages/cli/docs/calendar.md index 78505e91e..9309c88c6 100644 --- a/packages/cli/docs/calendar.md +++ b/packages/cli/docs/calendar.md @@ -12,7 +12,7 @@ epilot's calendar API. epilot calendar # Call an operation -epilot calendar listCalendars +epilot calendar listUsersAbsence -p from=example -p to=example ``` ## Common Flags @@ -35,37 +35,72 @@ epilot calendar listCalendars ## Operations +**Absence** +- [`listUsersAbsence`](#listusersabsence) — List organization users with known absence metadata in the requested time window. +- [`searchAbsence`](#searchabsence) — Search known absence for candidate users in the requested time window. +- [`searchNowAbsence`](#searchnowabsence) — Search known absence for candidate users at the current server time. +- [`listAbsenceAdjustments`](#listabsenceadjustments) — List absence adjustments for a user in a time window. +- [`createAbsenceAdjustment`](#createabsenceadjustment) — Create a time-bound absence adjustment for a user in the caller organization. +- [`getAbsenceAdjustment`](#getabsenceadjustment) — Get an absence adjustment by ID. +- [`patchAbsenceAdjustment`](#patchabsenceadjustment) — Update an absence adjustment in the caller organization. +- [`deleteAbsenceAdjustment`](#deleteabsenceadjustment) — Delete an absence adjustment. +- [`getUserAbsence`](#getuserabsence) — Get known absence for a user in a time window. absent=false means no known absence, not guaranteed availability. + +**Working Hours** +- [`getWorkingHours`](#getworkinghours) — Get the recurring weekly working hours of a user. 404 means no record exists and the user is treated as always available +- [`putWorkingHours`](#putworkinghours) — Create or fully replace the working hours of a user in the caller organization. This is a full replace, not a merge. +- [`deleteWorkingHours`](#deleteworkinghours) — Delete the working hours of a user. The user is then treated as always available again. + **Calendars** - [`listCalendars`](#listcalendars) — List calendars visible to the caller. +- [`createCalendar`](#createcalendar) — Create a native epilot calendar. +- [`addOutlookCalendar`](#addoutlookcalendar) — Registers one of the caller's Outlook calendars as an epilot calendar. +- [`listOutlookCalendars`](#listoutlookcalendars) — Lists the calling user's Outlook calendars available to import as epilot calendars. +- [`deleteOutlookCalendar`](#deleteoutlookcalendar) — Disconnects a previously registered Outlook calendar. +- [`outlookWebhook`](#outlookwebhook) — Public Microsoft Graph webhook receiver for per-user Outlook calendar - [`getCalendar`](#getcalendar) — Get a single calendar by its epilot ID. +- [`updateCalendar`](#updatecalendar) — Update local calendar details. Changes to synced calendars do not modify the provider calendar. +- [`deleteCalendar`](#deletecalendar) — Delete a native epilot calendar or disconnect a synced calendar, including its locally stored events. -**Events** +**Calendar Events** - [`listEvents`](#listevents) — List events in a time window. Recurring events are returned as expanded occurrences – each instance appears as its own e +- [`createEvent`](#createevent) — Create a native epilot calendar event. Omit `calendar_id` to use the caller’s epilot default calendar. - [`getEvent`](#getevent) — Get a single event by its epilot ID. +- [`updateEvent`](#updateevent) — Update a native epilot calendar event. +- [`deleteEvent`](#deleteevent) — Delete a native epilot calendar event. +- [`shareEvent`](#shareevent) — Share a calendar event with another user of the same organization, view-only. Owner-only: recipients of a share cannot r +- [`unshareEvent`](#unshareevent) — Revoke a per-event share. The recipient loses access immediately. Owner-only. -### `listCalendars` +### `listUsersAbsence` -List calendars visible to the caller. +List organization users with known absence metadata in the requested time window. -`GET /v1/calendar` +`GET /v1/calendar/absence/users` **Parameters** | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `size` | query | number | No | | -| `cursor` | query | string | No | Opaque cursor from a previous response | +| `from` | query | string (date-time) | Yes | Start of the time window (inclusive). Maximum window: 31 days. | +| `to` | query | string (date-time) | Yes | End of the time window (exclusive). Must be after from. | +| `include_busy` | query | boolean | No | Include busy inputs in addition to out-of-office absence. Defaults to false. | +| `working_hours_granularity` | query | "time" \| "day" | No | At what granularity working hours contribute to absence. "time" (default): all time outside a user's working windows counts as absent. "day": only full days with no working windows count as absent (wh | +| `query` | query | string | No | Optional user directory search query. | +| `limit` | query | number | No | Maximum users to return. | +| `offset` | query | number | No | User directory offset. | **Sample Call** ```bash -epilot calendar listCalendars +epilot calendar listUsersAbsence \ + -p from=example \ + -p to=example ``` With JSONata filter: ```bash -epilot calendar listCalendars --jsonata 'results[0]' +epilot calendar listUsersAbsence -p from=example -p to=example --jsonata 'results[0]' ```
@@ -73,29 +108,31 @@ epilot calendar listCalendars --jsonata 'results[0]' ```json { + "from": "1970-01-01T00:00:00.000Z", + "to": "1970-01-01T00:00:00.000Z", + "hits": 0, "results": [ { - "_id": "string", - "_schema": "calendar", - "_org": "string", - "_title": "string", - "name": "string", - "description": "string", - "color": "string", - "is_default": true, - "read_only": true, - "owner_email": "user@example.com", - "source": { - "type": "native", - "provider": "outlook", - "provider_calendar_id": "string", - "last_synced_at": "1970-01-01T00:00:00.000Z" - }, - "_created_at": "1970-01-01T00:00:00.000Z", - "_updated_at": "1970-01-01T00:00:00.000Z" + "absent": true, + "absence_intervals": [ + { + "from": "1970-01-01T00:00:00.000Z", + "to": "1970-01-01T00:00:00.000Z", + "original_from": "1970-01-01T00:00:00.000Z", + "original_to": "1970-01-01T00:00:00.000Z", + "absent": true, + "source": "calendar_event", + "calendar_event_id": "string", + "absence_adjustment_id": "string", + "reason": "string" + } + ], + "user_id": "string", + "display_name": "string", + "email": "user@example.com", + "status": "string" } - ], - "next_cursor": "string" + ] } ``` @@ -103,35 +140,85 @@ epilot calendar listCalendars --jsonata 'results[0]' --- -### `getCalendar` +### `searchAbsence` -Get a single calendar by its epilot ID. +Search known absence for candidate users in the requested time window. -`GET /v1/calendar/{calendar_id}` +`POST /v1/calendar/absence:search` -**Parameters** +**Request Body** (required) -| Name | In | Type | Required | Description | -| ---- | -- | ---- | -------- | ----------- | -| `calendar_id` | path | string | Yes | | +**Sample Call** + +```bash +epilot calendar searchAbsence +``` + +With request body: + +```bash +epilot calendar searchAbsence \ + -d '{ + "from": "1970-01-01T00:00:00.000Z", + "to": "1970-01-01T00:00:00.000Z", + "user_ids": ["string"], + "include_busy": false, + "working_hours_granularity": "time" +}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot calendar searchAbsence +``` + +With JSONata filter: + +```bash +epilot calendar searchAbsence --jsonata 'from' +``` + +
+Sample Response + +```json +{ + "from": "1970-01-01T00:00:00.000Z", + "to": "1970-01-01T00:00:00.000Z", + "users": {} +} +``` + +
+ +--- + +### `searchNowAbsence` + +Search known absence for candidate users at the current server time. + +`POST /v1/calendar/absence:search-now` + +**Request Body** (required) **Sample Call** ```bash -epilot calendar getCalendar \ - -p calendar_id=123e4567-e89b-12d3-a456-426614174000 +epilot calendar searchNowAbsence \ + -d '{"user_ids":["string"],"include_busy":false,"working_hours_granularity":"time"}' ``` -Using positional args for path parameters: +Using stdin pipe: ```bash -epilot calendar getCalendar 123e4567-e89b-12d3-a456-426614174000 +cat body.json | epilot calendar searchNowAbsence ``` With JSONata filter: ```bash -epilot calendar getCalendar -p calendar_id=123e4567-e89b-12d3-a456-426614174000 --jsonata '_id' +epilot calendar searchNowAbsence --jsonata 'from' ```
@@ -139,24 +226,9 @@ epilot calendar getCalendar -p calendar_id=123e4567-e89b-12d3-a456-426614174000 ```json { - "_id": "string", - "_schema": "calendar", - "_org": "string", - "_title": "string", - "name": "string", - "description": "string", - "color": "string", - "is_default": true, - "read_only": true, - "owner_email": "user@example.com", - "source": { - "type": "native", - "provider": "outlook", - "provider_calendar_id": "string", - "last_synced_at": "1970-01-01T00:00:00.000Z" - }, - "_created_at": "1970-01-01T00:00:00.000Z", - "_updated_at": "1970-01-01T00:00:00.000Z" + "from": "1970-01-01T00:00:00.000Z", + "to": "1970-01-01T00:00:00.000Z", + "users": {} } ``` @@ -164,34 +236,39 @@ epilot calendar getCalendar -p calendar_id=123e4567-e89b-12d3-a456-426614174000 --- -### `listEvents` +### `listAbsenceAdjustments` -List events in a time window. Recurring events are returned as expanded occurrences – each instance appears as its own e +List absence adjustments for a user in a time window. -`GET /v1/calendar/events` +`GET /v1/calendar/absence/users/{user_id}/adjustments` **Parameters** | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `from` | query | string (date-time) | Yes | Start of the time window (inclusive) | -| `to` | query | string (date-time) | Yes | End of the time window (exclusive) | -| `calendar_id` | query | string | No | Filter to a single calendar. Omit to query across all accessible calendars. | -| `size` | query | number | No | | -| `cursor` | query | string | No | | +| `user_id` | path | string | Yes | | +| `from` | query | string (date-time) | Yes | Start of the time window (inclusive). Maximum window: 90 days. | +| `to` | query | string (date-time) | Yes | End of the time window (exclusive). Must be after from. | **Sample Call** ```bash -epilot calendar listEvents \ +epilot calendar listAbsenceAdjustments \ + -p user_id=123e4567-e89b-12d3-a456-426614174000 \ -p from=example \ -p to=example ``` +Using positional args for path parameters: + +```bash +epilot calendar listAbsenceAdjustments 123e4567-e89b-12d3-a456-426614174000 +``` + With JSONata filter: ```bash -epilot calendar listEvents -p from=example -p to=example --jsonata 'results[0]' +epilot calendar listAbsenceAdjustments -p user_id=123e4567-e89b-12d3-a456-426614174000 -p from=example -p to=example --jsonata 'results[0]' ```
@@ -199,54 +276,23 @@ epilot calendar listEvents -p from=example -p to=example --jsonata 'results[0]' ```json { + "from": "1970-01-01T00:00:00.000Z", + "to": "1970-01-01T00:00:00.000Z", "results": [ { - "_id": "string", - "_schema": "calendar_event", - "_org": "string", - "_title": "string", - "_tags": ["string"], - "calendar_id": "string", - "event_type": "singleInstance", - "description": "string", - "start_time": "1970-01-01T00:00:00.000Z", - "end_time": "1970-01-01T00:00:00.000Z", - "timezone": "string", - "is_all_day": true, - "location": "string", - "status": "free", - "busy": true, - "is_cancelled": true, - "sensitivity": "normal", - "importance": "low", - "is_online_meeting": true, - "online_meeting_url": "https://example.com/path", - "web_link": "https://example.com/path", - "response_status": "none", - "organizer_email": "user@example.com", - "attendees": [ - { - "email": "user@example.com", - "name": "string", - "response": "none", - "type": "required" - } - ], - "is_recurring": true, - "series_master_id": "string", - "source": { - "type": "native", - "provider": "outlook", - "provider_event_id": "string", - "provider_event_url": "https://example.com/path", - "last_synced_at": "1970-01-01T00:00:00.000Z", - "etag": "string" - }, - "_created_at": "1970-01-01T00:00:00.000Z", - "_updated_at": "1970-01-01T00:00:00.000Z" + "from": "1970-01-01T00:00:00.000Z", + "to": "1970-01-01T00:00:00.000Z", + "absent": true, + "status": "oof", + "type": "string", + "reason": "string", + "adjustment_id": "string", + "user_id": "string", + "created_by": "string", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z" } - ], - "next_cursor": "string" + ] } ``` @@ -254,35 +300,58 @@ epilot calendar listEvents -p from=example -p to=example --jsonata 'results[0]' --- -### `getEvent` +### `createAbsenceAdjustment` -Get a single event by its epilot ID. +Create a time-bound absence adjustment for a user in the caller organization. -`GET /v1/calendar/events/{event_id}` +`POST /v1/calendar/absence/users/{user_id}/adjustments` **Parameters** | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `event_id` | path | string | Yes | | +| `user_id` | path | string | Yes | | + +**Request Body** (required) **Sample Call** ```bash -epilot calendar getEvent \ - -p event_id=123e4567-e89b-12d3-a456-426614174000 +epilot calendar createAbsenceAdjustment \ + -p user_id=123e4567-e89b-12d3-a456-426614174000 +``` + +With request body: + +```bash +epilot calendar createAbsenceAdjustment \ + -p user_id=123e4567-e89b-12d3-a456-426614174000 \ + -d '{ + "from": "1970-01-01T00:00:00.000Z", + "to": "1970-01-01T00:00:00.000Z", + "absent": true, + "status": "oof", + "type": "string", + "reason": "string" +}' ``` Using positional args for path parameters: ```bash -epilot calendar getEvent 123e4567-e89b-12d3-a456-426614174000 +epilot calendar createAbsenceAdjustment 123e4567-e89b-12d3-a456-426614174000 +``` + +Using stdin pipe: + +```bash +cat body.json | epilot calendar createAbsenceAdjustment -p user_id=123e4567-e89b-12d3-a456-426614174000 ``` With JSONata filter: ```bash -epilot calendar getEvent -p event_id=123e4567-e89b-12d3-a456-426614174000 --jsonata '_id' +epilot calendar createAbsenceAdjustment -p user_id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'from' ```
@@ -290,52 +359,1485 @@ epilot calendar getEvent -p event_id=123e4567-e89b-12d3-a456-426614174000 --json ```json { - "_id": "string", - "_schema": "calendar_event", - "_org": "string", - "_title": "string", - "_tags": ["string"], - "calendar_id": "string", - "event_type": "singleInstance", - "description": "string", - "start_time": "1970-01-01T00:00:00.000Z", - "end_time": "1970-01-01T00:00:00.000Z", - "timezone": "string", - "is_all_day": true, - "location": "string", - "status": "free", - "busy": true, - "is_cancelled": true, - "sensitivity": "normal", - "importance": "low", - "is_online_meeting": true, - "online_meeting_url": "https://example.com/path", - "web_link": "https://example.com/path", - "response_status": "none", - "organizer_email": "user@example.com", - "attendees": [ - { - "email": "user@example.com", - "name": "string", - "response": "none", - "type": "required" - } - ], - "is_recurring": true, - "series_master_id": "string", - "source": { - "type": "native", - "provider": "outlook", - "provider_event_id": "string", - "provider_event_url": "https://example.com/path", - "last_synced_at": "1970-01-01T00:00:00.000Z", - "etag": "string" - }, - "_created_at": "1970-01-01T00:00:00.000Z", - "_updated_at": "1970-01-01T00:00:00.000Z" + "from": "1970-01-01T00:00:00.000Z", + "to": "1970-01-01T00:00:00.000Z", + "absent": true, + "status": "oof", + "type": "string", + "reason": "string", + "adjustment_id": "string", + "user_id": "string", + "created_by": "string", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z" +} +``` + +
+ +--- + +### `getAbsenceAdjustment` + +Get an absence adjustment by ID. + +`GET /v1/calendar/absence/users/{user_id}/adjustments/{adjustment_id}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `user_id` | path | string | Yes | | +| `adjustment_id` | path | string | Yes | | + +**Sample Call** + +```bash +epilot calendar getAbsenceAdjustment \ + -p user_id=123e4567-e89b-12d3-a456-426614174000 \ + -p adjustment_id=123e4567-e89b-12d3-a456-426614174000 +``` + +Using positional args for path parameters: + +```bash +epilot calendar getAbsenceAdjustment 123e4567-e89b-12d3-a456-426614174000 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot calendar getAbsenceAdjustment -p user_id=123e4567-e89b-12d3-a456-426614174000 -p adjustment_id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'from' +``` + +
+Sample Response + +```json +{ + "from": "1970-01-01T00:00:00.000Z", + "to": "1970-01-01T00:00:00.000Z", + "absent": true, + "status": "oof", + "type": "string", + "reason": "string", + "adjustment_id": "string", + "user_id": "string", + "created_by": "string", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z" } ```
--- + +### `patchAbsenceAdjustment` + +Update an absence adjustment in the caller organization. + +`PATCH /v1/calendar/absence/users/{user_id}/adjustments/{adjustment_id}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `user_id` | path | string | Yes | | +| `adjustment_id` | path | string | Yes | | + +**Request Body** (required) + +**Sample Call** + +```bash +epilot calendar patchAbsenceAdjustment \ + -p user_id=123e4567-e89b-12d3-a456-426614174000 \ + -p adjustment_id=123e4567-e89b-12d3-a456-426614174000 +``` + +With request body: + +```bash +epilot calendar patchAbsenceAdjustment \ + -p user_id=123e4567-e89b-12d3-a456-426614174000 \ + -p adjustment_id=123e4567-e89b-12d3-a456-426614174000 \ + -d '{ + "from": "1970-01-01T00:00:00.000Z", + "to": "1970-01-01T00:00:00.000Z", + "absent": true, + "status": "oof", + "type": "string", + "reason": "string" +}' +``` + +Using positional args for path parameters: + +```bash +epilot calendar patchAbsenceAdjustment 123e4567-e89b-12d3-a456-426614174000 123e4567-e89b-12d3-a456-426614174000 +``` + +Using stdin pipe: + +```bash +cat body.json | epilot calendar patchAbsenceAdjustment -p user_id=123e4567-e89b-12d3-a456-426614174000 -p adjustment_id=123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot calendar patchAbsenceAdjustment -p user_id=123e4567-e89b-12d3-a456-426614174000 -p adjustment_id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'from' +``` + +
+Sample Response + +```json +{ + "from": "1970-01-01T00:00:00.000Z", + "to": "1970-01-01T00:00:00.000Z", + "absent": true, + "status": "oof", + "type": "string", + "reason": "string", + "adjustment_id": "string", + "user_id": "string", + "created_by": "string", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z" +} +``` + +
+ +--- + +### `deleteAbsenceAdjustment` + +Delete an absence adjustment. + +`DELETE /v1/calendar/absence/users/{user_id}/adjustments/{adjustment_id}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `user_id` | path | string | Yes | | +| `adjustment_id` | path | string | Yes | | + +**Sample Call** + +```bash +epilot calendar deleteAbsenceAdjustment \ + -p user_id=123e4567-e89b-12d3-a456-426614174000 \ + -p adjustment_id=123e4567-e89b-12d3-a456-426614174000 +``` + +Using positional args for path parameters: + +```bash +epilot calendar deleteAbsenceAdjustment 123e4567-e89b-12d3-a456-426614174000 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot calendar deleteAbsenceAdjustment -p user_id=123e4567-e89b-12d3-a456-426614174000 -p adjustment_id=123e4567-e89b-12d3-a456-426614174000 --jsonata '$' +``` + +--- + +### `getUserAbsence` + +Get known absence for a user in a time window. absent=false means no known absence, not guaranteed availability. + +`GET /v1/calendar/absence/users/{user_id}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `user_id` | path | string | Yes | | +| `from` | query | string (date-time) | Yes | Start of the time window (inclusive). Maximum window: 31 days. | +| `to` | query | string (date-time) | Yes | End of the time window (exclusive). Must be after from. | +| `include_busy` | query | boolean | No | Include busy inputs in addition to out-of-office absence. Defaults to false. | +| `working_hours_granularity` | query | "time" \| "day" | No | At what granularity working hours contribute to absence. "time" (default): all time outside a user's working windows counts as absent. "day": only full days with no working windows count as absent (wh | + +**Sample Call** + +```bash +epilot calendar getUserAbsence \ + -p user_id=123e4567-e89b-12d3-a456-426614174000 \ + -p from=example \ + -p to=example +``` + +Using positional args for path parameters: + +```bash +epilot calendar getUserAbsence 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot calendar getUserAbsence -p user_id=123e4567-e89b-12d3-a456-426614174000 -p from=example -p to=example --jsonata 'absent' +``` + +
+Sample Response + +```json +{ + "absent": true, + "absence_intervals": [ + { + "from": "1970-01-01T00:00:00.000Z", + "to": "1970-01-01T00:00:00.000Z", + "original_from": "1970-01-01T00:00:00.000Z", + "original_to": "1970-01-01T00:00:00.000Z", + "absent": true, + "source": "calendar_event", + "calendar_event_id": "string", + "absence_adjustment_id": "string", + "reason": "string" + } + ], + "from": "1970-01-01T00:00:00.000Z", + "to": "1970-01-01T00:00:00.000Z", + "user_id": "string", + "external_calendars": [ + { + "provider": "outlook", + "last_synced_at": "1970-01-01T00:00:00.000Z" + } + ] +} +``` + +
+ +--- + +### `getWorkingHours` + +Get the recurring weekly working hours of a user. 404 means no record exists and the user is treated as always available + +`GET /v1/calendar/working-hours/users/{user_id}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `user_id` | path | string | Yes | | + +**Sample Call** + +```bash +epilot calendar getWorkingHours \ + -p user_id=123e4567-e89b-12d3-a456-426614174000 +``` + +Using positional args for path parameters: + +```bash +epilot calendar getWorkingHours 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot calendar getWorkingHours -p user_id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'monday' +``` + +
+Sample Response + +```json +{ + "monday": [ + { + "start": "string", + "end": "string" + } + ], + "tuesday": [ + { + "start": "string", + "end": "string" + } + ], + "wednesday": [ + { + "start": "string", + "end": "string" + } + ], + "thursday": [ + { + "start": "string", + "end": "string" + } + ], + "friday": [ + { + "start": "string", + "end": "string" + } + ], + "saturday": [ + { + "start": "string", + "end": "string" + } + ], + "sunday": [ + { + "start": "string", + "end": "string" + } + ], + "timezone": "string", + "user_id": "string", + "updated_by": "string", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z" +} +``` + +
+ +--- + +### `putWorkingHours` + +Create or fully replace the working hours of a user in the caller organization. This is a full replace, not a merge. + +`PUT /v1/calendar/working-hours/users/{user_id}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `user_id` | path | string | Yes | | + +**Request Body** (required) + +**Sample Call** + +```bash +epilot calendar putWorkingHours \ + -p user_id=123e4567-e89b-12d3-a456-426614174000 +``` + +With request body: + +```bash +epilot calendar putWorkingHours \ + -p user_id=123e4567-e89b-12d3-a456-426614174000 \ + -d '{ + "monday": [ + { + "start": "string", + "end": "string" + } + ], + "tuesday": [ + { + "start": "string", + "end": "string" + } + ], + "wednesday": [ + { + "start": "string", + "end": "string" + } + ], + "thursday": [ + { + "start": "string", + "end": "string" + } + ], + "friday": [ + { + "start": "string", + "end": "string" + } + ], + "saturday": [ + { + "start": "string", + "end": "string" + } + ], + "sunday": [ + { + "start": "string", + "end": "string" + } + ], + "timezone": "Europe/Berlin" +}' +``` + +Using positional args for path parameters: + +```bash +epilot calendar putWorkingHours 123e4567-e89b-12d3-a456-426614174000 +``` + +Using stdin pipe: + +```bash +cat body.json | epilot calendar putWorkingHours -p user_id=123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot calendar putWorkingHours -p user_id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'monday' +``` + +
+Sample Response + +```json +{ + "monday": [ + { + "start": "string", + "end": "string" + } + ], + "tuesday": [ + { + "start": "string", + "end": "string" + } + ], + "wednesday": [ + { + "start": "string", + "end": "string" + } + ], + "thursday": [ + { + "start": "string", + "end": "string" + } + ], + "friday": [ + { + "start": "string", + "end": "string" + } + ], + "saturday": [ + { + "start": "string", + "end": "string" + } + ], + "sunday": [ + { + "start": "string", + "end": "string" + } + ], + "timezone": "string", + "user_id": "string", + "updated_by": "string", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z" +} +``` + +
+ +--- + +### `deleteWorkingHours` + +Delete the working hours of a user. The user is then treated as always available again. + +`DELETE /v1/calendar/working-hours/users/{user_id}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `user_id` | path | string | Yes | | + +**Sample Call** + +```bash +epilot calendar deleteWorkingHours \ + -p user_id=123e4567-e89b-12d3-a456-426614174000 +``` + +Using positional args for path parameters: + +```bash +epilot calendar deleteWorkingHours 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot calendar deleteWorkingHours -p user_id=123e4567-e89b-12d3-a456-426614174000 --jsonata '$' +``` + +--- + +### `listCalendars` + +List calendars visible to the caller. + +`GET /v1/calendar` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `size` | query | number | No | | +| `cursor` | query | string | No | Opaque cursor from a previous response | + +**Sample Call** + +```bash +epilot calendar listCalendars +``` + +With JSONata filter: + +```bash +epilot calendar listCalendars --jsonata 'results[0]' +``` + +
+Sample Response + +```json +{ + "results": [ + { + "_id": "string", + "_schema": "calendar", + "_org": "string", + "_title": "string", + "name": "string", + "description": "string", + "color": "string", + "is_default": true, + "is_epilot_default": true, + "read_only": true, + "owner_email": "user@example.com", + "source": { + "type": "native", + "provider": "outlook", + "provider_calendar_id": "string", + "last_synced_at": "1970-01-01T00:00:00.000Z" + }, + "_created_at": "1970-01-01T00:00:00.000Z", + "_updated_at": "1970-01-01T00:00:00.000Z" + } + ], + "next_cursor": "string" +} +``` + +
+ +--- + +### `createCalendar` + +Create a native epilot calendar. + +`POST /v1/calendar` + +**Request Body** (required) + +**Sample Call** + +```bash +epilot calendar createCalendar \ + -d '{"name":"string","description":"string","color":"string"}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot calendar createCalendar +``` + +With JSONata filter: + +```bash +epilot calendar createCalendar --jsonata '_id' +``` + +
+Sample Response + +```json +{ + "_id": "string", + "_schema": "calendar", + "_org": "string", + "_title": "string", + "name": "string", + "description": "string", + "color": "string", + "is_default": true, + "is_epilot_default": true, + "read_only": true, + "owner_email": "user@example.com", + "source": { + "type": "native", + "provider": "outlook", + "provider_calendar_id": "string", + "last_synced_at": "1970-01-01T00:00:00.000Z" + }, + "_created_at": "1970-01-01T00:00:00.000Z", + "_updated_at": "1970-01-01T00:00:00.000Z" +} +``` + +
+ +--- + +### `addOutlookCalendar` + +Registers one of the caller's Outlook calendars as an epilot calendar. + +`POST /v1/calendar/sources/outlook` + +**Request Body** (required) + +**Sample Call** + +```bash +epilot calendar addOutlookCalendar \ + -d '{"provider_calendar_id":"string","name":"string"}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot calendar addOutlookCalendar +``` + +With JSONata filter: + +```bash +epilot calendar addOutlookCalendar --jsonata '_id' +``` + +
+Sample Response + +```json +{ + "_id": "string", + "_schema": "calendar", + "_org": "string", + "_title": "string", + "name": "string", + "description": "string", + "color": "string", + "is_default": true, + "is_epilot_default": true, + "read_only": true, + "owner_email": "user@example.com", + "source": { + "type": "native", + "provider": "outlook", + "provider_calendar_id": "string", + "last_synced_at": "1970-01-01T00:00:00.000Z" + }, + "_created_at": "1970-01-01T00:00:00.000Z", + "_updated_at": "1970-01-01T00:00:00.000Z" +} +``` + +
+ +--- + +### `listOutlookCalendars` + +Lists the calling user's Outlook calendars available to import as epilot calendars. + +`GET /v1/calendar/sources/outlook/available` + +**Sample Call** + +```bash +epilot calendar listOutlookCalendars +``` + +With JSONata filter: + +```bash +epilot calendar listOutlookCalendars --jsonata 'calendars' +``` + +
+Sample Response + +```json +{ + "calendars": [ + { + "provider_calendar_id": "string", + "name": "string", + "color": "string", + "is_default": true, + "can_edit": true, + "owner": "string" + } + ] +} +``` + +
+ +--- + +### `deleteOutlookCalendar` + +Disconnects a previously registered Outlook calendar. + +`DELETE /v1/calendar/sources/outlook/{calendar_id}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `calendar_id` | path | string | Yes | | + +**Sample Call** + +```bash +epilot calendar deleteOutlookCalendar \ + -p calendar_id=123e4567-e89b-12d3-a456-426614174000 +``` + +Using positional args for path parameters: + +```bash +epilot calendar deleteOutlookCalendar 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot calendar deleteOutlookCalendar -p calendar_id=123e4567-e89b-12d3-a456-426614174000 --jsonata '$' +``` + +--- + +### `outlookWebhook` + +Public Microsoft Graph webhook receiver for per-user Outlook calendar + +`POST /v1/calendar/outlook/webhook` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `validationToken` | query | string | No | Set by Graph on the subscription-validation handshake; echoed back verbatim. | + +**Sample Call** + +```bash +epilot calendar outlookWebhook +``` + +With JSONata filter: + +```bash +epilot calendar outlookWebhook --jsonata '$' +``` + +--- + +### `getCalendar` + +Get a single calendar by its epilot ID. + +`GET /v1/calendar/{calendar_id}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `calendar_id` | path | string | Yes | | + +**Sample Call** + +```bash +epilot calendar getCalendar \ + -p calendar_id=123e4567-e89b-12d3-a456-426614174000 +``` + +Using positional args for path parameters: + +```bash +epilot calendar getCalendar 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot calendar getCalendar -p calendar_id=123e4567-e89b-12d3-a456-426614174000 --jsonata '_id' +``` + +
+Sample Response + +```json +{ + "_id": "string", + "_schema": "calendar", + "_org": "string", + "_title": "string", + "name": "string", + "description": "string", + "color": "string", + "is_default": true, + "is_epilot_default": true, + "read_only": true, + "owner_email": "user@example.com", + "source": { + "type": "native", + "provider": "outlook", + "provider_calendar_id": "string", + "last_synced_at": "1970-01-01T00:00:00.000Z" + }, + "_created_at": "1970-01-01T00:00:00.000Z", + "_updated_at": "1970-01-01T00:00:00.000Z" +} +``` + +
+ +--- + +### `updateCalendar` + +Update local calendar details. Changes to synced calendars do not modify the provider calendar. + +`PATCH /v1/calendar/{calendar_id}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `calendar_id` | path | string | Yes | | + +**Request Body** (required) + +**Sample Call** + +```bash +epilot calendar updateCalendar \ + -p calendar_id=123e4567-e89b-12d3-a456-426614174000 \ + -d '{"name":"string","description":"string","color":"string"}' +``` + +Using positional args for path parameters: + +```bash +epilot calendar updateCalendar 123e4567-e89b-12d3-a456-426614174000 +``` + +Using stdin pipe: + +```bash +cat body.json | epilot calendar updateCalendar -p calendar_id=123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot calendar updateCalendar -p calendar_id=123e4567-e89b-12d3-a456-426614174000 --jsonata '_id' +``` + +
+Sample Response + +```json +{ + "_id": "string", + "_schema": "calendar", + "_org": "string", + "_title": "string", + "name": "string", + "description": "string", + "color": "string", + "is_default": true, + "is_epilot_default": true, + "read_only": true, + "owner_email": "user@example.com", + "source": { + "type": "native", + "provider": "outlook", + "provider_calendar_id": "string", + "last_synced_at": "1970-01-01T00:00:00.000Z" + }, + "_created_at": "1970-01-01T00:00:00.000Z", + "_updated_at": "1970-01-01T00:00:00.000Z" +} +``` + +
+ +--- + +### `deleteCalendar` + +Delete a native epilot calendar or disconnect a synced calendar, including its locally stored events. + +`DELETE /v1/calendar/{calendar_id}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `calendar_id` | path | string | Yes | | + +**Sample Call** + +```bash +epilot calendar deleteCalendar \ + -p calendar_id=123e4567-e89b-12d3-a456-426614174000 +``` + +Using positional args for path parameters: + +```bash +epilot calendar deleteCalendar 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot calendar deleteCalendar -p calendar_id=123e4567-e89b-12d3-a456-426614174000 --jsonata '$' +``` + +--- + +### `listEvents` + +List events in a time window. Recurring events are returned as expanded occurrences – each instance appears as its own e + +`GET /v1/calendar/events` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `from` | query | string (date-time) | Yes | Start of the time window (inclusive) | +| `to` | query | string (date-time) | Yes | End of the time window (exclusive) | +| `calendar_id` | query | string | No | Filter to a single calendar. Omit to query across all accessible calendars. | +| `size` | query | number | No | | +| `cursor` | query | string | No | | + +**Sample Call** + +```bash +epilot calendar listEvents \ + -p from=example \ + -p to=example +``` + +With JSONata filter: + +```bash +epilot calendar listEvents -p from=example -p to=example --jsonata 'results[0]' +``` + +
+Sample Response + +```json +{ + "results": [ + { + "_id": "string", + "_schema": "calendar_event", + "_org": "string", + "_title": "string", + "_tags": ["string"], + "calendar_id": "string", + "event_type": "singleInstance", + "description": "string", + "start_time": "1970-01-01T00:00:00.000Z", + "end_time": "1970-01-01T00:00:00.000Z", + "timezone": "string", + "is_all_day": true, + "location": "string", + "status": "free", + "busy": true, + "is_cancelled": true, + "is_draft": true, + "sensitivity": "normal", + "importance": "low", + "is_online_meeting": true, + "online_meeting_url": "https://example.com/path", + "web_link": "https://example.com/path", + "response_status": "none", + "organizer_email": "user@example.com", + "attendees": [ + { + "email": "user@example.com", + "name": "string", + "response": "none", + "type": "required" + } + ], + "metadata": {}, + "is_recurring": true, + "series_master_id": "string", + "source": { + "type": "native", + "provider": "outlook", + "provider_event_id": "string", + "provider_event_url": "https://example.com/path", + "last_synced_at": "1970-01-01T00:00:00.000Z", + "etag": "string" + }, + "_created_at": "1970-01-01T00:00:00.000Z", + "_updated_at": "1970-01-01T00:00:00.000Z" + } + ], + "next_cursor": "string" +} +``` + +
+ +--- + +### `createEvent` + +Create a native epilot calendar event. Omit `calendar_id` to use the caller’s epilot default calendar. + +`POST /v1/calendar/events` + +**Request Body** (required) + +**Sample Call** + +```bash +epilot calendar createEvent +``` + +With request body: + +```bash +epilot calendar createEvent \ + -d '{ + "calendar_id": "string", + "description": "string", + "start_time": "1970-01-01T00:00:00.000Z", + "end_time": "1970-01-01T00:00:00.000Z", + "timezone": "string", + "is_all_day": true, + "location": "string", + "status": "free", + "sensitivity": "normal", + "metadata": {}, + "_title": "string" +}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot calendar createEvent +``` + +With JSONata filter: + +```bash +epilot calendar createEvent --jsonata '_id' +``` + +
+Sample Response + +```json +{ + "_id": "string", + "_schema": "calendar_event", + "_org": "string", + "_title": "string", + "_tags": ["string"], + "calendar_id": "string", + "event_type": "singleInstance", + "description": "string", + "start_time": "1970-01-01T00:00:00.000Z", + "end_time": "1970-01-01T00:00:00.000Z", + "timezone": "string", + "is_all_day": true, + "location": "string", + "status": "free", + "busy": true, + "is_cancelled": true, + "is_draft": true, + "sensitivity": "normal", + "importance": "low", + "is_online_meeting": true, + "online_meeting_url": "https://example.com/path", + "web_link": "https://example.com/path", + "response_status": "none", + "organizer_email": "user@example.com", + "attendees": [ + { + "email": "user@example.com", + "name": "string", + "response": "none", + "type": "required" + } + ], + "metadata": {}, + "is_recurring": true, + "series_master_id": "string", + "source": { + "type": "native", + "provider": "outlook", + "provider_event_id": "string", + "provider_event_url": "https://example.com/path", + "last_synced_at": "1970-01-01T00:00:00.000Z", + "etag": "string" + }, + "_created_at": "1970-01-01T00:00:00.000Z", + "_updated_at": "1970-01-01T00:00:00.000Z" +} +``` + +
+ +--- + +### `getEvent` + +Get a single event by its epilot ID. + +`GET /v1/calendar/events/{event_id}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `event_id` | path | string | Yes | | + +**Sample Call** + +```bash +epilot calendar getEvent \ + -p event_id=123e4567-e89b-12d3-a456-426614174000 +``` + +Using positional args for path parameters: + +```bash +epilot calendar getEvent 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot calendar getEvent -p event_id=123e4567-e89b-12d3-a456-426614174000 --jsonata '_id' +``` + +
+Sample Response + +```json +{ + "_id": "string", + "_schema": "calendar_event", + "_org": "string", + "_title": "string", + "_tags": ["string"], + "calendar_id": "string", + "event_type": "singleInstance", + "description": "string", + "start_time": "1970-01-01T00:00:00.000Z", + "end_time": "1970-01-01T00:00:00.000Z", + "timezone": "string", + "is_all_day": true, + "location": "string", + "status": "free", + "busy": true, + "is_cancelled": true, + "is_draft": true, + "sensitivity": "normal", + "importance": "low", + "is_online_meeting": true, + "online_meeting_url": "https://example.com/path", + "web_link": "https://example.com/path", + "response_status": "none", + "organizer_email": "user@example.com", + "attendees": [ + { + "email": "user@example.com", + "name": "string", + "response": "none", + "type": "required" + } + ], + "metadata": {}, + "is_recurring": true, + "series_master_id": "string", + "source": { + "type": "native", + "provider": "outlook", + "provider_event_id": "string", + "provider_event_url": "https://example.com/path", + "last_synced_at": "1970-01-01T00:00:00.000Z", + "etag": "string" + }, + "_created_at": "1970-01-01T00:00:00.000Z", + "_updated_at": "1970-01-01T00:00:00.000Z" +} +``` + +
+ +--- + +### `updateEvent` + +Update a native epilot calendar event. + +`PATCH /v1/calendar/events/{event_id}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `event_id` | path | string | Yes | | + +**Request Body** (required) + +**Sample Call** + +```bash +epilot calendar updateEvent \ + -p event_id=123e4567-e89b-12d3-a456-426614174000 +``` + +With request body: + +```bash +epilot calendar updateEvent \ + -p event_id=123e4567-e89b-12d3-a456-426614174000 \ + -d '{ + "description": "string", + "start_time": "1970-01-01T00:00:00.000Z", + "end_time": "1970-01-01T00:00:00.000Z", + "timezone": "string", + "is_all_day": true, + "location": "string", + "status": "free", + "is_cancelled": true, + "sensitivity": "normal", + "_title": "string" +}' +``` + +Using positional args for path parameters: + +```bash +epilot calendar updateEvent 123e4567-e89b-12d3-a456-426614174000 +``` + +Using stdin pipe: + +```bash +cat body.json | epilot calendar updateEvent -p event_id=123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot calendar updateEvent -p event_id=123e4567-e89b-12d3-a456-426614174000 --jsonata '_id' +``` + +
+Sample Response + +```json +{ + "_id": "string", + "_schema": "calendar_event", + "_org": "string", + "_title": "string", + "_tags": ["string"], + "calendar_id": "string", + "event_type": "singleInstance", + "description": "string", + "start_time": "1970-01-01T00:00:00.000Z", + "end_time": "1970-01-01T00:00:00.000Z", + "timezone": "string", + "is_all_day": true, + "location": "string", + "status": "free", + "busy": true, + "is_cancelled": true, + "is_draft": true, + "sensitivity": "normal", + "importance": "low", + "is_online_meeting": true, + "online_meeting_url": "https://example.com/path", + "web_link": "https://example.com/path", + "response_status": "none", + "organizer_email": "user@example.com", + "attendees": [ + { + "email": "user@example.com", + "name": "string", + "response": "none", + "type": "required" + } + ], + "metadata": {}, + "is_recurring": true, + "series_master_id": "string", + "source": { + "type": "native", + "provider": "outlook", + "provider_event_id": "string", + "provider_event_url": "https://example.com/path", + "last_synced_at": "1970-01-01T00:00:00.000Z", + "etag": "string" + }, + "_created_at": "1970-01-01T00:00:00.000Z", + "_updated_at": "1970-01-01T00:00:00.000Z" +} +``` + +
+ +--- + +### `deleteEvent` + +Delete a native epilot calendar event. + +`DELETE /v1/calendar/events/{event_id}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `event_id` | path | string | Yes | | + +**Sample Call** + +```bash +epilot calendar deleteEvent \ + -p event_id=123e4567-e89b-12d3-a456-426614174000 +``` + +Using positional args for path parameters: + +```bash +epilot calendar deleteEvent 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot calendar deleteEvent -p event_id=123e4567-e89b-12d3-a456-426614174000 --jsonata '$' +``` + +--- + +### `shareEvent` + +Share a calendar event with another user of the same organization, view-only. Owner-only: recipients of a share cannot r + +`POST /v1/calendar/events/{event_id}/share` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `event_id` | path | string | Yes | | + +**Request Body** (required) + +**Sample Call** + +```bash +epilot calendar shareEvent \ + -p event_id=123e4567-e89b-12d3-a456-426614174000 \ + -d '{"user_id":"string"}' +``` + +Using positional args for path parameters: + +```bash +epilot calendar shareEvent 123e4567-e89b-12d3-a456-426614174000 +``` + +Using stdin pipe: + +```bash +cat body.json | epilot calendar shareEvent -p event_id=123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot calendar shareEvent -p event_id=123e4567-e89b-12d3-a456-426614174000 --jsonata '$' +``` + +--- + +### `unshareEvent` + +Revoke a per-event share. The recipient loses access immediately. Owner-only. + +`DELETE /v1/calendar/events/{event_id}/share/{user_id}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `event_id` | path | string | Yes | | +| `user_id` | path | string | Yes | | + +**Sample Call** + +```bash +epilot calendar unshareEvent \ + -p event_id=123e4567-e89b-12d3-a456-426614174000 \ + -p user_id=123e4567-e89b-12d3-a456-426614174000 +``` + +Using positional args for path parameters: + +```bash +epilot calendar unshareEvent 123e4567-e89b-12d3-a456-426614174000 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot calendar unshareEvent -p event_id=123e4567-e89b-12d3-a456-426614174000 -p user_id=123e4567-e89b-12d3-a456-426614174000 --jsonata '$' +``` + +--- diff --git a/packages/cli/docs/chat.md b/packages/cli/docs/chat.md new file mode 100644 index 000000000..f63d25cb9 --- /dev/null +++ b/packages/cli/docs/chat.md @@ -0,0 +1,627 @@ +# epilot Chat API + +- **Base URL:** `https://chat.sls.epilot.io` +- **API Docs:** [https://docs.epilot.io/api/chat](https://docs.epilot.io/api/chat) + +Widget management and anonymous browser chat. + +## Quick Start + +```bash +# List available operations +epilot chat + +# Call an operation +epilot chat listChatWidgets +``` + +## Common Flags + +| Flag | Description | +| ---- | ----------- | +| `-p key=value` | Set a named parameter | +| `-d '{...}'` | Request body JSON | +| `-H 'Key: Value'` | Custom header | +| `-t, --token ` | Bearer token for authentication | +| `--profile ` | Use a named profile | +| `-s, --server ` | Override server base URL | +| `-i, --include` | Include response headers in output | +| `--json` | Output raw JSON (no formatting) | +| `-v, --verbose` | Verbose output (show request details) | +| `--jsonata ` | JSONata expression to transform response | +| `--definition ` | Override OpenAPI spec file/URL | +| `--guided` | Prompt for all parameters interactively | +| `--no-interactive` | Disable interactive prompts | + +## Operations + +**Chat widgets** +- [`listChatWidgets`](#listchatwidgets) — GET /v1/widgets +- [`createChatWidget`](#createchatwidget) — POST /v1/widgets +- [`getChatWidget`](#getchatwidget) — GET /v1/widgets/{widget_id} +- [`updateChatWidget`](#updatechatwidget) — PUT /v1/widgets/{widget_id} +- [`deleteChatWidget`](#deletechatwidget) — DELETE /v1/widgets/{widget_id} + +**Other** +- [`getPublicChatWidget`](#getpublicchatwidget) — Resolve visitor-facing configuration for an independent widget and its current agent assignment. The widget ID is not an +- [`createPublicChatGrant`](#createpublicchatgrant) — Called by the host website with its widget_key. Checks the saved website origin allowlist and issues a single-use grant +- [`createAnonymousChatSession`](#createanonymouschatsession) — Exchanges an unexpired grant once for an independent 30-minute session. Expired or previously used grants return 401 INV +- [`sendAnonymousChatMessage`](#sendanonymouschatmessage) — Creates a turn or replays its persisted result. Reuse request_id and the exact + +### `listChatWidgets` + +`GET /v1/widgets` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `cursor` | query | string | No | | + +**Sample Call** + +```bash +epilot chat listChatWidgets +``` + +With JSONata filter: + +```bash +epilot chat listChatWidgets --jsonata 'widgets' +``` + +
+Sample Response + +```json +{ + "widgets": [ + { + "name": "string", + "agent_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "website_chat": { + "allowed_origins": ["string"], + "organisation_name": "string", + "default_locale": "en", + "design_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "authentication": { + "email_code": { + "email_template_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + } + } + }, + "widget_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "org_id": "string", + "binding_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "version": 1, + "created_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z", + "website_chat_embed": { + "script_url": "https://example.com/path", + "chat_api_origin": "https://example.com/path", + "demo_url": "https://example.com/path" + } + } + ], + "next_cursor": "string" +} +``` + +
+ +--- + +### `createChatWidget` + +`POST /v1/widgets` + +**Request Body** (required) + +**Sample Call** + +```bash +epilot chat createChatWidget +``` + +With request body: + +```bash +epilot chat createChatWidget \ + -d '{ + "name": "string", + "agent_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "website_chat": { + "allowed_origins": ["string"], + "organisation_name": "string", + "default_locale": "en", + "design_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "authentication": { + "email_code": { + "email_template_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + } + } + } +}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot chat createChatWidget +``` + +With JSONata filter: + +```bash +epilot chat createChatWidget --jsonata 'name' +``` + +
+Sample Response + +```json +{ + "name": "string", + "agent_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "website_chat": { + "allowed_origins": ["string"], + "organisation_name": "string", + "default_locale": "en", + "design_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "authentication": { + "email_code": { + "email_template_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + } + } + }, + "widget_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "org_id": "string", + "binding_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "version": 1, + "created_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z", + "website_chat_embed": { + "script_url": "https://example.com/path", + "chat_api_origin": "https://example.com/path", + "demo_url": "https://example.com/path" + } +} +``` + +
+ +--- + +### `getChatWidget` + +`GET /v1/widgets/{widget_id}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `widget_id` | path | string (uuid) | Yes | | + +**Sample Call** + +```bash +epilot chat getChatWidget \ + -p widget_id=123e4567-e89b-12d3-a456-426614174000 +``` + +Using positional args for path parameters: + +```bash +epilot chat getChatWidget 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot chat getChatWidget -p widget_id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'name' +``` + +
+Sample Response + +```json +{ + "name": "string", + "agent_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "website_chat": { + "allowed_origins": ["string"], + "organisation_name": "string", + "default_locale": "en", + "design_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "authentication": { + "email_code": { + "email_template_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + } + } + }, + "widget_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "org_id": "string", + "binding_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "version": 1, + "created_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z", + "website_chat_embed": { + "script_url": "https://example.com/path", + "chat_api_origin": "https://example.com/path", + "demo_url": "https://example.com/path" + } +} +``` + +
+ +--- + +### `updateChatWidget` + +`PUT /v1/widgets/{widget_id}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `widget_id` | path | string (uuid) | Yes | | + +**Request Body** (required) + +**Sample Call** + +```bash +epilot chat updateChatWidget \ + -p widget_id=123e4567-e89b-12d3-a456-426614174000 +``` + +With request body: + +```bash +epilot chat updateChatWidget \ + -p widget_id=123e4567-e89b-12d3-a456-426614174000 \ + -d '{ + "name": "string", + "agent_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "website_chat": { + "allowed_origins": ["string"], + "organisation_name": "string", + "default_locale": "en", + "design_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "authentication": { + "email_code": { + "email_template_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + } + } + }, + "version": 1 +}' +``` + +Using positional args for path parameters: + +```bash +epilot chat updateChatWidget 123e4567-e89b-12d3-a456-426614174000 +``` + +Using stdin pipe: + +```bash +cat body.json | epilot chat updateChatWidget -p widget_id=123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot chat updateChatWidget -p widget_id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'name' +``` + +
+Sample Response + +```json +{ + "name": "string", + "agent_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "website_chat": { + "allowed_origins": ["string"], + "organisation_name": "string", + "default_locale": "en", + "design_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "authentication": { + "email_code": { + "email_template_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + } + } + }, + "widget_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "org_id": "string", + "binding_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "version": 1, + "created_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z", + "website_chat_embed": { + "script_url": "https://example.com/path", + "chat_api_origin": "https://example.com/path", + "demo_url": "https://example.com/path" + } +} +``` + +
+ +--- + +### `deleteChatWidget` + +`DELETE /v1/widgets/{widget_id}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `widget_id` | path | string (uuid) | Yes | | +| `version` | query | number | Yes | | + +**Sample Call** + +```bash +epilot chat deleteChatWidget \ + -p widget_id=123e4567-e89b-12d3-a456-426614174000 \ + -p version=1 +``` + +Using positional args for path parameters: + +```bash +epilot chat deleteChatWidget 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot chat deleteChatWidget -p widget_id=123e4567-e89b-12d3-a456-426614174000 -p version=1 --jsonata '$' +``` + +--- + +### `getPublicChatWidget` + +Resolve visitor-facing configuration for an independent widget and its current agent assignment. The widget ID is not an + +`GET /v1/widgets/{widget_id}/configuration` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `widget_id` | path | string | Yes | Public widget ID, also used as widget_key in bootstrap and data-epilot-chat in the embed. | + +**Sample Call** + +```bash +epilot chat getPublicChatWidget \ + -p widget_id=550e8400-e29b-41d4-a716-446655440000 +``` + +Using positional args for path parameters: + +```bash +epilot chat getPublicChatWidget 550e8400-e29b-41d4-a716-446655440000 +``` + +With JSONata filter: + +```bash +epilot chat getPublicChatWidget -p widget_id=550e8400-e29b-41d4-a716-446655440000 --jsonata 'key' +``` + +
+Sample Response + +```json +{ + "key": "string", + "organisationName": "string", + "assistantName": "string", + "defaultLocale": "en", + "design": { + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "last_modified_at": "string", + "style": { + "palette": { + "primary": "string", + "background": "string" + }, + "typography": { + "font": { + "font_family": "string" + } + }, + "shape": { + "border_radius": 0 + } + }, + "spark_theme": { + "accentColor": "string", + "backgroundColor": "string", + "fontBody": "string", + "fontHeading": "string", + "radius": "string", + "scaling": "string", + "spacing": "string", + "appearance": "string", + "neutralColor": "string", + "styleVariant": "string", + "labelPosition": "string", + "inputStyle": "string", + "inputColor": "string", + "cardVariant": "string", + "cardColor": "string", + "highContrast": true + } + }, + "locales": ["en"] +} +``` + +
+ +--- + +### `createPublicChatGrant` + +Called by the host website with its widget_key. Checks the saved website origin allowlist and issues a single-use grant + +`POST /v1/bootstrap` + +**Request Body** (required) + +**Sample Call** + +```bash +epilot chat createPublicChatGrant \ + -d '{"widget_key":"string"}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot chat createPublicChatGrant +``` + +With JSONata filter: + +```bash +epilot chat createPublicChatGrant --jsonata 'grant' +``` + +
+Sample Response + +```json +{ + "grant": "string", + "expires_in": 60, + "widget_origin": "https://example.com/path" +} +``` + +
+ +--- + +### `createAnonymousChatSession` + +Exchanges an unexpired grant once for an independent 30-minute session. Expired or previously used grants return 401 INV + +`POST /v1/sessions` + +**Request Body** (required) + +**Sample Call** + +```bash +epilot chat createAnonymousChatSession \ + -d '{"grant":"string"}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot chat createAnonymousChatSession +``` + +With JSONata filter: + +```bash +epilot chat createAnonymousChatSession --jsonata 'token' +``` + +
+Sample Response + +```json +{ + "token": "string", + "conversation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "expires_at": 0, + "widget": { + "key": "string", + "organisationName": "string", + "assistantName": "string", + "defaultLocale": "en", + "design": { + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "last_modified_at": "string", + "style": { + "palette": { + "primary": "string", + "background": "string" + }, + "typography": { + "font": { + "font_family": "string" + } + }, + "shape": { + "border_radius": 0 + } + }, + "spark_theme": { + "accentColor": "string", + "backgroundColor": "string", + "fontBody": "string", + "fontHeading": "string", + "radius": "string", + "scaling": "string", + "spacing": "string", + "appearance": "string", + "neutralColor": "string", + "styleVariant": "string", + "labelPosition": "string", + "inputStyle": "string", + "inputColor": "string", + "cardVariant": "string", + "cardColor": "string", + "highContrast": true + } + }, + "locales": ["en"] + } +} +``` + +
+ +--- + +### `sendAnonymousChatMessage` + +Creates a turn or replays its persisted result. Reuse request_id and the exact + +`POST /v1/messages` + +**Request Body** (required) + +**Sample Call** + +```bash +epilot chat sendAnonymousChatMessage \ + -d '{"request_id":"3fa85f64-5717-4562-b3fc-2c963f66afa6","message":"string","locale":"en","simple_language":true}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot chat sendAnonymousChatMessage +``` + +With JSONata filter: + +```bash +epilot chat sendAnonymousChatMessage --jsonata '$' +``` + +--- diff --git a/packages/cli/docs/customer-portal.md b/packages/cli/docs/customer-portal.md index 343fef171..6629cc2d1 100644 --- a/packages/cli/docs/customer-portal.md +++ b/packages/cli/docs/customer-portal.md @@ -48,6 +48,8 @@ epilot customer-portal upsertPortal -p origin=example - [`getAllPortalConfigs`](#getallportalconfigs) — Retrieves all portal configurations. - [`getEmailTemplates`](#getemailtemplates) — Retrieves the email templates of a portal - [`upsertEmailTemplates`](#upsertemailtemplates) — Upserts the email templates of a portal +- [`migrateEmailTemplateReferences`](#migrateemailtemplatereferences) — Walk every email-template config row in the caller's org and re-point any +- [`listEmailTemplateReferences`](#listemailtemplatereferences) — Read-only sibling of migrateEmailTemplateReferences. Lists every portal in - [`getEmailTemplatesByPortalId`](#getemailtemplatesbyportalid) — Retrieves the email templates of a portal by portal ID - [`upsertEmailTemplatesByPortalId`](#upsertemailtemplatesbyportalid) — Upserts the email templates of a portal by portal ID - [`getPortalWidgetsV3`](#getportalwidgetsv3) — Retrieves the widgets of a portal by portal_id. @@ -81,10 +83,16 @@ epilot customer-portal upsertPortal -p origin=example - [`getPortalConfigV3`](#getportalconfigv3) — Retrieves a specific portal configuration by ID. - [`putPortalConfig`](#putportalconfig) — Updates a specific portal configuration by ID. - [`deletePortalConfig`](#deleteportalconfig) — Deletes a specific portal configuration by ID. +- [`listPortalRevisions`](#listportalrevisions) — Lists a portal's revision history, newest first. Metadata only — no config blob, no page bodies. `is_published` says whe +- [`createPortalRevision`](#createportalrevision) — Creates a new revision — a complete, immutable snapshot of the portal's configuration, pages and email templates. Nothin +- [`getPortalRevision`](#getportalrevision) — Returns the full content of one revision: the snapshotted config, its pages (in the live `Page` shape), email templates +- [`publishPortalRevision`](#publishportalrevision) — Makes one revision the portal's live configuration, atomically: either everything below takes effect or nothing does. Pu - [`listAllPortalConfigs`](#listallportalconfigs) — Retrieves all portal configurations. - [`swapPortalConfig`](#swapportalconfig) — Swaps the portal configuration of two portals. - [`clonePortalConfig`](#cloneportalconfig) — Creates a new portal by cloning configuration and pages from an existing portal. The new portal gets its own domain, use - [`verifyDns`](#verifydns) — Manually triggers DNS verification for a portal's domain setup. Runs the same verification logic as the scheduled proces +- [`getMobileConfig`](#getmobileconfig) — Returns the portal's mobile app configuration. By default the response is build-ready (resolved): base info (display_nam +- [`putMobileConfig`](#putmobileconfig) — Merges the provided fields into the portal's mobile app configuration **Public** - [`createUserV3`](#createuserv3) — Registers a portal user. @@ -99,12 +107,14 @@ epilot customer-portal upsertPortal -p origin=example - [`getOrganizationSettingsByDomain`](#getorganizationsettingsbydomain) — Retrieves organization settings by domain. Only public organization settings are returned. - [`checkContactExists`](#checkcontactexists) — True if contact with given identifiers exists. - [`checkContactExistsV3`](#checkcontactexistsv3) — True if contact with given identifiers exists. +- [`identifyContact`](#identifycontact) — Identify a contact by the portal's configured registration identifiers and, on a match, - [`checkAccountExists`](#checkaccountexists) — True if account with given identifiers exists. - [`confirmUser`](#confirmuser) — Confirm a portal user - [`confirmUserWithUserId`](#confirmuserwithuserid) — Confirm a portal user - [`userExists`](#userexists) — Checks whether a user exists in the portal - [`userExistsV3`](#userexistsv3) — Checks whether a user exists in the portal. - [`getSSOProvider`](#getssoprovider) — Returns the public configuration of a single SSO identity provider with env var +- [`getPublicSSOProviderV3`](#getpublicssoproviderv3) — Returns the public configuration of a single SSO identity provider with env var - [`ssoRedirect`](#ssoredirect) — Handles the redirect from the external SSO provider. Validates the authorization `code` and `state` received from the pr - [`ssoCallback`](#ssocallback) — Handles the callback from the external SSO provider, validates the authorization `code` - [`getPublicPages`](#getpublicpages) — Fetch all public portal pages @@ -115,6 +125,8 @@ epilot customer-portal upsertPortal -p origin=example **ECP** - [`validateToken`](#validatetoken) — Validates Portal Token is valid. Pass the token via Authorization Header. - [`revokeToken`](#revoketoken) — Revokes all of the access tokens for the given Refresh Token. +- [`createExport`](#createexport) — Request an asynchronous CSV export of the portal user's entities for one schema. Columns come from the request Returns a +- [`getExport`](#getexport) — Get the status of an export job, including the download URL once ready. - [`getConsumption`](#getconsumption) — Get energy consumption data between a given time period. - [`prepareVisualizationExport`](#preparevisualizationexport) — Asks an installed App to prepare a downloadable export of a visualization (consumption chart, dynamic tariff chart, etc. - [`getVisualizationMetadata`](#getvisualizationmetadata) — Returns runtime metadata describing how a visualization (consumption / price / cost chart) should be rendered for a give @@ -127,13 +139,16 @@ epilot customer-portal upsertPortal -p origin=example - [`updatePortalUser`](#updateportaluser) — Update the portal user details - [`deletePortalUser`](#deleteportaluser) — Delete the portal user - [`updatePortalUserEmail`](#updateportaluseremail) — Update portal user email +- [`changePortalUserPassword`](#changeportaluserpassword) — Hand over a password change to the third-party system configured via the `changePassword` portal extension hook. - [`postOrderAcceptance`](#postorderacceptance) — Accept/decline an offer by id +- [`getContractWithTemplates`](#getcontractwithtemplates) — Resolve Handlebars templates against a contract's related meters and return the contract with templates_output populated - [`addContractByIdentifiers`](#addcontractbyidentifiers) — Self-assign contract(s) by pre-configured identifiers. - [`validateCadenceEntityEditRules`](#validatecadenceentityeditrules) — Validate if cadence rule is valid for an entity - [`searchPaymentRelationsInEntities`](#searchpaymentrelationsinentities) — Search for entities that have the payment relation with the given payment id - [`createCustomEntityActivity`](#createcustomentityactivity) — Create a custom activity that can be displayed in activity feed of an entity. - [`saveEntityFile`](#saveentityfile) — Add files to an entity - [`deleteEntityFile`](#deleteentityfile) — Delete files from an entity +- [`getFilePreview`](#getfilepreview) — resolves an in-portal preview for a file. Returns a Content-Disposition: inline URL for directly-previewable files (PDF, - [`trackFileDownloaded`](#trackfiledownloaded) — Track that user has downloaded a file - [`getBillingEvents`](#getbillingevents) — Fetch billing events for a portal user - [`triggerEntityAccessEvent`](#triggerentityaccessevent) — Trigger entity access event for a portal user @@ -143,6 +158,9 @@ epilot customer-portal upsertPortal -p origin=example - [`getAutomationContext`](#getautomationcontext) — Retrieves the automation context. - [`updateWorkflowStepAsDone`](#updateworkflowstepasdone) — Update a workflow step as done - [`getEntityWorkflows`](#getentityworkflows) — Get all workflows associated with an entity (requires access to the entity) +- [`getOutstandingTasks`](#getoutstandingtasks) — Returns all outstanding workflow journey tasks for the authenticated portal user, across their opportunity, order and co +- [`getEntityPortalWorkflows`](#getentityportalworkflows) — Get all portal-relevant workflows associated with an entity (requires access to the entity), +- [`getEntityPortalWorkflowsBatch`](#getentityportalworkflowsbatch) — Batch variant of `getEntityPortalWorkflows`: returns portal-relevant workflows for - [`uploadMeterReadingPhoto`](#uploadmeterreadingphoto) — Uploads a Meter Reading photo and - if enabled - gives back data extracted from the photo. - [`createMeterReading`](#createmeterreading) — Inserts a new meter reading. - [`getAllowedMeterReadingRange`](#getallowedmeterreadingrange) — Get allowed reading range for all counters of a meter from the configured @@ -153,6 +171,10 @@ epilot customer-portal upsertPortal -p origin=example - [`getPortalPageBlocks`](#getportalpageblocks) — Fetch all portal page blocks - [`getPortalPageBlock`](#getportalpageblock) — Fetch a portal page block by id - [`updateCampaignPortalBlockStatus`](#updatecampaignportalblockstatus) — Updates the status of a campaign portal block for multiple recipients. +- [`listPortalNotifications`](#listportalnotifications) — Lists the 360 notifications addressed to the authenticated portal user, newest first. The organization and the portal us +- [`getPortalNotificationsUnreadCount`](#getportalnotificationsunreadcount) — Returns the number of unread notifications for the authenticated portal user. +- [`markAllPortalNotificationsRead`](#markallportalnotificationsread) — Marks all notifications of the authenticated portal user as read. +- [`markPortalNotificationRead`](#markportalnotificationread) — Marks a single notification of the authenticated portal user as read. - [`updateNotificationsStatus`](#updatenotificationsstatus) — Updates the statuses of multiple notifications at once. - [`invitePartner`](#invitepartner) — Invites a partner to a portal - [`listBusinessPartners`](#listbusinesspartners) — Lists all business partners linked to the businessaccount @@ -231,9 +253,47 @@ epilot customer-portal upsertPortal \ "attribute_mappings": {}, "entity_matching": {}, "oidc_config": {}, - "mobile_oidc_config": {} + "mobile_oidc_config": {}, + "expose_client_secret": false + } + ], + "mobile_config": { + "portal_id": "string", + "enabled": true, + "display_name": "string", + "app_host": "string", + "environment": "prod", + "branding": { + "app_icon": "string", + "splash": "string", + "splash_dark": "string", + "icon_background_color": "string", + "splash_background_color": "string", + "splash_background_color_dark": "string" + }, + "ios": { + "bundle_id": "string", + "team_id": "string", + "credentials_status": "not_configured", + "app_store_id": "string", + "store_url": "string", + "last_build": {} + }, + "android": { + "package_name": "string", + "credentials_status": "not_configured", + "upload_key_status": "not_configured", + "store_url": "string", + "last_build": {} + }, + "ota": { + "enabled": true, + "channel": "canary", + "auto_update": true, + "update_strategy": "next-launch", + "min_native_version": "string" } - ], + }, "enabled": true, "name": "Installer Portal", "domain": "abc.com", @@ -255,7 +315,9 @@ epilot customer-portal upsertPortal \ "start_page": true, "billing": true, "change_due_date": true, - "new_design": true + "new_design": true, + "mcp_enabled": true, + "mcp_grant_version": 0 }, "accessToken": "string", "advanced_mfa": { @@ -267,7 +329,8 @@ epilot customer-portal upsertPortal \ }, "entry_point": "PASSWORD", "preferred_sso_providers": ["office-365-login"], - "auto_redirect_to_sso": true + "auto_redirect_to_sso": true, + "prevent_user_enumeration": true }, "cognito_details": { "cognito_user_pool_client_id": "6bsd0jkgoie74k2i8mrhc1vest", @@ -289,7 +352,8 @@ epilot customer-portal upsertPortal \ "require_lowercase": true, "require_uppercase": true, "require_numbers": true, - "require_symbols": true + "require_symbols": true, + "password_history_size": 3 } }, "config": "string", @@ -350,6 +414,16 @@ epilot customer-portal upsertPortal \ "schema": "contract" } ], + "surfaces": [ + { + "id": "website-journeys", + "name": "Website journeys", + "description": "string", + "authentication": "login", + "token_ttl_seconds": 300, + "data_access": {} + } + ], "contact_identifiers_for_account": [ { "name": "first_name", @@ -408,11 +482,21 @@ epilot customer-portal upsertPortal \ "inactive_contract_cutoff_years": 0, "is_dummy": true, "is_v3_item": true, + "published_revision_id": "2026-08-25T14:03:11.482Z-a7f3c1d9", "portal_id": "453ad7bf-86d5-46c8-8252-bcc868df5e3c", "portal_sk_v3": "PORTAL_CONFIG#453ad7bf-86d5-46c8-8252-bcc868df5e3c", "origin": "string", "pages": {}, - "global_blocks": {} + "global_blocks": {}, + "notification_triggers": [ + { + "trigger_type": "entity_created", + "entity_schema": "opportunity", + "enabled": true, + "template_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + } + ], + "engagement_center_enabled": true }' ``` @@ -433,6 +517,43 @@ epilot customer-portal upsertPortal -p origin=example --jsonata '$' ```json { + "mobile_config": { + "portal_id": "string", + "enabled": true, + "display_name": "string", + "app_host": "string", + "environment": "prod", + "branding": { + "app_icon": "string", + "splash": "string", + "splash_dark": "string", + "icon_background_color": "string", + "splash_background_color": "string", + "splash_background_color_dark": "string" + }, + "ios": { + "bundle_id": "string", + "team_id": "string", + "credentials_status": "not_configured", + "app_store_id": "string", + "store_url": "string", + "last_build": {} + }, + "android": { + "package_name": "string", + "credentials_status": "not_configured", + "upload_key_status": "not_configured", + "store_url": "string", + "last_build": {} + }, + "ota": { + "enabled": true, + "channel": "canary", + "auto_update": true, + "update_strategy": "next-launch", + "min_native_version": "string" + } + }, "enabled": true, "name": "Installer Portal", "domain": "abc.com", @@ -454,7 +575,9 @@ epilot customer-portal upsertPortal -p origin=example --jsonata '$' "start_page": true, "billing": true, "change_due_date": true, - "new_design": true + "new_design": true, + "mcp_enabled": true, + "mcp_grant_version": 0 }, "accessToken": "string", "advanced_mfa": { @@ -466,7 +589,8 @@ epilot customer-portal upsertPortal -p origin=example --jsonata '$' }, "entry_point": "PASSWORD", "preferred_sso_providers": ["office-365-login"], - "auto_redirect_to_sso": true + "auto_redirect_to_sso": true, + "prevent_user_enumeration": true }, "cognito_details": { "cognito_user_pool_client_id": "6bsd0jkgoie74k2i8mrhc1vest", @@ -488,7 +612,8 @@ epilot customer-portal upsertPortal -p origin=example --jsonata '$' "require_lowercase": true, "require_uppercase": true, "require_numbers": true, - "require_symbols": true + "require_symbols": true, + "password_history_size": 3 } }, "config": "string", @@ -549,6 +674,16 @@ epilot customer-portal upsertPortal -p origin=example --jsonata '$' "schema": "contract" } ], + "surfaces": [ + { + "id": "website-journeys", + "name": "Website journeys", + "description": "string", + "authentication": "login", + "token_ttl_seconds": 300, + "data_access": {} + } + ], "contact_identifiers_for_account": [ { "name": "first_name", @@ -607,11 +742,21 @@ epilot customer-portal upsertPortal -p origin=example --jsonata '$' "inactive_contract_cutoff_years": 0, "is_dummy": true, "is_v3_item": true, + "published_revision_id": "2026-08-25T14:03:11.482Z-a7f3c1d9", "portal_id": "453ad7bf-86d5-46c8-8252-bcc868df5e3c", "portal_sk_v3": "PORTAL_CONFIG#453ad7bf-86d5-46c8-8252-bcc868df5e3c", "origin": "string", "pages": {}, "global_blocks": {}, + "notification_triggers": [ + { + "trigger_type": "entity_created", + "entity_schema": "opportunity", + "enabled": true, + "template_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + } + ], + "engagement_center_enabled": true, "id": 12345, "organization_id": 12345, "org_settings": { @@ -821,6 +966,43 @@ epilot customer-portal getPortalConfigByDomain -p domain=example.com --jsonata ' ```json { + "mobile_config": { + "portal_id": "string", + "enabled": true, + "display_name": "string", + "app_host": "string", + "environment": "prod", + "branding": { + "app_icon": "string", + "splash": "string", + "splash_dark": "string", + "icon_background_color": "string", + "splash_background_color": "string", + "splash_background_color_dark": "string" + }, + "ios": { + "bundle_id": "string", + "team_id": "string", + "credentials_status": "not_configured", + "app_store_id": "string", + "store_url": "string", + "last_build": {} + }, + "android": { + "package_name": "string", + "credentials_status": "not_configured", + "upload_key_status": "not_configured", + "store_url": "string", + "last_build": {} + }, + "ota": { + "enabled": true, + "channel": "canary", + "auto_update": true, + "update_strategy": "next-launch", + "min_native_version": "string" + } + }, "enabled": true, "name": "Installer Portal", "domain": "abc.com", @@ -842,7 +1024,9 @@ epilot customer-portal getPortalConfigByDomain -p domain=example.com --jsonata ' "start_page": true, "billing": true, "change_due_date": true, - "new_design": true + "new_design": true, + "mcp_enabled": true, + "mcp_grant_version": 0 }, "accessToken": "string", "advanced_mfa": { @@ -854,7 +1038,8 @@ epilot customer-portal getPortalConfigByDomain -p domain=example.com --jsonata ' }, "entry_point": "PASSWORD", "preferred_sso_providers": ["office-365-login"], - "auto_redirect_to_sso": true + "auto_redirect_to_sso": true, + "prevent_user_enumeration": true }, "cognito_details": { "cognito_user_pool_client_id": "6bsd0jkgoie74k2i8mrhc1vest", @@ -876,7 +1061,8 @@ epilot customer-portal getPortalConfigByDomain -p domain=example.com --jsonata ' "require_lowercase": true, "require_uppercase": true, "require_numbers": true, - "require_symbols": true + "require_symbols": true, + "password_history_size": 3 } }, "config": "string", @@ -937,6 +1123,16 @@ epilot customer-portal getPortalConfigByDomain -p domain=example.com --jsonata ' "schema": "contract" } ], + "surfaces": [ + { + "id": "website-journeys", + "name": "Website journeys", + "description": "string", + "authentication": "login", + "token_ttl_seconds": 300, + "data_access": {} + } + ], "contact_identifiers_for_account": [ { "name": "first_name", @@ -995,11 +1191,21 @@ epilot customer-portal getPortalConfigByDomain -p domain=example.com --jsonata ' "inactive_contract_cutoff_years": 0, "is_dummy": true, "is_v3_item": true, + "published_revision_id": "2026-08-25T14:03:11.482Z-a7f3c1d9", "portal_id": "453ad7bf-86d5-46c8-8252-bcc868df5e3c", "portal_sk_v3": "PORTAL_CONFIG#453ad7bf-86d5-46c8-8252-bcc868df5e3c", "origin": "string", "pages": {}, "global_blocks": {}, + "notification_triggers": [ + { + "trigger_type": "entity_created", + "entity_schema": "opportunity", + "enabled": true, + "template_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + } + ], + "engagement_center_enabled": true, "id": 12345, "organization_id": 12345, "org_settings": { @@ -1062,6 +1268,43 @@ epilot customer-portal getPortalConfig --jsonata '$' ```json { + "mobile_config": { + "portal_id": "string", + "enabled": true, + "display_name": "string", + "app_host": "string", + "environment": "prod", + "branding": { + "app_icon": "string", + "splash": "string", + "splash_dark": "string", + "icon_background_color": "string", + "splash_background_color": "string", + "splash_background_color_dark": "string" + }, + "ios": { + "bundle_id": "string", + "team_id": "string", + "credentials_status": "not_configured", + "app_store_id": "string", + "store_url": "string", + "last_build": {} + }, + "android": { + "package_name": "string", + "credentials_status": "not_configured", + "upload_key_status": "not_configured", + "store_url": "string", + "last_build": {} + }, + "ota": { + "enabled": true, + "channel": "canary", + "auto_update": true, + "update_strategy": "next-launch", + "min_native_version": "string" + } + }, "enabled": true, "name": "Installer Portal", "domain": "abc.com", @@ -1083,7 +1326,9 @@ epilot customer-portal getPortalConfig --jsonata '$' "start_page": true, "billing": true, "change_due_date": true, - "new_design": true + "new_design": true, + "mcp_enabled": true, + "mcp_grant_version": 0 }, "accessToken": "string", "advanced_mfa": { @@ -1095,7 +1340,8 @@ epilot customer-portal getPortalConfig --jsonata '$' }, "entry_point": "PASSWORD", "preferred_sso_providers": ["office-365-login"], - "auto_redirect_to_sso": true + "auto_redirect_to_sso": true, + "prevent_user_enumeration": true }, "cognito_details": { "cognito_user_pool_client_id": "6bsd0jkgoie74k2i8mrhc1vest", @@ -1117,7 +1363,8 @@ epilot customer-portal getPortalConfig --jsonata '$' "require_lowercase": true, "require_uppercase": true, "require_numbers": true, - "require_symbols": true + "require_symbols": true, + "password_history_size": 3 } }, "config": "string", @@ -1178,6 +1425,16 @@ epilot customer-portal getPortalConfig --jsonata '$' "schema": "contract" } ], + "surfaces": [ + { + "id": "website-journeys", + "name": "Website journeys", + "description": "string", + "authentication": "login", + "token_ttl_seconds": 300, + "data_access": {} + } + ], "contact_identifiers_for_account": [ { "name": "first_name", @@ -1236,11 +1493,21 @@ epilot customer-portal getPortalConfig --jsonata '$' "inactive_contract_cutoff_years": 0, "is_dummy": true, "is_v3_item": true, + "published_revision_id": "2026-08-25T14:03:11.482Z-a7f3c1d9", "portal_id": "453ad7bf-86d5-46c8-8252-bcc868df5e3c", "portal_sk_v3": "PORTAL_CONFIG#453ad7bf-86d5-46c8-8252-bcc868df5e3c", "origin": "string", "pages": {}, "global_blocks": {}, + "notification_triggers": [ + { + "trigger_type": "entity_created", + "entity_schema": "opportunity", + "enabled": true, + "template_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + } + ], + "engagement_center_enabled": true, "id": 12345, "organization_id": 12345, "org_settings": { @@ -1301,6 +1568,119 @@ epilot customer-portal deletePortal -p origin=example --jsonata '$' --- +### `createExport` + +Request an asynchronous CSV export of the portal user's entities for one schema. Columns come from the request Returns a + +`POST /v1/portal/exports` + +**Request Body** (required) + +**Sample Call** + +```bash +epilot customer-portal createExport +``` + +With request body: + +```bash +epilot customer-portal createExport \ + -d '{ + "schema": "string", + "search": { + "q": "string", + "q_fields": ["string"], + "filters": [ + {} + ], + "filters_context": [ + {} + ], + "sort": {} + }, + "columns": [ + { + "key": "vertragsnummer", + "header": { + "de": "Vertragsnummer", + "en": "Contract number" + }, + "source": { + "path": ["customer"], + "attribute": "customer_number", + "address_field": "full" + }, + "formatter": "text", + "enum_labels": {} + } + ], + "expand_over": "string", + "language": "de" +}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot customer-portal createExport +``` + +With JSONata filter: + +```bash +epilot customer-portal createExport --jsonata '$' +``` + +--- + +### `getExport` + +Get the status of an export job, including the download URL once ready. + +`GET /v1/portal/exports/{jobId}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `jobId` | path | string | Yes | The export job id returned by createExport. | + +**Sample Call** + +```bash +epilot customer-portal getExport \ + -p jobId=123e4567-e89b-12d3-a456-426614174000 +``` + +Using positional args for path parameters: + +```bash +epilot customer-portal getExport 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot customer-portal getExport -p jobId=123e4567-e89b-12d3-a456-426614174000 --jsonata 'jobId' +``` + +
+Sample Response + +```json +{ + "jobId": "string", + "status": "queued", + "downloadUrl": "string", + "error": "string" +} +``` + +
+ +--- + ### `getPortalExtensions` Retrieves the installed portal extensions. @@ -1439,6 +1819,51 @@ epilot customer-portal getPublicPortalExtensionDetails -p org_id=12324 -p origin "hook": { "plausibility_mode": "check" } + }, + "changeEmail": { + "app": { + "app_id": "string", + "name": {} + }, + "extension": { + "id": "string", + "name": {} + }, + "hook": { + "id": "string", + "change_mode": "asynchronous", + "require_password_confirmation": true, + "explanation": {} + } + }, + "changePassword": { + "app": { + "app_id": "string", + "name": {} + }, + "extension": { + "id": "string", + "name": {} + }, + "hook": { + "id": "string", + "require_new_password": false, + "explanation": {} + } + }, + "deleteAccount": { + "app": { + "app_id": "string", + "name": {} + }, + "extension": { + "id": "string", + "name": {} + }, + "hook": { + "id": "string", + "explanation": {} + } } } ``` @@ -1585,6 +2010,51 @@ epilot customer-portal getPublicPortalExtensionDetailsV3 --jsonata 'consumptionD "hook": { "plausibility_mode": "check" } + }, + "changeEmail": { + "app": { + "app_id": "string", + "name": {} + }, + "extension": { + "id": "string", + "name": {} + }, + "hook": { + "id": "string", + "change_mode": "asynchronous", + "require_password_confirmation": true, + "explanation": {} + } + }, + "changePassword": { + "app": { + "app_id": "string", + "name": {} + }, + "extension": { + "id": "string", + "name": {} + }, + "hook": { + "id": "string", + "require_new_password": false, + "explanation": {} + } + }, + "deleteAccount": { + "app": { + "app_id": "string", + "name": {} + }, + "extension": { + "id": "string", + "name": {} + }, + "hook": { + "id": "string", + "explanation": {} + } } } ``` @@ -1609,7 +2079,7 @@ Get energy consumption data between a given time period. | `meter_id` | query | string | No | Meter ID for consumption data. Deprecated - use context_entities instead. | | `from` | query | string (date-time) | Yes | Start date for consumption data (ISO 8601 format). | | `to` | query | string (date-time) | Yes | End date for consumption data (ISO 8601 format). | -| `interval` | query | "PT15M" \| "PT1H" \| "P1D" \| "P1M" | Yes | Interval between consumption data points (e.g., PT15M for 15 minutes, PT1H for hourly). Not all intervals have to be supported. | +| `interval` | query | "PT15M" \| "PT1H" \| "P1D" \| "P1M" \| "P1Y" \| "custom" | Yes | 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 thei | | `context_entities` | query | object[] | No | Additional entities to include in the context for variable interpolation in the hook. | **Sample Call** @@ -1638,7 +2108,15 @@ epilot customer-portal getConsumption -p extensionId=123e4567-e89b-12d3-a456-426 "timestamp": "1970-01-01T00:00:00.000Z", "value": 0, "type": "nt", - "unit": "kWh" + "unit": "kWh", + "label": { + "en": "Billing period 1", + "de": "Abrechnungszeitraum 1" + }, + "period": { + "from": "2024-01-03T00:00:00.000Z", + "to": "2025-01-05T00:00:00.000Z" + } } ] } @@ -1781,7 +2259,7 @@ Get energy cost data between a given time period. | `meter_id` | query | string | No | Meter ID for cost data. Deprecated - use context_entities instead. | | `from` | query | string (date-time) | Yes | Start date for cost data (ISO 8601 format). | | `to` | query | string (date-time) | Yes | End date for cost data (ISO 8601 format). | -| `interval` | query | "PT15M" \| "PT1H" \| "P1D" \| "P1M" | Yes | Interval between cost data points (e.g., PT15M for 15 minutes, PT1H for hourly). Not all intervals have to be supported. | +| `interval` | query | "PT15M" \| "PT1H" \| "P1D" \| "P1M" \| "P1Y" | Yes | Interval between cost data points (e.g., PT15M for 15 minutes, PT1H for hourly). Not all intervals have to be supported. | | `context_entities` | query | object[] | No | Additional entities to include in the context for variable interpolation in the hook. | **Sample Call** @@ -1838,7 +2316,7 @@ Get energy prices data between a given time period. | `meter_id` | query | string | No | Meter ID for price data. Deprecated - use context_entities instead. | | `from` | query | string (date-time) | Yes | Start date for price data (ISO 8601 format). | | `to` | query | string (date-time) | Yes | End date for price data (ISO 8601 format). | -| `interval` | query | "PT15M" \| "PT1H" \| "P1D" \| "P1M" | Yes | Interval between price data points (e.g., PT15M for 15 minutes, PT1H for hourly). Not all intervals have to be supported. | +| `interval` | query | "PT15M" \| "PT1H" \| "P1D" \| "P1M" \| "P1Y" | Yes | Interval between price data points (e.g., PT15M for 15 minutes, PT1H for hourly). Not all intervals have to be supported. | | `context_entities` | query | object[] | No | Additional entities to include in the context for variable interpolation in the hook. | **Sample Call** @@ -2075,6 +2553,43 @@ epilot customer-portal getPublicPortalConfig -p org_id=12324 -p origin=example - ```json { + "mobile_config": { + "portal_id": "string", + "enabled": true, + "display_name": "string", + "app_host": "string", + "environment": "prod", + "branding": { + "app_icon": "string", + "splash": "string", + "splash_dark": "string", + "icon_background_color": "string", + "splash_background_color": "string", + "splash_background_color_dark": "string" + }, + "ios": { + "bundle_id": "string", + "team_id": "string", + "credentials_status": "not_configured", + "app_store_id": "string", + "store_url": "string", + "last_build": {} + }, + "android": { + "package_name": "string", + "credentials_status": "not_configured", + "upload_key_status": "not_configured", + "store_url": "string", + "last_build": {} + }, + "ota": { + "enabled": true, + "channel": "canary", + "auto_update": true, + "update_strategy": "next-launch", + "min_native_version": "string" + } + }, "enabled": true, "name": "Installer Portal", "domain": "abc.com", @@ -2096,7 +2611,9 @@ epilot customer-portal getPublicPortalConfig -p org_id=12324 -p origin=example - "start_page": true, "billing": true, "change_due_date": true, - "new_design": true + "new_design": true, + "mcp_enabled": true, + "mcp_grant_version": 0 }, "accessToken": "string", "advanced_mfa": { @@ -2108,7 +2625,8 @@ epilot customer-portal getPublicPortalConfig -p org_id=12324 -p origin=example - }, "entry_point": "PASSWORD", "preferred_sso_providers": ["office-365-login"], - "auto_redirect_to_sso": true + "auto_redirect_to_sso": true, + "prevent_user_enumeration": true }, "cognito_details": { "cognito_user_pool_client_id": "6bsd0jkgoie74k2i8mrhc1vest", @@ -2130,7 +2648,8 @@ epilot customer-portal getPublicPortalConfig -p org_id=12324 -p origin=example - "require_lowercase": true, "require_uppercase": true, "require_numbers": true, - "require_symbols": true + "require_symbols": true, + "password_history_size": 3 } }, "config": "string", @@ -2191,6 +2710,16 @@ epilot customer-portal getPublicPortalConfig -p org_id=12324 -p origin=example - "schema": "contract" } ], + "surfaces": [ + { + "id": "website-journeys", + "name": "Website journeys", + "description": "string", + "authentication": "login", + "token_ttl_seconds": 300, + "data_access": {} + } + ], "contact_identifiers_for_account": [ { "name": "first_name", @@ -2249,11 +2778,21 @@ epilot customer-portal getPublicPortalConfig -p org_id=12324 -p origin=example - "inactive_contract_cutoff_years": 0, "is_dummy": true, "is_v3_item": true, + "published_revision_id": "2026-08-25T14:03:11.482Z-a7f3c1d9", "portal_id": "453ad7bf-86d5-46c8-8252-bcc868df5e3c", "portal_sk_v3": "PORTAL_CONFIG#453ad7bf-86d5-46c8-8252-bcc868df5e3c", "origin": "string", "pages": {}, "global_blocks": {}, + "notification_triggers": [ + { + "trigger_type": "entity_created", + "entity_schema": "opportunity", + "enabled": true, + "template_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + } + ], + "engagement_center_enabled": true, "id": 12345, "organization_id": 12345, "org_settings": { @@ -2317,6 +2856,43 @@ epilot customer-portal getOrgPortalConfig -p origin=example --jsonata '$' ```json { + "mobile_config": { + "portal_id": "string", + "enabled": true, + "display_name": "string", + "app_host": "string", + "environment": "prod", + "branding": { + "app_icon": "string", + "splash": "string", + "splash_dark": "string", + "icon_background_color": "string", + "splash_background_color": "string", + "splash_background_color_dark": "string" + }, + "ios": { + "bundle_id": "string", + "team_id": "string", + "credentials_status": "not_configured", + "app_store_id": "string", + "store_url": "string", + "last_build": {} + }, + "android": { + "package_name": "string", + "credentials_status": "not_configured", + "upload_key_status": "not_configured", + "store_url": "string", + "last_build": {} + }, + "ota": { + "enabled": true, + "channel": "canary", + "auto_update": true, + "update_strategy": "next-launch", + "min_native_version": "string" + } + }, "enabled": true, "name": "Installer Portal", "domain": "abc.com", @@ -2338,7 +2914,9 @@ epilot customer-portal getOrgPortalConfig -p origin=example --jsonata '$' "start_page": true, "billing": true, "change_due_date": true, - "new_design": true + "new_design": true, + "mcp_enabled": true, + "mcp_grant_version": 0 }, "accessToken": "string", "advanced_mfa": { @@ -2350,7 +2928,8 @@ epilot customer-portal getOrgPortalConfig -p origin=example --jsonata '$' }, "entry_point": "PASSWORD", "preferred_sso_providers": ["office-365-login"], - "auto_redirect_to_sso": true + "auto_redirect_to_sso": true, + "prevent_user_enumeration": true }, "cognito_details": { "cognito_user_pool_client_id": "6bsd0jkgoie74k2i8mrhc1vest", @@ -2372,7 +2951,8 @@ epilot customer-portal getOrgPortalConfig -p origin=example --jsonata '$' "require_lowercase": true, "require_uppercase": true, "require_numbers": true, - "require_symbols": true + "require_symbols": true, + "password_history_size": 3 } }, "config": "string", @@ -2433,6 +3013,16 @@ epilot customer-portal getOrgPortalConfig -p origin=example --jsonata '$' "schema": "contract" } ], + "surfaces": [ + { + "id": "website-journeys", + "name": "Website journeys", + "description": "string", + "authentication": "login", + "token_ttl_seconds": 300, + "data_access": {} + } + ], "contact_identifiers_for_account": [ { "name": "first_name", @@ -2491,11 +3081,21 @@ epilot customer-portal getOrgPortalConfig -p origin=example --jsonata '$' "inactive_contract_cutoff_years": 0, "is_dummy": true, "is_v3_item": true, + "published_revision_id": "2026-08-25T14:03:11.482Z-a7f3c1d9", "portal_id": "453ad7bf-86d5-46c8-8252-bcc868df5e3c", "portal_sk_v3": "PORTAL_CONFIG#453ad7bf-86d5-46c8-8252-bcc868df5e3c", "origin": "string", "pages": {}, "global_blocks": {}, + "notification_triggers": [ + { + "trigger_type": "entity_created", + "entity_schema": "opportunity", + "enabled": true, + "template_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + } + ], + "engagement_center_enabled": true, "id": 12345, "organization_id": 12345, "org_settings": { @@ -2522,7 +3122,8 @@ epilot customer-portal getOrgPortalConfig -p origin=example --jsonata '$' "mobile_oidc_config": {}, "provider_type": "OIDC", "attribute_mappings": {}, - "entity_matching": {} + "entity_matching": {}, + "expose_client_secret": false } ], "certificate_details": { @@ -2568,6 +3169,43 @@ epilot customer-portal getPublicPortalConfigV3 -p org_id=12324 -p portal_id=453a ```json { + "mobile_config": { + "portal_id": "string", + "enabled": true, + "display_name": "string", + "app_host": "string", + "environment": "prod", + "branding": { + "app_icon": "string", + "splash": "string", + "splash_dark": "string", + "icon_background_color": "string", + "splash_background_color": "string", + "splash_background_color_dark": "string" + }, + "ios": { + "bundle_id": "string", + "team_id": "string", + "credentials_status": "not_configured", + "app_store_id": "string", + "store_url": "string", + "last_build": {} + }, + "android": { + "package_name": "string", + "credentials_status": "not_configured", + "upload_key_status": "not_configured", + "store_url": "string", + "last_build": {} + }, + "ota": { + "enabled": true, + "channel": "canary", + "auto_update": true, + "update_strategy": "next-launch", + "min_native_version": "string" + } + }, "enabled": true, "name": "Installer Portal", "domain": "abc.com", @@ -2589,7 +3227,9 @@ epilot customer-portal getPublicPortalConfigV3 -p org_id=12324 -p portal_id=453a "start_page": true, "billing": true, "change_due_date": true, - "new_design": true + "new_design": true, + "mcp_enabled": true, + "mcp_grant_version": 0 }, "accessToken": "string", "advanced_mfa": { @@ -2601,7 +3241,8 @@ epilot customer-portal getPublicPortalConfigV3 -p org_id=12324 -p portal_id=453a }, "entry_point": "PASSWORD", "preferred_sso_providers": ["office-365-login"], - "auto_redirect_to_sso": true + "auto_redirect_to_sso": true, + "prevent_user_enumeration": true }, "cognito_details": { "cognito_user_pool_client_id": "6bsd0jkgoie74k2i8mrhc1vest", @@ -2623,7 +3264,8 @@ epilot customer-portal getPublicPortalConfigV3 -p org_id=12324 -p portal_id=453a "require_lowercase": true, "require_uppercase": true, "require_numbers": true, - "require_symbols": true + "require_symbols": true, + "password_history_size": 3 } }, "config": "string", @@ -2684,6 +3326,16 @@ epilot customer-portal getPublicPortalConfigV3 -p org_id=12324 -p portal_id=453a "schema": "contract" } ], + "surfaces": [ + { + "id": "website-journeys", + "name": "Website journeys", + "description": "string", + "authentication": "login", + "token_ttl_seconds": 300, + "data_access": {} + } + ], "contact_identifiers_for_account": [ { "name": "first_name", @@ -2742,11 +3394,21 @@ epilot customer-portal getPublicPortalConfigV3 -p org_id=12324 -p portal_id=453a "inactive_contract_cutoff_years": 0, "is_dummy": true, "is_v3_item": true, + "published_revision_id": "2026-08-25T14:03:11.482Z-a7f3c1d9", "portal_id": "453ad7bf-86d5-46c8-8252-bcc868df5e3c", "portal_sk_v3": "PORTAL_CONFIG#453ad7bf-86d5-46c8-8252-bcc868df5e3c", "origin": "string", "pages": {}, "global_blocks": {}, + "notification_triggers": [ + { + "trigger_type": "entity_created", + "entity_schema": "opportunity", + "enabled": true, + "template_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + } + ], + "engagement_center_enabled": true, "id": 12345, "organization_id": 12345, "org_settings": { @@ -2810,6 +3472,43 @@ epilot customer-portal getOrgPortalConfigV3 -p portal_id=453ad7bf-86d5-46c8-8252 ```json { + "mobile_config": { + "portal_id": "string", + "enabled": true, + "display_name": "string", + "app_host": "string", + "environment": "prod", + "branding": { + "app_icon": "string", + "splash": "string", + "splash_dark": "string", + "icon_background_color": "string", + "splash_background_color": "string", + "splash_background_color_dark": "string" + }, + "ios": { + "bundle_id": "string", + "team_id": "string", + "credentials_status": "not_configured", + "app_store_id": "string", + "store_url": "string", + "last_build": {} + }, + "android": { + "package_name": "string", + "credentials_status": "not_configured", + "upload_key_status": "not_configured", + "store_url": "string", + "last_build": {} + }, + "ota": { + "enabled": true, + "channel": "canary", + "auto_update": true, + "update_strategy": "next-launch", + "min_native_version": "string" + } + }, "enabled": true, "name": "Installer Portal", "domain": "abc.com", @@ -2831,7 +3530,9 @@ epilot customer-portal getOrgPortalConfigV3 -p portal_id=453ad7bf-86d5-46c8-8252 "start_page": true, "billing": true, "change_due_date": true, - "new_design": true + "new_design": true, + "mcp_enabled": true, + "mcp_grant_version": 0 }, "accessToken": "string", "advanced_mfa": { @@ -2843,7 +3544,8 @@ epilot customer-portal getOrgPortalConfigV3 -p portal_id=453ad7bf-86d5-46c8-8252 }, "entry_point": "PASSWORD", "preferred_sso_providers": ["office-365-login"], - "auto_redirect_to_sso": true + "auto_redirect_to_sso": true, + "prevent_user_enumeration": true }, "cognito_details": { "cognito_user_pool_client_id": "6bsd0jkgoie74k2i8mrhc1vest", @@ -2865,7 +3567,8 @@ epilot customer-portal getOrgPortalConfigV3 -p portal_id=453ad7bf-86d5-46c8-8252 "require_lowercase": true, "require_uppercase": true, "require_numbers": true, - "require_symbols": true + "require_symbols": true, + "password_history_size": 3 } }, "config": "string", @@ -2926,6 +3629,16 @@ epilot customer-portal getOrgPortalConfigV3 -p portal_id=453ad7bf-86d5-46c8-8252 "schema": "contract" } ], + "surfaces": [ + { + "id": "website-journeys", + "name": "Website journeys", + "description": "string", + "authentication": "login", + "token_ttl_seconds": 300, + "data_access": {} + } + ], "contact_identifiers_for_account": [ { "name": "first_name", @@ -2984,11 +3697,21 @@ epilot customer-portal getOrgPortalConfigV3 -p portal_id=453ad7bf-86d5-46c8-8252 "inactive_contract_cutoff_years": 0, "is_dummy": true, "is_v3_item": true, + "published_revision_id": "2026-08-25T14:03:11.482Z-a7f3c1d9", "portal_id": "453ad7bf-86d5-46c8-8252-bcc868df5e3c", "portal_sk_v3": "PORTAL_CONFIG#453ad7bf-86d5-46c8-8252-bcc868df5e3c", "origin": "string", "pages": {}, "global_blocks": {}, + "notification_triggers": [ + { + "trigger_type": "entity_created", + "entity_schema": "opportunity", + "enabled": true, + "template_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + } + ], + "engagement_center_enabled": true, "id": 12345, "organization_id": 12345, "org_settings": { @@ -3015,7 +3738,8 @@ epilot customer-portal getOrgPortalConfigV3 -p portal_id=453ad7bf-86d5-46c8-8252 "mobile_oidc_config": {}, "provider_type": "OIDC", "attribute_mappings": {}, - "entity_matching": {} + "entity_matching": {}, + "expose_client_secret": false } ], "certificate_details": { @@ -3054,6 +3778,7 @@ epilot customer-portal getAllPortalConfigs --jsonata 'data' { "data": [ { + "mobile_config": {}, "enabled": true, "name": "Installer Portal", "domain": "abc.com", @@ -3081,6 +3806,7 @@ epilot customer-portal getAllPortalConfigs --jsonata 'data' "contract_identifiers": [], "contract_selector_config": {}, "registration_identifiers": [], + "surfaces": [], "contact_identifiers_for_account": [], "additional_contact_attributes": [], "triggered_journeys": [], @@ -3091,11 +3817,14 @@ epilot customer-portal getAllPortalConfigs --jsonata 'data' "inactive_contract_cutoff_years": 0, "is_dummy": true, "is_v3_item": true, + "published_revision_id": "2026-08-25T14:03:11.482Z-a7f3c1d9", "portal_id": "453ad7bf-86d5-46c8-8252-bcc868df5e3c", "portal_sk_v3": "PORTAL_CONFIG#453ad7bf-86d5-46c8-8252-bcc868df5e3c", "origin": "string", "pages": {}, "global_blocks": {}, + "notification_triggers": [], + "engagement_center_enabled": true, "id": 12345, "organization_id": 12345, "org_settings": {}, @@ -3250,35 +3979,31 @@ epilot customer-portal upsertEmailTemplates -p origin=example --jsonata 'message --- -### `getEmailTemplatesByPortalId` - -Retrieves the email templates of a portal by portal ID +### `migrateEmailTemplateReferences` -`GET /v3/portal/email-templates/{portal_id}` +Walk every email-template config row in the caller's org and re-point any -**Parameters** +`POST /v3/portal/email-templates:migrate-references` -| Name | In | Type | Required | Description | -| ---- | -- | ---- | -------- | ----------- | -| `portal_id` | path | string | Yes | ID of the portal | +**Request Body** (required) **Sample Call** ```bash -epilot customer-portal getEmailTemplatesByPortalId \ - -p portal_id=123e4567-e89b-12d3-a456-426614174000 +epilot customer-portal migrateEmailTemplateReferences \ + -d '{"source_template_id":"string","destination_template_id":"string"}' ``` -Using positional args for path parameters: +Using stdin pipe: ```bash -epilot customer-portal getEmailTemplatesByPortalId 123e4567-e89b-12d3-a456-426614174000 +cat body.json | epilot customer-portal migrateEmailTemplateReferences ``` With JSONata filter: ```bash -epilot customer-portal getEmailTemplatesByPortalId -p portal_id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'confirmAccount' +epilot customer-portal migrateEmailTemplateReferences --jsonata 'migrated_portal_count' ```
@@ -3286,8 +4011,99 @@ epilot customer-portal getEmailTemplatesByPortalId -p portal_id=123e4567-e89b-12 ```json { - "confirmAccount": "5da0a718-c822-403d-9f5d-20d4584e0528", - "advancedAuth": "5da0a718-c822-403d-9f5d-20d4584e0528", + "migrated_portal_count": 2, + "migrated_portal_ids": ["string"], + "failed_portal_ids": ["string"] +} +``` + +
+ +--- + +### `listEmailTemplateReferences` + +Read-only sibling of migrateEmailTemplateReferences. Lists every portal in + +`POST /v3/portal/email-templates:list-references` + +**Request Body** (required) + +**Sample Call** + +```bash +epilot customer-portal listEmailTemplateReferences \ + -d '{"template_id":"string"}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot customer-portal listEmailTemplateReferences +``` + +With JSONata filter: + +```bash +epilot customer-portal listEmailTemplateReferences --jsonata 'portals' +``` + +
+Sample Response + +```json +{ + "portals": [ + { + "id": "string", + "name": "string" + } + ] +} +``` + +
+ +--- + +### `getEmailTemplatesByPortalId` + +Retrieves the email templates of a portal by portal ID + +`GET /v3/portal/email-templates/{portal_id}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `portal_id` | path | string | Yes | ID of the portal | + +**Sample Call** + +```bash +epilot customer-portal getEmailTemplatesByPortalId \ + -p portal_id=123e4567-e89b-12d3-a456-426614174000 +``` + +Using positional args for path parameters: + +```bash +epilot customer-portal getEmailTemplatesByPortalId 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot customer-portal getEmailTemplatesByPortalId -p portal_id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'confirmAccount' +``` + +
+Sample Response + +```json +{ + "confirmAccount": "5da0a718-c822-403d-9f5d-20d4584e0528", + "advancedAuth": "5da0a718-c822-403d-9f5d-20d4584e0528", "advancedMFA": "5da0a718-c822-403d-9f5d-20d4584e0528", "journeySignUp": "5da0a718-c822-403d-9f5d-20d4584e0528", "journeySignInOneTimePassword": "5da0a718-c822-403d-9f5d-20d4584e0528", @@ -4366,7 +5182,8 @@ epilot customer-portal checkContactExists \ "contract": { "contract_number": "123456" } - } + }, + "trigger_identifiers_check": true }' ``` @@ -4388,7 +5205,8 @@ epilot customer-portal checkContactExists -p origin=example --jsonata 'exists' ```json { "exists": true, - "contactId": "5da0a718-c822-403d-9f5d-20d4584e0528" + "contactId": "5da0a718-c822-403d-9f5d-20d4584e0528", + "reason": "TIMEOUT" } ``` @@ -4430,7 +5248,8 @@ epilot customer-portal checkContactExistsV3 \ "contract": { "contract_number": "123456" } - } + }, + "trigger_identifiers_check": true }' ``` @@ -4453,7 +5272,80 @@ epilot customer-portal checkContactExistsV3 --jsonata 'exists' { "exists": true, "contactId": "5da0a718-c822-403d-9f5d-20d4584e0528", - "accountId": "5da0a718-c822-403d-9f5d-20d4584e0528" + "accountId": "5da0a718-c822-403d-9f5d-20d4584e0528", + "reason": "TIMEOUT" +} +``` + +
+ +--- + +### `identifyContact` + +Identify a contact by the portal's configured registration identifiers and, on a match, + +`POST /v3/portal/public/contact/identify` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `portal_id` | query | string | No | PortalId of the portal (required if domain is not provided) | +| `domain` | query | string | No | Portal domain for identification (alternative to portal_id) | + +**Request Body** (required) + +**Sample Call** + +```bash +epilot customer-portal identifyContact +``` + +With request body: + +```bash +epilot customer-portal identifyContact \ + -d '{ + "org_id": 728, + "registration_identifiers": { + "contact": { + "email": "john.doe@example.com" + }, + "contract": { + "contract_number": "123456" + } + }, + "trigger_identifiers_check": true, + "surface_id": "website-journeys" +}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot customer-portal identifyContact +``` + +With JSONata filter: + +```bash +epilot customer-portal identifyContact --jsonata 'contact_id' +``` + +
+Sample Response + +```json +{ + "contact_id": "5da0a718-c822-403d-9f5d-20d4584e0528", + "account_id": "5da0a718-c822-403d-9f5d-20d4584e0528", + "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", + "token_type": "contact_identification", + "surface_id": "website-journeys", + "expires_at": "2026-08-11T10:35:00.000Z", + "allowed_operations": [], + "reason": "TIMEOUT" } ``` @@ -4734,6 +5626,46 @@ epilot customer-portal updatePortalUserEmail --jsonata 'message' --- +### `changePortalUserPassword` + +Hand over a password change to the third-party system configured via the `changePassword` portal extension hook. + +`PUT /v2/portal/user/change/password` + +**Request Body** + +**Sample Call** + +```bash +epilot customer-portal changePortalUserPassword \ + -d '{"new_password":"string"}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot customer-portal changePortalUserPassword +``` + +With JSONata filter: + +```bash +epilot customer-portal changePortalUserPassword --jsonata 'message' +``` + +
+Sample Response + +```json +{ + "message": "string" +} +``` + +
+ +--- + ### `resendConfirmationEmail` Resend confirmation email @@ -5194,31 +6126,59 @@ epilot customer-portal postOrderAcceptance -p id=5da0a718-c822-403d-9f5d-20d4584 --- -### `addContractByIdentifiers` +### `getContractWithTemplates` -Self-assign contract(s) by pre-configured identifiers. +Resolve Handlebars templates against a contract's related meters and return the contract with templates_output populated -`POST /v2/portal/contract/by-identifiers` +`POST /v2/portal/contract/{id}/resolve-templates` -**Request Body** (required) +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `id` | path | string (uuid) | Yes | The ID of the contract | + +**Request Body** **Sample Call** ```bash -epilot customer-portal addContractByIdentifiers \ - -d '{"contract":{"contract_number":"123456"},"meter":{"meter_number":"123456"}}' +epilot customer-portal getContractWithTemplates \ + -p id=5da0a718-c822-403d-9f5d-20d4584e0528 +``` + +With request body: + +```bash +epilot customer-portal getContractWithTemplates \ + -p id=5da0a718-c822-403d-9f5d-20d4584e0528 \ + -d '{ + "templates": {}, + "templates_ref": { + "page_id": "string", + "block_id": "string", + "config_id": "string", + "global_search_config_id": "string" + } +}' +``` + +Using positional args for path parameters: + +```bash +epilot customer-portal getContractWithTemplates 5da0a718-c822-403d-9f5d-20d4584e0528 ``` Using stdin pipe: ```bash -cat body.json | epilot customer-portal addContractByIdentifiers +cat body.json | epilot customer-portal getContractWithTemplates -p id=5da0a718-c822-403d-9f5d-20d4584e0528 ``` With JSONata filter: ```bash -epilot customer-portal addContractByIdentifiers --jsonata 'data' +epilot customer-portal getContractWithTemplates -p id=5da0a718-c822-403d-9f5d-20d4584e0528 --jsonata 'entity._title' ```
@@ -5226,7 +6186,41 @@ epilot customer-portal addContractByIdentifiers --jsonata 'data' ```json { - "data": [ + "entity": { + "_id": "5da0a718-c822-403d-9f5d-20d4584e0528", + "_title": "Example Entity", + "_org": "123", + "_tags": ["example", "mock"], + "_created_at": "2021-02-09T12:41:43.662Z", + "_updated_at": "2021-02-09T12:41:43.662Z", + "templates_output_highlighted": {}, + "search_snippets": [ + {} + ], + "contract_name": "Grid Contract", + "contract_number": "12345", + "status": "approved", + "description": "This contract is for the supply of widgets.", + "account_number": "67890", + "branch": "power", + "billing_address": "123 Main St, Anytown", + "delivery_address": "456 Elm St, Anytown", + "additional_addresses": "789 Oak St, Anytown", + "termination_date": "2022-01-01", + "termination_reason": "Non-payment", + "billing_period": "monthly", + "billing_duration_amount": 30, + "renewal_duration_amount": 365, + "renewal_duration_unit": "years", + "notice_time_amount": 30, + "notice_time_unit": "months", + "start_date": "2021-01-01", + "billing_due_day": 2, + "installment_amount": 10050, + "balance": 8990, + "balance_currency": "EUR" + }, + "orders": [ { "_id": "5da0a718-c822-403d-9f5d-20d4584e0528", "_title": "Example Entity", @@ -5235,48 +6229,162 @@ epilot customer-portal addContractByIdentifiers --jsonata 'data' "_created_at": "2021-02-09T12:41:43.662Z", "_updated_at": "2021-02-09T12:41:43.662Z", "templates_output_highlighted": {}, - "search_snippets": [ - { - "field": "string", - "fragment": "string" - } - ], - "templates_output": { - "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" - } - }, - "_schema": "contact" + "search_snippets": [], + "_schema": "order" } ], - "hits": 0 -} -``` - -
- ---- - -### `getEntityIdentifiers` - -Retrieve a list of entity identifiers used for entity search by portal users. - -`GET /v2/portal/entity/identifiers/{slug}` - -**Parameters** - -| Name | In | Type | Required | Description | -| ---- | -- | ---- | -------- | ----------- | -| `slug` | path | string | Yes | The slug of an entity | - -**Sample Call** - -```bash -epilot customer-portal getEntityIdentifiers \ + "meters": [ + { + "_id": "5da0a718-c822-403d-9f5d-20d4584e0528", + "_title": "Example Entity", + "_org": "123", + "_tags": ["example", "mock"], + "_created_at": "2021-02-09T12:41:43.662Z", + "_updated_at": "2021-02-09T12:41:43.662Z", + "templates_output_highlighted": {}, + "search_snippets": [], + "_schema": "meter", + "templates_output": {} + } + ], + "files": [ + { + "_id": "5da0a718-c822-403d-9f5d-20d4584e0528", + "_title": "Example Entity", + "_org": "123", + "_tags": ["example", "mock"], + "_created_at": "2021-02-09T12:41:43.662Z", + "_updated_at": "2021-02-09T12:41:43.662Z", + "templates_output_highlighted": {}, + "search_snippets": [], + "_schema": "file" + } + ], + "relations": [ + { + "_id": "5da0a718-c822-403d-9f5d-20d4584e0528", + "_title": "Example Entity", + "_org": "123", + "_tags": ["example", "mock"], + "_created_at": "2021-02-09T12:41:43.662Z", + "_updated_at": "2021-02-09T12:41:43.662Z", + "templates_output_highlighted": {}, + "search_snippets": [], + "templates_output": {}, + "_schema": "contact" + } + ], + "workflow": [ + { + "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": [] + } + ], + "journey_actions": [ + { + "journey_id": "string", + "action_label": {}, + "slug": "string", + "rules": [] + } + ] +} +``` + +
+ +--- + +### `addContractByIdentifiers` + +Self-assign contract(s) by pre-configured identifiers. + +`POST /v2/portal/contract/by-identifiers` + +**Request Body** (required) + +**Sample Call** + +```bash +epilot customer-portal addContractByIdentifiers \ + -d '{"contract":{"contract_number":"123456"},"meter":{"meter_number":"123456"}}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot customer-portal addContractByIdentifiers +``` + +With JSONata filter: + +```bash +epilot customer-portal addContractByIdentifiers --jsonata 'data' +``` + +
+Sample Response + +```json +{ + "data": [ + { + "_id": "5da0a718-c822-403d-9f5d-20d4584e0528", + "_title": "Example Entity", + "_org": "123", + "_tags": ["example", "mock"], + "_created_at": "2021-02-09T12:41:43.662Z", + "_updated_at": "2021-02-09T12:41:43.662Z", + "templates_output_highlighted": {}, + "search_snippets": [ + { + "field": "string", + "fragment": "string" + } + ], + "templates_output": { + "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" + } + }, + "_schema": "contact" + } + ], + "hits": 0 +} +``` + +
+ +--- + +### `getEntityIdentifiers` + +Retrieve a list of entity identifiers used for entity search by portal users. + +`GET /v2/portal/entity/identifiers/{slug}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `slug` | path | string | Yes | The slug of an entity | + +**Sample Call** + +```bash +epilot customer-portal getEntityIdentifiers \ -p slug=contact ``` @@ -5800,6 +6908,53 @@ epilot customer-portal getRegistrationIdentifiers --jsonata 'data' --- +### `getFilePreview` + +resolves an in-portal preview for a file. Returns a Content-Disposition: inline URL for directly-previewable files (PDF, + +`GET /v2/portal/user/file/{id}/preview` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `id` | path | string (uuid) | Yes | The Id of a file | + +**Sample Call** + +```bash +epilot customer-portal getFilePreview \ + -p id=5da0a718-c822-403d-9f5d-20d4584e0528 +``` + +Using positional args for path parameters: + +```bash +epilot customer-portal getFilePreview 5da0a718-c822-403d-9f5d-20d4584e0528 +``` + +With JSONata filter: + +```bash +epilot customer-portal getFilePreview -p id=5da0a718-c822-403d-9f5d-20d4584e0528 --jsonata 'kind' +``` + +
+Sample Response + +```json +{ + "kind": "pdf", + "url": "https://example.com/path", + "requires_auth": true, + "download_url": "https://example.com/path" +} +``` + +
+ +--- + ### `trackFileDownloaded` Track that user has downloaded a file @@ -5864,7 +7019,8 @@ epilot customer-portal trackFileDownloaded -p id=5da0a718-c822-403d-9f5d-20d4584 "_title": "Opportunity ABC" } ], - "is_new": true + "is_new": true, + "custom_download_url_auth": "token" } } ``` @@ -6252,6 +7408,12 @@ epilot customer-portal getPortalUserEntity \ "subtitle": "{{contract.contract_number}}" } }, + "templates_ref": { + "page_id": "string", + "block_id": "string", + "config_id": "string", + "global_search_config_id": "string" + }, "filters": [ { "term": { @@ -6364,6 +7526,12 @@ epilot customer-portal searchPortalUserEntities \ "main_content_name": "{{contract.contract_name}} ({{contract.contract_number}})", "content_bottom_name": "{{custom_contract_delivery_address}}" }, + "templates_ref": { + "page_id": "string", + "block_id": "string", + "config_id": "string", + "global_search_config_id": "string" + }, "filters": [ { "term": { @@ -6700,42 +7868,22 @@ epilot customer-portal getEntityWorkflows -p slug=contact -p id=abc123 --jsonata --- -### `uploadMeterReadingPhoto` - -Uploads a Meter Reading photo and - if enabled - gives back data extracted from the photo. +### `getOutstandingTasks` -`POST /v2/portal/metering/reading/photo` +Returns all outstanding workflow journey tasks for the authenticated portal user, across their opportunity, order and co -**Request Body** (required) +`GET /v2/portal/engagement/tasks` **Sample Call** ```bash -epilot customer-portal uploadMeterReadingPhoto -``` - -With request body: - -```bash -epilot customer-portal uploadMeterReadingPhoto \ - -d '{ - "filename": "Reading 10.01.2025.jpg", - "mime_type": "image/jpeg", - "contents": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBxMTEhUTExMWFhUXGBgYGBgYGBgYGBgYGBgYFxgYFxgYHSggGBolHRgXITEhJSkrLi4uFx8zODMtNygtLisBCgoKDg0OGhAQGy0lHyUt", - "meter_id": "5da0a718-c822-403d-9f5d-20d4584e0528" -}' -``` - -Using stdin pipe: - -```bash -cat body.json | epilot customer-portal uploadMeterReadingPhoto +epilot customer-portal getOutstandingTasks ``` With JSONata filter: ```bash -epilot customer-portal uploadMeterReadingPhoto --jsonata 'data' +epilot customer-portal getOutstandingTasks --jsonata 'tasks' ```
@@ -6743,17 +7891,20 @@ epilot customer-portal uploadMeterReadingPhoto --jsonata 'data' ```json { - "data": { - "filename": "Reading 10.01.2025.jpg", - "s3ref": { - "bucket": "meter-readings", - "key": "uuid/reading-10.01.2025.jpg" - }, - "reading": "000123.45", - "sector": "water", - "meter_numbers": ["00123456"], - "file_id": "abc123def456" - } + "tasks": [ + { + "entity_id": "string", + "entity_schema": "string", + "entity_title": "string", + "workflow_id": "string", + "workflow_name": "string", + "step_id": "string", + "step_name": "string", + "journey_id": "string", + "complete_task_automatically": true + } + ], + "total": 0 } ``` @@ -6761,56 +7912,281 @@ epilot customer-portal uploadMeterReadingPhoto --jsonata 'data' --- -### `createMeterReading` +### `getEntityPortalWorkflows` -Inserts a new meter reading. +Get all portal-relevant workflows associated with an entity (requires access to the entity), -`POST /v2/portal/metering/reading` +`GET /v2/portal/entity/{slug}/{id}/workflows/linearized` **Parameters** | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `override_plausibility` | query | boolean | No | Override plausibility check | - -**Request Body** (required) +| `slug` | path | string | Yes | | +| `id` | path | string | Yes | | **Sample Call** ```bash -epilot customer-portal createMeterReading +epilot customer-portal getEntityPortalWorkflows \ + -p slug=opportunity \ + -p id=abc123 ``` -With request body: +Using positional args for path parameters: ```bash -epilot customer-portal createMeterReading \ - -d '{ - "value": 240, - "read_by": "John Doe", - "reason": "Storing the feed-in record", - "meter_id": "5da0a718-c822-403d-9f5d-20d4584e0528", - "counter_id": "5da0a718-c822-403d-9f5d-20d4584e0528", - "direction": "feed-in", - "timestamp": "2022-10-10", - "source": "ECP", - "status": "valid", - "external_id": "string", - "remark": "Customer reported unusual consumption", - "metadata": { - "registration_id": "1234567890", - "business_unit": "ABC" - } -}' +epilot customer-portal getEntityPortalWorkflows opportunity abc123 ``` -Using stdin pipe: +With JSONata filter: ```bash -cat body.json | epilot customer-portal createMeterReading +epilot customer-portal getEntityPortalWorkflows -p slug=opportunity -p id=abc123 --jsonata 'portal_workflows' ``` -With JSONata filter: +
+Sample Response + +```json +{ + "portal_workflows": [ + { + "id": "string", + "definition_id": "string", + "name": "string", + "status": "STARTED", + "version": 2, + "created_at": "string", + "updated_at": "string", + "completed_at": "string", + "due_date": "string", + "assigned_to": ["string"], + "contexts": [ + { + "entity_id": "string", + "entity_schema": "string", + "is_primary": true + } + ], + "is_path_complete": true, + "tasks": [ + { + "id": "string", + "name": "string", + "order": 0, + "status": "COMPLETED", + "is_active": true, + "ecp": { + "enabled": true, + "label": "string", + "description": "string", + "journey": { + "id": "string", + "journeyId": "string", + "name": "string", + "complete_task_automatically": true + } + }, + "installer": { + "enabled": true, + "label": "string", + "description": "string", + "journey": { + "id": "string", + "journeyId": "string", + "name": "string", + "complete_task_automatically": true + } + }, + "journey": { + "id": "string", + "journeyId": "string", + "name": "string", + "complete_task_automatically": true + }, + "assigned_to": ["string"], + "phase_id": "string", + "phase_name": "string", + "stage_id": "string", + "completed_at": "string", + "updated_at": "string" + } + ], + "stages": [ + { + "id": "string", + "name": "string", + "description": "string", + "status": "COMPLETED", + "completed_at": "string" + } + ] + } + ] +} +``` + +
+ +--- + +### `getEntityPortalWorkflowsBatch` + +Batch variant of `getEntityPortalWorkflows`: returns portal-relevant workflows for + +`POST /v2/portal/entities/workflows/linearized/batch` + +**Request Body** (required) + +**Sample Call** + +```bash +epilot customer-portal getEntityPortalWorkflowsBatch \ + -d '{"entities":[{"id":"string","slug":"order"}]}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot customer-portal getEntityPortalWorkflowsBatch +``` + +With JSONata filter: + +```bash +epilot customer-portal getEntityPortalWorkflowsBatch --jsonata 'results[0]' +``` + +
+Sample Response + +```json +{ + "results": [ + { + "entity_id": "string", + "portal_workflows": [] + } + ] +} +``` + +
+ +--- + +### `uploadMeterReadingPhoto` + +Uploads a Meter Reading photo and - if enabled - gives back data extracted from the photo. + +`POST /v2/portal/metering/reading/photo` + +**Request Body** (required) + +**Sample Call** + +```bash +epilot customer-portal uploadMeterReadingPhoto +``` + +With request body: + +```bash +epilot customer-portal uploadMeterReadingPhoto \ + -d '{ + "filename": "Reading 10.01.2025.jpg", + "mime_type": "image/jpeg", + "contents": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBxMTEhUTExMWFhUXGBgYGBgYGBgYGBgYGBgYFxgYFxgYHSggGBolHRgXITEhJSkrLi4uFx8zODMtNygtLisBCgoKDg0OGhAQGy0lHyUt", + "meter_id": "5da0a718-c822-403d-9f5d-20d4584e0528" +}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot customer-portal uploadMeterReadingPhoto +``` + +With JSONata filter: + +```bash +epilot customer-portal uploadMeterReadingPhoto --jsonata 'data' +``` + +
+Sample Response + +```json +{ + "data": { + "filename": "Reading 10.01.2025.jpg", + "s3ref": { + "bucket": "meter-readings", + "key": "uuid/reading-10.01.2025.jpg" + }, + "reading": "000123.45", + "sector": "water", + "meter_numbers": ["00123456"], + "file_id": "abc123def456" + } +} +``` + +
+ +--- + +### `createMeterReading` + +Inserts a new meter reading. + +`POST /v2/portal/metering/reading` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `override_plausibility` | query | boolean | No | Override plausibility check | + +**Request Body** (required) + +**Sample Call** + +```bash +epilot customer-portal createMeterReading +``` + +With request body: + +```bash +epilot customer-portal createMeterReading \ + -d '{ + "value": 240, + "read_by": "John Doe", + "reason": "Storing the feed-in record", + "meter_id": "5da0a718-c822-403d-9f5d-20d4584e0528", + "counter_id": "5da0a718-c822-403d-9f5d-20d4584e0528", + "direction": "feed-in", + "timestamp": "2022-10-10", + "source": "ECP", + "status": "valid", + "external_id": "string", + "remark": "Customer reported unusual consumption", + "metadata": { + "registration_id": "1234567890", + "business_unit": "ABC" + } +}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot customer-portal createMeterReading +``` + +With JSONata filter: ```bash epilot customer-portal createMeterReading --jsonata 'data' @@ -6908,9 +8284,29 @@ Fetches meter readings for a counter and optionally resolves Handlebars **Sample Call** +```bash +epilot customer-portal getMeterReadings +``` + +With request body: + ```bash epilot customer-portal getMeterReadings \ - -d '{"meter_id":"string","counter_id":"string","sort":"desc","from":0,"size":10,"templates":{},"counter_templates":{}}' + -d '{ + "meter_id": "string", + "counter_id": "string", + "sort": "desc", + "from": 0, + "size": 10, + "templates": {}, + "counter_templates": {}, + "templates_ref": { + "page_id": "string", + "block_id": "string", + "config_id": "string", + "global_search_config_id": "string" + } +}' ``` Using stdin pipe: @@ -7019,6 +8415,77 @@ epilot customer-portal getSSOProvider -p provider_slug=office-365-login --jsonat --- +### `getPublicSSOProviderV3` + +Returns the public configuration of a single SSO identity provider with env var + +`GET /v3/portal/public/sso/providers/{provider_slug}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `provider_slug` | path | string | Yes | Provider slug (organization-unique) | +| `org_id` | query | string | Yes | epilot organization id | +| `portal_id` | query | string | Yes | ID of the Portal | + +**Sample Call** + +```bash +epilot customer-portal getPublicSSOProviderV3 \ + -p provider_slug=office-365-login \ + -p org_id=123 \ + -p portal_id=453ad7bf-86d5-46c8-8252-bcc868df5e3c +``` + +Using positional args for path parameters: + +```bash +epilot customer-portal getPublicSSOProviderV3 office-365-login +``` + +With JSONata filter: + +```bash +epilot customer-portal getPublicSSOProviderV3 -p provider_slug=office-365-login -p org_id=123 -p portal_id=453ad7bf-86d5-46c8-8252-bcc868df5e3c --jsonata 'slug' +``` + +
+Sample Response + +```json +{ + "slug": "office-365-login", + "display_name": "Office 365 Login", + "oidc_config": { + "type": "implicit", + "oidc_issuer": "https://login.microsoftonline.com/33d4f3e5-3df2-421e-b92e-a63cfa680a88/v2.0", + "redirect_uri": "https://customer-portal.com/login", + "client_id": "ab81daf8-8b1f-42d6-94ca-c51621054c75", + "client_secret": "7BIUnn~6shh.7fNtXb..3k1Mp3s6k6WK3B", + "has_client_secret": true, + "scope": "openid email", + "metadata": { + "response_modes_supported": ["form_post"], + "authorization_endpoint": "https://www.facebook.com/v12.0/dialog/oauth", + "token_endpoint": "https://graph.facebook.com/v12.0/oauth/access_token", + "userinfo_endpoint": "https://graph.facebook.com/me", + "logout_uri": "https://login.microsoftonline.com/common/oauth2/v2.0/logout", + "logout_redirect_uri": "https://customer-portal.com/login", + "skip_login_as_logout": false, + "mobile_redirect_uri": "msauth.io.epilot.ecp://auth", + "test_auth_username": "test@epilot.io", + "test_auth_password": "string" + }, + "prompt": "login" + } +} +``` + +
+ +--- + ### `ssoLogin` Initiate login using external SSO identity. @@ -7091,7 +8558,7 @@ Initiate login using external SSO identity. ```bash epilot customer-portal ssoLoginV3 \ - -d '{"provider_slug":"office-365-login"}' + -d '{"provider_slug":"office-365-login","login_only":false}' ``` Using stdin pipe: @@ -7176,7 +8643,7 @@ epilot customer-portal ssoCallback \ "token_endpoint": "https://www.facebook.com/v12.0/dialog/oauth", "grant_type": "authorization_code", "code": "123456", - "redirect_uri": "https://customer-portal.com/login", + "redirect_uri": "https://customer-portal.com/sso", "client_id": "123456", "code_verifier": "123456" }' @@ -8376,13 +9843,154 @@ epilot customer-portal updateCampaignPortalBlockStatus -p campaign_id=123e4567-e --- -### `updateNotificationsStatus` +### `listPortalNotifications` -Updates the statuses of multiple notifications at once. +Lists the 360 notifications addressed to the authenticated portal user, newest first. The organization and the portal us -`PUT /v2/portal/notifications/entity:status` +`GET /v2/portal/notifications` -**Request Body** (required) +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `cursor` | query | string | No | Base64 encoded cursor returned by a previous call, used for pagination. | +| `limit` | query | number | No | The maximum number of notifications to return. | + +**Sample Call** + +```bash +epilot customer-portal listPortalNotifications +``` + +With JSONata filter: + +```bash +epilot customer-portal listPortalNotifications --jsonata 'results[0]' +``` + +
+Sample Response + +```json +{ + "cursor": "string", + "total_unread": 0, + "results": [ + { + "id": "1234567890", + "notification_id": 1234567890, + "type": "workflow_step_overdue", + "title": { + "en": "string", + "de": "string" + }, + "message": { + "en": "string", + "de": "string" + }, + "created_at": "1970-01-01T00:00:00.000Z", + "read": false, + "redirect_url": "string" + } + ] +} +``` + +
+ +--- + +### `getPortalNotificationsUnreadCount` + +Returns the number of unread notifications for the authenticated portal user. + +`GET /v2/portal/notifications/unread-count` + +**Sample Call** + +```bash +epilot customer-portal getPortalNotificationsUnreadCount +``` + +With JSONata filter: + +```bash +epilot customer-portal getPortalNotificationsUnreadCount --jsonata 'count' +``` + +
+Sample Response + +```json +{ + "count": 3 +} +``` + +
+ +--- + +### `markAllPortalNotificationsRead` + +Marks all notifications of the authenticated portal user as read. + +`PUT /v2/portal/notifications/read-all` + +**Sample Call** + +```bash +epilot customer-portal markAllPortalNotificationsRead +``` + +With JSONata filter: + +```bash +epilot customer-portal markAllPortalNotificationsRead --jsonata '$' +``` + +--- + +### `markPortalNotificationRead` + +Marks a single notification of the authenticated portal user as read. + +`PUT /v2/portal/notifications/{id}/read` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `id` | path | number | Yes | Numeric id of the notification to mark as read. | + +**Sample Call** + +```bash +epilot customer-portal markPortalNotificationRead \ + -p id=1 +``` + +Using positional args for path parameters: + +```bash +epilot customer-portal markPortalNotificationRead 1 +``` + +With JSONata filter: + +```bash +epilot customer-portal markPortalNotificationRead -p id=1 --jsonata '$' +``` + +--- + +### `updateNotificationsStatus` + +Updates the statuses of multiple notifications at once. + +`PUT /v2/portal/notifications/entity:status` + +**Request Body** (required) **Sample Call** @@ -8561,9 +10169,47 @@ epilot customer-portal createPortalConfig \ "attribute_mappings": {}, "entity_matching": {}, "oidc_config": {}, - "mobile_oidc_config": {} + "mobile_oidc_config": {}, + "expose_client_secret": false + } + ], + "mobile_config": { + "portal_id": "string", + "enabled": true, + "display_name": "string", + "app_host": "string", + "environment": "prod", + "branding": { + "app_icon": "string", + "splash": "string", + "splash_dark": "string", + "icon_background_color": "string", + "splash_background_color": "string", + "splash_background_color_dark": "string" + }, + "ios": { + "bundle_id": "string", + "team_id": "string", + "credentials_status": "not_configured", + "app_store_id": "string", + "store_url": "string", + "last_build": {} + }, + "android": { + "package_name": "string", + "credentials_status": "not_configured", + "upload_key_status": "not_configured", + "store_url": "string", + "last_build": {} + }, + "ota": { + "enabled": true, + "channel": "canary", + "auto_update": true, + "update_strategy": "next-launch", + "min_native_version": "string" } - ], + }, "enabled": true, "name": "Installer Portal", "domain": "abc.com", @@ -8585,7 +10231,9 @@ epilot customer-portal createPortalConfig \ "start_page": true, "billing": true, "change_due_date": true, - "new_design": true + "new_design": true, + "mcp_enabled": true, + "mcp_grant_version": 0 }, "accessToken": "string", "advanced_mfa": { @@ -8597,7 +10245,8 @@ epilot customer-portal createPortalConfig \ }, "entry_point": "PASSWORD", "preferred_sso_providers": ["office-365-login"], - "auto_redirect_to_sso": true + "auto_redirect_to_sso": true, + "prevent_user_enumeration": true }, "cognito_details": { "cognito_user_pool_client_id": "6bsd0jkgoie74k2i8mrhc1vest", @@ -8619,7 +10268,8 @@ epilot customer-portal createPortalConfig \ "require_lowercase": true, "require_uppercase": true, "require_numbers": true, - "require_symbols": true + "require_symbols": true, + "password_history_size": 3 } }, "config": "string", @@ -8680,6 +10330,16 @@ epilot customer-portal createPortalConfig \ "schema": "contract" } ], + "surfaces": [ + { + "id": "website-journeys", + "name": "Website journeys", + "description": "string", + "authentication": "login", + "token_ttl_seconds": 300, + "data_access": {} + } + ], "contact_identifiers_for_account": [ { "name": "first_name", @@ -8738,6 +10398,7 @@ epilot customer-portal createPortalConfig \ "inactive_contract_cutoff_years": 0, "is_dummy": true, "is_v3_item": true, + "published_revision_id": "2026-08-25T14:03:11.482Z-a7f3c1d9", "portal_id": "453ad7bf-86d5-46c8-8252-bcc868df5e3c", "portal_sk_v3": "PORTAL_CONFIG#453ad7bf-86d5-46c8-8252-bcc868df5e3c", "origin": "string", @@ -8810,9 +10471,47 @@ epilot customer-portal createPortalConfig --jsonata '$' "attribute_mappings": {}, "entity_matching": {}, "oidc_config": {}, - "mobile_oidc_config": {} + "mobile_oidc_config": {}, + "expose_client_secret": false + } + ], + "mobile_config": { + "portal_id": "string", + "enabled": true, + "display_name": "string", + "app_host": "string", + "environment": "prod", + "branding": { + "app_icon": "string", + "splash": "string", + "splash_dark": "string", + "icon_background_color": "string", + "splash_background_color": "string", + "splash_background_color_dark": "string" + }, + "ios": { + "bundle_id": "string", + "team_id": "string", + "credentials_status": "not_configured", + "app_store_id": "string", + "store_url": "string", + "last_build": {} + }, + "android": { + "package_name": "string", + "credentials_status": "not_configured", + "upload_key_status": "not_configured", + "store_url": "string", + "last_build": {} + }, + "ota": { + "enabled": true, + "channel": "canary", + "auto_update": true, + "update_strategy": "next-launch", + "min_native_version": "string" } - ], + }, "enabled": true, "name": "Installer Portal", "domain": "abc.com", @@ -8834,7 +10533,9 @@ epilot customer-portal createPortalConfig --jsonata '$' "start_page": true, "billing": true, "change_due_date": true, - "new_design": true + "new_design": true, + "mcp_enabled": true, + "mcp_grant_version": 0 }, "accessToken": "string", "advanced_mfa": { @@ -8846,7 +10547,8 @@ epilot customer-portal createPortalConfig --jsonata '$' }, "entry_point": "PASSWORD", "preferred_sso_providers": ["office-365-login"], - "auto_redirect_to_sso": true + "auto_redirect_to_sso": true, + "prevent_user_enumeration": true }, "cognito_details": { "cognito_user_pool_client_id": "6bsd0jkgoie74k2i8mrhc1vest", @@ -8868,7 +10570,8 @@ epilot customer-portal createPortalConfig --jsonata '$' "require_lowercase": true, "require_uppercase": true, "require_numbers": true, - "require_symbols": true + "require_symbols": true, + "password_history_size": 3 } }, "config": "string", @@ -8929,6 +10632,16 @@ epilot customer-portal createPortalConfig --jsonata '$' "schema": "contract" } ], + "surfaces": [ + { + "id": "website-journeys", + "name": "Website journeys", + "description": "string", + "authentication": "login", + "token_ttl_seconds": 300, + "data_access": {} + } + ], "contact_identifiers_for_account": [ { "name": "first_name", @@ -8987,6 +10700,7 @@ epilot customer-portal createPortalConfig --jsonata '$' "inactive_contract_cutoff_years": 0, "is_dummy": true, "is_v3_item": true, + "published_revision_id": "2026-08-25T14:03:11.482Z-a7f3c1d9", "portal_id": "453ad7bf-86d5-46c8-8252-bcc868df5e3c", "portal_sk_v3": "PORTAL_CONFIG#453ad7bf-86d5-46c8-8252-bcc868df5e3c", "origin": "string", @@ -9103,9 +10817,47 @@ epilot customer-portal getPortalConfigV3 -p portal_id=5da0a718-c822-403d-9f5d-20 "attribute_mappings": {}, "entity_matching": {}, "oidc_config": {}, - "mobile_oidc_config": {} + "mobile_oidc_config": {}, + "expose_client_secret": false + } + ], + "mobile_config": { + "portal_id": "string", + "enabled": true, + "display_name": "string", + "app_host": "string", + "environment": "prod", + "branding": { + "app_icon": "string", + "splash": "string", + "splash_dark": "string", + "icon_background_color": "string", + "splash_background_color": "string", + "splash_background_color_dark": "string" + }, + "ios": { + "bundle_id": "string", + "team_id": "string", + "credentials_status": "not_configured", + "app_store_id": "string", + "store_url": "string", + "last_build": {} + }, + "android": { + "package_name": "string", + "credentials_status": "not_configured", + "upload_key_status": "not_configured", + "store_url": "string", + "last_build": {} + }, + "ota": { + "enabled": true, + "channel": "canary", + "auto_update": true, + "update_strategy": "next-launch", + "min_native_version": "string" } - ], + }, "enabled": true, "name": "Installer Portal", "domain": "abc.com", @@ -9127,7 +10879,9 @@ epilot customer-portal getPortalConfigV3 -p portal_id=5da0a718-c822-403d-9f5d-20 "start_page": true, "billing": true, "change_due_date": true, - "new_design": true + "new_design": true, + "mcp_enabled": true, + "mcp_grant_version": 0 }, "accessToken": "string", "advanced_mfa": { @@ -9139,7 +10893,8 @@ epilot customer-portal getPortalConfigV3 -p portal_id=5da0a718-c822-403d-9f5d-20 }, "entry_point": "PASSWORD", "preferred_sso_providers": ["office-365-login"], - "auto_redirect_to_sso": true + "auto_redirect_to_sso": true, + "prevent_user_enumeration": true }, "cognito_details": { "cognito_user_pool_client_id": "6bsd0jkgoie74k2i8mrhc1vest", @@ -9161,7 +10916,8 @@ epilot customer-portal getPortalConfigV3 -p portal_id=5da0a718-c822-403d-9f5d-20 "require_lowercase": true, "require_uppercase": true, "require_numbers": true, - "require_symbols": true + "require_symbols": true, + "password_history_size": 3 } }, "config": "string", @@ -9222,6 +10978,16 @@ epilot customer-portal getPortalConfigV3 -p portal_id=5da0a718-c822-403d-9f5d-20 "schema": "contract" } ], + "surfaces": [ + { + "id": "website-journeys", + "name": "Website journeys", + "description": "string", + "authentication": "login", + "token_ttl_seconds": 300, + "data_access": {} + } + ], "contact_identifiers_for_account": [ { "name": "first_name", @@ -9280,6 +11046,7 @@ epilot customer-portal getPortalConfigV3 -p portal_id=5da0a718-c822-403d-9f5d-20 "inactive_contract_cutoff_years": 0, "is_dummy": true, "is_v3_item": true, + "published_revision_id": "2026-08-25T14:03:11.482Z-a7f3c1d9", "portal_id": "453ad7bf-86d5-46c8-8252-bcc868df5e3c", "portal_sk_v3": "PORTAL_CONFIG#453ad7bf-86d5-46c8-8252-bcc868df5e3c", "origin": "string", @@ -9343,6 +11110,9 @@ Updates a specific portal configuration by ID. | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | | `portal_id` | path | string (uuid) | Yes | Portal ID (readonly UUID generated on portal creation) | +| `page_upsert_mode` | query | "id" \| "slug" | No | Determines how pages are matched for upsert operations: +- `id` (default): Match pages by their ID. Use this when page IDs are stable and known upfront. +- `slug`: Match pages by their slug. When a requ | **Request Body** (required) @@ -9387,9 +11157,47 @@ epilot customer-portal putPortalConfig \ "attribute_mappings": {}, "entity_matching": {}, "oidc_config": {}, - "mobile_oidc_config": {} + "mobile_oidc_config": {}, + "expose_client_secret": false + } + ], + "mobile_config": { + "portal_id": "string", + "enabled": true, + "display_name": "string", + "app_host": "string", + "environment": "prod", + "branding": { + "app_icon": "string", + "splash": "string", + "splash_dark": "string", + "icon_background_color": "string", + "splash_background_color": "string", + "splash_background_color_dark": "string" + }, + "ios": { + "bundle_id": "string", + "team_id": "string", + "credentials_status": "not_configured", + "app_store_id": "string", + "store_url": "string", + "last_build": {} + }, + "android": { + "package_name": "string", + "credentials_status": "not_configured", + "upload_key_status": "not_configured", + "store_url": "string", + "last_build": {} + }, + "ota": { + "enabled": true, + "channel": "canary", + "auto_update": true, + "update_strategy": "next-launch", + "min_native_version": "string" } - ], + }, "enabled": true, "name": "Installer Portal", "domain": "abc.com", @@ -9411,7 +11219,9 @@ epilot customer-portal putPortalConfig \ "start_page": true, "billing": true, "change_due_date": true, - "new_design": true + "new_design": true, + "mcp_enabled": true, + "mcp_grant_version": 0 }, "accessToken": "string", "advanced_mfa": { @@ -9423,7 +11233,8 @@ epilot customer-portal putPortalConfig \ }, "entry_point": "PASSWORD", "preferred_sso_providers": ["office-365-login"], - "auto_redirect_to_sso": true + "auto_redirect_to_sso": true, + "prevent_user_enumeration": true }, "cognito_details": { "cognito_user_pool_client_id": "6bsd0jkgoie74k2i8mrhc1vest", @@ -9445,7 +11256,8 @@ epilot customer-portal putPortalConfig \ "require_lowercase": true, "require_uppercase": true, "require_numbers": true, - "require_symbols": true + "require_symbols": true, + "password_history_size": 3 } }, "config": "string", @@ -9506,6 +11318,16 @@ epilot customer-portal putPortalConfig \ "schema": "contract" } ], + "surfaces": [ + { + "id": "website-journeys", + "name": "Website journeys", + "description": "string", + "authentication": "login", + "token_ttl_seconds": 300, + "data_access": {} + } + ], "contact_identifiers_for_account": [ { "name": "first_name", @@ -9564,6 +11386,7 @@ epilot customer-portal putPortalConfig \ "inactive_contract_cutoff_years": 0, "is_dummy": true, "is_v3_item": true, + "published_revision_id": "2026-08-25T14:03:11.482Z-a7f3c1d9", "portal_id": "453ad7bf-86d5-46c8-8252-bcc868df5e3c", "portal_sk_v3": "PORTAL_CONFIG#453ad7bf-86d5-46c8-8252-bcc868df5e3c", "origin": "string", @@ -9663,9 +11486,47 @@ epilot customer-portal putPortalConfig -p portal_id=5da0a718-c822-403d-9f5d-20d4 "attribute_mappings": {}, "entity_matching": {}, "oidc_config": {}, - "mobile_oidc_config": {} + "mobile_oidc_config": {}, + "expose_client_secret": false + } + ], + "mobile_config": { + "portal_id": "string", + "enabled": true, + "display_name": "string", + "app_host": "string", + "environment": "prod", + "branding": { + "app_icon": "string", + "splash": "string", + "splash_dark": "string", + "icon_background_color": "string", + "splash_background_color": "string", + "splash_background_color_dark": "string" + }, + "ios": { + "bundle_id": "string", + "team_id": "string", + "credentials_status": "not_configured", + "app_store_id": "string", + "store_url": "string", + "last_build": {} + }, + "android": { + "package_name": "string", + "credentials_status": "not_configured", + "upload_key_status": "not_configured", + "store_url": "string", + "last_build": {} + }, + "ota": { + "enabled": true, + "channel": "canary", + "auto_update": true, + "update_strategy": "next-launch", + "min_native_version": "string" } - ], + }, "enabled": true, "name": "Installer Portal", "domain": "abc.com", @@ -9687,7 +11548,9 @@ epilot customer-portal putPortalConfig -p portal_id=5da0a718-c822-403d-9f5d-20d4 "start_page": true, "billing": true, "change_due_date": true, - "new_design": true + "new_design": true, + "mcp_enabled": true, + "mcp_grant_version": 0 }, "accessToken": "string", "advanced_mfa": { @@ -9699,7 +11562,8 @@ epilot customer-portal putPortalConfig -p portal_id=5da0a718-c822-403d-9f5d-20d4 }, "entry_point": "PASSWORD", "preferred_sso_providers": ["office-365-login"], - "auto_redirect_to_sso": true + "auto_redirect_to_sso": true, + "prevent_user_enumeration": true }, "cognito_details": { "cognito_user_pool_client_id": "6bsd0jkgoie74k2i8mrhc1vest", @@ -9721,7 +11585,8 @@ epilot customer-portal putPortalConfig -p portal_id=5da0a718-c822-403d-9f5d-20d4 "require_lowercase": true, "require_uppercase": true, "require_numbers": true, - "require_symbols": true + "require_symbols": true, + "password_history_size": 3 } }, "config": "string", @@ -9782,6 +11647,16 @@ epilot customer-portal putPortalConfig -p portal_id=5da0a718-c822-403d-9f5d-20d4 "schema": "contract" } ], + "surfaces": [ + { + "id": "website-journeys", + "name": "Website journeys", + "description": "string", + "authentication": "login", + "token_ttl_seconds": 300, + "data_access": {} + } + ], "contact_identifiers_for_account": [ { "name": "first_name", @@ -9840,6 +11715,7 @@ epilot customer-portal putPortalConfig -p portal_id=5da0a718-c822-403d-9f5d-20d4 "inactive_contract_cutoff_years": 0, "is_dummy": true, "is_v3_item": true, + "published_revision_id": "2026-08-25T14:03:11.482Z-a7f3c1d9", "portal_id": "453ad7bf-86d5-46c8-8252-bcc868df5e3c", "portal_sk_v3": "PORTAL_CONFIG#453ad7bf-86d5-46c8-8252-bcc868df5e3c", "origin": "string", @@ -9925,22 +11801,37 @@ epilot customer-portal deletePortalConfig -p portal_id=5da0a718-c822-403d-9f5d-2 --- -### `listAllPortalConfigs` +### `listPortalRevisions` -Retrieves all portal configurations. +Lists a portal's revision history, newest first. Metadata only — no config blob, no page bodies. `is_published` says whe -`GET /v3/portal/configs` +`GET /v3/portal/config/{portal_id}/revisions` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `portal_id` | path | string (uuid) | Yes | Portal ID (readonly UUID generated on portal creation) | +| `limit` | query | number | No | Maximum number of revisions to return | +| `cursor` | query | string | No | Opaque pagination cursor, taken from a previous response's `next_cursor` | **Sample Call** ```bash -epilot customer-portal listAllPortalConfigs +epilot customer-portal listPortalRevisions \ + -p portal_id=5da0a718-c822-403d-9f5d-20d4584e0528 +``` + +Using positional args for path parameters: + +```bash +epilot customer-portal listPortalRevisions 5da0a718-c822-403d-9f5d-20d4584e0528 ``` With JSONata filter: ```bash -epilot customer-portal listAllPortalConfigs --jsonata 'data' +epilot customer-portal listPortalRevisions -p portal_id=5da0a718-c822-403d-9f5d-20d4584e0528 --jsonata 'results[0]' ```
@@ -9948,61 +11839,20 @@ epilot customer-portal listAllPortalConfigs --jsonata 'data' ```json { - "data": [ + "results": [ { - "entity_actions": [], - "extensions": [], - "extension_hooks": {}, - "default_user_to_notify": {}, - "identity_providers": [], - "enabled": true, - "name": "Installer Portal", - "domain": "abc.com", - "is_epilot_domain": true, - "epilot_domain": "example-portal-1.ecp.epilot.io", - "domain_settings": {}, - "design_id": "5da0a718-c822-403d-9f5d-20d4584e0528", - "allowed_portal_entities": ["contact", "contract"], - "self_registration_setting": "ALLOW_WITH_CONTACT_CREATION", - "self_registration_account_setting": "ALLOW_WITH_CREATION", - "block_registration_if_portal_user_exists": true, - "self_registration_entity": "contact", - "user_account_self_management": false, - "feature_settings": {}, - "accessToken": "string", - "advanced_mfa": {}, - "auth_settings": {}, - "cognito_details": {}, - "config": "string", - "contact_identifiers": ["email", "last_name"], - "approval_state_attributes": {}, - "email_templates": {}, - "images": {}, - "entity_identifiers": {}, - "contract_identifiers": [], - "contract_selector_config": {}, - "registration_identifiers": [], - "contact_identifiers_for_account": [], - "additional_contact_attributes": [], - "triggered_journeys": [], - "entity_edit_rules": [], - "allowed_file_extensions": {}, - "prevent_search_engine_indexing": true, - "meter_reading_grace_period": 0, - "inactive_contract_cutoff_years": 0, - "is_dummy": true, - "is_v3_item": true, - "portal_id": "453ad7bf-86d5-46c8-8252-bcc868df5e3c", - "portal_sk_v3": "PORTAL_CONFIG#453ad7bf-86d5-46c8-8252-bcc868df5e3c", - "origin": "string", - "global_blocks": {}, - "organization_id": 12345, - "org_settings": {}, - "feature_flags": {}, - "grants": [], - "pages": [] + "revision_id": "2026-08-25T14:03:11.482Z-a7f3c1d9", + "created_at": "1970-01-01T00:00:00.000Z", + "created_by": "string", + "name": "FAQ page launch", + "description": "string", + "page_count": 0, + "published_at": "1970-01-01T00:00:00.000Z", + "published_by": "string", + "is_published": true } - ] + ], + "next_cursor": "string" } ``` @@ -10010,52 +11860,659 @@ epilot customer-portal listAllPortalConfigs --jsonata 'data' --- -### `swapPortalConfig` +### `createPortalRevision` -Swaps the portal configuration of two portals. +Creates a new revision — a complete, immutable snapshot of the portal's configuration, pages and email templates. Nothin -`POST /v3/portal/config/swap` +`POST /v3/portal/config/{portal_id}/revisions` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `portal_id` | path | string (uuid) | Yes | Portal ID (readonly UUID generated on portal creation) | **Request Body** (required) **Sample Call** ```bash -epilot customer-portal swapPortalConfig +epilot customer-portal createPortalRevision \ + -p portal_id=5da0a718-c822-403d-9f5d-20d4584e0528 ``` With request body: ```bash -epilot customer-portal swapPortalConfig \ +epilot customer-portal createPortalRevision \ + -p portal_id=5da0a718-c822-403d-9f5d-20d4584e0528 \ -d '{ - "source_portal_id": "453ad7bf-86d5-46c8-8252-bcc868df5e3c", - "target_portal_id": "453ad7bf-86d5-46c8-8252-bcc868df5e3c", - "items_to_swap": ["all"] -}' -``` - -Using stdin pipe: - -```bash -cat body.json | epilot customer-portal swapPortalConfig -``` - -With JSONata filter: - -```bash -epilot customer-portal swapPortalConfig --jsonata 'message' -``` - -
-Sample Response - -```json -{ - "message": "Domain and users swapped successfully." -} -``` - + "entity_actions": [ + { + "journey_id": "5da0a718-c822-403d-9f5d-20d4584e0528", + "slug": "contact", + "action_Label": {} + } + ], + "extensions": [ + { + "id": "string", + "status": "installed", + "options": {} + } + ], + "extension_hooks": {}, + "default_user_to_notify": { + "onPendingUser": [ + {} + ] + }, + "identity_providers": [ + { + "slug": "office-365-login", + "display_name": "Office 365 Login", + "provider_type": "OIDC", + "attribute_mappings": {}, + "entity_matching": {}, + "oidc_config": {}, + "mobile_oidc_config": {}, + "expose_client_secret": false + } + ], + "mobile_config": { + "portal_id": "string", + "enabled": true, + "display_name": "string", + "app_host": "string", + "environment": "prod", + "branding": { + "app_icon": "string", + "splash": "string", + "splash_dark": "string", + "icon_background_color": "string", + "splash_background_color": "string", + "splash_background_color_dark": "string" + }, + "ios": { + "bundle_id": "string", + "team_id": "string", + "credentials_status": "not_configured", + "app_store_id": "string", + "store_url": "string", + "last_build": {} + }, + "android": { + "package_name": "string", + "credentials_status": "not_configured", + "upload_key_status": "not_configured", + "store_url": "string", + "last_build": {} + }, + "ota": { + "enabled": true, + "channel": "canary", + "auto_update": true, + "update_strategy": "next-launch", + "min_native_version": "string" + } + }, + "enabled": true, + "name": "Installer Portal", + "domain": "abc.com", + "is_epilot_domain": true, + "epilot_domain": "example-portal-1.ecp.epilot.io", + "domain_settings": { + "is_custom_domain_enabled": true, + "is_epilot_domain_enabled": true, + "is_redirection_enabled": true + }, + "design_id": "5da0a718-c822-403d-9f5d-20d4584e0528", + "allowed_portal_entities": ["contact", "contract"], + "self_registration_setting": "ALLOW_WITH_CONTACT_CREATION", + "self_registration_account_setting": "ALLOW_WITH_CREATION", + "block_registration_if_portal_user_exists": true, + "self_registration_entity": "contact", + "user_account_self_management": false, + "feature_settings": { + "start_page": true, + "billing": true, + "change_due_date": true, + "new_design": true, + "mcp_enabled": true, + "mcp_grant_version": 0 + }, + "accessToken": "string", + "advanced_mfa": { + "enabled": true + }, + "auth_settings": { + "passwordless_login": { + "enabled": true + }, + "entry_point": "PASSWORD", + "preferred_sso_providers": ["office-365-login"], + "auto_redirect_to_sso": true, + "prevent_user_enumeration": true + }, + "cognito_details": { + "cognito_user_pool_client_id": "6bsd0jkgoie74k2i8mrhc1vest", + "cognito_user_pool_arn": "arn:aws:cognito-idp:us-east-1:123412341234:userpool/us-east-1_123412341", + "cognito_user_pool_id": "eu-central-1_CUEQRNbUb", + "timeouts": { + "refresh_token": 300, + "access_token": 300, + "id_token": 300 + }, + "advanced_authentication": { + "user_activity_logging": true, + "adaptive_authentication": true, + "compromised_credentials_detection": true + }, + "password_policy": { + "minimum_length": 8, + "maximum_length": 256, + "require_lowercase": true, + "require_uppercase": true, + "require_numbers": true, + "require_symbols": true, + "password_history_size": 3 + } + }, + "config": "string", + "contact_identifiers": ["email", "last_name"], + "approval_state_attributes": { + "contact": ["name", "address"], + "contract": ["installment_amount"] + }, + "email_templates": { + "confirmAccount": "5da0a718-c822-403d-9f5d-20d4584e0528", + "advancedAuth": "5da0a718-c822-403d-9f5d-20d4584e0528", + "advancedMFA": "5da0a718-c822-403d-9f5d-20d4584e0528", + "journeySignUp": "5da0a718-c822-403d-9f5d-20d4584e0528", + "journeySignInOneTimePassword": "5da0a718-c822-403d-9f5d-20d4584e0528", + "journeyLoginOTP": "5da0a718-c822-403d-9f5d-20d4584e0528", + "forgotPassword": "5da0a718-c822-403d-9f5d-20d4584e0528", + "invitation": "5da0a718-c822-403d-9f5d-20d4584e0528", + "partnerInvitation": "5da0a718-c822-403d-9f5d-20d4584e0528", + "onNewQuote": "5da0a718-c822-403d-9f5d-20d4584e0528", + "onMapAPendingUser": "5da0a718-c822-403d-9f5d-20d4584e0528", + "onDocUpload": "5da0a718-c822-403d-9f5d-20d4584e0528", + "onWorkflowStepAssigned": "5da0a718-c822-403d-9f5d-20d4584e0528", + "confirmEmailUpdate": "5da0a718-c822-403d-9f5d-20d4584e0528", + "verifyCodeToSetPassword": "5da0a718-c822-403d-9f5d-20d4584e0528" + }, + "images": { + "orderLeftTeaser": "https://epilot-bucket.s3.eu-central-1.amazonaws.com/12344/6538fddb-f0e9-4f0f-af51-6e57891ff20a/order-left-teaser.jpeg", + "orderRightTeaser": "https://epilot-bucket.s3.eu-central-1.amazonaws.com/12344/6538fddb-f0e9-4f0f-af51-6e57891ff20a/order-right-teaser.jpeg", + "welcomeBanner": "https://epilot-bucket.s3.eu-central-1.amazonaws.com/12344/6538fddb-f0e9-4f0f-af51-6e57891ff20a/welcome-banner.jpeg" + }, + "entity_identifiers": { + "type": { + "isEnabled": true, + "attributes": ["contract_number"] + } + }, + "contract_identifiers": [ + { + "name": "email", + "schema": "contact" + }, + { + "name": "last_name", + "schema": "contact" + } + ], + "contract_selector_config": { + "show_inactive": true, + "title_path": "string" + }, + "registration_identifiers": [ + { + "name": "last_name", + "schema": "contact" + }, + { + "name": "contract_number", + "schema": "contract" + } + ], + "surfaces": [ + { + "id": "website-journeys", + "name": "Website journeys", + "description": "string", + "authentication": "login", + "token_ttl_seconds": 300, + "data_access": {} + } + ], + "contact_identifiers_for_account": [ + { + "name": "first_name", + "schema": "contact" + }, + { + "name": "last_name", + "schema": "contact" + } + ], + "additional_contact_attributes": [ + { + "name": "first_name", + "required": true + }, + { + "name": "last_name", + "required": true + } + ], + "triggered_journeys": [ + { + "trigger_name": "FIRST_LOGIN", + "journey_id": "5da0a718-c822-403d-9f5d-20d4584e0528", + "context_params": [] + } + ], + "entity_edit_rules": [ + { + "slug": "contact", + "attribute": "first_name", + "rule_type": "cadence", + "cadence_period_type": "days", + "cadence_period": 1, + "changes_allowed": 1, + "grace_period": 1, + "allowed_increment": "10%", + "allowed_decrement": "10%", + "number_of_days_before_restriction": 10 + } + ], + "allowed_file_extensions": { + "document": ["pdf"], + "image": ["jpg"], + "spreadsheet": ["xls"], + "presentation": ["ppt"], + "audioVideo": ["mp4"], + "email": ["eml"], + "archive": ["zip"], + "cad": ["cad"], + "calendar": ["ics"], + "other": ["txt"] + }, + "prevent_search_engine_indexing": true, + "meter_reading_grace_period": 0, + "inactive_contract_cutoff_years": 0, + "is_dummy": true, + "is_v3_item": true, + "published_revision_id": "2026-08-25T14:03:11.482Z-a7f3c1d9", + "portal_id": "453ad7bf-86d5-46c8-8252-bcc868df5e3c", + "portal_sk_v3": "PORTAL_CONFIG#453ad7bf-86d5-46c8-8252-bcc868df5e3c", + "origin": "string", + "global_blocks": {}, + "based_on_revision_id": "string", + "pages": [ + { + "id": "c495fef9-eeca-4019-a989-8390dcd9825b", + "slug": "dashboard", + "order": 0, + "blocks": {} + } + ] +}' +``` + +Using positional args for path parameters: + +```bash +epilot customer-portal createPortalRevision 5da0a718-c822-403d-9f5d-20d4584e0528 +``` + +Using stdin pipe: + +```bash +cat body.json | epilot customer-portal createPortalRevision -p portal_id=5da0a718-c822-403d-9f5d-20d4584e0528 +``` + +With JSONata filter: + +```bash +epilot customer-portal createPortalRevision -p portal_id=5da0a718-c822-403d-9f5d-20d4584e0528 --jsonata '$' +``` + +
+Sample Response + +```json +{ + "revision_id": "2026-08-25T14:03:11.482Z-a7f3c1d9", + "created_at": "1970-01-01T00:00:00.000Z", + "created_by": "string", + "name": "FAQ page launch", + "description": "string", + "page_count": 0, + "published_at": "1970-01-01T00:00:00.000Z", + "published_by": "string", + "is_published": true, + "identity_providers": [ + { + "slug": "office-365-login", + "display_name": "Office 365 Login", + "provider_type": "OIDC", + "attribute_mappings": {}, + "entity_matching": {}, + "oidc_config": {}, + "mobile_oidc_config": {}, + "expose_client_secret": false + } + ] +} +``` + +
+ +--- + +### `getPortalRevision` + +Returns the full content of one revision: the snapshotted config, its pages (in the live `Page` shape), email templates + +`GET /v3/portal/config/{portal_id}/revisions/{revision_id}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `portal_id` | path | string (uuid) | Yes | Portal ID (readonly UUID generated on portal creation) | +| `revision_id` | path | string | Yes | Revision ID. Contains `:` characters — percent-encode it. | + +**Sample Call** + +```bash +epilot customer-portal getPortalRevision \ + -p portal_id=5da0a718-c822-403d-9f5d-20d4584e0528 \ + -p revision_id=2026-08-25T14:03:11.482Z-a7f3c1d9 +``` + +Using positional args for path parameters: + +```bash +epilot customer-portal getPortalRevision 5da0a718-c822-403d-9f5d-20d4584e0528 2026-08-25T14:03:11.482Z-a7f3c1d9 +``` + +With JSONata filter: + +```bash +epilot customer-portal getPortalRevision -p portal_id=5da0a718-c822-403d-9f5d-20d4584e0528 -p revision_id=2026-08-25T14:03:11.482Z-a7f3c1d9 --jsonata '$' +``` + +
+Sample Response + +```json +{ + "revision_id": "2026-08-25T14:03:11.482Z-a7f3c1d9", + "created_at": "1970-01-01T00:00:00.000Z", + "created_by": "string", + "name": "FAQ page launch", + "description": "string", + "page_count": 0, + "published_at": "1970-01-01T00:00:00.000Z", + "published_by": "string", + "is_published": true, + "config": {}, + "pages": [ + { + "slug": "dashboard", + "path": "/dashboard", + "schema": ["string"], + "visibility": {}, + "content": {}, + "design": {}, + "blocks": {}, + "order": 1, + "is_system": false, + "is_detail": false, + "detail_schema": "contact", + "show_in_navigation": false, + "is_public": true, + "parentId": "c495fef9-eeca-4019-a989-8390dcd9825b", + "is_entry_route": false, + "is_deleted": false, + "id": "c495fef9-eeca-4019-a989-8390dcd9825b", + "last_modified_at": "2021-02-09T12:41:43.662Z", + "portal_id": "453ad7bf-86d5-46c8-8252-bcc868df5e3c", + "past_routes": ["old-dashboard", "home"], + "org_id": "string" + } + ], + "identity_providers": [ + { + "slug": "office-365-login", + "display_name": "Office 365 Login", + "provider_type": "OIDC", + "attribute_mappings": {}, + "entity_matching": {}, + "oidc_config": {}, + "mobile_oidc_config": {}, + "expose_client_secret": false + } + ], + "email_templates": { + "confirmAccount": "5da0a718-c822-403d-9f5d-20d4584e0528", + "advancedAuth": "5da0a718-c822-403d-9f5d-20d4584e0528", + "advancedMFA": "5da0a718-c822-403d-9f5d-20d4584e0528", + "journeySignUp": "5da0a718-c822-403d-9f5d-20d4584e0528", + "journeySignInOneTimePassword": "5da0a718-c822-403d-9f5d-20d4584e0528", + "journeyLoginOTP": "5da0a718-c822-403d-9f5d-20d4584e0528", + "forgotPassword": "5da0a718-c822-403d-9f5d-20d4584e0528", + "invitation": "5da0a718-c822-403d-9f5d-20d4584e0528", + "partnerInvitation": "5da0a718-c822-403d-9f5d-20d4584e0528", + "onNewQuote": "5da0a718-c822-403d-9f5d-20d4584e0528", + "onMapAPendingUser": "5da0a718-c822-403d-9f5d-20d4584e0528", + "onDocUpload": "5da0a718-c822-403d-9f5d-20d4584e0528", + "onWorkflowStepAssigned": "5da0a718-c822-403d-9f5d-20d4584e0528", + "confirmEmailUpdate": "5da0a718-c822-403d-9f5d-20d4584e0528", + "verifyCodeToSetPassword": "5da0a718-c822-403d-9f5d-20d4584e0528" + }, + "email_template_settings": {} +} +``` + +
+ +--- + +### `publishPortalRevision` + +Makes one revision the portal's live configuration, atomically: either everything below takes effect or nothing does. Pu + +`POST /v3/portal/config/{portal_id}/publish` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `portal_id` | path | string (uuid) | Yes | Portal ID (readonly UUID generated on portal creation) | + +**Request Body** (required) + +**Sample Call** + +```bash +epilot customer-portal publishPortalRevision \ + -p portal_id=5da0a718-c822-403d-9f5d-20d4584e0528 \ + -d '{"revision_id":"2026-08-25T14:03:11.482Z-a7f3c1d9","name":"FAQ page launch","description":"string"}' +``` + +Using positional args for path parameters: + +```bash +epilot customer-portal publishPortalRevision 5da0a718-c822-403d-9f5d-20d4584e0528 +``` + +Using stdin pipe: + +```bash +cat body.json | epilot customer-portal publishPortalRevision -p portal_id=5da0a718-c822-403d-9f5d-20d4584e0528 +``` + +With JSONata filter: + +```bash +epilot customer-portal publishPortalRevision -p portal_id=5da0a718-c822-403d-9f5d-20d4584e0528 --jsonata 'revision_id' +``` + +
+Sample Response + +```json +{ + "revision_id": "string", + "published_at": "1970-01-01T00:00:00.000Z", + "published_by": "string", + "name": "FAQ page launch", + "post_publish_warnings": ["allowed_entities_change"] +} +``` + +
+ +--- + +### `listAllPortalConfigs` + +Retrieves all portal configurations. + +`GET /v3/portal/configs` + +**Sample Call** + +```bash +epilot customer-portal listAllPortalConfigs +``` + +With JSONata filter: + +```bash +epilot customer-portal listAllPortalConfigs --jsonata 'data' +``` + +
+Sample Response + +```json +{ + "data": [ + { + "entity_actions": [], + "extensions": [], + "extension_hooks": {}, + "default_user_to_notify": {}, + "identity_providers": [], + "mobile_config": {}, + "enabled": true, + "name": "Installer Portal", + "domain": "abc.com", + "is_epilot_domain": true, + "epilot_domain": "example-portal-1.ecp.epilot.io", + "domain_settings": {}, + "design_id": "5da0a718-c822-403d-9f5d-20d4584e0528", + "allowed_portal_entities": ["contact", "contract"], + "self_registration_setting": "ALLOW_WITH_CONTACT_CREATION", + "self_registration_account_setting": "ALLOW_WITH_CREATION", + "block_registration_if_portal_user_exists": true, + "self_registration_entity": "contact", + "user_account_self_management": false, + "feature_settings": {}, + "accessToken": "string", + "advanced_mfa": {}, + "auth_settings": {}, + "cognito_details": {}, + "config": "string", + "contact_identifiers": ["email", "last_name"], + "approval_state_attributes": {}, + "email_templates": {}, + "images": {}, + "entity_identifiers": {}, + "contract_identifiers": [], + "contract_selector_config": {}, + "registration_identifiers": [], + "surfaces": [], + "contact_identifiers_for_account": [], + "additional_contact_attributes": [], + "triggered_journeys": [], + "entity_edit_rules": [], + "allowed_file_extensions": {}, + "prevent_search_engine_indexing": true, + "meter_reading_grace_period": 0, + "inactive_contract_cutoff_years": 0, + "is_dummy": true, + "is_v3_item": true, + "published_revision_id": "2026-08-25T14:03:11.482Z-a7f3c1d9", + "portal_id": "453ad7bf-86d5-46c8-8252-bcc868df5e3c", + "portal_sk_v3": "PORTAL_CONFIG#453ad7bf-86d5-46c8-8252-bcc868df5e3c", + "origin": "string", + "global_blocks": {}, + "organization_id": 12345, + "org_settings": {}, + "feature_flags": {}, + "grants": [], + "pages": [] + } + ] +} +``` + +
+ +--- + +### `swapPortalConfig` + +Swaps the portal configuration of two portals. + +`POST /v3/portal/config/swap` + +**Request Body** (required) + +**Sample Call** + +```bash +epilot customer-portal swapPortalConfig +``` + +With request body: + +```bash +epilot customer-portal swapPortalConfig \ + -d '{ + "source_portal_id": "453ad7bf-86d5-46c8-8252-bcc868df5e3c", + "target_portal_id": "453ad7bf-86d5-46c8-8252-bcc868df5e3c", + "items_to_swap": ["email_templates"] +}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot customer-portal swapPortalConfig +``` + +With JSONata filter: + +```bash +epilot customer-portal swapPortalConfig --jsonata 'message' +``` + +
+Sample Response + +```json +{ + "message": "Domain and users swapped successfully." +} +``` +
--- @@ -10120,9 +12577,47 @@ epilot customer-portal clonePortalConfig --jsonata '$' "attribute_mappings": {}, "entity_matching": {}, "oidc_config": {}, - "mobile_oidc_config": {} + "mobile_oidc_config": {}, + "expose_client_secret": false + } + ], + "mobile_config": { + "portal_id": "string", + "enabled": true, + "display_name": "string", + "app_host": "string", + "environment": "prod", + "branding": { + "app_icon": "string", + "splash": "string", + "splash_dark": "string", + "icon_background_color": "string", + "splash_background_color": "string", + "splash_background_color_dark": "string" + }, + "ios": { + "bundle_id": "string", + "team_id": "string", + "credentials_status": "not_configured", + "app_store_id": "string", + "store_url": "string", + "last_build": {} + }, + "android": { + "package_name": "string", + "credentials_status": "not_configured", + "upload_key_status": "not_configured", + "store_url": "string", + "last_build": {} + }, + "ota": { + "enabled": true, + "channel": "canary", + "auto_update": true, + "update_strategy": "next-launch", + "min_native_version": "string" } - ], + }, "enabled": true, "name": "Installer Portal", "domain": "abc.com", @@ -10144,7 +12639,9 @@ epilot customer-portal clonePortalConfig --jsonata '$' "start_page": true, "billing": true, "change_due_date": true, - "new_design": true + "new_design": true, + "mcp_enabled": true, + "mcp_grant_version": 0 }, "accessToken": "string", "advanced_mfa": { @@ -10156,7 +12653,8 @@ epilot customer-portal clonePortalConfig --jsonata '$' }, "entry_point": "PASSWORD", "preferred_sso_providers": ["office-365-login"], - "auto_redirect_to_sso": true + "auto_redirect_to_sso": true, + "prevent_user_enumeration": true }, "cognito_details": { "cognito_user_pool_client_id": "6bsd0jkgoie74k2i8mrhc1vest", @@ -10178,7 +12676,8 @@ epilot customer-portal clonePortalConfig --jsonata '$' "require_lowercase": true, "require_uppercase": true, "require_numbers": true, - "require_symbols": true + "require_symbols": true, + "password_history_size": 3 } }, "config": "string", @@ -10239,6 +12738,16 @@ epilot customer-portal clonePortalConfig --jsonata '$' "schema": "contract" } ], + "surfaces": [ + { + "id": "website-journeys", + "name": "Website journeys", + "description": "string", + "authentication": "login", + "token_ttl_seconds": 300, + "data_access": {} + } + ], "contact_identifiers_for_account": [ { "name": "first_name", @@ -10297,6 +12806,7 @@ epilot customer-portal clonePortalConfig --jsonata '$' "inactive_contract_cutoff_years": 0, "is_dummy": true, "is_v3_item": true, + "published_revision_id": "2026-08-25T14:03:11.482Z-a7f3c1d9", "portal_id": "453ad7bf-86d5-46c8-8252-bcc868df5e3c", "portal_sk_v3": "PORTAL_CONFIG#453ad7bf-86d5-46c8-8252-bcc868df5e3c", "origin": "string", @@ -10359,20 +12869,9 @@ Invites a partner to a portal **Sample Call** -```bash -epilot customer-portal invitePartner -``` - -With request body: - ```bash epilot customer-portal invitePartner \ - -d '{ - "email": "string", - "represents_contact_list": ["5da0a718-c822-403d-9f5d-20d4584e0528"], - "contact_data": {}, - "portal_user_data": {} -}' + -d '{"email":"string","contact_data":{},"portal_user_data":{}}' ``` Using stdin pipe: @@ -10697,3 +13196,219 @@ epilot customer-portal portalProxyExecute --jsonata 'data'
--- + +### `getMobileConfig` + +Returns the portal's mobile app configuration. By default the response is build-ready (resolved): base info (display_nam + +`GET /v1/portal/mobile-config` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `portal_id` | query | string | Yes | Portal ID | +| `raw` | query | boolean | No | Return only the stored mobile_config without resolving base info/branding. | + +**Sample Call** + +```bash +epilot customer-portal getMobileConfig \ + -p portal_id=123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot customer-portal getMobileConfig -p portal_id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'portal_id' +``` + +
+Sample Response + +```json +{ + "portal_id": "string", + "enabled": true, + "display_name": "string", + "app_host": "string", + "environment": "prod", + "branding": { + "app_icon": "string", + "splash": "string", + "splash_dark": "string", + "icon_background_color": "string", + "splash_background_color": "string", + "splash_background_color_dark": "string" + }, + "ios": { + "bundle_id": "string", + "team_id": "string", + "credentials_status": "not_configured", + "app_store_id": "string", + "store_url": "string", + "last_build": { + "version": "string", + "build_number": 0, + "track": "string", + "status": "building", + "updated_at": "1970-01-01T00:00:00.000Z", + "error": "string" + } + }, + "android": { + "package_name": "string", + "credentials_status": "not_configured", + "upload_key_status": "not_configured", + "store_url": "string", + "last_build": { + "version": "string", + "build_number": 0, + "track": "string", + "status": "building", + "updated_at": "1970-01-01T00:00:00.000Z", + "error": "string" + } + }, + "ota": { + "enabled": true, + "channel": "canary", + "auto_update": true, + "update_strategy": "next-launch", + "min_native_version": "string" + } +} +``` + +
+ +--- + +### `putMobileConfig` + +Merges the provided fields into the portal's mobile app configuration + +`PUT /v1/portal/mobile-config` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `portal_id` | query | string | Yes | Portal ID | + +**Request Body** (required) + +**Sample Call** + +```bash +epilot customer-portal putMobileConfig \ + -p portal_id=123e4567-e89b-12d3-a456-426614174000 +``` + +With request body: + +```bash +epilot customer-portal putMobileConfig \ + -p portal_id=123e4567-e89b-12d3-a456-426614174000 \ + -d '{ + "enabled": true, + "ios": { + "bundle_id": "string", + "team_id": "string", + "store_url": "string", + "app_store_id": "string" + }, + "android": { + "package_name": "string", + "store_url": "string" + }, + "branding": { + "app_icon": "string", + "splash": "string", + "splash_dark": "string", + "icon_background_color": "string", + "splash_background_color": "string", + "splash_background_color_dark": "string" + }, + "ota": { + "enabled": true, + "channel": "canary", + "auto_update": true, + "update_strategy": "next-launch", + "min_native_version": "string" + } +}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot customer-portal putMobileConfig -p portal_id=123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot customer-portal putMobileConfig -p portal_id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'portal_id' +``` + +
+Sample Response + +```json +{ + "portal_id": "string", + "enabled": true, + "display_name": "string", + "app_host": "string", + "environment": "prod", + "branding": { + "app_icon": "string", + "splash": "string", + "splash_dark": "string", + "icon_background_color": "string", + "splash_background_color": "string", + "splash_background_color_dark": "string" + }, + "ios": { + "bundle_id": "string", + "team_id": "string", + "credentials_status": "not_configured", + "app_store_id": "string", + "store_url": "string", + "last_build": { + "version": "string", + "build_number": 0, + "track": "string", + "status": "building", + "updated_at": "1970-01-01T00:00:00.000Z", + "error": "string" + } + }, + "android": { + "package_name": "string", + "credentials_status": "not_configured", + "upload_key_status": "not_configured", + "store_url": "string", + "last_build": { + "version": "string", + "build_number": 0, + "track": "string", + "status": "building", + "updated_at": "1970-01-01T00:00:00.000Z", + "error": "string" + } + }, + "ota": { + "enabled": true, + "channel": "canary", + "auto_update": true, + "update_strategy": "next-launch", + "min_native_version": "string" + } +} +``` + +
+ +--- diff --git a/packages/cli/docs/dashboard.md b/packages/cli/docs/dashboard.md index 44d94f85a..fac1d649e 100644 --- a/packages/cli/docs/dashboard.md +++ b/packages/cli/docs/dashboard.md @@ -38,10 +38,23 @@ epilot dashboard listDashboards **Dashboards** - [`listDashboards`](#listdashboards) — List dashboards available to the user - [`createDashboard`](#createdashboard) — Create new dashboard +- [`listFavoriteDashboardIds`](#listfavoritedashboardids) — Returns the current user's favorited dashboard ids, with no dashboard metadata. Lets a +- [`favoriteDashboard`](#favoritedashboard) — Marks the dashboard as favorited by the current user. Idempotent — favoriting an +- [`unfavoriteDashboard`](#unfavoritedashboard) — Removes the current user's favorite for the dashboard. Idempotent — unfavoriting a - [`getDashboard`](#getdashboard) — Get dashboard by ID - [`putDashboard`](#putdashboard) — Update a dashboard by ID +- [`patchDashboard`](#patchdashboard) — Partially update a dashboard by ID. Update content (title/tiles) and/or manage sharing - [`deleteDashboard`](#deletedashboard) — Delete a dashboard by ID +**Insights** +- [`listInsights`](#listinsights) — List insights (saved charts) available to the user +- [`createInsight`](#createinsight) — Create a new insight (saved chart). The caller becomes the owner. +- [`listInsightTags`](#listinsighttags) — List the distinct tags used by insights in the organization (for filter facets) +- [`getInsight`](#getinsight) — Get insight by ID +- [`putInsight`](#putinsight) — Replace an insight's content by ID. Sharing is managed via patchInsight. +- [`patchInsight`](#patchinsight) — Partially update an insight by ID. Update content (name/description/visualisation/tags) and/or +- [`deleteInsight`](#deleteinsight) — Delete an insight by ID. Only owners may delete. + **Visualisations** - [`listAvailableVisualisations`](#listavailablevisualisations) — Returns list of available Visualisations to configure new dashboard tiles @@ -54,6 +67,31 @@ List dashboards available to the user `GET /v1/dashboard/dashboards` +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `q` | query | string | No | Free-text search over name/title and description (case-insensitive substring). | +| `created_by` | query | string | No | Filter by the id of the creating user. | +| `created_after` | query | string (date-time) | No | | +| `created_before` | query | string (date-time) | No | | +| `updated_after` | query | string (date-time) | No | | +| `updated_before` | query | string (date-time) | No | | +| `shared_with` | query | string[] | No | Filter to resources shared with any of the given user ids. | +| `owner` | query | string[] | No | Filter to resources owned by any of the given user ids. | +| `accessible_to` | query | string | No | Filter to resources the given user id may view (owner, shared, org-wide or legacy). | +| `favorite` | query | boolean | No | Dashboards only. Filter to only (`true`) or exclude (`false`) the requesting user's +favorited dashboards. A user with zero favorites and `favorite=true` gets an empty page, +not the unfiltered list. + | +| `favorites_first` | query | boolean | No | Dashboards only. No filtering — sorts the requesting user's favorited dashboards first, +preserving the existing secondary ordering (`sort`/`order`) otherwise. + | +| `sort` | query | "created_at" \| "updated_at" \| "name" \| "title" | No | Field to sort by. `name` and `title` are aliases for the resource's display name and are normalised per resource: dashboards sort by `title` and insights by `name`, whichever of the two values is sent | +| `order` | query | "asc" \| "desc" | No | Sort direction. Defaults to ascending (preserving the pre-migration order). | +| `limit` | query | number | No | Maximum results to return (max 200). Omit to return all matching results (pagination is opt-in). | +| `offset` | query | number | No | Number of results to skip, for pagination. Use with limit (page N = offset N*limit). | + **Sample Call** ```bash @@ -73,42 +111,37 @@ epilot dashboard listDashboards --jsonata 'results[0]' { "results": [ { + "created_by": "10598", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_by": "10598", + "updated_at": "1970-01-01T00:00:00.000Z", + "owner_org_id": "739224", + "owners": ["10598"], + "shared_with": [ + { + "user_id": "10598", + "permission": "view" + } + ], + "org_access": "view", "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "title": "Employee Dashboard", "tiles": [ { "id": "e4af1297-1fd6-440f-9846-f475f580d40f", "coordinates": {}, - "visualisation": "timechart", - "visualisation_config": { - "query": { - "dataset": "entity_operations", - "measure": "count_operations", - "filters": [ - { - "operation": ["createEntity"] - }, - { - "entity_schema": ["opportunity"] - } - ], - "dimensions": [ - { - "time_with_granularity": "month" - }, - { - "entity_attribute": "source.title" - } - ] - }, - "options": { - "type": "line" - } - } + "insight_id": "8d2e1c7a-3b4f-4a2e-9c1d-2f3a4b5c6d7e" } - ] + ], + "favorited": false } - ] + ], + "pagination": { + "total": 0, + "limit": 0, + "offset": 0, + "has_more": true + } } ``` @@ -135,40 +168,29 @@ With request body: ```bash epilot dashboard createDashboard \ -d '{ + "created_by": "10598", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_by": "10598", + "updated_at": "1970-01-01T00:00:00.000Z", + "owner_org_id": "739224", + "owners": ["10598"], + "shared_with": [ + { + "user_id": "10598", + "permission": "view" + } + ], + "org_access": "view", "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "title": "Employee Dashboard", "tiles": [ { "id": "e4af1297-1fd6-440f-9846-f475f580d40f", "coordinates": {}, - "visualisation": "timechart", - "visualisation_config": { - "query": { - "dataset": "entity_operations", - "measure": "count_operations", - "filters": [ - { - "operation": ["createEntity"] - }, - { - "entity_schema": ["opportunity"] - } - ], - "dimensions": [ - { - "time_with_granularity": "month" - }, - { - "entity_attribute": "source.title" - } - ] - }, - "options": { - "type": "line" - } - } + "insight_id": "8d2e1c7a-3b4f-4a2e-9c1d-2f3a4b5c6d7e" } - ] + ], + "favorited": false }' ``` @@ -181,7 +203,7 @@ cat body.json | epilot dashboard createDashboard With JSONata filter: ```bash -epilot dashboard createDashboard --jsonata 'id' +epilot dashboard createDashboard --jsonata '$' ```
@@ -189,40 +211,61 @@ epilot dashboard createDashboard --jsonata 'id' ```json { + "created_by": "10598", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_by": "10598", + "updated_at": "1970-01-01T00:00:00.000Z", + "owner_org_id": "739224", + "owners": ["10598"], + "shared_with": [ + { + "user_id": "10598", + "permission": "view" + } + ], + "org_access": "view", "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "title": "Employee Dashboard", "tiles": [ { "id": "e4af1297-1fd6-440f-9846-f475f580d40f", "coordinates": {}, - "visualisation": "timechart", - "visualisation_config": { - "query": { - "dataset": "entity_operations", - "measure": "count_operations", - "filters": [ - { - "operation": ["createEntity"] - }, - { - "entity_schema": ["opportunity"] - } - ], - "dimensions": [ - { - "time_with_granularity": "month" - }, - { - "entity_attribute": "source.title" - } - ] - }, - "options": { - "type": "line" - } - } + "insight_id": "8d2e1c7a-3b4f-4a2e-9c1d-2f3a4b5c6d7e" } - ] + ], + "favorited": false +} +``` + +
+ +--- + +### `listFavoriteDashboardIds` + +Returns the current user's favorited dashboard ids, with no dashboard metadata. Lets a + +`GET /v1/dashboard/dashboards/favorites` + +**Sample Call** + +```bash +epilot dashboard listFavoriteDashboardIds +``` + +With JSONata filter: + +```bash +epilot dashboard listFavoriteDashboardIds --jsonata 'dashboard_ids' +``` + +
+Sample Response + +```json +{ + "dashboard_ids": ["3fa85f64-5717-4562-b3fc-2c963f66afa6"], + "total": 0 } ``` @@ -230,6 +273,72 @@ epilot dashboard createDashboard --jsonata 'id' --- +### `favoriteDashboard` + +Marks the dashboard as favorited by the current user. Idempotent — favoriting an + +`PUT /v1/dashboard/dashboards/{id}/favorite` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `id` | path | string (uuid) | Yes | | + +**Sample Call** + +```bash +epilot dashboard favoriteDashboard \ + -p id=3fa85f64-5717-4562-b3fc-2c963f66afa6 +``` + +Using positional args for path parameters: + +```bash +epilot dashboard favoriteDashboard 3fa85f64-5717-4562-b3fc-2c963f66afa6 +``` + +With JSONata filter: + +```bash +epilot dashboard favoriteDashboard -p id=3fa85f64-5717-4562-b3fc-2c963f66afa6 --jsonata '$' +``` + +--- + +### `unfavoriteDashboard` + +Removes the current user's favorite for the dashboard. Idempotent — unfavoriting a + +`DELETE /v1/dashboard/dashboards/{id}/favorite` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `id` | path | string (uuid) | Yes | | + +**Sample Call** + +```bash +epilot dashboard unfavoriteDashboard \ + -p id=3fa85f64-5717-4562-b3fc-2c963f66afa6 +``` + +Using positional args for path parameters: + +```bash +epilot dashboard unfavoriteDashboard 3fa85f64-5717-4562-b3fc-2c963f66afa6 +``` + +With JSONata filter: + +```bash +epilot dashboard unfavoriteDashboard -p id=3fa85f64-5717-4562-b3fc-2c963f66afa6 --jsonata '$' +``` + +--- + ### `getDashboard` Get dashboard by ID @@ -258,7 +367,7 @@ epilot dashboard getDashboard 3fa85f64-5717-4562-b3fc-2c963f66afa6 With JSONata filter: ```bash -epilot dashboard getDashboard -p id=3fa85f64-5717-4562-b3fc-2c963f66afa6 --jsonata 'id' +epilot dashboard getDashboard -p id=3fa85f64-5717-4562-b3fc-2c963f66afa6 --jsonata '$' ```
@@ -266,40 +375,29 @@ epilot dashboard getDashboard -p id=3fa85f64-5717-4562-b3fc-2c963f66afa6 --jsona ```json { + "created_by": "10598", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_by": "10598", + "updated_at": "1970-01-01T00:00:00.000Z", + "owner_org_id": "739224", + "owners": ["10598"], + "shared_with": [ + { + "user_id": "10598", + "permission": "view" + } + ], + "org_access": "view", "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "title": "Employee Dashboard", "tiles": [ { "id": "e4af1297-1fd6-440f-9846-f475f580d40f", "coordinates": {}, - "visualisation": "timechart", - "visualisation_config": { - "query": { - "dataset": "entity_operations", - "measure": "count_operations", - "filters": [ - { - "operation": ["createEntity"] - }, - { - "entity_schema": ["opportunity"] - } - ], - "dimensions": [ - { - "time_with_granularity": "month" - }, - { - "entity_attribute": "source.title" - } - ] - }, - "options": { - "type": "line" - } - } + "insight_id": "8d2e1c7a-3b4f-4a2e-9c1d-2f3a4b5c6d7e" } - ] + ], + "favorited": false } ``` @@ -334,40 +432,29 @@ With request body: epilot dashboard putDashboard \ -p id=3fa85f64-5717-4562-b3fc-2c963f66afa6 \ -d '{ + "created_by": "10598", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_by": "10598", + "updated_at": "1970-01-01T00:00:00.000Z", + "owner_org_id": "739224", + "owners": ["10598"], + "shared_with": [ + { + "user_id": "10598", + "permission": "view" + } + ], + "org_access": "view", "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "title": "Employee Dashboard", "tiles": [ { "id": "e4af1297-1fd6-440f-9846-f475f580d40f", "coordinates": {}, - "visualisation": "timechart", - "visualisation_config": { - "query": { - "dataset": "entity_operations", - "measure": "count_operations", - "filters": [ - { - "operation": ["createEntity"] - }, - { - "entity_schema": ["opportunity"] - } - ], - "dimensions": [ - { - "time_with_granularity": "month" - }, - { - "entity_attribute": "source.title" - } - ] - }, - "options": { - "type": "line" - } - } + "insight_id": "8d2e1c7a-3b4f-4a2e-9c1d-2f3a4b5c6d7e" } - ] + ], + "favorited": false }' ``` @@ -386,7 +473,7 @@ cat body.json | epilot dashboard putDashboard -p id=3fa85f64-5717-4562-b3fc-2c96 With JSONata filter: ```bash -epilot dashboard putDashboard -p id=3fa85f64-5717-4562-b3fc-2c963f66afa6 --jsonata 'id' +epilot dashboard putDashboard -p id=3fa85f64-5717-4562-b3fc-2c963f66afa6 --jsonata '$' ```
@@ -394,40 +481,29 @@ epilot dashboard putDashboard -p id=3fa85f64-5717-4562-b3fc-2c963f66afa6 --jsona ```json { + "created_by": "10598", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_by": "10598", + "updated_at": "1970-01-01T00:00:00.000Z", + "owner_org_id": "739224", + "owners": ["10598"], + "shared_with": [ + { + "user_id": "10598", + "permission": "view" + } + ], + "org_access": "view", "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "title": "Employee Dashboard", "tiles": [ { "id": "e4af1297-1fd6-440f-9846-f475f580d40f", "coordinates": {}, - "visualisation": "timechart", - "visualisation_config": { - "query": { - "dataset": "entity_operations", - "measure": "count_operations", - "filters": [ - { - "operation": ["createEntity"] - }, - { - "entity_schema": ["opportunity"] - } - ], - "dimensions": [ - { - "time_with_granularity": "month" - }, - { - "entity_attribute": "source.title" - } - ] - }, - "options": { - "type": "line" - } - } + "insight_id": "8d2e1c7a-3b4f-4a2e-9c1d-2f3a4b5c6d7e" } - ] + ], + "favorited": false } ``` @@ -435,11 +511,11 @@ epilot dashboard putDashboard -p id=3fa85f64-5717-4562-b3fc-2c963f66afa6 --jsona --- -### `deleteDashboard` +### `patchDashboard` -Delete a dashboard by ID +Partially update a dashboard by ID. Update content (title/tiles) and/or manage sharing -`DELETE /v1/dashboard/dashboards/{id}` +`PATCH /v1/dashboard/dashboards/{id}` **Parameters** @@ -447,64 +523,844 @@ Delete a dashboard by ID | ---- | -- | ---- | -------- | ----------- | | `id` | path | string (uuid) | Yes | | +**Request Body** + **Sample Call** ```bash -epilot dashboard deleteDashboard \ +epilot dashboard patchDashboard \ -p id=3fa85f64-5717-4562-b3fc-2c963f66afa6 ``` -Using positional args for path parameters: - -```bash -epilot dashboard deleteDashboard 3fa85f64-5717-4562-b3fc-2c963f66afa6 -``` - -With JSONata filter: +With request body: ```bash -epilot dashboard deleteDashboard -p id=3fa85f64-5717-4562-b3fc-2c963f66afa6 --jsonata 'id' -``` - -
-Sample Response - -```json -{ - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", +epilot dashboard patchDashboard \ + -p id=3fa85f64-5717-4562-b3fc-2c963f66afa6 \ + -d '{ + "owners": ["string"], + "shared_with": [ + { + "user_id": "10598", + "permission": "view" + } + ], + "org_access": "view", "title": "Employee Dashboard", "tiles": [ { "id": "e4af1297-1fd6-440f-9846-f475f580d40f", "coordinates": {}, - "visualisation": "timechart", + "title": "Number of opportunities created by journeys every month", + "insight_id": "8d2e1c7a-3b4f-4a2e-9c1d-2f3a4b5c6d7e", + "visualisation_id": "timechart", "visualisation_config": { "query": { "dataset": "entity_operations", "measure": "count_operations", - "filters": [ - { - "operation": ["createEntity"] - }, + "dimensions": [ { - "entity_schema": ["opportunity"] + "time_with_granularity": "year-month" } ], - "dimensions": [ - { - "time_with_granularity": "month" - }, + "filters": [ { - "entity_attribute": "source.title" + "entity_schema": "opportunity" } ] }, "options": { - "type": "line" + "type": "bar" } } } ] +}' +``` + +Using positional args for path parameters: + +```bash +epilot dashboard patchDashboard 3fa85f64-5717-4562-b3fc-2c963f66afa6 +``` + +Using stdin pipe: + +```bash +cat body.json | epilot dashboard patchDashboard -p id=3fa85f64-5717-4562-b3fc-2c963f66afa6 +``` + +With JSONata filter: + +```bash +epilot dashboard patchDashboard -p id=3fa85f64-5717-4562-b3fc-2c963f66afa6 --jsonata '$' +``` + +
+Sample Response + +```json +{ + "created_by": "10598", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_by": "10598", + "updated_at": "1970-01-01T00:00:00.000Z", + "owner_org_id": "739224", + "owners": ["10598"], + "shared_with": [ + { + "user_id": "10598", + "permission": "view" + } + ], + "org_access": "view", + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "title": "Employee Dashboard", + "tiles": [ + { + "id": "e4af1297-1fd6-440f-9846-f475f580d40f", + "coordinates": {}, + "insight_id": "8d2e1c7a-3b4f-4a2e-9c1d-2f3a4b5c6d7e" + } + ], + "favorited": false +} +``` + +
+ +--- + +### `deleteDashboard` + +Delete a dashboard by ID + +`DELETE /v1/dashboard/dashboards/{id}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `id` | path | string (uuid) | Yes | | + +**Sample Call** + +```bash +epilot dashboard deleteDashboard \ + -p id=3fa85f64-5717-4562-b3fc-2c963f66afa6 +``` + +Using positional args for path parameters: + +```bash +epilot dashboard deleteDashboard 3fa85f64-5717-4562-b3fc-2c963f66afa6 +``` + +With JSONata filter: + +```bash +epilot dashboard deleteDashboard -p id=3fa85f64-5717-4562-b3fc-2c963f66afa6 --jsonata '$' +``` + +
+Sample Response + +```json +{ + "created_by": "10598", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_by": "10598", + "updated_at": "1970-01-01T00:00:00.000Z", + "owner_org_id": "739224", + "owners": ["10598"], + "shared_with": [ + { + "user_id": "10598", + "permission": "view" + } + ], + "org_access": "view", + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "title": "Employee Dashboard", + "tiles": [ + { + "id": "e4af1297-1fd6-440f-9846-f475f580d40f", + "coordinates": {}, + "insight_id": "8d2e1c7a-3b4f-4a2e-9c1d-2f3a4b5c6d7e" + } + ], + "favorited": false +} +``` + +
+ +--- + +### `listInsights` + +List insights (saved charts) available to the user + +`GET /v1/dashboard/insights` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `q` | query | string | No | Free-text search over name/title and description (case-insensitive substring). | +| `visualisation_id` | query | "kpi" \| "funnel" \| "toplist" \| "timechart" \| "pie" \| "bar" \| "entity_list" \| "markdown" \| "news_feed" \| "workflow"[] | No | Filter insights by one or more visualisation ids. | +| `tags` | query | string[] | No | Filter by one or more tags (see tags_match for any/all semantics). | +| `tags_match` | query | "any" \| "all" | No | Whether a result must match any (default) or all of the given tags. | +| `created_by` | query | string | No | Filter by the id of the creating user. | +| `created_after` | query | string (date-time) | No | | +| `created_before` | query | string (date-time) | No | | +| `updated_after` | query | string (date-time) | No | | +| `updated_before` | query | string (date-time) | No | | +| `shared_with` | query | string[] | No | Filter to resources shared with any of the given user ids. | +| `owner` | query | string[] | No | Filter to resources owned by any of the given user ids. | +| `accessible_to` | query | string | No | Filter to resources the given user id may view (owner, shared, org-wide or legacy). | +| `sort` | query | "created_at" \| "updated_at" \| "name" \| "title" | No | Field to sort by. `name` and `title` are aliases for the resource's display name and are normalised per resource: dashboards sort by `title` and insights by `name`, whichever of the two values is sent | +| `order` | query | "asc" \| "desc" | No | Sort direction. Defaults to ascending (preserving the pre-migration order). | +| `limit` | query | number | No | Maximum results to return (max 200). Omit to return all matching results (pagination is opt-in). | +| `offset` | query | number | No | Number of results to skip, for pagination. Use with limit (page N = offset N*limit). | + +**Sample Call** + +```bash +epilot dashboard listInsights +``` + +With JSONata filter: + +```bash +epilot dashboard listInsights --jsonata 'results[0]' +``` + +
+Sample Response + +```json +{ + "results": [ + { + "created_by": "10598", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_by": "10598", + "updated_at": "1970-01-01T00:00:00.000Z", + "owner_org_id": "739224", + "owners": ["10598"], + "shared_with": [ + { + "user_id": "10598", + "permission": "view" + } + ], + "org_access": "view", + "id": "8d2e1c7a-3b4f-4a2e-9c1d-2f3a4b5c6d7e", + "name": "Opportunities created by journeys every month", + "description": "Monthly count of opportunities grouped by source journey", + "visualisation_id": "timechart", + "visualisation_config": { + "query": { + "dataset": "entity_operations", + "measure": "count_operations", + "dimensions": [ + { + "time_with_granularity": "year-month" + } + ], + "filters": [ + { + "entity_schema": "opportunity" + } + ] + }, + "options": { + "type": "bar" + } + }, + "tags": ["revenue", "marketing"] + } + ], + "pagination": { + "total": 0, + "limit": 0, + "offset": 0, + "has_more": true + } +} +``` + +
+ +--- + +### `createInsight` + +Create a new insight (saved chart). The caller becomes the owner. + +`POST /v1/dashboard/insights` + +**Request Body** + +**Sample Call** + +```bash +epilot dashboard createInsight +``` + +With request body: + +```bash +epilot dashboard createInsight \ + -d '{ + "created_by": "10598", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_by": "10598", + "updated_at": "1970-01-01T00:00:00.000Z", + "owner_org_id": "739224", + "owners": ["10598"], + "shared_with": [ + { + "user_id": "10598", + "permission": "view" + } + ], + "org_access": "view", + "id": "8d2e1c7a-3b4f-4a2e-9c1d-2f3a4b5c6d7e", + "name": "Opportunities created by journeys every month", + "description": "Monthly count of opportunities grouped by source journey", + "visualisation_id": "timechart", + "visualisation_config": { + "query": { + "dataset": "entity_operations", + "measure": "count_operations", + "dimensions": [ + { + "time_with_granularity": "year-month" + } + ], + "filters": [ + { + "entity_schema": "opportunity" + } + ] + }, + "options": { + "type": "bar" + } + }, + "tags": ["revenue", "marketing"] +}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot dashboard createInsight +``` + +With JSONata filter: + +```bash +epilot dashboard createInsight --jsonata '$' +``` + +
+Sample Response + +```json +{ + "created_by": "10598", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_by": "10598", + "updated_at": "1970-01-01T00:00:00.000Z", + "owner_org_id": "739224", + "owners": ["10598"], + "shared_with": [ + { + "user_id": "10598", + "permission": "view" + } + ], + "org_access": "view", + "id": "8d2e1c7a-3b4f-4a2e-9c1d-2f3a4b5c6d7e", + "name": "Opportunities created by journeys every month", + "description": "Monthly count of opportunities grouped by source journey", + "visualisation_id": "timechart", + "visualisation_config": { + "query": { + "dataset": "entity_operations", + "measure": "count_operations", + "dimensions": [ + { + "time_with_granularity": "year-month" + } + ], + "filters": [ + { + "entity_schema": "opportunity" + } + ] + }, + "options": { + "type": "bar" + } + }, + "tags": ["revenue", "marketing"] +} +``` + +
+ +--- + +### `listInsightTags` + +List the distinct tags used by insights in the organization (for filter facets) + +`GET /v1/dashboard/insights/tags` + +**Sample Call** + +```bash +epilot dashboard listInsightTags +``` + +With JSONata filter: + +```bash +epilot dashboard listInsightTags --jsonata 'results[0]' +``` + +
+Sample Response + +```json +{ + "results": ["string"] +} +``` + +
+ +--- + +### `getInsight` + +Get insight by ID + +`GET /v1/dashboard/insights/{id}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `id` | path | string (uuid) | Yes | | + +**Sample Call** + +```bash +epilot dashboard getInsight \ + -p id=8d2e1c7a-3b4f-4a2e-9c1d-2f3a4b5c6d7e +``` + +Using positional args for path parameters: + +```bash +epilot dashboard getInsight 8d2e1c7a-3b4f-4a2e-9c1d-2f3a4b5c6d7e +``` + +With JSONata filter: + +```bash +epilot dashboard getInsight -p id=8d2e1c7a-3b4f-4a2e-9c1d-2f3a4b5c6d7e --jsonata '$' +``` + +
+Sample Response + +```json +{ + "created_by": "10598", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_by": "10598", + "updated_at": "1970-01-01T00:00:00.000Z", + "owner_org_id": "739224", + "owners": ["10598"], + "shared_with": [ + { + "user_id": "10598", + "permission": "view" + } + ], + "org_access": "view", + "id": "8d2e1c7a-3b4f-4a2e-9c1d-2f3a4b5c6d7e", + "name": "Opportunities created by journeys every month", + "description": "Monthly count of opportunities grouped by source journey", + "visualisation_id": "timechart", + "visualisation_config": { + "query": { + "dataset": "entity_operations", + "measure": "count_operations", + "dimensions": [ + { + "time_with_granularity": "year-month" + } + ], + "filters": [ + { + "entity_schema": "opportunity" + } + ] + }, + "options": { + "type": "bar" + } + }, + "tags": ["revenue", "marketing"] +} +``` + +
+ +--- + +### `putInsight` + +Replace an insight's content by ID. Sharing is managed via patchInsight. + +`PUT /v1/dashboard/insights/{id}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `id` | path | string (uuid) | Yes | | + +**Request Body** + +**Sample Call** + +```bash +epilot dashboard putInsight \ + -p id=8d2e1c7a-3b4f-4a2e-9c1d-2f3a4b5c6d7e +``` + +With request body: + +```bash +epilot dashboard putInsight \ + -p id=8d2e1c7a-3b4f-4a2e-9c1d-2f3a4b5c6d7e \ + -d '{ + "created_by": "10598", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_by": "10598", + "updated_at": "1970-01-01T00:00:00.000Z", + "owner_org_id": "739224", + "owners": ["10598"], + "shared_with": [ + { + "user_id": "10598", + "permission": "view" + } + ], + "org_access": "view", + "id": "8d2e1c7a-3b4f-4a2e-9c1d-2f3a4b5c6d7e", + "name": "Opportunities created by journeys every month", + "description": "Monthly count of opportunities grouped by source journey", + "visualisation_id": "timechart", + "visualisation_config": { + "query": { + "dataset": "entity_operations", + "measure": "count_operations", + "dimensions": [ + { + "time_with_granularity": "year-month" + } + ], + "filters": [ + { + "entity_schema": "opportunity" + } + ] + }, + "options": { + "type": "bar" + } + }, + "tags": ["revenue", "marketing"] +}' +``` + +Using positional args for path parameters: + +```bash +epilot dashboard putInsight 8d2e1c7a-3b4f-4a2e-9c1d-2f3a4b5c6d7e +``` + +Using stdin pipe: + +```bash +cat body.json | epilot dashboard putInsight -p id=8d2e1c7a-3b4f-4a2e-9c1d-2f3a4b5c6d7e +``` + +With JSONata filter: + +```bash +epilot dashboard putInsight -p id=8d2e1c7a-3b4f-4a2e-9c1d-2f3a4b5c6d7e --jsonata '$' +``` + +
+Sample Response + +```json +{ + "created_by": "10598", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_by": "10598", + "updated_at": "1970-01-01T00:00:00.000Z", + "owner_org_id": "739224", + "owners": ["10598"], + "shared_with": [ + { + "user_id": "10598", + "permission": "view" + } + ], + "org_access": "view", + "id": "8d2e1c7a-3b4f-4a2e-9c1d-2f3a4b5c6d7e", + "name": "Opportunities created by journeys every month", + "description": "Monthly count of opportunities grouped by source journey", + "visualisation_id": "timechart", + "visualisation_config": { + "query": { + "dataset": "entity_operations", + "measure": "count_operations", + "dimensions": [ + { + "time_with_granularity": "year-month" + } + ], + "filters": [ + { + "entity_schema": "opportunity" + } + ] + }, + "options": { + "type": "bar" + } + }, + "tags": ["revenue", "marketing"] +} +``` + +
+ +--- + +### `patchInsight` + +Partially update an insight by ID. Update content (name/description/visualisation/tags) and/or + +`PATCH /v1/dashboard/insights/{id}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `id` | path | string (uuid) | Yes | | + +**Request Body** + +**Sample Call** + +```bash +epilot dashboard patchInsight \ + -p id=8d2e1c7a-3b4f-4a2e-9c1d-2f3a4b5c6d7e +``` + +With request body: + +```bash +epilot dashboard patchInsight \ + -p id=8d2e1c7a-3b4f-4a2e-9c1d-2f3a4b5c6d7e \ + -d '{ + "owners": ["string"], + "shared_with": [ + { + "user_id": "10598", + "permission": "view" + } + ], + "org_access": "view", + "name": "string", + "description": "string", + "visualisation_id": "timechart", + "visualisation_config": { + "query": { + "dataset": "entity_operations", + "measure": "count_operations", + "dimensions": [ + { + "time_with_granularity": "year-month" + } + ], + "filters": [ + { + "entity_schema": "opportunity" + } + ] + }, + "options": { + "type": "bar" + } + }, + "tags": ["revenue", "marketing"] +}' +``` + +Using positional args for path parameters: + +```bash +epilot dashboard patchInsight 8d2e1c7a-3b4f-4a2e-9c1d-2f3a4b5c6d7e +``` + +Using stdin pipe: + +```bash +cat body.json | epilot dashboard patchInsight -p id=8d2e1c7a-3b4f-4a2e-9c1d-2f3a4b5c6d7e +``` + +With JSONata filter: + +```bash +epilot dashboard patchInsight -p id=8d2e1c7a-3b4f-4a2e-9c1d-2f3a4b5c6d7e --jsonata '$' +``` + +
+Sample Response + +```json +{ + "created_by": "10598", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_by": "10598", + "updated_at": "1970-01-01T00:00:00.000Z", + "owner_org_id": "739224", + "owners": ["10598"], + "shared_with": [ + { + "user_id": "10598", + "permission": "view" + } + ], + "org_access": "view", + "id": "8d2e1c7a-3b4f-4a2e-9c1d-2f3a4b5c6d7e", + "name": "Opportunities created by journeys every month", + "description": "Monthly count of opportunities grouped by source journey", + "visualisation_id": "timechart", + "visualisation_config": { + "query": { + "dataset": "entity_operations", + "measure": "count_operations", + "dimensions": [ + { + "time_with_granularity": "year-month" + } + ], + "filters": [ + { + "entity_schema": "opportunity" + } + ] + }, + "options": { + "type": "bar" + } + }, + "tags": ["revenue", "marketing"] +} +``` + +
+ +--- + +### `deleteInsight` + +Delete an insight by ID. Only owners may delete. + +`DELETE /v1/dashboard/insights/{id}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `id` | path | string (uuid) | Yes | | + +**Sample Call** + +```bash +epilot dashboard deleteInsight \ + -p id=8d2e1c7a-3b4f-4a2e-9c1d-2f3a4b5c6d7e +``` + +Using positional args for path parameters: + +```bash +epilot dashboard deleteInsight 8d2e1c7a-3b4f-4a2e-9c1d-2f3a4b5c6d7e +``` + +With JSONata filter: + +```bash +epilot dashboard deleteInsight -p id=8d2e1c7a-3b4f-4a2e-9c1d-2f3a4b5c6d7e --jsonata '$' +``` + +
+Sample Response + +```json +{ + "created_by": "10598", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_by": "10598", + "updated_at": "1970-01-01T00:00:00.000Z", + "owner_org_id": "739224", + "owners": ["10598"], + "shared_with": [ + { + "user_id": "10598", + "permission": "view" + } + ], + "org_access": "view", + "id": "8d2e1c7a-3b4f-4a2e-9c1d-2f3a4b5c6d7e", + "name": "Opportunities created by journeys every month", + "description": "Monthly count of opportunities grouped by source journey", + "visualisation_id": "timechart", + "visualisation_config": { + "query": { + "dataset": "entity_operations", + "measure": "count_operations", + "dimensions": [ + { + "time_with_granularity": "year-month" + } + ], + "filters": [ + { + "entity_schema": "opportunity" + } + ] + }, + "options": { + "type": "bar" + } + }, + "tags": ["revenue", "marketing"] } ``` diff --git a/packages/cli/docs/deduplication.md b/packages/cli/docs/deduplication.md index 1ded34d10..382742cee 100644 --- a/packages/cli/docs/deduplication.md +++ b/packages/cli/docs/deduplication.md @@ -38,6 +38,13 @@ epilot deduplication deduplicate - [`deduplicate`](#deduplicate) — Deduplicates Entities - [`deduplicateAsync`](#deduplicateasync) — Submits an async deduplication job. Returns a job ID immediately. Poll GET /v1/deduplicate/jobs/{jobId} for status. - [`getDeduplicationJob`](#getdeduplicationjob) — Returns the current status of an async deduplication job +- [`detectDuplicates`](#detectduplicates) — Detects potential duplicate entities for the given entity using the schema's prioritized uniqueness rules. Returns match +- [`dismissDuplicates`](#dismissduplicates) — Confirms entities as NOT duplicates: clears the internal duplicate-detection flags (_matching_entities) on each given en +- [`listUniquenessCriteria`](#listuniquenesscriteria) — Lists UniquenessCriteria for the requesting organization. Optionally filtered by schema. +- [`createUniquenessCriteria`](#createuniquenesscriteria) — Creates a new UniquenessCriteria record. +- [`getUniquenessCriteria`](#getuniquenesscriteria) — Fetch a single UniquenessCriteria record. +- [`updateUniquenessCriteria`](#updateuniquenesscriteria) — Replace the matchRules on an existing UniquenessCriteria record. +- [`deleteUniquenessCriteria`](#deleteuniquenesscriteria) — Delete a UniquenessCriteria record. ### `deduplicate` @@ -63,33 +70,35 @@ cat body.json | epilot deduplication deduplicate With JSONata filter: ```bash -epilot deduplication deduplicate --jsonata '$' +epilot deduplication deduplicate --jsonata 'deduplicatedEntities' ```
Sample Response ```json -[ - { - "_id": "string", - "_org": "string", - "_schema": "string", - "_created_at": "1970-01-01T00:00:00.000Z", - "_updated_at": "1970-01-01T00:00:00.000Z", - "_created_by": "string", - "created_by": "string", - "_tags": ["string"], - "_acl": {}, - "_owners": [ - { - "org_id": "string", - "user_id": "string" - } - ], - "type": "string" - } -] +{ + "deduplicatedEntities": [ + { + "_id": "string", + "_org": "string", + "_schema": "string", + "_created_at": "1970-01-01T00:00:00.000Z", + "_updated_at": "1970-01-01T00:00:00.000Z", + "_created_by": "string", + "created_by": "string", + "_tags": ["string"], + "_acl": {}, + "_owners": [ + { + "org_id": "string", + "user_id": "string" + } + ], + "type": "string" + } + ] +} ```
@@ -192,3 +201,377 @@ epilot deduplication getDeduplicationJob -p jobId=123e4567-e89b-12d3-a456-426614
--- + +### `detectDuplicates` + +Detects potential duplicate entities for the given entity using the schema's prioritized uniqueness rules. Returns match + +`POST /v1/detect-duplicates` + +**Request Body** + +**Sample Call** + +```bash +epilot deduplication detectDuplicates \ + -d '{"schema":"string","entity":{}}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot deduplication detectDuplicates +``` + +With JSONata filter: + +```bash +epilot deduplication detectDuplicates --jsonata 'matches' +``` + +
+Sample Response + +```json +{ + "matches": [ + { + "entity": { + "_id": "string", + "_org": "string", + "_schema": "string", + "_created_at": "1970-01-01T00:00:00.000Z", + "_updated_at": "1970-01-01T00:00:00.000Z", + "_created_by": "string", + "created_by": "string", + "_tags": ["string"], + "_acl": {}, + "_owners": [ + { + "org_id": "string", + "user_id": "string" + } + ], + "type": "string" + }, + "confidence": 0, + "matched_attributes": ["string"] + } + ] +} +``` + +
+ +--- + +### `dismissDuplicates` + +Confirms entities as NOT duplicates: clears the internal duplicate-detection flags (_matching_entities) on each given en + +`POST /v1/duplicates/dismiss` + +**Request Body** + +**Sample Call** + +```bash +epilot deduplication dismissDuplicates \ + -d '{"schema":"string","entityIds":["string"]}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot deduplication dismissDuplicates +``` + +With JSONata filter: + +```bash +epilot deduplication dismissDuplicates --jsonata 'dismissed' +``` + +
+Sample Response + +```json +{ + "dismissed": ["string"] +} +``` + +
+ +--- + +### `listUniquenessCriteria` + +Lists UniquenessCriteria for the requesting organization. Optionally filtered by schema. + +`GET /v1/uniqueness-criteria` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `schema` | query | string | No | Filter results to a specific entity schema. | + +**Sample Call** + +```bash +epilot deduplication listUniquenessCriteria +``` + +With JSONata filter: + +```bash +epilot deduplication listUniquenessCriteria --jsonata 'items[0]' +``` + +
+Sample Response + +```json +{ + "items": [ + { + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "orgId": "string", + "schema": "string", + "matchRules": [ + { + "name": "string", + "attributes": [ + { + "attribute": "string" + } + ], + "confidence": 0 + } + ], + "createdAt": "1970-01-01T00:00:00.000Z", + "updatedAt": "1970-01-01T00:00:00.000Z" + } + ] +} +``` + +
+ +--- + +### `createUniquenessCriteria` + +Creates a new UniquenessCriteria record. + +`POST /v1/uniqueness-criteria` + +**Request Body** + +**Sample Call** + +```bash +epilot deduplication createUniquenessCriteria \ + -d '{"schema":"string","matchRules":[{"name":"string","attributes":[{"attribute":"string"}],"confidence":0}]}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot deduplication createUniquenessCriteria +``` + +With JSONata filter: + +```bash +epilot deduplication createUniquenessCriteria --jsonata 'id' +``` + +
+Sample Response + +```json +{ + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "orgId": "string", + "schema": "string", + "matchRules": [ + { + "name": "string", + "attributes": [ + { + "attribute": "string" + } + ], + "confidence": 0 + } + ], + "createdAt": "1970-01-01T00:00:00.000Z", + "updatedAt": "1970-01-01T00:00:00.000Z" +} +``` + +
+ +--- + +### `getUniquenessCriteria` + +Fetch a single UniquenessCriteria record. + +`GET /v1/uniqueness-criteria/{schema}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `schema` | path | string | Yes | Entity schema slug. | + +**Sample Call** + +```bash +epilot deduplication getUniquenessCriteria \ + -p schema=example +``` + +Using positional args for path parameters: + +```bash +epilot deduplication getUniquenessCriteria example +``` + +With JSONata filter: + +```bash +epilot deduplication getUniquenessCriteria -p schema=example --jsonata 'id' +``` + +
+Sample Response + +```json +{ + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "orgId": "string", + "schema": "string", + "matchRules": [ + { + "name": "string", + "attributes": [ + { + "attribute": "string" + } + ], + "confidence": 0 + } + ], + "createdAt": "1970-01-01T00:00:00.000Z", + "updatedAt": "1970-01-01T00:00:00.000Z" +} +``` + +
+ +--- + +### `updateUniquenessCriteria` + +Replace the matchRules on an existing UniquenessCriteria record. + +`PUT /v1/uniqueness-criteria/{schema}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `schema` | path | string | Yes | Entity schema slug. | + +**Request Body** + +**Sample Call** + +```bash +epilot deduplication updateUniquenessCriteria \ + -p schema=example \ + -d '{"matchRules":[{"name":"string","attributes":[{"attribute":"string"}],"confidence":0}]}' +``` + +Using positional args for path parameters: + +```bash +epilot deduplication updateUniquenessCriteria example +``` + +Using stdin pipe: + +```bash +cat body.json | epilot deduplication updateUniquenessCriteria -p schema=example +``` + +With JSONata filter: + +```bash +epilot deduplication updateUniquenessCriteria -p schema=example --jsonata 'id' +``` + +
+Sample Response + +```json +{ + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "orgId": "string", + "schema": "string", + "matchRules": [ + { + "name": "string", + "attributes": [ + { + "attribute": "string" + } + ], + "confidence": 0 + } + ], + "createdAt": "1970-01-01T00:00:00.000Z", + "updatedAt": "1970-01-01T00:00:00.000Z" +} +``` + +
+ +--- + +### `deleteUniquenessCriteria` + +Delete a UniquenessCriteria record. + +`DELETE /v1/uniqueness-criteria/{schema}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `schema` | path | string | Yes | Entity schema slug. | + +**Sample Call** + +```bash +epilot deduplication deleteUniquenessCriteria \ + -p schema=example +``` + +Using positional args for path parameters: + +```bash +epilot deduplication deleteUniquenessCriteria example +``` + +With JSONata filter: + +```bash +epilot deduplication deleteUniquenessCriteria -p schema=example --jsonata '$' +``` + +--- diff --git a/packages/cli/docs/design.md b/packages/cli/docs/design.md index ad268466f..3fb7c85bf 100644 --- a/packages/cli/docs/design.md +++ b/packages/cli/docs/design.md @@ -39,6 +39,7 @@ epilot design getAllDesigns - [`getDesign`](#getdesign) — Search for a especific design owned by user organization - [`updateDesign`](#updatedesign) — Update a especific design owned by user organization - [`deleteDesign`](#deletedesign) — Search and delete for a especific design owned by user organization +- [`duplicateDesign`](#duplicatedesign) — Duplicate an existing design owned by the user organization. The copy carries over the style, custom CSS and name (marke - [`getThemeFromDesign`](#getthemefromdesign) — Search for a especific design owned by user organization and parse them to a new or old theme - [`getFiles`](#getfiles) — List all files for the user organization bucket - [`uploadFile`](#uploadfile) — Upload a new file for the user organization bucket @@ -613,6 +614,138 @@ epilot design deleteDesign -p designId=4a062990-a6a3-11eb-9828-4f3da7d4935a --js --- +### `duplicateDesign` + +Duplicate an existing design owned by the user organization. The copy carries over the style, custom CSS and name (marke + +`POST /v1/designs/{designId}/duplicate` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `designId` | path | string | Yes | | + +**Sample Call** + +```bash +epilot design duplicateDesign \ + -p designId=4a062990-a6a3-11eb-9828-4f3da7d4935a +``` + +Using positional args for path parameters: + +```bash +epilot design duplicateDesign 4a062990-a6a3-11eb-9828-4f3da7d4935a +``` + +With JSONata filter: + +```bash +epilot design duplicateDesign -p designId=4a062990-a6a3-11eb-9828-4f3da7d4935a --jsonata 'design' +``` + +
+Sample Response + +```json +{ + "design": { + "id": "string", + "created_at": "2021-01-30T08:30:00Z", + "created_by": "string", + "edited": true, + "last_modified_at": "string", + "brand_id": "string", + "brand_name": "string", + "user": { + "emailaddress": "string", + "fullname": "string", + "name": "string", + "userid": "string" + }, + "style_name": "string", + "style": { + "logo": {}, + "palette": {}, + "typography": {}, + "shape": {}, + "consumer": {} + }, + "is_default": true, + "_manifest": ["string"], + "custom_theme": "string", + "use_custom_theme": true, + "design_tokens": { + "coupon": "string", + "cashback": "string", + "custom_css": "string", + "accent_color": "string", + "outline_color": "string", + "divider_color": "string", + "link_color": "string", + "link_hover_color": "string", + "font_size_scale": "xs", + "topbar_height": 0, + "topbar_logo_alignment": "flex-start", + "logo_size": 0, + "input_background": "string", + "input_border_color": "string", + "input_text_color": "string", + "input_label_color": "string", + "input_border_radius": 0, + "input_height": 0, + "input_variant": "outlined", + "button_primary_bg": "string", + "button_primary_text": "string", + "button_primary_hover_bg": "string", + "button_primary_hover_text": "string", + "button_outlined_border": "string", + "button_outlined_text": "string", + "button_outlined_hover_bg": "string", + "button_outlined_hover_text": "string", + "button_ghost_bg": "string", + "button_ghost_text": "string", + "button_ghost_hover_bg": "string", + "button_ghost_hover_text": "string", + "button_border_radius": 0, + "button_height": 0, + "card_background": "string", + "card_border_color": "string", + "card_variant": "shadow", + "summary_card_background": "string", + "toggle_selected_bg": "string", + "toggle_selected_text": "string", + "toggle_hover_bg": "string", + "toggle_hover_text": "string", + "toggle_border_color": "string", + "dropdown_hover_bg": "string", + "dropdown_hover_text": "string", + "dropdown_selected_bg": "string", + "dropdown_selected_text": "string", + "switch_unchecked_color": "string", + "switch_unchecked_bg": "string", + "switch_border_radius": 0, + "checkbox_unchecked_color": "string", + "checkbox_label_color": "string", + "radio_unchecked_color": "string", + "radio_label_color": "string", + "datepicker_selected_bg": "string", + "datepicker_selected_color": "string", + "datepicker_border_radius": 0, + "chip_background": "string", + "chip_hover_background": "string", + "chip_text_color": "string", + "chip_hover_text_color": "string" + } + } +} +``` + +
+ +--- + ### `getThemeFromDesign` Search for a especific design owned by user organization and parse them to a new or old theme diff --git a/packages/cli/docs/document.md b/packages/cli/docs/document.md index 97fe1816e..e3bafd3eb 100644 --- a/packages/cli/docs/document.md +++ b/packages/cli/docs/document.md @@ -39,6 +39,7 @@ epilot document getTemplateMeta - [`getTemplateMeta`](#gettemplatemeta) — Get metadata for a document template - [`generateDocumentV2`](#generatedocumentv2) — Generates documents from templates with variables. - [`convertDocument`](#convertdocument) — Converts a document to a different format. +- [`validateTemplate`](#validatetemplate) — Validates a document template's variable syntax and, optionally, proposes a hotfixed copy of it. ### `getTemplateMeta` @@ -46,7 +47,7 @@ Get metadata for a document template `POST /v2/documents:meta` -**Request Body** +**Request Body** (required) **Sample Call** @@ -107,7 +108,7 @@ Generates documents from templates with variables. - download - preview_url provides a link to download the file | -**Request Body** +**Request Body** (required) **Sample Call** @@ -128,7 +129,7 @@ epilot document generateDocumentV2 \ } }, "context_entity_id": "bcd0aab9-b544-42b0-8bfb-6d449d02eacc", - "user_id": 100321, + "user_id": "100321", "language": "de", "variable_payload": { "additionalProperties": "string" @@ -150,7 +151,7 @@ epilot document generateDocumentV2 \ "template_with_datatable": false, "enabled_template_settings_persistence": false, "misconfigured_margins": false, - "file_entity_id": "1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p" + "file_entity_id": "123e4567-e89b-12d3-a456-426614174000" } }' ``` @@ -244,7 +245,7 @@ epilot document generateDocumentV2 --jsonata 'job_id' "template_with_datatable": false, "enabled_template_settings_persistence": false, "misconfigured_margins": false, - "file_entity_id": "1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p" + "file_entity_id": "123e4567-e89b-12d3-a456-426614174000" } } ``` @@ -259,7 +260,7 @@ Converts a document to a different format. `POST /v2/documents:convert` -**Request Body** +**Request Body** (required) **Sample Call** @@ -314,3 +315,89 @@ epilot document convertDocument --jsonata 'output_document'
--- + +### `validateTemplate` + +Validates a document template's variable syntax and, optionally, proposes a hotfixed copy of it. + +`POST /v2/templates:validate` + +**Request Body** (required) + +**Sample Call** + +```bash +epilot document validateTemplate +``` + +With request body: + +```bash +epilot document validateTemplate \ + -d '{ + "template_document": { + "filename": "Umzugsmeldung.xlsx", + "s3ref": { + "bucket": "document-api-prod", + "key": "uploads/my-template.pdf" + } + }, + "fix": true, + "fix_level": "safe" +}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot document validateTemplate +``` + +With JSONata filter: + +```bash +epilot document validateTemplate --jsonata 'valid' +``` + +
+Sample Response + +```json +{ + "valid": false, + "fixed": true, + "issues": [ + { + "id": "unopened_tag", + "file": "xl/sharedStrings.xml", + "location": "Tabelle1!N4", + "context": "…Datum: {system.date}} Unterschrift…", + "explanation": "The tag is missing an opening brace.", + "fixable": true, + "confidence": "high", + "rule": "balance_opening_delimiter", + "before": "{system.date}}", + "after": "{{system.date}}" + } + ], + "unresolved_errors": [ + { + "id": "string", + "context": "string", + "explanation": "string" + } + ], + "fixed_document": { + "s3ref": { + "bucket": "document-api-prod", + "key": "uploads/my-template.pdf" + }, + "filename": "Umzugsmeldung (fixed).xlsx", + "preview_url": "https://example.com/path" + } +} +``` + +
+ +--- diff --git a/packages/cli/docs/email-settings.md b/packages/cli/docs/email-settings.md index bccbec2a5..6577ed5c2 100644 --- a/packages/cli/docs/email-settings.md +++ b/packages/cli/docs/email-settings.md @@ -57,6 +57,9 @@ epilot email-settings provisionEpilotEmailAddress **O365 Outlook Connection** - [`connectOutlook`](#connectoutlook) — Returns the Microsoft authorization URL for Outlook OAuth. - [`getOutlookConnectionStatus`](#getoutlookconnectionstatus) — Returns all Microsoft 365 / Outlook connections for the organization. +- [`getCalendarAdminConsentStatus`](#getcalendaradminconsentstatus) — Reports whether the caller's organization can connect personal Outlook +- [`getMyCalendarConnection`](#getmycalendarconnection) — Returns the calling user's personal Outlook calendar connection, +- [`disconnectMyCalendar`](#disconnectmycalendar) — Removes the calling user's personal calendar connection. - [`disconnectOutlook`](#disconnectoutlook) — Removes the Microsoft 365 / Outlook connection for a specific tenant. - [`connectOutlookMailbox`](#connectoutlookmailbox) — Connects an Outlook mailbox: - [`disconnectOutlookMailbox`](#disconnectoutlookmailbox) — Disconnects a single Outlook mailbox by email address. @@ -71,6 +74,17 @@ epilot email-settings provisionEpilotEmailAddress - [`disconnectMsTeams`](#disconnectmsteams) — Disconnects Microsoft Teams channel for the organization. - [`getMsTeamsStatus`](#getmsteamsstatus) — Returns the connection status of the Microsoft Teams channel for the organization. +**Custom SMTP** +- [`listSmtpConnections`](#listsmtpconnections) — Returns all custom SMTP connections configured for the organization. +- [`createSmtpConnection`](#createsmtpconnection) — Creates a new custom SMTP connection. Runs a live verify against the SMTP server +- [`getSmtpConnection`](#getsmtpconnection) — Returns a single custom SMTP connection by id. The password is never returned. +- [`updateSmtpConnection`](#updatesmtpconnection) — Partial update; omitted fields keep their existing values. The merged +- [`deleteSmtpConnection`](#deletesmtpconnection) — Deletes a custom SMTP connection. Messages already sent are unaffected. +- [`testSmtpConnection`](#testsmtpconnection) — Re-runs a live SMTP verify against the saved configuration (EHLO + AUTH + NOOP + QUIT) +- [`listSmtpSenders`](#listsmtpsenders) — Returns every address registered to send through a custom SMTP connection. +- [`connectSmtpSender`](#connectsmtpsender) — Registers an address as a sender on a custom SMTP connection: +- [`disconnectSmtpSender`](#disconnectsmtpsender) — Removes a sender address: deletes the email address and its binding to the SMTP + **Settings** - [`getSettings`](#getsettings) — Retrieves settings of a specific type for the organization. - [`addSetting`](#addsetting) — Creates a new setting of the specified type. @@ -797,7 +811,7 @@ Returns the Microsoft authorization URL for Outlook OAuth. ```bash epilot email-settings connectOutlook \ - -d '{"mail":true,"calendar":true}' + -d '{"mail":true,"calendar":true,"return_to":"string"}' ``` Using stdin pipe: @@ -873,6 +887,99 @@ epilot email-settings getOutlookConnectionStatus --jsonata 'connections' --- +### `getCalendarAdminConsentStatus` + +Reports whether the caller's organization can connect personal Outlook + +`GET /v2/outlook/calendar/admin-consent-status` + +**Sample Call** + +```bash +epilot email-settings getCalendarAdminConsentStatus +``` + +With JSONata filter: + +```bash +epilot email-settings getCalendarAdminConsentStatus --jsonata 'admin_consented' +``` + +
+Sample Response + +```json +{ + "admin_consented": true, + "admin_consent_url": "https://example.com/path" +} +``` + +
+ +--- + +### `getMyCalendarConnection` + +Returns the calling user's personal Outlook calendar connection, + +`GET /v2/outlook/calendar/me` + +**Sample Call** + +```bash +epilot email-settings getMyCalendarConnection +``` + +With JSONata filter: + +```bash +epilot email-settings getMyCalendarConnection --jsonata 'connection' +``` + +
+Sample Response + +```json +{ + "connection": { + "user_id": "string", + "tenant_id": "string", + "status": "connected", + "scopes": ["string"], + "connected_by_display_name": "string", + "connected_by_email": "user@example.com", + "connected_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z", + "expires_at": "1970-01-01T00:00:00.000Z" + } +} +``` + +
+ +--- + +### `disconnectMyCalendar` + +Removes the calling user's personal calendar connection. + +`DELETE /v2/outlook/calendar/me` + +**Sample Call** + +```bash +epilot email-settings disconnectMyCalendar +``` + +With JSONata filter: + +```bash +epilot email-settings disconnectMyCalendar --jsonata '$' +``` + +--- + ### `disconnectOutlook` Removes the Microsoft 365 / Outlook connection for a specific tenant. @@ -1021,9 +1128,23 @@ Connects an Outlook mailbox: **Sample Call** +```bash +epilot email-settings connectOutlookMailbox +``` + +With request body: + ```bash epilot email-settings connectOutlookMailbox \ - -d '{"email":"user@example.com","shared_inbox_id":"default","mailboxSyncTimeframe":"5m"}' + -d '{ + "email": "user@example.com", + "shared_inbox_id": "default", + "name": "string", + "user_ids": ["string"], + "group_ids": ["string"], + "default_signature_id": "string", + "mailboxSyncTimeframe": "5m" +}' ``` Using stdin pipe: @@ -1298,6 +1419,506 @@ epilot email-settings getConnectedOutlookEmails --jsonata 'outlook_emails' --- +### `listSmtpConnections` + +Returns all custom SMTP connections configured for the organization. + +`GET /v2/smtp/connections` + +**Sample Call** + +```bash +epilot email-settings listSmtpConnections +``` + +With JSONata filter: + +```bash +epilot email-settings listSmtpConnections --jsonata 'connections' +``` + +
+Sample Response + +```json +{ + "connections": [ + { + "connection_id": "string", + "smtp_host": "string", + "smtp_port": "string", + "smtp_secure": "tls", + "smtp_username": "string", + "smtp_password": "string", + "connected_by_display_name": "string", + "connected_by_email": "user@example.com", + "connected_by_user_id": "string", + "connected_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z", + "last_tested_at": "1970-01-01T00:00:00.000Z", + "last_test_status": "ok", + "last_test_error": "string" + } + ], + "has_connections": true +} +``` + +
+ +--- + +### `createSmtpConnection` + +Creates a new custom SMTP connection. Runs a live verify against the SMTP server + +`POST /v2/smtp/connections` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `skip_test` | query | boolean | No | Save without running the live verify. Use when the server is not reachable from +epilot yet, or when a referenced `{{ env.* }}` secret has not been set. The +connection is stored untested — `last_test_s | + +**Request Body** (required) + +**Sample Call** + +```bash +epilot email-settings createSmtpConnection +``` + +With request body: + +```bash +epilot email-settings createSmtpConnection \ + -d '{ + "smtp_host": "string", + "smtp_port": "string", + "smtp_secure": "tls", + "smtp_username": "string", + "smtp_password": "{{ env.smtp_password }}" +}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot email-settings createSmtpConnection +``` + +With JSONata filter: + +```bash +epilot email-settings createSmtpConnection --jsonata 'connection_id' +``` + +
+Sample Response + +```json +{ + "connection_id": "string", + "smtp_host": "string", + "smtp_port": "string", + "smtp_secure": "tls", + "smtp_username": "string", + "smtp_password": "string", + "connected_by_display_name": "string", + "connected_by_email": "user@example.com", + "connected_by_user_id": "string", + "connected_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z", + "last_tested_at": "1970-01-01T00:00:00.000Z", + "last_test_status": "ok", + "last_test_error": "string" +} +``` + +
+ +--- + +### `getSmtpConnection` + +Returns a single custom SMTP connection by id. The password is never returned. + +`GET /v2/smtp/connections/{connectionId}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `connectionId` | path | string | Yes | | + +**Sample Call** + +```bash +epilot email-settings getSmtpConnection \ + -p connectionId=123e4567-e89b-12d3-a456-426614174000 +``` + +Using positional args for path parameters: + +```bash +epilot email-settings getSmtpConnection 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot email-settings getSmtpConnection -p connectionId=123e4567-e89b-12d3-a456-426614174000 --jsonata 'connection_id' +``` + +
+Sample Response + +```json +{ + "connection_id": "string", + "smtp_host": "string", + "smtp_port": "string", + "smtp_secure": "tls", + "smtp_username": "string", + "smtp_password": "string", + "connected_by_display_name": "string", + "connected_by_email": "user@example.com", + "connected_by_user_id": "string", + "connected_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z", + "last_tested_at": "1970-01-01T00:00:00.000Z", + "last_test_status": "ok", + "last_test_error": "string" +} +``` + +
+ +--- + +### `updateSmtpConnection` + +Partial update; omitted fields keep their existing values. The merged + +`PUT /v2/smtp/connections/{connectionId}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `connectionId` | path | string | Yes | | +| `skip_test` | query | boolean | No | Save without running the live verify. Use when the server is not reachable from +epilot yet, or when a referenced `{{ env.* }}` secret has not been set. The +connection is stored untested — `last_test_s | + +**Request Body** (required) + +**Sample Call** + +```bash +epilot email-settings updateSmtpConnection \ + -p connectionId=123e4567-e89b-12d3-a456-426614174000 \ + -d '{"smtp_host":"string","smtp_port":"string","smtp_secure":"tls","smtp_username":"string","smtp_password":"string"}' +``` + +Using positional args for path parameters: + +```bash +epilot email-settings updateSmtpConnection 123e4567-e89b-12d3-a456-426614174000 +``` + +Using stdin pipe: + +```bash +cat body.json | epilot email-settings updateSmtpConnection -p connectionId=123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot email-settings updateSmtpConnection -p connectionId=123e4567-e89b-12d3-a456-426614174000 --jsonata 'connection_id' +``` + +
+Sample Response + +```json +{ + "connection_id": "string", + "smtp_host": "string", + "smtp_port": "string", + "smtp_secure": "tls", + "smtp_username": "string", + "smtp_password": "string", + "connected_by_display_name": "string", + "connected_by_email": "user@example.com", + "connected_by_user_id": "string", + "connected_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z", + "last_tested_at": "1970-01-01T00:00:00.000Z", + "last_test_status": "ok", + "last_test_error": "string" +} +``` + +
+ +--- + +### `deleteSmtpConnection` + +Deletes a custom SMTP connection. Messages already sent are unaffected. + +`DELETE /v2/smtp/connections/{connectionId}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `connectionId` | path | string | Yes | | + +**Sample Call** + +```bash +epilot email-settings deleteSmtpConnection \ + -p connectionId=123e4567-e89b-12d3-a456-426614174000 +``` + +Using positional args for path parameters: + +```bash +epilot email-settings deleteSmtpConnection 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot email-settings deleteSmtpConnection -p connectionId=123e4567-e89b-12d3-a456-426614174000 --jsonata 'success' +``` + +
+Sample Response + +```json +{ + "success": true, + "connection_id": "string" +} +``` + +
+ +--- + +### `testSmtpConnection` + +Re-runs a live SMTP verify against the saved configuration (EHLO + AUTH + NOOP + QUIT) + +`POST /v2/smtp/connections/{connectionId}/test` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `connectionId` | path | string | Yes | | + +**Sample Call** + +```bash +epilot email-settings testSmtpConnection \ + -p connectionId=123e4567-e89b-12d3-a456-426614174000 +``` + +Using positional args for path parameters: + +```bash +epilot email-settings testSmtpConnection 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot email-settings testSmtpConnection -p connectionId=123e4567-e89b-12d3-a456-426614174000 --jsonata 'status' +``` + +
+Sample Response + +```json +{ + "status": "ok", + "error": "string", + "tested_at": "1970-01-01T00:00:00.000Z" +} +``` + +
+ +--- + +### `listSmtpSenders` + +Returns every address registered to send through a custom SMTP connection. + +`GET /v2/smtp/senders` + +**Sample Call** + +```bash +epilot email-settings listSmtpSenders +``` + +With JSONata filter: + +```bash +epilot email-settings listSmtpSenders --jsonata 'senders' +``` + +
+Sample Response + +```json +{ + "senders": [ + { + "email": "user@example.com", + "connection_id": "string", + "reply_to_email": "string", + "connected_at": "1970-01-01T00:00:00.000Z", + "connected_by_user_id": "string" + } + ] +} +``` + +
+ +--- + +### `connectSmtpSender` + +Registers an address as a sender on a custom SMTP connection: + +`POST /v2/smtp/senders` + +**Request Body** (required) + +**Sample Call** + +```bash +epilot email-settings connectSmtpSender +``` + +With request body: + +```bash +epilot email-settings connectSmtpSender \ + -d '{ + "email": "user@example.com", + "connection_id": "string", + "name": "string", + "reply_to_email": "string", + "shared_inbox_id": "default", + "user_ids": ["string"], + "group_ids": ["string"], + "default_signature_id": "string" +}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot email-settings connectSmtpSender +``` + +With JSONata filter: + +```bash +epilot email-settings connectSmtpSender --jsonata 'email_address' +``` + +
+Sample Response + +```json +{ + "email_address": { + "id": "a10bd0ff-4391-4cfc-88ee-b19d718a9bf7", + "created_at": "2024-01-15T10:30:00Z", + "updated_at": "2024-01-20T14:45:00Z", + "created_by": "user-123", + "updated_by": "user-456", + "address": "sales@yourcompany.com", + "name": "Sales Team", + "user_ids": ["user-123", "user-456"], + "group_ids": ["group-789"], + "default_signature_id": "sig-abc", + "shared_inbox_id": "inbox-xyz", + "is_active": true, + "is_primary": false, + "is_epilot_email_address": false + }, + "sender": { + "email": "user@example.com", + "connection_id": "string", + "reply_to_email": "string", + "connected_at": "1970-01-01T00:00:00.000Z", + "connected_by_user_id": "string" + } +} +``` + +
+ +--- + +### `disconnectSmtpSender` + +Removes a sender address: deletes the email address and its binding to the SMTP + +`DELETE /v2/smtp/senders/{email}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `email` | path | string (email) | Yes | | + +**Sample Call** + +```bash +epilot email-settings disconnectSmtpSender \ + -p email=user@example.com +``` + +Using positional args for path parameters: + +```bash +epilot email-settings disconnectSmtpSender user@example.com +``` + +With JSONata filter: + +```bash +epilot email-settings disconnectSmtpSender -p email=user@example.com --jsonata 'email' +``` + +
+Sample Response + +```json +{ + "success": true, + "email": "user@example.com" +} +``` + +
+ +--- + ### `outlookOAuthCallback` Exchanges authorization code for tokens and stores them. @@ -1318,6 +1939,7 @@ Exchanges authorization code for tokens and stores them. | `error_uri` | query | string | No | | | `admin_consent` | query | string | No | | | `tenant` | query | string | No | | +| `clientdata` | query | string | No | | **Sample Call** diff --git a/packages/cli/docs/entity-mapping.md b/packages/cli/docs/entity-mapping.md index c5fd7a616..ce6dd9dc4 100644 --- a/packages/cli/docs/entity-mapping.md +++ b/packages/cli/docs/entity-mapping.md @@ -88,13 +88,15 @@ epilot entity-mapping storeConfig \ "allow_failure": true, "target_schema": "string", "target_unique": ["string"], + "use_uniqueness_criteria": false, "loop_config": {}, "conditionMode": "oneOf", "conditions": [], "mapping_attributes": [], "relation_attributes": [], "linkback_relation_attribute": "mapped_entities", - "linkback_relation_tags": ["string"] + "linkback_relation_tags": ["string"], + "graph_context": [] } ], "created_at": "1970-01-01T00:00:00.000Z", @@ -145,13 +147,15 @@ epilot entity-mapping storeConfig --jsonata '$' "allow_failure": true, "target_schema": "string", "target_unique": ["string"], + "use_uniqueness_criteria": false, "loop_config": {}, "conditionMode": "oneOf", "conditions": [], "mapping_attributes": [], "relation_attributes": [], "linkback_relation_attribute": "mapped_entities", - "linkback_relation_tags": ["string"] + "linkback_relation_tags": ["string"], + "graph_context": [] } ], "created_at": "1970-01-01T00:00:00.000Z", @@ -225,13 +229,15 @@ epilot entity-mapping getConfig -p id=70542580-2b38-4bfc-af8d-bb90102f9f47 --jso "allow_failure": true, "target_schema": "string", "target_unique": ["string"], + "use_uniqueness_criteria": false, "loop_config": {}, "conditionMode": "oneOf", "conditions": [], "mapping_attributes": [], "relation_attributes": [], "linkback_relation_attribute": "mapped_entities", - "linkback_relation_tags": ["string"] + "linkback_relation_tags": ["string"], + "graph_context": [] } ], "created_at": "1970-01-01T00:00:00.000Z", @@ -305,13 +311,15 @@ epilot entity-mapping deleteConfig -p id=70542580-2b38-4bfc-af8d-bb90102f9f47 -- "allow_failure": true, "target_schema": "string", "target_unique": ["string"], + "use_uniqueness_criteria": false, "loop_config": {}, "conditionMode": "oneOf", "conditions": [], "mapping_attributes": [], "relation_attributes": [], "linkback_relation_attribute": "mapped_entities", - "linkback_relation_tags": ["string"] + "linkback_relation_tags": ["string"], + "graph_context": [] } ], "created_at": "1970-01-01T00:00:00.000Z", @@ -432,13 +440,15 @@ epilot entity-mapping storeNewVersion \ "allow_failure": true, "target_schema": "string", "target_unique": ["string"], + "use_uniqueness_criteria": false, "loop_config": {}, "conditionMode": "oneOf", "conditions": [], "mapping_attributes": [], "relation_attributes": [], "linkback_relation_attribute": "mapped_entities", - "linkback_relation_tags": ["string"] + "linkback_relation_tags": ["string"], + "graph_context": [] } ], "created_at": "1970-01-01T00:00:00.000Z", @@ -495,13 +505,15 @@ epilot entity-mapping storeNewVersion -p id=70542580-2b38-4bfc-af8d-bb90102f9f47 "allow_failure": true, "target_schema": "string", "target_unique": ["string"], + "use_uniqueness_criteria": false, "loop_config": {}, "conditionMode": "oneOf", "conditions": [], "mapping_attributes": [], "relation_attributes": [], "linkback_relation_attribute": "mapped_entities", - "linkback_relation_tags": ["string"] + "linkback_relation_tags": ["string"], + "graph_context": [] } ], "created_at": "1970-01-01T00:00:00.000Z", @@ -577,13 +589,15 @@ epilot entity-mapping getConfigVersion -p id=70542580-2b38-4bfc-af8d-bb90102f9f4 "allow_failure": true, "target_schema": "string", "target_unique": ["string"], + "use_uniqueness_criteria": false, "loop_config": {}, "conditionMode": "oneOf", "conditions": [], "mapping_attributes": [], "relation_attributes": [], "linkback_relation_attribute": "mapped_entities", - "linkback_relation_tags": ["string"] + "linkback_relation_tags": ["string"], + "graph_context": [] } ], "created_at": "1970-01-01T00:00:00.000Z", @@ -636,6 +650,10 @@ epilot entity-mapping executeMapping \ "entity_id": "string", "entity_schema": "submission" }, + "loop_ref": { + "entity_id": "string", + "entity_schema": "submission" + }, "targets": [ { "id": "string", @@ -643,66 +661,15 @@ epilot entity-mapping executeMapping \ "allow_failure": true, "target_schema": "string", "target_unique": ["string"], - "loop_config": { - "source_path": "string", - "length": "the length of the array" - }, + "use_uniqueness_criteria": false, + "loop_config": {}, "conditionMode": "oneOf", - "conditions": [ - { - "_exists": { - "source": "string", - "value": "string" - }, - "_equals": { - "source": "string", - "value": "string" - }, - "_not_exists": { - "source": "string", - "value": "string" - }, - "_any_of": { - "source": "string", - "value": "string" - } - } - ], - "mapping_attributes": [ - { - "target": "_tags", - "operation": { - "_append": ["new", "tags"], - "_uniq": true - } - }, - { - "mode": "copy_if_exists", - "target": "string", - "value": {} - } - ], - "relation_attributes": [ - { - "target": "string", - "target_tags": ["string"], - "target_tags_include_source": false, - "override_with_source_filter": false, - "source_filter": { - "limit": 0, - "schema": "string", - "attribute": "string", - "relation_tag": "string", - "tag": "string", - "self": false - }, - "related_to": {}, - "mode": "append", - "origin": "system_recommendation" - } - ], + "conditions": [], + "mapping_attributes": [], + "relation_attributes": [], "linkback_relation_attribute": "mapped_entities", - "linkback_relation_tags": ["string"] + "linkback_relation_tags": ["string"], + "graph_context": [] } ] }' @@ -803,13 +770,15 @@ epilot entity-mapping searchConfigs --jsonata '$' "allow_failure": true, "target_schema": "string", "target_unique": ["string"], + "use_uniqueness_criteria": false, "loop_config": {}, "conditionMode": "oneOf", "conditions": [], "mapping_attributes": [], "relation_attributes": [], "linkback_relation_attribute": "mapped_entities", - "linkback_relation_tags": ["string"] + "linkback_relation_tags": ["string"], + "graph_context": [] } ], "created_at": "1970-01-01T00:00:00.000Z", @@ -904,6 +873,7 @@ epilot entity-mapping executeRelations \ "target": "string", "target_tags": ["string"], "target_tags_include_source": false, + "target_entity_tags": ["string"], "override_with_source_filter": false, "source_filter": { "limit": 0, @@ -1014,13 +984,15 @@ epilot entity-mapping getMappingConfig -p id=70542580-2b38-4bfc-af8d-bb90102f9f4 "allow_failure": true, "target_schema": "string", "target_unique": ["string"], + "use_uniqueness_criteria": false, "loop_config": {}, "conditionMode": "oneOf", "conditions": [], "mapping_attributes": [], "relation_attributes": [], "linkback_relation_attribute": "mapped_entities", - "linkback_relation_tags": ["string"] + "linkback_relation_tags": ["string"], + "graph_context": [] } ], "created_at": "1970-01-01T00:00:00.000Z", @@ -1085,13 +1057,15 @@ epilot entity-mapping putMappingConfig \ "allow_failure": true, "target_schema": "string", "target_unique": ["string"], + "use_uniqueness_criteria": false, "loop_config": {}, "conditionMode": "oneOf", "conditions": [], "mapping_attributes": [], "relation_attributes": [], "linkback_relation_attribute": "mapped_entities", - "linkback_relation_tags": ["string"] + "linkback_relation_tags": ["string"], + "graph_context": [] } ], "created_at": "1970-01-01T00:00:00.000Z", @@ -1148,13 +1122,15 @@ epilot entity-mapping putMappingConfig -p id=70542580-2b38-4bfc-af8d-bb90102f9f4 "allow_failure": true, "target_schema": "string", "target_unique": ["string"], + "use_uniqueness_criteria": false, "loop_config": {}, "conditionMode": "oneOf", "conditions": [], "mapping_attributes": [], "relation_attributes": [], "linkback_relation_attribute": "mapped_entities", - "linkback_relation_tags": ["string"] + "linkback_relation_tags": ["string"], + "graph_context": [] } ], "created_at": "1970-01-01T00:00:00.000Z", diff --git a/packages/cli/docs/entity.md b/packages/cli/docs/entity.md index 1cabbb910..eded54738 100644 --- a/packages/cli/docs/entity.md +++ b/packages/cli/docs/entity.md @@ -38,14 +38,12 @@ epilot entity listSchemas **Schemas** - [`listSchemas`](#listschemas) — Get the latest versions of all schemas - [`listSchemasV2`](#listschemasv2) — Get the latest versions of all schemas. -- [`getSchema`](#getschema) — By default gets the current version of the Schema (frozen version if frozen, otherwise latest). +- [`getSchema`](#getschema) — Gets the latest version of the Schema. - [`putSchema`](#putschema) — Create or update a schema with a new version. - [`deleteSchema`](#deleteschema) — Delete a schema, or a specific version of a schema - [`getJsonSchema`](#getjsonschema) — Get formal JSON schema definition draft 2020-12 for the given epilot schema - [`getSchemaExample`](#getschemaexample) — Get a full example entity for the given schema - [`getSchemaVersions`](#getschemaversions) — Get all versions of this schema ordered by the latest versions including drafts. -- [`freezeSchema`](#freezeschema) — Freeze a schema at its current version, or at a specific version. -- [`unfreezeSchema`](#unfreezeschema) — Unfreeze a schema. Promotes the latest version to the current version for all users. - [`listAvailableCapabilities`](#listavailablecapabilities) — List available capabilities for schema - [`listSchemaBlueprints`](#listschemablueprints) — List canonical versions of all available schemas - [`listTaxonomyClassificationsForSchema`](#listtaxonomyclassificationsforschema) — List taxonomy classifications for a given schema @@ -106,6 +104,7 @@ epilot entity listSchemas **Import-Export** - [`exportEntities`](#exportentities) — Export entity data in a CSV-format. The export will export data as close as possible to what is visible on Entity UI tab - [`importEntities`](#importentities) — This endpoint enables the import of entities into the platform. +- [`abortEntityImport`](#abortentityimport) — Asks a running import to stop. Rows already imported are **kept** - this is a stop, not a **Saved Views** - [`listSavedViews`](#listsavedviews) — Get the Saved Views based on the schema @@ -147,7 +146,8 @@ Get the latest versions of all schemas | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | | `unpublished` | query | boolean | No | Return unpublished draft schemas | -| `latest` | query | boolean | No | When true, return the latest version instead of the frozen version for frozen schemas. | +| `latest` | query | boolean | No | No longer has any effect: schema freezing is retired and the latest version is always returned. + | | `exclude` | query | string[] | No | List of schema slugs to exclude from the results. Accepts a comma-separated list of slugs to exclude from the results. | | `include` | query | string[] | No | List of schema slugs to include in the results. When provided, only these schemas are returned. Accepts a comma-separated list of slugs. | @@ -201,7 +201,8 @@ epilot entity listSchemas --jsonata 'results[0]' "_purpose": ["string"], "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], "explicit_search_mappings": {}, - "group_headlines": [] + "group_headlines": [], + "conditions": [] } ] } @@ -223,7 +224,8 @@ Get the latest versions of all schemas. | ---- | -- | ---- | -------- | ----------- | | `full` | query | boolean | No | Return full schemas including all attributes and capabilities | | `unpublished` | query | boolean | No | Return unpublished draft schemas | -| `latest` | query | boolean | No | When true, return the latest version instead of the frozen version for frozen schemas. | +| `latest` | query | boolean | No | No longer has any effect: schema freezing is retired and the latest version is always returned. + | | `exclude` | query | string[] | No | List of schema slugs to exclude from the results. Accepts a comma-separated list of slugs to exclude from the results. | | `include` | query | string[] | No | List of schema slugs to include in the results. When provided, only these schemas are returned. Accepts a comma-separated list of slugs. | @@ -277,7 +279,8 @@ epilot entity listSchemasV2 --jsonata 'results[0]' "_purpose": ["string"], "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], "explicit_search_mappings": {}, - "group_headlines": [] + "group_headlines": [], + "conditions": [] } ] } @@ -289,7 +292,7 @@ epilot entity listSchemasV2 --jsonata 'results[0]' ### `getSchema` -By default gets the current version of the Schema (frozen version if frozen, otherwise latest). +Gets the latest version of the Schema. `GET /v1/entity/schemas/{slug}` @@ -299,7 +302,8 @@ By default gets the current version of the Schema (frozen version if frozen, oth | ---- | -- | ---- | -------- | ----------- | | `slug` | path | string | Yes | Entity Type | | `id` | query | string (uuid) | No | | -| `latest` | query | boolean | No | When true, return the latest version instead of the frozen version for frozen schemas. | +| `latest` | query | boolean | No | No longer has any effect: schema freezing is retired and the latest version is always returned. + | **Sample Call** @@ -379,6 +383,20 @@ epilot entity getSchema -p slug=contact --jsonata '$' }, "sharing": { "show_sharing_button": true + }, + "grid_layout": { + "id": "string", + "columns": 0, + "cells": [0] + }, + "widget_order": ["string"], + "widget_visibility": { + "next_best_action": true, + "address_map": false + }, + "widget_widths": { + "address_map": "full_width", + "recent_communications": "one_third_width" } }, "capabilities": [ @@ -387,9 +405,11 @@ epilot entity getSchema -p slug=contact --jsonata '$' "name": "customer_messaging", "title": "Messaging", "attributes": [], + "overridable_attribute": false, "_purpose": ["taxonomy-slug:classification-slug"], "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], "app_id": "123e4567-e89b-12d3-a456-426614174000", + "adjust_installment": {}, "ui_config": {}, "ui_hooks": [], "feature_flag": "FF_MY_FEATURE_FLAG", @@ -452,6 +472,14 @@ epilot entity getSchema -p slug=contact --jsonata '$' "_purpose": ["taxonomy-slug:classification-slug"], "_manifest": ["123e4567-e89b-12d3-a456-426614174000"] } + ], + "conditions": [ + { + "id": "d5839b94-ba20-4225-a78e-76951d352bd6", + "name": "delivery_area", + "label": "Delivery Area", + "type": "string" + } ] } ``` @@ -531,6 +559,20 @@ epilot entity putSchema \ }, "sharing": { "show_sharing_button": true + }, + "grid_layout": { + "id": "string", + "columns": 0, + "cells": [0] + }, + "widget_order": ["string"], + "widget_visibility": { + "next_best_action": true, + "address_map": false + }, + "widget_widths": { + "address_map": "full_width", + "recent_communications": "one_third_width" } }, "capabilities": [ @@ -539,9 +581,11 @@ epilot entity putSchema \ "name": "customer_messaging", "title": "Messaging", "attributes": [], + "overridable_attribute": false, "_purpose": ["taxonomy-slug:classification-slug"], "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], "app_id": "123e4567-e89b-12d3-a456-426614174000", + "adjust_installment": {}, "ui_config": {}, "ui_hooks": [], "feature_flag": "FF_MY_FEATURE_FLAG", @@ -604,6 +648,14 @@ epilot entity putSchema \ "_purpose": ["taxonomy-slug:classification-slug"], "_manifest": ["123e4567-e89b-12d3-a456-426614174000"] } + ], + "conditions": [ + { + "id": "d5839b94-ba20-4225-a78e-76951d352bd6", + "name": "delivery_area", + "label": "Delivery Area", + "type": "string" + } ] }' ``` @@ -685,6 +737,20 @@ epilot entity putSchema -p slug=contact --jsonata '$' }, "sharing": { "show_sharing_button": true + }, + "grid_layout": { + "id": "string", + "columns": 0, + "cells": [0] + }, + "widget_order": ["string"], + "widget_visibility": { + "next_best_action": true, + "address_map": false + }, + "widget_widths": { + "address_map": "full_width", + "recent_communications": "one_third_width" } }, "capabilities": [ @@ -693,9 +759,11 @@ epilot entity putSchema -p slug=contact --jsonata '$' "name": "customer_messaging", "title": "Messaging", "attributes": [], + "overridable_attribute": false, "_purpose": ["taxonomy-slug:classification-slug"], "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], "app_id": "123e4567-e89b-12d3-a456-426614174000", + "adjust_installment": {}, "ui_config": {}, "ui_hooks": [], "feature_flag": "FF_MY_FEATURE_FLAG", @@ -758,6 +826,14 @@ epilot entity putSchema -p slug=contact --jsonata '$' "_purpose": ["taxonomy-slug:classification-slug"], "_manifest": ["123e4567-e89b-12d3-a456-426614174000"] } + ], + "conditions": [ + { + "id": "d5839b94-ba20-4225-a78e-76951d352bd6", + "name": "delivery_area", + "label": "Delivery Area", + "type": "string" + } ] } ``` @@ -1062,7 +1138,8 @@ epilot entity getSchemaVersions -p slug=contact --jsonata 'versions' "_purpose": ["string"], "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], "explicit_search_mappings": {}, - "group_headlines": [] + "group_headlines": [], + "conditions": [] } ], "drafts": [ @@ -1098,7 +1175,8 @@ epilot entity getSchemaVersions -p slug=contact --jsonata 'versions' "_purpose": ["string"], "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], "explicit_search_mappings": {}, - "group_headlines": [] + "group_headlines": [], + "conditions": [] } ], "versions_more": true, @@ -1111,357 +1189,6 @@ epilot entity getSchemaVersions -p slug=contact --jsonata 'versions' --- -### `freezeSchema` - -Freeze a schema at its current version, or at a specific version. - -`POST /v1/entity/schemas/{slug}/freeze` - -**Parameters** - -| Name | In | Type | Required | Description | -| ---- | -- | ---- | -------- | ----------- | -| `slug` | path | string | Yes | Entity Type | - -**Request Body** - -**Sample Call** - -```bash -epilot entity freezeSchema \ - -p slug=contact \ - -d '{"version_id":"3fa85f64-5717-4562-b3fc-2c963f66afa6"}' -``` - -Using positional args for path parameters: - -```bash -epilot entity freezeSchema contact -``` - -Using stdin pipe: - -```bash -cat body.json | epilot entity freezeSchema -p slug=contact -``` - -With JSONata filter: - -```bash -epilot entity freezeSchema -p slug=contact --jsonata '$' -``` - -
-Sample Response - -```json -{ - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "created_at": "string", - "updated_at": "string", - "comment": "string", - "source": { - "id": "string", - "type": "string" - }, - "frozen": true, - "latest": true, - "_summary": true, - "slug": "contact", - "version": 1, - "blueprint": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "feature_flag": "FF_MY_FEATURE_FLAG", - "enable_setting": ["360_features"], - "name": "Contact", - "plural": "Contacts", - "description": "Example description", - "docs_url": "https://docs.epilot.io/docs/pricing/entities", - "category": "customer_relations", - "published": false, - "draft": false, - "icon": "person", - "title_template": "{{first_name}} {{last_name}}", - "ui_config": { - "table_view": { - "view_type": "default", - "row_actions": ["string"], - "bulk_actions": ["string"], - "navbar_actions": [], - "enable_thumbnails": false - }, - "create_view": { - "view_type": "default", - "search_params": {} - }, - "edit_view": { - "view_type": "default", - "search_params": {}, - "summary_attributes": ["email"] - }, - "single_view": { - "view_type": "default", - "search_params": {}, - "summary_attributes": ["email"] - }, - "list_item": { - "summary_attributes": [], - "quick_actions": [], - "ui_config": {} - }, - "sharing": { - "show_sharing_button": true - } - }, - "capabilities": [ - { - "id": "d5839b94-ba20-4225-a78e-76951d352bd6", - "name": "customer_messaging", - "title": "Messaging", - "attributes": [], - "_purpose": ["taxonomy-slug:classification-slug"], - "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], - "app_id": "123e4567-e89b-12d3-a456-426614174000", - "ui_config": {}, - "ui_hooks": [], - "feature_flag": "FF_MY_FEATURE_FLAG", - "settings_flag": [], - "schemas": [] - } - ], - "group_settings": [ - { - "id": "e18a532b-ae79-4d86-a6a5-e5dbfb579d14", - "label": "Contact Details", - "expanded": true, - "order": 1 - }, - { - "id": "e9a1ae28-27ba-4fa0-a79c-e279cc5c4a6e", - "label": "Address Details", - "expanded": false, - "order": 2, - "info_tooltip_title": {} - } - ], - "layout_settings": { - "grid_gap": "string", - "grid_template_columns": "string" - }, - "dialog_config": {}, - "attributes": [ - { - "name": "email", - "type": "email", - "label": "Email", - "required": true - }, - { - "name": "first_name", - "type": "string", - "label": "First Name" - } - ], - "_purpose": ["string"], - "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], - "explicit_search_mappings": { - "image": { - "type": "keyword", - "index": false - } - }, - "group_headlines": [ - { - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "name": "string", - "label": "string", - "layout": "string", - "group": "string", - "order": 0, - "type": "headline", - "enable_divider": false, - "divider": "top_divider", - "_purpose": ["taxonomy-slug:classification-slug"], - "_manifest": ["123e4567-e89b-12d3-a456-426614174000"] - } - ] -} -``` - -
- ---- - -### `unfreezeSchema` - -Unfreeze a schema. Promotes the latest version to the current version for all users. - -`POST /v1/entity/schemas/{slug}/unfreeze` - -**Parameters** - -| Name | In | Type | Required | Description | -| ---- | -- | ---- | -------- | ----------- | -| `slug` | path | string | Yes | Entity Type | - -**Sample Call** - -```bash -epilot entity unfreezeSchema \ - -p slug=contact -``` - -Using positional args for path parameters: - -```bash -epilot entity unfreezeSchema contact -``` - -With JSONata filter: - -```bash -epilot entity unfreezeSchema -p slug=contact --jsonata '$' -``` - -
-Sample Response - -```json -{ - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "created_at": "string", - "updated_at": "string", - "comment": "string", - "source": { - "id": "string", - "type": "string" - }, - "frozen": true, - "latest": true, - "_summary": true, - "slug": "contact", - "version": 1, - "blueprint": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "feature_flag": "FF_MY_FEATURE_FLAG", - "enable_setting": ["360_features"], - "name": "Contact", - "plural": "Contacts", - "description": "Example description", - "docs_url": "https://docs.epilot.io/docs/pricing/entities", - "category": "customer_relations", - "published": false, - "draft": false, - "icon": "person", - "title_template": "{{first_name}} {{last_name}}", - "ui_config": { - "table_view": { - "view_type": "default", - "row_actions": ["string"], - "bulk_actions": ["string"], - "navbar_actions": [], - "enable_thumbnails": false - }, - "create_view": { - "view_type": "default", - "search_params": {} - }, - "edit_view": { - "view_type": "default", - "search_params": {}, - "summary_attributes": ["email"] - }, - "single_view": { - "view_type": "default", - "search_params": {}, - "summary_attributes": ["email"] - }, - "list_item": { - "summary_attributes": [], - "quick_actions": [], - "ui_config": {} - }, - "sharing": { - "show_sharing_button": true - } - }, - "capabilities": [ - { - "id": "d5839b94-ba20-4225-a78e-76951d352bd6", - "name": "customer_messaging", - "title": "Messaging", - "attributes": [], - "_purpose": ["taxonomy-slug:classification-slug"], - "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], - "app_id": "123e4567-e89b-12d3-a456-426614174000", - "ui_config": {}, - "ui_hooks": [], - "feature_flag": "FF_MY_FEATURE_FLAG", - "settings_flag": [], - "schemas": [] - } - ], - "group_settings": [ - { - "id": "e18a532b-ae79-4d86-a6a5-e5dbfb579d14", - "label": "Contact Details", - "expanded": true, - "order": 1 - }, - { - "id": "e9a1ae28-27ba-4fa0-a79c-e279cc5c4a6e", - "label": "Address Details", - "expanded": false, - "order": 2, - "info_tooltip_title": {} - } - ], - "layout_settings": { - "grid_gap": "string", - "grid_template_columns": "string" - }, - "dialog_config": {}, - "attributes": [ - { - "name": "email", - "type": "email", - "label": "Email", - "required": true - }, - { - "name": "first_name", - "type": "string", - "label": "First Name" - } - ], - "_purpose": ["string"], - "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], - "explicit_search_mappings": { - "image": { - "type": "keyword", - "index": false - } - }, - "group_headlines": [ - { - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "name": "string", - "label": "string", - "layout": "string", - "group": "string", - "order": 0, - "type": "headline", - "enable_divider": false, - "divider": "top_divider", - "_purpose": ["taxonomy-slug:classification-slug"], - "_manifest": ["123e4567-e89b-12d3-a456-426614174000"] - } - ] -} -``` - -
- ---- - ### `listAvailableCapabilities` List available capabilities for schema @@ -1504,9 +1231,11 @@ epilot entity listAvailableCapabilities -p slug=contact --jsonata 'results[0]' "name": "customer_messaging", "title": "Messaging", "attributes": [], + "overridable_attribute": false, "_purpose": ["taxonomy-slug:classification-slug"], "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], "app_id": "123e4567-e89b-12d3-a456-426614174000", + "adjust_installment": {}, "ui_config": {}, "ui_hooks": [], "feature_flag": "FF_MY_FEATURE_FLAG", @@ -1577,7 +1306,8 @@ epilot entity listSchemaBlueprints --jsonata 'results[0]' "_purpose": ["string"], "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], "explicit_search_mappings": {}, - "group_headlines": [] + "group_headlines": [], + "conditions": [] } ] } @@ -1612,6 +1342,7 @@ epilot entity searchEntities \ "from": 0, "size": 10, "hydrate": false, + "anonymize": false, "fields": ["_id", "_title", "first_name", "account", "!account.*._files", "**._product"], "aggs": { "contact-count-per-tag": { @@ -1737,6 +1468,7 @@ epilot entity listEntities \ "from": 0, "size": 10, "hydrate": false, + "anonymize": false, "fields": ["_id", "_title", "first_name", "account", "!account.*._files", "**._product"], "aggs": { "contact-count-per-tag": { @@ -2333,6 +2065,8 @@ Gets Entity by id. | `id` | path | string (uuid) | Yes | Entity id | | `slug` | path | string | Yes | Entity Type | | `hydrate` | query | boolean | No | When true, enables entity hydration to resolve nested $relation & $relation_ref references in-place. | +| `anonymize` | query | boolean | No | When true, anonymizes PII in the response: identifiers (names, emails, phone numbers, IBANs) are replaced +with deterministic pseudonyms (stable within an org), addresses are generalized to postal code | | `fields` | query | string[] | No | List of entity fields to include in results | | `apply_changesets` | query | boolean | No | When true, applies pending changeset proposed values in-place on the response entity. The response includes both the hydrated values and the raw _changesets field. @@ -2546,6 +2280,8 @@ Gets Entity and relations by id. | `id` | path | string (uuid) | Yes | Entity id | | `slug` | path | string | Yes | Entity Type | | `hydrate` | query | boolean | No | When true, enables entity hydration to resolve nested $relation & $relation_ref references in-place. | +| `anonymize` | query | boolean | No | When true, anonymizes PII in the response: identifiers (names, emails, phone numbers, IBANs) are replaced +with deterministic pseudonyms (stable within an org), addresses are generalized to postal code | **Sample Call** @@ -2885,6 +2621,8 @@ Autocomplete entity attributes | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | +| `anonymize` | query | boolean | No | When true, anonymizes PII in the response: identifiers (names, emails, phone numbers, IBANs) are replaced +with deterministic pseudonyms (stable within an org), addresses are generalized to postal code | | `input` | query | string | No | Input to autocomplete | | `attribute` | query | string | Yes | Autocomplete attribute | | `slug` | query | string | No | Limit results to entity schema | @@ -3051,6 +2789,8 @@ Get activity by id | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | +| `anonymize` | query | boolean | No | When true, anonymizes PII in the response: identifiers (names, emails, phone numbers, IBANs) are replaced +with deterministic pseudonyms (stable within an org), addresses are generalized to postal code | | `id` | path | string (ulid) | Yes | Activity Id | | `operations_size` | query | number | No | Maximum number of operations to include in response (default: 10) | @@ -3109,7 +2849,9 @@ epilot entity getActivity -p id=01F130Q52Q6MWSNS8N2AVXV4JN --jsonata '$' "params": {}, "payload": {}, "diff": {}, - "_workflow_origin": {} + "source_context": {}, + "_workflow_origin": {}, + "_automation_chain": ["string"] } ] } @@ -3345,6 +3087,8 @@ Returns all pending changesets for an entity. | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | +| `anonymize` | query | boolean | No | When true, anonymizes PII in the response: identifiers (names, emails, phone numbers, IBANs) are replaced +with deterministic pseudonyms (stable within an org), addresses are generalized to postal code | | `slug` | path | string | Yes | Entity Type | | `id` | path | string (uuid) | Yes | Entity id | @@ -3389,6 +3133,8 @@ Get activity feed for an entity | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | +| `anonymize` | query | boolean | No | When true, anonymizes PII in the response: identifiers (names, emails, phone numbers, IBANs) are replaced +with deterministic pseudonyms (stable within an org), addresses are generalized to postal code | | `slug` | path | string | Yes | Entity Type | | `id` | path | string (uuid) | Yes | Entity id | | `after` | query | string (date-time) | No | Get activities strictly after this timestamp. Cannot be used with 'before', 'start_date', 'end_date', or 'preset_range'. | @@ -3462,6 +3208,8 @@ Returns 1st level direct relations for an entity. | `slug` | path | string | Yes | Entity Type | | `id` | path | string (uuid) | Yes | Entity id | | `hydrate` | query | boolean | No | When true, enables entity hydration to resolve nested $relation & $relation_ref references in-place. | +| `anonymize` | query | boolean | No | When true, anonymizes PII in the response: identifiers (names, emails, phone numbers, IBANs) are replaced +with deterministic pseudonyms (stable within an org), addresses are generalized to postal code | | `include_reverse` | query | boolean | No | When true, includes reverse relations in response (other entities pointing to this entity) | | `from` | query | number | No | Starting page number | | `size` | query | number | No | Number of results to return per page | @@ -3683,6 +3431,8 @@ Returns 1st level direct relations for an entity with pagination. | `slug` | path | string | Yes | Entity Type | | `id` | path | string (uuid) | Yes | Entity id | | `hydrate` | query | boolean | No | When true, enables entity hydration to resolve nested $relation & $relation_ref references in-place. | +| `anonymize` | query | boolean | No | When true, anonymizes PII in the response: identifiers (names, emails, phone numbers, IBANs) are replaced +with deterministic pseudonyms (stable within an org), addresses are generalized to postal code | | `query` | query | string | No | Input to filter search results | | `include_reverse` | query | boolean | No | When true, includes reverse relations in response (other entities pointing to this entity) | | `from` | query | number | No | Starting page number | @@ -3765,6 +3515,8 @@ Returns 1st level direct relations for an entity with pagination. | `slug` | path | string | Yes | Entity Type | | `id` | path | string (uuid) | Yes | Entity id | | `hydrate` | query | boolean | No | When true, enables entity hydration to resolve nested $relation & $relation_ref references in-place. | +| `anonymize` | query | boolean | No | When true, anonymizes PII in the response: identifiers (names, emails, phone numbers, IBANs) are replaced +with deterministic pseudonyms (stable within an org), addresses are generalized to postal code | | `include_reverse` | query | boolean | No | When true, includes reverse relations in response (other entities pointing to this entity) *It gets overriden by mode query parameter.* | @@ -4032,6 +3784,7 @@ epilot entity exportEntities \ "from": 0, "size": 10, "hydrate": false, + "anonymize": false, "fields": ["_id", "_title", "first_name", "account", "!account.*._files", "**._product"], "aggs": { "contact-count-per-tag": { @@ -4097,6 +3850,35 @@ epilot entity importEntities --jsonata '$' --- +### `abortEntityImport` + +Asks a running import to stop. Rows already imported are **kept** - this is a stop, not a + +`POST /v1/entity:abortImport` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `job_id` | query | string | Yes | The ID of the import job to stop. | +| `schema` | query | string | Yes | Slug of the schema being imported, used to authorize the caller. | + +**Sample Call** + +```bash +epilot entity abortEntityImport \ + -p job_id=abc123 \ + -p schema=example +``` + +With JSONata filter: + +```bash +epilot entity abortEntityImport -p job_id=abc123 -p schema=example --jsonata '$' +``` + +--- + ### `listSavedViews` Get the Saved Views based on the schema @@ -4111,6 +3893,7 @@ Get the Saved Views based on the schema | `sort` | query | string | No | The sort key to use if present | | `from` | query | number | No | | | `size` | query | number | No | Number of saved views to return | +| `q` | query | string | No | Free-text search on the saved view name (case-insensitive) | | `fields` | query | string[] | No | | **Sample Call** @@ -4142,7 +3925,8 @@ epilot entity listSavedViews --jsonata 'results[0]' "shared": true, "isFavoritedBy": ["11701"], "created_by": { - "user_id": "10598" + "user_id": "10598", + "org_id": "739224" }, "ui_config": { "filters": { @@ -4193,7 +3977,8 @@ epilot entity createSavedView \ "shared": true, "isFavoritedBy": ["11701"], "created_by": { - "user_id": "10598" + "user_id": "10598", + "org_id": "739224" }, "ui_config": { "filters": { @@ -4239,7 +4024,8 @@ epilot entity createSavedView --jsonata '$' "shared": true, "isFavoritedBy": ["11701"], "created_by": { - "user_id": "10598" + "user_id": "10598", + "org_id": "739224" }, "ui_config": { "filters": { @@ -4308,7 +4094,8 @@ epilot entity getSavedView -p id=123e4567-e89b-12d3-a456-426614174000 --jsonata "shared": true, "isFavoritedBy": ["11701"], "created_by": { - "user_id": "10598" + "user_id": "10598", + "org_id": "739224" }, "ui_config": { "filters": { @@ -4368,7 +4155,8 @@ epilot entity updateSavedView \ "shared": true, "isFavoritedBy": ["11701"], "created_by": { - "user_id": "10598" + "user_id": "10598", + "org_id": "739224" }, "ui_config": { "filters": { @@ -4420,7 +4208,8 @@ epilot entity updateSavedView -p id=123e4567-e89b-12d3-a456-426614174000 --jsona "shared": true, "isFavoritedBy": ["11701"], "created_by": { - "user_id": "10598" + "user_id": "10598", + "org_id": "739224" }, "ui_config": { "filters": { @@ -4477,7 +4266,8 @@ epilot entity patchSavedView \ "shared": true, "isFavoritedBy": ["11701"], "created_by": { - "user_id": "10598" + "user_id": "10598", + "org_id": "739224" }, "ui_config": { "filters": { @@ -4529,7 +4319,8 @@ epilot entity patchSavedView -p id=123e4567-e89b-12d3-a456-426614174000 --jsonat "shared": true, "isFavoritedBy": ["11701"], "created_by": { - "user_id": "10598" + "user_id": "10598", + "org_id": "739224" }, "ui_config": { "filters": { @@ -4620,7 +4411,8 @@ epilot entity listFavoriteViewsForUser --jsonata 'results[0]' "shared": true, "isFavoritedBy": ["11701"], "created_by": { - "user_id": "10598" + "user_id": "10598", + "org_id": "739224" }, "ui_config": { "filters": { @@ -5837,6 +5629,7 @@ epilot entity createSchemaAttribute \ "required": false, "readonly": false, "deprecated": false, + "overridable_attribute": false, "default_value": {}, "group": "string", "order": 0, @@ -5844,6 +5637,7 @@ epilot entity createSchemaAttribute \ "hide_label": true, "icon": "string", "render_condition": "string", + "data_classification": "public", "_purpose": ["taxonomy-slug:classification-slug"], "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], "constraints": { @@ -5921,6 +5715,7 @@ epilot entity createSchemaAttribute --jsonata '$' "required": false, "readonly": false, "deprecated": false, + "overridable_attribute": false, "default_value": {}, "group": "string", "order": 0, @@ -5928,6 +5723,7 @@ epilot entity createSchemaAttribute --jsonata '$' "hide_label": true, "icon": "string", "render_condition": "string", + "data_classification": "public", "_purpose": ["taxonomy-slug:classification-slug"], "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], "constraints": { @@ -6028,6 +5824,7 @@ epilot entity getSchemaAttribute -p composite_id=contact:97644baa-083f-4e49-9188 "required": false, "readonly": false, "deprecated": false, + "overridable_attribute": false, "default_value": {}, "group": "string", "order": 0, @@ -6035,6 +5832,7 @@ epilot entity getSchemaAttribute -p composite_id=contact:97644baa-083f-4e49-9188 "hide_label": true, "icon": "string", "render_condition": "string", + "data_classification": "public", "_purpose": ["taxonomy-slug:classification-slug"], "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], "constraints": { @@ -6126,6 +5924,7 @@ epilot entity putSchemaAttribute \ "required": false, "readonly": false, "deprecated": false, + "overridable_attribute": false, "default_value": {}, "group": "string", "order": 0, @@ -6133,6 +5932,7 @@ epilot entity putSchemaAttribute \ "hide_label": true, "icon": "string", "render_condition": "string", + "data_classification": "public", "_purpose": ["taxonomy-slug:classification-slug"], "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], "constraints": { @@ -6216,6 +6016,7 @@ epilot entity putSchemaAttribute -p composite_id=contact:97644baa-083f-4e49-9188 "required": false, "readonly": false, "deprecated": false, + "overridable_attribute": false, "default_value": {}, "group": "string", "order": 0, @@ -6223,6 +6024,7 @@ epilot entity putSchemaAttribute -p composite_id=contact:97644baa-083f-4e49-9188 "hide_label": true, "icon": "string", "render_condition": "string", + "data_classification": "public", "_purpose": ["taxonomy-slug:classification-slug"], "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], "constraints": { @@ -6323,6 +6125,7 @@ epilot entity deleteSchemaAttribute -p composite_id=contact:97644baa-083f-4e49-9 "required": false, "readonly": false, "deprecated": false, + "overridable_attribute": false, "default_value": {}, "group": "string", "order": 0, @@ -6330,6 +6133,7 @@ epilot entity deleteSchemaAttribute -p composite_id=contact:97644baa-083f-4e49-9 "hide_label": true, "icon": "string", "render_condition": "string", + "data_classification": "public", "_purpose": ["taxonomy-slug:classification-slug"], "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], "constraints": { @@ -6418,6 +6222,7 @@ epilot entity createSchemaCapability \ "required": false, "readonly": false, "deprecated": false, + "overridable_attribute": false, "default_value": {}, "group": "string", "order": 0, @@ -6425,6 +6230,7 @@ epilot entity createSchemaCapability \ "hide_label": true, "icon": "string", "render_condition": "string", + "data_classification": "public", "_purpose": ["taxonomy-slug:classification-slug"], "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], "constraints": {}, @@ -6457,6 +6263,7 @@ epilot entity createSchemaCapability \ "required": false, "readonly": false, "deprecated": false, + "overridable_attribute": false, "default_value": {}, "group": "string", "order": 0, @@ -6464,6 +6271,7 @@ epilot entity createSchemaCapability \ "hide_label": true, "icon": "string", "render_condition": "string", + "data_classification": "public", "_purpose": ["taxonomy-slug:classification-slug"], "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], "constraints": {}, @@ -6483,9 +6291,13 @@ epilot entity createSchemaCapability \ "type": "link" } ], + "overridable_attribute": false, "_purpose": ["taxonomy-slug:classification-slug"], "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], "app_id": "123e4567-e89b-12d3-a456-426614174000", + "adjust_installment": { + "journey_id": "adjust-installment-flow" + }, "ui_config": { "is_filterable": true }, @@ -6502,7 +6314,10 @@ epilot entity createSchemaCapability \ "icon": "email", "disabled": true, "header": true, - "requiredPermission": {} + "requiredPermission": {}, + "default_visible": false, + "default_width": "half_width", + "pricing_tier_setting": "entity_address_maps" } ], "feature_flag": "FF_MY_FEATURE_FLAG", @@ -6554,6 +6369,7 @@ epilot entity createSchemaCapability --jsonata '$' "required": false, "readonly": false, "deprecated": false, + "overridable_attribute": false, "default_value": {}, "group": "string", "order": 0, @@ -6561,6 +6377,7 @@ epilot entity createSchemaCapability --jsonata '$' "hide_label": true, "icon": "string", "render_condition": "string", + "data_classification": "public", "_purpose": ["taxonomy-slug:classification-slug"], "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], "constraints": {}, @@ -6593,6 +6410,7 @@ epilot entity createSchemaCapability --jsonata '$' "required": false, "readonly": false, "deprecated": false, + "overridable_attribute": false, "default_value": {}, "group": "string", "order": 0, @@ -6600,6 +6418,7 @@ epilot entity createSchemaCapability --jsonata '$' "hide_label": true, "icon": "string", "render_condition": "string", + "data_classification": "public", "_purpose": ["taxonomy-slug:classification-slug"], "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], "constraints": {}, @@ -6619,9 +6438,13 @@ epilot entity createSchemaCapability --jsonata '$' "type": "link" } ], + "overridable_attribute": false, "_purpose": ["taxonomy-slug:classification-slug"], "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], "app_id": "123e4567-e89b-12d3-a456-426614174000", + "adjust_installment": { + "journey_id": "adjust-installment-flow" + }, "ui_config": { "is_filterable": true }, @@ -6638,7 +6461,10 @@ epilot entity createSchemaCapability --jsonata '$' "icon": "email", "disabled": true, "header": true, - "requiredPermission": {} + "requiredPermission": {}, + "default_visible": false, + "default_width": "half_width", + "pricing_tier_setting": "entity_address_maps" } ], "feature_flag": "FF_MY_FEATURE_FLAG", @@ -6713,6 +6539,7 @@ epilot entity getSchemaCapability -p composite_id=contact:97644baa-083f-4e49-918 "required": false, "readonly": false, "deprecated": false, + "overridable_attribute": false, "default_value": {}, "group": "string", "order": 0, @@ -6720,6 +6547,7 @@ epilot entity getSchemaCapability -p composite_id=contact:97644baa-083f-4e49-918 "hide_label": true, "icon": "string", "render_condition": "string", + "data_classification": "public", "_purpose": ["taxonomy-slug:classification-slug"], "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], "constraints": {}, @@ -6752,6 +6580,7 @@ epilot entity getSchemaCapability -p composite_id=contact:97644baa-083f-4e49-918 "required": false, "readonly": false, "deprecated": false, + "overridable_attribute": false, "default_value": {}, "group": "string", "order": 0, @@ -6759,6 +6588,7 @@ epilot entity getSchemaCapability -p composite_id=contact:97644baa-083f-4e49-918 "hide_label": true, "icon": "string", "render_condition": "string", + "data_classification": "public", "_purpose": ["taxonomy-slug:classification-slug"], "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], "constraints": {}, @@ -6778,9 +6608,13 @@ epilot entity getSchemaCapability -p composite_id=contact:97644baa-083f-4e49-918 "type": "link" } ], + "overridable_attribute": false, "_purpose": ["taxonomy-slug:classification-slug"], "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], "app_id": "123e4567-e89b-12d3-a456-426614174000", + "adjust_installment": { + "journey_id": "adjust-installment-flow" + }, "ui_config": { "is_filterable": true }, @@ -6797,7 +6631,10 @@ epilot entity getSchemaCapability -p composite_id=contact:97644baa-083f-4e49-918 "icon": "email", "disabled": true, "header": true, - "requiredPermission": {} + "requiredPermission": {}, + "default_visible": false, + "default_width": "half_width", + "pricing_tier_setting": "entity_address_maps" } ], "feature_flag": "FF_MY_FEATURE_FLAG", @@ -6863,6 +6700,7 @@ epilot entity putSchemaCapability \ "required": false, "readonly": false, "deprecated": false, + "overridable_attribute": false, "default_value": {}, "group": "string", "order": 0, @@ -6870,6 +6708,7 @@ epilot entity putSchemaCapability \ "hide_label": true, "icon": "string", "render_condition": "string", + "data_classification": "public", "_purpose": ["taxonomy-slug:classification-slug"], "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], "constraints": {}, @@ -6902,6 +6741,7 @@ epilot entity putSchemaCapability \ "required": false, "readonly": false, "deprecated": false, + "overridable_attribute": false, "default_value": {}, "group": "string", "order": 0, @@ -6909,6 +6749,7 @@ epilot entity putSchemaCapability \ "hide_label": true, "icon": "string", "render_condition": "string", + "data_classification": "public", "_purpose": ["taxonomy-slug:classification-slug"], "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], "constraints": {}, @@ -6928,9 +6769,13 @@ epilot entity putSchemaCapability \ "type": "link" } ], + "overridable_attribute": false, "_purpose": ["taxonomy-slug:classification-slug"], "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], "app_id": "123e4567-e89b-12d3-a456-426614174000", + "adjust_installment": { + "journey_id": "adjust-installment-flow" + }, "ui_config": { "is_filterable": true }, @@ -6947,7 +6792,10 @@ epilot entity putSchemaCapability \ "icon": "email", "disabled": true, "header": true, - "requiredPermission": {} + "requiredPermission": {}, + "default_visible": false, + "default_width": "half_width", + "pricing_tier_setting": "entity_address_maps" } ], "feature_flag": "FF_MY_FEATURE_FLAG", @@ -7005,6 +6853,7 @@ epilot entity putSchemaCapability -p composite_id=contact:97644baa-083f-4e49-918 "required": false, "readonly": false, "deprecated": false, + "overridable_attribute": false, "default_value": {}, "group": "string", "order": 0, @@ -7012,6 +6861,7 @@ epilot entity putSchemaCapability -p composite_id=contact:97644baa-083f-4e49-918 "hide_label": true, "icon": "string", "render_condition": "string", + "data_classification": "public", "_purpose": ["taxonomy-slug:classification-slug"], "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], "constraints": {}, @@ -7044,6 +6894,7 @@ epilot entity putSchemaCapability -p composite_id=contact:97644baa-083f-4e49-918 "required": false, "readonly": false, "deprecated": false, + "overridable_attribute": false, "default_value": {}, "group": "string", "order": 0, @@ -7051,6 +6902,7 @@ epilot entity putSchemaCapability -p composite_id=contact:97644baa-083f-4e49-918 "hide_label": true, "icon": "string", "render_condition": "string", + "data_classification": "public", "_purpose": ["taxonomy-slug:classification-slug"], "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], "constraints": {}, @@ -7070,9 +6922,13 @@ epilot entity putSchemaCapability -p composite_id=contact:97644baa-083f-4e49-918 "type": "link" } ], + "overridable_attribute": false, "_purpose": ["taxonomy-slug:classification-slug"], "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], "app_id": "123e4567-e89b-12d3-a456-426614174000", + "adjust_installment": { + "journey_id": "adjust-installment-flow" + }, "ui_config": { "is_filterable": true }, @@ -7089,7 +6945,10 @@ epilot entity putSchemaCapability -p composite_id=contact:97644baa-083f-4e49-918 "icon": "email", "disabled": true, "header": true, - "requiredPermission": {} + "requiredPermission": {}, + "default_visible": false, + "default_width": "half_width", + "pricing_tier_setting": "entity_address_maps" } ], "feature_flag": "FF_MY_FEATURE_FLAG", @@ -7164,6 +7023,7 @@ epilot entity deleteSchemaCapability -p composite_id=contact:97644baa-083f-4e49- "required": false, "readonly": false, "deprecated": false, + "overridable_attribute": false, "default_value": {}, "group": "string", "order": 0, @@ -7171,6 +7031,7 @@ epilot entity deleteSchemaCapability -p composite_id=contact:97644baa-083f-4e49- "hide_label": true, "icon": "string", "render_condition": "string", + "data_classification": "public", "_purpose": ["taxonomy-slug:classification-slug"], "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], "constraints": {}, @@ -7203,6 +7064,7 @@ epilot entity deleteSchemaCapability -p composite_id=contact:97644baa-083f-4e49- "required": false, "readonly": false, "deprecated": false, + "overridable_attribute": false, "default_value": {}, "group": "string", "order": 0, @@ -7210,6 +7072,7 @@ epilot entity deleteSchemaCapability -p composite_id=contact:97644baa-083f-4e49- "hide_label": true, "icon": "string", "render_condition": "string", + "data_classification": "public", "_purpose": ["taxonomy-slug:classification-slug"], "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], "constraints": {}, @@ -7229,9 +7092,13 @@ epilot entity deleteSchemaCapability -p composite_id=contact:97644baa-083f-4e49- "type": "link" } ], + "overridable_attribute": false, "_purpose": ["taxonomy-slug:classification-slug"], "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], "app_id": "123e4567-e89b-12d3-a456-426614174000", + "adjust_installment": { + "journey_id": "adjust-installment-flow" + }, "ui_config": { "is_filterable": true }, @@ -7248,7 +7115,10 @@ epilot entity deleteSchemaCapability -p composite_id=contact:97644baa-083f-4e49- "icon": "email", "disabled": true, "header": true, - "requiredPermission": {} + "requiredPermission": {}, + "default_visible": false, + "default_width": "half_width", + "pricing_tier_setting": "entity_address_maps" } ], "feature_flag": "FF_MY_FEATURE_FLAG", diff --git a/packages/cli/docs/environments.md b/packages/cli/docs/environments.md index f9eb1ba5b..19cc16990 100644 --- a/packages/cli/docs/environments.md +++ b/packages/cli/docs/environments.md @@ -37,12 +37,12 @@ epilot environments listEnvironmentVariables **environments** - [`listEnvironmentVariables`](#listenvironmentvariables) — List all environment variables for the organization. Returns metadata only, no secret values. -- [`createEnvironmentVariable`](#createenvironmentvariable) — Create a new environment variable or secret for the organization. +- [`createEnvironmentVariable`](#createenvironmentvariable) — Create a new environment variable or secret for the organization. If `group` is provided and the group does not yet exis - [`listEnvironmentGroups`](#listenvironmentgroups) — List all environment groups for the organization. -- [`putEnvironmentGroup`](#putenvironmentgroup) — PUT /v1/environments/groups/{name} +- [`putEnvironmentGroup`](#putenvironmentgroup) — Create or update an environment group by name. Acts as an upsert — creates the group if it does not exist. - [`deleteEnvironmentGroup`](#deleteenvironmentgroup) — Deletes a group. Variables assigned to this group become ungrouped. -- [`getEnvironmentVariable`](#getenvironmentvariable) — Get an environment variable by key. Returns value only for String type, omitted for SecretString. -- [`updateEnvironmentVariable`](#updateenvironmentvariable) — Create or update an environment variable. Acts as an upsert — creates the variable if it does not exist. +- [`getEnvironmentVariable`](#getenvironmentvariable) — Get an environment variable by key. Returns value for non-secret types, omitted for SecretString. +- [`updateEnvironmentVariable`](#updateenvironmentvariable) — Create or update an environment variable. Acts as an upsert — creates the variable if it does not exist. If `group` is p - [`deleteEnvironmentVariable`](#deleteenvironmentvariable) — Delete an environment variable by key. ### `listEnvironmentVariables` @@ -72,9 +72,11 @@ epilot environments listEnvironmentVariables --jsonata 'items[0]' { "key": "string", "type": "String", + "item_type": "String", "description": "string", "group": "string", "value": "string", + "protected": true, "created_at": "1970-01-01T00:00:00.000Z", "updated_at": "1970-01-01T00:00:00.000Z" } @@ -88,7 +90,7 @@ epilot environments listEnvironmentVariables --jsonata 'items[0]' ### `createEnvironmentVariable` -Create a new environment variable or secret for the organization. +Create a new environment variable or secret for the organization. If `group` is provided and the group does not yet exis `POST /v1/environments` @@ -98,7 +100,7 @@ Create a new environment variable or secret for the organization. ```bash epilot environments createEnvironmentVariable \ - -d '{"key":"string","type":"String","description":"string","group":"string","value":"string"}' + -d '{"key":"string","type":"String","description":"string","group":"string","value":"string","protected":true}' ``` Using stdin pipe: @@ -120,9 +122,11 @@ epilot environments createEnvironmentVariable --jsonata 'key' { "key": "string", "type": "String", + "item_type": "String", "description": "string", "group": "string", "value": "string", + "protected": true, "created_at": "1970-01-01T00:00:00.000Z", "updated_at": "1970-01-01T00:00:00.000Z" } @@ -172,6 +176,8 @@ epilot environments listEnvironmentGroups --jsonata 'items[0]' ### `putEnvironmentGroup` +Create or update an environment group by name. Acts as an upsert — creates the group if it does not exist. + `PUT /v1/environments/groups/{name}` **Parameters** @@ -259,7 +265,7 @@ epilot environments deleteEnvironmentGroup -p name=example --jsonata '$' ### `getEnvironmentVariable` -Get an environment variable by key. Returns value only for String type, omitted for SecretString. +Get an environment variable by key. Returns value for non-secret types, omitted for SecretString. `GET /v1/environments/{key}` @@ -295,9 +301,11 @@ epilot environments getEnvironmentVariable -p key=example --jsonata 'key' { "key": "string", "type": "String", + "item_type": "String", "description": "string", "group": "string", "value": "string", + "protected": true, "created_at": "1970-01-01T00:00:00.000Z", "updated_at": "1970-01-01T00:00:00.000Z" } @@ -309,7 +317,7 @@ epilot environments getEnvironmentVariable -p key=example --jsonata 'key' ### `updateEnvironmentVariable` -Create or update an environment variable. Acts as an upsert — creates the variable if it does not exist. +Create or update an environment variable. Acts as an upsert — creates the variable if it does not exist. If `group` is p `PUT /v1/environments/{key}` @@ -326,7 +334,7 @@ Create or update an environment variable. Acts as an upsert — creates the vari ```bash epilot environments updateEnvironmentVariable \ -p key=example \ - -d '{"type":"String","value":"string","description":"string","group":"string"}' + -d '{"type":"String","value":"string","description":"string","group":"string","protected":true}' ``` Using positional args for path parameters: @@ -354,9 +362,11 @@ epilot environments updateEnvironmentVariable -p key=example --jsonata 'key' { "key": "string", "type": "String", + "item_type": "String", "description": "string", "group": "string", "value": "string", + "protected": true, "created_at": "1970-01-01T00:00:00.000Z", "updated_at": "1970-01-01T00:00:00.000Z" } diff --git a/packages/cli/docs/event-catalog.md b/packages/cli/docs/event-catalog.md index 8b940ae7f..126a1a4d5 100644 --- a/packages/cli/docs/event-catalog.md +++ b/packages/cli/docs/event-catalog.md @@ -37,8 +37,13 @@ epilot event-catalog listEvents **Event Catalog** - [`listEvents`](#listevents) — Retrieve list of available business events +- [`createCustomEvent`](#createcustomevent) — Reserve an org-scoped custom event name and persist its immutable v1.0 draft definition. Custom events are always projec - [`getEvent`](#getevent) — Retrieve the configuration of a specific business event +- [`replaceCustomEventDraft`](#replacecustomeventdraft) — Replace the complete v1.0 definition of an org-scoped custom event while it is still an - [`patchEvent`](#patchevent) — Update the configuration of a specific business event for the organization +- [`deprecateCustomEvent`](#deprecatecustomevent) — Soft-deprecate an org-scoped custom event. Definitions and v1.0 history remain readable. +- [`previewCustomEvent`](#previewcustomevent) — Assemble and fully validate a persisted custom-event draft without publishing it. +- [`publishCustomEventDefinition`](#publishcustomeventdefinition) — Conditionally activate an immutable custom-event v1.0 definition. - [`getEventJSONSchema`](#geteventjsonschema) — Retrieve the JSON Schema of a specific business event. Pass an optional - [`getEventExample`](#geteventexample) — Generate a sample event payload based on the event's JSON Schema. Pass an - [`listEventVersions`](#listeventversions) — List every known version of an event, along with the `latest` @@ -99,11 +104,191 @@ epilot event-catalog listEvents --jsonata 'results[0]' "operation": ["createEntity", "updateEntity"], "schema": ["contact", "contract", "order"], "attribute": ["email", "phone", "status"], - "purpose": ["Kündigung", "Umzug/Auszug"] + "purpose": ["Kündigung", "Umzug/Auszug"], + "purpose_filters": [ + { + "id": "string", + "display_name": "string" + } + ] }, "enabled": true, "auto_trigger": true, - "automation_trigger": true + "automation_trigger": true, + "api_trigger": true, + "automation_trigger_only": true, + "automation_trigger_seed_node": "ticket", + "event_origin": "builtin", + "mapping": { + "mode": "guided", + "jsonata": "string" + }, + "lineage": { + "base_event_name": "string", + "base_event_version": "string" + }, + "success_criteria": [ + { + "entity_schema": "contract", + "attribute": "installment_amount" + }, + { + "entity_schema": "billing_account", + "attribute": "due_date" + } + ] + } + ] +} +``` + +
+ +--- + +### `createCustomEvent` + +Reserve an org-scoped custom event name and persist its immutable v1.0 draft definition. Custom events are always projec + +`POST /v1/events` + +**Request Body** (required) + +**Sample Call** + +```bash +epilot event-catalog createCustomEvent +``` + +With request body: + +```bash +epilot event-catalog createCustomEvent \ + -d '{ + "event_name": "string", + "event_title": "string", + "event_description": "string", + "event_tags": ["string"], + "schema_fields": {}, + "entity_graph": { + "nodes": [ + { + "id": "contact", + "schema": "contact", + "cardinality": "one", + "fields": ["_id", "_title", "first_name", "account", "!account.*._files", "**._product"] + } + ], + "edges": [ + { + "from": "contact", + "to": "billing_account" + } + ] + }, + "entity_operation": { + "operation": ["createEntity", "updateEntity"], + "schema": ["contact", "contract", "order"], + "attribute": ["email", "phone", "status"], + "purpose": ["Kündigung", "Umzug/Auszug"], + "purpose_filters": [ + { + "id": "string", + "display_name": "string" + } + ] + }, + "automation_trigger": true, + "api_trigger": true, + "automation_trigger_only": false, + "automation_trigger_seed_node": "string", + "mapping": { + "mode": "guided", + "jsonata": "string" + }, + "lineage": { + "base_event_name": "string", + "base_event_version": "string" + }, + "example": {} +}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot event-catalog createCustomEvent +``` + +With JSONata filter: + +```bash +epilot event-catalog createCustomEvent --jsonata '$' +``` + +
+Sample Response + +```json +{ + "event_name": "AddMeterReading", + "event_title": "Add Meter Reading", + "event_description": "Triggered when a new meter reading is added", + "event_version": "1.0", + "event_status": "active", + "event_tags": ["builtin", "metering", "erp"], + "schema_fields": {}, + "entity_graph": { + "nodes": [ + { + "id": "contact", + "schema": "contact", + "cardinality": "one", + "fields": ["_id", "_title", "first_name", "account", "!account.*._files", "**._product"] + } + ], + "edges": [ + { + "from": "contact", + "to": "billing_account" + } + ] + }, + "entity_operation": { + "operation": ["createEntity", "updateEntity"], + "schema": ["contact", "contract", "order"], + "attribute": ["email", "phone", "status"], + "purpose": ["Kündigung", "Umzug/Auszug"], + "purpose_filters": [ + { + "id": "string", + "display_name": "string" + } + ] + }, + "enabled": true, + "auto_trigger": true, + "automation_trigger": true, + "api_trigger": true, + "automation_trigger_only": true, + "automation_trigger_seed_node": "ticket", + "event_origin": "builtin", + "mapping": { + "mode": "guided", + "jsonata": "string" + }, + "lineage": { + "base_event_name": "string", + "base_event_version": "string" + }, + "success_criteria": [ + { + "entity_schema": "contract", + "attribute": "installment_amount" + }, + { + "entity_schema": "billing_account", + "attribute": "due_date" } ] } @@ -176,11 +361,39 @@ epilot event-catalog getEvent -p event_name=example --jsonata '$' "operation": ["createEntity", "updateEntity"], "schema": ["contact", "contract", "order"], "attribute": ["email", "phone", "status"], - "purpose": ["Kündigung", "Umzug/Auszug"] + "purpose": ["Kündigung", "Umzug/Auszug"], + "purpose_filters": [ + { + "id": "string", + "display_name": "string" + } + ] }, "enabled": true, "auto_trigger": true, - "automation_trigger": true + "automation_trigger": true, + "api_trigger": true, + "automation_trigger_only": true, + "automation_trigger_seed_node": "ticket", + "event_origin": "builtin", + "mapping": { + "mode": "guided", + "jsonata": "string" + }, + "lineage": { + "base_event_name": "string", + "base_event_version": "string" + }, + "success_criteria": [ + { + "entity_schema": "contract", + "attribute": "installment_amount" + }, + { + "entity_schema": "billing_account", + "attribute": "due_date" + } + ] } ``` @@ -188,33 +401,105 @@ epilot event-catalog getEvent -p event_name=example --jsonata '$' --- -### `patchEvent` +### `replaceCustomEventDraft` -Update the configuration of a specific business event for the organization +Replace the complete v1.0 definition of an org-scoped custom event while it is still an -`PATCH /v1/events/{event_name}` +`PUT /v1/events/{event_name}` **Parameters** | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `event_name` | path | string | Yes | Unique human readable name of the event | +| `event_name` | path | string | Yes | | **Request Body** (required) **Sample Call** ```bash -epilot event-catalog patchEvent \ +epilot event-catalog replaceCustomEventDraft \ -p event_name=example ``` With request body: ```bash -epilot event-catalog patchEvent \ +epilot event-catalog replaceCustomEventDraft \ -p event_name=example \ -d '{ + "event_name": "string", + "event_title": "string", + "event_description": "string", + "event_tags": ["string"], + "schema_fields": {}, + "entity_graph": { + "nodes": [ + { + "id": "contact", + "schema": "contact", + "cardinality": "one", + "fields": ["_id", "_title", "first_name", "account", "!account.*._files", "**._product"] + } + ], + "edges": [ + { + "from": "contact", + "to": "billing_account" + } + ] + }, + "entity_operation": { + "operation": ["createEntity", "updateEntity"], + "schema": ["contact", "contract", "order"], + "attribute": ["email", "phone", "status"], + "purpose": ["Kündigung", "Umzug/Auszug"], + "purpose_filters": [ + { + "id": "string", + "display_name": "string" + } + ] + }, + "automation_trigger": true, + "api_trigger": true, + "automation_trigger_only": false, + "automation_trigger_seed_node": "string", + "mapping": { + "mode": "guided", + "jsonata": "string" + }, + "lineage": { + "base_event_name": "string", + "base_event_version": "string" + }, + "example": {} +}' +``` + +Using positional args for path parameters: + +```bash +epilot event-catalog replaceCustomEventDraft example +``` + +Using stdin pipe: + +```bash +cat body.json | epilot event-catalog replaceCustomEventDraft -p event_name=example +``` + +With JSONata filter: + +```bash +epilot event-catalog replaceCustomEventDraft -p event_name=example --jsonata '$' +``` + +
+Sample Response + +```json +{ "event_name": "AddMeterReading", "event_title": "Add Meter Reading", "event_description": "Triggered when a new meter reading is added", @@ -242,12 +527,66 @@ epilot event-catalog patchEvent \ "operation": ["createEntity", "updateEntity"], "schema": ["contact", "contract", "order"], "attribute": ["email", "phone", "status"], - "purpose": ["Kündigung", "Umzug/Auszug"] + "purpose": ["Kündigung", "Umzug/Auszug"], + "purpose_filters": [ + { + "id": "string", + "display_name": "string" + } + ] }, "enabled": true, "auto_trigger": true, - "automation_trigger": true -}' + "automation_trigger": true, + "api_trigger": true, + "automation_trigger_only": true, + "automation_trigger_seed_node": "ticket", + "event_origin": "builtin", + "mapping": { + "mode": "guided", + "jsonata": "string" + }, + "lineage": { + "base_event_name": "string", + "base_event_version": "string" + }, + "success_criteria": [ + { + "entity_schema": "contract", + "attribute": "installment_amount" + }, + { + "entity_schema": "billing_account", + "attribute": "due_date" + } + ] +} +``` + +
+ +--- + +### `patchEvent` + +Update the configuration of a specific business event for the organization + +`PATCH /v1/events/{event_name}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `event_name` | path | string | Yes | Unique human readable name of the event | + +**Request Body** (required) + +**Sample Call** + +```bash +epilot event-catalog patchEvent \ + -p event_name=example \ + -d '{"enabled":true,"auto_trigger":true,"success_criteria":[{"entity_schema":"contract","attribute":"installment_amount"}]}' ``` Using positional args for path parameters: @@ -300,11 +639,257 @@ epilot event-catalog patchEvent -p event_name=example --jsonata '$' "operation": ["createEntity", "updateEntity"], "schema": ["contact", "contract", "order"], "attribute": ["email", "phone", "status"], - "purpose": ["Kündigung", "Umzug/Auszug"] + "purpose": ["Kündigung", "Umzug/Auszug"], + "purpose_filters": [ + { + "id": "string", + "display_name": "string" + } + ] }, "enabled": true, "auto_trigger": true, - "automation_trigger": true + "automation_trigger": true, + "api_trigger": true, + "automation_trigger_only": true, + "automation_trigger_seed_node": "ticket", + "event_origin": "builtin", + "mapping": { + "mode": "guided", + "jsonata": "string" + }, + "lineage": { + "base_event_name": "string", + "base_event_version": "string" + }, + "success_criteria": [ + { + "entity_schema": "contract", + "attribute": "installment_amount" + }, + { + "entity_schema": "billing_account", + "attribute": "due_date" + } + ] +} +``` + +
+ +--- + +### `deprecateCustomEvent` + +Soft-deprecate an org-scoped custom event. Definitions and v1.0 history remain readable. + +`DELETE /v1/events/{event_name}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `event_name` | path | string | Yes | | + +**Sample Call** + +```bash +epilot event-catalog deprecateCustomEvent \ + -p event_name=example +``` + +Using positional args for path parameters: + +```bash +epilot event-catalog deprecateCustomEvent example +``` + +With JSONata filter: + +```bash +epilot event-catalog deprecateCustomEvent -p event_name=example --jsonata '$' +``` + +--- + +### `previewCustomEvent` + +Assemble and fully validate a persisted custom-event draft without publishing it. + +`POST /v1/events/{event_name}:preview` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `event_name` | path | string | Yes | | + +**Request Body** (required) + +**Sample Call** + +```bash +epilot event-catalog previewCustomEvent \ + -p event_name=example +``` + +With request body: + +```bash +epilot event-catalog previewCustomEvent \ + -p event_name=example \ + -d '{ + "seed": { + "entity_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "node_id": "ticket" + }, + "_trigger_source_type": "automation", + "_trigger_source": "execution-id/action-id" +}' +``` + +Using positional args for path parameters: + +```bash +epilot event-catalog previewCustomEvent example +``` + +Using stdin pipe: + +```bash +cat body.json | epilot event-catalog previewCustomEvent -p event_name=example +``` + +With JSONata filter: + +```bash +epilot event-catalog previewCustomEvent -p event_name=example --jsonata 'payload' +``` + +
+Sample Response + +```json +{ + "payload": {}, + "errors": [ + { + "path": "string", + "message": "string" + } + ] +} +``` + +
+ +--- + +### `publishCustomEventDefinition` + +Conditionally activate an immutable custom-event v1.0 definition. + +`POST /v1/events/{event_name}:publish` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `event_name` | path | string | Yes | | + +**Request Body** + +**Sample Call** + +```bash +epilot event-catalog publishCustomEventDefinition \ + -p event_name=example \ + -d '{"enabled":true,"auto_trigger":true,"base_auto_trigger_enabled":true}' +``` + +Using positional args for path parameters: + +```bash +epilot event-catalog publishCustomEventDefinition example +``` + +Using stdin pipe: + +```bash +cat body.json | epilot event-catalog publishCustomEventDefinition -p event_name=example +``` + +With JSONata filter: + +```bash +epilot event-catalog publishCustomEventDefinition -p event_name=example --jsonata '$' +``` + +
+Sample Response + +```json +{ + "event_name": "AddMeterReading", + "event_title": "Add Meter Reading", + "event_description": "Triggered when a new meter reading is added", + "event_version": "1.0", + "event_status": "active", + "event_tags": ["builtin", "metering", "erp"], + "schema_fields": {}, + "entity_graph": { + "nodes": [ + { + "id": "contact", + "schema": "contact", + "cardinality": "one", + "fields": ["_id", "_title", "first_name", "account", "!account.*._files", "**._product"] + } + ], + "edges": [ + { + "from": "contact", + "to": "billing_account" + } + ] + }, + "entity_operation": { + "operation": ["createEntity", "updateEntity"], + "schema": ["contact", "contract", "order"], + "attribute": ["email", "phone", "status"], + "purpose": ["Kündigung", "Umzug/Auszug"], + "purpose_filters": [ + { + "id": "string", + "display_name": "string" + } + ] + }, + "enabled": true, + "auto_trigger": true, + "automation_trigger": true, + "api_trigger": true, + "automation_trigger_only": true, + "automation_trigger_seed_node": "ticket", + "event_origin": "builtin", + "mapping": { + "mode": "guided", + "jsonata": "string" + }, + "lineage": { + "base_event_name": "string", + "base_event_version": "string" + }, + "success_criteria": [ + { + "entity_schema": "contract", + "attribute": "installment_amount" + }, + { + "entity_schema": "billing_account", + "attribute": "due_date" + } + ] } ``` @@ -840,12 +1425,10 @@ epilot event-catalog triggerEvent \ -d '{ "seed": { "entity_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "node_id": "string" + "node_id": "ticket" }, - "fields": {}, - "skip_hydration": ["string"], - "_trigger_source_type": "string", - "_trigger_source": "string" + "_trigger_source_type": "automation", + "_trigger_source": "execution-id/action-id" }' ``` diff --git a/packages/cli/docs/file.md b/packages/cli/docs/file.md index 7b28846ac..76b7ac807 100644 --- a/packages/cli/docs/file.md +++ b/packages/cli/docs/file.md @@ -45,7 +45,15 @@ epilot file uploadFileV2 - [`downloadFiles`](#downloadfiles) — Bulk generate pre-signed download URLs for multiple files in a single request. - [`createZipJob`](#createzipjob) — Create a background job to ZIP multiple files and send a download link via email. - [`getZipJob`](#getzipjob) — Get the status of a ZIP job -- [`generateFileSummary`](#generatefilesummary) — Request AI generation of preview and short summaries for a file entity. +- [`getFileSummary`](#getfilesummary) — Get summary text for a file entity together with the current summary job status when available. +- [`getFileSummaryFeedback`](#getfilesummaryfeedback) — Get the authenticated user's feedback for the current generated file summary. +- [`putFileSummaryFeedback`](#putfilesummaryfeedback) — Upsert thumbs up/down feedback for the current generated file summary. +- [`createFileSummaryJob`](#createfilesummaryjob) — Create or return the current AI summary job for a file entity. +- [`getCurrentFileSummaryJob`](#getcurrentfilesummaryjob) — Get the latest AI summary job for the file entity's current source. +- [`getFileSummaryJob`](#getfilesummaryjob) — Get an AI summary job by id. +- [`generateFileSummary`](#generatefilesummary) — Compatibility alias for creating or returning the current AI summary job for a file entity. +- [`getFileText`](#getfiletext) — Get the plain-text representation of a file entity. Returns status `not_ready` while text is being prepared and `unsuppo +- [`validateFile`](#validatefile) — Validate an uploaded file against one or more document validation rules - [`verifyCustomDownloadUrl`](#verifycustomdownloadurl) — Verify that a custom download URL is valid and has not expired. - [`uploadFilePublic`](#uploadfilepublic) — Create a pre-signed S3 URL for uploading a file without authentication. @@ -178,14 +186,16 @@ epilot file saveFileV2 \ "mime_type": "application/pdf", "size_bytes": 1234, "readable_size": "1.2 MB", + "etag": "9bb58f26192e4ba00f01e2e7b136bbd8", "access_control": "private", "public_url": "https://epilot-prod-user-content.s3.eu-central-1.amazonaws.com/123/4d689aeb-1497-4410-a9fe-b36ca9ac4389/document.pdf", "custom_download_url": "https://some-api-url.com/download?file_id=123", + "custom_download_url_auth": "token", "preview_summary_de": "string", "short_summary_de": "string", "preview_summary_en": "string", "short_summary_en": "string", - "summary_status": "processing", + "file_summary_execution_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "s3ref": {} }' ``` @@ -219,14 +229,16 @@ epilot file saveFileV2 --jsonata '$' "mime_type": "application/pdf", "size_bytes": 1234, "readable_size": "1.2 MB", + "etag": "9bb58f26192e4ba00f01e2e7b136bbd8", "access_control": "private", "public_url": "https://epilot-prod-user-content.s3.eu-central-1.amazonaws.com/123/4d689aeb-1497-4410-a9fe-b36ca9ac4389/document.pdf", "custom_download_url": "https://some-api-url.com/download?file_id=123", + "custom_download_url_auth": "token", "preview_summary_de": "string", "short_summary_de": "string", "preview_summary_en": "string", "short_summary_en": "string", - "summary_status": "processing", + "file_summary_execution_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "source_url": "https://productengineer-content.s3.eu-west-1.amazonaws.com/product-engineer-checklist.pdf", "s3ref": {}, "versions": [ @@ -235,7 +247,8 @@ epilot file saveFileV2 --jsonata '$' "filename": "document.pdf", "size_bytes": 1234, "readable_size": "1.2 MB", - "mime_type": "image/jpeg" + "mime_type": "image/jpeg", + "etag": "9bb58f26192e4ba00f01e2e7b136bbd8" } ], "_updated_at": "1970-01-01T00:00:00.000Z", @@ -310,14 +323,16 @@ epilot file getFile -p id=ef7d985c-2385-44f4-9c71-ae06a52264f8 --jsonata '$' "mime_type": "application/pdf", "size_bytes": 1234, "readable_size": "1.2 MB", + "etag": "9bb58f26192e4ba00f01e2e7b136bbd8", "access_control": "private", "public_url": "https://epilot-prod-user-content.s3.eu-central-1.amazonaws.com/123/4d689aeb-1497-4410-a9fe-b36ca9ac4389/document.pdf", "custom_download_url": "https://some-api-url.com/download?file_id=123", + "custom_download_url_auth": "token", "preview_summary_de": "string", "short_summary_de": "string", "preview_summary_en": "string", "short_summary_en": "string", - "summary_status": "processing", + "file_summary_execution_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "source_url": "https://productengineer-content.s3.eu-west-1.amazonaws.com/product-engineer-checklist.pdf", "s3ref": {}, "versions": [ @@ -326,7 +341,8 @@ epilot file getFile -p id=ef7d985c-2385-44f4-9c71-ae06a52264f8 --jsonata '$' "filename": "document.pdf", "size_bytes": 1234, "readable_size": "1.2 MB", - "mime_type": "image/jpeg" + "mime_type": "image/jpeg", + "etag": "9bb58f26192e4ba00f01e2e7b136bbd8" } ], "_updated_at": "1970-01-01T00:00:00.000Z", @@ -401,14 +417,16 @@ epilot file deleteFile -p id=ef7d985c-2385-44f4-9c71-ae06a52264f8 --jsonata '$' "mime_type": "application/pdf", "size_bytes": 1234, "readable_size": "1.2 MB", + "etag": "9bb58f26192e4ba00f01e2e7b136bbd8", "access_control": "private", "public_url": "https://epilot-prod-user-content.s3.eu-central-1.amazonaws.com/123/4d689aeb-1497-4410-a9fe-b36ca9ac4389/document.pdf", "custom_download_url": "https://some-api-url.com/download?file_id=123", + "custom_download_url_auth": "token", "preview_summary_de": "string", "short_summary_de": "string", "preview_summary_en": "string", "short_summary_en": "string", - "summary_status": "processing", + "file_summary_execution_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "source_url": "https://productengineer-content.s3.eu-west-1.amazonaws.com/product-engineer-checklist.pdf", "s3ref": {}, "versions": [ @@ -417,7 +435,8 @@ epilot file deleteFile -p id=ef7d985c-2385-44f4-9c71-ae06a52264f8 --jsonata '$' "filename": "document.pdf", "size_bytes": 1234, "readable_size": "1.2 MB", - "mime_type": "image/jpeg" + "mime_type": "image/jpeg", + "etag": "9bb58f26192e4ba00f01e2e7b136bbd8" } ], "_updated_at": "1970-01-01T00:00:00.000Z", @@ -454,6 +473,9 @@ Generate a pre-signed download URL for a file. | `id` | path | string \| string (uuid) | Yes | The UUID of the file entity | | `version` | query | number | No | Index of the file version to download (0 = latest) | | `attachment` | query | boolean | No | Controls the Content-Disposition header. Set to `true` to trigger browser download dialog, `false` to display inline. | +| `x-track` | query | object | No | Download-attribution channel. Provide any number of query parameters whose names +start with `x-track-` (e.g. `x-track-portal-id`, `x-track-contact-id`); each is signed +into the pre-signed URL verbatim | **Sample Call** @@ -497,7 +519,10 @@ Generate a pre-signed download URL for a file using its S3 reference. | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `s3_key` | query | string | Yes | The S3 object key | +| `s3_key` | query | string | Yes | The S3 object key, as returned in `s3ref.key` of the file entity. + +Note: object keys store the filename segment percent-encoded. For example, a file named +`Straße 1.pdf` is stored under the key `.../S | | `s3_bucket` | query | string | Yes | The S3 bucket name | | `attachment` | query | boolean | No | Controls the Content-Disposition header. Set to `true` to trigger browser download dialog, `false` to display inline. | @@ -505,14 +530,14 @@ Generate a pre-signed download URL for a file using its S3 reference. ```bash epilot file downloadS3File \ - -p s3_key=123/4d689aeb-1497-4410-a9fe-b36ca9ac4389/document.pdf \ + -p s3_key=123/4d689aeb-1497-4410-a9fe-b36ca9ac4389/Stra%C3%9Fe%201.pdf \ -p s3_bucket=epilot-prod-user-content ``` With JSONata filter: ```bash -epilot file downloadS3File -p s3_key=123/4d689aeb-1497-4410-a9fe-b36ca9ac4389/document.pdf -p s3_bucket=epilot-prod-user-content --jsonata 'download_url' +epilot file downloadS3File -p s3_key=123/4d689aeb-1497-4410-a9fe-b36ca9ac4389/Stra%C3%9Fe%201.pdf -p s3_bucket=epilot-prod-user-content --jsonata 'download_url' ```
@@ -661,9 +686,301 @@ epilot file getZipJob -p job_id=123e4567-e89b-12d3-a456-426614174000 --jsonata ' --- +### `getFileSummary` + +Get summary text for a file entity together with the current summary job status when available. + +`GET /v1/files/{id}/summary` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `id` | path | string \| string (uuid) | Yes | The UUID of the file entity | + +**Sample Call** + +```bash +epilot file getFileSummary \ + -p id=ef7d985c-2385-44f4-9c71-ae06a52264f8 +``` + +Using positional args for path parameters: + +```bash +epilot file getFileSummary ef7d985c-2385-44f4-9c71-ae06a52264f8 +``` + +With JSONata filter: + +```bash +epilot file getFileSummary -p id=ef7d985c-2385-44f4-9c71-ae06a52264f8 --jsonata 'status' +``` + +
+Sample Response + +```json +{ + "status": "queued", + "job_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "feedback_available": true, + "preview_summary_de": "string", + "short_summary_de": "string", + "preview_summary_en": "string", + "short_summary_en": "string" +} +``` + +
+ +--- + +### `getFileSummaryFeedback` + +Get the authenticated user's feedback for the current generated file summary. + +`GET /v1/files/{id}/summary/feedback` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `id` | path | string \| string (uuid) | Yes | The UUID of the file entity | + +**Sample Call** + +```bash +epilot file getFileSummaryFeedback \ + -p id=ef7d985c-2385-44f4-9c71-ae06a52264f8 +``` + +Using positional args for path parameters: + +```bash +epilot file getFileSummaryFeedback ef7d985c-2385-44f4-9c71-ae06a52264f8 +``` + +With JSONata filter: + +```bash +epilot file getFileSummaryFeedback -p id=ef7d985c-2385-44f4-9c71-ae06a52264f8 --jsonata 'feedback' +``` + +
+Sample Response + +```json +{ + "feedback": { + "rating": "up", + "comment": "string", + "user_id": "string", + "submitted_at": "1970-01-01T00:00:00.000Z" + } +} +``` + +
+ +--- + +### `putFileSummaryFeedback` + +Upsert thumbs up/down feedback for the current generated file summary. + +`PUT /v1/files/{id}/summary/feedback` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `id` | path | string \| string (uuid) | Yes | The UUID of the file entity | + +**Request Body** (required) + +**Sample Call** + +```bash +epilot file putFileSummaryFeedback \ + -p id=ef7d985c-2385-44f4-9c71-ae06a52264f8 \ + -d '{"rating":"up","comment":"string"}' +``` + +Using positional args for path parameters: + +```bash +epilot file putFileSummaryFeedback ef7d985c-2385-44f4-9c71-ae06a52264f8 +``` + +Using stdin pipe: + +```bash +cat body.json | epilot file putFileSummaryFeedback -p id=ef7d985c-2385-44f4-9c71-ae06a52264f8 +``` + +With JSONata filter: + +```bash +epilot file putFileSummaryFeedback -p id=ef7d985c-2385-44f4-9c71-ae06a52264f8 --jsonata 'feedback' +``` + +
+Sample Response + +```json +{ + "feedback": { + "rating": "up", + "comment": "string", + "user_id": "string", + "submitted_at": "1970-01-01T00:00:00.000Z" + } +} +``` + +
+ +--- + +### `createFileSummaryJob` + +Create or return the current AI summary job for a file entity. + +`POST /v1/files/{id}/summary-jobs` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `id` | path | string \| string (uuid) | Yes | The UUID of the file entity | + +**Sample Call** + +```bash +epilot file createFileSummaryJob \ + -p id=ef7d985c-2385-44f4-9c71-ae06a52264f8 +``` + +Using positional args for path parameters: + +```bash +epilot file createFileSummaryJob ef7d985c-2385-44f4-9c71-ae06a52264f8 +``` + +With JSONata filter: + +```bash +epilot file createFileSummaryJob -p id=ef7d985c-2385-44f4-9c71-ae06a52264f8 --jsonata '$' +``` + +--- + +### `getCurrentFileSummaryJob` + +Get the latest AI summary job for the file entity's current source. + +`GET /v1/files/{id}/summary-jobs/current` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `id` | path | string \| string (uuid) | Yes | The UUID of the file entity | + +**Sample Call** + +```bash +epilot file getCurrentFileSummaryJob \ + -p id=ef7d985c-2385-44f4-9c71-ae06a52264f8 +``` + +Using positional args for path parameters: + +```bash +epilot file getCurrentFileSummaryJob ef7d985c-2385-44f4-9c71-ae06a52264f8 +``` + +With JSONata filter: + +```bash +epilot file getCurrentFileSummaryJob -p id=ef7d985c-2385-44f4-9c71-ae06a52264f8 --jsonata 'job_id' +``` + +
+Sample Response + +```json +{ + "job_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "file_id": "ef7d985c-2385-44f4-9c71-ae06a52264f8", + "status": "queued", + "error": "string", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z", + "completed_at": "1970-01-01T00:00:00.000Z" +} +``` + +
+ +--- + +### `getFileSummaryJob` + +Get an AI summary job by id. + +`GET /v1/files/{id}/summary-jobs/{job_id}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `id` | path | string \| string (uuid) | Yes | The UUID of the file entity | +| `job_id` | path | string (uuid) | Yes | The UUID of the summary job | + +**Sample Call** + +```bash +epilot file getFileSummaryJob \ + -p id=ef7d985c-2385-44f4-9c71-ae06a52264f8 \ + -p job_id=123e4567-e89b-12d3-a456-426614174000 +``` + +Using positional args for path parameters: + +```bash +epilot file getFileSummaryJob ef7d985c-2385-44f4-9c71-ae06a52264f8 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot file getFileSummaryJob -p id=ef7d985c-2385-44f4-9c71-ae06a52264f8 -p job_id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'job_id' +``` + +
+Sample Response + +```json +{ + "job_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "file_id": "ef7d985c-2385-44f4-9c71-ae06a52264f8", + "status": "queued", + "error": "string", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z", + "completed_at": "1970-01-01T00:00:00.000Z" +} +``` + +
+ +--- + ### `generateFileSummary` -Request AI generation of preview and short summaries for a file entity. +Compatibility alias for creating or returning the current AI summary job for a file entity. `POST /v1/files/{id}/summary:generate` @@ -694,6 +1011,138 @@ epilot file generateFileSummary -p id=ef7d985c-2385-44f4-9c71-ae06a52264f8 --jso --- +### `getFileText` + +Get the plain-text representation of a file entity. Returns status `not_ready` while text is being prepared and `unsuppo + +`GET /v1/files/{id}/text` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `id` | path | string \| string (uuid) | Yes | The UUID of the file entity | + +**Sample Call** + +```bash +epilot file getFileText \ + -p id=ef7d985c-2385-44f4-9c71-ae06a52264f8 +``` + +Using positional args for path parameters: + +```bash +epilot file getFileText ef7d985c-2385-44f4-9c71-ae06a52264f8 +``` + +With JSONata filter: + +```bash +epilot file getFileText -p id=ef7d985c-2385-44f4-9c71-ae06a52264f8 --jsonata 'status' +``` + +
+Sample Response + +```json +{ + "status": "ready", + "text": "string", + "truncated": true, + "total_chars": 0 +} +``` + +
+ +--- + +### `validateFile` + +Validate an uploaded file against one or more document validation rules + +`POST /v1/files/validations` + +**Request Body** (required) + +**Sample Call** + +```bash +epilot file validateFile \ + -d '{"file":{"entity_id":"ef7d985c-2385-44f4-9c71-ae06a52264f8"},"rule_ids":["string"]}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot file validateFile +``` + +With JSONata filter: + +```bash +epilot file validateFile --jsonata 'results[0]' +``` + +
+Sample Response + +```json +{ + "id": "string", + "status": "completed", + "file": { + "entity_id": "ef7d985c-2385-44f4-9c71-ae06a52264f8", + "s3ref": {}, + "filename": "string", + "mime_type": "string", + "size_bytes": 0 + }, + "observation": { + "facts": { + "mime_type": "string", + "size_bytes": 0, + "page_count": 0, + "longer_edge_px": 0, + "is_blank": true, + "is_password_protected": true + }, + "judgements": {}, + "document_type": { + "slug": "string", + "confidence": 0 + } + }, + "results": [ + { + "rule_id": "string", + "rule_title": "string", + "level": "basic", + "is_valid": true, + "failed_parts": ["string"], + "warnings": ["string"], + "conditions": [ + { + "id": "string", + "operator": "string", + "outcome": "pass", + "blocking": true, + "message": "string", + "evidence": "string", + "explanation": "string" + } + ] + } + ], + "created_at": "1970-01-01T00:00:00.000Z" +} +``` + +
+ +--- + ### `previewFile` Generate a thumbnail preview for a file entity. @@ -1387,14 +1836,16 @@ epilot file getFilesInCollection -p id=ef7d985c-2385-44f4-9c71-ae06a52264f8 -p c "mime_type": "application/pdf", "size_bytes": 1234, "readable_size": "1.2 MB", + "etag": "9bb58f26192e4ba00f01e2e7b136bbd8", "access_control": "private", "public_url": "https://epilot-prod-user-content.s3.eu-central-1.amazonaws.com/123/4d689aeb-1497-4410-a9fe-b36ca9ac4389/document.pdf", "custom_download_url": "https://some-api-url.com/download?file_id=123", + "custom_download_url_auth": "token", "preview_summary_de": "string", "short_summary_de": "string", "preview_summary_en": "string", "short_summary_en": "string", - "summary_status": "processing", + "file_summary_execution_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "source_url": "https://productengineer-content.s3.eu-west-1.amazonaws.com/product-engineer-checklist.pdf", "s3ref": {}, "versions": [ @@ -1403,7 +1854,8 @@ epilot file getFilesInCollection -p id=ef7d985c-2385-44f4-9c71-ae06a52264f8 -p c "filename": "document.pdf", "size_bytes": 1234, "readable_size": "1.2 MB", - "mime_type": "image/jpeg" + "mime_type": "image/jpeg", + "etag": "9bb58f26192e4ba00f01e2e7b136bbd8" } ], "_updated_at": "1970-01-01T00:00:00.000Z", diff --git a/packages/cli/docs/index.md b/packages/cli/docs/index.md index 0f8f00409..6d2941c71 100644 --- a/packages/cli/docs/index.md +++ b/packages/cli/docs/index.md @@ -1,6 +1,6 @@ # epilot CLI — API Reference -All 51 epilot APIs available via the CLI. +All 52 epilot APIs available via the CLI. ## Setup @@ -12,56 +12,57 @@ npx epilot auth login | API | Command | Operations | Docs | | --- | ------- | ---------- | ---- | -| Access Token API | `epilot access-token` | 9 | [access-token.md](./access-token.md) | +| Access Token API | `epilot access-token` | 11 | [access-token.md](./access-token.md) | | Address API | `epilot address` | 3 | [address.md](./address.md) | | Address Suggestions API | `epilot address-suggestions` | 4 | [address-suggestions.md](./address-suggestions.md) | -| AI Agents API - OpenAPI 3.0 | `epilot ai-agents` | 12 | [ai-agents.md](./ai-agents.md) | -| App API | `epilot app` | 32 | [app.md](./app.md) | +| AI Agents API | `epilot ai-agents` | 21 | [ai-agents.md](./ai-agents.md) | +| App API | `epilot app` | 36 | [app.md](./app.md) | | Audit Log | `epilot audit-logs` | 2 | [audit-logs.md](./audit-logs.md) | -| Automation API | `epilot automation` | 16 | [automation.md](./automation.md) | -| Billing API | `epilot billing` | 10 | [billing.md](./billing.md) | -| Blueprint Manifest API | `epilot blueprint-manifest` | 61 | [blueprint-manifest.md](./blueprint-manifest.md) | -| Calendar API | `epilot calendar` | 4 | [calendar.md](./calendar.md) | +| Automation API | `epilot automation` | 17 | [automation.md](./automation.md) | +| Billing API | `epilot billing` | 14 | [billing.md](./billing.md) | +| Blueprint Manifest API | `epilot blueprint-manifest` | 73 | [blueprint-manifest.md](./blueprint-manifest.md) | +| Calendar API | `epilot calendar` | 28 | [calendar.md](./calendar.md) | +| epilot Chat API | `epilot chat` | 9 | [chat.md](./chat.md) | | Configuration Hub API | `epilot configuration-hub` | 21 | [configuration-hub.md](./configuration-hub.md) | | Consent API | `epilot consent` | 3 | [consent.md](./consent.md) | -| Portal API | `epilot customer-portal` | 149 | [customer-portal.md](./customer-portal.md) | -| Dashboard API | `epilot dashboard` | 7 | [dashboard.md](./dashboard.md) | +| Portal API | `epilot customer-portal` | 173 | [customer-portal.md](./customer-portal.md) | +| Dashboard API | `epilot dashboard` | 18 | [dashboard.md](./dashboard.md) | | Data Governance API | `epilot data-governance` | 10 | [data-governance.md](./data-governance.md) | -| Deduplication API | `epilot deduplication` | 3 | [deduplication.md](./deduplication.md) | -| Design Builder API v2 | `epilot design` | 13 | [design.md](./design.md) | -| Document API | `epilot document` | 3 | [document.md](./document.md) | -| Messaging Settings API | `epilot email-settings` | 36 | [email-settings.md](./email-settings.md) | +| Deduplication API | `epilot deduplication` | 10 | [deduplication.md](./deduplication.md) | +| Design Builder API v2 | `epilot design` | 14 | [design.md](./design.md) | +| Document API | `epilot document` | 4 | [document.md](./document.md) | +| Messaging Settings API | `epilot email-settings` | 48 | [email-settings.md](./email-settings.md) | | Email template API | `epilot email-template` | 7 | [email-template.md](./email-template.md) | -| Entity API | `epilot entity` | 87 | [entity.md](./entity.md) | +| Entity API | `epilot entity` | 88 | [entity.md](./entity.md) | | Entity Mapping API | `epilot entity-mapping` | 12 | [entity-mapping.md](./entity-mapping.md) | | Environments API | `epilot environments` | 8 | [environments.md](./environments.md) | -| Event Catalog API | `epilot event-catalog` | 10 | [event-catalog.md](./event-catalog.md) | -| File API | `epilot file` | 30 | [file.md](./file.md) | +| Event Catalog API | `epilot event-catalog` | 15 | [event-catalog.md](./event-catalog.md) | +| File API | `epilot file` | 38 | [file.md](./file.md) | | Iban API | `epilot iban` | 1 | [iban.md](./iban.md) | -| Integration Toolkit API | `epilot integration-toolkit` | 51 | [integration-toolkit.md](./integration-toolkit.md) | -| Journey API | `epilot journey` | 16 | [journey.md](./journey.md) | +| Integration Toolkit API | `epilot integration-toolkit` | 70 | [integration-toolkit.md](./integration-toolkit.md) | +| Journey API | `epilot journey` | 23 | [journey.md](./journey.md) | | Kanban API | `epilot kanban` | 10 | [kanban.md](./kanban.md) | -| Message API | `epilot message` | 52 | [message.md](./message.md) | -| Metering API | `epilot metering` | 20 | [metering.md](./metering.md) | +| Message API | `epilot message` | 54 | [message.md](./message.md) | +| Metering API | `epilot metering` | 21 | [metering.md](./metering.md) | | Notes API | `epilot notes` | 14 | [notes.md](./notes.md) | | Notification API | `epilot notification` | 14 | [notification.md](./notification.md) | | Organization API | `epilot organization` | 8 | [organization.md](./organization.md) | | Partner Directory API | `epilot partner-directory` | 17 | [partner-directory.md](./partner-directory.md) | | Permissions API | `epilot permissions` | 13 | [permissions.md](./permissions.md) | -| Pricing API | `epilot pricing` | 22 | [pricing.md](./pricing.md) | +| Pricing API | `epilot pricing` | 39 | [pricing.md](./pricing.md) | | Pricing Tier API | `epilot pricing-tier` | 1 | [pricing-tier.md](./pricing-tier.md) | | Purpose API | `epilot purpose` | 6 | [purpose.md](./purpose.md) | | Query API | `epilot query` | 21 | [query.md](./query.md) | | Sandbox API | `epilot sandbox` | 7 | [sandbox.md](./sandbox.md) | | Sharing API | `epilot sharing` | 12 | [sharing.md](./sharing.md) | -| Snapshot API | `epilot snapshot` | 9 | [snapshot.md](./snapshot.md) | +| Snapshot API | `epilot snapshot` | 12 | [snapshot.md](./snapshot.md) | | Submission API | `epilot submission` | 2 | [submission.md](./submission.md) | -| Targeting API | `epilot targeting` | 12 | [targeting.md](./targeting.md) | +| Targeting API | `epilot targeting` | 16 | [targeting.md](./targeting.md) | | Template Variables API | `epilot template-variables` | 12 | [template-variables.md](./template-variables.md) | -| User API | `epilot user` | 41 | [user.md](./user.md) | +| User API | `epilot user` | 42 | [user.md](./user.md) | | Validation Rules API | `epilot validation-rules` | 7 | [validation-rules.md](./validation-rules.md) | | Webhooks | `epilot webhooks` | 15 | [webhooks.md](./webhooks.md) | -| Workflows Executions | `epilot workflow` | 24 | [workflow.md](./workflow.md) | +| Workflows Executions | `epilot workflow` | 25 | [workflow.md](./workflow.md) | | Workflows Definitions | `epilot workflow-definition` | 22 | [workflow-definition.md](./workflow-definition.md) | ## Global Flags diff --git a/packages/cli/docs/integration-toolkit.md b/packages/cli/docs/integration-toolkit.md index 735dc9250..885b900bd 100644 --- a/packages/cli/docs/integration-toolkit.md +++ b/packages/cli/docs/integration-toolkit.md @@ -40,6 +40,7 @@ epilot integration-toolkit acknowledgeTracking - [`triggerErp`](#triggererp) — Triggers the ERP integration process - [`processErpUpdatesEventsV3`](#processerpupdateseventsv3) — Handles updates from ERP systems using integration_id directly. - [`simulateMappingV2`](#simulatemappingv2) — Test v2.0 mapping configuration by transforming a payload using the provided mapping rules without persisting data. +- [`simulateDirect`](#simulatedirect) — Dry run for direct-mode payloads: validates a `DirectPayload` against a `direct: true` - [`simulateMapping`](#simulatemapping) — Test mapping configuration by transforming a payload using the provided mapping rules without persisting data. **integrations** @@ -56,17 +57,25 @@ epilot integration-toolkit acknowledgeTracking - [`updateUseCase`](#updateusecase) — Update an existing use case configuration - [`deleteUseCase`](#deleteusecase) — Delete a use case from an integration - [`listUseCaseHistory`](#listusecasehistory) — Retrieve historical versions of a use case's configuration. +- [`listDocumentationPages`](#listdocumentationpages) — Retrieve all documentation pages of an integration, without their markdown content. +- [`getDocumentationPage`](#getdocumentationpage) — Retrieve a single documentation page including its markdown content +- [`upsertDocumentationPage`](#upsertdocumentationpage) — Create or update the documentation page identified by docId. +- [`deleteDocumentationPage`](#deletedocumentationpage) — Delete a documentation page - [`listIntegrationsV2`](#listintegrationsv2) — Retrieve all integrations with embedded use cases for the authenticated organization - [`createIntegrationV2`](#createintegrationv2) — Create a new integration with embedded use cases. - [`getIntegrationV2`](#getintegrationv2) — Retrieve a specific integration with all its embedded use cases - [`updateIntegrationV2`](#updateintegrationv2) — Update an existing integration with embedded use cases. - [`deleteIntegrationV2`](#deleteintegrationv2) — Delete an integration and all its use cases +- [`listNotificationHistory`](#listnotificationhistory) — Returns the cursor-paginated, newest-first notification history for an +- [`testSendNotification`](#testsendnotification) — Renders and sends ONE representative notification of the requested kind/type to +- [`getNotificationStatus`](#getnotificationstatus) — Returns the live per-rule alert state and (for 'auto' rules) the current - [`getSecureProxyWhitelist`](#getsecureproxywhitelist) — Returns the current allowed_domains, allowed_ips, and vpc_mode for a secure_proxy use case. - [`updateSecureProxyWhitelist`](#updatesecureproxywhitelist) — Replaces allowed_domains and/or allowed_ips on a secure_proxy use case. - [`listSecureProxyWhitelistHistory`](#listsecureproxywhitelisthistory) — Returns the most recent USECASE_HISTORY entries for a secure_proxy use case, - [`setIntegrationAppMapping`](#setintegrationappmapping) — Creates or updates a mapping from an app/component to an integration. - [`deleteIntegrationAppMapping`](#deleteintegrationappmapping) — Removes a mapping from an app/component to an integration. - [`getOutboundStatus`](#getoutboundstatus) — Get the status of all outbound use cases for a specific integration. +- [`getEntitySyncStatus`](#getentitysyncstatus) — Get the inbound ERP sync status of an entity: when each integration last - [`pollOutboundMessages`](#polloutboundmessages) — Poll outbound messages for an integration's poll-mode use cases. - [`ackOutboundMessages`](#ackoutboundmessages) — Acknowledge polled outbound messages. Acks are validated against the - [`listOutboundDlqMessages`](#listoutbounddlqmessages) — List an integration's dead-lettered outbound queue messages @@ -78,15 +87,13 @@ epilot integration-toolkit acknowledgeTracking - [`commitTypes`](#committypes) — Commits the generated types by locking use case configurations and updating version tracking. Should be called after the **monitoring** -- [`queryInboundMonitoringEvents`](#queryinboundmonitoringevents) — Query inbound monitoring events for a specific integration. -- [`getMonitoringStats`](#getmonitoringstats) — Get aggregated statistics for both inbound and outbound monitoring events for a specific integration. -- [`getMonitoringTimeSeries`](#getmonitoringtimeseries) — Get time-series aggregated event counts for monitoring charts. - [`queryAccessLogs`](#queryaccesslogs) — Query API access logs for a specific integration's organization. -- [`queryOutboundMonitoringEvents`](#queryoutboundmonitoringevents) — Query outbound monitoring events for a specific integration. - [`queryMonitoringEventsV2`](#querymonitoringeventsv2) — Query monitoring events from the unified erp_monitoring_v2 table. - [`getMonitoringStatsV2`](#getmonitoringstatsv2) — Get aggregated statistics from the unified erp_monitoring_v2 table. - [`getMonitoringTimeSeriesV2`](#getmonitoringtimeseriesv2) — Get time-series aggregated event counts from the unified erp_monitoring_v2 table. - [`getAssociatedMonitoringEvents`](#getassociatedmonitoringevents) — Returns all monitoring events sharing the same event_id, ordered chronologically. +- [`ingestExternalMonitoringEvents`](#ingestexternalmonitoringevents) — Ingest monitoring spans produced by an EXTERNAL system (e.g. an integration +- [`getMonitoringTraceByCorrelation`](#getmonitoringtracebycorrelation) — Returns the cross-system event trace for a `correlation_id`: every monitoring **proxy** - [`secureProxy`](#secureproxy) — Routes an HTTP request through a VPC with either static IP egress or VPN secure link access. @@ -94,6 +101,16 @@ epilot integration-toolkit acknowledgeTracking **managed-call** - [`managedCallExecute`](#managedcallexecute) — Execute a managed call operation synchronously. The slug in the path acts as the RPC method name. +**erp-imports** +- [`listErpImports`](#listerpimports) — List recent pricing-file import jobs for the org, newest first. +- [`createErpImport`](#createerpimport) — Register an already-uploaded file (S3 ref) as a pricing-file import job. Returns the job and a file preview. Nothing run +- [`getErpImport`](#geterpimport) — Get a pricing-file import job (status, counts, result links). +- [`deleteErpImport`](#deleteerpimport) — Remove an import and the file it owns. Allowed from any status: an import whose run is still in flight is stopped by the +- [`validateErpImport`](#validateerpimport) — Choose the use case to read this file with, and start the validate phase. +- [`suggestErpImportUseCases`](#suggesterpimportusecases) — Rank the org's inbound use cases against this file's columns — the input to the ranked picker ("matches 6 of your 7 colu +- [`executeErpImport`](#executeerpimport) — Confirm and run the write phase of a validated import. Only a READY job may be executed; any other status returns 409. +- [`abortErpImport`](#aborterpimport) — Ask a running import to stop. Valid while the job is VALIDATING or PROCESSING; any other status returns 409. + ### `acknowledgeTracking` Acknowledges an ERP tracking record by removing it from the tracking table, requires public authentication @@ -278,6 +295,7 @@ With request body: epilot integration-toolkit simulateMappingV2 \ -d '{ "event_configuration": { + "direct": true, "entities": [ {} ], @@ -311,7 +329,14 @@ epilot integration-toolkit simulateMappingV2 --jsonata 'entity_updates' { "entity_slug": "string", "unique_identifiers": {}, - "attributes": {} + "attributes": {}, + "pricing": { + "config": {}, + "data": [ + {} + ] + }, + "mode": "upsert" } ], "meter_readings_updates": [ @@ -322,7 +347,106 @@ epilot integration-toolkit simulateMappingV2 --jsonata 'entity_updates' "meter_counter": { "$entity_unique_ids": {} }, - "attributes": {} + "attributes": {}, + "mode": "upsert" + } + ], + "prune_scope_updates": [ + { + "entity_slug": "string", + "scope": { + "scope_mode": "relations", + "schema": "string", + "unique_ids": {}, + "query": {} + }, + "keep_unique_ids": [ + {} + ], + "deletion_mode": "delete" + } + ], + "meter_readings_prune_scope_updates": [ + { + "meter": { + "$entity_unique_ids": {} + }, + "meter_counter": { + "$entity_unique_ids": {} + }, + "keep_external_ids": ["string"], + "source": "string" + } + ], + "warnings": [ + { + "entity_schema": "string", + "field": "string", + "message": "string" + } + ] +} +``` + +
+ +--- + +### `simulateDirect` + +Dry run for direct-mode payloads: validates a `DirectPayload` against a `direct: true` + +`POST /v1/erp/updates/direct_simulation` + +**Request Body** (required) + +**Sample Call** + +```bash +epilot integration-toolkit simulateDirect +``` + +With request body: + +```bash +epilot integration-toolkit simulateDirect \ + -d '{ + "event_configuration": { + "direct": true, + "entities": [ + {} + ], + "meter_readings": [ + {} + ] + }, + "payload": "string" +}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot integration-toolkit simulateDirect +``` + +With JSONata filter: + +```bash +epilot integration-toolkit simulateDirect --jsonata 'valid' +``` + +
+Sample Response + +```json +{ + "valid": true, + "errors": [ + { + "code": "DIRECT_PAYLOAD_INVALID", + "message": "string", + "operation_index": 0 } ], "warnings": [ @@ -331,6 +455,36 @@ epilot integration-toolkit simulateMappingV2 --jsonata 'entity_updates' "field": "string", "message": "string" } + ], + "entity_updates": [ + { + "entity_slug": "string", + "unique_identifiers": {}, + "attributes": {}, + "pricing": { + "config": {}, + "data": [ + {} + ] + }, + "mode": "upsert", + "unique_identifiers_metadata": {} + } + ], + "meter_reading_updates": [ + { + "meter": { + "$entity_unique_ids": {} + }, + "meter_counter": { + "$entity_unique_ids": {} + }, + "attributes": {}, + "mode": "upsert", + "_config": { + "reading_matching": "external_id" + } + } ] } ``` @@ -391,7 +545,14 @@ epilot integration-toolkit simulateMapping --jsonata 'entity_updates' { "entity_slug": "string", "unique_identifiers": {}, - "attributes": {} + "attributes": {}, + "pricing": { + "config": {}, + "data": [ + {} + ] + }, + "mode": "upsert" } ], "meter_readings_updates": [ @@ -402,7 +563,35 @@ epilot integration-toolkit simulateMapping --jsonata 'entity_updates' "meter_counter": { "$entity_unique_ids": {} }, - "attributes": {} + "attributes": {}, + "mode": "upsert" + } + ], + "prune_scope_updates": [ + { + "entity_slug": "string", + "scope": { + "scope_mode": "relations", + "schema": "string", + "unique_ids": {}, + "query": {} + }, + "keep_unique_ids": [ + {} + ], + "deletion_mode": "delete" + } + ], + "meter_readings_prune_scope_updates": [ + { + "meter": { + "$entity_unique_ids": {} + }, + "meter_counter": { + "$entity_unique_ids": {} + }, + "keep_external_ids": ["string"], + "source": "string" } ], "warnings": [ @@ -452,52 +641,11 @@ epilot integration-toolkit listIntegrations --jsonata 'integrations' "description": "string", "access_token_ids": ["string"], "app_ids": ["string"], - "environment_config": [ - { - "key": "string", - "label": "string", - "type": "String", - "description": "string", - "required": false, - "order": 0 - } - ], - "settings": { - "autoRefresh": { - "enabled": false, - "freshnessThresholdMinutes": 1 - } - }, + "environment_config": [], + "maps": [], + "settings": {}, "integration_type": "erp", - "connector_config": { - "base_url": "string", - "auth": { - "type": "oauth2_client_credentials", - "token_url": "string", - "client_id": "string", - "client_secret": "string", - "scope": "string", - "audience": "string", - "resource": "string", - "body_params": {}, - "headers": {}, - "query_params": {}, - "api_key_header": "string", - "api_key": "string", - "token": "string" - }, - "types_versions": [ - { - "version": "string", - "package_name": "string", - "generated_at": "1970-01-01T00:00:00.000Z", - "generated_by": "string", - "status": "active" - } - ], - "latest_types_version": "string", - "latest_types_package_name": "string" - }, + "connector_config": {}, "protected": true, "_manifest": ["string"] } @@ -542,10 +690,28 @@ epilot integration-toolkit createIntegration \ "order": 0 } ], + "maps": [ + { + "key": "string", + "label": "string", + "description": "string", + "value": {} + } + ], "settings": { "autoRefresh": { "enabled": false, "freshnessThresholdMinutes": 1 + }, + "notifications": { + "enabled": true, + "recipients": [], + "defaultChannels": {}, + "monitoredUseCases": ["string"], + "monitoredCodes": ["string"], + "rules": [], + "digest": {}, + "muteUntil": "1970-01-01T00:00:00.000Z" } }, "integration_type": "erp", @@ -567,13 +733,7 @@ epilot integration-toolkit createIntegration \ "token": "string" }, "types_versions": [ - { - "version": "string", - "package_name": "string", - "generated_at": "1970-01-01T00:00:00.000Z", - "generated_by": "string", - "status": "active" - } + {} ], "latest_types_version": "string", "latest_types_package_name": "string" @@ -618,10 +778,28 @@ epilot integration-toolkit createIntegration --jsonata '$' "order": 0 } ], + "maps": [ + { + "key": "string", + "label": "string", + "description": "string", + "value": {} + } + ], "settings": { "autoRefresh": { "enabled": false, "freshnessThresholdMinutes": 1 + }, + "notifications": { + "enabled": true, + "recipients": [], + "defaultChannels": {}, + "monitoredUseCases": ["string"], + "monitoredCodes": ["string"], + "rules": [], + "digest": {}, + "muteUntil": "1970-01-01T00:00:00.000Z" } }, "integration_type": "erp", @@ -643,13 +821,7 @@ epilot integration-toolkit createIntegration --jsonata '$' "token": "string" }, "types_versions": [ - { - "version": "string", - "package_name": "string", - "generated_at": "1970-01-01T00:00:00.000Z", - "generated_by": "string", - "status": "active" - } + {} ], "latest_types_version": "string", "latest_types_package_name": "string" @@ -717,10 +889,28 @@ epilot integration-toolkit getIntegration -p integrationId=123e4567-e89b-12d3-a4 "order": 0 } ], + "maps": [ + { + "key": "string", + "label": "string", + "description": "string", + "value": {} + } + ], "settings": { "autoRefresh": { "enabled": false, "freshnessThresholdMinutes": 1 + }, + "notifications": { + "enabled": true, + "recipients": [], + "defaultChannels": {}, + "monitoredUseCases": ["string"], + "monitoredCodes": ["string"], + "rules": [], + "digest": {}, + "muteUntil": "1970-01-01T00:00:00.000Z" } }, "integration_type": "erp", @@ -742,13 +932,7 @@ epilot integration-toolkit getIntegration -p integrationId=123e4567-e89b-12d3-a4 "token": "string" }, "types_versions": [ - { - "version": "string", - "package_name": "string", - "generated_at": "1970-01-01T00:00:00.000Z", - "generated_by": "string", - "status": "active" - } + {} ], "latest_types_version": "string", "latest_types_package_name": "string" @@ -825,10 +1009,28 @@ epilot integration-toolkit updateIntegration -p integrationId=123e4567-e89b-12d3 "order": 0 } ], + "maps": [ + { + "key": "string", + "label": "string", + "description": "string", + "value": {} + } + ], "settings": { "autoRefresh": { "enabled": false, "freshnessThresholdMinutes": 1 + }, + "notifications": { + "enabled": true, + "recipients": [], + "defaultChannels": {}, + "monitoredUseCases": ["string"], + "monitoredCodes": ["string"], + "rules": [], + "digest": {}, + "muteUntil": "1970-01-01T00:00:00.000Z" } }, "integration_type": "erp", @@ -850,13 +1052,7 @@ epilot integration-toolkit updateIntegration -p integrationId=123e4567-e89b-12d3 "token": "string" }, "types_versions": [ - { - "version": "string", - "package_name": "string", - "generated_at": "1970-01-01T00:00:00.000Z", - "generated_by": "string", - "status": "active" - } + {} ], "latest_types_version": "string", "latest_types_package_name": "string" @@ -986,6 +1182,7 @@ epilot integration-toolkit queryEvents -p integrationId=123e4567-e89b-12d3-a456- "format": "json", "payload": "string", "deduplication_id": "evt-2025-05-01-12345-create-bp", + "correlation_id": "bp-8f3a2c-7d4e-4b1a-9c2f-1e6d5a4b3c21", "use_case_id": "string" } ], @@ -1038,7 +1235,7 @@ cat body.json | epilot integration-toolkit replayEvents -p integrationId=123e456 With JSONata filter: ```bash -epilot integration-toolkit replayEvents -p integrationId=123e4567-e89b-12d3-a456-426614174000 --jsonata 'event_ids' +epilot integration-toolkit replayEvents -p integrationId=123e4567-e89b-12d3-a456-426614174000 --jsonata 'results[0]' ```
@@ -1046,6 +1243,15 @@ epilot integration-toolkit replayEvents -p integrationId=123e4567-e89b-12d3-a456 ```json { + "replayed": 2, + "results": [ + { + "event_id": "string", + "status": "success", + "replay_event_id": "string", + "message": "string" + } + ], "event_ids": ["string"] } ``` @@ -1099,6 +1305,7 @@ epilot integration-toolkit listUseCases -p integrationId=123e4567-e89b-12d3-a456 "type": "inbound", "enabled": true, "change_description": "string", + "changed_by": "string", "created_at": "1970-01-01T00:00:00.000Z", "updated_at": "1970-01-01T00:00:00.000Z", "configuration": {} @@ -1143,6 +1350,7 @@ epilot integration-toolkit createUseCase \ "enabled": true, "type": "inbound", "configuration": { + "direct": true, "entities": [ {} ], @@ -1183,9 +1391,11 @@ epilot integration-toolkit createUseCase -p integrationId=123e4567-e89b-12d3-a45 "type": "inbound", "enabled": true, "change_description": "string", + "changed_by": "string", "created_at": "1970-01-01T00:00:00.000Z", "updated_at": "1970-01-01T00:00:00.000Z", "configuration": { + "direct": true, "entities": [ {} ], @@ -1245,9 +1455,11 @@ epilot integration-toolkit getUseCase -p integrationId=123e4567-e89b-12d3-a456-4 "type": "inbound", "enabled": true, "change_description": "string", + "changed_by": "string", "created_at": "1970-01-01T00:00:00.000Z", "updated_at": "1970-01-01T00:00:00.000Z", "configuration": { + "direct": true, "entities": [ {} ], @@ -1298,6 +1510,7 @@ epilot integration-toolkit updateUseCase \ "change_description": "string", "type": "inbound", "configuration": { + "direct": true, "entities": [ {} ], @@ -1338,9 +1551,11 @@ epilot integration-toolkit updateUseCase -p integrationId=123e4567-e89b-12d3-a45 "type": "inbound", "enabled": true, "change_description": "string", + "changed_by": "string", "created_at": "1970-01-01T00:00:00.000Z", "updated_at": "1970-01-01T00:00:00.000Z", "configuration": { + "direct": true, "entities": [ {} ], @@ -1467,22 +1682,35 @@ epilot integration-toolkit listUseCaseHistory -p integrationId=123e4567-e89b-12d --- -### `listIntegrationsV2` +### `listDocumentationPages` -Retrieve all integrations with embedded use cases for the authenticated organization +Retrieve all documentation pages of an integration, without their markdown content. -`GET /v2/integrations` +`GET /v1/integrations/{integrationId}/documentation` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `integrationId` | path | string (uuid) | Yes | The integration ID | **Sample Call** ```bash -epilot integration-toolkit listIntegrationsV2 +epilot integration-toolkit listDocumentationPages \ + -p integrationId=123e4567-e89b-12d3-a456-426614174000 +``` + +Using positional args for path parameters: + +```bash +epilot integration-toolkit listDocumentationPages 123e4567-e89b-12d3-a456-426614174000 ``` With JSONata filter: ```bash -epilot integration-toolkit listIntegrationsV2 --jsonata 'integrations' +epilot integration-toolkit listDocumentationPages -p integrationId=123e4567-e89b-12d3-a456-426614174000 --jsonata 'pages' ```
@@ -1490,23 +1718,17 @@ epilot integration-toolkit listIntegrationsV2 --jsonata 'integrations' ```json { - "integrations": [ + "pages": [ { - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "orgId": "string", + "id": "general", + "integration_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "scope": "integration", + "use_case_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "title": "string", "created_at": "1970-01-01T00:00:00.000Z", + "created_by": "string", "updated_at": "1970-01-01T00:00:00.000Z", - "name": "string", - "description": "string", - "access_token_ids": ["string"], - "app_ids": ["string"], - "environment_config": [], - "settings": {}, - "integration_type": "erp", - "connector_config": {}, - "protected": true, - "_manifest": ["string"], - "use_cases": [] + "updated_by": "string" } ] } @@ -1516,61 +1738,294 @@ epilot integration-toolkit listIntegrationsV2 --jsonata 'integrations' --- -### `createIntegrationV2` +### `getDocumentationPage` -Create a new integration with embedded use cases. +Retrieve a single documentation page including its markdown content -`POST /v2/integrations` +`GET /v1/integrations/{integrationId}/documentation/{docId}` -**Request Body** (required) +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `integrationId` | path | string (uuid) | Yes | The integration ID | +| `docId` | path | string | Yes | 'general' for the integration-wide page, otherwise a use case ID | **Sample Call** ```bash -epilot integration-toolkit createIntegrationV2 +epilot integration-toolkit getDocumentationPage \ + -p integrationId=123e4567-e89b-12d3-a456-426614174000 \ + -p docId=123e4567-e89b-12d3-a456-426614174000 ``` -With request body: +Using positional args for path parameters: ```bash -epilot integration-toolkit createIntegrationV2 \ - -d '{ - "name": "string", - "description": "string", - "access_token_ids": ["string"], - "app_ids": ["string"], - "environment_config": [ - { - "key": "string", - "label": "string", - "type": "String", - "description": "string", - "required": false, - "order": 0 - } - ], - "settings": { - "autoRefresh": { - "enabled": false, - "freshnessThresholdMinutes": 1 - } - }, - "integration_type": "erp", - "connector_config": { - "base_url": "string", - "auth": { - "type": "oauth2_client_credentials", - "token_url": "string", - "client_id": "string", - "client_secret": "string", - "scope": "string", - "audience": "string", - "resource": "string", - "body_params": {}, - "headers": {}, - "query_params": {}, - "api_key_header": "string", - "api_key": "string", +epilot integration-toolkit getDocumentationPage 123e4567-e89b-12d3-a456-426614174000 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot integration-toolkit getDocumentationPage -p integrationId=123e4567-e89b-12d3-a456-426614174000 -p docId=123e4567-e89b-12d3-a456-426614174000 --jsonata '$' +``` + +
+Sample Response + +```json +{ + "id": "general", + "integration_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "scope": "integration", + "use_case_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "title": "string", + "created_at": "1970-01-01T00:00:00.000Z", + "created_by": "string", + "updated_at": "1970-01-01T00:00:00.000Z", + "updated_by": "string", + "content": "string" +} +``` + +
+ +--- + +### `upsertDocumentationPage` + +Create or update the documentation page identified by docId. + +`PUT /v1/integrations/{integrationId}/documentation/{docId}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `integrationId` | path | string (uuid) | Yes | The integration ID | +| `docId` | path | string | Yes | 'general' for the integration-wide page, otherwise a use case ID | + +**Request Body** (required) + +**Sample Call** + +```bash +epilot integration-toolkit upsertDocumentationPage \ + -p integrationId=123e4567-e89b-12d3-a456-426614174000 \ + -p docId=123e4567-e89b-12d3-a456-426614174000 \ + -d '{"title":"string","content":"string"}' +``` + +Using positional args for path parameters: + +```bash +epilot integration-toolkit upsertDocumentationPage 123e4567-e89b-12d3-a456-426614174000 123e4567-e89b-12d3-a456-426614174000 +``` + +Using stdin pipe: + +```bash +cat body.json | epilot integration-toolkit upsertDocumentationPage -p integrationId=123e4567-e89b-12d3-a456-426614174000 -p docId=123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot integration-toolkit upsertDocumentationPage -p integrationId=123e4567-e89b-12d3-a456-426614174000 -p docId=123e4567-e89b-12d3-a456-426614174000 --jsonata '$' +``` + +
+Sample Response + +```json +{ + "id": "general", + "integration_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "scope": "integration", + "use_case_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "title": "string", + "created_at": "1970-01-01T00:00:00.000Z", + "created_by": "string", + "updated_at": "1970-01-01T00:00:00.000Z", + "updated_by": "string", + "content": "string" +} +``` + +
+ +--- + +### `deleteDocumentationPage` + +Delete a documentation page + +`DELETE /v1/integrations/{integrationId}/documentation/{docId}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `integrationId` | path | string (uuid) | Yes | The integration ID | +| `docId` | path | string | Yes | 'general' for the integration-wide page, otherwise a use case ID | + +**Sample Call** + +```bash +epilot integration-toolkit deleteDocumentationPage \ + -p integrationId=123e4567-e89b-12d3-a456-426614174000 \ + -p docId=123e4567-e89b-12d3-a456-426614174000 +``` + +Using positional args for path parameters: + +```bash +epilot integration-toolkit deleteDocumentationPage 123e4567-e89b-12d3-a456-426614174000 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot integration-toolkit deleteDocumentationPage -p integrationId=123e4567-e89b-12d3-a456-426614174000 -p docId=123e4567-e89b-12d3-a456-426614174000 --jsonata 'message' +``` + +
+Sample Response + +```json +{ + "message": "string" +} +``` + +
+ +--- + +### `listIntegrationsV2` + +Retrieve all integrations with embedded use cases for the authenticated organization + +`GET /v2/integrations` + +**Sample Call** + +```bash +epilot integration-toolkit listIntegrationsV2 +``` + +With JSONata filter: + +```bash +epilot integration-toolkit listIntegrationsV2 --jsonata 'integrations' +``` + +
+Sample Response + +```json +{ + "integrations": [ + { + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "orgId": "string", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z", + "name": "string", + "description": "string", + "access_token_ids": ["string"], + "app_ids": ["string"], + "environment_config": [], + "maps": [], + "settings": {}, + "integration_type": "erp", + "connector_config": {}, + "protected": true, + "_manifest": ["string"], + "use_cases": [] + } + ] +} +``` + +
+ +--- + +### `createIntegrationV2` + +Create a new integration with embedded use cases. + +`POST /v2/integrations` + +**Request Body** (required) + +**Sample Call** + +```bash +epilot integration-toolkit createIntegrationV2 +``` + +With request body: + +```bash +epilot integration-toolkit createIntegrationV2 \ + -d '{ + "name": "string", + "description": "string", + "access_token_ids": ["string"], + "app_ids": ["string"], + "environment_config": [ + { + "key": "string", + "label": "string", + "type": "String", + "description": "string", + "required": false, + "order": 0 + } + ], + "maps": [ + { + "key": "string", + "label": "string", + "description": "string", + "value": {} + } + ], + "settings": { + "autoRefresh": { + "enabled": false, + "freshnessThresholdMinutes": 1 + }, + "notifications": { + "enabled": true, + "recipients": [], + "defaultChannels": {}, + "monitoredUseCases": ["string"], + "monitoredCodes": ["string"], + "rules": [], + "digest": {}, + "muteUntil": "1970-01-01T00:00:00.000Z" + } + }, + "integration_type": "erp", + "connector_config": { + "base_url": "string", + "auth": { + "type": "oauth2_client_credentials", + "token_url": "string", + "client_id": "string", + "client_secret": "string", + "scope": "string", + "audience": "string", + "resource": "string", + "body_params": {}, + "headers": {}, + "query_params": {}, + "api_key_header": "string", + "api_key": "string", "token": "string" }, "types_versions": [ @@ -1630,10 +2085,28 @@ epilot integration-toolkit createIntegrationV2 --jsonata '$' "order": 0 } ], + "maps": [ + { + "key": "string", + "label": "string", + "description": "string", + "value": {} + } + ], "settings": { "autoRefresh": { "enabled": false, "freshnessThresholdMinutes": 1 + }, + "notifications": { + "enabled": true, + "recipients": [], + "defaultChannels": {}, + "monitoredUseCases": ["string"], + "monitoredCodes": ["string"], + "rules": [], + "digest": {}, + "muteUntil": "1970-01-01T00:00:00.000Z" } }, "integration_type": "erp", @@ -1671,6 +2144,7 @@ epilot integration-toolkit createIntegrationV2 --jsonata '$' "type": "inbound", "enabled": true, "change_description": "string", + "changed_by": "string", "created_at": "1970-01-01T00:00:00.000Z", "updated_at": "1970-01-01T00:00:00.000Z", "configuration": {} @@ -1737,10 +2211,28 @@ epilot integration-toolkit getIntegrationV2 -p integrationId=123e4567-e89b-12d3- "order": 0 } ], + "maps": [ + { + "key": "string", + "label": "string", + "description": "string", + "value": {} + } + ], "settings": { "autoRefresh": { "enabled": false, "freshnessThresholdMinutes": 1 + }, + "notifications": { + "enabled": true, + "recipients": [], + "defaultChannels": {}, + "monitoredUseCases": ["string"], + "monitoredCodes": ["string"], + "rules": [], + "digest": {}, + "muteUntil": "1970-01-01T00:00:00.000Z" } }, "integration_type": "erp", @@ -1778,6 +2270,7 @@ epilot integration-toolkit getIntegrationV2 -p integrationId=123e4567-e89b-12d3- "type": "inbound", "enabled": true, "change_description": "string", + "changed_by": "string", "created_at": "1970-01-01T00:00:00.000Z", "updated_at": "1970-01-01T00:00:00.000Z", "configuration": {} @@ -1831,10 +2324,28 @@ epilot integration-toolkit updateIntegrationV2 \ "order": 0 } ], + "maps": [ + { + "key": "string", + "label": "string", + "description": "string", + "value": {} + } + ], "settings": { "autoRefresh": { "enabled": false, "freshnessThresholdMinutes": 1 + }, + "notifications": { + "enabled": true, + "recipients": [], + "defaultChannels": {}, + "monitoredUseCases": ["string"], + "monitoredCodes": ["string"], + "rules": [], + "digest": {}, + "muteUntil": "1970-01-01T00:00:00.000Z" } }, "integration_type": "erp", @@ -1918,10 +2429,28 @@ epilot integration-toolkit updateIntegrationV2 -p integrationId=123e4567-e89b-12 "order": 0 } ], + "maps": [ + { + "key": "string", + "label": "string", + "description": "string", + "value": {} + } + ], "settings": { "autoRefresh": { "enabled": false, "freshnessThresholdMinutes": 1 + }, + "notifications": { + "enabled": true, + "recipients": [], + "defaultChannels": {}, + "monitoredUseCases": ["string"], + "monitoredCodes": ["string"], + "rules": [], + "digest": {}, + "muteUntil": "1970-01-01T00:00:00.000Z" } }, "integration_type": "erp", @@ -1959,6 +2488,7 @@ epilot integration-toolkit updateIntegrationV2 -p integrationId=123e4567-e89b-12 "type": "inbound", "enabled": true, "change_description": "string", + "changed_by": "string", "created_at": "1970-01-01T00:00:00.000Z", "updated_at": "1970-01-01T00:00:00.000Z", "configuration": {} @@ -2015,6 +2545,187 @@ epilot integration-toolkit deleteIntegrationV2 -p integrationId=123e4567-e89b-12 --- +### `listNotificationHistory` + +Returns the cursor-paginated, newest-first notification history for an + +`GET /v2/integrations/{integrationId}/notifications/history` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `integrationId` | path | string (uuid) | Yes | The integration ID | +| `cursor` | query | string | No | Opaque base64 pagination cursor returned as `next_cursor` by a prior page. | +| `limit` | query | number | No | Requested number of items to return. Values above 100 are accepted and clamped server-side to 100 (the enforcement point is the handler, not this schema), so a large value never 400s at the contract l | +| `type` | query | string | No | Optional notification type filter (e.g. `critical_error`, `error_threshold`). | + +**Sample Call** + +```bash +epilot integration-toolkit listNotificationHistory \ + -p integrationId=123e4567-e89b-12d3-a456-426614174000 +``` + +Using positional args for path parameters: + +```bash +epilot integration-toolkit listNotificationHistory 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot integration-toolkit listNotificationHistory -p integrationId=123e4567-e89b-12d3-a456-426614174000 --jsonata 'history' +``` + +
+Sample Response + +```json +{ + "history": [ + { + "id": "string", + "type": "string", + "state_transition": "string", + "severity": "error", + "title": "string", + "occurred_at": "1970-01-01T00:00:00.000Z", + "notified": true, + "suppressed_reason": "muted", + "recipients": ["string"], + "context": {}, + "created_at": "1970-01-01T00:00:00.000Z" + } + ], + "next_cursor": "string" +} +``` + +
+ +--- + +### `testSendNotification` + +Renders and sends ONE representative notification of the requested kind/type to + +`POST /v2/integrations/{integrationId}/notifications/test` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `integrationId` | path | string (uuid) | Yes | The integration ID | + +**Request Body** (required) + +**Sample Call** + +```bash +epilot integration-toolkit testSendNotification \ + -p integrationId=123e4567-e89b-12d3-a456-426614174000 \ + -d '{"kind":"alert","type":"string","channels":["email"]}' +``` + +Using positional args for path parameters: + +```bash +epilot integration-toolkit testSendNotification 123e4567-e89b-12d3-a456-426614174000 +``` + +Using stdin pipe: + +```bash +cat body.json | epilot integration-toolkit testSendNotification -p integrationId=123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot integration-toolkit testSendNotification -p integrationId=123e4567-e89b-12d3-a456-426614174000 --jsonata '$' +``` + +--- + +### `getNotificationStatus` + +Returns the live per-rule alert state and (for 'auto' rules) the current + +`GET /v2/integrations/{integrationId}/notifications/status` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `integrationId` | path | string (uuid) | Yes | The integration ID | +| `include` | query | "baseline_series" | No | Add `baseline_series` to also return all 168 hour-of-week buckets per +'auto' rule (heavier; omit for just the current-bucket markers). + | + +**Sample Call** + +```bash +epilot integration-toolkit getNotificationStatus \ + -p integrationId=123e4567-e89b-12d3-a456-426614174000 +``` + +Using positional args for path parameters: + +```bash +epilot integration-toolkit getNotificationStatus 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot integration-toolkit getNotificationStatus -p integrationId=123e4567-e89b-12d3-a456-426614174000 --jsonata 'health' +``` + +
+Sample Response + +```json +{ + "health": "healthy", + "evaluated_at": "1970-01-01T00:00:00.000Z", + "rules": [ + { + "rule_id": "string", + "state": "ok", + "last_fired_at": "1970-01-01T00:00:00.000Z", + "last_cleared_at": "1970-01-01T00:00:00.000Z", + "last_evaluation": { + "evaluated_at": "1970-01-01T00:00:00.000Z", + "observed": 0, + "threshold": 0, + "suppressed_reason": "sample_size" + }, + "baseline": { + "is_mature": true, + "computed_at": "1970-01-01T00:00:00.000Z", + "median": 0, + "mad": 0, + "upper": 0, + "buckets": [ + { + "dow": 1, + "hour": 0, + "median": 0, + "mad": 0 + } + ] + } + } + ] +} +``` + +
+ +--- + ### `getSecureProxyWhitelist` Returns the current allowed_domains, allowed_ips, and vpc_mode for a secure_proxy use case. @@ -2307,11 +3018,11 @@ epilot integration-toolkit deleteIntegrationAppMapping -p integrationId=123e4567 --- -### `queryInboundMonitoringEvents` +### `getOutboundStatus` -Query inbound monitoring events for a specific integration. +Get the status of all outbound use cases for a specific integration. -`POST /v1/integrations/{integrationId}/monitoring/inbound-events` +`GET /v1/integrations/{integrationId}/outbound-status` **Parameters** @@ -2319,56 +3030,23 @@ Query inbound monitoring events for a specific integration. | ---- | -- | ---- | -------- | ----------- | | `integrationId` | path | string (uuid) | Yes | The integration ID | -**Request Body** (required) - **Sample Call** ```bash -epilot integration-toolkit queryInboundMonitoringEvents \ +epilot integration-toolkit getOutboundStatus \ -p integrationId=123e4567-e89b-12d3-a456-426614174000 ``` -With request body: - -```bash -epilot integration-toolkit queryInboundMonitoringEvents \ - -p integrationId=123e4567-e89b-12d3-a456-426614174000 \ - -d '{ - "use_case_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "event_type": "CREATE", - "sync_type": "entity", - "status": "success", - "error_category": "validation", - "correlation_id": "string", - "object_type": "string", - "event_name": "string", - "event_id": "string", - "from_date": "2025-01-01T00:00:00Z", - "to_date": "2025-01-31T23:59:59Z", - "limit": 50, - "cursor": { - "completed_at": "1970-01-01T00:00:00.000Z", - "event_id": "string" - } -}' -``` - Using positional args for path parameters: ```bash -epilot integration-toolkit queryInboundMonitoringEvents 123e4567-e89b-12d3-a456-426614174000 -``` - -Using stdin pipe: - -```bash -cat body.json | epilot integration-toolkit queryInboundMonitoringEvents -p integrationId=123e4567-e89b-12d3-a456-426614174000 +epilot integration-toolkit getOutboundStatus 123e4567-e89b-12d3-a456-426614174000 ``` With JSONata filter: ```bash -epilot integration-toolkit queryInboundMonitoringEvents -p integrationId=123e4567-e89b-12d3-a456-426614174000 --jsonata 'data' +epilot integration-toolkit getOutboundStatus -p integrationId=123e4567-e89b-12d3-a456-426614174000 --jsonata 'useCases' ```
@@ -2376,30 +3054,49 @@ epilot integration-toolkit queryInboundMonitoringEvents -p integrationId=123e456 ```json { - "data": [ + "useCases": [ { - "org_id": "string", - "event_id": "string", - "correlation_id": "string", - "integration_id": "string", - "use_case_id": "string", - "event_type": "CREATE", - "object_type": "string", - "sync_type": "entity", - "status": "success", - "error_code": "string", - "error_message": "string", - "error_category": "validation", - "processing_duration_ms": 0, - "received_at": "1970-01-01T00:00:00.000Z", - "completed_at": "1970-01-01T00:00:00.000Z" + "useCaseId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "name": "string", + "useCaseEnabled": true, + "eventCatalogEvent": "contract.created", + "eventEnabled": true, + "webhooks": [ + { + "webhookId": "string", + "webhookName": "string", + "enabled": true + } + ], + "status": "ok", + "conflicts": [ + { + "type": "event_disabled", + "webhookId": "string", + "message": "string" + } + ], + "poll": { + "queue_depth": 0, + "oldest_unconsumed_age_seconds": 0, + "last_poll_at": "1970-01-01T00:00:00.000Z", + "last_ack_at": "1970-01-01T00:00:00.000Z", + "blocked": true, + "dlq_count": 0 + }, + "file_proxy": [ + { + "mapping_id": "string", + "use_case_slug": "string", + "resolved": true, + "target_use_case_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "target_enabled": true, + "target_updated_at": "1970-01-01T00:00:00.000Z", + "unresolved_reason": "not_found" + } + ] } - ], - "next_cursor": { - "completed_at": "1970-01-01T00:00:00.000Z", - "event_id": "string" - }, - "has_more": true + ] } ``` @@ -2407,56 +3104,36 @@ epilot integration-toolkit queryInboundMonitoringEvents -p integrationId=123e456 --- -### `getMonitoringStats` +### `getEntitySyncStatus` -Get aggregated statistics for both inbound and outbound monitoring events for a specific integration. +Get the inbound ERP sync status of an entity: when each integration last -`POST /v1/integrations/{integrationId}/monitoring/stats` +`GET /v1/integrations/entities/{entityId}/sync-status` **Parameters** | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `integrationId` | path | string (uuid) | Yes | The integration ID | - -**Request Body** (required) +| `entityId` | path | string (uuid) | Yes | The entity ID | +| `integration_id` | query | string (uuid) | No | Narrow the result to a single integration | **Sample Call** ```bash -epilot integration-toolkit getMonitoringStats \ - -p integrationId=123e4567-e89b-12d3-a456-426614174000 -``` - -With request body: - -```bash -epilot integration-toolkit getMonitoringStats \ - -p integrationId=123e4567-e89b-12d3-a456-426614174000 \ - -d '{ - "from_date": "2025-01-01T00:00:00Z", - "to_date": "2025-01-31T23:59:59Z", - "inbound_group_by": ["use_case_id", "status"], - "outbound_group_by": ["event_name", "status"] -}' +epilot integration-toolkit getEntitySyncStatus \ + -p entityId=123e4567-e89b-12d3-a456-426614174000 ``` Using positional args for path parameters: ```bash -epilot integration-toolkit getMonitoringStats 123e4567-e89b-12d3-a456-426614174000 -``` - -Using stdin pipe: - -```bash -cat body.json | epilot integration-toolkit getMonitoringStats -p integrationId=123e4567-e89b-12d3-a456-426614174000 +epilot integration-toolkit getEntitySyncStatus 123e4567-e89b-12d3-a456-426614174000 ``` With JSONata filter: ```bash -epilot integration-toolkit getMonitoringStats -p integrationId=123e4567-e89b-12d3-a456-426614174000 --jsonata 'inbound' +epilot integration-toolkit getEntitySyncStatus -p entityId=123e4567-e89b-12d3-a456-426614174000 --jsonata 'entity_id' ```
@@ -2464,30 +3141,22 @@ epilot integration-toolkit getMonitoringStats -p integrationId=123e4567-e89b-12d ```json { - "inbound": { - "total_events": 0, - "total_correlations": 0, - "success_count": 0, - "error_count": 0, - "skipped_count": 0, - "warning_count": 0, - "success_rate": 0, - "last_error_at": "1970-01-01T00:00:00.000Z", - "breakdown": [ - {} - ] - }, - "outbound": { - "total_events": 0, - "success_count": 0, - "error_count": 0, - "pending_count": 0, - "success_rate": 0, - "last_error_at": "1970-01-01T00:00:00.000Z", - "breakdown": [ - {} - ] - } + "entity_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "sync_states": [ + { + "entity_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "integration_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "entity_slug": "contract", + "use_case_id": "string", + "last_synced_at": "1970-01-01T00:00:00.000Z", + "last_changed_at": "1970-01-01T00:00:00.000Z", + "last_operation": "create", + "last_event_id": "string", + "readings_last_synced_at": "1970-01-01T00:00:00.000Z", + "readings_last_operation": "upsert", + "readings_last_event_id": "string" + } + ] } ``` @@ -2495,11 +3164,11 @@ epilot integration-toolkit getMonitoringStats -p integrationId=123e4567-e89b-12d --- -### `getMonitoringTimeSeries` +### `pollOutboundMessages` -Get time-series aggregated event counts for monitoring charts. +Poll outbound messages for an integration's poll-mode use cases. -`POST /v1/integrations/{integrationId}/monitoring/timeseries` +`POST /v1/integrations/{integrationId}/outbound/messages/poll` **Parameters** @@ -2507,32 +3176,32 @@ Get time-series aggregated event counts for monitoring charts. | ---- | -- | ---- | -------- | ----------- | | `integrationId` | path | string (uuid) | Yes | The integration ID | -**Request Body** (required) +**Request Body** **Sample Call** ```bash -epilot integration-toolkit getMonitoringTimeSeries \ +epilot integration-toolkit pollOutboundMessages \ -p integrationId=123e4567-e89b-12d3-a456-426614174000 \ - -d '{"from_date":"2025-01-01T00:00:00Z","to_date":"2025-01-31T23:59:59Z","interval":"1h","direction":"both"}' + -d '{"limit":10}' ``` Using positional args for path parameters: ```bash -epilot integration-toolkit getMonitoringTimeSeries 123e4567-e89b-12d3-a456-426614174000 +epilot integration-toolkit pollOutboundMessages 123e4567-e89b-12d3-a456-426614174000 ``` Using stdin pipe: ```bash -cat body.json | epilot integration-toolkit getMonitoringTimeSeries -p integrationId=123e4567-e89b-12d3-a456-426614174000 +cat body.json | epilot integration-toolkit pollOutboundMessages -p integrationId=123e4567-e89b-12d3-a456-426614174000 ``` With JSONata filter: ```bash -epilot integration-toolkit getMonitoringTimeSeries -p integrationId=123e4567-e89b-12d3-a456-426614174000 --jsonata 'interval' +epilot integration-toolkit pollOutboundMessages -p integrationId=123e4567-e89b-12d3-a456-426614174000 --jsonata 'messages' ```
@@ -2540,155 +3209,7 @@ epilot integration-toolkit getMonitoringTimeSeries -p integrationId=123e4567-e89 ```json { - "interval": "5m", - "from_date": "1970-01-01T00:00:00.000Z", - "to_date": "1970-01-01T00:00:00.000Z", - "buckets": [ - { - "timestamp": "1970-01-01T00:00:00.000Z", - "inbound": { - "success_count": 0, - "error_count": 0, - "warning_count": 0, - "skipped_count": 0, - "total_count": 0 - }, - "outbound": { - "success_count": 0, - "error_count": 0, - "pending_count": 0, - "total_count": 0 - } - } - ] -} -``` - -
- ---- - -### `getOutboundStatus` - -Get the status of all outbound use cases for a specific integration. - -`GET /v1/integrations/{integrationId}/outbound-status` - -**Parameters** - -| Name | In | Type | Required | Description | -| ---- | -- | ---- | -------- | ----------- | -| `integrationId` | path | string (uuid) | Yes | The integration ID | - -**Sample Call** - -```bash -epilot integration-toolkit getOutboundStatus \ - -p integrationId=123e4567-e89b-12d3-a456-426614174000 -``` - -Using positional args for path parameters: - -```bash -epilot integration-toolkit getOutboundStatus 123e4567-e89b-12d3-a456-426614174000 -``` - -With JSONata filter: - -```bash -epilot integration-toolkit getOutboundStatus -p integrationId=123e4567-e89b-12d3-a456-426614174000 --jsonata 'useCases' -``` - -
-Sample Response - -```json -{ - "useCases": [ - { - "useCaseId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "name": "string", - "useCaseEnabled": true, - "eventCatalogEvent": "contract.created", - "eventEnabled": true, - "webhooks": [ - { - "webhookId": "string", - "webhookName": "string", - "enabled": true - } - ], - "status": "ok", - "conflicts": [ - { - "type": "event_disabled", - "webhookId": "string", - "message": "string" - } - ], - "poll": { - "queue_depth": 0, - "oldest_unconsumed_age_seconds": 0, - "last_poll_at": "1970-01-01T00:00:00.000Z", - "last_ack_at": "1970-01-01T00:00:00.000Z", - "blocked": true, - "dlq_count": 0 - } - } - ] -} -``` - -
- ---- - -### `pollOutboundMessages` - -Poll outbound messages for an integration's poll-mode use cases. - -`POST /v1/integrations/{integrationId}/outbound/messages/poll` - -**Parameters** - -| Name | In | Type | Required | Description | -| ---- | -- | ---- | -------- | ----------- | -| `integrationId` | path | string (uuid) | Yes | The integration ID | - -**Request Body** - -**Sample Call** - -```bash -epilot integration-toolkit pollOutboundMessages \ - -p integrationId=123e4567-e89b-12d3-a456-426614174000 \ - -d '{"limit":10}' -``` - -Using positional args for path parameters: - -```bash -epilot integration-toolkit pollOutboundMessages 123e4567-e89b-12d3-a456-426614174000 -``` - -Using stdin pipe: - -```bash -cat body.json | epilot integration-toolkit pollOutboundMessages -p integrationId=123e4567-e89b-12d3-a456-426614174000 -``` - -With JSONata filter: - -```bash -epilot integration-toolkit pollOutboundMessages -p integrationId=123e4567-e89b-12d3-a456-426614174000 --jsonata 'messages' -``` - -
-Sample Response - -```json -{ - "messages": [ + "messages": [ { "id": "string", "lease_token": "string", @@ -3033,98 +3554,6 @@ epilot integration-toolkit queryAccessLogs -p integrationId=123e4567-e89b-12d3-a --- -### `queryOutboundMonitoringEvents` - -Query outbound monitoring events for a specific integration. - -`POST /v1/integrations/{integrationId}/monitoring/outbound-events` - -**Parameters** - -| Name | In | Type | Required | Description | -| ---- | -- | ---- | -------- | ----------- | -| `integrationId` | path | string (uuid) | Yes | The integration ID | - -**Request Body** (required) - -**Sample Call** - -```bash -epilot integration-toolkit queryOutboundMonitoringEvents \ - -p integrationId=123e4567-e89b-12d3-a456-426614174000 -``` - -With request body: - -```bash -epilot integration-toolkit queryOutboundMonitoringEvents \ - -p integrationId=123e4567-e89b-12d3-a456-426614174000 \ - -d '{ - "event_name": "automation_flow_target", - "status": "succeeded", - "webhook_config_id": "string", - "from_date": "2025-01-01T00:00:00Z", - "to_date": "2025-01-31T23:59:59Z", - "limit": 50, - "cursor": { - "created_at": "1970-01-01T00:00:00.000Z", - "event_id": "string" - } -}' -``` - -Using positional args for path parameters: - -```bash -epilot integration-toolkit queryOutboundMonitoringEvents 123e4567-e89b-12d3-a456-426614174000 -``` - -Using stdin pipe: - -```bash -cat body.json | epilot integration-toolkit queryOutboundMonitoringEvents -p integrationId=123e4567-e89b-12d3-a456-426614174000 -``` - -With JSONata filter: - -```bash -epilot integration-toolkit queryOutboundMonitoringEvents -p integrationId=123e4567-e89b-12d3-a456-426614174000 --jsonata 'data' -``` - -
-Sample Response - -```json -{ - "data": [ - { - "org_id": "string", - "event_id": "string", - "event_name": "string", - "status": "succeeded", - "url": "string", - "http_method": "string", - "http_response": {}, - "webhook_config_id": "string", - "metadata": {}, - "execution_context": {}, - "payload": {}, - "created_at": "1970-01-01T00:00:00.000Z", - "updated_at": "1970-01-01T00:00:00.000Z" - } - ], - "next_cursor": { - "created_at": "1970-01-01T00:00:00.000Z", - "event_id": "string" - }, - "has_more": true -} -``` - -
- ---- - ### `queryMonitoringEventsV2` Query monitoring events from the unified erp_monitoring_v2 table. @@ -3249,6 +3678,7 @@ epilot integration-toolkit getMonitoringStatsV2 \ "from_date": "2025-01-01T00:00:00Z", "to_date": "2025-01-31T23:59:59Z", "use_case_type": "inbound", + "use_case_types": ["inbound"], "group_by": "use_case_id", "source": "monitoring" }' @@ -3282,6 +3712,7 @@ epilot integration-toolkit getMonitoringStatsV2 -p integrationId=123e4567-e89b-1 "error_count": 0, "warning_count": 0, "skipped_count": 0, + "info_count": 0, "ack_timeout_count": 0, "success_rate": 0, "last_error_at": "1970-01-01T00:00:00.000Z", @@ -3363,6 +3794,7 @@ epilot integration-toolkit getMonitoringTimeSeriesV2 -p integrationId=123e4567-e "error_count": 0, "warning_count": 0, "skipped_count": 0, + "info_count": 0, "total_count": 0, "breakdown": [ { @@ -3372,6 +3804,7 @@ epilot integration-toolkit getMonitoringTimeSeriesV2 -p integrationId=123e4567-e "error_count": 0, "warning_count": 0, "skipped_count": 0, + "info_count": 0, "total_count": 0 } ] @@ -3446,64 +3879,192 @@ epilot integration-toolkit getAssociatedMonitoringEvents -p integrationId=123e45 --- -### `listSecureProxies` +### `ingestExternalMonitoringEvents` -Lists all secure_proxy use cases across all integrations for the authenticated organization. +Ingest monitoring spans produced by an EXTERNAL system (e.g. an integration -`GET /v1/integrations/secure-proxies` +`POST /v2/integrations/{integrationId}/monitoring/external-events` -**Sample Call** +**Parameters** -```bash -epilot integration-toolkit listSecureProxies -``` +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `integrationId` | path | string (uuid) | Yes | The integration ID | -With JSONata filter: +**Request Body** (required) + +**Sample Call** ```bash -epilot integration-toolkit listSecureProxies --jsonata 'secure_proxies' +epilot integration-toolkit ingestExternalMonitoringEvents \ + -p integrationId=123e4567-e89b-12d3-a456-426614174000 ``` -
-Sample Response +With request body: -```json -{ - "secure_proxies": [ +```bash +epilot integration-toolkit ingestExternalMonitoringEvents \ + -p integrationId=123e4567-e89b-12d3-a456-426614174000 \ + -d '{ + "events": [ { - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "name": "string", - "slug": "string", - "enabled": true, - "vpc_mode": "static_ip", - "allowed_domains": ["string"], - "allowed_ips": ["string"], - "integration_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "integration_name": "string" + "correlation_id": "string", + "level": "string", + "use_case_slug": "string", + "occurred_at": "1970-01-01T00:00:00.000Z", + "message": "string", + "detail": {} } ] -} +}' ``` -
- ---- - -### `secureProxy` - -Routes an HTTP request through a VPC with either static IP egress or VPN secure link access. - -`POST /v1/secure-proxy` +Using positional args for path parameters: -**Request Body** (required) +```bash +epilot integration-toolkit ingestExternalMonitoringEvents 123e4567-e89b-12d3-a456-426614174000 +``` -**Sample Call** +Using stdin pipe: ```bash -epilot integration-toolkit secureProxy +cat body.json | epilot integration-toolkit ingestExternalMonitoringEvents -p integrationId=123e4567-e89b-12d3-a456-426614174000 ``` -With request body: +With JSONata filter: + +```bash +epilot integration-toolkit ingestExternalMonitoringEvents -p integrationId=123e4567-e89b-12d3-a456-426614174000 --jsonata '$' +``` + +--- + +### `getMonitoringTraceByCorrelation` + +Returns the cross-system event trace for a `correlation_id`: every monitoring + +`GET /v2/integrations/{integrationId}/monitoring/traces/{correlationId}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `integrationId` | path | string (uuid) | Yes | The integration ID | +| `correlationId` | path | string | Yes | The trace id (correlation_id) shared across systems | + +**Sample Call** + +```bash +epilot integration-toolkit getMonitoringTraceByCorrelation \ + -p integrationId=123e4567-e89b-12d3-a456-426614174000 \ + -p correlationId=123e4567-e89b-12d3-a456-426614174000 +``` + +Using positional args for path parameters: + +```bash +epilot integration-toolkit getMonitoringTraceByCorrelation 123e4567-e89b-12d3-a456-426614174000 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot integration-toolkit getMonitoringTraceByCorrelation -p integrationId=123e4567-e89b-12d3-a456-426614174000 -p correlationId=123e4567-e89b-12d3-a456-426614174000 --jsonata 'correlation_id' +``` + +
+Sample Response + +```json +{ + "correlation_id": "string", + "status": "success", + "started_at": "1970-01-01T00:00:00.000Z", + "ended_at": "1970-01-01T00:00:00.000Z", + "span_count": 0, + "truncated": true, + "spans": [ + { + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "org_id": "string", + "integration_id": "string", + "event_id": "string", + "correlation_id": "string", + "use_case_id": "string", + "use_case_type": "inbound", + "level": "success", + "code": "string", + "message": "string", + "detail": {}, + "created_at": "1970-01-01T00:00:00.000Z" + } + ], + "inbound_event": {} +} +``` + +
+ +--- + +### `listSecureProxies` + +Lists all secure_proxy use cases across all integrations for the authenticated organization. + +`GET /v1/integrations/secure-proxies` + +**Sample Call** + +```bash +epilot integration-toolkit listSecureProxies +``` + +With JSONata filter: + +```bash +epilot integration-toolkit listSecureProxies --jsonata 'secure_proxies' +``` + +
+Sample Response + +```json +{ + "secure_proxies": [ + { + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "name": "string", + "slug": "string", + "enabled": true, + "vpc_mode": "static_ip", + "allowed_domains": ["string"], + "allowed_ips": ["string"], + "integration_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "integration_name": "string" + } + ] +} +``` + +
+ +--- + +### `secureProxy` + +Routes an HTTP request through a VPC with either static IP egress or VPN secure link access. + +`POST /v1/secure-proxy` + +**Request Body** (required) + +**Sample Call** + +```bash +epilot integration-toolkit secureProxy +``` + +With request body: ```bash epilot integration-toolkit secureProxy \ @@ -3753,3 +4314,504 @@ epilot integration-toolkit commitTypes -p integrationId=123e4567-e89b-12d3-a456-
--- + +### `listErpImports` + +List recent pricing-file import jobs for the org, newest first. + +`GET /v2/erp/imports` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `limit` | query | number | No | Page size. Values above 100 are clamped to 100. | +| `cursor` | query | string | No | Opaque cursor from a prior page's `next_cursor`. | + +**Sample Call** + +```bash +epilot integration-toolkit listErpImports +``` + +With JSONata filter: + +```bash +epilot integration-toolkit listErpImports --jsonata 'results[0]' +``` + +
+Sample Response + +```json +{ + "results": [ + { + "import_id": "string", + "org_id": "string", + "created_by": "string", + "integration_id": "string", + "use_case_slug": "string", + "format": "csv", + "status": "PENDING", + "s3_input_ref": { + "bucket": "string", + "key": "string" + }, + "size_bytes": 0, + "column_count": 0, + "validation": { + "total_rows": 0, + "blocking": 0, + "warnings": 0, + "entities": {}, + "entity_details": [ + { + "mapping_index": 0, + "entity_schema": "string", + "business_key": {}, + "variants": 0, + "versions": 0 + } + ], + "entity_details_truncated": true, + "issues": [ + { + "code": "UNIQUE_ID_COLUMN_MISSING", + "severity": "warning", + "columns": [ + { + "name": "string", + "entity": "string" + } + ], + "subject": "string", + "row": 0 + } + ] + }, + "progress": { + "processed_rows": 0, + "total_rows": 0 + }, + "error": { + "code": "VALIDATION_BLOCKED", + "message": "string" + }, + "correlation_id": "string", + "activity_id": "string", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z" + } + ], + "next_cursor": "string" +} +``` + +
+ +--- + +### `createErpImport` + +Register an already-uploaded file (S3 ref) as a pricing-file import job. Returns the job and a file preview. Nothing run + +`POST /v2/erp/imports` + +**Request Body** (required) + +**Sample Call** + +```bash +epilot integration-toolkit createErpImport \ + -d '{"s3_reference":{"bucket":"string","key":"string"},"include_preview":false,"import_id":"string"}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot integration-toolkit createErpImport +``` + +With JSONata filter: + +```bash +epilot integration-toolkit createErpImport --jsonata 'job' +``` + +
+Sample Response + +```json +{ + "job": { + "import_id": "string", + "org_id": "string", + "created_by": "string", + "integration_id": "string", + "use_case_slug": "string", + "format": "csv", + "status": "PENDING", + "s3_input_ref": { + "bucket": "string", + "key": "string" + }, + "size_bytes": 0, + "column_count": 0, + "validation": { + "total_rows": 0, + "blocking": 0, + "warnings": 0, + "entities": {}, + "entity_details": [ + { + "mapping_index": 0, + "entity_schema": "string", + "business_key": {}, + "variants": 0, + "versions": 0 + } + ], + "entity_details_truncated": true, + "issues": [ + { + "code": "UNIQUE_ID_COLUMN_MISSING", + "severity": "warning", + "columns": [ + { + "name": "string", + "entity": "string" + } + ], + "subject": "string", + "row": 0 + } + ] + }, + "progress": { + "processed_rows": 0, + "total_rows": 0 + }, + "error": { + "code": "VALIDATION_BLOCKED", + "message": "string" + }, + "correlation_id": "string", + "activity_id": "string", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z" + }, + "preview": { + "columns": ["string"], + "rows": [ + ["string"] + ] + } +} +``` + +
+ +--- + +### `getErpImport` + +Get a pricing-file import job (status, counts, result links). + +`GET /v2/erp/imports/{importId}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `importId` | path | string | Yes | | + +**Sample Call** + +```bash +epilot integration-toolkit getErpImport \ + -p importId=123e4567-e89b-12d3-a456-426614174000 +``` + +Using positional args for path parameters: + +```bash +epilot integration-toolkit getErpImport 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot integration-toolkit getErpImport -p importId=123e4567-e89b-12d3-a456-426614174000 --jsonata 'import_id' +``` + +
+Sample Response + +```json +{ + "import_id": "string", + "org_id": "string", + "created_by": "string", + "integration_id": "string", + "use_case_slug": "string", + "format": "csv", + "status": "PENDING", + "s3_input_ref": { + "bucket": "string", + "key": "string" + }, + "size_bytes": 0, + "column_count": 0, + "validation": { + "total_rows": 0, + "blocking": 0, + "warnings": 0, + "entities": {}, + "entity_details": [ + { + "mapping_index": 0, + "entity_schema": "string", + "business_key": {}, + "variants": 0, + "versions": 0 + } + ], + "entity_details_truncated": true, + "issues": [ + { + "code": "UNIQUE_ID_COLUMN_MISSING", + "severity": "warning", + "columns": [ + { + "name": "string", + "entity": "string" + } + ], + "subject": "string", + "row": 0 + } + ] + }, + "progress": { + "processed_rows": 0, + "total_rows": 0 + }, + "error": { + "code": "VALIDATION_BLOCKED", + "message": "string" + }, + "correlation_id": "string", + "activity_id": "string", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z" +} +``` + +
+ +--- + +### `deleteErpImport` + +Remove an import and the file it owns. Allowed from any status: an import whose run is still in flight is stopped by the + +`DELETE /v2/erp/imports/{importId}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `importId` | path | string | Yes | | + +**Sample Call** + +```bash +epilot integration-toolkit deleteErpImport \ + -p importId=123e4567-e89b-12d3-a456-426614174000 +``` + +Using positional args for path parameters: + +```bash +epilot integration-toolkit deleteErpImport 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot integration-toolkit deleteErpImport -p importId=123e4567-e89b-12d3-a456-426614174000 --jsonata '$' +``` + +--- + +### `validateErpImport` + +Choose the use case to read this file with, and start the validate phase. + +`POST /v2/erp/imports/{importId}:validate` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `importId` | path | string | Yes | | + +**Request Body** (required) + +**Sample Call** + +```bash +epilot integration-toolkit validateErpImport \ + -p importId=123e4567-e89b-12d3-a456-426614174000 \ + -d '{"integration_id":"string","use_case_slug":"string"}' +``` + +Using positional args for path parameters: + +```bash +epilot integration-toolkit validateErpImport 123e4567-e89b-12d3-a456-426614174000 +``` + +Using stdin pipe: + +```bash +cat body.json | epilot integration-toolkit validateErpImport -p importId=123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot integration-toolkit validateErpImport -p importId=123e4567-e89b-12d3-a456-426614174000 --jsonata '$' +``` + +--- + +### `suggestErpImportUseCases` + +Rank the org's inbound use cases against this file's columns — the input to the ranked picker ("matches 6 of your 7 colu + +`POST /v2/erp/imports/{importId}:suggest-use-cases` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `importId` | path | string | Yes | | + +**Sample Call** + +```bash +epilot integration-toolkit suggestErpImportUseCases \ + -p importId=123e4567-e89b-12d3-a456-426614174000 +``` + +Using positional args for path parameters: + +```bash +epilot integration-toolkit suggestErpImportUseCases 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot integration-toolkit suggestErpImportUseCases -p importId=123e4567-e89b-12d3-a456-426614174000 --jsonata 'file_columns' +``` + +
+Sample Response + +```json +{ + "file_columns": 0, + "suggestions": [ + { + "integration_id": "string", + "integration_name": "string", + "use_case_slug": "string", + "use_case_name": "string", + "entity_types": 0, + "matched_columns": 0 + } + ] +} +``` + +
+ +--- + +### `executeErpImport` + +Confirm and run the write phase of a validated import. Only a READY job may be executed; any other status returns 409. + +`POST /v2/erp/imports/{importId}:execute` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `importId` | path | string | Yes | | + +**Request Body** + +**Sample Call** + +```bash +epilot integration-toolkit executeErpImport \ + -p importId=123e4567-e89b-12d3-a456-426614174000 \ + -d '{"ack_warnings":true}' +``` + +Using positional args for path parameters: + +```bash +epilot integration-toolkit executeErpImport 123e4567-e89b-12d3-a456-426614174000 +``` + +Using stdin pipe: + +```bash +cat body.json | epilot integration-toolkit executeErpImport -p importId=123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot integration-toolkit executeErpImport -p importId=123e4567-e89b-12d3-a456-426614174000 --jsonata '$' +``` + +--- + +### `abortErpImport` + +Ask a running import to stop. Valid while the job is VALIDATING or PROCESSING; any other status returns 409. + +`POST /v2/erp/imports/{importId}:abort` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `importId` | path | string | Yes | | + +**Sample Call** + +```bash +epilot integration-toolkit abortErpImport \ + -p importId=123e4567-e89b-12d3-a456-426614174000 +``` + +Using positional args for path parameters: + +```bash +epilot integration-toolkit abortErpImport 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot integration-toolkit abortErpImport -p importId=123e4567-e89b-12d3-a456-426614174000 --jsonata '$' +``` + +--- diff --git a/packages/cli/docs/journey.md b/packages/cli/docs/journey.md index 05720ccf7..c71c925d5 100644 --- a/packages/cli/docs/journey.md +++ b/packages/cli/docs/journey.md @@ -39,6 +39,13 @@ epilot journey getJourneysByOrgId -p id=123 - [`getJourneysByOrgId`](#getjourneysbyorgid) — Get all journeys by organization id - [`getJourney`](#getjourney) — Get journey by id. Private journeys requires valid private token to be passed - [`removeJourney`](#removejourney) — Remove journey by id +- [`listJourneyRevisions`](#listjourneyrevisions) — Lists the journey's revision history, newest first. Metadata only, no configuration payload. `is_published` says whether +- [`createJourneyRevision`](#createjourneyrevision) — Creates a revision: an immutable copy of the complete journey configuration, identified by `revision_id`. Nothing custom +- [`getJourneyRevision`](#getjourneyrevision) — Returns one revision with its full configuration, in the shape the builder holds a journey in, so it can be loaded strai +- [`publishJourneyRevision`](#publishjourneyrevision) — Makes one revision the published version, the one customers receive, in a single transaction: the revision's configurati +- [`getJourneyPublishState`](#getjourneypublishstate) — Answers which revision is the published version without paging through the history. A journey that has not adopted versi +- [`getJourneyEnvironment`](#getjourneyenvironment) — Resolve the environment variables referenced by this journey. Only browser-safe value types are returned. +- [`getJourneyEnvironmentVariables`](#getjourneyenvironmentvariables) — List the organization's environment variables that a journey block may use as an options source. Only Map variables that - [`getJourneyProducts`](#getjourneyproducts) — Get products available in the journey by id. requires public journey token to be passed. - [`createJourney`](#createjourney) — Create a Journey - [`updateJourney`](#updatejourney) — Update a Journey @@ -109,8 +116,8 @@ Get journey by id. Private journeys requires valid private token to be passed | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | | `id` | path | string (uuid) | Yes | Journey ID | -| `version` | query | number | No | DynamoDB version to fetch. `0` (default) is the live row; positive integers are historical snapshots created on each save. Note: this is distinct from the `revisions` counter on the row body. - | +| `version` | query | number | No | Revision row to read. `0` (default) is the journey record, the published version every customer receives; a positive integer `n` reads revision row `n` (used by config-engine for history reads). Disti | +| `revision_id` | query | string | No | Serve the named revision instead of the published version, so a save-and-continue session can stay on the revision it started on. Only revisions that have been published at least once are served on th | | `source` | query | string | No | What source ID. Journey or Entity ID | | `orgId` | query | string | No | Organization ID | @@ -150,6 +157,7 @@ epilot journey getJourney -p id=509cdffe-424f-457a-95c2-9708c304ce77 --jsonata ' "showStepSubtitle": true, "showStepper": true, "showStepperLabels": true, + "stepperType": "numbers", "hideNextButton": true, "name": "string", "stepId": "string", @@ -185,7 +193,8 @@ epilot journey getJourney -p id=509cdffe-424f-457a-95c2-9708c304ce77 --jsonata ' "type": "string", "paramKey": "string", "isRequired": true, - "shouldLoadEntity": true + "shouldLoadEntity": true, + "description": "string" } ], "journey_type": "Sales template (Premium)", @@ -206,6 +215,7 @@ epilot journey getJourney -p id=509cdffe-424f-457a-95c2-9708c304ce77 --jsonata ' "templateId": "string", "entityId": "string", "mappingsAutomationId": "string", + "newMappings": true, "targetedCustomer": "string", "description": "string", "organizationSettings": {}, @@ -223,20 +233,24 @@ epilot journey getJourney -p id=509cdffe-424f-457a-95c2-9708c304ce77 --jsonata ' "useAustrianLabels": true, "enableDarkMode": true, "accessMode": "PUBLIC", + "authGate": { + "stepId": "string" + }, "isPublished": true, "status": "string", "isActive": true, "savingProgress": { - "savingMode": "auto", - "supportedVersion": 0 + "mode": "auto", + "supportedRevision": 0 }, "thirdPartyCookies": true }, "validationRules": { "block1": "rule123", - "block2": { - "field1": "rule456", - "field2": "rule789" + "block2": ["rule456", "rule789"], + "block3": { + "field1": "rule101", + "field2": ["rule102", "rule103"] } }, "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], @@ -248,7 +262,8 @@ epilot journey getJourney -p id=509cdffe-424f-457a-95c2-9708c304ce77 --jsonata ' "deletedAt": "string", "version": 0, "revisions": 0, - "featureFlags": {} + "featureFlags": {}, + "revision_id": "42" } ``` @@ -289,6 +304,582 @@ epilot journey removeJourney -p id=509cdffe-424f-457a-95c2-9708c304ce77 --jsonat --- +### `listJourneyRevisions` + +Lists the journey's revision history, newest first. Metadata only, no configuration payload. `is_published` says whether + +`GET /v1/journey/configuration/{id}/revisions` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `id` | path | string (uuid) | Yes | Journey ID | +| `limit` | query | number | No | Maximum number of revisions to return | +| `cursor` | query | string | No | Opaque pagination cursor, taken from a previous response's `next_cursor` | + +**Sample Call** + +```bash +epilot journey listJourneyRevisions \ + -p id=509cdffe-424f-457a-95c2-9708c304ce77 +``` + +Using positional args for path parameters: + +```bash +epilot journey listJourneyRevisions 509cdffe-424f-457a-95c2-9708c304ce77 +``` + +With JSONata filter: + +```bash +epilot journey listJourneyRevisions -p id=509cdffe-424f-457a-95c2-9708c304ce77 --jsonata 'results[0]' +``` + +
+Sample Response + +```json +{ + "results": [ + { + "revision_id": "42", + "created_at": "1970-01-01T00:00:00.000Z", + "created_by": "string", + "name": "Summer campaign", + "description": "string", + "published_at": "1970-01-01T00:00:00.000Z", + "is_published": true, + "mapping_config_version": 12 + } + ], + "next_cursor": "string" +} +``` + +
+ +--- + +### `createJourneyRevision` + +Creates a revision: an immutable copy of the complete journey configuration, identified by `revision_id`. Nothing custom + +`POST /v1/journey/configuration/{id}/revisions` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `id` | path | string (uuid) | Yes | Journey ID | + +**Request Body** (required) + +**Sample Call** + +```bash +epilot journey createJourneyRevision \ + -p id=509cdffe-424f-457a-95c2-9708c304ce77 +``` + +With request body: + +```bash +epilot journey createJourneyRevision \ + -p id=509cdffe-424f-457a-95c2-9708c304ce77 \ + -d '{ + "journeyId": "string", + "organizationId": "string", + "brandId": "string", + "name": "string", + "steps": [ + { + "showStepName": true, + "title": "string", + "subTitle": "string", + "showStepSubtitle": true, + "showStepper": true, + "showStepperLabels": true, + "stepperType": "numbers", + "hideNextButton": true, + "name": "string", + "stepId": "string", + "schema": {}, + "uischema": {}, + "maxWidth": "small" + } + ], + "design": { + "logoUrl": "string", + "theme": {}, + "designTokens": {} + }, + "rules": [ + { + "type": "inject", + "sourceType": "journey", + "source": "string", + "target": "string" + } + ], + "logics": [ + { + "autoGeneratedId": "string", + "conditions": ["string"], + "actions": ["string"] + } + ], + "logicsV4": {}, + "contextSchema": [ + { + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type": "string", + "paramKey": "string", + "isRequired": true, + "shouldLoadEntity": true, + "description": "string" + } + ], + "journey_type": "Sales template (Premium)", + "protected": true, + "protectedEditable": ["string"], + "settings": { + "embedOptions": { + "mode": "full-screen", + "lang": "de", + "width": "string", + "topBar": true, + "scrollToTop": true, + "button": {} + }, + "safeModeAutomation": true, + "canary": true, + "designId": "string", + "templateId": "string", + "entityId": "string", + "mappingsAutomationId": "string", + "newMappings": true, + "targetedCustomer": "string", + "description": "string", + "organizationSettings": {}, + "publicToken": "string", + "runtimeEntities": ["ORDER"], + "filePurposes": ["string"], + "entityTags": ["string"], + "addressSuggestionsFileUrl": "string", + "addressSuggestionsFileId": "string", + "addressSuggestionsCountryCode": "string", + "addressSuggestionsEnableAutoComplete": true, + "addressSuggestionsSource": ["string"], + "addressSuggestionsEnableFreeText": true, + "useNewDesign": true, + "useAustrianLabels": true, + "enableDarkMode": true, + "accessMode": "PUBLIC", + "authGate": { + "stepId": "string" + }, + "isPublished": true, + "status": "string", + "isActive": true, + "savingProgress": { + "mode": "auto", + "supportedRevision": 0 + }, + "thirdPartyCookies": true + }, + "validationRules": { + "block1": "rule123", + "block2": ["rule456", "rule789"], + "block3": { + "field1": "rule101", + "field2": ["rule102", "rule103"] + } + }, + "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], + "createdBy": "string", + "updatedBy": "string", + "__lastModifiedAt": "string", + "parent_revision_id": "41", + "based_on_revision_id": "37", + "mapping_config_version": 12, + "revision_name": "Changes to Steps and Logic" +}' +``` + +Using positional args for path parameters: + +```bash +epilot journey createJourneyRevision 509cdffe-424f-457a-95c2-9708c304ce77 +``` + +Using stdin pipe: + +```bash +cat body.json | epilot journey createJourneyRevision -p id=509cdffe-424f-457a-95c2-9708c304ce77 +``` + +With JSONata filter: + +```bash +epilot journey createJourneyRevision -p id=509cdffe-424f-457a-95c2-9708c304ce77 --jsonata 'revision_id' +``` + +
+Sample Response + +```json +{ + "revision_id": "42", + "created_at": "1970-01-01T00:00:00.000Z", + "created_by": "string", + "name": "Summer campaign", + "description": "string", + "published_at": "1970-01-01T00:00:00.000Z", + "is_published": true, + "mapping_config_version": 12 +} +``` + +
+ +--- + +### `getJourneyRevision` + +Returns one revision with its full configuration, in the shape the builder holds a journey in, so it can be loaded strai + +`GET /v1/journey/configuration/{id}/revisions/{revision_id}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `id` | path | string (uuid) | Yes | Journey ID | +| `revision_id` | path | string | Yes | Revision ID, as returned in a revision summary | + +**Sample Call** + +```bash +epilot journey getJourneyRevision \ + -p id=509cdffe-424f-457a-95c2-9708c304ce77 \ + -p revision_id=42 +``` + +Using positional args for path parameters: + +```bash +epilot journey getJourneyRevision 509cdffe-424f-457a-95c2-9708c304ce77 42 +``` + +With JSONata filter: + +```bash +epilot journey getJourneyRevision -p id=509cdffe-424f-457a-95c2-9708c304ce77 -p revision_id=42 --jsonata '$' +``` + +
+Sample Response + +```json +{ + "revision_id": "42", + "created_at": "1970-01-01T00:00:00.000Z", + "created_by": "string", + "name": "Summer campaign", + "description": "string", + "published_at": "1970-01-01T00:00:00.000Z", + "is_published": true, + "mapping_config_version": 12, + "configuration": { + "journeyId": "string", + "organizationId": "string", + "brandId": "string", + "name": "string", + "steps": [ + {} + ], + "design": { + "logoUrl": "string", + "theme": {}, + "designTokens": {} + }, + "rules": [ + {} + ], + "logics": [ + {} + ], + "logicsV4": {}, + "contextSchema": [ + {} + ], + "journey_type": "Sales template (Premium)", + "protected": true, + "protectedEditable": ["string"], + "settings": { + "embedOptions": {}, + "safeModeAutomation": true, + "canary": true, + "designId": "string", + "templateId": "string", + "entityId": "string", + "mappingsAutomationId": "string", + "newMappings": true, + "targetedCustomer": "string", + "description": "string", + "organizationSettings": {}, + "publicToken": "string", + "runtimeEntities": ["ORDER"], + "filePurposes": ["string"], + "entityTags": ["string"], + "addressSuggestionsFileUrl": "string", + "addressSuggestionsFileId": "string", + "addressSuggestionsCountryCode": "string", + "addressSuggestionsEnableAutoComplete": true, + "addressSuggestionsSource": ["string"], + "addressSuggestionsEnableFreeText": true, + "useNewDesign": true, + "useAustrianLabels": true, + "enableDarkMode": true, + "accessMode": "PUBLIC", + "authGate": {}, + "isPublished": true, + "status": "string", + "isActive": true, + "savingProgress": {}, + "thirdPartyCookies": true + }, + "validationRules": { + "block1": "rule123", + "block2": ["rule456", "rule789"], + "block3": {} + }, + "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], + "createdBy": "string", + "updatedBy": "string", + "__lastModifiedAt": "string", + "createdAt": "string", + "lastModifiedAt": "string", + "deletedAt": "string", + "version": 0, + "revisions": 0, + "featureFlags": {} + } +} +``` + +
+ +--- + +### `publishJourneyRevision` + +Makes one revision the published version, the one customers receive, in a single transaction: the revision's configurati + +`POST /v1/journey/configuration/{id}/publish` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `id` | path | string (uuid) | Yes | Journey ID | + +**Request Body** (required) + +**Sample Call** + +```bash +epilot journey publishJourneyRevision \ + -p id=509cdffe-424f-457a-95c2-9708c304ce77 \ + -d '{"revision_id":"42","name":"Summer campaign","description":"string"}' +``` + +Using positional args for path parameters: + +```bash +epilot journey publishJourneyRevision 509cdffe-424f-457a-95c2-9708c304ce77 +``` + +Using stdin pipe: + +```bash +cat body.json | epilot journey publishJourneyRevision -p id=509cdffe-424f-457a-95c2-9708c304ce77 +``` + +With JSONata filter: + +```bash +epilot journey publishJourneyRevision -p id=509cdffe-424f-457a-95c2-9708c304ce77 --jsonata 'revision_id' +``` + +
+Sample Response + +```json +{ + "revision_id": "42", + "name": "Summer campaign", + "published_at": "1970-01-01T00:00:00.000Z", + "published_by": "string", + "post_publish_warnings": ["string"] +} +``` + +
+ +--- + +### `getJourneyPublishState` + +Answers which revision is the published version without paging through the history. A journey that has not adopted versi + +`GET /v1/journey/configuration/{id}/publish-state` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `id` | path | string (uuid) | Yes | Journey ID | + +**Sample Call** + +```bash +epilot journey getJourneyPublishState \ + -p id=509cdffe-424f-457a-95c2-9708c304ce77 +``` + +Using positional args for path parameters: + +```bash +epilot journey getJourneyPublishState 509cdffe-424f-457a-95c2-9708c304ce77 +``` + +With JSONata filter: + +```bash +epilot journey getJourneyPublishState -p id=509cdffe-424f-457a-95c2-9708c304ce77 --jsonata 'journey_id' +``` + +
+Sample Response + +```json +{ + "journey_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "published_revision_id": "42", + "published_at": "1970-01-01T00:00:00.000Z", + "published_by": "string" +} +``` + +
+ +--- + +### `getJourneyEnvironment` + +Resolve the environment variables referenced by this journey. Only browser-safe value types are returned. + +`GET /v1/journey/configuration/{id}/environment` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `id` | path | string (uuid) | Yes | Journey ID bound to the supplied journey access token | + +**Sample Call** + +```bash +epilot journey getJourneyEnvironment \ + -p id=123e4567-e89b-12d3-a456-426614174000 +``` + +Using positional args for path parameters: + +```bash +epilot journey getJourneyEnvironment 123e4567-e89b-12d3-a456-426614174000 +``` + +With JSONata filter: + +```bash +epilot journey getJourneyEnvironment -p id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'items[0]' +``` + +
+Sample Response + +```json +{ + "items": [ + { + "datasourceId": "string", + "type": "Text", + "value": "string" + } + ], + "errors": [ + { + "datasourceId": "string", + "code": "not_found" + } + ] +} +``` + +
+ +--- + +### `getJourneyEnvironmentVariables` + +List the organization's environment variables that a journey block may use as an options source. Only Map variables that + +`GET /v1/journey/environment-variables` + +**Sample Call** + +```bash +epilot journey getJourneyEnvironmentVariables +``` + +With JSONata filter: + +```bash +epilot journey getJourneyEnvironmentVariables --jsonata 'items[0]' +``` + +
+Sample Response + +```json +{ + "items": [ + { + "key": "string", + "type": "Map", + "value": { + "fallbackLanguage": "de", + "options": [ + { + "key": "string", + "value": "string" + } + ] + }, + "description": "string" + } + ] +} +``` + +
+ +--- + ### `getJourneyProducts` Get products available in the journey by id. requires public journey token to be passed. @@ -395,6 +986,7 @@ epilot journey createJourney \ "showStepSubtitle": true, "showStepper": true, "showStepperLabels": true, + "stepperType": "numbers", "hideNextButton": true, "name": "string", "stepId": "string", @@ -430,7 +1022,8 @@ epilot journey createJourney \ "type": "string", "paramKey": "string", "isRequired": true, - "shouldLoadEntity": true + "shouldLoadEntity": true, + "description": "string" } ], "journey_type": "Sales template (Premium)", @@ -451,6 +1044,7 @@ epilot journey createJourney \ "templateId": "string", "entityId": "string", "mappingsAutomationId": "string", + "newMappings": true, "targetedCustomer": "string", "description": "string", "organizationSettings": {}, @@ -468,20 +1062,24 @@ epilot journey createJourney \ "useAustrianLabels": true, "enableDarkMode": true, "accessMode": "PUBLIC", + "authGate": { + "stepId": "string" + }, "isPublished": true, "status": "string", "isActive": true, "savingProgress": { - "savingMode": "auto", - "supportedVersion": 0 + "mode": "auto", + "supportedRevision": 0 }, "thirdPartyCookies": true }, "validationRules": { "block1": "rule123", - "block2": { - "field1": "rule456", - "field2": "rule789" + "block2": ["rule456", "rule789"], + "block3": { + "field1": "rule101", + "field2": ["rule102", "rule103"] } }, "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], @@ -520,6 +1118,7 @@ epilot journey createJourney --jsonata '$' "showStepSubtitle": true, "showStepper": true, "showStepperLabels": true, + "stepperType": "numbers", "hideNextButton": true, "name": "string", "stepId": "string", @@ -555,7 +1154,8 @@ epilot journey createJourney --jsonata '$' "type": "string", "paramKey": "string", "isRequired": true, - "shouldLoadEntity": true + "shouldLoadEntity": true, + "description": "string" } ], "journey_type": "Sales template (Premium)", @@ -576,6 +1176,7 @@ epilot journey createJourney --jsonata '$' "templateId": "string", "entityId": "string", "mappingsAutomationId": "string", + "newMappings": true, "targetedCustomer": "string", "description": "string", "organizationSettings": {}, @@ -593,20 +1194,24 @@ epilot journey createJourney --jsonata '$' "useAustrianLabels": true, "enableDarkMode": true, "accessMode": "PUBLIC", + "authGate": { + "stepId": "string" + }, "isPublished": true, "status": "string", "isActive": true, "savingProgress": { - "savingMode": "auto", - "supportedVersion": 0 + "mode": "auto", + "supportedRevision": 0 }, "thirdPartyCookies": true }, "validationRules": { "block1": "rule123", - "block2": { - "field1": "rule456", - "field2": "rule789" + "block2": ["rule456", "rule789"], + "block3": { + "field1": "rule101", + "field2": ["rule102", "rule103"] } }, "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], @@ -657,6 +1262,7 @@ epilot journey updateJourney \ "showStepSubtitle": true, "showStepper": true, "showStepperLabels": true, + "stepperType": "numbers", "hideNextButton": true, "name": "string", "stepId": "string", @@ -692,7 +1298,8 @@ epilot journey updateJourney \ "type": "string", "paramKey": "string", "isRequired": true, - "shouldLoadEntity": true + "shouldLoadEntity": true, + "description": "string" } ], "journey_type": "Sales template (Premium)", @@ -713,6 +1320,7 @@ epilot journey updateJourney \ "templateId": "string", "entityId": "string", "mappingsAutomationId": "string", + "newMappings": true, "targetedCustomer": "string", "description": "string", "organizationSettings": {}, @@ -730,20 +1338,24 @@ epilot journey updateJourney \ "useAustrianLabels": true, "enableDarkMode": true, "accessMode": "PUBLIC", + "authGate": { + "stepId": "string" + }, "isPublished": true, "status": "string", "isActive": true, "savingProgress": { - "savingMode": "auto", - "supportedVersion": 0 + "mode": "auto", + "supportedRevision": 0 }, "thirdPartyCookies": true }, "validationRules": { "block1": "rule123", - "block2": { - "field1": "rule456", - "field2": "rule789" + "block2": ["rule456", "rule789"], + "block3": { + "field1": "rule101", + "field2": ["rule102", "rule103"] } }, "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], @@ -833,6 +1445,7 @@ epilot journey patchUpdateJourney --jsonata 'createdJourney' "templateId": "string", "entityId": "string", "mappingsAutomationId": "string", + "newMappings": true, "targetedCustomer": "string", "description": "string", "organizationSettings": {}, @@ -850,6 +1463,7 @@ epilot journey patchUpdateJourney --jsonata 'createdJourney' "useAustrianLabels": true, "enableDarkMode": true, "accessMode": "PUBLIC", + "authGate": {}, "isPublished": true, "status": "string", "isActive": true, @@ -858,7 +1472,8 @@ epilot journey patchUpdateJourney --jsonata 'createdJourney' }, "validationRules": { "block1": "rule123", - "block2": {} + "block2": ["rule456", "rule789"], + "block3": {} }, "_manifest": ["123e4567-e89b-12d3-a456-426614174000"], "createdBy": "string", @@ -950,9 +1565,22 @@ Builds document generated from a template with journey values." **Sample Call** +```bash +epilot journey generateDocument +``` + +With request body: + ```bash epilot journey generateDocument \ - -d '{"file_id":"1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p","context_data":{"additionalProperties":"string"},"language":"de"}' + -d '{ + "file_id": "1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p", + "context_data": { + "additionalProperties": "string" + }, + "context_entity_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "language": "de" +}' ``` Using stdin pipe: @@ -1055,6 +1683,7 @@ epilot journey createJourneyV2 \ "showStepSubtitle": true, "showStepper": true, "showStepperLabels": true, + "stepperType": "numbers", "hideNextButton": true, "name": "string", "stepId": "string", @@ -1090,7 +1719,8 @@ epilot journey createJourneyV2 \ "type": "string", "paramKey": "string", "isRequired": true, - "shouldLoadEntity": true + "shouldLoadEntity": true, + "description": "string" } ], "journey_type": "Sales template (Premium)", @@ -1109,6 +1739,7 @@ epilot journey createJourneyV2 \ "designId": "string", "entityId": "string", "mappingsAutomationId": "string", + "newMappings": true, "templateId": "string", "targetedCustomer": "string", "description": "string", @@ -1125,14 +1756,23 @@ epilot journey createJourneyV2 \ "useNewDesign": true, "thirdPartyCookies": true, "accessMode": "PUBLIC", + "authGate": { + "stepId": "string" + }, "enableDarkMode": true, - "isActive": true + "useAustrianLabels": true, + "isActive": true, + "savingProgress": { + "mode": "auto", + "supportedRevision": 0 + } }, "validationRules": { "block1": "rule123", - "block2": { - "field1": "rule456", - "field2": "rule789" + "block2": ["rule456", "rule789"], + "block3": { + "field1": "rule101", + "field2": ["rule102", "rule103"] } }, "_manifest": ["123e4567-e89b-12d3-a456-426614174000"] @@ -1167,6 +1807,7 @@ epilot journey createJourneyV2 --jsonata 'journeyId' "showStepSubtitle": true, "showStepper": true, "showStepperLabels": true, + "stepperType": "numbers", "hideNextButton": true, "name": "string", "stepId": "string", @@ -1202,7 +1843,8 @@ epilot journey createJourneyV2 --jsonata 'journeyId' "type": "string", "paramKey": "string", "isRequired": true, - "shouldLoadEntity": true + "shouldLoadEntity": true, + "description": "string" } ], "journey_type": "Sales template (Premium)", @@ -1221,6 +1863,7 @@ epilot journey createJourneyV2 --jsonata 'journeyId' "designId": "string", "entityId": "string", "mappingsAutomationId": "string", + "newMappings": true, "templateId": "string", "targetedCustomer": "string", "description": "string", @@ -1237,14 +1880,23 @@ epilot journey createJourneyV2 --jsonata 'journeyId' "useNewDesign": true, "thirdPartyCookies": true, "accessMode": "PUBLIC", + "authGate": { + "stepId": "string" + }, "enableDarkMode": true, - "isActive": true + "useAustrianLabels": true, + "isActive": true, + "savingProgress": { + "mode": "auto", + "supportedRevision": 0 + } }, "validationRules": { "block1": "rule123", - "block2": { - "field1": "rule456", - "field2": "rule789" + "block2": ["rule456", "rule789"], + "block3": { + "field1": "rule101", + "field2": ["rule102", "rule103"] } }, "_manifest": ["123e4567-e89b-12d3-a456-426614174000"] @@ -1285,6 +1937,7 @@ epilot journey updateJourneyV2 \ "showStepSubtitle": true, "showStepper": true, "showStepperLabels": true, + "stepperType": "numbers", "hideNextButton": true, "name": "string", "stepId": "string", @@ -1320,7 +1973,8 @@ epilot journey updateJourneyV2 \ "type": "string", "paramKey": "string", "isRequired": true, - "shouldLoadEntity": true + "shouldLoadEntity": true, + "description": "string" } ], "journey_type": "Sales template (Premium)", @@ -1339,6 +1993,7 @@ epilot journey updateJourneyV2 \ "designId": "string", "entityId": "string", "mappingsAutomationId": "string", + "newMappings": true, "templateId": "string", "targetedCustomer": "string", "description": "string", @@ -1355,14 +2010,23 @@ epilot journey updateJourneyV2 \ "useNewDesign": true, "thirdPartyCookies": true, "accessMode": "PUBLIC", + "authGate": { + "stepId": "string" + }, "enableDarkMode": true, - "isActive": true + "useAustrianLabels": true, + "isActive": true, + "savingProgress": { + "mode": "auto", + "supportedRevision": 0 + } }, "validationRules": { "block1": "rule123", - "block2": { - "field1": "rule456", - "field2": "rule789" + "block2": ["rule456", "rule789"], + "block3": { + "field1": "rule101", + "field2": ["rule102", "rule103"] } }, "_manifest": ["123e4567-e89b-12d3-a456-426614174000"] @@ -1397,6 +2061,7 @@ epilot journey updateJourneyV2 --jsonata 'journeyId' "showStepSubtitle": true, "showStepper": true, "showStepperLabels": true, + "stepperType": "numbers", "hideNextButton": true, "name": "string", "stepId": "string", @@ -1432,7 +2097,8 @@ epilot journey updateJourneyV2 --jsonata 'journeyId' "type": "string", "paramKey": "string", "isRequired": true, - "shouldLoadEntity": true + "shouldLoadEntity": true, + "description": "string" } ], "journey_type": "Sales template (Premium)", @@ -1451,6 +2117,7 @@ epilot journey updateJourneyV2 --jsonata 'journeyId' "designId": "string", "entityId": "string", "mappingsAutomationId": "string", + "newMappings": true, "templateId": "string", "targetedCustomer": "string", "description": "string", @@ -1467,14 +2134,23 @@ epilot journey updateJourneyV2 --jsonata 'journeyId' "useNewDesign": true, "thirdPartyCookies": true, "accessMode": "PUBLIC", + "authGate": { + "stepId": "string" + }, "enableDarkMode": true, - "isActive": true + "useAustrianLabels": true, + "isActive": true, + "savingProgress": { + "mode": "auto", + "supportedRevision": 0 + } }, "validationRules": { "block1": "rule123", - "block2": { - "field1": "rule456", - "field2": "rule789" + "block2": ["rule456", "rule789"], + "block3": { + "field1": "rule101", + "field2": ["rule102", "rule103"] } }, "_manifest": ["123e4567-e89b-12d3-a456-426614174000"] @@ -1528,6 +2204,7 @@ epilot journey patchUpdateJourneyV2 --jsonata 'journeyId' "showStepSubtitle": true, "showStepper": true, "showStepperLabels": true, + "stepperType": "numbers", "hideNextButton": true, "name": "string", "stepId": "string", @@ -1563,7 +2240,8 @@ epilot journey patchUpdateJourneyV2 --jsonata 'journeyId' "type": "string", "paramKey": "string", "isRequired": true, - "shouldLoadEntity": true + "shouldLoadEntity": true, + "description": "string" } ], "journey_type": "Sales template (Premium)", @@ -1582,6 +2260,7 @@ epilot journey patchUpdateJourneyV2 --jsonata 'journeyId' "designId": "string", "entityId": "string", "mappingsAutomationId": "string", + "newMappings": true, "templateId": "string", "targetedCustomer": "string", "description": "string", @@ -1598,14 +2277,23 @@ epilot journey patchUpdateJourneyV2 --jsonata 'journeyId' "useNewDesign": true, "thirdPartyCookies": true, "accessMode": "PUBLIC", + "authGate": { + "stepId": "string" + }, "enableDarkMode": true, - "isActive": true + "useAustrianLabels": true, + "isActive": true, + "savingProgress": { + "mode": "auto", + "supportedRevision": 0 + } }, "validationRules": { "block1": "rule123", - "block2": { - "field1": "rule456", - "field2": "rule789" + "block2": ["rule456", "rule789"], + "block3": { + "field1": "rule101", + "field2": ["rule102", "rule103"] } }, "_manifest": ["123e4567-e89b-12d3-a456-426614174000"] @@ -1627,8 +2315,7 @@ Get journey by id | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | | `id` | path | string (uuid) | Yes | Journey ID | -| `version` | query | number | No | DynamoDB version to fetch. `0` (default) is the live row; positive integers are historical snapshots created on each save. Note: this is distinct from the `revisions` counter on the row body. - | +| `version` | query | number | No | Revision row to read. `0` (default) is the journey record, the published version every customer receives; a positive integer `n` reads revision row `n` (used by config-engine for history reads). Disti | **Sample Call** @@ -1646,7 +2333,7 @@ epilot journey getJourneyV2 509cdffe-424f-457a-95c2-9708c304ce77 With JSONata filter: ```bash -epilot journey getJourneyV2 -p id=509cdffe-424f-457a-95c2-9708c304ce77 --jsonata 'journeyId' +epilot journey getJourneyV2 -p id=509cdffe-424f-457a-95c2-9708c304ce77 --jsonata '$' ```
@@ -1665,6 +2352,7 @@ epilot journey getJourneyV2 -p id=509cdffe-424f-457a-95c2-9708c304ce77 --jsonata "showStepSubtitle": true, "showStepper": true, "showStepperLabels": true, + "stepperType": "numbers", "hideNextButton": true, "name": "string", "stepId": "string", @@ -1700,7 +2388,8 @@ epilot journey getJourneyV2 -p id=509cdffe-424f-457a-95c2-9708c304ce77 --jsonata "type": "string", "paramKey": "string", "isRequired": true, - "shouldLoadEntity": true + "shouldLoadEntity": true, + "description": "string" } ], "journey_type": "Sales template (Premium)", @@ -1719,6 +2408,7 @@ epilot journey getJourneyV2 -p id=509cdffe-424f-457a-95c2-9708c304ce77 --jsonata "designId": "string", "entityId": "string", "mappingsAutomationId": "string", + "newMappings": true, "templateId": "string", "targetedCustomer": "string", "description": "string", @@ -1735,14 +2425,23 @@ epilot journey getJourneyV2 -p id=509cdffe-424f-457a-95c2-9708c304ce77 --jsonata "useNewDesign": true, "thirdPartyCookies": true, "accessMode": "PUBLIC", + "authGate": { + "stepId": "string" + }, "enableDarkMode": true, - "isActive": true + "useAustrianLabels": true, + "isActive": true, + "savingProgress": { + "mode": "auto", + "supportedRevision": 0 + } }, "validationRules": { "block1": "rule123", - "block2": { - "field1": "rule456", - "field2": "rule789" + "block2": ["rule456", "rule789"], + "block3": { + "field1": "rule101", + "field2": ["rule102", "rule103"] } }, "_manifest": ["123e4567-e89b-12d3-a456-426614174000"] diff --git a/packages/cli/docs/message.md b/packages/cli/docs/message.md index bc552acf2..8de4d9ade 100644 --- a/packages/cli/docs/message.md +++ b/packages/cli/docs/message.md @@ -48,6 +48,7 @@ epilot message sendMessage - [`markReadMessageV2`](#markreadmessagev2) — Mark message as read within a scope - [`markUnreadMessage`](#markunreadmessage) — Mark message as unread - [`getUnread`](#getunread) — Get all unread messages by actor +- [`getUnreadCounts`](#getunreadcounts) — Unread counts for several named scopes in one request. - [`markUnreadMessageV2`](#markunreadmessagev2) — Mark message as unread within a scope - [`spamMessage`](#spammessage) — Mark a single message as spam. Also marks the parent thread as spam if all messages in the thread are spam. - [`unspamMessage`](#unspammessage) — Remove spam marking from a single message. Also removes spam from the parent thread if no other messages are spam. @@ -57,6 +58,7 @@ epilot message sendMessage - [`searchThreads`](#searchthreads) — Search for threads of email messages. - [`searchThreadsV2`](#searchthreadsv2) — Search for threads of email messages. - [`searchIds`](#searchids) — Return all thread id's that match a criteria +- [`getAssigneeWorkload`](#getassigneeworkload) — Return the open-thread workload for a set of user ids. - [`updateThread`](#updatethread) — Modify thread metadata - [`deleteThread`](#deletethread) — Immediately and permanently delete a thread. This operation cannot be undone. - [`moveThread`](#movethread) — Move thread to a different Inbox @@ -169,6 +171,7 @@ epilot message updateMessage --jsonata '$' "_updated_at": "2021-02-10T09:14:31.990Z", "message_id": "<0102017b97a502f8-a67f01c2-68cc-4928-b91b-45853f34e259-000000@eu-west-1.amazonses.com>", "sender": "206801", + "updated_by": "206801", "subject": "Request for solar panel price", "html": "
We at ABC GmbH would like to request a price quote for the solar panel.
", "text": "We at ABC GmbH would like to request a price quote for the solar panel.", @@ -222,7 +225,8 @@ epilot message updateMessage --jsonata '$' "may_be_signature_attachment": true, "cid": "fb222496-a1a5-4639-94f2-07b5e35e4068", "inline": false, - "send_as_link": false + "send_as_link": false, + "save_to_entity": true } ] }, @@ -285,6 +289,7 @@ epilot message getMessage -p id=4d74976d-fb64-47fd-85e2-65eea140f5eb --jsonata ' "_updated_at": "2021-02-10T09:14:31.990Z", "message_id": "<0102017b97a502f8-a67f01c2-68cc-4928-b91b-45853f34e259-000000@eu-west-1.amazonses.com>", "sender": "206801", + "updated_by": "206801", "subject": "Request for solar panel price", "html": "
We at ABC GmbH would like to request a price quote for the solar panel.
", "text": "We at ABC GmbH would like to request a price quote for the solar panel.", @@ -338,7 +343,8 @@ epilot message getMessage -p id=4d74976d-fb64-47fd-85e2-65eea140f5eb --jsonata ' "may_be_signature_attachment": true, "cid": "fb222496-a1a5-4639-94f2-07b5e35e4068", "inline": false, - "send_as_link": false + "send_as_link": false, + "save_to_entity": true } ] }, @@ -443,6 +449,24 @@ epilot message searchMessages \ -d '{ "inbox_id": "3f34ce73-089c-4d45-a5ee-c161234e41c3", "q": "subject:\"Request for solar panel price\" AND _tags:INBOX", + "view": { + "folder": "inbox", + "mailbox": "organization", + "labels": ["string"], + "purposes": ["string"], + "filters": ["unread"], + "from": ["string"], + "to": ["string"], + "assigned_to": ["string"], + "include_unassigned": true, + "date_from_days_ago": 0, + "date_to_days_ago": 0, + "email_filter": ["string"], + "text": "string", + "thread_ids": ["string"], + "pinned_by": "string" + }, + "user_groups": ["string"], "fields": ["_id", "_title", "first_name", "account", "!account.*._files", "**._product"], "from": 0, "size": 10, @@ -482,63 +506,16 @@ epilot message searchMessages --jsonata 'results[0]' "_updated_at": "2021-02-10T09:14:31.990Z", "message_id": "<0102017b97a502f8-a67f01c2-68cc-4928-b91b-45853f34e259-000000@eu-west-1.amazonses.com>", "sender": "206801", + "updated_by": "206801", "subject": "Request for solar panel price", "html": "
We at ABC GmbH would like to request a price quote for the solar panel.
", "text": "We at ABC GmbH would like to request a price quote for the solar panel.", - "from": { - "name": "epilot", - "address": "messaging@epilot.cloud", - "email_type": "INTERNAL", - "send_status": "SEND", - "send_error": {} - }, - "reply_to": { - "name": "epilot", - "address": "messaging@epilot.cloud", - "email_type": "INTERNAL", - "send_status": "SEND", - "send_error": {} - }, - "to": [ - { - "name": "epilot", - "address": "messaging@epilot.cloud", - "email_type": "INTERNAL", - "send_status": "SEND", - "send_error": {} - } - ], - "cc": [ - { - "name": "epilot", - "address": "messaging@epilot.cloud", - "email_type": "INTERNAL", - "send_status": "SEND", - "send_error": {} - } - ], - "bcc": [ - { - "name": "epilot", - "address": "messaging@epilot.cloud", - "email_type": "INTERNAL", - "send_status": "SEND", - "send_error": {} - } - ], - "file": { - "$relation": [ - { - "entity_id": "f820ce3b-07b0-45ae-bcc6-babb2f53f79f", - "filename": "Produktinformationen_epilot360_Double_Opt_in.pdf", - "is_message_attachment": true, - "may_be_signature_attachment": true, - "cid": "fb222496-a1a5-4639-94f2-07b5e35e4068", - "inline": false, - "send_as_link": false - } - ] - }, + "from": {}, + "reply_to": {}, + "to": [], + "cc": [], + "bcc": [], + "file": {}, "references": "<0102017b97a502f8-a67f01c2-68cc-4928-b91b-45853f34e259-000000@eu-west-1.amazonses.com> ", "in_reply_to": "", "user_read_message": ["206801", "200109"], @@ -778,6 +755,67 @@ epilot message getUnread -p actor=example --jsonata 'count' --- +### `getUnreadCounts` + +Unread counts for several named scopes in one request. + +`POST /v1/message/unread:counts` + +**Request Body** (required) + +**Sample Call** + +```bash +epilot message getUnreadCounts +``` + +With request body: + +```bash +epilot message getUnreadCounts \ + -d '{ + "actor": "organization", + "email_filter": ["string"], + "user_groups": ["string"], + "scopes": [ + { + "name": "inbox-support", + "type": "organization", + "q": "_tags.keyword:inbox AND !_tags.keyword:trash", + "view_id": "3f34ce73-089c-4d45-a5ee-c161234e41c3", + "inbox_id": "3f34ce73-089c-4d45-a5ee-c161234e41c3" + } + ] +}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot message getUnreadCounts +``` + +With JSONata filter: + +```bash +epilot message getUnreadCounts --jsonata 'enabled' +``` + +
+Sample Response + +```json +{ + "enabled": true, + "counts": {}, + "omitted": ["string"] +} +``` + +
+ +--- + ### `markUnreadMessageV2` Mark message as unread within a scope @@ -899,6 +937,24 @@ epilot message searchThreadsV2 \ -d '{ "inbox_id": "3f34ce73-089c-4d45-a5ee-c161234e41c3", "q": "subject:\"Request for solar panel price\" AND _tags:INBOX", + "view": { + "folder": "inbox", + "mailbox": "organization", + "labels": ["string"], + "purposes": ["string"], + "filters": ["unread"], + "from": ["string"], + "to": ["string"], + "assigned_to": ["string"], + "include_unassigned": true, + "date_from_days_ago": 0, + "date_to_days_ago": 0, + "email_filter": ["string"], + "text": "string", + "thread_ids": ["string"], + "pinned_by": "string" + }, + "user_groups": ["string"], "fields": ["_id", "_title", "first_name", "account", "!account.*._files", "**._product"], "from": 0, "size": 10, @@ -962,9 +1018,36 @@ Return all thread id's that match a criteria **Sample Call** +```bash +epilot message searchIds +``` + +With request body: + ```bash epilot message searchIds \ - -d '{"q":"subject:\"Request for solar panel price\" AND _tags:INBOX"}' + -d '{ + "view": { + "folder": "inbox", + "mailbox": "organization", + "labels": ["string"], + "purposes": ["string"], + "filters": ["unread"], + "from": ["string"], + "to": ["string"], + "assigned_to": ["string"], + "include_unassigned": true, + "date_from_days_ago": 0, + "date_to_days_ago": 0, + "email_filter": ["string"], + "text": "string", + "thread_ids": ["string"], + "pinned_by": "string" + }, + "user_groups": ["string"], + "inbox_id": "3f34ce73-089c-4d45-a5ee-c161234e41c3", + "q": "subject:\"Request for solar panel price\" AND _tags:INBOX" +}' ``` Using stdin pipe: @@ -993,6 +1076,51 @@ epilot message searchIds --jsonata 'results[0]' --- +### `getAssigneeWorkload` + +Return the open-thread workload for a set of user ids. + +`POST /v2/message/threads:workload` + +**Request Body** (required) + +**Sample Call** + +```bash +epilot message getAssigneeWorkload \ + -d '{"user_ids":["11000902","11001053"]}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot message getAssigneeWorkload +``` + +With JSONata filter: + +```bash +epilot message getAssigneeWorkload --jsonata 'workloads' +``` + +
+Sample Response + +```json +{ + "workloads": [ + { + "user_id": "11000902", + "open_threads": 2 + } + ] +} +``` + +
+ +--- + ### `updateThread` Modify thread metadata @@ -1030,6 +1158,7 @@ epilot message updateThread --jsonata '$' "latest_message": { "message_id": "<0102017b97a502f8-a67f01c2-68cc-4928-b91b-45853f34e259-000000@eu-west-1.amazonses.com>", "sender": "206801", + "updated_by": "206801", "subject": "Request for solar panel price", "html": "
We at ABC GmbH would like to request a price quote for the solar panel.
", "text": "We at ABC GmbH would like to request a price quote for the solar panel.", @@ -1070,6 +1199,7 @@ epilot message updateThread --jsonata '$' "latest_trash_message": { "message_id": "<0102017b97a502f8-a67f01c2-68cc-4928-b91b-45853f34e259-000000@eu-west-1.amazonses.com>", "sender": "206801", + "updated_by": "206801", "subject": "Request for solar panel price", "html": "
We at ABC GmbH would like to request a price quote for the solar panel.
", "text": "We at ABC GmbH would like to request a price quote for the solar panel.", @@ -1294,12 +1424,24 @@ epilot message getThreadTimeline -p id=123e4567-e89b-12d3-a456-426614174000 --js { "events": [ { + "id": "string", "data": { "type": "THREAD_DONE", "user_id": "123", "organization_id": "456" }, - "timestamp": "2024-01-01T00:00:00Z" + "timestamp": "2024-01-01T00:00:00.000Z", + "message_id": "string", + "source": "user", + "automated": true, + "actor": { + "user_id": "string", + "email": "string" + }, + "automation": { + "id": "string", + "name": "string" + } } ] } @@ -2296,6 +2438,7 @@ epilot message createDraft --jsonata '$' "_updated_at": "2021-02-10T09:14:31.990Z", "message_id": "<0102017b97a502f8-a67f01c2-68cc-4928-b91b-45853f34e259-000000@eu-west-1.amazonses.com>", "sender": "206801", + "updated_by": "206801", "subject": "Request for solar panel price", "html": "
We at ABC GmbH would like to request a price quote for the solar panel.
", "text": "We at ABC GmbH would like to request a price quote for the solar panel.", @@ -2349,7 +2492,8 @@ epilot message createDraft --jsonata '$' "may_be_signature_attachment": true, "cid": "fb222496-a1a5-4639-94f2-07b5e35e4068", "inline": false, - "send_as_link": false + "send_as_link": false, + "save_to_entity": true } ] }, @@ -2399,6 +2543,7 @@ epilot message sendDraft --jsonata '$' "_updated_at": "2021-02-10T09:14:31.990Z", "message_id": "<0102017b97a502f8-a67f01c2-68cc-4928-b91b-45853f34e259-000000@eu-west-1.amazonses.com>", "sender": "206801", + "updated_by": "206801", "subject": "Request for solar panel price", "html": "
We at ABC GmbH would like to request a price quote for the solar panel.
", "text": "We at ABC GmbH would like to request a price quote for the solar panel.", @@ -2452,7 +2597,8 @@ epilot message sendDraft --jsonata '$' "may_be_signature_attachment": true, "cid": "fb222496-a1a5-4639-94f2-07b5e35e4068", "inline": false, - "send_as_link": false + "send_as_link": false, + "save_to_entity": true } ] }, @@ -2517,6 +2663,7 @@ epilot message getMessageV2 -p id=4d74976d-fb64-47fd-85e2-65eea140f5eb --jsonata "_updated_at": "2021-02-10T09:14:31.990Z", "message_id": "<0102017b97a502f8-a67f01c2-68cc-4928-b91b-45853f34e259-000000@eu-west-1.amazonses.com>", "sender": "206801", + "updated_by": "206801", "subject": "Request for solar panel price", "html": "
We at ABC GmbH would like to request a price quote for the solar panel.
", "text": "We at ABC GmbH would like to request a price quote for the solar panel.", @@ -2570,7 +2717,8 @@ epilot message getMessageV2 -p id=4d74976d-fb64-47fd-85e2-65eea140f5eb --jsonata "may_be_signature_attachment": true, "cid": "fb222496-a1a5-4639-94f2-07b5e35e4068", "inline": false, - "send_as_link": false + "send_as_link": false, + "save_to_entity": true } ] }, diff --git a/packages/cli/docs/metering.md b/packages/cli/docs/metering.md index 81ac07f88..7aad0fa87 100644 --- a/packages/cli/docs/metering.md +++ b/packages/cli/docs/metering.md @@ -1,5 +1,6 @@ # Metering API +- **Base URL:** `https://metering.sls.epilot.io` - **API Docs:** [https://docs.epilot.io/api/metering](https://docs.epilot.io/api/metering) The Metering API manages smart meter data, meter counters, and meter readings for epilot customers and administrators. @@ -47,6 +48,7 @@ epilot metering getCustomerMeters - [`createMeterReading`](#createmeterreading) — Inserts a new meter reading. - [`createMeterReadings`](#createmeterreadings) — Inserts multiple meter readings at once. Limited to 100 readings per request. - [`batchWriteMeterReadings`](#batchwritemeterreadings) — Upserts or deletes multiple meter readings at once. Limited to 100 readings per request. +- [`pruneMeterReadings`](#prunemeterreadings) — Deletes every reading of a meter whose `external_id` is NOT in the provided keep list — in a single request. - [`createMeterReadingFromSubmission`](#createmeterreadingfromsubmission) — Creates meter readings from a journey submission payload. - [`getAllowedReadingForMeter`](#getallowedreadingformeter) — Returns the allowed min/max reading range for each counter of the given meter. - [`createReadingWithMeter`](#createreadingwithmeter) — Creates a meter reading along with meter lookup or creation by MA-LO ID and OBIS number. @@ -686,6 +688,9 @@ Inserts multiple meter readings at once. Limited to 100 readings per request. If set to false or not provided, the system performs the following validations: Validation Rule | | `direct` | query | boolean | No | When true, bypasses changeset interception and writes directly to ClickHouse. Used by trusted integrations (e.g., ERP sync) to confirm changes and auto-clear matching pending changesets. | +| `create_ticket` | query | boolean | No | Controls whether a manual-intervention ("Übermittlung Zählerstand") ticket is created for the +written readings. Defaults to true, preserving portal/journey behaviour (readings with +source ECP or journ | **Request Body** (required) @@ -888,6 +893,9 @@ If set to false or not provided, the system performs the following validations: Validation Rule | | `activity_id` | query | string (ulid) | No | Activity to include in event feed | | `direct` | query | boolean | No | When true, bypasses changeset interception and writes directly to ClickHouse. Used by trusted integrations (e.g., ERP sync) to confirm changes and auto-clear matching pending changesets. | +| `create_ticket` | query | boolean | No | Controls whether a manual-intervention ("Übermittlung Zählerstand") ticket is created for the +written readings. Defaults to true, preserving portal/journey behaviour (readings with +source ECP or journ | **Request Body** (required) @@ -973,6 +981,71 @@ epilot metering batchWriteMeterReadings --jsonata 'data' --- +### `pruneMeterReadings` + +Deletes every reading of a meter whose `external_id` is NOT in the provided keep list — in a single request. + +`POST /v2/metering/readings/prune` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `async` | query | boolean | No | Don't wait for the deletions to become visible in GetReadings API. Useful for large prunes | +| `activity_id` | query | string (ulid) | No | Activity to include in event feed | +| `create_ticket` | query | boolean | No | Controls whether a manual-intervention ("Übermittlung Zählerstand") ticket is created for the +written readings. Defaults to true, preserving portal/journey behaviour (readings with +source ECP or journ | +| `dry_run` | query | boolean | No | When true, computes and returns the deleted/kept counts without deleting anything. | + +**Request Body** (required) + +**Sample Call** + +```bash +epilot metering pruneMeterReadings +``` + +With request body: + +```bash +epilot metering pruneMeterReadings \ + -d '{ + "meter_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "counter_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "source": "ECP", + "keep_external_ids": ["erp-reading-1", "erp-reading-2"] +}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot metering pruneMeterReadings +``` + +With JSONata filter: + +```bash +epilot metering pruneMeterReadings --jsonata 'data' +``` + +
+Sample Response + +```json +{ + "data": { + "deleted_count": 42, + "kept_count": 12 + } +} +``` + +
+ +--- + ### `createMeterReadingFromSubmission` Creates meter readings from a journey submission payload. diff --git a/packages/cli/docs/notification.md b/packages/cli/docs/notification.md index 9a8fc8ffb..84ce4a9ef 100644 --- a/packages/cli/docs/notification.md +++ b/packages/cli/docs/notification.md @@ -151,7 +151,8 @@ epilot notification getNotificationsV2 --jsonata 'results[0]' "email": false, "in_app": false } - } + }, + "allowed_channels": ["in_app"] } ] } @@ -258,7 +259,8 @@ epilot notification getNotifications --jsonata 'results[0]' "email": false, "in_app": false } - } + }, + "allowed_channels": ["in_app"] } ] } @@ -348,6 +350,7 @@ epilot notification createNotification \ "in_app": false } }, + "allowed_channels": ["in_app"], "read_state": false, "visibility_user_ids": ["1", "2", "3", "4", "5"] }' @@ -465,7 +468,8 @@ epilot notification getNotification -p id=1 --jsonata '$' "email": false, "in_app": false } - } + }, + "allowed_channels": ["in_app"] } ``` diff --git a/packages/cli/docs/permissions.md b/packages/cli/docs/permissions.md index e880b3e92..6ef3d5c7c 100644 --- a/packages/cli/docs/permissions.md +++ b/packages/cli/docs/permissions.md @@ -1,5 +1,6 @@ # Permissions API +- **Base URL:** `https://permissions.sls.epilot.io` - **API Docs:** [https://docs.epilot.io/api/permissions](https://docs.epilot.io/api/permissions) Flexible Role-based Access Control for epilot diff --git a/packages/cli/docs/pricing.md b/packages/cli/docs/pricing.md index b12d828ee..316b250d7 100644 --- a/packages/cli/docs/pricing.md +++ b/packages/cli/docs/pricing.md @@ -75,6 +75,25 @@ epilot pricing $calculatePricingDetails **Product Recommendations API** - [`$productRecommendations`](#$productrecommendations) — Get a list of product recommendations based on the search parameters. +**Conditional Pricing API** +- [`$getConditionSets`](#$getconditionsets) — Returns the condition sets built in for one conditional entity type: the situations a +- [`$resolveConditionalEntity`](#$resolveconditionalentity) — Resolves which of a conditional entity's variants apply, and returns each one composed: the +- [`$createConditionalVariant`](#$createconditionalvariant) — Creates one variant of a conditional entity, together with the first version carrying its +- [`$listConditionalVariants`](#$listconditionalvariants) — Lists a conditional entity's variants and the conditions each one pins — the browse, filter +- [`$getConditionalVariantTree`](#$getconditionalvarianttree) — The variants list, each row carrying the version in effect at `as_of` — the Entity UI's main +- [`$getActiveConditionalVariantVersion`](#$getactiveconditionalvariantversion) — Returns the version of this variant that is currently in effect — the one with the latest +- [`$replaceActiveConditionalVariantVersion`](#$replaceactiveconditionalvariantversion) — Replaces the values of the version currently in effect, wholesale. +- [`$patchActiveConditionalVariantVersion`](#$patchactiveconditionalvariantversion) — Changes only the fields it names on the version currently in effect. +- [`$deleteConditionalVariant`](#$deleteconditionalvariant) — Removes one variant of a conditional entity: the condition tuple it holds, its registration +- [`$listConditionalVariantVersions`](#$listconditionalvariantversions) — Lists one variant's versions — its whole timeline, oldest first, which is what expanding a row +- [`$appendConditionalVariantVersion`](#$appendconditionalvariantversion) — Appends a version to a variant: a new set of values taking effect at its own instant. +- [`$getConditionalVariantVersion`](#$getconditionalvariantversion) — Returns one specific version of a variant, by the instant it takes effect — what a form editing +- [`$replaceConditionalVariantVersion`](#$replaceconditionalvariantversion) — Replaces one version's values wholesale, addressed by its `valid_from`. +- [`$patchConditionalVariantVersion`](#$patchconditionalvariantversion) — Changes only the fields it names on one version, addressed by its `valid_from`. +- [`$deleteConditionalVariantVersion`](#$deleteconditionalvariantversion) — Removes one version of a variant. +- [`$batchUpsertConditionalVariants`](#$batchupsertconditionalvariants) — Writes up to 100 variants or versions in one call — the endpoint a bulk importer drives a +- [`$batchDeleteConditionalVariants`](#$batchdeleteconditionalvariants) — Removes up to 100 variants or versions in one call — the symmetric bulk withdrawal, so + ### `$calculatePricingDetails` Computes a set of pricing details that can be persisted on an entity with the pricing capability enabled, e.g: Orders or @@ -427,7 +446,7 @@ Update an existing Order | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `id` | path | string | Yes | Order entity ID | +| `id` | path | string (uuid) | Yes | Order entity ID | **Request Body** (required) @@ -2130,3 +2149,1480 @@ epilot pricing $productRecommendations --jsonata 'results[0]'
--- + +### `$getConditionSets` + +Returns the condition sets built in for one conditional entity type: the situations a + +`GET /v1/conditional-pricing/{slug}/condition-sets` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `slug` | path | "product" \| "price" \| "coupon" | Yes | The conditional entity type whose built-in condition sets to return | + +**Sample Call** + +```bash +epilot pricing $getConditionSets \ + -p slug=price +``` + +Using positional args for path parameters: + +```bash +epilot pricing $getConditionSets price +``` + +With JSONata filter: + +```bash +epilot pricing $getConditionSets -p slug=price --jsonata 'results[0]' +``` + +
+Sample Response + +```json +{ + "results": [ + { + "id": "delivery_area", + "label": "Delivery Area", + "description": "string", + "conditions": [ + { + "id": "d5839b94-ba20-4225-a78e-76951d352bd6", + "name": "postal_code", + "label": "Postal Code", + "type": "string", + "options": [ + "private", + { + "value": "commercial", + "title": "Commercial customers" + } + ], + "format": "zipcode" + } + ] + } + ] +} +``` + +
+ +--- + +### `$resolveConditionalEntity` + +Resolves which of a conditional entity's variants apply, and returns each one composed: the + +`POST /v1/conditional-pricing:resolve` + +**Request Body** (required) + +**Sample Call** + +```bash +epilot pricing $resolveConditionalEntity +``` + +With request body: + +```bash +epilot pricing $resolveConditionalEntity \ + -d '{ + "schema": "product", + "entity_id": "price-sp26d1yo", + "context": { + "postal_code": "46045", + "consumption": { + "lt": 5000 + } + }, + "as_of": "2027-03-15T00:00:00Z", + "options": { + "resolve_one": false, + "hydrate": false + } +}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot pricing $resolveConditionalEntity +``` + +With JSONata filter: + +```bash +epilot pricing $resolveConditionalEntity --jsonata 'results[0]' +``` + +
+Sample Response + +```json +{ + "results": [ + { + "_id": "price-sp26d1yo", + "_variant_id": "var-46045", + "_version_valid_from": "2027-01-01T00:00:00.000Z", + "_conditions": { + "postal_code": "46045", + "default": false + }, + "_inert_overrides": [ + { + "attribute": "unit_amount", + "reason": "ATTRIBUTE_NOT_OVERRIDABLE" + } + ] + } + ] +} +``` + +
+ +--- + +### `$createConditionalVariant` + +Creates one variant of a conditional entity, together with the first version carrying its + +`POST /v1/conditional-pricing/{slug}/entities/{entity_id}/variants` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `slug` | path | "product" \| "price" \| "coupon" | Yes | The conditional entity type this variant belongs to | +| `entity_id` | path | string | Yes | The conditional entity to add a variant to | + +**Request Body** (required) + +**Sample Call** + +```bash +epilot pricing $createConditionalVariant \ + -p slug=price \ + -p entity_id=price-sp26d1yo +``` + +With request body: + +```bash +epilot pricing $createConditionalVariant \ + -p slug=price \ + -p entity_id=price-sp26d1yo \ + -d '{ + "conditions": { + "postal_code": "46045" + }, + "default": false, + "valid_from": "2027-01-01T00:00:00Z", + "values": { + "unit_amount": 2499, + "unit_amount_decimal": "24.99" + } +}' +``` + +Using positional args for path parameters: + +```bash +epilot pricing $createConditionalVariant price price-sp26d1yo +``` + +Using stdin pipe: + +```bash +cat body.json | epilot pricing $createConditionalVariant -p slug=price -p entity_id=price-sp26d1yo +``` + +With JSONata filter: + +```bash +epilot pricing $createConditionalVariant -p slug=price -p entity_id=price-sp26d1yo --jsonata 'variant_id' +``` + +
+Sample Response + +```json +{ + "variant_id": "var-46045", + "entity_id": "price-sp26d1yo", + "schema": "product", + "conditions": { + "postal_code": "46045", + "default": false + }, + "valid_from": "2027-01-01T00:00:00.000Z", + "values": { + "unit_amount": 2499, + "unit_amount_decimal": "24.99" + }, + "_created_at": "string", + "_updated_at": "string", + "_revision": 0, + "warnings": [ + { + "code": "VARIANT_COUNT_APPROACHING_CAP", + "message": "string", + "details": { + "variant_count": 0, + "cap": 0 + } + } + ] +} +``` + +
+ +--- + +### `$listConditionalVariants` + +Lists a conditional entity's variants and the conditions each one pins — the browse, filter + +`POST /v1/conditional-pricing/{slug}/entities/{entity_id}/variants:list` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `slug` | path | "product" \| "price" \| "coupon" | Yes | The conditional entity type the variants belong to | +| `entity_id` | path | string | Yes | The conditional entity whose variants to list | + +**Request Body** (required) + +**Sample Call** + +```bash +epilot pricing $listConditionalVariants \ + -p slug=price \ + -p entity_id=price-sp26d1yo +``` + +With request body: + +```bash +epilot pricing $listConditionalVariants \ + -p slug=price \ + -p entity_id=price-sp26d1yo \ + -d '{ + "conditions": { + "postal_code": "46045", + "consumption": { + "lt": 5000 + } + }, + "search": "460", + "sort": "conditions.postal_code:asc", + "from": 0, + "size": 10, + "cursor": "eyJmcm9tIjoyNSwibGlzdGluZyI6IjNmOWMxZTJhIn0" +}' +``` + +Using positional args for path parameters: + +```bash +epilot pricing $listConditionalVariants price price-sp26d1yo +``` + +Using stdin pipe: + +```bash +cat body.json | epilot pricing $listConditionalVariants -p slug=price -p entity_id=price-sp26d1yo +``` + +With JSONata filter: + +```bash +epilot pricing $listConditionalVariants -p slug=price -p entity_id=price-sp26d1yo --jsonata 'results[0]' +``` + +
+Sample Response + +```json +{ + "hits": 8128, + "results": [ + { + "variant_id": "var-46045", + "entity_id": "price-sp26d1yo", + "schema": "product", + "conditions": { + "postal_code": "46045", + "default": false + } + } + ], + "next": "eyJmcm9tIjoyNSwibGlzdGluZyI6IjNmOWMxZTJhIn0" +} +``` + +
+ +--- + +### `$getConditionalVariantTree` + +The variants list, each row carrying the version in effect at `as_of` — the Entity UI's main + +`POST /v1/conditional-pricing/{slug}/entities/{entity_id}/variants:tree` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `slug` | path | "product" \| "price" \| "coupon" | Yes | The conditional entity type the variants belong to | +| `entity_id` | path | string | Yes | The conditional entity whose variants to list | + +**Request Body** (required) + +**Sample Call** + +```bash +epilot pricing $getConditionalVariantTree \ + -p slug=price \ + -p entity_id=price-sp26d1yo +``` + +With request body: + +```bash +epilot pricing $getConditionalVariantTree \ + -p slug=price \ + -p entity_id=price-sp26d1yo \ + -d '{ + "conditions": { + "postal_code": "46045", + "consumption": { + "lt": 5000 + } + }, + "search": "460", + "sort": "conditions.postal_code:asc", + "from": 0, + "size": 10, + "cursor": "eyJmcm9tIjoyNSwibGlzdGluZyI6IjNmOWMxZTJhIn0", + "as_of": "2027-03-15T00:00:00Z" +}' +``` + +Using positional args for path parameters: + +```bash +epilot pricing $getConditionalVariantTree price price-sp26d1yo +``` + +Using stdin pipe: + +```bash +cat body.json | epilot pricing $getConditionalVariantTree -p slug=price -p entity_id=price-sp26d1yo +``` + +With JSONata filter: + +```bash +epilot pricing $getConditionalVariantTree -p slug=price -p entity_id=price-sp26d1yo --jsonata 'results[0]' +``` + +
+Sample Response + +```json +{ + "hits": 8128, + "results": [ + { + "variant_id": "var-46045", + "entity_id": "price-sp26d1yo", + "schema": "product", + "conditions": { + "postal_code": "46045", + "default": false + }, + "status": "active", + "version": { + "variant_id": "var-46045", + "entity_id": "price-sp26d1yo", + "schema": "product", + "conditions": { + "postal_code": "46045", + "default": false + }, + "valid_from": "2027-01-01T00:00:00.000Z", + "values": { + "unit_amount": 2499, + "unit_amount_decimal": "24.99" + }, + "_created_at": "string", + "_updated_at": "string" + } + } + ], + "next": "eyJmcm9tIjoyNSwibGlzdGluZyI6IjNmOWMxZTJhIn0" +} +``` + +
+ +--- + +### `$getActiveConditionalVariantVersion` + +Returns the version of this variant that is currently in effect — the one with the latest + +`GET /v1/conditional-pricing/{slug}/entities/{entity_id}/variants/{variant_id}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `slug` | path | "product" \| "price" \| "coupon" | Yes | The conditional entity type this variant belongs to | +| `entity_id` | path | string | Yes | The conditional entity the variant belongs to | +| `variant_id` | path | string | Yes | The variant whose timeline this call addresses | + +**Sample Call** + +```bash +epilot pricing $getActiveConditionalVariantVersion \ + -p slug=price \ + -p entity_id=price-sp26d1yo \ + -p variant_id=var-46045 +``` + +Using positional args for path parameters: + +```bash +epilot pricing $getActiveConditionalVariantVersion price price-sp26d1yo var-46045 +``` + +With JSONata filter: + +```bash +epilot pricing $getActiveConditionalVariantVersion -p slug=price -p entity_id=price-sp26d1yo -p variant_id=var-46045 --jsonata 'variant_id' +``` + +
+Sample Response + +```json +{ + "variant_id": "var-46045", + "entity_id": "price-sp26d1yo", + "schema": "product", + "conditions": { + "postal_code": "46045", + "default": false + }, + "valid_from": "2027-01-01T00:00:00.000Z", + "values": { + "unit_amount": 2499, + "unit_amount_decimal": "24.99" + }, + "_created_at": "string", + "_updated_at": "string", + "_revision": 3 +} +``` + +
+ +--- + +### `$replaceActiveConditionalVariantVersion` + +Replaces the values of the version currently in effect, wholesale. + +`PUT /v1/conditional-pricing/{slug}/entities/{entity_id}/variants/{variant_id}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `slug` | path | "product" \| "price" \| "coupon" | Yes | The conditional entity type this variant belongs to | +| `entity_id` | path | string | Yes | The conditional entity the variant belongs to | +| `variant_id` | path | string | Yes | The variant whose timeline this call addresses | + +**Request Body** (required) + +**Sample Call** + +```bash +epilot pricing $replaceActiveConditionalVariantVersion \ + -p slug=price \ + -p entity_id=price-sp26d1yo \ + -p variant_id=var-46045 +``` + +With request body: + +```bash +epilot pricing $replaceActiveConditionalVariantVersion \ + -p slug=price \ + -p entity_id=price-sp26d1yo \ + -p variant_id=var-46045 \ + -d '{ + "values": { + "unit_amount": 2499, + "unit_amount_decimal": "24.99" + }, + "_revision": 3, + "valid_from": "string", + "conditions": { + "postal_code": "46045" + } +}' +``` + +Using positional args for path parameters: + +```bash +epilot pricing $replaceActiveConditionalVariantVersion price price-sp26d1yo var-46045 +``` + +Using stdin pipe: + +```bash +cat body.json | epilot pricing $replaceActiveConditionalVariantVersion -p slug=price -p entity_id=price-sp26d1yo -p variant_id=var-46045 +``` + +With JSONata filter: + +```bash +epilot pricing $replaceActiveConditionalVariantVersion -p slug=price -p entity_id=price-sp26d1yo -p variant_id=var-46045 --jsonata '$' +``` + +
+Sample Response + +```json +{ + "variant_id": "var-46045", + "entity_id": "price-sp26d1yo", + "schema": "product", + "conditions": { + "postal_code": "46045", + "default": false + }, + "valid_from": "2027-01-01T00:00:00.000Z", + "values": { + "unit_amount": 2499, + "unit_amount_decimal": "24.99" + }, + "_created_at": "string", + "_updated_at": "string", + "_revision": 3, + "warnings": [ + { + "code": "VARIANT_COUNT_APPROACHING_CAP", + "message": "string", + "details": { + "variant_count": 0, + "cap": 0 + } + } + ] +} +``` + +
+ +--- + +### `$patchActiveConditionalVariantVersion` + +Changes only the fields it names on the version currently in effect. + +`PATCH /v1/conditional-pricing/{slug}/entities/{entity_id}/variants/{variant_id}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `slug` | path | "product" \| "price" \| "coupon" | Yes | The conditional entity type this variant belongs to | +| `entity_id` | path | string | Yes | The conditional entity the variant belongs to | +| `variant_id` | path | string | Yes | The variant whose timeline this call addresses | + +**Request Body** (required) + +**Sample Call** + +```bash +epilot pricing $patchActiveConditionalVariantVersion \ + -p slug=price \ + -p entity_id=price-sp26d1yo \ + -p variant_id=var-46045 +``` + +With request body: + +```bash +epilot pricing $patchActiveConditionalVariantVersion \ + -p slug=price \ + -p entity_id=price-sp26d1yo \ + -p variant_id=var-46045 \ + -d '{ + "values": { + "unit_amount": 2499, + "unit_amount_decimal": "24.99" + }, + "_revision": 3, + "valid_from": "string", + "conditions": { + "postal_code": "46045" + } +}' +``` + +Using positional args for path parameters: + +```bash +epilot pricing $patchActiveConditionalVariantVersion price price-sp26d1yo var-46045 +``` + +Using stdin pipe: + +```bash +cat body.json | epilot pricing $patchActiveConditionalVariantVersion -p slug=price -p entity_id=price-sp26d1yo -p variant_id=var-46045 +``` + +With JSONata filter: + +```bash +epilot pricing $patchActiveConditionalVariantVersion -p slug=price -p entity_id=price-sp26d1yo -p variant_id=var-46045 --jsonata '$' +``` + +
+Sample Response + +```json +{ + "variant_id": "var-46045", + "entity_id": "price-sp26d1yo", + "schema": "product", + "conditions": { + "postal_code": "46045", + "default": false + }, + "valid_from": "2027-01-01T00:00:00.000Z", + "values": { + "unit_amount": 2499, + "unit_amount_decimal": "24.99" + }, + "_created_at": "string", + "_updated_at": "string", + "_revision": 3, + "warnings": [ + { + "code": "VARIANT_COUNT_APPROACHING_CAP", + "message": "string", + "details": { + "variant_count": 0, + "cap": 0 + } + } + ] +} +``` + +
+ +--- + +### `$deleteConditionalVariant` + +Removes one variant of a conditional entity: the condition tuple it holds, its registration + +`DELETE /v1/conditional-pricing/{slug}/entities/{entity_id}/variants/{variant_id}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `slug` | path | "product" \| "price" \| "coupon" | Yes | The conditional entity type this variant belongs to | +| `entity_id` | path | string | Yes | The conditional entity the variant belongs to | +| `variant_id` | path | string | Yes | The variant to remove | + +**Sample Call** + +```bash +epilot pricing $deleteConditionalVariant \ + -p slug=price \ + -p entity_id=price-sp26d1yo \ + -p variant_id=var-46045 +``` + +Using positional args for path parameters: + +```bash +epilot pricing $deleteConditionalVariant price price-sp26d1yo var-46045 +``` + +With JSONata filter: + +```bash +epilot pricing $deleteConditionalVariant -p slug=price -p entity_id=price-sp26d1yo -p variant_id=var-46045 --jsonata 'variant_id' +``` + +
+Sample Response + +```json +{ + "variant_id": "var-46045", + "entity_id": "price-sp26d1yo", + "schema": "product", + "tuple_released": true, + "versions_deleted": 0 +} +``` + +
+ +--- + +### `$listConditionalVariantVersions` + +Lists one variant's versions — its whole timeline, oldest first, which is what expanding a row + +`GET /v1/conditional-pricing/{slug}/entities/{entity_id}/variants/{variant_id}/versions` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `slug` | path | "product" \| "price" \| "coupon" | Yes | The conditional entity type this variant belongs to | +| `entity_id` | path | string | Yes | The conditional entity the variant belongs to | +| `variant_id` | path | string | Yes | The variant whose timeline to list | +| `limit` | query | number | No | Versions per page. Defaults to 100, which is also the maximum; a larger value is clamped +silently. A variant's timeline is usually short enough to fit one page. + | +| `order` | query | "asc" \| "desc" | No | Which end of the timeline to read from: `asc` oldest first, `desc` newest first. Defaults +to `asc`. + +Baked into every cursor this read issues: a cursor resumes one direction, and replaying it +against | +| `cursor` | query | string | No | Continue from a previous response's `next`. Opaque: it encodes the position and the order +it was issued for, and nothing a client should read or construct. + | + +**Sample Call** + +```bash +epilot pricing $listConditionalVariantVersions \ + -p slug=price \ + -p entity_id=price-sp26d1yo \ + -p variant_id=var-46045 +``` + +Using positional args for path parameters: + +```bash +epilot pricing $listConditionalVariantVersions price price-sp26d1yo var-46045 +``` + +With JSONata filter: + +```bash +epilot pricing $listConditionalVariantVersions -p slug=price -p entity_id=price-sp26d1yo -p variant_id=var-46045 --jsonata 'results[0]' +``` + +
+Sample Response + +```json +{ + "results": [ + { + "variant_id": "var-46045", + "entity_id": "price-sp26d1yo", + "schema": "product", + "conditions": { + "postal_code": "46045", + "default": false + }, + "valid_from": "2027-01-01T00:00:00.000Z", + "values": { + "unit_amount": 2499, + "unit_amount_decimal": "24.99" + }, + "_created_at": "string", + "_updated_at": "string" + } + ], + "next": "eyJzayI6IlYjcHJpY2Utc3AyNmQxeW8jdmFyLTQ2MDQ1IzIwMjYtMDEtMDFUMDA6MDA6MDAuMDAwWiIsIm9yZGVyIjoiYXNjIn0" +} +``` + +
+ +--- + +### `$appendConditionalVariantVersion` + +Appends a version to a variant: a new set of values taking effect at its own instant. + +`POST /v1/conditional-pricing/{slug}/entities/{entity_id}/variants/{variant_id}/versions` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `slug` | path | "product" \| "price" \| "coupon" | Yes | The conditional entity type this variant belongs to | +| `entity_id` | path | string | Yes | The conditional entity the variant belongs to | +| `variant_id` | path | string | Yes | The variant whose timeline this call addresses | + +**Request Body** (required) + +**Sample Call** + +```bash +epilot pricing $appendConditionalVariantVersion \ + -p slug=price \ + -p entity_id=price-sp26d1yo \ + -p variant_id=var-46045 +``` + +With request body: + +```bash +epilot pricing $appendConditionalVariantVersion \ + -p slug=price \ + -p entity_id=price-sp26d1yo \ + -p variant_id=var-46045 \ + -d '{ + "valid_from": "2027-01-01T00:00:00Z", + "values": { + "unit_amount": 2499, + "unit_amount_decimal": "24.99" + }, + "conditions": { + "postal_code": "46045" + } +}' +``` + +Using positional args for path parameters: + +```bash +epilot pricing $appendConditionalVariantVersion price price-sp26d1yo var-46045 +``` + +Using stdin pipe: + +```bash +cat body.json | epilot pricing $appendConditionalVariantVersion -p slug=price -p entity_id=price-sp26d1yo -p variant_id=var-46045 +``` + +With JSONata filter: + +```bash +epilot pricing $appendConditionalVariantVersion -p slug=price -p entity_id=price-sp26d1yo -p variant_id=var-46045 --jsonata '$' +``` + +
+Sample Response + +```json +{ + "variant_id": "var-46045", + "entity_id": "price-sp26d1yo", + "schema": "product", + "conditions": { + "postal_code": "46045", + "default": false + }, + "valid_from": "2027-01-01T00:00:00.000Z", + "values": { + "unit_amount": 2499, + "unit_amount_decimal": "24.99" + }, + "_created_at": "string", + "_updated_at": "string", + "_revision": 3, + "warnings": [ + { + "code": "VARIANT_COUNT_APPROACHING_CAP", + "message": "string", + "details": { + "variant_count": 0, + "cap": 0 + } + } + ] +} +``` + +
+ +--- + +### `$getConditionalVariantVersion` + +Returns one specific version of a variant, by the instant it takes effect — what a form editing + +`GET /v1/conditional-pricing/{slug}/entities/{entity_id}/variants/{variant_id}/versions/{valid_from}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `slug` | path | "product" \| "price" \| "coupon" | Yes | The conditional entity type this variant belongs to | +| `entity_id` | path | string | Yes | The conditional entity the variant belongs to | +| `variant_id` | path | string | Yes | The variant whose timeline this call addresses | +| `valid_from` | path | string | Yes | The version to address, by the instant it takes effect. + +An RFC 3339 date (`2026-01-01`, read as midnight UTC) or date-time +(`2026-01-01T00:00:00Z`), to at most millisecond precision. Written any acce | + +**Sample Call** + +```bash +epilot pricing $getConditionalVariantVersion \ + -p slug=price \ + -p entity_id=price-sp26d1yo \ + -p variant_id=var-46045 \ + -p valid_from=2027-01-01T00:00:00.000Z +``` + +Using positional args for path parameters: + +```bash +epilot pricing $getConditionalVariantVersion price price-sp26d1yo var-46045 2027-01-01T00:00:00.000Z +``` + +With JSONata filter: + +```bash +epilot pricing $getConditionalVariantVersion -p slug=price -p entity_id=price-sp26d1yo -p variant_id=var-46045 -p valid_from=2027-01-01T00:00:00.000Z --jsonata 'variant_id' +``` + +
+Sample Response + +```json +{ + "variant_id": "var-46045", + "entity_id": "price-sp26d1yo", + "schema": "product", + "conditions": { + "postal_code": "46045", + "default": false + }, + "valid_from": "2027-01-01T00:00:00.000Z", + "values": { + "unit_amount": 2499, + "unit_amount_decimal": "24.99" + }, + "_created_at": "string", + "_updated_at": "string", + "_revision": 3 +} +``` + +
+ +--- + +### `$replaceConditionalVariantVersion` + +Replaces one version's values wholesale, addressed by its `valid_from`. + +`PUT /v1/conditional-pricing/{slug}/entities/{entity_id}/variants/{variant_id}/versions/{valid_from}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `slug` | path | "product" \| "price" \| "coupon" | Yes | The conditional entity type this variant belongs to | +| `entity_id` | path | string | Yes | The conditional entity the variant belongs to | +| `variant_id` | path | string | Yes | The variant whose timeline this call addresses | +| `valid_from` | path | string | Yes | The version to address, by the instant it takes effect. + +An RFC 3339 date (`2026-01-01`, read as midnight UTC) or date-time +(`2026-01-01T00:00:00Z`), to at most millisecond precision. Written any acce | + +**Request Body** (required) + +**Sample Call** + +```bash +epilot pricing $replaceConditionalVariantVersion \ + -p slug=price \ + -p entity_id=price-sp26d1yo \ + -p variant_id=var-46045 \ + -p valid_from=2027-01-01T00:00:00.000Z +``` + +With request body: + +```bash +epilot pricing $replaceConditionalVariantVersion \ + -p slug=price \ + -p entity_id=price-sp26d1yo \ + -p variant_id=var-46045 \ + -p valid_from=2027-01-01T00:00:00.000Z \ + -d '{ + "values": { + "unit_amount": 2499, + "unit_amount_decimal": "24.99" + }, + "_revision": 3, + "valid_from": "string", + "conditions": { + "postal_code": "46045" + } +}' +``` + +Using positional args for path parameters: + +```bash +epilot pricing $replaceConditionalVariantVersion price price-sp26d1yo var-46045 2027-01-01T00:00:00.000Z +``` + +Using stdin pipe: + +```bash +cat body.json | epilot pricing $replaceConditionalVariantVersion -p slug=price -p entity_id=price-sp26d1yo -p variant_id=var-46045 -p valid_from=2027-01-01T00:00:00.000Z +``` + +With JSONata filter: + +```bash +epilot pricing $replaceConditionalVariantVersion -p slug=price -p entity_id=price-sp26d1yo -p variant_id=var-46045 -p valid_from=2027-01-01T00:00:00.000Z --jsonata '$' +``` + +
+Sample Response + +```json +{ + "variant_id": "var-46045", + "entity_id": "price-sp26d1yo", + "schema": "product", + "conditions": { + "postal_code": "46045", + "default": false + }, + "valid_from": "2027-01-01T00:00:00.000Z", + "values": { + "unit_amount": 2499, + "unit_amount_decimal": "24.99" + }, + "_created_at": "string", + "_updated_at": "string", + "_revision": 3, + "warnings": [ + { + "code": "VARIANT_COUNT_APPROACHING_CAP", + "message": "string", + "details": { + "variant_count": 0, + "cap": 0 + } + } + ] +} +``` + +
+ +--- + +### `$patchConditionalVariantVersion` + +Changes only the fields it names on one version, addressed by its `valid_from`. + +`PATCH /v1/conditional-pricing/{slug}/entities/{entity_id}/variants/{variant_id}/versions/{valid_from}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `slug` | path | "product" \| "price" \| "coupon" | Yes | The conditional entity type this variant belongs to | +| `entity_id` | path | string | Yes | The conditional entity the variant belongs to | +| `variant_id` | path | string | Yes | The variant whose timeline this call addresses | +| `valid_from` | path | string | Yes | The version to address, by the instant it takes effect. + +An RFC 3339 date (`2026-01-01`, read as midnight UTC) or date-time +(`2026-01-01T00:00:00Z`), to at most millisecond precision. Written any acce | + +**Request Body** (required) + +**Sample Call** + +```bash +epilot pricing $patchConditionalVariantVersion \ + -p slug=price \ + -p entity_id=price-sp26d1yo \ + -p variant_id=var-46045 \ + -p valid_from=2027-01-01T00:00:00.000Z +``` + +With request body: + +```bash +epilot pricing $patchConditionalVariantVersion \ + -p slug=price \ + -p entity_id=price-sp26d1yo \ + -p variant_id=var-46045 \ + -p valid_from=2027-01-01T00:00:00.000Z \ + -d '{ + "values": { + "unit_amount": 2499, + "unit_amount_decimal": "24.99" + }, + "_revision": 3, + "valid_from": "string", + "conditions": { + "postal_code": "46045" + } +}' +``` + +Using positional args for path parameters: + +```bash +epilot pricing $patchConditionalVariantVersion price price-sp26d1yo var-46045 2027-01-01T00:00:00.000Z +``` + +Using stdin pipe: + +```bash +cat body.json | epilot pricing $patchConditionalVariantVersion -p slug=price -p entity_id=price-sp26d1yo -p variant_id=var-46045 -p valid_from=2027-01-01T00:00:00.000Z +``` + +With JSONata filter: + +```bash +epilot pricing $patchConditionalVariantVersion -p slug=price -p entity_id=price-sp26d1yo -p variant_id=var-46045 -p valid_from=2027-01-01T00:00:00.000Z --jsonata '$' +``` + +
+Sample Response + +```json +{ + "variant_id": "var-46045", + "entity_id": "price-sp26d1yo", + "schema": "product", + "conditions": { + "postal_code": "46045", + "default": false + }, + "valid_from": "2027-01-01T00:00:00.000Z", + "values": { + "unit_amount": 2499, + "unit_amount_decimal": "24.99" + }, + "_created_at": "string", + "_updated_at": "string", + "_revision": 3, + "warnings": [ + { + "code": "VARIANT_COUNT_APPROACHING_CAP", + "message": "string", + "details": { + "variant_count": 0, + "cap": 0 + } + } + ] +} +``` + +
+ +--- + +### `$deleteConditionalVariantVersion` + +Removes one version of a variant. + +`DELETE /v1/conditional-pricing/{slug}/entities/{entity_id}/variants/{variant_id}/versions/{valid_from}` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `slug` | path | "product" \| "price" \| "coupon" | Yes | The conditional entity type this variant belongs to | +| `entity_id` | path | string | Yes | The conditional entity the variant belongs to | +| `variant_id` | path | string | Yes | The variant whose timeline this call addresses | +| `valid_from` | path | string | Yes | The version to address, by the instant it takes effect. + +An RFC 3339 date (`2026-01-01`, read as midnight UTC) or date-time +(`2026-01-01T00:00:00Z`), to at most millisecond precision. Written any acce | +| `_revision` | query | number | Yes | The revision marker read from the version being deleted. The delete is refused if the +version has been written since. + +The same marker the write bodies carry as `_revision`. + | + +**Sample Call** + +```bash +epilot pricing $deleteConditionalVariantVersion \ + -p slug=price \ + -p entity_id=price-sp26d1yo \ + -p variant_id=var-46045 \ + -p valid_from=2027-01-01T00:00:00.000Z \ + -p _revision=3 +``` + +Using positional args for path parameters: + +```bash +epilot pricing $deleteConditionalVariantVersion price price-sp26d1yo var-46045 2027-01-01T00:00:00.000Z +``` + +With JSONata filter: + +```bash +epilot pricing $deleteConditionalVariantVersion -p slug=price -p entity_id=price-sp26d1yo -p variant_id=var-46045 -p valid_from=2027-01-01T00:00:00.000Z -p _revision=3 --jsonata 'variant_id' +``` + +
+Sample Response + +```json +{ + "variant_id": "var-46045", + "entity_id": "price-sp26d1yo", + "schema": "product", + "valid_from": "2027-01-01T00:00:00.000Z", + "warnings": [ + { + "code": "VARIANT_COUNT_APPROACHING_CAP", + "message": "string", + "details": { + "variant_count": 0, + "cap": 0 + } + } + ] +} +``` + +
+ +--- + +### `$batchUpsertConditionalVariants` + +Writes up to 100 variants or versions in one call — the endpoint a bulk importer drives a + +`POST /v1/conditional-pricing/{slug}/variants:batchUpsert` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `slug` | path | "product" \| "price" \| "coupon" | Yes | The conditional entity type every item in this call writes under | + +**Request Body** (required) + +**Sample Call** + +```bash +epilot pricing $batchUpsertConditionalVariants \ + -p slug=price +``` + +With request body: + +```bash +epilot pricing $batchUpsertConditionalVariants \ + -p slug=price \ + -d '{ + "correlation_id": "tariff-refresh-2027-01", + "items": [ + { + "entity_id": "price-sp26d1yo", + "conditions": { + "postal_code": "46045" + }, + "default": false, + "valid_from": "2027-01-01T00:00:00Z", + "values": { + "unit_amount": 2499, + "unit_amount_decimal": "24.99" + } + } + ] +}' +``` + +Using positional args for path parameters: + +```bash +epilot pricing $batchUpsertConditionalVariants price +``` + +Using stdin pipe: + +```bash +cat body.json | epilot pricing $batchUpsertConditionalVariants -p slug=price +``` + +With JSONata filter: + +```bash +epilot pricing $batchUpsertConditionalVariants -p slug=price --jsonata 'results[0]' +``` + +
+Sample Response + +```json +{ + "correlation_id": "tariff-refresh-2027-01", + "counts": { + "variant_created": 1, + "version_created": 1, + "updated": 1, + "skipped": 1, + "error": 1 + }, + "results": [ + { + "outcome": "variant_created", + "entity_id": "price-sp26d1yo", + "variant_id": "var-46045", + "valid_from": "2027-01-01T00:00:00.000Z", + "warnings": [ + { + "code": "VARIANT_COUNT_APPROACHING_CAP", + "message": "string", + "details": { + "variant_count": 0, + "cap": 0 + } + } + ], + "error": { + "code": "SCHEMA_NOT_FOUND", + "details": { + "schema": "price" + } + } + } + ] +} +``` + +
+ +--- + +### `$batchDeleteConditionalVariants` + +Removes up to 100 variants or versions in one call — the symmetric bulk withdrawal, so + +`POST /v1/conditional-pricing/{slug}/variants:batchDelete` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `slug` | path | "product" \| "price" \| "coupon" | Yes | The conditional entity type every item in this call removes from | + +**Request Body** (required) + +**Sample Call** + +```bash +epilot pricing $batchDeleteConditionalVariants \ + -p slug=price +``` + +With request body: + +```bash +epilot pricing $batchDeleteConditionalVariants \ + -p slug=price \ + -d '{ + "correlation_id": "postal-code-cleanup-2026-09", + "items": [ + { + "entity_id": "price-sp26d1yo", + "variant_id": "var-46045", + "valid_from": "2027-01-01T00:00:00Z" + } + ] +}' +``` + +Using positional args for path parameters: + +```bash +epilot pricing $batchDeleteConditionalVariants price +``` + +Using stdin pipe: + +```bash +cat body.json | epilot pricing $batchDeleteConditionalVariants -p slug=price +``` + +With JSONata filter: + +```bash +epilot pricing $batchDeleteConditionalVariants -p slug=price --jsonata 'results[0]' +``` + +
+Sample Response + +```json +{ + "correlation_id": "postal-code-cleanup-2026-09", + "counts": { + "deleted": 1, + "skipped": 1, + "error": 1 + }, + "results": [ + { + "outcome": "deleted", + "entity_id": "price-sp26d1yo", + "variant_id": "var-46045", + "valid_from": "2027-01-01T00:00:00.000Z", + "warnings": [ + { + "code": "VARIANT_COUNT_APPROACHING_CAP", + "message": "string", + "details": { + "variant_count": 0, + "cap": 0 + } + } + ], + "error": { + "code": "SCHEMA_NOT_FOUND", + "details": { + "schema": "price" + } + } + } + ] +} +``` + +
+ +--- diff --git a/packages/cli/docs/snapshot.md b/packages/cli/docs/snapshot.md index 854d927ef..934e8e07e 100644 --- a/packages/cli/docs/snapshot.md +++ b/packages/cli/docs/snapshot.md @@ -38,7 +38,7 @@ epilot snapshot listSnapshots **Snapshots** - [`listSnapshots`](#listsnapshots) — List snapshots for the caller's organization, newest first. - [`createSnapshot`](#createsnapshot) — Create a new snapshot of the given resources. Async — returns immediately -- [`captureOrgSnapshot`](#captureorgsnapshot) — Snapshot the caller's whole organization now. Fetches a fresh inventory +- [`captureOrgSnapshot`](#captureorgsnapshot) — Snapshot the caller's whole organization now. Creates a `scope: "org"` - [`getSnapshot`](#getsnapshot) — Fetch a snapshot's metadata. Poll this endpoint to track create/restore progress. - [`deleteSnapshot`](#deletesnapshot) — Delete a snapshot's metadata and S3 manifest. - [`restoreSnapshot`](#restoresnapshot) — Restore a snapshot to the org. Async — returns immediately; client polls @@ -46,6 +46,11 @@ epilot snapshot listSnapshots - [`getSnapshotResource`](#getsnapshotresource) — Fetch one captured resource with its full payload. For UI views - [`listDependencies`](#listdependencies) — Walk the dependency tree for a set of resources and return the full +**ScheduledSnapshots** +- [`getOrgSnapshotSchedule`](#getorgsnapshotschedule) — Return the scheduled-snapshot enrollment config for the caller's org. +- [`putOrgSnapshotSchedule`](#putorgsnapshotschedule) — Create or update the scheduled-snapshot enrollment config for the +- [`deleteOrgSnapshotSchedule`](#deleteorgsnapshotschedule) — Remove the scheduled-snapshot enrollment for the caller's org. + ### `listSnapshots` List snapshots for the caller's organization, newest first. @@ -62,6 +67,9 @@ List snapshots for the caller's organization, newest first. `:`. Split on the first colon — the `` half may contain colons (e.g., role acl ids like `role:acl:internal:foo`). Repeat the | +| `trigger` | query | "manual" \| "sync" \| "blueprint_install" \| "scheduled" | No | Filter to snapshots with a specific trigger. Uses the `byTrigger` GSI +for an efficient indexed query — no table scan. Only snapshots created +after the GSI was added carry this index entry; pre-existin | **Sample Call** @@ -101,13 +109,7 @@ epilot snapshot listSnapshots --jsonata 'results[0]' "name": "string", "user_id": "string", "token_id": "string" - }, - "skipped": [ - { - "lineage_id": "string", - "reason": "modified" - } - ] + } }, "restores": [ { @@ -120,13 +122,7 @@ epilot snapshot listSnapshots --jsonata 'results[0]' "name": "string", "user_id": "string", "token_id": "string" - }, - "skipped": [ - { - "lineage_id": "string", - "reason": "modified" - } - ] + } } ], "matched_count": 0, @@ -195,7 +191,7 @@ epilot snapshot createSnapshot --jsonata '$' ### `captureOrgSnapshot` -Snapshot the caller's whole organization now. Fetches a fresh inventory +Snapshot the caller's whole organization now. Creates a `scope: "org"` `POST /v1/snapshots:capture-org` @@ -275,13 +271,7 @@ epilot snapshot getSnapshot -p id=123e4567-e89b-12d3-a456-426614174000 --jsonata "name": "string", "user_id": "string", "token_id": "string" - }, - "skipped": [ - { - "lineage_id": "string", - "reason": "modified" - } - ] + } }, "restores": [ { @@ -294,13 +284,7 @@ epilot snapshot getSnapshot -p id=123e4567-e89b-12d3-a456-426614174000 --jsonata "name": "string", "user_id": "string", "token_id": "string" - }, - "skipped": [ - { - "lineage_id": "string", - "reason": "modified" - } - ] + } } ], "matched_count": 0, @@ -371,7 +355,7 @@ Restore a snapshot to the org. Async — returns immediately; client polls ```bash epilot snapshot restoreSnapshot \ -p id=123e4567-e89b-12d3-a456-426614174000 \ - -d '{"preserve_modified":false,"preserve_co_owned":false}' + -d '{"exclude_target_ids":["string"]}' ``` Using positional args for path parameters: @@ -495,6 +479,142 @@ epilot snapshot getSnapshotResource -p id=123e4567-e89b-12d3-a456-426614174000 - --- +### `getOrgSnapshotSchedule` + +Return the scheduled-snapshot enrollment config for the caller's org. + +`GET /v1/org-snapshot-schedule` + +**Sample Call** + +```bash +epilot snapshot getOrgSnapshotSchedule +``` + +With JSONata filter: + +```bash +epilot snapshot getOrgSnapshotSchedule --jsonata 'org_id' +``` + +
+Sample Response + +```json +{ + "org_id": "string", + "enabled": true, + "cron_expression": "cron(0 2 * * ? *)", + "timezone": "string", + "retention": { + "value": 1, + "unit": "days" + }, + "excluded_types": ["string"], + "schedule_name": "string", + "last_started_at": "1970-01-01T00:00:00.000Z", + "last_completed_at": "1970-01-01T00:00:00.000Z", + "last_status": "completed", + "created_by": "string", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z" +} +``` + +
+ +--- + +### `putOrgSnapshotSchedule` + +Create or update the scheduled-snapshot enrollment config for the + +`PUT /v1/org-snapshot-schedule` + +**Request Body** + +**Sample Call** + +```bash +epilot snapshot putOrgSnapshotSchedule +``` + +With request body: + +```bash +epilot snapshot putOrgSnapshotSchedule \ + -d '{ + "enabled": true, + "cron_expression": "cron(0 2 * * ? *)", + "timezone": "Europe/Berlin", + "retention": { + "value": 1, + "unit": "days" + }, + "excluded_types": ["string"] +}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot snapshot putOrgSnapshotSchedule +``` + +With JSONata filter: + +```bash +epilot snapshot putOrgSnapshotSchedule --jsonata 'org_id' +``` + +
+Sample Response + +```json +{ + "org_id": "string", + "enabled": true, + "cron_expression": "cron(0 2 * * ? *)", + "timezone": "string", + "retention": { + "value": 1, + "unit": "days" + }, + "excluded_types": ["string"], + "schedule_name": "string", + "last_started_at": "1970-01-01T00:00:00.000Z", + "last_completed_at": "1970-01-01T00:00:00.000Z", + "last_status": "completed", + "created_by": "string", + "created_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z" +} +``` + +
+ +--- + +### `deleteOrgSnapshotSchedule` + +Remove the scheduled-snapshot enrollment for the caller's org. + +`DELETE /v1/org-snapshot-schedule` + +**Sample Call** + +```bash +epilot snapshot deleteOrgSnapshotSchedule +``` + +With JSONata filter: + +```bash +epilot snapshot deleteOrgSnapshotSchedule --jsonata '$' +``` + +--- + ### `listDependencies` Walk the dependency tree for a set of resources and return the full diff --git a/packages/cli/docs/targeting.md b/packages/cli/docs/targeting.md index b1ed0c0db..26159f92b 100644 --- a/packages/cli/docs/targeting.md +++ b/packages/cli/docs/targeting.md @@ -41,6 +41,7 @@ epilot targeting changeCampaignStatus -p campaign_id=b8c01433-5556-4e2b-aad4-6f5 - [`getCampaignPortals`](#getcampaignportals) — Get the list of portals and its widgets where the campaign is used. - [`setupCampaign`](#setupcampaign) — Creates a `campaign` entity together with its related entities and configurations in a single call. - [`matchCampaigns`](#matchcampaigns) — Match campaigns based on target entities. +- [`discoverCampaigns`](#discovercampaigns) — Given an entity, returns the Next Best Actions it should see on the Entity-UI channel. **Campaign Delivery** - [`retriggerCampaignAutomations`](#retriggercampaignautomations) — Retrigger automation executions for specific campaign recipients that have failed. @@ -53,7 +54,10 @@ epilot targeting changeCampaignStatus -p campaign_id=b8c01433-5556-4e2b-aad4-6f5 - [`createRecipient`](#createrecipient) — Creates a new recipient associated with a campaign. - [`updateRecipient`](#updaterecipient) — Updates a recipient's attributes. - [`updateRecipientPortalStatus`](#updaterecipientportalstatus) — Updates the portal status for a specific campaign recipient. +- [`updateRecipientEntityUiStatus`](#updaterecipiententityuistatus) — Records a Next Best Action interaction for a recipient on the Entity-UI channel. +- [`restoreRecipientEntityUiStatus`](#restorerecipiententityuistatus) — Reverses a dismissal so the Next Best Action becomes visible to the entity again. This is - [`getRecipients`](#getrecipients) — Get a paginated list of recipients for a campaign. +- [`getEmailStats`](#getemailstats) — Aggregate email delivery counts for a campaign, for the KPI summary on the campaign UI. ### `changeCampaignStatus` @@ -483,6 +487,102 @@ epilot targeting matchCampaigns --jsonata 'results[0]' --- +### `discoverCampaigns` + +Given an entity, returns the Next Best Actions it should see on the Entity-UI channel. + +`POST /v1/campaign:discover` + +**Request Body** (required) + +**Sample Call** + +```bash +epilot targeting discoverCampaigns \ + -d '{"entity_id":"b8c01433-5556-4e2b-aad4-6f5348d1df84","entity_schema":"string"}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot targeting discoverCampaigns +``` + +With JSONata filter: + +```bash +epilot targeting discoverCampaigns --jsonata 'results[0]' +``` + +
+Sample Response + +```json +{ + "hits": 0, + "results": [ + { + "campaign_id": "b8c01433-5556-4e2b-aad4-6f5348d1df84", + "nba": { + "category": "string", + "icon": { + "name": "string", + "color": "string" + }, + "title": "string", + "body": "string", + "priority": "medium", + "is_dismissable": true, + "cta": { + "type": "journey", + "target": "string", + "context_params": [ + { + "key": "string", + "value": "string" + } + ] + } + }, + "status": "seen", + "status_updated_at": "1970-01-01T00:00:00.000Z" + } + ], + "dismissed": [ + { + "campaign_id": "b8c01433-5556-4e2b-aad4-6f5348d1df84", + "nba": { + "category": "string", + "icon": { + "name": "string", + "color": "string" + }, + "title": "string", + "body": "string", + "priority": "medium", + "is_dismissable": true, + "cta": { + "type": "journey", + "target": "string", + "context_params": [ + { + "key": "string", + "value": "string" + } + ] + } + }, + "status": "seen", + "status_updated_at": "1970-01-01T00:00:00.000Z" + } + ] +} +``` + +
+ +--- + ### `matchTargets` Find targets from the provided list that include the provide entities. @@ -676,6 +776,29 @@ epilot targeting createRecipient -p campaign_id=b8c01433-5556-4e2b-aad4-6f5348d1 "portal_status": "sent", "portal_status_updated_at": "1970-01-01T00:00:00.000Z", "portal_state": {}, + "entity_ui_status": "seen", + "entity_ui_status_updated_at": "1970-01-01T00:00:00.000Z", + "entity_ui_status_before_dismiss": "seen", + "message_entity_id": "string", + "email_status": "sent", + "email_status_updated_at": "1970-01-01T00:00:00.000Z", + "email_bounce_type": "permanent", + "email_bounce_subtype": "string", + "email_complaint_type": "string", + "email_bounce_reason": "string", + "email_send_error": {}, + "email_unsubscribed_at": "1970-01-01T00:00:00.000Z", + "email_open_count": 0, + "email_first_opened_at": "1970-01-01T00:00:00.000Z", + "email_last_opened_at": "1970-01-01T00:00:00.000Z", + "email_click_count": 0, + "email_first_clicked_at": "1970-01-01T00:00:00.000Z", + "email_clicked_links": [ + { + "url": "string", + "clicked_at": "1970-01-01T00:00:00.000Z" + } + ], "updated_at": "1970-01-01T00:00:00.000Z" } ``` @@ -739,6 +862,29 @@ epilot targeting updateRecipient -p campaign_id=b8c01433-5556-4e2b-aad4-6f5348d1 "portal_status": "sent", "portal_status_updated_at": "1970-01-01T00:00:00.000Z", "portal_state": {}, + "entity_ui_status": "seen", + "entity_ui_status_updated_at": "1970-01-01T00:00:00.000Z", + "entity_ui_status_before_dismiss": "seen", + "message_entity_id": "string", + "email_status": "sent", + "email_status_updated_at": "1970-01-01T00:00:00.000Z", + "email_bounce_type": "permanent", + "email_bounce_subtype": "string", + "email_complaint_type": "string", + "email_bounce_reason": "string", + "email_send_error": {}, + "email_unsubscribed_at": "1970-01-01T00:00:00.000Z", + "email_open_count": 0, + "email_first_opened_at": "1970-01-01T00:00:00.000Z", + "email_last_opened_at": "1970-01-01T00:00:00.000Z", + "email_click_count": 0, + "email_first_clicked_at": "1970-01-01T00:00:00.000Z", + "email_clicked_links": [ + { + "url": "string", + "clicked_at": "1970-01-01T00:00:00.000Z" + } + ], "updated_at": "1970-01-01T00:00:00.000Z" } ``` @@ -802,6 +948,192 @@ epilot targeting updateRecipientPortalStatus -p campaign_id=b8c01433-5556-4e2b-a "portal_status": "sent", "portal_status_updated_at": "1970-01-01T00:00:00.000Z", "portal_state": {}, + "entity_ui_status": "seen", + "entity_ui_status_updated_at": "1970-01-01T00:00:00.000Z", + "entity_ui_status_before_dismiss": "seen", + "message_entity_id": "string", + "email_status": "sent", + "email_status_updated_at": "1970-01-01T00:00:00.000Z", + "email_bounce_type": "permanent", + "email_bounce_subtype": "string", + "email_complaint_type": "string", + "email_bounce_reason": "string", + "email_send_error": {}, + "email_unsubscribed_at": "1970-01-01T00:00:00.000Z", + "email_open_count": 0, + "email_first_opened_at": "1970-01-01T00:00:00.000Z", + "email_last_opened_at": "1970-01-01T00:00:00.000Z", + "email_click_count": 0, + "email_first_clicked_at": "1970-01-01T00:00:00.000Z", + "email_clicked_links": [ + { + "url": "string", + "clicked_at": "1970-01-01T00:00:00.000Z" + } + ], + "updated_at": "1970-01-01T00:00:00.000Z" +} +``` + +
+ +--- + +### `updateRecipientEntityUiStatus` + +Records a Next Best Action interaction for a recipient on the Entity-UI channel. + +`PATCH /v1/campaign/{campaign_id}/recipient/{recipient_id}/entity_ui:status` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `campaign_id` | path | string (uuid) | Yes | The campaign ID | +| `recipient_id` | path | string (uuid) | Yes | The entity ID of the recipient | + +**Request Body** (required) + +**Sample Call** + +```bash +epilot targeting updateRecipientEntityUiStatus \ + -p campaign_id=b8c01433-5556-4e2b-aad4-6f5348d1df84 \ + -p recipient_id=b8c01433-5556-4e2b-aad4-6f5348d1df84 \ + -d '{"status":"seen","entity_schema":"string"}' +``` + +Using positional args for path parameters: + +```bash +epilot targeting updateRecipientEntityUiStatus b8c01433-5556-4e2b-aad4-6f5348d1df84 b8c01433-5556-4e2b-aad4-6f5348d1df84 +``` + +Using stdin pipe: + +```bash +cat body.json | epilot targeting updateRecipientEntityUiStatus -p campaign_id=b8c01433-5556-4e2b-aad4-6f5348d1df84 -p recipient_id=b8c01433-5556-4e2b-aad4-6f5348d1df84 +``` + +With JSONata filter: + +```bash +epilot targeting updateRecipientEntityUiStatus -p campaign_id=b8c01433-5556-4e2b-aad4-6f5348d1df84 -p recipient_id=b8c01433-5556-4e2b-aad4-6f5348d1df84 --jsonata 'entity_id' +``` + +
+Sample Response + +```json +{ + "entity_id": "b8c01433-5556-4e2b-aad4-6f5348d1df84", + "entity_schema": "string", + "title": "string", + "automation_status": "pending", + "automation_execution_id": "string", + "portal_status": "sent", + "portal_status_updated_at": "1970-01-01T00:00:00.000Z", + "portal_state": {}, + "entity_ui_status": "seen", + "entity_ui_status_updated_at": "1970-01-01T00:00:00.000Z", + "entity_ui_status_before_dismiss": "seen", + "message_entity_id": "string", + "email_status": "sent", + "email_status_updated_at": "1970-01-01T00:00:00.000Z", + "email_bounce_type": "permanent", + "email_bounce_subtype": "string", + "email_complaint_type": "string", + "email_bounce_reason": "string", + "email_send_error": {}, + "email_unsubscribed_at": "1970-01-01T00:00:00.000Z", + "email_open_count": 0, + "email_first_opened_at": "1970-01-01T00:00:00.000Z", + "email_last_opened_at": "1970-01-01T00:00:00.000Z", + "email_click_count": 0, + "email_first_clicked_at": "1970-01-01T00:00:00.000Z", + "email_clicked_links": [ + { + "url": "string", + "clicked_at": "1970-01-01T00:00:00.000Z" + } + ], + "updated_at": "1970-01-01T00:00:00.000Z" +} +``` + +
+ +--- + +### `restoreRecipientEntityUiStatus` + +Reverses a dismissal so the Next Best Action becomes visible to the entity again. This is + +`POST /v1/campaign/{campaign_id}/recipient/{recipient_id}/entity_ui:restore` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `campaign_id` | path | string (uuid) | Yes | The campaign ID | +| `recipient_id` | path | string (uuid) | Yes | The entity ID of the recipient | + +**Sample Call** + +```bash +epilot targeting restoreRecipientEntityUiStatus \ + -p campaign_id=b8c01433-5556-4e2b-aad4-6f5348d1df84 \ + -p recipient_id=b8c01433-5556-4e2b-aad4-6f5348d1df84 +``` + +Using positional args for path parameters: + +```bash +epilot targeting restoreRecipientEntityUiStatus b8c01433-5556-4e2b-aad4-6f5348d1df84 b8c01433-5556-4e2b-aad4-6f5348d1df84 +``` + +With JSONata filter: + +```bash +epilot targeting restoreRecipientEntityUiStatus -p campaign_id=b8c01433-5556-4e2b-aad4-6f5348d1df84 -p recipient_id=b8c01433-5556-4e2b-aad4-6f5348d1df84 --jsonata 'entity_id' +``` + +
+Sample Response + +```json +{ + "entity_id": "b8c01433-5556-4e2b-aad4-6f5348d1df84", + "entity_schema": "string", + "title": "string", + "automation_status": "pending", + "automation_execution_id": "string", + "portal_status": "sent", + "portal_status_updated_at": "1970-01-01T00:00:00.000Z", + "portal_state": {}, + "entity_ui_status": "seen", + "entity_ui_status_updated_at": "1970-01-01T00:00:00.000Z", + "entity_ui_status_before_dismiss": "seen", + "message_entity_id": "string", + "email_status": "sent", + "email_status_updated_at": "1970-01-01T00:00:00.000Z", + "email_bounce_type": "permanent", + "email_bounce_subtype": "string", + "email_complaint_type": "string", + "email_bounce_reason": "string", + "email_send_error": {}, + "email_unsubscribed_at": "1970-01-01T00:00:00.000Z", + "email_open_count": 0, + "email_first_opened_at": "1970-01-01T00:00:00.000Z", + "email_last_opened_at": "1970-01-01T00:00:00.000Z", + "email_click_count": 0, + "email_first_clicked_at": "1970-01-01T00:00:00.000Z", + "email_clicked_links": [ + { + "url": "string", + "clicked_at": "1970-01-01T00:00:00.000Z" + } + ], "updated_at": "1970-01-01T00:00:00.000Z" } ``` @@ -824,8 +1156,12 @@ Get a paginated list of recipients for a campaign. | `limit` | query | number | No | Number of items to return | | `next` | query | string | No | Cursor for pagination | | `q` | query | string | No | Search by recipient title | -| `automation_status` | query | "pending" \| "in_progress" \| "success" \| "failed" \| "cancelled" | No | Filter by automation status | +| `automation_status` | query | "pending" \| "in_progress" \| "success" \| "failed" \| "cancelled"[] | No | Filter by automation status. Repeat the parameter to match multiple +statuses in one request +(e.g. automation_status=failed&automation_status=cancelled). + | | `portal_status` | query | "sent" \| "seen" \| "dismissed" \| "clicked" | No | Filter by portal status | +| `email_status` | query | "sent" \| "delivered" \| "bounced" \| "complained" \| "failed" | No | Filter by email delivery status (e.g. bounced, delivered). The response total reflects the count for the filtered status. | **Sample Call** @@ -861,6 +1197,29 @@ epilot targeting getRecipients -p campaign_id=b8c01433-5556-4e2b-aad4-6f5348d1df "portal_status": "sent", "portal_status_updated_at": "1970-01-01T00:00:00.000Z", "portal_state": {}, + "entity_ui_status": "seen", + "entity_ui_status_updated_at": "1970-01-01T00:00:00.000Z", + "entity_ui_status_before_dismiss": "seen", + "message_entity_id": "string", + "email_status": "sent", + "email_status_updated_at": "1970-01-01T00:00:00.000Z", + "email_bounce_type": "permanent", + "email_bounce_subtype": "string", + "email_complaint_type": "string", + "email_bounce_reason": "string", + "email_send_error": {}, + "email_unsubscribed_at": "1970-01-01T00:00:00.000Z", + "email_open_count": 0, + "email_first_opened_at": "1970-01-01T00:00:00.000Z", + "email_last_opened_at": "1970-01-01T00:00:00.000Z", + "email_click_count": 0, + "email_first_clicked_at": "1970-01-01T00:00:00.000Z", + "email_clicked_links": [ + { + "url": "string", + "clicked_at": "1970-01-01T00:00:00.000Z" + } + ], "updated_at": "1970-01-01T00:00:00.000Z" } ], @@ -872,3 +1231,59 @@ epilot targeting getRecipients -p campaign_id=b8c01433-5556-4e2b-aad4-6f5348d1df
--- + +### `getEmailStats` + +Aggregate email delivery counts for a campaign, for the KPI summary on the campaign UI. + +`GET /v1/campaign/{campaign_id}/email-stats` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `campaign_id` | path | string (uuid) | Yes | The campaign ID | + +**Sample Call** + +```bash +epilot targeting getEmailStats \ + -p campaign_id=b8c01433-5556-4e2b-aad4-6f5348d1df84 +``` + +Using positional args for path parameters: + +```bash +epilot targeting getEmailStats b8c01433-5556-4e2b-aad4-6f5348d1df84 +``` + +With JSONata filter: + +```bash +epilot targeting getEmailStats -p campaign_id=b8c01433-5556-4e2b-aad4-6f5348d1df84 --jsonata 'total_emailed' +``` + +
+Sample Response + +```json +{ + "total_emailed": 0, + "sent": 0, + "delivered": 0, + "bounced": 0, + "bounced_hard": 0, + "bounced_soft": 0, + "complained": 0, + "failed": 0, + "unsubscribed": 0, + "opened": 0, + "clicked": 0, + "total_opens": 0, + "total_clicks": 0 +} +``` + +
+ +--- diff --git a/packages/cli/docs/user.md b/packages/cli/docs/user.md index 087b4655f..01d1d3214 100644 --- a/packages/cli/docs/user.md +++ b/packages/cli/docs/user.md @@ -53,6 +53,7 @@ epilot user signUpUser - [`getGroupsForUser`](#getgroupsforuser) — Get groups of a user - [`verifyEmailWithToken`](#verifyemailwithtoken) — Update new email using an verification token - [`requestPasswordReset`](#requestpasswordreset) — Request a password reset email for the given email address. Always +- [`resetPassword`](#resetpassword) — Set a new password using a reset token from the password reset email. - [`checkInviteToken`](#checkinvitetoken) — Check an invite token - [`activateUser`](#activateuser) — Activate user using an invite token - [`rejectInvite`](#rejectinvite) — Reject an invite @@ -230,6 +231,8 @@ epilot user getMeV2 --jsonata 'id' "email": "user@example.com", "draft_email": "user@example.com", "draft_email_expires_at": "1970-01-01T00:00:00.000Z", + "password_reset_token_hash": "string", + "password_reset_token_expires_at": "1970-01-01T00:00:00.000Z", "department": "Sales", "phone": 1234567890, "secondary_phone": 1234567890, @@ -271,12 +274,19 @@ epilot user getMeV2 --jsonata 'id' "created_opportunity_auto": true, "deleted_opportunity": true }, + "in_app_notification_setting": { + "integration_critical_error": true, + "integration_error_threshold": true, + "assigned_opportunity": true, + "assigned_task": true + }, "properties": [ { "name": "profileImageName", "value": "avatar.png" } - ] + ], + "tags": ["non-billable"] } ``` @@ -555,6 +565,8 @@ epilot user listUsersV2 --jsonata 'results[0]' "email": "user@example.com", "draft_email": "user@example.com", "draft_email_expires_at": "1970-01-01T00:00:00.000Z", + "password_reset_token_hash": "string", + "password_reset_token_expires_at": "1970-01-01T00:00:00.000Z", "department": "Sales", "phone": 1234567890, "secondary_phone": 1234567890, @@ -596,12 +608,19 @@ epilot user listUsersV2 --jsonata 'results[0]' "created_opportunity_auto": true, "deleted_opportunity": true }, + "in_app_notification_setting": { + "integration_critical_error": true, + "integration_error_threshold": true, + "assigned_opportunity": true, + "assigned_task": true + }, "properties": [ { "name": "profileImageName", "value": "avatar.png" } - ] + ], + "tags": ["non-billable"] } ] } @@ -656,6 +675,8 @@ epilot user getUserV2 -p id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'id' "email": "user@example.com", "draft_email": "user@example.com", "draft_email_expires_at": "1970-01-01T00:00:00.000Z", + "password_reset_token_hash": "string", + "password_reset_token_expires_at": "1970-01-01T00:00:00.000Z", "department": "Sales", "phone": 1234567890, "secondary_phone": 1234567890, @@ -697,12 +718,19 @@ epilot user getUserV2 -p id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'id' "created_opportunity_auto": true, "deleted_opportunity": true }, + "in_app_notification_setting": { + "integration_critical_error": true, + "integration_error_threshold": true, + "assigned_opportunity": true, + "assigned_task": true + }, "properties": [ { "name": "profileImageName", "value": "avatar.png" } - ] + ], + "tags": ["non-billable"] } ``` @@ -746,6 +774,8 @@ epilot user updateUserV2 \ "email": "user@example.com", "draft_email": "user@example.com", "draft_email_expires_at": "1970-01-01T00:00:00.000Z", + "password_reset_token_hash": "string", + "password_reset_token_expires_at": "1970-01-01T00:00:00.000Z", "department": "Sales", "phone": 1234567890, "secondary_phone": 1234567890, @@ -787,12 +817,19 @@ epilot user updateUserV2 \ "created_opportunity_auto": true, "deleted_opportunity": true }, + "in_app_notification_setting": { + "integration_critical_error": true, + "integration_error_threshold": true, + "assigned_opportunity": true, + "assigned_task": true + }, "properties": [ { "name": "profileImageName", "value": "avatar.png" } - ] + ], + "tags": ["non-billable"] }' ``` @@ -828,6 +865,8 @@ epilot user updateUserV2 -p id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'i "email": "user@example.com", "draft_email": "user@example.com", "draft_email_expires_at": "1970-01-01T00:00:00.000Z", + "password_reset_token_hash": "string", + "password_reset_token_expires_at": "1970-01-01T00:00:00.000Z", "department": "Sales", "phone": 1234567890, "secondary_phone": 1234567890, @@ -869,12 +908,19 @@ epilot user updateUserV2 -p id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'i "created_opportunity_auto": true, "deleted_opportunity": true }, + "in_app_notification_setting": { + "integration_critical_error": true, + "integration_error_threshold": true, + "assigned_opportunity": true, + "assigned_task": true + }, "properties": [ { "name": "profileImageName", "value": "avatar.png" } - ] + ], + "tags": ["non-billable"] } ``` @@ -985,6 +1031,8 @@ epilot user inviteUser --jsonata 'id' "email": "user@example.com", "draft_email": "user@example.com", "draft_email_expires_at": "1970-01-01T00:00:00.000Z", + "password_reset_token_hash": "string", + "password_reset_token_expires_at": "1970-01-01T00:00:00.000Z", "department": "Sales", "phone": 1234567890, "secondary_phone": 1234567890, @@ -1026,12 +1074,19 @@ epilot user inviteUser --jsonata 'id' "created_opportunity_auto": true, "deleted_opportunity": true }, + "in_app_notification_setting": { + "integration_critical_error": true, + "integration_error_threshold": true, + "assigned_opportunity": true, + "assigned_task": true + }, "properties": [ { "name": "profileImageName", "value": "avatar.png" } - ] + ], + "tags": ["non-billable"] } ``` @@ -1080,6 +1135,8 @@ epilot user resendUserInvitation --jsonata 'id' "email": "user@example.com", "draft_email": "user@example.com", "draft_email_expires_at": "1970-01-01T00:00:00.000Z", + "password_reset_token_hash": "string", + "password_reset_token_expires_at": "1970-01-01T00:00:00.000Z", "department": "Sales", "phone": 1234567890, "secondary_phone": 1234567890, @@ -1121,12 +1178,19 @@ epilot user resendUserInvitation --jsonata 'id' "created_opportunity_auto": true, "deleted_opportunity": true }, + "in_app_notification_setting": { + "integration_critical_error": true, + "integration_error_threshold": true, + "assigned_opportunity": true, + "assigned_task": true + }, "properties": [ { "name": "profileImageName", "value": "avatar.png" } - ] + ], + "tags": ["non-billable"] } ``` @@ -1227,6 +1291,8 @@ epilot user getGroupsForUser -p id=123e4567-e89b-12d3-a456-426614174000 --jsonat "email": "user@example.com", "draft_email": "user@example.com", "draft_email_expires_at": "1970-01-01T00:00:00.000Z", + "password_reset_token_hash": "string", + "password_reset_token_expires_at": "1970-01-01T00:00:00.000Z", "department": "Sales", "phone": 1234567890, "secondary_phone": 1234567890, @@ -1244,7 +1310,9 @@ epilot user getGroupsForUser -p id=123e4567-e89b-12d3-a456-426614174000 --jsonat "image_uri": {}, "favorites": {}, "email_notification_setting": {}, + "in_app_notification_setting": {}, "properties": [], + "tags": ["non-billable"], "crt_index": 3 }, "users": [ @@ -1364,6 +1432,8 @@ epilot user createGroup --jsonata 'id' "email": "user@example.com", "draft_email": "user@example.com", "draft_email_expires_at": "1970-01-01T00:00:00.000Z", + "password_reset_token_hash": "string", + "password_reset_token_expires_at": "1970-01-01T00:00:00.000Z", "department": "Sales", "phone": 1234567890, "secondary_phone": 1234567890, @@ -1402,9 +1472,16 @@ epilot user createGroup --jsonata 'id' "created_opportunity_auto": true, "deleted_opportunity": true }, + "in_app_notification_setting": { + "integration_critical_error": true, + "integration_error_threshold": true, + "assigned_opportunity": true, + "assigned_task": true + }, "properties": [ {} ], + "tags": ["non-billable"], "crt_index": 3 }, "users": [ @@ -1418,6 +1495,8 @@ epilot user createGroup --jsonata 'id' "email": "user@example.com", "draft_email": "user@example.com", "draft_email_expires_at": "1970-01-01T00:00:00.000Z", + "password_reset_token_hash": "string", + "password_reset_token_expires_at": "1970-01-01T00:00:00.000Z", "department": "Sales", "phone": 1234567890, "secondary_phone": 1234567890, @@ -1435,7 +1514,9 @@ epilot user createGroup --jsonata 'id' "image_uri": {}, "favorites": {}, "email_notification_setting": {}, - "properties": [] + "in_app_notification_setting": {}, + "properties": [], + "tags": ["non-billable"] } ], "image_uri": { @@ -1502,6 +1583,8 @@ epilot user getGroup -p id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'id' "email": "user@example.com", "draft_email": "user@example.com", "draft_email_expires_at": "1970-01-01T00:00:00.000Z", + "password_reset_token_hash": "string", + "password_reset_token_expires_at": "1970-01-01T00:00:00.000Z", "department": "Sales", "phone": 1234567890, "secondary_phone": 1234567890, @@ -1540,9 +1623,16 @@ epilot user getGroup -p id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'id' "created_opportunity_auto": true, "deleted_opportunity": true }, + "in_app_notification_setting": { + "integration_critical_error": true, + "integration_error_threshold": true, + "assigned_opportunity": true, + "assigned_task": true + }, "properties": [ {} ], + "tags": ["non-billable"], "crt_index": 3 }, "users": [ @@ -1556,6 +1646,8 @@ epilot user getGroup -p id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'id' "email": "user@example.com", "draft_email": "user@example.com", "draft_email_expires_at": "1970-01-01T00:00:00.000Z", + "password_reset_token_hash": "string", + "password_reset_token_expires_at": "1970-01-01T00:00:00.000Z", "department": "Sales", "phone": 1234567890, "secondary_phone": 1234567890, @@ -1573,7 +1665,9 @@ epilot user getGroup -p id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'id' "image_uri": {}, "favorites": {}, "email_notification_setting": {}, - "properties": [] + "in_app_notification_setting": {}, + "properties": [], + "tags": ["non-billable"] } ], "image_uri": { @@ -1648,6 +1742,8 @@ epilot user updateGroup -p id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'id "email": "user@example.com", "draft_email": "user@example.com", "draft_email_expires_at": "1970-01-01T00:00:00.000Z", + "password_reset_token_hash": "string", + "password_reset_token_expires_at": "1970-01-01T00:00:00.000Z", "department": "Sales", "phone": 1234567890, "secondary_phone": 1234567890, @@ -1686,9 +1782,16 @@ epilot user updateGroup -p id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'id "created_opportunity_auto": true, "deleted_opportunity": true }, + "in_app_notification_setting": { + "integration_critical_error": true, + "integration_error_threshold": true, + "assigned_opportunity": true, + "assigned_task": true + }, "properties": [ {} ], + "tags": ["non-billable"], "crt_index": 3 }, "users": [ @@ -1702,6 +1805,8 @@ epilot user updateGroup -p id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'id "email": "user@example.com", "draft_email": "user@example.com", "draft_email_expires_at": "1970-01-01T00:00:00.000Z", + "password_reset_token_hash": "string", + "password_reset_token_expires_at": "1970-01-01T00:00:00.000Z", "department": "Sales", "phone": 1234567890, "secondary_phone": 1234567890, @@ -1719,7 +1824,9 @@ epilot user updateGroup -p id=123e4567-e89b-12d3-a456-426614174000 --jsonata 'id "image_uri": {}, "favorites": {}, "email_notification_setting": {}, - "properties": [] + "in_app_notification_setting": {}, + "properties": [], + "tags": ["non-billable"] } ], "image_uri": { @@ -1818,6 +1925,8 @@ epilot user advanceUserAssignment -p id=123e4567-e89b-12d3-a456-426614174000 --j "email": "user@example.com", "draft_email": "user@example.com", "draft_email_expires_at": "1970-01-01T00:00:00.000Z", + "password_reset_token_hash": "string", + "password_reset_token_expires_at": "1970-01-01T00:00:00.000Z", "department": "Sales", "phone": 1234567890, "secondary_phone": 1234567890, @@ -1856,9 +1965,16 @@ epilot user advanceUserAssignment -p id=123e4567-e89b-12d3-a456-426614174000 --j "created_opportunity_auto": true, "deleted_opportunity": true }, + "in_app_notification_setting": { + "integration_critical_error": true, + "integration_error_threshold": true, + "assigned_opportunity": true, + "assigned_task": true + }, "properties": [ {} ], + "tags": ["non-billable"], "crt_index": 3 }, "users": [ @@ -1872,6 +1988,8 @@ epilot user advanceUserAssignment -p id=123e4567-e89b-12d3-a456-426614174000 --j "email": "user@example.com", "draft_email": "user@example.com", "draft_email_expires_at": "1970-01-01T00:00:00.000Z", + "password_reset_token_hash": "string", + "password_reset_token_expires_at": "1970-01-01T00:00:00.000Z", "department": "Sales", "phone": 1234567890, "secondary_phone": 1234567890, @@ -1889,7 +2007,9 @@ epilot user advanceUserAssignment -p id=123e4567-e89b-12d3-a456-426614174000 --j "image_uri": {}, "favorites": {}, "email_notification_setting": {}, - "properties": [] + "in_app_notification_setting": {}, + "properties": [], + "tags": ["non-billable"] } ], "image_uri": { @@ -2151,6 +2271,46 @@ epilot user requestPasswordReset --jsonata 'message' --- +### `resetPassword` + +Set a new password using a reset token from the password reset email. + +`POST /v2/users/public/resetPassword` + +**Request Body** (required) + +**Sample Call** + +```bash +epilot user resetPassword \ + -d '{"email":"test@example.com","token":"string","password":"string"}' +``` + +Using stdin pipe: + +```bash +cat body.json | epilot user resetPassword +``` + +With JSONata filter: + +```bash +epilot user resetPassword --jsonata 'success' +``` + +
+Sample Response + +```json +{ + "success": true +} +``` + +
+ +--- + ### `checkInviteToken` Check an invite token @@ -2316,7 +2476,8 @@ epilot user getUserLoginParametersV2 -p username=example --jsonata 'login_parame "cognito_oauth_domain": "epilot-org-123", "cognito_oauth_scopes": ["openid"], "oauth_response_type": "code", - "passkey_enabled": true + "passkey_enabled": true, + "passkeys_registered": true } ] } @@ -2446,7 +2607,8 @@ epilot user resolveDiscoverableCredential --jsonata 'email' "cognito_oauth_domain": "epilot-org-123", "cognito_oauth_scopes": ["openid"], "oauth_response_type": "code", - "passkey_enabled": true + "passkey_enabled": true, + "passkeys_registered": true } ] } @@ -2874,7 +3036,8 @@ epilot user getUserLoginParameters -p username=example --jsonata 'login_paramete "cognito_oauth_domain": "epilot-org-123", "cognito_oauth_scopes": ["openid"], "oauth_response_type": "code", - "passkey_enabled": true + "passkey_enabled": true, + "passkeys_registered": true } ] } diff --git a/packages/cli/docs/validation-rules.md b/packages/cli/docs/validation-rules.md index aed974980..b041c9b24 100644 --- a/packages/cli/docs/validation-rules.md +++ b/packages/cli/docs/validation-rules.md @@ -3,6 +3,8 @@ - **Base URL:** `https://validation-rules.sls.epilot.io` - **API Docs:** [https://docs.epilot.io/api/validation-rules](https://docs.epilot.io/api/validation-rules) +The Validation Rules API manages reusable input validation rules for epilot journeys and entity attributes. + ## Quick Start ```bash @@ -34,17 +36,17 @@ epilot validation-rules getValidationRules ## Operations **Validation Rules** -- [`getValidationRules`](#getvalidationrules) — Gets all validation rules by organization Id -- [`createValidationRule`](#createvalidationrule) — Creates a new validation rule -- [`getValidationRuleById`](#getvalidationrulebyid) — Retrieves a specific validation rule by its ID -- [`updateValidationRule`](#updatevalidationrule) — Updates an existing validation rule partially by ID -- [`deleteValidationRule`](#deletevalidationrule) — Deletes a validation rule by ID -- [`addUsedByReference`](#addusedbyreference) — Adds a single reference to the usedBy array of a validation rule -- [`removeUsedByReference`](#removeusedbyreference) — Removes a specific reference from the usedBy array of a validation rule +- [`getValidationRules`](#getvalidationrules) — Returns all validation rules belonging to the authenticated user's organization. +- [`createValidationRule`](#createvalidationrule) — Creates a new validation rule for the authenticated organization. +- [`getValidationRuleById`](#getvalidationrulebyid) — Retrieves a specific validation rule by its unique ID. +- [`updateValidationRule`](#updatevalidationrule) — Partially updates an existing validation rule by ID. Only the fields provided in the request body are updated. +- [`deleteValidationRule`](#deletevalidationrule) — Permanently deletes a validation rule by ID. Any journeys or entity attributes referencing this rule should be updated b +- [`addUsedByReference`](#addusedbyreference) — Adds a single `used_by` reference to an existing validation rule. +- [`removeUsedByReference`](#removeusedbyreference) — Removes a specific `used_by` reference from an existing validation rule. ### `getValidationRules` -Gets all validation rules by organization Id +Returns all validation rules belonging to the authenticated user's organization. `GET /v1/validation-rules` @@ -72,8 +74,7 @@ epilot validation-rules getValidationRules --jsonata 'results[0]' "used_by": [ { "type": "journey", - "schema_slug": "string", - "source_id": "string" + "source_id": "journey-xyz789" } ], "rule": { @@ -103,6 +104,11 @@ epilot validation-rules getValidationRules --jsonata 'results[0]' ] } }, + "contexts": [ + { + "schema": "contract" + } + ], "_schema_version": "string", "_id": "string", "_organization_id": "string", @@ -121,11 +127,11 @@ epilot validation-rules getValidationRules --jsonata 'results[0]' ### `createValidationRule` -Creates a new validation rule +Creates a new validation rule for the authenticated organization. `POST /v1/validation-rules` -**Request Body** +**Request Body** (required) **Sample Call** @@ -143,8 +149,7 @@ epilot validation-rules createValidationRule \ "used_by": [ { "type": "journey", - "schema_slug": "string", - "source_id": "string" + "source_id": "journey-xyz789" } ], "rule": { @@ -173,7 +178,12 @@ epilot validation-rules createValidationRule \ } ] } - } + }, + "contexts": [ + { + "schema": "contract" + } + ] }' ``` @@ -199,8 +209,7 @@ epilot validation-rules createValidationRule --jsonata '$' "used_by": [ { "type": "journey", - "schema_slug": "string", - "source_id": "string" + "source_id": "journey-xyz789" } ], "rule": { @@ -230,6 +239,11 @@ epilot validation-rules createValidationRule --jsonata '$' ] } }, + "contexts": [ + { + "schema": "contract" + } + ], "_schema_version": "string", "_id": "string", "_organization_id": "string", @@ -246,7 +260,7 @@ epilot validation-rules createValidationRule --jsonata '$' ### `getValidationRuleById` -Retrieves a specific validation rule by its ID +Retrieves a specific validation rule by its unique ID. `GET /v1/validation-rules/{ruleId}` @@ -260,19 +274,19 @@ Retrieves a specific validation rule by its ID ```bash epilot validation-rules getValidationRuleById \ - -p ruleId=123e4567-e89b-12d3-a456-426614174000 + -p ruleId=rule-abc123 ``` Using positional args for path parameters: ```bash -epilot validation-rules getValidationRuleById 123e4567-e89b-12d3-a456-426614174000 +epilot validation-rules getValidationRuleById rule-abc123 ``` With JSONata filter: ```bash -epilot validation-rules getValidationRuleById -p ruleId=123e4567-e89b-12d3-a456-426614174000 --jsonata '$' +epilot validation-rules getValidationRuleById -p ruleId=rule-abc123 --jsonata '$' ```
@@ -285,8 +299,7 @@ epilot validation-rules getValidationRuleById -p ruleId=123e4567-e89b-12d3-a456- "used_by": [ { "type": "journey", - "schema_slug": "string", - "source_id": "string" + "source_id": "journey-xyz789" } ], "rule": { @@ -316,6 +329,11 @@ epilot validation-rules getValidationRuleById -p ruleId=123e4567-e89b-12d3-a456- ] } }, + "contexts": [ + { + "schema": "contract" + } + ], "_schema_version": "string", "_id": "string", "_organization_id": "string", @@ -332,7 +350,7 @@ epilot validation-rules getValidationRuleById -p ruleId=123e4567-e89b-12d3-a456- ### `updateValidationRule` -Updates an existing validation rule partially by ID +Partially updates an existing validation rule by ID. Only the fields provided in the request body are updated. `PATCH /v1/validation-rules/{ruleId}` @@ -348,22 +366,21 @@ Updates an existing validation rule partially by ID ```bash epilot validation-rules updateValidationRule \ - -p ruleId=123e4567-e89b-12d3-a456-426614174000 + -p ruleId=rule-abc123 ``` With request body: ```bash epilot validation-rules updateValidationRule \ - -p ruleId=123e4567-e89b-12d3-a456-426614174000 \ + -p ruleId=rule-abc123 \ -d '{ "title": "string", "placeholder": "string", "used_by": [ { "type": "journey", - "schema_slug": "string", - "source_id": "string" + "source_id": "journey-xyz789" } ], "rule": { @@ -392,26 +409,31 @@ epilot validation-rules updateValidationRule \ } ] } - } + }, + "contexts": [ + { + "schema": "contract" + } + ] }' ``` Using positional args for path parameters: ```bash -epilot validation-rules updateValidationRule 123e4567-e89b-12d3-a456-426614174000 +epilot validation-rules updateValidationRule rule-abc123 ``` Using stdin pipe: ```bash -cat body.json | epilot validation-rules updateValidationRule -p ruleId=123e4567-e89b-12d3-a456-426614174000 +cat body.json | epilot validation-rules updateValidationRule -p ruleId=rule-abc123 ``` With JSONata filter: ```bash -epilot validation-rules updateValidationRule -p ruleId=123e4567-e89b-12d3-a456-426614174000 --jsonata '$' +epilot validation-rules updateValidationRule -p ruleId=rule-abc123 --jsonata '$' ```
@@ -424,8 +446,7 @@ epilot validation-rules updateValidationRule -p ruleId=123e4567-e89b-12d3-a456-4 "used_by": [ { "type": "journey", - "schema_slug": "string", - "source_id": "string" + "source_id": "journey-xyz789" } ], "rule": { @@ -455,6 +476,11 @@ epilot validation-rules updateValidationRule -p ruleId=123e4567-e89b-12d3-a456-4 ] } }, + "contexts": [ + { + "schema": "contract" + } + ], "_schema_version": "string", "_id": "string", "_organization_id": "string", @@ -471,7 +497,7 @@ epilot validation-rules updateValidationRule -p ruleId=123e4567-e89b-12d3-a456-4 ### `deleteValidationRule` -Deletes a validation rule by ID +Permanently deletes a validation rule by ID. Any journeys or entity attributes referencing this rule should be updated b `DELETE /v1/validation-rules/{ruleId}` @@ -485,26 +511,26 @@ Deletes a validation rule by ID ```bash epilot validation-rules deleteValidationRule \ - -p ruleId=123e4567-e89b-12d3-a456-426614174000 + -p ruleId=rule-abc123 ``` Using positional args for path parameters: ```bash -epilot validation-rules deleteValidationRule 123e4567-e89b-12d3-a456-426614174000 +epilot validation-rules deleteValidationRule rule-abc123 ``` With JSONata filter: ```bash -epilot validation-rules deleteValidationRule -p ruleId=123e4567-e89b-12d3-a456-426614174000 --jsonata '$' +epilot validation-rules deleteValidationRule -p ruleId=rule-abc123 --jsonata '$' ``` --- ### `addUsedByReference` -Adds a single reference to the usedBy array of a validation rule +Adds a single `used_by` reference to an existing validation rule. `POST /v1/validation-rules/{ruleId}/used-by` @@ -520,26 +546,26 @@ Adds a single reference to the usedBy array of a validation rule ```bash epilot validation-rules addUsedByReference \ - -p ruleId=123e4567-e89b-12d3-a456-426614174000 \ - -d '{"type":"journey","schema_slug":"string","source_id":"string"}' + -p ruleId=rule-abc123 \ + -d '{"type":"journey","source_id":"journey-xyz789"}' ``` Using positional args for path parameters: ```bash -epilot validation-rules addUsedByReference 123e4567-e89b-12d3-a456-426614174000 +epilot validation-rules addUsedByReference rule-abc123 ``` Using stdin pipe: ```bash -cat body.json | epilot validation-rules addUsedByReference -p ruleId=123e4567-e89b-12d3-a456-426614174000 +cat body.json | epilot validation-rules addUsedByReference -p ruleId=rule-abc123 ``` With JSONata filter: ```bash -epilot validation-rules addUsedByReference -p ruleId=123e4567-e89b-12d3-a456-426614174000 --jsonata '$' +epilot validation-rules addUsedByReference -p ruleId=rule-abc123 --jsonata '$' ```
@@ -552,8 +578,7 @@ epilot validation-rules addUsedByReference -p ruleId=123e4567-e89b-12d3-a456-426 "used_by": [ { "type": "journey", - "schema_slug": "string", - "source_id": "string" + "source_id": "journey-xyz789" } ], "rule": { @@ -583,6 +608,11 @@ epilot validation-rules addUsedByReference -p ruleId=123e4567-e89b-12d3-a456-426 ] } }, + "contexts": [ + { + "schema": "contract" + } + ], "_schema_version": "string", "_id": "string", "_organization_id": "string", @@ -599,7 +629,7 @@ epilot validation-rules addUsedByReference -p ruleId=123e4567-e89b-12d3-a456-426 ### `removeUsedByReference` -Removes a specific reference from the usedBy array of a validation rule +Removes a specific `used_by` reference from an existing validation rule. `DELETE /v1/validation-rules/{ruleId}/used-by` @@ -615,26 +645,26 @@ Removes a specific reference from the usedBy array of a validation rule ```bash epilot validation-rules removeUsedByReference \ - -p ruleId=123e4567-e89b-12d3-a456-426614174000 \ - -d '{"type":"journey","schema_slug":"string","source_id":"string"}' + -p ruleId=rule-abc123 \ + -d '{"type":"journey","source_id":"journey-xyz789"}' ``` Using positional args for path parameters: ```bash -epilot validation-rules removeUsedByReference 123e4567-e89b-12d3-a456-426614174000 +epilot validation-rules removeUsedByReference rule-abc123 ``` Using stdin pipe: ```bash -cat body.json | epilot validation-rules removeUsedByReference -p ruleId=123e4567-e89b-12d3-a456-426614174000 +cat body.json | epilot validation-rules removeUsedByReference -p ruleId=rule-abc123 ``` With JSONata filter: ```bash -epilot validation-rules removeUsedByReference -p ruleId=123e4567-e89b-12d3-a456-426614174000 --jsonata '$' +epilot validation-rules removeUsedByReference -p ruleId=rule-abc123 --jsonata '$' ```
@@ -647,8 +677,7 @@ epilot validation-rules removeUsedByReference -p ruleId=123e4567-e89b-12d3-a456- "used_by": [ { "type": "journey", - "schema_slug": "string", - "source_id": "string" + "source_id": "journey-xyz789" } ], "rule": { @@ -678,6 +707,11 @@ epilot validation-rules removeUsedByReference -p ruleId=123e4567-e89b-12d3-a456- ] } }, + "contexts": [ + { + "schema": "contract" + } + ], "_schema_version": "string", "_id": "string", "_organization_id": "string", diff --git a/packages/cli/docs/webhooks.md b/packages/cli/docs/webhooks.md index 959863de9..441454b6e 100644 --- a/packages/cli/docs/webhooks.md +++ b/packages/cli/docs/webhooks.md @@ -585,7 +585,7 @@ Replay a batch of webhook events ```bash epilot webhooks batchReplayEvents \ -p configId=7hj28aasgag2gha2 \ - -d '{"eventIds":["2f1b7cf8-ff55-4359-966f-e56f39a52c94","48c984bf-466b-470b-b743-d07cea168243"]}' + -d '{"eventIds":["2f1b7cf8-ff55-4359-966f-e56f39a52c94","48c984bf-466b-470b-b743-d07cea168243"],"reapply_transform":false}' ``` Using positional args for path parameters: @@ -674,7 +674,10 @@ epilot webhooks getEventById -p configId=7hj28aasgag2gha2 -p eventId=123e4567-e8 "status": "succeeded", "http_method": "GET", "payload": "string", - "retry_attempt": 0 + "original_payload": "string", + "retry_attempt": 0, + "can_reapply_transform": true, + "can_reapply_transform_reason": "available" } ``` @@ -695,12 +698,15 @@ Replay a webhook event | `configId` | path | string | Yes | Short uuid to identify the webhook configuration. | | `eventId` | path | string | Yes | Event id | +**Request Body** + **Sample Call** ```bash epilot webhooks replayEvent \ -p configId=7hj28aasgag2gha2 \ - -p eventId=123e4567-e89b-12d3-a456-426614174000 + -p eventId=123e4567-e89b-12d3-a456-426614174000 \ + -d '{"reapply_transform":false}' ``` Using positional args for path parameters: @@ -709,6 +715,12 @@ Using positional args for path parameters: epilot webhooks replayEvent 7hj28aasgag2gha2 123e4567-e89b-12d3-a456-426614174000 ``` +Using stdin pipe: + +```bash +cat body.json | epilot webhooks replayEvent -p configId=7hj28aasgag2gha2 -p eventId=123e4567-e89b-12d3-a456-426614174000 +``` + With JSONata filter: ```bash @@ -880,7 +892,10 @@ epilot webhooks getWebhookEventsV2 -p configId=7hj28aasgag2gha2 --jsonata 'data' "status": "succeeded", "http_method": "GET", "payload": "string", - "retry_attempt": 0 + "original_payload": "string", + "retry_attempt": 0, + "can_reapply_transform": true, + "can_reapply_transform_reason": "available" } ], "next_cursor": { diff --git a/packages/cli/docs/workflow-definition.md b/packages/cli/docs/workflow-definition.md index c4a7580a9..acc72b4e8 100644 --- a/packages/cli/docs/workflow-definition.md +++ b/packages/cli/docs/workflow-definition.md @@ -3,7 +3,9 @@ - **Base URL:** `https://workflows-definition.sls.epilot.io` - **API Docs:** [https://docs.epilot.io/api/workflow-definition](https://docs.epilot.io/api/workflow-definition) -Service for Workflow Definitions for different processes inside of an Organization +The Workflows Definitions API enables you to create, manage, and configure reusable workflow templates +within your organization. Workflow definitions serve as blueprints that define the structure and behavior +of business processes, which can then be instantiated as workflow executions. ## Quick Start @@ -37,8 +39,8 @@ epilot workflow-definition getMaxAllowedLimit **Workflows** - [`getMaxAllowedLimit`](#getmaxallowedlimit) — Get limits and number of created executions for an Organization. -- [`getDefinitions`](#getdefinitions) — Retrieve all Workflow Definitions from an Organization -- [`createDefinition`](#createdefinition) — Create a Workflow Definition. +- [`getDefinitions`](#getdefinitions) — Retrieve all V1 workflow definitions belonging to the authenticated organization. +- [`createDefinition`](#createdefinition) — Create a new V1 workflow definition. The definition consists of sections and steps - [`getDefinition`](#getdefinition) — Get specific Definition by id from the Organization. - [`updateDefinition`](#updatedefinition) — Update Workflow Definition. - [`deleteDefinition`](#deletedefinition) — Delete Workflow Definition. @@ -47,12 +49,12 @@ epilot workflow-definition getMaxAllowedLimit **Flows V2** - [`listFlowTemplates`](#listflowtemplates) — List all Flow Templates for a customer. Optionally, you can filter flow templates by trigger values. -- [`createFlowTemplate`](#createflowtemplate) — Create a new Flow Template. +- [`createFlowTemplate`](#createflowtemplate) — Create a new Flow Template (V2 workflow definition). - [`searchFlowTemplates`](#searchflowtemplates) — Search for flow templates by name, trigger type, enabled status, and more. -- [`getFlowTemplate`](#getflowtemplate) — Get specific FLow template for a customer +- [`getFlowTemplate`](#getflowtemplate) — Retrieve a specific flow template by its unique identifier. - [`updateFlowTemplate`](#updateflowtemplate) — Update Flow Template. - [`deleteFlowTemplate`](#deleteflowtemplate) — Delete Flow Template. -- [`duplicateFlowTemplate`](#duplicateflowtemplate) — Duplicate a Flow Template from an existing workflow. +- [`duplicateFlowTemplate`](#duplicateflowtemplate) — Create a copy of an existing flow template. The duplicated template will have a new **Closing Reason** - [`getAllClosingReasons`](#getallclosingreasons) — Get all Closing Reasons defined in the organization by default all Active. @@ -96,7 +98,7 @@ epilot workflow-definition getMaxAllowedLimit --jsonata 'currentNoOfWorkflows' ### `getDefinitions` -Retrieve all Workflow Definitions from an Organization +Retrieve all V1 workflow definitions belonging to the authenticated organization. `GET /v1/workflows/definitions` @@ -158,7 +160,7 @@ epilot workflow-definition getDefinitions --jsonata '$' ### `createDefinition` -Create a Workflow Definition. +Create a new V1 workflow definition. The definition consists of sections and steps `POST /v1/workflows/definitions` @@ -385,7 +387,9 @@ epilot workflow-definition listFlowTemplates --jsonata 'results[0]' "entity_sync": [], "taxonomies": ["string"], "singleClosingReasonSelection": true, - "_manifest": ["string"] + "_manifest": ["string"], + "linear": true, + "limit_warnings": [] } ] } @@ -397,10 +401,16 @@ epilot workflow-definition listFlowTemplates --jsonata 'results[0]' ### `createFlowTemplate` -Create a new Flow Template. +Create a new Flow Template (V2 workflow definition). `POST /v2/flows/templates` +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `enforce_limits` | query | boolean | No | When true, size/count limit violations (max incoming edges per task, max branches, max entity-sync rules, ...) are enforced as 400 errors. Used by the flow-builder UI for interactive authoring. On upd | + **Request Body** (required) **Sample Call** @@ -525,7 +535,21 @@ epilot workflow-definition createFlowTemplate \ ], "taxonomies": ["string"], "singleClosingReasonSelection": true, - "_manifest": ["string"] + "_manifest": ["string"], + "linear": true, + "limit_warnings": [ + { + "i18nKey": "string", + "message": "string", + "max": 0, + "current": 0, + "node_id": "string", + "task_name": "string", + "branch_name": "string", + "param_name": "string", + "path": [] + } + ] }' ``` @@ -657,7 +681,21 @@ epilot workflow-definition createFlowTemplate --jsonata '$' ], "taxonomies": ["string"], "singleClosingReasonSelection": true, - "_manifest": ["string"] + "_manifest": ["string"], + "linear": true, + "limit_warnings": [ + { + "i18nKey": "string", + "message": "string", + "max": 0, + "current": 0, + "node_id": "string", + "task_name": "string", + "branch_name": "string", + "param_name": "string", + "path": [] + } + ] } ``` @@ -736,7 +774,9 @@ epilot workflow-definition searchFlowTemplates --jsonata 'results[0]' "entity_sync": [], "taxonomies": ["string"], "singleClosingReasonSelection": true, - "_manifest": ["string"] + "_manifest": ["string"], + "linear": true, + "limit_warnings": [] } ] } @@ -748,7 +788,7 @@ epilot workflow-definition searchFlowTemplates --jsonata 'results[0]' ### `getFlowTemplate` -Get specific FLow template for a customer +Retrieve a specific flow template by its unique identifier. `GET /v2/flows/templates/{flowId}` @@ -756,7 +796,7 @@ Get specific FLow template for a customer | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `flowId` | path | string | Yes | | +| `flowId` | path | string | Yes | Unique identifier of the flow template to retrieve. | **Sample Call** @@ -893,7 +933,21 @@ epilot workflow-definition getFlowTemplate -p flowId=7hj28akg --jsonata '$' ], "taxonomies": ["string"], "singleClosingReasonSelection": true, - "_manifest": ["string"] + "_manifest": ["string"], + "linear": true, + "limit_warnings": [ + { + "i18nKey": "string", + "message": "string", + "max": 0, + "current": 0, + "node_id": "string", + "task_name": "string", + "branch_name": "string", + "param_name": "string", + "path": [] + } + ] } ``` @@ -911,7 +965,8 @@ Update Flow Template. | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `flowId` | path | string | Yes | | +| `flowId` | path | string | Yes | Unique identifier of the flow template to update. | +| `enforce_limits` | query | boolean | No | When true, size/count limit violations (max incoming edges per task, max branches, max entity-sync rules, ...) are enforced as 400 errors. Used by the flow-builder UI for interactive authoring. On upd | **Request Body** (required) @@ -1039,7 +1094,21 @@ epilot workflow-definition updateFlowTemplate \ ], "taxonomies": ["string"], "singleClosingReasonSelection": true, - "_manifest": ["string"] + "_manifest": ["string"], + "linear": true, + "limit_warnings": [ + { + "i18nKey": "string", + "message": "string", + "max": 0, + "current": 0, + "node_id": "string", + "task_name": "string", + "branch_name": "string", + "param_name": "string", + "path": [] + } + ] }' ``` @@ -1177,7 +1246,21 @@ epilot workflow-definition updateFlowTemplate -p flowId=7hj28akg --jsonata '$' ], "taxonomies": ["string"], "singleClosingReasonSelection": true, - "_manifest": ["string"] + "_manifest": ["string"], + "linear": true, + "limit_warnings": [ + { + "i18nKey": "string", + "message": "string", + "max": 0, + "current": 0, + "node_id": "string", + "task_name": "string", + "branch_name": "string", + "param_name": "string", + "path": [] + } + ] } ``` @@ -1195,7 +1278,7 @@ Delete Flow Template. | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `flowId` | path | string | Yes | Id of the flow template to de deleted. | +| `flowId` | path | string | Yes | Unique identifier of the flow template to be deleted. | **Sample Call** @@ -1220,7 +1303,7 @@ epilot workflow-definition deleteFlowTemplate -p flowId=7hj28akg --jsonata '$' ### `duplicateFlowTemplate` -Duplicate a Flow Template from an existing workflow. +Create a copy of an existing flow template. The duplicated template will have a new `POST /v2/flows/templates/{flowId}/duplicate` @@ -1228,7 +1311,7 @@ Duplicate a Flow Template from an existing workflow. | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `flowId` | path | string | Yes | | +| `flowId` | path | string | Yes | Unique identifier of the flow template to duplicate. | **Sample Call** @@ -1365,7 +1448,21 @@ epilot workflow-definition duplicateFlowTemplate -p flowId=7hj28akg --jsonata '$ ], "taxonomies": ["string"], "singleClosingReasonSelection": true, - "_manifest": ["string"] + "_manifest": ["string"], + "linear": true, + "limit_warnings": [ + { + "i18nKey": "string", + "message": "string", + "max": 0, + "current": 0, + "node_id": "string", + "task_name": "string", + "branch_name": "string", + "param_name": "string", + "path": [] + } + ] } ``` @@ -1675,7 +1772,7 @@ Delete Workflow Definition. | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `definitionId` | path | string | Yes | Id of the definition to de deleted. | +| `definitionId` | path | string | Yes | Unique identifier of the workflow definition to be deleted. | **Sample Call** @@ -1708,7 +1805,7 @@ Get all Closing Reasons defined in the organization by default all Active. | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `includeInactive` | query | boolean | No | Filter Closing Reasons by status like active inactiv | +| `includeInactive` | query | boolean | No | When set to true, includes inactive closing reasons in the response. By default, only active closing reasons are returned. | **Sample Call** diff --git a/packages/cli/docs/workflow.md b/packages/cli/docs/workflow.md index bfe67f8b3..a91aa3902 100644 --- a/packages/cli/docs/workflow.md +++ b/packages/cli/docs/workflow.md @@ -3,7 +3,10 @@ - **Base URL:** `https://workflows-execution.sls.epilot.io` - **API Docs:** [https://docs.epilot.io/api/workflow](https://docs.epilot.io/api/workflow) -Service for Workflow Executions which covers executions of processes defined in an Organization +Overview +The Workflows Executions API manages the runtime instances of workflow processes within an organization. +While **Workflow Definitions** (managed by the Workflows Definitions API) serve as reusable templates +that define the structure, phases, and tasks of a process, **Workflow Executions** a ## Quick Start @@ -37,31 +40,32 @@ epilot workflow getExecutions **Workflow Executions** - [`getExecutions`](#getexecutions) — Retrieve Workflow Executions. Optionally, you can filter them by context & schema. Please be aware, these executions are -- [`createExecution`](#createexecution) — Create a Workflow Execution. Start a new workflow execution, based on a workflow definition (template). -- [`getExecution`](#getexecution) — Get a full workflow execution, included steps information, by execution id. +- [`createExecution`](#createexecution) — Creates a new V1 Workflow Execution from a workflow definition (template). +- [`getExecution`](#getexecution) — Retrieves a complete V1 workflow execution by ID, including all steps information. - [`updateExecution`](#updateexecution) — Patches updates like assignees, status, closingReason for a single Workflow Execution. - [`deleteExecution`](#deleteexecution) — Delete workflow execution by id. Workflow contexts will NOT be deleted. **Workflow Steps** - [`createStep`](#createstep) — Create a new step in current workflow execution. -- [`updateStep`](#updatestep) — Patches various changes to a workflow execution step. +- [`updateStep`](#updatestep) — Updates a workflow execution step with new values for status, assignees, due date, position, and more. - [`deleteStep`](#deletestep) — Deletes a step from a workflow execution. **Closing Reasons** - [`getClosingReasonExecution`](#getclosingreasonexecution) — Shows all Closing Reasons defined at the moment of starting the Workflow Execution. **Flows V2** -- [`startFlowExecution`](#startflowexecution) — Starts a new Flow Execution based on a flow template. -- [`getFlowExecution`](#getflowexecution) — Get a full flow execution, included tasks, phases, edges & analytics. +- [`startFlowExecution`](#startflowexecution) — Starts a new Flow Execution based on a flow template (definition). +- [`getFlowExecution`](#getflowexecution) — Retrieves a complete flow execution by ID, including all phases, tasks, edges, contexts, and analytics. - [`patchFlowExecution`](#patchflowexecution) — Patch flow execution with new assignees, status, analytics & other changes. - [`deleteFlowExecution`](#deleteflowexecution) — Deletes a specific execution of a flow, identified by id. Flow contexts will NOT be deleted. - [`searchFlowExecutions`](#searchflowexecutions) — Search Flow Executions for a specific Entity. -- [`patchTask`](#patchtask) — Changes various attributes of a flow task, like assignees, status, due date, etc. +- [`patchTask`](#patchtask) — Updates attributes of a flow task including status, assignees, due date, and more. - [`runTaskAutomation`](#runtaskautomation) — Runs configured automation for a flow task - [`executeTask`](#executetask) — Executes any kind of flow task immediately. - [`patchPhase`](#patchphase) — Apply updates to a phase within flow execution - [`addTask`](#addtask) — Create a new task in current workflow execution. - [`cancelTaskSchedule`](#canceltaskschedule) — Cancels a scheduled task, deleting the schedule and marking the task as skipped. +- [`reconcileAutomationTask`](#reconcileautomationtask) — Reconciles an automation task's status against its linked automation execution. - [`runTaskScheduleNow`](#runtaskschedulenow) — Cancels the pending schedule for a task and immediately triggers its automation execution. ### `getExecutions` @@ -138,7 +142,7 @@ epilot workflow getExecutions --jsonata '$' ### `createExecution` -Create a Workflow Execution. Start a new workflow execution, based on a workflow definition (template). +Creates a new V1 Workflow Execution from a workflow definition (template). `POST /v1/workflows/executions` @@ -232,7 +236,7 @@ epilot workflow createExecution --jsonata '$' ### `getExecution` -Get a full workflow execution, included steps information, by execution id. +Retrieves a complete V1 workflow execution by ID, including all steps information. `GET /v1/workflows/executions/{executionId}` @@ -572,7 +576,7 @@ epilot workflow createStep -p executionId=wd56125gah --jsonata '$' ### `updateStep` -Patches various changes to a workflow execution step. +Updates a workflow execution step with new values for status, assignees, due date, position, and more. `PATCH /v1/workflows/executions/{executionId}/steps/{stepId}` @@ -580,8 +584,8 @@ Patches various changes to a workflow execution step. | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `executionId` | path | string | Yes | Id of the execution | -| `stepId` | path | string | Yes | Short uuid (length 6) to identify the Workflow Execution Step. | +| `executionId` | path | string | Yes | Unique identifier of the workflow execution | +| `stepId` | path | string | Yes | Short unique identifier (typically 6 characters) of the step within the execution | **Request Body** (required) @@ -828,7 +832,7 @@ epilot workflow getClosingReasonExecution -p executionId=wd561 --jsonata 'reason ### `startFlowExecution` -Starts a new Flow Execution based on a flow template. +Starts a new Flow Execution based on a flow template (definition). `POST /v2/flows/executions` @@ -845,7 +849,7 @@ With request body: ```bash epilot workflow startFlowExecution \ -d '{ - "flow_template_id": "string", + "flow_template_id": "tpl_abc123def456", "trigger": { "type": "MANUAL", "automation_config": { @@ -856,7 +860,9 @@ epilot workflow startFlowExecution \ "input_context": { "source": "trigger", "task_id": "string" - } + }, + "heal_attempts": 0, + "last_heal_attempted_at": "1970-01-01T00:00:00.000Z" } }, "contexts": [ @@ -919,7 +925,8 @@ epilot workflow startFlowExecution --jsonata 'id' ], "crt_tasks": [ { - "id": "string" + "id": "string", + "crt_since": "1970-01-01T00:00:00.000Z" } ], "phases": [ @@ -1002,10 +1009,13 @@ epilot workflow startFlowExecution --jsonata 'id' "execution_id": "string", "execution_status": "string", "error_reason": "string", - "input_context": {} + "input_context": {}, + "heal_attempts": 0, + "last_heal_attempted_at": "1970-01-01T00:00:00.000Z" } }, - "singleClosingReasonSelection": true + "singleClosingReasonSelection": true, + "linear": true } ``` @@ -1015,7 +1025,7 @@ epilot workflow startFlowExecution --jsonata 'id' ### `getFlowExecution` -Get a full flow execution, included tasks, phases, edges & analytics. +Retrieves a complete flow execution by ID, including all phases, tasks, edges, contexts, and analytics. `GET /v2/flows/executions/{execution_id}` @@ -1023,25 +1033,27 @@ Get a full flow execution, included tasks, phases, edges & analytics. | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `execution_id` | path | string | Yes | Id of the execution | +| `execution_id` | path | string | Yes | Unique identifier for the flow execution. This ID is generated when the execution +is created and remains constant throughout its lifecycle. + | **Sample Call** ```bash epilot workflow getFlowExecution \ - -p execution_id=wd561 + -p execution_id=exec_abc123def456 ``` Using positional args for path parameters: ```bash -epilot workflow getFlowExecution wd561 +epilot workflow getFlowExecution exec_abc123def456 ``` With JSONata filter: ```bash -epilot workflow getFlowExecution -p execution_id=wd561 --jsonata 'id' +epilot workflow getFlowExecution -p execution_id=exec_abc123def456 --jsonata 'id' ```
@@ -1081,7 +1093,8 @@ epilot workflow getFlowExecution -p execution_id=wd561 --jsonata 'id' ], "crt_tasks": [ { - "id": "string" + "id": "string", + "crt_since": "1970-01-01T00:00:00.000Z" } ], "phases": [ @@ -1164,10 +1177,13 @@ epilot workflow getFlowExecution -p execution_id=wd561 --jsonata 'id' "execution_id": "string", "execution_status": "string", "error_reason": "string", - "input_context": {} + "input_context": {}, + "heal_attempts": 0, + "last_heal_attempted_at": "1970-01-01T00:00:00.000Z" } }, - "singleClosingReasonSelection": true + "singleClosingReasonSelection": true, + "linear": true } ``` @@ -1185,7 +1201,9 @@ Patch flow execution with new assignees, status, analytics & other changes. | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `execution_id` | path | string | Yes | Id of the execution | +| `execution_id` | path | string | Yes | Unique identifier for the flow execution. This ID is generated when the execution +is created and remains constant throughout its lifecycle. + | **Request Body** (required) @@ -1193,14 +1211,14 @@ Patch flow execution with new assignees, status, analytics & other changes. ```bash epilot workflow patchFlowExecution \ - -p execution_id=wd561 + -p execution_id=exec_abc123def456 ``` With request body: ```bash epilot workflow patchFlowExecution \ - -p execution_id=wd561 \ + -p execution_id=exec_abc123def456 \ -d '{ "status": "STARTED", "assigned_to": ["string"], @@ -1240,19 +1258,19 @@ epilot workflow patchFlowExecution \ Using positional args for path parameters: ```bash -epilot workflow patchFlowExecution wd561 +epilot workflow patchFlowExecution exec_abc123def456 ``` Using stdin pipe: ```bash -cat body.json | epilot workflow patchFlowExecution -p execution_id=wd561 +cat body.json | epilot workflow patchFlowExecution -p execution_id=exec_abc123def456 ``` With JSONata filter: ```bash -epilot workflow patchFlowExecution -p execution_id=wd561 --jsonata 'id' +epilot workflow patchFlowExecution -p execution_id=exec_abc123def456 --jsonata 'id' ```
@@ -1292,7 +1310,8 @@ epilot workflow patchFlowExecution -p execution_id=wd561 --jsonata 'id' ], "crt_tasks": [ { - "id": "string" + "id": "string", + "crt_since": "1970-01-01T00:00:00.000Z" } ], "phases": [ @@ -1375,10 +1394,13 @@ epilot workflow patchFlowExecution -p execution_id=wd561 --jsonata 'id' "execution_id": "string", "execution_status": "string", "error_reason": "string", - "input_context": {} + "input_context": {}, + "heal_attempts": 0, + "last_heal_attempted_at": "1970-01-01T00:00:00.000Z" } }, - "singleClosingReasonSelection": true + "singleClosingReasonSelection": true, + "linear": true } ``` @@ -1396,26 +1418,30 @@ Deletes a specific execution of a flow, identified by id. Flow contexts will NOT | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `execution_id` | path | string | Yes | Id of the execution | -| `soft` | query | boolean | No | If true, the execution will NOT be deleted permanently, but rather kept for archive purpose. | +| `execution_id` | path | string | Yes | Unique identifier for the flow execution. This ID is generated when the execution +is created and remains constant throughout its lifecycle. + | +| `soft` | query | boolean | No | When true, the execution is marked as deleted but retained in storage for archival +and audit purposes. When false (default), the execution is permanently removed. +Soft-deleted executions do not appear | **Sample Call** ```bash epilot workflow deleteFlowExecution \ - -p execution_id=wd561 + -p execution_id=exec_abc123def456 ``` Using positional args for path parameters: ```bash -epilot workflow deleteFlowExecution wd561 +epilot workflow deleteFlowExecution exec_abc123def456 ``` With JSONata filter: ```bash -epilot workflow deleteFlowExecution -p execution_id=wd561 --jsonata '$' +epilot workflow deleteFlowExecution -p execution_id=exec_abc123def456 --jsonata '$' ``` --- @@ -1476,7 +1502,8 @@ epilot workflow searchFlowExecutions --jsonata 'results[0]' "entity_sync": [], "taxonomies": ["string"], "trigger": {}, - "singleClosingReasonSelection": true + "singleClosingReasonSelection": true, + "linear": true } ] } @@ -1488,7 +1515,7 @@ epilot workflow searchFlowExecutions --jsonata 'results[0]' ### `patchTask` -Changes various attributes of a flow task, like assignees, status, due date, etc. +Updates attributes of a flow task including status, assignees, due date, and more. `PATCH /v2/flows/executions/{execution_id}/tasks/{task_id}` @@ -1496,8 +1523,12 @@ Changes various attributes of a flow task, like assignees, status, due date, etc | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `execution_id` | path | string | Yes | Id of the execution | -| `task_id` | path | string | Yes | Id of the task | +| `execution_id` | path | string | Yes | Unique identifier for the flow execution. This ID is generated when the execution +is created and remains constant throughout its lifecycle. + | +| `task_id` | path | string | Yes | Unique identifier for a task within the execution. Each task has a unique ID +that distinguishes it from other tasks in the same execution. + | **Request Body** (required) @@ -1505,20 +1536,20 @@ Changes various attributes of a flow task, like assignees, status, due date, etc ```bash epilot workflow patchTask \ - -p execution_id=wd561 \ - -p task_id=7hj28a + -p execution_id=exec_abc123def456 \ + -p task_id=task_7hj28a ``` With request body: ```bash epilot workflow patchTask \ - -p execution_id=wd561 \ - -p task_id=7hj28a \ + -p execution_id=exec_abc123def456 \ + -p task_id=task_7hj28a \ -d '{ - "name": "string", + "name": "Review customer application", "status": "UNASSIGNED", - "due_date": "2021-04-27T12:00:00.000Z", + "due_date": "2026-05-28T00:00:00.000", "due_date_config": { "duration": 0, "unit": "minutes", @@ -1536,7 +1567,9 @@ epilot workflow patchTask \ "input_context": { "source": "trigger", "task_id": "string" - } + }, + "heal_attempts": 0, + "last_heal_attempted_at": "1970-01-01T00:00:00.000Z" }, "description": { "enabled": true, @@ -1569,27 +1602,28 @@ epilot workflow patchTask \ "label": "string", "description": "string" }, - "next_condition_id": "string", - "revert_execution": false + "next_condition_id": "cond_branch_approved", + "revert_execution": false, + "completed_via_journey": false }' ``` Using positional args for path parameters: ```bash -epilot workflow patchTask wd561 7hj28a +epilot workflow patchTask exec_abc123def456 task_7hj28a ``` Using stdin pipe: ```bash -cat body.json | epilot workflow patchTask -p execution_id=wd561 -p task_id=7hj28a +cat body.json | epilot workflow patchTask -p execution_id=exec_abc123def456 -p task_id=task_7hj28a ``` With JSONata filter: ```bash -epilot workflow patchTask -p execution_id=wd561 -p task_id=7hj28a --jsonata '$' +epilot workflow patchTask -p execution_id=exec_abc123def456 -p task_id=task_7hj28a --jsonata '$' ```
@@ -1692,27 +1726,31 @@ Runs configured automation for a flow task | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `execution_id` | path | string | Yes | Id of the execution | -| `task_id` | path | string | Yes | Id of the task | +| `execution_id` | path | string | Yes | Unique identifier for the flow execution. This ID is generated when the execution +is created and remains constant throughout its lifecycle. + | +| `task_id` | path | string | Yes | Unique identifier for a task within the execution. Each task has a unique ID +that distinguishes it from other tasks in the same execution. + | **Sample Call** ```bash epilot workflow runTaskAutomation \ - -p execution_id=wd561 \ - -p task_id=7hj28a + -p execution_id=exec_abc123def456 \ + -p task_id=task_7hj28a ``` Using positional args for path parameters: ```bash -epilot workflow runTaskAutomation wd561 7hj28a +epilot workflow runTaskAutomation exec_abc123def456 task_7hj28a ``` With JSONata filter: ```bash -epilot workflow runTaskAutomation -p execution_id=wd561 -p task_id=7hj28a --jsonata '$' +epilot workflow runTaskAutomation -p execution_id=exec_abc123def456 -p task_id=task_7hj28a --jsonata '$' ```
@@ -1802,7 +1840,9 @@ epilot workflow runTaskAutomation -p execution_id=wd561 -p task_id=7hj28a --json "input_context": { "source": "trigger", "task_id": "string" - } + }, + "heal_attempts": 0, + "last_heal_attempted_at": "1970-01-01T00:00:00.000Z" }, "automation_execution_id": "string", "trigger_mode": "manual", @@ -1830,27 +1870,31 @@ Executes any kind of flow task immediately. | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `execution_id` | path | string | Yes | Id of the execution | -| `task_id` | path | string | Yes | Id of the task | +| `execution_id` | path | string | Yes | Unique identifier for the flow execution. This ID is generated when the execution +is created and remains constant throughout its lifecycle. + | +| `task_id` | path | string | Yes | Unique identifier for a task within the execution. Each task has a unique ID +that distinguishes it from other tasks in the same execution. + | **Sample Call** ```bash epilot workflow executeTask \ - -p execution_id=wd561 \ - -p task_id=7hj28a + -p execution_id=exec_abc123def456 \ + -p task_id=task_7hj28a ``` Using positional args for path parameters: ```bash -epilot workflow executeTask wd561 7hj28a +epilot workflow executeTask exec_abc123def456 task_7hj28a ``` With JSONata filter: ```bash -epilot workflow executeTask -p execution_id=wd561 -p task_id=7hj28a --jsonata '$' +epilot workflow executeTask -p execution_id=exec_abc123def456 -p task_id=task_7hj28a --jsonata '$' ```
@@ -1953,8 +1997,12 @@ Apply updates to a phase within flow execution | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `execution_id` | path | string | Yes | Id of the execution | -| `phase_id` | path | string | Yes | Id of the phase | +| `execution_id` | path | string | Yes | Unique identifier for the flow execution. This ID is generated when the execution +is created and remains constant throughout its lifecycle. + | +| `phase_id` | path | string | Yes | Unique identifier for a phase within the execution. Phases group related tasks +together and track collective progress. + | **Request Body** (required) @@ -1962,16 +2010,16 @@ Apply updates to a phase within flow execution ```bash epilot workflow patchPhase \ - -p execution_id=wd561 \ - -p phase_id=9gjs2952j + -p execution_id=exec_abc123def456 \ + -p phase_id=phase_9gjs2952j ``` With request body: ```bash epilot workflow patchPhase \ - -p execution_id=wd561 \ - -p phase_id=9gjs2952j \ + -p execution_id=exec_abc123def456 \ + -p phase_id=phase_9gjs2952j \ -d '{ "name": "string", "due_date": "2021-04-27T12:00:00.000Z", @@ -1989,19 +2037,19 @@ epilot workflow patchPhase \ Using positional args for path parameters: ```bash -epilot workflow patchPhase wd561 9gjs2952j +epilot workflow patchPhase exec_abc123def456 phase_9gjs2952j ``` Using stdin pipe: ```bash -cat body.json | epilot workflow patchPhase -p execution_id=wd561 -p phase_id=9gjs2952j +cat body.json | epilot workflow patchPhase -p execution_id=exec_abc123def456 -p phase_id=phase_9gjs2952j ``` With JSONata filter: ```bash -epilot workflow patchPhase -p execution_id=wd561 -p phase_id=9gjs2952j --jsonata 'id' +epilot workflow patchPhase -p execution_id=exec_abc123def456 -p phase_id=phase_9gjs2952j --jsonata 'id' ```
@@ -2055,7 +2103,9 @@ Create a new task in current workflow execution. | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `execution_id` | path | string | Yes | Id of the execution | +| `execution_id` | path | string | Yes | Unique identifier for the flow execution. This ID is generated when the execution +is created and remains constant throughout its lifecycle. + | **Request Body** (required) @@ -2063,14 +2113,14 @@ Create a new task in current workflow execution. ```bash epilot workflow addTask \ - -p execution_id=wd561 + -p execution_id=exec_abc123def456 ``` With request body: ```bash epilot workflow addTask \ - -p execution_id=wd561 \ + -p execution_id=exec_abc123def456 \ -d '{ "previous_task_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "next_task_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", @@ -2096,7 +2146,9 @@ epilot workflow addTask \ "input_context": { "source": "trigger", "task_id": "string" - } + }, + "heal_attempts": 0, + "last_heal_attempted_at": "1970-01-01T00:00:00.000Z" }, "phase_id": "string", "task_type": "MANUAL" @@ -2107,19 +2159,19 @@ epilot workflow addTask \ Using positional args for path parameters: ```bash -epilot workflow addTask wd561 +epilot workflow addTask exec_abc123def456 ``` Using stdin pipe: ```bash -cat body.json | epilot workflow addTask -p execution_id=wd561 +cat body.json | epilot workflow addTask -p execution_id=exec_abc123def456 ``` With JSONata filter: ```bash -epilot workflow addTask -p execution_id=wd561 --jsonata '$' +epilot workflow addTask -p execution_id=exec_abc123def456 --jsonata '$' ```
@@ -2222,29 +2274,205 @@ Cancels a scheduled task, deleting the schedule and marking the task as skipped. | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `execution_id` | path | string | Yes | Id of the execution | -| `task_id` | path | string | Yes | Id of the task | +| `execution_id` | path | string | Yes | Unique identifier for the flow execution. This ID is generated when the execution +is created and remains constant throughout its lifecycle. + | +| `task_id` | path | string | Yes | Unique identifier for a task within the execution. Each task has a unique ID +that distinguishes it from other tasks in the same execution. + | **Sample Call** ```bash epilot workflow cancelTaskSchedule \ - -p execution_id=wd561 \ - -p task_id=7hj28a + -p execution_id=exec_abc123def456 \ + -p task_id=task_7hj28a +``` + +Using positional args for path parameters: + +```bash +epilot workflow cancelTaskSchedule exec_abc123def456 task_7hj28a +``` + +With JSONata filter: + +```bash +epilot workflow cancelTaskSchedule -p execution_id=exec_abc123def456 -p task_id=task_7hj28a --jsonata '$' +``` + +--- + +### `reconcileAutomationTask` + +Reconciles an automation task's status against its linked automation execution. + +`POST /v2/flows/executions/{execution_id}/tasks/{task_id}/reconcile-automation` + +**Parameters** + +| Name | In | Type | Required | Description | +| ---- | -- | ---- | -------- | ----------- | +| `execution_id` | path | string | Yes | Unique identifier for the flow execution. This ID is generated when the execution +is created and remains constant throughout its lifecycle. + | +| `task_id` | path | string | Yes | Unique identifier for a task within the execution. Each task has a unique ID +that distinguishes it from other tasks in the same execution. + | + +**Sample Call** + +```bash +epilot workflow reconcileAutomationTask \ + -p execution_id=exec_abc123def456 \ + -p task_id=task_7hj28a ``` Using positional args for path parameters: ```bash -epilot workflow cancelTaskSchedule wd561 7hj28a +epilot workflow reconcileAutomationTask exec_abc123def456 task_7hj28a ``` With JSONata filter: ```bash -epilot workflow cancelTaskSchedule -p execution_id=wd561 -p task_id=7hj28a --jsonata '$' +epilot workflow reconcileAutomationTask -p execution_id=exec_abc123def456 -p task_id=task_7hj28a --jsonata 'id' +``` + +
+Sample Response + +```json +{ + "id": "string", + "flow_template_id": "string", + "org_id": "string", + "name": "string", + "created_at": "string", + "updated_at": "string", + "due_date": "string", + "due_date_config": { + "duration": 0, + "unit": "minutes", + "type": "WORKFLOW_STARTED", + "task_id": "string", + "phase_id": "string" + }, + "status": "STARTED", + "assigned_to": ["string"], + "analytics": { + "started_at": "string", + "completed_at": "string", + "closed_at": "string", + "started_by": "string", + "closed_by": "string" + }, + "contexts": [ + { + "entity_id": "string", + "entity_schema": "string", + "is_primary": false + } + ], + "crt_tasks": [ + { + "id": "string", + "crt_since": "1970-01-01T00:00:00.000Z" + } + ], + "phases": [ + { + "id": "string", + "template_id": "string", + "name": "string", + "status": "OPEN", + "updated_at": "string", + "due_date": "2021-04-27T12:00:00.000Z", + "due_date_config": {}, + "assigned_to": ["string"], + "analytics": {}, + "taxonomies": ["string"], + "loop_config": {} + } + ], + "tasks": [ + { + "id": "string", + "template_id": "string", + "name": "string", + "description": {}, + "status": "UNASSIGNED", + "journey": {}, + "due_date": "2021-04-27T12:00:00.000Z", + "due_date_config": {}, + "requirements": [], + "assigned_to": ["string"], + "analytics": {}, + "created_at": "1970-01-01T00:00:00.000Z", + "updated_at": "1970-01-01T00:00:00.000Z", + "manually_created": true, + "enabled": true, + "ecp": {}, + "installer": {}, + "partner": {}, + "taxonomies": ["string"], + "phase_id": "string", + "task_type": "MANUAL", + "loop_config": {} + } + ], + "edges": [ + { + "id": "string", + "from_id": "string", + "to_id": "string", + "condition_id": "abc123", + "none_met": true + } + ], + "_execution_chain": { + "parent_execution_id": "string", + "parent_task_id": "string", + "depth": 0 + }, + "closing_reason": { + "selected_reasons": [ + {} + ], + "configured_reasons": [ + {} + ], + "extra_description": "string" + }, + "available_in_ecp": true, + "entity_sync": [ + { + "trigger": {}, + "target": {}, + "value": {} + } + ], + "taxonomies": ["string"], + "trigger": { + "type": "MANUAL", + "automation_config": { + "flow_id": "string", + "execution_id": "string", + "execution_status": "string", + "error_reason": "string", + "input_context": {}, + "heal_attempts": 0, + "last_heal_attempted_at": "1970-01-01T00:00:00.000Z" + } + }, + "singleClosingReasonSelection": true, + "linear": true +} ``` +
+ --- ### `runTaskScheduleNow` @@ -2257,27 +2485,31 @@ Cancels the pending schedule for a task and immediately triggers its automation | Name | In | Type | Required | Description | | ---- | -- | ---- | -------- | ----------- | -| `execution_id` | path | string | Yes | Id of the execution | -| `task_id` | path | string | Yes | Id of the task | +| `execution_id` | path | string | Yes | Unique identifier for the flow execution. This ID is generated when the execution +is created and remains constant throughout its lifecycle. + | +| `task_id` | path | string | Yes | Unique identifier for a task within the execution. Each task has a unique ID +that distinguishes it from other tasks in the same execution. + | **Sample Call** ```bash epilot workflow runTaskScheduleNow \ - -p execution_id=wd561 \ - -p task_id=7hj28a + -p execution_id=exec_abc123def456 \ + -p task_id=task_7hj28a ``` Using positional args for path parameters: ```bash -epilot workflow runTaskScheduleNow wd561 7hj28a +epilot workflow runTaskScheduleNow exec_abc123def456 task_7hj28a ``` With JSONata filter: ```bash -epilot workflow runTaskScheduleNow -p execution_id=wd561 -p task_id=7hj28a --jsonata '$' +epilot workflow runTaskScheduleNow -p execution_id=exec_abc123def456 -p task_id=task_7hj28a --jsonata '$' ```
diff --git a/packages/cli/package.json b/packages/cli/package.json index a3d93eabc..5d5302cbd 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@epilot/cli", - "version": "0.1.166", + "version": "0.1.168", "description": "CLI for epilot APIs", "type": "module", "bin": { diff --git a/packages/cli/src/commands/apis/ai-agents.ts b/packages/cli/src/commands/apis/ai-agents.ts index 436465123..0dce909ec 100644 --- a/packages/cli/src/commands/apis/ai-agents.ts +++ b/packages/cli/src/commands/apis/ai-agents.ts @@ -3,7 +3,7 @@ import { defineCommand } from 'citty'; import { callApi } from '../../lib/call.js'; export default defineCommand({ - meta: { name: 'ai-agents', description: 'AI Agents API - OpenAPI 3.0' }, + meta: { name: 'ai-agents', description: 'AI Agents API' }, args: { operation: { type: 'positional', description: 'operationId to call', required: false }, param: { type: 'string', alias: 'p', description: 'Parameter key=value' }, diff --git a/packages/cli/src/commands/apis/chat.ts b/packages/cli/src/commands/apis/chat.ts new file mode 100644 index 000000000..15242a105 --- /dev/null +++ b/packages/cli/src/commands/apis/chat.ts @@ -0,0 +1,48 @@ +// Auto-generated by scripts/generate.ts — do not edit +import { defineCommand } from 'citty'; +import { callApi } from '../../lib/call.js'; + +export default defineCommand({ + meta: { name: 'chat', description: 'epilot Chat API' }, + args: { + operation: { type: 'positional', description: 'operationId to call', required: false }, + param: { type: 'string', alias: 'p', description: 'Parameter key=value' }, + data: { type: 'string', alias: 'd', description: 'Request body JSON' }, + header: { type: 'string', alias: 'H', description: 'Custom header' }, + include: { type: 'boolean', alias: 'i', description: 'Include response headers' }, + definition: { type: 'string', description: 'Override OpenAPI spec file/URL' }, + server: { type: 'string', alias: 's', description: 'Override server base URL' }, + 'use-dev': { type: 'boolean', description: 'Target dev environment' }, + 'use-staging': { type: 'boolean', description: 'Target staging environment' }, + profile: { type: 'string', description: 'Use a named profile' }, + token: { type: 'string', alias: 't', description: 'Bearer token' }, + json: { type: 'boolean', description: 'Output raw JSON' }, + verbose: { type: 'boolean', alias: 'v', description: 'Verbose output' }, + guided: { type: 'boolean', description: 'Prompt for all parameters interactively' }, + interactive: { type: 'boolean', description: 'Interactive mode' }, + jsonata: { type: 'string', description: 'JSONata expression to transform response' }, + _ophelp: { type: 'boolean', description: 'Show operation help', required: false }, + _apihelp: { type: 'boolean', description: 'Show API help', required: false }, + }, + run: ({ args, rawArgs }) => { + // Extract additional positional args (after operationId) + const positionalArgs: string[] = []; + if (args.operation && rawArgs) { + const opIdx = rawArgs.indexOf(args.operation); + if (opIdx >= 0) { + for (let i = opIdx + 1; i < rawArgs.length; i++) { + const arg = rawArgs[i]; + if (arg.startsWith('-')) break; + positionalArgs.push(arg); + } + } + } + + return callApi('chat', { + ...args, + help: !!(args as Record)._ophelp, + _apihelp: !!(args as Record)._apihelp, + _args: positionalArgs, + }); + }, +}); diff --git a/packages/cli/src/generated/api-list.ts b/packages/cli/src/generated/api-list.ts index 350c8606e..b011edc55 100644 --- a/packages/cli/src/generated/api-list.ts +++ b/packages/cli/src/generated/api-list.ts @@ -14,7 +14,7 @@ export const API_LIST: ApiInfo[] = [ kebabName: 'access-token', title: 'Access Token API', serverUrl: 'https://access-token.sls.epilot.io', - operationCount: 9, + operationCount: 11, operationIds: [ 'listAccessTokens', 'createAccessToken', @@ -25,6 +25,8 @@ export const API_LIST: ApiInfo[] = [ 'getPortalPreviewTokenJwks', 'getPublicTokenOIDC', 'getPortalPreviewTokenOIDC', + 'getContactIdentificationTokenJwks', + 'getContactIdentificationTokenOIDC', ], }, { @@ -46,9 +48,9 @@ export const API_LIST: ApiInfo[] = [ { apiName: 'aiAgents', kebabName: 'ai-agents', - title: 'AI Agents API - OpenAPI 3.0', + title: 'AI Agents API', serverUrl: 'https://ai-agents.sls.epilot.io', - operationCount: 12, + operationCount: 21, operationIds: [ 'listAgents', 'createAgent', @@ -56,12 +58,21 @@ export const API_LIST: ApiInfo[] = [ 'updateAgentById', 'deleteAgentById', 'executeAgent', + 'executeAgentStream', 'listExecutions', 'getExecution', 'cancelExecution', 'getExecutionTrace', + 'getExecutionFeedback', + 'putExecutionFeedback', 'approveExecution', 'rejectExecution', + 'streamExecution', + 'chat', + 'listConversations', + 'getConversation', + 'deleteConversation', + 'submitConversationFeedback', ], }, { @@ -69,7 +80,7 @@ export const API_LIST: ApiInfo[] = [ kebabName: 'app', title: 'App API', serverUrl: 'https://app.sls.epilot.io', - operationCount: 32, + operationCount: 36, operationIds: [ 'getPublicFacingComponent', 'listConfigurations', @@ -100,9 +111,13 @@ export const API_LIST: ApiInfo[] = [ 'patchInstallation', 'uninstall', 'promoteVersion', + 'resolveOptions', 'ingestEvent', 'publicProxyGet', 'publicProxyPost', + 'publicProxyPut', + 'publicProxyPatch', + 'publicProxyDelete', ], }, { @@ -118,7 +133,7 @@ export const API_LIST: ApiInfo[] = [ kebabName: 'automation', title: 'Automation API', serverUrl: 'https://automation.sls.epilot.io', - operationCount: 16, + operationCount: 17, operationIds: [ 'searchFlows', 'createFlow', @@ -128,6 +143,7 @@ export const API_LIST: ApiInfo[] = [ 'deleteFlow', 'getExecutions', 'startExecution', + 'searchExecutions', 'bulkTriggerExecutions', 'getBulkJob', 'patchBulkJob', @@ -143,7 +159,7 @@ export const API_LIST: ApiInfo[] = [ kebabName: 'billing', title: 'Billing API', serverUrl: 'https://billing.sls.epilot.io', - operationCount: 10, + operationCount: 14, operationIds: [ 'getBillingEvents', 'createBillingEvent', @@ -154,6 +170,10 @@ export const API_LIST: ApiInfo[] = [ 'createContractEntity', 'updateContractEntity', 'deleteContractEntity', + 'getContractPricingInformation', + 'getBillingAccountPricingInformation', + 'getContractConfigurationHistory', + 'getBillingAccountConfigurationHistory', 'getCustomerBalance', ], }, @@ -162,7 +182,7 @@ export const API_LIST: ApiInfo[] = [ kebabName: 'blueprint-manifest', title: 'Blueprint Manifest API', serverUrl: 'https://blueprint-manifest.sls.epilot.io', - operationCount: 61, + operationCount: 73, operationIds: [ 'getJob', 'createExport', @@ -183,6 +203,9 @@ export const API_LIST: ApiInfo[] = [ 'getBlueprint', 'updateBlueprint', 'deleteBlueprint', + 'addBlueprintNote', + 'updateBlueprintNote', + 'deleteBlueprintNote', 'validateBlueprint', 'verifyBlueprint', 'detectPatchChanges', @@ -206,6 +229,7 @@ export const API_LIST: ApiInfo[] = [ 'listBlueprintJobs', 'getBlueprintJob', 'continueInstallationJob', + 'retryInstallationJob', 'cancelBlueprintJob', 'getMarketplaceListing', 'createMarketplaceListing', @@ -217,10 +241,18 @@ export const API_LIST: ApiInfo[] = [ 'createMarketplaceListingVersion', 'updateMarketplaceListingVersion', 'publishMarketplaceListingVersion', + 'publishBlueprintV3', + 'preInstallBlueprintV3', 'installBlueprintV3', 'restoreBlueprintDeploymentV3', 'getRestorePreview', + 'triggerDeploymentHealthCheckV3', + 'getDeploymentHealthReportV3', 'getBlueprintLineageV3', + 'createBulkInstallV3', + 'getBulkInstallV3', + 'listBulkInstallTargetsV3', + 'retryBulkInstallTargetV3', 'listUniquenessCriteria', 'getUniquenessCriteria', 'putUniquenessCriteria', @@ -232,8 +264,55 @@ export const API_LIST: ApiInfo[] = [ kebabName: 'calendar', title: 'Calendar API', serverUrl: 'https://calendar.sls.epilot.io', - operationCount: 4, - operationIds: ['listCalendars', 'getCalendar', 'listEvents', 'getEvent'], + operationCount: 28, + operationIds: [ + 'listUsersAbsence', + 'searchAbsence', + 'searchNowAbsence', + 'listAbsenceAdjustments', + 'createAbsenceAdjustment', + 'getAbsenceAdjustment', + 'patchAbsenceAdjustment', + 'deleteAbsenceAdjustment', + 'getUserAbsence', + 'getWorkingHours', + 'putWorkingHours', + 'deleteWorkingHours', + 'listCalendars', + 'createCalendar', + 'addOutlookCalendar', + 'listOutlookCalendars', + 'deleteOutlookCalendar', + 'outlookWebhook', + 'getCalendar', + 'updateCalendar', + 'deleteCalendar', + 'listEvents', + 'createEvent', + 'getEvent', + 'updateEvent', + 'deleteEvent', + 'shareEvent', + 'unshareEvent', + ], + }, + { + apiName: 'chat', + kebabName: 'chat', + title: 'epilot Chat API', + serverUrl: 'https://chat.sls.epilot.io', + operationCount: 9, + operationIds: [ + 'listChatWidgets', + 'createChatWidget', + 'getChatWidget', + 'updateChatWidget', + 'deleteChatWidget', + 'getPublicChatWidget', + 'createPublicChatGrant', + 'createAnonymousChatSession', + 'sendAnonymousChatMessage', + ], }, { apiName: 'configurationHub', @@ -278,7 +357,7 @@ export const API_LIST: ApiInfo[] = [ kebabName: 'customer-portal', title: 'Portal API', serverUrl: 'https://customer-portal-api.sls.epilot.io', - operationCount: 149, + operationCount: 173, operationIds: [ 'upsertPortal', 'createUser', @@ -288,6 +367,8 @@ export const API_LIST: ApiInfo[] = [ 'getPortalConfigByDomain', 'getPortalConfig', 'deletePortal', + 'createExport', + 'getExport', 'getPortalExtensions', 'getPublicPortalExtensionDetails', 'getPortalExtensionsV3', @@ -309,6 +390,8 @@ export const API_LIST: ApiInfo[] = [ 'getAllPortalConfigs', 'getEmailTemplates', 'upsertEmailTemplates', + 'migrateEmailTemplateReferences', + 'listEmailTemplateReferences', 'getEmailTemplatesByPortalId', 'upsertEmailTemplatesByPortalId', 'getPublicPortalWidgets', @@ -332,12 +415,14 @@ export const API_LIST: ApiInfo[] = [ 'getECPContact', 'checkContactExists', 'checkContactExistsV3', + 'identifyContact', 'checkAccountExists', 'getValidSecondaryAttributes', 'getPortalUser', 'updatePortalUser', 'deletePortalUser', 'updatePortalUserEmail', + 'changePortalUserPassword', 'resendConfirmationEmail', 'fetchPortalUsersByRelatedEntity', 'confirmUser', @@ -360,6 +445,7 @@ export const API_LIST: ApiInfo[] = [ 'getAllContracts', 'getContract', 'updateContract', + 'getContractWithTemplates', 'addContractByIdentifiers', 'getEntityIdentifiers', 'getEntityActivityFeed', @@ -372,6 +458,7 @@ export const API_LIST: ApiInfo[] = [ 'getRegistrationIdentifiers', 'getAllFiles', 'getFileById', + 'getFilePreview', 'trackFileDownloaded', 'getFilesCountByEntity', 'getBillingEvents', @@ -382,15 +469,21 @@ export const API_LIST: ApiInfo[] = [ 'triggerEntityAccessEventV3', 'getPortalUserEntity', 'searchPortalUserEntities', + 'createPortalUserEntity', + 'patchPortalUserEntity', 'canTriggerPortalFlow', 'getAutomationContext', 'updateWorkflowStepAsDone', 'getEntityWorkflows', + 'getOutstandingTasks', + 'getEntityPortalWorkflows', + 'getEntityPortalWorkflowsBatch', 'uploadMeterReadingPhoto', 'createMeterReading', 'getAllowedMeterReadingRange', 'getMeterReadings', 'getSSOProvider', + 'getPublicSSOProviderV3', 'ssoLogin', 'ssoLoginV3', 'ssoRedirect', @@ -411,6 +504,10 @@ export const API_LIST: ApiInfo[] = [ 'deletePortalPageBlock', 'getUserEntryPoint', 'updateCampaignPortalBlockStatus', + 'listPortalNotifications', + 'getPortalNotificationsUnreadCount', + 'markAllPortalNotificationsRead', + 'markPortalNotificationRead', 'updateNotificationsStatus', 'deRegisterMLoginUser', 'notifyMLoginInterestChange', @@ -418,6 +515,10 @@ export const API_LIST: ApiInfo[] = [ 'getPortalConfigV3', 'putPortalConfig', 'deletePortalConfig', + 'listPortalRevisions', + 'createPortalRevision', + 'getPortalRevision', + 'publishPortalRevision', 'listAllPortalConfigs', 'swapPortalConfig', 'clonePortalConfig', @@ -429,6 +530,8 @@ export const API_LIST: ApiInfo[] = [ 'enablePartner', 'verifyDns', 'portalProxyExecute', + 'getMobileConfig', + 'putMobileConfig', ], }, { @@ -436,13 +539,24 @@ export const API_LIST: ApiInfo[] = [ kebabName: 'dashboard', title: 'Dashboard API', serverUrl: 'https://dashboard.sls.epilot.io', - operationCount: 7, + operationCount: 18, operationIds: [ 'listDashboards', 'createDashboard', + 'listFavoriteDashboardIds', + 'favoriteDashboard', + 'unfavoriteDashboard', 'getDashboard', 'putDashboard', + 'patchDashboard', 'deleteDashboard', + 'listInsights', + 'createInsight', + 'listInsightTags', + 'getInsight', + 'putInsight', + 'patchInsight', + 'deleteInsight', 'listAvailableVisualisations', 'listAvailableExamples', ], @@ -471,21 +585,33 @@ export const API_LIST: ApiInfo[] = [ kebabName: 'deduplication', title: 'Deduplication API', serverUrl: 'https://deduplication.sls.epilot.io', - operationCount: 3, - operationIds: ['deduplicate', 'deduplicateAsync', 'getDeduplicationJob'], + operationCount: 10, + operationIds: [ + 'deduplicate', + 'deduplicateAsync', + 'getDeduplicationJob', + 'detectDuplicates', + 'dismissDuplicates', + 'listUniquenessCriteria', + 'createUniquenessCriteria', + 'getUniquenessCriteria', + 'updateUniquenessCriteria', + 'deleteUniquenessCriteria', + ], }, { apiName: 'design', kebabName: 'design', title: 'Design Builder API v2', serverUrl: 'https://design-builder-api.sls.epilot.io', - operationCount: 13, + operationCount: 14, operationIds: [ 'getAllDesigns', 'addDesign', 'getDesign', 'updateDesign', 'deleteDesign', + 'duplicateDesign', 'getThemeFromDesign', 'getFiles', 'uploadFile', @@ -501,15 +627,15 @@ export const API_LIST: ApiInfo[] = [ kebabName: 'document', title: 'Document API', serverUrl: 'https://document.sls.epilot.io', - operationCount: 3, - operationIds: ['getTemplateMeta', 'generateDocumentV2', 'convertDocument'], + operationCount: 4, + operationIds: ['getTemplateMeta', 'generateDocumentV2', 'convertDocument', 'validateTemplate'], }, { apiName: 'emailSettings', kebabName: 'email-settings', title: 'Messaging Settings API', serverUrl: 'https://email-settings.sls.epilot.io', - operationCount: 36, + operationCount: 48, operationIds: [ 'provisionEpilotEmailAddress', 'setEmailAddressPrimary', @@ -526,6 +652,9 @@ export const API_LIST: ApiInfo[] = [ 'listInboxBuckets', 'connectOutlook', 'getOutlookConnectionStatus', + 'getCalendarAdminConsentStatus', + 'getMyCalendarConnection', + 'disconnectMyCalendar', 'disconnectOutlook', 'connectMsTeams', 'disconnectMsTeams', @@ -536,6 +665,15 @@ export const API_LIST: ApiInfo[] = [ 'getMailboxSyncStatus', 'retryMailboxSync', 'getConnectedOutlookEmails', + 'listSmtpConnections', + 'createSmtpConnection', + 'getSmtpConnection', + 'updateSmtpConnection', + 'deleteSmtpConnection', + 'testSmtpConnection', + 'listSmtpSenders', + 'connectSmtpSender', + 'disconnectSmtpSender', 'outlookOAuthCallback', 'getSettings', 'addSetting', @@ -570,7 +708,7 @@ export const API_LIST: ApiInfo[] = [ kebabName: 'entity', title: 'Entity API', serverUrl: 'https://entity.sls.epilot.io', - operationCount: 87, + operationCount: 88, operationIds: [ 'listSchemas', 'listSchemasV2', @@ -617,6 +755,7 @@ export const API_LIST: ApiInfo[] = [ 'deleteRelation', 'exportEntities', 'importEntities', + 'abortEntityImport', 'listSavedViews', 'createSavedView', 'getSavedView', @@ -704,11 +843,16 @@ export const API_LIST: ApiInfo[] = [ kebabName: 'event-catalog', title: 'Event Catalog API', serverUrl: 'https://event-catalog.sls.epilot.io', - operationCount: 10, + operationCount: 15, operationIds: [ 'listEvents', + 'createCustomEvent', 'getEvent', + 'replaceCustomEventDraft', 'patchEvent', + 'deprecateCustomEvent', + 'previewCustomEvent', + 'publishCustomEventDefinition', 'getEventJSONSchema', 'getEventExample', 'listEventVersions', @@ -723,7 +867,7 @@ export const API_LIST: ApiInfo[] = [ kebabName: 'file', title: 'File API', serverUrl: 'https://file.sls.epilot.io', - operationCount: 30, + operationCount: 38, operationIds: [ 'uploadFileV2', 'saveFileV2', @@ -736,7 +880,15 @@ export const API_LIST: ApiInfo[] = [ 'downloadFiles', 'createZipJob', 'getZipJob', + 'getFileSummary', + 'getFileSummaryFeedback', + 'putFileSummaryFeedback', + 'createFileSummaryJob', + 'getCurrentFileSummaryJob', + 'getFileSummaryJob', 'generateFileSummary', + 'getFileText', + 'validateFile', 'previewFile', 'previewS3FileGet', 'previewS3File', @@ -770,7 +922,7 @@ export const API_LIST: ApiInfo[] = [ kebabName: 'integration-toolkit', title: 'Integration Toolkit API', serverUrl: 'https://integration-toolkit.sls.epilot.io', - operationCount: 51, + operationCount: 70, operationIds: [ 'acknowledgeTracking', 'triggerErp', @@ -778,6 +930,7 @@ export const API_LIST: ApiInfo[] = [ 'processErpUpdatesEventsV2', 'processErpUpdatesEventsV3', 'simulateMappingV2', + 'simulateDirect', 'simulateMapping', 'listIntegrations', 'createIntegration', @@ -792,11 +945,18 @@ export const API_LIST: ApiInfo[] = [ 'updateUseCase', 'deleteUseCase', 'listUseCaseHistory', + 'listDocumentationPages', + 'getDocumentationPage', + 'upsertDocumentationPage', + 'deleteDocumentationPage', 'listIntegrationsV2', 'createIntegrationV2', 'getIntegrationV2', 'updateIntegrationV2', 'deleteIntegrationV2', + 'listNotificationHistory', + 'testSendNotification', + 'getNotificationStatus', 'getSecureProxyWhitelist', 'updateSecureProxyWhitelist', 'listSecureProxyWhitelistHistory', @@ -806,6 +966,7 @@ export const API_LIST: ApiInfo[] = [ 'getMonitoringStats', 'getMonitoringTimeSeries', 'getOutboundStatus', + 'getEntitySyncStatus', 'pollOutboundMessages', 'ackOutboundMessages', 'listOutboundDlqMessages', @@ -817,12 +978,22 @@ export const API_LIST: ApiInfo[] = [ 'getMonitoringStatsV2', 'getMonitoringTimeSeriesV2', 'getAssociatedMonitoringEvents', + 'ingestExternalMonitoringEvents', + 'getMonitoringTraceByCorrelation', 'listSecureProxies', 'secureProxy', 'managedCallExecute', 'generateTypesPreview', 'generateTypes', 'commitTypes', + 'listErpImports', + 'createErpImport', + 'getErpImport', + 'deleteErpImport', + 'validateErpImport', + 'suggestErpImportUseCases', + 'executeErpImport', + 'abortErpImport', ], }, { @@ -830,11 +1001,18 @@ export const API_LIST: ApiInfo[] = [ kebabName: 'journey', title: 'Journey API', serverUrl: 'https://journey-config.sls.epilot.io', - operationCount: 16, + operationCount: 23, operationIds: [ 'getJourneysByOrgId', 'getJourney', 'removeJourney', + 'listJourneyRevisions', + 'createJourneyRevision', + 'getJourneyRevision', + 'publishJourneyRevision', + 'getJourneyPublishState', + 'getJourneyEnvironment', + 'getJourneyEnvironmentVariables', 'getJourneyProducts', 'createJourney', 'updateJourney', @@ -874,7 +1052,7 @@ export const API_LIST: ApiInfo[] = [ kebabName: 'message', title: 'Message API', serverUrl: 'https://message.sls.epilot.io', - operationCount: 52, + operationCount: 54, operationIds: [ 'sendMessage', 'updateMessage', @@ -888,10 +1066,12 @@ export const API_LIST: ApiInfo[] = [ 'markReadMessageV2', 'markUnreadMessage', 'getUnread', + 'getUnreadCounts', 'markUnreadMessageV2', 'searchThreads', 'searchThreadsV2', 'searchIds', + 'getAssigneeWorkload', 'updateThread', 'deleteThread', 'moveThread', @@ -934,8 +1114,8 @@ export const API_LIST: ApiInfo[] = [ apiName: 'metering', kebabName: 'metering', title: 'Metering API', - serverUrl: '', - operationCount: 20, + serverUrl: 'https://metering.sls.epilot.io', + operationCount: 21, operationIds: [ 'getCustomerMeters', 'getMetersByContractId', @@ -947,6 +1127,7 @@ export const API_LIST: ApiInfo[] = [ 'createMeterReadings', 'createPortalMeterReadings', 'batchWriteMeterReadings', + 'pruneMeterReadings', 'createMeterReadingFromSubmission', 'getAllowedReadingForMeter', 'createReadingWithMeter', @@ -1052,7 +1233,7 @@ export const API_LIST: ApiInfo[] = [ apiName: 'permissions', kebabName: 'permissions', title: 'Permissions API', - serverUrl: '', + serverUrl: 'https://permissions.sls.epilot.io', operationCount: 13, operationIds: [ 'listCurrentRoles', @@ -1075,7 +1256,7 @@ export const API_LIST: ApiInfo[] = [ kebabName: 'pricing', title: 'Pricing API', serverUrl: 'https://pricing-api.sls.epilot.io', - operationCount: 22, + operationCount: 39, operationIds: [ '$calculatePricingDetails', 'createOrder', @@ -1099,6 +1280,23 @@ export const API_LIST: ApiInfo[] = [ '$getExternalCatalogProducts', '$getExternalCatalogProductRecommendations', '$productRecommendations', + '$getConditionSets', + '$resolveConditionalEntity', + '$createConditionalVariant', + '$listConditionalVariants', + '$getConditionalVariantTree', + '$getActiveConditionalVariantVersion', + '$replaceActiveConditionalVariantVersion', + '$patchActiveConditionalVariantVersion', + '$deleteConditionalVariant', + '$listConditionalVariantVersions', + '$appendConditionalVariantVersion', + '$getConditionalVariantVersion', + '$replaceConditionalVariantVersion', + '$patchConditionalVariantVersion', + '$deleteConditionalVariantVersion', + '$batchUpsertConditionalVariants', + '$batchDeleteConditionalVariants', ], }, { @@ -1196,7 +1394,7 @@ export const API_LIST: ApiInfo[] = [ kebabName: 'snapshot', title: 'Snapshot API', serverUrl: 'https://snapshot.sls.epilot.io', - operationCount: 9, + operationCount: 12, operationIds: [ 'listSnapshots', 'createSnapshot', @@ -1206,6 +1404,9 @@ export const API_LIST: ApiInfo[] = [ 'restoreSnapshot', 'listSnapshotResources', 'getSnapshotResource', + 'getOrgSnapshotSchedule', + 'putOrgSnapshotSchedule', + 'deleteOrgSnapshotSchedule', 'listDependencies', ], }, @@ -1222,7 +1423,7 @@ export const API_LIST: ApiInfo[] = [ kebabName: 'targeting', title: 'Targeting API', serverUrl: 'https://targeting.sls.epilot.io', - operationCount: 12, + operationCount: 16, operationIds: [ 'changeCampaignStatus', 'getCampaignJobStatus', @@ -1230,12 +1431,16 @@ export const API_LIST: ApiInfo[] = [ 'retriggerCampaignAutomations', 'setupCampaign', 'matchCampaigns', + 'discoverCampaigns', 'matchTargets', 'getTargetQueries', 'createRecipient', 'updateRecipient', 'updateRecipientPortalStatus', + 'updateRecipientEntityUiStatus', + 'restoreRecipientEntityUiStatus', 'getRecipients', + 'getEmailStats', ], }, { @@ -1264,7 +1469,7 @@ export const API_LIST: ApiInfo[] = [ kebabName: 'user', title: 'User API', serverUrl: 'https://user.sls.epilot.io', - operationCount: 41, + operationCount: 42, operationIds: [ 'signUpUser', 'getMeV2', @@ -1291,6 +1496,7 @@ export const API_LIST: ApiInfo[] = [ 'getNavigation', 'verifyEmailWithToken', 'requestPasswordReset', + 'resetPassword', 'checkInviteToken', 'activateUser', 'rejectInvite', @@ -1354,7 +1560,7 @@ export const API_LIST: ApiInfo[] = [ kebabName: 'workflow', title: 'Workflows Executions', serverUrl: 'https://workflows-execution.sls.epilot.io', - operationCount: 24, + operationCount: 25, operationIds: [ 'getExecutions', 'createExecution', @@ -1378,6 +1584,7 @@ export const API_LIST: ApiInfo[] = [ 'patchPhase', 'addTask', 'cancelTaskSchedule', + 'reconcileAutomationTask', 'runTaskScheduleNow', 'cancelSchedule', ], diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 5c2854d21..eda1fd464 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -3,7 +3,7 @@ import { defineCommand } from 'citty'; export const main = defineCommand({ meta: { name: 'epilot', - version: '0.1.51', + version: '0.1.168', description: 'CLI for epilot APIs', }, args: { @@ -34,6 +34,7 @@ export const main = defineCommand({ billing: () => import('./commands/apis/billing.js').then((m) => m.default), 'blueprint-manifest': () => import('./commands/apis/blueprint-manifest.js').then((m) => m.default), calendar: () => import('./commands/apis/calendar.js').then((m) => m.default), + chat: () => import('./commands/apis/chat.js').then((m) => m.default), 'configuration-hub': () => import('./commands/apis/configuration-hub.js').then((m) => m.default), consent: () => import('./commands/apis/consent.js').then((m) => m.default), 'customer-portal': () => import('./commands/apis/customer-portal.js').then((m) => m.default), diff --git a/packages/epilot-sdk-v2/docs/document.md b/packages/epilot-sdk-v2/docs/document.md index 3aa56ccb3..3066e462a 100644 --- a/packages/epilot-sdk-v2/docs/document.md +++ b/packages/epilot-sdk-v2/docs/document.md @@ -28,6 +28,7 @@ const { data } = await documentClient.getTemplateMeta(...) - [`getTemplateMeta`](#gettemplatemeta) - [`generateDocumentV2`](#generatedocumentv2) - [`convertDocument`](#convertdocument) +- [`validateTemplate`](#validatetemplate) **Schemas** - [`S3Reference`](#s3reference) @@ -39,6 +40,9 @@ const { data } = await documentClient.getTemplateMeta(...) - [`DocxTemplaterErrorDetails`](#docxtemplatererrordetails) - [`DocxTemplaterErrorDetail`](#docxtemplatererrordetail) - [`ErrorCode`](#errorcode) +- [`TemplateValidationRequest`](#templatevalidationrequest) +- [`TemplateValidationResponse`](#templatevalidationresponse) +- [`TemplateIssue`](#templateissue) - [`TemplateSettings`](#templatesettings) - [`DocumentMetaRequest`](#documentmetarequest) - [`DocumentMetaResponse`](#documentmetaresponse) @@ -110,7 +114,7 @@ const { data } = await client.generateDocumentV2( } }, context_entity_id: 'bcd0aab9-b544-42b0-8bfb-6d449d02eacc', - user_id: 100321, + user_id: '100321', language: 'de', variable_payload: { additionalProperties: 'string' @@ -132,7 +136,7 @@ const { data } = await client.generateDocumentV2( template_with_datatable: false, enabled_template_settings_persistence: false, misconfigured_margins: false, - file_entity_id: '1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p' + file_entity_id: '123e4567-e89b-12d3-a456-426614174000' } }, ) @@ -215,7 +219,7 @@ const { data } = await client.generateDocumentV2( "template_with_datatable": false, "enabled_template_settings_persistence": false, "misconfigured_margins": false, - "file_entity_id": "1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p" + "file_entity_id": "123e4567-e89b-12d3-a456-426614174000" } } ``` @@ -266,6 +270,72 @@ const { data } = await client.convertDocument( --- +### `validateTemplate` + +Validates a document template's variable syntax and, optionally, proposes a hotfixed copy of it. + +`POST /v2/templates:validate` + +```ts +const { data } = await client.validateTemplate( + null, + { + template_document: { + filename: 'Umzugsmeldung.xlsx', + s3ref: { + bucket: 'document-api-prod', + key: 'uploads/my-template.pdf' + } + }, + fix: true, + fix_level: 'safe' + }, +) +``` + +
+Response + +```json +{ + "valid": false, + "fixed": true, + "issues": [ + { + "id": "unopened_tag", + "file": "xl/sharedStrings.xml", + "location": "Tabelle1!N4", + "context": "…Datum: {system.date}} Unterschrift…", + "explanation": "The tag is missing an opening brace.", + "fixable": true, + "confidence": "high", + "rule": "balance_opening_delimiter", + "before": "{system.date}}", + "after": "{{system.date}}" + } + ], + "unresolved_errors": [ + { + "id": "string", + "context": "string", + "explanation": "string" + } + ], + "fixed_document": { + "s3ref": { + "bucket": "document-api-prod", + "key": "uploads/my-template.pdf" + }, + "filename": "Umzugsmeldung (fixed).xlsx", + "preview_url": "https://example.com/path" + } +} +``` + +
+ +--- + ## Schemas ### `S3Reference` @@ -282,7 +352,7 @@ type S3Reference = { ```ts type ErrorOutput = { error_message?: string - error_code?: "PARSE_ERROR" | "DOC_TO_PDF_CONVERT_ERROR" | "INTERNAL_ERROR" | "INVALID_TEMPLATE_FORMAT" + error_code?: "PARSE_ERROR" | "DOC_TO_PDF_CONVERT_ERROR" | "INTERNAL_ERROR" | "INVALID_TEMPLATE_FORMAT" | "TEMPLATE_NOT_FOUND" error_details?: Array<{ explanation?: string context?: { @@ -395,7 +465,76 @@ Error codes for document generation: - INTERNAL_ERROR - In ```ts -type ErrorCode = "PARSE_ERROR" | "DOC_TO_PDF_CONVERT_ERROR" | "INTERNAL_ERROR" | "INVALID_TEMPLATE_FORMAT" +type ErrorCode = "PARSE_ERROR" | "DOC_TO_PDF_CONVERT_ERROR" | "INTERNAL_ERROR" | "INVALID_TEMPLATE_FORMAT" | "TEMPLATE_NOT_FOUND" +``` + +### `TemplateValidationRequest` + +```ts +type TemplateValidationRequest = { + template_document: { + filename?: string + s3ref: { + bucket: { ... } + key: { ... } + } + } + fix?: boolean + fix_level?: "safe" | "aggressive" +} +``` + +### `TemplateValidationResponse` + +```ts +type TemplateValidationResponse = { + valid?: boolean + fixed?: boolean + issues?: Array<{ + id?: string + file?: string + location?: string + context?: string + explanation?: string + fixable?: boolean + confidence?: "high" | "medium" | "low" + rule?: string + before?: string + after?: string + }> + unresolved_errors?: Array<{ + id?: string + context?: string + explanation?: string + }> + fixed_document?: { + s3ref?: { + bucket: { ... } + key: { ... } + } + filename?: string + preview_url?: string // uri + } +} +``` + +### `TemplateIssue` + +A single template syntax problem, and the repair proposed for it + +```ts +type TemplateIssue = { + id?: string + file?: string + location?: string + context?: string + explanation?: string + fixable?: boolean + confidence?: "high" | "medium" | "low" + rule?: string + before?: string + after?: string +} ``` ### `TemplateSettings` @@ -525,7 +664,7 @@ type DocumentGenerationV2Response = { } error_output?: { error_message?: string - error_code?: "PARSE_ERROR" | "DOC_TO_PDF_CONVERT_ERROR" | "INTERNAL_ERROR" | "INVALID_TEMPLATE_FORMAT" + error_code?: "PARSE_ERROR" | "DOC_TO_PDF_CONVERT_ERROR" | "INTERNAL_ERROR" | "INVALID_TEMPLATE_FORMAT" | "TEMPLATE_NOT_FOUND" error_details?: Array<{ explanation?: { ... } context?: { ... } diff --git a/packages/epilot-sdk-v2/package.json b/packages/epilot-sdk-v2/package.json index 009d7ccf5..033350534 100644 --- a/packages/epilot-sdk-v2/package.json +++ b/packages/epilot-sdk-v2/package.json @@ -1,6 +1,6 @@ { "name": "@epilot/sdk", - "version": "2.20.26", + "version": "2.20.27", "description": "JavaScript/TypeScript SDK for epilot APIs", "type": "module", "main": "./dist/index.cjs", diff --git a/packages/epilot-sdk-v2/src/definitions/document-runtime.json b/packages/epilot-sdk-v2/src/definitions/document-runtime.json index f29b50450..bf5eb6a77 100644 --- a/packages/epilot-sdk-v2/src/definitions/document-runtime.json +++ b/packages/epilot-sdk-v2/src/definitions/document-runtime.json @@ -1 +1 @@ -{"s":"https://document.sls.epilot.io","o":[["getTemplateMeta","post","/v2/documents:meta",null,1],["generateDocumentV2","post","/v2/documents:generate",[["job_id","q"],["mode","q"],["preview_mode","q"]],1],["convertDocument","post","/v2/documents:convert",null,1]]} \ No newline at end of file +{"s":"https://document.sls.epilot.io","o":[["getTemplateMeta","post","/v2/documents:meta",null,1],["generateDocumentV2","post","/v2/documents:generate",[["job_id","q"],["mode","q"],["preview_mode","q"]],1],["convertDocument","post","/v2/documents:convert",null,1],["validateTemplate","post","/v2/templates:validate",null,1]]} \ No newline at end of file diff --git a/packages/epilot-sdk-v2/src/docs/document.json b/packages/epilot-sdk-v2/src/docs/document.json index 9aa06c586..914195b5c 100644 --- a/packages/epilot-sdk-v2/src/docs/document.json +++ b/packages/epilot-sdk-v2/src/docs/document.json @@ -1 +1 @@ -"# Document API\n\n- **Base URL:** `https://document.sls.epilot.io`\n- **Full API Docs:** [https://docs.epilot.io/api/document](https://docs.epilot.io/api/document)\n\n## Usage\n\n```ts\nimport { epilot } from '@epilot/sdk'\n\nepilot.authorize(() => '')\nconst { data } = await epilot.document.getTemplateMeta(...)\n```\n\n### Tree-shakeable import\n\n```ts\nimport { getClient, authorize } from '@epilot/sdk/document'\n\nconst documentClient = getClient()\nauthorize(documentClient, () => '')\nconst { data } = await documentClient.getTemplateMeta(...)\n```\n\n## Operations\n\n**Documents**\n- [`getTemplateMeta`](#gettemplatemeta)\n- [`generateDocumentV2`](#generatedocumentv2)\n- [`convertDocument`](#convertdocument)\n\n**Schemas**\n- [`S3Reference`](#s3reference)\n- [`ErrorOutput`](#erroroutput)\n- [`InvalidCustomVariableErrorDetails`](#invalidcustomvariableerrordetails)\n- [`InvalidCustomVariableErrorDetail`](#invalidcustomvariableerrordetail)\n- [`InternalErrorDetails`](#internalerrordetails)\n- [`InternalErrorDetail`](#internalerrordetail)\n- [`DocxTemplaterErrorDetails`](#docxtemplatererrordetails)\n- [`DocxTemplaterErrorDetail`](#docxtemplatererrordetail)\n- [`ErrorCode`](#errorcode)\n- [`TemplateSettings`](#templatesettings)\n- [`DocumentMetaRequest`](#documentmetarequest)\n- [`DocumentMetaResponse`](#documentmetaresponse)\n- [`DocumentGenerationV2Request`](#documentgenerationv2request)\n- [`DocumentGenerationV2Response`](#documentgenerationv2response)\n- [`ConvertDocumentRequest`](#convertdocumentrequest)\n- [`ConvertDocumentResponse`](#convertdocumentresponse)\n\n### `getTemplateMeta`\n\nGet metadata for a document template\n\n`POST /v2/documents:meta`\n\n```ts\nconst { data } = await client.getTemplateMeta(\n null,\n {\n template_document: {\n s3ref: {\n bucket: 'document-api-prod',\n key: 'uploads/my-template.pdf'\n }\n }\n },\n)\n```\n\n
\nResponse\n\n```json\n{\n \"page_margins\": {\n \"top\": 2.54,\n \"bottom\": 2.54,\n \"left\": 2.54,\n \"right\": 2.54,\n \"header\": 2.54,\n \"footer\": 2.54\n },\n \"variables\": [\"order.billing_contact.0.salutation\", \"order.billing_contact.0.title\", \"order_table\", \"stayHardStatic\", \"opportunity[attribute_name]\", \"opportunity[\\\"attribute_name\\\"]\", \"opportunity.[attribute_name]\", \"attribute_name\", \"opportunities.0.attribute_name\", \"opportunities[0].attribute_name\", \"contact.opportunities[0].attribute_name\", \"opportunities[Primary].attribute_name\"]\n}\n```\n\n
\n\n---\n\n### `generateDocumentV2`\n\nGenerates documents from templates with variables.\n\n`POST /v2/documents:generate`\n\n```ts\nconst { data } = await client.generateDocumentV2(\n {\n job_id: 'example',\n mode: 'example',\n preview_mode: 'example',\n },\n {\n template_document: {\n filename: 'my-template-{{order.order_number}}.docx',\n s3ref: {\n bucket: 'document-api-prod',\n key: 'uploads/my-template.pdf'\n }\n },\n context_entity_id: 'bcd0aab9-b544-42b0-8bfb-6d449d02eacc',\n user_id: 100321,\n language: 'de',\n variable_payload: {\n additionalProperties: 'string'\n },\n context_data: {\n additionalProperties: 'string'\n },\n template_settings: {\n custom_margins: {\n top: 2.54,\n bottom: 2.54\n },\n suggested_margins: {\n top: 2.54,\n bottom: 2.54\n },\n display_margin_guidelines: true,\n enable_data_table_margin_autofix: false,\n template_with_datatable: false,\n enabled_template_settings_persistence: false,\n misconfigured_margins: false,\n file_entity_id: '1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p'\n }\n },\n)\n```\n\n
\nResponse\n\n```json\n{\n \"job_id\": \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n \"job_status\": \"STARTED\",\n \"message\": \"string\",\n \"ics_output\": {\n \"output_document\": {\n \"s3ref\": {\n \"bucket\": \"document-api-preview-prod\",\n \"key\": \"preview/my-appointment.ics\"\n }\n }\n },\n \"pdf_output\": {\n \"preview_url\": \"https://document-api-prod.s3.eu-central-1.amazonaws.com/preview/my-template-OR-001.pdf\",\n \"output_document\": {\n \"s3ref\": {\n \"bucket\": \"document-api-preview-prod\",\n \"key\": \"preview/my-template.pdf\"\n }\n }\n },\n \"docx_output\": {\n \"preview_url\": \"https://document-api-prod.s3.eu-central-1.amazonaws.com/preview/my-template-OR-001.docx\",\n \"output_document\": {\n \"s3ref\": {\n \"bucket\": \"document-api-preview-prod\",\n \"key\": \"preview/my-template.docx\"\n }\n }\n },\n \"xlsx_output\": {\n \"preview_url\": \"https://document-api-prod.s3.eu-central-1.amazonaws.com/preview/my-template-OR-001.xlsx\",\n \"output_document\": {\n \"s3ref\": {\n \"bucket\": \"document-api-preview-prod\",\n \"key\": \"preview/my-template.xlsx\"\n }\n }\n },\n \"error_output\": {\n \"error_message\": \"string\",\n \"error_code\": \"PARSE_ERROR\",\n \"error_details\": [\n {\n \"explanation\": \"string\",\n \"context\": {\n \"invalid_variables\": [\n {\n \"variable\": \"string\",\n \"error\": \"string\"\n }\n ]\n }\n }\n ]\n },\n \"variable_payload\": {\n \"additionalProperties\": \"string\"\n },\n \"template_settings\": {\n \"custom_margins\": {\n \"top\": 2.54,\n \"bottom\": 2.54\n },\n \"suggested_margins\": {\n \"top\": 2.54,\n \"bottom\": 2.54\n },\n \"display_margin_guidelines\": true,\n \"enable_data_table_margin_autofix\": false,\n \"template_with_datatable\": false,\n \"enabled_template_settings_persistence\": false,\n \"misconfigured_margins\": false,\n \"file_entity_id\": \"1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p\"\n }\n}\n```\n\n
\n\n---\n\n### `convertDocument`\n\nConverts a document to a different format.\n\n`POST /v2/documents:convert`\n\n```ts\nconst { data } = await client.convertDocument(\n null,\n {\n language: 'de',\n input_document: {\n s3ref: {\n bucket: 'document-api-prod',\n key: 'uploads/my-template.pdf'\n }\n },\n output_format: 'pdf',\n output_filename: 'converted.pdf'\n },\n)\n```\n\n
\nResponse\n\n```json\n{\n \"output_document\": {\n \"preview_url\": \"https://document-api-prod.s3.eu-central-1.amazonaws.com/preview/converted.pdf\",\n \"s3ref\": {\n \"bucket\": \"document-api-prod\",\n \"key\": \"uploads/my-template.pdf\"\n }\n }\n}\n```\n\n
\n\n---\n\n## Schemas\n\n### `S3Reference`\n\n```ts\ntype S3Reference = {\n bucket: string\n key: string\n}\n```\n\n### `ErrorOutput`\n\n```ts\ntype ErrorOutput = {\n error_message?: string\n error_code?: \"PARSE_ERROR\" | \"DOC_TO_PDF_CONVERT_ERROR\" | \"INTERNAL_ERROR\" | \"INVALID_TEMPLATE_FORMAT\"\n error_details?: Array<{\n explanation?: string\n context?: {\n invalid_variables?: { ... }\n }\n }> | Array<{\n items?: {\n name?: { ... }\n message?: { ... }\n stack?: { ... }\n cause?: { ... }\n }\n }> | Array<{\n id?: string\n context?: string\n explanation?: string\n }>\n}\n```\n\n### `InvalidCustomVariableErrorDetails`\n\nError details for invalid custom variables. This error will appear under 'PARSE_ERROR' error code.\n\n```ts\ntype InvalidCustomVariableErrorDetails = Array<{\n explanation?: string\n context?: {\n invalid_variables?: Array<{\n variable?: { ... }\n error?: { ... }\n }>\n }\n}>\n```\n\n### `InvalidCustomVariableErrorDetail`\n\n```ts\ntype InvalidCustomVariableErrorDetail = {\n explanation?: string\n context?: {\n invalid_variables?: Array<{\n variable?: { ... }\n error?: { ... }\n }>\n }\n}\n```\n\n### `InternalErrorDetails`\n\nError details for internal error. This error will appear under 'INTERNAL_ERROR' error code.\n\n```ts\ntype InternalErrorDetails = Array<{\n items?: {\n name?: string\n message?: string\n stack?: string\n cause?: string\n }\n}>\n```\n\n### `InternalErrorDetail`\n\nInternal error detail\n\n```ts\ntype InternalErrorDetail = {\n name?: string\n message?: string\n stack?: string\n cause?: string\n}\n```\n\n### `DocxTemplaterErrorDetails`\n\nError details for DocxTemplater error. This error will appear under 'PARSE_ERROR' error code.\nSee https://docxtemplater.com/docs/errors/#error-schema for more details.\n\n\n```ts\ntype DocxTemplaterErrorDetails = Array<{\n id?: string\n context?: string\n explanation?: string\n}>\n```\n\n### `DocxTemplaterErrorDetail`\n\nDocxTemplater error detail\n\n```ts\ntype DocxTemplaterErrorDetail = {\n id?: string\n context?: string\n explanation?: string\n}\n```\n\n### `ErrorCode`\n\nError codes for document generation:\n- PARSE_ERROR - Error while parsing the document. Normally related with a bad template using the wrong DocxTemplater syntax.\n- DOC_TO_PDF_CONVERT_ERROR - Error while converting the document to PDF. Normally related with a ConvertAPI failure.\n- INTERNAL_ERROR - In\n\n```ts\ntype ErrorCode = \"PARSE_ERROR\" | \"DOC_TO_PDF_CONVERT_ERROR\" | \"INTERNAL_ERROR\" | \"INVALID_TEMPLATE_FORMAT\"\n```\n\n### `TemplateSettings`\n\nTemplate Settings for document generation\n\n```ts\ntype TemplateSettings = {\n custom_margins?: {\n top?: number\n bottom?: number\n }\n suggested_margins?: {\n top?: number\n bottom?: number\n }\n display_margin_guidelines?: boolean\n enable_data_table_margin_autofix?: boolean\n template_with_datatable?: boolean\n enabled_template_settings_persistence?: boolean\n misconfigured_margins?: boolean\n file_entity_id?: string // uuid\n}\n```\n\n### `DocumentMetaRequest`\n\n```ts\ntype DocumentMetaRequest = {\n template_document?: {\n s3ref?: {\n bucket: { ... }\n key: { ... }\n }\n }\n}\n```\n\n### `DocumentMetaResponse`\n\n```ts\ntype DocumentMetaResponse = {\n page_margins?: {\n top?: number\n bottom?: number\n left?: number\n right?: number\n header?: number\n footer?: number\n }\n variables?: string[]\n}\n```\n\n### `DocumentGenerationV2Request`\n\n```ts\ntype DocumentGenerationV2Request = {\n template_document: {\n filename?: string\n s3ref?: {\n bucket: { ... }\n key: { ... }\n }\n }\n context_entity_id?: string // uuid\n user_id?: string\n language?: string\n variable_payload?: {\n additionalProperties?: string\n }\n context_data?: {\n additionalProperties?: string\n }\n template_settings?: {\n custom_margins?: {\n top?: { ... }\n bottom?: { ... }\n }\n suggested_margins?: {\n top?: { ... }\n bottom?: { ... }\n }\n display_margin_guidelines?: boolean\n enable_data_table_margin_autofix?: boolean\n template_with_datatable?: boolean\n enabled_template_settings_persistence?: boolean\n misconfigured_margins?: boolean\n file_entity_id?: string // uuid\n }\n}\n```\n\n### `DocumentGenerationV2Response`\n\n```ts\ntype DocumentGenerationV2Response = {\n job_id?: string // uuid\n job_status?: \"STARTED\" | \"PROCESSING\" | \"SUCCESS\" | \"FAILED\"\n message?: string\n ics_output?: {\n output_document?: {\n filename?: { ... }\n s3ref?: { ... }\n }\n }\n pdf_output?: {\n preview_url?: string\n output_document?: {\n filename?: { ... }\n s3ref?: { ... }\n }\n }\n docx_output?: {\n preview_url?: string\n output_document?: {\n filename?: { ... }\n s3ref?: { ... }\n }\n }\n xlsx_output?: {\n preview_url?: string\n output_document?: {\n filename?: { ... }\n s3ref?: { ... }\n }\n }\n error_output?: {\n error_message?: string\n error_code?: \"PARSE_ERROR\" | \"DOC_TO_PDF_CONVERT_ERROR\" | \"INTERNAL_ERROR\" | \"INVALID_TEMPLATE_FORMAT\"\n error_details?: Array<{\n explanation?: { ... }\n context?: { ... }\n }> | Array<{\n items?: { ... }\n }> | Array<{\n id?: { ... }\n context?: { ... }\n explanation?: { ... }\n }>\n }\n variable_payload?: {\n additionalProperties?: string\n }\n template_settings?: {\n custom_margins?: {\n top?: { ... }\n bottom?: { ... }\n }\n suggested_margins?: {\n top?: { ... }\n bottom?: { ... }\n }\n display_margin_guidelines?: boolean\n enable_data_table_margin_autofix?: boolean\n template_with_datatable?: boolean\n enabled_template_settings_persistence?: boolean\n misconfigured_margins?: boolean\n file_entity_id?: string // uuid\n }\n}\n```\n\n### `ConvertDocumentRequest`\n\n```ts\ntype ConvertDocumentRequest = {\n language?: string\n input_document: {\n s3ref: {\n bucket: { ... }\n key: { ... }\n }\n }\n output_format: \"pdf\"\n output_filename?: string\n}\n```\n\n### `ConvertDocumentResponse`\n\n```ts\ntype ConvertDocumentResponse = {\n output_document?: {\n preview_url?: string\n s3ref?: {\n bucket: { ... }\n key: { ... }\n }\n }\n}\n```\n" +"# Document API\n\n- **Base URL:** `https://document.sls.epilot.io`\n- **Full API Docs:** [https://docs.epilot.io/api/document](https://docs.epilot.io/api/document)\n\n## Usage\n\n```ts\nimport { epilot } from '@epilot/sdk'\n\nepilot.authorize(() => '')\nconst { data } = await epilot.document.getTemplateMeta(...)\n```\n\n### Tree-shakeable import\n\n```ts\nimport { getClient, authorize } from '@epilot/sdk/document'\n\nconst documentClient = getClient()\nauthorize(documentClient, () => '')\nconst { data } = await documentClient.getTemplateMeta(...)\n```\n\n## Operations\n\n**Documents**\n- [`getTemplateMeta`](#gettemplatemeta)\n- [`generateDocumentV2`](#generatedocumentv2)\n- [`convertDocument`](#convertdocument)\n- [`validateTemplate`](#validatetemplate)\n\n**Schemas**\n- [`S3Reference`](#s3reference)\n- [`ErrorOutput`](#erroroutput)\n- [`InvalidCustomVariableErrorDetails`](#invalidcustomvariableerrordetails)\n- [`InvalidCustomVariableErrorDetail`](#invalidcustomvariableerrordetail)\n- [`InternalErrorDetails`](#internalerrordetails)\n- [`InternalErrorDetail`](#internalerrordetail)\n- [`DocxTemplaterErrorDetails`](#docxtemplatererrordetails)\n- [`DocxTemplaterErrorDetail`](#docxtemplatererrordetail)\n- [`ErrorCode`](#errorcode)\n- [`TemplateValidationRequest`](#templatevalidationrequest)\n- [`TemplateValidationResponse`](#templatevalidationresponse)\n- [`TemplateIssue`](#templateissue)\n- [`TemplateSettings`](#templatesettings)\n- [`DocumentMetaRequest`](#documentmetarequest)\n- [`DocumentMetaResponse`](#documentmetaresponse)\n- [`DocumentGenerationV2Request`](#documentgenerationv2request)\n- [`DocumentGenerationV2Response`](#documentgenerationv2response)\n- [`ConvertDocumentRequest`](#convertdocumentrequest)\n- [`ConvertDocumentResponse`](#convertdocumentresponse)\n\n### `getTemplateMeta`\n\nGet metadata for a document template\n\n`POST /v2/documents:meta`\n\n```ts\nconst { data } = await client.getTemplateMeta(\n null,\n {\n template_document: {\n s3ref: {\n bucket: 'document-api-prod',\n key: 'uploads/my-template.pdf'\n }\n }\n },\n)\n```\n\n
\nResponse\n\n```json\n{\n \"page_margins\": {\n \"top\": 2.54,\n \"bottom\": 2.54,\n \"left\": 2.54,\n \"right\": 2.54,\n \"header\": 2.54,\n \"footer\": 2.54\n },\n \"variables\": [\"order.billing_contact.0.salutation\", \"order.billing_contact.0.title\", \"order_table\", \"stayHardStatic\", \"opportunity[attribute_name]\", \"opportunity[\\\"attribute_name\\\"]\", \"opportunity.[attribute_name]\", \"attribute_name\", \"opportunities.0.attribute_name\", \"opportunities[0].attribute_name\", \"contact.opportunities[0].attribute_name\", \"opportunities[Primary].attribute_name\"]\n}\n```\n\n
\n\n---\n\n### `generateDocumentV2`\n\nGenerates documents from templates with variables.\n\n`POST /v2/documents:generate`\n\n```ts\nconst { data } = await client.generateDocumentV2(\n {\n job_id: 'example',\n mode: 'example',\n preview_mode: 'example',\n },\n {\n template_document: {\n filename: 'my-template-{{order.order_number}}.docx',\n s3ref: {\n bucket: 'document-api-prod',\n key: 'uploads/my-template.pdf'\n }\n },\n context_entity_id: 'bcd0aab9-b544-42b0-8bfb-6d449d02eacc',\n user_id: '100321',\n language: 'de',\n variable_payload: {\n additionalProperties: 'string'\n },\n context_data: {\n additionalProperties: 'string'\n },\n template_settings: {\n custom_margins: {\n top: 2.54,\n bottom: 2.54\n },\n suggested_margins: {\n top: 2.54,\n bottom: 2.54\n },\n display_margin_guidelines: true,\n enable_data_table_margin_autofix: false,\n template_with_datatable: false,\n enabled_template_settings_persistence: false,\n misconfigured_margins: false,\n file_entity_id: '123e4567-e89b-12d3-a456-426614174000'\n }\n },\n)\n```\n\n
\nResponse\n\n```json\n{\n \"job_id\": \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n \"job_status\": \"STARTED\",\n \"message\": \"string\",\n \"ics_output\": {\n \"output_document\": {\n \"s3ref\": {\n \"bucket\": \"document-api-preview-prod\",\n \"key\": \"preview/my-appointment.ics\"\n }\n }\n },\n \"pdf_output\": {\n \"preview_url\": \"https://document-api-prod.s3.eu-central-1.amazonaws.com/preview/my-template-OR-001.pdf\",\n \"output_document\": {\n \"s3ref\": {\n \"bucket\": \"document-api-preview-prod\",\n \"key\": \"preview/my-template.pdf\"\n }\n }\n },\n \"docx_output\": {\n \"preview_url\": \"https://document-api-prod.s3.eu-central-1.amazonaws.com/preview/my-template-OR-001.docx\",\n \"output_document\": {\n \"s3ref\": {\n \"bucket\": \"document-api-preview-prod\",\n \"key\": \"preview/my-template.docx\"\n }\n }\n },\n \"xlsx_output\": {\n \"preview_url\": \"https://document-api-prod.s3.eu-central-1.amazonaws.com/preview/my-template-OR-001.xlsx\",\n \"output_document\": {\n \"s3ref\": {\n \"bucket\": \"document-api-preview-prod\",\n \"key\": \"preview/my-template.xlsx\"\n }\n }\n },\n \"error_output\": {\n \"error_message\": \"string\",\n \"error_code\": \"PARSE_ERROR\",\n \"error_details\": [\n {\n \"explanation\": \"string\",\n \"context\": {\n \"invalid_variables\": [\n {\n \"variable\": \"string\",\n \"error\": \"string\"\n }\n ]\n }\n }\n ]\n },\n \"variable_payload\": {\n \"additionalProperties\": \"string\"\n },\n \"template_settings\": {\n \"custom_margins\": {\n \"top\": 2.54,\n \"bottom\": 2.54\n },\n \"suggested_margins\": {\n \"top\": 2.54,\n \"bottom\": 2.54\n },\n \"display_margin_guidelines\": true,\n \"enable_data_table_margin_autofix\": false,\n \"template_with_datatable\": false,\n \"enabled_template_settings_persistence\": false,\n \"misconfigured_margins\": false,\n \"file_entity_id\": \"123e4567-e89b-12d3-a456-426614174000\"\n }\n}\n```\n\n
\n\n---\n\n### `convertDocument`\n\nConverts a document to a different format.\n\n`POST /v2/documents:convert`\n\n```ts\nconst { data } = await client.convertDocument(\n null,\n {\n language: 'de',\n input_document: {\n s3ref: {\n bucket: 'document-api-prod',\n key: 'uploads/my-template.pdf'\n }\n },\n output_format: 'pdf',\n output_filename: 'converted.pdf'\n },\n)\n```\n\n
\nResponse\n\n```json\n{\n \"output_document\": {\n \"preview_url\": \"https://document-api-prod.s3.eu-central-1.amazonaws.com/preview/converted.pdf\",\n \"s3ref\": {\n \"bucket\": \"document-api-prod\",\n \"key\": \"uploads/my-template.pdf\"\n }\n }\n}\n```\n\n
\n\n---\n\n### `validateTemplate`\n\nValidates a document template's variable syntax and, optionally, proposes a hotfixed copy of it.\n\n`POST /v2/templates:validate`\n\n```ts\nconst { data } = await client.validateTemplate(\n null,\n {\n template_document: {\n filename: 'Umzugsmeldung.xlsx',\n s3ref: {\n bucket: 'document-api-prod',\n key: 'uploads/my-template.pdf'\n }\n },\n fix: true,\n fix_level: 'safe'\n },\n)\n```\n\n
\nResponse\n\n```json\n{\n \"valid\": false,\n \"fixed\": true,\n \"issues\": [\n {\n \"id\": \"unopened_tag\",\n \"file\": \"xl/sharedStrings.xml\",\n \"location\": \"Tabelle1!N4\",\n \"context\": \"…Datum: {system.date}} Unterschrift…\",\n \"explanation\": \"The tag is missing an opening brace.\",\n \"fixable\": true,\n \"confidence\": \"high\",\n \"rule\": \"balance_opening_delimiter\",\n \"before\": \"{system.date}}\",\n \"after\": \"{{system.date}}\"\n }\n ],\n \"unresolved_errors\": [\n {\n \"id\": \"string\",\n \"context\": \"string\",\n \"explanation\": \"string\"\n }\n ],\n \"fixed_document\": {\n \"s3ref\": {\n \"bucket\": \"document-api-prod\",\n \"key\": \"uploads/my-template.pdf\"\n },\n \"filename\": \"Umzugsmeldung (fixed).xlsx\",\n \"preview_url\": \"https://example.com/path\"\n }\n}\n```\n\n
\n\n---\n\n## Schemas\n\n### `S3Reference`\n\n```ts\ntype S3Reference = {\n bucket: string\n key: string\n}\n```\n\n### `ErrorOutput`\n\n```ts\ntype ErrorOutput = {\n error_message?: string\n error_code?: \"PARSE_ERROR\" | \"DOC_TO_PDF_CONVERT_ERROR\" | \"INTERNAL_ERROR\" | \"INVALID_TEMPLATE_FORMAT\" | \"TEMPLATE_NOT_FOUND\"\n error_details?: Array<{\n explanation?: string\n context?: {\n invalid_variables?: { ... }\n }\n }> | Array<{\n items?: {\n name?: { ... }\n message?: { ... }\n stack?: { ... }\n cause?: { ... }\n }\n }> | Array<{\n id?: string\n context?: string\n explanation?: string\n }>\n}\n```\n\n### `InvalidCustomVariableErrorDetails`\n\nError details for invalid custom variables. This error will appear under 'PARSE_ERROR' error code.\n\n```ts\ntype InvalidCustomVariableErrorDetails = Array<{\n explanation?: string\n context?: {\n invalid_variables?: Array<{\n variable?: { ... }\n error?: { ... }\n }>\n }\n}>\n```\n\n### `InvalidCustomVariableErrorDetail`\n\n```ts\ntype InvalidCustomVariableErrorDetail = {\n explanation?: string\n context?: {\n invalid_variables?: Array<{\n variable?: { ... }\n error?: { ... }\n }>\n }\n}\n```\n\n### `InternalErrorDetails`\n\nError details for internal error. This error will appear under 'INTERNAL_ERROR' error code.\n\n```ts\ntype InternalErrorDetails = Array<{\n items?: {\n name?: string\n message?: string\n stack?: string\n cause?: string\n }\n}>\n```\n\n### `InternalErrorDetail`\n\nInternal error detail\n\n```ts\ntype InternalErrorDetail = {\n name?: string\n message?: string\n stack?: string\n cause?: string\n}\n```\n\n### `DocxTemplaterErrorDetails`\n\nError details for DocxTemplater error. This error will appear under 'PARSE_ERROR' error code.\nSee https://docxtemplater.com/docs/errors/#error-schema for more details.\n\n\n```ts\ntype DocxTemplaterErrorDetails = Array<{\n id?: string\n context?: string\n explanation?: string\n}>\n```\n\n### `DocxTemplaterErrorDetail`\n\nDocxTemplater error detail\n\n```ts\ntype DocxTemplaterErrorDetail = {\n id?: string\n context?: string\n explanation?: string\n}\n```\n\n### `ErrorCode`\n\nError codes for document generation:\n- PARSE_ERROR - Error while parsing the document. Normally related with a bad template using the wrong DocxTemplater syntax.\n- DOC_TO_PDF_CONVERT_ERROR - Error while converting the document to PDF. Normally related with a ConvertAPI failure.\n- INTERNAL_ERROR - In\n\n```ts\ntype ErrorCode = \"PARSE_ERROR\" | \"DOC_TO_PDF_CONVERT_ERROR\" | \"INTERNAL_ERROR\" | \"INVALID_TEMPLATE_FORMAT\" | \"TEMPLATE_NOT_FOUND\"\n```\n\n### `TemplateValidationRequest`\n\n```ts\ntype TemplateValidationRequest = {\n template_document: {\n filename?: string\n s3ref: {\n bucket: { ... }\n key: { ... }\n }\n }\n fix?: boolean\n fix_level?: \"safe\" | \"aggressive\"\n}\n```\n\n### `TemplateValidationResponse`\n\n```ts\ntype TemplateValidationResponse = {\n valid?: boolean\n fixed?: boolean\n issues?: Array<{\n id?: string\n file?: string\n location?: string\n context?: string\n explanation?: string\n fixable?: boolean\n confidence?: \"high\" | \"medium\" | \"low\"\n rule?: string\n before?: string\n after?: string\n }>\n unresolved_errors?: Array<{\n id?: string\n context?: string\n explanation?: string\n }>\n fixed_document?: {\n s3ref?: {\n bucket: { ... }\n key: { ... }\n }\n filename?: string\n preview_url?: string // uri\n }\n}\n```\n\n### `TemplateIssue`\n\nA single template syntax problem, and the repair proposed for it\n\n```ts\ntype TemplateIssue = {\n id?: string\n file?: string\n location?: string\n context?: string\n explanation?: string\n fixable?: boolean\n confidence?: \"high\" | \"medium\" | \"low\"\n rule?: string\n before?: string\n after?: string\n}\n```\n\n### `TemplateSettings`\n\nTemplate Settings for document generation\n\n```ts\ntype TemplateSettings = {\n custom_margins?: {\n top?: number\n bottom?: number\n }\n suggested_margins?: {\n top?: number\n bottom?: number\n }\n display_margin_guidelines?: boolean\n enable_data_table_margin_autofix?: boolean\n template_with_datatable?: boolean\n enabled_template_settings_persistence?: boolean\n misconfigured_margins?: boolean\n file_entity_id?: string // uuid\n}\n```\n\n### `DocumentMetaRequest`\n\n```ts\ntype DocumentMetaRequest = {\n template_document?: {\n s3ref?: {\n bucket: { ... }\n key: { ... }\n }\n }\n}\n```\n\n### `DocumentMetaResponse`\n\n```ts\ntype DocumentMetaResponse = {\n page_margins?: {\n top?: number\n bottom?: number\n left?: number\n right?: number\n header?: number\n footer?: number\n }\n variables?: string[]\n}\n```\n\n### `DocumentGenerationV2Request`\n\n```ts\ntype DocumentGenerationV2Request = {\n template_document: {\n filename?: string\n s3ref?: {\n bucket: { ... }\n key: { ... }\n }\n }\n context_entity_id?: string // uuid\n user_id?: string\n language?: string\n variable_payload?: {\n additionalProperties?: string\n }\n context_data?: {\n additionalProperties?: string\n }\n template_settings?: {\n custom_margins?: {\n top?: { ... }\n bottom?: { ... }\n }\n suggested_margins?: {\n top?: { ... }\n bottom?: { ... }\n }\n display_margin_guidelines?: boolean\n enable_data_table_margin_autofix?: boolean\n template_with_datatable?: boolean\n enabled_template_settings_persistence?: boolean\n misconfigured_margins?: boolean\n file_entity_id?: string // uuid\n }\n}\n```\n\n### `DocumentGenerationV2Response`\n\n```ts\ntype DocumentGenerationV2Response = {\n job_id?: string // uuid\n job_status?: \"STARTED\" | \"PROCESSING\" | \"SUCCESS\" | \"FAILED\"\n message?: string\n ics_output?: {\n output_document?: {\n filename?: { ... }\n s3ref?: { ... }\n }\n }\n pdf_output?: {\n preview_url?: string\n output_document?: {\n filename?: { ... }\n s3ref?: { ... }\n }\n }\n docx_output?: {\n preview_url?: string\n output_document?: {\n filename?: { ... }\n s3ref?: { ... }\n }\n }\n xlsx_output?: {\n preview_url?: string\n output_document?: {\n filename?: { ... }\n s3ref?: { ... }\n }\n }\n error_output?: {\n error_message?: string\n error_code?: \"PARSE_ERROR\" | \"DOC_TO_PDF_CONVERT_ERROR\" | \"INTERNAL_ERROR\" | \"INVALID_TEMPLATE_FORMAT\" | \"TEMPLATE_NOT_FOUND\"\n error_details?: Array<{\n explanation?: { ... }\n context?: { ... }\n }> | Array<{\n items?: { ... }\n }> | Array<{\n id?: { ... }\n context?: { ... }\n explanation?: { ... }\n }>\n }\n variable_payload?: {\n additionalProperties?: string\n }\n template_settings?: {\n custom_margins?: {\n top?: { ... }\n bottom?: { ... }\n }\n suggested_margins?: {\n top?: { ... }\n bottom?: { ... }\n }\n display_margin_guidelines?: boolean\n enable_data_table_margin_autofix?: boolean\n template_with_datatable?: boolean\n enabled_template_settings_persistence?: boolean\n misconfigured_margins?: boolean\n file_entity_id?: string // uuid\n }\n}\n```\n\n### `ConvertDocumentRequest`\n\n```ts\ntype ConvertDocumentRequest = {\n language?: string\n input_document: {\n s3ref: {\n bucket: { ... }\n key: { ... }\n }\n }\n output_format: \"pdf\"\n output_filename?: string\n}\n```\n\n### `ConvertDocumentResponse`\n\n```ts\ntype ConvertDocumentResponse = {\n output_document?: {\n preview_url?: string\n s3ref?: {\n bucket: { ... }\n key: { ... }\n }\n }\n}\n```\n" diff --git a/packages/epilot-sdk-v2/src/types/configuration-hub.d.ts b/packages/epilot-sdk-v2/src/types/configuration-hub.d.ts index df4e6dcb2..d4eaf6586 100644 --- a/packages/epilot-sdk-v2/src/types/configuration-hub.d.ts +++ b/packages/epilot-sdk-v2/src/types/configuration-hub.d.ts @@ -1248,307 +1248,307 @@ export declare namespace Paths { export interface OperationMethods { /** * listConfigTypes - listConfigTypes - * + * * Returns the static list of available configuration types with display metadata. * This is a cheap call — no fan-out to downstream APIs. Returns all known types * with labels and icons. The frontend should then call `listConfigs` separately * for each type it wants to load. - * + * */ 'listConfigTypes'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * listConfigs - listConfigs - * + * * List configs of a given type with pagination. Returns summary metadata only * (not full payloads). The frontend calls this per type folder when expanding. - * + * * Supports offset-based pagination via `from` and `size` parameters. - * + * */ 'listConfigs'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * getConfigDependencies - getConfigDependencies - * + * * Get configs that are referenced by the given config. * Used to render children when expanding a config node in the tree. - * + * * Resolves dependencies by fetching the config payload server-side and scanning * for references (UUIDs, source IDs, slug-based references). - * + * */ 'getConfigDependencies'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * getConfigUsedBy - getConfigUsedBy - * + * * Get configs that reference the given config (reverse dependencies). * Scans the indexed config items for references to this config's ID or aliases. - * + * */ 'getConfigUsedBy'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * getIndex - getIndex - * + * * Return the current index build state for the caller's organization. * Clients poll this to decide whether to show a "building" indicator * and when to refetch data. - * + * */ 'getIndex'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * compareConfigs - compareConfigs - * + * * Compare the caller org's configs of a single type against another * (source) org, side by side. Rows are paired via the lineage registry in * both sync directions (caller imported from source, or source imported * from caller); configs without a counterpart come back as `only_current` * or `only_source`. - * + * * `source_auth_token` must be a valid token for `source_org_id` — the * frontend mints one via the pipeline pairing, mirroring `createSyncJob`'s * `target_auth_token`. The token is verified against `source_org_id` * before any source-org data is read. - * + * * POST because the request carries a token; the operation reads only. - * + * */ 'compareConfigs'( parameters?: Parameters | null, data?: Paths.CompareConfigs.RequestBody, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * suggestMatches - suggestMatches - * + * * Run the sync-grade heuristic match (`lookupByHeuristic` — name / slug / * unique key) for a batch of source-org configs that have no lineage * entry, and return candidate counterparts in the caller's org. - * + * * Suggestions are ephemeral — nothing is persisted. The client offers * each candidate to the user, and a confirmed pair is written via * `confirmLineage`. Batches are capped at 25 ids; the client pages * through unmatched rows across successive calls. - * + * */ 'suggestMatches'( parameters?: Parameters | null, data?: Paths.SuggestMatches.RequestBody, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * confirmLineage - confirmLineage - * + * * Persist a lineage entry pairing a source-org config with a config in * the caller's org — used to confirm a heuristic suggestion from the * Compare view. Writes to the caller org's lineage partition (caller as * sync target), so subsequent syncs PATCH the confirmed target instead of * creating a duplicate. Conflicts (an existing entry pointing at a * different target) return 409 so the client can refresh. - * + * */ 'confirmLineage'( parameters?: Parameters | null, data?: Paths.ConfirmLineage.RequestBody, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * breakLineage - breakLineage - * + * * Delete a lineage entry from the caller org's partition (caller as sync * target), identified by `type` + `source_id`. Used to break a wrong or * stale match from the Compare view. Note: a future sync can re-match the * same pair heuristically — the entry is deleted, not blocklisted. To * break a match recorded in the OTHER direction, call this operation * authenticated as the other org (pipeline token). - * + * */ 'breakLineage'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * listSyncJobs - listSyncJobs - * + * * List sync jobs scoped to the caller's organization, paginated with an opaque * cursor. Defaults to most-recent first. - * + * */ 'listSyncJobs'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * createSyncJob - createSyncJob - * + * * Create a new cross-org sync job. The job is enqueued for asynchronous execution * by the worker Lambda; the response returns the persisted job header with status * `pending`. - * + * * See `docs/sync/INTERFACES.md` for the locked request/response contract. - * + * */ 'createSyncJob'( parameters?: Parameters | null, data?: Paths.CreateSyncJob.RequestBody, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * getSyncJob - getSyncJob - * + * * Fetch a single sync job by ID. Returns the job header, counts summary, * current phase pointer, and the latest activity events. Frontend polls this * endpoint with a ramping interval. - * + * */ 'getSyncJob'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * retrySyncJob - retrySyncJob - * + * * Retry the unresolved resources from a prior sync job: `failed` rows, plus * rows the original run left at `pending`/`in_progress` because it stopped * early. Creates a new job whose scope is that `(type, source_id)` set and * enqueues it for execution. Optionally accepts inline payload overrides. - * + * */ 'retrySyncJob'( parameters?: Parameters | null, data?: Paths.RetrySyncJob.RequestBody, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * cancelSyncJob - cancelSyncJob - * + * * Cancel a running sync job. Marks the job `cancelled` with a `finished_at` * so it stops being reported as in-flight, and the worker stops at its next * batch boundary — phases re-read the job status and abort rather than * overwrite a cancellation with their own outcome. - * + * * Resources already written to the target are NOT rolled back; cancelling * stops further work. Jobs already in a terminal state are rejected with * 409. - * + * */ 'cancelSyncJob'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * listSyncJobResources - listSyncJobResources - * + * * List the per-resource rows for a sync job. Supports filtering by status * (e.g. `failed`) and cursor pagination. Used by the failures table and the * dry-run plan view in the frontend. - * + * */ 'listSyncJobResources'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * listDeleteJobs - listDeleteJobs - * + * * List bulk-delete jobs scoped to the caller's organization, paginated * with an opaque cursor. Most-recent first. - * + * */ 'listDeleteJobs'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * createDeleteJob - createDeleteJob - * + * * Create a bulk-delete job for the caller's organization. The selected * resources are deleted asynchronously by a worker Lambda; the response * returns the persisted job header with status `pending`. - * + * * Raw delete — no dependency checks are performed. On completion the * worker auto-triggers an index rebuild so the config list reflects the * deletions. - * + * */ 'createDeleteJob'( parameters?: Parameters | null, data?: Paths.CreateDeleteJob.RequestBody, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * getDeleteJob - getDeleteJob - * + * * Fetch a single bulk-delete job by ID. Returns the job header and counts * summary. Frontend polls this endpoint while the job runs. - * + * */ 'getDeleteJob'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * listDeleteJobResources - listDeleteJobResources - * + * * List the per-resource rows for a delete job, cursor-paginated. Used by * the failures view in the frontend. - * + * */ 'listDeleteJobResources'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * rebuildIndex - rebuildIndex - * + * * Rebuild the configuration index for the caller's organization. * Fire-and-forget: invokes the async worker and returns immediately. * A new rebuild will cancel any in-flight build (see `build_token`). - * + * */ 'rebuildIndex'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * getConfigInventory - getConfigInventory - * + * * Returns a fresh inventory of an org's configuration resources — `{ type, id }` identities only, * no full payloads. Calls every adapter's `list()` live (bypasses the 7-day DynamoDB index). * Intended for snapshot-api to consume when taking a full-org snapshot. - * + * */ 'getConfigInventory'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } @@ -1556,341 +1556,341 @@ export interface PathsDictionary { ['/v1/configs/types']: { /** * listConfigTypes - listConfigTypes - * + * * Returns the static list of available configuration types with display metadata. * This is a cheap call — no fan-out to downstream APIs. Returns all known types * with labels and icons. The frontend should then call `listConfigs` separately * for each type it wants to load. - * + * */ 'get'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/configs/{type}']: { /** * listConfigs - listConfigs - * + * * List configs of a given type with pagination. Returns summary metadata only * (not full payloads). The frontend calls this per type folder when expanding. - * + * * Supports offset-based pagination via `from` and `size` parameters. - * + * */ 'get'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/configs/{type}/{id}/dependencies']: { /** * getConfigDependencies - getConfigDependencies - * + * * Get configs that are referenced by the given config. * Used to render children when expanding a config node in the tree. - * + * * Resolves dependencies by fetching the config payload server-side and scanning * for references (UUIDs, source IDs, slug-based references). - * + * */ 'get'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/configs/{type}/{id}/used_by']: { /** * getConfigUsedBy - getConfigUsedBy - * + * * Get configs that reference the given config (reverse dependencies). * Scans the indexed config items for references to this config's ID or aliases. - * + * */ 'get'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/configs/index']: { /** * getIndex - getIndex - * + * * Return the current index build state for the caller's organization. * Clients poll this to decide whether to show a "building" indicator * and when to refetch data. - * + * */ 'get'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/configs/compare']: { /** * compareConfigs - compareConfigs - * + * * Compare the caller org's configs of a single type against another * (source) org, side by side. Rows are paired via the lineage registry in * both sync directions (caller imported from source, or source imported * from caller); configs without a counterpart come back as `only_current` * or `only_source`. - * + * * `source_auth_token` must be a valid token for `source_org_id` — the * frontend mints one via the pipeline pairing, mirroring `createSyncJob`'s * `target_auth_token`. The token is verified against `source_org_id` * before any source-org data is read. - * + * * POST because the request carries a token; the operation reads only. - * + * */ 'post'( parameters?: Parameters | null, data?: Paths.CompareConfigs.RequestBody, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/configs/compare/suggestions']: { /** * suggestMatches - suggestMatches - * + * * Run the sync-grade heuristic match (`lookupByHeuristic` — name / slug / * unique key) for a batch of source-org configs that have no lineage * entry, and return candidate counterparts in the caller's org. - * + * * Suggestions are ephemeral — nothing is persisted. The client offers * each candidate to the user, and a confirmed pair is written via * `confirmLineage`. Batches are capped at 25 ids; the client pages * through unmatched rows across successive calls. - * + * */ 'post'( parameters?: Parameters | null, data?: Paths.SuggestMatches.RequestBody, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/configs/lineage']: { /** * confirmLineage - confirmLineage - * + * * Persist a lineage entry pairing a source-org config with a config in * the caller's org — used to confirm a heuristic suggestion from the * Compare view. Writes to the caller org's lineage partition (caller as * sync target), so subsequent syncs PATCH the confirmed target instead of * creating a duplicate. Conflicts (an existing entry pointing at a * different target) return 409 so the client can refresh. - * + * */ 'post'( parameters?: Parameters | null, data?: Paths.ConfirmLineage.RequestBody, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * breakLineage - breakLineage - * + * * Delete a lineage entry from the caller org's partition (caller as sync * target), identified by `type` + `source_id`. Used to break a wrong or * stale match from the Compare view. Note: a future sync can re-match the * same pair heuristically — the entry is deleted, not blocklisted. To * break a match recorded in the OTHER direction, call this operation * authenticated as the other org (pipeline token). - * + * */ 'delete'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/configs/sync-jobs']: { /** * createSyncJob - createSyncJob - * + * * Create a new cross-org sync job. The job is enqueued for asynchronous execution * by the worker Lambda; the response returns the persisted job header with status * `pending`. - * + * * See `docs/sync/INTERFACES.md` for the locked request/response contract. - * + * */ 'post'( parameters?: Parameters | null, data?: Paths.CreateSyncJob.RequestBody, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * listSyncJobs - listSyncJobs - * + * * List sync jobs scoped to the caller's organization, paginated with an opaque * cursor. Defaults to most-recent first. - * + * */ 'get'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/configs/sync-jobs/{id}']: { /** * getSyncJob - getSyncJob - * + * * Fetch a single sync job by ID. Returns the job header, counts summary, * current phase pointer, and the latest activity events. Frontend polls this * endpoint with a ramping interval. - * + * */ 'get'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/configs/sync-jobs/{id}/retry']: { /** * retrySyncJob - retrySyncJob - * + * * Retry the unresolved resources from a prior sync job: `failed` rows, plus * rows the original run left at `pending`/`in_progress` because it stopped * early. Creates a new job whose scope is that `(type, source_id)` set and * enqueues it for execution. Optionally accepts inline payload overrides. - * + * */ 'post'( parameters?: Parameters | null, data?: Paths.RetrySyncJob.RequestBody, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/configs/sync-jobs/{id}/cancel']: { /** * cancelSyncJob - cancelSyncJob - * + * * Cancel a running sync job. Marks the job `cancelled` with a `finished_at` * so it stops being reported as in-flight, and the worker stops at its next * batch boundary — phases re-read the job status and abort rather than * overwrite a cancellation with their own outcome. - * + * * Resources already written to the target are NOT rolled back; cancelling * stops further work. Jobs already in a terminal state are rejected with * 409. - * + * */ 'post'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/configs/sync-jobs/{id}/resources']: { /** * listSyncJobResources - listSyncJobResources - * + * * List the per-resource rows for a sync job. Supports filtering by status * (e.g. `failed`) and cursor pagination. Used by the failures table and the * dry-run plan view in the frontend. - * + * */ 'get'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/configs/delete-jobs']: { /** * createDeleteJob - createDeleteJob - * + * * Create a bulk-delete job for the caller's organization. The selected * resources are deleted asynchronously by a worker Lambda; the response * returns the persisted job header with status `pending`. - * + * * Raw delete — no dependency checks are performed. On completion the * worker auto-triggers an index rebuild so the config list reflects the * deletions. - * + * */ 'post'( parameters?: Parameters | null, data?: Paths.CreateDeleteJob.RequestBody, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse /** * listDeleteJobs - listDeleteJobs - * + * * List bulk-delete jobs scoped to the caller's organization, paginated * with an opaque cursor. Most-recent first. - * + * */ 'get'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/configs/delete-jobs/{id}']: { /** * getDeleteJob - getDeleteJob - * + * * Fetch a single bulk-delete job by ID. Returns the job header and counts * summary. Frontend polls this endpoint while the job runs. - * + * */ 'get'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/configs/delete-jobs/{id}/resources']: { /** * listDeleteJobResources - listDeleteJobResources - * + * * List the per-resource rows for a delete job, cursor-paginated. Used by * the failures view in the frontend. - * + * */ 'get'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/configs/index:rebuild']: { /** * rebuildIndex - rebuildIndex - * + * * Rebuild the configuration index for the caller's organization. * Fire-and-forget: invokes the async worker and returns immediately. * A new rebuild will cancel any in-flight build (see `build_token`). - * + * */ 'post'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } ['/v1/configs/inventory']: { /** * getConfigInventory - getConfigInventory - * + * * Returns a fresh inventory of an org's configuration resources — `{ type, id }` identities only, * no full payloads. Calls every adapter's `list()` live (bypasses the 7-day DynamoDB index). * Intended for snapshot-api to consume when taking a full-org snapshot. - * + * */ 'get'( parameters?: Parameters | null, data?: any, - config?: AxiosRequestConfig + config?: AxiosRequestConfig ): OperationResponse } } diff --git a/packages/epilot-sdk-v2/src/types/document.d.ts b/packages/epilot-sdk-v2/src/types/document.d.ts index 7783ff122..4e6adcea0 100644 --- a/packages/epilot-sdk-v2/src/types/document.d.ts +++ b/packages/epilot-sdk-v2/src/types/document.d.ts @@ -1,6 +1,4 @@ /* Auto-copied from document-client */ -/* eslint-disable */ - import type { OpenAPIClient, Parameters, @@ -307,9 +305,10 @@ export declare namespace Components { * - DOC_TO_PDF_CONVERT_ERROR - Error while converting the document to PDF. Normally related with a ConvertAPI failure. * - INTERNAL_ERROR - Internal error. Please contact support. * - INVALID_TEMPLATE_FORMAT - Invalid template format (only .docx is supported). This can happen due to a bad word file or an unsupported file extension. + * - TEMPLATE_NOT_FOUND - Template file was not found in S3. This indicates the template was likely deleted. * */ - export type ErrorCode = "PARSE_ERROR" | "DOC_TO_PDF_CONVERT_ERROR" | "INTERNAL_ERROR" | "INVALID_TEMPLATE_FORMAT"; + export type ErrorCode = "PARSE_ERROR" | "DOC_TO_PDF_CONVERT_ERROR" | "INTERNAL_ERROR" | "INVALID_TEMPLATE_FORMAT" | "TEMPLATE_NOT_FOUND"; export interface ErrorOutput { /** * Error message @@ -321,6 +320,7 @@ export declare namespace Components { * - DOC_TO_PDF_CONVERT_ERROR - Error while converting the document to PDF. Normally related with a ConvertAPI failure. * - INTERNAL_ERROR - Internal error. Please contact support. * - INVALID_TEMPLATE_FORMAT - Invalid template format (only .docx is supported). This can happen due to a bad word file or an unsupported file extension. + * - TEMPLATE_NOT_FOUND - Template file was not found in S3. This indicates the template was likely deleted. * */ ErrorCode; @@ -399,6 +399,75 @@ export declare namespace Components { */ key: string; } + /** + * A single template syntax problem, and the repair proposed for it + */ + export interface TemplateIssue { + /** + * Problem kind. Reuses docxtemplater's error vocabulary where one exists + * (`unopened_tag`, `unclosed_tag`, `duplicate_open_tag`, `duplicate_close_tag`), plus + * `malformed_tag` for single-brace placeholders and `typographic_characters` for + * editor-substituted characters inside an otherwise valid tag. + * + * example: + * unopened_tag + */ + id?: string; + /** + * Zip part the problem was found in + * example: + * xl/sharedStrings.xml + */ + file?: string; + /** + * Where the user can find it — a cell reference for spreadsheets, a part label + * (`Document body`, `Header 1`, `Slide 3`) otherwise. + * + * example: + * Tabelle1!N4 + */ + location?: string; + /** + * The offending text with surrounding context + * example: + * …Datum: {system.date}} Unterschrift… + */ + context?: string; + /** + * Why the template parser rejects it + * example: + * The tag is missing an opening brace. + */ + explanation?: string; + /** + * Whether the proposed repair was applied to the fixed copy + * example: + * true + */ + fixable?: boolean; + /** + * How sure we are that the repair is what the author meant + */ + confidence?: "high" | "medium" | "low"; + /** + * The repair rule that produced `after` + * example: + * balance_opening_delimiter + */ + rule?: string; + /** + * The text as it is in the template + * example: + * {system.date}} + */ + before?: string; + /** + * The text as it would be in the fixed copy + * example: + * {{system.date}} + */ + after?: string; + } /** * Template Settings for document generation */ @@ -470,10 +539,81 @@ export declare namespace Components { /** * The file entity id, used when persisting a new template version with updated settings * example: - * 1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p + * 123e4567-e89b-12d3-a456-426614174000 */ file_entity_id?: string; // uuid } + export interface TemplateValidationRequest { + /** + * Input template document + */ + template_document: { + /** + * Document original filename, used to name the fixed copy + * example: + * Umzugsmeldung.xlsx + */ + filename?: string; + s3ref: S3Reference; + }; + /** + * Attempt to produce a hotfixed copy of the template. When false, the template is only + * inspected and no file is written. + * + */ + fix?: boolean; + /** + * How far the hotfix may go: + * - safe - only repairs where the author's intent is unambiguous from the syntax + * (unbalanced or duplicated braces, editor-substituted characters inside a tag). + * - aggressive - additionally promotes single-brace placeholders such as `{contact.name}` + * to `{{contact.name}}`. These are common in customer templates but indistinguishable + * from prose that uses braces, so they are reported with `confidence: low`. + * + */ + fix_level?: "safe" | "aggressive"; + } + export interface TemplateValidationResponse { + /** + * Whether the template compiled cleanly before any repair was attempted + * example: + * false + */ + valid?: boolean; + /** + * Whether a corrected copy of the template could be produced + * example: + * true + */ + fixed?: boolean; + /** + * Everything found in the template, whether or not it could be repaired + */ + issues?: /* A single template syntax problem, and the repair proposed for it */ TemplateIssue[]; + /** + * Parser errors that remain after the hotfix (or the original errors when nothing was + * fixed). Empty when the template is valid. + * + */ + unresolved_errors?: /* DocxTemplater error detail */ DocxTemplaterErrorDetail[]; + /** + * The corrected copy, for the user to review and accept. Absent when no repair was + * applied. The original template is left untouched. + * + */ + fixed_document?: { + s3ref?: S3Reference; + /** + * example: + * Umzugsmeldung (fixed).xlsx + */ + filename?: string; + /** + * Short-lived download link for the corrected copy + */ + preview_url?: string; // uri + }; + } } } export declare namespace Paths { @@ -486,13 +626,33 @@ export declare namespace Paths { namespace GenerateDocumentV2 { namespace Parameters { export type JobId = string; + /** + * - partial_generation: Generates a partial document for user validation before final generation + * - full_generation: Completes the entire document generation process in one step + * + */ export type Mode = "partial_generation" | "full_generation"; + /** + * - open: Preview URL opens the file directly in browser + * - download: Preview URL triggers a download of the file + * + */ export type PreviewMode = "open" | "download"; } export interface QueryParameters { job_id?: Parameters.JobId; - mode?: Parameters.Mode; - preview_mode?: Parameters.PreviewMode; + mode?: /** + * - partial_generation: Generates a partial document for user validation before final generation + * - full_generation: Completes the entire document generation process in one step + * + */ + Parameters.Mode; + preview_mode?: /** + * - open: Preview URL opens the file directly in browser + * - download: Preview URL triggers a download of the file + * + */ + Parameters.PreviewMode; } export type RequestBody = Components.Schemas.DocumentGenerationV2Request; namespace Responses { @@ -508,6 +668,15 @@ export declare namespace Paths { export type $415 = Components.Schemas.ErrorOutput; } } + namespace ValidateTemplate { + export type RequestBody = Components.Schemas.TemplateValidationRequest; + namespace Responses { + export type $200 = Components.Schemas.TemplateValidationResponse; + export type $403 = Components.Schemas.ErrorOutput; + export type $413 = Components.Schemas.ErrorOutput; + export type $415 = Components.Schemas.ErrorOutput; + } + } } @@ -565,6 +734,33 @@ export interface OperationMethods { data?: Paths.ConvertDocument.RequestBody, config?: AxiosRequestConfig ): OperationResponse + /** + * validateTemplate - validateTemplate + * + * Validates a document template's variable syntax and, optionally, proposes a hotfixed copy of it. + * + * The endpoint compiles the template with docxtemplater's core parser (the xlsx module for + * spreadsheets), so a delimiter or tag error reported here is one generation would fail on. + * Failures specific to the image or HTML modules are not covered. Every problem it can repair + * unambiguously — a missing brace, an extra brace, a smart quote or a non-breaking space that + * Word substituted inside a tag — is applied to a **copy** of the template, which is uploaded + * and returned as `fixed_document`. + * + * The original template is never modified. Accepting the fix is an explicit, separate step: + * the caller shows the user `issues` (each with its `before`/`after`), lets them download + * `fixed_document.preview_url`, and only then replaces the template. + * + * Supported input document types: + * - .docx, .docm, .dotx + * - .xlsx, .xlsm + * - .pptx + * + */ + 'validateTemplate'( + parameters?: Parameters | null, + data?: Paths.ValidateTemplate.RequestBody, + config?: AxiosRequestConfig + ): OperationResponse } export interface PathsDictionary { @@ -627,6 +823,35 @@ export interface PathsDictionary { config?: AxiosRequestConfig ): OperationResponse } + ['/v2/templates:validate']: { + /** + * validateTemplate - validateTemplate + * + * Validates a document template's variable syntax and, optionally, proposes a hotfixed copy of it. + * + * The endpoint compiles the template with docxtemplater's core parser (the xlsx module for + * spreadsheets), so a delimiter or tag error reported here is one generation would fail on. + * Failures specific to the image or HTML modules are not covered. Every problem it can repair + * unambiguously — a missing brace, an extra brace, a smart quote or a non-breaking space that + * Word substituted inside a tag — is applied to a **copy** of the template, which is uploaded + * and returned as `fixed_document`. + * + * The original template is never modified. Accepting the fix is an explicit, separate step: + * the caller shows the user `issues` (each with its `before`/`after`), lets them download + * `fixed_document.preview_url`, and only then replaces the template. + * + * Supported input document types: + * - .docx, .docm, .dotx + * - .xlsx, .xlsm + * - .pptx + * + */ + 'post'( + parameters?: Parameters | null, + data?: Paths.ValidateTemplate.RequestBody, + config?: AxiosRequestConfig + ): OperationResponse + } } export type Client = OpenAPIClient @@ -647,4 +872,7 @@ export type InternalErrorDetails = Components.Schemas.InternalErrorDetails; export type InvalidCustomVariableErrorDetail = Components.Schemas.InvalidCustomVariableErrorDetail; export type InvalidCustomVariableErrorDetails = Components.Schemas.InvalidCustomVariableErrorDetails; export type S3Reference = Components.Schemas.S3Reference; +export type TemplateIssue = Components.Schemas.TemplateIssue; export type TemplateSettings = Components.Schemas.TemplateSettings; +export type TemplateValidationRequest = Components.Schemas.TemplateValidationRequest; +export type TemplateValidationResponse = Components.Schemas.TemplateValidationResponse;