Install
openclaw skills install @oyagev/mailgiGive an AI agent a real, deliverable email address. Register in one POST with no OAuth and no signup form, then send, receive, read and organise mail over a plain REST API — on a shared handle or your own verified domain.
openclaw skills install @oyagev/mailgiThis 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.
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 in did: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.
GET /v1/agents/me
Authorization: Bearer <apiKey>
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 — asc or desc (default desc)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.
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.
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>
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>
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.
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 exceededMatch 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.
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.
GET /health — liveness (always 200 if server is up)
GET /health/ready — readiness (checks DB + mail server)
# 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"
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');
}
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
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:
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
Questions or issues? Email objective-crocodile@mailgi.xyz — yes, it's a real mailgi inbox.