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
Open Configure → Developers
The Webhooks card is under API keys. Only workspace owners and admins can add, edit, test, rotate or remove endpoints.
Enter the Endpoint URL
A public
https://address on your server, for examplehttps://example.com/hooks/pepochat.Pick the events
Tick the events this URL should receive under Events.
conversation.escalated,conversation.resolvedandlead.createdare ticked to start with.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.Send a test
Click Send test, then Recent deliveries to see whether your server accepted it.
Events
| Event | Sent when |
|---|---|
conversation.created | A visitor started a conversation: in the widget or the hosted chat page, through the API, on a messaging channel or by voice. |
conversation.escalated | A 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.resolved | A conversation was resolved: by the agent, by a teammate, through the API, or by the visitor with End chat in the widget. |
message.received | A visitor sent a message, on any channel. |
lead.created | A new contact was captured from the chat, the pre-chat form, email verification, a booking or a data form. |
collect_data.submitted | The 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:
| Event | data fields |
|---|---|
message.received | The conversation fields above, plus message: id, text, attachments (how many files came with it) and createdAt. |
lead.created | contactId, email, name, phone, company, externalId, attributes, source, conversationId, createdAt. |
collect_data.submitted | submissionId, 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
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | PepoChat-Webhooks/1.0 |
X-PepoChat-Event | The event type, for example lead.created. |
X-PepoChat-Delivery | The delivery id. It stays the same across retries of one delivery. |
X-PepoChat-Signature | t=<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
2xxresponse counts as delivered. Reply quickly and do slow work afterwards: PepoChat waits 10 seconds for a response. - A timeout, a network error, a
5xx, a408or a429is 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, a401or 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
createdAtto order them. A retry after a timeout can repeat an event your server already handled, so ignore anidyou 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
idstarts withevt_test_. Whatever the event, itsdatais a short conversation-style sample:"test": true, theconversationIdtest_conversation, astatusthat 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
| Message | Meaning |
|---|---|
| The webhook URL must start with https:// | Plain http:// addresses are refused. |
| URL host is not allowed | The host is localhost, a private network address or an internal name such as one ending in .local or .internal. |
| URL must not contain credentials | Remove the user:password@ part and authenticate with the signature instead. |
| URL is not valid | The address could not be parsed. |
| A workspace can have at most 5 webhook endpoints | Remove an endpoint first. |
| Only workspace owners and admins can manage this | Ask 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 10s | Your server took longer than 10 seconds. The delivery is retried. |
| Endpoint is switched off | The endpoint was off when the delivery was due. |
Something missing or wrong on this page? Tell us and we will fix it.
