Get started
Webhooks
Events we send, the payload of each, delivery and retries, and how to verify the signature in Node.js, PHP and Python.
Webhooks send a POST request with JSON to your URL when something happens — for Zapier, Make, n8n, your CRM or your own server.
Add a webhook
Open your agent's Integrations page and pick Custom webhook (or Zapier, Make, n8n, Google Sheets, HubSpot — these use the same webhook under the hood). Paste the URL, choose the events under Send when… and save. Each agent can have up to 20 integrations.
- Show signing secret reveals the secret (
whsec_…) used to sign requests. - Send test posts a sample of the first selected event right away (with
"test": true) and shows what your server answered — Zapier needs this to see the fields.
Events
| Event | Sent when |
|---|---|
conversation.created | A new conversation starts (website, messaging apps, email, API) |
lead.created | The agent saves a lead, a form is submitted, a flow saves a lead, or MCP create_lead runs |
handoff.requested | A chat is handed to your team |
conversation.closed | A conversation is closed by your team or closed automatically |
csat.received | The customer rates the chat |
Tests never send webhooks.
Request
POST /your/webhook HTTP/1.1
Content-Type: application/json
User-Agent: AnytimeReply-Webhook/1.0
X-AnytimeReply-Event: lead.created
X-AnytimeReply-Signature: sha256=5b2c…e91fEvery body has the same envelope:
{
"event": "lead.created",
"agent": { "id": "AGENT_ID", "name": "Support" },
"created_at": "2026-10-19T09:30:00.000Z",
"data": { "...": "event-specific, see below" }
}When the event belongs to a conversation, data.link is the chat in your dashboard.
Payloads
conversation.created
{
"conversation_id": "c1a2…",
"channel": "WHATSAPP",
"contact": { "name": "Nadia", "phone": "+8801711111111" },
"link": "https://anytimereply.com/dashboard/agents/AGENT_ID/inbox?c=c1a2…"
}For the website chat, contact is replaced by page_url (the page the visitor started on). For the API, contact has name and email.
lead.created
{
"conversation_id": "c1a2…",
"lead": {
"name": "Rahim",
"email": "[email protected]",
"phone": "+8801700000000",
"form": "Call me back",
"data": { "name": "Rahim", "phone": "+8801700000000", "order_id": "A-1001" }
},
"link": "https://anytimereply.com/dashboard/agents/AGENT_ID/inbox?c=c1a2…"
}form and data (every submitted field) are present for form submissions. Leads the AI saves have notes; flow leads have "source": "flow". MCP leads have conversation_id: null.
handoff.requested
{
"conversation_id": "c1a2…",
"channel": "WIDGET",
"reason": "Customer asked for a person",
"mood": "frustrated",
"summary": "Paid for the Pro plan but the account is still on Free.",
"contact": { "name": "Nadia", "phone": null },
"link": "https://anytimereply.com/dashboard/agents/AGENT_ID/inbox?c=c1a2…"
}conversation.closed
{
"conversation_id": "c1a2…",
"channel": "TELEGRAM",
"contact": { "name": "Nadia", "phone": null, "email": null },
"auto": true,
"link": "https://anytimereply.com/dashboard/agents/AGENT_ID/inbox?c=c1a2…"
}auto: true appears when the chat was closed automatically after your team answered.
csat.received
{
"conversation_id": "c1a2…",
"rating": 5,
"comment": "Super fast, thanks!",
"link": "https://anytimereply.com/dashboard/agents/AGENT_ID/inbox?c=c1a2…"
}rating is 1–5. Website ratings can include a comment; ratings from messaging apps include contact instead.
Verify the signature
X-AnytimeReply-Signature is sha256= followed by the hex HMAC-SHA256 of the raw request body, using your webhook's signing secret. Compute it over the exact bytes you received (before parsing JSON) and compare in constant time.
const crypto = require("crypto");
const express = require("express");
const app = express();
// keep the raw body for the signature check
app.post("/webhooks/anytimereply", express.raw({ type: "application/json" }), (req, res) => {
const expected = "sha256=" + crypto.createHmac("sha256", process.env.ANYTIMEREPLY_WEBHOOK_SECRET).update(req.body).digest("hex");
const got = req.get("X-AnytimeReply-Signature") || "";
const ok = got.length === expected.length && crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected));
if (!ok) return res.status(401).send("bad signature");
const event = JSON.parse(req.body.toString("utf8"));
if (event.event === "lead.created") {
// save event.data.lead …
}
res.sendStatus(200);
});Delivery and retries
- Reply with any 2xx status within 10 seconds. Anything else counts as a failure.
- Failed deliveries are retried: 3 attempts in total, about 15 and 30 seconds apart.
- Deliveries happen in the background and never slow down the chat. Order isn't guaranteed — use
created_atandconversation_id. - The last status and error of each webhook are shown on the Integrations page.
- Your URL must be publicly reachable over HTTP(S); private network addresses are refused.
Respond quickly with 200 and do slow work (CRM calls, emails) in a background job. Use conversation_id to ignore duplicates if a retry arrives after a slow success.
Slack alerts
Pick Slack on the Integrations page and paste a Slack incoming webhook URL (Slack: create an app → Incoming Webhooks → Add New Webhook to Workspace → pick a channel). The same events arrive in that channel as short, readable messages with an Open chat link — no signature needed. To let the agent answer in Slack, see Slack channel.
Trigger webhooks
Triggers have their own Call a webhook action with a different payload (the whole conversation context) and an optional secret, sent as a plain hex signature without the sha256= prefix.