---
title: "Data webhooks"
subtitle: "Send spans, traces, scores, and billing events to your own endpoint."
---

A data webhook sends a JSON `POST` to your URL each time an event happens in Respan, such as a span being logged or your credit balance running low. Use it to copy spans into your own warehouse or to trigger alerts in your systems.

<Note>
To get alerts when a metric crosses a threshold, use a [monitor](/docs/documentation/features/monitoring/monitors) instead. Monitors have their own webhook destinations.
</Note>

## Create a webhook

<Steps>
<Step title="Open Webhooks">

Go to **Settings** › **Integrations**, open the **Webhooks** tab, and click **Create webhook**.

</Step>
<Step title="Fill in the webhook">

Enter a **Name** and the full endpoint **URL**. Pick an **Event type**, choose the **API key** whose traffic it covers, and click **Create**.

<img src={"/docs/images/admin/webhooks-create-light.png"} width="420" className="block dark:hidden" alt="The Create webhook dialog with Name, URL, Event type set to span, and API key set to Acme default" />
<img src={"/docs/images/admin/webhooks-create-dark.png"} width="420" className="hidden dark:block" alt="The Create webhook dialog with Name, URL, Event type set to span, and API key set to Acme default" />

</Step>
<Step title="Copy the secret">

The dialog now shows the **Webhook secret**. Copy it and store it on your server, so you can verify deliveries.

To see it again later, click the webhook and click the eye icon next to **Webhook secret**. The refresh icon rotates it, and the old secret stops working right away.

</Step>
</Steps>

Use the **Status** switch in the list to pause a webhook. To delete one, open the row's menu and click **Delete**.

## Event types

| Event type | Sent when | `event_type` in the payload |
| --- | --- | --- |
| **span** | A span is logged | `request_log` |
| **completed trace** | A trace finishes | `trace_completed` |
| **score** | An evaluator score is recorded | `on_eval_result_ingested` |
| **User budget limit reached** | One of your users goes over their budget | `customer_budget_limit_reached` |
| **Credit low balance threshold reached** | Your credit balance drops below the warning amount on [Billing](/docs/documentation/admin/credits-billing#set-a-low-balance-warning) | `credit_low_balance_threshold_reached` |

Events that come from requests only include traffic sent with the webhook's **API key**. Create one webhook per event type.

## What Respan sends

Each delivery is a `POST` with a JSON body: the event type, plus the event's data. For a span, `data` is the span as Respan logged it, with fields such as `model`, `prompt_tokens`, `completion_tokens`, `cost`, `latency`, and `customer_identifier`.

```json
{
  "event_type": "credit_low_balance_threshold_reached",
  "data": {
    "organization_id": "5f1d2c7e-8a41-4d07-9c52-3b6e95a0c1d4",
    "organization_name": "Acme",
    "current_balance": 48.72,
    "warning_threshold": 50.0,
    "timestamp": "2026-09-30T17:24:05.312481+00:00"
  }
}
```

Each request carries these headers:

| Header | Value |
| --- | --- |
| `X-Keywordsai-Signature` | Hex HMAC-SHA256 of the raw request body, keyed with your webhook secret |
| `Keywordsai-Timestamp` | When the delivery was sent, in Unix seconds |
| `X-Signature-Secret-Type` | `wh_secret` |

Check the signature against the raw body before you parse it:

<Tabs>
<Tab title="Python">
```python
import hashlib
import hmac
import os

def is_from_respan(raw_body: bytes, signature: str) -> bool:
    secret = os.environ["RESPAN_WEBHOOK_SECRET"].encode()
    expected = hmac.new(secret, raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)
```
</Tab>
<Tab title="TypeScript">
```typescript
import { createHmac, timingSafeEqual } from "node:crypto";

export function isFromRespan(rawBody: Buffer, signature: string): boolean {
  const expected = createHmac("sha256", process.env.RESPAN_WEBHOOK_SECRET!)
    .update(rawBody)
    .digest("hex");
  return (
    expected.length === signature.length &&
    timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
  );
}
```
</Tab>
</Tabs>

## Delivery

- Return a `2xx` within 30 seconds. Queue slow work and respond first.
- Failed deliveries aren't retried. After 5 failures in a row, Respan pauses the webhook for 5 minutes and emails your organization's owner. Events during the pause are dropped.
- The URL must be publicly reachable. Private and internal addresses are blocked.
- A Slack incoming webhook URL (`hooks.slack.com`) gets a formatted Slack message instead of the raw JSON.
