---
title: "updateIntegrationV2"
method: PUT
path: "/v2/integrations/{integrationId}"
tags: ["integrations"]
---

# updateIntegrationV2

`PUT /v2/integrations/{integrationId}`

Update an existing integration with embedded use cases.
The integration must already exist.
Use cases are updated declaratively:
- Use cases in the request with matching IDs are updated
- Use cases in the request without matching IDs are created
- Existing use cases not in the request are deleted

## Path parameters

- `integrationId` string, uuid, required

## Request body

- UpsertIntegrationWithUseCasesRequest — Request to create or update an integration with embedded use cases (upsert). This is a declarative operation - the request represents the desired state.
  - `name` string, required — Integration name
  - `description` string — Optional description of the integration
  - `access_token_ids` string[] — List of access token IDs associated with this integration
  - `app_ids` string[] — List of app IDs associated with this integration
  - `environment_config` EnvironmentFieldConfig[] — Configuration defining environment variables needed by this integration. Values are stored in the Environments API.
    - `key` string, required — Environment variable key, used to look up the value in the Environments API.
    - `label` string, required — Display label for the field in the UI
    - `type` 'String' | 'SecretString', required — Whether the value is a plain string or an encrypted secret
    - `description` string — Help text shown below the field
    - `required` boolean — Whether this field must be filled before the integration can be used
    - `order` integer — Sort order for display and drag-to-reorder
  - `settings` IntegrationSettings — Settings for the integration
    - `autoRefresh` AutoRefreshSettings — Auto-refresh settings for keeping integration data fresh
      - `enabled` boolean — Whether auto-refresh is enabled
      - `freshnessThresholdMinutes` integer — Maximum age (in minutes) of data before it is considered stale and eligible for refresh
    - `notifications` IntegrationNotificationConfig — 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.
      - `enabled` boolean, required — Master switch for this integration's notifications.
      - `recipients` NotificationRecipient[], required — 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.
        - `user_id` string, required — 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.
      - `defaultChannels` NotificationChannelSet, required — Delivery channel toggles. New channels added in svc-notification-api inherit here.
        - `email` boolean, required
        - `in_app` boolean, required
      - `monitoredUseCases` string[] — Integration-level use-case include-filter; absent/empty means all use cases.
      - `monitoredCodes` string[] — Integration-level code scope; absent/empty resolves to ['_error_']. Accepts concrete monitoring error codes or group sentinels (_error_, _warning_, _success_, _info_, _any_, _parent_).
      - `rules` NotificationRule[], required — Enabled triggers and their params. A type MAY repeat; capped at 20 rules (enforced at the write boundary).
        - `id` string — Stable AlertState + baseline key. Optional on write — the server mints a ULID when omitted; a supplied id is preserved verbatim.
        - `name` string — Optional human label disambiguating two rules of the same type.
        - `type` 'critical_error' | 'error_threshold' | 'warning_threshold' | 'success_rate_drop' | 'recovery' | 'silence', required — Rule trigger type. These are the only supported types; each is produced by a real alerter.
        - `enabled` boolean, required
        - `channels` NotificationChannelSet — Delivery channel toggles. New channels added in svc-notification-api inherit here.
          - `email` boolean, required
          - `in_app` boolean, required
        - `codes` string[] — Per-rule code scope. Event-matching rules default to ['_parent_']; silence defaults to ['_any_']. success_rate_drop and recovery take no codes.
        - `threshold` union — Count or percentage; 'auto' selects anomaly-baseline mode.
          - number
          - 'auto'
        - `sensitivity` 'low' | 'medium' | 'high' — Band width for 'auto' mode.
        - `fallbackThreshold` number — Static value used while the 'auto' baseline is immature (cold start).
        - `window` string — Evaluation window, e.g. '15m', '1h', '24h'.
        - `minSampleSize` integer — success_rate_drop minimum sample size guard.
        - `quietPeriod` string — silence quiet period, e.g. '12h'.
      - `digest` NotificationDigestConfig, required — Digest schedule and content configuration.
        - `enabled` boolean, required
        - `frequency` 'daily' | 'weekly', required
        - `dayOfWeek` 0 | 1 | 2 | 3 | 4 | 5 | 6 — Weekly only. 0 = Sunday … 6 = Saturday.
        - `timeOfDay` string, required — HH:mm
        - `timezone` string, required — IANA timezone, e.g. 'Europe/Berlin'.
        - `channels` NotificationChannelSet, required — Delivery channel toggles. New channels added in svc-notification-api inherit here.
          - `email` boolean, required
          - `in_app` boolean, required
        - `includeHealthy` boolean, required — List all integrations vs. only ones with issues.
        - `skipIfEmpty` boolean, required — Suppress the digest when nothing happened.
      - `muteUntil` string, date-time, nullable — ISO instant; snooze all non-digest alerts until this time. `null` means not muted.
  - `integration_type` 'erp' | 'connector' — Type of integration. "erp" is the ERP integration with inbound/outbound use cases. "connector" is for complex proxy integrations with external APIs.
  - `connector_config` ConnectorConfig — Shared configuration for connector-type integrations
    - `base_url` string — Base URL for the partner API
    - `auth` ManagedCallAuth — Authentication configuration for managed call requests
      - `type` 'oauth2_client_credentials' | 'api_key' | 'bearer' — Authentication type
      - `token_url` string — OAuth2 token URL. Can be plain text or {{env.key}} reference.
      - `client_id` string — OAuth2 client ID. Can be plain text or {{env.key}} reference.
      - `client_secret` string — OAuth2 client secret. Must be an {{env.key}} reference (secret).
      - `scope` string — OAuth2 scope
      - `audience` string — OAuth2 audience parameter (e.g. for Auth0, Azure AD). Can be plain text or {{env.key}} reference.
      - `resource` string — OAuth2 resource parameter (e.g. for Azure AD). Can be plain text or {{env.key}} reference.
      - `body_params` object — Additional key-value pairs for the OAuth2 token request body. Values can be {{env.key}} references.
      - `headers` object — Additional headers for the OAuth2 token request. Values can be {{env.key}} references.
      - `query_params` object — Additional query parameters for the OAuth2 token URL. Values can be {{env.key}} references.
      - `api_key_header` string — Header name for API key auth (default X-API-Key)
      - `api_key` string — API key value. Must be an {{env.key}} reference (secret).
      - `token` string — Bearer token value. Must be an {{env.key}} reference (secret).
    - `types_versions` object[] — History of generated type package versions
      - `version` string, required
      - `package_name` string, required
      - `generated_at` string, date-time, required
      - `generated_by` string, required
      - `status` 'active' | 'deprecated', required
    - `latest_types_version` string — Latest active types package version
    - `latest_types_package_name` string — Latest active types package name
  - `protected` boolean — If true, integration is displayed in read-only mode in the UI to discourage changes
  - `_manifest` string[] — The manifest IDs associated with this integration
  - `use_cases` EmbeddedUseCaseRequest[] — Full list of use cases (declarative). This replaces ALL existing use cases. - Use cases with an `id` field matching an existing use case will be updated - Use cases without an `id` or with a non-matching `id` will be created - Existing use cases not in this list will be deleted
    - union
      - EmbeddedInboundUseCaseRequest
        - `id` string, uuid — Optional use case ID for update matching. - If provided and matches an existing use case, that use case is updated - If provided but no match, a new use case with this ID is created - If omitted, a new use case with auto-generated ID is created
        - `name` string, required — Use case name
        - `slug` string — URL-safe identifier for the use case. Optional on this upsert/sync endpoint — when omitted, the server derives one from the name and ensures uniqueness within the integration. Immutable after creation.
        - `enabled` boolean, required — Whether the use case is enabled
        - `change_description` string — Optional description of this change (like a commit message)
        - `type` 'inbound', required — Use case type
        - `configuration` InboundIntegrationEventConfiguration — Configuration for inbound use cases (ERP to epilot)
          - `direct` boolean — Enables direct mode for this use case. When `true`, every event routed to this use case must carry a `DirectPayload` - pre-mapped entity and meter reading operations - instead of raw ERP data, and the mapping engine is skipped entirely. Everything else in the inbound pipeline (deduplication, ordering, create-vs-update resolution, relation resolution, monitoring) behaves exactly as in mapped mode. In direct mode, `entities` doubles as an optional allowlist: when non-empty, each operation's `entity_slug` must match an entry's `entity_schema`, and when that entry declares `unique_ids`, the operation's unique-id keys must be exactly that set (or just `_id`). Entries need only `entity_schema` and `unique_ids` - `fields` is not required and is ignored. `meter_readings` is ignored in direct mode. Flag changes take up to 5 minutes to propagate (configuration cache).
          - `entities` IntegrationEntity[] — Array of entity configurations for this event
            - `entity_schema` string, required — Target entity schema (e.g., 'contact', 'contract')
            - `unique_ids` string[], required — Array of attribute names that uniquely identify this entity. The _type hint for repeatable fields (e.g., email, phone) should be specified on the corresponding field definition in the fields array.
            - `jsonataExpression` string — Optional JSONata expression to pre-process the event data before field mapping
            - `enabled` union — Controls whether this entity mapping should be processed. Can be a boolean or a JSONata expression (string) that evaluates to a boolean.
              - …
            - `mode` 'upsert' | 'delete' | 'purge' | 'upsert-prune-scope-purge' | 'upsert-prune-scope-delete' — Operation mode for entity mapping: - 'upsert': Create or update the entity (default) - 'delete': Soft delete the entity (marks as deleted) - 'purge': Hard delete the entity (permanent removal) - 'upsert-prune-scope-purge': Upsert entities from array, then purge entities in scope that weren't upserted - 'upsert-prune-scope-delete': Upsert entities from array, then soft delete entities in scope that weren't upserted
            - `scope` PruneScopeConfig — Scope configuration for upsert-prune-scope modes. Defines how to find entities that should be pruned if not in the upsert payload. The scope is resolved against the original event payload (not individual array items).
              - …
            - `fields` IntegrationEntityField[] — Field mapping definitions. Required for mapped-mode entity configurations; may be omitted for allowlist entries of a `direct: true` use case, where no mapping runs.
              - …
          - `meter_readings` IntegrationMeterReading[] — Array of meter reading configurations for this event
            - `jsonataExpression` string — Optional JSONata expression to extract meter reading items from the event data. If not provided, the entire payload is used as the reading data. Useful when you need to extract an array of readings from a nested structure (e.g., "$.readings").
            - `reading_matching` 'external_id' | 'strict-date' — Strategy for matching incoming readings against existing readings. - 'external_id': Match readings by external_id attribute (default behavior) - 'strict-date': Match by meter_id + counter_id + direction + date (German timezone). Useful when readings originate from ECP and are echoed back by the ERP with truncated timestamps.
            - `mode` 'upsert' | 'delete' | 'upsert-prune-scope' — Operation mode for meter reading mapping: - 'upsert': Create or update meter readings (default) - 'delete': Delete the meter reading - 'upsert-prune-scope': Upsert readings from array, then delete all other readings for the same meter+counter that weren't upserted
            - `scope` MeterReadingPruneScopeConfig — Scope configuration for meter reading upsert-prune-scope mode. The scope is all readings for the same meter + counter.
              - …
            - `meter` MeterUniqueIdsConfig, required
              - …
            - `meter_counter` MeterUniqueIdsConfig
              - …
            - `fields` IntegrationEntityField[], required — Field mapping definitions for meter reading attributes
              - …
      - EmbeddedOutboundUseCaseRequest
        - `id` string, uuid — Optional use case ID for update matching. - If provided and matches an existing use case, that use case is updated - If provided but no match, a new use case with this ID is created - If omitted, a new use case with auto-generated ID is created
        - `name` string, required — Use case name
        - `slug` string — URL-safe identifier for the use case. Optional on this upsert/sync endpoint — when omitted, the server derives one from the name and ensures uniqueness within the integration. Immutable after creation.
        - `enabled` boolean, required — Whether the use case is enabled
        - `change_description` string — Optional description of this change (like a commit message)
        - `type` 'outbound', required — Use case type
        - `configuration` OutboundIntegrationEventConfiguration — Configuration for outbound use cases. Defines the event that triggers the flow and the webhook mappings.
          - `event_catalog_event` string, required — The Event Catalog event name that triggers this outbound flow
          - `event_filter` string — JSONata boolean predicate over the hydrated event payload. The use case handles the event only when this evaluates truthy; when absent it handles every event of its name, which is the behaviour of every pre-existing configuration. This is where event scoping belongs — narrowing to certain ticket purposes, contract types or channels. Evaluation input is the full hydrated event, so relation nodes such as `ticket` and `contact` are populated. An expression that throws is treated as no match and logged, so one malformed filter cannot block the other use cases subscribed to the same event.
          - `mappings` OutboundMapping[], required — List of mappings that transform and deliver the event
            - `id` string, uuid — Unique identifier for this mapping; generated by the API when omitted
            - `name` string, required — Human-readable name for this mapping
            - `jsonata_expression` string — 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.
            - `enabled` boolean, required — Whether this mapping is active
            - `delivery` union, required — 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)
              - …
            - `created_at` string, date-time — Timestamp when the mapping was created
            - `updated_at` string, date-time — Timestamp when the mapping was last updated
          - `ack_tracking` 'on' | 'off' — Whether this use case participates in the acknowledgement protocol. `on` (the default, and the behaviour of every pre-existing use case) records an ACK_PENDING on each event and expects the consumer to confirm receipt via `POST /v1/erp/tracking/acknowledgement`; unconfirmed events raise ACK_TIMEOUT after the timeout window. `off` opts the use case out entirely: no tracking row, no ACK_PENDING, no ACK_TIMEOUT. Set it for consumers that never acknowledge — otherwise every event produces a guaranteed timeout warning — and for deliveries that already keep their own durable per-item record, such as `file_proxy`. The tracking row is per-event, not per-use-case, so it is suppressed only when EVERY enabled use case matching the event has opted out.
      - EmbeddedFileProxyUseCaseRequest
        - `id` string, uuid — Optional use case ID for update matching. - If provided and matches an existing use case, that use case is updated - If provided but no match, a new use case with this ID is created - If omitted, a new use case with auto-generated ID is created
        - `name` string, required — Use case name
        - `slug` string — URL-safe identifier for the use case. Optional on this upsert/sync endpoint — when omitted, the server derives one from the name and ensures uniqueness within the integration. Immutable after creation.
        - `enabled` boolean, required — Whether the use case is enabled
        - `change_description` string — Optional description of this change (like a commit message)
        - `type` 'file_proxy', required — Use case type
        - `configuration` FileProxyUseCaseConfiguration — Configuration for file_proxy use cases. Defines how to authenticate and move files between epilot and an external document system, in either direction (see `direction`). **Download** (`direction: download`, the default) fetches a file from the external system and serves it to a browser. The download URL always requires `orgId`, `integrationId`, and either `useCaseSlug` (recommended) or `useCaseId` (legacy UUID) as query parameters. The `orgId` is included in the signed URL to establish organization context without requiring authentication. Additional use-case-specific parameters are declared in the `params` array. `response` is REQUIRED for download use cases. **Upload** (`direction: upload`) pushes epilot files to the external system. It is not reachable over the download endpoint; an outbound use case points at it via a `file_proxy` delivery, and this configuration owns everything about what gets sent: `fan_out` decides how many deliveries one event produces, and every step builds its own request body with `body_jsonata` — or leaves it empty to send the delivery's files unchanged. `upload` is REQUIRED and `response` MUST be omitted. Two expression languages, split by what they produce. **Handlebars composes strings**: `url` and `headers` on upload, plus a form-encoded, XML or plain-text `body` on download. **JSONata produces data**: `body_jsonata` and the per-step `enabled`. JSONata omits keys whose value is undefined, so an optional field needs no conditional guard — it is simply absent from the serialized body. Handlebars templates are rendered EXACTLY ONCE, against a single context holding `env`, `file_data`, `steps` and `auth_token`. Writing `{{ env.some_var }}` resolves it. The legacy `\{{ env.some_var }}` escape belongs to the two-pass renderer and is NOT rewritten here — it renders as the literal text `{{ env.some_var }}`, which is rejected (see below) rather than shipped. Two guards run on every rendered upload template, because single-pass rendering fails quietly by default. Both are terminal, and each names what to fix: - **residual `{{` after rendering** — a configuration still carrying the `\{{` escape. Rewrite it without the backslash. - **a referenced `env` key absent from the environment** — checked BEFORE the URL is parsed, because an empty value in host position turns `https://{{env.host}}/document/import` into `https:///document/import`, whose host then parses as `document`. Provision the environment variable. A key that exists and is legitimately empty is fine; only absence fails. OpenAPI 3.0 cannot express this conditional requiredness, so it is enforced by the server-side validator, which returns an explicit message naming the offending field.
          - `direction` 'download' | 'upload' — Direction of file travel. `download` (default) pulls a file from the external system into epilot; `upload` pushes an epilot file out to the external system. Omitted means `download`, so every pre-existing configuration keeps its exact meaning. Note this is the direction of the FILE, not the epilot use-case type — an `upload` file_proxy use case is still a `file_proxy` use case, never an `outbound` one.
          - `upload` FileProxyUploadConfig — Upload-side settings for a file_proxy use case with `direction: upload`. The surrounding file_proxy configuration owns WHAT and HOW to send: `fan_out`, auth, and the steps with their `body_jsonata`. This nested object governs upload-specific limits and how the final external response is judged. The outbound mapping remains a pure pointer to the recipe (see `FileProxyDeliveryConfig`).
            - `max_total_bytes` integer — Ceiling for ALL of a delivery's files together, in bytes. Capped by the platform limit, which it may lower but never raise. Only reachable with `fan_out.enabled: false`, where one delivery carries every attachment and the worker holds them all resident at once. base64 inflates each by about a third, so the combined figure is what matters rather than any single file's size.
            - `max_file_bytes` integer — Per-file ceiling for this use case, in bytes. Files above it fail terminally with `FILE_TOO_LARGE` before any bytes are fetched. Defaults to — and is clamped by — the platform ceiling of 100 MiB (104857600).
            - `max_delivery_attempts` integer — Maximum delivery attempts per item before the delivery is marked failed. Attempts are spaced by an exponential, jittered backoff. The default 8 attempts schedule at most 7 delays, totaling about 7 hours 40 minutes before jitter, so a normal ERP maintenance window does not immediately exhaust them. Lives here rather than on the outbound delivery because how hard to retry is a property of the transport — the same judgement as `max_file_bytes` — and the outbound delivery is a pure pointer.
          - `fan_out` FileProxyFanOutConfig — Whether one event produces one delivery per file, or a single delivery carrying all of them. The split is always over the event's `event_attachments` — there is no expression to write, because an upload only ever runs on events that declare that field. Each resulting delivery is fully independent: its own idempotency record, its own retry schedule, its own monitoring events. A four-file event can therefore end up three-of-four delivered, which is the honest state to report. The split is evaluated ONCE, when the event is enqueued, so item indices — and therefore idempotency keys — stay stable across retries. Either way, expressions read the same `$file_data` binding, so no expression needs to know which mode it runs in. An event carrying no attachments is recorded as `skipped`.
            - `enabled` boolean, required — When true, one delivery per attachment, and `$file_data` holds exactly that one file. When false (or absent), the event produces exactly one delivery and `$file_data` holds every attachment on the event. An empty `event_attachments` produces no deliveries and one info-level `FAN_OUT_EMPTY`, which is the normal outcome for a catch-all subscription seeing an event with nothing to send.
          - `secure_proxy` FileProxySecureProxyAttachment
            - `use_case_slug` string, required — Slug of the secure_proxy use case in the same integration.
          - `auth` FileProxyAuth
            - `type` 'oauth2_client_credentials' | 'oauth2_password', required — Authentication type
            - `token_url` string, required — Handlebars template for the OAuth2 token endpoint URL
            - `client_id` string, required — Handlebars template for the OAuth2 client ID
            - `client_secret` string, required — Handlebars template for the OAuth2 client secret
            - `scope` string — Optional OAuth2 scope
            - `audience` string — Handlebars template for the OAuth2 audience parameter
            - `resource` string — Handlebars template for the OAuth2 resource parameter
            - `username` string — Handlebars template for the OAuth2 resource owner username. Required when type is oauth2_password.
            - `password` string — Handlebars template for the OAuth2 resource owner password. Required when type is oauth2_password.
            - `body_params` object — Additional key-value pairs to include in the token request body. Values support Handlebars templates.
            - `headers` object — Additional headers to include in the token request. Values support Handlebars templates.
            - `query_params` object — Additional query parameters to append to the token URL. Values support Handlebars templates.
          - `params` FileProxyParam[] — Download-only. Additional use-case-specific parameters expected in the download URL query string (beyond the required orgId, integrationId, and useCaseSlug or useCaseId). Rejected when `direction` is `upload`.
            - `name` string, required — Parameter name as it appears in the query string
            - `required` boolean, required — Whether this parameter is required
            - `description` string — Human-readable description of the parameter
          - `allowed_origins` string[] — Download-only. Additional origins permitted to call /download for this use case (CORS, exact match). Portal origins are always allowed. Rejected when `direction` is `upload`.
          - `steps` FileProxyStep[], required — Ordered list of HTTP steps to execute. For `download` these retrieve the file; for `upload` they deliver it, each assembling its own request body — `body_jsonata` for JSON, `body` for anything else.
            - `url` string, required — Handlebars template for the request URL
            - `method` 'GET' | 'POST' | 'PUT' | 'PATCH', required — HTTP method
            - `headers` object — Handlebars templates for request headers
            - `body_jsonata` string — Upload-only. JSONata producing the request body as DATA; the result is serialized to JSON and sent. Use this for every JSON body — it cannot emit malformed JSON, and it omits a key whose value is undefined instead of sending it empty, which is what makes optional fields work without a conditional guard. The evaluation root is the hydrated event, so `contact.customer_pin` and `ticket._purpose` are reachable directly, unprefixed. **Everything else is a `$`-prefixed binding**: `$file_data` (the files this delivery carries — see `FileProxyFanOutConfig` for what it holds in each mode), `$ack_id`, `$env`, `$steps` (results of the steps already executed, each `{statusCode, headers, body}`), `$germanDate(iso)` and `$now()`. Writing `file_data[0].filename` instead of `$file_data[0].filename` yields nothing — it reads a field named `file_data` on the event, which does not exist. Leave this empty to send the delivery's files exactly as they are: the single attachment object when fanning out, the whole array when not. No mapping is needed for the common case. Must evaluate to an object or an array. Anything else fails the delivery terminally with `MAPPING_EXPRESSION_FAILED`, naming the step. **Only `undefined` omits a key.** `null`, `""`, `false` and `0` are values and are all sent. Two traps follow from that, and one idiom avoids both: - `x ? $string(x)` omits the key when `x` is `0` or `""`, because JSONata reads both as false. A meter number of `"0"` would silently vanish. - `: undefined` is not a literal — JSONata has no `undefined` keyword, so it is a path lookup that happens to find nothing. Against an event that really does carry a field named `undefined`, it returns that field's value. Write the **two-arm ternary with no else branch**, which omits the key when the test is false: `{ "pin": $exists(contact.pin) ? $string(contact.pin) }`. Every `$` binding is checked at save time against the known set, so a mistyped binding (`$lookup.documentType` for `$file_data[0].filename`) is rejected rather than silently producing a missing key. Not accepted together with `body`, which the upload direction rejects outright.
            - `enabled` string — Upload-only, OPTIONAL. JSONata returning a boolean, deciding whether this step runs at all. Absent means it runs. A false result is a BREAK: this step is skipped and so is every step after it, and the delivery is recorded as `skipped` rather than delivered or failed. It is acknowledged and never retried, and a `STEP_DISABLED` monitoring event is emitted at level `info` — a disabled step is the configuration working, not a fault. Reads the same bindings a body does, `$steps` included, so it can branch on what an earlier step returned. This is how a delivery is filtered out: with one delivery per attachment, a false result on the first step drops that file. An expression that throws, or returns a non-boolean, is a terminal `MAPPING_EXPRESSION_FAILED` instead — a broken predicate must not read as a deliberate skip.
            - `body` string — Handlebars template for the request body (write methods only), for bodies that are NOT JSON — form-encoded, XML, plain text. For a JSON body use `body_jsonata` instead; it cannot produce malformed JSON. **Download-only.** The upload direction rejects it: use `body_jsonata`, or leave that empty to send the files unchanged. Rendered once, against the context described on `FileProxyUseCaseConfiguration`: `{{ params.* }}`, `{{ env.* }}`, `{{ steps.N.body }}`, `{{ auth_token }}`. If a JSON body is written here anyway, **route every user-controlled value through the `json` helper**: the template engine does not escape, so `"name":"{{ params.documentName }}"` produces invalid JSON the moment a value contains a quote. Write `"name": {{json params.documentName}}` instead — the helper emits the surrounding quotes itself and renders absent values as `null`. Mutually exclusive with `body_jsonata`, which is upload-only.
            - `response_type` 'json' | 'binary', required — Expected response type
          - `response` FileProxyResponseConfig — How to extract the file from the step results. REQUIRED when `direction` is `download`; rejected when `direction` is `upload` (an upload has no file to extract).
            - `body` string, required — JSONata expression to extract file content from step results
            - `encoding` 'base64' | 'binary', required — Encoding of the extracted body
            - `filename` string — JSONata expression to extract the filename
            - `content_type` string — JSONata expression to extract the content type
          - `prevent_indirect_serving` boolean — Download-only; rejected when `direction` is `upload`. When `true`, this use case is served via the streaming endpoint: mapped file URLs are built as `/stream/download`, files of any size are streamed inline over HTTP response streaming, and buffered `/download` requests for oversize files are 307-redirected to `/stream`. Files never transit epilot's temporary S3 storage on the streaming path. Defaults to `false` (small files are served directly and large files are transparently served via a temporary S3 redirect).
      - EmbeddedManagedCallUseCaseRequest
        - `id` string, uuid — Optional use case ID for update matching. - If provided and matches an existing use case, that use case is updated - If provided but no match, a new use case with this ID is created - If omitted, a new use case with auto-generated ID is created
        - `name` string, required — Use case name
        - `slug` string — URL-safe identifier for the use case. Optional on this upsert/sync endpoint — when omitted, the server derives one from the name and ensures uniqueness within the integration. Immutable after creation.
        - `enabled` boolean, required — Whether the use case is enabled
        - `change_description` string — Optional description of this change (like a commit message)
        - `type` 'managed_call', required — Use case type for managed API calls
        - `configuration` ManagedCallOperationConfig — Configuration for managed_call use cases. Defines a single API operation with JSONata mapping.
          - `operation` ManagedCallOperation, required — HTTP operation configuration for managed calls
            - `method` 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE', required
            - `path` string, required — URL path template with {{variable}} interpolation
            - `headers` object — Custom HTTP headers for the request. Values support {{variable}} interpolation from the request payload and {{env.VAR}} references for environment variables.
            - `query_params` object — Query parameters for the request. Values support {{variable}} interpolation from the request payload.
          - `request_mapping` string — JSONata expression for outbound body transformation
          - `response_mapping` string — JSONata expression for inbound response transformation
          - `inbound_use_case_slug` string — Slug of the inbound use case to route responses to for async entity processing. When set, the managed call response is queued to the inbound pipeline and processed using the referenced inbound use case's mapping configuration.
      - EmbeddedSecureProxyUseCaseRequest
        - `id` string, uuid — Optional use case ID for update matching. - If provided and matches an existing use case, that use case is updated - If provided but no match, a new use case with this ID is created - If omitted, a new use case with auto-generated ID is created
        - `name` string, required — Use case name
        - `slug` string — URL-safe identifier for the use case. Optional on this upsert/sync endpoint — when omitted, the server derives one from the name and ensures uniqueness within the integration. Immutable after creation.
        - `enabled` boolean, required — Whether the use case is enabled
        - `change_description` string — Optional description of this change (like a commit message)
        - `type` 'secure_proxy', required — Use case type
        - `configuration` SecureProxyUseCaseConfiguration — Configuration for secure_proxy use cases. Defines how to route requests through a secure VPC.
          - `vpc_mode` 'static_ip' | 'secure_link', required — VPC routing mode. Read-only after creation. - static_ip: Routes through a VPC with static outbound IP (NAT Gateway) for IP-allowlisted external APIs. - secure_link: Routes through a VPN VPC for accessing private customer networks.
          - `allowed_domains` string[] — Domain whitelist for secure_link mode. Admin-only — can only be modified directly in DynamoDB via admin script. Supports exact match (e.g., "api.wemag.com") and wildcard prefix (e.g., "*.wemag.com").
          - `allowed_ips` string[] — IP allowlist (CIDR notation) for secure_link mode. Admin-only — can only be modified directly in DynamoDB via admin script. Required for secure_link mode. All DNS-resolved IPs must match at least one range. Example: ["10.0.1.0/24", "192.168.1.0/24"]

## Response `200`

Integration updated successfully with use cases

- IntegrationWithUseCases — Integration with embedded use cases for atomic CRUD operations
  - `id` string, uuid, required — Unique identifier for the integration
  - `orgId` string, required — Organization ID
  - `created_at` string, date-time, required — ISO-8601 timestamp when the integration was created
  - `updated_at` string, date-time, required — ISO-8601 timestamp when the integration was last updated
  - `name` string, required — Integration name
  - `description` string — Optional description of the integration
  - `access_token_ids` string[] — List of access token IDs associated with this integration
  - `app_ids` string[] — List of app IDs associated with this integration
  - `environment_config` EnvironmentFieldConfig[] — Configuration defining environment variables needed by this integration. Values are stored in the Environments API.
    - `key` string, required — Environment variable key, used to look up the value in the Environments API.
    - `label` string, required — Display label for the field in the UI
    - `type` 'String' | 'SecretString', required — Whether the value is a plain string or an encrypted secret
    - `description` string — Help text shown below the field
    - `required` boolean — Whether this field must be filled before the integration can be used
    - `order` integer — Sort order for display and drag-to-reorder
  - `settings` IntegrationSettings — Settings for the integration
    - `autoRefresh` AutoRefreshSettings — Auto-refresh settings for keeping integration data fresh
      - `enabled` boolean — Whether auto-refresh is enabled
      - `freshnessThresholdMinutes` integer — Maximum age (in minutes) of data before it is considered stale and eligible for refresh
    - `notifications` IntegrationNotificationConfig — 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.
      - `enabled` boolean, required — Master switch for this integration's notifications.
      - `recipients` NotificationRecipient[], required — 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.
        - `user_id` string, required — 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.
      - `defaultChannels` NotificationChannelSet, required — Delivery channel toggles. New channels added in svc-notification-api inherit here.
        - `email` boolean, required
        - `in_app` boolean, required
      - `monitoredUseCases` string[] — Integration-level use-case include-filter; absent/empty means all use cases.
      - `monitoredCodes` string[] — Integration-level code scope; absent/empty resolves to ['_error_']. Accepts concrete monitoring error codes or group sentinels (_error_, _warning_, _success_, _info_, _any_, _parent_).
      - `rules` NotificationRule[], required — Enabled triggers and their params. A type MAY repeat; capped at 20 rules (enforced at the write boundary).
        - `id` string — Stable AlertState + baseline key. Optional on write — the server mints a ULID when omitted; a supplied id is preserved verbatim.
        - `name` string — Optional human label disambiguating two rules of the same type.
        - `type` 'critical_error' | 'error_threshold' | 'warning_threshold' | 'success_rate_drop' | 'recovery' | 'silence', required — Rule trigger type. These are the only supported types; each is produced by a real alerter.
        - `enabled` boolean, required
        - `channels` NotificationChannelSet — Delivery channel toggles. New channels added in svc-notification-api inherit here.
          - `email` boolean, required
          - `in_app` boolean, required
        - `codes` string[] — Per-rule code scope. Event-matching rules default to ['_parent_']; silence defaults to ['_any_']. success_rate_drop and recovery take no codes.
        - `threshold` union — Count or percentage; 'auto' selects anomaly-baseline mode.
          - number
          - 'auto'
        - `sensitivity` 'low' | 'medium' | 'high' — Band width for 'auto' mode.
        - `fallbackThreshold` number — Static value used while the 'auto' baseline is immature (cold start).
        - `window` string — Evaluation window, e.g. '15m', '1h', '24h'.
        - `minSampleSize` integer — success_rate_drop minimum sample size guard.
        - `quietPeriod` string — silence quiet period, e.g. '12h'.
      - `digest` NotificationDigestConfig, required — Digest schedule and content configuration.
        - `enabled` boolean, required
        - `frequency` 'daily' | 'weekly', required
        - `dayOfWeek` 0 | 1 | 2 | 3 | 4 | 5 | 6 — Weekly only. 0 = Sunday … 6 = Saturday.
        - `timeOfDay` string, required — HH:mm
        - `timezone` string, required — IANA timezone, e.g. 'Europe/Berlin'.
        - `channels` NotificationChannelSet, required — Delivery channel toggles. New channels added in svc-notification-api inherit here.
          - `email` boolean, required
          - `in_app` boolean, required
        - `includeHealthy` boolean, required — List all integrations vs. only ones with issues.
        - `skipIfEmpty` boolean, required — Suppress the digest when nothing happened.
      - `muteUntil` string, date-time, nullable — ISO instant; snooze all non-digest alerts until this time. `null` means not muted.
  - `integration_type` 'erp' | 'connector' — Type of integration. "erp" is the ERP integration with inbound/outbound use cases. "connector" is for complex proxy integrations with external APIs.
  - `connector_config` ConnectorConfig — Shared configuration for connector-type integrations
    - `base_url` string — Base URL for the partner API
    - `auth` ManagedCallAuth — Authentication configuration for managed call requests
      - `type` 'oauth2_client_credentials' | 'api_key' | 'bearer' — Authentication type
      - `token_url` string — OAuth2 token URL. Can be plain text or {{env.key}} reference.
      - `client_id` string — OAuth2 client ID. Can be plain text or {{env.key}} reference.
      - `client_secret` string — OAuth2 client secret. Must be an {{env.key}} reference (secret).
      - `scope` string — OAuth2 scope
      - `audience` string — OAuth2 audience parameter (e.g. for Auth0, Azure AD). Can be plain text or {{env.key}} reference.
      - `resource` string — OAuth2 resource parameter (e.g. for Azure AD). Can be plain text or {{env.key}} reference.
      - `body_params` object — Additional key-value pairs for the OAuth2 token request body. Values can be {{env.key}} references.
      - `headers` object — Additional headers for the OAuth2 token request. Values can be {{env.key}} references.
      - `query_params` object — Additional query parameters for the OAuth2 token URL. Values can be {{env.key}} references.
      - `api_key_header` string — Header name for API key auth (default X-API-Key)
      - `api_key` string — API key value. Must be an {{env.key}} reference (secret).
      - `token` string — Bearer token value. Must be an {{env.key}} reference (secret).
    - `types_versions` object[] — History of generated type package versions
      - `version` string, required
      - `package_name` string, required
      - `generated_at` string, date-time, required
      - `generated_by` string, required
      - `status` 'active' | 'deprecated', required
    - `latest_types_version` string — Latest active types package version
    - `latest_types_package_name` string — Latest active types package name
  - `protected` boolean — If true, integration is displayed in read-only mode in the UI to discourage changes
  - `_manifest` string[] — The manifest IDs associated with this integration
  - `use_cases` UseCase[], required — All use cases belonging to this integration
    - union
      - InboundUseCase
        - `id` string, uuid, required — Unique identifier for the use case
        - `integrationId` string, uuid, required — Parent integration ID
        - `name` string, required — Use case name
        - `slug` string — URL-safe identifier for the use case. Recommended for portable cross-environment referencing. Unique per integration. Immutable after creation. Lowercase alphanumeric, hyphens, and underscores only.
        - `type` 'inbound', required — Use case type
        - `enabled` boolean, required
        - `change_description` string — Reason given for the update that produced the current version. Absent when the update supplied none — it is not inherited from the previous version.
        - `changed_by` string — User ID recorded for the update that produced the current version. Absent when the update had no user context, e.g. an automated sync.
        - `created_at` string, date-time, required — ISO-8601 timestamp when the use case was created
        - `updated_at` string, date-time, required — ISO-8601 timestamp when the use case was last updated
        - `configuration` InboundIntegrationEventConfiguration — Configuration for inbound use cases (ERP to epilot)
          - `direct` boolean — Enables direct mode for this use case. When `true`, every event routed to this use case must carry a `DirectPayload` - pre-mapped entity and meter reading operations - instead of raw ERP data, and the mapping engine is skipped entirely. Everything else in the inbound pipeline (deduplication, ordering, create-vs-update resolution, relation resolution, monitoring) behaves exactly as in mapped mode. In direct mode, `entities` doubles as an optional allowlist: when non-empty, each operation's `entity_slug` must match an entry's `entity_schema`, and when that entry declares `unique_ids`, the operation's unique-id keys must be exactly that set (or just `_id`). Entries need only `entity_schema` and `unique_ids` - `fields` is not required and is ignored. `meter_readings` is ignored in direct mode. Flag changes take up to 5 minutes to propagate (configuration cache).
          - `entities` IntegrationEntity[] — Array of entity configurations for this event
            - `entity_schema` string, required — Target entity schema (e.g., 'contact', 'contract')
            - `unique_ids` string[], required — Array of attribute names that uniquely identify this entity. The _type hint for repeatable fields (e.g., email, phone) should be specified on the corresponding field definition in the fields array.
            - `jsonataExpression` string — Optional JSONata expression to pre-process the event data before field mapping
            - `enabled` union — Controls whether this entity mapping should be processed. Can be a boolean or a JSONata expression (string) that evaluates to a boolean.
              - …
            - `mode` 'upsert' | 'delete' | 'purge' | 'upsert-prune-scope-purge' | 'upsert-prune-scope-delete' — Operation mode for entity mapping: - 'upsert': Create or update the entity (default) - 'delete': Soft delete the entity (marks as deleted) - 'purge': Hard delete the entity (permanent removal) - 'upsert-prune-scope-purge': Upsert entities from array, then purge entities in scope that weren't upserted - 'upsert-prune-scope-delete': Upsert entities from array, then soft delete entities in scope that weren't upserted
            - `scope` PruneScopeConfig — Scope configuration for upsert-prune-scope modes. Defines how to find entities that should be pruned if not in the upsert payload. The scope is resolved against the original event payload (not individual array items).
              - …
            - `fields` IntegrationEntityField[] — Field mapping definitions. Required for mapped-mode entity configurations; may be omitted for allowlist entries of a `direct: true` use case, where no mapping runs.
              - …
          - `meter_readings` IntegrationMeterReading[] — Array of meter reading configurations for this event
            - `jsonataExpression` string — Optional JSONata expression to extract meter reading items from the event data. If not provided, the entire payload is used as the reading data. Useful when you need to extract an array of readings from a nested structure (e.g., "$.readings").
            - `reading_matching` 'external_id' | 'strict-date' — Strategy for matching incoming readings against existing readings. - 'external_id': Match readings by external_id attribute (default behavior) - 'strict-date': Match by meter_id + counter_id + direction + date (German timezone). Useful when readings originate from ECP and are echoed back by the ERP with truncated timestamps.
            - `mode` 'upsert' | 'delete' | 'upsert-prune-scope' — Operation mode for meter reading mapping: - 'upsert': Create or update meter readings (default) - 'delete': Delete the meter reading - 'upsert-prune-scope': Upsert readings from array, then delete all other readings for the same meter+counter that weren't upserted
            - `scope` MeterReadingPruneScopeConfig — Scope configuration for meter reading upsert-prune-scope mode. The scope is all readings for the same meter + counter.
              - …
            - `meter` MeterUniqueIdsConfig, required
              - …
            - `meter_counter` MeterUniqueIdsConfig
              - …
            - `fields` IntegrationEntityField[], required — Field mapping definitions for meter reading attributes
              - …
      - OutboundUseCase
        - `id` string, uuid, required — Unique identifier for the use case
        - `integrationId` string, uuid, required — Parent integration ID
        - `name` string, required — Use case name
        - `slug` string — URL-safe identifier for the use case. Recommended for portable cross-environment referencing. Unique per integration. Immutable after creation. Lowercase alphanumeric, hyphens, and underscores only.
        - `type` 'outbound', required — Use case type
        - `enabled` boolean, required
        - `change_description` string — Reason given for the update that produced the current version. Absent when the update supplied none — it is not inherited from the previous version.
        - `changed_by` string — User ID recorded for the update that produced the current version. Absent when the update had no user context, e.g. an automated sync.
        - `created_at` string, date-time, required — ISO-8601 timestamp when the use case was created
        - `updated_at` string, date-time, required — ISO-8601 timestamp when the use case was last updated
        - `configuration` OutboundIntegrationEventConfiguration — Configuration for outbound use cases. Defines the event that triggers the flow and the webhook mappings.
          - `event_catalog_event` string, required — The Event Catalog event name that triggers this outbound flow
          - `event_filter` string — JSONata boolean predicate over the hydrated event payload. The use case handles the event only when this evaluates truthy; when absent it handles every event of its name, which is the behaviour of every pre-existing configuration. This is where event scoping belongs — narrowing to certain ticket purposes, contract types or channels. Evaluation input is the full hydrated event, so relation nodes such as `ticket` and `contact` are populated. An expression that throws is treated as no match and logged, so one malformed filter cannot block the other use cases subscribed to the same event.
          - `mappings` OutboundMapping[], required — List of mappings that transform and deliver the event
            - `id` string, uuid — Unique identifier for this mapping; generated by the API when omitted
            - `name` string, required — Human-readable name for this mapping
            - `jsonata_expression` string — 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.
            - `enabled` boolean, required — Whether this mapping is active
            - `delivery` union, required — 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)
              - …
            - `created_at` string, date-time — Timestamp when the mapping was created
            - `updated_at` string, date-time — Timestamp when the mapping was last updated
          - `ack_tracking` 'on' | 'off' — Whether this use case participates in the acknowledgement protocol. `on` (the default, and the behaviour of every pre-existing use case) records an ACK_PENDING on each event and expects the consumer to confirm receipt via `POST /v1/erp/tracking/acknowledgement`; unconfirmed events raise ACK_TIMEOUT after the timeout window. `off` opts the use case out entirely: no tracking row, no ACK_PENDING, no ACK_TIMEOUT. Set it for consumers that never acknowledge — otherwise every event produces a guaranteed timeout warning — and for deliveries that already keep their own durable per-item record, such as `file_proxy`. The tracking row is per-event, not per-use-case, so it is suppressed only when EVERY enabled use case matching the event has opted out.
      - FileProxyUseCase
        - `id` string, uuid, required — Unique identifier for the use case
        - `integrationId` string, uuid, required — Parent integration ID
        - `name` string, required — Use case name
        - `slug` string — URL-safe identifier for the use case. Recommended for portable cross-environment referencing. Unique per integration. Immutable after creation. Lowercase alphanumeric, hyphens, and underscores only.
        - `type` 'file_proxy', required — Use case type
        - `enabled` boolean, required
        - `change_description` string — Reason given for the update that produced the current version. Absent when the update supplied none — it is not inherited from the previous version.
        - `changed_by` string — User ID recorded for the update that produced the current version. Absent when the update had no user context, e.g. an automated sync.
        - `created_at` string, date-time, required — ISO-8601 timestamp when the use case was created
        - `updated_at` string, date-time, required — ISO-8601 timestamp when the use case was last updated
        - `configuration` FileProxyUseCaseConfiguration — Configuration for file_proxy use cases. Defines how to authenticate and move files between epilot and an external document system, in either direction (see `direction`). **Download** (`direction: download`, the default) fetches a file from the external system and serves it to a browser. The download URL always requires `orgId`, `integrationId`, and either `useCaseSlug` (recommended) or `useCaseId` (legacy UUID) as query parameters. The `orgId` is included in the signed URL to establish organization context without requiring authentication. Additional use-case-specific parameters are declared in the `params` array. `response` is REQUIRED for download use cases. **Upload** (`direction: upload`) pushes epilot files to the external system. It is not reachable over the download endpoint; an outbound use case points at it via a `file_proxy` delivery, and this configuration owns everything about what gets sent: `fan_out` decides how many deliveries one event produces, and every step builds its own request body with `body_jsonata` — or leaves it empty to send the delivery's files unchanged. `upload` is REQUIRED and `response` MUST be omitted. Two expression languages, split by what they produce. **Handlebars composes strings**: `url` and `headers` on upload, plus a form-encoded, XML or plain-text `body` on download. **JSONata produces data**: `body_jsonata` and the per-step `enabled`. JSONata omits keys whose value is undefined, so an optional field needs no conditional guard — it is simply absent from the serialized body. Handlebars templates are rendered EXACTLY ONCE, against a single context holding `env`, `file_data`, `steps` and `auth_token`. Writing `{{ env.some_var }}` resolves it. The legacy `\{{ env.some_var }}` escape belongs to the two-pass renderer and is NOT rewritten here — it renders as the literal text `{{ env.some_var }}`, which is rejected (see below) rather than shipped. Two guards run on every rendered upload template, because single-pass rendering fails quietly by default. Both are terminal, and each names what to fix: - **residual `{{` after rendering** — a configuration still carrying the `\{{` escape. Rewrite it without the backslash. - **a referenced `env` key absent from the environment** — checked BEFORE the URL is parsed, because an empty value in host position turns `https://{{env.host}}/document/import` into `https:///document/import`, whose host then parses as `document`. Provision the environment variable. A key that exists and is legitimately empty is fine; only absence fails. OpenAPI 3.0 cannot express this conditional requiredness, so it is enforced by the server-side validator, which returns an explicit message naming the offending field.
          - `direction` 'download' | 'upload' — Direction of file travel. `download` (default) pulls a file from the external system into epilot; `upload` pushes an epilot file out to the external system. Omitted means `download`, so every pre-existing configuration keeps its exact meaning. Note this is the direction of the FILE, not the epilot use-case type — an `upload` file_proxy use case is still a `file_proxy` use case, never an `outbound` one.
          - `upload` FileProxyUploadConfig — Upload-side settings for a file_proxy use case with `direction: upload`. The surrounding file_proxy configuration owns WHAT and HOW to send: `fan_out`, auth, and the steps with their `body_jsonata`. This nested object governs upload-specific limits and how the final external response is judged. The outbound mapping remains a pure pointer to the recipe (see `FileProxyDeliveryConfig`).
            - `max_total_bytes` integer — Ceiling for ALL of a delivery's files together, in bytes. Capped by the platform limit, which it may lower but never raise. Only reachable with `fan_out.enabled: false`, where one delivery carries every attachment and the worker holds them all resident at once. base64 inflates each by about a third, so the combined figure is what matters rather than any single file's size.
            - `max_file_bytes` integer — Per-file ceiling for this use case, in bytes. Files above it fail terminally with `FILE_TOO_LARGE` before any bytes are fetched. Defaults to — and is clamped by — the platform ceiling of 100 MiB (104857600).
            - `max_delivery_attempts` integer — Maximum delivery attempts per item before the delivery is marked failed. Attempts are spaced by an exponential, jittered backoff. The default 8 attempts schedule at most 7 delays, totaling about 7 hours 40 minutes before jitter, so a normal ERP maintenance window does not immediately exhaust them. Lives here rather than on the outbound delivery because how hard to retry is a property of the transport — the same judgement as `max_file_bytes` — and the outbound delivery is a pure pointer.
          - `fan_out` FileProxyFanOutConfig — Whether one event produces one delivery per file, or a single delivery carrying all of them. The split is always over the event's `event_attachments` — there is no expression to write, because an upload only ever runs on events that declare that field. Each resulting delivery is fully independent: its own idempotency record, its own retry schedule, its own monitoring events. A four-file event can therefore end up three-of-four delivered, which is the honest state to report. The split is evaluated ONCE, when the event is enqueued, so item indices — and therefore idempotency keys — stay stable across retries. Either way, expressions read the same `$file_data` binding, so no expression needs to know which mode it runs in. An event carrying no attachments is recorded as `skipped`.
            - `enabled` boolean, required — When true, one delivery per attachment, and `$file_data` holds exactly that one file. When false (or absent), the event produces exactly one delivery and `$file_data` holds every attachment on the event. An empty `event_attachments` produces no deliveries and one info-level `FAN_OUT_EMPTY`, which is the normal outcome for a catch-all subscription seeing an event with nothing to send.
          - `secure_proxy` FileProxySecureProxyAttachment
            - `use_case_slug` string, required — Slug of the secure_proxy use case in the same integration.
          - `auth` FileProxyAuth
            - `type` 'oauth2_client_credentials' | 'oauth2_password', required — Authentication type
            - `token_url` string, required — Handlebars template for the OAuth2 token endpoint URL
            - `client_id` string, required — Handlebars template for the OAuth2 client ID
            - `client_secret` string, required — Handlebars template for the OAuth2 client secret
            - `scope` string — Optional OAuth2 scope
            - `audience` string — Handlebars template for the OAuth2 audience parameter
            - `resource` string — Handlebars template for the OAuth2 resource parameter
            - `username` string — Handlebars template for the OAuth2 resource owner username. Required when type is oauth2_password.
            - `password` string — Handlebars template for the OAuth2 resource owner password. Required when type is oauth2_password.
            - `body_params` object — Additional key-value pairs to include in the token request body. Values support Handlebars templates.
            - `headers` object — Additional headers to include in the token request. Values support Handlebars templates.
            - `query_params` object — Additional query parameters to append to the token URL. Values support Handlebars templates.
          - `params` FileProxyParam[] — Download-only. Additional use-case-specific parameters expected in the download URL query string (beyond the required orgId, integrationId, and useCaseSlug or useCaseId). Rejected when `direction` is `upload`.
            - `name` string, required — Parameter name as it appears in the query string
            - `required` boolean, required — Whether this parameter is required
            - `description` string — Human-readable description of the parameter
          - `allowed_origins` string[] — Download-only. Additional origins permitted to call /download for this use case (CORS, exact match). Portal origins are always allowed. Rejected when `direction` is `upload`.
          - `steps` FileProxyStep[], required — Ordered list of HTTP steps to execute. For `download` these retrieve the file; for `upload` they deliver it, each assembling its own request body — `body_jsonata` for JSON, `body` for anything else.
            - `url` string, required — Handlebars template for the request URL
            - `method` 'GET' | 'POST' | 'PUT' | 'PATCH', required — HTTP method
            - `headers` object — Handlebars templates for request headers
            - `body_jsonata` string — Upload-only. JSONata producing the request body as DATA; the result is serialized to JSON and sent. Use this for every JSON body — it cannot emit malformed JSON, and it omits a key whose value is undefined instead of sending it empty, which is what makes optional fields work without a conditional guard. The evaluation root is the hydrated event, so `contact.customer_pin` and `ticket._purpose` are reachable directly, unprefixed. **Everything else is a `$`-prefixed binding**: `$file_data` (the files this delivery carries — see `FileProxyFanOutConfig` for what it holds in each mode), `$ack_id`, `$env`, `$steps` (results of the steps already executed, each `{statusCode, headers, body}`), `$germanDate(iso)` and `$now()`. Writing `file_data[0].filename` instead of `$file_data[0].filename` yields nothing — it reads a field named `file_data` on the event, which does not exist. Leave this empty to send the delivery's files exactly as they are: the single attachment object when fanning out, the whole array when not. No mapping is needed for the common case. Must evaluate to an object or an array. Anything else fails the delivery terminally with `MAPPING_EXPRESSION_FAILED`, naming the step. **Only `undefined` omits a key.** `null`, `""`, `false` and `0` are values and are all sent. Two traps follow from that, and one idiom avoids both: - `x ? $string(x)` omits the key when `x` is `0` or `""`, because JSONata reads both as false. A meter number of `"0"` would silently vanish. - `: undefined` is not a literal — JSONata has no `undefined` keyword, so it is a path lookup that happens to find nothing. Against an event that really does carry a field named `undefined`, it returns that field's value. Write the **two-arm ternary with no else branch**, which omits the key when the test is false: `{ "pin": $exists(contact.pin) ? $string(contact.pin) }`. Every `$` binding is checked at save time against the known set, so a mistyped binding (`$lookup.documentType` for `$file_data[0].filename`) is rejected rather than silently producing a missing key. Not accepted together with `body`, which the upload direction rejects outright.
            - `enabled` string — Upload-only, OPTIONAL. JSONata returning a boolean, deciding whether this step runs at all. Absent means it runs. A false result is a BREAK: this step is skipped and so is every step after it, and the delivery is recorded as `skipped` rather than delivered or failed. It is acknowledged and never retried, and a `STEP_DISABLED` monitoring event is emitted at level `info` — a disabled step is the configuration working, not a fault. Reads the same bindings a body does, `$steps` included, so it can branch on what an earlier step returned. This is how a delivery is filtered out: with one delivery per attachment, a false result on the first step drops that file. An expression that throws, or returns a non-boolean, is a terminal `MAPPING_EXPRESSION_FAILED` instead — a broken predicate must not read as a deliberate skip.
            - `body` string — Handlebars template for the request body (write methods only), for bodies that are NOT JSON — form-encoded, XML, plain text. For a JSON body use `body_jsonata` instead; it cannot produce malformed JSON. **Download-only.** The upload direction rejects it: use `body_jsonata`, or leave that empty to send the files unchanged. Rendered once, against the context described on `FileProxyUseCaseConfiguration`: `{{ params.* }}`, `{{ env.* }}`, `{{ steps.N.body }}`, `{{ auth_token }}`. If a JSON body is written here anyway, **route every user-controlled value through the `json` helper**: the template engine does not escape, so `"name":"{{ params.documentName }}"` produces invalid JSON the moment a value contains a quote. Write `"name": {{json params.documentName}}` instead — the helper emits the surrounding quotes itself and renders absent values as `null`. Mutually exclusive with `body_jsonata`, which is upload-only.
            - `response_type` 'json' | 'binary', required — Expected response type
          - `response` FileProxyResponseConfig — How to extract the file from the step results. REQUIRED when `direction` is `download`; rejected when `direction` is `upload` (an upload has no file to extract).
            - `body` string, required — JSONata expression to extract file content from step results
            - `encoding` 'base64' | 'binary', required — Encoding of the extracted body
            - `filename` string — JSONata expression to extract the filename
            - `content_type` string — JSONata expression to extract the content type
          - `prevent_indirect_serving` boolean — Download-only; rejected when `direction` is `upload`. When `true`, this use case is served via the streaming endpoint: mapped file URLs are built as `/stream/download`, files of any size are streamed inline over HTTP response streaming, and buffered `/download` requests for oversize files are 307-redirected to `/stream`. Files never transit epilot's temporary S3 storage on the streaming path. Defaults to `false` (small files are served directly and large files are transparently served via a temporary S3 redirect).
      - ManagedCallUseCase
        - `id` string, uuid, required — Unique identifier for the use case
        - `integrationId` string, uuid, required — Parent integration ID
        - `name` string, required — Use case name
        - `slug` string — URL-safe identifier for the use case. Recommended for portable cross-environment referencing. Unique per integration. Immutable after creation. Lowercase alphanumeric, hyphens, and underscores only.
        - `type` 'managed_call', required — Use case type for managed API calls
        - `enabled` boolean, required
        - `change_description` string — Reason given for the update that produced the current version. Absent when the update supplied none — it is not inherited from the previous version.
        - `changed_by` string — User ID recorded for the update that produced the current version. Absent when the update had no user context, e.g. an automated sync.
        - `created_at` string, date-time, required — ISO-8601 timestamp when the use case was created
        - `updated_at` string, date-time, required — ISO-8601 timestamp when the use case was last updated
        - `configuration` ManagedCallOperationConfig — Configuration for managed_call use cases. Defines a single API operation with JSONata mapping.
          - `operation` ManagedCallOperation, required — HTTP operation configuration for managed calls
            - `method` 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE', required
            - `path` string, required — URL path template with {{variable}} interpolation
            - `headers` object — Custom HTTP headers for the request. Values support {{variable}} interpolation from the request payload and {{env.VAR}} references for environment variables.
            - `query_params` object — Query parameters for the request. Values support {{variable}} interpolation from the request payload.
          - `request_mapping` string — JSONata expression for outbound body transformation
          - `response_mapping` string — JSONata expression for inbound response transformation
          - `inbound_use_case_slug` string — Slug of the inbound use case to route responses to for async entity processing. When set, the managed call response is queued to the inbound pipeline and processed using the referenced inbound use case's mapping configuration.
        - `type_annotations` TypeAnnotations — Developer-provided type annotations for a use case's request and response fields
          - `request` object — Type annotations for request fields, keyed by dot-path (e.g., "vendors[].id" -> "string")
          - `response` object — Type annotations for response fields
        - `types_locked` boolean — Whether types have been generated for this use case
      - SecureProxyUseCase
        - `id` string, uuid, required — Unique identifier for the use case
        - `integrationId` string, uuid, required — Parent integration ID
        - `name` string, required — Use case name
        - `slug` string — URL-safe identifier for the use case. Recommended for portable cross-environment referencing. Unique per integration. Immutable after creation. Lowercase alphanumeric, hyphens, and underscores only.
        - `type` 'secure_proxy', required — Use case type
        - `enabled` boolean, required
        - `change_description` string — Reason given for the update that produced the current version. Absent when the update supplied none — it is not inherited from the previous version.
        - `changed_by` string — User ID recorded for the update that produced the current version. Absent when the update had no user context, e.g. an automated sync.
        - `created_at` string, date-time, required — ISO-8601 timestamp when the use case was created
        - `updated_at` string, date-time, required — ISO-8601 timestamp when the use case was last updated
        - `configuration` SecureProxyUseCaseConfiguration — Configuration for secure_proxy use cases. Defines how to route requests through a secure VPC.
          - `vpc_mode` 'static_ip' | 'secure_link', required — VPC routing mode. Read-only after creation. - static_ip: Routes through a VPC with static outbound IP (NAT Gateway) for IP-allowlisted external APIs. - secure_link: Routes through a VPN VPC for accessing private customer networks.
          - `allowed_domains` string[] — Domain whitelist for secure_link mode. Admin-only — can only be modified directly in DynamoDB via admin script. Supports exact match (e.g., "api.wemag.com") and wildcard prefix (e.g., "*.wemag.com").
          - `allowed_ips` string[] — IP allowlist (CIDR notation) for secure_link mode. Admin-only — can only be modified directly in DynamoDB via admin script. Required for secure_link mode. All DNS-resolved IPs must match at least one range. Example: ["10.0.1.0/24", "192.168.1.0/24"]

## Other responses

- `400` — Bad request
- `401` — Unauthorized request
- `404` — Integration not found
- `500` — Internal Server Error

---

[API](https://skmtc.dev/epilot/apis/integration-toolkit-api.md) · [All operations](https://skmtc.dev/epilot/apis/integration-toolkit-api/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/epilot/integration-toolkit-api/revisions/a765dc127359/schema)
