LeadAdapter

Documentation

API and webhooks

Webhooks push events to your systems as they happen. This page contains everything needed to verify one, precisely enough to implement from.

API keys

Create a key under Settings, then API keys. It starts with la_live_ and it is shown once — we store only a hash of it, so a lost key is replaced rather than recovered.

Keys belong to a workspace, not to a person, and they survive that person leaving. Revoke one and it stops working immediately, everywhere, including in any agent that was using it.

A key is also the way to connect an MCP client that cannot open a browser. Send it as a bearer token.

What the REST API covers today

Webhook subscriptions and the MCP endpoint are available now. The full read and write REST API is an Operator feature scheduled for a later phase, and this page will describe it when it exists rather than before.

If you need to read or change something programmatically today, the MCP tools are the supported route, and they work from an ordinary HTTP client as well as from an agent.

Webhooks

A webhook is an HTTPS endpoint of yours plus a secret we generate. We POST a JSON body to it whenever a subscribed event happens, signed so you can prove it came from us.

Webhooks are available on Pro and above. Each delivery is recorded with its attempts, the response code and the first part of the response body, so a receiver that is quietly rejecting deliveries is visible rather than mysterious.

Events

These names are a stable part of the interface. We may add to this list; we will not rename anything on it.

The events a webhook can subscribe to.
Event Sent when
comment.captured A comment on a post one of your automations watches was read.
lead.created A new lead was created, from any source.
lead.email_captured A lead gave an email address on a lead page.
dm.sent A direct message was delivered by your browser.
dm.replied Someone replied to a message you sent.
connect.accepted A connection request you sent was accepted.
approval.requested Something is waiting in the approval queue.
approval.decided Someone approved or rejected a queued action.
page.viewed A lead page was viewed.

A webhook may subscribe to a list of these, or to * for everything — including events added later.

The signature scheme

Every delivery is signed with HMAC over the timestamp and the raw body. The timestamp is inside the signed payload, not merely a header beside it — that is the part most re-implementations get wrong, and it matters: signing the body alone produces a signature valid forever, and an unsigned timestamp can be rewritten to defeat your own freshness check.

Headers on every webhook delivery.
Header Meaning
X-LeadAdapter-Signature The HMAC of the signed payload, lowercase hex, prefixed with the algorithm.
X-LeadAdapter-Timestamp Unix seconds, UTC, at the moment this attempt was signed. Retries are re-signed with a new timestamp.
X-LeadAdapter-Delivery The delivery id. It is the same on every retry of the same event — use it as your idempotency key.
X-LeadAdapter-Event The event name, which is also in the body.

How the signature is computed

signed_payload = <X-LeadAdapter-Timestamp> + "." + <raw request body>
signature      = "sha256=" + lowercase_hex(
                     HMAC_SHA256(signed_payload, webhook_secret)
                 )

A worked example

These three inputs produce exactly this signature. Paste them into your implementation and compare — the value below is computed by the same code that signs your deliveries.

secret    = whsec_example_do_not_use
timestamp = 1789012345
body      = {"id":"01JBXQ8P2R4S6T8V0W2X4Y6Z8A","event":"lead.created"}

X-LeadAdapter-Signature: sha256=b959110ea48a9bb42c68cf1b0be05720089c84a7d05d92a24644144d8e55870f

Verifying, in PHP

$body      = file_get_contents('php://input');   // raw, unparsed
$timestamp = $_SERVER['HTTP_X_LEADADAPTER_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_LEADADAPTER_SIGNATURE'] ?? '';

if (abs(time() - (int) $timestamp) > 300) {
    http_response_code(400); exit;               // outside the replay window
}

$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, $secret);

if (! hash_equals($expected, $signature)) {
    http_response_code(400); exit;               // constant-time, never ===
}

Retries

We retry on a 5xx, a 429, a 408 and on a transport failure. We do not retry on any other 4xx: your endpoint understood the request and rejected it, and the same bytes will get the same answer.

Every retry carries the same delivery id and the same body, with a fresh timestamp and therefore a fresh signature. That is deliberate, so a retry arriving two hours later still passes your freshness check.

Delivery is attempted up to 6 times.
Attempt Delay before it
2 10 seconds
3 1 minutes
4 5 minutes
5 30 minutes
6 120 minutes

What your receiver must do

Six things, in this order. The first is the one that breaks implementations: read the raw bytes before any JSON middleware touches them, because re-serialising the body changes it and the signature will not match.

  1. Read the raw request body, unparsed.
  2. Reject the delivery if the timestamp is more than the tolerance away from your own clock.
  3. Recompute the signature and compare it in constant time. Never with a plain string equality check.
  4. Only then parse the JSON.
  5. Deduplicate on the delivery id — retries repeat it.
  6. Answer with a 2xx quickly, and do the real work afterwards.

Every cap, limit, timeout and tool name on this page is read from the running product when the page loads. If the software changes, this page changes with it.