# Rate limits

The REST API limits how many requests a business can make per second, separately for each module, and Email also has a daily sending quota. This page lists the limits, the headers that tell you where you stand, and how to retry when you hit one.

## Requests per second

Limits count requests per **business** (not per API key) over a sliding one-second window. All keys of a business share the same budget.

| Endpoints | Limit |
|---|---|
| Support, Products and CRM: everything under `/chat/` | 50 requests per second |
| Payments: `/payments` and `/einvoices` | 20 requests per second |
| Email and the rest: `/emails`, `/domains`, `/templates`, `/contacts`, `/audiences`, `/segments`, `/topics`, `/broadcasts`, `/automations`, `/events`, `/webhooks`, `/api-keys`, `/logs`, `/usage` | 10 requests per second |

Each module has its own budget: a burst of email sends does not slow down your chat or payments calls.

The chat gateway at `chat.hatang.dev`, which the widget and the mobile SDKs use, has its own limits on visitors and messages. See [Limits](https://hatang.dev/docs/reference/limits.md).

## Rate limit headers

Every response from a rate-limited endpoint carries these headers:

| Header | Meaning |
|---|---|
| `ratelimit-limit` | The number of requests allowed per window for this module |
| `ratelimit-remaining` | Requests left in the current window |
| `ratelimit-reset` | Seconds until the window resets (always `1`) |
| `retry-after` | Only on a `429`: seconds to wait before retrying |

```http
HTTP/1.1 429 Too Many Requests
ratelimit-limit: 10
ratelimit-remaining: 0
ratelimit-reset: 1
retry-after: 1
content-type: application/json

{
  "statusCode": 429,
  "name": "rate_limit_exceeded",
  "message": "Too many requests. You can only make 10 requests per second."
}
```

## Daily email quota

On top of the per-second limit, each business has a **daily email quota**: the number of emails it can accept per day. Every accepted email counts once, whether it came from `POST /emails`, a batch, the SMTP relay or a broadcast. Email replies sent from the Support inbox do not count. The count resets at **midnight UTC**.

Past the quota, sends fail with `429 daily_quota_exceeded`:

```json
{
  "statusCode": 429,
  "name": "daily_quota_exceeded",
  "message": "You have reached your daily email sending quota of 10000. It resets at midnight UTC."
}
```

Retrying does not help until the quota resets. You can see the quota and today's count in **Email → Email settings** (**Daily quota**), or with `GET /usage` (response shortened):

```json
{
  "object": "usage",
  "emails": {
    "daily": { "used": 1520, "limit": 10000, "resets_at": "2026-10-24T00:00:00.000Z" }
  }
}
```

A member with **Manage** access to Email can set a lower or higher quota for the business in Email settings, which is a useful safety net against a runaway loop. Leave the field empty to use the platform default. `GET /usage` needs a key with a `write` scope.

## Retry with backoff

Retry `429 rate_limit_exceeded` and `5xx` errors, never `4xx` errors that come from the request itself. Wait for `retry-after` seconds when the header is present, otherwise back off exponentially, and add some random jitter so many workers do not retry in step. Combine retries with an [`Idempotency-Key`](https://hatang.dev/docs/guides/getting-started/idempotency.md) so a retried send never creates a duplicate.

**Node.js**

```js
import { Hatang } from "@hatang/sdk";

const hatang = new Hatang(process.env.HATANG_API_KEY, { baseUrl: "https://api.hatang.dev" });
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

// statusCode 0 means the API could not be reached (network error).
const retryable = (e) => e.name === "rate_limit_exceeded" || e.statusCode >= 500 || e.statusCode === 0;

async function withRetry(call, { attempts = 5 } = {}) {
  for (let i = 0; ; i++) {
    const result = await call();
    if (!result.error || !retryable(result.error) || i === attempts - 1) return result;
    const backoff = Math.min(1000 * 2 ** i, 30_000);
    await sleep(backoff + Math.random() * 250);
  }
}

const { data, error } = await withRetry(() =>
  hatang.emails.send(
    { from: "Shop <hello@shop.vn>", to: "customer@example.com", subject: "Hi", html: "<p>Hi</p>" },
    { idempotencyKey: "welcome-user-42" },
  ),
);
```

**Python**

```python
import random, time
from hatang import Hatang, HatangError

hatang = Hatang()  # reads HATANG_API_KEY

def with_retry(call, attempts=5):
    for i in range(attempts):
        try:
            return call()
        except HatangError as err:
            # retryable: rate_limit_exceeded (429), 5xx, or the API could not be reached (status_code 0)
            if not err.retryable or i == attempts - 1:
                raise
            time.sleep(min(2 ** i, 30) + random.random() / 4)

email = with_retry(lambda: hatang.emails.send(
    {"from": "Shop <hello@shop.vn>", "to": "customer@example.com", "subject": "Hi", "html": "<p>Hi</p>"},
    idempotency_key="welcome-user-42",
))
```

The Node SDK resolves to `{ data, error }` and does not retry on its own, so wrap calls that matter, as above.

## Stay under the limits

- **Batch email.** `POST /emails/batch` sends up to 100 emails in one request, which counts as one request against the per-second limit.
- **Use webhooks instead of polling.** Subscribe to `email.delivered` or `chat.message.created` rather than asking for status in a loop ([Webhooks](https://hatang.dev/docs/guides/getting-started/webhooks.md)).
- **Queue bulk work.** Run imports and syncs through a queue with a fixed concurrency rather than firing every request at once.
- **Read the headers.** Slow down when `ratelimit-remaining` gets close to `0`.

## Next steps

- [Errors](https://hatang.dev/docs/guides/getting-started/errors.md)
- [Idempotency](https://hatang.dev/docs/guides/getting-started/idempotency.md)
- [Limits reference](https://hatang.dev/docs/reference/limits.md)

Source: https://hatang.dev/docs/guides/getting-started/rate-limits
