Documentation menu

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:

HeaderValue
ratelimit-limitRequests allowed per second: 10.
ratelimit-remainingRequests left in the current second.
ratelimit-resetSeconds until the window resets: 1.
retry-afterOnly 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:

ScopeRecipients per hour
Per company (all of its keys together)450
All companies together1,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, cc and bcc counts 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 /emails and POST /emails/batch answer 429 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_exceeded and nothing is sent.
  • Without an active subscription, sending is refused with 403 account_inactive.

Per-email limits

WhatLimit
Recipients (to + cc + bcc)50
reply_to addresses50
Sender display name100 characters
Address320 characters
subject998 characters, one line
html, text2,000,000 characters each
Attachments20 files, 20 MB in total
Attachment file name / content id255 / 100 characters
Custom headers30, values up to 998 characters
Tags20, name and value up to 256 characters each
scheduled_atUp 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

WhatLimitAnswer when exceeded
Request body of POST /emails and POST /emails/batch30 MB413 (no JSON body)
Request body of /mailboxes and /groups64 KB413 payload_too_large
Request body of every other endpoint1 MB413 payload_too_large
Recipients waiting in scheduled emails, per company20,000429 rate_limit_exceeded
Requests in progress, per company25429 rate_limit_exceeded
Request bodies over 2 MB in progress1 per company, a few across the platform429 rate_limit_exceeded
Idempotency-Key1 to 256 characters, kept 24 hours400 invalid_idempotency_key
Page size (limit)1 to 100, default 20422 validation_error
Webhooks per company10403 validation_error
Test events waiting per webhook5429 rate_limit_exceeded
Domains waiting for ownership verification, per company10429 validation_error
Members per group200422 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 company422 validation_error / 429 rate_limit_exceeded
API key name50 characters422 validation_error

Errors

Every error has the same JSON body, with the HTTP status repeated in statusCode:

Error
{
  "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).

StatusNameWhen
400invalid_idempotency_keyThe Idempotency-Key header is not 1 to 256 characters long.
401missing_api_keyNo Authorization: Bearer … header.
401restricted_api_keyA sending-access key called an endpoint other than sending.
403invalid_api_keyThe key is malformed, unknown or revoked, the company is suspended, or the person who created the key no longer has access to the company.
403service_disabledtsmartSend is turned off for the company.
403account_suspendedThe 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.
403ip_not_allowedThe caller's address is not on the company's IP allow-list.
403invalid_permissionThe person who created the key lacks the console privilege the call needs.
403invalid_from_addressThe from domain is not a verified domain of the company, or the key is restricted to another domain.
403domain_not_verifiedThe from domain's SPF or DKIM record is not in place.
403account_inactiveThe company's subscription is not active (sending, adding a domain, creating a mailbox).
403validation_errorThe 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.
404not_foundUnknown path, or the email, domain, key, webhook, event, suppression, mailbox, alternate email or group does not exist in your company.
405method_not_allowedThe path exists but not with this HTTP method.
409invalid_idempotent_requestThe idempotency key was used in the last 24 hours with a different body.
409concurrent_idempotent_requestsA request with the same idempotency key is still running.
409resource_in_useThe domain still has mailboxes and cannot be deleted.
409already_existsThe address of a new mailbox, alternate email or group is already used by a mailbox, a group or an alternate email.
413payload_too_largeThe 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.
422missing_required_fieldA required field is missing (for example from, to or subject).
422invalid_attachmentAn attachment is not valid base64, has an invalid name, type or content id, uses a path or URL, or the attachments exceed 20 MB.
422validation_errorThe 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.
429rate_limit_exceededToo 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.
429monthly_quota_exceededThe plan's monthly allowance would be exceeded.
429validation_errorThe company already has 10 domains waiting for ownership verification.
500application_errorAn unexpected error, or the email could not be handed to the mail server (it is then marked failed).
502mail_server_errorThe 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 same Idempotency-Key is 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-after seconds 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_error on POST /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-Key and 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.
Node.js
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}`);
  }
}
Python
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