Skip to main content

Respond fast

Reply 2xx within 10 seconds and do the actual work afterwards — put the event in your own queue and return. A slow endpoint times out, is retried and receives the same event again.

Deduplicate

Delivery is at-least-once: the same event can arrive more than once. The simplest way to make it harmless is to store messages by messageId and always write with an upsert.

Messages

If no row was inserted, you have already processed that message: don’t reply to it or trigger anything again. Fields without a value are omitted from the payload, so read them as optional: data.text ?? null becomes NULL in your table, and if (data.quotedMessageId) tells whether the message is a reply. sender only exists when someone else wrote: when fromMe is true there is no sender, so data.sender?.phone is the safe read.

Statuses

A status carries only messageIds and status: the conversation and the recipient come from the MESSAGE.SENT of the same message, already in your table. Use an upsert here too, not an UPDATE: a DELIVERED can arrive before the MESSAGE.SENT of the same message, and even before your system stores the response of the send request. An UPDATE would find no row and lose the status. The rank makes the status only move forward: a repeated DELIVERED, or a DELIVERED that arrives after the READ, changes nothing.

By event id

Every event also has an id (the webhook-id header) that is identical in every retry. To deduplicate generically — for example before putting events in your queue — store the processed ids for 24 hours and drop repeats. id identifies the event, not the message: the DELIVERED and the READ of the same message have different ids. Don’t use messageId to drop events, or you will drop statuses.

Order

There is no ordering guarantee. Messages sent in a burst (several photos at once) and events retried after a failure can arrive out of order.
  • To order messages, use timestamp.
  • Statuses only move forward (MESSAGE.SENT → DELIVERED → READ): ignore any status older than the one you already have, as in the upsert above.

Groups

  • chat.type is GROUP, chat.id identifies the group (…@g.us) and sender is the participant who wrote. Your own messages in the group have fromMe: true and no sender.
  • A status means the message reached it for at least one participant: READ is “someone read it”, not “everyone read it”. DELIVERED and READ each arrive once per message, not once per participant.

Filtering

phone is the number exactly as WhatsApp has it registered. Older Brazilian mobile lines come without the 9th digit: you send to 5544997075632 and the events show 554497075632. When comparing with the numbers you store, treat 55 + area code + 8 digits and 55 + area code + 9 + 8 digits as the same Brazilian mobile. To reply, always use chat.id as received. And phone is absent when WhatsApp hides the number.
To stop receiving group messages or status updates at all, turn on ignoreGroups or ignoreStatus in the instance settings:
This call replaces all the instance settings: send the full object. Read the current values first with GET /v1/instance/settings. The setting applies to every webhook of the instance.

Connection

If you build your own pairing screen: show the QR Code of each QRCODE.UPDATED with status: GENERATED (always the latest), offer to try again on EXPIRED, and close the screen on CONNECTION.UPDATED with status: CONNECTED.

Sending and duplicates

The send endpoints return the messageId; it is what ties your send to the MESSAGE.SENT and the MESSAGE.STATUS_UPDATED events that follow. Store it. Zapfy does not deduplicate send requests. If a send request fails without a response (a timeout, a dropped connection), retrying it may send the message twice. Before retrying, check the MESSAGE.SENT events of that conversation to see what actually left your number.