Start here · Published 2026-09-04 · Updated 2026-10-31 · AgentiSend
Quickstart
Verify a domain, create a key, send the first email — in Node, Python or curl.
An agent can open an account without a person at the keyboard: agentisend agent sign-up --name <name> --human <email>, then agentisend agent verify --code <code> once the human has the code. Until then the key emails only that human. The rest of this page is the path a person walks.
You can send before a domain is verified. From onboarding@agentisend.com (GET /account reports it as onboarding_sender.from, and the address the mail arrives from as onboarding_sender.delivered_from), mail goes to your team: these sends reach member sign-in addresses only. Any address at simulator.agentisend.com is accepted, costs nothing and reaches nobody. Everything else needs the three steps below.
1. Verify a domain
Add the domain, then read the DNS records back and publish them at your registrar.
curl -X POST https://api.agentisend.com/domains \
-H "Authorization: Bearer $AGENTISEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "yourdomain.com"}'Region is optional and defaults to us. Passing "region": "eu" records that value on the domain. All mail is sent from the US today.
GET /domains/{id} returns the records. Three are required, and the domain verifies once all three resolve:
| Record | Name | Needed |
|---|---|---|
DKIM TXT | as1._domainkey.yourdomain.com | Required |
Return-path MX | send.yourdomain.com | Required |
Return-path TXT | send.yourdomain.com | Required |
SPF TXT | yourdomain.com | Recommended |
DMARC TXT | _dmarc.yourdomain.com | Recommended |
Tracking CNAME | links.yourdomain.com | Optional, listed when tracking is available |
Publish the three required records, then call POST /domains/{id}/verify. Until it returns verified, a send from that domain answers domain_not_verified, and the error names the endpoint to call next. The recommended records never block sending: root SPF makes the domain harder to spoof, and DMARC tells you what is being sent in its name.
If the root already has an SPF record, merge ours into it rather than publishing a second one: two SPF records on one name invalidate both. include:_spf.agentisend-dns.com is the short form, and it only verifies once that include currently lists every sending IP; include:_spf.agentisend.com still works.
See Domains and DNS for what each record does.
2. Create a key with a budget
A key is created from the console or from POST /api-keys. Give it the narrowest permission that does the job — sending_access for a service that only sends — and give it a budget before you give it to an agent. See Budgets and the kill switch.
3. Send
Node. This block is the one in the repository README, and it is run by the test suite against a local AgentiSend server, so a sample that stops working fails the build rather than your first send.
import { AgentiSend } from 'agentisend';
const agentisend = new AgentiSend(process.env.AGENTISEND_API_KEY);
const { id } = await agentisend.emails.send({
from: 'you@yourdomain.com',
to: 'someone@example.com',
subject: 'Hello from AgentiSend',
text: 'Ten minutes, start to sent.',
});
console.log(id);Python, with nothing installed. Set AGENTISEND_API_KEY and MAIL_FROM first:
"""Send one email with AgentiSend from Python, with nothing installed.
Set AGENTISEND_API_KEY and MAIL_FROM (an address on your verified domain), then: python3 send.py
"""
import json
import os
import urllib.error
import urllib.request
BASE_URL = os.environ.get("AGENTISEND_BASE_URL", "https://api.agentisend.com")
request = urllib.request.Request(
f"{BASE_URL}/emails",
method="POST",
headers={
"Authorization": f"Bearer {os.environ['AGENTISEND_API_KEY']}",
"Content-Type": "application/json",
# Named after the send, not the moment: a retry after a timeout replays it instead of mailing twice.
"Idempotency-Key": "hello/someone@example.com",
},
data=json.dumps({
"from": os.environ["MAIL_FROM"],
"to": "someone@example.com",
"subject": "Hello from AgentiSend",
"text": "Ten minutes, start to sent.",
}).encode(),
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
print(json.load(response)["id"])
except urllib.error.HTTPError as err:
# Every refusal carries a code and a fix that says what to do next.
error = json.load(err).get("error", {})
raise SystemExit(f"{error.get('code')}: {error.get('fix')}")curl, assembled from the POST /emails operation in openapi.json:
curl -X POST https://api.agentisend.com/emails \
-H "Authorization: Bearer $AGENTISEND_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"from": "hello@yourdomain.com",
"to": "someone@example.com",
"subject": "Your receipt",
"text": "Thanks — the details are attached."
}'Send the same request twice
Every mutating operation takes an Idempotency-Key header. Send it. An agent that retries a timeout without one is an agent that mails the same person twice, and a duplicate receipt is the kind of defect a customer reports before you notice it.
What comes back when it goes wrong
Every 4xx and 5xx carries code, message, fix and docs_url, and a wait hint exactly when waiting can help. The error catalogue lists all 122 of them.
More examples
One runnable directory per integration, each with the exact variables it needs. Every one of them is run by the test suite against a local AgentiSend server, so an example that stops working fails the build rather than your first send.
- Next.js: a route handler and a Server Action
- Supabase: the Send Email Hook, so you own your sign-up and password-reset mail
- Better Auth: the verification and password-reset callbacks
- Auth.js: the sign-in link send, over HTTPS rather than SMTP
- Hono, Express, and a SvelteKit form action
- An agent with a ceiling and a loop guard, refusing the fourth identical send
- The same agent in Python, with nothing installed
- FastAPI, Flask and Django: one POST that checks the address, sends with an idempotency key, and returns a refusal as
codeandfix - A Vercel AI SDK tool and a LangChain.js tool, each sending through the agent's own budgeted key and returning a refusal as
codeandfix - Claude Agent SDK, OpenAI Agents SDK, CrewAI, Mastra and LangChain for Python: the same tool, the same refusal, and how a reply is read
- Fastify, Astro, Nuxt, React Router and Remix and NestJS: one server-side handler that checks the address, sends with an idempotency key, and returns a refusal as
codeandfix - Node.js, Remix, Bun and Deno: the same handler on
node:http, a Remix action, and fetch - Go, PHP, Laravel and Rails:
POST /emailswith the language's own HTTP client - Vercel Functions, Cloudflare Workers and AWS Lambda: one handler that checks the address, sends with an idempotency key, and returns a refusal as
codeandfix - Ruby, Rust, Java, .NET and Elixir:
POST /emailswith that language's HTTP client - TanStack Start, RedwoodJS, Railway, Encore (TypeScript) and Encore (Go): one server-side function that checks the address, sends with an idempotency key, and returns a refusal as
codeandfix - Supabase Edge Functions and Deno Deploy: a fetch handler, separate from the Supabase Send Email Hook and the Deno runtime
- Symfony, Sinatra, Axum and Phoenix:
POST /emailswith that language's HTTP client - Auth0: the custom email provider Action, which posts the notification Auth0 already rendered
All examples, as one directory each