> **Building with AI coding agents?** Install the authstack plugin with one command. This equips your agent with accurate Scalekit implementation patterns.
>
> **Recommended** (the `-y` flags skip prompts, so the command runs without an interactive terminal):
> ```bash
> npx -y @scalekit-inc/cli setup -y
> ```
>
> Global:
> ```bash
> npm install -g @scalekit-inc/cli
> scalekit setup -y
> ```
>
> Supports Claude Code, Cursor, GitHub Copilot, Codex + skills for other Agent Skills-compatible agents in `~/.agents/skills`.
> Skills: integrate-agentkit, implement-saaskit, add-mcp-oauth, implement-sso, implement-scim.
> [Full setup guide](https://docs.scalekit.com/dev-kit/build-with-ai/)

---

# Triggers

A trigger tells your app when something changes in a user's connected app, such as a new lead in Salesforce or a message in Slack. Subscribe a connected account, or a whole connection, to a trigger, and Scalekit sends each event to your delivery endpoint. See [How triggers work](https://docs.scalekit.com/agentkit/triggers/overview/) and [Receive trigger events](https://docs.scalekit.com/agentkit/triggers/receive-events/).

A webhook trigger also needs a webhook URL for the connection, registered in your app at the provider, and a signing secret for it. For an app that signs its events, that's the app's own secret: [set it](https://docs.scalekit.com/agentkit/reference/triggers/set-a-signing-secret/). For an app that doesn't, Scalekit [generates one](https://docs.scalekit.com/agentkit/reference/triggers/generate-a-signing-secret/). This secret covers the events the app sends to Scalekit. It isn't your delivery endpoint's `whsec_` secret, which Scalekit signs the events it sends to you with. A polling trigger needs neither: Scalekit checks the app on the interval you set.

While Triggers isn't turned on for your environment, every endpoint here returns `404`. [Contact us](https://docs.scalekit.com/support/contact-us/) to turn it on.

One page per endpoint, each with its own markdown copy:

| Endpoint | Request | What it does | Python SDK |
| --- | --- | --- | --- |
| [List available triggers (beta)](https://docs.scalekit.com/agentkit/reference/triggers/list-available-triggers.md) | `GET /api/v1/connections/{connection_id}/triggers` | List a connection's triggers, and whether each one can run on it. | `actions.triggers.list_available` |
| [List accounts eligible for a trigger (beta)](https://docs.scalekit.com/agentkit/reference/triggers/list-eligible-accounts.md) | `GET /api/v1/connections/{connection_id}/triggers/{trigger_name}/eligible-accounts` | List a connection's connected accounts, and whether each can subscribe to a trigger. | `actions.triggers.list_eligible_accounts` |
| [List trigger subscriptions (beta)](https://docs.scalekit.com/agentkit/reference/triggers/list-trigger-subscriptions.md) | `GET /api/v1/connections/{connection_id}/trigger-subscriptions` | List a connection's trigger subscriptions, with each one's status. | `actions.triggers.list_subscriptions` |
| [Subscribe to a trigger (beta)](https://docs.scalekit.com/agentkit/reference/triggers/create-a-trigger-subscription.md) | `POST /api/v1/connections/{connection_id}/trigger-subscriptions` | Subscribe one connected account, or a whole connection, to a trigger. | `actions.triggers.create_subscription` |
| [Subscribe several accounts to a trigger (beta)](https://docs.scalekit.com/agentkit/reference/triggers/batch-create-trigger-subscriptions.md) | `POST /api/v1/connections/{connection_id}/trigger-subscriptions:batchCreate` | Subscribe up to 100 connected accounts to a trigger, with a result for each. | `actions.triggers.batch_create_subscriptions` |
| [Delete a trigger subscription (beta)](https://docs.scalekit.com/agentkit/reference/triggers/delete-a-trigger-subscription.md) | `DELETE /api/v1/connections/{connection_id}/trigger-subscriptions/{subscription_id}` | Delete one trigger subscription. | `actions.triggers.delete_subscription` |
| [Delete several trigger subscriptions (beta)](https://docs.scalekit.com/agentkit/reference/triggers/batch-delete-trigger-subscriptions.md) | `POST /api/v1/connections/{connection_id}/trigger-subscriptions:batchDelete` | Delete up to 100 subscriptions, by ID or by account, with a result for each. | `actions.triggers.batch_delete_subscriptions` |
| [Create a webhook URL (beta)](https://docs.scalekit.com/agentkit/reference/triggers/create-a-webhook-url.md) | `POST /api/v1/connections/{connection_id}/trigger-ingress` | Create the URL an app sends a connection's webhook events to. | `actions.triggers.create_ingress_endpoint` |
| [Get a webhook URL (beta)](https://docs.scalekit.com/agentkit/reference/triggers/get-a-webhook-url.md) | `GET /api/v1/connections/{connection_id}/trigger-ingress` | Get a connection's webhook URL and the state of its signing secret. | `actions.triggers.get_ingress_endpoint` |
| [Set a signing secret (beta)](https://docs.scalekit.com/agentkit/reference/triggers/set-a-signing-secret.md) | `PUT /api/v1/connections/{connection_id}/trigger-ingress/signing-secret` | Save the signing secret the app signs its webhook events with. | `actions.triggers.set_signing_secret` |
| [Confirm a signing secret rotation (beta)](https://docs.scalekit.com/agentkit/reference/triggers/confirm-a-signing-secret-rotation.md) | `POST /api/v1/connections/{connection_id}/trigger-ingress/signing-secret/confirm` | Retire the previous signing secret once the app uses the new one. | `actions.triggers.confirm_signing_secret_rotation` |
| [Generate a signing secret (beta)](https://docs.scalekit.com/agentkit/reference/triggers/generate-a-signing-secret.md) | `POST /api/v1/connections/{connection_id}/trigger-ingress/signing-secret/generate` | Generate a secret for an app that sends no signature of its own. | `actions.triggers.generate_signing_secret` |
| [List deliveries (beta)](https://docs.scalekit.com/agentkit/reference/triggers/list-deliveries.md) | `GET /api/v1/trigger-delivery/endpoints/{endpoint_id}/deliveries` | List the trigger events sent to your delivery endpoint, with their state. | `actions.triggers.list_deliveries` |
| [List the attempts for a delivery (beta)](https://docs.scalekit.com/agentkit/reference/triggers/list-attempts-for-a-delivery.md) | `GET /api/v1/trigger-delivery/endpoints/{endpoint_id}/deliveries/{msg_id}/attempts` | List every attempt to deliver one event to your endpoint. | `actions.triggers.list_delivery_attempts_by_message` |
| [List recent delivery attempts (beta)](https://docs.scalekit.com/agentkit/reference/triggers/list-recent-delivery-attempts.md) | `GET /api/v1/trigger-delivery/endpoints/{endpoint_id}/attempts` | List the most recent delivery attempts to your endpoint. | `actions.triggers.list_delivery_attempts_by_endpoint` |
| [Look up a delivery (beta)](https://docs.scalekit.com/agentkit/reference/triggers/look-up-a-delivery.md) | `GET /api/v1/trigger-delivery/deliveries/{id}` | Find one event by its message ID or event ID, with its delivery attempts. | `actions.triggers.lookup_delivery` |
| [Look up several deliveries (beta)](https://docs.scalekit.com/agentkit/reference/triggers/look-up-several-deliveries.md) | `POST /api/v1/trigger-delivery/deliveries:batchLookup` | Find up to 25 events by message ID or event ID, with a result for each. | `actions.triggers.batch_lookup_deliveries` |
| [Resend deliveries (beta)](https://docs.scalekit.com/agentkit/reference/triggers/resend-deliveries.md) | `POST /api/v1/trigger-delivery/deliveries:batchResend` | Send up to 50 events to your endpoint again, with a result for each. | `actions.triggers.batch_resend_deliveries` |
| [Recover failed deliveries (beta)](https://docs.scalekit.com/agentkit/reference/triggers/recover-failed-deliveries.md) | `POST /api/v1/trigger-delivery/endpoints/{endpoint_id}:recover` | Resend every event that failed to reach your endpoint in a time window. | `actions.triggers.recover_deliveries` |
| [Get trigger activity (beta)](https://docs.scalekit.com/agentkit/reference/triggers/get-trigger-activity.md) | `GET /api/v1/trigger-activity` | Count what happened to your environment's trigger events in the last 24 hours. | `actions.triggers.get_activity` |
| [Get trigger usage (beta)](https://docs.scalekit.com/agentkit/reference/triggers/get-trigger-usage.md) | `GET /api/v1/trigger-usage` | Get the trigger deliveries metered for your environment, by hour or day. | `actions.triggers.get_usage` |


## List available triggers (beta)

`GET /api/v1/connections/{connection_id}/triggers`

This endpoint is in beta. Its request and response may change.

Lists every trigger the connection's app offers. A trigger that can't run on this connection is still listed, with `available` set to `false` and a `reason` and `remedy`. Each trigger says whether it's a webhook or polling trigger, which scopes it needs, which filters it takes in `config_fields`, and for a polling trigger its default and minimum interval.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `connection_id` | string | Yes | ID of the connection, in this environment. Up to 64 characters. |

**Response (200)**

| Name | Type | Description |
| --- | --- | --- |
| `connection_uses_shared_app` | boolean | Whether the connection is configured to use Scalekit's shared provider app rather than your own. Read from the connection's saved configuration. When true, any trigger with platform_credentials UNSUPPORTED is unavailable until you add your own provider app to the connection. |
| `triggers` | array of object | Every trigger the connection's provider publishes, including unavailable ones. Check available on each before offering it. |
| `triggers.available` | boolean | Whether this trigger can run on this connection. False does not mean the trigger is unsupported by the provider — it means something about THIS connection prevents it, and reason and remedy say what. |
| `triggers.config_fields` | array of object | Values a subscription can set in its config map to narrow which events it receives. A config key not listed here is refused. |
| `triggers.config_fields.display_name` | string | Human-readable label for the field. |
| `triggers.config_fields.help_text` | string | Guidance on what to enter. Empty when the trigger provides none. |
| `triggers.config_fields.key` | string | The key to send in a subscription's config map. Send it exactly as returned, without changing its case: a key that does not match is refused. |
| `triggers.config_fields.placeholder` | string | Example value to show in an empty input. Empty when the trigger provides none. |
| `triggers.config_fields.required` | boolean | Whether a subscription must set this field to a non-empty value. A subscription request that omits a required field is refused. |
| `triggers.default_interval_seconds` | integer | Polling interval, in seconds, a subscription uses when it does not set interval_seconds. Zero for a webhook trigger. |
| `triggers.delivery_scope` | string (enum) | Whether this trigger delivers one event per connected account or one event for the whole connection, whatever accounts exist on it. Decides how the trigger is subscribed: an account-scoped trigger is subscribed on one connected account, a connection-scoped one on the connection itself, naming no account. One of: `ACCOUNT`, `CONNECTION`. |
| `triggers.description` | string | What event the trigger reports. |
| `triggers.display_name` | string | Human-readable name of the trigger. |
| `triggers.min_interval_seconds` | integer | Shortest polling interval, in seconds, a subscription to this trigger may request. A lower interval_seconds is refused rather than rounded up. Zero for a webhook trigger, which has no interval. |
| `triggers.mode` | string (enum) | How the trigger detects events. WEBHOOK - the provider sends each event to the connection's webhook URL, so the connection needs a webhook URL registered with the provider. POLL - Scalekit checks the provider on an interval, so a subscription can set interval_seconds. One of: `WEBHOOK`, `POLL`. |
| `triggers.name` | string | Unique name of the trigger within its provider. Pass it as trigger_name when listing eligible accounts or creating a subscription. |
| `triggers.platform_credentials` | string (enum) | Whether this trigger works on a connection that uses Scalekit's shared provider app. UNSUPPORTED means the connection needs your own provider app before this trigger can run; combine it with connection_uses_shared_app on the list response to decide whether to ask for one. UNSPECIFIED when the trigger declares a value this server does not recognise. One of: `SUPPORTED`, `UNSUPPORTED`. |
| `triggers.provider` | string | The provider the trigger belongs to, the same as the connection's provider. |
| `triggers.provides_bearer_secret` | boolean | Whether Scalekit generates this trigger's signing secret ([Generate a signing secret](https://docs.scalekit.com/agentkit/reference/triggers/generate-a-signing-secret/)) rather than you copying one from the provider's console. True only for providers that send no signature of their own. Always false for a polled trigger. |
| `triggers.reason` | string | Why the trigger is unavailable on this connection. Populated whenever available is false. |
| `triggers.refusal_code` | string (enum) | The stable code of the connection-level rule that made this trigger unavailable. SHARED_APP — the connection uses Scalekit's shared provider app. POLL_CONNECTION_SCOPE — a polled trigger cannot be connection-scoped. UNSPECIFIED when the trigger is available, or when a rule without a code refused it; reason and remedy are populated either way. One of: `SHARED_APP`, `POLL_CONNECTION_SCOPE`. |
| `triggers.register_hint` | string | Provider-specific guidance on where to register the connection's ingress URL in the provider's console, written by the connector author. At most 500 characters, with no control characters. Empty when the connector provides none, and always empty for a polled trigger. |
| `triggers.remedy` | string | What to do to make the trigger available. Populated whenever available is false. |
| `triggers.required_scopes` | array of object | Permissions a connected account's grant must include to subscribe to this trigger. A subscription for an account whose grant lacks one of them is refused, naming the missing scope. |
| `triggers.required_scopes.grant` | string | Which grant channel must carry the scope. Some providers fan events out differently for a bot install than for a user install, so this is not merely descriptive. |
| `triggers.required_scopes.scope` | string | The provider's own name for the permission, exactly as it appears in their console. |
| `triggers.requires_signing_secret` | boolean | Whether the provider signs this trigger's events with a secret you copy from the provider's console and sets on the connection's ingress endpoint. Always false for a polled trigger, which receives no inbound requests. |
| `triggers.secret_hint` | string | Provider-specific guidance on where the provider shows the signing secret to copy, written by the connector author. At most 500 characters, with no control characters. Empty when the connector provides none, and always empty for a polled trigger. |

**Errors**

- `400`: Invalid request - connection_id is not a valid connection ID
- `404`: Not found - the connection does not exist in this environment, or Triggers isn't turned on for your environment
- `500`: Internal error - the triggers for this connection could not be read; retry the request

**Request**

```bash
curl -sS -X GET \
  "$SCALEKIT_ENVIRONMENT_URL/api/v1/connections/conn_133400101014995480/triggers" \
  -H "Authorization: Bearer $TOKEN"
```

**Response**

```json
{
  "triggers": [
    {
      "name": "salesforce_lead_created",
      "display_name": "Lead created",
      "description": "Fires when a new lead is created in Salesforce. Edits to an existing lead do not fire it.",
      "provider": "salesforce",
      "mode": "POLL",
      "required_scopes": [
        {
          "scope": "api",
          "grant": "default"
        }
      ],
      "min_interval_seconds": 300,
      "default_interval_seconds": 300,
      "config_fields": [
        {
          "key": "lead_source",
          "display_name": "Lead source",
          "help_text": "Only fire for leads from this source, e.g. Web. Leave blank for all leads.",
          "placeholder": "",
          "required": false
        }
      ],
      "available": true,
      "reason": "",
      "remedy": "",
      "delivery_scope": "ACCOUNT",
      "platform_credentials": "UNSUPPORTED",
      "refusal_code": "TRIGGER_REFUSAL_CODE_UNSPECIFIED",
      "requires_signing_secret": false,
      "register_hint": "",
      "secret_hint": "",
      "provides_bearer_secret": false
    }
  ],
  "connection_uses_shared_app": false
}
```

**Python SDK:** `scalekit_client.actions.triggers.list_available`

List every trigger a connection offers and whether each is usable there.

```python
scalekit_client.actions.triggers.list_available(
    connection_id: str,
) -> ListAvailableTriggersResponse
```

Example:

```python
result = scalekit_client.actions.triggers.list_available(
    connection_id="conn_133400101014995480",
)
```

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `connection_id` | `str` | Yes | The connection (`conn_...`). |

Returns `ListAvailableTriggersResponse`: All triggers (not paginated), plus `connection_uses_shared_app`.

## List accounts eligible for a trigger (beta)

`GET /api/v1/connections/{connection_id}/triggers/{trigger_name}/eligible-accounts`

This endpoint is in beta. Its request and response may change.

Lists the connected accounts on a connection, and marks each one eligible or not for one trigger. An account that can't subscribe is still listed, with a `reason` and a `remedy`, such as reconnecting to grant a missing scope. Pass `query` to search account identifiers. Call this before you [subscribe accounts](https://docs.scalekit.com/agentkit/reference/triggers/batch-create-trigger-subscriptions/).

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `connection_id` | string | Yes | ID of the connection, in this environment. Up to 64 characters. |
| `trigger_name` | string | Yes | Name of the trigger, as returned in the name field of [List available triggers](https://docs.scalekit.com/agentkit/reference/triggers/list-available-triggers/). Up to 128 characters. |

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `page_size` | integer | No | How many accounts to return. Defaults to 10; a larger value than 100 is capped at 100 rather than refused. |
| `page_token` | string | No | The next_page_token or prev_page_token from a previous response. Omit it to read the first page. A value that is not a token from a previous response is refused with 400. |
| `query` | string | No | Narrows the list to accounts whose identifier contains this text, case-insensitively. Matched against every account on the connection, not only the current page. Up to 100 characters. |

**Response (200)**

| Name | Type | Description |
| --- | --- | --- |
| `accounts` | array of object | One page of the connection's connected accounts, eligible and ineligible, each with its health for this trigger. |
| `accounts.eligible` | boolean | Whether this account can be subscribed to the trigger. When false, reason and remedy explain why and what to do. |
| `accounts.health` | object | The account's own state, evaluated for what this trigger needs captured on the account: a webhook trigger needs the provider workspace, and an account-scoped webhook trigger also needs the provider user. A polled trigger needs neither, so the same account can be READY for one trigger and AWAITING_CONFIRMATION for another. Carried alongside eligible because they answer different questions: eligible is whether this trigger can be subscribed on this account, health is the account's own state and the single action that resolves it. |
| `accounts.health.action` | string (enum) | The single action that resolves this state. Unspecified when there is nothing to do. One of: `RECONNECT`, `COMPLETE_SETUP`. |
| `accounts.health.blocks_tool_calls` | boolean | Whether the account's credential itself is unusable, so tool calls made with it fail too. True only for NEEDS_REAUTHENTICATION. False for AWAITING_CONFIRMATION and WRONG_APP: the credential still works and tool calls still succeed, so an agent should keep using the account; only trigger events cannot reach it. |
| `accounts.health.reason` | string | Why the account is in this state. Empty when the state is READY. |
| `accounts.health.remedy` | string | What to do about it. Empty when the state is READY. |
| `accounts.health.state` | string (enum) | READY — the account can receive triggers. AWAITING_CONFIRMATION — connected and working, but Scalekit has not confirmed which provider workspace or user it belongs to, so inbound events cannot be routed to it. WRONG_APP — authorized under a different OAuth app than the connection now uses. DISCONNECTED — the account was disconnected. NEEDS_REAUTHENTICATION — the credential itself is unusable, because it has expired or the account has not finished connecting. One of: `READY`, `AWAITING_CONFIRMATION`, `WRONG_APP`, `DISCONNECTED`, `NEEDS_REAUTHENTICATION`. |
| `accounts.id` | string | ID of the connected account. Pass it as connected_account_id when creating a subscription. |
| `accounts.identifier` | string | Your app's ID for the user, the value passed when the account was created. |
| `accounts.reason` | string | Why this account cannot receive the trigger. Populated whenever eligible is false. |
| `accounts.remedy` | string | What to do so the account can receive the trigger. Populated whenever eligible is false. |
| `next_page_token` | string | Pass as page_token to read the next page. Empty when there is no next page. |
| `prev_page_token` | string | Pass as page_token to read the previous page. Empty when there is no previous page. |
| `total_size` | integer | How many of this connection's accounts match the query, across every page. Accounts on other connections are never counted. |

**Errors**

- `400`: Invalid request - connection_id is not a valid connection ID, page_token is not a token from a previous response, or query is longer than 100 characters
- `404`: Not found - the connection does not exist in this environment, the connection's provider publishes no trigger with this name, or Triggers isn't turned on for your environment
- `500`: Internal error - the accounts for this trigger could not be read; retry the request

**Request**

```bash
curl -sS -X GET \
  "$SCALEKIT_ENVIRONMENT_URL/api/v1/connections/conn_133400101014995480/triggers/salesforce_lead_created/eligible-accounts" \
  -H "Authorization: Bearer $TOKEN"
```

**Response**

```json
{
  "accounts": [
    {
      "id": "ca_133400349586228019",
      "identifier": "user_123",
      "eligible": true,
      "reason": "",
      "remedy": "",
      "health": {
        "state": "READY",
        "action": "ACCOUNT_HEALTH_ACTION_UNSPECIFIED",
        "blocks_tool_calls": false,
        "reason": "",
        "remedy": ""
      }
    }
  ],
  "next_page_token": "",
  "prev_page_token": "",
  "total_size": 1
}
```

**Python SDK:** `scalekit_client.actions.triggers.list_eligible_accounts`

List a connection's connected accounts and whether each can subscribe.

```python
scalekit_client.actions.triggers.list_eligible_accounts(
    connection_id: str,
    trigger_name: str,
    query: str | None = None,
    page_size: int | None = None,
    page_token: str | None = None,
) -> ListEligibleAccountsResponse
```

Example:

```python
result = scalekit_client.actions.triggers.list_eligible_accounts(
    connection_id="conn_133400101014995480",
    trigger_name="salesforce_lead_created",
)
```

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `connection_id` | `str` | Yes | The connection (`conn_...`). |
| `trigger_name` | `str` | Yes | `AvailableTrigger.name`. |
| `query` | `str \| None` | No | Filter accounts by identifier. |
| `page_size` | `int \| None` | No | Accounts per page; the server default is 10 and values above 100 are capped. |
| `page_token` | `str \| None` | No | `next_page_token` or `prev_page_token` of a previous page. |

Returns `ListEligibleAccountsResponse`: One page of accounts with their eligibility and health.

## List trigger subscriptions (beta)

`GET /api/v1/connections/{connection_id}/trigger-subscriptions`

This endpoint is in beta. Its request and response may change.

Lists the trigger subscriptions on a connection, newest first. Filter by `trigger_name` and `statuses`. A polling subscription also returns when it last ran and why its last run failed. `status_counts` counts the subscriptions in each status across every page.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `connection_id` | string | Yes | ID of the connection, in this environment. Up to 64 characters. |

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `page_size` | integer | No | How many subscriptions to return. Defaults to 10; a larger value than 100 is capped at 100 rather than refused. |
| `page_token` | string | No | The next_page_token or prev_page_token from a previous response. Omit it to read the first page. A value that is not a token from a previous response is refused with 400. |
| `statuses` | array of string (enum) | No | Narrows the list to subscriptions in these statuses, at most 4. Omit it for every status; TRIGGER_SUBSCRIPTION_STATUS_UNSPECIFIED is refused. Does not affect status_counts, so a client filtering to one status still sees the totals for all of them. - ACTIVE: Delivering. For a polled trigger, at the requested interval. - THROTTLED: Running slower than requested, not stopped. Still delivering. - PAUSED: Stopped by you. - PAUSED_CREDENTIAL: Stopped because the account's credential no longer works. Distinct from PAUSED so the two have distinguishable explanations for missing events. |
| `trigger_name` | string | No | Narrows the list to one trigger's subscriptions. Omit it to list every subscription on the connection. |

**Response (200)**

| Name | Type | Description |
| --- | --- | --- |
| `next_page_token` | string | Pass as page_token to read the next page. Empty when there is no next page. |
| `prev_page_token` | string | Pass as page_token to read the previous page. Empty when there is no previous page. |
| `status_counts` | object | How many subscriptions sit in each status, keyed by the status name. Counted over the whole matching set rather than the current page, so a client can show per-status totals without fetching every page. Honours trigger_name but ignores statuses, so the totals stay still while you switch between status filters. |
| `subscriptions` | array of object | One page of the connection's trigger subscriptions that match the filters, newest first. |
| `subscriptions.account_health` | string (enum) | How the subscribed account stands for this trigger. AWAITING_CONFIRMATION means Scalekit cannot route this trigger's events to the account yet, so the subscription will not fire until the account's user reconnects it. Absent for connection-scoped subscriptions. One of: `READY`, `AWAITING_CONFIRMATION`, `WRONG_APP`, `DISCONNECTED`, `NEEDS_REAUTHENTICATION`. |
| `subscriptions.config` | object | The subscription's filter values, keyed by the trigger's config_fields keys. Keys are returned exactly as stored; send them back without changing their case. |
| `subscriptions.connected_account_id` | string | ID of the subscribed connected account. Empty for a connection-scoped subscription. |
| `subscriptions.connected_account_identifier` | string | The identifier the connected account was created with, your app's ID for the user, such as `user_123`, so you can label the subscription without fetching each account. Empty for a connection-scoped subscription, and empty when the account has no identifier recorded — fall back to connected_account_id rather than showing a blank. |
| `subscriptions.connection_id` | string | ID of the connection the subscription belongs to. |
| `subscriptions.created_at` | string | When the subscription was created. Returned so a list can be ordered or explained by age without a second lookup. |
| `subscriptions.delivery_scope` | string (enum) | Whether this subscription receives one event per connected account or one event for the whole connection. A connection-scoped subscription carries no connected_account_id — the connection is the only recipient. One of: `ACCOUNT`, `CONNECTION`. |
| `subscriptions.effective_interval_seconds` | integer | The polling interval, in seconds, the subscription actually runs at. Larger than requested_interval_seconds when the environment's polling budget is shared across many polled subscriptions; the subscription's status is then THROTTLED. Zero for a webhook trigger. |
| `subscriptions.id` | string | Unique identifier of the trigger subscription. |
| `subscriptions.poll_state` | object | How a POLL subscription's feed is doing: when it last ran, why the last run failed, and whether polling is held off. Absent for a WEBHOOK subscription, which is never polled. |
| `subscriptions.poll_state.backoff_until` | string | Polling is held off until this time, because the provider asked Scalekit to slow down or a run failed in a way retrying cannot fix. Absent when no hold-off has been recorded. A time in the past means the hold-off has ended and the next scheduled run polls again. |
| `subscriptions.poll_state.consecutive_failures` | integer | How many runs in a row have failed. Zero after any successful run. |
| `subscriptions.poll_state.last_error` | string | A short, human-readable explanation of why the most recent run failed and what happens next. Empty when the most recent run succeeded or no run has happened yet. Chosen from a fixed set of messages by last_error_code and never contains the provider's response. The wording can change: match on last_error_code, not on this text. |
| `subscriptions.poll_state.last_error_code` | string (enum) | Why the most recent run failed, as a stable code to branch on. TRIGGER_POLL_ERROR_CODE_UNSPECIFIED when the most recent run succeeded or no run has happened yet. CREDENTIAL_REVOKED and INVALID_CONFIGURATION need a change from you before polling can succeed; every other code is retried automatically. One of: `RATE_LIMITED`, `CREDENTIAL_REVOKED`, `INVALID_CONFIGURATION`, `PROVIDER_ERROR`, `DELIVERY_FAILED`, `TIMED_OUT`, `POLL_FAILED`. |
| `subscriptions.poll_state.last_run_at` | string | When Scalekit last polled the provider for this subscription, whether or not the run succeeded. Absent until the first run. A value far older than the effective interval means the feed has stopped running. |
| `subscriptions.poll_state.watermark_updated_at` | string | When the feed's position last moved forward — the last time a run found and delivered newer events. A run that finds nothing new leaves it unchanged. |
| `subscriptions.provider` | string | The provider the trigger belongs to. |
| `subscriptions.requested_interval_seconds` | integer | The polling interval, in seconds, the subscription asked for, or the trigger's default when it did not set one. Returned next to effective_interval_seconds so a subscription running slower than requested is visible. Zero for a webhook trigger. |
| `subscriptions.status` | string (enum) | Whether the subscription is delivering. ACTIVE — delivering. THROTTLED — delivering, but polling slower than requested; compare requested_interval_seconds with effective_interval_seconds. PAUSED — stopped by you. PAUSED_CREDENTIAL — stopped because the account's credential no longer works. One of: `ACTIVE`, `THROTTLED`, `PAUSED`, `PAUSED_CREDENTIAL`. |
| `subscriptions.trigger_name` | string | Name of the subscribed trigger, as returned by [List available triggers](https://docs.scalekit.com/agentkit/reference/triggers/list-available-triggers/). |
| `total_size` | integer | How many subscriptions match the filters in total, across every page. |

**Errors**

- `400`: Invalid request - connection_id is not a valid connection ID, page_token is not a token from a previous response, or statuses holds TRIGGER_SUBSCRIPTION_STATUS_UNSPECIFIED or an unknown status
- `404`: Not found - the connection does not exist in this environment, or Triggers isn't turned on for your environment
- `500`: Internal error - the subscriptions could not be read; retry the request

**Request**

```bash
curl -sS -G -X GET \
  "$SCALEKIT_ENVIRONMENT_URL/api/v1/connections/conn_133400101014995480/trigger-subscriptions" \
  -H "Authorization: Bearer $TOKEN" \
  --data-urlencode "trigger_name=salesforce_lead_created"
```

**Response**

```json
{
  "subscriptions": [
    {
      "id": "trgs_141020933185734981",
      "connected_account_id": "ca_133400349586228019",
      "connected_account_identifier": "user_123",
      "connection_id": "conn_133400101014995480",
      "provider": "salesforce",
      "trigger_name": "salesforce_lead_created",
      "status": "ACTIVE",
      "delivery_scope": "ACCOUNT",
      "requested_interval_seconds": 300,
      "effective_interval_seconds": 300,
      "config": {
        "lead_source": "Web"
      },
      "created_at": "2026-10-28T09:12:44.120Z",
      "poll_state": {
        "last_run_at": "2026-10-28T09:17:44.310Z",
        "last_error": "",
        "last_error_code": "TRIGGER_POLL_ERROR_CODE_UNSPECIFIED",
        "consecutive_failures": 0,
        "watermark_updated_at": "2026-10-28T09:17:44.310Z"
      },
      "account_health": "READY"
    }
  ],
  "next_page_token": "",
  "prev_page_token": "",
  "total_size": 1,
  "status_counts": {
    "ACTIVE": 1
  }
}
```

**Python SDK:** `scalekit_client.actions.triggers.list_subscriptions`

List a connection's trigger subscriptions, one page at a time.

```python
scalekit_client.actions.triggers.list_subscriptions(
    connection_id: str,
    trigger_name: str | None = None,
    statuses: Sequence[TriggerSubscriptionStatus | str | int] | None = None,
    page_size: int | None = None,
    page_token: str | None = None,
) -> ListTriggerSubscriptionsResponse
```

Example:

```python
result = scalekit_client.actions.triggers.list_subscriptions(
    connection_id="conn_133400101014995480",
    trigger_name="salesforce_lead_created",
)
```

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `connection_id` | `str` | Yes | The connection (`conn_...`). |
| `trigger_name` | `str \| None` | No | Only subscriptions to this trigger. |
| `statuses` | `Sequence[TriggerSubscriptionStatus \| str \| int] \| None` | No | Only subscriptions in these states: `TriggerSubscriptionStatus` members, their names, or raw numbers. |
| `page_size` | `int \| None` | No | Subscriptions per page; the server caps it at 100. |
| `page_token` | `str \| None` | No | `next_page_token` or `prev_page_token` of a previous page. |

Returns `ListTriggerSubscriptionsResponse`: One page of subscriptions, `total_size` and `status_counts`.

## Subscribe to a trigger (beta)

`POST /api/v1/connections/{connection_id}/trigger-subscriptions`

This endpoint is in beta. Its request and response may change.

Subscribes one connected account, or the whole connection, to a trigger. Pass `connected_account_id` for a per-account trigger, and leave it out for a per-connection trigger. Set the trigger's filters in `config`, using the keys from the trigger's `config_fields`. For a polling trigger, `interval_seconds` sets how often Scalekit checks the app; it defaults to the trigger's default interval, and a value below its minimum is refused.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `connection_id` | string | Yes | ID of the connection, in this environment. Up to 64 characters. |

**Request body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `trigger_name` | string | Yes | Name of the trigger to subscribe to, as returned in the name field of [List available triggers](https://docs.scalekit.com/agentkit/reference/triggers/list-available-triggers/). Up to 128 characters. |
| `config` | object | No | Filter values for the subscription, keyed by the trigger's config_fields keys, spelled exactly as returned. Every required field must be set. An unknown key, or a value longer than 512 characters, is refused. Subscribing again with different values creates another subscription; subscribing again with identical values returns 409. |
| `connected_account_id` | string | No | Connected account to subscribe on, for an account-scoped trigger. Omit it for a connection-scoped trigger — the trigger is then subscribed on the connection itself and no account is named. Whether omitting it is valid depends on the trigger's own delivery_scope. |
| `interval_seconds` | integer | No | Polling interval in seconds, for a polled trigger only. Omit it to use the trigger's default_interval_seconds. A value below the trigger's min_interval_seconds is refused, naming the minimum, rather than rounded up. Setting it for a webhook trigger is refused. The subscription can run slower than requested when the environment's polling budget is shared across many polled subscriptions; effective_interval_seconds on the response shows the interval it runs at. |

**Response (200)**

| Name | Type | Description |
| --- | --- | --- |
| `account_health` | string (enum) | How the subscribed account stands for this trigger. AWAITING_CONFIRMATION means Scalekit cannot route this trigger's events to the account yet, so the subscription will not fire until the account's user reconnects it. Absent for connection-scoped subscriptions. One of: `READY`, `AWAITING_CONFIRMATION`, `WRONG_APP`, `DISCONNECTED`, `NEEDS_REAUTHENTICATION`. |
| `config` | object | The subscription's filter values, keyed by the trigger's config_fields keys. Keys are returned exactly as stored; send them back without changing their case. |
| `connected_account_id` | string | ID of the subscribed connected account. Empty for a connection-scoped subscription. |
| `connected_account_identifier` | string | The identifier the connected account was created with, your app's ID for the user, such as `user_123`, so you can label the subscription without fetching each account. Empty for a connection-scoped subscription, and empty when the account has no identifier recorded — fall back to connected_account_id rather than showing a blank. |
| `connection_id` | string | ID of the connection the subscription belongs to. |
| `created_at` | string | When the subscription was created. Returned so a list can be ordered or explained by age without a second lookup. |
| `delivery_scope` | string (enum) | Whether this subscription receives one event per connected account or one event for the whole connection. A connection-scoped subscription carries no connected_account_id — the connection is the only recipient. One of: `ACCOUNT`, `CONNECTION`. |
| `effective_interval_seconds` | integer | The polling interval, in seconds, the subscription actually runs at. Larger than requested_interval_seconds when the environment's polling budget is shared across many polled subscriptions; the subscription's status is then THROTTLED. Zero for a webhook trigger. |
| `id` | string | Unique identifier of the trigger subscription. |
| `poll_state` | object | How a POLL subscription's feed is doing: when it last ran, why the last run failed, and whether polling is held off. Absent for a WEBHOOK subscription, which is never polled. |
| `poll_state.backoff_until` | string | Polling is held off until this time, because the provider asked Scalekit to slow down or a run failed in a way retrying cannot fix. Absent when no hold-off has been recorded. A time in the past means the hold-off has ended and the next scheduled run polls again. |
| `poll_state.consecutive_failures` | integer | How many runs in a row have failed. Zero after any successful run. |
| `poll_state.last_error` | string | A short, human-readable explanation of why the most recent run failed and what happens next. Empty when the most recent run succeeded or no run has happened yet. Chosen from a fixed set of messages by last_error_code and never contains the provider's response. The wording can change: match on last_error_code, not on this text. |
| `poll_state.last_error_code` | string (enum) | Why the most recent run failed, as a stable code to branch on. TRIGGER_POLL_ERROR_CODE_UNSPECIFIED when the most recent run succeeded or no run has happened yet. CREDENTIAL_REVOKED and INVALID_CONFIGURATION need a change from you before polling can succeed; every other code is retried automatically. One of: `RATE_LIMITED`, `CREDENTIAL_REVOKED`, `INVALID_CONFIGURATION`, `PROVIDER_ERROR`, `DELIVERY_FAILED`, `TIMED_OUT`, `POLL_FAILED`. |
| `poll_state.last_run_at` | string | When Scalekit last polled the provider for this subscription, whether or not the run succeeded. Absent until the first run. A value far older than the effective interval means the feed has stopped running. |
| `poll_state.watermark_updated_at` | string | When the feed's position last moved forward — the last time a run found and delivered newer events. A run that finds nothing new leaves it unchanged. |
| `provider` | string | The provider the trigger belongs to. |
| `requested_interval_seconds` | integer | The polling interval, in seconds, the subscription asked for, or the trigger's default when it did not set one. Returned next to effective_interval_seconds so a subscription running slower than requested is visible. Zero for a webhook trigger. |
| `status` | string (enum) | Whether the subscription is delivering. ACTIVE — delivering. THROTTLED — delivering, but polling slower than requested; compare requested_interval_seconds with effective_interval_seconds. PAUSED — stopped by you. PAUSED_CREDENTIAL — stopped because the account's credential no longer works. One of: `ACTIVE`, `THROTTLED`, `PAUSED`, `PAUSED_CREDENTIAL`. |
| `trigger_name` | string | Name of the subscribed trigger, as returned by [List available triggers](https://docs.scalekit.com/agentkit/reference/triggers/list-available-triggers/). |

**Errors**

- `400`: Invalid request - the message names what to change. Returned when an ID is malformed; config holds an unknown key, misses a required key, or holds a value longer than 512 characters; interval_seconds is below the trigger's minimum, or is set for a webhook trigger; connected_account_id is missing for an account-scoped trigger, or present for a connection-scoped one; the trigger polls and is connection-scoped, which is not supported; or the account cannot receive the trigger because its grant lacks a required scope or the bot install the trigger needs, it was authorized under a different app, the trigger needs your own provider app, its provider workspace or user is not yet confirmed, or it holds no usable credential
- `404`: Not found - the connection or connected account does not exist in this environment, the connected account belongs to a different connection, the connection's provider publishes no trigger with this name, or Triggers isn't turned on for your environment
- `409`: Conflict - this account, or this connection for a connection-scoped trigger, already has a subscription to this trigger with exactly these config values
- `500`: Internal error - the subscription could not be created; nothing was created
- `503`: Service unavailable - polling for the new subscription could not be scheduled, so the subscription was not created; retry the same request

**Request**

```bash
curl -sS -X POST \
  "$SCALEKIT_ENVIRONMENT_URL/api/v1/connections/conn_133400101014995480/trigger-subscriptions" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "trigger_name": "salesforce_lead_created",
    "connected_account_id": "ca_133400349586228019",
    "config": {
      "lead_source": "Web"
    },
    "interval_seconds": 300
  }'
```

**Response**

```json
{
  "id": "trgs_141020933185734981",
  "connected_account_id": "ca_133400349586228019",
  "connected_account_identifier": "user_123",
  "connection_id": "conn_133400101014995480",
  "provider": "salesforce",
  "trigger_name": "salesforce_lead_created",
  "status": "ACTIVE",
  "delivery_scope": "ACCOUNT",
  "requested_interval_seconds": 300,
  "effective_interval_seconds": 300,
  "config": {
    "lead_source": "Web"
  },
  "created_at": "2026-10-28T09:12:44.120Z",
  "poll_state": {
    "last_error": "",
    "last_error_code": "TRIGGER_POLL_ERROR_CODE_UNSPECIFIED",
    "consecutive_failures": 0,
    "watermark_updated_at": "2026-10-28T09:12:44.120Z"
  },
  "account_health": "READY"
}
```

**Python SDK:** `scalekit_client.actions.triggers.create_subscription`

Subscribe a connected account, or the whole connection, to a trigger. Not retried on `UNAVAILABLE`: a retry could create the subscription twice. Creating the same subscription again raises `ScalekitConflictException`.

```python
scalekit_client.actions.triggers.create_subscription(
    connection_id: str,
    trigger_name: str,
    connected_account_id: str | None = None,
    interval: timedelta | None = None,
    config: Mapping[str, str] | None = None,
) -> TriggerSubscription
```

Example:

```python
from datetime import timedelta

result = scalekit_client.actions.triggers.create_subscription(
    connection_id="conn_133400101014995480",
    trigger_name="salesforce_lead_created",
    connected_account_id="ca_133400349586228019",
    interval=timedelta(minutes=5),
    config={"lead_source": "Web"},
)
```

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `connection_id` | `str` | Yes | The connection (`conn_...`). |
| `trigger_name` | `str` | Yes | `AvailableTrigger.name`. |
| `connected_account_id` | `str \| None` | No | The account (`ca_...`). Leave out only for a connection-scoped trigger. |
| `interval` | `timedelta \| None` | No | Poll interval for a poll trigger, in whole seconds; at least the trigger's `min_interval`. Leave out for the default and for webhook triggers. |
| `config` | `Mapping[str, str] \| None` | No | Values for the trigger's `config_fields`. |

Returns `TriggerSubscription`: The new subscription.

## Subscribe several accounts to a trigger (beta)

`POST /api/v1/connections/{connection_id}/trigger-subscriptions:batchCreate`

This endpoint is in beta. Its request and response may change.

Subscribes up to 100 connected accounts to one trigger with the same filters, and returns a result for each account. An account that can't subscribe doesn't stop the others: its result is `REFUSED`, `NOT_FOUND` or `FAILED`, with a `reason`. Retrying the request is safe; an account subscribed earlier comes back as `ALREADY_SUBSCRIBED`. For a per-connection trigger, send an empty `connected_account_ids`.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `connection_id` | string | Yes | ID of the connection, in this environment. Up to 64 characters. |

**Request body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `trigger_name` | string | Yes | Name of the trigger to subscribe to, as returned in the name field of [List available triggers](https://docs.scalekit.com/agentkit/reference/triggers/list-available-triggers/). Up to 128 characters. |
| `config` | object | No | Filter values applied to every account in the batch, keyed by the trigger's config_fields keys. Each account gets its own subscription with these values, so subscribing the same accounts again with different values adds subscriptions rather than replacing these. Invalid values refuse the whole request. |
| `connected_account_ids` | array of string | No | Connected accounts to subscribe, at most 100 — one page of the account picker — with no repeats. Which to send depends on the trigger's delivery_scope, from [List available triggers](https://docs.scalekit.com/agentkit/reference/triggers/list-available-triggers/). For an ACCOUNT-scoped trigger, list at least one account; an empty list refuses the whole request with a 400. For a CONNECTION-scoped trigger, send an empty list: the trigger is subscribed once, on the connection itself, and results holds exactly one result, with an empty connected_account_id. Naming accounts for a connection-scoped trigger refuses each of them (REFUSED), since that trigger is not subscribed per account. |
| `interval_seconds` | integer | No | Polling interval in seconds, for a polled trigger only, applied to every account in the batch. Omit it to use the trigger's default_interval_seconds. A value below the trigger's min_interval_seconds, or any value for a webhook trigger, refuses the whole request. A large batch of polled subscriptions shares the environment's polling budget, so the subscriptions can run slower than requested; effective_interval_seconds on each subscription shows the interval it runs at. |

**Response (200)**

| Name | Type | Description |
| --- | --- | --- |
| `created_count` | integer | How many subscriptions this call created. Excludes accounts that were already subscribed. |
| `results` | array of object | One result per requested account, in the order the accounts were sent, so a client can pair them up positionally without matching on id. For a connection-scoped trigger subscribed with an empty connected_account_ids, exactly one result, with an empty connected_account_id. |
| `results.connected_account_id` | string | The connected account ID this result is for, as sent in the request. |
| `results.outcome` | string (enum) | What happened to this account. CREATED — a subscription was created. ALREADY_SUBSCRIBED — the account already had a subscription to this trigger with these exact config values; nothing new was created, and list subscriptions to read the existing one. REFUSED — the account cannot receive this trigger; reason says why and what to do. NOT_FOUND — no such account on this connection in this environment. FAILED — the attempt failed for a reason unrelated to the account; nothing was created, and retrying may succeed. One of: `CREATED`, `ALREADY_SUBSCRIBED`, `REFUSED`, `NOT_FOUND`, `FAILED`. |
| `results.reason` | string | Why the account was not subscribed, and for a refusal what to do about it, in one sentence — the same text [Subscribe to a trigger](https://docs.scalekit.com/agentkit/reference/triggers/create-a-trigger-subscription/) returns for the same account. Empty for CREATED. |
| `results.subscription` | object | The created subscription, for CREATED. Absent for every other outcome, including ALREADY_SUBSCRIBED. |
| `results.subscription.account_health` | string (enum) | How the subscribed account stands for this trigger. AWAITING_CONFIRMATION means Scalekit cannot route this trigger's events to the account yet, so the subscription will not fire until the account's user reconnects it. Absent for connection-scoped subscriptions. One of: `READY`, `AWAITING_CONFIRMATION`, `WRONG_APP`, `DISCONNECTED`, `NEEDS_REAUTHENTICATION`. |
| `results.subscription.config` | object | The subscription's filter values, keyed by the trigger's config_fields keys. Keys are returned exactly as stored; send them back without changing their case. |
| `results.subscription.connected_account_id` | string | ID of the subscribed connected account. Empty for a connection-scoped subscription. |
| `results.subscription.connected_account_identifier` | string | The identifier the connected account was created with, your app's ID for the user, such as `user_123`, so you can label the subscription without fetching each account. Empty for a connection-scoped subscription, and empty when the account has no identifier recorded — fall back to connected_account_id rather than showing a blank. |
| `results.subscription.connection_id` | string | ID of the connection the subscription belongs to. |
| `results.subscription.created_at` | string | When the subscription was created. Returned so a list can be ordered or explained by age without a second lookup. |
| `results.subscription.delivery_scope` | string (enum) | Whether this subscription receives one event per connected account or one event for the whole connection. A connection-scoped subscription carries no connected_account_id — the connection is the only recipient. One of: `ACCOUNT`, `CONNECTION`. |
| `results.subscription.effective_interval_seconds` | integer | The polling interval, in seconds, the subscription actually runs at. Larger than requested_interval_seconds when the environment's polling budget is shared across many polled subscriptions; the subscription's status is then THROTTLED. Zero for a webhook trigger. |
| `results.subscription.id` | string | Unique identifier of the trigger subscription. |
| `results.subscription.poll_state` | object | How a POLL subscription's feed is doing: when it last ran, why the last run failed, and whether polling is held off. Absent for a WEBHOOK subscription, which is never polled. |
| `results.subscription.provider` | string | The provider the trigger belongs to. |
| `results.subscription.requested_interval_seconds` | integer | The polling interval, in seconds, the subscription asked for, or the trigger's default when it did not set one. Returned next to effective_interval_seconds so a subscription running slower than requested is visible. Zero for a webhook trigger. |
| `results.subscription.status` | string (enum) | Whether the subscription is delivering. ACTIVE — delivering. THROTTLED — delivering, but polling slower than requested; compare requested_interval_seconds with effective_interval_seconds. PAUSED — stopped by you. PAUSED_CREDENTIAL — stopped because the account's credential no longer works. One of: `ACTIVE`, `THROTTLED`, `PAUSED`, `PAUSED_CREDENTIAL`. |
| `results.subscription.trigger_name` | string | Name of the subscribed trigger, as returned by [List available triggers](https://docs.scalekit.com/agentkit/reference/triggers/list-available-triggers/). |

**Errors**

- `400`: Invalid request, nothing created - connection_id or an entry of connected_account_ids is malformed; config is invalid for this trigger; interval_seconds is below the trigger's minimum, or is set for a webhook trigger; the trigger polls and is connection-scoped, which is not supported; or connected_account_ids is empty for a trigger that is subscribed per connected account. A well-formed ID naming an account that does not exist is not this case: it comes back as a per-account NOT_FOUND result with a 200, so one bad ID in a batch of a hundred does not discard the ninety-nine that would have succeeded
- `404`: Not found - the connection does not exist in this environment, the connection's provider publishes no trigger with this name, or Triggers isn't turned on for your environment
- `500`: Internal error - the connection or the trigger's definition could not be read, so no account was processed; retry the request

**Request**

```bash
curl -sS -X POST \
  "$SCALEKIT_ENVIRONMENT_URL/api/v1/connections/conn_133400101014995480/trigger-subscriptions:batchCreate" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "trigger_name": "salesforce_lead_created",
    "connected_account_ids": [
      "ca_133400349586228019",
      "ca_133400349586231107"
    ],
    "config": {
      "lead_source": "Web"
    },
    "interval_seconds": 300
  }'
```

**Response**

```json
{
  "results": [
    {
      "connected_account_id": "ca_133400349586228019",
      "outcome": "ALREADY_SUBSCRIBED",
      "reason": "this principal already has a subscription to this trigger with these exact settings"
    },
    {
      "connected_account_id": "ca_133400349586231107",
      "outcome": "CREATED",
      "reason": "",
      "subscription": {
        "id": "trgs_141020933185736602",
        "connected_account_id": "ca_133400349586231107",
        "connected_account_identifier": "user_456",
        "connection_id": "conn_133400101014995480",
        "provider": "salesforce",
        "trigger_name": "salesforce_lead_created",
        "status": "ACTIVE",
        "delivery_scope": "ACCOUNT",
        "requested_interval_seconds": 300,
        "effective_interval_seconds": 300,
        "config": {
          "lead_source": "Web"
        },
        "created_at": "2026-10-28T09:20:02.480Z",
        "poll_state": {
          "last_error": "",
          "last_error_code": "TRIGGER_POLL_ERROR_CODE_UNSPECIFIED",
          "consecutive_failures": 0,
          "watermark_updated_at": "2026-10-28T09:20:02.480Z"
        },
        "account_health": "READY"
      }
    }
  ],
  "created_count": 1
}
```

**Python SDK:** `scalekit_client.actions.triggers.batch_create_subscriptions`

Subscribe up to 100 connected accounts to a trigger in one call. Not atomic: each account gets its own outcome in `results` (`CREATED`, `ALREADY_SUBSCRIBED`, `REFUSED`, `NOT_FOUND`, `FAILED`), and those never raise. Safe to call again for the `FAILED` ones. For a connection-scoped trigger, leave `connected_account_ids` out.

```python
scalekit_client.actions.triggers.batch_create_subscriptions(
    connection_id: str,
    trigger_name: str,
    connected_account_ids: Sequence[str] | None = None,
    interval: timedelta | None = None,
    config: Mapping[str, str] | None = None,
) -> BatchCreateTriggerSubscriptionsResponse
```

Example:

```python
from datetime import timedelta

result = scalekit_client.actions.triggers.batch_create_subscriptions(
    connection_id="conn_133400101014995480",
    trigger_name="salesforce_lead_created",
    connected_account_ids=["ca_133400349586228019", "ca_133400349586231107"],
    interval=timedelta(minutes=5),
    config={"lead_source": "Web"},
)
```

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `connection_id` | `str` | Yes | The connection (`conn_...`). |
| `trigger_name` | `str` | Yes | `AvailableTrigger.name`. |
| `connected_account_ids` | `Sequence[str] \| None` | No | The accounts (`ca_...`), unique. |
| `interval` | `timedelta \| None` | No | Poll interval, in whole seconds (see `create_subscription`). |
| `config` | `Mapping[str, str] \| None` | No | Values for the trigger's `config_fields`. |

Returns `BatchCreateTriggerSubscriptionsResponse`: Per-account results and `created_count`.

## Delete a trigger subscription (beta)

`DELETE /api/v1/connections/{connection_id}/trigger-subscriptions/{subscription_id}`

This endpoint is in beta. Its request and response may change.

Deletes a trigger subscription, so the account or connection stops receiving the trigger. A polling subscription stops polling. An event already on its way to your endpoint is still delivered. To receive the trigger again, create a new subscription.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `connection_id` | string | Yes | ID of the connection, in this environment. Up to 64 characters. |
| `subscription_id` | string | Yes | ID of the trigger subscription to delete. It must belong to the connection named in the path. |

**Errors**

- `400`: Invalid request - connection_id is not a valid connection ID, or subscription_id is not a valid trigger subscription ID
- `404`: Not found - no subscription with this ID exists on this connection in this environment, or Triggers isn't turned on for your environment
- `500`: Internal error - the subscription could not be deleted; retry the request

**Request**

```bash
curl -sS -X DELETE \
  "$SCALEKIT_ENVIRONMENT_URL/api/v1/connections/conn_133400101014995480/trigger-subscriptions/trgs_141020933185734981" \
  -H "Authorization: Bearer $TOKEN"
```

**Response**

```json
{}
```

**Python SDK:** `scalekit_client.actions.triggers.delete_subscription`

Delete one trigger subscription. Retried on `UNAVAILABLE`, so a `ScalekitNotFoundException` can also mean an earlier attempt of this same call already deleted it.

```python
scalekit_client.actions.triggers.delete_subscription(
    connection_id: str,
    subscription_id: str,
) -> None
```

Example:

```python
result = scalekit_client.actions.triggers.delete_subscription(
    connection_id="conn_133400101014995480",
    subscription_id="trgs_141020933185734981",
)
```

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `connection_id` | `str` | Yes | The connection (`conn_...`). |
| `subscription_id` | `str` | Yes | The subscription (`trgs_...`). |

Returns `None`.

## Delete several trigger subscriptions (beta)

`POST /api/v1/connections/{connection_id}/trigger-subscriptions:batchDelete`

This endpoint is in beta. Its request and response may change.

Deletes trigger subscriptions on one connection and returns a result for each. Name them in one of two ways: by `subscription_ids`, or by `trigger_name` together with `connected_account_ids`, which deletes every subscription those accounts have to that trigger. A subscription that's already gone, such as one you deleted earlier, comes back as `BATCH_DELETE_OUTCOME_NOT_FOUND`, so retrying the request is safe. Up to 60 requests a minute per environment.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `connection_id` | string | Yes | ID of the connection whose subscriptions to delete. Only subscriptions on this connection are deleted: a subscription ID from another connection comes back as NOT_FOUND. |

**Request body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `connected_account_ids` | array of string | No | With trigger_name, the other selector: connected account IDs (`ca_`) whose subscriptions to trigger_name to delete, at most 100, with no repeats. One account can hold several subscriptions to one trigger, one per filter; each is deleted and gets its own result. Results are grouped by account in the order sent and, within an account, ordered by ascending subscription ID. An account with no matching subscription gets one NOT_FOUND result with an empty subscription_id. A connection-scoped subscription names no account, so it is never matched here: delete it by subscription_ids. |
| `subscription_ids` | array of string | No | Trigger subscription IDs (`trgs_`) to delete, at most 100, with no repeats. One of the two selectors: send either this, or trigger_name with connected_account_ids, never both. Results come back one per ID, in the order sent. Works for account-scoped and connection-scoped subscriptions alike. |
| `trigger_name` | string | No | With connected_account_ids, the other selector: deletes every subscription the listed accounts hold to this trigger on this connection, whatever its filter. Must be sent together with connected_account_ids; sent alone it is refused, so no request can delete every subscription to a trigger. Not checked against the trigger catalogue: a name no subscription uses gives NOT_FOUND for every account. At most 128 characters. |

**Response (200)**

| Name | Type | Description |
| --- | --- | --- |
| `deleted_count` | integer | How many subscriptions this call deleted. Excludes NOT_FOUND results, so a subscription an earlier attempt already deleted is not counted again. |
| `results` | array of object | For subscription_ids, one result per ID in the order sent, so a client can pair them up positionally. For trigger_name with connected_account_ids, the results are grouped by account in the order sent, ordered by ascending subscription ID within an account, with one NOT_FOUND result for an account that held no matching subscription. |
| `results.connected_account_id` | string | For trigger_name with connected_account_ids, the account this result belongs to, as sent. Always empty for a subscription_ids request. |
| `results.outcome` | string (enum) | What happened to this subscription. BATCH_DELETE_OUTCOME_DELETED — deleted by this call. BATCH_DELETE_OUTCOME_NOT_FOUND — no such subscription on this connection in this environment, including one an earlier attempt already deleted; treat it like DELETED when retrying. BATCH_DELETE_OUTCOME_FAILED — the delete could not be completed and may or may not have happened; retrying is safe. BATCH_DELETE_OUTCOME_NOT_ATTEMPTED — the request ran out of time before reaching this subscription; nothing was changed for it, so send it again. One of: `BATCH_DELETE_OUTCOME_DELETED`, `BATCH_DELETE_OUTCOME_NOT_FOUND`, `BATCH_DELETE_OUTCOME_FAILED`, `BATCH_DELETE_OUTCOME_NOT_ATTEMPTED`. |
| `results.reason` | string | One sentence explaining the outcome. Empty for BATCH_DELETE_OUTCOME_DELETED. Never contains internal error detail. The wording can change: match on outcome, not on this text. |
| `results.subscription_id` | string | The subscription this result is about. For subscription_ids, the ID exactly as sent. For trigger_name with connected_account_ids, a subscription found for the account; empty only on the single NOT_FOUND result of an account that held no matching subscription. |

**Errors**

- `400`: Invalid request, nothing deleted - both selectors or neither were sent, or `trigger_name` and `connected_account_ids` were not sent together; a list holds more than 100 IDs or repeats one, including two spellings of the same ID; `connection_id` is not a valid connection ID, an entry of `subscription_ids` is not a trigger subscription ID (`trgs_`), or an entry of `connected_account_ids` is not a connected account ID (`ca_`); or the named accounts hold more than 1000 subscriptions to the trigger, so fewer `connected_account_ids` must be sent per request. A well-formed ID naming a subscription that does not exist, was already deleted, or belongs to another connection is not this case: it comes back as a NOT_FOUND result with a 200, so one stale ID does not discard the rest of the batch.
- `404`: Not found - Triggers isn't turned on for your environment, or `connection_id` can never name a connection. Nothing was deleted. A missing subscription is not this case: it is a NOT_FOUND result with a 200.
- `429`: Too many requests - this environment has sent too many batch delete requests in the last minute. Nothing was deleted; retry after a short wait.
- `500`: Internal error - the subscriptions that `trigger_name` and `connected_account_ids` name could not be read, so nothing was deleted. Only that selector fails this way; retrying is safe.

**Request**

```bash
curl -sS -X POST \
  "$SCALEKIT_ENVIRONMENT_URL/api/v1/connections/conn_133400101014995480/trigger-subscriptions:batchDelete" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "subscription_ids": [
      "trgs_141020933185734981",
      "trgs_141020933185736602"
    ]
  }'
```

**Response**

```json
{
  "results": [
    {
      "subscription_id": "trgs_141020933185734981",
      "connected_account_id": "",
      "outcome": "BATCH_DELETE_OUTCOME_NOT_FOUND",
      "reason": "trigger subscription not found"
    },
    {
      "subscription_id": "trgs_141020933185736602",
      "connected_account_id": "",
      "outcome": "BATCH_DELETE_OUTCOME_DELETED",
      "reason": ""
    }
  ],
  "deleted_count": 1
}
```

**Python SDK:** `scalekit_client.actions.triggers.batch_delete_subscriptions`

Delete many trigger subscriptions in one call. Choose one selector: `subscription_ids` (up to 100), or `trigger_name` with `connected_account_ids` (up to 100). Each subscription gets its own outcome in `results` (`DELETED`, `NOT_FOUND`, `FAILED`, `NOT_ATTEMPTED`), and those never raise. Safe to call again.

```python
scalekit_client.actions.triggers.batch_delete_subscriptions(
    connection_id: str,
    subscription_ids: Sequence[str] | None = None,
    trigger_name: str | None = None,
    connected_account_ids: Sequence[str] | None = None,
) -> BatchDeleteTriggerSubscriptionsResponse
```

Example:

```python
result = scalekit_client.actions.triggers.batch_delete_subscriptions(
    connection_id="conn_133400101014995480",
    subscription_ids=["trgs_141020933185734981", "trgs_141020933185736602"],
)
```

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `connection_id` | `str` | Yes | The connection (`conn_...`). |
| `subscription_ids` | `Sequence[str] \| None` | No | The subscriptions to delete (`trgs_...`). |
| `trigger_name` | `str \| None` | No | With `connected_account_ids`: the trigger to unsubscribe. |
| `connected_account_ids` | `Sequence[str] \| None` | No | With `trigger_name`: the accounts (`ca_...`). |

Returns `BatchDeleteTriggerSubscriptionsResponse`: Per-subscription results and `deleted_count`.

## Create a webhook URL (beta)

`POST /api/v1/connections/{connection_id}/trigger-ingress`

This endpoint is in beta. Its request and response may change.

Creates the webhook URL the app sends this connection's events to. Register the URL in your app's settings at the provider, then [set the signing secret](https://docs.scalekit.com/agentkit/reference/triggers/set-a-signing-secret/). Only a connection whose app has a webhook trigger can have one. If the connection already has a URL, it's returned unchanged, so retrying is safe. Keep the URL private.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `connection_id` | string | Yes | ID of the connection, in this environment. Up to 64 characters. |

**Response (200)**

| Name | Type | Description |
| --- | --- | --- |
| `connection_id` | string | ID of the connection this webhook URL belongs to. |
| `id` | string | Unique identifier of the connection's webhook endpoint. |
| `masked_signing_secret` | string | The stored secret with all but its last four characters replaced, or every character replaced for a secret of 8 characters or fewer. Empty when none is configured. Enough to compare against the secret the provider's app configuration shows, and never enough to sign with. |
| `previous_signing_secret_expires_at` | string | When the previous secret stops being accepted even if the rotation is never confirmed: 24 hours after the rotation started. Absent when no rotation is in progress. |
| `rotation_in_progress` | boolean | Whether the previous signing secret is still accepted alongside the current one. True from the moment a different secret replaces a stored one until the rotation is confirmed, or until previous_signing_secret_expires_at passes. |
| `signing_secret_configured` | boolean | Whether a signing secret is stored. While false, every event sent to the URL is answered with 503 so the provider retries it, and the provider's own URL verification fails. |
| `signing_secret_rotation_id` | string | Identifies the latest signing-secret rotation; it increases by one each time a rotation starts. Zero when none has ever started. Pass this value to [Confirm a signing secret rotation](https://docs.scalekit.com/agentkit/reference/triggers/confirm-a-signing-secret-rotation/) to confirm this specific rotation. A value naming a rotation that a newer one has since replaced is refused rather than confirming the wrong rotation. |
| `url` | string | The URL to paste into the provider's app configuration as the event destination. It is built on your environment's domain, using its custom domain when one is set, and ends in a random token unique to this connection, so treat it as confidential. Returned whether or not a signing secret has been set. The URL accepts request bodies up to 1 MB and answers a larger one with 413. Until a signing secret is set it answers events with 503, so the provider retries them; an event that fails signature verification is answered with 400. |

**Errors**

- `400`: Invalid request, nothing created - connection_id is not a valid connection ID; the connection's provider publishes no webhook trigger, so it has no use for a webhook URL (error_code TRIGGER_INGRESS_NOT_APPLICABLE); or the provider's webhook trigger definitions are temporarily unavailable (error_code TRIGGER_INGRESS_DEFINITIONS_UNAVAILABLE)
- `404`: Not found - the connection does not exist in this environment, or Triggers isn't turned on for your environment
- `500`: Internal error - the webhook URL could not be created or read back; retry the request

**Request**

```bash
curl -sS -X POST \
  "$SCALEKIT_ENVIRONMENT_URL/api/v1/connections/conn_133400101014997214/trigger-ingress" \
  -H "Authorization: Bearer $TOKEN"
```

**Response**

```json
{
  "id": "tie_149528599802678310",
  "connection_id": "conn_133400101014997214",
  "url": "https://your-env.scalekit.dev/triggers/v1/q7Rk2mVx9TbL4nWc8YpZ3s",
  "signing_secret_configured": false,
  "masked_signing_secret": "",
  "rotation_in_progress": false,
  "signing_secret_rotation_id": "0"
}
```

**Python SDK:** `scalekit_client.actions.triggers.create_ingress_endpoint`

Create a connection's webhook URL, or return the existing one.

```python
scalekit_client.actions.triggers.create_ingress_endpoint(
    connection_id: str,
) -> TriggerIngressEndpoint
```

Example:

```python
result = scalekit_client.actions.triggers.create_ingress_endpoint(
    connection_id="conn_133400101014997214",
)
```

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `connection_id` | `str` | Yes | The connection (`conn_...`). |

Returns `TriggerIngressEndpoint`: The URL to paste into the provider's app configuration.

## Get a webhook URL (beta)

`GET /api/v1/connections/{connection_id}/trigger-ingress`

This endpoint is in beta. Its request and response may change.

Gets the connection's webhook URL, whether a signing secret is set, the secret's last four characters, and whether a secret rotation is in progress. The secret itself is never returned.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `connection_id` | string | Yes | ID of the connection, in this environment. Up to 64 characters. |

**Response (200)**

| Name | Type | Description |
| --- | --- | --- |
| `connection_id` | string | ID of the connection this webhook URL belongs to. |
| `id` | string | Unique identifier of the connection's webhook endpoint. |
| `masked_signing_secret` | string | The stored secret with all but its last four characters replaced, or every character replaced for a secret of 8 characters or fewer. Empty when none is configured. Enough to compare against the secret the provider's app configuration shows, and never enough to sign with. |
| `previous_signing_secret_expires_at` | string | When the previous secret stops being accepted even if the rotation is never confirmed: 24 hours after the rotation started. Absent when no rotation is in progress. |
| `rotation_in_progress` | boolean | Whether the previous signing secret is still accepted alongside the current one. True from the moment a different secret replaces a stored one until the rotation is confirmed, or until previous_signing_secret_expires_at passes. |
| `signing_secret_configured` | boolean | Whether a signing secret is stored. While false, every event sent to the URL is answered with 503 so the provider retries it, and the provider's own URL verification fails. |
| `signing_secret_rotation_id` | string | Identifies the latest signing-secret rotation; it increases by one each time a rotation starts. Zero when none has ever started. Pass this value to [Confirm a signing secret rotation](https://docs.scalekit.com/agentkit/reference/triggers/confirm-a-signing-secret-rotation/) to confirm this specific rotation. A value naming a rotation that a newer one has since replaced is refused rather than confirming the wrong rotation. |
| `url` | string | The URL to paste into the provider's app configuration as the event destination. It is built on your environment's domain, using its custom domain when one is set, and ends in a random token unique to this connection, so treat it as confidential. Returned whether or not a signing secret has been set. The URL accepts request bodies up to 1 MB and answers a larger one with 413. Until a signing secret is set it answers events with 503, so the provider retries them; an event that fails signature verification is answered with 400. |

**Errors**

- `400`: Invalid request - connection_id is not a valid connection ID
- `404`: Not found - no webhook URL has been created for this connection yet (error_code TRIGGER_INGRESS_ENDPOINT_NOT_CREATED; call [Create a webhook URL](https://docs.scalekit.com/agentkit/reference/triggers/create-a-webhook-url/)), the connection does not exist in this environment (error_code RESOURCE_NOT_FOUND), or Triggers isn't turned on for your environment (error_code RESOURCE_NOT_FOUND)
- `500`: Internal error - the webhook URL or its signing-secret state could not be read; retry the request

**Request**

```bash
curl -sS -X GET \
  "$SCALEKIT_ENVIRONMENT_URL/api/v1/connections/conn_133400101014997214/trigger-ingress" \
  -H "Authorization: Bearer $TOKEN"
```

**Response**

```json
{
  "id": "tie_149528599802678310",
  "connection_id": "conn_133400101014997214",
  "url": "https://your-env.scalekit.dev/triggers/v1/q7Rk2mVx9TbL4nWc8YpZ3s",
  "signing_secret_configured": true,
  "masked_signing_secret": "****************************1bef",
  "rotation_in_progress": false,
  "signing_secret_rotation_id": "0"
}
```

**Python SDK:** `scalekit_client.actions.triggers.get_ingress_endpoint`

Read the webhook URL a provider sends a connection's events to.

```python
scalekit_client.actions.triggers.get_ingress_endpoint(
    connection_id: str,
) -> TriggerIngressEndpoint
```

Example:

```python
result = scalekit_client.actions.triggers.get_ingress_endpoint(
    connection_id="conn_133400101014997214",
)
```

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `connection_id` | `str` | Yes | The connection (`conn_...`). |

Returns `TriggerIngressEndpoint`: The URL and the state of its signing secret (never the secret itself).

## Set a signing secret (beta)

`PUT /api/v1/connections/{connection_id}/trigger-ingress/signing-secret`

This endpoint is in beta. Its request and response may change.

Saves the secret the app signs its webhook events with, copied from your app's settings at the provider, so Scalekit can verify each event. Until a secret is set, the webhook URL doesn't accept events. If a secret is already set, a new one starts a rotation: both are accepted until you [confirm the rotation](https://docs.scalekit.com/agentkit/reference/triggers/confirm-a-signing-secret-rotation/), or for at most 24 hours. Read the secret from your server's secrets, never from code. For an app that sends no signature of its own, [generate a secret](https://docs.scalekit.com/agentkit/reference/triggers/generate-a-signing-secret/) instead.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `connection_id` | string | Yes | ID of the connection, in this environment. Up to 64 characters. |

**Request body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `signing_secret` | string | Yes | The secret the provider signs its events with, copied from the provider's app configuration. For Slack this is the Signing Secret under Basic Information, not an API token. Between 8 and 4096 characters. For Gong, the RSA public key shown by 'Show public key'. Never returned by any endpoint and never written to a log. |

**Response (200)**

| Name | Type | Description |
| --- | --- | --- |
| `connection_id` | string | ID of the connection this webhook URL belongs to. |
| `id` | string | Unique identifier of the connection's webhook endpoint. |
| `masked_signing_secret` | string | The stored secret with all but its last four characters replaced, or every character replaced for a secret of 8 characters or fewer. Empty when none is configured. Enough to compare against the secret the provider's app configuration shows, and never enough to sign with. |
| `previous_signing_secret_expires_at` | string | When the previous secret stops being accepted even if the rotation is never confirmed: 24 hours after the rotation started. Absent when no rotation is in progress. |
| `rotation_in_progress` | boolean | Whether the previous signing secret is still accepted alongside the current one. True from the moment a different secret replaces a stored one until the rotation is confirmed, or until previous_signing_secret_expires_at passes. |
| `signing_secret_configured` | boolean | Whether a signing secret is stored. While false, every event sent to the URL is answered with 503 so the provider retries it, and the provider's own URL verification fails. |
| `signing_secret_rotation_id` | string | Identifies the latest signing-secret rotation; it increases by one each time a rotation starts. Zero when none has ever started. Pass this value to [Confirm a signing secret rotation](https://docs.scalekit.com/agentkit/reference/triggers/confirm-a-signing-secret-rotation/) to confirm this specific rotation. A value naming a rotation that a newer one has since replaced is refused rather than confirming the wrong rotation. |
| `url` | string | The URL to paste into the provider's app configuration as the event destination. It is built on your environment's domain, using its custom domain when one is set, and ends in a random token unique to this connection, so treat it as confidential. Returned whether or not a signing secret has been set. The URL accepts request bodies up to 1 MB and answers a larger one with 413. Until a signing secret is set it answers events with 503, so the provider retries them; an event that fails signature verification is answered with 400. |

**Errors**

- `400`: Invalid request, nothing written - connection_id is not a valid connection ID; signing_secret is shorter than 8 characters once leading and trailing whitespace is removed, or longer than 4096 characters; the connection's provider sends no signature of its own, so its secret is generated by Scalekit rather than set (error_code TRIGGER_SIGNING_SECRET_GENERATED_ONLY; call [Generate a signing secret](https://docs.scalekit.com/agentkit/reference/triggers/generate-a-signing-secret/) instead); or the provider's webhook trigger definitions are temporarily unavailable (error_code TRIGGER_INGRESS_DEFINITIONS_UNAVAILABLE)
- `404`: Not found - no webhook URL has been created for this connection yet (error_code TRIGGER_INGRESS_ENDPOINT_NOT_CREATED; call [Create a webhook URL](https://docs.scalekit.com/agentkit/reference/triggers/create-a-webhook-url/) first), the connection does not exist in this environment (error_code RESOURCE_NOT_FOUND), or Triggers isn't turned on for your environment (error_code RESOURCE_NOT_FOUND)
- `409`: Conflict - a rotation is already in progress. Confirm it with [Confirm a signing secret rotation](https://docs.scalekit.com/agentkit/reference/triggers/confirm-a-signing-secret-rotation/), or wait for the previous secret's expiry, before setting another secret
- `500`: Internal error - the signing secret could not be stored; retry the request

**Request**

```bash
curl -sS -X PUT \
  "$SCALEKIT_ENVIRONMENT_URL/api/v1/connections/conn_133400101014997214/trigger-ingress/signing-secret" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "signing_secret": "<Signing Secret from Basic Information in your Slack app>"
  }'
```

**Response**

```json
{
  "id": "tie_149528599802678310",
  "connection_id": "conn_133400101014997214",
  "url": "https://your-env.scalekit.dev/triggers/v1/q7Rk2mVx9TbL4nWc8YpZ3s",
  "signing_secret_configured": true,
  "masked_signing_secret": "****************************a4f9",
  "rotation_in_progress": true,
  "signing_secret_rotation_id": "1",
  "previous_signing_secret_expires_at": "2026-10-29T09:30:00Z"
}
```

**Python SDK:** `scalekit_client.actions.triggers.set_signing_secret`

Store the signing secret the provider signs its events with. The first call stores it. A call with a different secret starts a rotation: both secrets are accepted until `confirm_signing_secret_rotation` or for 24 hours. Not retried on `UNAVAILABLE`, because a retry could advance the rotation.

```python
scalekit_client.actions.triggers.set_signing_secret(
    connection_id: str,
    signing_secret: str,
) -> TriggerIngressEndpoint
```

Example:

```python
import os

result = scalekit_client.actions.triggers.set_signing_secret(
    connection_id="conn_133400101014997214",
    signing_secret=os.environ["SLACK_SIGNING_SECRET"],
)
```

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `connection_id` | `str` | Yes | The connection (`conn_...`). |
| `signing_secret` | `str` | Yes | The secret from the provider's app configuration (8-4096 characters; surrounding whitespace is trimmed by the server). |

Returns `TriggerIngressEndpoint`: The endpoint, with `masked_signing_secret` and rotation state.

## Confirm a signing secret rotation (beta)

`POST /api/v1/connections/{connection_id}/trigger-ingress/signing-secret/confirm`

This endpoint is in beta. Its request and response may change.

Retires the previous signing secret, so only the new one is accepted. Call it once the app signs with the new secret. Pass `signing_secret_rotation_id` from the latest set or get response, so you confirm the rotation you checked. With no rotation in progress, the call succeeds and changes nothing.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `connection_id` | string | Yes | ID of the connection, in this environment. Up to 64 characters. |

**Request body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `signing_secret_rotation_id` | string | No | The rotation to confirm: the signing_secret_rotation_id from the latest [Set a signing secret](https://docs.scalekit.com/agentkit/reference/triggers/set-a-signing-secret/) or [Get a webhook URL](https://docs.scalekit.com/agentkit/reference/triggers/get-a-webhook-url/) response. Required while a rotation is in progress; a missing or non-matching value is then refused with 400, rather than confirming a rotation you did not look at. Ignored when no rotation is in progress. |

**Response (200)**

| Name | Type | Description |
| --- | --- | --- |
| `connection_id` | string | ID of the connection this webhook URL belongs to. |
| `id` | string | Unique identifier of the connection's webhook endpoint. |
| `masked_signing_secret` | string | The stored secret with all but its last four characters replaced, or every character replaced for a secret of 8 characters or fewer. Empty when none is configured. Enough to compare against the secret the provider's app configuration shows, and never enough to sign with. |
| `previous_signing_secret_expires_at` | string | When the previous secret stops being accepted even if the rotation is never confirmed: 24 hours after the rotation started. Absent when no rotation is in progress. |
| `rotation_in_progress` | boolean | Whether the previous signing secret is still accepted alongside the current one. True from the moment a different secret replaces a stored one until the rotation is confirmed, or until previous_signing_secret_expires_at passes. |
| `signing_secret_configured` | boolean | Whether a signing secret is stored. While false, every event sent to the URL is answered with 503 so the provider retries it, and the provider's own URL verification fails. |
| `signing_secret_rotation_id` | string | Identifies the latest signing-secret rotation; it increases by one each time a rotation starts. Zero when none has ever started. Pass this value to [Confirm a signing secret rotation](https://docs.scalekit.com/agentkit/reference/triggers/confirm-a-signing-secret-rotation/) to confirm this specific rotation. A value naming a rotation that a newer one has since replaced is refused rather than confirming the wrong rotation. |
| `url` | string | The URL to paste into the provider's app configuration as the event destination. It is built on your environment's domain, using its custom domain when one is set, and ends in a random token unique to this connection, so treat it as confidential. Returned whether or not a signing secret has been set. The URL accepts request bodies up to 1 MB and answers a larger one with 413. Until a signing secret is set it answers events with 503, so the provider retries them; an event that fails signature verification is answered with 400. |

**Errors**

- `400`: Invalid request - connection_id is not a valid connection ID, or a rotation is in progress and signing_secret_rotation_id is missing or does not match it, for example because a newer rotation has started. The message names the current rotation ID; read the endpoint again with [Get a webhook URL](https://docs.scalekit.com/agentkit/reference/triggers/get-a-webhook-url/) before confirming
- `404`: Not found - no webhook URL has been created for this connection yet (error_code TRIGGER_INGRESS_ENDPOINT_NOT_CREATED; call [Create a webhook URL](https://docs.scalekit.com/agentkit/reference/triggers/create-a-webhook-url/) first), the connection does not exist in this environment (error_code RESOURCE_NOT_FOUND), or Triggers isn't turned on for your environment (error_code RESOURCE_NOT_FOUND)
- `500`: Internal error - the rotation could not be confirmed; retry the request

**Request**

```bash
curl -sS -X POST \
  "$SCALEKIT_ENVIRONMENT_URL/api/v1/connections/conn_133400101014997214/trigger-ingress/signing-secret/confirm" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "signing_secret_rotation_id": "1"
  }'
```

**Response**

```json
{
  "id": "tie_149528599802678310",
  "connection_id": "conn_133400101014997214",
  "url": "https://your-env.scalekit.dev/triggers/v1/q7Rk2mVx9TbL4nWc8YpZ3s",
  "signing_secret_configured": true,
  "masked_signing_secret": "****************************a4f9",
  "rotation_in_progress": false,
  "signing_secret_rotation_id": "1"
}
```

**Python SDK:** `scalekit_client.actions.triggers.confirm_signing_secret_rotation`

Finish a signing-secret rotation so only the new secret is accepted.

```python
scalekit_client.actions.triggers.confirm_signing_secret_rotation(
    connection_id: str,
    signing_secret_rotation_id: int | None = None,
) -> TriggerIngressEndpoint
```

Example:

```python
result = scalekit_client.actions.triggers.confirm_signing_secret_rotation(
    connection_id="conn_133400101014997214",
    signing_secret_rotation_id=1,
)
```

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `connection_id` | `str` | Yes | The connection (`conn_...`). |
| `signing_secret_rotation_id` | `int \| None` | No | `TriggerIngressEndpoint.signing_secret_rotation_id` of the rotation to confirm. Required while a rotation is in progress; ignored when none is. |

Returns `TriggerIngressEndpoint`: The endpoint after the confirmation.

## Generate a signing secret (beta)

`POST /api/v1/connections/{connection_id}/trigger-ingress/signing-secret/generate`

This endpoint is in beta. Its request and response may change.

Generates a signing secret for an app that doesn't sign its events (`provides_bearer_secret` is `true` on its triggers), and returns it once. A generated secret starts with `skwh_`. Give it to the app, which sends it with every event: in an `Authorization: Bearer` header, in the `X-Scalekit-Webhook-Secret` header, or as the last segment of `url_with_secret` when the app can't set a header. Store it with your server's secrets now; it's never returned again. If a secret already exists, a new one starts a rotation. An app that signs its own events takes its secret through [Set a signing secret](https://docs.scalekit.com/agentkit/reference/triggers/set-a-signing-secret/) instead.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `connection_id` | string | Yes | The connection whose webhook URL receives the generated secret. Between 1 and 64 characters. |

**Response (200)**

| Name | Type | Description |
| --- | --- | --- |
| `endpoint` | object | The connection's webhook endpoint after the secret was stored, rendered exactly as [Get a webhook URL](https://docs.scalekit.com/agentkit/reference/triggers/get-a-webhook-url/) renders it: the secret appears here only in masked form, and rotation_in_progress is true when this call started a rotation. |
| `endpoint.connection_id` | string | ID of the connection this webhook URL belongs to. |
| `endpoint.id` | string | Unique identifier of the connection's webhook endpoint. |
| `endpoint.masked_signing_secret` | string | The stored secret with all but its last four characters replaced, or every character replaced for a secret of 8 characters or fewer. Empty when none is configured. Enough to compare against the secret the provider's app configuration shows, and never enough to sign with. |
| `endpoint.previous_signing_secret_expires_at` | string | When the previous secret stops being accepted even if the rotation is never confirmed: 24 hours after the rotation started. Absent when no rotation is in progress. |
| `endpoint.rotation_in_progress` | boolean | Whether the previous signing secret is still accepted alongside the current one. True from the moment a different secret replaces a stored one until the rotation is confirmed, or until previous_signing_secret_expires_at passes. |
| `endpoint.signing_secret_configured` | boolean | Whether a signing secret is stored. While false, every event sent to the URL is answered with 503 so the provider retries it, and the provider's own URL verification fails. |
| `endpoint.signing_secret_rotation_id` | string | Identifies the latest signing-secret rotation; it increases by one each time a rotation starts. Zero when none has ever started. Pass this value to [Confirm a signing secret rotation](https://docs.scalekit.com/agentkit/reference/triggers/confirm-a-signing-secret-rotation/) to confirm this specific rotation. A value naming a rotation that a newer one has since replaced is refused rather than confirming the wrong rotation. |
| `endpoint.url` | string | The URL to paste into the provider's app configuration as the event destination. It is built on your environment's domain, using its custom domain when one is set, and ends in a random token unique to this connection, so treat it as confidential. Returned whether or not a signing secret has been set. The URL accepts request bodies up to 1 MB and answers a larger one with 413. Until a signing secret is set it answers events with 503, so the provider retries them; an event that fails signature verification is answered with 400. |
| `signing_secret` | string | The generated secret, in plain text. Returned only in this response and never again by any endpoint, nor written to a log. The provider presents it back on every event in exactly one of three ways: an `Authorization: Bearer <secret>` header, the `X-Scalekit-Webhook-Secret` header, or the final path segment of url_with_secret. |
| `url_with_secret` | string | Endpoint.url with the secret appended as a final path segment. Paste this into the provider's console when the provider cannot send a custom header. It is as sensitive as signing_secret and is likewise returned only in this response. |

**Errors**

- `400`: Invalid request, nothing written - connection_id is not a valid connection ID; the connection's provider signs its own events, so its secret is copied from the provider's app configuration and stored with [Set a signing secret](https://docs.scalekit.com/agentkit/reference/triggers/set-a-signing-secret/) instead (error_code TRIGGER_SIGNING_SECRET_NOT_GENERATABLE); or the provider's webhook trigger definitions are temporarily unavailable (error_code TRIGGER_INGRESS_DEFINITIONS_UNAVAILABLE)
- `404`: Not found - no webhook URL has been created for this connection yet (error_code TRIGGER_INGRESS_ENDPOINT_NOT_CREATED; call [Create a webhook URL](https://docs.scalekit.com/agentkit/reference/triggers/create-a-webhook-url/) first), the connection does not exist in this environment (error_code RESOURCE_NOT_FOUND), or Triggers isn't turned on for your environment (error_code RESOURCE_NOT_FOUND)
- `409`: Conflict - a rotation is already in progress. Confirm it with [Confirm a signing secret rotation](https://docs.scalekit.com/agentkit/reference/triggers/confirm-a-signing-secret-rotation/), or wait for the previous secret's expiry, before generating another secret
- `500`: Internal error - the secret could not be generated or stored, or the endpoint could not be read back; retry the request

**Request**

```bash
curl -sS -X POST \
  "$SCALEKIT_ENVIRONMENT_URL/api/v1/connections/conn_133400101014998936/trigger-ingress/signing-secret/generate" \
  -H "Authorization: Bearer $TOKEN"
```

**Response**

```json
{
  "signing_secret": "skwh_Xq3vR8nT1bLm6KpZ2wYc9DfH4sJg7AeU0oNi5tGr1Bk",
  "url_with_secret": "https://your-env.scalekit.dev/triggers/v1/h3Lp8wQz1nRt6vYc4KmXb9/skwh_Xq3vR8nT1bLm6KpZ2wYc9DfH4sJg7AeU0oNi5tGr1Bk",
  "endpoint": {
    "id": "tie_149528599802681734",
    "connection_id": "conn_133400101014998936",
    "url": "https://your-env.scalekit.dev/triggers/v1/h3Lp8wQz1nRt6vYc4KmXb9",
    "signing_secret_configured": true,
    "masked_signing_secret": "********************************************r1Bk",
    "rotation_in_progress": false,
    "signing_secret_rotation_id": "0"
  }
}
```

**Python SDK:** `scalekit_client.actions.triggers.generate_signing_secret`

Generate a signing secret, for providers that use one Scalekit creates. The secret is returned in plaintext only in this response; it is left out of `repr()`. Not retried on `UNAVAILABLE`: a retry would replace the secret.

```python
scalekit_client.actions.triggers.generate_signing_secret(
    connection_id: str,
) -> GenerateTriggerSigningSecretResponse
```

Example:

```python
result = scalekit_client.actions.triggers.generate_signing_secret(
    connection_id="conn_133400101014998936",
)
```

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `connection_id` | `str` | Yes | The connection (`conn_...`). |

Returns `GenerateTriggerSigningSecretResponse`: The endpoint, the secret and the URL that includes it.

## List deliveries (beta)

`GET /api/v1/trigger-delivery/endpoints/{endpoint_id}/deliveries`

This endpoint is in beta. Its request and response may change.

Lists the trigger events sent to one of your delivery endpoints, newest first, one entry per event however many attempts it took. Filter by `status`, by `channel` (a connection or subscription ID) and by time with `after` and `before`. Deliveries are kept for 90 days. Pass `next_page_token` as `page_token` to read older ones. The event's payload isn't returned.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `endpoint_id` | string | Yes | ID of your delivery endpoint (`ep_...`), the URL on your server that you added under **Developers** > **Trigger Delivery**. [Look up a delivery](https://docs.scalekit.com/agentkit/reference/triggers/look-up-a-delivery/) returns it as `endpoint_id` on each attempt. |

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `after` | string | No | Only deliveries of events created after this instant. Omit it to read back to the oldest retained delivery. Deliveries are kept for 90 days, so a value earlier than that is moved forward to the start of the retained window. Refused when later than `before`. Read to the second. |
| `before` | string | No | Only deliveries of events created before this instant. Omit it to start from the newest delivery. A value later than the current time is treated as the current time. With `after`, reads a time range; the list stays newest first within it. Read to the second. |
| `channel` | string | No | Narrows the list to deliveries of events from one connection (a `conn_` ID) or one trigger subscription (a `trgs_` ID). The ID must exist in this environment; any other value is refused with 400. At most 64 characters. |
| `page_size` | integer | No | Maximum number of deliveries to return, newest first. Defaults to 20 when omitted or zero; a larger value than 100 is capped at 100 rather than refused. |
| `page_token` | string | No | The next_page_token from a previous response, to read the next page of older deliveries. Omit it to read the newest page. A token is bound to the environment, the endpoint and the filters it was issued with: a token sent with a different endpoint or different filters is refused rather than mixing two result sets. At most 1024 characters. |
| `status` | string (enum) | No | Narrows the list to deliveries in this state. Omit it, or send TRIGGER_DELIVERY_STATUS_UNSPECIFIED, for every state. - TRIGGER_DELIVERY_STATUS_DELIVERED: An attempt succeeded. No further attempt is made. - TRIGGER_DELIVERY_STATUS_PENDING: Accepted for delivery; no attempt has finished yet. - TRIGGER_DELIVERY_STATUS_FAILED: Every scheduled attempt failed. No further attempt is made unless one is started by hand. - TRIGGER_DELIVERY_STATUS_RETRYING: An attempt failed and another is scheduled, at next_attempt_at. One of: `TRIGGER_DELIVERY_STATUS_DELIVERED`, `TRIGGER_DELIVERY_STATUS_PENDING`, `TRIGGER_DELIVERY_STATUS_FAILED`, `TRIGGER_DELIVERY_STATUS_RETRYING`. |

**Response (200)**

| Name | Type | Description |
| --- | --- | --- |
| `deliveries` | array of object | One page of the endpoint's deliveries that match the filters, newest first. |
| `deliveries.connection_id` | string | ID of the connection the event came from. Present even after the connection is deleted. Empty when the event carries none. |
| `deliveries.connection_key_id` | string | Name of the connection the event came from, the value other AgentKit endpoints take as `connector`. Empty when the connection no longer exists. |
| `deliveries.connection_provider` | string | Provider of the connection the event came from. Empty when the connection no longer exists. |
| `deliveries.endpoint_id` | string | ID of the endpoint the event was delivered to. |
| `deliveries.event_id` | string | Event ID of the event: the payload's `dedupe_key` and `correlation_id`. Empty when the event was sent without one. |
| `deliveries.message_timestamp` | string | When the event was accepted for delivery. |
| `deliveries.msg_id` | string | Message ID of the event, sent to the endpoint as the `webhook-id` header. |
| `deliveries.next_attempt_at` | string | When the next attempt is scheduled. Present only while status is TRIGGER_DELIVERY_STATUS_RETRYING. |
| `deliveries.status` | string (enum) | The delivery's state across all its attempts. One of: `TRIGGER_DELIVERY_STATUS_DELIVERED`, `TRIGGER_DELIVERY_STATUS_PENDING`, `TRIGGER_DELIVERY_STATUS_FAILED`, `TRIGGER_DELIVERY_STATUS_RETRYING`. |
| `deliveries.subscription_id` | string | ID of the trigger subscription that sent the event. Present even after the subscription is deleted. Empty when the event carries none. |
| `deliveries.trigger_mode` | string (enum) | How the trigger detects events, from the newest version of its definition: WEBHOOK or POLL. The trigger's mode now, not a record of how this one event was detected. TRIGGER_MODE_UNSPECIFIED when the subscription or the definition no longer exists. One of: `WEBHOOK`, `POLL`. |
| `deliveries.trigger_name` | string | Key of the trigger that fired, as the subscription was created with. Empty when the subscription no longer exists. |
| `next_page_token` | string | Pass this as page_token, with the same endpoint_id and filters, to read the next page of older deliveries. Empty when there are no older deliveries. |

**Errors**

- `400`: Invalid request - endpoint_id is missing, is longer than 256 characters, contains characters other than letters, digits, `.`, `_` and `-`, or does not name a trigger delivery endpoint in this environment; status is not a known delivery status; channel is not a connection ID (`conn_`) or trigger subscription ID (`trgs_`) in this environment; `after` or `before` is not a valid timestamp, or `after` is later than `before`; or page_token is malformed or was issued for a different endpoint or set of filters
- `404`: Not found - Triggers isn't turned on for your environment
- `429`: Too many requests - deliveries are being read too often. Retry shortly; the request is not retried on your behalf
- `500`: Internal error - the deliveries could not be read; retry the request

**Request**

```bash
curl -sS -G -X GET \
  "$SCALEKIT_ENVIRONMENT_URL/api/v1/trigger-delivery/endpoints/ep_2qGv8XHk3mT5bJr9LwNc7pZeYfA/deliveries" \
  -H "Authorization: Bearer $TOKEN" \
  --data-urlencode "status=TRIGGER_DELIVERY_STATUS_FAILED"
```

**Response**

```json
{
  "deliveries": [
    {
      "msg_id": "msg_2qGvA4uPf2HjQ7zTn5KrW8dCxLb",
      "event_id": "ca_5F133400349586228019.trgs_5F141020933185734981.00Q5g00000AbCdEFGJ",
      "endpoint_id": "ep_2qGv8XHk3mT5bJr9LwNc7pZeYfA",
      "status": "TRIGGER_DELIVERY_STATUS_FAILED",
      "message_timestamp": "2026-10-28T10:02:44.910Z",
      "subscription_id": "trgs_141020933185734981",
      "trigger_name": "salesforce_lead_created",
      "trigger_mode": "POLL",
      "connection_id": "conn_133400101014995480",
      "connection_key_id": "salesforce",
      "connection_provider": "SALESFORCE"
    }
  ],
  "next_page_token": ""
}
```

**Python SDK:** `scalekit_client.actions.triggers.list_deliveries`

List trigger events sent to one of your endpoints, newest first.

```python
scalekit_client.actions.triggers.list_deliveries(
    endpoint_id: str,
    page_size: int | None = None,
    page_token: str | None = None,
    status: TriggerDeliveryStatus | str | int | None = None,
    channel: str | None = None,
    after: datetime | None = None,
    before: datetime | None = None,
) -> ListTriggerDeliveriesByEndpointResponse
```

Example:

```python
from scalekit.actions import TriggerDeliveryStatus

result = scalekit_client.actions.triggers.list_deliveries(
    endpoint_id="ep_2qGv8XHk3mT5bJr9LwNc7pZeYfA",
    status=TriggerDeliveryStatus.FAILED,
)
```

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `endpoint_id` | `str` | Yes | ID of your delivery endpoint (`ep_...`), the URL on your server that you added under **Developers** > **Trigger Delivery**. [Look up a delivery](https://docs.scalekit.com/agentkit/reference/triggers/look-up-a-delivery/) returns it as `endpoint_id` on each attempt. |
| `page_size` | `int \| None` | No | Messages per page; the server default is 20, capped at 100. |
| `page_token` | `str \| None` | No | `next_page_token` of a previous page (with the same filters). |
| `status` | `TriggerDeliveryStatus \| str \| int \| None` | No | Only messages in this delivery state. |
| `channel` | `str \| None` | No | Only messages for this connection (`conn_...`) or subscription (`trgs_...`). |
| `after` | `datetime \| None` | No | Only messages after this aware instant (at most 90 days back). |
| `before` | `datetime \| None` | No | Only messages before this aware instant. |

Returns `ListTriggerDeliveriesByEndpointResponse`: One page of messages and `next_page_token`.

## List the attempts for a delivery (beta)

`GET /api/v1/trigger-delivery/endpoints/{endpoint_id}/deliveries/{msg_id}/attempts`

This endpoint is in beta. Its request and response may change.

Lists every attempt to deliver one event to one of your endpoints, oldest first: the first attempt, each automatic retry and each resend. Each attempt has its HTTP status code, your endpoint's response body (its first 4096 bytes) and how long it took. Returns at most the newest 50 attempts; `truncated` is `true` when there are more.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `endpoint_id` | string | Yes | ID of your delivery endpoint (`ep_...`), the URL on your server that you added under **Developers** > **Trigger Delivery**. [Look up a delivery](https://docs.scalekit.com/agentkit/reference/triggers/look-up-a-delivery/) returns it as `endpoint_id` on each attempt. |
| `msg_id` | string | Yes | Message ID of the delivery, as returned in msg_id by [List deliveries](https://docs.scalekit.com/agentkit/reference/triggers/list-deliveries/): `msg_` followed by 1 to 64 letters or digits. |

**Response (200)**

| Name | Type | Description |
| --- | --- | --- |
| `attempts` | array of object | The delivery's attempts to this endpoint, oldest first. |
| `attempts.endpoint_id` | string | ID of the endpoint this attempt was made against. |
| `attempts.endpoint_url` | string | URL this attempt was sent to. |
| `attempts.id` | string | ID of this delivery attempt. |
| `attempts.msg_id` | string | ID of the trigger event message this attempt delivered. Every attempt to deliver the same event shares this ID. |
| `attempts.response` | string | Response body the endpoint returned for this attempt. A long body may be truncated. |
| `attempts.response_duration_ms` | integer | How long the endpoint took to respond to this attempt, in milliseconds. |
| `attempts.response_status_code` | integer | HTTP status code the endpoint returned for this attempt. |
| `attempts.response_truncated` | boolean | True when `response` was cut to its first 4096 bytes. Set only by [List the attempts for a delivery](https://docs.scalekit.com/agentkit/reference/triggers/list-attempts-for-a-delivery/) and [Look up a delivery](https://docs.scalekit.com/agentkit/reference/triggers/look-up-a-delivery/), which apply that cap. |
| `attempts.status` | string (enum) | Whether this attempt succeeded, is still in flight, failed, or was canceled. A failed delivery is retried, so FAIL on one attempt does not mean the event was never delivered — check the newest attempt for that message. One of: `SUCCESS`, `PENDING`, `FAIL`, `SENDING`, `CANCELED`. |
| `attempts.timestamp` | string | When this attempt was made. |
| `attempts.trigger_type` | string (enum) | Why this attempt was made: TRIGGER_DELIVERY_ATTEMPT_TRIGGER_TYPE_SCHEDULED for the first attempt or an automatic retry, TRIGGER_DELIVERY_ATTEMPT_TRIGGER_TYPE_MANUAL for a retry someone started by hand. One of: `TRIGGER_DELIVERY_ATTEMPT_TRIGGER_TYPE_SCHEDULED`, `TRIGGER_DELIVERY_ATTEMPT_TRIGGER_TYPE_MANUAL`. |
| `truncated` | boolean | True when the delivery has more than the 50 attempts returned. The newest 50 are returned, so the first attempt is missing when this is true. |

**Errors**

- `400`: Invalid request - endpoint_id is missing, is longer than 256 characters or contains characters other than letters, digits, `.`, `_` and `-`; msg_id is not a message ID (`msg_` followed by 1 to 64 letters or digits); or the endpoint or the message does not exist in this environment
- `404`: Not found - Triggers isn't turned on for your environment
- `429`: Too many requests - deliveries are being read too often. Retry shortly; the request is not retried on your behalf
- `500`: Internal error - the attempts could not be read; retry the request

**Request**

```bash
curl -sS -X GET \
  "$SCALEKIT_ENVIRONMENT_URL/api/v1/trigger-delivery/endpoints/ep_2qGv8XHk3mT5bJr9LwNc7pZeYfA/deliveries/msg_2qGvA1sKd8RtY6nWm3LpX9cBhVe/attempts" \
  -H "Authorization: Bearer $TOKEN"
```

**Response**

```json
{
  "attempts": [
    {
      "id": "atmpt_2qGvA2bNf7KcW4xRm8TjP5sLhYd",
      "msg_id": "msg_2qGvA1sKd8RtY6nWm3LpX9cBhVe",
      "endpoint_id": "ep_2qGv8XHk3mT5bJr9LwNc7pZeYfA",
      "endpoint_url": "https://api.example.com/scalekit/triggers",
      "status": "SUCCESS",
      "response_status_code": 200,
      "response": "{\"ok\":true}",
      "response_truncated": false,
      "response_duration_ms": 84,
      "timestamp": "2026-10-28T09:17:45.020Z",
      "trigger_type": "TRIGGER_DELIVERY_ATTEMPT_TRIGGER_TYPE_SCHEDULED"
    }
  ],
  "truncated": false
}
```

**Python SDK:** `scalekit_client.actions.triggers.list_delivery_attempts_by_message`

List every attempt to deliver one message to one of your endpoints.

```python
scalekit_client.actions.triggers.list_delivery_attempts_by_message(
    endpoint_id: str,
    msg_id: str,
) -> ListTriggerDeliveryAttemptsByMessageResponse
```

Example:

```python
result = scalekit_client.actions.triggers.list_delivery_attempts_by_message(
    endpoint_id="ep_2qGv8XHk3mT5bJr9LwNc7pZeYfA",
    msg_id="msg_2qGvA1sKd8RtY6nWm3LpX9cBhVe",
)
```

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `endpoint_id` | `str` | Yes | ID of your delivery endpoint (`ep_...`), the URL on your server that you added under **Developers** > **Trigger Delivery**. [Look up a delivery](https://docs.scalekit.com/agentkit/reference/triggers/look-up-a-delivery/) returns it as `endpoint_id` on each attempt. |
| `msg_id` | `str` | Yes | The message (`msg_...`). |

Returns `ListTriggerDeliveryAttemptsByMessageResponse`: Up to 50 attempts, oldest first, and `truncated`.

## List recent delivery attempts (beta)

`GET /api/v1/trigger-delivery/endpoints/{endpoint_id}/attempts`

This endpoint is in beta. Its request and response may change.

Lists the most recent attempts to deliver trigger events to one of your endpoints, newest first, up to `page_size` (default 20, at most 100). There's no page token for older attempts. A failed attempt is retried, so check the newest attempt for an event before you decide it wasn't delivered.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `endpoint_id` | string | Yes | ID of your delivery endpoint (`ep_...`), the URL on your server that you added under **Developers** > **Trigger Delivery**. [Look up a delivery](https://docs.scalekit.com/agentkit/reference/triggers/look-up-a-delivery/) returns it as `endpoint_id` on each attempt. |

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `page_size` | integer | No | Maximum number of attempts to return, newest first. Defaults to 20 when omitted or zero; a larger value than 100 is capped at 100 rather than refused. |

**Response (200)**

| Name | Type | Description |
| --- | --- | --- |
| `attempts` | array of object | The endpoint's most recent delivery attempts, newest first. |
| `attempts.endpoint_id` | string | ID of the endpoint this attempt was made against. |
| `attempts.endpoint_url` | string | URL this attempt was sent to. |
| `attempts.id` | string | ID of this delivery attempt. |
| `attempts.msg_id` | string | ID of the trigger event message this attempt delivered. Every attempt to deliver the same event shares this ID. |
| `attempts.response` | string | Response body the endpoint returned for this attempt. A long body may be truncated. |
| `attempts.response_duration_ms` | integer | How long the endpoint took to respond to this attempt, in milliseconds. |
| `attempts.response_status_code` | integer | HTTP status code the endpoint returned for this attempt. |
| `attempts.response_truncated` | boolean | True when `response` was cut to its first 4096 bytes. Set only by [List the attempts for a delivery](https://docs.scalekit.com/agentkit/reference/triggers/list-attempts-for-a-delivery/) and [Look up a delivery](https://docs.scalekit.com/agentkit/reference/triggers/look-up-a-delivery/), which apply that cap. |
| `attempts.status` | string (enum) | Whether this attempt succeeded, is still in flight, failed, or was canceled. A failed delivery is retried, so FAIL on one attempt does not mean the event was never delivered — check the newest attempt for that message. One of: `SUCCESS`, `PENDING`, `FAIL`, `SENDING`, `CANCELED`. |
| `attempts.timestamp` | string | When this attempt was made. |
| `attempts.trigger_type` | string (enum) | Why this attempt was made: TRIGGER_DELIVERY_ATTEMPT_TRIGGER_TYPE_SCHEDULED for the first attempt or an automatic retry, TRIGGER_DELIVERY_ATTEMPT_TRIGGER_TYPE_MANUAL for a retry someone started by hand. One of: `TRIGGER_DELIVERY_ATTEMPT_TRIGGER_TYPE_SCHEDULED`, `TRIGGER_DELIVERY_ATTEMPT_TRIGGER_TYPE_MANUAL`. |

**Errors**

- `400`: Invalid request - endpoint_id is missing, is longer than 256 characters or contains characters other than letters, digits, `.`, `_` and `-`, or does not name a trigger delivery endpoint in this environment
- `404`: Not found - Triggers isn't turned on for your environment
- `500`: Internal error - the delivery attempts could not be read; retry the request

**Request**

```bash
curl -sS -G -X GET \
  "$SCALEKIT_ENVIRONMENT_URL/api/v1/trigger-delivery/endpoints/ep_2qGv8XHk3mT5bJr9LwNc7pZeYfA/attempts" \
  -H "Authorization: Bearer $TOKEN" \
  --data-urlencode "page_size=20"
```

**Response**

```json
{
  "attempts": [
    {
      "id": "atmpt_2qGvA2bNf7KcW4xRm8TjP5sLhYd",
      "msg_id": "msg_2qGvA1sKd8RtY6nWm3LpX9cBhVe",
      "endpoint_id": "ep_2qGv8XHk3mT5bJr9LwNc7pZeYfA",
      "endpoint_url": "https://api.example.com/scalekit/triggers",
      "status": "SUCCESS",
      "response_status_code": 200,
      "response": "{\"ok\":true}",
      "response_truncated": false,
      "response_duration_ms": 84,
      "timestamp": "2026-10-28T09:17:45.020Z",
      "trigger_type": "TRIGGER_DELIVERY_ATTEMPT_TRIGGER_TYPE_SCHEDULED"
    }
  ]
}
```

**Python SDK:** `scalekit_client.actions.triggers.list_delivery_attempts_by_endpoint`

List the most recent attempts to deliver trigger events to one of your endpoints.

```python
scalekit_client.actions.triggers.list_delivery_attempts_by_endpoint(
    endpoint_id: str,
    page_size: int | None = None,
) -> ListTriggerDeliveryAttemptsByEndpointResponse
```

Example:

```python
result = scalekit_client.actions.triggers.list_delivery_attempts_by_endpoint(
    endpoint_id="ep_2qGv8XHk3mT5bJr9LwNc7pZeYfA",
    page_size=20,
)
```

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `endpoint_id` | `str` | Yes | ID of your delivery endpoint (`ep_...`), the URL on your server that you added under **Developers** > **Trigger Delivery**. [Look up a delivery](https://docs.scalekit.com/agentkit/reference/triggers/look-up-a-delivery/) returns it as `endpoint_id` on each attempt. |
| `page_size` | `int \| None` | No | Attempts to return; the server default is 20, capped at 100. |

Returns `ListTriggerDeliveryAttemptsByEndpointResponse`: The attempts (one page; there is no page token).

## Look up a delivery (beta)

`GET /api/v1/trigger-delivery/deliveries/{id}`

This endpoint is in beta. Its request and response may change.

Finds one trigger event by an ID your server received: the message ID from the `webhook-id` header, or the event ID, which is the payload's `dedupe_key`. Returns the event's trigger, subscription and connection, and every attempt to deliver it to each of your endpoints, oldest first. The event's payload isn't returned.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes | The message ID (`msg_...`, the `webhook-id` header) or the event ID (the payload's `dedupe_key`). |

**Response (200)**

| Name | Type | Description |
| --- | --- | --- |
| `attempts` | array of object | The event's delivery attempts to every endpoint in this environment, oldest first. Each names the endpoint_id and endpoint_url it was sent to. |
| `attempts.endpoint_id` | string | ID of the endpoint this attempt was made against. |
| `attempts.endpoint_url` | string | URL this attempt was sent to. |
| `attempts.id` | string | ID of this delivery attempt. |
| `attempts.msg_id` | string | ID of the trigger event message this attempt delivered. Every attempt to deliver the same event shares this ID. |
| `attempts.response` | string | Response body the endpoint returned for this attempt. A long body may be truncated. |
| `attempts.response_duration_ms` | integer | How long the endpoint took to respond to this attempt, in milliseconds. |
| `attempts.response_status_code` | integer | HTTP status code the endpoint returned for this attempt. |
| `attempts.response_truncated` | boolean | True when `response` was cut to its first 4096 bytes. Set only by [List the attempts for a delivery](https://docs.scalekit.com/agentkit/reference/triggers/list-attempts-for-a-delivery/) and [Look up a delivery](https://docs.scalekit.com/agentkit/reference/triggers/look-up-a-delivery/), which apply that cap. |
| `attempts.status` | string (enum) | Whether this attempt succeeded, is still in flight, failed, or was canceled. A failed delivery is retried, so FAIL on one attempt does not mean the event was never delivered — check the newest attempt for that message. One of: `SUCCESS`, `PENDING`, `FAIL`, `SENDING`, `CANCELED`. |
| `attempts.timestamp` | string | When this attempt was made. |
| `attempts.trigger_type` | string (enum) | Why this attempt was made: TRIGGER_DELIVERY_ATTEMPT_TRIGGER_TYPE_SCHEDULED for the first attempt or an automatic retry, TRIGGER_DELIVERY_ATTEMPT_TRIGGER_TYPE_MANUAL for a retry someone started by hand. One of: `TRIGGER_DELIVERY_ATTEMPT_TRIGGER_TYPE_SCHEDULED`, `TRIGGER_DELIVERY_ATTEMPT_TRIGGER_TYPE_MANUAL`. |
| `connection_id` | string | ID of the connection the event came from. Present even after the connection is deleted. Empty when the event carries none. |
| `connection_key_id` | string | Name of the connection the event came from, the value other AgentKit endpoints take as `connector`. Empty when the connection no longer exists. |
| `connection_provider` | string | Provider of the connection the event came from. Empty when the connection no longer exists. |
| `event_id` | string | Event ID of the event: the payload's `dedupe_key` and `correlation_id`. Empty when the event was sent without one. |
| `message_timestamp` | string | When the event was accepted for delivery. |
| `msg_id` | string | Message ID of the event, sent to each endpoint as the `webhook-id` header. |
| `subscription_id` | string | ID of the trigger subscription that sent the event. Present even after the subscription is deleted. Empty when the event carries none. |
| `trigger_mode` | string (enum) | How the trigger detects events, from the newest version of its definition: WEBHOOK or POLL. TRIGGER_MODE_UNSPECIFIED when the subscription or the definition no longer exists. One of: `WEBHOOK`, `POLL`. |
| `trigger_name` | string | Key of the trigger that fired, as the subscription was created with. Empty when the subscription no longer exists. |
| `truncated` | boolean | True when the event has more than the 50 attempts returned. The newest 50 are returned, so the earliest attempts are missing when this is true. |

**Errors**

- `400`: Invalid request - id is empty, longer than 256 characters, contains characters other than letters, digits, `.`, `_` and `-`, or is an attempt ID (`atmpt_`)
- `404`: Not found - no trigger event with this message ID or event ID exists in this environment (error_code NOT_FOUND), or Triggers isn't turned on for your environment (error_code RESOURCE_NOT_FOUND)
- `429`: Too many requests - deliveries are being read too often. Retry shortly; the request is not retried on your behalf
- `500`: Internal error - the delivery could not be looked up; retry the request

**Request**

```bash
curl -sS -X GET \
  "$SCALEKIT_ENVIRONMENT_URL/api/v1/trigger-delivery/deliveries/msg_2qGvA1sKd8RtY6nWm3LpX9cBhVe" \
  -H "Authorization: Bearer $TOKEN"
```

**Response**

```json
{
  "msg_id": "msg_2qGvA1sKd8RtY6nWm3LpX9cBhVe",
  "event_id": "ca_5F133400349586228019.trgs_5F141020933185734981.00Q5g00000AbCdEFGH",
  "message_timestamp": "2026-10-28T09:17:44.950Z",
  "subscription_id": "trgs_141020933185734981",
  "trigger_name": "salesforce_lead_created",
  "trigger_mode": "POLL",
  "connection_id": "conn_133400101014995480",
  "connection_key_id": "salesforce",
  "connection_provider": "SALESFORCE",
  "attempts": [
    {
      "id": "atmpt_2qGvA2bNf7KcW4xRm8TjP5sLhYd",
      "msg_id": "msg_2qGvA1sKd8RtY6nWm3LpX9cBhVe",
      "endpoint_id": "ep_2qGv8XHk3mT5bJr9LwNc7pZeYfA",
      "endpoint_url": "https://api.example.com/scalekit/triggers",
      "status": "SUCCESS",
      "response_status_code": 200,
      "response": "{\"ok\":true}",
      "response_truncated": false,
      "response_duration_ms": 84,
      "timestamp": "2026-10-28T09:17:45.020Z",
      "trigger_type": "TRIGGER_DELIVERY_ATTEMPT_TRIGGER_TYPE_SCHEDULED"
    }
  ],
  "truncated": false
}
```

**Python SDK:** `scalekit_client.actions.triggers.lookup_delivery`

Look up one delivered message by its message ID or by its event ID.

```python
scalekit_client.actions.triggers.lookup_delivery(
    delivery_id: str,
) -> LookupTriggerDeliveryResponse
```

Example:

```python
result = scalekit_client.actions.triggers.lookup_delivery(
    delivery_id="msg_2qGvA1sKd8RtY6nWm3LpX9cBhVe",
)
```

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `delivery_id` | `str` | Yes | A message ID (`msg_...`) or the event ID your endpoint received (`TriggerEvent.dedupe_key`). |

Returns `LookupTriggerDeliveryResponse`: The message, its trigger and up to 50 attempts.

## Look up several deliveries (beta)

`POST /api/v1/trigger-delivery/deliveries:batchLookup`

This endpoint is in beta. Its request and response may change.

Finds up to 25 trigger events by message ID or event ID, in any mix, and returns a result for each, in the order sent. An ID that matches nothing comes back as `BATCH_LOOKUP_OUTCOME_NOT_FOUND`. Attempts here have no response body; [look up one delivery](https://docs.scalekit.com/agentkit/reference/triggers/look-up-a-delivery/) to read it. The call only reads, so retrying is safe. Up to 60 requests a minute per environment.

**Request body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `ids` | array of string | Yes | Message IDs (`msg_…`, sent to the endpoint as the `webhook-id` header) or event IDs (the payload's `dedupe_key`) of the trigger events to find, in any mix: at least 1 and at most 25, with no repeats. Each is 1 to 256 characters: letters, digits, `.`, `_` and `-`. An attempt ID (`atmpt_…`) refuses the whole request. |

**Response (200)**

| Name | Type | Description |
| --- | --- | --- |
| `found_count` | integer | How many of the requested IDs were found. |
| `results` | array of object | One result per requested ID, in the order the IDs were sent, so a client can pair them up positionally. |
| `results.delivery` | object | The event and its delivery attempts to every endpoint, oldest first, as [Look up a delivery](https://docs.scalekit.com/agentkit/reference/triggers/look-up-a-delivery/) returns them, except that no attempt carries a response body: each attempt's `response` is empty and its `response_truncated` is false. `truncated` keeps its meaning: true when the event has more than the 50 attempts returned. Present only for BATCH_LOOKUP_OUTCOME_FOUND. |
| `results.delivery.attempts` | array of object | The event's delivery attempts to every endpoint in this environment, oldest first. Each names the endpoint_id and endpoint_url it was sent to. |
| `results.delivery.connection_id` | string | ID of the connection the event came from. Present even after the connection is deleted. Empty when the event carries none. |
| `results.delivery.connection_key_id` | string | Name of the connection the event came from, the value other AgentKit endpoints take as `connector`. Empty when the connection no longer exists. |
| `results.delivery.connection_provider` | string | Provider of the connection the event came from. Empty when the connection no longer exists. |
| `results.delivery.event_id` | string | Event ID of the event: the payload's `dedupe_key` and `correlation_id`. Empty when the event was sent without one. |
| `results.delivery.message_timestamp` | string | When the event was accepted for delivery. |
| `results.delivery.msg_id` | string | Message ID of the event, sent to each endpoint as the `webhook-id` header. |
| `results.delivery.subscription_id` | string | ID of the trigger subscription that sent the event. Present even after the subscription is deleted. Empty when the event carries none. |
| `results.delivery.trigger_mode` | string (enum) | How the trigger detects events, from the newest version of its definition: WEBHOOK or POLL. TRIGGER_MODE_UNSPECIFIED when the subscription or the definition no longer exists. One of: `WEBHOOK`, `POLL`. |
| `results.delivery.trigger_name` | string | Key of the trigger that fired, as the subscription was created with. Empty when the subscription no longer exists. |
| `results.delivery.truncated` | boolean | True when the event has more than the 50 attempts returned. The newest 50 are returned, so the earliest attempts are missing when this is true. |
| `results.id` | string | The ID this result is about, exactly as sent. |
| `results.outcome` | string (enum) | What happened to this ID. BATCH_LOOKUP_OUTCOME_FOUND — the event was found and delivery holds it. BATCH_LOOKUP_OUTCOME_NOT_FOUND — no trigger event with this message ID or event ID exists in this environment. BATCH_LOOKUP_OUTCOME_RATE_LIMITED — the read was refused for volume; look the ID up again shortly. BATCH_LOOKUP_OUTCOME_FAILED — the lookup could not be completed; retrying may succeed. BATCH_LOOKUP_OUTCOME_NOT_ATTEMPTED — not looked up, because an earlier ID was rate limited or the request ran out of time; send it again. One of: `BATCH_LOOKUP_OUTCOME_FOUND`, `BATCH_LOOKUP_OUTCOME_NOT_FOUND`, `BATCH_LOOKUP_OUTCOME_RATE_LIMITED`, `BATCH_LOOKUP_OUTCOME_FAILED`, `BATCH_LOOKUP_OUTCOME_NOT_ATTEMPTED`. |
| `results.reason` | string | One sentence explaining the outcome. Empty for BATCH_LOOKUP_OUTCOME_FOUND. Never contains internal error detail. The wording can change: match on outcome, not on this text. |

**Errors**

- `400`: Invalid request, nothing looked up - `ids` is empty, holds more than 25 IDs or repeats one; an ID is longer than 256 characters or contains characters other than letters, digits, `.`, `_` and `-`; or an ID is an attempt ID (`atmpt_`), which [Look up a delivery](https://docs.scalekit.com/agentkit/reference/triggers/look-up-a-delivery/) refuses too. A well-formed ID that names no trigger event in this environment is not this case: it comes back as a NOT_FOUND result with a 200
- `404`: Not found - Triggers isn't turned on for your environment. An ID that names no trigger event is not this case: it is a NOT_FOUND result with a 200
- `429`: Too many requests - this environment has sent too many batch lookup requests in the last minute. Nothing was looked up; retry after a short wait. Reads refused for volume partway through a batch are not this case: they are reported per ID as RATE_LIMITED, with a 200
- `500`: Internal error - the environment's trigger deliveries could not be reached, so nothing was looked up; retry the request

**Request**

```bash
curl -sS -X POST \
  "$SCALEKIT_ENVIRONMENT_URL/api/v1/trigger-delivery/deliveries:batchLookup" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "ids": [
      "msg_2qGvA1sKd8RtY6nWm3LpX9cBhVe",
      "msg_2qGvA9zZz0000000000000000"
    ]
  }'
```

**Response**

```json
{
  "results": [
    {
      "id": "msg_2qGvA1sKd8RtY6nWm3LpX9cBhVe",
      "outcome": "BATCH_LOOKUP_OUTCOME_FOUND",
      "reason": "",
      "delivery": {
        "msg_id": "msg_2qGvA1sKd8RtY6nWm3LpX9cBhVe",
        "event_id": "ca_5F133400349586228019.trgs_5F141020933185734981.00Q5g00000AbCdEFGH",
        "message_timestamp": "2026-10-28T09:17:44.950Z",
        "subscription_id": "trgs_141020933185734981",
        "trigger_name": "salesforce_lead_created",
        "trigger_mode": "POLL",
        "connection_id": "conn_133400101014995480",
        "connection_key_id": "salesforce",
        "connection_provider": "SALESFORCE",
        "attempts": [
          {
            "id": "atmpt_2qGvA2bNf7KcW4xRm8TjP5sLhYd",
            "msg_id": "msg_2qGvA1sKd8RtY6nWm3LpX9cBhVe",
            "endpoint_id": "ep_2qGv8XHk3mT5bJr9LwNc7pZeYfA",
            "endpoint_url": "https://api.example.com/scalekit/triggers",
            "status": "SUCCESS",
            "response_status_code": 200,
            "response": "",
            "response_truncated": false,
            "response_duration_ms": 84,
            "timestamp": "2026-10-28T09:17:45.020Z",
            "trigger_type": "TRIGGER_DELIVERY_ATTEMPT_TRIGGER_TYPE_SCHEDULED"
          }
        ],
        "truncated": false
      }
    },
    {
      "id": "msg_2qGvA9zZz0000000000000000",
      "outcome": "BATCH_LOOKUP_OUTCOME_NOT_FOUND",
      "reason": "no trigger delivery with this id exists in this environment"
    }
  ],
  "found_count": 1
}
```

**Python SDK:** `scalekit_client.actions.triggers.batch_lookup_deliveries`

Look up 1 to 25 delivered messages by message ID or event ID. Each ID gets its own outcome in `results` (`FOUND`, `NOT_FOUND`, `RATE_LIMITED`, `FAILED`, `NOT_ATTEMPTED`), and those never raise. Attempts in the results carry no response bodies.

```python
scalekit_client.actions.triggers.batch_lookup_deliveries(
    delivery_ids: Sequence[str],
) -> BatchLookupTriggerDeliveriesResponse
```

Example:

```python
result = scalekit_client.actions.triggers.batch_lookup_deliveries(
    delivery_ids=["msg_2qGvA1sKd8RtY6nWm3LpX9cBhVe", "msg_2qGvA9zZz0000000000000000"],
)
```

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `delivery_ids` | `Sequence[str]` | Yes | Message IDs (`msg_...`) or event IDs, unique. |

Returns `BatchLookupTriggerDeliveriesResponse`: Per-ID results and `found_count`.

## Resend deliveries (beta)

`POST /api/v1/trigger-delivery/deliveries:batchResend`

This endpoint is in beta. Its request and response may change.

Queues one more attempt to deliver each event to the endpoint you name, for up to 50 events. `BATCH_RESEND_OUTCOME_QUEUED` means the attempt was queued, not that it succeeded; check it with [List the attempts for a delivery](https://docs.scalekit.com/agentkit/reference/triggers/list-attempts-for-a-delivery/). Each call queues another attempt, so after a partial failure resend only the events that didn't come back `QUEUED`. Up to 60 requests a minute per environment.

**Request body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `targets` | array of object | Yes | The deliveries to retry, each a message ID and an endpoint ID: at least 1 and at most 50. A msg_id and endpoint_id pair may appear only once; a repeated pair refuses the whole request rather than queueing two attempts. The same message may appear with different endpoints, and the same endpoint with different messages. |
| `targets.endpoint_id` | string | Yes | ID of your delivery endpoint (`ep_...`), the URL on your server that you added under **Developers** > **Trigger Delivery**. [Look up a delivery](https://docs.scalekit.com/agentkit/reference/triggers/look-up-a-delivery/) returns it as `endpoint_id` on each attempt. |
| `targets.msg_id` | string | Yes | Message ID of the trigger event to deliver again, as returned in msg_id by [List deliveries](https://docs.scalekit.com/agentkit/reference/triggers/list-deliveries/): `msg_` followed by 1 to 64 letters or digits. |

**Response (200)**

| Name | Type | Description |
| --- | --- | --- |
| `queued_count` | integer | How many new delivery attempts this call queued. |
| `results` | array of object | One result per target, in the order the targets were sent, so a client can pair them up positionally. |
| `results.endpoint_id` | string | Endpoint ID of the target, exactly as sent. |
| `results.msg_id` | string | Message ID of the target, exactly as sent. |
| `results.outcome` | string (enum) | What happened to this target. BATCH_RESEND_OUTCOME_QUEUED — a new manual attempt was queued; it has not been delivered yet. BATCH_RESEND_OUTCOME_NOT_FOUND — the message or the endpoint does not exist in this environment. BATCH_RESEND_OUTCOME_RATE_LIMITED — refused for volume; send this target again shortly. BATCH_RESEND_OUTCOME_FAILED — no new attempt could be confirmed as queued; one whose request timed out may still have been, so a retry can queue a second attempt. A target whose event payload is no longer stored is also FAILED, with a reason saying it cannot be resent; retrying it will not help. BATCH_RESEND_OUTCOME_NOT_ATTEMPTED — not sent, because an earlier target was rate limited or the request ran out of time; nothing was queued for it. One of: `BATCH_RESEND_OUTCOME_QUEUED`, `BATCH_RESEND_OUTCOME_NOT_FOUND`, `BATCH_RESEND_OUTCOME_RATE_LIMITED`, `BATCH_RESEND_OUTCOME_FAILED`, `BATCH_RESEND_OUTCOME_NOT_ATTEMPTED`. |
| `results.reason` | string | One sentence explaining the outcome. Empty for BATCH_RESEND_OUTCOME_QUEUED. Never contains internal error detail. The wording can change: match on outcome, not on this text. |

**Errors**

- `400`: Invalid request, nothing queued - `targets` is empty or holds more than 50; a `msg_id` is not a message ID (`msg_` followed by 1 to 64 letters or digits) or an `endpoint_id` is not an endpoint ID (`ep_` followed by 1 to 64 letters or digits); or the same `msg_id` and `endpoint_id` pair appears twice, since sending it twice would queue two attempts. A well-formed message or endpoint ID that does not exist in this environment is not this case: it comes back as a NOT_FOUND result with a 200
- `404`: Not found - Triggers isn't turned on for your environment. Nothing was queued. A message or endpoint that does not exist is not this case: it is a NOT_FOUND result with a 200
- `429`: Too many requests - this environment has sent too many batch resend requests in the last minute. Nothing was queued; retry after a short wait. A rate limit reached partway through a batch is not this case: it is reported per target as RATE_LIMITED, with a 200
- `500`: Internal error - the environment's trigger deliveries could not be reached, so nothing was queued; retry the request

**Request**

```bash
curl -sS -X POST \
  "$SCALEKIT_ENVIRONMENT_URL/api/v1/trigger-delivery/deliveries:batchResend" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "targets": [
      {
        "msg_id": "msg_2qGvA4uPf2HjQ7zTn5KrW8dCxLb",
        "endpoint_id": "ep_2qGv8XHk3mT5bJr9LwNc7pZeYfA"
      }
    ]
  }'
```

**Response**

```json
{
  "results": [
    {
      "msg_id": "msg_2qGvA4uPf2HjQ7zTn5KrW8dCxLb",
      "endpoint_id": "ep_2qGv8XHk3mT5bJr9LwNc7pZeYfA",
      "outcome": "BATCH_RESEND_OUTCOME_QUEUED",
      "reason": ""
    }
  ],
  "queued_count": 1
}
```

**Python SDK:** `scalekit_client.actions.triggers.batch_resend_deliveries`

Send 1 to 50 messages to endpoints again. Each target makes a real delivery attempt and gets its own outcome in `results` (`QUEUED`, `NOT_FOUND`, `RATE_LIMITED`, `FAILED`, `NOT_ATTEMPTED`), which never raise. Not retried on `UNAVAILABLE`: a retry could deliver the messages twice.

```python
scalekit_client.actions.triggers.batch_resend_deliveries(
    targets: Sequence[TriggerDeliveryResendTarget | TriggerDeliveryResendTargetParam],
) -> BatchResendTriggerDeliveriesResponse
```

Example:

```python
result = scalekit_client.actions.triggers.batch_resend_deliveries(
    targets=[{"msg_id": "msg_2qGvA4uPf2HjQ7zTn5KrW8dCxLb", "endpoint_id": "ep_2qGv8XHk3mT5bJr9LwNc7pZeYfA"}],
)
```

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `targets` | `Sequence[TriggerDeliveryResendTarget \| TriggerDeliveryResendTargetParam]` | Yes | `TriggerDeliveryResendTarget` models or `{"msg_id": "msg_...", "endpoint_id": "ep_..."}` dicts. |

Returns `BatchResendTriggerDeliveriesResponse`: Per-target results and `queued_count`.

## Recover failed deliveries (beta)

`POST /api/v1/trigger-delivery/endpoints/{endpoint_id}:recover`

This endpoint is in beta. Its request and response may change.

Resends every event whose delivery to this endpoint failed between `since` and `until`, once your endpoint is working again. Leave out `until` to recover up to now. The window is at most 14 days, and one call recovers at most 10,000 events, so cover a long outage with several narrower windows. The recovery runs in the background; the response only confirms it started. Calling again resends events that are still failed. Up to 6 requests a minute per environment.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `endpoint_id` | string | Yes | ID of your delivery endpoint (`ep_...`), the URL on your server that you added under **Developers** > **Trigger Delivery**. [Look up a delivery](https://docs.scalekit.com/agentkit/reference/triggers/look-up-a-delivery/) returns it as `endpoint_id` on each attempt. |

**Request body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `since` | string | Yes | Start of the window, inclusive: events with a failed attempt to this endpoint at or after this instant are recovered. Must not be in the future, and must be at most 14 days before `until`, or before now when `until` is omitted. |
| `until` | string | No | End of the window, exclusive. Omit it to recover up to now. Must be after `since`, must not be in the future, and must be at most 14 days after `since`. |

**Response (200)**

| Name | Type | Description |
| --- | --- | --- |
| `status` | string | The recovery's state when this call returned. `running` in practice: the recovery continues after the response, and its later state is not reported. |
| `task_id` | string | Identifies this recovery in Scalekit's logs; quote it when contacting support. It cannot be looked up or polled through the API. |

**Errors**

- `400`: Invalid request, no recovery started - `endpoint_id` is not an endpoint ID (`ep_` followed by 1 to 64 letters or digits); `since` is missing, or `since` or `until` is not a valid timestamp; `since` is in the future; `until` is in the future; `until` is not after `since`; or `until` is more than 14 days after `since`, measured to now when `until` is omitted, so the period must be split into several calls
- `404`: Not found - the endpoint is not one of this environment's trigger delivery endpoints, or Triggers isn't turned on for your environment. No recovery started
- `429`: Too many requests - this environment has started too many recoveries in a short period, or requests are being refused for volume. No recovery started; retry after a short wait
- `500`: Internal error - the recovery was not confirmed. Either it could not be started, or the request timed out or failed after it may already have been accepted, in which case the error message says it may be running. In that case look for new manual attempts with [List deliveries](https://docs.scalekit.com/agentkit/reference/triggers/list-deliveries/) before calling again, because a second call can queue a second attempt for the same event

**Request**

```bash
curl -sS -X POST \
  "$SCALEKIT_ENVIRONMENT_URL/api/v1/trigger-delivery/endpoints/ep_2qGv8XHk3mT5bJr9LwNc7pZeYfA:recover" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "since": "2026-10-28T09:00:00Z"
  }'
```

**Response**

```json
{
  "task_id": "qtask_2qGvB3mKd9XcV1nTq6RpW4sLhZe",
  "status": "running"
}
```

**Python SDK:** `scalekit_client.actions.triggers.recover_deliveries`

Resend every message that failed to reach an endpoint in a time range. Not retried on `UNAVAILABLE`. A `ScalekitInternalServerException` here means the recovery may be running anyway: check the endpoint's deliveries before calling again.

```python
scalekit_client.actions.triggers.recover_deliveries(
    endpoint_id: str,
    since: datetime,
    until: datetime | None = None,
) -> RecoverTriggerDeliveriesResponse
```

Example:

```python
from datetime import datetime, timezone

result = scalekit_client.actions.triggers.recover_deliveries(
    endpoint_id="ep_2qGv8XHk3mT5bJr9LwNc7pZeYfA",
    since=datetime(2026, 10, 28, 9, 0, tzinfo=timezone.utc),
)
```

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `endpoint_id` | `str` | Yes | ID of your delivery endpoint (`ep_...`), the URL on your server that you added under **Developers** > **Trigger Delivery**. [Look up a delivery](https://docs.scalekit.com/agentkit/reference/triggers/look-up-a-delivery/) returns it as `endpoint_id` on each attempt. |
| `since` | `datetime` | Yes | Start of the range, an aware instant not in the future. |
| `until` | `datetime \| None` | No | End of the range (default now), at most 14 days after `since`. |

Returns `RecoverTriggerDeliveriesResponse`: The recovery task and its status (`"running"` or `"finished"`).

## Get trigger activity (beta)

`GET /api/v1/trigger-activity`

This endpoint is in beta. Its request and response may change.

Counts what happened to the environment's trigger events in the last 24 hours, by outcome, across every connection. Use it to find why a trigger didn't fire: the counts tell apart events that never arrived, events that matched no subscription, and events that failed signature verification. They're totals only; to follow one event, [look up its delivery](https://docs.scalekit.com/agentkit/reference/triggers/look-up-a-delivery/).

**Response (200)**

| Name | Type | Description |
| --- | --- | --- |
| `counts` | object | Event counts over the window, keyed by outcome. Every key is always present, zeroes included. received — events that arrived at one of the environment's webhook URLs, including those that then failed signature verification. filtered — verified events that no subscription's filter or account matched. unresolved — verified events for a provider workspace that no connected account holds. delivered — events handed off for delivery to your endpoints; this does not confirm that your endpoint received them. delivery_failed — events whose handoff for delivery failed. duplicate — repeated deliveries of an event already received, such as provider retries, which are ignored. signature_failed — events that failed signature verification. discarded — events received while Triggers wasn't turned on for your environment, and dropped. |
| `degraded` | boolean | True when some counts could not be read, so the counts are incomplete. Do not read zero counts as "nothing arrived" while this is true. |
| `delivery_details_path` | string | Path, relative to the Scalekit dashboard, of the trigger delivery page. That page shows each delivery attempt to your endpoints, including failures after an event was handed off for delivery. |
| `explains_individual_events` | boolean | Always false. Trigger events are not stored individually, so no endpoint can explain what happened to one specific event. |
| `signature_banner` | boolean | True when at least 5 events in the window failed signature verification and they make up at least half of the events received. This usually means the signing secret set on a connection does not match the provider's. Show it prominently: a provider can disable a webhook URL that keeps failing, and re-enabling it is a manual step in the provider's app configuration. |
| `window_hours` | integer | How many hours back the counts reach, ending now. |

**Errors**

- `404`: Not found - Triggers isn't turned on for your environment
- `500`: Internal error - the activity counts could not be read; retry the request

**Request**

```bash
curl -sS -X GET \
  "$SCALEKIT_ENVIRONMENT_URL/api/v1/trigger-activity" \
  -H "Authorization: Bearer $TOKEN"
```

**Response**

```json
{
  "window_hours": 24,
  "counts": {
    "received": "45",
    "filtered": "6",
    "unresolved": "0",
    "delivered": "39",
    "delivery_failed": "0",
    "duplicate": "2",
    "signature_failed": "0",
    "discarded": "0"
  },
  "signature_banner": false,
  "degraded": false,
  "explains_individual_events": false,
  "delivery_details_path": "/trigger-delivery"
}
```

**Python SDK:** `scalekit_client.actions.triggers.get_activity`

Count what happened to this environment's trigger events recently.

```python
scalekit_client.actions.triggers.get_activity() -> GetTriggerActivityResponse
```

Example:

```python
result = scalekit_client.actions.triggers.get_activity()
```

Returns `GetTriggerActivityResponse`: Counts per outcome over `window`, plus signature and degraded flags.

## Get trigger usage (beta)

`GET /api/v1/trigger-usage`

This endpoint is in beta. Its request and response may change.

Gets the trigger deliveries metered for your environment. One unit is one event sent to one of your endpoints. Ask for `period` (`24h` by the hour, `7d` or `30d` by the UTC day), or for a range of UTC dates with `from` and `to`, up to 30 days back. Hours and days with no usage are left out, so fill the gaps with zero.

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `from` | string | No | First UTC calendar date to include, as YYYY-MM-DD. Inclusive. Required when `period` is not sent, and must be left empty when it is. Must be a real date, must not be after `to`, and must not be after today's UTC date. A date more than 30 days before today's UTC date is clamped to the first day of the readable window, 30 days before today. |
| `period` | string (enum) | No | A preset period ending now, sent instead of `from` and `to`: exactly `24h`, `7d` or `30d`, lowercase. `24h` reads hourly counts for the 24 UTC clock hours ending with the hour in progress. `7d` and `30d` read daily counts for the UTC calendar days ending with today's UTC date: the week ending today, or the 30 days ending today. Leave `from` and `to` empty when this is set; when it is not, both are required. One of: `24h`, `7d`, `30d`. |
| `to` | string | No | Last UTC calendar date to include, as YYYY-MM-DD. Inclusive. Required when `period` is not sent, and must be left empty when it is. Must be a real date and must not be more than 30 days before today's UTC date. A date after today's UTC date is clamped to today. |

**Response (200)**

| Name | Type | Description |
| --- | --- | --- |
| `bucket` | string (enum) | What each entry counts. `TRIGGER_USAGE_BUCKET_HOUR` for `period=24h`, whose counts are in `hours`. `TRIGGER_USAGE_BUCKET_DAY` for `period=7d` and `period=30d`, whose counts are in `days`. `TRIGGER_USAGE_BUCKET_UNSPECIFIED` when the request used `from` and `to`; those counts are always by UTC day, in `days`. One of: `TRIGGER_USAGE_BUCKET_DAY`, `TRIGGER_USAGE_BUCKET_HOUR`. |
| `current_hour` | string | Only for `period=24h`, and empty otherwise. The start of the UTC clock hour in progress on the server, as an RFC 3339 timestamp in UTC (`Z`). It is the last of the 24 hours `hours` covers, and its count grows until the hour ends. Returned so a client lays out the 24 hours and marks the one in progress without relying on its own clock. |
| `days` | array of object | One entry per UTC day within the range read (`from`..`to`) that has metered deliveries, oldest first. A day in that range with none is absent rather than listed as zero, so a client that needs a continuous series fills the missing days itself, over the returned `from` and `to` rather than the range it requested. Days outside the range read were not read and must not be drawn as zero. Always empty for `period=24h`, whose counts are in `hours`. |
| `days.date` | string | The UTC calendar date, as YYYY-MM-DD. |
| `days.units` | string | Trigger deliveries metered for this environment on this day: one for each trigger event sent to one receiving endpoint. Always greater than zero; a day with none is left out of the response. |
| `from` | string | First UTC calendar date of the range read, as YYYY-MM-DD. For a `from` and `to` request, the requested `from`, or the first day of the readable window, 30 days before `today`, when the request started earlier. For a `period` request, the UTC date of the first hour (`24h`) or the first day (`7d`, `30d`) of the period. |
| `hours` | array of object | Only for `period=24h`, and empty otherwise. One entry per UTC clock hour that has metered deliveries, oldest first, among the 24 hours from 23 hours before `current_hour` through `current_hour`. An hour in that range with none is absent rather than listed as zero, so a client that needs a continuous series fills the missing hours itself over those 24 hours. Hours outside them were not read and must not be drawn. |
| `hours.start` | string | The start of the UTC clock hour, as an RFC 3339 timestamp in UTC (`Z`) with zero minutes and seconds. The hour runs from `start` up to, not including, `start` plus one hour. |
| `hours.units` | string | Trigger deliveries metered for this environment in this hour: one for each trigger event sent to one receiving endpoint. Always greater than zero; an hour with none is left out of the response. |
| `period` | string | The `period` the request asked for: `24h`, `7d` or `30d`. Empty when the request used `from` and `to`. |
| `to` | string | Last UTC calendar date of the range read, as YYYY-MM-DD. For a `from` and `to` request, the requested `to`, or `today` when the request ended later. For a `period` request, always `today`. |
| `today` | string | The server's current UTC calendar date, as YYYY-MM-DD. Deliveries on this day are still being metered, so its count grows until the day ends. The readable window and any clamping of `from` and `to` are measured from this date. Returned so a client tells the day in progress apart from completed days without relying on its own clock. |

**Errors**

- `400`: Invalid request - `period` is not exactly `24h`, `7d` or `30d`; `period` is sent together with `from` or `to`; `period` is not sent and `from` or `to` is missing; a date is not a real calendar date in YYYY-MM-DD form; `from` is after `to`; `from` is after today's UTC date; or `to` is more than 30 days before today's UTC date. A range that only extends past the readable window is clamped, not refused
- `404`: Not found - trigger usage is not metered for this environment (error_code TRIGGER_USAGE_NOT_METERED), or Triggers isn't turned on for your environment (error_code RESOURCE_NOT_FOUND)
- `500`: Internal error - the usage could not be read; retry the request
- `503`: Service unavailable - usage data could not be read right now. No partial figures are returned; retry the request later

**Request**

```bash
curl -sS -G -X GET \
  "$SCALEKIT_ENVIRONMENT_URL/api/v1/trigger-usage" \
  -H "Authorization: Bearer $TOKEN" \
  --data-urlencode "period=7d"
```

**Response**

```json
{
  "period": "7d",
  "bucket": "TRIGGER_USAGE_BUCKET_DAY",
  "from": "2026-10-22",
  "to": "2026-10-28",
  "today": "2026-10-28",
  "days": [
    {
      "date": "2026-10-27",
      "units": "112"
    },
    {
      "date": "2026-10-28",
      "units": "39"
    }
  ],
  "hours": [],
  "current_hour": ""
}
```

**Python SDK:** `scalekit_client.actions.triggers.get_usage`

Read metered trigger deliveries for a preset period or a UTC date range. Pass either `period` or both `from_date` and `to_date`. The server checks the combination and clamps the range to the last 30 days.

```python
scalekit_client.actions.triggers.get_usage(
    period: TriggerUsagePeriod | str | None = None,
    from_date: date | str | None = None,
    to_date: date | str | None = None,
) -> GetTriggerUsageResponse
```

Example:

```python
result = scalekit_client.actions.triggers.get_usage(
    period="7d",
)
```

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `period` | `TriggerUsagePeriod \| str \| None` | No | `"24h"` (hourly counts) or `"7d"`/`"30d"` (daily counts). |
| `from_date` | `date \| str \| None` | No | First UTC date, inclusive: a `date`, an aware `datetime` (its UTC date is used) or a `"YYYY-MM-DD"` string. |
| `to_date` | `date \| str \| None` | No | Last UTC date, inclusive, in the same forms. |

Returns `GetTriggerUsageResponse`: Daily or hourly counts and the range actually read.


---

## More Scalekit documentation

| Resource | What it contains | When to use it |
|----------|-----------------|----------------|
| [/llms.txt](/llms.txt) | Structured index with routing hints per product area | Start here — find which documentation set covers your topic before loading full content |
| [/llms-full.txt](/llms-full.txt) | Complete documentation for all Scalekit products in one file | Use when you need exhaustive context across multiple products or when the topic spans several areas |
| [sitemap-0.xml](https://docs.scalekit.com/sitemap-0.xml) | Full URL list of every documentation page | Use to discover specific page URLs you can fetch for targeted, page-level answers |
