Skip to main content
This example uses 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.
Call 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.
Change delivery is at least once. Apply upserts and tombstones idempotently by 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 stable client_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.