> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zapfy.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Handling events

> Respond fast, deduplicate, deal with order and filter what matters to you.

## 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

```sql theme={null}
-- MESSAGE.RECEIVED / MESSAGE.SENT
INSERT INTO messages (message_id, chat_id, from_me, sender_phone, text, status)
VALUES ($messageId, $chatId, $fromMe, $senderPhone, $text, 'SENT')
ON CONFLICT (message_id) DO NOTHING;
```

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

```sql theme={null}
-- MESSAGE.STATUS_UPDATED, for each id in data.messageIds
INSERT INTO messages (message_id, status)
VALUES ($messageId, $status)
ON CONFLICT (message_id) DO UPDATE SET status = EXCLUDED.status
WHERE status_rank(messages.status) < status_rank(EXCLUDED.status);
-- status_rank: SENT = 1, DELIVERED = 2, READ = 3
```

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`](/en/webhooks/events#envelope) (the `webhook-id`
header) that is identical in every retry. To deduplicate generically — for example
before putting events in your queue — store the processed `id`s for 24 hours and drop
repeats.

`id` identifies the **event**, not the message: the `DELIVERED` and the `READ` of the
same message have different `id`s. 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

| I want… | How |
| - | - |
| Only group messages | `chat.type === "GROUP"` |
| Messages from a specific number (in a direct chat or a group) | Compare `sender.phone` (see the note below) |
| The conversation with a specific number | Compare `chat.phone` (see the note below) |
| Only what I received | Subscribe only to `MESSAGE.RECEIVED` |
| Skip my own messages | `fromMe === false` (your messages have no `sender`) |
| Ignore status updates from contacts | `chat.type !== "STATUS"` |
| Reply | Send to `to: chat.id`, exactly as received |
| Key conversations in my system | `chat.id` |
| Tie an event to a message I sent | `messageId` — the same value the send endpoint returned |
| Know which option a contact picked in my buttons or list | The message you sent arrives as `MESSAGE.SENT` with `messageType: "INTERACTIVE"`; when the contact taps, a `MESSAGE.RECEIVED` with `messageType: "BUTTON_REPLY"` arrives, and `buttonReply.id` is the ID you defined |
| Tie a status to its message | `messageIds` — the conversation comes from that message's `MESSAGE.SENT` |

<Warning>
  `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.
</Warning>

To stop receiving group messages or status updates **at all**, turn on `ignoreGroups`
or `ignoreStatus` in the instance settings:

```bash theme={null}
curl -X POST https://api.zapfy.io/v1/instance/settings \
  -H "Authorization: Bearer zpfy_inst_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "rejectCall": false,
    "msgRejectCall": "",
    "readMessages": false,
    "ignoreGroups": true,
    "ignoreStatus": true
  }'
```

<Warning>
  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.
</Warning>

## Connection

| `CONNECTION.UPDATED` `status` | What to do |
| - | - |
| `CONNECTED` | Nothing. `phone` tells which number is connected |
| `DISCONNECTED` | Nothing — the number reconnects by itself |
| `LOGGED_OUT` | Pair again (new QR Code or pairing code) |
| `BANNED` | Wait `expiresIn` seconds and review what caused it (`reason`) |

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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.