API & Webhook Documentation

Send and receive WhatsApp messages through the ZapClaw gateway.

ZapClaw bridges WhatsApp Business numbers and your systems: send messages with a REST API, and receive inbound messages and delivery status updates on your own webhook URL. A machine-readable OpenAPI specification is available at openapi.yaml.

Last updated: 2026-05-22

Contents

Getting started

Base URL

https://api.zapclaw.app

Authentication

Every REST API request is authenticated with an API key sent in the X-API-Key header. Create and manage keys in the ZapClaw dashboard under API Keys. The full key is shown only once, on creation — store it securely.

X-API-Key: zc_live_xxxxxxxxxxxxxxxxxxxxxxxx

Requests carry a JSON body and should include Content-Type: application/json.

Endpoint reference

Every public endpoint at a glance. All paths are relative to the base URL and sit under the /api/v1 base path. Every endpoint is authenticated with the X-API-Key header.

MethodPathDescription
POST/send/textSend a text message
POST/send/templateSend an approved template message
POST/send/mediaSend an image, video, document, audio or sticker message
POST/send/locationSend a static location pin
POST/send/contactsSend one or more contact cards
POST/send/reactionReact to a message with an emoji
POST/send/interactiveSend an interactive message (buttons, list, call-to-action URL)
POST/send/typingShow a typing indicator (also marks the message as read)
POST/send/readMark an inbound message as read
GET/send/messagesList recent messages
GET/send/messages/{id}Get a single message by its ZapClaw id
GET/send/numbersList connected WhatsApp numbers
GET/send/templatesList approved templates
POST/send/templatesCreate a message template
POST/send/templates/{id}Edit a template by its Meta template id
DELETE/send/templates/{name}Delete a template by name
GET/media/{mediaId}Download received media
POST/mediaUpload a media file
DELETE/media/{mediaId}Delete uploaded media from Meta
GET/profileRead the business profile
PATCH/profileUpdate business profile text fields
POST/profile/nameSubmit a new display name
POST/profile/photoSet the profile picture
GET/blockList blocked users
POST/blockBlock users
DELETE/blockUnblock users
GET/qr-codesList QR codes / managed links
POST/qr-codesCreate a QR code / managed link
DELETE/qr-codes/{code}Delete a QR code
GET/automationRead conversational automation config
POST/automationUpdate conversational automation config
GET/analytics/pricingPer-message pricing analytics

Coexistence limitations

ZapClaw connects WhatsApp Business numbers through Meta's Coexistence (Coex) mode, which lets a business keep using the WhatsApp Business app on a phone while the same number is also reachable through the Cloud API. Coexistence carries a few hard constraints that you should design around. They are imposed by Meta and cannot be lifted by ZapClaw.

Throughput is fixed at about 5 messages per second. This cap applies regardless of your WhatsApp messaging tier. Raising your tier increases the daily number of unique recipients you can reach, but it does not raise the per-second send rate on a Coexistence number. Pace your outbound sends accordingly.
Messages sent from WhatsApp for Windows or WearOS do not generate webhooks. When the business replies to a customer from the WhatsApp desktop app for Windows or from a WearOS smartwatch, that outbound message is not reported to the Cloud API, so ZapClaw cannot forward a message.received or message.status event for it. This is an unavoidable data gap: your records may be missing messages the business sent from those surfaces. Replies sent from the WhatsApp Business app on Android or iOS are reported normally.
Unsupported features. A Coexistence number does not support voice or video calls, the Official Business Account green badge (OBA), or ephemeral, view-once and group messages through the API. These message types and capabilities are not available and requests for them will not succeed.

API

Conventions

Authentication templates cannot be sent to a BSUID. One-tap, zero-tap and copy-code OTP templates require a phone (Meta error 131062). Other template categories (utility, marketing) accept either.

Send a text message

POST /api/v1/send/text

Body parameters

FieldTypeRequiredDescription
fromstringyesSender number (E.164)
tostringyesRecipient number (E.164)
textstringyesMessage body, up to 4096 characters
previewUrlbooleannoWhen true, WhatsApp renders a link preview for the first URL found in text. Defaults to false.
replyTostringnoThe wamid of a message to quote. The message is delivered as a quoted reply in WhatsApp.

Request

curl -X POST https://api.zapclaw.app/api/v1/send/text \
  -H "X-API-Key: zc_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "5511999999999",
    "to": "5511888888888",
    "text": "Hello from ZapClaw"
  }'

Response 201

{
  "message": {
    "id": "9f1c2d3e-...",
    "to": "5511888888888",
    "type": "text",
    "wamid": "wamid.HBg...",
    "status": "sent",
    "sentAt": "2026-05-21T14:00:00.000Z"
  }
}

Send a template message

POST /api/v1/send/template

Template messages can be sent at any time (outside the 24-hour customer service window). The template must be approved on your WhatsApp Business Account.

Body parameters

FieldTypeRequiredDescription
fromstringyesSender number (E.164)
tostringyesRecipient number (E.164)
templateNamestringyesApproved template name
languagestringnoTemplate language code (default pt_BR)
componentsarraynoTemplate components to fill variables (Meta format)
replyTostringnoThe wamid of a message to quote. The message is delivered as a quoted reply in WhatsApp.

Request

curl -X POST https://api.zapclaw.app/api/v1/send/template \
  -H "X-API-Key: zc_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "5511999999999",
    "to": "5511888888888",
    "templateName": "order_update",
    "language": "pt_BR",
    "components": [
      { "type": "body",
        "parameters": [ { "type": "text", "text": "#1234" } ] }
    ]
  }'

Send a media message

POST /api/v1/send/media

The media is referenced either by a public link or by a mediaId handle obtained from Upload media. Provide exactly one of the two.

Set type to sticker to send a WhatsApp sticker. A sticker takes a link or a mediaId like the other media types, but it does not accept a caption. Stickers must be WebP images.

Body parameters

FieldTypeRequiredDescription
fromstringyesSender number (E.164)
tostringyesRecipient number (E.164)
typestringyesOne of: image, video, document, audio, sticker
linkstringone ofPublic https URL of the media file
mediaIdstringone ofMedia handle from POST /api/v1/media
captionstringnoCaption (image, video, document). Not supported for sticker or audio.
filenamestringnoFile name (document only)
replyTostringnoThe wamid of a message to quote. The message is delivered as a quoted reply in WhatsApp.

Exactly one of link or mediaId is required. Sending both, or neither, returns HTTP 400.

Request — by public URL

curl -X POST https://api.zapclaw.app/api/v1/send/media \
  -H "X-API-Key: zc_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "5511999999999",
    "to": "5511888888888",
    "type": "image",
    "link": "https://example.com/photo.jpg",
    "caption": "Optional caption"
  }'

Request — by uploaded media handle

curl -X POST https://api.zapclaw.app/api/v1/send/media \
  -H "X-API-Key: zc_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "5511999999999",
    "to": "5511888888888",
    "type": "image",
    "mediaId": "1029384756102938",
    "caption": "Optional caption"
  }'

Request — sticker

curl -X POST https://api.zapclaw.app/api/v1/send/media \
  -H "X-API-Key: zc_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "5511999999999",
    "to": "5511888888888",
    "type": "sticker",
    "link": "https://example.com/sticker.webp"
  }'

Send a location

POST /api/v1/send/location

Sends a static location pin. The recipient sees a map preview they can open in their maps app.

Body parameters

FieldTypeRequiredDescription
fromstringyesSender number (E.164)
tostringyesRecipient number (E.164)
latitudenumberyesLatitude, between -90 and 90
longitudenumberyesLongitude, between -180 and 180
namestringnoName of the location (for example a venue name)
addressstringnoStreet address of the location
replyTostringnoThe wamid of a message to quote. The message is delivered as a quoted reply in WhatsApp.

Request

curl -X POST https://api.zapclaw.app/api/v1/send/location \
  -H "X-API-Key: zc_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "5511999999999",
    "to": "5511888888888",
    "latitude": -23.561414,
    "longitude": -46.655881,
    "name": "Avenida Paulista",
    "address": "Av. Paulista, 1578 - São Paulo, SP"
  }'

Response 201

{
  "message": {
    "id": "9f1c2d3e-...",
    "to": "5511888888888",
    "type": "location",
    "wamid": "wamid.HBg...",
    "status": "sent",
    "sentAt": "2026-05-22T14:00:00.000Z"
  }
}

Send contacts

POST /api/v1/send/contacts

Sends one or more contact cards. The contacts array follows the WhatsApp Cloud API contacts object shape and is passed through to Meta unchanged.

Body parameters

FieldTypeRequiredDescription
fromstringyesSender number (E.164)
tostringyesRecipient number (E.164)
contactsarrayyesA non-empty array of WhatsApp Cloud API contact objects
replyTostringnoThe wamid of a message to quote. The message is delivered as a quoted reply in WhatsApp.

Request

curl -X POST https://api.zapclaw.app/api/v1/send/contacts \
  -H "X-API-Key: zc_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "5511999999999",
    "to": "5511888888888",
    "contacts": [
      {
        "name": {
          "formatted_name": "Maria Silva",
          "first_name": "Maria",
          "last_name": "Silva"
        },
        "phones": [
          { "phone": "+5511777777777", "type": "WORK", "wa_id": "5511777777777" }
        ]
      }
    ]
  }'

Response 201

{
  "message": {
    "id": "9f1c2d3e-...",
    "to": "5511888888888",
    "type": "contacts",
    "wamid": "wamid.HBg...",
    "status": "sent",
    "sentAt": "2026-05-22T14:00:00.000Z"
  }
}

React to a message

POST /api/v1/send/reaction

Reacts to a message with an emoji. An empty or omitted emoji removes a reaction you previously sent.

Body parameters

FieldTypeRequiredDescription
fromstringyesSender number (E.164)
tostringyesRecipient number (E.164)
messageIdstringyesThe wamid of the message being reacted to
emojistringnoThe reaction emoji. Empty or omitted removes a previous reaction.

Request

curl -X POST https://api.zapclaw.app/api/v1/send/reaction \
  -H "X-API-Key: zc_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "5511999999999",
    "to": "5511888888888",
    "messageId": "wamid.HBg...",
    "emoji": "👍"
  }'

Response 201

{
  "message": {
    "id": "9f1c2d3e-...",
    "to": "5511888888888",
    "type": "reaction",
    "wamid": "wamid.HBg...",
    "status": "sent",
    "sentAt": "2026-05-22T14:00:00.000Z"
  }
}

Send an interactive message

POST /api/v1/send/interactive

Sends an interactive message: reply buttons, a list, or a call-to-action URL. The interactive object follows the WhatsApp Cloud API interactive object shape and is passed through to Meta unchanged.

Body parameters

FieldTypeRequiredDescription
fromstringyesSender number (E.164)
tostringyesRecipient number (E.164)
interactiveobjectyesThe WhatsApp Cloud API interactive object
replyTostringnoThe wamid of a message to quote. The message is delivered as a quoted reply in WhatsApp.

Request — reply buttons

curl -X POST https://api.zapclaw.app/api/v1/send/interactive \
  -H "X-API-Key: zc_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "5511999999999",
    "to": "5511888888888",
    "interactive": {
      "type": "button",
      "body": { "text": "Did your order arrive?" },
      "action": {
        "buttons": [
          { "type": "reply",
            "reply": { "id": "yes", "title": "Yes" } },
          { "type": "reply",
            "reply": { "id": "no", "title": "No" } }
        ]
      }
    }
  }'

Request — list

curl -X POST https://api.zapclaw.app/api/v1/send/interactive \
  -H "X-API-Key: zc_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "5511999999999",
    "to": "5511888888888",
    "interactive": {
      "type": "list",
      "header": { "type": "text", "text": "Support" },
      "body": { "text": "How can we help you today?" },
      "footer": { "text": "Pick an option" },
      "action": {
        "button": "Choose a topic",
        "sections": [
          {
            "title": "Orders",
            "rows": [
              { "id": "track", "title": "Track an order",
                "description": "See where your package is" },
              { "id": "cancel", "title": "Cancel an order",
                "description": "Cancel a recent purchase" }
            ]
          },
          {
            "title": "Account",
            "rows": [
              { "id": "billing", "title": "Billing question" }
            ]
          }
        ]
      }
    }
  }'

Response 201

{
  "message": {
    "id": "9f1c2d3e-...",
    "to": "5511888888888",
    "type": "interactive",
    "wamid": "wamid.HBg...",
    "status": "sent",
    "sentAt": "2026-05-22T14:00:00.000Z"
  }
}

Show a typing indicator

POST /api/v1/send/typing

Shows a typing indicator to the contact. This also marks the referenced inbound message as read. The indicator clears after about 25 seconds, or as soon as the next message is sent, whichever comes first.

Body parameters

FieldTypeRequiredDescription
fromstringyesThe connected number that received the message (E.164)
messageIdstringyesThe wamid of the inbound message to respond to

Request

curl -X POST https://api.zapclaw.app/api/v1/send/typing \
  -H "X-API-Key: zc_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "5511999999999",
    "messageId": "wamid.HBg..."
  }'

Response 200

{ "status": "typing" }

Upload media

POST /api/v1/media

Uploads a file and returns a media_id handle that can then be sent with Send a media message. Useful when the file is not reachable at a public URL. The request is multipart/form-data.

Form fields

FieldTypeRequiredDescription
filebinaryyesThe media file to upload
fromstringyesA connected WhatsApp number (E.164)

Size limits

TypeMaximum size
Image5 MB
Video16 MB
Audio16 MB
Document100 MB

A file larger than the cap for its type returns HTTP 413.

Request

curl -X POST https://api.zapclaw.app/api/v1/media \
  -H "X-API-Key: zc_live_xxx" \
  -F "from=5511999999999" \
  -F "file=@/path/to/photo.jpg"

Response 201

{
  "media_id": "1029384756102938",
  "mime_type": "image/jpeg",
  "file_size": 12345
}

Download received media

GET /api/v1/media/{mediaId}

Downloads the bytes of media received on an inbound WhatsApp message — an image, audio, video or document. The response is the raw file, streamed with the correct Content-Type header.

Inbound media messages arrive on your webhook with a ready-to-use media_url field that points at this endpoint — fetch that URL with your API key rather than building the path by hand.

Request

curl https://api.zapclaw.app/api/v1/media/1029384756102938 \
  -H "X-API-Key: zc_live_xxx" \
  -o received-photo.jpg

Errors

StatusMeaning
404The media id is unknown to your account
410The media has aged out on Meta's side (around 30 days)

Delete uploaded media

DELETE /api/v1/media/{mediaId}

Deletes a media file you uploaded to Meta with Upload media. Use this to release a media handle once you no longer need it. Uploaded media also expires on Meta's side automatically after about 30 days.

Query parameters

ParamRequiredDescription
fromyesA connected WhatsApp number (E.164)

Request

curl -X DELETE "https://api.zapclaw.app/api/v1/media/1029384756102938?from=5511999999999" \
  -H "X-API-Key: zc_live_xxx"

Response 200

{ "deleted": true }

Mark a message as read

POST /api/v1/send/read

Sends a read receipt for an inbound message, marking it as read in the sender's WhatsApp app.

Body parameters

FieldTypeRequiredDescription
fromstringyesThe connected number that received the message (E.164)
messageIdstringyesThe inbound message wamid (Meta message id) to mark as read

Request

curl -X POST https://api.zapclaw.app/api/v1/send/read \
  -H "X-API-Key: zc_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "5511999999999",
    "messageId": "wamid.HBg..."
  }'

Response 200

{ "status": "read" }

List connected numbers

GET /api/v1/send/numbers

Lists your connected WhatsApp numbers. Use this to discover valid from values for the send endpoints.

Request

curl https://api.zapclaw.app/api/v1/send/numbers \
  -H "X-API-Key: zc_live_xxx"

Response 200

{
  "numbers": [
    {
      "phoneNumberId": "964283400112599",
      "displayPhoneNumber": "+1 555 161-7409",
      "displayName": "My Business",
      "label": "Support line",
      "qualityRating": "GREEN",
      "status": "active"
    }
  ]
}

List messages

GET /api/v1/send/messages

Lists recent messages — useful for delivery status lookups.

Query parameters

ParamRequiredDescription
fromnoFilter by sender number (E.164)
limitnoMax results, 1–100 (default 20)
curl "https://api.zapclaw.app/api/v1/send/messages?limit=20" \
  -H "X-API-Key: zc_live_xxx"

Get a single message

GET /api/v1/send/messages/{id}

Looks up a single message by its ZapClaw id. Returns the same message shape as List messages, or HTTP 404 if no message with that id belongs to your account.

Request

curl https://api.zapclaw.app/api/v1/send/messages/9f1c2d3e-... \
  -H "X-API-Key: zc_live_xxx"

Response 200

{
  "message": {
    "id": "9f1c2d3e-...",
    "direction": "outbound",
    "to": "5511888888888",
    "type": "text",
    "wamid": "wamid.HBg...",
    "status": "delivered",
    "sentAt": "2026-05-21T14:00:00.000Z",
    "createdAt": "2026-05-21T14:00:00.000Z"
  }
}

List templates

GET /api/v1/send/templates

Lists approved templates available to send. Optional query param from (E.164) selects which connected number's templates to return.

Request

curl "https://api.zapclaw.app/api/v1/send/templates?from=5511999999999" \
  -H "X-API-Key: zc_live_xxx"

Response 200

Each template carries an id field: the Meta template id. Pass it to Edit a template.

{
  "templates": [
    {
      "id": "1234567890123456",
      "name": "order_update",
      "language": "pt_BR",
      "category": "UTILITY",
      "status": "APPROVED",
      "bodyText": "Your order {{1}} has shipped."
    }
  ]
}

Create a template

POST /api/v1/send/templates

Creates a message template and submits it to Meta for review. A newly created template is not sendable until Meta approves it; poll List templates or watch for an account.<field> webhook to learn when its status changes.

Body parameters

FieldTypeRequiredDescription
fromstringyesA connected WhatsApp number (E.164)
namestringyesTemplate name (lowercase, digits and underscores)
categorystringyesTemplate category: UTILITY, MARKETING or AUTHENTICATION
languagestringyesTemplate language code (for example pt_BR)
componentsarrayyesNon-empty array of template components (Meta format)

Request

curl -X POST https://api.zapclaw.app/api/v1/send/templates \
  -H "X-API-Key: zc_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "5511999999999",
    "name": "order_shipped",
    "category": "UTILITY",
    "language": "pt_BR",
    "components": [
      { "type": "BODY",
        "text": "Your order {{1}} has shipped." }
    ]
  }'

Response 201

{
  "template": {
    "id": "1234567890123456",
    "status": "PENDING",
    "category": "UTILITY"
  }
}

Edit a template

POST /api/v1/send/templates/{id}

Edits an existing template, identified by its Meta template id (the id field returned by List templates). Provide at least one of category or components. An edited template is re-submitted to Meta for review.

Body parameters

FieldTypeRequiredDescription
fromstringyesA connected WhatsApp number (E.164)
categorystringone ofNew template category
componentsarrayone ofNew template components (Meta format)

At least one of category or components is required.

Request

curl -X POST https://api.zapclaw.app/api/v1/send/templates/1234567890123456 \
  -H "X-API-Key: zc_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "5511999999999",
    "components": [
      { "type": "BODY",
        "text": "Your order {{1}} is on its way." }
    ]
  }'

Response 200

{ "updated": true }

Delete a template

DELETE /api/v1/send/templates/{name}

Deletes a template by its name.

Query parameters

ParamRequiredDescription
fromyesA connected WhatsApp number (E.164)

Request

curl -X DELETE "https://api.zapclaw.app/api/v1/send/templates/order_shipped?from=5511999999999" \
  -H "X-API-Key: zc_live_xxx"

Response 200

{ "deleted": true }

Read the business profile

GET /api/v1/profile

Reads the WhatsApp business profile for one of your connected numbers.

Query parameters

ParamRequiredDescription
fromyesA connected WhatsApp number (E.164)

Request

curl "https://api.zapclaw.app/api/v1/profile?from=5511999999999" \
  -H "X-API-Key: zc_live_xxx"

Response 200

{
  "profile": {
    "about": "Open Monday to Friday, 9am to 6pm",
    "address": "Av. Paulista, 1578 - São Paulo, SP",
    "description": "We sell handmade leather goods.",
    "email": "contato@example.com",
    "websites": [ "https://example.com" ],
    "vertical": "RETAIL",
    "profile_picture_url": "https://scontent.whatsapp.net/..."
  }
}

Update the business profile

PATCH /api/v1/profile

Updates the business profile text fields. Provide at least one field beyond from. To change the display name, use Submit a display name; to change the photo, use Set the profile picture.

Body parameters

FieldTypeRequiredDescription
fromstringyesA connected WhatsApp number (E.164)
aboutstringnoThe profile "about" text
addressstringnoBusiness street address
descriptionstringnoBusiness description
emailstringnoContact email
websitesarraynoUp to two website URLs
verticalstringnoBusiness industry vertical (Meta enum)

Request

curl -X PATCH https://api.zapclaw.app/api/v1/profile \
  -H "X-API-Key: zc_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "5511999999999",
    "about": "Open Monday to Friday, 9am to 6pm",
    "email": "contato@example.com",
    "websites": [ "https://example.com" ]
  }'

Response 200

{ "updated": true }

Submit a display name

POST /api/v1/profile/name

Submits a new display name for one of your connected numbers. A display name change is reviewed by Meta before it takes effect, so the response confirms the request was submitted, not that the name is already live.

Body parameters

FieldTypeRequiredDescription
fromstringyesA connected WhatsApp number (E.164)
namestringyesThe new display name

Request

curl -X POST https://api.zapclaw.app/api/v1/profile/name \
  -H "X-API-Key: zc_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "5511999999999",
    "name": "My Business"
  }'

Response 200

{ "submitted": true }

Set the profile picture

POST /api/v1/profile/photo

Sets the business profile picture. The request is multipart/form-data. The image file is capped at 5 MB.

Form fields

FieldTypeRequiredDescription
filebinaryyesThe profile picture image
fromstringyesA connected WhatsApp number (E.164)

Request

curl -X POST https://api.zapclaw.app/api/v1/profile/photo \
  -H "X-API-Key: zc_live_xxx" \
  -F "from=5511999999999" \
  -F "file=@/path/to/logo.jpg"

Response 200

{ "updated": true }

List blocked users

GET /api/v1/block

Lists the users currently blocked on one of your connected numbers.

Query parameters

ParamRequiredDescription
fromyesA connected WhatsApp number (E.164)

Request

curl "https://api.zapclaw.app/api/v1/block?from=5511999999999" \
  -H "X-API-Key: zc_live_xxx"

Response 200

{
  "blocked": [
    { "wa_id": "5511777777777" }
  ]
}

Block users

POST /api/v1/block

Blocks one or more users on a connected number. A blocked user can no longer send messages to that number.

Meta only allows blocking a user who has messaged the business within the last 24 hours. A request to block a number outside that window is rejected by Meta and surfaces as an error in the result.

Body parameters

FieldTypeRequiredDescription
fromstringyesA connected WhatsApp number (E.164)
numbersarrayyesNon-empty array of recipient numbers (E.164) to block

Request

curl -X POST https://api.zapclaw.app/api/v1/block \
  -H "X-API-Key: zc_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "5511999999999",
    "numbers": [ "5511777777777" ]
  }'

Response 200

{
  "result": {
    "added_users": [
      { "input": "5511777777777", "wa_id": "5511777777777" }
    ]
  }
}

Unblock users

DELETE /api/v1/block

Unblocks one or more previously blocked users on a connected number.

Body parameters

FieldTypeRequiredDescription
fromstringyesA connected WhatsApp number (E.164)
numbersarrayyesNon-empty array of recipient numbers (E.164) to unblock

Request

curl -X DELETE https://api.zapclaw.app/api/v1/block \
  -H "X-API-Key: zc_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "5511999999999",
    "numbers": [ "5511777777777" ]
  }'

Response 200

{
  "result": {
    "removed_users": [
      { "input": "5511777777777", "wa_id": "5511777777777" }
    ]
  }
}

List QR codes

GET /api/v1/qr-codes

Lists the QR codes (managed links) created on one of your connected numbers. A QR code links to a wa.me conversation that opens with a prefilled message.

Query parameters

ParamRequiredDescription
fromyesA connected WhatsApp number (E.164)

Request

curl "https://api.zapclaw.app/api/v1/qr-codes?from=5511999999999" \
  -H "X-API-Key: zc_live_xxx"

Response 200

{
  "qrCodes": [
    {
      "code": "4O4ABCDEFG123",
      "prefilled_message": "Hi, I would like to know more",
      "deep_link_url": "https://wa.me/message/4O4ABCDEFG123",
      "qr_image_url": "https://scontent.whatsapp.net/v/..."
    }
  ]
}

Create a QR code

POST /api/v1/qr-codes

Creates a QR code (managed link) for one of your connected numbers.

Body parameters

FieldTypeRequiredDescription
fromstringyesA connected WhatsApp number (E.164)
prefilledMessagestringyesThe message text prefilled when a user opens the link
imageFormatstringnoFormat of the QR image: PNG (default) or SVG

Request

curl -X POST https://api.zapclaw.app/api/v1/qr-codes \
  -H "X-API-Key: zc_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "5511999999999",
    "prefilledMessage": "Hi, I would like to know more",
    "imageFormat": "PNG"
  }'

Response 201

{
  "qrCode": {
    "code": "4O4ABCDEFG123",
    "prefilled_message": "Hi, I would like to know more",
    "deep_link_url": "https://wa.me/message/4O4ABCDEFG123",
    "qr_image_url": "https://scontent.whatsapp.net/v/..."
  }
}

Delete a QR code

DELETE /api/v1/qr-codes/{code}

Deletes a QR code by its code identifier.

Query parameters

ParamRequiredDescription
fromyesA connected WhatsApp number (E.164)

Request

curl -X DELETE "https://api.zapclaw.app/api/v1/qr-codes/4O4ABCDEFG123?from=5511999999999" \
  -H "X-API-Key: zc_live_xxx"

Response 200

{ "deleted": true }

Read conversational automation

GET /api/v1/automation

Reads the conversational automation configuration for a connected number: the welcome message toggle, slash commands, and ice breaker prompts.

Query parameters

ParamRequiredDescription
fromyesA connected WhatsApp number (E.164)

Request

curl "https://api.zapclaw.app/api/v1/automation?from=5511999999999" \
  -H "X-API-Key: zc_live_xxx"

Response 200

{
  "automation": {
    "enable_welcome_message": true,
    "commands": [
      { "command_name": "menu",
        "command_description": "Show the main menu" }
    ],
    "prompts": [ "Track my order", "Talk to support" ]
  }
}

Update conversational automation

POST /api/v1/automation

Updates the conversational automation configuration. Provide at least one field beyond from.

Body parameters

FieldTypeRequiredDescription
fromstringyesA connected WhatsApp number (E.164)
enableWelcomeMessagebooleannoWhether the welcome message is shown when a customer opens a new conversation
commandsarraynoArray of { command_name, command_description } objects
promptsarraynoIce breaker prompts: an array of strings, maximum 4

Request

curl -X POST https://api.zapclaw.app/api/v1/automation \
  -H "X-API-Key: zc_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "5511999999999",
    "enableWelcomeMessage": true,
    "commands": [
      { "command_name": "menu",
        "command_description": "Show the main menu" }
    ],
    "prompts": [ "Track my order", "Talk to support" ]
  }'

Response 200

{ "updated": true }

Pricing analytics

GET /api/v1/analytics/pricing

Returns Meta's per-message pricing analytics for a time window. Use it to reconcile billing alongside the pricing and conversation objects on the message.status webhook.

Query parameters

ParamRequiredDescription
fromyesA connected WhatsApp number (E.164)
startyesStart of the window, a Unix timestamp (epoch seconds)
endyesEnd of the window, a Unix timestamp (epoch seconds)
granularitynoBucket size: DAILY (default) or MONTHLY

Request

curl "https://api.zapclaw.app/api/v1/analytics/pricing?from=5511999999999&start=1746057600&end=1748736000&granularity=DAILY" \
  -H "X-API-Key: zc_live_xxx"

Response 200

{
  "analytics": {
    "data_points": [
      {
        "start": 1746057600,
        "end": 1746144000,
        "cost": 12.34,
        "volume": 420
      }
    ]
  }
}

Rate limits

The REST API allows 1000 requests per 15 minutes per API key. Exceeding the limit returns HTTP 429.

Webhook

Configuring the webhook

In the ZapClaw dashboard, under Webhook, set the URL where ZapClaw should deliver events and choose which events to receive. ZapClaw generates a signing secret — use it to verify that requests genuinely came from ZapClaw.

ZapClaw sends an HTTP POST with a JSON body to your URL for each event. Respond with any 2xx status to acknowledge.

The events are message.received (inbound message), message.status (delivery status) and the account.<field> family (WABA account events). Account events are always delivered regardless of your event subscription.

Event: message.received

Delivered when one of your numbers receives an inbound WhatsApp message.

{
  "event": "message.received",
  "sequence": 1042,
  "timestamp": "2026-05-21T14:00:00.000Z",
  "data": {
    "from": "5511888888888",
    "user_id": "BR.1A2B3C4D5E6F7G8H9I0J",
    "contact_name": "Maria Silva",
    "contact_username": "maria_lima",
    "message_id": "wamid.HBg...",
    "type": "text",
    "timestamp": "1747836000",
    "text": { "body": "Hello" }
  },
  "account": {
    "phone_number_id": "964283400112599",
    "display_name": "My Business",
    "display_phone_number": "+1 555 161-7409"
  }
}

The top-level sequence is a strictly increasing integer assigned to every forwarded message.received event in the order ZapClaw received it from Meta. Because data.timestamp has only second granularity, messages sent in quick succession can share a timestamp; order by sequence to break the tie reliably.

BSUID and usernames (Meta rollout, 2026). Meta started emitting a user_id field on every webhook on 2026-03-31 — the Business-Scoped User ID. From June 2026 end users can adopt a @username and hide their phone from businesses they haven't interacted with. When that happens, from may be null on first-touch events. Treat user_id as the durable identifier; keep from only as a display attribute. ZapClaw stores both for you when known and surfaces contact_username when the user has set one.

data fields

The Always column marks fields present on every message.received event. Fields marked Conditional appear only in the situation described.

FieldAlwaysDescription
user_idyesMeta Business-Scoped User ID (BSUID) of the sender. Format CC.alphanumeric where CC is an ISO country code. Stable per business — use this as your primary key for contacts.
fromconditionalSender's phone number in E.164. Present whenever Meta knows it. May be null for username-only contacts on first-touch (June 2026+).
message_idyesMeta message id (wamid)
typeyesMessage type: text, image, audio, video, document, sticker, and others
timestampyesMeta's send time, Unix epoch seconds as a string (see Timestamp formats)
contact_nameconditionalSender's WhatsApp profile name. Present on every event but may be null when the name is unknown.
contact_usernameconditionalSender's @username, when they've adopted one (June 2026+). null otherwise.
media object
(image, audio, video, document, sticker)
conditionalPresent only on media messages. The key matches type. See Media messages.
textconditionalPresent only when type is text. Holds { "body": "..." }.
contextconditionalPresent only when the message is a reply to (or quote of) another message. See Replies.
referralconditionalPresent only when the message came from a click-to-WhatsApp ad. Carries the ad attribution.
identityconditionalPresent only when the contact's identity changed (security notification).
errorsconditionalPresent only when Meta reported an error for the message.
ZapClaw forwards every field Meta includes in the message. The table above lists the common ones; any additional field Meta sends, now or in the future, is passed through to your webhook unchanged.

Timestamp formats

Webhook payloads carry two timestamps in two different formats. Do not confuse them.

FieldFormatExample
Top-level timestampISO 8601 string (the moment ZapClaw delivered the event)2026-05-22T17:30:00.000Z
data.timestampUnix epoch seconds, as a string (comes straight from Meta)"1716394200"
The top-level timestamp is an ISO 8601 string, while data.timestamp is epoch seconds expressed as a string. Parse each one with the matching format.

Media messages

For media messages (image, audio, video, document, sticker), the media object inside data includes a ready-to-use media_url field: a direct download URL you can fetch with your API key. Use media_url instead of the raw Meta id, which cannot be resolved on its own.

The media object always carries id, mime_type, sha256 and media_url. Audio messages also carry a voice boolean (true for a voice note). Document messages also carry a filename. The caption field appears on image, video and document messages when the sender added one.

Image

{
  "event": "message.received",
  "timestamp": "2026-05-22T14:00:00.000Z",
  "data": {
    "from": "5511888888888",
    "contact_name": "Maria Silva",
    "message_id": "wamid.HBg...",
    "type": "image",
    "timestamp": "1747836000",
    "image": {
      "id": "1029384756102938",
      "mime_type": "image/jpeg",
      "sha256": "q1w2e3...",
      "caption": "Here is the photo",
      "media_url": "https://api.zapclaw.app/api/v1/media/1029384756102938"
    }
  },
  "account": {
    "phone_number_id": "964283400112599",
    "display_name": "My Business",
    "display_phone_number": "+1 555 161-7409"
  }
}

Audio

Audio messages carry a voice boolean: true when the message is a voice note, false for a regular audio file.

{
  "event": "message.received",
  "timestamp": "2026-05-22T14:00:00.000Z",
  "data": {
    "from": "5511888888888",
    "contact_name": "Maria Silva",
    "message_id": "wamid.HBg...",
    "type": "audio",
    "timestamp": "1747836000",
    "audio": {
      "id": "2938475610293847",
      "mime_type": "audio/ogg; codecs=opus",
      "sha256": "a1s2d3...",
      "voice": true,
      "media_url": "https://api.zapclaw.app/api/v1/media/2938475610293847"
    }
  },
  "account": {
    "phone_number_id": "964283400112599",
    "display_name": "My Business",
    "display_phone_number": "+1 555 161-7409"
  }
}

Video

{
  "event": "message.received",
  "timestamp": "2026-05-22T14:00:00.000Z",
  "data": {
    "from": "5511888888888",
    "contact_name": "Maria Silva",
    "message_id": "wamid.HBg...",
    "type": "video",
    "timestamp": "1747836000",
    "video": {
      "id": "3847561029384756",
      "mime_type": "video/mp4",
      "sha256": "z1x2c3...",
      "caption": "Watch this",
      "media_url": "https://api.zapclaw.app/api/v1/media/3847561029384756"
    }
  },
  "account": {
    "phone_number_id": "964283400112599",
    "display_name": "My Business",
    "display_phone_number": "+1 555 161-7409"
  }
}

Document

Document messages carry a filename with the original file name.

{
  "event": "message.received",
  "timestamp": "2026-05-22T14:00:00.000Z",
  "data": {
    "from": "5511888888888",
    "contact_name": "Maria Silva",
    "message_id": "wamid.HBg...",
    "type": "document",
    "timestamp": "1747836000",
    "document": {
      "id": "4756102938475610",
      "mime_type": "application/pdf",
      "sha256": "p1o2i3...",
      "filename": "invoice-1234.pdf",
      "caption": "Your invoice",
      "media_url": "https://api.zapclaw.app/api/v1/media/4756102938475610"
    }
  },
  "account": {
    "phone_number_id": "964283400112599",
    "display_name": "My Business",
    "display_phone_number": "+1 555 161-7409"
  }
}

Replies (quoted messages)

When the contact replies to a specific message, data includes a context object. Its id is the wamid of the message that was replied to: use it to look up which of your messages the contact answered, and from is the number that sent the quoted message.

{
  "event": "message.received",
  "timestamp": "2026-05-22T14:00:00.000Z",
  "data": {
    "from": "5511888888888",
    "contact_name": "Maria Silva",
    "message_id": "wamid.HBg...",
    "type": "text",
    "timestamp": "1747836000",
    "text": { "body": "Yes, that works for me" },
    "context": {
      "from": "5511999999999",
      "id": "wamid.HBgQUVO_THE_QUOTED_MESSAGE"
    }
  },
  "account": {
    "phone_number_id": "964283400112599",
    "display_name": "My Business",
    "display_phone_number": "+1 555 161-7409"
  }
}

Event: message.status

Delivered when an outbound message changes status (sent, delivered, read, failed).

{
  "event": "message.status",
  "timestamp": "2026-05-21T14:00:05.000Z",
  "data": {
    "message_id": "wamid.HBg...",
    "status": "delivered",
    "recipient_id": "5511888888888",
    "timestamp": "1747836005"
  },
  "account": { "phone_number_id": "964283400112599" }
}

Billing fields: conversation & pricing

When Meta includes billing details on a status change, the data object also carries a conversation object and a pricing object, passed through from Meta unchanged. Use them to reconcile billing per message. They are present only when Meta supplies them (typically on the sent or delivered status of a billable message), so treat both as conditional.

{
  "event": "message.status",
  "timestamp": "2026-05-21T14:00:05.000Z",
  "data": {
    "message_id": "wamid.HBg...",
    "status": "sent",
    "recipient_id": "5511888888888",
    "timestamp": "1747836005",
    "conversation": {
      "id": "abcd1234efgh5678",
      "origin": { "type": "utility" },
      "expiration_timestamp": "1747922405"
    },
    "pricing": {
      "billable": true,
      "pricing_model": "PMP",
      "category": "utility",
      "type": "regular"
    }
  },
  "account": { "phone_number_id": "964283400112599" }
}

For an aggregated view of message cost over a time window, see the Pricing analytics endpoint.

Event: account.<field>

WABA-level events about your WhatsApp Business Account (account bans and restrictions, phone number quality changes, and template status updates) are forwarded as account.<field> events, where <field> is the Meta webhook field (for example account.account_update).

Account events are always delivered, regardless of which events you subscribed to in the dashboard. They carry critical alerts you should not miss.

The data object is the raw Meta webhook value for that field. Its shape depends on the field.

{
  "event": "account.account_update",
  "field": "account_update",
  "timestamp": "2026-05-21T14:00:00.000Z",
  "data": {
    "phone_number": "+1 555 161-7409",
    "event": "ACCOUNT_RESTRICTION",
    "restriction_info": [
      { "restriction_type": "RESTRICTED_BIZ_INITIATED_MESSAGING",
        "expiration_timestamp": "1748000000" }
    ]
  },
  "account": {
    "phone_number_id": "964283400112599",
    "display_name": "My Business",
    "display_phone_number": "+1 555 161-7409"
  }
}

Request headers

Every webhook POST carries these headers:

HeaderDescription
X-ZapClaw-EventThe event type, e.g. message.received.
X-ZapClaw-DeliveryUnique id of this delivery attempt.
X-ZapClaw-SignatureHMAC-SHA256 signature of the body (see below).
X-ZapClaw-RedeliveryPresent only on a manual replay triggered from the dashboard. Its value is the id of the original delivery being replayed.
A redelivery carries the same payload (and therefore the same message_id) as the original. If you deduplicate by message_id, check for X-ZapClaw-Redelivery: when it is present, process the event even though you have seen that message_id before, since the replay was requested on purpose.

Signature verification

Every webhook request includes an X-ZapClaw-Signature header: an HMAC-SHA256 of the raw request body, keyed with your webhook signing secret, prefixed with sha256=. Always verify it before trusting the payload.

Node.js

const crypto = require('crypto');

function verify(rawBody, signature, secret) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature), Buffer.from(expected));
}

Python

import hmac, hashlib

def verify(raw_body: bytes, signature: str, secret: str) -> bool:
    expected = 'sha256=' + hmac.new(
        secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

Retries & acknowledgement

Respond with any 2xx status to acknowledge a webhook. If your endpoint returns a non-2xx status, times out (10 seconds), or is unreachable, ZapClaw retries the delivery up to 3 times with a backoff of 0s, 30s and 120s. Delivery attempts are logged and visible in the dashboard.

Delivery guarantees

The message_id (Meta wamid) is stable and globally unique. It is safe to use as a deduplication key. ZapClaw also deduplicates internally, so it will not POST the same event to your URL twice, even though Meta resends webhooks aggressively.

Webhook delivery order is not guaranteed. Events are delivered concurrently and a retry can arrive after a later event. Order message.received events by the top-level sequence (a strictly increasing integer); it disambiguates messages that share the same second-granularity data.timestamp. Keep your handler idempotent.

Errors

Errors return a non-2xx HTTP status with a JSON body of this shape:

{ "error": "human-readable message" }

The table below lists every HTTP status the API can return.

StatusWhen it occurs
200Success. Returned by read endpoints and by POST /send/read and POST /send/typing.
201Success. A resource was created: a message accepted and sent (the POST /send/* message endpoints), an uploaded file (POST /media), a created template (POST /send/templates) or a created QR code (POST /qr-codes).
202Accepted. The request was accepted for asynchronous processing on Meta's side: a display name change (POST /profile/name) or a template create or edit (POST /send/templates, POST /send/templates/{id}) that is queued for Meta review.
400Invalid request: a missing or malformed field, an unknown from number, or both link and mediaId supplied to POST /send/media.
401Missing or invalid API key.
404The requested message or media was not found, or does not belong to your account.
410Received media has aged out on Meta's side (around 30 days) and can no longer be downloaded.
413An uploaded file exceeds the size cap for its type.
429Rate limit exceeded (1000 requests / 15 minutes per API key).
500An unexpected error on the ZapClaw side.
502The Meta API returned a failure or could not be reached.

Use ZapClaw with Chatwoot

If you run Chatwoot self-hosted as your conversation manager, ZapClaw ships a drop-in compatibility mode that lets Chatwoot's native WhatsApp Cloud channel talk to ZapClaw instead of graph.facebook.com directly. You skip the Meta App + Business Verification + Tech Provider review that Chatwoot otherwise requires, and your customers connect through our Embedded Signup in minutes.

The mode is opt-in per webhook configuration. Native consumers (custom backends, CRMs, automations on n8n / Make / Zapier) keep receiving the normalized ZapClaw JSON envelope — see Webhook.

Which mode do I want?

NativeChatwoot
Best forCustom backend, CRM, n8n / Make / ZapierChatwoot self-hosted
Inbound payloadZapClaw JSON envelope ({ event, data, account })Raw Meta envelope ({ object, entry[] })
Inbound signature headerX-ZapClaw-SignatureX-Hub-Signature-256
Outbound API/api/v1/send/* with X-API-Key/v25.0/{phone_id}/messages with Authorization: Bearer
Media URLsWe enrich messages with media_urlResolve via GET /v25.0/{media_id}
Chatwoot env changesnone2 env vars + restart
Code changes on your sideYou write the webhook handlerZero — Chatwoot already has one

Inbound: ZapClaw → Chatwoot

In the ZapClaw dashboard, open Webhook and switch Integration mode to Chatwoot (Meta passthrough). Set the URL to your Chatwoot base (e.g. https://chatwoot.your-domain.com) — ZapClaw appends /webhooks/whatsapp/<phone_number_id> automatically.

ZapClaw then posts the raw Meta payload — exactly the { "object": "whatsapp_business_account", "entry": [...] } envelope Chatwoot expects to receive from Meta — and signs it with X-Hub-Signature-256 using your webhook signing secret.

On your Chatwoot self-hosted server, paste the env vars block ZapClaw shows on the webhook page into your .env (or docker-compose environment) and restart Chatwoot:

# From the Chatwoot setup panel in your ZapClaw dashboard:
WHATSAPP_APP_SECRET=whsec_REPLACE_WITH_YOUR_SECRET
WHATSAPP_CLOUD_BASE_URL=https://api.zapclaw.app

WHATSAPP_APP_SECRET is what Chatwoot uses for the signature check on inbound webhooks; WHATSAPP_CLOUD_BASE_URL makes Chatwoot's outbound calls hit ZapClaw instead of Meta. The same two values come up in the ZapClaw dashboard in a ready-to-copy block.

Outbound: Chatwoot → ZapClaw

ZapClaw exposes a Meta-compatible REST surface under /v25.0/... (and any other version segment — we accept them all and route to the version ZapClaw is pinned to). Chatwoot's WhatsappCloudService works against it without code changes once WHATSAPP_CLOUD_BASE_URL is set.

Use a ZapClaw API key as the access token Chatwoot stores for the inbox. The Meta-compat endpoints accept the key as either X-API-Key (our native style) or Authorization: Bearer (Meta style — what Chatwoot sends).

Endpoints exposed

These mirror the Graph API exactly — same request bodies, same response shapes, same error envelope.

MethodPathUse
POST/v25.0/{phone_number_id}/messagesSend any message type (text, template, media, interactive, reaction, location, contacts)
POST/v25.0/{phone_number_id}/mediaUpload an outbound media file (multipart)
GET/v25.0/{media_id}Resolve an inbound media id to a ZapClaw download URL
GET/v25.0/{waba_id}/message_templatesList message templates for a WABA

Quick smoke test (cURL)

Before configuring Chatwoot, you can sanity-check the Meta-compat endpoint with cURL. Both shapes below work — they go through the same code path:

# With Meta-style Authorization (what Chatwoot sends)
curl -X POST https://api.zapclaw.app/v25.0/<phone_number_id>/messages \
  -H "Authorization: Bearer zc_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "messaging_product": "whatsapp",
    "to": "5511999999999",
    "type": "text",
    "text": { "body": "Hello from ZapClaw" }
  }'

# 200 OK
# {
#   "messaging_product": "whatsapp",
#   "contacts": [{ "input": "5511999999999", "wa_id": "5511999999999" }],
#   "messages": [{ "id": "wamid.HBg...", "message_status": "accepted" }]
# }

Step-by-step setup

  1. Sign up at ZapClaw and connect your WhatsApp number through our Embedded Signup. You will not need to create a Meta app yourself.
  2. In API Keys, create a key — copy it once, you will paste it into Chatwoot.
  3. In Webhook, set the URL to your Chatwoot base (e.g. https://chatwoot.your-domain.com) and switch Integration mode to Chatwoot (Meta passthrough). Save. ZapClaw now shows a Chatwoot setup panel with:
    • a ready-to-copy two-line env var block,
    • the Phone Number ID + Business Account ID for every connected number,
    • shortcuts to API Keys and back here.
  4. On the Chatwoot self-hosted host, paste the env var block into your .env (or docker-compose environment) and restart Chatwoot.
  5. In Chatwoot, create an inbox of type WhatsAppWhatsApp Cloud. Fill in:
    • Phone Number ID — from the credentials card on the ZapClaw webhook page.
    • Business Account ID — same place.
    • API key / Access token — your ZapClaw API key.
    • Webhook verify token — anything; ZapClaw doesn't use this field (Meta is upstream of us).
  6. Send a test message from the Chatwoot inbox to a WhatsApp number. The conversation appears on both sides — inbound arrives in Chatwoot, outbound shows up in the ZapClaw dashboard's Conversations.

Troubleshooting

Most failures fit one of these patterns. ZapClaw logs every webhook delivery — open Logs in the dashboard to see the raw response Chatwoot returned, which usually pinpoints the issue faster than the Chatwoot logs do.

Changelog

Most recent changes first.