Skip to content
Browse the docs

Developers

Webhooks

Receive signed events when visitors leave details, leads change, conversations end, questions go unanswered or sources sync, and verify every delivery.

Last updated

On this page

Webhooks send what happens in your workspace to your own systems as it happens: a CRM, a help desk, Zapier, Make or your own code. ASDK POSTs a signed JSON event to every endpoint that subscribed to it, usually within a few seconds.

Events

EventWhen it is sent
lead.createdA visitor left their contact details (chat, forms or an AI agent through WebMCP).
lead.updatedA lead's email, name, company, phone, note, status or qualification changed. data.changes names the fields, data.previous their earlier values.
conversation.closedA conversation had no new message for 30 minutes. The payload carries the transcript (the latest 100 messages). If the visitor comes back and goes quiet again, it is sent again.
knowledge_gap.createdVisitors asked something your knowledge base could not answer, for the first time.
source.syncedA knowledge source finished syncing: status is ready, or error with the reason.

Leads and conversations from the dashboard's preview are sent too, marked "preview": true, so you can test your integration end to end. The full payload of every event is in the API reference.

JSON
{
  "id": "evt_2Hq7Lm9Xc4Tz8Pw1Nb6V",
  "type": "lead.created",
  "createdAt": "2026-10-24T09:12:44Z",
  "workspaceId": "wsp_8fK2nQ7xLm3Pz9Tb4Wv1",
  "data": {
    "site": { "id": "site_…", "name": "Acme", "origin": "https://acme.com" },
    "lead": {
      "id": "lead_…",
      "siteId": "site_…",
      "conversationId": "conv_…",
      "email": "jane@example.com",
      "name": "Jane Doe",
      "company": null,
      "phone": null,
      "note": null,
      "consent": true,
      "pageUrl": "https://acme.com/pricing",
      "status": "new",
      "score": null,
      "qualifiedAt": null,
      "createdAt": "2026-10-24T09:12:43Z",
      "preview": false,
      "dashboardUrl": "https://app.asdk.com/w/wsp_…/leads/lead_…"
    }
  }
}

Add an endpoint

Create it

In the dashboard, open Settings → Webhooks, choose Add endpoint, enter a public https:// address and pick the events. Owners and admins manage endpoints; API keys with the write scope can too (POST /v1/workspaces/{id}/webhooks), which is how automation tools subscribe. A workspace can have 10 endpoints.

Store the signing secret

The secret (whsec_…) is shown once. Store it with your handler's configuration; you need it to verify deliveries.

Send a test event

Send test event posts a signed test event right away and shows what your endpoint answered. Use it until your handler answers with a 2xx.

What a delivery looks like

Each delivery is a POST with a JSON body and these headers:

HeaderValue
Content-Typeapplication/json
User-AgentASDK-Webhooks/1.0
X-ASDK-EventThe event type, such as lead.created (or test).
X-ASDK-DeliveryThe delivery id (whd_…), the same on every retry. Use it to ignore duplicates.
X-ASDK-Signaturet=<unix seconds>,v1=<signature>

Answer with any 2xx within 15 seconds to confirm the delivery; do slow work after answering. ASDK does not follow redirects.

Verify the signature

The signature is the hex HMAC-SHA256 of the timestamp, a dot and the raw request body, keyed with the endpoint's secret: HMAC-SHA256(secret, "<t>.<raw body>"). To verify a delivery:

  1. Read the raw body before parsing it as JSON (parsing and serializing again changes the bytes).
  2. Split X-ASDK-Signature at commas into t and one or more v1 values.
  3. Reject the delivery when t is more than five minutes from your clock (a replayed request).
  4. Compute the HMAC and compare it with each v1 in constant time. Any match is valid.

JavaScript (Node.js)

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

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

With @asdk/js, verifyWebhook({ secret, body, signature }) does the same with the Web Crypto API (Node.js, edge functions, Workers) and returns the parsed event.

Python

Python
import hashlib
import hmac
import time


def verify_asdk_signature(secret: str, header: str, raw_body: bytes, tolerance: int = 300) -> bool:
    t, signatures = 0, []
    for part in (header or "").split(","):
        key, _, value = part.strip().partition("=")
        if key == "t" and value.isdigit():
            t = int(value)
        elif key == "v1":
            signatures.append(value)
    if not t or abs(time.time() - t) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(s, expected) for s in signatures)

Go

Go
import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"strconv"
	"strings"
	"time"
)

// VerifyASDKSignature checks an X-ASDK-Signature header against the raw body.
func VerifyASDKSignature(secret, header string, body []byte, now time.Time) bool {
	var t int64
	var signatures []string
	for _, part := range strings.Split(header, ",") {
		key, value, _ := strings.Cut(strings.TrimSpace(part), "=")
		switch key {
		case "t":
			t, _ = strconv.ParseInt(value, 10, 64)
		case "v1":
			signatures = append(signatures, value)
		}
	}
	if t == 0 || now.Sub(time.Unix(t, 0)).Abs() > 5*time.Minute {
		return false
	}
	mac := hmac.New(sha256.New, []byte(secret))
	fmt.Fprintf(mac, "%d.", t)
	mac.Write(body)
	expected := hex.EncodeToString(mac.Sum(nil))
	for _, s := range signatures {
		if hmac.Equal([]byte(s), []byte(expected)) {
			return true
		}
	}
	return false
}

Check your code

With the secret whsec_0123456789abcdef0123456789abcdef0123456789abcdef, the body {"id":"evt_test","type":"test"} and t=1767225600, the signature is:

Text
9d0492ab65fa50095a75db12fab32250affedfafc76969e8feda47c4fa59d146

Retries

Any 2xx answer is a delivery. ASDK retries 408, 429, 5xx answers and requests that got no answer (a timeout or a refused connection) after 30 seconds, 2 minutes, 10 minutes, 1 hour and 6 hours; a Retry-After header makes the wait longer (up to 6 hours). Other answers, such as 400 or 404, fail the delivery at once. After 15 deliveries in a row fail for good, the endpoint is switched off; switch it on again on the Webhooks page once it is fixed.

The delivery log on the Webhooks page shows every delivery of the last 30 days with its payload, the status your endpoint answered and the start of its answer. Resend sends a delivery's event again.

Rotate the secret

Rotate secret creates a new secret and shows it once. For the next 24 hours every delivery is signed with both secrets (two v1 values), so you can deploy the new secret without missing an event. After that only the new secret signs.