Sync changes

.md

What has changed and what has disappeared since your last sync.

Scopesmail:read
GET/v1/me/mail/changes

The 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"
ParameterDescription
sinceRequired. An ISO 8601 timestamp the first time, then the next_since of your last sync
limitUp 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" }
}
FieldDescription
updatedMessages that changed, in the same shape as a list row. New mail is in here too: arriving is a change
deletedThe id of every message that no longer exists, such as msg_x2m7k4q9w3n8t5vd. Nothing else about them is kept
has_moreThere are more changes waiting. Call again straight away with next_since
next_sinceWhere 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

StatusCodeMeaning
400missing_sinceNo since was sent
400invalid_sinceNot a timestamp and not one of our handles, or a handle from before 2026-10-08
400sync_window_expiredOlder than the 45 day window. Rebuild the cache
400invalid_limitNot a positive integer
403insufficient_scopeThis device does not hold mail:read
403mailbox_not_permittedThe account may not open a mailbox at all