# Mail

When a broker connects Gmail or Microsoft 365 in BrokerPlus, the API can read that mail too. You
don't need a second connection to Gmail or Microsoft Graph. This page walks through one job end to
end: a daily review that finds the email that needs an answer.

This API is in beta. Any endpoint can change in a breaking way without notice, so watch the
[changelog](/changelog).

## What a key can read

A key reads the mailboxes its broker can read in BrokerPlus, and no others. A teammate who hasn't
shared their mailbox stays private.

BrokerPlus holds roughly the last six months of a mailbox and keeps it current from the moment it is
connected. Sent and received mail are in the same list. Drafts aren't.

## Read the mail

Every email is a communication. `GET /v1/communications?channel=email` lists them, newest first.

<CodeTabs syncKey="lang">

```bash title="curl"
curl "https://api.mos.getbrokerplus.ca/v1/communications?channel=email&page_size=1" \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY"
```

```ts title="TypeScript"
const res = await fetch("https://api.mos.getbrokerplus.ca/v1/communications?channel=email&page_size=1", {
  headers: { Authorization: `Bearer ${process.env.BROKERPLUS_API_KEY}` },
});
const { data } = await res.json();
```

```python title="Python"
import os

import requests

res = requests.get(
    "https://api.mos.getbrokerplus.ca/v1/communications",
    params={"channel": "email", "page_size": 1},
    headers={"Authorization": f"Bearer {os.environ['BROKERPLUS_API_KEY']}"},
)
data = res.json()["data"]
```

</CodeTabs>

```json
{
  "success": true,
  "data": [
    {
      "object": "communication",
      "id": "cmsclloss01wap3ium49cw7rq",
      "deal_id": "cmscllt4h01xyp3iuq7dk2m8z",
      "channel": "email",
      "direction": "inbound",
      "subject": "Re: Mortgage commitment",
      "body": "Here is the signed commitment.",
      "body_html": "<p>Here is the signed commitment.</p>",
      "status": "received",
      "from": { "name": "Jordan Client", "email": "jordan@example.com" },
      "to": [{ "name": "Alex Broker", "email": "alex@example.com" }],
      "cc": [],
      "mailbox": "alex@example.com",
      "thread_id": "19fed14c88a5b164",
      "attachments": [{ "id": "att_8f21", "name": "commitment.pdf", "content_type": "application/pdf", "size": 184320, "inline": false }],
      "folders": [{ "name": "Inbox", "role": "inbox" }],
      "read_at": null,
      "starred": false,
      "sent_at": null,
      "received_at": "2026-09-30T18:04:11.000Z",
      "created_at": "2026-09-30T18:04:14.221Z"
    }
  ],
  "pagination": { "page": 1, "page_size": 1, "total": 412, "total_pages": 412 }
}
```

`read_at` is null for mail nobody has opened. `body` is plain text and `body_html` is the sender's
HTML. BrokerPlus doesn't sanitize `body_html`, so sanitize it before you show it to anyone.

### Attachments

Ask for an attachment by its `id` to get a download link:

```sh
curl https://api.mos.getbrokerplus.ca/v1/communications/cmsclloss01wap3ium49cw7rq/attachments/att_8f21 \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY"
```

```json
{
  "success": true,
  "data": {
    "object": "attachment",
    "id": "att_8f21",
    "name": "commitment.pdf",
    "content_type": "application/pdf",
    "size": 184320,
    "inline": false,
    "download_url": "https://files.getbrokerplus.ca/...",
    "download_url_expires_at": "2026-09-30T18:12:00.000Z"
  }
}
```

The link works like a [document download URL](/files#download-urls): it carries its own
authorization, so follow it right away and don't store or share it. BrokerPlus fetches the file from
the mail provider the first time anyone asks, so the first call on an old message is slower. If the
mailbox no longer has the message, you get `410 ATTACHMENT_UNAVAILABLE`. A key with the
`no_documents` limit can't download attachments.

## The filters a review needs

| Filter              | What it does                                                                                       |
| ------------------- | -------------------------------------------------------------------------------------------------- |
| `unread=true`       | Only mail nobody has opened.                                                                       |
| `folder=inbox`      | One folder, by role or by the provider's own folder name.                                          |
| `thread_id=...`     | Every message in one conversation.                                                                 |
| `direction=inbound` | Only mail that came in.                                                                            |
| `updated_since=...` | Mail that changed at or after this time: read, starred, filed on a deal, or moved between folders. |

A daily review asks for the unread inbox mail since the last run:

<CodeTabs syncKey="lang">

```bash title="curl"
curl "https://api.mos.getbrokerplus.ca/v1/communications?channel=email&folder=inbox&unread=true&updated_since=2026-09-30T06:00:00Z" \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY"
```

```ts title="TypeScript"
const url = new URL("https://api.mos.getbrokerplus.ca/v1/communications");
url.search = new URLSearchParams({
  channel: "email",
  folder: "inbox",
  unread: "true",
  updated_since: "2026-09-30T06:00:00Z",
}).toString();
const res = await fetch(url, {
  headers: { Authorization: `Bearer ${process.env.BROKERPLUS_API_KEY}` },
});
const { data } = await res.json();
```

```python title="Python"
import os

import requests

res = requests.get(
    "https://api.mos.getbrokerplus.ca/v1/communications",
    params={
        "channel": "email",
        "folder": "inbox",
        "unread": "true",
        "updated_since": "2026-09-30T06:00:00Z",
    },
    headers={"Authorization": f"Bearer {os.environ['BROKERPLUS_API_KEY']}"},
)
data = res.json()["data"]
```

</CodeTabs>

With `updated_since` the list runs oldest first, so you can page forward through a backlog; see
[Sync](/sync). To read the conversation around a message, filter by its `thread_id`.

## Get told instead of asking

Subscribe a [webhook](/webhooks) to `email.received`, and BrokerPlus calls you when mail arrives.

```sh
curl https://api.mos.getbrokerplus.ca/v1/webhooks \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/hooks/brokerplus", "events": ["email.received"] }'
```

The delivery carries the message's id; read it with `GET /v1/communications/{id}`. The event fires
for new mail only, not for junk and not for the history imported when a mailbox is first connected.

## Act on a conversation

`PATCH /v1/communications/{id}` does what the inbox does: mark read or unread, mark done, star, set a
reminder, or file the mail on a deal. Each change applies to the whole conversation, as in the inbox,
and nothing goes to the client.

```bash
curl -X PATCH https://api.mos.getbrokerplus.ca/v1/communications/cmsm2a1... \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"read": true, "remind_at": "2026-10-09T13:00:00Z", "remind_condition": "if_no_reply"}'
```

```json
{
  "success": true,
  "data": {
    "object": "communication",
    "id": "cmsm2a1...",
    "read_at": "2026-10-07T18:02:11.000Z",
    "remind_at": "2026-10-09T13:00:00.000Z",
    "done_at": null
  }
}
```

The response is shortened here. Marking mail read also marks it read in the broker's own mailbox. At
`remind_at`, the conversation comes back to the top of the inbox and the broker gets a notification;
with `if_no_reply`, the reminder is dropped if the client replies first.

Send `deal_id` to file the mail on a deal. Add `"deal_scope": "message"` to file only this message,
for example when one client writes about two deals in the same thread.

## Tag a conversation

Inbox tags are the brokerage's own labels, such as Rate Hold or Docs Outstanding. They are a
separate list from contact tags and deal tags. `GET /v1/communication-tags` lists them, and
`POST`, `PATCH` and `DELETE` on `/v1/communication-tags` add, rename and archive them, the same as the
tag editor in the inbox.

`POST /v1/communications/{id}/tags` with a `tag_id` puts a tag on every message in the conversation.
`DELETE /v1/communications/{id}/tags/{tagId}` takes it off again. Each message lists its tags in
`tags`.

```bash
curl -X POST https://api.mos.getbrokerplus.ca/v1/communications/cmsm2a1.../tags \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"tag_id": "cmtg81x..."}'
```

On an Outlook mailbox, the tag is also written to Outlook as a category. BrokerPlus does not change
the labels of a Gmail mailbox, so a tag on Gmail mail gets `409 TAG_SYNC_UNSUPPORTED`. A tag that
came from the mailbox itself cannot be renamed or archived while synced mail carries it.

## File a conversation in a folder

Folders are the mailbox's own folders and labels, read from Gmail or Outlook.
`GET /v1/communication-folders` lists the folders of the broker's mailbox. Pass `mailbox` with an
address to list another mailbox the broker can read. Each message lists the folders it sits in, with
their ids, in `folders`.

`POST /v1/communications/{id}/folders` with a `folder_id` files every message in the conversation
into that folder, and `DELETE /v1/communications/{id}/folders/{folderId}` takes it out. The move
also happens in the broker's mail app a few seconds later. On Gmail a conversation can carry several
labels; on Outlook it moves to the one folder. Make, rename and delete folders in the mail app.

## Snippets

Snippets are a broker's own saved replies. The broker types a snippet's `shortcut` in the composer to
insert it. `GET /v1/snippets` lists the snippets of the broker your key acts as, and `POST`, `PATCH`
and `DELETE` on `/v1/snippets` add, edit and delete them. You never see or change another broker's
snippets. Saving a snippet sends nothing.

```bash
curl -X POST https://api.mos.getbrokerplus.ca/v1/snippets \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"shortcut": "docs", "name": "Documents", "body": "Please send your last two pay stubs."}'
```

A shortcut is unique among the broker's snippets, so reusing one gets `409 DUPLICATE_SHORTCUT`. If
you send `body_html`, unsafe HTML is removed from it, the same as for an email template. A snippet's
files are listed in `attachments`. `GET /v1/snippets/{id}/attachments/{attachmentId}` gives a link to
one of them that works for five minutes.

`POST /v1/snippets/{id}/attachments` adds a file, sent as `multipart/form-data` in a `file` field or as
JSON with `file_name` and `content_base64`. BrokerPlus reads the file's type from its content, not its
name. A PDF, JPEG, PNG, TIFF, WebP, Word or Excel file is stored as what it is. Content with no
recognisable type is accepted only as CSV, so a CSV sent as JSON needs a name ending in `.csv`.
Anything else gets `400 INVALID_TYPE`. A snippet holds up to 10 files of at most 25 MB each.
`DELETE /v1/snippets/{id}/attachments/{attachmentId}` removes one.

## Reply to a client

`POST /v1/communications/{id}/reply` answers a message in its own conversation, the same as Reply in
the inbox. The reply goes from the mailbox the conversation is on, with the conversation's subject,
so the client's mail program shows it in the same thread. A text is answered from the broker's
number, in the same text conversation.

```bash
curl -X POST https://api.mos.getbrokerplus.ca/v1/communications/cmsm2a1.../reply \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: reply-cmsm2a1-1" \
  -d '{"body": "Hi Sam, the lender approved the file this morning. I will send the commitment shortly."}'
```

```json
{
  "success": true,
  "data": {
    "object": "draft",
    "id": "cmdr91f...",
    "channel": "email",
    "status": "sent",
    "subject": "Re: Renewal documents",
    "to": [{ "email": "sam@example.com", "name": "Sam Renewal" }],
    "cc": [],
    "reply_to_communication_id": "cmsm2a1...",
    "forward": false,
    "communication_id": "cmsm7c4..."
  }
}
```

The response is shortened here. The reply goes out at once, as the broker your key acts as, so a key
with the `no_sending` limit gets `403 KEY_LIMITED`. See [Reaching clients](/reaching-clients). An
email is sent in the background, so `communication_id` can be null for a short time.

`mode` picks who the reply goes to:

| `mode` | Goes to |
|---|---|
| `reply` (default) | The sender. On a message the broker sent, the people it was sent to. |
| `reply_all` | The sender and everyone the message was sent to and copied, except the broker's own mailbox. |
| `forward` | The addresses you send in `to`. The original message is quoted below `body`, and its files go with it. |

Send `to` to replace the addresses the mode picks, and `cc` or `bcc` to copy other people. To attach
a file, send it in `attachments` with `file_name`, `content_type`, and `content_base64`. A file can
be at most 10 MB, and the files of one email at most 25 MB together.

A forward carries the files of the email it forwards, before any you add. Send
`"include_attachments": false` to forward the text only. A picture shown inside the email body is
not sent as a file. If the email has a file that BrokerPlus can't send, such as a web page, the
forward gets `400`, and you can forward it without its files.

When the conversation is on a mailbox the broker can read but cannot send from, such as a teammate's
shared mailbox, the reply gets `403 THREAD_VIEW_ONLY`, as it does in the inbox.

### Let the broker read it first

`POST /v1/communications/{id}/drafts` takes the same body and sends nothing. The reply waits in the
broker's drafts, in its conversation, and the broker sends, edits, or discards it in BrokerPlus.
Your system can also change it and send it:

- `PATCH /v1/drafts/{id}` changes the subject, the text, the addresses, or the files.
- `POST /v1/drafts/{id}/send` sends it now.
- `POST /v1/drafts/{id}/discard` throws it away.

### Send it later

Add `send_at` to a reply or to `POST /v1/contacts/{id}/messages` to send it at that time. The draft
is `scheduled` until then. `POST /v1/drafts/{id}/schedule` moves it to another time, or schedules a
draft that waits for review. `POST /v1/drafts/{id}/unschedule` stops it, and the draft waits for
review again.

## Mail merges

A mail merge is one email that each person on a list gets on their own: nobody sees who else got
it. `POST /v1/mail-merges` sends one now, the same as Send merge in BrokerPlus. The response is `201`
with the mail merge and a count of the addresses in each state. A key with the `no_sending` limit
gets `403 KEY_LIMITED`.

```bash
curl -X POST https://api.mos.getbrokerplus.ca/v1/mail-merges \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"subject": "Rates moved this week", "body": "Fixed rates dropped on Monday.", "recipients": [{"email": "ann@example.ca", "name": "Ann Able"}, {"email": "bo@example.ca"}]}'
```

The merge goes from the mailbox of the broker your key acts as, to at most 500 addresses. Everybody
gets the same words, so a merge field such as `{{first_name}}` gets `400 TOKENS_NOT_SUPPORTED`. A
person who unsubscribed or withdrew consent is skipped. Send `schedule_at` to send it later.

`GET /v1/mail-merges` lists the merges sent from the broker's mailbox, with how many addresses are
in each state. `GET /v1/mail-merges/{id}/recipients` shows what became of each address.
BrokerPlus does not keep the address on the merge: `communication_id` is the copy in Sent mail, and
`contact_id` is the contact with that address.

## What you can't do yet

- Read the drafts a broker writes in BrokerPlus. You can read the drafts you file; see
  [Drafts](/model#drafts).
- Delete mail.

Tell us which of these you need, and we'll tell you where it sits in the queue.
