# Stop Duplicate WhatsApp API Messages

> Why WhatsApp API sends get duplicated by timeouts and retries, and how WhaBuzz's Idempotency-Key header and webhook dedupe_key prevent double sends.

Source: https://whabuzz.com/blog/whatsapp-api-idempotency/
By Smaran Challapalli. Published 16 Oct 2026. Updated 16 Oct 2026.

An idempotency key is a string you attach to a send request so that retrying the request cannot send the message twice. In WhaBuzz it is the `Idempotency-Key` header on `POST /v1/messages`. This post explains how duplicates happen, what WhaBuzz does with the header, and how to dedupe on the receiving side.

## Why do WhatsApp API sends get duplicated?

Almost always because of a retry you wrote yourself. Your server calls the API, the network drops or your HTTP client times out, and your code cannot tell whether the message went out. The safe-looking move is to send again. If the first request had in fact reached WhatsApp, the customer now has two copies of "Your order has shipped."

Three common causes:

- **Client timeouts.** Your timeout is shorter than the time the call takes. The send completes after you gave up.
- **Job queue retries.** A worker crashes after sending but before it marks the job done. The queue hands the job to another worker.
- **Double clicks and double triggers.** A user submits twice, or two internal events fire for the same order.

The fourth cause is upstream. Per the [WhaBuzz errors page](https://docs.whabuzz.com/errors/), a `502 meta_unreachable` means the Graph API did not answer within 15 seconds, and the outcome is unknown. Meta may have accepted the message anyway.

A webhook from the other direction duplicates too. That is a separate problem, covered near the end.

## What does the Idempotency-Key header do?

It makes a retry return the first answer instead of sending again. According to the [WhaBuzz idempotency guide](https://docs.whabuzz.com/guides/idempotency/), you add the header to `POST /v1/messages`, the value can be up to 255 characters, and it can be any string you generate. A UUID works.

> Networks fail between "we sent it" and "you heard back".
>
> Source: [WhaBuzz idempotency guide](https://docs.whabuzz.com/guides/idempotency/)

```bash
curl -X POST https://api.whabuzz.com/v1/messages \
  -H "Authorization: Bearer $WHABUZZ_KEY" \
  -H "Idempotency-Key: order-48213-shipped" \
  -H "Content-Type: application/json" -d @body.json
```

The body is Meta's own `/messages` body plus `from`, as described in the [sending messages guide](https://docs.whabuzz.com/guides/messages/). Nothing about the body changes when you add the key.

## What happens when you reuse a key?

The result depends on what state the first request is in and whether the body is the same. The guide lists four situations:

| Situation | Result |
|---|---|
| First request with the key | Processed normally. The answer is stored for 24 hours. |
| Same key, same body, original finished | The stored answer is replayed with the header `Idempotent-Replayed: true`. Nothing is sent to Meta. |
| Same key, original still running | `409 idempotency_conflict`. Wait and retry. |
| Same key, different body | `422 idempotency_conflict`. Use a new key. |

So the window is 24 hours from the first request. Inside it, a finished request is replayed, which means your retry gets the same response, including the same `wamid`, without a second message going to Meta. Check for the `Idempotent-Replayed: true` response header if you want to know that a replay happened.

Both conflict cases use the code `idempotency_conflict`, with different HTTP statuses. The `409` means wait and try again. The `422` means your code reused a key for a different message, which is a bug in how you build keys.

The guide also says: do not fire two requests with the same key at the same time.

## How should you choose a key?

Derive the key from the business event, not from the attempt. `order-48213-shipped` is a good key, because every retry of "tell the customer order 48213 shipped" produces the same string. A fresh random UUID per attempt defeats the purpose, since each retry then looks like a new send.

A few rules that follow from the table above:

1. **One key per intended message.** If you send order-shipped and order-delivered messages for the same order, they need different keys.
2. **Same key, same body.** If you change the message text on retry, you get `422`. Store the body with the job and resend it unchanged.
3. **Generate the key before the first attempt** and persist it with the job, so a crashed worker retries with the same one.

The same pattern applies in code. This is a minimal retry loop using the Node SDK, where `idempotencyKey` is the option the [Node SDK page](https://docs.whabuzz.com/sdks/node/) documents for `send`:

```js
import { WhaBuzz } from 'whabuzz';
const wb = new WhaBuzz();

async function sendOnce(body, key) {
  return wb.messages.send(body, { idempotencyKey: key });
}
```

## What if WhaBuzz cannot reach Meta?

This is the case that causes most real duplicates, so it gets its own rule. If WhaBuzz could not reach Meta, you receive `502 meta_unreachable` and the outcome is unknown. Per the idempotency guide, that 502 is stored against the key too. A retry with the same key returns the same 502, rather than risking a duplicate.

That looks strict, and it is on purpose. You do not get a second attempt for free. You decide after checking. The guide says to use a new key only after you have confirmed that the first message was not sent, by looking at [`GET /v1/events`](https://docs.whabuzz.com/reference/) or at a `message.status` event for it.

In practice:

1. Retry with the same key and get the same 502.
2. Look for a `message.outbound` or `message.status` event for that send.
3. If none exists, send again with a new key. If one exists, you are done.

Batches work differently. `POST /v1/messages/batch` takes a `client_ref`, your own reference that is unique per tenant, and repeating it returns the existing batch. See the batch section of the messages guide.

## How do you dedupe on the receiving side?

Sending is half of it. Delivery of webhooks is at least once, so your handler will see some events twice. Every WhaBuzz envelope carries a `dedupe_key`. For an inbound message it is `msg:{wamid}`, and for a status it is `st:{wamid}:{status}:{timestamp}`.

The pattern is a table with a unique constraint and an insert that skips on conflict:

```sql
INSERT INTO processed_events (dedupe_key) VALUES ($1) ON CONFLICT DO NOTHING RETURNING dedupe_key;
-- no row returned: already handled, skip
```

If you want the full treatment, including signatures, `seq` and replay, read [WhatsApp webhooks that never lose an event](https://whabuzz.com/blog/whatsapp-webhook-reliability/). It covers the retry ladder and how long to keep keys.

The two mechanisms are independent. The `Idempotency-Key` stops your own double sends. The `dedupe_key` stops you processing the same incoming event twice. You need both for a flow such as "send a template, then act on the delivery status".

## Frequently asked questions

### How long does WhaBuzz remember an Idempotency-Key?

The answer to the first request is stored for 24 hours, per the idempotency guide.

### What is the maximum length of the key?

255 characters. It can be any string you generate, and a UUID works.

### What happens if I retry with the same key but change the message?

You get `422 idempotency_conflict`. Use a new key for a different message.

### Does the header work on batch sends?

The guide documents the header for `POST /v1/messages`. Batches use `client_ref` for the same purpose.

### Is a replayed response sent to the customer again?

No. A replay returns the stored answer with `Idempotent-Replayed: true` and nothing is sent to Meta.

## Related reading

- The [idempotency guide](https://docs.whabuzz.com/guides/idempotency/) and the [errors page](https://docs.whabuzz.com/errors/) are the source for every behaviour above.
- [WhatsApp webhooks that never lose an event](https://whabuzz.com/blog/whatsapp-webhook-reliability/) covers receive-side dedupe in full.
- [Send WhatsApp messages from Node.js](https://whabuzz.com/blog/whatsapp-api-nodejs/) shows the `idempotencyKey` option in a complete sender.
- [All guides by task](https://whabuzz.com/guides/) and the [WhatsApp API overview](https://whabuzz.com/whatsapp-api/).
