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.
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/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:
{
"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):
{
"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 so a retried send never creates a duplicate.
- Node.js
- Python
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(
{ idempotencyKey: "welcome-user-42" },
),
);
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 <[email protected]>", "to": "[email protected]", "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/batchsends 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.deliveredorchat.message.createdrather than asking for status in a loop (Webhooks). - 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-remaininggets close to0.