Webhooks
Register a public endpoint, verify signed raw payloads, deduplicate events, and operate retries safely.
Register a public endpoint, verify signed raw payloads, deduplicate events, and operate retries safely.
Developer webhooks send account events to your server so you do not need to poll. Delivery is at least once: events can be duplicated, delayed, or arrive out of order.
Register an endpoint#
Create a key with webhooks:write, then register a public HTTPS receiver and the smallest event set you need.
curl --fail-with-body https://makeagent.fast/api/v1/webhook-endpoints \
-X POST \
-H "Authorization: Bearer $MAF_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: webhook-production-v1" \
-d '{
"url": "https://api.example.com/webhooks/make-agent-fast",
"events": ["lead.created", "conversation.started"]
}'The URL must use HTTPS, cannot contain a username, password, fragment, or custom port, and must resolve only to public IP addresses. Redirects are not followed during delivery.
The 201 response reveals a whsec_... signing secret once. Store it in the receiver's secret manager before closing the response. Listing endpoints later returns URL, events, status, last delivery, and last error—but not the secret.
Event types#
| Event | Emitted when |
|---|---|
site.published | A site becomes published |
site.unpublished | A site is returned to draft state |
lead.created | The agent captures a new lead |
conversation.started | A new conversation thread begins |
conversation.message.created | A message is added to a conversation |
knowledge.ready | A knowledge source completes processing |
broadcast.sent | A broadcast finishes its send workflow |
domain.activated | A custom domain becomes active |
Request format#
Make Agent Fast sends POST with Content-Type: application/json, User-Agent: MakeAgentFast-Webhooks/1.0, and these headers:
maf-event-id: evt_...
maf-event-type: lead.created
maf-signature: t=1784210566,v1=HEX_HMAC_SHA256The JSON body has a stable envelope. Fields inside data depend on the event and can receive additive fields.
{
"id": "evt_2c34...",
"type": "lead.created",
"created_at": "2026-07-16T10:02:46.000Z",
"data": {
"site_id": "SITE_ID",
"lead_id": "LEAD_ID"
}
}Use the body id or maf-event-id as the deduplication key. Do not use delivery time or a generated database ID.
Verify the signature in Node.js#
Read the exact raw bytes before JSON parsing. The signed value is TIMESTAMP + "." + RAW_BODY.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyWebhook(rawBody: Buffer, header: string, secret: string) {
const values = Object.fromEntries(header.split(",").map((part) => part.split("=", 2)));
const timestamp = values.t;
const supplied = values.v1;
if (!timestamp || !supplied || !/^[a-f0-9]{64}$/.test(supplied)) return false;
const age = Math.abs(Date.now() / 1_000 - Number(timestamp));
if (!Number.isFinite(age) || age > 300) return false;
const expected = createHmac("sha256", secret)
.update(`${timestamp}.`)
.update(rawBody)
.digest("hex");
return timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(supplied, "hex"));
}Reject malformed signatures and timestamps older than your accepted replay window; five minutes is a reasonable default. Compare fixed-length bytes in constant time.
Verify the signature in Python#
import hashlib
import hmac
import time
def verify_webhook(raw_body: bytes, header: str, secret: str) -> bool:
values = dict(part.split("=", 1) for part in header.split(",") if "=" in part)
timestamp = values.get("t", "")
supplied = values.get("v1", "")
try:
if abs(time.time() - int(timestamp)) > 300:
return False
except ValueError:
return False
signed = timestamp.encode() + b"." + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, supplied)Only parse and process the JSON after signature verification succeeds.
Acknowledge and process#
- Verify the signature and timestamp.
- Insert the event ID into a table with a unique constraint.
- If it already exists, return
204without repeating the side effect. - Commit the event or enqueue your internal job.
- Return a
2xxresponse quickly.
The Make Agent Fast sender times out after 10 seconds. It considers only 2xx successful and does not follow redirects.
Retry behavior#
Failed deliveries are queued for up to eight attempts. Backoff starts around two seconds, doubles with jitter, and is capped at one hour; the actual delivery time can be later when workers are busy. Because retries can outlive the initiating request and arrive out of order, never depend on one event arriving immediately before another.
Return a non-2xx response only when you want Make Agent Fast to retry. For a permanently unsupported event version or deleted destination, accept and record it or remove the endpoint instead of producing endless transient failures.
Rotate a signing secret#
Signing secrets cannot be revealed or edited. To rotate safely:
- Create a second endpoint pointing to a temporary or versioned receiver path.
- Store its newly revealed secret.
- Accept and deduplicate events from both endpoints.
- Verify the new endpoint receives valid deliveries.
- Delete the old endpoint with
DELETE /webhook-endpoints/{endpoint_id}.
If the receiver URL remains the same, make the receiving application accept both secrets during the overlap and use event IDs to prevent duplicate side effects.
Troubleshooting#
| Symptom | Check |
|---|---|
| No deliveries | Endpoint status, selected events, public DNS, HTTPS certificate, and whether the event actually occurred |
| Signature mismatch | Raw body access, exact timestamp.body concatenation, correct endpoint secret, and proxy body transformations |
| Repeated deliveries | Return code, 10-second timeout, internal exceptions, and event-ID uniqueness |
| Private-address error | DNS records must resolve only to public addresses; localhost and internal ranges are rejected |
| Redirect failure | Register the final HTTPS URL directly; redirects are not followed |
Never include the signing secret, bearer key, or full visitor payload in a support message. Include the endpoint ID, event ID, failure time, and sanitized receiver logs.