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

# Webhooks overview

> Get notified on your own server when things happen in a workspace.

Webhooks let Audienceful notify **your** server when things happen in a workspace. Each webhook endpoint is an HTTPS URL subscribed to one or more events. Deliveries are HMAC-signed and retried with backoff.

Managing endpoints requires the `webhooks:read` scope (to read) and `webhooks:write` scope (to create, update, or delete).

<Steps>
  <Step title="Create an endpoint">
    [Create a webhook endpoint](/api-reference/webhooks/create) with your HTTPS URL and the events you care about. The response includes the signing `secret` — this is the **only** time it's returned, so store it.
  </Step>

  <Step title="Verify the signature">
    On each incoming request, verify the `X-Audienceful-Signature` header against the raw request body using your secret. See [Verifying signatures](/api-reference/webhooks/verify-signatures) for the scheme and code examples.
  </Step>

  <Step title="Respond 2xx">
    Return a `2xx` status to acknowledge the delivery. Non-2xx responses are retried.
  </Step>
</Steps>

## Available events

Fetch the current event vocabulary from [`GET /webhook-events`](/api-reference/webhooks/events). Events are grouped by area:

**Contacts**

| Event                 | Fires when                                                                                                                                                                                          |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `person.created`      | A new contact is added to your workspace.                                                                                                                                                           |
| `person.updated`      | A contact's details change — email, name, custom fields, subscription status, etc. Automatic engagement counters (open/click counts, last activity) are **excluded**, so ingestion never spams you. |
| `person.deleted`      | A contact is deleted from your workspace.                                                                                                                                                           |
| `person.unsubscribed` | A contact unsubscribes or is marked unsubscribed (the `unsubscribed` state goes from unset to set). A single unsubscribe also emits `person.updated` — subscribe to whichever you want.             |

**Email**

| Event             | Fires when                                                                                                               |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `bulk_email.sent` | A bulk email finishes sending. Exactly **one** summary event per send, with the subject and a summary of the recipients. |

**Audiences**

| Event                     | Fires when                    |
| ------------------------- | ----------------------------- |
| `audience.member_added`   | A contact enters an audience. |
| `audience.member_removed` | A contact leaves an audience. |

<Note>
  **Real-time changes only.** Events fire for real-time, per-contact activity — a form signup, an API call, a manual edit, a single delete. High-volume **bulk** operations are intentionally webhook-silent so a large job can't emit tens of thousands of deliveries at once:

  * CSV imports and bulk upserts don't fire `person.created`, `person.updated`, or `audience.member_*`.
  * Mass deletes don't fire `person.deleted`.
  * A full-audience resync (e.g. after an import) doesn't fire `audience.member_added` / `audience.member_removed`.
</Note>

<Info>
  `person.updated` replaces the former `person.changed` event, and now fires on **any** meaningful change to a contact (not just audience-relevant fields). Existing endpoints subscribed to `person.changed` were migrated automatically.
</Info>

## Payload format

Every delivery is JSON with an `event` name and a `data` object:

```json theme={null}
{
  "event": "person.created",
  "data": { "...": "..." }
}
```

The shape of `data` depends on the event.

<AccordionGroup>
  <Accordion title="person.created · person.updated · person.unsubscribed">
    These three events share the same contact payload. `extra_data` is keyed by each custom field's `data_name`.

    ```json theme={null}
    {
      "event": "person.updated",
      "data": {
        "email": "person@example.com",
        "created_at": "2026-07-04T12:00:00Z",
        "extra_data": { "plan": "pro" },
        "audiences": ["Newsletter"],
        "tags": ["vip"],
        "double_opt_in": "not_required"
      }
    }
    ```
  </Accordion>

  <Accordion title="person.deleted">
    The deleted contact is gone, so the payload is a minimal identifier.

    ```json theme={null}
    {
      "event": "person.deleted",
      "data": {
        "id": "jQKdwqp3YRRtTrwqUJEp7d",
        "email": "person@example.com",
        "workspace_id": "wRk2sPq7Yb1nKq8Zs4pLm"
      }
    }
    ```
  </Accordion>

  <Accordion title="bulk_email.sent">
    A summary of the completed send. `recipients` is the recipient **count**. `draft` is `null` for sends with no associated draft (e.g. SMS).

    ```json theme={null}
    {
      "event": "bulk_email.sent",
      "data": {
        "task_id": "tSk3nKq8Zs4pLm9vRb2xJc",
        "sent_at": "2026-07-04T12:00:00Z",
        "subject": "Our July newsletter",
        "preheader": "What's new this month",
        "recipients": 4820,
        "audiences": ["Newsletter"],
        "excluded_audiences": ["Bounced"],
        "draft": {
          "id": "dRa3ftPq7Yb1nKq8Zs4pLm",
          "title": "July Newsletter",
          "description": "Monthly update"
        }
      }
    }
    ```
  </Accordion>

  <Accordion title="audience.member_added · audience.member_removed">
    One delivery per contact–audience pair. If a single recompute changes several audiences for a contact, you receive one delivery per audience.

    ```json theme={null}
    {
      "event": "audience.member_added",
      "data": {
        "person": {
          "id": "jQKdwqp3YRRtTrwqUJEp7d",
          "email": "person@example.com"
        },
        "audience": {
          "id": "aUd3nKq8Zs4pLm9vRb2xJc",
          "name": "Newsletter"
        }
      }
    }
    ```
  </Accordion>
</AccordionGroup>

## Delivery headers

Every delivery carries these headers:

```
Content-Type: application/json
X-Audienceful-Event: person.created
X-Audienceful-Delivery-Id: wYt3nKq8Zs4pLm9vRb2xJc
X-Audienceful-Signature: t=1751630400,v1=<hex hmac-sha256>
```

Always [verify the signature](/api-reference/webhooks/verify-signatures) before trusting a delivery's contents — see that page for the scheme and code examples in Python, TypeScript, and PHP.

## Delivery, retries & auto-disable

* Deliveries are retried on any non-2xx response or network error, with exponential backoff — roughly `30s, 1m, 2m, 4m, 8m, …` (each interval jittered ±15% and capped at 4 hours), up to **8 retries** over about 2 hours.
* Respond `2xx` to acknowledge. Respond `410 Gone` to permanently unsubscribe — the endpoint is disabled with `disabled_reason: "410_gone"`.
* After **20** consecutive failed deliveries (each counted only once its retries are exhausted) an endpoint auto-disables with `disabled_reason: "too_many_failures"`. Re-enable it with [`PATCH`](/api-reference/webhooks/update) `{ "is_active": true }`, which also clears its failure state.
* You can inspect recent attempts via the [delivery log](/api-reference/webhooks/deliveries) and send a test event with [ping](/api-reference/webhooks/ping). Delivery log rows are retained for 30 days.

## Delivery rate limit

Each endpoint is capped at **10 deliveries per second**. When a burst of events exceeds that (say, a few hundred contacts edited in quick succession), the excess deliveries are automatically **re-queued and delivered a moment later** — they are never dropped. Being throttled does **not** count as a failed attempt, so a busy endpoint can never trip the auto-disable threshold on rate limiting alone. Sustained throughput averages the cap.

## Automation "Send Webhook" action

Automations can include a **Send Webhook** step. When a contact reaches that step, Audienceful POSTs to the configured URL. This is separate from the webhook *endpoints* described here (it fires from inside an automation, not from an event subscription), but uses the same signing scheme.

```json theme={null}
{
  "event": "automation.webhook",
  "automation": { "id": "auto_...", "name": "Welcome series" },
  "person": {
    "id": "jQKdwqp3YRRtTrwqUJEp7d",
    "email": "person@example.com",
    "extra_data": { "plan": "pro" }
  },
  "event_properties": { "...": "..." }
}
```

Any keys configured on the action's extra payload are merged in at the top level. The request carries the same `X-Audienceful-Signature` and `X-Audienceful-Event` (`automation.webhook`) headers, signed with the action's own secret (shown in the action config) — [verify it](/api-reference/webhooks/verify-signatures) the same way. Delivery is fire-and-forget with retries: a slow or failing endpoint never blocks or stalls the contact's progress through the automation.
