EZWhatsApp Examples

Current examples

Use these examples for the current EZWhatsApp API.

These examples cover Channel API health and sends, webhook setup, inbound contact identity, managed customer tags, workspace sends, campaigns, inbox reads, and retry-safe receivers.

Check health before sending

curl 'https://app.ezw.solutions/api/v1/health?wakeup=false&channel_type=web' \
  -H 'Authorization: Bearer {lineApiToken}'

Send text

Direct phone recipient

curl -X POST 'https://app.ezw.solutions/api/v1/messages/text' \
  -H 'Authorization: Bearer {lineApiToken}' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: order-123-ready' \
  -d '{
    "to": "+972501234567",
    "body": "Your order is ready.",
    "ctaUrlButton": {
      "displayText": "Open order",
      "url": "https://store.example.com/orders/123"
    }
  }'

Existing conversation

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-123' \
  -d '{
    "conversationId": "{conversationId}",
    "body": "Thanks, I am checking this now."
  }'

WhatsApp group JID

curl -X POST 'https://app.ezw.solutions/api/v1/messages/text' \
  -H 'Authorization: Bearer {lineApiToken}' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: group-update-123' \
  -d '{
    "groupId": "120363012345678901@g.us",
    "body": "Group update"
  }'

Send URL media

Channel API media must be reachable through an external HTTP/HTTPS URL. Use to or groupId; this route does not accept conversationId.

Direct document URL

curl -X POST 'https://app.ezw.solutions/api/v1/messages/media' \
  -H 'Authorization: Bearer {lineApiToken}' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: invoice-123' \
  -d '{
    "to": "+972501234567",
    "type": "document",
    "media": {
      "url": "https://cdn.example.com/invoices/123.pdf",
      "mime_type": "application/pdf",
      "filename": "invoice-123.pdf"
    },
    "caption": "Invoice 123"
  }'

Group image URL

curl -X POST 'https://app.ezw.solutions/api/v1/messages/media' \
  -H 'Authorization: Bearer {lineApiToken}' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: group-photo-123' \
  -d '{
    "groupJid": "120363012345678901@g.us",
    "type": "image",
    "mediaUrl": "https://cdn.example.com/photo.jpg",
    "contentType": "image/jpeg",
    "caption": "Photo update"
  }'

Configure a line webhook

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
  }'

Resolve one inbound message's contact identity

curl -X POST 'https://app.ezw.solutions/api/v1/messages/contact-identity' \
  -H 'Authorization: Bearer {lineApiToken}' \
  -H 'Content-Type: application/json' \
  -d '{"providerMessageId":"{exactProviderMessageId}"}'

Use the exact unmodified provider message id. A valid retained individual inbound can still return contactIdentity.state: "missing" or "conflict", with no phone number. Do not infer a phone from a WhatsApp LID or a contact name.

Send as a workspace user

Text reply

curl -X POST 'https://app.ezw.solutions/api/lines/{lineId}/send' \
  -H 'Authorization: Bearer {idToken}' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: workspace-reply-123' \
  -d '{
    "conversationId": "{conversationId}",
    "bodyText": "Thanks, I am checking this now."
  }'

Direct phone send

curl -X POST 'https://app.ezw.solutions/api/lines/{lineId}/send' \
  -H 'Authorization: Bearer {idToken}' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: workspace-direct-123' \
  -d '{
    "recipientPhoneNumber": "+972501234567",
    "bodyText": "Hello from the workspace."
  }'

One uploaded image

curl -X POST 'https://app.ezw.solutions/api/lines/{lineId}/send' \
  -H 'Authorization: Bearer {idToken}' \
  -H 'Idempotency-Key: image-send-123' \
  -F 'conversationId={conversationId}' \
  -F 'bodyText=Attached photo' \
  -F 'media=@/absolute/path/photo.jpg;type=image/jpeg'

Forward stored message

curl -X POST 'https://app.ezw.solutions/api/lines/{lineId}/send' \
  -H 'Authorization: Bearer {idToken}' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: forward-123' \
  -d '{
    "recipientPhoneNumber": "+972501234567",
    "forwardedFromMessageId": "{messageId}"
  }'

Contacts

Set the final operator display name from an external service

curl -X PATCH 'https://app.ezw.solutions/api/v1/contacts/by-phone' \
  -H 'Authorization: Bearer {lineApiToken}' \
  -H 'Content-Type: application/json' \
  -d '{
    "phoneNumber": "+972501234567",
    "displayName": "Dana Levi · Order 12345"
  }'

The phone must start with +. HTTP 200 with matched: false means no contact on that token's line matched; the request is still complete.

Import workspace directory contacts

curl -X POST 'https://app.ezw.solutions/api/contacts' \
  -H 'Authorization: Bearer {idToken}' \
  -H 'Content-Type: application/json' \
  -d '{
    "sourceFilename": "customers.csv",
    "rows": [
      {
        "phoneNumber": "+972501234567",
        "displayName": "Dana Levi",
        "company": "Acme",
        "email": "dana@example.com",
        "tags": ["vip", "orders"],
        "externalId": "crm-123"
      }
    ]
  }'

Managed customer tags

Organization owners/admins create definitions. Every active line token can use the shared organization directory tags. Imported free-text tags are a separate field and do not become managed definitions.

curl -X POST 'https://app.ezw.solutions/api/customer-tags' \
  -H 'Authorization: Bearer {idToken}' \
  -H 'Content-Type: application/json' \
  -d '{"name":"VIP"}'

curl 'https://app.ezw.solutions/api/v1/customer-tags' \
  -H 'Authorization: Bearer {lineApiToken}'

Use the returned UUID tagId. The by-phone routes require an existing exact directory phone match; they never create a customer.

curl -X PUT 'https://app.ezw.solutions/api/v1/contacts/by-phone/tags' \
  -H 'Authorization: Bearer {lineApiToken}' \
  -H 'Content-Type: application/json' \
  -d '{"phoneNumber":"+972501234567","tagId":"{tagId}"}'

curl --get 'https://app.ezw.solutions/api/v1/contacts/by-phone/tags' \
  -H 'Authorization: Bearer {lineApiToken}' \
  --data-urlencode 'phoneNumber=+972501234567'

curl -X DELETE 'https://app.ezw.solutions/api/v1/contacts/by-phone/tags' \
  -H 'Authorization: Bearer {lineApiToken}' \
  -H 'Content-Type: application/json' \
  -d '{"phoneNumber":"+972501234567","tagId":"{tagId}"}'

Check the returned contactId, canonicalPhoneNumber, tags, and changed after writes. Query-string + must be encoded; --data-urlencode does this.

Campaigns

Create draft

curl -X POST 'https://app.ezw.solutions/api/campaigns' \
  -H 'Authorization: Bearer {idToken}' \
  -F 'name=May offers' \
  -F 'lineId={lineId}' \
  -F 'bodyText=Hello, {name}. Your offer is ready.' \
  -F 'recipientsCsv=phone,name
+972501234567,Dana Levi' \
  -F 'sourceFilename=may-offers.csv' \
  -F 'minDelaySeconds=30' \
  -F 'maxDelaySeconds=120' \
  -F 'hourlyCap=60'

Launch and inspect

curl 'https://app.ezw.solutions/api/campaigns?limit=100' \
  -H 'Authorization: Bearer {idToken}'

curl -X POST 'https://app.ezw.solutions/api/campaigns/{campaignId}/launch' \
  -H 'Authorization: Bearer {idToken}'

curl 'https://app.ezw.solutions/api/campaigns/{campaignId}' \
  -H 'Authorization: Bearer {idToken}'

Read inbox data

curl 'https://app.ezw.solutions/api/conversations?lineId={lineId}&limit=25' \
  -H 'Authorization: Bearer {idToken}'

curl 'https://app.ezw.solutions/api/conversations/{conversationId}/messages?limit=50' \
  -H 'Authorization: Bearer {idToken}'

Record internal team visibility

curl -X POST 'https://app.ezw.solutions/api/conversations/{conversationId}/messages/seen' \
  -H 'Authorization: Bearer {idToken}' \
  -H 'Content-Type: application/json' \
  -d '{"messageIds":["{inboundMessageId}"]}'

Submit 1–100 unique visible inbound message UUIDs from the same conversation after they are displayed to an operator. This records internal team seen state, not customer read or delivery status.

JavaScript fetch example

const response = await fetch("https://app.ezw.solutions/api/v1/messages/text", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.EZW_LINE_API_TOKEN}`,
    "Content-Type": "application/json",
    "Idempotency-Key": `order:${order.id}:ready`
  },
  body: JSON.stringify({
    to: order.customerPhone,
    body: `Your order ${order.number} is ready.`,
    ctaUrlButton: {
      displayText: "Open order",
      url: `https://store.example.com/orders/${order.id}`
    }
  })
});

if (!response.ok) {
  throw new Error(`EZWhatsApp send failed: ${response.status} ${await response.text()}`);
}

const queued = await response.json();

Webhook receiver example

export async function POST(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 payload = 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 saveEvent({ deliveryId, event, lineId, payload });
  return new Response("accepted", { status: 200 });
}