kotlinx.serialization throughout. It includes the DTOs used
by the Retrofit interface, a raw refresh client, a bearer interceptor, and the
bulk-history result handling. There are no Moshi annotations in this example.
Dependencies
The following is a small JVM/Android-compatible Gradle setup. Pin the versions in your own dependency catalog and update them as a unit when you upgrade Kotlin:DTOs and API
Use@SerialName for the API’s snake-case fields. Response objects such as
nextCursor, resourceId, and changedAt are camelCase, so they keep their
Kotlin property names.
Refresh without recursive interception
Build two Retrofit clients. The raw client has no bearer interceptor and is used for device authorization and refresh. Only the authenticated client installs the interceptor. This matters because a refresh request must still work when the access token that triggered the refresh is expired.clients.raw.refresh(...) only through TokenStore; call protected sync
methods on clients.authenticated. Retry a 401 once. If the replay fails,
send the user through device authorization again. Because refresh tokens rotate,
the mutex and the raw refresh client are both required.
Initial sync and cursor ownership
Keep one encrypted token record, mirror, and cursor per PunchPlay account and app. A cursor is also scoped by the read permissions available to that token. If the user later grants additional read scopes, request a new cursor and snapshot the newly visible resources; an older cursor is not a backfill for data that was previously hidden.resource plus resourceId, and save the cursor only in the same transaction
as the page. A cursor can remain valid for 90 days; after that the API asks for a
reset and a fresh snapshot.
Bulk history with stable idempotency
Use one stableclient_item_id per source watch. Persist one fresh UUID as
the Idempotency-Key for each logical batch before the first request. If the
request times out, retry the same body with the same persisted key. If you
change the body, generate and persist a new key.
deferred means PunchPlay stored the item and will retry resolution; do not
place it in your retry queue. For 409 request_in_progress, wait and retry the
same key. For 409 idempotency_indeterminate, reconcile through the change feed
and retry the unresolved remainder with a new key.
See Partner Sync for scope combinations, identifier formats,
and the complete outcome table.