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

# Webhooks

> Webhooks let your app react in real time to what happens in the workspaces that installed it: form submissions, contacts, companies, deals, products and quotes, tasks, meetings, calls and lists.

Your app subscribes per workspace, with the access token the workspace granted it. Deliveries are signed, retried, logged, and stop on their own when the app is uninstalled or re-authorized with fewer scopes.

## Available events

> [card] Submissions
>
> `submission.created`, `submission.qualified`, `submission.updated`, `submission.deleted`

> [card] Contacts
>
> `contact.created`, `contact.updated`, `contact.archived`, `contact.deleted`, `contact.tag_added`, `contact.tag_removed`, `contact.association_changed`, `contact.job_changed`

> [card] Companies
>
> `company.created`, `company.updated`, `company.deleted`, `company.restored`, `company.merged`, `company.tag_added`, `company.tag_removed`

> [card] Deals
>
> `deal.created`, `deal.updated`, `deal.stage_changed`, `deal.won`, `deal.lost`, `deal.reopened`, `deal.deleted`, `deal.restored`, `deal.merged`, `deal.tag_added`, `deal.tag_removed`, `deal.association_changed`, `deal.line_item_added`, `deal.line_item_updated`, `deal.line_item_removed`

> [card] Products
>
> `product.created`, `product.updated`, `product.archived`, `product.restored`

> [card] Quotes
>
> `quote.created`, `quote.updated`, `quote.published`, `quote.viewed`, `quote.accepted`, `quote.voided`, `quote.deleted`

> [card] Tasks
>
> `task.created`, `task.updated`, `task.completed`, `task.deleted`, `task.restored`

> [card] Meetings
>
> `meeting.booked`, `meeting.rescheduled`, `meeting.cancelled`

> [card] Calls
>
> `call.completed`, `call.transcribed`, `call.analyzed`

> [card] Lists
>
> `list.member_added`, `list.member_removed`

Each event's payload is documented on the [events page](https://docs.useorbitcrm.ai/developers/webhooks/events.md). `GET /api/v1/webhooks/events` returns the same catalog, with `subscribable` telling you which groups the current token may subscribe to.

## Scopes your app needs

| To subscribe to | Request these scopes |
| - | - |
| Anything | `webhooks:write` (and `webhooks:read` to list webhooks and read the delivery log) |
| `submission.*` | `submissions:read` |
| `contact.*` | `contacts:read` |
| `company.*` | `companies:read` |
| `deal.*` | `deals:read` |
| `task.*` | `tasks:read` |
| `meeting.*` | `meetings:read` |
| `call.*` | `dialer:read` |
| `list.*` | `crm:read` |

The read scope is checked twice: when you subscribe (`403 insufficient_scope`, with `required_scopes`, if the token lacks it) and on every delivery, against the app's current installation in that workspace. A workspace that removes a scope from your app, or uninstalls it, stops receiving the matching deliveries without you doing anything. Register the scopes in the [Developer Portal](https://docs.useorbitcrm.ai/apps/portal.md) and request them during [OAuth](https://docs.useorbitcrm.ai/apps/oauth.md).

## Subscribe after install

Once a workspace has authorized your app, create the subscription with its access token. Do it once per workspace, right after the OAuth callback, and keep the returned `id` and `secret` with that workspace's record.

```bash theme={null}
curl -X POST https://app.useorbitcrm.ai/api/v1/webhooks \
  -H "Authorization: Bearer $WORKSPACE_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: install-$INSTALLATION_ID" \
  -d '{
    "url": "https://yourapp.example/orbit/events?workspace='"$INSTALLATION_ID"'",
    "events": ["contact.created", "contact.updated", "deal.created", "deal.stage_changed", "meeting.booked", "call.analyzed"]
  }'
```

```json theme={null}
{
  "data": {
    "id": "f0e1d2c3-...",
    "url": "https://yourapp.example/orbit/events?workspace=...",
    "events": ["contact.created", "contact.updated", "deal.created", "deal.stage_changed", "meeting.booked", "call.analyzed"],
    "is_active": true,
    "form_id": null,
    "created_at": "2026-10-27T14:00:00Z",
    "secret": "a1b2c3...64 hex characters"
  },
  "message": "Webhook created. Save the secret - it will only be shown once."
}
```

* The `Idempotency-Key` makes the call safe to retry; a replay returns the webhook without the secret.
* Your app only ever sees the webhooks it registered. `GET /api/v1/webhooks` with a workspace's token lists yours in that workspace.
* Limits: 5 webhooks per workspace while your app is in development, 100 once it is approved. Creation is rate limited to 60 an hour per app per workspace (500 once approved); the response and a `429` carry `RateLimit-*` headers. One webhook can subscribe to any number of events (`X-Orbit-Event` and the envelope's `event` say which one arrived), so a single subscription per workspace is usually enough, and `PATCH` with `is_active` pauses and resumes it without a delete and a new secret.
* `form_id` limits a webhook to one form's submission events. Every other event is workspace-wide, and a webhook with any of them refuses a form filter.
* `filters` limits deal events to deals in given pipelines or stages: `{ "pipeline_ids": [...], "stage_ids": [...] }`, checked against where the deal is when the event happens (so `deal.stage_changed` is judged by the stage the deal moved to). Other events on the same webhook are not filtered. There are no field filters: subscribe to the specific event you need (`deal.won` rather than `deal.updated`) to keep deliveries down.
* The URL's host is refused only when it points at a private or internal address. A host that does not resolve yet is accepted, so you can register a development endpoint before it is public; deliveries re-check the host every time. Localhost is never reachable from Orbit, so use a tunnel for local testing.
* A replayed create (same `Idempotency-Key`) returns the webhook without its secret. If the original response was lost before you stored the secret, delete that webhook and create it again under a new key.

## Receiving deliveries

Every delivery is a POST with the same envelope:

```json theme={null}
{
  "id": "5f0c1f2e-9c0a-5a3b-8d1e-6b2f0c9a4d11",
  "event": "contact.created",
  "object": "contact",
  "object_id": "7c1d2e3f-...",
  "team_id": "0c2b4d6e-...",
  "occurred_at": "2026-10-27T14:30:05.120Z",
  "timestamp": "2026-10-27T14:30:05.120Z",
  "actor": { "user_id": null, "app_id": null },
  "source": "app",
  "data": {
    "contact_id": "7c1d2e3f-...",
    "contact": { "id": "7c1d2e3f-...", "email": "jane@acme.com", "first_name": "Jane", "last_name": "Doe", "company": "Acme", "status": "active" }
  }
}
```

`team_id` tells you which workspace the event belongs to, so one endpoint can serve every installation. `actor.app_id` is your app's id (`credential.app.id` from `GET /api/v1/me`) when the change came from your own API call, so a sync can skip its own writes; see [Changes your app made](https://docs.useorbitcrm.ai/developers/webhooks/overview.md#changes-your-app-made). `source` is `app` for ordinary activity and `import` for records a bulk import or CRM migration wrote; import events are only delivered once the workspace turned that on (see [Imports](https://docs.useorbitcrm.ai/developers/webhooks/overview.md#imports)), so a customer migrating from another CRM does not flood your app, and when they do turn it on you can tell those deliveries apart. Verify `X-Orbit-Signature` with the secret you stored for that webhook before reading anything else; see [Webhook Security](https://docs.useorbitcrm.ai/developers/webhooks/security.md).

```javascript theme={null}
const crypto = require('crypto');

function verifyWebhook(payload, signature, secret) {
  const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(payload, 'utf8').digest('hex');
  return signature.length === expected.length && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
```

## Delivery, retries and logs

* Submissions and meetings are delivered as they happen; CRM, call and submission-change events within about a minute.
* A failed delivery (timeout, connection error, 5xx or 429) is retried with exponential backoff, up to 3 retries by default. `Retry-After` on a 429 is honoured. Other 4xx responses are final.
* 10 consecutive failed events switch the webhook off. `GET /api/v1/webhooks/:id` shows `is_active`, `consecutive_failures` and `last_error`; `PATCH` with `{ "is_active": true }` turns it back on.
* Deliveries are at-least-once. De-duplicate on `id` (also sent as `X-Orbit-Event-ID`); a retry carries the same one. Order is not guaranteed; use `occurred_at`.
* `GET /api/v1/webhooks/:id/deliveries` lists every attempt, with status codes and errors, for debugging a customer's installation.

## Best practices

> [card] Respond quickly
>
> Return a 2xx within a few seconds and process the event in a queue. The default timeout is 30 seconds.

> [card] Verify signatures
>
> Validate `X-Orbit-Signature` with the webhook's secret before processing.

> [card] Handle retries
>
> Store processed event ids so a retried delivery is a no-op.

> [card] Fetch what you need
>
> Payloads carry the record that changed, not its related records. Use the [API](https://docs.useorbitcrm.ai/developers/api/overview.md) for a deal's contacts, a call's transcript or a submission's form.

> [card] Expect imports to be off
>
> Records written by a CSV import, the API's bulk endpoints or a CRM migration are not delivered unless the workspace's owner turned on **Send imported records to integrations** (Settings → Integrations → Connected apps). If your app needs a customer's existing records, read them through the [API](https://docs.useorbitcrm.ai/developers/api/overview.md) instead of waiting for `contact.created`; when a workspace does turn import delivery on, deliveries carry `"source": "import"`.

> [card] Clean up on uninstall
>
> Deliveries stop on their own when a workspace removes your app, and every token you hold for that workspace is revoked (`401 invalid_token`, refreshes `400 invalid_grant`). Set an **uninstall URL** in the Developer Portal to be told right away: Orbit POSTs `{ "event": "app.uninstalled", "app_id", "client_id", "team_id", "uninstalled_at", "reason" }` there, signed in `X-Orbit-Signature` with your app's signing secret (same HMAC-SHA256 scheme as deliveries), with `X-Orbit-Event: app.uninstalled` and `X-Orbit-Timestamp`. Answer 2xx. A `429`, a `5xx` or no answer in time is retried three times; any other `4xx` is final. `reason` is `workspace_removed` or `app_suspended`. Revoking the last live token you hold for a workspace ends its installation too, without a notification; while another member's token is live, the installation stays.

## Next steps

> [card] [Event payloads](https://docs.useorbitcrm.ai/developers/webhooks/events.md)
>
> Every event's `data`

> [card] [Webhooks API](https://docs.useorbitcrm.ai/developers/api/webhooks.md)
>
> Create, change, delete, and read the delivery log


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.