Skip to page content
Docs
Make Agent Fast documentation

Webhooks

Register a public endpoint, verify signed raw payloads, deduplicate events, and operate retries safely.

At a glance

Register a public endpoint, verify signed raw payloads, deduplicate events, and operate retries safely.

Webhook deliverySigned events leave the platform to your endpoint
Platform eventSignHMACHTTPS POSTYour app
HMAC-SHA256At least once8 attempts

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#

EventEmitted when
site.publishedA site becomes published
site.unpublishedA site is returned to draft state
lead.createdThe agent captures a new lead
conversation.startedA new conversation thread begins
conversation.message.createdA message is added to a conversation
knowledge.readyA knowledge source completes processing
broadcast.sentA broadcast finishes its send workflow
domain.activatedA 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_SHA256

The 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#

  1. Verify the signature and timestamp.
  2. Insert the event ID into a table with a unique constraint.
  3. If it already exists, return 204 without repeating the side effect.
  4. Commit the event or enqueue your internal job.
  5. Return a 2xx response 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:

  1. Create a second endpoint pointing to a temporary or versioned receiver path.
  2. Store its newly revealed secret.
  3. Accept and deduplicate events from both endpoints.
  4. Verify the new endpoint receives valid deliveries.
  5. 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#

SymptomCheck
No deliveriesEndpoint status, selected events, public DNS, HTTPS certificate, and whether the event actually occurred
Signature mismatchRaw body access, exact timestamp.body concatenation, correct endpoint secret, and proxy body transformations
Repeated deliveriesReturn code, 10-second timeout, internal exceptions, and event-ID uniqueness
Private-address errorDNS records must resolve only to public addresses; localhost and internal ranges are rejected
Redirect failureRegister 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.