Skip to main content

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

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).
OpenAPI spec: https://api.zapier.com/trigger-inbox/api/v1/openapi.json

What you need

RequirementHow to obtain
OAuth bearer token (server-side)Step 1 — client credentials or JWT exchange
Connection identifier for the user’s app accountConnection 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.
  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.

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.
npx zapier-sdk create-client-credentials
Then exchange for a bearer token:
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:
{
  "access_token": "eyJ...",
  "token_type": "Bearer",
  "expires_in": 3600
}
See Deploy with client credentials 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.
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
The grant type uses the standard RFC 8693 token exchange format. See Token exchange — Pattern A for full details. Use Option A to unblock development while this path is being verified.

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:
GET https://api.zapier.com/v2/authentications/?app=slack
Authorization: Bearer YOUR_ACCESS_TOKEN
Response fields relevant to this flow:
FieldTypeDescription
idstringConnection identifier — pass this as connection in later steps
appstringApp key (e.g. SlackCLIAPI)
is_expiredbooleantrue if the connection’s token has expired and the user needs to reconnect
If no valid connection exists, follow 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.
GET https://api.zapier.com/trigger-inbox/api/v1/triggers/?app=slack
Authorization: Bearer YOUR_ACCESS_TOKEN
Query parameters:
ParameterRequiredDescription
appYesApp key (e.g. slack). Use Get Apps to look up the key if unknown.
Example response:
{
  "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:
FieldTypeDescription
keystringStable identifier for this trigger — pass as action when creating an inbox
titlestringHuman-readable trigger name
descriptionstringWhat the trigger fires on
app_keystringVersioned 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.
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:
ParameterRequiredDescription
appYesApp key
actionYesTrigger key from Step 3
connectionYesConnection ID from Step 2 — used to load dynamic options (e.g. the list of channels the user is a member of)
Example response:
{
  "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:
FieldTypeDescription
keystringField identifier — use as the key in the inputs object when creating an inbox
titlestringHuman-readable label to show in your UI
value_typestringSTRING, BOOLEAN, INTEGER
formatstringSELECT means fetch choices before presenting options to the user; TEXT means free input
is_requiredbooleanMust be provided when creating the inbox
default_valuestringUse 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.
Non-standard: upsert by nameThis endpoint uses PUT /{name}/ as an upsert — it creates the inbox if no inbox with that name exists, or returns the existing one if it does. This is intentionally non-standard REST (which would use POST to create and PUT /{id}/ to update by ID). The name-based upsert makes inbox creation idempotent: restarting your process or re-deploying will not create duplicate subscriptions.
PUT https://api.zapier.com/trigger-inbox/api/v1/inboxes/my-slack-inbox/
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json

{
  "app": "slack",
  "action": "channel_message",
  "connection": "CONNECTION_ID",
  "inputs": {
    "channel": "C0123ABC456"
  }
}
Request body fields:
FieldRequiredDescription
appYesApp key
actionYesTrigger key from Step 3
connectionYesConnection ID from Step 2
inputsYesKey-value object of trigger input values from Step 4. All is_required fields must be present.
notification_urlNoIf set, Zapier POSTs to this URL when new messages arrive (see push instead of poll)
Example response:
{
  "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:
StatusMeaning
initializingZapier is setting up the subscription with the upstream app. Do not lease messages yet — no events are being collected.
activeSubscription is live. Zapier is collecting and buffering events.
pausedCollection stopped. Buffered messages are preserved.
deletingScheduled for removal. Subscription is being cancelled.
initialization_failureSetup failed. Check paused_reason for detail.
Poll GET /trigger-inbox/api/v1/inboxes/{name}/ until status is active before leasing messages. A newly created inbox starts as initializing.

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.
PUT https://api.zapier.com/trigger-inbox/api/v1/inboxes/my-slack-inbox/
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json

{
  "app": "slack",
  "action": "channel_message",
  "connection": "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. Each message includes a lease_token you use to ack or release it.
POST https://api.zapier.com/trigger-inbox/api/v1/inboxes/my-slack-inbox/messages/lease/
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json

{
  "lease_seconds": 30
}
Request body fields:
FieldRequiredDescription
lease_secondsNoHow long (in seconds) to hold the lease before messages return to the pool. Defaults to 30. Set higher than your expected processing time — if your process crashes, messages return automatically after this window.
max_messagesNoMaximum number of messages to lease in one call.
Example response:
{
  "results": [
    {
      "id": "msg_abc123",
      "created_at": "2026-05-21T12:01:00.000000Z",
      "status": "leased",
      "lease_token": "lt_xyz789",
      "payload": {
        "text": "Hello from Slack",
        "user": "U0123456",
        "channel": "C0123ABC456"
      },
      "message_attributes": {
        "lease_count": 1,
        "error_message": null,
        "possible_duplicate_data": false
      }
    }
  ]
}
Message fields:
FieldTypeDescription
idstringStable message identifier
lease_tokenstringOpaque token — pass to ack or release. Unique per lease, not per message.
payloadobjectRaw event data from the upstream app. Shape varies by trigger.
message_attributes.lease_countintegerHow many times this message has been leased. At 5, the message is quarantined and permanently removed from the available pool.
message_attributes.error_messagestring|nullLast processing error, if any
message_attributes.possible_duplicate_databooleantrue if dedup was not possible for this event — your handler should be idempotent
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.

Step 7: Acknowledge or release messages

Ack a message after successful processing to permanently remove it from the inbox:
POST https://api.zapier.com/trigger-inbox/api/v1/inboxes/my-slack-inbox/messages/ack/
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json

{
  "lease_tokens": ["lt_xyz789"]
}
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:
POST https://api.zapier.com/trigger-inbox/api/v1/inboxes/my-slack-inbox/messages/release/
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json

{
  "lease_tokens": ["lt_xyz789"]
}
Both endpoints accept an array of lease_tokens — you can ack or release a full batch in one call.

Inbox management

List inboxes

GET https://api.zapier.com/trigger-inbox/api/v1/inboxes/
Authorization: Bearer YOUR_ACCESS_TOKEN
Query parameters:
ParameterDescription
statusFilter by status: active, paused, initializing, deleting, initialization_failure
nameFilter by exact inbox name

Pause and resume

Pausing stops event collection without discarding buffered messages. Resuming restarts collection.
POST https://api.zapier.com/trigger-inbox/api/v1/inboxes/my-slack-inbox/pause/
Authorization: Bearer YOUR_ACCESS_TOKEN
POST https://api.zapier.com/trigger-inbox/api/v1/inboxes/my-slack-inbox/resume/
Authorization: Bearer YOUR_ACCESS_TOKEN

Delete

Marks the inbox for deletion and cancels the server-side subscription:
DELETE https://api.zapier.com/trigger-inbox/api/v1/inboxes/my-slack-inbox/
Authorization: Bearer YOUR_ACCESS_TOKEN

Common errors

HTTP statuscodeCauseFix
401authentication_failedMissing or expired bearer tokenRe-exchange credentials for a fresh token
401invalid_audienceToken audience does not include trigger-inboxEnsure your token was issued via POST /oauth/token against the Public API Gateway
404not_foundInbox name does not existCheck the name or create the inbox first
409conflictInbox with this name exists with a different (app, action, connection)Use a different name or delete the existing inbox
422validation_errorMissing required field or invalid input valueCheck the detail field in the response for the specific field
429rate_limitedToo many requestsBack 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.
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 is an upsert by name — idempotent, safe to call on every startup.
resp = requests.put(
    f"{BASE_URL}/trigger-inbox/api/v1/inboxes/{INBOX_NAME}/",
    json={
        "app": "slack",
        "action": "channel_message",
        "connection": CONNECTION_ID,
        "inputs": {"channel": CHANNEL_ID},
    },
    headers=HEADERS,
)
resp.raise_for_status()
inbox = resp.json()

# 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_NAME}/", 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_NAME}/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()
    messages = resp.json().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:
        lease_token = message["lease_token"]
        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_NAME}/messages/ack/",
                json={"lease_tokens": [lease_token]},
                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_NAME}/messages/ack/",
                    json={"lease_tokens": [lease_token]},
                    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_NAME}/messages/release/",
                    json={"lease_tokens": [lease_token]},
                    headers=HEADERS,
                ).raise_for_status()