# SendForAI > Transactional and broadcast email for AI agents. Like Resend, at cost: one API for one-to-one transactional sends (receipts, sign-in links, resets) and broadcasts to a list (newsletters, launches). Amazon SES underneath, unlimited contacts, zero markup on sending. An agent can sign up, build and test with no human; the only human step is adding credit before emailing the outside world. ## Start here (60 seconds, no domain, no DNS, no human) ``` npx sendforai sandbox # creates a key and a sandbox identity, prints {"domain": "k....sandbox.sendforai.com", ...} npx sendforai send --from agent@ --to delivered@simulator.sendforai.com --subject Hello --markdown "It works." ``` A sandbox identity can use the whole API but only reaches @simulator.sendforai.com (delivered@, bounced@, complained@ produce those events; free, never delivered). To send real email, add a domain you control: `npx sendforai dns setup --domain example.com` with a Vercel or Cloudflare token in the environment, or `npx sendforai domains add example.com` after publishing the record it names. The CLI prints JSON and never prompts. ## Machine-readable - Full docs as markdown: https://sendforai.com/docs.md (this file plus the docs in one fetch: https://sendforai.com/llms-full.txt) - OpenAPI 3.1: https://sendforai.com/openapi.json - API index: GET https://api.sendforai.com/v1 · pricing: GET /v1/pricing (markdown: https://sendforai.com/pricing.md) · health: GET /v1/status - MCP manifest: https://sendforai.com/.well-known/mcp.json - Agent skill (the whole workflow as instructions): https://sendforai.com/skill.md · Claude Code plugin: `/plugin marketplace add https://sendforai.com/marketplace.json` then `/plugin install sendforai@sendforai` ## Ways to connect - Shell: `npx sendforai ` (`--help` lists them). - Node: `npm i @sendforai/sdk`. `new SendForAI({ domain, privateKey })`, or `SendForAI.sandbox()` for a no-DNS identity. - MCP, hosted: https://sendforai.com/mcp (Streamable HTTP) with header `Authorization: Bearer `. Bearer token, not OAuth. - MCP, local: `npx -y @sendforai/mcp` with env SENDFORAI_DOMAIN and SENDFORAI_PRIVATE_KEY_FILE. Adds generate_identity, setup_dns, create_connection_token. - Any language or curl: plain HTTP with `Authorization: Bearer `. ## Identity and auth (no API keys are issued) Your identity is an Ed25519 key. It is bound either to a domain, by publishing DNS TXT `_sendforai.` = `v=sfa1; k=ed25519; p=`, or to a sandbox name derived from the key, `k.sandbox.sendforai.com` (no DNS). The first authenticated request creates the account. Base URL https://api.sendforai.com. Two credentials work: 1. Connection token: `Authorization: Bearer sfat1..`. payload = base64url(no padding) of `{"d":"","iat":,"exp":}` (sandbox adds `"p":""`); signature = base64url Ed25519 signature over the ASCII string `sfat1.`. Max lifetime 90 days. Minted locally (`npx sendforai token`, `sfa.tokens.create()`); nothing is stored server-side; rotating the key revokes it. 2. Per-request signature: headers `SendForAI-Domain`, `SendForAI-Timestamp` (unix seconds, within 300s), `SendForAI-Signature: sfa1=` over these six lines joined by "\n": `sfa1`, METHOD, path with query, timestamp, domain (lowercase), SHA-256 hex of the exact body bytes (hash of the empty string when there is no body). Sandbox identities also send `SendForAI-Public-Key: `. Copy-paste Python and shell examples: https://sendforai.com/docs.md (section "Signing"). ## Pricing (machine readable: GET /v1/pricing) - $0 to build, forever: unlimited simulated sends to any address @simulator.sendforai.com (delivered@, bounced@, complained@ produce those events; never delivered, never billed) and 25 real emails a day to addresses at your own domains. - $5 in months you send: the first send in a calendar month (UTC) to anyone outside your own domains takes $5 from your prepaid balance and includes 10,000 emails that month. Months you don't send to the outside world cost $0. - Then $0.11 per 1,000, at published cost (SES $0.100 + tenant $0.005 + card ~$0.004 + $0.001 rounding). - Prepaid: top up from $10 (a human pays via a checkout link you hand them); optional auto top-up from their saved card. Usage per month is capped by a budget only a human can raise ($20 by default). - Up to 50 sending domains per account, no per-domain fee. Over 1,000,000 emails/month: dedicated IP ($24.95/mo) and $0.02 per 1,000 infrastructure, both at cost. ## Domains GET /v1/domains/{domain}/setup?p= (public) returns the exact identity record. To send real mail, POST /v1/domains/{domain}/verify returns SES's DKIM and MAIL FROM records to publish. Automatic DNS: with a Vercel or Cloudflare API token, `npx sendforai dns setup` / `sfa.dns.setup({ provider: dnsProviderFromEnv() })` / local MCP tool `setup_dns` publishes the identity or delegation record, waits for it to propagate, then publishes the DKIM and MAIL FROM records and a DMARC record if none exists. The token stays on your machine; SendForAI never sees it. Safe to re-run. To send from more domains with the same key (or to give a sandbox account its first real domain), publish on each one `_sendforai.` TXT = `v=sfa1; delegate=` and call POST /v1/domains {"name": ""}. Subdomains of a domain you have are covered automatically. Balance and allowance are shared; reputation, warmup and daily caps stay per domain. ## Endpoints (all JSON; responses carry `cost` and `capacity` where relevant; every error carries `fix`) - POST /v1/emails — send. Fields: from, to, subject, html|markdown|text, reply_to, headers, idempotency_key, dry_run, max_cost_usd, stream. `from` may be any address on any of the account's domains. dry_run shows cost including `month_fee_usd` when a send would start a sending month. - GET /v1/account — plan this month, balance_usd, auto_topup, budget (monthly, used, remaining), usage, every domain. Check this before large sends. - POST /v1/account/topup {amount_usd >= 10, auto_topup} — returns checkout_url for a human; account.topped_up appears in /v1/events. (POST /v1/account/upgrade is an alias.) - POST /v1/account/budget {monthly_usd} — lowering applies now; raising returns approval_url for a human. - POST /v1/account/auto-topup {enabled, amount_usd} — turn off or change the amount (turning on needs a card from a top-up). - GET /v1/account/billing — Stripe portal link for a human (card, receipts). - GET /v1/domains · POST /v1/domains {name} · GET /v1/domains/{domain} · POST /v1/domains/{domain}/verify — domains, SES identity, DKIM and MAIL FROM records. - GET /v1/capacity — daily cap, sent today, remaining, warmup stage. GET /v1/usage — month to date. GET /v1/pricing — public pricing. - POST /v1/broadcasts — create; defaults to awaiting_approval and returns approval_url for a human. GET /v1/broadcasts/{id} (progress: sent, queued, failed, cancelled, resumes_at), POST /v1/broadcasts/{id}/send (202, background), POST /v1/broadcasts/{id}/cancel (also mid-send; the unsent part is refunded). - POST /v1/contacts (upsert one) · GET /v1/contacts · POST /v1/contacts/import (in a sending month) · POST /v1/contacts/unsubscribe. - GET /v1/events?cursor= — pullable event log (email.*, broadcast.*, account.month_started, account.topped_up, account.budget_changed, account.auto_topup_failed, domain.added, contact.unsubscribed). ## Errors you will meet, and what to do - 402 balance_required — not enough credit for a send or a new sending month. Call POST /v1/account/topup and hand checkout_url to your operator. Simulator and own-domain sends keep working. - 402 budget_exceeded — this month's usage budget is used. Send less, wait, or POST /v1/account/budget to get an approval link for your operator. - 400 from not on your domains — publish the delegation record in details.record and POST /v1/domains. - 409 sandbox_identity — a sandbox identity tried to send real email. Add a domain you control (see Domains). - 401 unauthenticated — details say what was wrong: clock skew, key mismatch, expired token, or missing DNS record. - 422 send_rejected — Amazon SES refused every recipient; nothing was sent or billed; details.recipients has the reason per address. ## Guides - /agents — Per-client install snippets (Claude Code, Cursor, Copilot, any MCP client), and what a human still has to do. - /docs — Human-readable quickstart: identity, domains, sending, dry_run/max_cost_usd/idempotency_key, contacts, broadcasts with approval, credit and budgets, events, errors, MCP config. - /pricing — Pricing in full with worked examples. - /amazon-ses-pricing — Amazon SES pricing explained: the 2026 Essentials/Pro/Enterprise plans vs $0.10 à la carte, a calculator, and what SES doesn't include. - /resend-pricing — Resend pricing explained, and what 7 workloads cost on Resend vs SendForAI. - /resend-alternatives — Resend alternatives by reason for switching, with published costs at 10k, 50k, 100k and 1M emails for SendGrid, Mailgun, Postmark, useSend, Plunk, Amazon SES and SendForAI. - /resend-vs-sendgrid — Resend vs SendGrid: pricing at four volumes, free tiers, marketing email, templates, dedicated IPs and SSO, and which to pick. - /sendgrid-pricing — SendGrid (Twilio) Email API and Marketing Campaigns pricing explained, with what each volume costs. - /sendgrid-alternatives — SendGrid alternatives compared on real cost at several volumes, and which fits each reason for switching. - /email-api-for-ai-agents — What an AI agent needs from an email sending API (vs an inbox API), as questions to ask any provider. - /mailgun-pricing — Mailgun pricing explained: Free, Basic, Foundation and Scale, overage, features, add-ons, and what each plan costs from 10k to 2.5M emails. - /mailgun-alternatives — SendGrid, Postmark, Resend, Amazon SES and SendForAI priced against Mailgun at 10k–2.5M emails/month, with a reason-to-switch table and migration checklist. - /postmark-pricing — Postmark pricing explained: Basic, Pro and Platform at every volume, overage, broadcasts, and cost vs Resend and SendForAI. - /transactional-vs-marketing-email — Transactional vs marketing email under CAN-SPAM and Gmail/Yahoo sender rules, with 14 common emails classified and how to send each. ## Limits worth knowing - Send-only: no inbound email or inboxes. No open or click tracking. - The SDK is Node only; other languages use HTTP with a connection token. - New sending domains warm up from a low daily cap. ## Contact hello@sendforai.com · abuse reports: abuse@sendforai.com --- # SendForAI docs SendForAI is one JSON API for transactional email (one message to one person) and broadcasts (one message to a list), built so an AI agent can set it up and run it. Amazon SES underneath, sold at cost. - Base URL: `https://api.sendforai.com` - Index of everything: `GET https://api.sendforai.com/v1` - OpenAPI: https://sendforai.com/openapi.json - Short version of this page: https://sendforai.com/llms.txt - Pricing as markdown: https://sendforai.com/pricing.md, as JSON: `GET /v1/pricing` - Health: `GET /v1/status` There are no API keys to request and no dashboard. Your identity is an Ed25519 key. The first authenticated request creates the account. ## Fastest start (no domain, no DNS, no human) ```bash npx sendforai sandbox # -> { "domain": "k3f9c...sandbox.sendforai.com", "key_file": "~/.sendforai/k3f9c....key" } npx sendforai send \ --from agent@k3f9c...sandbox.sendforai.com \ --to delivered@simulator.sendforai.com \ --subject "Hello" --markdown "It works." ``` A sandbox identity can use the whole API, but only reaches the simulator. To send real email, add a domain you control (see "Domains"). The CLI prints JSON and never prompts. `npx sendforai --help` lists every command. ### The same start in Python, no npm (`pip install cryptography`) ```python import base64, hashlib, json, time, urllib.request from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey API = "https://api.sendforai.com" # 1. A key, and the sandbox identity named after it. Save the PEM somewhere private. key = Ed25519PrivateKey.generate() pem = key.private_bytes(serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption()) pub = key.public_key().public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw) domain = "k" + hashlib.sha256(pub).hexdigest()[:24] + ".sandbox.sendforai.com" # 2. A connection token, signed locally. Nothing is registered anywhere first. b64u = lambda b: base64.urlsafe_b64encode(b).rstrip(b"=").decode() now = int(time.time()) payload = {"d": domain, "iat": now, "exp": now + 30 * 86400, "p": base64.b64encode(pub).decode()} signed = "sfat1." + b64u(json.dumps(payload).encode()) token = signed + "." + b64u(key.sign(signed.encode())) # 3. Send to the simulator. The first authenticated request creates the account. body = json.dumps({"from": f"agent@{domain}", "to": "delivered@simulator.sendforai.com", "subject": "Hello", "markdown": "It works."}).encode() req = urllib.request.Request(API + "/v1/emails", data=body, method="POST", headers={"Authorization": "Bearer " + token, "content-type": "application/json"}) print(json.load(urllib.request.urlopen(req))["status"]) # "sent" ``` Keep `pem` (the private key) and reuse it: the same key always gives the same sandbox identity and account. ## Ways to connect | You are | Use | |---|---| | A shell-driven agent | `npx sendforai ` | | Node code | `npm i @sendforai/sdk` | | An MCP client that can run a local process | `npx -y @sendforai/mcp` (see "MCP") | | An MCP client that connects to a URL | `https://sendforai.com/mcp` with a connection token (see "MCP") | | Any other language, curl, no-code tools | Plain HTTP with a connection token (see "Signing") | ## Sandbox A sandbox identity is named after its own key, so it needs no DNS: ``` k.sandbox.sendforai.com ``` - Sign requests exactly as for a domain (below), with that name as the domain. - On signed requests also send `SendForAI-Public-Key: `. Connection tokens for sandbox identities already carry the key. - Sends are accepted only to `@simulator.sendforai.com`. Anything else returns `409 sandbox_identity` with the steps to add a real domain. - Simulator addresses: `delivered@`, `bounced@`, `complained@` produce `email.delivery`, `email.bounce` and `email.complaint` events, the same names real mail produces. Any other local part is delivered. Simulated sends are free, never leave SendForAI, and are not capped. - Adding a real domain to a sandbox account keeps the same key, balance and contacts. ## Signing Two credentials are accepted. Both prove you hold the identity's private key. ### Option 1: connection token (simplest for curl and other languages) A token is minted locally with the private key. Nothing is stored on the server. It expires on its own (30 days by default, 90 at most) and stops working if the key is rotated. Treat it like a password. ```bash TOKEN=$(npx sendforai token --days 30 | jq -r .token) curl https://api.sendforai.com/v1/emails \ -H "Authorization: Bearer $TOKEN" -H "content-type: application/json" \ -d '{"from":"hi@example.com","to":"ada@example.org","subject":"Hello","markdown":"Hi.","max_cost_usd":0.01}' ``` Format, if you mint it yourself: `sfat1..` - `payload` = base64url (no padding) of the JSON `{"d":"","iat":,"exp":}`. Sandbox identities add `"p":""`. - `signature` = base64url (no padding) of the Ed25519 signature over the ASCII bytes of `sfat1.`. - `exp - iat` may be at most 7,776,000 seconds (90 days). Python (`pip install cryptography`): ```python import base64, json, time from cryptography.hazmat.primitives import serialization key = serialization.load_pem_private_key(open("sendforai.key", "rb").read(), password=None) b64u = lambda b: base64.urlsafe_b64encode(b).rstrip(b"=").decode() now = int(time.time()) signed = "sfat1." + b64u(json.dumps({"d": "example.com", "iat": now, "exp": now + 30 * 86400}).encode()) token = signed + "." + b64u(key.sign(signed.encode())) ``` ### Option 2: sign every request Send three headers: ``` SendForAI-Domain: example.com SendForAI-Timestamp: SendForAI-Signature: sfa1= ``` The signature is over this string, six lines joined by `\n`, no trailing newline: ``` sfa1 ``` Python: ```python import base64, hashlib, json, time, urllib.request from cryptography.hazmat.primitives import serialization key = serialization.load_pem_private_key(open("sendforai.key", "rb").read(), password=None) domain, method, path = "example.com", "POST", "/v1/emails" body = json.dumps({"from": "hi@example.com", "to": "ada@example.org", "subject": "Hello", "text": "Hi."}).encode() ts = str(int(time.time())) canonical = "\n".join(["sfa1", method, path, ts, domain, hashlib.sha256(body).hexdigest()]) sig = base64.b64encode(key.sign(canonical.encode())).decode() req = urllib.request.Request("https://api.sendforai.com" + path, data=body, method=method, headers={ "SendForAI-Domain": domain, "SendForAI-Timestamp": ts, "SendForAI-Signature": "sfa1=" + sig, "content-type": "application/json"}) print(urllib.request.urlopen(req).read().decode()) ``` Shell (OpenSSL 3 or newer): ```bash openssl genpkey -algorithm ed25519 -out sendforai.key PUB=$(openssl pkey -in sendforai.key -pubout -outform DER | tail -c 32 | base64) # goes in DNS DOMAIN=example.com; METHOD=GET; REQ_PATH=/v1/account; BODY='' TS=$(date +%s) HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | sed 's/^.*= //') printf 'sfa1\n%s\n%s\n%s\n%s\n%s' "$METHOD" "$REQ_PATH" "$TS" "$DOMAIN" "$HASH" > /tmp/sfa-canonical SIG=$(openssl pkeyutl -sign -inkey sendforai.key -rawin -in /tmp/sfa-canonical | base64 | tr -d '\n') curl "https://api.sendforai.com$REQ_PATH" \ -H "SendForAI-Domain: $DOMAIN" -H "SendForAI-Timestamp: $TS" -H "SendForAI-Signature: sfa1=$SIG" ``` Node: `new SendForAI({ domain, privateKey })` from `@sendforai/sdk` signs for you. `SendForAI.sandbox()` returns a ready client with a new sandbox identity. ## Domains ### Identity domain Publish the public key (raw 32 bytes, base64) as a TXT record. Whoever controls the domain's DNS controls the account. ``` _sendforai.example.com TXT "v=sfa1; k=ed25519; p=" ``` `GET /v1/domains/{domain}/setup?p=` (public) returns the exact record. `npx sendforai init example.com` generates the key and prints it. ### Sending records To send real mail, Amazon SES also needs DKIM and MAIL FROM records. `POST /v1/domains/{domain}/verify` creates the SES identity and returns them; `GET /v1/domains/{domain}` shows their status. Until SES has verified the domain, a send returns `409 domain_not_verified` listing the records still missing. ### Automatic DNS With a Vercel or Cloudflare token in the environment (`VERCEL_TOKEN` plus `VERCEL_TEAM_ID` for team domains, or `CLOUDFLARE_API_TOKEN`), one call publishes the identity or delegation record, the DKIM and MAIL FROM records, and a DMARC record if the domain has none. The token is used on your machine and is never sent to SendForAI. Safe to run again. ```bash npx sendforai dns setup --domain example.com --wait ``` SDK: `sfa.dns.setup({ provider: dnsProviderFromEnv(), waitForVerification: true })`. Local MCP: `setup_dns`. On any other DNS host, `GET /v1/domains/{domain}` lists the records for a person to add. ### More domains on one account (and graduating from a sandbox) One key can send for up to 50 domains. On each extra domain publish a delegation record pointing at your identity (a domain, or a sandbox name), then add it: ``` _sendforai.news.example.org TXT "v=sfa1; delegate=" ``` ```bash npx sendforai domains add news.example.org # POST /v1/domains {"name": "..."} npx sendforai domains verify news.example.org # DKIM + MAIL FROM records to publish ``` Subdomains of a domain you have are covered automatically. Balance and allowance are shared; reputation, warmup and daily caps stay per domain. ## Send an email `POST /v1/emails` | Field | Notes | |---|---| | `from` | An address on any of the account's domains. `"Name "` is allowed. | | `to` | One address or an array of up to 50. | | `subject` | Up to 998 characters. | | `markdown`, `html`, `text` | At least one. Markdown is rendered to HTML with a plain-text part. | | `reply_to`, `headers` | Optional. | | `stream` | `transactional` (default) or `broadcast` (adds one-click unsubscribe). | | `dry_run` | `true` plans the send: cost, capacity and lint warnings, nothing sent. | | `max_cost_usd` | The send is refused with 402 if it would bill more. | | `idempotency_key` | A retry with the same key returns the original result; nothing is sent twice. | Response (201): ```json { "id": "3f9c2a7e-...", "status": "sent", "recipients": [{ "to": "ada@example.org", "status": "accepted" }], "cost": { "emails": 1, "ses_usd": 0.0001, "billed_usd": 0.00011, "included_remaining": 9999, "note": "..." }, "capacity": { "daily_cap": 1000, "remaining_today": 999 }, "lint": [], "receipt": "" } ``` Addresses that previously bounced, complained or unsubscribed come back with status `suppressed` and are not sent to. A dry run of a send that would start a sending month shows `cost.month_fee_usd`. ## Money - $0 to build: unlimited simulated sends, and 25 real emails a day to addresses at your own domains. - The first send in a calendar month (UTC) to anyone outside your own domains starts a sending month: $5 from the prepaid balance, 10,000 emails included. Months with no outside sends cost $0. - After the included emails: $0.11 per 1,000. - `GET /v1/account` returns the balance, the budget used and remaining, usage, and every domain. Check it before a large send. - `POST /v1/account/topup {"amount_usd": 10, "auto_topup": true}` returns a `checkout_url`. Hand it to a person; this is the one step that needs a human. `account.topped_up` appears in the event log when they pay. - Usage charges in a month never exceed the budget ($20 by default). `POST /v1/account/budget {"monthly_usd": n}`: lowering applies immediately; raising returns an `approval_url` for a person. - `POST /v1/account/auto-topup {"enabled": false}` turns auto top-up off or changes its amount. `GET /v1/account/billing` returns a billing portal link for a person. ## Contacts and broadcasts - `POST /v1/contacts` upserts one contact by email (`data` is any JSON you can filter on; `subscribed` flag). `GET /v1/contacts` pages with `limit` and `cursor`. `POST /v1/contacts/unsubscribe`. `POST /v1/contacts/import` takes up to 10,000 per call in a sending month. Contacts are unlimited and free. - `POST /v1/broadcasts` creates a broadcast to every subscribed contact, or those matching `filter`. By default it is `awaiting_approval` and returns an `approval_url` for a person to click. Pass `require_approval: false` if your workflow has its own sign-off. - `POST /v1/broadcasts/{id}/send` starts an approved broadcast in the background (202). `GET /v1/broadcasts/{id}` shows status and `progress` (sent, queued, failed, cancelled, resumes_at). `POST /v1/broadcasts/{id}/cancel` stops it, also mid-send; the unsent part is refunded. ## Events `GET /v1/events?cursor=&limit=&type=` is a pullable log, so no webhook endpoint is needed. Keep `next_cursor` and pass it back to get only what is new. Types include `email.sent`, `email.delivery`, `email.bounce`, `email.complaint`, `email.failed`, `broadcast.awaiting_approval`, `broadcast.approved`, `broadcast.rejected`, `broadcast.sending`, `broadcast.deferred`, `broadcast.sent`, `broadcast.cancelled`, `account.month_started`, `account.topped_up`, `account.budget_changed`, `account.auto_topup_failed`, `domain.added`, `domain.paused`, `contacts.imported` and `contact.unsubscribed`. ## Capacity and warmup `GET /v1/capacity` returns today's cap, what has been sent, what remains and when it resets. `GET /v1/usage` returns the month so far. New sending domains start with a daily cap that rises in stages while bounce and complaint rates stay low. A domain, or a whole account, whose rates get too high is paused, and the error says how to clean the list. ## Errors Every error has the same shape, and `fix` is written for an agent to act on: ```json { "error": { "code": "balance_required", "message": "...", "fix": "...", "details": {}, "docs": "..." } } ``` | Status and code | What to do | |---|---| | 401 `unauthenticated` | Sign the request or send a valid token. `details` says what was wrong (clock, key mismatch, missing DNS record). | | 400 `bad_request` | Fix the fields in `details.issues`. If `from` is not on your domains, `details.record` is the delegation record to publish. | | 402 `balance_required` | `POST /v1/account/topup` and hand `checkout_url` to your operator. Simulator and own-domain sends keep working. | | 402 `budget_exceeded` | Send less, wait for next month, or `POST /v1/account/budget` for an approval link. Also returned when a send exceeds `max_cost_usd`. | | 409 `sandbox_identity` | Sandbox identities only reach the simulator. Add a real domain. | | 409 `domain_not_verified` | Publish the records in `details.records`, then verify again. | | 409 (other codes) | The broadcast needs approval, a domain is taken, or the domain limit is reached; the `fix` says which. | | 422 `send_rejected` | Amazon SES refused every recipient. Nothing was sent or billed. `details.recipients` has the reason per address. | | 429 `capacity_exceeded` | Today's cap is used. `details.resets_at` says when it resets. | | 404 `not_found` | Unknown endpoint or id. | ## MCP Hosted (nothing to install; Streamable HTTP): ```json { "mcpServers": { "sendforai": { "url": "https://sendforai.com/mcp", "headers": { "Authorization": "Bearer " } } } } ``` Claude Code: `claude mcp add --transport http sendforai https://sendforai.com/mcp --header "Authorization: Bearer "` Get the token with `npx sendforai token`. The hosted server authenticates with that bearer token, not OAuth; a client that only supports OAuth connectors should use the local server. `npx sendforai mcp` prints both configs filled in. Local (holds the key itself, and adds `generate_identity`, `setup_dns` and `create_connection_token`): ```json { "mcpServers": { "sendforai": { "command": "npx", "args": ["-y", "@sendforai/mcp"], "env": { "SENDFORAI_DOMAIN": "example.com", "SENDFORAI_PRIVATE_KEY_FILE": "/path/to/sendforai.key" } } } } ``` No identity yet? Start the local server without env and call `generate_identity` (omit `domain` for a sandbox identity). Tools: `whoami`, `get_account`, `get_pricing`, `get_capacity`, `get_usage`, `plan_email`, `send_email`, `list_domains`, `add_domain`, `get_domain_status`, `verify_domain`, `upsert_contact`, `list_contacts`, `import_contacts`, `create_broadcast`, `get_broadcast`, `send_broadcast`, `cancel_broadcast`, `add_credit`, `set_budget`, `set_auto_topup`, `billing_portal`, `list_events`. ## Claude Code plugin The plugin adds the SendForAI skill (the whole set-up-and-send workflow) and the local MCP server: ``` /plugin marketplace add https://sendforai.com/marketplace.json /plugin install sendforai@sendforai ``` From a shell: `claude plugin install sendforai --marketplace https://sendforai.com/marketplace.json`. The skill on its own is at https://sendforai.com/skill.md. ## Limits worth knowing - SendForAI sends email; it does not receive it. There are no inboxes. - The SDK is Node only. Other languages use plain HTTP with a connection token. - New sending domains warm up, so a new domain cannot take a large volume on day one. - Open and click tracking are not offered. ## Endpoint list | Endpoint | Auth | What it does | |---|---|---| | `GET /v1` | public | Index of docs, spec, MCP and status. | | `GET /v1/pricing` | public | Pricing as JSON. | | `GET /v1/status` | public | Health check. | | `GET /v1/domains/{domain}/setup?p=` | public | The identity TXT record for a public key. | | `POST /v1/emails` | yes | Send, or plan with `dry_run`. | | `GET /v1/account` | yes | Balance, budget, usage, domains. | | `POST /v1/account/topup` | yes | Checkout link for a person to add credit. | | `POST /v1/account/budget` | yes | Lower the budget, or get an approval link to raise it. | | `POST /v1/account/auto-topup` | yes | Turn auto top-up off or change its amount. | | `GET /v1/account/billing` | yes | Billing portal link for a person. | | `GET /v1/domains`, `POST /v1/domains` | yes | List and add sending domains. | | `GET /v1/domains/{domain}` | yes | Identity and SES status, records to publish. | | `POST /v1/domains/{domain}/verify` | yes | Create the SES identity; returns DKIM and MAIL FROM records. | | `GET /v1/capacity`, `GET /v1/usage` | yes | Today's cap; month to date. | | `POST /v1/contacts`, `GET /v1/contacts` | yes | Upsert and list contacts. | | `POST /v1/contacts/import`, `POST /v1/contacts/unsubscribe` | yes | Bulk import; unsubscribe. | | `POST /v1/broadcasts`, `GET /v1/broadcasts/{id}` | yes | Create and read a broadcast. | | `POST /v1/broadcasts/{id}/send`, `POST /v1/broadcasts/{id}/cancel` | yes | Start and cancel. | | `GET /v1/events` | yes | Event log. | Questions: hello@sendforai.com. Abuse reports: abuse@sendforai.com. --- # SendForAI pricing Updated 2026-11-05. Machine-readable: GET https://api.sendforai.com/v1/pricing. Human page: https://sendforai.com/pricing. **$0 to build. $5 in months you send. Then at cost.** | | Price | What you get | |---|---|---| | Building | $0, forever | Unlimited simulated sends to @simulator.sendforai.com; 25 real emails a day to addresses at your own domains; up to 50 sending domains; full API, CLI, SDK and MCP. | | A sending month | $5 | Charged the first time in a calendar month (UTC) you send to anyone outside your own domains. Includes 10,000 emails that month. Months with no outside sends cost $0. | | Beyond the included emails | $0.11 per 1,000 | Published cost: Amazon SES $0.100 + SES tenant $0.005 + card processing ~$0.004 + rounding $0.001. | | Over 1,000,000 emails a month | $24.95/mo + $0.02 per 1,000 over 1M | A dedicated IP at AWS's price and measured infrastructure cost, both passed through. | Contacts are unlimited and free. There is no per-contact or per-domain fee. ## What a month costs | Emails sent to the outside world | Total | |---|---| | 0 | $0.00 | | 1 to 10,000 | $5.00 | | 50,000 | $9.40 | | 100,000 | $14.90 | | 500,000 | $58.90 | | 1,000,000 | $113.90 | ## How paying works - Everything is paid from a prepaid balance. Minimum top-up $10. - An agent calls POST /v1/account/topup and hands the returned checkout_url to a person. That is the one step that needs a human. - Optional auto top-up refills the balance from the saved card ($10 by default) when a send would not fit. - Usage charges in a month never exceed a budget ($20 by default). An agent can lower it; only a person can raise it. - Every send response reports what it cost. dry_run shows the cost first; max_cost_usd refuses a send that would cost more. 0% on sending. The $5 sending-month fee is the only place we make money; every email beyond the included 10,000 is billed at published cost. --- --- name: sendforai description: Set up and send transactional or broadcast email with SendForAI (sendforai.com), an email API an agent can sign up for and run without a dashboard or API key. Use when a project needs to send email (sign-in links, receipts, password resets, notifications, newsletters), when asked to add or replace an email provider such as Resend, SendGrid, Postmark or Mailgun with a cheaper one, or when the user mentions SendForAI. --- # SendForAI Email at cost for agents: $0 to build, $5 in months that send to the outside world (10,000 emails included), then $0.11 per 1,000. Send-only (no inbound). Full reference: https://sendforai.com/docs.md. Short reference: https://sendforai.com/llms.txt. Use the CLI (`npx sendforai`, prints JSON, never prompts), the Node SDK (`@sendforai/sdk`), the MCP tools if a `sendforai` MCP server is connected, or plain HTTP with a connection token. All of them return the same JSON. ## Workflow 1. **Get an identity.** No API key exists; the identity is a local Ed25519 key. - No domain decided yet: `npx sendforai sandbox`. Works immediately, reaches only the simulator. - Domain known: `npx sendforai init example.com`. It prints the DNS TXT record that binds the key to the domain. - Keys are saved under `~/.sendforai/` with mode 0600. Never print, commit or paste a private key. In an app, load it from an environment variable or secret store (`SENDFORAI_PRIVATE_KEY`). 2. **Prove it works against the simulator** before touching DNS or money: `npx sendforai send --from agent@ --to delivered@simulator.sendforai.com --subject Test --markdown "Hello"`. `bounced@` and `complained@simulator.sendforai.com` simulate those outcomes; check them with `npx sendforai events`. 3. **Set up the real domain.** - If `VERCEL_TOKEN` or `CLOUDFLARE_API_TOKEN` is in the environment: `npx sendforai dns setup --domain example.com --wait`. It publishes every record and waits. The token stays local. - Otherwise run `npx sendforai domains status example.com` (after `init` or `domains add`) and give the user the exact records to add at their DNS host. Do not guess records. - From a sandbox identity, the real domain is added by delegation; `dns setup --domain` and `domains add` both handle it and keep the same account. 4. **Check cost before sending for real.** `--dry-run` (API: `"dry_run": true`) returns cost, capacity and lint warnings without sending. Always pass `--max-cost` (API: `max_cost_usd`) on real sends so a mistake cannot overspend. 5. **Hand money to the human.** Sending to anyone outside the account's own domains needs prepaid credit. On `402 balance_required`, run `npx sendforai topup --amount 10` and give the user the `checkout_url`. Do not try to pay it yourself. Raising the monthly budget works the same way (`approval_url`). 6. **Send.** Use `idempotency_key` on anything that might be retried. Read `cost` and `capacity` from the response instead of assuming. 7. **Broadcasts need a person.** `npx sendforai broadcasts create ...` returns an `approval_url`; give it to the user and wait. Only pass `require_approval: false` when the user has explicitly said they approve sends themselves. 8. **Watch results** with `npx sendforai events` (keep the cursor). No webhook endpoint is needed. ## Integrating into an app (Node) ```ts import { SendForAI } from "@sendforai/sdk"; const sfa = new SendForAI({ domain: process.env.SENDFORAI_DOMAIN!, privateKey: process.env.SENDFORAI_PRIVATE_KEY! }); const res = await sfa.emails.send({ from: "login@example.com", to: user.email, subject: "Your sign-in link", markdown: `Click to sign in: ${url}`, idempotency_key: `signin-${tokenId}`, max_cost_usd: 0.01, }); ``` Other languages: mint a connection token (`npx sendforai token --days 90`), store it as a secret, and `POST https://api.sendforai.com/v1/emails` with `Authorization: Bearer `. Tokens expire (90 days at most), so tell the user a renewal is needed, or sign each request instead (recipe in docs.md, "Signing"). ## Rules - Every error has a `fix` field. Read it and do what it says before trying anything else. - Never send to purchased, scraped or guessed lists. Transactional mail goes to people who triggered it; broadcasts go to contacts who opted in. Policy: https://sendforai.com/acceptable-use. - Keep transactional and broadcast mail on their own streams (`stream: "broadcast"` for list mail; it adds one-click unsubscribe). - New domains warm up from a low daily cap. Check `npx sendforai capacity` before promising a volume, and tell the user if a migration has to be gradual. - When replacing another provider, switch one email type first, behind a flag, and keep the old provider until real sends are confirmed in `events`. - State plainly what still needs the human: DNS records (unless a DNS token is available), the first top-up, budget increases, and broadcast approval.