FullVision

Webhooks

Get a signed HTTP callback when a customer pays or a form is submitted

A webhook is FullVision calling your server the moment something happens in your workspace — a first payment lands, a form is submitted, one of your own product events fires.

Use it when you want to react to an event rather than poll a report for it: kick off an onboarding email on first payment, push a form submission into your CRM, page a channel when a big customer signs up.

Creating one

Create and manage endpoints in the dashboard. For each endpoint you set a URL and the list of event names you want delivered; FullVision generates the signing secret for you, and changing the URL issues a new one. The picker offers three groups:

GroupEvent nameFires when
Stripe$stripe_new_customerA customer makes their first payment and has tracked web visits. A customer with no web journey — one imported by a Stripe backfill, say — never fires this.
Forms$form_submitA tracked form is submitted.
Your eventsany name from your workspace's event catalogYou send that event to POST /v1/events.

An endpoint receives only the event names you selected. Selecting nothing delivers nothing.

Headers

HeaderDescription
webhook-idUnique ID for this delivery. Stable across retries — dedupe on it, not on payload content.
webhook-timestampUnix timestamp (seconds) the delivery was sent.
webhook-signaturev1,<base64> — base64-encoded HMAC-SHA256 of the signed content, computed with your endpoint's signing secret.

Verifying a delivery

The signed content is ${webhook-id}.${webhook-timestamp}.${raw body} — the three values joined with .. Compute the same HMAC-SHA256 over that string with your signing secret, then compare against the v1, value in webhook-signature using a constant-time comparison.

import { createHmac, timingSafeEqual } from "node:crypto";

function verifyWebhook(
  body: string,
  headers: { "webhook-id": string; "webhook-timestamp": string; "webhook-signature": string },
  secret: string,
): boolean {
  const { "webhook-id": id, "webhook-timestamp": timestamp, "webhook-signature": signature } = headers;

  // Reject stale or future-dated deliveries — 5 minute tolerance.
  const ageSeconds = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (ageSeconds > 5 * 60) return false;

  const signedContent = `${id}.${timestamp}.${body}`;
  const expected = createHmac("sha256", secret).update(signedContent).digest("base64");

  const received = signature.split(",")[1] ?? "";
  const expectedBuf = Buffer.from(expected);
  const receivedBuf = Buffer.from(received);
  if (expectedBuf.length !== receivedBuf.length) return false;

  return timingSafeEqual(expectedBuf, receivedBuf);
}

Always compare signatures with a constant-time function (timingSafeEqual). A plain === string comparison leaks timing information an attacker can use to forge a valid signature byte-by-byte.

Timestamp tolerance

Reject any delivery whose webhook-timestamp is more than 5 minutes old or in the future. This bounds the replay window even if a signature were ever compromised — an attacker capturing a valid delivery can't replay it after the tolerance window closes.

Event types

New webhook event types may be added at any time without a version bump — see API Stability. Switch on type with a default branch instead of an exhaustive list, and ignore event types you don't handle yet.

Delivery guarantee

Delivery is at-least-once. A delivery that fails is retried on a widening backoff: attempts 1 through 7 run from about a minute after the failure out to about 24 hours after it. If the 8th attempt fails the delivery is marked dead and is not retried again.

Because retries exist, your endpoint will sometimes see the same event twice. webhook-id is stable across every retry of one delivery — store it and ignore a delivery whose id you have already processed. Do not dedupe on payload content; two genuinely different events can carry identical bodies.

Return a 2xx as soon as you have accepted the delivery. Do the slow work afterwards — a slow handler is a failed delivery and buys you a retry you didn't want.

Legacy signature header

The legacy x-fullvision-signature header is still sent alongside the Standard Webhooks headers during migration. Move to webhook-signature — the legacy header will be removed in a future version, announced ahead of time.

On this page