Your server
Receive events with a webhook
Your server gets a request for each message and each Case opened or resolved.
Updated October 28, 2026
The webhook sends a JSON POST request to your server for each event of the App. For example, to send a push notification from your server to a Contact who left the app: see Send push notifications.
Set it up
- Open App settings > Developers > Webhook.
- Enter your server’s https address, then save.
- Copy the signing secret that appears and keep it on your server.
- While you develop, receive the events on your computer with the Zean CLI. See Receive webhooks on your computer.
Events
| Event | Data | Description |
|---|---|---|
| message.contact | data.message | The Contact wrote. |
| message.member | data.message, data.push | A Member replied to the Contact. Internal notes and automatic AI replies send no event. |
| case.opened | data.caseId | A Case opens: on the first message, or when the conversation reopens. |
| case.resolved | data.caseId | A Case is resolved, by a Member or automatically. |
The request
- Method
POST, with theContent-Type: application/jsonheader. Zean-Eventheader: the event type.Zean-Signatureheader: the signature, to check before anything else. See Verify a webhook signature.
The body, for message.contact:
{ "id": "3f1c9a52-6a0e-4c43-9a51-2d1f0e8b7c11", "type": "message.contact", "test": false, "createdAt": 1760515200000, "appId": "…", "data": { "conversationId": "…", "contact": { "id": "…", "externalId": "42", "name": "Awa Diallo", "email": "awa.diallo@example.com" }, "message": { "id": "…", "body": "Hi, has my order shipped?", "attachments": [ { "name": "receipt.jpg", "type": "image/jpeg", "size": 184320 } ], "createdAt": 1760515200000 } }}| Field | Type | Description |
|---|---|---|
| id | string | The event’s ID. It stays the same from one try to the next: use it to skip a duplicate. |
| type | string | The type, as in the Zean-Event header. |
| test | boolean | true for a test event, sent only to the Zean CLI, never to the webhook address. false otherwise. |
| createdAt | number | When the event happened, in Unix milliseconds. |
| appId | string | The App it concerns. |
| data.conversationId | string | The Contact’s conversation. |
| data.contact | object | null | id, then externalId (a Verified Contact’s sub), name and email, null when unknown. null if the Contact no longer exists. |
| data.message | object | For message.contact and message.member: id, body, attachments (each file’s name, type and size; the files stay private), createdAt, and memberId for message.member. |
| data.push | object | For message.member: the push notification ready to send to the Contact. See below. |
| data.caseId | string | For case.opened and case.resolved, in place of data.message. |
A Member’s reply and its push notification
Only a Member’s reply sends message.member. Its body carries data.push, the notification ready to send to each of the Contact’s devices:
{ "id": "8b2e4f10-3c7d-4a9e-b5f1-6d0a2c9e7b34", "type": "message.member", "test": false, "createdAt": 1760515260000, "appId": "…", "data": { "conversationId": "…", "contact": { "id": "…", "externalId": "42", "name": "Awa Diallo", "email": "awa.diallo@example.com" }, "message": { "id": "…", "body": "Your order left the warehouse this morning.", "attachments": [], "memberId": "…", "createdAt": 1760515260000 }, "push": { "title": "Kibo Shop", "body": "Your order left the warehouse this morning.", "language": "en", "tokens": ["ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]"], "data": { "zean": "message", "appId": "…", "conversationId": "…" } } }}| Field | Type | Description |
|---|---|---|
| push.title | string | The App’s name. |
| push.body | string | The reply, cut to 150 characters with “…”. For files only: “Sent a photo”, “Sent 3 photos”, “Sent a file” or “Sent 3 files”, or “A envoyé une photo”, “A envoyé 3 photos”, “A envoyé un fichier” or “A envoyé 3 fichiers” for a Contact who speaks French. |
| push.language | string | The Contact’s language, such as fr or pt-BR. A language starting with fr gives a French body. |
| push.tokens | string[] | The push token of each of the Contact’s devices that gave one with setPushToken(): Expo, FCM or APNs. Empty when there is none. |
| push.data | object | To attach to the notification as its data. The SDK recognises it on a tap and opens the chat. See Send push notifications. |
Delivery
- The address must be public and use https: localhost and private networks are refused. On your computer, use the Zean CLI.
- Each event goes to the webhook address and to every running Zean CLI.
- Answer with a 2xx code within 10 seconds. Do long work after answering.
- Otherwise, the call to the address is tried again up to 5 times: after 1 minute, 5 minutes, 30 minutes, 2 hours, then 6 hours.
- Each try carries a fresh signature, but the same
id.
Recent deliveries
App settings > Developers > Webhook > “Recent deliveries” shows the App’s last 50 calls, test events and “Send again” included. For each:
- the event, and where it went: “Webhook address” or “Zean CLI”;
- your server’s status code, or the error: timed out, connection refused, address not found, certificate error, no answer;
- the start of your server’s reply, the time it took, when it was sent and the attempt number;
- the “Test” badge for a test event.
“Send again” sends the same event once more, with the same id and the next attempt number. A successful send again to the address stops that event’s automatic retries.
Test events
A test event goes only to the running Zean CLIs, never to the webhook address: your production server never gets a test, and no real Contact is notified. Its test field is true.
- Run the Zean CLI on your computer. See Receive webhooks on your computer.
- Open the chat from a development build of your app or site: Expo Go, a React Native build in
__DEV__, or a site on localhost. For 24 hours, this device can be “Your test device”. - In App settings > Developers > Webhook > “Test events”, pick the event, then “Send a test event”. The button works while a Zean CLI runs.
- The event carries your test device’s Contact. Without a test device, a sample Contact, with
externalIdset to"user_123". - For
message.member, once your test device has written, Zean writes a real test reply in its Conversation: the push notification, the badge and the tap that opens the chat are real. It is marked “Test reply” in the inbox; no AI reads it and the stats do not count it. - The result shows in “Recent deliveries”. A test event is never retried.
Every event is always sent: ignore the ones you do not need. Leave the address empty to receive nothing at the address.