Skip to content

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.

  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

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.

Plainterms currently sends four event names.

EventWhen it is sentHuman meaning
document.capturedA quote document is captured.A new document is available for downstream systems.
document.approvedA quote document is approved.The document passed the approval step.
landing_page.openedA visitor opens a public landing page.The quote or bundle page was viewed.
landing_page.clickedA 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 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.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.

Every delivery includes these headers:

HeaderPurpose
Webhook-Signaturesha256=<hex hmac> signature for the raw body.
Webhook-TimestampUnix timestamp in seconds. Metadata, not part of the HMAC input.
Webhook-IdStable delivery event ID for idempotency.
Content-TypeUsually 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.

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 valueMeaning
Webhook-Id headerStable event delivery ID. Matches delivery-log event_id.
event_idDelivery-log copy of the Webhook-Id value.
webhook_idPlainterms endpoint configuration UUID. Not the dedupe key.
eventEvent name inside the request payload.
event_typeDelivery-log copy of the event name.

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

Use the delivery log to inspect:

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

Common checks:

SymptomWhat to check
No deliveriesConfirm the webhook is enabled and the triggering event happened.
Failed statusOpen the delivery row and inspect HTTP status, error, request, and response.
Signature mismatchVerify against the raw request body, not parsed JSON.
Duplicate processingDeduplicate with Webhook-Id.
Timeout or network errorMake the receiver public, keep handling fast, and return 2xx only after acceptance.

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