Iterochat Docs
← Back to home
Iterochat Docs

Get started

  • Introduction
  • Quickstart
  • The chat widget
  • Connect WordPress

Configure & operate

  • Knowledge base
  • AI configuration
  • Handling conversations
  • WhatsApp channel
  • Team & roles
  • Accounts & organizations
  • Plans & usage
  • FAQ & troubleshooting
  • Data & privacy
  • Outbound webhooks
DocsConfigure & operateOutbound webhooks

Outbound webhooks

Quick Answer: In Settings → Notifications, add a subscription with your HTTPS endpoint URL, copy the signing secret, and enable escalation.created and/or escalation.unclaimed. IteroChat POSTs signed JSON to your URL; verify X-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

  1. Open Settings → Notifications.
  2. Choose New webhook.
  3. Enter a name (for your reference) and your URL.
  4. Enable the events you want:
    • escalation.created for an immediate alert on every handoff.
    • escalation.unclaimed for a follow-up if nobody claims the chat in time.
  5. 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_url and handoff_summary to 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, and last_customer_message.
  • Load-aware routing: use data.load.agents_available and data.load.queued_count to 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

HeaderDescription
Content-TypeAlways application/json
X-IteroChat-Event-IdUnique id for this event (stable across retries)
X-IteroChat-Event-TypeEvent name, e.g. escalation.created
X-IteroChat-TimestampUnix timestamp in seconds, included in the signature
X-IteroChat-AttemptDelivery attempt number (starts at 1)
X-IteroChat-SignatureHMAC-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": { }
}
FieldDescription
idSame value as X-IteroChat-Event-Id
typeSame value as X-IteroChat-Event-Type
created_atWhen the event was created (ISO 8601, UTC)
organization_idYour IteroChat organization id
dataEvent-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 escalation
  • queued_count: how many conversations are currently in the queue
  • agents_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.

Last updated: Mon Jul 27 2026 00:00:00 GMT+0000 (Coordinated Universal Time)

PreviousData & privacy

On this page

  • Quickstart: integrate your endpoint
  • 1. Create a public HTTPS endpoint
  • 2. Add a subscription in the dashboard
  • 3. Verify signatures on every request
  • 4. Send a test event
  • 5. Handle real events
  • Example: Express (Node.js)
  • Example: Flask (Python)
  • Routing ideas
  • Events
  • `escalation.created`
  • `escalation.unclaimed`
  • Request format
  • Headers
  • JSON envelope
  • Escalation event data
  • Verifying signatures
  • Python example
  • Node.js example
  • Retries
  • Test events (`ping`)
  • Related