# Configuration

Configure integrations and use cases

This guide covers how to configure integrations, use cases, and manage your Integration Toolkit setup.

## Integration Management

### Integration Types

Integrations support two types:

| Type | Description |
|------|-------------|
| `erp` (default) | Standard ERP integration with inbound/outbound use cases |
| `connector` | Complex proxy integration with external APIs using managed calls |

### Creating an Integration

```bash
curl -X POST 'https://integration-toolkit.sls.epilot.io/v2/integrations' \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "SAP Integration",
    "description": "Main ERP integration for customer and contract data"
  }'
```

**Creating a Connector Integration:**

```bash
curl -X POST 'https://integration-toolkit.sls.epilot.io/v2/integrations' \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Partner API Connector",
    "description": "Connector for partner API calls",
    "integration_type": "connector",
    "connector_config": {
      "base_url": "https://api.partner.com",
      "auth": {
        "type": "oauth2_client_credentials",
        "token_url": "{{env.PARTNER_TOKEN_URL}}",
        "client_id": "{{env.PARTNER_CLIENT_ID}}",
        "client_secret": "{{env.PARTNER_CLIENT_SECRET}}",
        "scope": "api:read api:write"
      }
    }
  }'
```

### Integration Properties

| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `name` | string | Yes | Display name |
| `description` | string | No | Human-readable description |
| `integration_type` | string | No | `erp` (default) or `connector` |
| `connector_config` | object | No | Shared config for connector-type integrations (base URL, auth) |
| `protected` | boolean | No | When `true`, prevents deletion and restricts modifications to admin users |
| `maps` | array | No | [Key/Value Maps](./key-value-maps.md) declared on this integration (`key`, `label`, `description`, `value`). Saving the integration writes each map to the organization's environment as a `JSON` variable. |

### Listing Integrations

```bash
curl -X GET 'https://integration-toolkit.sls.epilot.io/v2/integrations' \
  -H 'Authorization: Bearer <token>'
```

### Updating an Integration

```bash
curl -X PUT 'https://integration-toolkit.sls.epilot.io/v2/integrations/{integrationId}' \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "SAP Production Integration",
    "description": "Updated description"
  }'
```

### Deleting an Integration

> **Warning: Deleting an integration also removes all associated use cases.**

```bash
curl -X DELETE 'https://integration-toolkit.sls.epilot.io/v2/integrations/{integrationId}' \
  -H 'Authorization: Bearer <token>'
```

## Use Case Configuration

### Creating a Use Case

```bash
curl -X POST 'https://integration-toolkit.sls.epilot.io/v1/integrations/{integrationId}/use-cases' \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Customer Sync",
    "type": "inbound",
    "enabled": true,
    "configuration": {
      "entities": [...]
    }
  }'
```

### Use Case Properties

| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `name` | string | Yes | Display name for the use case |
| `type` | string | Yes | `inbound`, `outbound`, `file_proxy`, `managed_call`, or `secure_proxy` |
| `enabled` | boolean | Yes | Whether the use case is active |
| `configuration` | object | Yes | Type-specific configuration (see sections below) |

> **Info: - `inbound` — Mapped by default; set `"direct": true` in the configuration to skip mapping and send entity-shaped payloads directly. See [Direct Mode](./inbound/direct-mode.md).**
- `file_proxy` — On-demand file serving from external systems or outbound delivery of epilot files. See the [File Proxy](./file-proxy.md) and [Outbound File Delivery](./outbound-file-delivery.md) guides.
- `managed_call` — Synchronous external API calls with JSONata mapping. See [Managed Call Use Cases](#managed-call-use-cases).
- `secure_proxy` — Route requests through epilot's secure proxy for static IP or VPN access. See [Secure Proxy Use Cases](#secure-proxy-use-cases).

### Inbound Use Case Options

Beyond the mapping itself, an inbound use case's `configuration` accepts the following options.

| Option | Values | Default | Description |
|--------|--------|---------|-------------|
| `direct` | boolean | `false` | Skip the mapping engine and accept entity-shaped payloads. See [Direct Mode](./inbound/direct-mode.md). |
| `interactive` | `disabled`, `on_request`, `always` | `disabled` | Whether events of this use case may be processed on the interactive lane, so that an end customer can wait for them. See [Interactive Registration](./interactive-registration.md#allow-interactive-processing). |

```json title="Allow the request to decide"
{
  "entities": [ "..." ],
  "interactive": "on_request"
}
```

- `on_request` is the normal setting: one mapping serves both the scheduled sync and the registration push, and each inbound request selects the interactive lane by sending `interactive: true`.
- `always` makes every event of the use case interactive, also on requests that do not ask, and charges every one to the organization's interactive budget. Reserve it for a use case that serves registrations only.
- `disabled` processes a request that asks for interactive as bulk and says so in each result; nothing is rejected. Absent means `disabled`, so remove the option rather than setting it to `disabled`.

In the Integration Hub, the option is the **Interactive processing** mode on the use case's **General** card.

If a caller is going to wait on your use cases by slug (`wait_for_use_cases`), model one inbound use case per partner event type and give every use case a slug. See [Use Case Requirements](./interactive-registration.md#use-case-requirements).

### Enabling/Disabling a Use Case

```bash
curl -X PUT 'https://integration-toolkit.sls.epilot.io/v1/integrations/{integrationId}/use-cases/{useCaseId}' \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{
    "enabled": false
  }'
```

> **Note: Configuration propagation**
Use case configurations are cached by the event-processing pipeline: any configuration change —
mapping edits, enabling/disabling, mode switches — can take up to **5 minutes** to take effect for
incoming events. Plan cutovers accordingly.

### Use Case History

View the change history for a use case:

```bash
curl -X GET 'https://integration-toolkit.sls.epilot.io/v1/integrations/{integrationId}/use-cases/{useCaseId}/history' \
  -H 'Authorization: Bearer <token>'
```

### Outbound Use Case Configuration

Outbound use cases deliver standardized epilot events (event-catalog events) to your external system. The configuration consists of an `event_catalog_event` and one or more `mappings`:

```bash
curl -X POST 'https://integration-toolkit.sls.epilot.io/v1/integrations/{integrationId}/use-cases' \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Contract Sync",
    "type": "outbound",
    "enabled": true,
    "configuration": {
      "event_catalog_event": "contract.updated",
      "mappings": [
        {
          "id": "b8f1c9a0-58dd-4f7a-9a3e-000000000001",
          "name": "ERP Contract Sync",
          "enabled": true,
          "jsonata_expression": "{ \"id\": entity._id, \"customer\": entity.customer_name }",
          "delivery": {
            "type": "webhook",
            "webhook_id": "wh-123"
          }
        }
      ]
    }
  }'
```

#### Event Filter

`event_filter` is an optional JSONata predicate on the use case configuration, next to `event_catalog_event`. It narrows which events of that name the use case handles — for example, only tickets with a certain purpose, or only certain contract types:

```json
{
  "event_catalog_event": "CustomerRequestSubmitted",
  "event_filter": "$count(ticket._purpose[$ = $env.move_request_purpose]) > 0",
  "mappings": [ … ]
}
```

- **Input:** the full hydrated event-catalog event, so relation nodes such as `ticket` and `contact` are populated.
- **Bindings:** `$env` (the organization's non-secret environment variables, including [Key/Value Maps](./key-value-maps.md)), `$mapValue` and `$mapKey`. Referencing `$env` instead of hard-coding organization-specific values such as entity IDs keeps the filter portable between organizations, for example in a blueprint.
- **Result:** the use case handles the event only when the filter evaluates truthy. When `event_filter` is absent, every event of the configured name is handled.
- **Errors:** a filter that throws while evaluating is treated as **no match** and logged, so one malformed filter cannot stop the other use cases subscribed to the same event.
- **Validation on save:** the filter must be a non-empty string, valid JSONata, and use no bindings other than `$env`, `$mapValue` and `$mapKey` (names the expression binds itself with `:=` are allowed).

An event the filter rejects is not processed by the use case at all: no [Pollable Outbound](./pollable-outbound.md) queue item and no [file delivery](./outbound-file-delivery.md). `event_filter` currently applies to poll and file proxy deliveries; it is not evaluated for webhook deliveries.

#### Mapping Properties

| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `id` | string (UUID) | No | Unique identifier for the mapping; generated when omitted |
| `name` | string | Yes | Display name for the mapping |
| `enabled` | boolean | Yes | Whether this mapping is active |
| `jsonata_expression` | string | For `webhook` delivery | JSONata expression to transform the event payload. Required for `webhook` (evaluated by the webhook service). Optional for `poll`: evaluated at enqueue time against the standardized event-catalog event with `$env` / `$mapValue` / `$mapKey`, must return a JSON object, and an empty value delivers the raw event — see [Payload Mapping](./pollable-outbound.md#payload-mapping). Rejected for `file_proxy` delivery |
| `delivery` | object | Yes | How the event is delivered — discriminated on `type`: `webhook`, `poll`, or `file_proxy` |

Outbound configurations are validated on save. The v1 use case endpoints require a `jsonata_expression` on every `webhook` mapping. The v2 integration upsert (`POST` / `PUT /v2/integrations`) validates an outbound use case only when it is new or its configuration changed, so configurations stored before a rule existed can be re-sent unchanged. It also accepts a `webhook` mapping with **no** `jsonata_expression` at all, for configurations that predate that requirement — such a mapping never enables or updates its webhook. An empty expression and invalid JSONata are rejected on both versions.

#### Delivery Types

**Webhook delivery (push):** the event payload is transformed with the mapping's `jsonata_expression` and pushed to a pre-configured webhook (epilot Webhooks):

```json
"delivery": {
  "type": "webhook",
  "webhook_id": "wh-123"
}
```

| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `webhook_id` | string | Yes | Reference to the webhook configuration in epilot Webhooks |
| `webhook_name` | string | No | Cached webhook name for display purposes |

**Poll delivery (pull):** for ERPs that cannot expose an inbound HTTP endpoint (firewalled, on-prem, batch systems). Items are placed on a pull-based queue that your system fetches and acknowledges. Poll items carry the **raw standardized event payload**, unless the mapping sets a `jsonata_expression` — then they carry its output, evaluated once at enqueue time (see [Payload Mapping](./pollable-outbound.md#payload-mapping)). See [Pollable Outbound](./pollable-outbound.md) for the full feature documentation (polling API, payload mapping, ordering guarantees, dead-letter handling, monitoring):

```jsonc
// DeliveryConfig — poll variant
"delivery": {
  "type": "poll",                    // discriminator: 'webhook' | 'poll'
  "retention_days": 30,              // optional; default 30, min 1, max 90
  "poison_policy": "dead_letter",    // 'dead_letter' (default) | 'block'
  "max_delivery_attempts": 5         // delivery attempts before poison_policy applies
}
```

| Property | Type | Required | Default | Bounds | Description |
|----------|------|----------|---------|--------|-------------|
| `retention_days` | integer | No | `30` | min 1, max 90 | How long undelivered queue items are retained before expiry |
| `poison_policy` | string | No | `"dead_letter"` | `dead_letter` \| `block` | What happens when an item exhausts `max_delivery_attempts`: `dead_letter` routes the item to the dead-letter queue; `block` halts the queue until action is taken |
| `max_delivery_attempts` | integer | No | `5` | min 1, max 100 | Maximum delivery attempts before the `poison_policy` is applied |

> **Info: - At most **one** poll mapping is allowed per use case. Webhook mappings may coexist alongside the poll mapping — push and poll for the same event is allowed.**
- A `poll` delivery must not carry webhook fields (`webhook_id`, `webhook_name`), and a `webhook` delivery must not carry poll fields (`retention_days`, `poison_policy`, `max_delivery_attempts`).

Everything beyond the configuration contract — the polling and acknowledgement API, payload mapping and its preview endpoint, lease and ordering semantics, retention and expiry behavior, the dead-letter queue and operator actions, and poll-mode monitoring — is documented on the dedicated [Pollable Outbound](./pollable-outbound.md) page.

**File proxy delivery (push):** points to an upload-direction `file_proxy` use case in the same integration. The referenced recipe owns fan-out, payload mapping, authentication, and HTTP steps. `jsonata_expression` is rejected on this mapping type, and the use case's `event_catalog_event` must declare `event_attachments`. See [Outbound File Delivery](./outbound-file-delivery.md) for the complete setup and runtime behavior.

```json
"delivery": {
  "type": "file_proxy",
  "use_case_slug": "customer-request-document-upload"
}
```

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `use_case_slug` | string | Yes | Slug of an upload-direction `file_proxy` use case in the same integration |

## Mapping Configuration Schema

### Version 2.0 Structure

```json
{
  "entities": [
    {
      "entity_schema": "string",
      "unique_ids": ["string"],
      "enabled": "boolean | string",
      "mode": "upsert | delete | purge | upsert-prune-scope-purge | upsert-prune-scope-delete",
      "scope": {},
      "jsonataExpression": "string",
      "fields": [
        {
          "attribute": "string",
          "field": "string",
          "jsonataExpression": "string",
          "constant": "any",
          "_type": "email | phone",
          "enabled": "boolean | string",
          "relations": {
            "operation": "_set | _append | _append_all",
            "items": [
              {
                "entity_schema": "string",
                "_tags": ["string"],
                "unique_ids": [
                  {
                    "attribute": "string",
                    "field": "string"
                  }
                ]
              }
            ],
            "jsonataExpression": "string"
          },
          "relation_refs": {
            "operation": "_set | _append | _append_all",
            "items": [
              {
                "entity_schema": "string",
                "unique_ids": [{ "attribute": "string", "field": "string" }],
                "path": "string",
                "value": {
                  "attribute": "string",
                  "operation": "_set | _append",
                  "field": "string",
                  "jsonataExpression": "string",
                  "constant": "any"
                }
              }
            ]
          }
        }
      ],
      "pricing": {
        "jsonataExpression": "string",
        "items": [],
        "items_jsonata": "string",
        "result_mapping": {},
        "on_error": "fail | skip | warn"
      },
      "sharing": {
        "partners": [
          { "partner_org_id": "string", "enabled": "boolean | JSONata string" }
        ],
        "partner_lookups": [
          {
            "unique_ids": [{ "attribute": "string", "field": "string" }],
            "schema": "string",
            "partner_org_id_attribute": "string",
            "enabled": "boolean | JSONata string"
          }
        ],
        "exclusive": "boolean"
      }
    }
  ],
  "meter_readings": [
    {
      "jsonataExpression": "string",
      "mode": "upsert | delete | upsert-prune-scope",
      "reading_matching": "external_id | strict-date",
      "scope": { "source": "string" },
      "meter": {
        "unique_ids": [
          {
            "attribute": "string",
            "field": "string"
          }
        ]
      },
      "meter_counter": {
        "unique_ids": [
          {
            "attribute": "string",
            "field": "string"
          }
        ]
      },
      "fields": []
    }
  ]
}
```

## Managed Call Use Cases

Managed call use cases define synchronous API operations against external partner systems. They are typically used with `connector`-type integrations.

### Creating a Managed Call Use Case

```bash
curl -X POST 'https://integration-toolkit.sls.epilot.io/v1/integrations/{integrationId}/use-cases' \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Get Customer",
    "slug": "get-customer",
    "type": "managed_call",
    "enabled": true,
    "configuration": {
      "operation": {
        "method": "GET",
        "path": "/v1/customers/{{customer_id}}",
        "headers": {
          "Accept": "application/json"
        }
      },
      "response_mapping": "{ \"name\": data.full_name, \"email\": data.contact.email }",
      "inbound_use_case_slug": "sync-customer"
    }
  }'
```

### Executing a Managed Call

Managed calls are executed via the `/v1/managed-call/{slug}/execute` endpoint, where the slug acts as the RPC method name:

```bash
curl -X POST 'https://integration-toolkit.sls.epilot.io/v1/managed-call/get-customer/execute' \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{
    "integration_id": "<integration-id>",
    "payload": {
      "customer_id": "C001"
    }
  }'
```

**Response:**

```json
{
  "success": true,
  "data": {
    "name": "John Doe",
    "email": "john@example.com"
  },
  "inbound_queued": true,
  "inbound_event_id": "evt-abc-123"
}
```

### Managed Call Configuration Fields

| Field | Required | Description |
|-------|----------|-------------|
| `operation.method` | Yes | HTTP method: `GET`, `POST`, `PUT`, `PATCH`, or `DELETE` |
| `operation.path` | Yes | URL path template with `{{variable}}` interpolation, appended to the connector's `base_url` |
| `operation.headers` | No | Additional request headers |
| `operation.query_params` | No | Query parameters |
| `request_mapping` | No | JSONata expression to transform the request body before sending |
| `response_mapping` | No | JSONata expression to transform the response before returning |
| `inbound_use_case_slug` | No | Slug of an inbound use case to route the response to for async entity processing |

### Connector Authentication

Authentication is configured at the integration level via `connector_config.auth` and applied automatically to all managed call requests:

| Auth Type | Fields |
|-----------|--------|
| `oauth2_client_credentials` | `token_url`, `client_id`, `client_secret`, `scope`, `audience`, `resource`, `body_params`, `headers`, `query_params` |
| `api_key` | `api_key_header` (default: `X-API-Key`), `api_key` |
| `bearer` | `token` |

Secrets must use `{{env.KEY}}` references to resolve values from the [Environments API](/docs/integrations/webhooks/environments-secrets).

## Secure Proxy Use Cases

Secure proxy use cases route HTTP requests through epilot's dedicated proxy infrastructure, providing either a static IP for egress or VPN access to customer private networks.

### Creating a Secure Proxy Use Case

```bash
curl -X POST 'https://integration-toolkit.sls.epilot.io/v1/integrations/{integrationId}/use-cases' \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Partner API Proxy",
    "slug": "partner-api",
    "type": "secure_proxy",
    "enabled": true,
    "configuration": {
      "vpc_mode": "secure_link"
    }
  }'
```

### Secure Proxy Configuration Fields

| Field | Required | Mutable | Description |
|-------|----------|---------|-------------|
| `vpc_mode` | Yes | No (immutable) | `"static_ip"` (fixed outbound IP) or `"secure_link"` (VPN for private networks) |
| `allowed_domains` | No | Read-only | Array of allowed domain patterns. Supports exact match and wildcard prefix (e.g., `*.example.com`). Managed by epilot support. |
| `allowed_ips` | No | Read-only | Array of allowed IP ranges in CIDR notation (e.g., `10.0.1.0/24`). Required for `secure_link` mode. Managed by epilot support. |

### Sending a Proxy Request

```bash
curl -X POST 'https://integration-toolkit.sls.epilot.io/v1/secure-proxy' \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{
    "integration_id": "<integration-id>",
    "use_case_slug": "partner-api",
    "url": "https://api.partner.com/v1/data",
    "method": "GET",
    "headers": {
      "Authorization": "Bearer external-token"
    }
  }'
```

### Domain Whitelist and IP Allowlist

- **Domain whitelist**: Controls which hostnames the proxy can reach. Wildcard patterns must have at least 2 suffix labels (e.g., `*.example.com` is valid, `*.com` is rejected).
- **IP allowlist**: Controls which IP addresses are permitted in `secure_link` mode using CIDR notation. Validation is applied both at the URL level (direct IP targets) and DNS level (resolved IPs must match).
- Both fields are read-only in the API. To add, remove, or change allowed domains or IP ranges, contact epilot support.

### Security

| Concern | Static IP mode | Secure Link mode |
|---------|---------------|-----------------|
| SSRF protection | Full (private IPs blocked) | Protocol + localhost only (private IPs allowed for VPN) |
| Domain whitelist | Optional | Required |
| IP allowlist | N/A | Required |
| Request size limit | 4 MB | 4 MB |
| Timeout | 30s | 30s |

## Event Configuration

### Event Message Structure

The structure of a `POST /v3/erp/updates/events` request:

```json
{
  "integration_id": "uuid",
  "correlation_id": "string (optional)",
  "interactive": "boolean (optional)",
  "correlation_complete": "boolean (optional)",
  "events": [
    {
      "use_case_slug": "string (recommended, 1-255 chars, pattern: ^[a-z0-9][a-z0-9_-]*$, required if event_name is not provided)",
      "event_name": "string (legacy fallback, required if use_case_slug is not provided)",
      "timestamp": "ISO 8601 datetime",
      "format": "json | xml",
      "payload": "string (JSON or XML)",
      "deduplication_id": "string (optional)"
    }
  ]
}
```

For each event, include `use_case_slug` whenever possible. `event_name` is supported as a legacy fallback, and `use_case_slug` takes precedence if both are present.

v3 takes `correlation_id` at request level and has no `event_type`. The deprecated v1 and v2 endpoints take
`event_type` and, on v1, a `meta` object; v3 drops both without an error, so a `meta.correlation_id` sent to v3 is
lost. `interactive` and `correlation_complete` are for pushes an end customer is waiting for — see
[Interactive Registration](./interactive-registration.md#request-fields).

`use_case_slug` format: `^[a-z0-9][a-z0-9_-]*$` with length `1..255` (must start with a lowercase letter or digit; then lowercase letters, digits, `_`, `-`).

### Event Types (v1 and v2)

The deprecated v1 and v2 endpoints classify each event with `event_type`. v3 has no such field.

| Type | Description |
|------|-------------|
| `CREATE` | Create a new entity (fails if exists) |
| `UPDATE` | Update existing or create new entity |
| `DELETE` | Mark entity as deleted |

### Payload Formats

**JSON Format:**

```json
{
  "format": "json",
  "payload": "{\"customerId\":\"C001\",\"name\":\"John Doe\"}"
}
```

**XML Format:**

```json
{
  "format": "xml",
  "payload": "<customer><id>C001</id><name>John Doe</name></customer>"
}
```

## Deduplication

### Deduplication ID

Prevent duplicate processing by including a unique deduplication ID:

```json
{
  "events": [
    {
      "deduplication_id": "customer-C001-update-20240115-001",
      "payload": "..."
    }
  ]
}
```

### Deduplication Window

- Events are deduplicated within a **24-hour window**
- Same deduplication ID within 24 hours = skipped
- After 24 hours, the same ID can be processed again

### Best Practices

```
# Good - includes version or timestamp
customer-C001-update-v3
customer-C001-update-20240115T103000

# Bad - no version control, may skip legitimate updates
customer-C001
```

## Correlation Tracking

### Correlation ID

Include a correlation ID at request level to track related events:

```json
{
  "integration_id": "uuid",
  "correlation_id": "<uuidv4()>",
  "events": [...]
}
```

When you omit it, epilot generates one and returns it in the response. For a correlation id an end customer is
waiting on, see [Interactive Registration](./interactive-registration.md#correlation-ids).

### Using Correlation IDs

- Group related events for debugging
- Filter monitoring data by batch
- Trace event processing through the pipeline

## Monitoring

Every inbound and outbound event the toolkit processes is recorded as a monitoring
event, queryable per integration and visible in the Integration Hub's **Monitoring**
tab. A one-off example — the last day's error events for one integration:

```bash
curl -X POST 'https://integration-toolkit.sls.epilot.io/v2/integrations/{integrationId}/monitoring/events' \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{ "level": "error", "from_date": "2026-01-15T00:00:00Z", "limit": 50 }'
```

Querying, stats and time series, event traces, replay, the code reference, and
alerting are documented in the [Monitoring section](./monitoring/overview.md).

## Rate Limits

| Endpoint | Limit |
|----------|-------|
| Event submission | 100 events per request |
| API requests | 1000 requests per minute per organization |

## Security

### Authentication

All API requests require a valid Bearer token:

```bash
curl -H 'Authorization: Bearer <your-token>' ...
```

### Organization Isolation

Each organization's data is fully isolated. Integration IDs are scoped to organizations, and cross-organization access is not permitted.

### Permissions

Four actions govern the Integration Toolkit. They are checked against the
organization the token belongs to, and — where the endpoint names one — against the
specific integration.

| Permission | Grants |
|---|---|
| `integration:view` | Read integrations and use cases; read monitoring events, stats, time series, traces, access logs, notification history and status |
| `integration:manage` | Create, update and delete integrations and use cases; edit notification configuration; push external monitoring events; send test notifications; replay events |
| `integration:ingest` | Push inbound events to the integration (`POST /v1/erp/updates/events`, `/v2/…`, `/v3/…`). It grants nothing else — not reading what landed, and not replay |
| `integration:consume` | Poll and acknowledge the outbound queue. Scoped to [Pollable Outbound](./pollable-outbound.md) only — it grants nothing else |

The `view` / `manage` split is the one that catches people out when minting a scoped
token for a middleware:

| Endpoint | Needs |
|---|---|
| `POST /v2/integrations/{id}/monitoring/events`, `…/stats`, `…/time-series` | `integration:view` |
| `GET /v2/integrations/{id}/monitoring/traces/{correlationId}` | `integration:view` |
| `GET /v2/integrations/{id}/notifications/history`, `…/status` | `integration:view` |
| `POST /v2/integrations/{id}/monitoring/external-events` | `integration:manage` |
| `POST /v2/integrations/{id}/notifications/test` | `integration:manage` |
| `POST /v1/integrations/{id}/events/replay` | `integration:manage`, checked organization-wide |
| `POST /v3/erp/updates/events` (and v1, v2) | `integration:ingest` |
| `GET /v3/erp/updates/events/{event_id}`, `GET /v3/erp/updates/events?correlation_id=…`, `GET /v3/erp/updates/correlations` | `integration:view` |
| `POST /v1/integrations/{id}/outbound/messages/poll`, `…/ack` | `integration:consume` |

The monitoring events list (`POST /v2/integrations/{id}/monitoring/events`) returns
`403` without `integration:view` on that integration. Its event details can carry customer
data — a mapped [poll message](./pollable-outbound.md#what-a-mapped-message-looks-like-in-monitoring)
includes its delivered payload.

Inbound event submission needs `integration:ingest` on the integration it pushes to;
`integration:*` and the owner role include it. See [Inbound Getting Started](./inbound/getting-started.md). A replay of inbound events
needs `integration:manage` instead; a push token's `integration:ingest` does not cover it.