# Common tasks

> Read a conversation, search, mark as read, send, prepare a draft for review, reply in a thread and let Maylee classify email, with copyable requests.

Source: https://docs.maylee.app/common-tasks

Every example assumes a key in `$MAYLEE_KEY` (see [Authentication](/authentication)) and a mailbox id from [`GET /mailboxes`](/api-reference/mailboxes/list-mailboxes).

## Read an email and its conversation

A list gives metadata only: the body costs a round trip to storage, so it is fetched on demand.

```bash
curl https://api.maylee.app/api/v1/emails/cmemx1a2b3c4d5e6f7g8h9i0/body \
  -H "Authorization: Bearer $MAYLEE_KEY"
```

```json
{
  "emailId": "cmemx1a2b3c4d5e6f7g8h9i0",
  "html": "<p>Hello, could you send us a quote for...</p>",
  "text": "Hello, could you send us a quote for...",
  "thread": [
    {
      "id": "cmemx1a2b3c4d5e6f7g8h9i0",
      "messageId": "<CAF3k9x1@mail.example.com>",
      "references": [],
      "fromAddress": "client@acme.com",
      "fromName": "Jane Client",
      "toAddresses": [{ "email": "contact@your-company.com" }],
      "date": "2026-08-25T13:47:02.000Z",
      "subject": "Quote request",
      "html": "<p>Hello, could you send us a quote for...</p>",
      "text": "Hello, could you send us a quote for...",
      "isMe": false
    }
  ]
}
```

`html` and `text` are the body of the email you asked for. `thread` holds every message of the conversation, oldest first, including your own replies (`isMe: true`) and any draft in progress.

## Search

```bash
curl -G https://api.maylee.app/api/v1/emails/search \
  -H "Authorization: Bearer $MAYLEE_KEY" \
  --data-urlencode 'q=quote'
```

The term (2 to 200 characters) is matched as one piece of text, ignoring case, against the subject, the sender name and address, the preview snippet and the To recipients. Full bodies and Cc recipients are not searched. Results are ranked by relevance, subject matches first.

The search spans **all folders**: whoever looks for an invoice does not know whether it is still in the inbox. Add `folderType=SENT` to narrow it down. For structured criteria (sender, labels, dates, read state), use [filters](/filters) on `GET /emails`.

## Mark as read

```bash
curl -X PATCH https://api.maylee.app/api/v1/emails/cmemx1a2b3c4d5e6f7g8h9i0 \
  -H "Authorization: Bearer $MAYLEE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"isRead": true}'
```

The change is pushed in real time to open Maylee windows and propagated to the provider in the background. To process many emails at once, use [`POST /emails/bulk`](/api-reference/emails/bulk-update-emails) (up to 500 ids, one quota unit per email).

## Send an email

Sending needs the `write` scope **and** the **Send emails** permission of the key. With the scope alone you get `403 OUTBOUND_SEND_NOT_ALLOWED`.

**cURL**

```bash  
curl -X POST https://api.maylee.app/api/v1/drafts/send \
  -H "Authorization: Bearer $MAYLEE_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "mailboxId": "cmbx1a2b3c4d5e6f7g8h9i0j",
    "to": [{ "email": "client@acme.com", "name": "Jane Client" }],
    "subject": "Your quote",
    "body": { "text": "Hello,\n\nPlease find the requested quote below." }
  }'
```

**JavaScript**

```js
const response = await fetch('https://api.maylee.app/api/v1/drafts/send', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.MAYLEE_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({
    mailboxId: 'cmbx1a2b3c4d5e6f7g8h9i0j',
    to: [{ email: 'client@acme.com', name: 'Jane Client' }],
    subject: 'Your quote',
    body: { text: 'Hello,\n\nPlease find the requested quote below.' },
  }),
})
const { messageId, threadId } = await response.json()
```

**Python**

```python
import os
import uuid
import requests

response = requests.post(
    "https://api.maylee.app/api/v1/drafts/send",
    headers={
        "Authorization": f"Bearer {os.environ['MAYLEE_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "mailboxId": "cmbx1a2b3c4d5e6f7g8h9i0j",
        "to": [{"email": "client@acme.com", "name": "Jane Client"}],
        "subject": "Your quote",
        "body": {"text": "Hello,\n\nPlease find the requested quote below."},
    },
)
print(response.status_code, response.json())
```

```json
{
  "messageId": "<20260825140233.1a2b3c@your-company.com>",
  "threadId": "18c2f5a9e7b1d3c4"
}
```

> **Always send an Idempotency-Key**
>
> A network timeout does not mean the send failed. Without the key, a retry delivers the message a second time. See [Idempotency](/idempotency).

Attachments cannot be sent through the API yet.

## Prepare a draft for a person to review

When a person should check the email before it goes out, create a draft instead. It takes the same fields, and the `write` scope is enough: nothing leaves the workspace.

```bash
curl -X POST https://api.maylee.app/api/v1/drafts \
  -H "Authorization: Bearer $MAYLEE_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "mailboxId": "cmbx1a2b3c4d5e6f7g8h9i0j",
    "to": [{ "email": "client@acme.com", "name": "Jane Client" }],
    "subject": "Your quote",
    "body": { "text": "Hello,\n\nPlease find the requested quote below." }
  }'
```

The draft appears at once in the Drafts folder of the Maylee app, where the user reviews it, edits it and sends it. The response is `201` with the draft itself (`isDraft: true`). The threading fields below work the same way on drafts.

## Reply in a thread

A reply is a send with three extra fields taken from the original email, so that every mail client groups it in the same conversation. First read the original with [`GET /emails/{emailId}`](/api-reference/emails/get-email) and keep its `mailboxId`, `messageId`, `references`, `from` and `subject`. Then:

```bash
curl -X POST https://api.maylee.app/api/v1/drafts/send \
  -H "Authorization: Bearer $MAYLEE_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "mailboxId": "cmbx1a2b3c4d5e6f7g8h9i0j",
    "to": [{ "email": "client@acme.com", "name": "Jane Client" }],
    "subject": "Re: Quote request",
    "inReplyTo": "<CAF3k9x1@mail.example.com>",
    "references": ["<CAF3k9x1@mail.example.com>"],
    "body": { "text": "Hello Jane,\n\nHere is the quote you asked for." }
  }'
```

The rules:

* `inReplyTo` is the `messageId` of the email you answer.
* `references` is the original's `references` array **plus** its `messageId`, in that order. On a first reply the original has no references, so the array holds the single `messageId`.
* Reply to the `from` of the original, and prefix the subject with `Re: ` unless it already starts with it.
* Send from the `mailboxId` the original arrived in.

The response's `threadId` is the provider's conversation id when the provider exposes one. Maylee's own thread view relies on the headers above and works either way.

## Let Maylee classify email

Everything you configure by hand in the app can be configured through the API. Two calls set up an AI label and a view that shows it:

```bash
curl -X POST https://api.maylee.app/api/v1/ai-label-rules \
  -H "Authorization: Bearer $MAYLEE_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "name": "Prospecting",
    "color": "Purple",
    "condition": "An unsolicited sales or partnership pitch from someone we have never worked with."
  }'
```

`color` is one of `Pink`, `Blue`, `Red`, `Yellow`, `Green`, `Purple` or `Orange`: these are the names the app renders. Keep `rule.label.id` from the response, then create a view on it:

```bash
curl -X POST https://api.maylee.app/api/v1/views \
  -H "Authorization: Bearer $MAYLEE_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "name": "Prospecting",
    "icon": "📣",
    "filters": { "id": "c1", "label": "ai_label", "type": "contains", "value": "<rule.label.id>" }
  }'
```

From now on, every matching email is labelled when it arrives and grouped in the view. Rules apply to emails received after their creation: to classify the emails already there, call [`POST /ai-label-rules/backfill`](/api-reference/ai-label-rules/classify-existing-emails) once. Add `"hideFromInbox": true` to the rule and those emails leave the main inbox, still reachable in the view and with `GET /emails?filtered=only`.

## What next

* **Branch on `code`, never on `error`.** Messages are written for humans and may be reworded; codes are a contract. See [Errors](/errors).
* **Read the quota headers.** `X-RateLimit-Remaining` tells you when to slow down before you hit a `429`. See [Rate limits and quotas](/rate-limits).
* **Webhooks are coming soon.** Until then, poll with a [cursor](/pagination) at a reasonable interval rather than in a tight loop. See [Webhooks](/webhooks).
