Per-minute request limits, monthly email limits, probation caps for new accounts, and per-domain daily limits
Four different limits apply, and they fail in different ways. Knowing which one you hit tells you what to do about it.
| Limit | Scope | Response when exceeded |
|---|---|---|
| Requests per minute | Send endpoints, per API key | 429 rate_limited, retry after Retry-After seconds |
| Emails per month | Whole account | 429 monthly_limit_reached, resets on the first of the month or when you upgrade |
| Probation caps (hourly, daily, recipient domains) | New accounts, for their first 72 hours | 429 probation_limit_reached, retry at resetsAt |
| Emails per day per domain | Each sending domain or sender | The email is queued but held until the day rolls over; see below |
Applies to POST /v1/send/emails/send and POST /v1/send/emails/batch only. Reading emails, managing domains, and the other endpoints are not rate limited in this way.
| Plan | Send requests per minute |
|---|---|
| Hobby | 10 |
| Pro | 60 |
| Enterprise | 600 by default; custom on request |
A batch request counts as one request regardless of how many emails it contains, so batching is the way to send more than the per-minute limit allows.
Every response from a send endpoint includes:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Your per-minute limit. |
X-RateLimit-Remaining | Requests left in the current minute. |
When you exceed the limit the response is 429 with code rate_limited and an additional header:
| Header | Meaning |
|---|---|
Retry-After | Seconds to wait before retrying. |
{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded: 10 requests per minute"
}
}429Wait Retry-After seconds and retry the same request. Sending was not attempted, so retrying does not risk a duplicate email. A minimal loop:
async function sendWithRetry(body, attempts = 5) {
for (let i = 0; i < attempts; i++) {
const res = await fetch("https://api.do.dev/v1/send/emails/send", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SEND_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (res.status !== 429) return res;
const wait = Number(res.headers.get("Retry-After") ?? 5);
await new Promise((r) => setTimeout(r, wait * 1000));
}
throw new Error("rate limited after retries");
}Only retry quickly on 429 with code rate_limited. A 429 with code probation_limit_reached clears at resetsAt, which may be an hour away; one with code monthly_limit_reached will not clear by waiting at all.
Each plan includes a number of emails per calendar month. Every email queued by a send or batch request counts as one, whether it is later delivered or bounces.
| Plan | Emails per month |
|---|---|
| Hobby | 3,000 |
| Pro | 50,000 |
| Enterprise | Unlimited |
When the limit is reached, send and batch requests return 429 with code monthly_limit_reached until the first of the next month (UTC) or until you upgrade in the dashboard. Nothing is queued. Your current usage is shown in the dashboard. See Pricing.
{
"error": {
"code": "monthly_limit_reached",
"message": "Monthly email limit reached",
"limit": 3000,
"used": 3000,
"resetsAt": "2026-10-01T00:00:00.000Z",
"upgradeUrl": "https://send.dev/dashboard/billing/overview"
}
}resetsAt is when the counter clears. For a batch request, the check is applied to the whole batch: if accepting every item would cross the limit, nothing is queued.
New accounts are not reviewed by a person before they can send. Instead, every new account spends its first 72 hours on probation with low caps, while send.dev watches how the first messages land. The caps apply on top of your plan's limits, on both API hosts, and count every email in a send or batch request.
| Trust stage | Emails per hour | Emails per day | Distinct recipient domains per hour | How long |
|---|---|---|---|---|
probation (standard) | 50 | 200 | 25 | 72 hours |
probation (higher-risk signup) | 20 | 100 | 10 | 7 days |
established | Plan limits only | Plan limits only, and the per-domain ladder below | Until 14 further clean days | |
trusted | Plan limits only | Plan limits only, and the per-domain ladder below |
"Higher-risk" is decided by automated signals at signup and domain creation (for example a free-mail account paired with a freshly registered domain). Nothing else about the two probation flavors differs.
The hourly and daily windows are rolling. The recipient-domain cap counts distinct domains after the @ across all recipients in the last hour; it exists because a legitimate new transactional sender writes to a handful of domains, while a purchased list hits hundreds.
Exceeding any cap returns:
{
"error": {
"code": "probation_limit_reached",
"message": "Your account is in its probation period and is limited to 50 emails per hour. Limits lift automatically once the probation period ends with healthy delivery.",
"limit": 50,
"window": "hour",
"metric": "sends",
"resetsAt": "2026-09-05T15:00:00.000Z"
}
}| Field | Meaning |
|---|---|
limit | The cap that was hit. |
window | hour or day. |
metric | sends for the hourly or daily email cap, recipient_domains for the distinct-domain cap. |
resetsAt | ISO-8601 time the window clears. Also sent as Retry-After in seconds. |
The check runs before the email is recorded, so a rejected request does not count against the window or your reputation. A batch that would cross a cap is rejected whole.
At the end of the probation window, an account moves to established if its 24-hour bounce rate is under 2% and it has had no spam complaints; otherwise it stays on probation until a window passes cleanly. established becomes trusted after 14 further days of clean sending. Promotion is checked hourly; nothing needs to be requested. GET /v1/send/tenants returns the current trustStage and, while on probation, probationEndsAt. See Domains.
Sending is paused (403 sending_paused) without waiting for the window to end when the early signals look like abuse:
A pause resets probation, so the caps start over once it is lifted. The notification email explains what tripped; reply to it or contact support.
Independently of your plan and of account probation, each sending domain (and each single-address sender) has a daily cap that grows with its track record:
| Tier | Emails per day | How a domain gets here |
|---|---|---|
| New | 200 | Every domain starts here after verification. |
| Warming | 2,000 | Automatically, after at least 100 emails and 3 days with a bounce rate under 1% and complaint rate under 0.05%. |
| Trusted | 50,000 | Automatically, after at least 5,000 emails and 14 days with a bounce rate under 0.5% and complaint rate under 0.03%. |
This is abuse protection. A brand-new domain sending thousands of messages on day one is exactly what a compromised account or a purchased list looks like, both to send.dev and to Gmail, Outlook, and Yahoo, and it hurts deliverability for every send.dev customer. The ladder makes a new domain ramp up the way mailbox providers expect.
If you have a legitimate launch that needs more volume than the ladder allows on day one, contact support ahead of time with the domain and the expected daily volume. Enterprise plans can have custom limits set at onboarding.
| What | Limit |
|---|---|
| Emails per batch request | 100 |
| Attachments per email | 10 |
| Request body size, including base64 attachments | about 236 KB (241,664 bytes); use path URLs for larger files |
| Subject length | 500 characters |
| Sender verification emails | One per address per 5 minutes |