Skip to content
Browse the docs

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:

RecordNameNeeded
DKIM TXTas1._domainkey.yourdomain.comRequired
Return-path MXsend.yourdomain.comRequired
Return-path TXTsend.yourdomain.comRequired
SPF TXTyourdomain.comRecommended
DMARC TXT_dmarc.yourdomain.comRecommended
Tracking CNAMElinks.yourdomain.comOptional, 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);
the repository README; run by the test suite against a local AgentiSend server

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')}")
examples/python-send/send.py, run by the test suite against a local AgentiSend server

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."
  }'
assembled from POST /emails in openapi.json

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.

All examples, as one directory each

Next