# Webhooks (/docs/automate/workflow-webhooks)

Webhooks send a signed HTTPS request to your server when something happens in Sift. An endpoint can subscribe to action events, such as a tag added or a status change. A notification rule can also send to an endpoint.

## At a glance [#at-a-glance]

| | |
| --- | --- |
| Available to Super Admins | ✅ |
| Public HTTPS endpoints | ✅ required |
| Subscribe to action events | ✅ tags, status, assignee, work queue, fields, forms, moderation |
| Only for actions matching a saved search | ✅ optional |
| Send from notification rules | ✅ |
| Generated signing secret | ✅ shown once, 24-hour overlap on rotation |
| Delivery log, send test, and redeliver | ✅ |
| Automatic retries for temporary failures | ✅ for up to 24 hours |
| Order of events for one action | ✅ in order per endpoint |
| Exactly-once delivery | ❌ at least once |

## Set it up [#set-it-up]

1. Go to **Settings → Developer access → Webhooks**.
2. Select **New endpoint**. Enter a name and a public HTTPS URL, and pick the events to receive.
3. Copy the signing secret. Sift shows it only once.
4. Optional: open the endpoint and choose a saved search under **Only for actions matching**. Events for other actions are not sent.
5. Select **Send test**. The endpoint must return a status from `200` through `299`.

To send from a rule, open **Settings → Workflows → Notifications**, edit a notification, and select the endpoint under **Webhooks**.

## Events [#events]

| Event | Sent when |
| --- | --- |
| `action.created` | A new action opens |
| `action.tag_added` / `action.tag_removed` | A person, rule, AI, or API call changes a tag |
| `action.status_changed` | The action's status changes |
| `action.assigned` | The assignee changes |
| `action.queue_changed` | The action moves to another work queue |
| `action.field_changed` | A custom field value changes |
| `form.submitted` | An action form is submitted |
| `moderation.decided` | A moderation decision is recorded |
| `workflow.notification` | A notification rule sends to the endpoint |
| `webhook.test` | You select **Send test** |

An event request body looks like this. `change` describes what happened. `action` is the action's state when Sift built the event; a retry sends the same body.

```json
{
  "id": "evt_qy1b3lsr19x0qjoljxgiys9k",
  "type": "action.tag_added",
  "apiVersion": "2026-11-01",
  "occurredAt": "2026-10-28T18:55:19.175Z",
  "orgId": "org_...",
  "data": {
    "action": {
      "id": "act_...",
      "displayId": "ACME-1842",
      "url": "https://app.getsift.ai/app/record/act_...?entity=action",
      "status": "OPEN",
      "platform": "CUSTOM_API",
      "channel": "social-ai-api-DEV",
      "queueId": "queue_...",
      "assigneeId": null,
      "externalId": "post-184",
      "sourceId": "src_...",
      "tags": [{ "id": "tag_...", "name": "Remove post" }],
      "customFields": []
    },
    "change": {
      "tag": { "id": "tag_...", "name": "Remove post" },
      "actor": { "type": "USER", "id": "user_...", "name": "Dana Kim" }
    }
  }
}
```

`apiVersion` is a date. New fields can appear without a new date. A breaking change ships with a new date.

## Rule notification format [#rule-notification-format]

A notification rule sends a JSON envelope. The `data` object contains the workflow notification.

```json
{
  "subjectType": "workflow_webhook",
  "subjectId": "organization",
  "orgId": "org_...",
  "deliveredAt": "2026-09-01T18:02:11.000Z",
  "data": {
    "id": "workflow-notification:...",
    "type": "workflow.notification",
    "version": 1,
    "occurredAt": "2026-09-01T18:02:11.000Z",
    "orgId": "org_...",
    "message": "Optional custom message",
    "workflow": {
      "id": "wf_...",
      "executionId": "wfx_...",
      "triggerId": "trg_...",
      "name": "Escalate buying signals"
    },
    "action": {
      "id": "act_...",
      "easyId": "ACME-1842",
      "status": "OPEN",
      "queueId": "queue_...",
      "assigneeId": null,
      "platform": "x",
      "externalId": "feedback-184",
      "sourceId": "src_feedback",
      "customFields": [
        {
          "fieldName": "decision",
          "displayName": "Decision",
          "fieldType": "string",
          "value": "Remove",
          "provenance": { "source": "human", "at": "2026-09-01T18:01:40.000Z" }
        }
      ]
    }
  }
}
```

`action.customFields` lists the action's custom field values at the time the workflow fired, one entry per value, with the field's current display name and type. Fields with no value are absent. Only fields registered under Settings → Custom fields appear; Sift's own bookkeeping keys never do. `action.externalId` is the item's id in the source system and `action.sourceId` is the Custom API source id; both are `null` for other platforms. A subscriber can act on a tag change without a second read.

`action.status` is the operational status of the action, as shown in the inbox: `OPEN`, `RESPONDED`, `WAITING_FOR_USER`, `RESOLVED`, `CLOSED`, `DISMISSED`, `SNOOZED`, or a custom status token such as `CUSTOM_STATUS_1`. It is `null` when the action has no operational status. `action.easyId` is the action ID with your organization prefix, as shown in the app.

Fields can be absent when they do not apply to the action or workflow. Additive fields can appear in version `1`. Use the `version` field before you depend on a new payload version.

## Verify the signature [#verify-the-signature]

Every request carries [Standard Webhooks](https://www.standardwebhooks.com) headers. Use any Standard Webhooks library with the endpoint's signing secret.

```text
webhook-id: evt_...                 (stable for retries; use it to drop duplicates)
webhook-timestamp: 1793210097       (seconds; reject requests older than five minutes)
webhook-signature: v1,<base64> v1,<base64>
x-sift-event: action.tag_added
x-sift-signature: sha256=<hex>      (HMAC-SHA256 of the body, for receivers built before Standard Webhooks)
```

```js
import { Webhook } from "standardwebhooks"

export function verifySiftWebhook(rawBody, headers, secret) {
  // Throws when the signature or timestamp is wrong.
  return new Webhook(secret).verify(rawBody, headers)
}
```

Verify the raw body before you parse JSON. After you rotate a secret, the old secret also signs for 24 hours, so `webhook-signature` can hold two signatures. Accept the request if one matches.

## Delivery and retries [#delivery-and-retries]

Sift sends an event about a second after the change. Delivery is at least once: use `webhook-id` to drop a duplicate. Events for one action reach an endpoint in the order they happened.

Sift retries network failures, timeouts, `408`, `425`, `429`, and `5xx` responses for up to 24 hours. Other `4xx` responses are terminal. A `410 Gone` turns the endpoint off. Return a `2xx` response only after your system accepts the event.

Each endpoint shows its deliveries for the last days. Select **Redeliver** on a failed event to send the same body again. If every delivery fails for five days, Sift turns the endpoint off and emails your admins. Turn it on again from the endpoint.

## Endpoint safety [#endpoint-safety]

Webhook endpoints must use HTTPS and resolve only to public addresses. Sift rejects URLs with credentials, private or reserved addresses, mixed public and private DNS results, and redirects.

## Tips [#tips]

- Keep the signing secret in your secret manager.
- Respond quickly. Process long work after you return a `2xx` response.
- Use `webhook-id` as the unique key for your handler.
- Turn the endpoint off before planned maintenance if you do not want retries.
- Rotate the secret after a team member or integration loses access.

## Related [#related]

<Cards>
  <Card href="/docs/automate/workflows" title="Workflows">Build ordered automation steps.</Card>
  <Card href="/docs/automate/workflow-triggers" title="Workflow Triggers">Choose when a workflow runs.</Card>
  <Card href="/docs/configure/automation" title="Automation Settings">Manage workflow and notification settings.</Card>
</Cards>
