Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.veridianhp.com/llms.txt

Use this file to discover all available pages before exploring further.

Webhooks

Webhooks are how Veridian tells your server about events that happen asynchronously — most importantly, when a payment clears. Webhooks are the source of truth for payment outcomes. The hosted page and the patient’s browser do not communicate payment results to your backend. Always wait for the webhook.

Configuring an endpoint

In the dashboard, open Settings → Webhooks and add an endpoint URL. We require HTTPS — HTTP endpoints are rejected. Each endpoint has its own signing secret (whsec_...). Treat it like an API key.

Event types

EventWhen it fires
session.createdA session was minted via the API.
session.openedThe patient first opened the hosted page.
session.payment.processingThe patient confirmed; ACH is in flight.
session.payment.succeededFunds confirmed. Mark the invoice paid.
session.payment.failedBank declined or processor error.
session.payment.reversedAn already-succeeded payment was reversed (NSF, etc.).
session.cancelledPractice or patient cancelled the session.
session.expiredSession reached its TTL with no payment.

Event payload

Every webhook payload has the same envelope:
{
  "id": "evt_01HZX9ABCDEF",
  "type": "session.payment.succeeded",
  "createdAt": "2026-05-30T08:15:00Z",
  "data": {
    "sessionId": "ses_01HZX9ABCDEF",
    "invoiceId": "INV-2026-001",
    "status": "succeeded",
    "amountCents": 14500,
    "settledAt": "2026-05-30T08:15:00Z",
    "metadata": { "providerId": "prov_42" }
  }
}

Signature verification

Every request carries an HMAC signature header:
Veridian-Signature: t=1717000000,v1=4f2c8a...
Verify it before trusting the payload:
import crypto from "node:crypto";

function verifyVeridianSignature(rawBody, header, secret) {
  const parts = Object.fromEntries(
    header.split(",").map((p) => p.split("="))
  );
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(parts.v1, "hex")
  );
}
Use the raw request body, byte-for-byte, before any JSON parsing. Body re-serialization breaks the signature. Reject any request whose timestamp is more than 5 minutes off from your server clock. This prevents replay attacks.

Idempotency

Veridian may deliver the same event more than once — for example, if your server returned 200 but our network dropped the response. Always dedupe by event.id in your handler:
async function handle(event) {
  if (await alreadyProcessed(event.id)) return; // dedupe
  await markProcessed(event.id);
  await applyEvent(event);
}
A simple table with event_id PRIMARY KEY works perfectly.

Retries

Veridian retries on:
  • Connection failures
  • HTTP 5xx responses
  • HTTP 408 (request timeout)
  • HTTP 429 (rate limit) — with exponential backoff
Veridian does not retry on:
  • 2xx responses (treated as accepted)
  • 4xx responses other than 408 and 429 (treated as your bug)
Retry schedule: roughly 1m, 5m, 30m, 2h, 12h, 24h, then we give up and flag the endpoint in your dashboard. Don’t return 200 unless you’ve durably recorded the event.

Ordering

Events are delivered in best-effort chronological order, but network realities mean you may receive them out of order. Process by event.createdAt and your dedupe table, not by arrival order.

Replay from the dashboard

If your endpoint was down, you can replay individual events or a time range from the dashboard. Replayed events have the same event.id as the original, so your dedupe logic keeps you safe.

What’s next

Errors

Error responses for webhook config endpoints.

Sessions

The events on this page describe sessions — read the schema there.