Reference

Rate limits

Sendara allows 10 requests per second per account. Responses processed by the account limiter report the capacity left.

Sendara counts your requests in a sliding window. The window is 1 second long, and the limit is 10 requests. After authentication, responses processed by the account limiter carry the X-RateLimit-* headers on both success and failure.

How it works

Sendara applies the limit to every route under /v1 that takes an API key. Sendara counts against your account ID. Every key on your account, live or test, counts into the same total. A request that fails authentication does not reach this account limiter.

The limit is 10 requests per second, and it is the same for every plan. Sendara removes an entry from the window as soon as it is more than 1 second old, so capacity returns continuously. Read X-RateLimit-Limit on any response in place of a hardcoded number.

Sendara also limits every unauthenticated /v1 route to 10 requests per second for each client IP address. This covers sign-up, log-in, the login code, log-out, password reset, email verification, the Google and GitHub start and callback routes, test-recipient verification, the public form config, the form submit and confirm routes, the hosted form page, and asset and BIMI logo serving. The staff routes under /v1/admin use the same per-IP limit. That limit is separate from your account limit. A provider webhook that a payload signature authenticates is not limited by IP address, so a legitimate burst is not dropped.

Rate limit headers

Read these headers on authenticated responses processed by the account limiter to track your remaining budget. The first 3 are present when the limiter is available. Retry-After is present only on a 429 response. If the limiter itself is unavailable, Sendara fails open and the response does not carry rate-limit headers.

X-RateLimit-LimitintegerOptional
The number of requests allowed in the 1-second window. It is 10 for every account.
X-RateLimit-RemainingintegerOptional
The requests left in the current window. At 0 the next request returns 429, until the window slides forward.
X-RateLimit-ResetintegerOptional
The Unix timestamp in seconds at which the current window ends. It is 1 second after the request.
Retry-AfterintegerOptional
Present only on a 429 response. Wait this many seconds, then retry. It is at most 1 second.

Read the headers

Each response lowers X-RateLimit-Remaining by 1. At 0 the next request returns 429 Too Many Requests with a Retry-After header. That header gives the seconds to wait.

200.txt
HTTP/1.1 200 OK
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 7
X-RateLimit-Reset: 1749895200
Content-Type: application/json

Inspect your remaining budget

Do not wait for a 429 before you react. Read X-RateLimit-Remaining on each response, and slow down as it approaches zero.

inspect.sh
curl -i https://api.sendara.dev/v1/messages \
  -H "Authorization: Bearer sk_live_xxx"

# HTTP/1.1 200 OK
# X-RateLimit-Limit: 10
# X-RateLimit-Remaining: 6
# X-RateLimit-Reset: 1749895200

The 429 response

When you pass the limit, Sendara returns the standard error envelope. The code is rate_limit_exceeded and the HTTP status is 429. The response carries a Retry-After header in seconds. Wait that long before you retry.

{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded",
    "status": 429
  }
}
A 429 means that Sendara did not process the request. Sendara sent no email, and it billed nothing. A retry after the wait is safe. Every send carries an idempotency_key, so a retry never sends the message twice, even after a lost response.

Wait for an exponential delay

Put your client in a retry loop that reads Retry-After. If that header is absent, use an exponential delay with jitter. The jitter separates the retries, so a fleet of workers does not send them all at one moment.

with-retry.ts
async function sendWithRetry(body: unknown, maxRetries = 5): Promise<Response> {
  for (let attempt = 0; ; attempt++) {
    const res = await fetch("https://api.sendara.dev/v1/send", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.SENDARA_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(body),
    });

    if (res.status !== 429) return res;
    if (attempt >= maxRetries) return res;

    // Prefer the server's Retry-After. Otherwise back off exponentially.
    const retryAfter = Number(res.headers.get("Retry-After"));
    const base = Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : 2 ** attempt * 200;
    const jitter = Math.random() * 200;

    await new Promise((resolve) => setTimeout(resolve, base + jitter));
  }
}
The official SDKs already retry a 429 response and a transient 5xx response. They apply the exponential delay and the jitter for you.

How to stay under the limit

  • Use the batch endpoint. Call POST /v1/send/batch to send up to 1000 messages in one request, in place of one call for each recipient. The batch takes 1 slot of your 10.
  • Read the headers. Slow down as X-RateLimit-Remaining moves toward zero. Do not wait for a rejection.
  • Obey Retry-After. It gives the moment at which capacity returns. An earlier retry returns another 429.
  • Add jitter. Randomize each delay, so concurrent workers do not synchronize their retries.
  • Keep the idempotency key on a retry. Reuse the same idempotency_key, so a lost response never becomes a duplicate email.