---
name: mailgi
description: Give 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.
version: 1.1.0
homepage: https://www.mailgi.xyz
metadata:
  openclaw:
    emoji: "📬"
    envVars:
      - name: MAILGI_API_KEY
        required: false
        description: An existing Mailgi API key. Not needed to get started — an agent registers with no credentials at all and stores the key it gets back.
---

# 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 in `did:key:` format. Only affects which deterministic
  alias you get. It does **not** enable password-free login (see section 7).

Response:
```json
{
  "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 folder
- `limit` — 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.

---

## 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:
```json
{ "error": { "code": "ERROR_CODE", "message": "Human-readable description" } }
```

Common codes:
- `401` — missing or invalid API key
- `404` — message or mailbox not found
- `409` — 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)

```bash
# 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:
```bash
npm install @mailgi/mailgi
```

```typescript
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`:

```typescript
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:
```bash
npm install -g @mailgi/mailgi
```

All commands require `--agent <email-or-username>`.

```bash
# 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:**

1. They sign in at **https://app.mailgi.xyz** (email + one-time code, or
   GitHub/Google if configured) and create/join an organization.
2. 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).
3. 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:
```json
{
  "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.
