Guides
Limits and errors
Limits keep the shared sending infrastructure healthy and protect your domain if a key leaks. Every limit answers with a clear error, so your code can wait and retry.
Request rate
Each company may make 10 requests per second, counted over a sliding one-second window and shared by
all of its keys. Every authenticated response except a restricted_api_key error carries the current
state:
| Header | Value |
|---|---|
ratelimit-limit | Requests allowed per second: 10. |
ratelimit-remaining | Requests left in the current second. |
ratelimit-reset | Seconds until the window resets: 1. |
retry-after | Only on a 429 for the request rate: seconds to wait. |
Over the limit the API answers 429 rate_limit_exceeded and nothing is done. To send many emails, use
batches of up to 100 instead of many single requests.
Requests in progress
A company may have 25 requests running at the same time. Request bodies larger than
2 MB (attachments, large batches) are also limited: one at a time per company, and only a few at a
time across the platform. Over either limit the API answers 429 rate_limit_exceeded with a
retry-after header (1 or 2 seconds) and nothing is done. Send large attachments one after the other
rather than in parallel.
Hourly sending limits
Recipients are counted per hour, over the last 60 minutes:
| Scope | Recipients per hour |
|---|---|
| Per company (all of its keys together) | 450 |
| All companies together | 1,800 |
- The company limit is a fair share: no company may use more than a quarter of the platform's hourly capacity, so one busy sender cannot hold up the others. Part of the capacity is always kept for account mail such as invitations, password and security notifications.
- Every address in
to,ccandbcccounts as one recipient. - An email counts in the hour it is handed to the mail server: immediate emails when you send them, scheduled emails when they go out.
-
Over the limit,
POST /emailsandPOST /emails/batchanswer429 rate_limit_exceeded(Hourly sending limit reached) and nothing is sent. A scheduled email that hits the limit when it comes due waits five minutes and is tried again. - The limit for all companies together protects the shared infrastructure and answers the same way in the rare case it is reached.
Monthly allowance
Your company's plan sets how many recipients tsmartSend may send per calendar month. The console shows the usage under Apps → Core services → tsmartSend.
- Recipients count when the email is accepted, including scheduled emails. Emails that failed, were canceled or whose recipients were all suppressed do not count.
- A request that would go over the allowance is refused with
429 monthly_quota_exceededand nothing is sent. - Without an active subscription, sending is refused with
403 account_inactive.
Per-email limits
| What | Limit |
|---|---|
Recipients (to + cc + bcc) | 50 |
reply_to addresses | 50 |
| Sender display name | 100 characters |
| Address | 320 characters |
subject | 998 characters, one line |
html, text | 2,000,000 characters each |
| Attachments | 20 files, 20 MB in total |
| Attachment file name / content id | 255 / 100 characters |
| Custom headers | 30, values up to 998 characters |
| Tags | 20, name and value up to 256 characters each |
scheduled_at | Up to 30 days ahead |
Batch limits
- 1 to 100 emails per request, each within the per-email limits.
- No attachments in batch emails.
- Hourly and monthly limits are checked for all valid emails of the batch together: if they do not fit, none is sent.
Other limits
| What | Limit | Answer when exceeded |
|---|---|---|
Request body of POST /emails and POST /emails/batch | 30 MB | 413 (no JSON body) |
Request body of /mailboxes and /groups | 64 KB | 413 payload_too_large |
| Request body of every other endpoint | 1 MB | 413 payload_too_large |
| Recipients waiting in scheduled emails, per company | 20,000 | 429 rate_limit_exceeded |
| Requests in progress, per company | 25 | 429 rate_limit_exceeded |
| Request bodies over 2 MB in progress | 1 per company, a few across the platform | 429 rate_limit_exceeded |
Idempotency-Key | 1 to 256 characters, kept 24 hours | 400 invalid_idempotency_key |
Page size (limit) | 1 to 100, default 20 | 422 validation_error |
| Webhooks per company | 10 | 403 validation_error |
| Test events waiting per webhook | 5 | 429 rate_limit_exceeded |
| Domains waiting for ownership verification, per company | 10 | 429 validation_error |
| Members per group | 200 | 422 validation_error |
Mailbox size (quota_mb) | 256 to 51200 MB, and at most the plan's mailbox size (larger values are lowered) | 422 validation_error outside 256 to 51200 |
Metrics range (GET /emails/metrics) | 366 days; one query at a time per company | 422 validation_error / 429 rate_limit_exceeded |
| API key name | 50 characters | 422 validation_error |
Errors
Every error has the same JSON body, with the HTTP status repeated in statusCode:
{
"statusCode": 422,
"name": "validation_error",
"message": "`subject` is required and must be a single line"
}
Branch on name, not on message: names are stable, messages may be reworded. Unknown paths on
the API host also answer in this format (404 not_found).
| Status | Name | When |
|---|---|---|
| 400 | invalid_idempotency_key | The Idempotency-Key header is not 1 to 256 characters long. |
| 401 | missing_api_key | No Authorization: Bearer … header. |
| 401 | restricted_api_key | A sending-access key called an endpoint other than sending. |
| 403 | invalid_api_key | The key is malformed, unknown or revoked, the company is suspended, or the person who created the key no longer has access to the company. |
| 403 | service_disabled | tsmartSend is turned off for the company. |
| 403 | account_suspended | The company has an overdue invoice: the service is suspended until it is paid. The message names the invoice and the payment page; paying restores access at once. |
| 403 | ip_not_allowed | The caller's address is not on the company's IP allow-list. |
| 403 | invalid_permission | The person who created the key lacks the console privilege the call needs. |
| 403 | invalid_from_address | The from domain is not a verified domain of the company, or the key is restricted to another domain. |
| 403 | domain_not_verified | The from domain's SPF or DKIM record is not in place. |
| 403 | account_inactive | The company's subscription is not active (sending, adding a domain, creating a mailbox). |
| 403 | validation_error | The domain cannot be added (already registered, held by another company's claim that is waiting for verification, not allowed, or over the plan's domain limit), the company already has 10 webhooks, or a new mailbox would exceed the plan's user limit. |
| 404 | not_found | Unknown path, or the email, domain, key, webhook, event, suppression, mailbox, alternate email or group does not exist in your company. |
| 405 | method_not_allowed | The path exists but not with this HTTP method. |
| 409 | invalid_idempotent_request | The idempotency key was used in the last 24 hours with a different body. |
| 409 | concurrent_idempotent_requests | A request with the same idempotency key is still running. |
| 409 | resource_in_use | The domain still has mailboxes and cannot be deleted. |
| 409 | already_exists | The address of a new mailbox, alternate email or group is already used by a mailbox, a group or an alternate email. |
| 413 | payload_too_large | The request body is larger than 1 MB (every endpoint except sending; 64 KB for mailboxes and groups). A sending request over 30 MB is refused with a 413 that has no JSON body. |
| 422 | missing_required_field | A required field is missing (for example from, to or subject). |
| 422 | invalid_attachment | An attachment is not valid base64, has an invalid name, type or content id, uses a path or URL, or the attachments exceed 20 MB. |
| 422 | validation_error | The body or a parameter is invalid; the message says which. Also for actions that do not fit the current state, such as canceling an email that was already sent. |
| 429 | rate_limit_exceeded | Too many requests per second or in progress, too many large request bodies at once, the hourly recipient limit, the scheduled-email backlog, waiting test events, or a metrics query already running. |
| 429 | monthly_quota_exceeded | The plan's monthly allowance would be exceeded. |
| 429 | validation_error | The company already has 10 domains waiting for ownership verification. |
| 500 | application_error | An unexpected error, or the email could not be handed to the mail server (it is then marked failed). |
| 502 | mail_server_error | The mail server refused a mailbox, alternate email or group change; the message carries its error codes. |
Errors from the key check (missing_api_key, invalid_api_key, service_disabled,
ip_not_allowed) come before the request rate is counted and carry no ratelimit-* headers.
A restricted_api_key error (a sending-access key on a full-access endpoint) is counted against the
request rate but carries no ratelimit-* headers either.
Handling errors and retrying
- 4xx except 429 and
409 concurrent_idempotent_requests: fix the request; sending it again unchanged gives the same answer. -
409 concurrent_idempotent_requests: the first request with the sameIdempotency-Keyis still running. Wait briefly and retry with the same key and the identical body: you get the first request's response once it has finished, or, if it failed, the retry is processed normally. - 429 for the request rate or for requests in progress: wait
retry-afterseconds and retry. Nothing was sent. - 429 for hourly or monthly limits: nothing was sent. Retry later, spread sending over time, or contact us about a larger plan.
-
429 validation_erroronPOST /domains: 10 domains are waiting for ownership verification. Verify or delete one of them first; retrying sooner does not help. - 5xx and network errors: retry with the same
Idempotency-Keyand the identical body. If the first request did go through, you get its response back instead of a second email; if it failed, the key was released and the retry is processed normally.
const { data, error, headers } = await tsmart.emails.send(email, { idempotencyKey: `order-${order.id}/confirmation` });
if (error) {
switch (error.name) {
case "monthly_quota_exceeded":
case "rate_limit_exceeded":
// Nothing was sent: queue the email and try again later.
break;
case "domain_not_verified":
// Fix the domain's DNS records in the console.
break;
default:
throw new Error(`${error.statusCode} ${error.name}: ${error.message}`);
}
} from tsmart import TsmartError
try:
client.emails.send(email, idempotency_key=f"order-{order_id}/confirmation")
except TsmartError as e:
if e.name in ("monthly_quota_exceeded", "rate_limit_exceeded"):
pass # nothing was sent: queue the email and try again later
else:
raise