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
| Requirement | How to obtain |
|---|
| OAuth bearer token (server-side) | Step 1 — client credentials or JWT exchange |
| Connection identifier for the user’s app account | Connection flow |
High-level flow
- 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.
- Check existing connections: list the user’s connections for the app and reuse a valid connection when possible.
- 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.
- Connect if needed: launch Connect UI with the connect token to create/reconnect a connection and store the resulting connection identifier.
- Discover triggers: list available triggers for the target app and let the user choose one.
- Inspect trigger inputs: fetch the input field definitions for the chosen trigger (for example, which Slack channel to watch).
- 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.
- Drain or watch for messages: poll the inbox to retrieve buffered events and process them in your code.
- 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:
| 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 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:
| Parameter | Required | Description |
|---|
app | Yes | App 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:
| 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) |
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:
| 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:
{
"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.
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:
| Field | Required | Description |
|---|
app | Yes | App key |
action | Yes | Trigger key from Step 3 |
connection | Yes | Connection ID from Step 2 |
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) |
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:
| 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. |
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:
| Field | Required | Description |
|---|
lease_seconds | No | How 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_messages | No | Maximum 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:
| Field | Type | Description |
|---|
id | string | Stable message identifier |
lease_token | string | Opaque token — pass to ack or release. Unique per lease, not per message. |
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 |
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:
| 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.
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 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.
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()