# Webhooks

> Receive HMAC-signed HTTP callbacks from AY Inbox when something happens, such as a reply landing or a campaign completing, and update a CRM without polling.

Source: https://docs.ayinbox.com/guides/webhooks/

Webhooks send a signed HTTP POST to your own URL when something happens in your workspace, such as a reply landing or a campaign completing. Use them to update your CRM or trigger other tools without polling the API.

Add a webhook [#add-a-webhook]

<Steps>
  <Step>
    Open **Settings -> Webhooks** and click **Add endpoint**.
  </Step>

  <Step>
    Enter the public HTTPS URL where AY Inbox should POST events.
  </Step>

  <Step>
    Choose the events to subscribe to, or leave the list empty to receive all standard events.
  </Step>

  <Step>
    Click **Create** and copy the signing secret right away. It is shown only once.
  </Step>

  <Step>
    Answer AY Inbox's test request with any `2xx` to verify the endpoint, and real events start.
  </Step>
</Steps>

Good to know [#good-to-know]

* An unverified endpoint receives only a challenge request, not real events.
* High-volume **firehose** events (campaign sends, opens, clicks, inbox syncs) must be added explicitly.
* Check the `X-Ayinbox-Signature` header on every delivery against the raw request body.
* A failed delivery is retried for up to 8 attempts, then marked abandoned.
* Every attempt is logged on the endpoint's **Deliveries** tab, where a failed one can be redelivered.

<Fold>
  What webhooks are for [#what-webhooks-are-for]

  Browsing filters, searches and view choices are [remembered in your browser tab](/guides/browsing/) across refreshes and navigation, separately for each user and workspace.

  Webhooks let external systems react to AY Inbox activity without polling the API. When an event fires (a reply lands, a campaign completes, a meeting is booked), AY Inbox sends a signed HTTP POST to every matching endpoint your workspace has registered. From there you can update your CRM, trigger a Slack message, or hand off to any other tool.

  If you prefer an in-product alternative that works behind a firewall (no public URL required), the [automations](/guides/automations/) Fire Event step publishes custom events to the realtime socket instead.
</Fold>

<Fold>
  Setting up a webhook [#setting-up-a-webhook]

  Open **Settings -> Webhooks** in the dashboard and click **Add endpoint**.

  1. Enter the HTTPS URL where AY Inbox should POST events. The URL must be publicly routable. Private, loopback, and link-local addresses are rejected.
  2. Optionally add a description so you can tell endpoints apart later.
  3. Choose which events to subscribe to. Leave the list empty to receive all standard events (high-volume firehose events are opt-in separately, see below).
  4. Click **Create**. The signing secret is shown exactly once. Copy it now and store it somewhere safe (a secrets manager or environment variable). It is never shown again. Use **Rotate secret** if you need a new one later.

  The secret is encrypted at rest with the instance's credential key, the same one that protects mailbox passwords, and is decrypted only when a delivery is signed. It is never returned by the API after creation, and never shown in the admin panel.

  After creation the endpoint is **unverified** and will receive only a challenge request, not real events.
</Fold>

<Fold>
  Verifying your endpoint [#verifying-your-endpoint]

  AY Inbox sends a `POST` to your URL with `X-Ayinbox-Event: webhook.test` immediately after creation (and whenever you click **Send test** in the dashboard or call `POST /webhooks/{id}/verify`). The body is:

  ```json
  {
    "event_type": "webhook.test",
    "data": {
      "challenge": "whcg_...",
      "endpoint_id": "...",
      "message": "This is an AY Inbox webhook verification request."
    }
  }
  ```

  Respond with any `2xx` to confirm reachability. That is all that is required for standard dashboard-registered endpoints: AY Inbox marks the endpoint verified and begins delivering real events.

  If you are building an OAuth app that registers endpoints on behalf of users, you must also prove URL ownership by echoing the `challenge` value back, either in the response body (`{ "challenge": "whcg_..." }`) or in a `X-Ayinbox-Webhook-Challenge` response header.

  **Changing an endpoint's URL host re-arms verification.** The endpoint goes back to unverified and must pass the challenge again.
</Fold>

<Fold>
  Selecting events [#selecting-events]

  The event picker in Settings shows the full catalog grouped by category. A few things to keep in mind:

  * Leaving the list empty subscribes to all standard (non-firehose) events.
  * High-volume events (campaign sends, opens, clicks, inbox syncs) are marked **firehose** and must be added explicitly. Subscribe to them only if your receiver can handle the sustained volume.
  * You can update the event list at any time without affecting the signing secret or verification state.

  The complete event catalog is available programmatically at `GET /webhooks/event-types`. See the [webhooks API reference](/api/reference/webhooks/) for the full list.
</Fold>

<Fold>
  Reading the delivery log [#reading-the-delivery-log]

  Every delivery attempt is logged. Open an endpoint in Settings -> Webhooks and go to the **Deliveries** tab to see:

  * status (`delivered`, `failed`, `abandoned`, `pending`)
  * the event type and the timestamp
  * the HTTP response code and a snippet of the response body
  * how many attempts have been made

  You can also query deliveries across all endpoints with `GET /webhooks/deliveries`, or scope it to one endpoint with `GET /webhooks/{id}/deliveries`. Both support filtering by status and event type, and return opaque cursor pagination.

  If an event was throttled rather than delivered, it appears in `GET /webhooks/throttle-drops` instead of the delivery log.
</Fold>

<Fold>
  Redelivering a failed event [#redelivering-a-failed-event]

  In the dashboard, open any failed delivery and click **Redeliver**. Programmatically, call `POST /webhooks/deliveries/{deliveryId}/redeliver`. Redelivery re-enqueues the event with its original payload, and the same `X-Ayinbox-Event-Id` value, so your idempotency logic will dedupe it correctly if the original delivery eventually arrived.
</Fold>

<Fold>
  Verifying signatures in your receiver [#verifying-signatures-in-your-receiver]

  Every delivery carries an `X-Ayinbox-Signature` header in the form `t=<unix>,v1=<hex>`. Verify it against the **raw request body** (before any JSON parsing):

  ```python
  import hashlib, hmac, time

  def verify(secret: str, header: str, raw_body: bytes) -> bool:
      parts = dict(p.split("=", 1) for p in header.split(","))
      t, v1 = parts.get("t"), parts.get("v1")
      if not t or not v1:
          return False
      if abs(time.time() - int(t)) > 300:
          return False  # reject replays older than 5 minutes
      key = secret.encode()  # your whsec_... value as bytes
      signed = f"{t}.".encode() + raw_body
      expected = hmac.new(key, signed, hashlib.sha256).hexdigest()
      return hmac.compare_digest(expected, v1)
  ```

  Key points:

  * Use constant-time comparison (`hmac.compare_digest` or equivalent) to avoid timing attacks.
  * Reject deliveries whose timestamp is more than 5 minutes from now to limit replay windows.
  * Only accept schemes you recognize (`v1`). Ignore any unknown scheme prefix.
  * Use `X-Ayinbox-Event-Id` (or the payload `id` field) as an idempotency key. The same event id may arrive more than once if a retry fires before AY Inbox receives your `2xx`.
</Fold>

<Fold>
  Retries and auto-disable [#retries-and-auto-disable]

  If your endpoint returns a non-`2xx` response (or times out), AY Inbox retries with exponential backoff: 30 seconds, 1 minute, 2 minutes, 4 minutes, and so on, capped at 1 hour between attempts, for up to 8 attempts. After 8 failed attempts the delivery is marked abandoned.

  Redirects are never followed. `3xx` responses are treated as failures.

  If an endpoint fails continuously over a sustained window, AY Inbox auto-disables it to protect both your downstream systems and any third-party receiving infrastructure. Re-enable it in Settings once the receiver is healthy, then use the delivery log to redeliver any abandoned events you need to recover.
</Fold>

<Fold>
  OAuth app allowed domains [#oauth-app-allowed-domains]

  If your OAuth app registers webhook endpoints on behalf of its users, it must declare the allowed domains in `allowed_webhook_domains` when creating or updating the app (see [OAuth](/api/oauth/)). Any endpoint URL your app registers must have a host within that list. An empty list means webhook registration is blocked for the app.

  Domain matching is subdomain-inclusive when you prefix the entry with a dot: `.acme.com` matches `acme.com`, `hooks.acme.com`, and any deeper subdomain. A bare entry like `acme.com` matches that exact host only.
</Fold>

What comes next [#what-comes-next]

* [Webhooks API reference](/api/reference/webhooks/) for the full endpoint, payload, and delivery log spec.
* [Automations](/guides/automations/) if you want to react to events inside AY Inbox without a public URL.
* [Integrations](/guides/integrations/) for connecting third-party CRM, Slack, and automation tools.
* [Get notified of positive replies](/api/recipes/positive-reply-webhook/) for a worked example on the reply event.
