Skip to main content

API Contracts

:::info Source Sourced from services/sync-service/API_CONTRACTS.md in the documentation repo. :::

1. Surfaces

  • Sync protocol REST (/sync/v1/…) — frozen (F07).
  • Admin REST (/api/v1/sync/…) for registrations, health, conflicts.

2. Versioning

  • /sync/v1/ frozen at M0 end; additive within v1; breaking → /sync/v2/.
  • Admin API: /api/v1/sync/… standard versioning.

3. Endpoints

3.1 Sync Protocol (Frozen F07)

POST /sync/v1/push body: { mutations: LocalMutation[] }
POST /sync/v1/pull body: { scope, cursor, pageSize? }
POST /sync/v1/ack body: { scope, cursor }
POST /sync/v1/resolve-conflict body: { conflictId, resolution, mergedPayload? }
GET /sync/v1/registrations (list replicable entity types)
GET /sync/v1/status (per-device sync health)

3.2 Admin

GET /api/v1/sync/registrations
GET /api/v1/sync/devices/{deviceId}/health
GET /api/v1/sync/conflicts ?tenantId=&userId=&status=pending
POST /api/v1/sync/devices/{deviceId}/force-resync
GET /api/v1/sync/metrics (aggregate sync health)

4. Request / Response

Push Request

{
"mutations": [
{
"clientMutationId": "cm_01H...",
"service": "progress",
"entityType": "Statement",
"entityId": "stmt_01H...",
"op": "create",
"baseVersion": null,
"vectorClock": { "dev_01H...": 42 },
"payload": { "...xAPI statement..." },
"occurredAt": "2026-04-15T10:00:00Z"
}
]
}

Push Response

{
"results": [
{ "clientMutationId": "cm_01H...", "status": "applied" },
{ "clientMutationId": "cm_02H...", "status": "conflicted", "conflictId": "cnf_01H..." },
{ "clientMutationId": "cm_03H...", "status": "rejected", "error": { "code": "sync.mutation.rejected", "message": "Server-authoritative entity changed" } }
]
}

Pull Request

{
"scope": "progress:user:u_01H...",
"cursor": { "lamport": 123456 },
"pageSize": 500
}

Pull Response

{
"cursor": { "lamport": 123789 },
"upserts": {
"Attempt": [ { "id": "att_01H...", "state": "closed", "..." } ],
"CompletionRecord": [ { "..." } ]
},
"deletes": {
"Statement": []
},
"hasMore": false
}

5. Error Model

  • sync.conflict.detected (409) — mutation conflicts with server state.
  • sync.mutation.rejected (409) — policy rejects mutation.
  • sync.cursor.out_of_range (410) — full resync needed.
  • sync.payload.too_large (413) — batch > 10 MB.
  • sync.device.unbound (403) — device not registered.
  • content.bundle.tampered (409) — reported by device.
  • content.bundle.revoked (410) — bundle revoked.
  • content.license.expired (403).
  • content.license.device_unbound (403).

6. Pagination

Pull uses cursor (lamport-based). pageSize max 1000 entities per pull.

7. Rate Limits

  • Push: 100 mutations per batch; 60 batches/min per device.
  • Pull: 60/min per device.
  • Conflict resolution: 30/min per user.

8. Headers

  • Authorization: Bearer <jwt> required.
  • X-Tenant-Id: <tenantId> required; must match JWT tid.
  • X-Client-Mutation-Id on push (redundant with body; for tracing).
  • traceparent for OTel.

9. Security

  • Device binding verified on every sync operation.
  • Mutations scoped to caller's (tenantId, userId, deviceId) — cannot push for another user.
  • Payload validated against registered JSON Schema.

10. Idempotency

  • Push: clientMutationId PK; duplicate submissions return previous result.
  • Pull: idempotent (same cursor → same delta).
  • Ack: idempotent (cursor only advances; re-ack is no-op).

11. SLOs

  • Push p95 < 500ms (100-mutation batch).
  • Pull p95 < 300ms (500-entity delta).
  • Ack p95 < 100ms.
  • Conflict resolution p95 < 1s.