mailgi — SKILL FILE
This file teaches you how to use the mailgi email API. You are an AI agent. Read this file, then you can send and receive email.
Skill version 1.1.0 · updated 2026-08-30 Canonical copy: https://www.mailgi.xyz/SKILL.md — if yours is older, re-fetch it. This file tracks the live API; a stale copy will describe endpoints that changed.
Base URL: https://api.mailgi.xyz
Auth: Authorization: Bearer <apiKey> on all authenticated requests.
1. Get an email address
Register once. No password, no OAuth.
POST /v1/agents/register
Content-Type: application/json
{ "label": "my-agent" }
Optional fields:
label— a name for your own reference; shows up in the owner's dashboard.did— a W3C DID indid:key:format. Only affects which deterministic alias you get. It does not enable password-free login (see section 7).
Response:
{
"agentId": "clxxx...",
"emailAddress": "buzzing-falcon@mailgi.xyz",
"aliasAddress": "x7k3mwf2qr5b@mailgi.xyz",
"apiKey": "amb_...",
"apiKeyId": "clyyy..."
}
Store apiKey immediately. It is shown exactly once.
emailAddress is your friendly address. Use it for sending and tell others to send to it.
aliasAddress is a deterministic alias — both receive mail to the same inbox.
2. Check your profile
GET /v1/agents/me
Authorization: Bearer <apiKey>
3. Read your inbox
GET /v1/mail
Authorization: Bearer <apiKey>
Optional query params:
mailboxId— restrict to one folderlimit— max results (default 20, max 100)position— pagination offset (default 0)sort—ascordesc(defaultdesc)
There is no search. No full-text, subject, sender or date filtering exists — any other query param is ignored, not rejected. To find a message, page through the list and match it yourself.
Response: { messages: [...], total: N, position: N }
Each message has: id, subject, from, to, receivedAt, preview, seen.
There is no push delivery. No webhooks, no WebSocket, no long-poll. The only
way to learn about new mail is to call GET /v1/mail again. Poll every few
seconds when waiting for something specific (a verification code), and every
minute or two for a background inbox.
Get full body of a message:
GET /v1/mail/<id>
Authorization: Bearer <apiKey>
Response includes htmlBody and/or textBody. If body is a string, use it directly.
If it is an array of JMAP parts, look up bodyValues[part.partId].value for the text.
4. Send email
POST /v1/mail/send
Authorization: Bearer <apiKey>
Content-Type: application/json
{
"to": ["someone@example.com"],
"subject": "Hello from my agent",
"textBody": "Hi there."
}
Optional fields: cc, bcc, htmlBody, replyTo.
to, cc, bcc accept a single string or an array of strings.
Response: { "messageId": "..." }
Sending is free. Rate limit: 100 external emails per day per API key, plus 50/hour and 300/day per agent across all its keys. On a custom domain your organisation is also capped by domain age: 100/day under three days, 1000/day up to thirty, 5000/day after that.
Attachments are not supported. There is no attachments field — sending one
does nothing. Send links instead.
Sending right after registering can fail. The mailbox is provisioned
asynchronously; wait a couple of seconds after register before your first
send, and retry once on a 5xx.
5. Manage mailboxes (folders)
List folders:
GET /v1/mailboxes
Authorization: Bearer <apiKey>
Each mailbox has id, name, role (inbox/sent/trash/drafts/etc), totalEmails, unreadEmails.
Create a folder:
POST /v1/mailboxes
Authorization: Bearer <apiKey>
Content-Type: application/json
{ "name": "Projects", "parentId": "<optional parent id>" }
Move a message to a folder:
PATCH /v1/mail/<id>/move
Authorization: Bearer <apiKey>
Content-Type: application/json
{ "mailboxId": "<folder id>" }
Mark as read:
PATCH /v1/mail/<id>/flags
Authorization: Bearer <apiKey>
Content-Type: application/json
{ "seen": true }
Rename a folder:
PATCH /v1/mailboxes/<id>
Authorization: Bearer <apiKey>
Content-Type: application/json
{ "name": "Archive" }
Delete a folder:
DELETE /v1/mailboxes/<id>
Authorization: Bearer <apiKey>
Delete a message (moves to Trash):
DELETE /v1/mail/<id>
Authorization: Bearer <apiKey>
6. API keys
You can create additional API keys (e.g. one per task):
POST /v1/apikeys
Authorization: Bearer <apiKey>
Content-Type: application/json
{ "label": "task-runner", "expiresAt": "2026-12-31T00:00:00Z" }
Response includes apiKey (raw, shown once) and id.
List keys: GET /v1/apikeys
Revoke a key: DELETE /v1/apikeys/<keyId>
7. DID-based auth — NOT AVAILABLE YET
Do not use this. Use your API key.
POST /v1/auth/challenge and POST /v1/auth/verify exist and will return a
signed token, but no endpoint accepts that token yet — authentication is
API-key-only today, so the token returns 401 everywhere. Registering with a
did:key: DID is supported and does determine your alias address; the
challenge/response login it implies does not work.
This section will describe the real flow once it does.
8. Error responses
All errors follow:
{ "error": { "code": "ERROR_CODE", "message": "Human-readable description" } }
Common codes:
401— missing or invalid API key404— message or mailbox not found409— conflict (e.g. mailbox name already exists)RATE_LIMITED— rate limit exceeded
Match on error.code, not on the HTTP status. Rate limiting currently
returns 429 on some paths and 500 on others; the code is reliable where
the status is not.
9. Deleting your account
DELETE /v1/agents/me
Authorization: Bearer <apiKey>
Revokes every API key and removes the mailbox. Returns 204.
This is permanent, and it burns the address. A deleted address can never be registered again — not by you, not by anyone. There is no undo and no support path to reverse it. Do not call this to "reset" or "start clean": register a second agent instead and simply stop using the first.
10. Health
GET /health — liveness (always 200 if server is up)
GET /health/ready — readiness (checks DB + mail server)
Quick start (copy-paste)
# 1. Register
RESP=$(curl -s -X POST https://api.mailgi.xyz/v1/agents/register \
-H "Content-Type: application/json" \
-d '{"label":"my-agent"}')
EMAIL=$(echo $RESP | jq -r .emailAddress)
KEY=$(echo $RESP | jq -r .apiKey)
# 2. Send a message.
# Give the mailbox a moment first — sending in the same breath as registering
# can 500 while the mail server finishes provisioning. Retry once if it does.
sleep 3
curl -s -X POST https://api.mailgi.xyz/v1/mail/send \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d "{\"to\":[\"someone@example.com\"],\"subject\":\"Hi\",\"textBody\":\"Hello from $EMAIL\"}"
# 3. Read inbox
curl -s https://api.mailgi.xyz/v1/mail \
-H "Authorization: Bearer $KEY"
11. TypeScript / Node.js SDK
Install:
npm install @mailgi/mailgi
import { AgentMailboxClient } from '@mailgi/mailgi';
// Construct from a stored API key
const client = AgentMailboxClient.withApiKey(
'https://api.mailgi.xyz',
process.env.MAILGI_API_KEY!,
);
// Register a new agent (first time only — save the returned apiKey)
const reg = await client.agents.register({ label: 'my-agent' });
// reg.emailAddress => 'buzzing-falcon@mailgi.xyz'
// reg.apiKey => 'amb_...' (shown once — store it)
client.apiKey = reg.apiKey;
// Send email
const { messageId } = await client.mail.send({
to: ['someone@example.com'],
subject: 'Hello',
textBody: 'Hi from my agent.',
});
// Read inbox
const { messages } = await client.mail.list({ limit: 20, sort: 'desc' });
const email = await client.mail.get(messages[0].id);
console.log(email.subject, email.textBody);
// Mark as read
await client.mail.setFlags(email.id, { seen: true });
All SDK methods map 1-to-1 to the REST endpoints above. Errors extend AgentMailboxError with statusCode and code:
import { NotFoundError, UnauthorizedError } from '@mailgi/mailgi';
try {
await client.mail.get('bad-id');
} catch (err) {
if (err instanceof NotFoundError) console.error('Not found');
if (err instanceof UnauthorizedError) console.error('Bad API key');
}
12. CLI
Install globally:
npm install -g @mailgi/mailgi
All commands require --agent <email-or-username>.
# Register a new agent (saves API key to ~/.mailgi/config.json)
mailgi register --label my-agent --agent me@mailgi.xyz
# Save an existing agent by API key
mailgi login --agent buzzing-falcon@mailgi.xyz --apikey amb_...
# List saved agents
mailgi agents
# Show agent profile (live from API)
mailgi me --agent buzzing-falcon
# Read inbox
mailgi inbox --agent buzzing-falcon
mailgi inbox --agent buzzing-falcon --limit 50
# Read a message (auto-marks as seen)
mailgi read --agent buzzing-falcon <message-id>
# Send email
mailgi send --agent buzzing-falcon --to alice@example.com --subject "Hi" --body "Hello"
mailgi send --agent buzzing-falcon --to alice@example.com --subject "Hi" --body-file ./message.txt
# Delete a message
mailgi delete --agent buzzing-falcon <message-id>
# Mailboxes
mailgi mailboxes --agent buzzing-falcon
mailgi mailboxes create "Projects" --agent buzzing-falcon
mailgi mailboxes delete <id> --agent buzzing-falcon
# API keys
mailgi keys --agent buzzing-falcon
mailgi keys create --label task-key --agent buzzing-falcon
mailgi keys revoke <key-id> --agent buzzing-falcon
# Config
mailgi config show
mailgi config set-url https://api.mailgi.xyz
# Remove saved agent
mailgi logout --agent buzzing-falcon --yes
# Raw JSON output (any command)
mailgi inbox --agent buzzing-falcon --json
13. Custom domains & registration tokens
Everything above registers your agent on the shared @mailgi.xyz domain.
If a human wants their agents to send as you@theircompany.com instead,
that's a custom domain — a separate, dashboard-driven feature, not
something an agent sets up for itself.
This part is done by a human, in a browser, not by an API call:
- They sign in at https://app.mailgi.xyz (email + one-time code, or GitHub/Google if configured) and create/join an organization.
- They attach their domain and add the DNS records the dashboard shows them (TXT + MX for inbound; once that's verified, DKIM/MAIL-FROM/DMARC records appear for outbound sending — all auto-generated, nothing to configure by hand).
- Once the domain shows verified, they create a registration token
for it (
POST /v1/orgs/:orgId/domains/:domainId/registration-tokens, requires their dashboard session — not an API key). This returns a token string shown exactly once, meant to be handed to an agent.
This part is you, the agent — once you're given that token and a
local part to claim (e.g. "register as support on this token"):
POST /v1/agents/register
Content-Type: application/json
{ "domainToken": "<token from the human>", "localPart": "support" }
Response is the same shape as a normal registration, except
emailAddress is now support@theircompany.com instead of a random
@mailgi.xyz handle:
{
"agentId": "...",
"emailAddress": "support@theircompany.com",
"aliasAddress": "x7k3mwf2qr5b@theircompany.com",
"apiKey": "amb_...",
"apiKeyId": "..."
}
Everything from section 2 onward (profile, inbox, send, mailboxes, API
keys) works exactly the same afterward — the only difference custom
domains make is which address you register with. localPart must be
lowercase alphanumeric (./_/- allowed as separators), 1–64 chars,
and can't be a reserved name (postmaster, abuse, etc.) or already
taken on that domain.
A registration token can mint many agents on the same domain — a human
might hand you one token and ask you to self-register several teammates
(support, sales, billing, ...) in one go.
If the token is invalid, revoked, or the domain isn't verified yet, you
get back a generic 401 Invalid or unusable registration token —
deliberately the same error for all three cases, so tell the human to
check the dashboard rather than guessing which one it is.
Full interactive docs: https://api.mailgi.xyz/docs Machine-readable spec: https://api.mailgi.xyz/openapi.json
Support
Questions or issues? Email objective-crocodile@mailgi.xyz — yes, it's a real mailgi inbox.
