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

# Outbound Webhooks

> Receive signed HTTP notifications when events happen in your client workspaces, like a captured lead or a handoff request.

Webhooks let your systems react to platform events without polling. When an event happens in one of your client workspaces, SiteSpeak sends a signed POST request to your endpoint.

Manage endpoints under **Agency > Webhooks** in your dashboard, or via the [Agency API](/docs/api-reference/agency/create-webhook).

## Events

| Event                | Fires when                                                        |
| -------------------- | ----------------------------------------------------------------- |
| `lead.captured`      | A visitor leaves contact details in a client workspace's AI agent |
| `escalation.created` | A visitor asks to talk to a human                                 |
| `ping`               | You send a test from the dashboard or API                         |

Each endpoint can subscribe to specific events or receive all of them. Endpoints subscribed to all events also receive event types added in the future.

Two behaviors to know:

* `lead.captured` fires the **first time** a visitor leaves contact details. Repeat submissions and identify calls for the same visitor do not fire it again.
* A handoff request that includes contact details fires `escalation.created` only, not both events. The escalation payload carries the same visitor contact fields.

## Payload

Every request body has the same envelope:

```json theme={null}
{
  "id": "9194bb86-10ab-443c-829c-5c420c475e2f",
  "event": "lead.captured",
  "timestamp": "2026-08-18T11:43:08+00:00",
  "team_id": "8e7a1b4c-8c7c-4029-aa4e-c3bf3c8b7abc",
  "external_reference": "your-billing-id-123",
  "chatbot_id": "8cc9fd44-4c7c-4d5d-aebe-68b03e8236fd",
  "data": {
    "visitor": {
      "id": "a176e7eb-c8bb-4be0-bc1a-608f4377df8d",
      "visitor_id": "wh-visitor-1",
      "email": "customer@example.com",
      "first_name": "Jane",
      "last_name": "Doe",
      "phone": null,
      "custom_data": { "company": "Acme Co" }
    },
    "chatbot_name": "Support Agent"
  }
}
```

* `id` is unique per delivery and stays the same across retries, so you can deduplicate.
* `team_id` is the client workspace the event happened in.
* `external_reference` is the billing reference you set on the client workspace, so you can map events straight to your own billing system.
* `data` is event-specific. `escalation.created` adds `last_message` and `inbox_url`.

## Verifying signatures

Every request includes these headers:

| Header            | Value                                                                                         |
| ----------------- | --------------------------------------------------------------------------------------------- |
| `X-Signature-256` | `sha256=` followed by an HMAC SHA-256 of the raw request body, keyed with your webhook secret |
| `X-Webhook-Event` | The event type                                                                                |
| `X-Delivery-Id`   | The delivery id (same as `id` in the body)                                                    |

Your webhook secret is shown once when you create the endpoint. Verify each request by computing the HMAC over the raw body and comparing:

```javascript Node.js theme={null}
const crypto = require("crypto")

function verify(rawBody, signatureHeader, secret) {
  const expected =
    "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex")
  const received = Buffer.from(signatureHeader ?? "")
  const expectedBuffer = Buffer.from(expected)

  // timingSafeEqual throws on length mismatch, so check length first.
  return (
    received.length === expectedBuffer.length &&
    crypto.timingSafeEqual(received, expectedBuffer)
  )
}
```

```php PHP theme={null}
function verify(string $rawBody, ?string $signatureHeader, string $secret): bool
{
    if ($signatureHeader === null) {
        return false;
    }

    $expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);

    return hash_equals($expected, $signatureHeader);
}
```

Reject requests with a missing or invalid signature.

## Delivery and retries

* Respond with any `2xx` status within 15 seconds to acknowledge a delivery. Redirects are not followed.
* A failed delivery is attempted up to 5 times in total: the initial delivery plus 4 retries after 30 seconds, 2 minutes, 10 minutes, and 1 hour. A delivery that exhausts its attempts gets a last error ending in `(retries exhausted)`.
* The last 30 days of delivery attempts are visible under **Agency > Webhooks** and via the [deliveries endpoint](/docs/api-reference/agency/list-webhook-deliveries), including response status and the last error.
* To change an endpoint's URL or event subscriptions, use the [update endpoint](/docs/api-reference/agency/update-webhook); the signing secret stays the same. Deleting and recreating an endpoint generates a new secret.

Use the **Test** button in the dashboard (or the [test endpoint](/docs/api-reference/agency/test-webhook)) to send a signed `ping` event and confirm your endpoint and signature verification work end to end.

***

<Card title="Ready to automate your customer service with AI?" icon="bot" href="https://sitespeak.ai/register?utm_source=docs&utm_medium=cta&utm_campaign=primary-cta" arrow="true" cta="Create Your AI Agent">
  Join over 1000+ businesses, websites and startups automating their customer service and other tasks with a custom trained AI agent.
</Card>
