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

# Inbound webhook

> Receive HMAC-signed JSON from your own systems and bind blocks to the most recent payloads.

The webhook connector accepts JSON payloads from your own systems. Configure a data source, point your app at the resulting URL with the signing secret, and the platform stores the payloads — bound blocks read them via `webhook.latest`, `webhook.aggregate`, and `webhook.list` queries.

This is the "push from anywhere" connector. Use it when you have a service that emits events (cron job, status checker, Slack workflow) and you want the platform to display the latest values.

<Note>
  Webhook data sources are a **Builder beta** feature. Free workspaces can read this page, but connector creation and refresh tools return `PRO_REQUIRED`.
</Note>

## Add a webhook data source

<Steps>
  <Step title="Go to the wizard">
    `/settings/data-sources/new/webhook`.
  </Step>

  <Step title="Pick a label and save">
    The wizard creates the source and returns a single-use **secret-reveal panel** with the signing secret in plaintext. You'll see it exactly once. Store it in your app's secret manager.
  </Step>

  <Step title="Point your app at the URL">
    The data-sources list shows the receiver URL for each webhook source:

    ```
    https://<convex-deployment>.convex.cloud/webhooks/inbound/<dataSourceId>
    ```

    POST JSON with the signature headers below.
  </Step>
</Steps>

If you lose the secret, click **Rotate secret** on the data-sources list — it re-opens the same one-time-reveal modal with a fresh secret.

## Wire format

Every inbound request needs three things: a signature header, an idempotency key, and a JSON body under 256 KB.

### Headers

| Header                   | Required | Format                                                           |
| ------------------------ | -------- | ---------------------------------------------------------------- |
| `X-Chatblocks-Signature` | yes      | `t=<unix>,v1=<hex>` (Stripe-style; leaves room for a v2 scheme). |
| `Idempotency-Key`        | yes      | Any unique string per logical event. Reject without it (400).    |
| `Content-Type`           | yes      | `application/json`.                                              |

### Signing

The signed payload is `<unix>.<body>`, HMAC-SHA256 with the per-source signing secret, hex-encoded.

```ts theme={null}
import { createHmac } from "node:crypto";

const timestamp = Math.floor(Date.now() / 1000).toString();
const body = JSON.stringify({ value: 42, label: "Active sessions" });
const signature = createHmac("sha256", secret)
  .update(`${timestamp}.${body}`)
  .digest("hex");

await fetch(url, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
    "X-Chatblocks-Signature": `t=${timestamp},v1=${signature}`,
  },
  body,
});
```

### Limits and policies

| Constraint             | Value                                                 |
| ---------------------- | ----------------------------------------------------- |
| Body size              | 256 KB                                                |
| Replay window          | 5 minutes                                             |
| Clock skew tolerance   | ±60 seconds                                           |
| Idempotency-Key        | required; dedup via `webhookEvents`                   |
| Storage cap per source | 1000 most recent payloads (enforced inline on insert) |

Signature verification uses Web Crypto (`crypto.subtle.importKey` + `subtle.verify`) — Convex's V8 runtime constraint. Reject 401 on signature mismatch, 400 on missing/malformed headers, 413 on oversize body.

## Query catalog

Three first-class queries against stored payloads (see `apps/web/convex/lib/connectors/webhookQueries.ts`):

<AccordionGroup>
  <Accordion title="webhook.latest">
    Returns the most recent payload's body. No params.

    Result: the raw JSON body of the latest payload (or `null` if none).

    Use for "show me what just happened."
  </Accordion>

  <Accordion title="webhook.aggregate">
    Folds numeric values across payloads in a time window.

    ```json theme={null}
    {
      "type": "webhook.aggregate",
      "params": {
        "window": "1h",
        "field": "value",
        "op": "sum"
      }
    }
    ```

    `window` is one of `"15m"`, `"1h"`, `"24h"`, `"7d"`. `op` is `"sum"`, `"count"`, or `"avg"`. Numeric coercion via `Number(x)`; `null` and `NaN` values are skipped.

    Result:

    ```ts theme={null}
    { value: number; count: number; window: string; op: string }
    ```
  </Accordion>

  <Accordion title="webhook.list">
    Returns the N most recent payloads.

    ```json theme={null}
    {
      "type": "webhook.list",
      "params": {
        "limit": 10,
        "sortBy": "timestamp",
        "order": "desc"
      }
    }
    ```

    `sortBy` is an optional in-memory JSONPath sort over the payloads; `order` is `"asc"` or `"desc"`. Default order: most recent first.
  </Accordion>
</AccordionGroup>

## Bind a block

```json theme={null}
{
  "binding": {
    "dataSourceId": "<webhook-data-source-id>",
    "queryConfig": {
      "type": "webhook.aggregate",
      "params": { "window": "1h", "field": "errors", "op": "sum" }
    },
    "projection": {
      "fields": {
        "value": { "source": "value", "format": "number" },
        "label": { "literal": "Errors (1h)" }
      }
    }
  },
  "widget": {
    "template": "metric",
    "data": { "value": "0", "label": "Errors (1h)" }
  }
}
```

The aggregator runs the query, applies the projection, writes `widget.data`. On every inbound POST the receiver schedules a refresh — push-mode by default.

## Rotating and deleting

* **Rotate secret** — generates a fresh signing secret and shows it once. Old secret stops validating immediately.
* **Delete source** — sweeps the source's payloads and event-dedupe rows in addition to the source row itself. Bound blocks lose their data feed; the binding stays on the manifest until you update the block.

## What's next

<CardGroup cols={2}>
  <Card title="Outbound MCP" icon="plug" href="/connecting-data/outbound-mcp">
    Connect to a third-party MCP server.
  </Card>

  <Card title="Audit log" icon="scroll" href="/concepts/audit-log">
    Every payload + decrypt is logged with the calling actor.
  </Card>
</CardGroup>
