# AgentSend > The email API your agent can sign up for. Transactional and marketing email over REST and MCP. An agent creates its own sandbox account with one API call; a human claims it before real mail goes out. - API root: https://agentsend.co/api/v1 (`Authorization: Bearer `) - MCP (streamable HTTP): https://agentsend.co/api/mcp - OpenAPI 3.1: https://agentsend.co/api/v1/openapi.json - Full reference: https://agentsend.co/llms-full.txt - Docs: https://agentsend.co/docs - Every refusal is `{"error": {"code", "reason", "fix"}}`. Apply `fix` and retry. ## 1. Sign up (no human, no CAPTCHA) 1. `GET /api/v1/accounts/pow` returns `{"challenge", "difficulty", "expires_at", "algorithm": "sha256"}`. A challenge expires in 5 minutes and creates one account. 2. Find `nonce`: try 0, 1, 2, ... as decimal strings until sha256(UTF-8 of challenge + nonce) has at least `difficulty` leading zero bits (counted from the first byte's top bit). At the default 20 bits this takes about a second. 3. `POST /api/v1/accounts` with `{"name"?: string, "owner_email"?: string, "pow": {"challenge", "nonce"}}` returns 201 `{"id", "api_key", "api_key_id", "status": "sandbox", "claimed": false, "claim_url", "expires_at", "limits", "next_steps"}`. `api_key` is a full-access key shown only once: store it. With `owner_email`, the claim link is also emailed there. Errors: `pow_required` and `pow_invalid` (a fresh challenge is in `error.pow`), `signup_rate_limited` (3 per hour and 10 per day per IP), `signup_disabled`. ```sh POW=$(curl -s https://agentsend.co/api/v1/accounts/pow) NONCE=$(node -e 'const{createHash}=require("crypto");const{challenge,difficulty}=JSON.parse(process.argv[1]);for(let n=0;;n++){const h=createHash("sha256").update(challenge+n).digest();let z=0;for(const b of h){if(b){z+=Math.clz32(b)-24;break}z+=8}if(z>=difficulty){console.log(n);break}}' "$POW") CHALLENGE=$(printf %s "$POW" | sed 's/.*"challenge":"\([^"]*\)".*/\1/') curl -s -X POST https://agentsend.co/api/v1/accounts -H "Content-Type: application/json" \ -d "{\"name\": \"release-bot\", \"pow\": {\"challenge\": \"$CHALLENGE\", \"nonce\": \"$NONCE\"}}" ``` Python instead of Node for the nonce: ```sh NONCE=$(python3 -c 'import hashlib,itertools,json,sys;p=json.loads(sys.argv[1]);print(next(n for n in itertools.count() if int.from_bytes(hashlib.sha256((p["challenge"]+str(n)).encode()).digest(),"big")>>(256-p["difficulty"])==0))' "$POW") ``` TypeScript SDK (`npm i agentsend`, publishing soon): `const { data, error } = await AgentSend.signup({ name: "release-bot", ownerEmail: "ops@acme.com" })`, then `new AgentSend(data.apiKey)`. MCP: connect without a key and call `create_account`. The first call (no `pow`) returns a `pow_required` error carrying a challenge; solve it in code and call again with `pow: {challenge, nonce}`. Then reconnect with `Authorization: Bearer `; every other tool needs it. ## 2. Send right away (sandbox) Until a human claims the account it sends only to `delivered@`, `bounced@`, and `complained@simulator.agentsend.co` (anything else: `account_unclaimed`), from any address @agentsend.co, adds at most 1 domain, and is deleted at `expires_at` (7 days). ```sh curl -s -X POST https://agentsend.co/api/v1/emails -H "Authorization: Bearer $AGENTSEND_API_KEY" \ -H "Content-Type: application/json" \ -d '{"from": "Agent ", "to": ["delivered@simulator.agentsend.co"], "subject": "Hello", "text": "It works."}' curl -s "https://agentsend.co/api/v1/events?since=0" -H "Authorization: Bearer $AGENTSEND_API_KEY" ``` ## 3. Hand the account to your human Give them `claim_url`. They sign in or sign up, accept the Terms of Service and Acceptable Use Policy, and the account moves into their account: your API key keeps working, `GET /api/v1/account` shows `claimed: true` (its `id` becomes their account id), and an `account.claimed` event fires. Lost the link? `POST /api/v1/account/claim-link` (optional `{"owner_email"}` emails it) returns a new one; earlier links stop working. Real recipients after the claim: verify a domain (below) and have your human request live sending in the dashboard; an operator approves it. ## 4. Verify a sending domain 1. `POST /api/v1/domains {"name": "mail.acme.com"}` returns the DNS records to publish. 2. Publish them, then `POST /api/v1/domains/{id}/verify`. It returns `status` and `missing_records` with the exact name, type, and value still needed. Repeat until `status` is `verified`. ## 5. Check status `GET /api/v1/account` returns `{"status": "sandbox" | "live" | "paused", "claimed", "expires_at"?, "plan", "limits", "usage"}`. ## REST endpoints - `POST /api/v1/accounts`: Create an account (agent self-signup). No API key. Solve a challenge from GET /accounts/pow: find a decimal-string nonce where sha256(challenge + nonce) has `difficulty` leading zero bits. Each challenge works once; at most 3 signups per hour and 10 per day per IP. The account sends only to @simulator.agentsend.co until a person opens claim_url, and is deleted at expires_at if unclaimed. Errors: pow_required, pow_invalid, signup_rate_limited, signup_disabled. - `GET /api/v1/accounts/pow`: Get a signup proof-of-work challenge. - `GET /api/v1/account`: Get this key's account. - `POST /api/v1/account/claim-link`: Get a new claim link. Unclaimed agent accounts only. Replaces earlier links. With owner_email, also emails it there (3 per hour, 10 per day). - `POST /api/v1/emails`: Send an email. Every guardrail runs first. `Idempotency-Key` header for safe retries; `?dry_run=true` lints without sending. - `GET /api/v1/emails`: List emails. - `POST /api/v1/emails/batch`: Send up to 100 emails (all or nothing). - `GET /api/v1/emails/{id}`: Get an email. - `PATCH /api/v1/emails/{id}`: Reschedule a scheduled email. - `POST /api/v1/emails/{id}/cancel`: Cancel a scheduled email. - `POST /api/v1/domains`: Add a sending domain. - `GET /api/v1/domains`: List domains. - `GET /api/v1/domains/{id}`: Get a domain. - `DELETE /api/v1/domains/{id}`: Delete a domain. - `POST /api/v1/domains/{id}/verify`: Check the domain's DNS now. - `POST /api/v1/audiences`: Create an audience. - `GET /api/v1/audiences`: List audiences. - `DELETE /api/v1/audiences/{id}`: Delete an audience and its contacts. - `POST /api/v1/audiences/{id}/contacts`: Add a contact. - `GET /api/v1/audiences/{id}/contacts`: List an audience's contacts. - `PATCH /api/v1/audiences/{id}/contacts/{contact_id}`: Update a contact. - `DELETE /api/v1/audiences/{id}/contacts/{contact_id}`: Remove a contact. - `POST /api/v1/broadcasts`: Draft a broadcast. html or text must contain {{unsubscribe_url}}. - `GET /api/v1/broadcasts`: List broadcasts. - `GET /api/v1/broadcasts/{id}`: Get a broadcast. - `POST /api/v1/broadcasts/{id}/send`: Send a broadcast now or at scheduled_at. - `POST /api/v1/webhooks`: Create a webhook. - `GET /api/v1/webhooks`: List webhooks. - `PATCH /api/v1/webhooks/{id}`: Enable or disable a webhook. - `DELETE /api/v1/webhooks/{id}`: Delete a webhook. - `POST /api/v1/webhooks/{id}/test`: Send a signed webhook.test delivery. - `GET /api/v1/webhooks/{id}/deliveries`: List a webhook's latest deliveries. - `POST /api/v1/webhooks/{id}/deliveries/{delivery_id}/retry`: Attempt one delivery now. - `GET /api/v1/suppressions`: List suppressed addresses. - `DELETE /api/v1/suppressions/{email}`: Remove a suppression. - `GET /api/v1/events`: List events after a cursor. - `GET /api/v1/budget`: Get the calling key's effective budget. - `PUT /api/v1/budget`: Set a send budget. - `GET /api/v1/openapi.json`: This OpenAPI document. ## MCP tools - `create_account` (no API key): Create an AgentSend account with no human and no API key. Call it without `pow` first: the error carries a proof-of-work challenge in `pow`. Find a nonce (decimal string) where sha256(challenge + nonce) has `difficulty` leading zero bits (about a second of code), then call again with pow: {challenge, nonce}. Returns api_key (shown once) and claim_url for your human. The account sends only to @simulator.agentsend.co until a human claims it, and is deleted if unclaimed by expires_at. Then reconnect to this server with the header Authorization: Bearer . - `get_account`: This account: status (sandbox, live, paused), claimed, expires_at while unclaimed, plan, limits, and usage. - `get_claim_link`: A new claim link for your human (earlier links stop working). With owner_email, it is also emailed there. Only for unclaimed agent-created accounts. - `send_email`: Send one transactional email. Guardrails run first; a block returns {error: {code, reason, fix}} and nothing is sent. - `send_batch`: Send up to 100 emails in one call. If any email fails guardrails, none are sent. - `get_email`: Get an email's status (queued, sending, sent, delivered, bounced, complained, failed, blocked, ...). - `lint_email`: Dry run: run every guardrail and return errors and warnings without sending. - `create_domain`: Add a sending domain. Returns the DNS records (MX, SPF, DKIM, DMARC) to publish. - `verify_domain`: Check the domain's DNS now. Returns status and missing_records with the exact name/type/value still needed. - `list_domains`: List sending domains with their verification status and records. - `create_audience`: Create a marketing audience (contact list). - `add_contact`: Add a contact to an audience. Previously suppressed addresses stay unsubscribed. - `create_broadcast`: Draft a broadcast to an audience. html or text must contain {{unsubscribe_url}}; {{first_name}}, {{last_name}}, {{email}} are substituted. - `send_broadcast`: Send a draft broadcast now, or at scheduled_at. Adds List-Unsubscribe headers. - `list_events`: List events oldest-first after a cursor. Types: email.sent, email.delivered, email.delivery_delayed, email.bounced, email.complained, email.opened, email.clicked, email.failed, contact.unsubscribed, guardrail.blocked, account.sending_warned, account.sending_paused, webhook.disabled, suppression.removed, account.claimed, ops.alert. - `list_suppressions`: List suppressed addresses, newest first. hard_bounce and complaint block all mail (scope all); unsubscribe blocks broadcasts only (scope marketing). Filter by email (substring), scope, or reason; page with next_cursor. - `remove_suppression`: Remove an address from the suppression list (one scope, or every scope when scope is omitted); emits suppression.removed. Removing a complaint (scope all) requires confirm: "complaint"; pass it only when the recipient asked to receive mail again. - `set_budget`: Set send budgets for this API key (or api_key_id, or account: true for the account default). ## Events `email.sent`, `email.delivered`, `email.delivery_delayed`, `email.bounced`, `email.complained`, `email.opened`, `email.clicked`, `email.failed`, `contact.unsubscribed`, `guardrail.blocked`, `account.sending_warned`, `account.sending_paused`, `webhook.disabled`, `suppression.removed`, `account.claimed`, `ops.alert`. Read them with `GET /api/v1/events?since=` or subscribe a webhook (`POST /api/v1/webhooks`); deliveries are signed per Standard Webhooks.