Getting Started with Data Sync
The Quran.Foundation Sync API enables offline-first mobile applications to synchronize user data across devices. This guide covers the core concepts and helps you implement your first sync flow.
Overview​
The Sync Layer provides:
- Pull Changes — Fetch all mutations (creates, updates, deletes) that occurred after a timestamp
- Push Changes — Batch-submit local mutations made while offline
- Conflict Detection — Prevents data conflicts using
lastMutationAttimestamps
Prerequisites​
Before using the Sync API, ensure you have:
- OAuth2 Client Credentials — Set up OAuth2 for your app
- Required Scopes — Request these scopes during authorization:
sync— Required forGET /v1/syncandPOST /v1/syncbookmark— For syncing bookmarkscollection— For syncing collectionsnote— For syncing notes
You only receive mutations for resources your token has access to. If you request sync + bookmark but not collection, you won't see collection changes.
Supported Resources​
The Sync API currently tracks these resources. All /v1/sync calls also require the sync scope.
| Resource | Description | Required Scope |
|---|---|---|
BOOKMARK | User bookmarks (ayah, surah, etc.) | bookmark |
COLLECTION | User collections | collection |
COLLECTION_BOOKMARK | Bookmarks within collections | bookmark, collection |
NOTE | User notes/reflections | note |
For BOOKMARK, COLLECTION, and NOTE, sync mutations include a single resourceId.
For COLLECTION_BOOKMARK, there is no single ID; use data.collectionId and data.bookmarkId instead and omit resourceId in your mutation.
Key Concepts​
lastMutationAt​
A Unix timestamp (milliseconds) tracking the most recent mutation for a user. This is your sync cursor.
- Stored locally — Your app saves this after each successful sync
- Sent with requests — Include it to get only newer changes or validate your state
- Updated by server — The server assigns timestamps, ensuring consistent ordering
First Sync Behavior​
When a user has never synced before (no lastMutationAt stored on the server):
| Scenario | Server Response | Client Action |
|---|---|---|
| No sync history yet | lastMutationAt: -1 | Store -1, treat as first-sync state |
| First mutation from this device | Send lastMutationAt=-1 | Server accepts, returns new timestamp |
| User already has sync history | lastMutationAt: <timestamp> | Store timestamp, fetch mutations since 0 (or T) |
For a user's very first mutation via POST /v1/sync, you must send lastMutationAt=-1. Any other value will be rejected.
Quick Start: Your First Sync​
Step 1: Check Sync Status​
First, check if the user has any existing data using metadataOnly=true. This returns only lastMutationAt without fetching mutations. (You still need to pass mutationsSince, even when metadataOnly=true.)
lastMutationAt: -1means no sync history yet (first-sync state)- Any positive number means existing data exists
Step 2: Pull Existing Data​
If the user has data (lastMutationAt > -1), fetch all mutations and apply them locally. Store the lastMutationAt value for future requests.
Step 3: Push Local Changes​
When the user creates data offline, push mutations to the server using POST /v1/sync?lastMutationAt=.... Update your local lastMutationAt from the response.
API References​
- GET /v1/sync (Get mutations)
- POST /v1/sync (Sync local mutations)
- POST /v1/bookmarks (Add bookmark) — for bookmark field requirements
Using metadataOnly for Efficiency​
The metadataOnly=true parameter returns only the lastMutationAt without fetching mutations. Use it for:
- Pre-sync checks — Quickly check if new data exists before a full sync
- Background polling — Efficiently detect changes without heavy payloads
- Recovering state — If you missed the
X-Mutation-Atheader from a previous response
This is significantly faster than a full sync (single DB query vs multiple joins).
For the full request/response schema, see:
Filtering & Pagination​
GET /v1/sync supports resource filtering and pagination:
resources=BOOKMARK,COLLECTIONlimits results to those resourceslimitandpagepaginate results (maxlimitis 1000)- Continue fetching pages until
hasMore=false
When metadataOnly=true, the response includes only lastMutationAt (no pagination fields).
Important Notes​
- Mutation limits —
POST /v1/syncaccepts a maximum of 100 mutations per request. - Direct endpoints — Direct mutation endpoints (e.g.
POST /v1/bookmarks) requirelastMutationAtas a query param and return theX-Mutation-Atheader for updatinglastMutationAt.
Next Steps​
- Handling Conflicts — Learn how to handle 409 errors and resolve conflicts
- Offline-First Patterns — Best practices for client-side architecture