Sign in, open API keys, create one. Use sending_access (optionally scoped to one domain) for agents that only send.
export AGENTSEND_URL=https://www.agentsend.co
export AGENTSEND_API_KEY=as_...Docs · v1
From API key to an email delivered from your own domain. Every endpoint takes Authorization: Bearer $AGENTSEND_API_KEY. Every refusal is {error: {code, reason, fix}}; apply the fix and retry.
sandbox Delivery is simulated today: every send runs guardrails, events, webhooks, and suppression, but nothing leaves for the internet. Simulator recipients: delivered@, bounced@, and complained@simulator.agentsend.co. Before your domain verifies, you can send from @agentsend.co to simulator addresses only.
01Get a key
Sign in, open API keys, create one. Use sending_access (optionally scoped to one domain) for agents that only send.
export AGENTSEND_URL=https://www.agentsend.co
export AGENTSEND_API_KEY=as_...02Add and verify a domain
curl -X POST $AGENTSEND_URL/api/v1/domains \
-H "Authorization: Bearer $AGENTSEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "mail.acme.com"}'
# → records: MX + SPF TXT on send.mail.acme.com, DKIM CNAME, DMARC TXT
curl -X POST $AGENTSEND_URL/api/v1/domains/$DOMAIN_ID/verify \
-H "Authorization: Bearer $AGENTSEND_API_KEY"
# → {"status": "pending", "missing_records": [{"type": "CNAME", "name": "…", "value": "…",
# "problem": "missing", "found": []}], "fix": "Publish each record…"}Verification queries live DNS and names each record that is missing or has the wrong value. Call it again after publishing until status is verified.
03Send
curl -X POST $AGENTSEND_URL/api/v1/emails \
-H "Authorization: Bearer $AGENTSEND_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: signup-4821-welcome" \
-d '{"from": "Acme <hello@mail.acme.com>", "to": ["dana@example.com"],
"subject": "Welcome to Acme", "html": "<p>Thanks for signing up.</p>",
"tags": [{"name": "category", "value": "welcome"}]}'Add ?dry_run=true to run every guardrail and get errors and warnings without sending. Add scheduled_at to send later; cancel with POST /api/v1/emails/{id}/cancel.
04Broadcast to an audience
curl -X POST $AGENTSEND_URL/api/v1/audiences -H "Authorization: Bearer $AGENTSEND_API_KEY" \
-H "Content-Type: application/json" -d '{"name": "Customers"}'
curl -X POST $AGENTSEND_URL/api/v1/audiences/$AUDIENCE_ID/contacts \
-H "Authorization: Bearer $AGENTSEND_API_KEY" -H "Content-Type: application/json" \
-d '{"email": "dana@example.com", "first_name": "Dana"}'
curl -X POST $AGENTSEND_URL/api/v1/broadcasts -H "Authorization: Bearer $AGENTSEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{"audience_id": "'$AUDIENCE_ID'", "from": "Acme <news@mail.acme.com>",
"subject": "October update",
"html": "<p>Hi {{first_name}}</p><a href=\"{{unsubscribe_url}}\">Unsubscribe</a>"}'
curl -X POST $AGENTSEND_URL/api/v1/broadcasts/$BROADCAST_ID/send \
-H "Authorization: Bearer $AGENTSEND_API_KEY"Broadcasts require {{unsubscribe_url}} and go out with List-Unsubscribe and List-Unsubscribe-Post: List-Unsubscribe=One-Click. Unsubscribes suppress the address for good.
05Events and webhooks
curl "$AGENTSEND_URL/api/v1/events?since=0&type=email.bounced,guardrail.blocked" \
-H "Authorization: Bearer $AGENTSEND_API_KEY"
curl -X POST $AGENTSEND_URL/api/v1/webhooks -H "Authorization: Bearer $AGENTSEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{"endpoint": "https://api.acme.com/hooks/agentsend", "events": ["email.bounced"]}'Events: email.sent, email.delivered, email.bounced, email.complained, email.opened, email.clicked, contact.unsubscribed, guardrail.blocked. Open and click tracking are not emitted yet. Deliveries follow Standard Webhooks: verify webhook-signature (v1,<base64>) as HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{body} keyed with the base64-decoded part of your whsec_ secret.
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(secret: string, id: string, ts: string, body: string, header: string) {
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = "v1," + createHmac("sha256", key).update(`${id}.${ts}.${body}`).digest("base64");
const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
return fresh && header.split(" ").some((sig) =>
sig.length === expected.length && timingSafeEqual(Buffer.from(sig), Buffer.from(expected)));
}06Budgets
curl -X PUT $AGENTSEND_URL/api/v1/budget -H "Authorization: Bearer $AGENTSEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{"per_minute": 30, "per_day": 2000, "max_recipients_per_send": 10}'Applies to the calling key (or api_key_id, or "account": true for the account default). Without one: 60/min, 1,000/day, 50 recipients per send. Plan quotas apply on top.
07MCP
Streamable HTTP at https://www.agentsend.co/api/mcp. Tools: send_email, send_batch, get_email, lint_email, create_domain, verify_domain, list_domains, create_audience, add_contact, create_broadcast, send_broadcast, list_events, set_budget.
claude mcp add --transport http agentsend https://www.agentsend.co/api/mcp \
--header "Authorization: Bearer $AGENTSEND_API_KEY"{
"mcpServers": {
"agentsend": {
"type": "http",
"url": "https://www.agentsend.co/api/mcp",
"headers": { "Authorization": "Bearer as_..." }
}
}
}08Migrating from Resend
The shapes match where it matters: POST /emails with from, to, cc, bcc, reply_to, subject, html, text, headers, tags, attachments, scheduled_at, plus Idempotency-Key, batch sends of 100, audiences, contacts, and broadcasts. Point your base URL at https://www.agentsend.co/api/v1, swap the key, and re-add your domain (records differ). Differences: blocked sends return {error: {code, reason, fix}} instead of sending; broadcasts substitute {{first_name}}-style variables; attachments take base64 content (no remote path yet).
09Endpoints
| POST | /api/v1/emails | Send. Idempotency-Key header. ?dry_run=true lints only. |
| POST | /api/v1/emails/batch | Up to 100 emails; all or nothing. |
| GET | /api/v1/emails/{id} | Status: scheduled, delivered, bounced, complained, blocked… |
| PATCH | /api/v1/emails/{id} | Reschedule {scheduled_at} |
| POST | /api/v1/emails/{id}/cancel | Cancel a scheduled email |
| POST | /api/v1/domains | {name} → DNS records |
| GET | /api/v1/domains | List domains |
| GET | /api/v1/domains/{id} | Domain + record status |
| POST | /api/v1/domains/{id}/verify | Live DNS check → missing_records |
| DELETE | /api/v1/domains/{id} | Remove a domain |
| POST | /api/v1/audiences | {name} |
| GET | /api/v1/audiences | List audiences |
| DELETE | /api/v1/audiences/{id} | Delete with its contacts |
| POST | /api/v1/audiences/{id}/contacts | {email, first_name?, last_name?} |
| GET | /api/v1/audiences/{id}/contacts | List contacts |
| PATCH | /api/v1/audiences/{id}/contacts/{contact_id} | {first_name?, last_name?, unsubscribed?} |
| DELETE | /api/v1/audiences/{id}/contacts/{contact_id} | Remove a contact |
| POST | /api/v1/broadcasts | {audience_id, from, subject, html|text} → draft |
| GET | /api/v1/broadcasts/{id} | Broadcast status |
| POST | /api/v1/broadcasts/{id}/send | Send now or {scheduled_at} |
| POST | /api/v1/webhooks | {endpoint, events?} → signing secret (once) |
| GET | /api/v1/webhooks | List webhooks |
| DELETE | /api/v1/webhooks/{id} | Remove a webhook |
| POST | /api/v1/webhooks/{id}/test | Signed webhook.test delivery |
| GET | /api/v1/events | ?since=<cursor>&type=a,b → {data, next_cursor} |
| PUT | /api/v1/budget | {per_minute, per_day, max_recipients_per_send, api_key_id?|account?} |
10Error codes
| 401 | unauthorized | Missing, unknown, or revoked API key. |
| 403 | restricted_api_key | sending_access keys can only send email. |
| 403 | unverified_from_domain | from is not on a verified domain. |
| 403 | from_domain_not_allowed | Domain-scoped key used another domain. |
| 422 | empty_body / image_only_email | No real content. |
| 422 | unrendered_template | {{var}} or {var} left in subject or body. |
| 422 | missing_unsubscribe_link | Broadcast without {{unsubscribe_url}}. |
| 422 | recipient_suppressed | Hard bounce, complaint, or unsubscribe on file. |
| 422 | too_many_recipients | Over max_recipients_per_send. |
| 413 | payload_too_large | Over 40 MB. |
| 409 | duplicate_email | Same recipients + subject + body within 10 minutes. |
| 423 | loop_detected / api_key_paused | 3 duplicates in 10 minutes pause the key. |
| 429 | budget_exceeded_minute / _day | Per-key or account budget reached. |
| 429 | plan_quota_exceeded / plan_daily_quota_exceeded | Plan volume reached; upgrade. |
| 403 | plan_domain_limit / plan_contact_limit | Plan limit reached; upgrade. |
| 422 | batch_blocked | error.errors lists each failing index. |