Webhooks and signature verification

Webhooks push events to your CRM or automation tool the moment they happen. They are part of the Agency 1000 plan. Add an endpoint under Settings › Integrations, choose the events, and copy the signing secret. It is shown once.

Events

EventSent when
preview.readyA preview finished generating and is live.
preview.failedA preview could not be generated; the credit was refunded.
prospect.first_viewThe prospect opened the preview for the first time.
prospect.hotHot Lead Radar scored the prospect as hot.
pin.createdThe prospect dropped a feedback pin.
quote.requestedThe prospect asked for a quote from the Approve modal.
project.approvedThe prospect approved the redesign.
batch.completedEvery row of a bulk CSV batch has finished.
export.readyA requested export is ready to download.
hosted_site.publishedA hosted client site was published or updated.
hosted_site.unpublishedA hosted client site was taken offline.
form.submittedA visitor submitted a contact form handled by Form Relay.

Use Send test event in Settings to receive a ping with the same headers and signature.

Delivery

Every delivery is an HTTPS POST with a JSON body and these headers:

HeaderValue
Content-Typeapplication/json
User-AgentUIRevamper-Webhooks/1.0
X-Revamp-EventThe event type, or ping for the test event.
X-Revamp-Event-IdStable across retries and redeliveries: deduplicate on it.
X-Revamp-DeliveryUnique per delivery attempt.
X-Revamp-TimestampUnix seconds when the request was signed.
X-Revamp-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>; a second v1 appears during secret rotation.

Answer with any 2xx status within 10 seconds. Anything else is a failure and is retried: 6 attempts in total: the first, then retries after 1 min, 5 min, 30 min, 2 h, 12 h. After the last attempt fails the delivery is marked dead and the workspace gets an alert. An endpoint that fails 20 times in a row is paused until you turn it back on.

A retry or a manual redelivery carries the same X-Revamp-Event-Id, so store the ids you have processed and skip repeats.

Verify the signature

The signature header looks like this:

X-Revamp-Signature: t=1767225600,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

To verify it:

  1. Read the raw request body before any JSON parsing. Re-serialising changes the bytes.
  2. Split the header on commas. Take t and every v1 value.
  3. Reject the request when t is more than 300 seconds away from your clock. This stops replays.
  4. Compute HMAC-SHA256(secret, t + "." + rawBody) as lowercase hex.
  5. Compare it with each v1 in constant time. Accept when one matches.

During a secret rotation the header carries two v1 values, one per secret, so receivers that still hold the old secret keep working.

Node.js

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

export function verify(header, rawBody, secret, toleranceSeconds = 300) {
  let t = NaN;
  const v1 = [];
  for (const part of header.split(",")) {
    const [key, value = ""] = part.split("=", 2);
    if (key === "t") t = Number(value);
    if (key === "v1") v1.push(value);
  }
  if (!Number.isInteger(t) || v1.length === 0) return false;
  if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
  return v1.some((sig) => {
    const got = Buffer.from(sig, "hex");
    return got.length === expected.length && timingSafeEqual(got, expected);
  });
}

Python

import hashlib, hmac, time

def verify(header: str, raw_body: bytes, secret: str, tolerance: int = 300) -> bool:
    pairs = [p.split("=", 1) for p in header.split(",")]
    t = next((v for k, v in pairs if k == "t"), None)
    sigs = [v for k, v in pairs if k == "v1"]
    if t is None or not t.isdigit() or not sigs:
        return False
    if abs(time.time() - int(t)) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected, s) for s in sigs)

PHP

function verify(string $header, string $rawBody, string $secret, int $tolerance = 300): bool {
    $t = null; $sigs = [];
    foreach (explode(',', $header) as $part) {
        [$k, $v] = array_pad(explode('=', $part, 2), 2, '');
        if ($k === 't') $t = $v;
        if ($k === 'v1') $sigs[] = $v;
    }
    if (!ctype_digit((string) $t) || !$sigs || abs(time() - (int) $t) > $tolerance) return false;
    $expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
    foreach ($sigs as $s) if (hash_equals($expected, $s)) return true;
    return false;
}

Zapier, Make and GoHighLevel

These tools cannot verify an HMAC themselves. Give the endpoint a long, unguessable path, keep the URL private, and deduplicate on X-Revamp-Event-Id.