/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" }
}
| Field | Description |
|---|---|
id | The message you opened, as the path named it. Every id in the response is a string. See Ids |
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:
"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 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
}
| 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. 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
| 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 |