How should an AI agent handle email API rate limits?
When an email API answers 429, the agent should wait the number of seconds in Retry-After and retry the same request with the same idempotency key, so the retry can never send twice. On Unitpost the limit is per workspace, shared by every key, and each response carries RateLimit-Limit, -Remaining and -Reset headers. Batch sends of up to 100 emails use one request instead of a hundred.
Unitpost ·
An agent doesn't get tired of retrying. Given a 429 and no rules, it either gives up on a send that would have worked a second later, or hammers the API until every key in the workspace is throttled. Both are fixable with three habits.
What do the rate limit headers say?
Every Unitpost API response carries the workspace's budget:
RateLimit-Limit: requests allowed in the window;RateLimit-Remaining: what's left in it;RateLimit-Reset: when it refills, as a Unix time in seconds. Subtract the current time to get the wait; on a429,Retry-Afteralready gives it in seconds.
A request over the budget gets 429 with Retry-After, in seconds. The budget is per workspace and shared by every key, so an agent's burst also slows the app that sends your receipts. That's a reason to give agents the patience rules below, not a separate key for speed.
How should the agent retry?
- Wait
Retry-After, then retry once with the same body and the sameIdempotency-Key. The key makes the retry safe: if the first request did get through, the retry returns the original result (withIdempotent-Replayed: true) instead of sending again. Keys last 24 hours. - Back off on
5xxand network errors (1, 2, 4, 8 seconds), still with the same key, and give up after a few tries. - Don't retry other
4xx. A422names the field that's wrong, a403names the missing permission, and a409means the same key was sent with a different body. Report those to the person.
async function sendWithRetry(body: object, key: string, tries = 4) {
for (let i = 0; i < tries; i++) {
const res = await fetch("https://www.unitpost.com/api/v1/email", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.UNITPOST_API_KEY}`,
"Content-Type": "application/json",
"User-Agent": "my-agent/1.0",
"Idempotency-Key": key, // the same key on every try
},
body: JSON.stringify(body),
});
if (res.status === 429) {
await new Promise((r) => setTimeout(r, Number(res.headers.get("Retry-After") ?? 1) * 1000));
continue;
}
if (res.status >= 500) {
await new Promise((r) => setTimeout(r, 2 ** i * 1000));
continue;
}
return res.json(); // success, or a 4xx to report, not retry
}
throw new Error("Gave up after retries");
}Make the key once per message
Create the idempotency key when the agent decides to send a message, and pass it into every retry. A key generated inside the retry loop, or inside the tool, changes each time and a lost response becomes a second email.
How do you send less often?
- Batch.
POST /email/batchsends up to 100 emails in one request, with one idempotency key for the batch. An agent writing 300 personalized notes makes three requests, not 300. - Schedule. A batch or a single send can carry
scheduled_at, so the agent finishes its work in one pass and the sends go out at the right time (scheduling). - Campaigns for one-to-many. Marketing to a segment is one campaign call, not a send per contact, and a marketing send to more than one recipient on the transactional endpoint is refused anyway.
What should the agent tell a person?
When it gives up, it should say what it tried, the last status and the Retry-After it saw, and that nothing was sent twice. A message like "Rate limited: 3 of 40 emails are still waiting; retrying them at 14:05 with the same keys" lets a person decide whether to wait or ask for a higher limit.