# Meta's rate per message. Zero on top.

> WhatsApp Cloud API gateway. Send Meta's own request bodies, get signed webhooks with replay, pay ₹500 a month per number and nothing per message.

Source: https://whabuzz.com/

WhaBuzz is a WhatsApp Cloud API gateway. Meta bills your messages directly; we charge for the number, not the traffic.

[Start free](https://app.whabuzz.com/signup) [Read the docs](https://docs.whabuzz.com)

- **₹500**: a month for your first number
- **₹250**: a month for each number after
- **₹0**: per message on top of Meta's rate
- **24 h**: of webhook retries, then replay

## The request body is Meta's, plus one field

If you have read Meta's Cloud API docs, you already know our send format. Take Meta's message object, add `from` with the phone number ID you connected, and POST it to `/v1/messages`. What comes back is Meta's response, wamid and all.

Around that we added what Meta leaves to you. Test and live keys. Tenants, so one account can hold many businesses. An `Idempotency-Key` header, so a retried POST does not send twice. A batch call that takes 10,000 messages.

[Messages API reference](https://docs.whabuzz.com/api/messages/)

```bash
curl https://api.whabuzz.com/v1/messages \
  -H "Authorization: Bearer $WHABUZZ_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: renewal-4821" \
  -d '{
    "from": "1013622581841046",
    "messaging_product": "whatsapp",
    "to": "16505551234",
    "type": "template",
    "template": {
      "name": "renewal_reminder",
      "language": { "code": "en" },
      "components": [{
        "type": "body",
        "parameters": [
          { "type": "text", "text": "Kerry" },
          { "type": "text", "text": "31 Oct" }
        ]
      }]
    }
  }'
```

```js
const res = await fetch("https://api.whabuzz.com/v1/messages", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.WHABUZZ_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "renewal-4821",
  },
  body: JSON.stringify({
    from: "1013622581841046",
    messaging_product: "whatsapp",
    to: "16505551234",
    type: "template",
    template: {
      name: "renewal_reminder",
      language: { code: "en" },
      components: [{
        type: "body",
        parameters: [
          { type: "text", text: "Kerry" },
          { type: "text", text: "31 Oct" },
        ],
      }],
    },
  }),
});

const data = await res.json(); // Meta's response, unchanged
```

Response, HTTP 200, exactly as Meta sent it

```
{
  "messaging_product": "whatsapp",
  "contacts": [{ "input": "16505551234", "wa_id": "16505551234" }],
  "messages": [{
    "id": "wamid.HBgLMTY1MDU1NTEy…",
    "message_status": "accepted"
  }]
}
```

Each response also carries `Server-Timing: wb;dur=…, meta;dur=…`, so a slow send tells you whose milliseconds they were.

```bash
# What a delivery looks like. Point it at your handler to test it.
curl http://localhost:3000/whatsapp \
  -H "Content-Type: application/json" \
  -H "X-WhaBuzz-Signature: t=1792141200,v1=5f2b9c…e9" \
  -H "X-WhaBuzz-Event-Id: evt_01JAB3K9X2S8C4M7Q1V5R6T0YZ" \
  -H "X-WhaBuzz-Event-Type: message.inbound" \
  -H "X-WhaBuzz-Delivery-Id: dlv_01JAB3Y3B6D9F2H5K8N1Q4T7W0" \
  -H "X-WhaBuzz-Attempt: 1" \
  -d '{
    "id": "evt_01JAB3K9X2S8C4M7Q1V5R6T0YZ",
    "type": "message.inbound",
    "phone_number_id": "1013622581841046",
    "wa_id": "16505551234",
    "dedupe_key": "msg:wamid.HBgLMTY1MDU1NTEyMzQVAgASGBQzQUY5QTdGNkU0RTFGMkI3QzFBMDEAAA=",
    "data": {
      "message": {
        "type": "text",
        "text": { "body": "Hi, is the 7am batch open tomorrow?" }
      }
    },
    "raw_path": "entry[0].changes[0].value.messages[0]"
  }'
```

```js
import crypto from "node:crypto";
import express from "express";

const app = express();

app.post("/whatsapp", express.raw({ type: "*/*" }), (req, res) => {
  const raw = req.body.toString("utf8");
  const sig = req.get("X-WhaBuzz-Signature") ?? "";
  if (!verify(raw, sig, process.env.WHABUZZ_SECRET)) return res.sendStatus(401);

  const event = JSON.parse(raw);
  if (event.type === "webhook.verify") return res.json({ challenge: event.challenge });

  res.sendStatus(200); // answer within 3 seconds, then do the work
  jobs.add(event);     // your queue; dedupe on event.dedupe_key
});

function verify(raw, header, secret) {
  const parts = header.split(",").map((p) => p.split(/=(.*)/s));
  const t = Number(parts.find(([k]) => k === "t")?.[1]);
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
  const want = crypto.createHmac("sha256", secret).update(`${t}.${raw}`).digest();
  return parts.some(([k, v]) => {
    if (k !== "v1") return false;
    const got = Buffer.from(v, "hex");
    return got.length === want.length && crypto.timingSafeEqual(got, want);
  });
}
```

Headers on every delivery

- **X-WhaBuzz-Signature**: t=…,v1=…
- **X-WhaBuzz-Event-Id**: evt_…
- **X-WhaBuzz-Event-Type**: message.inbound
- **X-WhaBuzz-Delivery-Id**: dlv_…
- **X-WhaBuzz-Attempt**: 1

Answer 2xx inside 3 seconds and do the work after. A retry or a replay of the same event carries the same `dedupe_key`, so you can skip what you have already handled. [Webhooks guide](https://docs.whabuzz.com/guides/webhooks/)

## What you get, down to the header format

These are the numbers in our code today. The docs carry the same figures, with code.

- **Retry ladder**: The first attempt waits 3 seconds for a 2xx. After that we retry at **30 s, 1 m, 2 m, 5 m, 10 m, 20 m, 40 m and 1 h**, then hourly until 24 hours have passed. Retries wait 10 seconds each. Redirects are not followed.
- **Dead deliveries**: After 24 hours a delivery is marked dead. Replay one with `POST /v1/webhook-deliveries/{id}/replay`, or up to 10,000 at once by filter on the endpoint.
- **Signature**: HMAC-SHA256 of `{t}.{raw body}`, keyed with your `whsec_` secret. Reject anything more than 300 seconds from your clock. `X-WhaBuzz-Signature: t=<unix seconds>,v1=<hex>[,v1=<hex>]`
- **Secret rotation**: Both secrets sign during the overlap, so the header carries two `v1` values. The overlap is 24 hours unless you set 0 to 168.
- **Meta's errors**: Passed through with Meta's HTTP status and `"source": "meta"`. **131047** means the 24-hour window is closed and you need a template. **131009** means a parameter value is invalid. Our own errors say `"source": "whabuzz"`, for example `number_not_connected`.
- **Retention**: Message bodies are kept **90 days** by default. Set 30, 180 or 365 with `PATCH /v1/tenant`. Replay works inside that window. Media is streamed through and never stored.
- **Where it runs**: **Cloudflare's Asia-Pacific region**, for Workers and for data (D1, Durable Objects, R2).
- **Limits**: 10 webhook endpoints per tenant. 10,000 messages per batch call. Idempotency keys are held for 24 hours.

*Screenshot: The Deliveries tab of the WhaBuzz console: six message.inbound webhooks. The newest failed with HTTP 500 after 1382 ms; the rest were delivered with HTTP 200 in 611 to 725 ms. Each row has a Replay button.*

**Deliveries:** every attempt, with Replay.

*Screenshot: Two webhook endpoints in the WhaBuzz console. The first is active with one consecutive failure. The second is pending verification, with the note: Last verify failed: Endpoint answered 200 without the challenge.*

**Endpoints:** pending until the verify challenge echoes.

[Read the webhooks guide](https://docs.whabuzz.com/guides/webhooks/)

## What it is not

- **A reseller of Meta's conversations.** Meta bills your business on its own Meta account.
- **An inbox, campaign tool or CRM.** Your product owns the screens. We deliver the messages.
- **An opt-out manager.** A STOP reply reaches you as an ordinary `message.inbound` event. Keeping the list is your job.
- **A gatekeeper on the 24-hour window.** We do not check it. Meta does, and its 131047 comes back to you unchanged.

WhaBuzz is a Meta Tech Provider. Your business pays Meta directly, at Meta's rate.

₹500 a month, first number

₹250 for each number after that, plus 18% GST. Ten numbers come to ₹2,750 a month. Messages cost whatever Meta charges, on your own Meta bill.

[Work out your bill](https://whabuzz.com/pricing/) [Compare with a BSP](https://whabuzz.com/compare/)

Prices exclude GST. Sign-up is open to Indian businesses: confirm your email and a WhatsApp code to your mobile.

## Four things developers ask first

**What happens if the 24-hour window has closed?**

Meta rejects a free-form message with error 131047, and we return it to you as is, with `"source": "meta"`. Send an approved template instead. We never pre-check the window, so the rule you hit is always Meta's current one.

**Who bills for the messages?**

Meta, at Meta's rate, on the business's own Meta billing. We charge for connected numbers only: ₹500 a month for the first, ₹250 for each after, plus GST, drawn a day at a time from a prepaid wallet.

**What happens to a webhook my server misses?**

We retry on the ladder above for 24 hours. After that the delivery is marked dead and you can replay it for as long as the event is retained, 90 days by default. Replays carry the same `dedupe_key` as the original.

**Can I move a number from WATI, Plivo or another BSP?**

Yes, if the WhatsApp Business Account is in your business's name. Connect it, pull your approved templates across with `POST /v1/templates/sync`, then switch the webhook subscription. Expect a short gap where inbound messages reach neither provider. The [migration guide](https://docs.whabuzz.com/guides/migrating/) has the steps, including what to do if the provider owns the account.
