Your email API's next user is an AI agent. Here's what it needs

Your email API's next user is an AI agent. Here's what it needs

# ai# api# email# discuss
Your email API's next user is an AI agent. Here's what it needsCresca

AI agents now read docs, call APIs and retry on their own. Six things that make an email API safe and easy for them to use, with examples from Cresca's API.

If you ship an API in 2026, assume an AI agent will call it. Coding agents read your docs, write the request, read the error, fix it and try again, often with nobody watching.

For most APIs that's fine. Email is different: a mistake goes straight to someone's inbox, and you can't take it back.

Here are six things I think an email API needs before an agent uses it, and what we already have in Cresca's API. I'll also cover what we don't have yet.

1. Docs a model can read in one go

Agents don't click through a docs sidebar. They fetch a URL and read the text. So the docs need to work as plain text, with one page per endpoint and every field listed.

We publish an llms.txt file: a plain-text summary of what Cresca does, with links to the quickstart, the send endpoint, templates, idempotency, errors and rate limits. Point an agent at it, and it can find the right page without crawling the whole site.

2. Errors with a name, not just a message

Agents fix their own mistakes by reading error responses. A message like "Something went wrong" gives them nothing to work with. Every error from our API has the same shape:

{
  "statusCode": 403,
  "name": "invalid_from_address",
  "message": "The `from` address hello@acme.com is not an active sender on a verified domain. Add it under Sending domains in Cresca."
}
Enter fullscreen mode Exit fullscreen mode

The name is stable, so code (or an agent) can branch on it. The message says what to do next: an agent that gets this error knows to switch to a verified sender instead of retrying the same request.

The names an agent meets most: missing_api_key, invalid_api_key, restricted_api_key, invalid_from_address, validation_error, missing_required_field and rate_limit_exceeded.

3. Retries that can't send twice

Agents retry a lot. A timeout, an unclear response or a restarted task, and the same request goes out again. With email, that means a customer gets two receipts.

The fix is an Idempotency-Key header. Use the same key and the same body within 24 hours, and you get the original response back instead of a second email. Tell the agent to build the key from the event, not from a timestamp:

headers['Idempotency-Key'] = `order-receipt/${order.id}`;
Enter fullscreen mode Exit fullscreen mode

Two more cases an agent needs to handle: the same key with a different body returns 409 invalid_idempotent_request, and the same key while the first request is still running returns 409 concurrent_idempotent_requests.

4. Keys that can only do one thing

Don't give an agent your admin key. Give it the smallest key that does the job.

Our API has two kinds of key. A sending-access key can call POST /emails and POST /emails/batch, nothing else, and you can limit it to one verified domain. A full-access key can also read, cancel and manage templates and contacts. If an agent with a sending key tries anything else, it gets 401 restricted_api_key.

So even if an agent misbehaves, or a prompt injection tricks it, it can't read your email logs or send from a domain you didn't give it.

5. Templates instead of HTML

Letting an agent write the HTML of a password reset is asking for trouble: broken markup, a missing link, a made-up promise.

A template is safer. A person designs the email once and publishes it, and the agent only fills in values:

await fetch('https://api.cresca.xyz/api/v1/emails', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CRESCA_SENDING_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': `password-reset/${resetRequest.id}`,
  },
  body: JSON.stringify({
    to: user.email,
    template: { id: 'password-reset', variables: { NAME: user.name, RESET_URL: resetUrl } },
  }),
});
Enter fullscreen mode Exit fullscreen mode

Values are HTML-escaped, so they can't change the markup. And if the agent forgets a variable that has no fallback, the API returns 422 validation_error naming the missing variable instead of sending a broken email.

6. Limits it can read

Agents are fast and don't get tired, so they hit rate limits. The response should tell them how long to wait. Ours returns 429 rate_limit_exceeded with a retry-after header in seconds. The limits are per account: 2 requests per second on the Free plan, up to 100 on Enterprise.

One more limit to teach an agent: monthly_quota_exceeded is also a 429, but waiting won't fix it. Tell it to stop and report back instead of retrying.

The tool definition

Put all of that together and the tool you give the agent is small. Here's a provider-neutral version:

{
  "name": "send_password_reset",
  "description": "Send the published password-reset email to one user. Never call this twice for the same reset request.",
  "input_schema": {
    "type": "object",
    "properties": {
      "to": { "type": "string", "description": "The user's email address" },
      "name": { "type": "string" },
      "reset_url": { "type": "string" },
      "reset_request_id": { "type": "string", "description": "Used as the idempotency key" }
    },
    "required": ["to", "reset_url", "reset_request_id"]
  }
}
Enter fullscreen mode Exit fullscreen mode

Notice what the agent doesn't control: the sender, the subject, the HTML or the key. Your code fills those in. The agent decides when to send and to whom, and nothing else.

What we don't have yet

To be honest about the gaps:

  • No MCP server yet. Today an agent uses the plain REST API, which works with any agent that can make an HTTP request.
  • No webhooks yet. An agent checks delivery with GET /emails/:id, which returns the latest status as last_event.
  • Unknown fields are ignored, not rejected. That's forgiving for people, but an agent that invents a field won't get an error. Validate the payload in your tool before you send it.

Your turn

If you've let an agent call an API that can't take back its mistakes, such as email, payments or deploys, what guardrails did you add? I'd especially like to hear what went wrong the first time.