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

# Webhooks

Webhooks send a notification after a Planck request completes. Use them to
score responses, feed an analytics pipeline, or trigger another workflow.

## Delivery model

Planck uses a durable PostgreSQL outbox and delivers webhooks asynchronously.
A successful gateway request does not wait for your webhook endpoint.

* Delivery is **at least once**. Deduplicate with `Planck-Delivery-Id`.
* A delivery is retried for timeouts, HTTP 408, 425, 429, and 5xx responses.
* Redirects are not followed. Configure the final HTTPS endpoint directly.
* Other 4xx responses are treated as permanent failures.
* Each delivery attempt can run for up to two minutes.

Return a 2xx response quickly after durably accepting the event, then do slow
work from your own queue.

## Quick start

<Steps>
  <Step title="Create an HTTPS receiver">
    Your endpoint must accept `POST` requests over HTTPS. Preserve the raw request
    bytes so you can verify the HMAC signature before parsing JSON.
  </Step>

  <Step title="Add the webhook">
    Open the [webhooks page](https://us.inquantum.ai/webhooks), enter the endpoint,
    choose a sample rate, and optionally add property filters.

    You can also use the [REST API](/rest/webhooks/post-v1webhooks).
  </Step>

  <Step title="Verify the signature">
    Copy the HMAC key shown in the dashboard. The `Planck-Signature` header is the
    hex-encoded HMAC-SHA256 of the exact HTTP body bytes.

    ```javascript theme={null}
    import crypto from "node:crypto";
    import express from "express";

    const app = express();

    app.post(
      "/webhooks/planck",
      express.raw({ type: "application/json" }),
      async (req, res) => {
        const rawBody = req.body;
        const signature = req.header("Planck-Signature") ?? "";
        const expected = crypto
          .createHmac("sha256", process.env.PLANCK_WEBHOOK_SECRET)
          .update(rawBody)
          .digest("hex");

        const valid =
          /^[0-9a-f]{64}$/i.test(signature) &&
          crypto.timingSafeEqual(
            Buffer.from(expected, "hex"),
            Buffer.from(signature, "hex")
          );

        if (!valid) return res.status(401).send("invalid signature");

        const event = JSON.parse(rawBody.toString("utf8"));
        await queueEvent({
          deliveryId: req.header("Planck-Delivery-Id"),
          event,
        });
        return res.status(202).send("accepted");
      }
    );
    ```

    <Warning>
      Do not verify a re-serialized object such as `JSON.stringify(req.body)`. Even
      equivalent JSON can produce different bytes and fail signature validation.
    </Warning>
  </Step>
</Steps>

## Configuration

| Setting               | Meaning                                                            | Default  |
| --------------------- | ------------------------------------------------------------------ | -------- |
| Destination URL       | Public HTTPS URL that receives deliveries                          | Required |
| Sample rate           | Percentage of matching requests to send, from 0 to 100             | 100%     |
| Include enhanced data | Adds model, provider, metrics, and a full-body URL when one exists | Enabled  |
| Property filters      | Sends only when every configured key/value matches                 | None     |

Property names created from `Planck-Property-*` headers are lowercase. For
example, this request property:

```http theme={null}
Planck-Property-Environment: production
```

matches a webhook filter with key `environment` and value `production`.

## Payload

Small request and response bodies remain inline as their original JSON values:

```json theme={null}
{
  "request_id": "550e8400-e29b-41d4-a716-446655440000",
  "user_id": "user-123",
  "request_body": {
    "model": "gpt-4o-mini",
    "messages": [{ "role": "user", "content": "Hello" }]
  },
  "response_body": {
    "choices": [{ "message": { "role": "assistant", "content": "Hi" } }]
  },
  "model": "gpt-4o-mini",
  "provider": "OPENAI",
  "metadata": {
    "cost": 0.0015,
    "promptTokens": 10,
    "completionTokens": 15,
    "totalTokens": 25,
    "latencyMs": 1200
  }
}
```

Request and response bodies larger than 10 KB are represented by a truncation
message. When enhanced data is enabled, `request_response_url` is also present
and points to the complete S3-backed record:

```json theme={null}
{
  "request_id": "550e8400-e29b-41d4-a716-446655440000",
  "request_body": "Body too large for webhook; fetch the full request and response from request_response_url",
  "response_body": { "choices": [] },
  "request_response_url": "https://object-storage.example/..."
}
```

`request_response_url` is conditional, not present on every webhook. It is
generated only when the completed interaction was externalized to S3. The URL
expires after two hours by default; fetch it as soon as you accept the event.

<Warning>
  For organizations using application-level request/response encryption, the
  direct object-storage URL contains ciphertext rather than readable JSON. The
  dashboard and MCP server use the authenticated request API to decrypt these
  records. Full-body webhook retrieval for encrypted records requires a
  separate signed decryption endpoint and is not currently available.
</Warning>

## Storage behavior

Requests do not all go to S3. Planck stores searchable metadata and small
bodies in ClickHouse. It externalizes request/response data above 10 KB and
inline image assets to private object storage, while ClickHouse retains the
storage reference. This keeps ordinary requests queryable without creating an
object for every call and preserves a complete source for oversized webhook
bodies.

Use `Planck-Omit-Request: true` or `Planck-Omit-Response: true` when a body
must not be retained.

## Delivery headers

| Header                    | Description                                 |
| ------------------------- | ------------------------------------------- |
| `Planck-Signature`        | HMAC-SHA256 of the exact body bytes         |
| `Planck-Delivery-Id`      | Stable delivery ID to use for deduplication |
| `Planck-Delivery-Attempt` | One-based attempt number                    |

The dashboard's **Test** action sends a synchronous synthetic payload to check
URL reachability and HMAC handling. It does not exercise the production outbox
and retry worker. Follow the [production canary procedure](/features/webhooks-testing#production-end-to-end-canary)
for a full-path test.

## Related features

<CardGroup cols={2}>
  <Card title="Local and production testing" icon="laptop" href="/features/webhooks-testing">
    Test the receiver, HMAC verification, and durable delivery path
  </Card>

  <Card title="Custom properties" icon="tag" href="/features/advanced-usage/custom-properties">
    Control webhook delivery with request properties
  </Card>

  <Card title="Scores" icon="star" href="/features/advanced-usage/scores">
    Score LLM responses for quality monitoring
  </Card>

  <Card title="User metrics" icon="chart-line" href="/features/advanced-usage/user-metrics">
    Track per-user usage patterns and costs
  </Card>
</CardGroup>

***

<Accordion title="Need more help?">
  Additional questions or feedback? Reach out to
  [help@inquantum.ai](mailto:help@inquantum.ai) or [schedule a
  call](https://inquantum.ai/contact) with us.
</Accordion>
