Outbound webhooks
Quick Answer: In Settings → Notifications, add a subscription with your HTTPS endpoint URL, copy the signing secret, and enable
escalation.createdand/orescalation.unclaimed. IteroChat POSTs signed JSON to your URL; verifyX-IteroChat-Signature, respond with 2xx, and use Send test event to confirm everything works.
Outbound webhooks let your own systems know when a conversation needs a human. IteroChat sends a signed JSON POST to a URL you provide, so you can route alerts into paging, on-call, Slack bridges, or internal tooling you control.
Only owners and admins can create and manage webhooks. You can have up to five active webhook subscriptions per organization. Each subscription has its own URL, signing secret, and event toggles.
Quickstart: integrate your endpoint
Follow these steps to go from zero to a verified webhook receiver.
1. Create a public HTTPS endpoint
Your server must expose a URL that:
- Uses HTTPS on a public hostname (localhost and private IPs are rejected when you save the subscription).
- Accepts POST requests with a JSON body.
- Returns any 2xx status code within about 10 seconds when processing succeeds.
A minimal path like https://api.example.com/iterochat/webhooks is fine. IteroChat does not follow redirects on delivery.
2. Add a subscription in the dashboard
- Open Settings → Notifications.
- Choose New webhook.
- Enter a name (for your reference) and your URL.
- Enable the events you want:
escalation.createdfor an immediate alert on every handoff.escalation.unclaimedfor a follow-up if nobody claims the chat in time.
- Save the subscription and copy the signing secret immediately. It is shown only once (you can rotate it later, which also shows the new secret once).
3. Verify signatures on every request
Before you trust the payload, verify X-IteroChat-Signature using the secret from step 2. See Verifying signatures below for the exact algorithm and code samples.
Always verify against the raw request body (the exact bytes IteroChat sent). Parsing JSON and re-stringifying it can change whitespace or key order and break verification.
4. Send a test event
On the subscription, choose Send test event. Your endpoint should receive a ping event with the same headers and signing scheme as real deliveries. Fix any signature or connectivity issues before relying on the webhook in production.
5. Handle real events
When an escalation happens, IteroChat delivers escalation.created (if enabled). If the chat stays unclaimed past your threshold, escalation.unclaimed fires (if enabled). Use fields in data.conversation and data.load to route the alert (see Escalation event data).
Respond with 2xx as soon as you have accepted the event. You do not need to finish your full notification workflow before responding; do heavy work asynchronously if needed.
Example: Express (Node.js)
This example uses Express with a raw body parser so signature verification sees the exact bytes IteroChat signed.
const express = require("express");
const crypto = require("crypto");
const app = express();
const SECRET = process.env.ITEROCHAT_WEBHOOK_SECRET;
function verifySignature(secret, timestamp, body, signature) {
const signed = `${timestamp}.${body}`;
const expected =
"sha256=" +
crypto.createHmac("sha256", secret).update(signed).digest("hex");
try {
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
} catch {
return false;
}
}
app.post(
"/iterochat/webhooks",
express.raw({ type: "application/json" }),
(req, res) => {
const timestamp = req.headers["x-iterochat-timestamp"];
const signature = req.headers["x-iterochat-signature"];
const body = req.body; // Buffer
if (!timestamp || !signature || !verifySignature(SECRET, timestamp, body, signature)) {
return res.status(401).send("invalid signature");
}
const event = JSON.parse(body.toString("utf-8"));
if (event.type === "escalation.created" || event.type === "escalation.unclaimed") {
const conv = event.data.conversation;
const load = event.data.load;
// Example: page on-call when queue is backed up and nobody is available
if (load.agents_available === 0) {
console.log("PAGE:", conv.customer_name, conv.escalation_reason, conv.dashboard_url);
} else {
console.log("NOTIFY:", conv.customer_name, conv.handoff_summary);
}
}
res.sendStatus(200);
}
);
app.listen(3000);
Store ITEROCHAT_WEBHOOK_SECRET in your environment. Never commit the secret to source control.
Example: Flask (Python)
import hashlib
import hmac
import json
import os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["ITEROCHAT_WEBHOOK_SECRET"]
def verify_signature(secret: str, timestamp: str, body: bytes, signature: str) -> bool:
signed = f"{timestamp}.{body.decode('utf-8')}"
expected = "sha256=" + hmac.new(
secret.encode(), signed.encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
@app.post("/iterochat/webhooks")
def iterochat_webhook():
timestamp = request.headers.get("X-IteroChat-Timestamp", "")
signature = request.headers.get("X-IteroChat-Signature", "")
body = request.get_data()
if not verify_signature(SECRET, timestamp, body, signature):
abort(401)
event = json.loads(body)
if event["type"] in ("escalation.created", "escalation.unclaimed"):
conv = event["data"]["conversation"]
print("Escalation:", conv["customer_name"], conv.get("dashboard_url"))
return "", 200
Routing ideas
Webhooks send a generic JSON payload. You decide what to do with it:
- On-call / paging: forward
data.conversation.dashboard_urlandhandoff_summaryto PagerDuty, Opsgenie, or your internal pager. - Chat tools: post a formatted message to a Slack or Discord incoming webhook you manage (IteroChat does not format messages for those platforms; you map fields in your receiver).
- Ticket systems: open a ticket using
customer_name,escalation_reason, andlast_customer_message. - Load-aware routing: use
data.load.agents_availableanddata.load.queued_countto distinguish "team busy" from "nobody online."
Use escalation.created for every handoff. Add escalation.unclaimed when you also want a reminder if nobody claims the chat in time.
Events
Each subscription can enable one or both events independently.
escalation.created
Fires the moment a conversation is escalated to your human queue. This happens every time the AI hands off, with no throttling or presence checks.
Use this when you want an immediate alert that someone needs help.
escalation.unclaimed
Fires when an escalated conversation is still unclaimed after a waiting period you configure per subscription. The default threshold is 600 seconds (10 minutes).
With that default, expect the notification to arrive about 10 minutes after escalation. The exact timing can vary slightly.
The event only fires while the conversation remains in the queue and unclaimed. If an agent claims it first, you will not receive this event for that escalation.
When escalation.unclaimed is enabled, a threshold field appears in the subscription settings so you can shorten or lengthen the wait time.
Request format
Every delivery is an HTTP POST with a JSON body and several custom headers.
Headers
| Header | Description |
|---|---|
Content-Type | Always application/json |
X-IteroChat-Event-Id | Unique id for this event (stable across retries) |
X-IteroChat-Event-Type | Event name, e.g. escalation.created |
X-IteroChat-Timestamp | Unix timestamp in seconds, included in the signature |
X-IteroChat-Attempt | Delivery attempt number (starts at 1) |
X-IteroChat-Signature | HMAC-SHA256 signature (see below) |
JSON envelope
The body uses the same envelope for every event type:
{
"id": "a3f1c2e4-8b7d-4a1f-9c0e-123456789abc",
"type": "escalation.unclaimed",
"created_at": "2026-07-27T09:14:02.311Z",
"organization_id": "b7c2d1e0-5a4f-4b3c-9d2e-987654321def",
"data": { }
}
| Field | Description |
|---|---|
id | Same value as X-IteroChat-Event-Id |
type | Same value as X-IteroChat-Event-Type |
created_at | When the event was created (ISO 8601, UTC) |
organization_id | Your IteroChat organization id |
data | Event-specific payload (see below) |
Escalation event data
For escalation.created and escalation.unclaimed, data contains conversation details and load context:
{
"conversation": {
"id": "c1d2e3f4-5678-90ab-cdef-123456789012",
"channel": "widget",
"customer_name": "Alex",
"language": "en",
"code_mixed": false,
"escalation_type": "knowledge_gap",
"escalation_reason": "No matching answer in the knowledge base",
"handoff_summary": "Customer asked about refund policy. AI could not find a clear answer.",
"escalated_at": "2026-07-27T09:04:00.000Z",
"last_customer_message": "Can I get a refund?",
"dashboard_url": "https://app.iterochat.com/dashboard/conversations?id=c1d2e3f4-5678-90ab-cdef-123456789012"
},
"load": {
"waiting_seconds": 612,
"queued_count": 3,
"agents_available": 1
}
}
Conversation fields help you route and triage: channel, customer name, language, why the AI escalated, the handoff summary, and a link to open the conversation in the dashboard.
Load fields give you context at delivery time:
waiting_seconds: how long the customer has been waiting since escalationqueued_count: how many conversations are currently in the queueagents_available: how many team members are marked available
Use load context to tell the difference between "nobody is available" and "the team is busy with other chats."
Verifying signatures
Each subscription has a signing secret. IteroChat generates it when you create the subscription (or when you rotate the secret). The secret is shown once; copy it immediately and store it on your server.
The signature covers the exact request body and the timestamp, so a captured request cannot be replayed later.
Signed string: {timestamp}.{body}
Where timestamp is the value of X-IteroChat-Timestamp (as a string) and body is the raw HTTP request body (the JSON string, byte for byte).
Signature header: X-IteroChat-Signature: sha256=<hex>
The hex value is HMAC-SHA256(secret, "{timestamp}.{body}").
Python example
import hashlib
import hmac
def verify_iterochat_signature(secret: str, timestamp: str, body: bytes, signature: str) -> bool:
signed = f"{timestamp}.{body.decode('utf-8')}"
expected = "sha256=" + hmac.new(
secret.encode(),
signed.encode(),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature)
Node.js example
const crypto = require("crypto");
function verifyIteroChatSignature(secret, timestamp, body, signature) {
const signed = `${timestamp}.${body}`;
const expected =
"sha256=" +
crypto.createHmac("sha256", secret).update(signed).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
Verify the signature against the raw request body, not a re-serialized JSON object. Parsing and re-stringifying can change key order or whitespace and break verification.
Reject requests whose timestamp is too far from your server's current time (for example, more than five minutes). This limits replay even if a request is intercepted.
Retries
IteroChat treats any 2xx HTTP response as success. Timeouts, connection errors, and any other status code (including 4xx) are treated as failure and scheduled for retry.
After a failed attempt, IteroChat waits at least the following interval before the next try: 30 seconds, 1 minute, 2 minutes, 5 minutes, then 10 minutes, in that order.
There are five delivery attempts in total for each event. After the fifth failure, the delivery is marked as failed and no further automatic retries run for that event.
These intervals are minimum wait times. Retries may arrive slightly later depending on when the next delivery pass runs.
You can inspect past deliveries and manually redeliver failed events from the subscription's delivery log in Settings → Notifications.
If a subscription fails repeatedly, IteroChat emails your owners and admins and shows a warning on the subscription in the dashboard. The webhook stays active so you can fix your endpoint without losing future alerts.
Test events (ping)
Use Send test event on a subscription to POST a one-off ping event to your URL. This confirms your endpoint is reachable and your signature verification works.
The test payload uses the same envelope and headers as real events:
{
"id": "…",
"type": "ping",
"created_at": "2026-07-27T09:14:02.311Z",
"organization_id": "…",
"data": {
"message": "This is a test event from IteroChat."
}
}
ping is for testing only. It cannot be enabled as a subscription event, does not appear in the delivery log, and is not retried through the normal delivery queue.
Related
- Handling conversations: claiming, replying, and resolving escalated chats.
- AI configuration: when the AI escalates and how handoff summaries are written.