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 a 429, Retry-After already 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?

  1. Wait Retry-After, then retry once with the same body and the same Idempotency-Key. The key makes the retry safe: if the first request did get through, the retry returns the original result (with Idempotent-Replayed: true) instead of sending again. Keys last 24 hours.
  2. Back off on 5xx and network errors (1, 2, 4, 8 seconds), still with the same key, and give up after a few tries.
  3. Don't retry other 4xx. A 422 names the field that's wrong, a 403 names the missing permission, and a 409 means the same key was sent with a different body. Report those to the person.
cURL
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/batch sends 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.

Frequently asked questions

What is Unitpost's API rate limit?

10 requests a second per workspace by default, shared across all of its keys. Support can raise it for a workspace. A few write endpoints also apply abuse throttles on top, which answer 429 with Retry-After.

Should an agent retry every failed request?

No. Retry a 429 after Retry-After, and a 5xx or a network error with backoff, always with the same idempotency key. A 4xx other than 429 means the request is wrong, so an identical retry fails again: fix the request or stop and report it.

How does an MCP client see a rate limit?

The tool result carries the API's error object plus a hint with retry_after_seconds. The server's instructions tell the agent to wait that long and not to resend an identical call that failed with another 4xx.

Free to start

Start sending for free

$0/ mo

  • 5,000 emails / mo — no credit card
  • Your domain with DKIM, SPF & DMARC
  • REST API, SMTP & official SDKs
  • Visual editor + code templates
  • Open & click tracking + signed webhooks
  • 10,000 contacts · automatic suppression