> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zapier.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Embedded triggers

> Subscribe to real-time events from 9,000+ connected apps and process them in your product via the Trigger Inbox API.

# Embedded triggers

This use case is for products with an existing workflow builder, AI agent, or orchestration layer that want to react to events in connected apps without building native integrations or managing webhook infrastructure. Zapier subscribes to the app on your behalf and buffers incoming events. Your backend leases and processes them at its own pace.

This White Label use case differs from the generic Powered by Zapier docs:

* You **do not** need to have a public Zapier integration.
* You can subscribe to triggers across Zapier's catalog (you are **not** limited to triggers "owned by" your integration).

**API Reference**: [Trigger Inbox API Reference](/white-label/api-reference/inboxes/list-inboxes)

## What you need

| Requirement                                      | How to obtain                                                             |
| ------------------------------------------------ | ------------------------------------------------------------------------- |
| OAuth bearer token (server-side)                 | [Step 1](#step-1-get-a-bearer-token) — client credentials or JWT exchange |
| Connection identifier for the user's app account | [Connection flow](../connection-flow)                                     |

## High-level flow

1. **Get a bearer token**: exchange SDK client credentials (confirmed) or a partner-signed JWT for an OAuth bearer token via `POST https://zapier.com/oauth/token`.
2. **Check existing connections**: list the user's connections for the app and reuse a valid connection when possible.
3. **Request a connect token (if needed)**: when you need to connect or reconnect, exchange the access token for a connect token. Include **`resource`** set to the Connect UI URL you will open (`https://connect.zapier.com/to/{app}`), matching [Token exchange](../token-exchange).
4. **Connect if needed**: launch Connect UI with the connect token to create/reconnect a connection and store the resulting connection identifier.
5. **Discover triggers**: list available triggers for the target app and let the user choose one.
6. **Inspect trigger inputs**: fetch the input field definitions for the chosen trigger (for example, which Slack channel to watch).
7. **Create a trigger inbox**: subscribe to events by creating a named inbox bound to `(app, trigger, connection, inputs)`. Zapier manages the subscription and buffers incoming events.
8. **Drain or watch for messages**: poll the inbox to retrieve buffered events and process them in your code.
9. **Acknowledge messages**: ack each message after successful processing to remove it from the inbox. Release on failure so it can be retried.

```mermaid theme={null}
flowchart TD
    A([Start]) --> B["Get bearer token\n(client credentials or JWT exchange)"]
    B --> C[List connections for app]
    C --> D{Valid connection\nexists?}
    D -- Yes --> G[Use existing connection ID]
    D -- No --> E[Request connect token]
    E --> F[Launch Connect UI\nUser authenticates]
    F --> G
    G --> H[Discover triggers for app\nUser picks one]
    H --> I[Inspect trigger input fields\nUser fills in required inputs]
    I --> J["Create trigger inbox\n(app + trigger + connection + inputs)"]
    J --> K[Inbox active\nZapier subscribes and buffers events]
    K --> L[Lease messages from inbox]
    L --> M{Messages\navailable?}
    M -- No --> N[Back off and wait]
    N --> L
    M -- Yes --> O[Process message]
    O --> P{Success?}
    P -- Yes --> Q[Ack message\nremoved from inbox]
    P -- No --> R[Release message\nreturns to pool for retry]
    Q --> L
    R --> L
```

***

## Step 1: Get a bearer token

The Trigger Inbox API accepts any OAuth bearer token issued by `POST https://zapier.com/oauth/token`. The right approach depends on your integration pattern.

### Option A: SDK client credentials

Create a client ID and secret via the Zapier SDK CLI, then exchange them for a bearer token.

```bash theme={null}
npx zapier-sdk create-client-credentials
```

Then exchange for a bearer token:

```http theme={null}
POST https://zapier.com/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
&scope=external
```

Response:

```json theme={null}
{
  "access_token": "eyJ...",
  "token_type": "Bearer",
  "expires_in": 3600
}
```

See [Deploy with client credentials](https://docs.zapier.com/sdk/deploy) for how to store and use these in production.

### Option B: White Label JWT exchange

If you are operating on behalf of individual end users via White Label, exchange a partner-signed JWT for a user access token. The token exchange docs list Triggers API under Pattern A, and the Public API Gateway accepts white label tokens via the same `POST /oauth/token` endpoint.

```http theme={null}
POST https://zapier.com/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&client_id=ZAPIER_CLIENT_ID
&client_secret=ZAPIER_CLIENT_SECRET
&subject_token=YOUR_JWT
&subject_token_type=urn:ietf:params:oauth:token-type:external-jwt
&requested_token_type=urn:ietf:params:oauth:token-type:access-token
&scope=external
```

<Note>
  The grant type uses the standard [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) token exchange format. See [Token exchange — Pattern A](../token-exchange#a-actions-api--triggers-api-embedded) for full details. Use Option A to unblock development while this path is being verified.
</Note>

***

Use the `access_token` value as `Authorization: Bearer YOUR_ACCESS_TOKEN` on all subsequent API calls. Keep it server-side — never send it to the browser.

***

## Step 2: Get or create a connection

Before subscribing to a trigger, the user must have a connected app account. List their existing connections and reuse a valid one when possible:

```http theme={null}
GET https://api.zapier.com/v2/authentications/?app=slack
Authorization: Bearer YOUR_ACCESS_TOKEN
```

Response fields relevant to this flow:

| Field        | Type    | Description                                                                  |
| ------------ | ------- | ---------------------------------------------------------------------------- |
| `id`         | string  | Connection identifier — pass this as `connection` in later steps             |
| `app`        | string  | App key (e.g. `SlackCLIAPI`)                                                 |
| `is_expired` | boolean | `true` if the connection's token has expired and the user needs to reconnect |

If no valid connection exists, follow [Connection flow](../connection-flow) to open Connect UI and store the resulting `id`.

***

## Step 3: Discover triggers for an app

List the triggers available for a given app. The `key` field is what you pass as `action` when creating an inbox.

```http theme={null}
GET https://api.zapier.com/trigger-inbox/api/v1/triggers/?app=slack
Authorization: Bearer YOUR_ACCESS_TOKEN
```

Query parameters:

| Parameter | Required | Description                                                                                                                 |
| --------- | -------- | --------------------------------------------------------------------------------------------------------------------------- |
| `app`     | Yes      | App key (e.g. `slack`). Use [Get Apps](/powered-by-zapier/api-reference/apps/get-apps-\[v2]) to look up the key if unknown. |

Example response:

```json theme={null}
{
  "results": [
    {
      "key": "channel_message",
      "title": "New Message Posted to Channel",
      "description": "Triggers when a new message is posted to a specific #channel you choose.",
      "app_key": "SlackCLIAPI"
    },
    {
      "key": "mention",
      "title": "New Mention",
      "description": "Triggers when a username or highlight word is mentioned in a public #channel.",
      "app_key": "SlackCLIAPI"
    }
  ]
}
```

Response fields:

| Field         | Type   | Description                                                                  |
| ------------- | ------ | ---------------------------------------------------------------------------- |
| `key`         | string | Stable identifier for this trigger — pass as `action` when creating an inbox |
| `title`       | string | Human-readable trigger name                                                  |
| `description` | string | What the trigger fires on                                                    |
| `app_key`     | string | Versioned app identifier (informational)                                     |

***

## Step 4: Inspect trigger input fields

Fetch the input field definitions for the chosen trigger. These define the scope of events that get collected — for example, which Slack channel to watch.

```http theme={null}
GET https://api.zapier.com/trigger-inbox/api/v1/trigger-input-fields/?app=slack&action=channel_message&connection=CONNECTION_ID
Authorization: Bearer YOUR_ACCESS_TOKEN
```

Query parameters:

| Parameter    | Required | Description                                                                                                  |
| ------------ | -------- | ------------------------------------------------------------------------------------------------------------ |
| `app`        | Yes      | App key                                                                                                      |
| `action`     | Yes      | Trigger key from Step 3                                                                                      |
| `connection` | Yes      | Connection ID from Step 2 — used to load dynamic options (e.g. the list of channels the user is a member of) |

Example response:

```json theme={null}
{
  "results": [
    {
      "key": "channel",
      "title": "Channel",
      "value_type": "STRING",
      "format": "SELECT",
      "is_required": true,
      "description": "Only channels you are a member of will appear in this list."
    },
    {
      "key": "listen_for_bots",
      "title": "Trigger for Bot Messages?",
      "value_type": "BOOLEAN",
      "format": "SELECT",
      "is_required": false,
      "default_value": "no"
    }
  ]
}
```

Input field properties:

| Field           | Type    | Description                                                                                 |
| --------------- | ------- | ------------------------------------------------------------------------------------------- |
| `key`           | string  | Field identifier — use as the key in the `inputs` object when creating an inbox             |
| `title`         | string  | Human-readable label to show in your UI                                                     |
| `value_type`    | string  | `STRING`, `BOOLEAN`, `INTEGER`                                                              |
| `format`        | string  | `SELECT` means fetch choices before presenting options to the user; `TEXT` means free input |
| `is_required`   | boolean | Must be provided when creating the inbox                                                    |
| `default_value` | string  | Use when the user does not provide a value for an optional field                            |

Collect values for all `is_required` fields. Pass them as `inputs` in Step 5.

***

## Step 5: Create a trigger inbox

Create a named inbox to subscribe to events. Zapier connects to the app, sets up the subscription, and begins buffering incoming events.

The API offers two creation endpoints:

* **`POST /trigger-inbox/api/v1/inboxes`** — creates a new inbox. Returns `409 Conflict` if an inbox with the same name already exists.
* **`PUT /trigger-inbox/api/v1/inboxes`** — idempotent ensure. Returns the existing inbox if one with the same name and matching configuration exists, creates a new one otherwise, or returns `409` if the name exists with a different configuration.

<Note>
  Use `PUT` (ensure) when you want idempotent startup — restarting your process or re-deploying will not create duplicate subscriptions. Use `POST` (create) when you want strict control and want to know if a name is already taken.
</Note>

```http theme={null}
PUT https://api.zapier.com/trigger-inbox/api/v1/inboxes
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json

{
  "name": "my-slack-inbox",
  "subscription": {
    "app_key": "SlackCLIAPI",
    "action_key": "channel_message",
    "connection_id": "CONNECTION_ID",
    "inputs": {
      "channel": "C0123ABC456"
    }
  }
}
```

Request body fields:

| Field                        | Required              | Description                                                                                                            |
| ---------------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `name`                       | Yes (PUT) / No (POST) | A unique name for the inbox. Required for the ensure (PUT) endpoint.                                                   |
| `subscription.app_key`       | Yes                   | Versioned app key (e.g. `SlackCLIAPI@1.0.0`)                                                                           |
| `subscription.action_key`    | Yes                   | Trigger key from Step 3                                                                                                |
| `subscription.connection_id` | Yes                   | Connection ID from Step 2                                                                                              |
| `subscription.inputs`        | Yes                   | Key-value object of trigger input values from Step 4. All `is_required` fields must be present.                        |
| `notification_url`           | No                    | If set, Zapier POSTs to this URL when new messages arrive (see [push instead of poll](#optional-push-instead-of-poll)) |

Example response:

```json theme={null}
{
  "id": "01960a3e-4d5f-7b8e-9c0a-1b2c3d4e5f6a",
  "name": "my-slack-inbox",
  "status": "active",
  "created_at": "2026-05-21T12:00:00.000000Z",
  "paused_reason": null,
  "notification_url": null,
  "subscription": {
    "connection_id": "CONNECTION_ID",
    "app_key": "SlackCLIAPI",
    "action_key": "channel_message",
    "inputs": { "channel": "C0123ABC456" }
  }
}
```

Inbox status lifecycle:

| Status                   | Meaning                                                                                                                 |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
| `initializing`           | Zapier is setting up the subscription with the upstream app. Do not lease messages yet — no events are being collected. |
| `active`                 | Subscription is live. Zapier is collecting and buffering events.                                                        |
| `paused`                 | Collection stopped. Buffered messages are preserved.                                                                    |
| `deleting`               | Scheduled for removal. Subscription is being cancelled.                                                                 |
| `initialization_failure` | Setup failed. Check `paused_reason` for detail.                                                                         |

<Note>
  Poll `GET /trigger-inbox/api/v1/inboxes/{id}` until `status` is `active` before leasing messages. A newly created inbox starts as `initializing`.
</Note>

### Optional: push instead of poll

If you operate a server with a public endpoint, pass `notification_url` when creating the inbox. Zapier will POST to that URL whenever new messages arrive, so you can call the lease endpoint on-demand instead of running a continuous poll loop.

```http theme={null}
PUT https://api.zapier.com/trigger-inbox/api/v1/inboxes
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json

{
  "name": "my-slack-inbox",
  "subscription": {
    "app_key": "SlackCLIAPI",
    "action_key": "channel_message",
    "connection_id": "CONNECTION_ID",
    "inputs": { "channel": "C0123ABC456" }
  },
  "notification_url": "https://your-server.example/trigger-webhook"
}
```

***

## Step 6: Lease messages

Lease a batch of available messages from the inbox. Leased messages are hidden from other consumers while you process them. The response includes a `lease_id` you use to ack or release messages.

```http theme={null}
POST https://api.zapier.com/trigger-inbox/api/v1/inboxes/{inbox_id}/messages/lease
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json

{
  "lease_seconds": 300,
  "lease_limit": 10
}
```

Request body fields:

| Field           | Required | Description                                                                                                                                                                                                                      |
| --------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `lease_seconds` | No       | How long (in seconds) to hold the lease before messages return to the pool. Defaults to 300, max 3600. Set higher than your expected processing time — if your process crashes, messages return automatically after this window. |
| `lease_limit`   | No       | Maximum number of messages to lease in one call. Defaults to 10, max 100.                                                                                                                                                        |

Example response:

```json theme={null}
{
  "lease_id": "019ce2fe-5ed1-7318-8a60-33dffd911f6a",
  "leased_until": "2026-05-21T12:06:00.000000Z",
  "results": [
    {
      "id": "019ce2fe-5ed1-7318-8a60-33dffd911f6b",
      "created_at": "2026-05-21T12:01:00.000000Z",
      "status": "leased",
      "payload": {
        "text": "Hello from Slack",
        "user": "U0123456",
        "channel": "C0123ABC456"
      },
      "message_attributes": {
        "lease_count": 1,
        "error_message": null,
        "possible_duplicate_data": false
      }
    }
  ],
  "inbox_attributes": {
    "status": "active",
    "paused_reason": null
  }
}
```

Message fields:

| Field                                        | Type          | Description                                                                                                                        |
| -------------------------------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `lease_id`                                   | string (UUID) | Identifies this lease — pass to ack or release endpoints.                                                                          |
| `id`                                         | string (UUID) | Stable message identifier                                                                                                          |
| `payload`                                    | object        | Raw event data from the upstream app. Shape varies by trigger.                                                                     |
| `message_attributes.lease_count`             | integer       | How many times this message has been leased. At 5, the message is **quarantined** and permanently removed from the available pool. |
| `message_attributes.error_message`           | string\|null  | Last processing error, if any                                                                                                      |
| `message_attributes.possible_duplicate_data` | boolean       | `true` if dedup was not possible for this event — your handler should be idempotent                                                |

<Warning>
  **Quarantine**: when `lease_count` reaches 5, the message is quarantined and no longer returned by lease calls. It remains visible in list calls for inspection. If your handler can detect a known-bad message, ack it explicitly before it reaches the threshold rather than letting it quarantine.
</Warning>

***

## Step 7: Acknowledge or release messages

**Ack** a message after successful processing to permanently remove it from the inbox:

```http theme={null}
POST https://api.zapier.com/trigger-inbox/api/v1/inboxes/{inbox_id}/messages/ack
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json

{
  "lease_id": "019ce2fe-5ed1-7318-8a60-33dffd911f6a"
}
```

**Release** a message to return it to the available pool before the lease expires. Use this when processing fails and you want the message retried sooner than the lease timeout:

```http theme={null}
POST https://api.zapier.com/trigger-inbox/api/v1/inboxes/{inbox_id}/messages/release
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json

{
  "lease_id": "019ce2fe-5ed1-7318-8a60-33dffd911f6a"
}
```

Both endpoints accept a `lease_id` to ack or release the entire lease at once. Optionally pass `message_ids` to ack or release specific messages within a lease.

***

## Inbox management

### List inboxes

```http theme={null}
GET https://api.zapier.com/trigger-inbox/api/v1/inboxes/
Authorization: Bearer YOUR_ACCESS_TOKEN
```

Query parameters:

| Parameter | Description                                                                                |
| --------- | ------------------------------------------------------------------------------------------ |
| `status`  | Filter by status: `active`, `paused`, `initializing`, `deleting`, `initialization_failure` |
| `name`    | Filter by exact inbox name                                                                 |

### Pause and resume

Pausing stops event collection without discarding buffered messages. Resuming restarts collection.

```http theme={null}
POST https://api.zapier.com/trigger-inbox/api/v1/inboxes/{id}/pause
Authorization: Bearer YOUR_ACCESS_TOKEN
```

```http theme={null}
POST https://api.zapier.com/trigger-inbox/api/v1/inboxes/{id}/resume
Authorization: Bearer YOUR_ACCESS_TOKEN
```

### Delete

Marks the inbox for deletion and cancels the server-side subscription:

```http theme={null}
DELETE https://api.zapier.com/trigger-inbox/api/v1/inboxes/{id}
Authorization: Bearer YOUR_ACCESS_TOKEN
```

***

## Common errors

| HTTP status | `code`                  | Cause                                                                    | Fix                                                                                 |
| ----------- | ----------------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- |
| 401         | `authentication_failed` | Missing or expired bearer token                                          | Re-exchange credentials for a fresh token                                           |
| 401         | `invalid_audience`      | Token audience does not include `trigger-inbox`                          | Ensure your token was issued via `POST /oauth/token` against the Public API Gateway |
| 404         | `not_found`             | Inbox name does not exist                                                | Check the name or create the inbox first                                            |
| 409         | `conflict`              | Inbox with this name exists with a different `(app, action, connection)` | Use a different name or delete the existing inbox                                   |
| 422         | `validation_error`      | Missing required field or invalid input value                            | Check the `detail` field in the response for the specific field                     |
| 429         | `rate_limited`          | Too many requests                                                        | Back off and retry — respect `Retry-After` header if present                        |

***

## Complete example: subscribe to new Slack messages

This is a runnable end-to-end implementation. Substitute your credentials and connection ID and it should work without modification. Inline comments mark the non-obvious parts.

```python theme={null}
import time
import requests

# --- Configuration ---
# Option A (client credentials): obtain via `npx zapier-sdk create-client-credentials`
# Option B (White Label JWT): exchange your partner-signed JWT via POST /oauth/token
ACCESS_TOKEN = "YOUR_BEARER_TOKEN"
CONNECTION_ID = "YOUR_CONNECTION_ID"  # from white label connection flow

BASE_URL = "https://api.zapier.com"
INBOX_NAME = "my-slack-inbox"
HEADERS = {
    "Authorization": f"Bearer {ACCESS_TOKEN}",
    "Content-Type": "application/json",
}

# Step 1: Discover triggers for Slack
resp = requests.get(f"{BASE_URL}/trigger-inbox/api/v1/triggers/?app=slack", headers=HEADERS)
resp.raise_for_status()
triggers = resp.json()["results"]
# triggers[0] == {"key": "channel_message", "title": "New Message Posted to Channel", ...}

# Step 2: Inspect input fields for the chosen trigger.
# Pass connection so Zapier can return dynamic options (e.g. list of channels the user is in).
resp = requests.get(
    f"{BASE_URL}/trigger-inbox/api/v1/trigger-input-fields/",
    params={"app": "slack", "action": "channel_message", "connection": CONNECTION_ID},
    headers=HEADERS,
)
resp.raise_for_status()
# fields[0] == {"key": "channel", "is_required": True, "format": "SELECT", ...}
# In a real UI, present these to the user and collect values.
# Hard-coded here for the example.
CHANNEL_ID = "C0123ABC456"

# Step 3: Create (or reuse) the trigger inbox.
# PUT ensure is idempotent by name — safe to call on every startup.
resp = requests.put(
    f"{BASE_URL}/trigger-inbox/api/v1/inboxes",
    json={
        "name": INBOX_NAME,
        "subscription": {
            "app_key": "SlackCLIAPI",
            "action_key": "channel_message",
            "connection_id": CONNECTION_ID,
            "inputs": {"channel": CHANNEL_ID},
        },
    },
    headers=HEADERS,
)
resp.raise_for_status()
inbox = resp.json()
inbox_id = inbox["id"]

# Step 4: Wait for the inbox to become active.
# Status starts as "initializing" while Zapier sets up the subscription.
# Do not lease messages until status is "active" — no events are collected yet.
while inbox["status"] == "initializing":
    print("Inbox initializing, waiting...")
    time.sleep(2)
    resp = requests.get(f"{BASE_URL}/trigger-inbox/api/v1/inboxes/{inbox_id}", headers=HEADERS)
    resp.raise_for_status()
    inbox = resp.json()

if inbox["status"] != "active":
    raise RuntimeError(f"Inbox failed to activate: {inbox['status']} — {inbox.get('paused_reason')}")

print(f"Inbox active: {inbox_id}")

# Step 5: Poll for messages with exponential backoff.
# In production, run this in a persistent worker process (e.g. under systemd or pm2).
backoff = 5
max_backoff = 60

while True:
    resp = requests.post(
        f"{BASE_URL}/trigger-inbox/api/v1/inboxes/{inbox_id}/messages/lease",
        # lease_seconds must be longer than your expected processing time.
        # If your process crashes, messages return to the pool after this window.
        json={"lease_seconds": 30},
        headers=HEADERS,
    )
    resp.raise_for_status()
    lease_data = resp.json()
    lease_id = lease_data.get("lease_id")
    messages = lease_data.get("results", [])

    if not messages:
        # Back off when inbox is empty to avoid hammering the API.
        time.sleep(backoff)
        backoff = min(backoff * 2, max_backoff)
        continue

    backoff = 5  # reset backoff when messages arrive

    for message in messages:
        attrs = message["message_attributes"]

        # If possible_duplicate_data is True, the upstream app didn't provide a
        # dedup ID. Make your handler idempotent.
        if attrs["possible_duplicate_data"]:
            print(f"Warning: possible duplicate — message {message['id']}")

        try:
            payload = message["payload"]
            print(f"New message in {payload['channel']} from {payload['user']}: {payload['text']}")
            # --- your processing logic here ---

            # Ack removes the message from the inbox permanently.
            requests.post(
                f"{BASE_URL}/trigger-inbox/api/v1/inboxes/{inbox_id}/messages/ack",
                json={"lease_id": lease_id, "message_ids": [message["id"]]},
                headers=HEADERS,
            ).raise_for_status()

        except Exception as exc:
            print(f"Processing failed for {message['id']}: {exc}")
            # After 5 total leases the message is quarantined — ack poison
            # messages before they reach that threshold.
            if attrs["lease_count"] >= 4:
                print(f"Message {message['id']} near quarantine threshold — acking to drop.")
                requests.post(
                    f"{BASE_URL}/trigger-inbox/api/v1/inboxes/{inbox_id}/messages/ack",
                    json={"lease_id": lease_id, "message_ids": [message["id"]]},
                    headers=HEADERS,
                ).raise_for_status()
            else:
                # Release returns the message to the pool immediately rather than
                # waiting for the lease to expire.
                requests.post(
                    f"{BASE_URL}/trigger-inbox/api/v1/inboxes/{inbox_id}/messages/release",
                    json={"lease_id": lease_id, "message_ids": [message["id"]]},
                    headers=HEADERS,
                ).raise_for_status()
```

***

## Related guides

* [Token exchange](../token-exchange) — obtain a user access token via JWT exchange
* [Connection flow](../connection-flow) — popup or redirect flow for connecting user app accounts
* [Embedded actions](./embedded-actions) — run one-off actions via the Action Runs API
* [AI agent connections and automations](./ai-agent-connections) — MCP tool-calling for agents
* [Error handling](../error-handling) — error codes for token exchange, connection flow, and refresh
* [Deploy with client credentials](https://docs.zapier.com/sdk/deploy) — run in production without browser login
