:::endpoint GET /v1/me/mail/messages/{id}

The whole exchange the message sits in, oldest first, bodies and all. One call
rather than one per message: unfolding a reply has to be instant, and a five
message thread would otherwise be five round trips and a spinner where the words
should be.

:::warning This marks the conversation as read
Every message in it, and it also records who read it. There is no way to fetch a
body without that happening. See [Reading mail](/docs/api/mail/overview).
:::

## Request

```bash
curl https://api.proppertrading.com/v1/me/mail/messages/msg_h3k8q2w7n4r9t5vx \
  -H "Authorization: Bearer ptat_9f2c1a44_3b8e7d2f5c9a1b4e6d8f0a2c4e6b8d1f"
```

## Response

```json
{
    "data": {
        "id": "msg_h3k8q2w7n4r9t5vx",
        "object": "mail_conversation",
        "thread_id": "thr_c4n8w2k7p5x9m3qd",
        "address_id": "mbx_w6sy3skvgydh5e3b",
        "subject": "Payout request",
        "folder": "inbox",
        "labels": [
            {
                "id": "lbl_p6m3x8b2k9q4w7td",
                "name": "Payouts",
                "color": "blue"
            }
        ],
        "assignee": { "id": "usr_t7q3m8k2x9n4w6pb", "name": "Han Aarts" },
        "answered": {
            "id": "usr_t7q3m8k2x9n4w6pb",
            "name": "Han Aarts",
            "at": "2026-08-19T14:02:11.000000Z"
        },
        "notes": [
            {
                "object": "mail_note",
                "id": "mnote_r8t4k2n7q3w9x5hb",
                "body": "Called them back, waiting on the bank statement.",
                "author": "Han Aarts",
                "author_id": "usr_t7q3m8k2x9n4w6pb",
                "is_mine": false,
                "can_edit": false,
                "created_at": "2026-08-20T14:02:11.000000Z",
                "edited": false
            }
        ],
        "elsewhere": { "trash": 1 },
        "hidden": 0,
        "base_url": "https://acmefunded.com",
        "messages": [
            {
                "id": "msg_h3k8q2w7n4r9t5vx",
                "object": "mail_message",
                "address_id": "mbx_w6sy3skvgydh5e3b",
                "direction": "inbound",
                "from_email": "jamie@example.com",
                "from_name": "Jamie Rivera",
                "to_recipients": [
                    { "email": "support@mail.acmefunded.com", "name": null }
                ],
                "cc_recipients": [],
                "subject": "Payout request",
                "folder": "inbox",
                "is_starred": false,
                "message_at": "2026-08-19T13:30:36.000000Z",
                "spam_score": 0.4,
                "html": "<p>Hi, I passed the evaluation last week.</p>",
                "quoted": "",
                "text": "Hi, I passed the evaluation last week.",
                "preview": "Hi, I passed the evaluation last week.",
                "has_attachments": true,
                "attachments": [
                    {
                        "id": "att_w5k9n3q7h2t8x4mr",
                        "name": "statement-july.pdf",
                        "content_type": "application/pdf",
                        "size_bytes": 84213
                    }
                ],
                "was_unread": true,
                "sender_check": {
                    "state": "verified",
                    "spf": "pass",
                    "dkim": "pass",
                    "dmarc": "pass",
                    "ip": "209.85.220.41",
                    "ptr": "mail-sor-f41.google.com",
                    "helo": "mail-sor-f41.google.com",
                    "envelope_from": "jamie@example.com"
                },
                "readers": [
                    {
                        "user_id": "usr_t7q3m8k2x9n4w6pb",
                        "name": "Han Aarts",
                        "first_read_at": "2026-08-19T13:41:02.000000Z",
                        "last_read_at": "2026-08-20T09:12:45.000000Z"
                    }
                ]
            }
        ],
        "draft": null,
        "invitation": null
    },
    "meta": { "request_id": "req_01kz4bkr1aj1gv63d8fv3133ey" }
}
```

| Field                | Description                                                                                                             |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `id`                 | The message you opened, as the path named it. Every id in the response is a string. See [Ids](/docs/api/mail/overview)   |
| `thread_id`          | The conversation, such as `thr_c4n8w2k7p5x9m3qd`                                                                         |
| `subject`            | The subject the exchange started under, not the latest "Re: Re:" of it                                                  |
| `elsewhere`          | Messages of this conversation in folders this view does not reach, by folder. Always an object, `{}` when there are none |
| `hidden`             | Older messages left out. A conversation is capped at 30                                                                 |
| `base_url`           | What a web view resolves the relative image links in `html` against                                                     |
| `html`               | The message itself, sanitised, without the exchange it carries                                                          |
| `quoted`             | The exchange it carries, sanitised. Empty when there is none to fold away                                               |
| `text`               | The same half as `html`, in plain words. Not the raw text part of the mail, which still has the whole exchange under it |
| `was_unread`         | Whether this one was still unread when the conversation was opened. Unfold those, collapse the rest                     |
| `readers`            | Who has had this message in front of them, and when. `user_id` is the account, such as `usr_t7q3m8k2x9n4w6pb`            |
| `attachments[].name` | What the sender called the file. Their word, so do not put it into a path without cleaning it first                     |
| `content_type`       | What the sender said the file is. Also their word. The download answers `application/octet-stream` regardless           |
| `size_bytes`         | The stored size, in bytes                                                                                               |
| `draft`              | What a composer needs to reopen the opened message, when it is a draft. Null for everything else. See below             |

Inline images are not listed under `attachments`. They are already in `html`,
and listing the same picture twice reads as two pictures.

## Who really sent it

`sender_check` says which server delivered a message and whether the address
on it could be trusted. Null for mail sent from here, and for mail that arrived
before this was recorded.

| Field           | Description                                                                                          |
| --------------- | ---------------------------------------------------------------------------------------------------- |
| `state`         | `verified`, `unverified` or `unknown`. See below                                                      |
| `spf`           | The SPF result as the spam filter saw it, such as `pass`, `fail` or `softfail`. Null when not checked |
| `dkim`          | The DKIM result, likewise                                                                            |
| `dmarc`         | The DMARC result, likewise                                                                           |
| `ip`            | The address of the server that delivered it                                                          |
| `ptr`           | That server's name, from reverse DNS. Null when it has none                                          |
| `helo`          | What that server called itself                                                                       |
| `envelope_from` | The sender given to the server, which can differ from the `From` on the message                       |

`verified` only when DMARC passed, because that is the one check that ties the
address on the screen to the server that sent it. `unverified` when DMARC
failed, or when there was no DMARC and SPF failed without a valid DKIM
signature: the shape of a forged message. Show a warning for that one, with
`ptr` or `ip` as the server that really delivered it. Everything else is
`unknown` and deserves no warning, because most honest small senders land
there.

## Reopening a draft

Open a message from the `drafts` folder and `draft` carries what the
composer it was written in knew, beyond what the message itself shows:

```json
"draft": {
    "reply_to_id": "msg_h3k8q2w7n4r9t5vx",
    "signature_id": "sig_khuz893vv6fzuusz",
    "quoted": "<blockquote><p>Hi, I passed the evaluation last week.</p></blockquote>"
}
```

| Field          | Description                                                                                                              |
| -------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `reply_to_id`  | The message this draft answers. Send it back as `in_reply_to`. Null for a new message, or when that message is gone      |
| `signature_id` | The sign-off it was parked with, or null for none. Send it back as `signature_id`                                        |
| `quoted`       | The quoted exchange, kept apart from the body. Send it back as `quoted`. Not sanitised for display, so do not render it |

The body is the draft's own `html` in `messages`, and the recipients and the
subject are on that message too. Keep sending the opened id as `draft_id`, on
every [save](/docs/api/mail/draft) and on the [send](/docs/api/mail/send), or
the draft stays behind as a copy.

Leave out `in_reply_to` and the recipient's mail client files the answer as a
new exchange, apart from the thread it belongs to.

## A meeting invitation

`invitation` is null unless the conversation carries a meeting invitation. When
it does, the server has already put it on the calendar as `pending`, and this is
that appointment, in the same shape as
[the calendar's own list](/docs/api/calendar/events), plus one field:

```json
"invitation": {
    "id": "evt_b6t2q9m4x7f3k8hz",
    "object": "calendar_event",
    "occurrence_start": null,
    "calendar_id": "cal_r4d8h2m6t9w3k7nb",
    "title": "Propper introduction",
    "starts_at": "2026-10-01T13:00:00+00:00",
    "ends_at": "2026-10-01T13:30:00+00:00",
    "all_day": false,
    "status": "pending",
    "source": "mail",
    "conference": { "provider": "google_meet", "url": "https://meet.google.com/abc-defg-hij", "meeting_id": null, "passcode": null },
    "organizer": { "name": "Sarah Klein", "email": "sarah@acmefunded.com" },
    "can_respond": true
}
```

| Field         | Description                                                                                                                                                                    |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `status`      | `pending` until somebody answers, then `confirmed`. An ignored invitation is gone from the calendar, and `invitation` is null again                                            |
| `can_respond` | Whether this account can [answer it](/docs/api/calendar/respond). False once it is answered, without the `view_calendar` permission, or in a calendar this account may only read |

Looked up on the whole conversation, not on one message: the invitation arrives
on one mail and the exchange about it goes on for several more. Answering it
takes `calendar:write` on the device, which a token from before the apps asked
for the calendar does not have.

## Rendering the body

`html` is sanitised and safe to render. Put it in a web view with JavaScript
disabled and the base URL set to `base_url`, and keep your token out of that
view. `quoted` is the thread the message carries underneath it; fold it away.

`html`, `quoted` and `text` are three views of one split and always agree, so
render either `html` or `text` and put `quoted` behind a control. Where the
split falls is worked out from the markup, because that is where a mail client
says which part of a reply it wrote itself. Not every client says so and not
every one that does says it the same way, so `quoted` is empty for a message
whose quote could not be found with certainty, and the whole of it arrives in
`html` and `text`. Nothing is ever dropped: the two halves put back together
are the message that came in.

Remote images load. This is a shared business mailbox where the pictures are
invoices and signatures, so a notice above every one of them would be a tap
between reading and being able to read.

## Notes

`notes` is the team's own remarks on this conversation, oldest first. They are
never sent and never seen by whoever wrote in.

They come down here rather than from an endpoint of their own, because a remark
that arrives after somebody has started typing their reply is a remark that did
not do its job. Writing, changing and removing them is
[add a note](/docs/api/mail/note-create) and the two pages after it, which also
describe every field on one.

## Errors

| Status | Code                    | Meaning                                                                                                      |
| ------ | ----------------------- | ------------------------------------------------------------------------------------------------------------ |
| 403    | `insufficient_scope`    | This device does not hold `mail:read`                                                                        |
| 403    | `mailbox_not_permitted` | The account may not open a mailbox at all                                                                    |
| 404    | `resource_not_found`    | No such message in a mailbox you can read. A message in another mailbox, or another firm's, answers the same, and so does a number in the path |