# WhatsApp error 4: app rate limit reached — fix

URL: https://wa.genuka.com/en/docs/errors/4
Language: English

> WhatsApp error 4 (API Too Many Calls): the app hit its Meta API call rate limit. How it differs from 80007 and 130429, and how to retry safely.

WhatsApp error 4 means the application calling Meta's API has reached its call rate limit. It is a
temporary throttle: nothing is broken, you need to wait and send fewer requests. It applies to the
application as a whole, not to one number or one WhatsApp account, which is what sets it apart from
errors 80007 and 130429.

## What does error 4 mean?

Meta lists it among the Cloud API's throttling errors:

> "The app has reached its API call rate limit."
> — [Meta, Cloud API error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes#throttling-errors)

The suggested fix: open the app in the App Dashboard, **Application Rate Limit** section, confirm
the limit was reached, then try again later or reduce the frequency and number of requests. The
Graph documentation calls this code "API Too Many Calls", a temporary throttling issue: "Wait and
retry the operation, or examine your API request volume"
([Meta, Graph API error handling](https://developers.facebook.com/docs/graph-api/guides/error-handling)).
Its rate limit page adds that code 4 means the app whose token is used in the request has reached
its limit ([Meta, Rate limits](https://developers.facebook.com/docs/graph-api/overview/rate-limiting)).

### Four limits, four codes

| Code                             | What is counted                                                                                                    | Scope                            |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------ | -------------------------------- |
| **4**                            | The application's API calls                                                                                        | The whole application            |
| [80007](https://wa.genuka.com/en/docs/errors/80007)   | An app's calls on a WhatsApp account: 200 per hour by default, 5,000 on an active account with a registered number | One app and one WhatsApp account |
| [130429](https://wa.genuka.com/en/docs/errors/130429) | Messages per second: 80 by default                                                                                 | One number                       |
| [131056](https://wa.genuka.com/en/docs/errors/131056) | Messages to the same recipient: one every 6 seconds                                                                | One sender and recipient pair    |

The figures come from Meta's platform overview
([Meta, About the platform](https://developers.facebook.com/documentation/business-messaging/whatsapp/about-the-platform#rate-limits)).

## When does error 4 happen?

* **A burst of calls that are not sends**: listing templates in a loop, re-reading every number's
  health on each dashboard load, uploading media back to back.
* **Several services on the same app**: a campaign worker, a back office and a sync script sharing
  one token add up their calls.
* **A retry loop with no delay**: each failure fires another call immediately, and the limit moves
  further away instead of closer.

### Where do you see it in Genuka WA?

With Genuka WA, the Meta application making the call is Genuka's, as Tech Provider: the
application limit does not depend on your volume alone. Genuka puts code 4 in the `retryable`
class.

| Channel                  | What you get                                                                                                                    |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
| `POST /api/v1/messages`  | `400`, `"error": "send_retryable"`, `meta.code: 4`, `meta.retryable: true`                                                      |
| Campaign                 | Each recipient gets up to three attempts in total (two retries), waiting 0.25 to 0.5 s then 0.5 to 1 s, before turning `failed` |
| `POST /api/v1/templates` | `422`, `"error": "meta_rejected"`, `meta.retryable: true` if Meta answered with a 4xx; `502` if it answered 429 or 5xx          |

That `502` arrives without a JSON body: our CDN replaces the response with the single line
`error code: 502` ([API reference](https://wa.genuka.com/en/docs/api)). Treat it as a transient failure, without
looking for `meta` in it.

Genuka also limits the bursts it could cause on your behalf: `GET /api/v1/numbers?refresh=true`
refuses the call (`400 too_many_connections`) above 25 numbers, so narrow it with `?companyId=`;
and a template refresh processes WhatsApp accounts five at a time.

## How do I fix error 4?

1. **Retry with a growing delay, only when `meta.retryable` is `true`** or on a `5xx` with no JSON
   body. A single send is not retried by Genuka WA on your behalf: your code has to wait, doubling the
   delay on each attempt and adding a little randomness so your workers do not all restart together.

   For a send, retry only when `meta.code` is present (4, 80007, 130429…). A timeout between Genuka
   and Meta is also marked `retryable`, with no `meta.code`, even though Meta may already have
   accepted the message; the API has no idempotency key, and sending it again can deliver it twice.

2. **Drop unnecessary read calls.** `GET /api/v1/numbers` without `refresh=true` returns the stored
   state, kept current by Meta's webhooks. Template statuses arrive by [webhook](https://wa.genuka.com/en/docs/webhooks);
   the `POST /api/v1/templates/sync` refresh is a repair path, and once an hour is plenty
   ([API reference](https://wa.genuka.com/en/docs/api#templates)).

3. **If the error lasts more than an hour**, contact Genuka support with the `x-request-id` header and
   `meta.traceId`: Graph limits are counted over a rolling hour.

If you call the Cloud API with your own app, the App Dashboard shows the app's current Application
Rate Limit usage and the number of rate-limited users
([Meta, Rate limits](https://developers.facebook.com/docs/graph-api/overview/rate-limiting)).

**Node.js**

```ts
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

export async function callGenuka(path: string, body: unknown, maxAttempts = 5) {
  for (let attempt = 1; ; attempt++) {
    const response = await fetch(`https://wa.genuka.com/api/v1${path}`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.GENUKA_WA_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(body),
    });
    // A 5xx arrives without JSON: our CDN replaces it with "error code: 502".
    const json = await response.json().catch(() => null);
    if (response.ok) return json.data;

    // A send is only retried on a Meta code (4, 80007, 130429…): after a timeout, Meta may
    // already have accepted the message, and sending it again would deliver it twice.
    const retryable =
      path === "/messages"
        ? json?.meta?.retryable === true && json.meta.code !== undefined
        : response.status >= 500 || json?.meta?.retryable === true;
    if (!retryable || attempt >= maxAttempts) {
      throw new Error(`${response.status} ${json?.error ?? "no JSON body"} (Meta ${json?.meta?.code ?? "-"})`);
    }
    const base = Math.min(30_000, 1_000 * 2 ** (attempt - 1));
    await sleep(base / 2 + Math.random() * (base / 2));
  }
}
```

**Python**

```python
import os
import random
import time
import requests

def call_genuka(path: str, body: dict, max_attempts: int = 5) -> dict:
    for attempt in range(1, max_attempts + 1):
        response = requests.post(
            f"https://wa.genuka.com/api/v1{path}",
            headers={"Authorization": f"Bearer {os.environ['GENUKA_WA_API_KEY']}"},
            json=body,
            timeout=30,
        )
        try:
            payload = response.json()
        except ValueError:
            # A 5xx arrives without JSON: our CDN replaces it with "error code: 502".
            payload = {}
        if response.ok:
            return payload["data"]
        meta = payload.get("meta") or {}
        # A send is only retried on a Meta code (4, 80007, 130429…): after a timeout, Meta may
        # already have accepted the message, and sending it again would deliver it twice.
        if path == "/messages":
            retryable = bool(meta.get("retryable")) and meta.get("code") is not None
        else:
            retryable = response.status_code >= 500 or bool(meta.get("retryable"))
        if not retryable or attempt == max_attempts:
            raise RuntimeError(f"{response.status_code} {payload.get('error', 'no JSON body')} (Meta {meta.get('code')})")
        base = min(30.0, 2 ** (attempt - 1))
        time.sleep(base / 2 + random.random() * base / 2)
```

**PHP**

```php
<?php
function callGenuka(string $path, array $body, int $maxAttempts = 5): array
{
    for ($attempt = 1; ; $attempt++) {
        $ch = curl_init("https://wa.genuka.com/api/v1" . $path);
        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_USERAGENT => "acme-crm/1.0 (+https://example.com)",
            CURLOPT_HTTPHEADER => [
                "Authorization: Bearer " . getenv("GENUKA_WA_API_KEY"),
                "Content-Type: application/json",
            ],
            CURLOPT_POSTFIELDS => json_encode($body),
        ]);
        $raw = curl_exec($ch);
        $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); // 0 when the request never completed
        curl_close($ch);
        // A 5xx arrives without JSON: our CDN replaces it with "error code: 502".
        $json = is_string($raw) ? json_decode($raw, true) : null;
        if (!is_array($json)) $json = [];

        if ($status >= 200 && $status < 300) return $json["data"];
        // A send is only retried on a Meta code (4, 80007, 130429…): after a timeout, Meta may
        // already have accepted the message, and sending it again would deliver it twice.
        $meta = $json["meta"] ?? [];
        $retryable = $path === "/messages"
            ? !empty($meta["retryable"]) && isset($meta["code"])
            : $status === 0 || $status >= 500 || !empty($meta["retryable"]);
        if (!$retryable || $attempt >= $maxAttempts) {
            throw new RuntimeException("$status " . ($json["error"] ?? "no JSON body") . " (Meta " . ($meta["code"] ?? "-") . ")");
        }
        $base = min(30.0, 2 ** ($attempt - 1));
        usleep((int) (($base / 2 + mt_rand() / mt_getrandmax() * $base / 2) * 1_000_000));
    }
}
```

**curl**

```bash
# A single attempt: read meta.retryable before deciding to try again.
curl -s -X POST https://wa.genuka.com/api/v1/messages \
  -H "Authorization: Bearer $GENUKA_WA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "connectionId": "con_1", "to": "+237690000001", "text": "Your parcel is on its way." }' \
  | jq '{error, code: .meta.code, retryable: .meta.retryable}'
```

## How do I prevent error 4?

* **Prefer webhooks to polling.** Message statuses, template reviews and quality changes arrive on
  their own; asking for them in a loop burns the limit for nothing.
* **Send bulk through a campaign.** Genuka paces the send to the number's throughput and handles
  retries recipient by recipient — see [campaigns over the API](https://wa.genuka.com/en/docs/guides/campaigns-api).
* **Never retry at a fixed interval.** Three attempts one second apart replay the same burst at the
  same moment.

## Related error codes

* [80007](https://wa.genuka.com/en/docs/errors/80007): an app's call limit on one WhatsApp account.
* [130429](https://wa.genuka.com/en/docs/errors/130429): a number's messages-per-second throughput.
* [131056](https://wa.genuka.com/en/docs/errors/131056): too many messages to the same recipient in a short time.

## FAQ

### Does error 4 come from my Genuka API key?

No. It is a Meta code, relayed in `meta.code`. It concerns the Meta application making the call,
Genuka's, not your key.

### How long should I wait?

Meta gives no fixed duration for code 4: Graph limits are computed over a rolling hour. Start with a
growing delay, from one to thirty seconds, and cut down your calls; if the error lasts more than an
hour, tell support.

### Is a message refused with code 4 billed?

Meta did not accept it, so there is no message to bill. On the Genuka WA side, only messages Meta
accepted count against your plan's quota.

## Sources

* [Meta — Error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes)
* [Meta — Graph API, Handling errors](https://developers.facebook.com/docs/graph-api/guides/error-handling)
* [Meta — Graph API, Rate limits](https://developers.facebook.com/docs/graph-api/overview/rate-limiting)
* [Meta — About the platform, rate limits](https://developers.facebook.com/documentation/business-messaging/whatsapp/about-the-platform#rate-limits)
