/v1/me/mail/changesThe call to make when a client comes back, instead of fetching the mailbox again. Over a mobile connection the difference between four rows and two hundred is the difference between an app that opens and one that spins.
Two halves, because a cache can be wrong in two ways. Something changed, which is an ordinary row. Or something disappeared, which is an absence and can never turn up in a list of rows.
Request
curl "https://api.proppertrading.com/v1/me/mail/changes?since=2026-08-19T12:00:00Z&limit=50" \
-H "Authorization: Bearer ptat_9f2c1a44_3b8e7d2f5c9a1b4e6d8f0a2c4e6b8d1f"
| Parameter | Description |
|---|---|
since | Required. An ISO 8601 timestamp the first time, then the next_since of your last sync |
limit | Up to 100. Defaults to 20. Applies to updated only |
Response
{
"data": {
"object": "mail_changes",
"updated": [
{
"object": "mail_message",
"id": "msg_h3k8q2w7n4r9t5vx",
"folder": "trash",
"is_read": true
}
],
"deleted": ["msg_x2m7k4q9w3n8t5vd"],
"has_more": true,
"next_since": "MjAyNi0wOC0xOSAxODoyMDo0Ni4wMDAwMDB8bXNnX2gzazhxMnc3bjRyOXQ1dng"
},
"meta": { "request_id": "req_01kz4bkr1aj1gv63d8fv3133ey" }
}
| Field | Description |
|---|---|
updated | Messages that changed, in the same shape as a list row. New mail is in here too: arriving is a change |
deleted | The id of every message that no longer exists, such as msg_x2m7k4q9w3n8t5vd. Nothing else about them is kept |
has_more | There are more changes waiting. Call again straight away with next_since |
next_since | Where to resume. Hand it back untouched |
Walking it
Oldest change first, so a client that gets halfway and loses signal resumes
exactly where it stopped rather than starting over. Keep calling while
has_more is true, then store next_since until the next time.
next_since is a position, not a clock reading. It carries the id of the
last message as well as the timestamp, so two messages that changed in the same
second cannot straddle a page boundary and have one of them skipped.
deleted is not paged. Bare ids are small, and a client with more deletions than
fit in one response has a much worse problem than the size of it.
When a client has been away too long
Deletion records are kept for 45 days. A since older than that answers
sync_window_expired, because past that point we cannot promise the list of
deletions is complete, and a sync that quietly misses things is worse than being
told to start over. Fetch the message list again and
begin a fresh sync from now.
A cache from before 2026-10-08
Messages were named by numbers until then, and a next_since from that time
carries one. It answers invalid_since. Every id in a cache built before then
is a number that names nothing any more, so do the same as above: fetch the
message list again and begin a fresh sync from now. Messages erased before the
change are not in deleted, because they were never recorded under their new
id.
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | missing_since | No since was sent |
| 400 | invalid_since | Not a timestamp and not one of our handles, or a handle from before 2026-10-08 |
| 400 | sync_window_expired | Older than the 45 day window. Rebuild the cache |
| 400 | invalid_limit | Not a positive integer |
| 403 | insufficient_scope | This device does not hold mail:read |
| 403 | mailbox_not_permitted | The account may not open a mailbox at all |