Start here
Quickstart
Verify a domain, create a key, send the first email — in Node, Python or curl.
Three steps, and none of them are optional. A key without a verified domain can only send from onboarding.agentisend.dev, which is fine for a smoke test and wrong for anything a person reads.
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 (Oregon). Pass "region": "eu" for Helsinki. GET /domains/{id} returns the records: a DKIM TXT on as1._domainkey.yourdomain.com, an SPF TXT on the domain root, a DMARC TXT on _dmarc.yourdomain.com that is recommended rather than required, optional return-path records on send.yourdomain.com, and an optional tracking CNAME on links.yourdomain.com when tracking is available. Publish the required records, then call POST /domains/{id}/verify. Until that returns verified (or partially verified with DKIM and SPF in), a send from that domain answers domain_not_verified, and the error names the endpoint to call next.
If the root already has an SPF record, merge ours into it — 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 — rather than publishing a second one. Two SPF records on one name invalidate both.
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 packages/sdk-node/test/quickstart.test.ts executes it against the real API on every run — a sample that stops working fails the build.
import { AgentiSend } from '@agentisend/sdk-node';
const agentisend = new AgentiSend(process.env.AGENTISEND_API_KEY);
const { id } = await agentisend.emails.send({
from: '[email protected]',
to: '[email protected]',
subject: 'Hello from AgentiSend',
text: 'Ten minutes, start to sent.',
});
console.log(id);Python:
"""The AgentiSend Python quickstart — this file is executed by CI.
Run against a local API:
AGENTISEND_BASE_URL=http://127.0.0.1:5300 \
AGENTISEND_API_KEY=as_... python3 quickstart.py
Prints one JSON line per step. Exit 0 means every step worked.
"""
from __future__ import annotations
import json
import os
import sys
import time
from agentisend import AgentiSend, idempotency_key
def main() -> int:
client = AgentiSend() # reads AGENTISEND_API_KEY / AGENTISEND_BASE_URL
# 1. Send an email. The idempotency key is derived from the thing being
# done, so a retry can never double-send.
sent = client.send_email(
{
"from": "[email protected]",
"to": "[email protected]",
"subject": "Hello from the AgentiSend Python SDK",
"text": "First send via agentisend-python.",
},
idempotency=idempotency_key("welcome-email", "agent-1"),
)
print(json.dumps({"step": "send", "id": sent["id"]}))
email_id = sent["id"]
# 2. Poll until the message settles (fake/local transport: immediate).
deadline = time.time() + 15
while time.time() < deadline:
email = client.get_email(email_id)
if email.get("status") in {"sent", "failed", "bounced"}:
print(json.dumps({"step": "poll", "status": email["status"]}))
return 0 if email["status"] == "sent" else 1
time.sleep(0.2)
print(json.dumps({"step": "poll", "error": "timed out waiting for a terminal status"}))
return 1
if __name__ == "__main__":
sys.exit(main())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: receipt-2026-09-04" \
-H "Content-Type: application/json" \
-d '{
"from": "[email protected]",
"subject": "Your receipt",
"to": "[email protected]"
}'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 71 of them.
More examples
One runnable directory per integration, each with the exact variables it needs. Every one of them is executed against a real API server on every build, so an example that stops working fails the build rather than your first send.
- Next.js App Router: 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 daily ceiling and a loop guard, refusing the fourth identical send