Outbound webhooks

Receive signed JSON events when conversations start, escalate or resolve, visitors send messages, leads arrive or the agent collects a data form.

Webhooks tell your own systems what happens in PepoChat as it happens. When a conversation starts, is handed to your team or is resolved, when a visitor sends a message, when a new lead arrives or when the agent completes a data form, PepoChat POSTs a signed JSON event to each HTTPS endpoint you subscribed, and retries when your server fails. Webhooks are on every plan.

Add an endpoint

  1. Open Configure → Developers

    The Webhooks card is under API keys. Only workspace owners and admins can add, edit, test, rotate or remove endpoints.

  2. Enter the Endpoint URL

    A public https:// address on your server, for example https://example.com/hooks/pepochat.

  3. Pick the events

    Tick the events this URL should receive under Events. conversation.escalated, conversation.resolved and lead.created are ticked to start with.

  4. Click Add endpoint

    The endpoint is switched on at once and gets its own signing secret, whsec_ followed by 64 hexadecimal characters. Click the eye icon next to Secret to show it, or the copy icon to copy it.

  5. Send a test

    Click Send test, then Recent deliveries to see whether your server accepted it.

Events

EventSent when
conversation.createdA visitor started a conversation: in the widget or the hosted chat page, through the API, on a messaging channel or by voice.
conversation.escalatedA conversation was handed to the team: by the agent, by a teammate's reply or status change, through the API, when the agent could not generate a reply, or when a Free workspace has used its monthly AI replies.
conversation.resolvedA conversation was resolved: by the agent, by a teammate, through the API, or by the visitor with End chat in the widget.
message.receivedA visitor sent a message, on any channel.
lead.createdA new contact was captured from the chat, the pre-chat form, email verification, a booking or a data form.
collect_data.submittedThe agent collected a data form.

Chats in Test the agent never send events. Contacts that arrive by CSV import, through the REST API or with a visitor identity token do not send lead.created: they are your own records, not new leads.

The payload

Every event has the same envelope. id names the event, createdAt is a Unix timestamp in milliseconds, and data depends on the type.

{
  "id": "evt_5f0c2a9e71b3d4c8a6e2f019",
  "type": "conversation.escalated",
  "createdAt": 1762941600000,
  "workspaceId": "...",
  "data": {
    "conversationId": "k17...",
    "status": "escalated",
    "channel": "widget",
    "channelLabel": "Website widget",
    "priority": "normal",
    "tags": [],
    "assignedTo": null,
    "topic": "Shipping",
    "createdAt": 1762941480000,
    "escalatedAt": 1762941600000,
    "resolvedAt": null,
    "lastMessage": { "text": "Conversation escalated to a human operator.", "role": "assistant", "createdAt": 1762941600000 },
    "visitor": {
      "sessionId": "j57...",
      "name": "Ada Lovelace",
      "email": "[email protected]",
      "emailVerified": true,
      "country": "GB",
      "pageUrl": "https://example.com/pricing"
    },
    "url": "https://app.pepochat.com/conversations/k17...?org=..."
  }
}

The three conversation.* events carry the conversation as above, with url linking to it in your dashboard. They hold no message bodies besides lastMessage; fetch the transcript with GET /conversations/{id} from the REST API when you need it. The other events carry:

Eventdata fields
message.receivedThe conversation fields above, plus message: id, text, attachments (how many files came with it) and createdAt.
lead.createdcontactId, email, name, phone, company, externalId, attributes, source, conversationId, createdAt.
collect_data.submittedsubmissionId, collectionId, collectionName, conversationId, contactId, visitor (name, email, externalId), values (the answers by field key), submittedAt.

Optional fields without a value are null; tags and attributes are empty instead.

Headers

HeaderValue
Content-Typeapplication/json
User-AgentPepoChat-Webhooks/1.0
X-PepoChat-EventThe event type, for example lead.created.
X-PepoChat-DeliveryThe delivery id. It stays the same across retries of one delivery.
X-PepoChat-Signaturet=<timestamp>,v1=<signature>

Verify the signature

v1 is the HMAC-SHA256 of the timestamp, a dot and the raw request body, keyed with the endpoint's whole secret (including whsec_), in lowercase hex. t is in Unix milliseconds and is set again on every attempt, so a retry carries a fresh signature over the same body. Compute the HMAC over the body exactly as received, before any JSON parsing, compare in constant time, and reject old timestamps:

import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyPepoChat(rawBody, signatureHeader, secret) {
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((part) => part.split("=")),
  );
  const expected = createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");
  const fresh = Math.abs(Date.now() - Number(parts.t)) < 5 * 60 * 1000;
  return (
    fresh &&
    typeof parts.v1 === "string" &&
    expected.length === parts.v1.length &&
    timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
  );
}

Delivery and retries

  • Any 2xx response counts as delivered. Reply quickly and do slow work afterwards: PepoChat waits 10 seconds for a response.
  • A timeout, a network error, a 5xx, a 408 or a 429 is retried after 1 minute and again after 10 minutes. After the third attempt the delivery is marked failed.
  • Any other response, such as a 404, a 401 or a redirect, fails at once. Redirects are not followed.
  • Each event and each retry is sent on its own, so events can arrive out of order; use createdAt to order them. A retry after a timeout can repeat an event your server already handled, so ignore an id you have seen.
  • When 20 deliveries in a row fail, the endpoint is switched off. Its row shows the last error and how many failures came in a row, and switching it back on clears the count. While an endpoint is off, events are not sent to it and are not sent later; a retry that was already scheduled fails with "Endpoint is switched off".

Recent deliveries lists the endpoint's last 20 deliveries with the event, the result (pending, retrying while a retry is scheduled, delivered or failed), the HTTP status and the number of attempts. Delivery records are kept for 30 days.

Manage an endpoint

  • Edit changes the URL and the events. The secret stays the same.
  • The On and Off switch pauses and resumes deliveries.
  • Rotate replaces the secret after you click Rotate now to confirm: "Your receiver rejects events until it has the new secret." The old secret stops at once, and retries of earlier events are signed with the new one.
  • Send test sends a sample of the first event in the endpoint's list through the normal delivery path. The sample's id starts with evt_test_. Whatever the event, its data is a short conversation-style sample: "test": true, the conversationId test_conversation, a status that matches the event, and the visitor "Test visitor" with the email [email protected].
  • Remove asks you to confirm with Remove endpoint. Events stop going to the URL and its delivery log is deleted.

The Send to a webhook URL option under new-lead notifications in Widget Customization is a separate webhook with its own secret and a contact.created event; see Lead notifications.

Limits

  • 5 endpoints per workspace, each subscribed to any of the 6 events.
  • 3 attempts per delivery, 10 seconds per attempt.
  • Automatic switch-off after 20 failed deliveries in a row.
  • Send test can be used 30 times an hour per workspace, 5 in quick succession.

Common errors

MessageMeaning
The webhook URL must start with https://Plain http:// addresses are refused.
URL host is not allowedThe host is localhost, a private network address or an internal name such as one ending in .local or .internal.
URL must not contain credentialsRemove the user:password@ part and authenticate with the signature instead.
URL is not validThe address could not be parsed.
A workspace can have at most 5 webhook endpointsRemove an endpoint first.
Only workspace owners and admins can manage thisAsk an owner or admin to make the change.
HTTP 500 (or another status)Your server answered with that status. 5xx is retried, most 4xx are not.
No response within 10sYour server took longer than 10 seconds. The delivery is retried.
Endpoint is switched offThe endpoint was off when the delivery was due.

Something missing or wrong on this page? Tell us and we will fix it.