EZWhatsApp Webhooks

Webhook guide

Receive message and status events from one WhatsApp line.

EZWhatsApp webhooks are line-scoped integration events. Use them to sync inbound messages, outbound observations, chat updates, and delivery status into client systems without treating the integration as EZWhatsApp's source of truth.

Configure a webhook

Use a line API token for the current line, or a workspace ID token for an owner/admin managing a specific line.

Channel API

Configure with line token

curl -X POST 'https://app.ezw.solutions/api/v1/webhooks' \
  -H 'Authorization: Bearer {lineApiToken}' \
  -H 'Content-Type: application/json' \
  -d '{
    "targetUrl": "https://agency.example.com/ezw/webhook",
    "events": ["messages.post", "statuses.post", "chats.post"],
    "payloadFormat": "whapi",
    "enabled": true,
    "persistent": true
  }'
Workspace API

Configure with workspace token

curl -X POST 'https://app.ezw.solutions/api/lines/{lineId}/webhooks' \
  -H 'Authorization: Bearer {idToken}' \
  -H 'Content-Type: application/json' \
  -d '{
    "targetUrl": "https://agency.example.com/ezw/webhook",
    "events": ["messages.post", "statuses.post"],
    "enabled": true
  }'
Field Required Rules
targetUrl or url Yes HTTP/HTTPS URL. Localhost, loopback, private IP, and link-local targets are rejected.
events No Defaults to all supported events. Supported values are messages.post, statuses.post, and chats.post.
payloadFormat No Only whapi is currently supported.
enabled No Boolean. Defaults to true.
persistent No Boolean. Defaults to true.

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.

Delivery headers

Webhook deliveries include EZWhatsApp headers that the receiving service should log and use for idempotency.

X-EZW-Webhook-Event: messages.post
X-EZW-Line-Id: {lineId}
X-EZW-Webhook-Id: {webhookId}
X-EZW-Delivery-Id: {deliveryDeduplicationId}
Content-Type: application/json

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.