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 JWTtid.X-Client-Mutation-Idon push (redundant with body; for tracing).traceparentfor 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:
clientMutationIdPK; 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.