Open a conversation

.md

One message and the exchange around it, bodies included.

Scopesmail:read
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.

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.

Request

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

Response

{
    "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": "[email protected]",
                "from_name": "Jamie Rivera",
                "to_recipients": [
                    { "email": "[email protected]", "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": "[email protected]"
                },
                "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" }
}
FieldDescription
idThe message you opened, as the path named it. Every id in the response is a string. See Ids
thread_idThe conversation, such as thr_c4n8w2k7p5x9m3qd
subjectThe subject the exchange started under, not the latest "Re: Re:" of it
elsewhereMessages of this conversation in folders this view does not reach, by folder. Always an object, {} when there are none
hiddenOlder messages left out. A conversation is capped at 30
base_urlWhat a web view resolves the relative image links in html against
htmlThe message itself, sanitised, without the exchange it carries
quotedThe exchange it carries, sanitised. Empty when there is none to fold away
textThe same half as html, in plain words. Not the raw text part of the mail, which still has the whole exchange under it
was_unreadWhether this one was still unread when the conversation was opened. Unfold those, collapse the rest
readersWho has had this message in front of them, and when. user_id is the account, such as usr_t7q3m8k2x9n4w6pb
attachments[].nameWhat the sender called the file. Their word, so do not put it into a path without cleaning it first
content_typeWhat the sender said the file is. Also their word. The download answers application/octet-stream regardless
size_bytesThe stored size, in bytes
draftWhat 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.

FieldDescription
stateverified, unverified or unknown. See below
spfThe SPF result as the spam filter saw it, such as pass, fail or softfail. Null when not checked
dkimThe DKIM result, likewise
dmarcThe DMARC result, likewise
ipThe address of the server that delivered it
ptrThat server's name, from reverse DNS. Null when it has none
heloWhat that server called itself
envelope_fromThe 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:

"draft": {
    "reply_to_id": "msg_h3k8q2w7n4r9t5vx",
    "signature_id": "sig_khuz893vv6fzuusz",
    "quoted": "<blockquote><p>Hi, I passed the evaluation last week.</p></blockquote>"
}
FieldDescription
reply_to_idThe message this draft answers. Send it back as in_reply_to. Null for a new message, or when that message is gone
signature_idThe sign-off it was parked with, or null for none. Send it back as signature_id
quotedThe 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 and on the 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, plus one field:

"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": "[email protected]" },
    "can_respond": true
}
FieldDescription
statuspending until somebody answers, then confirmed. An ignored invitation is gone from the calendar, and invitation is null again
can_respondWhether this account can answer it. 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 and the two pages after it, which also describe every field on one.

Errors

StatusCodeMeaning
403insufficient_scopeThis device does not hold mail:read
403mailbox_not_permittedThe account may not open a mailbox at all
404resource_not_foundNo 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