> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lazyapp.in/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors

> Status codes, shapes and what to retry.

Every failure returns JSON with a `message`:

```json theme={null}
{ "message": "Template not found or not approved." }
```

Send refusals also carry a stable `code` so your client can branch without string-matching:

```json theme={null}
{
  "message": "The customer service window is closed for this recipient. Send an approved template instead.",
  "code": "customer_service_window_closed"
}
```

| Code | Meaning | What to do |
| - | - | - |
| `customer_service_window_closed` | More than 24 hours since the contact last wrote in | Send a [template](https://docs.lazyapp.in/sending/templates.md) instead |
| `contact_suppressed` | The contact opted out of all messages | Do not retry |
| `marketing_opt_out` | The contact opted out of marketing | Do not send marketing; transactional may still work |
| `no_connection` | No WhatsApp connection is configured, or none matches the number asked for | Tell the business to finish setup |
| `send_burst_limit` | Workspace hit its per-minute send ceiling | Back off; see [rate limits](https://docs.lazyapp.in/reference/rate-limits.md) |
| `recipient_rate_limit` | Too many messages to this contact in the last hour | Slow down to this recipient |
| `failure_rate_paused` | Most recent outbound messages failed | Fix recipients / connection before retrying |
| `plan_limit_reached` | A live key on a workspace below Pro. The API and webhooks are Pro features | Change the plan or buy an add-on in **Billing**. Returns `402`. See [below](#402-plan-limit-reached) |
| `insufficient_credits` | LazyApp pays Meta for this number (partner-managed funding) and the wallet that covers it is empty, or a monthly spending limit set on the account is reached | Add money to the wallet, or raise the limit. Returns `402` |
| `service_credits_exhausted` | Service sends paused on a partner-managed number after a settled charge, with the wallet at zero | Add money to the wallet. Returns `402` |
| `plan_message_allowance_exhausted` | A message cap configured on the account is used up | No current plan sets a message cap, so this does not fire on standard plans. Returns `402` |
| `brand_paused` | The brand owner paused sending | Wait until sending is turned back on. Returns `422` |

Check `service_window` on [`GET /v1/contacts/{contact}`](https://docs.lazyapp.in/api-reference/contacts/retrieve-a-contact) before sending to avoid the closed-window refusal.

### Stability contract

* `code` values are stable once shipped. Build retries and i18n against them.
* The `message` string can change freely. Treat it as display-only.
* New codes may be added at any time. Removing a code is a breaking change.
* Validation failures use the Laravel `errors` map (field → messages), not `code`.

Validation failures add a per-field `errors` map:

```json theme={null}
{
  "message": "The given data was invalid.",
  "errors": {
    "to": ["The to field format is invalid."],
    "category": ["The selected category is invalid."]
  }
}
```

## 402 plan limit reached

LazyApp's plans decide what a workspace can do in live mode. When a request asks for something the plan does not include, the API answers `402 Payment Required` instead of doing part of the work:

```json theme={null}
{
  "message": "The API is on the Pro plan. Upgrade in Billing to use live keys. Test keys keep working.",
  "code": "plan_limit_reached"
}
```

That exact message is what a **live** key gets on any workspace below Pro. The API and webhooks are Pro features, so the check runs on every live request, before the endpoint itself. Other plan limits come back differently through the API today: a broadcast that would go past the month's live broadcast sends, or connecting a number past your plan's numbers, returns `422` with a plain `message` (and, for numbers, an `errors` entry on `phone_number_id`), not `plan_limit_reached`.

**Test keys are never plan-limited.** Test mode runs on the shared test number, so a `sk_test_` key keeps working on any plan, or none. Build against test, then upgrade before you switch to `sk_live_`.

The body is the same `message` + `code` shape as a send refusal above, with no `errors` map. Branch on `code`, not on the status alone: a `402` can also mean `insufficient_credits` on a partner-managed number. Retrying will not help until the plan changes. See [Plans and limits](https://docs.lazyapp.in/billing/pricing-and-tiers.md#plan-limit-reached).

## Status codes

| Code | Meaning | Retry? |
| - | - | - |
| `400` | Malformed request | No — fix the request |
| `401` | Missing or invalid API key | No |
| `402` | Your plan does not cover this (`plan_limit_reached`), or a wallet that pays Meta for a partner-managed number is empty | After changing the plan or adding money |
| `403` | Key lacks the scope, or is restricted from this number | No |
| `404` | Not found in this workspace | No |
| `409` | An idempotent request with this key is still running | Yes, after a pause |
| `422` | Validation failed, or the provider rejected it | Only after changing the request |
| `429` | Rate limited | Yes, after the window |
| `500` | Something broke on our side | Yes, with backoff |
| `502` / `503` | Provider unreachable | Yes, with backoff |

## Which failures are worth retrying

LazyApp already retries retryable provider failures internally, and does not retry policy or invalid-recipient failures — retrying those would burn quota to get the same answer.

Apply the same rule to your own client:

> **Retry with backoff**
>
> `409`, `429`, `500`, `502`, `503`, and network timeouts. Always with an
> [Idempotency-Key](https://docs.lazyapp.in/concepts/idempotency.md) so a retry cannot double-send.

> **Do not retry**
>
> `400`, `401`, `402`, `403`, `404`, `422`. The same request will fail the same way. Surface it
> and fix the cause. A `402` clears only once the plan or wallet changes.

## A send can fail after it is accepted

`POST /v1/messages` returning `201` means LazyApp accepted the message and the provider took it. Delivery is asynchronous: the recipient's phone may be off, the number may not exist on WhatsApp, or the user may have blocked you.

So treat `201` as *accepted*, not *delivered*, and get the outcome from webhooks — `message.delivered`, `message.read`, `message.failed`. See [webhook events](https://docs.lazyapp.in/webhooks/events.md).

When a send fails immediately and synchronously, you get `422` with the provider's reason:

```json theme={null}
{ "message": "Message failed to send: recipient is not a valid WhatsApp user." }
```

## Common causes

> **Free-form message outside the 24-hour window**
>
> You can only send free-form content within 24 hours of the contact's last message.
> Outside it, send an approved [template](https://docs.lazyapp.in/sending/templates.md). This is enforced
> server-side, not just in the UI.

> **Template not approved**
>
> A template must be `approved` by Meta before it can be sent. Check status with
> `GET /v1/templates`, and remember approval can be revoked later — a template can move
> to `paused` or `disabled` on quality grounds.

> **Capability not enabled**
>
> Calling, block lists and India payments are gated by Meta per account. Check
> [`GET /v1/capabilities`](https://docs.lazyapp.in/concepts/capabilities) instead of assuming.

> **Contact suppressed**
>
> A contact who opted out is excluded from marketing sends. That is deliberate; do not
> work around it.

## Best practices

* Branch on `code` (and field keys under `errors`), never on `message` text — wording can change.
* Handle `429` by honouring `Retry-After`; see [rate limits](https://docs.lazyapp.in/reference/rate-limits.md).
* Treat provider-style `422` bodies as caller-fixable; treat `500` / `502` / `503` as transient.
* Prefer [webhooks](https://docs.lazyapp.in/webhooks/overview.md) over polling for delivery outcomes.
* Always send an [Idempotency-Key](https://docs.lazyapp.in/concepts/idempotency.md) when retrying writes.
* Monitor [`GET /v1/capabilities`](https://docs.lazyapp.in/concepts/capabilities) for quality and messaging-limit tier changes.
* Log the full response body plus `X-Request-Id` for support.

## Getting help

Quote the `X-Request-Id` from the failing response in your email to
[support@lazyapp.in](mailto:support@lazyapp.in). It identifies the exact request, including the provider call it made and the error the provider returned.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.