---
title: Sync Reference
path: reference/sync
status: published
---

Change tracking, cursor management, devices, preferences, and conflicts.

**Base path:** `/api/v1/sync/`

## Change object

```json
{
  "id": "chg_01J3L",
  "tenant_id": "tnt_01J3A",
  "share_id": "shr_01J3K",
  "resource_type": "file",
  "resource_id": "fil_01J3M",
  "change_type": "updated",
  "changed_by": "usr_01J3N",
  "changed_at": "2026-04-23T10:15:02Z",
  "old_path": "/Engineering/spec.pdf",
  "new_path": "/Engineering/spec.pdf",
  "version_id": "ver_01J3P",
  "metadata": {"size": 12345, "mime_type": "application/pdf"}
}
```

`change_type`: `created`, `updated`, `deleted`, `moved`, `renamed`.
`resource_type`: `file`, `folder`, `share`.

## Change tracking

### GET /api/v1/sync/changes

Fetch changes since a cursor.

**Query:**

| Param | Notes |
|-------|-------|
| `cursor` | Opaque cursor from prior response; omit to start from current position |
| `share_id` | Scope to one share |
| `limit` | 1–5000, default 1000 |

**Response:**

```json
{
  "changes": [...],
  "cursor": 1045000,
  "has_more": true,
  "share_id": "shr_01J3K"
}
```

**Required permission:** `READ` on the share.

### GET /api/v1/sync/status

Current cursor position and sync state.

**Query:** `share_id` (required), `device_id` (required).

**Response:**

```json
{
  "cursor_position": 1042387,
  "last_sync_at": "2026-04-23T10:15:00Z",
  "pending_changes": 0,
  "has_conflicts": false
}
```

Creates a cursor at the current head if none exists.

### POST /api/v1/sync/cursor

Advance the cursor.

**Query:** `share_id`, `device_id`, `position`.

**Response:** `{cursor_position, last_sync_at}`.

### POST /api/v1/sync/apply

Apply client-side changes to the server.

**Body:**

```json
{
  "device_id": "dev_01J3KX",
  "share_id": "shr_01J3K",
  "conflict_resolution": "LAST_WRITER_WINS",
  "changes": [
    {
      "resource_type": "file",
      "resource_id": "fil_01J3M",
      "change_type": "updated",
      "version_id": "ver_01J4Q",
      "base_version_id": "ver_01J3P",
      "old_path": "/Engineering/spec.pdf",
      "new_path": "/Engineering/spec.pdf",
      "metadata": {}
    }
  ]
}
```

**Resolution values:** `LAST_WRITER_WINS`, `KEEP_BOTH`, `SERVER_WINS`, `CLIENT_WINS`, `MANUAL`.

**Response:**

```json
{
  "applied": 8,
  "conflicts": [...],
  "errors": [...]
}
```

### GET /api/v1/sync/download/{file_id}

Download file content during sync (range-capable).

**Query:** `version`, `range_start`, `range_end`.

**Headers:** Supports `Range: bytes=start-end`.

**Required permission:** `READ` on the file.

## Conflicts

### GET /api/v1/sync/conflicts

List conflicts.

**Query:**

| Param | Notes |
|-------|-------|
| `device_id` | Filter by device |
| `share_id` | Filter by share |
| `include_resolved` | Default false |

**Response:** Array of conflict objects:

```json
{
  "id": "cfl_01J4R",
  "resource_type": "file",
  "resource_id": "fil_01J3M",
  "conflict_type": "version_mismatch",
  "client_version_id": "ver_01J4Q",
  "server_version_id": "ver_01J4S",
  "is_resolved": false,
  "resolution": null,
  "created_at": "2026-04-23T10:15:00Z"
}
```

### POST /api/v1/sync/conflicts/{conflict_id}/resolve

Resolve a conflict.

**Query:** `resolution` (one of the strategies).

**Response:** Updated conflict object.

## Devices

### GET /api/v1/sync/devices

List the caller's registered devices.

**Response:** Array of device objects:

```json
{
  "id": "dev_row_01J3K",
  "device_id": "dev_01J3KX",
  "device_name": "Alice MacBook",
  "platform": "macos",
  "client_version": "1.2.0",
  "is_active": true,
  "last_seen_at": "2026-04-23T10:14:58Z",
  "created_at": "2026-01-15T08:00:00Z"
}
```

### POST /api/v1/sync/devices

Register a device. Returns 201.

**Body:**

| Field | Notes |
|-------|-------|
| `device_id` | Client-generated; opaque string, typically a UUID |
| `device_name` | Display |
| `platform` | `windows`, `macos`, `linux`, `ios`, `android`, `web`, `cli` |
| `client_version` | Semver string |

### PATCH /api/v1/sync/devices/{device_id}

Update device.

**Body:** `device_name`, `is_active`.

### DELETE /api/v1/sync/devices/{device_id}

Remove device. Returns 204. Cursors associated with the device stop advancing.

### POST /api/v1/sync/devices/{device_id}/heartbeat

Record device activity. Returns 204. Updates `last_seen_at`.

## Sync preferences

### GET /api/v1/sync/devices/{device_id}/preferences

List per-share sync preferences for a device.

**Response:**

```json
[
  {
    "id": "pref_01J4A",
    "share_id": "shr_01J3K",
    "share_name": "Engineering",
    "sync_enabled": true,
    "selected_folders": ["fld_01J4A"],
    "excluded_folders": null
  }
]
```

### PUT /api/v1/sync/devices/{device_id}/preferences/{share_id}

Update a single preference.

**Body:** `sync_enabled`, `selected_folders`, `excluded_folders`.

### PUT /api/v1/sync/devices/{device_id}/preferences

Bulk update.

**Body:**

```json
{
  "preferences": [
    {"share_id": "shr_01J3K", "sync_enabled": true, "selected_folders": null, "excluded_folders": null}
  ]
}
```

## Real-time

### WebSocket /api/v1/sync/ws/{device_id}

Sync nudges for a device.

**Auth:** `?token=<JWT>` query parameter, or `Authorization: Bearer` header if the client library supports it.

**Server frames:** `change`, `sync_preference_updated`, `force_sync`, `pong`.
**Client frames:** `{"type": "ping"}`, `{"type": "subscribe"}`.

See [Real-time WebSocket](/docs/scaidrive/advanced/realtime-websocket).

## Error codes

| Code | HTTP | When |
|------|------|------|
| `SYNC_CONFLICT` | 409 | Version mismatch on apply |
| `SYNC_VERSION_STALE` | 409 | `base_version_id` outdated |
| `SYNC_CURSOR_INVALID` | 400 | Malformed or unknown cursor |
| `NOT_FOUND` | 404 | Device, conflict, or share not found |

## Related

- [Sync Model](/docs/scaidrive/core-concepts/sync-model)
- [Sync Guide](/docs/scaidrive/api-guides/sync)
- [Real-time WebSocket](/docs/scaidrive/advanced/realtime-websocket)