Skip to main content

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.

EndpointsLimit
Support, Products and CRM: everything under /chat/50 requests per second
Payments: /payments and /einvoices20 requests per second
Email and the rest: /emails, /domains, /templates, /contacts, /audiences, /segments, /topics, /broadcasts, /automations, /events, /webhooks, /api-keys, /logs, /usage10 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:

HeaderMeaning
ratelimit-limitThe number of requests allowed per window for this module
ratelimit-remainingRequests left in the current window
ratelimit-resetSeconds until the window resets (always 1)
retry-afterOnly 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.

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 <[email protected]>", to: "[email protected]", subject: "Hi", html: "<p>Hi</p>" },
{ idempotencyKey: "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).
  • 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​

View this page as Markdown