---
author: OOMOL
author_url: https://oomol.com/about/
title: Receive Provider Events with Triggers
description: Subscribe to new rows, emails, CRM changes, and other provider
  events for your end users, and receive them as signed project webhooks.
lang: en
canonical_url: https://oomol.com/docs/saas-triggers/
markdown_url: https://oomol.com/docs/saas-triggers.md
---

# Receive Provider Events with Triggers

A Trigger subscription watches one end user's connected account and sends new provider events to your project webhook as `trigger.message` events. When a user's Google Sheet gets a new row, a new email reaches their Gmail, or a Stripe payment changes state, your backend receives an event.

Connector runs the subscription: it polls providers and registers and receives provider webhooks. Your backend creates subscriptions and handles the events.

## How events arrive

| Mode | How Connector gets events | Examples |
| --- | --- | --- |
| Polling | Reads the account at a fixed interval and sends rows, messages, or records that are new since the previous read. | Gmail, Google Sheets, Google Calendar, Notion |
| Push | The provider sends events to a Connector callback URL. Connector verifies them and forwards matching events. | GitHub, Stripe, Shopify, Slack |

Push Triggers involve two URLs: your project webhook receives events from Connector, and the third-party callback URL (`push.callbackUrl`) receives events from the provider. Connector registers most callback URLs automatically; Slack, Microsoft Teams, Feishu, and Intercom need an administrator to enter it in the provider app.

## Before you start

1. Complete the [Connector for SaaS guide](/docs/connector-saas/) so that your backend has a Project API key and saves each user's `connectedAccountId`.
2. [Create a project webhook](/docs/saas-webhooks/#create-a-webhook) whose `eventTypes` include `trigger.message` and whose `authTypes` include the auth type of the accounts you subscribe to. Google accounts use `oauth2`. One webhook can receive events from every subscription in the project.
3. Have the end user connect the account that the Trigger reads.

If the account is [routed to a dedicated webhook](/docs/saas-webhooks/#route-an-accounts-events-to-a-dedicated-webhook), events go only to that webhook, so it must meet these conditions instead.

Connector checks the webhook when you create or resume a subscription. Without an active webhook that matches, the request fails with `trigger_delivery_unavailable`.

## Step 1: Choose a Trigger

List the published Triggers and read the config schema of the one you need:

```bash
curl --silent --show-error \
  'https://connector.oomol.com/v1/saas/trigger-types/googlesheets.on_row_added' \
  -H "Authorization: Bearer $PROJECT_API_KEY"
```

`GET /v1/saas/trigger-types` lists every published Trigger; add `?service=googlesheets` to filter by provider. The catalog is the same for every project.

| Field | Meaning |
| --- | --- |
| `id` | The Trigger ID used when creating a subscription. |
| `type` | `poll` or `push`. |
| `configSchema` | JSON Schema for the subscription `config`, including defaults and required fields. |
| `eventSchema` | JSON Schema for `data.payload` in delivered events. |
| `definitionVersion` | The Trigger definition version recorded on new subscriptions. |
| `defaultIntervalSeconds` / `minIntervalSeconds` | The default and shortest polling interval. `null` for push Triggers. |

## Step 2: Create a subscription

This example watches the `Orders` sheet for new rows:

```bash
curl --silent --show-error \
  'https://connector.oomol.com/v1/saas/trigger-subscriptions' \
  -H "Authorization: Bearer $PROJECT_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: orders-sheet-customer-1' \
  --data @- <<JSON
{
  "userId": "customer-1",
  "connectedAccountId": "$CONNECTED_ACCOUNT_ID",
  "triggerId": "googlesheets.on_row_added",
  "config": {
    "spreadsheetId": "<spreadsheet-id>",
    "sheet": "Orders"
  },
  "intervalSeconds": 300
}
JSON
```

| Field | Usage |
| --- | --- |
| `userId` | Required: your product's end-user ID that owns the connected account. |
| `connectedAccountId` | Required: the account the subscription reads. It cannot be changed later. |
| `triggerId` | Required: an `id` from the Trigger catalog. |
| `config` | Required: an object matching `configSchema`. It cannot be changed later; create a new subscription to watch something else. |
| `intervalSeconds` | Optional, polling only: the read interval, at least `minIntervalSeconds`. Push subscriptions reject this field. |

Send an `Idempotency-Key` header so that a retried request does not create a second subscription. The first request returns 201; replaying the same request with the same key returns 200 and the original subscription; reusing the key for a different request returns 409.

Save the returned `id` beside the user and account in your database. You can also create and manage subscriptions in Console: open the project and select **Event subscriptions**.

## Receive events

Each event arrives at your project webhook as a signed `trigger.message` delivery. [Verify the signature](/docs/saas-webhooks/#verify-each-delivery) first, then read the envelope:

```json
{
  "id": "evt_0199d2a8-4c1b-7e2f-8a3d-6b5c4d3e2f10",
  "type": "trigger.message",
  "version": 1,
  "createdAt": 1800000300000,
  "data": {
    "subscriptionId": "0199d290-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
    "triggerId": "googlesheets.on_row_added",
    "definitionVersion": 2,
    "externalUserId": "customer-1",
    "connectedAccountId": "0199d1f4-2e5f-7b61-8c72-9d0e1f2a3b4c",
    "providerConfigId": "0199d1a0-5f6e-7d8c-9b0a-1c2d3e4f5a6b",
    "service": "googlesheets",
    "payload": {
      "spreadsheetId": "<spreadsheet-id>",
      "sheetId": 0,
      "sheetTitle": "Orders",
      "rowNumber": 12,
      "row": { "Order ID": "A-1001", "Amount": 42 },
      "values": ["A-1001", 42]
    }
  }
}
```

`data.payload` follows the Trigger's `eventSchema`. Use `subscriptionId` or `externalUserId` to route the event to the right user, and deduplicate by envelope `id`, which stays the same across retries and webhooks.

The first polling read only records the current position; existing rows, emails, or records are not sent. The subscription's `initialized` field turns `true` after that read. Events created after it are sent on later reads.

Delivery uses the [project webhook retry schedule](/docs/saas-webhooks/#respond-and-retry). The same event can arrive more than once, and a delivery that runs out of retries or gets a non-retryable 4xx ends as `failed`; find those in **Deliveries** in Console or through the delivery records API. Each webhook has up to 5 requests in flight; a slow endpoint delays its own queue but not other webhooks. Events that reach a final state are kept for 7 days.

## Manage subscriptions

| Task | API |
| --- | --- |
| List subscriptions | `GET /v1/saas/trigger-subscriptions`, filtered by `userId`, `connectedAccountId`, or `status`, with `limit` (up to 100) and `offset`. |
| Read one subscription | `GET /v1/saas/trigger-subscriptions/{subscriptionId}` |
| Pause or resume | `PATCH /v1/saas/trigger-subscriptions/{subscriptionId}` with `{"status": "paused"}` or `{"status": "active"}` |
| Change the polling interval | `PATCH /v1/saas/trigger-subscriptions/{subscriptionId}` with `{"intervalSeconds": 600}` |
| Delete | `DELETE /v1/saas/trigger-subscriptions/{subscriptionId}` |

A subscription shows `status`, `pauseReason`, `initialized`, `nextPollAt`, `lastSuccessAt`, `lastError`, and `consecutiveFailures`. Push subscriptions also show `push.callbackUrl`, `push.setup` (`automatic` or `application`), and `push.ready`. Deleting a subscription stops new reads; events recorded before deletion are still delivered.

## When a subscription pauses

Connector pauses a subscription when it cannot continue safely. `pauseReason` tells you what to do:

| `pauseReason` | Cause | What to do |
| --- | --- | --- |
| `manual` | Paused through the API or Console. | Resume when ready. |
| `connection_unavailable` | The account, credential, permission, or provider config is unavailable. | Have the user reconnect the account, then resume. |
| `config_invalid` | The provider rejected the config, or the Trigger was retired. | Fix the issue on the provider side and resume, or create a subscription with a new config. |
| `delivery_unavailable` | No active webhook matched the account when an event was recorded. For a routed account, only its routed webhook counts. | Fix the webhook, then resume. |
| `checkpoint_reset_required` | Reading cannot continue from the saved position, for example after the sheet was replaced, rows were removed, or the provider's sync token expired. `pauseDetail` has the reason. | Create a new subscription. This one cannot be resumed, and changes in between are not sent. |

Temporary provider errors do not pause a subscription. Connector retries with backoff from 30 seconds up to 30 minutes and records the latest error in `lastError` and `consecutiveFailures`.

If the user reconnects the same provider account, the subscription keeps working. If the user replaces it with a different provider account under the same alias, related subscriptions pause with `checkpoint_reset_required`.

## Billing and team balance

Each event is billed once, when it is first delivered successfully to any webhook in the project. Reads, empty reads, retries, extra webhooks, and events that finally fail are not billed.

Triggers require a team balance that covers Connector usage:

- When the balance is below zero, creating or resuming a subscription returns `402 insufficient_credit`. Reads and deliveries stop. Viewing, changing the interval, pausing, and deleting still work.
- When the balance cannot be verified, the same requests return `503 balance_unavailable` with `Retry-After: 30`.
- While delivery is blocked, push events are buffered up to 10,000 pending items per project. Connector still acknowledges events beyond that limit to the provider but does not store them, so the provider does not resend them. After 7 days of confirmed insufficient credit, pending events expire and new push events are dropped the same way until the balance recovers. The `billing` view counts both cases.
- After the team adds credit, reads and deliveries resume automatically. Subscriptions paused for other reasons stay paused.

Subscription responses include a project `billing` summary with `state` (`allowed`, `insufficient_credit`, `balance_unavailable`, or `unverified`), the buffer size, and counts of rejected or expired events. List responses return it in `meta.billing`. `unverified` means the team balance has not been checked yet, usually because the project has no subscriptions; it does not block requests, and creating a subscription checks the balance.

## Errors

| HTTP status and `errorCode` | Meaning |
| --- | --- |
| 404 `trigger_not_found` | The Trigger ID is not published. |
| 409 `trigger_delivery_unavailable` | No active webhook receives `trigger.message` for this account's auth type. `data.reason` is `no_active_webhook`, `event_type_not_subscribed`, `auth_type_not_subscribed`, or `routed_webhook_unavailable` (the account's routed webhook is disabled or does not match); `data.webhookIds` lists webhooks you can update. |
| 409 `trigger_subscription_reset_required` | The subscription cannot be resumed. Create a new one. |
| 409 `app_not_ready` | The connected account cannot be used, for example after it was disconnected. Have the user reconnect it. |
| 402 `insufficient_credit` | The team balance is below zero. |
| 503 `balance_unavailable` | The balance could not be verified. Retry after the `Retry-After` delay. |

## Available Triggers

The catalog from `GET /v1/saas/trigger-types` is the source of truth.

| Polling Trigger ID | Watches | Default / minimum interval |
| --- | --- | --- |
| `airtable.on_record_changed` | New or updated table records | 300 / 120 seconds |
| `gmail.on_message_received` | New emails | 120 / 120 seconds |
| `googlecalendar.on_event_changed` | Calendar event changes | 120 / 120 seconds |
| `googledrive.on_file_change` | New or updated files | 120 / 120 seconds |
| `googlesheets.on_row_added` | New rows | 120 / 120 seconds |
| `notion.on_database_page_event` | New or edited database pages | 120 / 120 seconds |
| `one_drive.on_item_changed` | File or folder changes | 120 / 120 seconds |
| `outlook.on_message_received` | New emails, filtered by folder and message conditions | 120 / 120 seconds |

| Push Trigger ID | Watches | Setup |
| --- | --- | --- |
| `calendly.on_booking_event` | Bookings created or canceled | Automatic; uses a Personal Access Token |
| `clickup.on_task_event` | Task creation, status changes, and new comments | Automatic; needs a Workspace ID or List ID |
| `feishu_app_bot.on_event` | Enabled app events | Application callback; credential needs Verification Token and Encrypt Key |
| `github.on_repo_event` | Repository events | Automatic; needs permission to manage repository webhooks |
| `gitlab.on_project_event` | Project pushes, issues, merge requests, pipelines, and other selected hooks | Automatic project hook; verifies with a Secret Token |
| `intercom.on_conversation_event` | Customer conversations, customer replies, and new tickets | Application callback; uses the connection's OAuth client ID and Client Secret |
| `jira.on_issue_event` | Issues created, updated, or commented on | Automatic and renewed; needs Jira Cloud OAuth with webhook scopes |
| `linear.on_issue_event` | Issues created, updated, or deleted | Automatic; needs an admin API key or the OAuth `admin` scope |
| `microsoft_teams_bot.on_activity` | Bot messages and activities | Azure Bot messaging endpoint |
| `pandadoc.on_document_event` | Document status changes | Automatic |
| `pipedrive.on_crm_event` | Persons and deals created, or deal field changes | Automatic |
| `resend.on_email_event` | Inbound email, bounces, and complaints | Automatic |
| `shopify.on_shop_event` | Shop events | Automatic; OAuth or app client credentials |
| `slackbot.on_event` | Mentions, direct messages, and interactions | Slack app Events and Interactivity URLs; needs the app's Signing Secret |
| `stripe.on_event` | Stripe events | Automatic |
| `tally.on_form_response` | Completed form responses | Automatic |
| `telegram.on_update` | Bot messages and selected update types | Automatic setWebhook; uses the bot token |
| `typeform.on_form_response` | Completed form responses | Automatic |
| `vercel.on_deployment_event` | Successful and failed deployments | Automatic |
| `woocommerce.on_store_event` | Orders, products, coupons, and customer events | Automatic; needs a Read/Write consumer key |
| `zendesk.on_event` | Selected Zendesk events | Automatic |

## Provider notes

Polling sends the state visible at each read; edits between two reads are not sent one by one. Subscriptions on the same provider account, base, or workspace share that provider's rate limits.

| Provider | Notes |
| --- | --- |
| Airtable | `triggerField` must be a time field: Created time for new records, or a Last modified time field that tracks the fields you care about. Empty reads count toward API limits; one subscription at the default 5-minute interval makes about 8,640 reads every 30 days. Raise `intervalSeconds` if your plan needs it. |
| Notion | Share the database with the connection and enable Read content. Set `dataSourceId` when the database has more than one data source. The first read rounds up to the minute, so earlier changes are not sent. |
| OneDrive | `parentFolderId` matches only the direct parent folder. |
| Slack, Microsoft Teams, Feishu, Intercom | An app administrator enters `push.callbackUrl` in the provider app; subscriptions for the same app share it. After rotating the app's signing key, update the connection credential. Slack and Feishu also require verifying the URL again with the new key. |
| Slack, Feishu | Subscriptions for one app share a single receiver, and each new subscription replaces that receiver's signing key with the one in its connection credential. If that credential's key does not match the app, events for every subscription on the app fail verification. Keep the signing key in all of the app's connection credentials in sync with the app, both when creating subscriptions and when rotating the key. |
| Intercom | In the Intercom Developer Hub, subscribe the app to the topics that cover the Trigger's `eventTypes` (`conversation.user.created`, `conversation.user.replied`, `ticket.created`); Intercom only sends subscribed topics. Saving the callback URL sends an unsigned HEAD check, which Connector answers with 200. `push.ready` becomes `true` after a signed `ping` notification, and events are stored even before then. |
| Microsoft Teams | The `tenantId` in the connection credential is the token authority. The optional `tenantId` in the Trigger config filters activities by tenant. |
| Linear | Add the `admin` scope to the provider config and reconnect. Connections that granted only `read` cannot create webhooks. |
| Shopify | Connector verifies events with the current client secret. Follow [Shopify's rotation steps](https://shopify.dev/docs/apps/build/authentication-authorization/manage-credentials) when rotating it. |

## Troubleshooting

| Symptom | What to check |
| --- | --- |
| Creating a subscription returns `trigger_delivery_unavailable` | Follow `data.reason`: create a webhook, add `trigger.message` to its event types, or add the account's auth type. For `routed_webhook_unavailable`, update the routed webhook or remove the account's routing. |
| No events after creating a polling subscription | Wait for `initialized` to become `true`, then add new data. Existing data is never sent. |
| A push subscription stays `push.ready: false` | Check `lastError`. For Slack, Teams, Feishu, and Intercom, confirm that the provider app uses `push.callbackUrl`; for Intercom, also confirm that the app subscribes to the Trigger's event topics. |
| Events stopped and `pauseReason` is set | Follow the action in [When a subscription pauses](#when-a-subscription-pauses). |
| Events stopped and `billing.state` is not `allowed` | Add team credit or wait for the balance check to recover. |
