Rate limits and plan limits
Request rate limits and their headers, retrying 429 and 503, plan limits and modules, daily sends, and reading usage.
Every tenant runs under a plan that sets its request rate, its count limits and the modules it includes. This page covers what each limit counts, what answers when you reach it, and how to read where you stand with GET /v1/usage.
Request rate
Requests count against two fixed one-minute windows from the plan:
- one for the whole tenant (
apiCallsPerMinute), every credential together; - one per credential (
apiCallsPerMinutePerKey): each API key, account token, OAuth grant or console user on its own, so one busy key cannot starve the others.
A request counts on both windows or on neither. Over either, it is refused with 429 rate_limited and a Retry-After header in seconds.
A counted request answers the state of whichever window has fewer requests left:
| Header | Meaning |
|---|---|
RateLimit-Limit | Requests the window allows. |
RateLimit-Remaining | Requests the window still admits. |
RateLimit-Reset | Seconds until the window ends. |
Retry-After | On a 429 or 503: seconds to wait before trying again. |
The RateLimit-* headers are absent:
- on plans without request limits;
- on the 429 refusing a key whose own window is already spent before the request is read (it carries only
Retry-After); - while the rate limiter's store is unreachable, when requests are let through uncounted.
Retry 429 and 503
| Answer | Cause | Retry |
|---|---|---|
429 rate_limited | The tenant's or the credential's window is spent. | After Retry-After, when the window ends. |
429 too_many_requests | The credential has too many requests in flight at once. | After Retry-After. |
429 provider_throttled | The mail or calendar provider asked Workwae to slow down. | After Retry-After when the provider named a wait. |
503 overloaded | The API is shedding load. | After Retry-After. |
rate_limited, too_many_requests and overloaded are answered before the request does anything, so retrying is safe.
The SDK retries for you
The TypeScript SDK retries 429 and 503 answers twice by default. It waits for Retry-After, or backs off exponentially with jitter when there is none, and throws instead when Retry-After exceeds maxDelayMs (60 seconds). Tune it with createClient({ retry: { maxRetries, maxDelayMs } }).
Concurrency and abuse limits
Beyond the plan, each API process limits how much of it one caller takes:
- Past 16 requests in flight for one credential, a request answers 429
too_many_requests. A download counts until its body is sent. - When the process is overloaded, a request answers 503
overloaded. - A client address that fails authentication too often in a minute has its bearer tokens refused with
rate_limiteduntil the minute ends, except API keys that authenticated recently. POST /v1/accounts/{accountId}/syncallows 30 requests per account per minute; past it, 429rate_limited.
Downloads are sent chunked, without Content-Length. Sizes are in the message's size, each attachment's size and an export's bytes.
Plan limits
Count limits are in plan.limits; null means unlimited. They are abuse ceilings: usage between what the base fee includes (plan.limits.included) and the limit is billed as overage.
| Limit | Counts | Past it |
|---|---|---|
maxAccounts | Accounts not deleted, whatever their status. | Connecting another account answers 403 plan_limit_accounts. Connected accounts keep syncing. |
sendsPerDay | Sends queued per UTC day. | 403 plan_limit_sends. See Daily sends. |
webhookEndpoints | Webhook endpoints. | Adding another answers 403 plan_limit_webhooks. Existing endpoints keep delivering. |
storageBytes | Stored message content, in bytes. | Storing more answers 403 plan_limit_storage. |
bodyStorageAllowed | Whether body storage is available at all. | Setting body storage to cache or full answers 403 plan_limit_storage. |
apiCallsPerMinute, apiCallsPerMinutePerKey | Requests per minute. | 429 rate_limited. See Request rate. |
A hosted or headless connect flow stopped by the account limit returns to your redirectUri with status=error&error=plan_limit_accounts.
A tenant without an assigned plan runs under the platform defaults (plan.id is null). A canceled subscription falls back to them too.
Daily sends
sendsPerDay counts messages as they are queued, per UTC day:
- Past it, a send answers 403
plan_limit_sendsuntil 00:00 UTC. - In a mail merge, the entries beyond what the day has left fail with
plan_limit_sends; the others are queued. current.sendsTodayin usage is the count the limit checks.
The MCP send policy can set its own dailySendLimit per credential, up to 10,000, on top of the plan's.
Past-due payment
When a tenant's payment is past due beyond the grace period, new sends and new accounts answer 402 billing_past_due until an invoice is paid. Sends already queued or scheduled still go out, and sync and reads continue.
Modules
A plan names its modules in plan.limits.modules: calendar, contacts, tracking, ai, meetings and sharedMailboxes. Mail is always included.
Without a module:
- Its routes answer 403
plan_module_unavailable: Calendar, Contacts, meeting apps and connections, shared mailboxes, AI drafts, and tracking asked for on a send. - Its settings have no effect: no calendar or contacts sync, no calendar or contacts scopes on connect, no tracking defaults on sends, no reply classification.
- Shared-mailbox accounts added before the plan lost
sharedMailboxeskeep syncing and sending like any other account. Only adding new ones, and asking for the shared mailbox scope on connect, is refused.
Read usage
GET /v1/usage (read scope) returns the effective plan and the tenant's usage against it.
curl "https://api.workwae.com/v1/usage?from=2026-10-01&to=2026-10-18" \
-H "Authorization: Bearer $WORKWAE_API_KEY"from and to are UTC days, both included:
- Leave both out for the last 30 days, ending today.
fromalone runs to today;toalone covers the 30 days ending on it. - The range spans at most 366 days.
- A range that starts after it ends, or after today when
tois omitted, answers 400invalid_range.
| Field | Meaning |
|---|---|
plan | The effective plan: id (null for the platform defaults), name and limits. limits.included is what the base fee covers; limits.pricing is null when the plan is not billed. |
current | Where the tenant stands now: accounts (not deleted, what maxAccounts counts), webhookEndpoints, sendsToday (queued today, what sendsPerDay counts) and storedBytes (the last storage listing plus content stored since). |
days | One entry per UTC day that had usage, oldest first. Days without usage are absent. Today's counters are live. |
Each day counts:
| Field | Counts |
|---|---|
liveAccounts | Highest number of active accounts sampled that day. |
apiCalls | Calls per scope (read, write, send, admin). |
sends | Messages delivered. sendsPerDay counts messages as they are queued, which can be earlier. |
webhookDeliveries | Successful webhook deliveries. |
aiCalls | Answers from the AI gateway: reply classifications and drafts. |
storedBytes | Object storage from that day's listing; null until the listing has run. |