GhalaGhalaHelp Center
Back to ghala.ioghala.ioSign in
  • Getting Started
    • Ghala Documentation
    • Send Your First WhatsApp Message via API
    • Receiving Events with Webhooks
  • Commerce
    • Start Selling on WhatsApp
    • Set Up the AI Sales Agent
    • Get Paid with Snippe
  • Ai Automation
    • Configuring AI Auto-Reply for WhatsApp
    • Setting Up Human Handover Protocol
  • Campaigns Messaging
    • WhatsApp Message Templates: Complete Guide
    • Bulk WhatsApp Messaging: Complete Campaign Guide
  • Contacts Crm
    • WhatsApp Contact Management Guide
  • Best Practices
    • WhatsApp Customer Support Best Practices
    • Message Template Best Practices
  • Api Reference
    • Ghala Developer API Reference
    • Ghala API vs Meta Cloud API
    • Supported Capabilities
    • Multiple Numbers and Multi-Tenant Platforms
    • Errors and Retries
    • Limits and Quotas
    • Authentication and Access Tokens
    • Connecting and Onboarding a Number
    • Templates and Media
    • Developer Tooling
    • Versioning and Changelog

Products

  • Ghala
  • Sarufi
  • Snippe
  • Sema

Explore

  • Use Cases
  • Pricing
  • Ghala Academy
  • Blog

Developers

  • Docs
  • API Reference
  • API Quickstart
  • Webhooks Guide

Contact

  • SkyCity Mall, 9th Floor, Dar es Salaam, Tanzania
  • info@ghala.io
  • +255 699 920 009
© 2026 Neurotech Company LimitedTerms of ServicePrivacy PolicySitemap
  1. Help Center
  2. Api Reference
  3. Errors and Retries

Errors and Retries

v2

The full error catalogue, which failures are safe to retry, and how to recover when you do not know whether a send happened.

The error shape

Every error is a flat object with a machine-readable code and a human-readable message:

{
  "code": "outside_messaging_window",
  "message": "The customer last messaged more than 24 hours ago; send a template instead"
}

Branch on code, never on message. Messages are written for humans and get reworded; codes are the contract.

Validation failures from the request body use FastAPI's standard shape instead, with a detail array naming the offending field:

{
  "detail": [
    {
      "loc": ["body", "to"],
      "msg": "Field required",
      "type": "missing"
    }
  ]
}

So a robust handler reads code when present, falls back to detail, and treats anything else as an unknown failure.

The catalogue

Authentication

Status Code Cause Retryable
401 not_authenticated Missing Authorization header No
401 not_authenticated Unknown token, or a token invalidated by reconnecting the number No

A missing token and an unknown token return the same response deliberately, so the API cannot be used to discover which tokens exist.

The most common cause of a sudden 401 on a working integration is a reconnect. Reconnecting a number issues a new access token and invalidates the old one, with no overlap window. Fix it by copying the new token from Developer → Credentials.

Plan and entitlement

Status Code Cause Retryable
402 plan_feature_locked The team's plan does not include API access No, until the plan changes

This is per team. On a multi-tenant platform, one tenant can be locked while every other keeps working. Treat it as a configuration state for that tenant, not as an outage.

Validation

Status Cause Retryable
422 Malformed body: missing to, unknown type, a field too long, wrong JSON types No
422 An interactive message WhatsApp would reject: more than 3 buttons, more than 10 list rows, repeated option ids No
422 An invalid cursor or limit on GET /templates No
400 On GET /templates, a malformed query parameter No

Interactive limits are enforced locally rather than handed to Meta, so you get a precise failure instead of a 502.

Unsupported message types

There is no distinct code for an unsupported type. type is a closed set — text, template, image, document, video, audio, interactive — and anything else fails validation with 422. Location, contacts, reactions, stickers, and Flows are not sendable through Ghala at all; see the capability matrix.

The 24-hour messaging window

Status Code Cause Retryable
409 outside_messaging_window A free-form send more than 24 hours after the customer's last inbound message No, not as-is

Ghala checks this before calling Meta, so you get a clean code rather than Meta's error 131047 after the fact.

Retrying the same body will keep failing. The recovery is to send a template instead:

async function sendOrFallBackToTemplate(token, to, text) {
  const resp = await post(token, { to, type: "text", text });
  if (resp.ok) return resp.json();

  const error = await resp.json();
  if (resp.status !== 409 || error.code !== "outside_messaging_window") {
    throw error;
  }

  // The window is shut. Offer something we know will be accepted.
  const { items } = await get(token, "/templates?sendable=true&limit=1");
  if (!items.length) throw error;

  return post(token, {
    to,
    type: "template",
    template_name: items[0].name,
    template_language: items[0].language,
  }).then((r) => r.json());
}

Templates

Status Cause Retryable
400 The parameters supplied do not match the template's approved shape No
502 Meta refused the template: not approved, or name and language do not match No

GET /api/v2/templates is the cure for both. sendable tells you whether a send would be accepted; unsendable_reason says why not in words you can show a merchant; components gives you the approved shape so you can supply the right number of variables.

Watch quality_score. It turns RED before Meta pauses a template, which is the earliest warning you get that a working integration is about to stop working.

Media

Media failures surface as 502 from Meta, because Meta fetches the URL itself and Ghala never sees the file. The usual causes:

  • The URL is not publicly reachable. localhost, a VPC-internal host, or anything behind auth will fail; Meta is the client, not you.
  • The URL is not HTTPS.
  • The file exceeds Meta's size limit for that type, or the MIME type is not one WhatsApp accepts.
  • The URL redirects somewhere Meta will not follow.

Retrying does not help unless you fix the URL. See Templates and Media for the format and size constraints.

Rate limits

Status Cause Retryable
429 WhatsApp is rate-limiting this number Yes, after a pause

This is Meta's limit on the number, not a Ghala quota. Back off exponentially and retry with the same idempotency key. See Limits and Quotas.

Meta rejections and outages

Status Cause Retryable
502 WhatsApp rejected the message; the reason is in message Depends on the reason
5xx An unexpected failure on Ghala's side Yes, with an idempotency key

A 502 is Ghala reporting what Meta said. Two different things wear that status:

  • A rejection — an unapproved template, a blocked recipient, a policy refusal. Retrying is pointless.
  • An outage — Meta unavailable or timing out. Retrying is exactly right.

Today the response does not distinguish them programmatically; the reason arrives as prose in message. Until it does, treat 502 as retryable only with an idempotency key and a small, bounded number of attempts, and log the message so a human can tell the two apart.

Webhook delivery failures

These do not surface as API errors. They are visible in the dashboard under Developer → Events, per subscription, with the HTTP status your endpoint returned, the error, and the attempt count.

An endpoint is auto-disabled after 20 consecutive failed deliveries. Reactivate it by registering the endpoint again under Developer → Events; note that registering produces a new signing secret, shown once.

Delivery attempt history is pruned after 7 days, so it is a recent window rather than a full audit trail. The event log itself is retained 30 days.

Duplicates and idempotency

Status Code Cause Retryable
409 idempotency_in_progress A request with this key is still in flight Yes, after a short pause
422 idempotency_key_reused This key was used with a different body No

idempotency_in_progress is the one 4xx worth retrying: it means your first attempt is still running. Wait, then retry with the same key — you will get either the replayed original response or a fresh result.

idempotency_key_reused means you reused a key for different content. Replaying the first response would answer a question you did not ask, so the API refuses. Derive keys from the thing you are messaging about (order-1042-confirmation), not from a counter or a timestamp.

Retry policy, in one table

Status Retry? How
400 No Fix the request
401 No Refresh the stored token
402 No Upgrade the plan
409 outside_messaging_window No Send a template instead
409 idempotency_in_progress Yes Short pause, same key
422 No Fix the request
429 Yes Exponential backoff, same key
502 Cautiously Bounded attempts, same key; read message
5xx Yes Exponential backoff, same key

Always send an Idempotency-Key on anything you might retry. Without one, a retry after a timeout is a second message to a real person.

Recovering from an unknown outcome

The hard case is not a clean error. It is a request that timed out, or a process that crashed between sending and recording the result. You do not know whether the customer got the message.

There is no GET /api/v2/messages/{id} to look the message up with. The reconciliation procedure is idempotency replay:

Re-issue the identical request with the identical Idempotency-Key.

  • If the original send succeeded, you get the original response back, with Idempotency-Replayed: true in the headers. Nothing reaches WhatsApp. The id, wa_message_id, and status are the original ones, so you can now record what you failed to record the first time.
  • If the original send failed, its key was released, so this is a fresh attempt and the customer gets exactly one message.
  • If the original is still in flight, you get 409 idempotency_in_progress. Wait and repeat.
def send_and_reconcile(token, body, idempotency_key, attempts=5):
    """Safe to call after a timeout: replays rather than double-sending."""
    for attempt in range(attempts):
        resp = requests.post(
            "https://v2.ghala.io/api/v2/messages",
            headers={
                "Authorization": f"Bearer {token}",
                "Idempotency-Key": idempotency_key,
            },
            json=body,
            timeout=30,
        )

        if resp.ok:
            replayed = resp.headers.get("Idempotency-Replayed") == "true"
            return resp.json(), replayed

        error = resp.json() if resp.headers.get("content-type", "").startswith("application/json") else {}

        # Still running: wait for the first attempt to settle, do not resend.
        if resp.status_code == 409 and error.get("code") == "idempotency_in_progress":
            time.sleep(2**attempt * 0.5)
            continue

        if resp.status_code == 429 or resp.status_code >= 500:
            time.sleep(2**attempt * 0.5)
            continue

        raise GhalaError(error.get("message", resp.reason), resp.status_code, error.get("code"))

    raise TimeoutError(f"Could not settle {idempotency_key}")

This is why keys must be derived, not random. A random key regenerated after a crash reconciles nothing; order-1042-confirmation reconciles perfectly.

For a human answer to "what actually happened", the dashboard's Developer → Events log holds the number's events for 30 days, including the exact payload each subscriber was sent.

Known gaps

Stated plainly so you can plan around them rather than discover them:

  • No numeric upstream error code. A Meta rejection arrives as 502 with prose in message, not a structured meta_code. You cannot yet branch on Meta's error number.
  • No retryable flag. The table above is the contract; the response does not carry one.
  • No request_id. There is no correlation id on responses to quote in a support conversation. Log your own idempotency key instead, which is the closest equivalent and is meaningful on both sides.
  • No message-lookup endpoint. Idempotency replay above is the reconciliation path.

What's next

  • Limits and Quotas — payload caps and rate limiting
  • Ghala Developer API Reference — the endpoints themselves
  • Multiple Numbers and Multi-Tenant Platforms — retry code with tenant routing
PreviousMultiple Numbers and Multi-Tenant PlatformsNextLimits and Quotas

On this page

  • The error shape
  • The catalogue
  • Authentication
  • Plan and entitlement
  • Validation
  • Unsupported message types
  • The 24-hour messaging window
  • Templates
  • Media
  • Rate limits
  • Meta rejections and outages
  • Webhook delivery failures
  • Duplicates and idempotency
  • Retry policy, in one table
  • Recovering from an unknown outcome
  • Known gaps
  • What's next