---
title: Webhook Integration
description: Configure Plainterms webhooks, verify signatures, handle events,
  and debug deliveries.
editUrl: true
head: []
template: doc
sidebar:
  order: 1
  label: Human docs
  hidden: false
  attrs: {}
pagefind: true
draft: false
---

Plainterms sends webhook events when quote documents and public landing pages
change state. This guide is written for people setting up the integration:
where to configure the endpoint, what events to expect, how to verify the
signature, and how to debug failed deliveries.

For coding agents, use [`/llms.txt`](/llms.txt) first. The agent index is
shorter and more contract-oriented than this human guide.

## Integration Flow

1. Add a public receiver URL in Plainterms.
2. Copy the signing secret once and store it in your receiver environment.
3. Accept `POST` requests with JSON bodies.
4. Verify `Webhook-Signature` against the exact raw request body.
5. Deduplicate work with `Webhook-Id`.
6. Return a `2xx` only after the event is accepted or durably queued.
7. Use the Plainterms delivery log to inspect attempts, errors, and responses.

![Plainterms Webhooks settings](/screenshots/webhooks-overview.png)

## Configure A Webhook

Plainterms webhook configuration lives in the app under **Settings -> Webhooks**.
The user adding the endpoint must be an administrator.

1. Open **Settings -> Webhooks**.
2. Enter your public webhook endpoint URL.
3. Select **Add Webhook**.
4. Copy the signing secret when Plainterms shows it.
5. Store the secret as an environment variable in your receiver.
6. Leave the webhook enabled, or use the toggle to pause deliveries without
   deleting the endpoint.

Use HTTPS for production endpoints. Keep the receiver fast: Plainterms treats
slow receivers, network errors, and non-`2xx` responses as failed delivery
attempts.

## Event Types

Plainterms currently sends four event names.

| Event | When it is sent | Human meaning |
| --- | --- | --- |
| `document.captured` | A quote document is captured. | A new document is available for downstream systems. |
| `document.approved` | A quote document is approved. | The document passed the approval step. |
| `landing_page.opened` | A visitor opens a public landing page. | The quote or bundle page was viewed. |
| `landing_page.clicked` | A visitor clicks the landing-page action. | Interest was expressed; this is not a bind or payment event. |

All payloads include an `event` name and ISO 8601 `timestamp`.

### Document Events

Document events include `document_id`, `organization_id`, extracted quote data
when available, and a best-effort normalized `value`.

When Plainterms can grant a branded PDF download, document events can also
include:

- `download_url`
- `download_url_expires_at`

Treat `download_url` as a reusable bearer link until expiry. Do not put it in
logs, tickets, screenshots, or chat transcripts.

### Landing Page Events

`landing_page.opened` includes the landing-page identifiers and request context:

- `organization_id`
- `magic_link_id`
- `target_type`
- `document_id`
- `bundle_id`
- `landing_page_url`
- `user_agent`

For bundle links, `document_id` can be `null`. For document links, `bundle_id`
can be `null`. `landing_page_url` and `user_agent` can also be `null` when
unavailable.

`landing_page.clicked` is an interest signal. It includes the same landing-page
identifiers plus selected add-on information when applicable. For
document-backed click events, Plainterms can include the same temporary branded
PDF download fields used by document events.

## Verify Signatures

Every delivery includes these headers:

| Header | Purpose |
| --- | --- |
| `Webhook-Signature` | `sha256=<hex hmac>` signature for the raw body. |
| `Webhook-Timestamp` | Unix timestamp in seconds. Metadata, not part of the HMAC input. |
| `Webhook-Id` | Stable delivery event ID for idempotency. |
| `Content-Type` | Usually `application/json`. |

Compute an HMAC SHA-256 digest over the exact raw request body bytes using the
signing secret shown when the webhook was created. Compare it to the hex value
after `sha256=`. Do not verify against parsed and reserialized JSON.

```js
import { createHmac, timingSafeEqual } from 'node:crypto';

function headerValue(headers, name) {
  const value = headers[name] ?? headers[name.toLowerCase()];
  return Array.isArray(value) ? value[0] : value;
}

export function verifyPlaintermsWebhook(rawBody, headers, secret) {
  const signature = headerValue(headers, 'webhook-signature') ?? '';
  const prefix = 'sha256=';
  if (!signature.startsWith(prefix)) return false;

  const received = Buffer.from(signature.slice(prefix.length), 'hex');
  const expected = Buffer.from(
    createHmac('sha256', secret).update(rawBody).digest('hex'),
    'hex',
  );

  return received.length === expected.length && timingSafeEqual(received, expected);
}
```

Current Plainterms signatures cover the raw request body only. The timestamp is
useful as a freshness sanity check, but it is not cryptographic replay
protection. Use `Webhook-Id` as the idempotency key for side effects.

## Delivery Behavior

Plainterms starts webhook delivery after document capture, document approval,
landing-page open, and landing-page click events. Delivery startup is
best-effort and does not block the source user-facing action.

Enabled endpoints are delivered independently. Plainterms records attempts with
the event ID, event type, HTTP status, success flag, response body, error text,
request payload, and delivery timestamp.

Use this mapping when reconciling receiver logs with the Plainterms delivery
log:

| Wire value | Meaning |
| --- | --- |
| `Webhook-Id` header | Stable event delivery ID. Matches delivery-log `event_id`. |
| `event_id` | Delivery-log copy of the `Webhook-Id` value. |
| `webhook_id` | Plainterms endpoint configuration UUID. Not the dedupe key. |
| `event` | Event name inside the request payload. |
| `event_type` | Delivery-log copy of the event name. |

## Troubleshooting

Open **Settings -> Webhooks** to see recent deliveries across endpoints. Select
**Delivery log** on a registered webhook for searchable per-endpoint history.

![Plainterms webhook delivery log](/screenshots/webhook-delivery-log.png)

Use the delivery log to inspect:

- event ID
- event type
- attempt number
- HTTP status
- request payload
- response body
- error message
- delivery timestamp

Common checks:

| Symptom | What to check |
| --- | --- |
| No deliveries | Confirm the webhook is enabled and the triggering event happened. |
| Failed status | Open the delivery row and inspect HTTP status, error, request, and response. |
| Signature mismatch | Verify against the raw request body, not parsed JSON. |
| Duplicate processing | Deduplicate with `Webhook-Id`. |
| Timeout or network error | Make the receiver public, keep handling fast, and return `2xx` only after acceptance. |

## Agent Resources

Agents should start from [`/llms.txt`](/llms.txt). For more context, use
[`/llms-full.txt`](/llms-full.txt). To inspect this page as raw markdown, use
[`/docs.md`](/docs.md).