Webhook Integration
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 first. The agent index is
shorter and more contract-oriented than this human guide.
Integration Flow
Section titled “Integration Flow”- Add a public receiver URL in Plainterms.
- Copy the signing secret once and store it in your receiver environment.
- Accept
POSTrequests with JSON bodies. - Verify
Webhook-Signatureagainst the exact raw request body. - Deduplicate work with
Webhook-Id. - Return a
2xxonly after the event is accepted or durably queued. - Use the Plainterms delivery log to inspect attempts, errors, and responses.

Configure A Webhook
Section titled “Configure A Webhook”Plainterms webhook configuration lives in the app under Settings -> Webhooks. The user adding the endpoint must be an administrator.
- Open Settings -> Webhooks.
- Enter your public webhook endpoint URL.
- Select Add Webhook.
- Copy the signing secret when Plainterms shows it.
- Store the secret as an environment variable in your receiver.
- 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
Section titled “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
Section titled “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_urldownload_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
Section titled “Landing Page Events”landing_page.opened includes the landing-page identifiers and request context:
organization_idmagic_link_idtarget_typedocument_idbundle_idlanding_page_urluser_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
Section titled “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.
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
Section titled “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
Section titled “Troubleshooting”Open Settings -> Webhooks to see recent deliveries across endpoints. Select Delivery log on a registered webhook for searchable per-endpoint history.

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
Section titled “Agent Resources”Agents should start from /llms.txt. For more context, use
/llms-full.txt. To inspect this page as raw markdown, use
/docs.md.