Skip to content

Sync

Sync endpoints handle multi-device synchronization with conflict resolution.


GET /api/v1/sync/status

Returns the current synchronization status for the user.

Required (Cookie)

Terminal window
curl https://app.bitmarks.sh/api/v1/sync/status \
-b "bitmarks_session=YOUR_TOKEN"

Status: 200 OK

{
"user_id": "user_01ABC123",
"last_sync": 1735430400000,
"pending_changes": 5,
"conflict_count": 0,
"total_bookmarks": 150
}
FieldTypeDescription
user_idstringUser identifier
last_syncintegerLast sync timestamp (Unix ms)
pending_changesintegerNumber of unsynced changes
conflict_countintegerNumber of unresolved conflicts
total_bookmarksintegerTotal bookmark count

Status: 500 Internal Server Error

{
"error": "Failed to get sync status",
"details": "Database connection failed"
}

POST /api/v1/sync/pull

Pulls changes from the cloud since a given timestamp.

Required (Cookie)

FieldTypeRequiredDefaultDescription
sinceintegerNo0Unix timestamp (ms) to fetch changes after
limitintegerNo1000Maximum changes to return (max: 10000)
Terminal window
curl -X POST https://app.bitmarks.sh/api/v1/sync/pull \
-H "Content-Type: application/json" \
-b "bitmarks_session=YOUR_TOKEN" \
-d '{
"since": 1735430400000,
"limit": 100
}'

Status: 200 OK

{
"changes": [
{
"id": "sync_abc123",
"user_id": "user_01ABC123",
"entity_type": "bookmark",
"entity_id": "bm_xyz",
"operation": "create",
"timestamp": 1735430400000,
"device_id": "device_123",
"encrypted_data": "eyJhbGciOi...",
"data_hash": "a1b2c3d4...",
"conflict_resolved": false,
"resolution_strategy": null
}
],
"last_sync_timestamp": 1735430500000,
"has_more": false
}
FieldTypeDescription
idstringUnique sync event ID
user_idstringUser who made the change
entity_typestringbookmark, history, or group
entity_idstringID of the affected entity
operationstringcreate, update, or delete
timestampintegerWhen the change occurred (Unix ms)
device_idstringDevice that made the change
encrypted_datastringEncrypted data (for create/update)
data_hashstringHash for integrity verification
conflict_resolvedbooleanWhether this resolved a conflict
resolution_strategystringHow conflict was resolved

If has_more is true, make another request with since set to last_sync_timestamp.


POST /api/v1/sync/push

Pushes local changes to the cloud with automatic conflict resolution.

Required (Cookie)

FieldTypeRequiredDescription
changesSyncEvent[]YesArray of sync events to push
Terminal window
curl -X POST https://app.bitmarks.sh/api/v1/sync/push \
-H "Content-Type: application/json" \
-b "bitmarks_session=YOUR_TOKEN" \
-d '{
"changes": [
{
"id": "sync_local_1",
"user_id": "user_01ABC123",
"entity_type": "bookmark",
"entity_id": "bm_new",
"operation": "create",
"timestamp": 1735430600000,
"device_id": "device_456",
"encrypted_data": "eyJhbGciOi...",
"data_hash": "hash123...",
"conflict_resolved": false
}
]
}'

Status: 200 OK

{
"accepted": 5,
"accepted_ids": ["sync_1", "sync_2", "sync_3", "sync_4", "sync_5"],
"conflicts": [
{
"entity_id": "bm_xyz",
"reason": "Server version is newer",
"resolution": "rejected"
}
],
"timestamp": 1735430600000
}
FieldTypeDescription
acceptedintegerNumber of changes accepted
accepted_idsstring[]IDs of accepted sync events
conflictsConflictInfo[]Information about rejected changes
timestampintegerServer timestamp of the push

When a conflict is detected:

  1. Compare timestamps of local and server versions
  2. If timestamps are equal, use lexicographic device_id comparison
  3. Winner’s version is kept, loser is rejected
{
"entity_id": "bm_xyz",
"reason": "Server version is newer",
"resolution": "rejected"
}

Status: 400 Bad Request

{
"error": "Invalid changes format"
}

POST /api/v1/sync/import

Bulk imports bookmarks (e.g., from a Chrome export).

Required (Cookie)

HeaderRequiredDescription
X-Device-IDNoDevice identifier (default: unknown)
FieldTypeRequiredDescription
bookmarksImportBookmark[]YesArray of bookmarks to import
interface ImportBookmark {
id: string; // Unique ID
encrypted_data: string; // Encrypted bookmark
data_hash: string; // Hash for integrity
date_added?: number; // Original creation (Unix seconds)
date_modified?: number; // Original modification (Unix seconds)
}
Terminal window
curl -X POST https://app.bitmarks.sh/api/v1/sync/import \
-H "Content-Type: application/json" \
-H "X-Device-ID: chrome-extension" \
-b "bitmarks_session=YOUR_TOKEN" \
-d '{
"bookmarks": [
{
"id": "bm_import_1",
"encrypted_data": "eyJhbGciOi...",
"data_hash": "hash123...",
"date_added": 1735430400,
"date_modified": 1735430400
}
]
}'

Status: 200 OK

{
"imported": 100,
"imported_ids": ["bm_1", "bm_2", "..."],
"errors": [
{
"id": "bm_failed",
"error": "Duplicate bookmark ID"
}
],
"timestamp": 1735430600000
}

GET /api/v1/sync/realtime

Upgrades to WebSocket for real-time synchronization.

Required (Cookie)

HeaderRequiredDescription
UpgradeYesMust be websocket
X-User-IDYesUser ID
X-Device-IDYesDevice ID

Status: 101 Switching Protocols

WebSocket connection established.

Status: 426 Upgrade Required

{
"error": "WebSocket upgrade required"
}

See Real-time Sync Guide for detailed WebSocket documentation.

// Sync event
{ type: "sync", event: SyncEvent }
// Keep-alive ping
{ type: "ping" }
// Connection established
{ type: "connected", deviceId: string, timestamp: number }
// Pong response
{ type: "pong" }
// Sync broadcast from another device
{ type: "sync", event: SyncEvent }
// Error
{ type: "error", error: string }

class SyncManager {
constructor(encryptionKey) {
this.key = encryptionKey;
this.lastSync = 0;
this.pendingChanges = [];
}
async fullSync() {
// 1. Push pending local changes
if (this.pendingChanges.length > 0) {
const pushResult = await this.push(this.pendingChanges);
this.pendingChanges = this.pendingChanges.filter(
c => !pushResult.accepted_ids.includes(c.id)
);
}
// 2. Pull remote changes
let hasMore = true;
while (hasMore) {
const { changes, last_sync_timestamp, has_more } = await this.pull();
await this.applyChanges(changes);
this.lastSync = last_sync_timestamp;
hasMore = has_more;
}
}
async pull() {
const response = await fetch('/api/v1/sync/pull', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
since: this.lastSync,
limit: 1000
})
});
return response.json();
}
async push(changes) {
const response = await fetch('/api/v1/sync/push', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ changes })
});
return response.json();
}
async applyChanges(changes) {
for (const change of changes) {
const decrypted = await decryptObject(change.encrypted_data, this.key);
// Apply to local storage...
}
}
}