Webhooks overview

This document provides detailed technical information for developers integrating with Chatway Webhooks, including event schemas, delivery behavior, security, retries, and best practices.

Chatway sends HTTP POST requests to your configured HTTPS endpoint when a subscribed event occurs.

Replying to a visitor message

For the full end-to-end tutorial (subscribe to message.received, verify the signature, read data.conversation.id, and call POST /messages), start with Quickstart → Receive a message and send a reply in the sidebar.

Endpoint requirements

Requirement Value
Supported protocol HTTPS only
Request method POST
Expected response Any 2xx HTTP status code acknowledges receipt. 5xx, timeouts, and network errors are retried; most 4xx responses (including 410) are not.
Redirects Not followed. Your endpoint must respond directly (no 3xx redirect chain).
Network targets Public HTTPS endpoints only. Private, loopback, and link-local addresses (for example 127.0.0.1, 10.0.0.0/8, 169.254.169.254) are blocked at delivery time.

Request headers

Every webhook request includes the following headers:

Content-Type: application/json
X-Chatway-Event: message.received
X-Chatway-Webhook-Id: 9f3c2a1b-4d5e-6f70-8a9b-0c1d2e3f4a5b
X-Chatway-Delivery-Id: del_456
X-Chatway-Delivery-Attempt: 1
X-Chatway-Timestamp: 1785933000
X-Chatway-Signature: sha256=3c9f5e...
Header Description
X-Chatway-Event Event name (for example, message.received)
X-Chatway-Webhook-Id Unique webhook subscription ID
X-Chatway-Delivery-Id Unique ID for this HTTP delivery attempt (del_…). A new value is sent on each retry.
X-Chatway-Delivery-Attempt Attempt number for this event (1 on first try, then 2, 3, … up to 4). Sent as a decimal string in the header.
X-Chatway-Timestamp Unix timestamp in seconds when the request was signed. Sent as a decimal string in the header. Use with signature verification to limit replay attacks.
X-Chatway-Signature HMAC SHA256 of {timestamp}.{raw_request_body}

Signature verification

Chatway signs each request using HMAC SHA256.

Signature generation

signed_payload = timestamp + "." + raw_request_body
HMAC_SHA256(secret, signed_payload)

The header value is prefixed with sha256=. The timestamp is the value of X-Chatway-Timestamp.

Verification steps

  1. Read the raw request body before JSON parsing.
  2. Extract X-Chatway-Signature and X-Chatway-Timestamp.
  3. Reject requests whose timestamp is older than 300 seconds (recommended).
  4. Compute HMAC SHA256 over {timestamp}.{raw_body} using your webhook secret.
  5. Compare using a constant-time comparison.

Example (Node.js + Express)

const crypto = require("crypto");
const express = require("express");

const app = express();

// Preserve raw body for signature verification (required).
app.post(
  "/webhooks/chatway",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const secret = process.env.CHATWAY_WEBHOOK_SECRET;
    const signature = req.headers["x-chatway-signature"];
    const timestamp = req.headers["x-chatway-timestamp"];
    const rawBody = req.body; // Buffer

    if (!verifyChatwaySignature(rawBody, signature, timestamp, secret)) {
      return res.status(401).send("Invalid signature");
    }

    res.status(200).send("ok");
  },
);

function verifyChatwaySignature(rawBody, signatureHeader, timestampHeader, secret) {
  if (!signatureHeader || !timestampHeader || !secret) {
    return false;
  }

  const timestamp = Number(timestampHeader);
  if (!Number.isFinite(timestamp)) {
    return false;
  }

  const toleranceSeconds = 300;
  if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > toleranceSeconds) {
    return false;
  }

  const signedPayload = `${timestamp}.${rawBody.toString("utf8")}`;
  const expected = crypto.createHmac("sha256", secret).update(signedPayload).digest("hex");
  const expectedHeader = `sha256=${expected}`;

  const expectedBuffer = Buffer.from(expectedHeader);
  const actualBuffer = Buffer.from(signatureHeader);

  if (expectedBuffer.length !== actualBuffer.length) {
    return false;
  }

  return crypto.timingSafeEqual(expectedBuffer, actualBuffer);
}

Example (PHP)

function verifyChatwaySignature(
    string $rawBody,
    ?string $signatureHeader,
    ?string $timestampHeader,
    string $secret,
    int $toleranceSeconds = 300
): bool {
    if ($signatureHeader === null || $signatureHeader === '' || $timestampHeader === null || $timestampHeader === '') {
        return false;
    }

    $timestamp = filter_var($timestampHeader, FILTER_VALIDATE_INT);
    if ($timestamp === false) {
        return false;
    }

    if (abs(time() - $timestamp) > $toleranceSeconds) {
        return false;
    }

    $signedPayload = $timestamp.'.'.$rawBody;
    $expected = 'sha256='.hash_hmac('sha256', $signedPayload, $secret);

    return hash_equals($expected, $signatureHeader);
}

// Laravel example
$rawBody = $request->getContent();
$signature = $request->header('X-Chatway-Signature');
$timestamp = $request->header('X-Chatway-Timestamp');
$secret = config('services.chatway.webhook_secret');

if (! verifyChatwaySignature($rawBody, $signature, $timestamp, $secret)) {
    abort(401, 'Invalid webhook signature.');
}

Event payload structure

All webhook payloads share a common envelope:

{
    "id": "evt_1234567890abcdef1234567890abcdef",
    "event": "message.received",
    "data": {
        "message": {
            "id": "679f5ba4-3cbf-42eb-86dc-63566cec6b58",
            "content": "Hello, I need help",
            "attachments": [],
            "created_at": "2026-02-04T10:31:12Z"
        },
        "conversation": {
            "id": "750c42a1-1ce1-40ae-beba-4cc66f2b1023",
            "channel": "website",
            "status": "open",
            "widget_name": "Main site widget",
            "identifier": "wdg_abc123"
        },
        "visitor": {
            "id": "bfc39f4b-e8aa-4e1c-8e6a-162a5d869702",
            "name": "John Doe",
            "email": "[email protected]",
            "phone": "+1XXXXXXXXXX"
        }
    },
    "occurred_at": "2026-02-04T10:31:12Z",
    "version": "2026-09"
}
Field Type Description
id string Stable event ID (evt_…). Same value on every retry for this notification.
event string Event name
data object Event-specific payload documented on each webhook below.
occurred_at ISO 8601 Event timestamp
version string Payload version (2026-09)

Field reference

Common types, optional fields, and normalized channel values used across events.

Envelope

Field Type Notes
id string Stable event ID (evt_…). Identical across HTTP retries for the same notification to your endpoint. Use for idempotent processing.
event string Event name (for example message.received).
data object Event-specific payload.
occurred_at string (ISO 8601) When the business event occurred. Stable across retries for a given id.
version string Payload schema version.

Conversation

Field Type Notes
id string (UUID) Conversation ID.
channel string Normalized channel: website, email, messenger, whatsapp, fb, instagram.
status string open or resolved.
widget_name string (optional) Present when the conversation is tied to a website widget.
identifier string (optional) Widget identifier when widget_name is present.
resolved_at string (ISO 8601) (optional) Present on conversation.resolved when status is resolved.
created_at string (ISO 8601) (optional) Present on conversation.created.
first_page_url string (optional) Present on conversation.created when available.

Agent

Field Type Notes
id string (UUID) Agent ID.
name string Agent display name.
email string (optional) Agent email when available. Used on agent.assigned, conversation.resolved, and message.sent.

Visitor contact

Field Type Notes
id string (UUID) Visitor/contact ID.
name string null (optional)
email string null
phone string null

Contact source

Field Type Notes
source string Page URL when the visitor came from the widget (for example https://example.com/pricing), or a normalized channel label (email, messenger, whatsapp, fb) for integrated channels.

Assigned by

Field Type Notes
assigned_by object Who performed the action. Contains exactly one of: agent (id, name, email), automation (id UUID, name), or system (empty object). Used on tag.assigned, custom.data.assigned, and agent.assigned.

Removed by

Field Type Notes
removed_by object Same shape as assigned_by. Who removed the tag or custom data: agent, automation (id UUID, name), or system (empty object). Used on tag.removed and custom.data.removed.

Resolved by

Field Type Notes
resolved_by object Same shape as assigned_by. Who resolved the conversation: agent, automation (id UUID, name), or system (empty object). Used on conversation.resolved.

Message

Field Type Notes
content string (optional) Message text. May be omitted when attachments are present.
attachments array Zero or more attachment objects (see below).

Attachment

Field Type Notes
content string (URL) Public URL of the file.
type string (optional) Logical type (for example image, file, video).
mime string (optional) MIME type when known.
original_file_name string (optional) Original filename when available.
size integer null (optional)

Contact updated

Field Type Notes
previous_values object Map of field names to values before the update. Only includes keys listed in updated_fields.
current_values object Map of field names to values after the update.

Custom data

Field Type Notes
custom_data object Single key/value pair for custom.data.assigned and custom.data.removed. Values are strings or JSON-serializable scalars; complex objects may appear depending on how the field was stored.

Limits

Field Type Notes
attachment URLs string (optional) Up to 2048 characters per URL (aligned with upload validation).
attachments per message integer (optional) Typically up to 9 files per message in the product UI.

Idempotency & retries

Timeout & processing requirements

Chatway enforces a strict 4-second timeout for webhook deliveries. If your endpoint does not respond within 4 seconds, the delivery is marked as failed and retried according to the retry policy.

Delivery & retry policy

Retry conditions

No retry

Retry schedule

  1. Immediate
  2. +1 minute
  3. +5 minutes
  4. +15 minutes

Chatway makes up to 4 delivery attempts. After the final failure, the delivery is marked as failed.

Required behavior