# YAP mail API, for agents

The mail API YAP's own Mail screen uses, opened to an agent that holds a mail
token. It reaches the mailboxes the token's owner can open in YAP (today
**FERAL Presents**, `bookings@feralpresents.com`, and **FUSE**,
`bookings@fuseagency.io`, with its folders such as Advancing and Invoicing),
and nothing else: no chat, no settings.

- Base address: `https://mailapi.fuseagency.io`
- This document: `GET /`
- Every request: `Authorization: Bearer <token>` (the token starts `yap_`)
- Which mailbox: `X-Yap-Account: bookings@fuseagency.io` (or `?account=` on a
  URL). Leave it out and you get the first mailbox, FERAL Presents.
- Answers are JSON. An error is `{ "error": "<a sentence>" }` with a status
  code: 400 a bad request, 401 the token is wrong or was revoked, 403 not
  allowed (the sentence says why), 404 not found, 410 gone, 413 too big, 429
  slow down (see `Retry-After`).

Every message an agent sends is marked in YAP as sent by the agent (with the
token's name, "Clear agents"), so the people sharing the mailbox can see it.
The person receiving the email does not see that mark: to them it is from the
mailbox, under the token owner's name, like any other reply from the team.

```sh
T='yap_…'                                   # your token
A='X-Yap-Account: bookings@fuseagency.io'   # which mailbox
API=https://mailapi.fuseagency.io
curl -s "$API/api/accounts" -H "Authorization: Bearer $T"
```

## Reading

| what | request |
|---|---|
| the mailboxes you can open | `GET /api/accounts` |
| folders, with unread and total counts | `GET /api/folders` |
| a page of a folder, newest first | `GET /api/list?mailbox=INBOX&limit=50` · next page: `&beforeUid=<the last uid you got>` |
| search one folder | `GET /api/search?mailbox=INBOX&q=hemkade` |
| search every folder | `GET /api/search?scope=all&q=hemkade` · also `unseen=1`, `flagged=1`, `hasAttachment=1`, `label=<name>`, `limit=` |
| one message (HTML, text, attachments) | `GET /api/read?mailbox=INBOX&uid=1234` · add `&peek=1` to leave it unread |
| its whole conversation | `GET /api/thread?mailbox=INBOX&uid=1234` |
| an attachment, byte for byte | `GET /api/attachment?mailbox=INBOX&uid=1234&index=0` |
| the raw email (RFC 822), byte for byte | `GET /api/source?mailbox=INBOX&uid=1234` |
| addresses the mailbox has written to | `GET /api/contacts?q=ann` |
| the status labels the mailbox offers | `GET /api/labels` |
| live: tell me when a folder changes | `GET /api/events?mailbox=INBOX` (server-sent events; at most 2 open per token) |

A message is named by its folder and its `uid`. A uid belongs to a folder: after
a move, the message has a new uid in its new folder.

`/api/read` answers `{ "message": { … } }`. In it:

- `html` is the email made safe to show, `text` the plain text, `quoted` the
  history below the reply, `messageId`, `inReplyTo` and `references` for
  replying.
- `attachments` is a list of `{ index, filename, size, type, isImage, cid }`.
  Download any of them with `/api/attachment` and its `index`: the bytes are
  exactly the bytes in the email, whatever the type or the file name.
- **Pictures in the body.** An inline picture is an attachment with a `cid`.
  In `html` its `src` has been pointed at `/api/attachment`, so fetch it from
  there with your token. Use `/api/source` when you need the email exactly as it
  arrived, `cid:` links and all.
- Pictures from other websites are not loaded for senders the mailbox does not
  know (`blockedImages: true`). `&images=1` loads them.
- `sentBy`, on our own messages, says who on our side sent it:
  `{ "kind": "person", "name": "Raul Hansma", … }` or
  `{ "kind": "agent", "name": "Clear agents", "owner": "Raul Hansma" }`.
  Absent means YAP did not send it (Zoho's webmail, a phone, or before
  16 Oct 2026).

## Sending

`POST /api/send` with a JSON body. It answers when the mail server has taken the
message: `{ "ok": true, "messageId": "<…>", "accepted": […], "rejected": […] }`.

```sh
curl -s "$API/api/send" -H "Authorization: Bearer $T" -H "$A" -H 'Content-Type: application/json' -d '{
  "to": "promoter@example.com",
  "cc": "", "bcc": "",
  "from": "advancing@fuseagency.io",
  "subject": "Advancing: Hemkade 15 May",
  "text": "Hi Sam,\n\nThe rider is attached.\n",
  "html": "<p>Hi Sam,</p><p>The rider is attached.</p>",
  "signature": "none",
  "attachments": [ { "stagedId": "…" } ]
}'
```

| field | |
|---|---|
| `to`, `cc`, `bcc` | addresses, comma separated (`"Name" <a@b.c>` is fine) |
| `from` | the mailbox's own address or one of its aliases (FUSE: `bookings@`, `advancing@`, `invoicing@fuseagency.io`). Anything else is refused. Leave it out for the mailbox's default. |
| `subject`, `text` | plain text. Give `text` even when you give `html`, saying the same thing: it is what a phone preview and a plain-text reader show. |
| `html` | optional. Sent as you give it. |
| `signature` | optional. `"none"`, or an id from `GET /api/signatures`. Leave it out and the token owner's own signature is added as it would be for them. |
| `inReplyTo`, `references` | to reply in the conversation: the original's `messageId`, and its `references` with its `messageId` added. Reply-all is the same with every address in `to`/`cc`. Add `inReplyToMailbox` and `inReplyToUid` and the original is marked as answered. |
| `attachments` | up to 25 files, 25 MB in all. Each one is one of the three below. |
| `draftUid` | the draft this message was, if it was one: it is removed from Drafts once sent. |

Each attachment is one of:

- `{ "stagedId": "<id>" }`, a file you uploaded first with `POST /api/stage`
  (best for anything over a few hundred KB);
- `{ "source": { "mailbox": "INBOX", "uid": 1234, "index": 0 }, "filename": "…" }`,
  a file already on a message in the mailbox (what a forward uses: the bytes are
  copied server-side, you never download them);
- `{ "filename": "logo.png", "contentType": "image/png", "contentBase64": "…" }`.

Add `"cid": "logo@fuse"` to any of them to make it a **picture in the body**: write
`<img src="cid:logo@fuse">` in `html` and the picture shows there instead of as
an attachment. To forward an email with its pictures, use each picture's
`source` and its `cid` from `/api/read`, and in your `html` write
`cid:<that cid>` where the picture goes.

**Uploading a file first:**

```sh
curl -s "$API/api/stage" -H "Authorization: Bearer $T" -F "file=@rider.pdf"
# → { "staged": { "id": "…", "filename": "rider.pdf", "size": 182044, "contentType": "application/pdf" } }
```

A staged file is kept for 12 hours or until the message carrying it is sent.
`GET /api/stage` lists yours, `DELETE /api/stage` with `{ "id": "…" }` drops one.

**Drafts.** `PUT /api/send` with the same body saves a draft instead of sending
(`{ "uid": … }` comes back; send `draftUid` with the next save to replace it).
`DELETE /api/draft` with `{ "uid": … }` discards one. A person can open and
finish the draft in YAP.

**Undo.** A send with `"holdMs": 5000` waits five seconds first; `DELETE
/api/send` with `{ "queuedId": "…" }` stops it and `GET /api/send?queuedId=…`
says what happened. Leave `holdMs` out and it goes at once.

## Filing, labels, read and unread

`POST /api/act`, JSON:

| what | body |
|---|---|
| read · unread · flag · unflag | `{ "mailbox": "INBOX", "uids": [1234, 1235], "action": "read" }` |
| archive · trash · junk · notjunk | same, `"action": "archive"` |
| move to a folder | `{ "mailbox": "INBOX", "uids": [1234], "action": "move", "dest": "FUSE artists/2027/05 May/15 Hemkade" }` |
| file a whole conversation | `{ "action": "file", "dest": "…", "items": [ { "mailbox": "INBOX", "uid": 1234 }, { "mailbox": "Sent", "uid": 88 } ], "clear": true }` (sent copies are copied, the rest moved; `clear` takes the status labels off) |
| a status label on or off | `{ "mailbox": "INBOX", "uids": [1234], "action": "label", "label": "Waiting for reply", "on": true }` |

Folders: `POST /api/folders` `{ "path": "A/B" }` makes one (and its parents),
`PATCH /api/folders` `{ "from": "A/B", "to": "A/C" }` renames, `DELETE
/api/folders` `{ "path": "A/C" }` removes an empty one. `POST /api/mailbox`
`{ "op": "markAllRead", "mailbox": "INBOX" }` marks a folder read.

## What an agent cannot do

So that an agent's mistake is always one a person can put right, a mail token
cannot: delete a message for good (move it to Trash instead), empty the Trash,
change the mailbox's labels or shared signatures, or change anybody's saved
replies. Those answer 403 with a sentence. People do them in YAP.

## Limits

- 1,500 requests per 10 minutes per token, 8 at once, 2 event streams.
- 150 sends an hour per token. Over it, nothing is sent and you get 429.
- 40 MB a request; 25 MB of attachments a message.
- Presenting a token that does not work again and again from one address makes
  that address wait 15 minutes.

## The token

The token is a password for the mailboxes. Keep it in a secret store, never in
code, a prompt or a log. Its owner (or Harry) can revoke it in YAP at any time,
and a new one can be made the same day; a revoked token answers 401 everywhere.
