A webhook sends each new lead, and each change to one, to an address you choose, the moment it happens. Use it to put leads in your CRM, a spreadsheet or your email list, with Zapier, Make or your own code. Webhooks come with the Team plan.
Admins add them under Settings → Webhooks.
Events#
lead.created: a visitor leaves their details for the first time.lead.updated: a lead changes: the visitor leaves their details again, a teammate edits them, or its status changes (New or Followed up).
Each is a POST with a JSON body like this (a conversation's words are never sent):
{
"id": "evt_1f0c…",
"type": "lead.created",
"created": "2026-10-26T16:56:02+00:00",
"test": false,
"data": {
"lead": {
"id": 42,
"email": "riley@example.com",
"name": "Riley Chen",
"phone": "+1 555 0142",
"custom": {"planned_trip": "A week"},
"custom_labels": {"planned_trip": "Planned trip"},
"consent": {"asked": true, "given": true, "text": "Send me news and offers by email.", "answered_at": "2026-10-26T16:56:00+00:00"},
"status": "new",
"status_changed_at": null,
"page_url": "https://example.com/pricing",
"attribution": {"utm_source": "google", "utm_medium": "cpc", "utm_campaign": "fall-sale", "ref": "www.google.com"},
"created_at": "2026-10-26T16:56:00+00:00",
"updated_at": "2026-10-26T16:56:00+00:00"
},
"bot": {"id": 7, "name": "Help"},
"conversation_url": "https://tufu.ai/a/your-team/inbox/c/…/",
"team": {"slug": "your-team", "name": "Your team"}
}
}
With Zapier#
- In Zapier, make a Zap whose trigger is Webhooks by Zapier → Catch Hook, and copy its address.
- In tufu, Settings → Webhooks → Add a webhook: paste the address and keep A visitor leaves their details.
- Press Send test. Zapier receives a sample lead, so you can map its fields (
lead.email,lead.name, …) in the next step: HubSpot, Mailchimp, Google Sheets or anything else Zapier connects.
With Make#
- In Make, start a scenario with Webhooks → Custom webhook, and copy its address.
- Add it in tufu the same way, then press Send test: Make learns the fields from the sample.
- Add the modules that take it from there.
Checking a request came from tufu#
Each webhook's signing secret is shown once, when you add it (or press New secret). Every request carries a
Tufu-Signature header: t=<unix time>,v1=<signature>, where the signature is HMAC-SHA256 of <unix time>.<the
raw body>, keyed with the secret. Check it on the raw body, before parsing it, and refuse a time more than five
minutes from your clock:
import hashlib, hmac, time
def from_tufu(secret: str, header: str, body: bytes) -> bool:
parts = dict(item.split("=", 1) for item in header.split(","))
timestamp = int(parts["t"])
if abs(time.time() - timestamp) > 300:
return False
expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])
const crypto = require("crypto");
function fromTufu(secret, header, rawBody) {
const parts = Object.fromEntries(header.split(",").map((item) => item.split("=")));
const timestamp = Number(parts.t);
if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 || ""));
}
The id is the same on every retry of one event, so you can skip one you've already handled.
When an address doesn't answer#
tufu expects a 2xx answer within ten seconds. Otherwise it tries again: 5 tries in all, over about two and a half hours. Each try shows on the webhook's card, with the answer or the error. After 15 failed tries in a row, tufu pauses the webhook and emails your team's admins; press Resume once the address works again. Only https addresses on the public internet are accepted.