Webhooks
Receive every IotaBot message and conversation change on your server: endpoint verification, signature checks, retries, ordering and every event.
Add an endpoint in Developers → Webhooks: an HTTPS URL, the channels it covers, and the events it wants. IotaBot POSTs a JSON event there the moment something happens.
Verifying your endpoint
Before an endpoint receives anything — and again whenever its URL changes — IotaBot sends an endpoint.verification event. Answer it with a 2xx and the value of data.challenge in the response body.
// Express
app.post("/iotabot/webhook", express.raw({ type: "application/json" }), (req, res) => {
const event = JSON.parse(req.body);
if (event.type === "endpoint.verification") {
return res.json({ challenge: event.data.challenge });
}
// …check the signature, queue the event, then:
res.sendStatus(200);
});Checking the signature
Every request carries IotaBot-Signature: t=<unix seconds>,v1=<signature>. The signature is an HMAC-SHA256, keyed with the endpoint's signing secret, of the timestamp, a dot, and the raw request body. Compute it over the body exactly as received, compare in constant time, and reject timestamps more than 5 minutes old. While a secret is being rolled, two v1= values are sent — accept either.
// Node.js
const crypto = require("crypto");
function verify(rawBody, header, secret) {
const parts = header.split(",").map(p => p.split("="));
const t = Number(parts.find(([k]) => k === "t")?.[1]);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; // older than 5 minutes
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
return parts.filter(([k]) => k === "v1").some(([, v]) => {
const got = Buffer.from(v, "hex");
return got.length === expected.length && crypto.timingSafeEqual(got, expected);
});
}# Python
import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = [p.split("=", 1) for p in header.split(",")]
t = int(next(v for k, v in parts if k == "t"))
if abs(time.time() - t) > 300: # older than 5 minutes
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(v, expected) for k, v in parts if k == "v1")Delivery
- Answer fast. Any 2xx within 10 seconds is success. Queue the work and reply.
- Retries. Anything else is retried after 1 min, 5 min, 30 min, 2 h, 6 h, then every 12 hours for 3 days. An endpoint that fails for 3 days — or answers
410 Gone— is switched off and the workspace owner is emailed. - Duplicates. Delivery is at least once: de-duplicate on the event
id. A replay keeps the same id. - Order. Events for one conversation arrive in order.
- Attachments arrive as signed links valid for an hour, never inline.
- Source IPs are listed on the Webhooks page, for your firewall.
- Email bodies come twice:
htmlis sanitised and safe to show in a browser;html_rawis exactly what arrived, for archiving or your own processing. - Deletion. Deleting a conversation also deletes its events, deliveries and API request logs. Deleting a contact sends
contact.deleted, so a CRM can forget it too.
Every event
All events share one envelope — id, type, created_at, channel, website_id and data — so one handler parses every channel.
Metadata only
For regulated data, an endpoint can be set to metadata only: events carry ids, types, channels, status and times with "metadata_only": true, but no message text, email body, attachment names, customer name, email or phone, or form answers. Your system fetches content through the API — with its own key and permissions — when it needs it.
Processing off, forwarding on
Per WhatsApp number, Instagram account and email domain you can turn IotaBot's own processing off and keep forwarding on, in the Channels panel of the Webhooks page. Messages are then stored as External conversations — no AI replies, no agent routing — and your system answers them through POST /v1/messages. An agent can still open one and take over.
