Sync
Sync Endpoints
Section titled “Sync Endpoints”Sync endpoints handle multi-device synchronization with conflict resolution.
Get Sync Status
Section titled “Get Sync Status”GET /api/v1/sync/status
Returns the current synchronization status for the user.
Authentication
Section titled “Authentication”Required (Cookie)
curl https://app.bitmarks.sh/api/v1/sync/status \ -b "bitmarks_session=YOUR_TOKEN"const response = await fetch('https://app.bitmarks.sh/api/v1/sync/status', { credentials: 'include'});const status = await response.json();Response
Section titled “Response”Status: 200 OK
{ "user_id": "user_01ABC123", "last_sync": 1735430400000, "pending_changes": 5, "conflict_count": 0, "total_bookmarks": 150}Response Fields
Section titled “Response Fields”| Field | Type | Description |
|---|---|---|
user_id | string | User identifier |
last_sync | integer | Last sync timestamp (Unix ms) |
pending_changes | integer | Number of unsynced changes |
conflict_count | integer | Number of unresolved conflicts |
total_bookmarks | integer | Total bookmark count |
Error Response
Section titled “Error Response”Status: 500 Internal Server Error
{ "error": "Failed to get sync status", "details": "Database connection failed"}Pull Changes
Section titled “Pull Changes”POST /api/v1/sync/pull
Pulls changes from the cloud since a given timestamp.
Authentication
Section titled “Authentication”Required (Cookie)
Request Body
Section titled “Request Body”| Field | Type | Required | Default | Description |
|---|---|---|---|---|
since | integer | No | 0 | Unix timestamp (ms) to fetch changes after |
limit | integer | No | 1000 | Maximum changes to return (max: 10000) |
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 }'const response = await fetch('https://app.bitmarks.sh/api/v1/sync/pull', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ since: lastSyncTimestamp, limit: 100 })});const { changes, last_sync_timestamp, has_more } = await response.json();Response
Section titled “Response”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}SyncEvent Fields
Section titled “SyncEvent Fields”| Field | Type | Description |
|---|---|---|
id | string | Unique sync event ID |
user_id | string | User who made the change |
entity_type | string | bookmark, history, or group |
entity_id | string | ID of the affected entity |
operation | string | create, update, or delete |
timestamp | integer | When the change occurred (Unix ms) |
device_id | string | Device that made the change |
encrypted_data | string | Encrypted data (for create/update) |
data_hash | string | Hash for integrity verification |
conflict_resolved | boolean | Whether this resolved a conflict |
resolution_strategy | string | How conflict was resolved |
Pagination
Section titled “Pagination”If has_more is true, make another request with since set to last_sync_timestamp.
Push Changes
Section titled “Push Changes”POST /api/v1/sync/push
Pushes local changes to the cloud with automatic conflict resolution.
Authentication
Section titled “Authentication”Required (Cookie)
Request Body
Section titled “Request Body”| Field | Type | Required | Description |
|---|---|---|---|
changes | SyncEvent[] | Yes | Array of sync events to push |
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 } ] }'const response = await fetch('https://app.bitmarks.sh/api/v1/sync/push', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ changes: pendingChanges })});const result = await response.json();Response
Section titled “Response”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}Response Fields
Section titled “Response Fields”| Field | Type | Description |
|---|---|---|
accepted | integer | Number of changes accepted |
accepted_ids | string[] | IDs of accepted sync events |
conflicts | ConflictInfo[] | Information about rejected changes |
timestamp | integer | Server timestamp of the push |
Conflict Resolution
Section titled “Conflict Resolution”When a conflict is detected:
- Compare timestamps of local and server versions
- If timestamps are equal, use lexicographic device_id comparison
- Winner’s version is kept, loser is rejected
{ "entity_id": "bm_xyz", "reason": "Server version is newer", "resolution": "rejected"}Error Response
Section titled “Error Response”Status: 400 Bad Request
{ "error": "Invalid changes format"}Bulk Import
Section titled “Bulk Import”POST /api/v1/sync/import
Bulk imports bookmarks (e.g., from a Chrome export).
Authentication
Section titled “Authentication”Required (Cookie)
Headers
Section titled “Headers”| Header | Required | Description |
|---|---|---|
X-Device-ID | No | Device identifier (default: unknown) |
Request Body
Section titled “Request Body”| Field | Type | Required | Description |
|---|---|---|---|
bookmarks | ImportBookmark[] | Yes | Array 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)}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 } ] }'const response = await fetch('https://app.bitmarks.sh/api/v1/sync/import', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Device-ID': 'chrome-extension' }, credentials: 'include', body: JSON.stringify({ bookmarks: encryptedBookmarks })});const { imported, errors } = await response.json();Response
Section titled “Response”Status: 200 OK
{ "imported": 100, "imported_ids": ["bm_1", "bm_2", "..."], "errors": [ { "id": "bm_failed", "error": "Duplicate bookmark ID" } ], "timestamp": 1735430600000}Real-time Sync (WebSocket)
Section titled “Real-time Sync (WebSocket)”GET /api/v1/sync/realtime
Upgrades to WebSocket for real-time synchronization.
Authentication
Section titled “Authentication”Required (Cookie)
Headers
Section titled “Headers”| Header | Required | Description |
|---|---|---|
Upgrade | Yes | Must be websocket |
X-User-ID | Yes | User ID |
X-Device-ID | Yes | Device ID |
Response
Section titled “Response”Status: 101 Switching Protocols
WebSocket connection established.
Status: 426 Upgrade Required
{ "error": "WebSocket upgrade required"}WebSocket Messages
Section titled “WebSocket Messages”See Real-time Sync Guide for detailed WebSocket documentation.
Inbound (Client → Server)
Section titled “Inbound (Client → Server)”// Sync event{ type: "sync", event: SyncEvent }
// Keep-alive ping{ type: "ping" }Outbound (Server → Client)
Section titled “Outbound (Server → Client)”// 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 }Sync Implementation Example
Section titled “Sync Implementation Example”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... } }}