Payload caps, pagination, rate limiting, webhook retention, and the Meta limits that ultimately govern how much you can send.
These are enforced by Ghala on the way in, so exceeding one fails fast with 422 rather than being handed to Meta.
| Field | Limit |
|---|---|
text |
4096 characters |
media_caption |
1024 characters |
interactive.body |
Required; the message text above the options |
interactive.footer |
60 characters |
interactive.buttons |
Up to 3 |
interactive.button_text |
20 characters (list menus; defaults to View) |
interactive.sections[].title |
24 characters |
interactive.sections[].rows |
1 to 10 rows in total across all sections |
interactive.sections[].rows[].description |
72 characters |
The row cap is the one that catches people: it is ten rows for the whole menu, not ten per section. Repeated option ids are also rejected, because WhatsApp cannot tell you which one the customer tapped.
| Parameter | Default | Maximum |
|---|---|---|
limit |
25 | 100 |
cursor |
— | Pass next_cursor from the previous response |
Pagination is cursor-based, not offset-based. Read until has_more is false; do not assume a page count.
POST /api/v2/messages returns 429 when WhatsApp is rate-limiting the number. Back off exponentially and retry with the same Idempotency-Key, so a retry cannot become a second message.
Rate limiting is WhatsApp's, not Ghala's. There is no Ghala request-per-minute quota, on any plan. Two consequences:
429 always originates upstream. It reflects what Meta is allowing that number right now, which moves with the number's messaging tier and quality rating. It is not Ghala throttling you, and it will not be fixed by upgrading your plan.402 plan_feature_locked on every request; a team with it is limited only by WhatsApp.There are no X-RateLimit-* headers on responses, and no Retry-After. Use exponential backoff with jitter, starting around 500ms and capping at a few seconds, over a bounded number of attempts.
// 429 and 5xx: back off, keep the same key, give up after a bounded count.
for (let attempt = 0; attempt < 5; attempt++) {
const resp = await send(body, idempotencyKey);
if (resp.ok) return resp.json();
if (resp.status !== 429 && resp.status < 500) throw await resp.json();
const backoff = Math.min(2 ** attempt * 500, 8000);
await sleep(backoff + Math.random() * 250); // jitter, so retries do not sync
}
If you are driving a bulk send, pace it yourself rather than discovering the ceiling with 429s. Campaigns in the dashboard already drip rather than burst, which is the behaviour to imitate.
Ghala is not the constraint on how many messages you can send. Meta is, in three separate ways, all of which are properties of your WhatsApp Business Account rather than of Ghala:
| Meta limit | What it governs |
|---|---|
| Messaging limit tier | How many unique customers you may start a conversation with in a rolling 24 hours. Tiers step up as you send successfully |
| Quality rating | Green, yellow, or red. A falling rating caps your tier and can pause templates |
| Throughput | How many API calls per second Meta accepts for the number |
| Template quality | Per template. quality_score on GET /api/v2/templates turns RED before Meta pauses one |
These are managed in WhatsApp Manager, not in Ghala, and they change over time based on how customers respond to your messages. Meta's WhatsApp Business Platform documentation is the authority on the current numbers; anything Ghala printed here would go stale.
The practical consequences for an integration:
quality_score. It is the earliest warning available through the API that a template is about to stop working.429 this month if the rating dropped.Media is sent by public HTTPS URL and fetched by Meta, not by Ghala. So the constraints are Meta's:
localhost, or inside a VPC will fail with 502.Templates and Media covers formats and sizing in more detail.
| Behaviour | Value |
|---|---|
| Endpoint auto-disabled after | 20 consecutive failed deliveries |
| Signature timestamp tolerance | 300 seconds |
| Delivery attempt history retained | 7 days (settled attempts are pruned) |
| Event log retained | 30 days |
| Delivery guarantee | At-least-once, unordered |
| Endpoint scheme | Public HTTPS only. http://, loopback, link-local, and RFC1918 addresses are rejected with 400 |
Acknowledge within a few seconds and process asynchronously; a slow response counts as a failure, and twenty of those in a row disables the endpoint.
Not currently published: the per-attempt delivery timeout, the exact retry schedule and maximum retry count, and the concurrency with which deliveries are made to one endpoint. Design your handler so none of them matter: acknowledge immediately, process out of band, deduplicate on X-Ghala-Delivery, and treat handlers as idempotent.
| Data | Retained |
|---|---|
| Event log, including delivered payloads | 30 days |
| Webhook delivery attempts | 7 days |
| Idempotency keys | Until the request settles; a failed send releases its key |
Thirty days is also how far back a subscriber's history can be reconstructed, so anything you need beyond that should be persisted on your side as it arrives.
429 or a 502