> ## 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.

# Rate limits

> Two limits apply: ours and Meta's — plus send-velocity guards.

## LazyApp's API limit

Requests are limited **per API key**, at a ceiling set by the workspace's [plan](https://docs.lazyapp.in/billing/pricing-and-tiers.md). Capacity uses a fixed one-minute window.

| Plan | Requests a minute, per key |
| - | - |
| No plan (test keys only) | 100 |
| Starter | 300 |
| Growth | 600 |
| Pro | 1,000 |
| Platform | 2,000 |

The ceiling follows the plan, not the key's mode, so a test key on a Pro workspace gets 1,000 too. A [platform customer](https://docs.lazyapp.in/platform/overview.md)'s workspace uses its platform's plan. Live keys need Pro in the first place: below Pro, a live key gets `402` `plan_limit_reached` before the rate limit applies. An account that was given a custom ceiling keeps it.

Every response tells you where you stand:

```http theme={null}
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 97
```

Exceeding it returns `429` with a `Retry-After` header (seconds to wait). Back off and retry — always with an [Idempotency-Key](https://docs.lazyapp.in/concepts/idempotency.md), so a retry cannot double-send.

```json theme={null}
{ "message": "Too Many Attempts." }
```

Separate keys do **not** share a bucket. Create extra keys for access control (scopes, per-number restriction), not for throughput — ask support if you need a higher ceiling on one key.

Requests with a missing or invalid key are limited separately, **per IP** (`30` failed auth attempts per minute by default), so failed authentication cannot consume a valid key's budget.

`POST /v1/otp` and `POST /v1/otp/verify` each have their own per-key limiter on top of the general `/v1` limit: 60 sends and 120 verifications a minute by default. They are counted separately, so a busy sign-in page is not throttled by its own verifications. Abuse is held back per recipient instead (see [Verify](https://docs.lazyapp.in/sending/verify.md#limits)).

Need a higher ceiling? Email [support@lazyapp.in](mailto:support@lazyapp.in) with your expected peak.

## Send velocity (abuse guards)

Independent of HTTP rate limits, LazyApp refuses sends that look abusive **before** they reach Meta — protecting your number's quality rating:

| Guard | Default | Stable `code` |
| - | - | - |
| Workspace burst | 600 sends / minute / workspace·mode | `send_burst_limit` |
| Per recipient | 20 sends / hour / recipient | `recipient_rate_limit` |
| Replies in a conversation | 10 replies per message the contact sends, while its 24-hour window is open | `recipient_rate_limit` |
| Failure rate | ≥ 50% failed in the last 50 outbound (15 min window) | `failure_rate_paused` |

A free-form reply inside an open [customer service window](https://docs.lazyapp.in/sending/messages.md#the-24-hour-window) counts against the message the contact just sent, not the hourly cap, so a shop or booking bot can keep answering for as long as the contact keeps writing. Each new message from the contact allows 10 more replies. Templates, and sends outside a window, always count toward the hourly cap.

These return `422` with `code`, not `429`. Slow down, fix bad recipients, or contact support to raise a ceiling. See [Errors](https://docs.lazyapp.in/concepts/errors.md).

## Meta's messaging limit

Distinct, and usually the one that constrains you. Meta caps how many unique users a number may message in 24 hours, and raises the tier as you demonstrate good quality: 1K, 10K, 100K, then unlimited.

LazyApp paces [broadcasts](https://docs.lazyapp.in/sending/broadcasts.md) to your current tier rather than firing everything and collecting failures. [`GET /v1/capabilities`](https://docs.lazyapp.in/concepts/capabilities) reports the tier and the number's quality rating — that is LazyApp's equivalent of an "account health" check.

> [!WARNING]
>
> Quality is what moves the tier. Recipients marking you as spam or blocking you drives the
> rating to `RED`, which can cut your limit. Sending fewer, more wanted messages raises the
> ceiling faster than sending more.

## Writing a client that behaves

```js Node theme={null}
async function send(body, attempt = 1) {
  const response = await fetch('https://lazyapp.in/v1/messages', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.LAZYAPP_API_KEY}`,
      'Content-Type': 'application/json',
      // Same key across retries: that is what makes this safe.
      'Idempotency-Key': body.idempotency_key,
    },
    body: JSON.stringify(body),
  });

  if (response.status === 429) {
    const retryAfter = Number(response.headers.get('Retry-After') || 1);
    if (attempt < 5) {
      await new Promise((r) => setTimeout(r, retryAfter * 1000));
      return send(body, attempt + 1);
    }
  }

  const retryable = response.status >= 500;

  if (retryable && attempt < 5) {
    const wait = Math.min(2 ** attempt * 250, 8000) + Math.random() * 250;
    await new Promise((r) => setTimeout(r, wait));

    return send(body, attempt + 1);
  }

  return response;
}
```

Add jitter on `5xx`. Without it, a fleet of workers that hit an outage together will retry together and hit it again.

## Tips

* Prefer [webhooks](https://docs.lazyapp.in/webhooks/overview.md) over polling for delivery status.
* Use [cursor pagination](https://docs.lazyapp.in/concepts/pagination.md) instead of refetching large lists.
* Cache [`GET /v1/capabilities`](https://docs.lazyapp.in/concepts/capabilities) for a few minutes.
* One well-scoped key is enough for throughput; extra keys are for isolation.


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