Payload shape
messages.post
{
"event": "messages.post",
"messages": [
{
"id": "{providerMessageId}",
"message_id": "{providerMessageId}",
"chat_id": "{providerChatId}",
"from": "{providerChatId}",
"conversationId": "{conversationId}",
"conversation_id": "{conversationId}",
"contactPhoneNumber": "+972501234567",
"contact_phone_number": "+972501234567",
"from_me": false,
"timestamp": 1778580000,
"type": "text",
"text": { "body": "Hello" },
"body": "Hello"
}
]
}
statuses.post
{
"event": "statuses.post",
"statuses": [
{
"id": "{providerMessageId}",
"message_id": "{providerMessageId}",
"status": "delivered",
"timestamp": 1778580000
}
]
}
chats.post
{
"event": "chats.post",
"chats": [
{
"id": "{providerChatId}",
"chat_id": "{providerChatId}",
"conversationId": "{conversationId}",
"type": "individual",
"timestamp": 1778580000,
"last_message": {
"id": "{providerMessageId}",
"type": "text",
"body": "Hello",
"from_me": false
}
}
]
}
Payload rules
- Inbound and observed outbound messages can emit
messages.post and chats.post.
- Status updates emit
statuses.post.
- Use
conversationId for in-thread replies when present.
- Provider ids are not tenant ownership truth.
Minimal handler contract
export async function handleEzwWebhook(request) {
const deliveryId = request.headers.get("x-ezw-delivery-id");
const event = request.headers.get("x-ezw-webhook-event");
const lineId = request.headers.get("x-ezw-line-id");
const body = await request.json();
if (!deliveryId || !event || !lineId) {
return new Response("missing EZWhatsApp headers", { status: 400 });
}
if (await alreadyProcessed(deliveryId)) {
return new Response("duplicate accepted", { status: 200 });
}
await saveWebhookEvent({ deliveryId, event, lineId, body });
await processEventForClientWorkflow({ deliveryId, event, lineId, body });
return new Response("accepted", { status: 200 });
}
Reply in-thread from a webhook
When a webhook payload includes conversationId, use it instead of deriving a phone number from provider-specific ids.
curl -X POST 'https://app.ezw.solutions/api/v1/messages/text' \
-H 'Authorization: Bearer {lineApiToken}' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: webhook-reply:{deliveryId}' \
-d '{
"conversationId": "{conversationId}",
"body": "Thanks. A team member will continue here."
}'
Security and operations
- Use HTTPS for production webhook targets.
- Keep target URLs stable. Rotating a webhook should be an explicit deployment step.
- Do not put line API tokens in frontend code or public automation exports.
- Use your own receiving-service authentication if the client requires it, such as a secret path segment or gateway rule.
- Return 2xx only after the event is accepted durably enough for your workflow.
Troubleshooting
| Symptom |
Likely cause |
Next check |
400 invalid_channel_webhook |
Invalid body, event, boolean, or URL. |
Check targetUrl, events, and payloadFormat. |
401 invalid_channel_api_token |
Token is missing, revoked, or not attached to an active line. |
Rotate token from workspace and retest health. |
| No deliveries arrive |
Webhook disabled, target failing, or no qualifying line events yet. |
List webhooks, trigger a real inbound, and check receiving logs by line id. |
| Duplicate downstream action |
Receiver does not dedupe by delivery id. |
Store and check X-EZW-Delivery-Id before processing. |