HTTP API
All /sync/* endpoints are expected to be mounted behind authentication. The handlers require an
authenticated oversync.Actor{UserID, SourceID} in request context. GET /health and
GET /status do not require an authenticated actor.
All visible sync keys are structured JSON objects whose values are strings on the wire. UUID-valued
keys use canonical dashed lowercase UUID strings. Hidden server ownership columns such as
_sync_scope_id never appear in client-visible payloads, conflicts, committed bundle rows, or
snapshot rows.
All authenticated /sync/* requests must send:
Authorization: Bearer <token>Oversync-Source-ID: <current-source-id>
POST /sync/connect
Resolve the first-connect lifecycle for the authenticated (user_id, source_id).
Request:
{
"has_local_pending_rows": true
}
Response:
{
"resolution": "initialize_local",
"initialization_id": "6df0d8dd-a84b-43b6-bbca-8de70432922a",
"lease_expires_at": "2026-03-22T12:00:00Z"
}
Possible resolution values:
remote_authoritativeinitialize_localinitialize_emptyretry_later
Failure contract:
400 connect_invalid
Notes:
retry_lateris a normal lifecycle outcome, not an auth failure- the caller must carry
initialization_idinto the first seed push only when the response isinitialize_local
POST /sync/push-sessions
Create one staging push session for a logical dirty-set bundle.
Request:
{
"source_bundle_id": 7,
"planned_row_count": 1
}
Response:
{
"push_id": "6df0d8dd-a84b-43b6-bbca-8de70432922a",
"status": "staging",
"planned_row_count": 1,
"next_expected_row_ordinal": 0
}
Notes:
- creating a fresh session for the same
(user_id, source_id, source_bundle_id)hard-replaces any older uncommitted staging session for that tuple - repeating the same tuple after the server has already committed returns
status = "already_committed"plus the committed bundle metadata - session creation is serialized by
(user_id, source_id, source_bundle_id)
Create failure contract:
400 push_session_invalid409 history_pruned409 source_sequence_out_of_order409 source_retired409 scope_uninitialized409 scope_initializing409 initialization_stale410 initialization_expired
POST /sync/push-sessions/{push_id}/chunks
Upload one contiguous chunk into a staging push session.
Request:
{
"start_row_ordinal": 0,
"rows": [
{
"schema": "business",
"table": "users",
"key": {"id": "550e8400-e29b-41d4-a716-446655440000"},
"op": "INSERT",
"base_row_version": 0,
"payload": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "John Doe",
"email": "john@example.com"
}
}
]
}
Response:
{
"push_id": "6df0d8dd-a84b-43b6-bbca-8de70432922a",
"next_expected_row_ordinal": 1
}
Failure contract:
400 push_chunk_invalid403 push_session_forbidden404 push_session_not_found409 push_chunk_out_of_order409 initialization_stale410 push_session_expired410 initialization_expired
POST /sync/push-sessions/{push_id}/commit
Commit the fully staged push session atomically.
Response:
{
"bundle_seq": 143,
"source_id": "source-1",
"source_bundle_id": 7,
"row_count": 1,
"bundle_hash": "4c8d2d5f5d2c5a41d9aa6f4d2f3ac5d0d1d5d8bbf1d7a8c39f3b3a970f6af21a"
}
Failure contract:
400 push_commit_invalid403 push_session_forbidden404 push_session_not_found409 push_conflict409 source_sequence_changed409 source_retired409 initialization_stale410 push_session_expired410 initialization_expired
Push conflict response:
{
"error": "push_conflict",
"message": "update conflict on business.users 550e8400-e29b-41d4-a716-446655440000: expected version 7, got 3",
"conflict": {
"schema": "business",
"table": "users",
"key": {"id": "550e8400-e29b-41d4-a716-446655440000"},
"op": "UPDATE",
"base_row_version": 3,
"server_row_version": 7,
"server_row_deleted": false,
"server_row": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Jane Doe",
"email": "jane@example.com"
}
}
}
GET /sync/committed-bundles/{bundle_seq}/rows
Fetch one deterministic page of authoritative committed rows for accepted-push replay.
Query:
after_row_ordinalmax_rows(optional, defaults to the server capability and is capped by the server capability)
Response:
{
"bundle_seq": 143,
"source_id": "source-1",
"source_bundle_id": 7,
"row_count": 1,
"bundle_hash": "4c8d2d5f5d2c5a41d9aa6f4d2f3ac5d0d1d5d8bbf1d7a8c39f3b3a970f6af21a",
"rows": [
{
"schema": "business",
"table": "users",
"key": {"id": "550e8400-e29b-41d4-a716-446655440000"},
"op": "INSERT",
"row_version": 143,
"payload": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "John Doe",
"email": "john@example.com"
}
}
],
"next_row_ordinal": 0,
"has_more": false
}
DELETE /sync/push-sessions/{push_id}
Best-effort explicit cleanup for an abandoned uncommitted push session.
Response:
- HTTP
204with no body on success
GET /sync/pull
Pull complete committed bundles after the client checkpoint.
Query:
after_bundle_seqmax_bundles(optional, defaults to1000, capped at5000)target_bundle_seq
Response:
{
"stable_bundle_seq": 156,
"bundles": [
{
"bundle_seq": 143,
"source_id": "source-2",
"source_bundle_id": 18,
"rows": [
{
"schema": "business",
"table": "users",
"key": {"id": "550e8400-e29b-41d4-a716-446655440001"},
"op": "INSERT",
"row_version": 143,
"payload": {
"id": "550e8400-e29b-41d4-a716-446655440001",
"name": "Jane Doe",
"email": "jane@example.com"
}
}
]
}
],
"has_more": true
}
Notes:
- the first response freezes
stable_bundle_seq - follow-up requests in the same pull session must pass that value back as
target_bundle_seq - if the provided checkpoint is older than the retained bundle floor, the server returns HTTP
409witherror=history_pruned
Failure contract:
400 pull_invalid409 history_pruned409 scope_uninitialized409 scope_initializing
POST /sync/snapshot-sessions
Create one frozen snapshot session for hydration or destructive recovery.
The request body is optional.
Keep-source rebuild request:
{}
Rotated rebuild request:
{
"source_replacement": {
"previous_source_id": "device-old",
"new_source_id": "device-new",
"reason": "history_pruned"
}
}
Notes:
- omitting
source_replacementmeans keep-source hydrate/rebuild - including
source_replacementmeans rotated rebuild; the server reservesnew_source_idand retiresprevious_source_idatomically with snapshot-session creation previous_source_idmust match the authenticatedOversync-Source-ID- supported
reasonvalues are:history_prunedsource_sequence_out_of_ordersource_sequence_changedsource_retired
Response:
{
"snapshot_id": "2f2d3f7a-cd1f-42b9-8d08-c4d2b45d9f88",
"snapshot_bundle_seq": 156,
"row_count": 124,
"byte_count": 18240,
"expires_at": "2026-03-22T12:00:00Z"
}
Failure contract:
400 snapshot_session_invalid409 source_replacement_invalid409 source_retired409 scope_uninitialized409 scope_initializing
Structured source_retired response:
{
"error": "source_retired",
"message": "source device-old was retired for user user-123 and replaced by device-new",
"source_id": "device-old",
"replaced_by_source_id": "device-new"
}
GET /sync/snapshot-sessions/{snapshot_id}
Fetch one deterministic chunk from a frozen snapshot session.
Query:
after_row_ordinalmax_rows(optional, defaults to the server capability and is capped by the server capability)
Response:
{
"snapshot_id": "2f2d3f7a-cd1f-42b9-8d08-c4d2b45d9f88",
"snapshot_bundle_seq": 156,
"rows": [
{
"schema": "business",
"table": "users",
"key": {"id": "550e8400-e29b-41d4-a716-446655440001"},
"row_version": 143,
"payload": {
"id": "550e8400-e29b-41d4-a716-446655440001",
"name": "Jane Doe",
"email": "jane@example.com"
}
}
],
"next_row_ordinal": 1,
"has_more": true
}
DELETE /sync/snapshot-sessions/{snapshot_id}
Best-effort explicit cleanup for a completed or abandoned snapshot session.
Response:
- HTTP
204with no body on success
GET /sync/capabilities
Returns the protocol version, schema version, app name, registered tables, registered table specs, feature flags, and bundle limits.
Important feature flags:
bundle_pushbundle_pullconnect_lifecyclepush_session_chunkingcommitted_bundle_row_fetchsnapshot_chunkinghistory_pruned_errors
Important bundle limit fields:
max_bundles_per_pulldefault_rows_per_push_chunkmax_rows_per_push_chunkdefault_rows_per_committed_bundle_chunkmax_rows_per_committed_bundle_chunkdefault_rows_per_snapshot_chunkmax_rows_per_snapshot_chunkpush_session_ttl_secondssnapshot_session_ttl_secondsmax_rows_per_snapshot_sessionmax_bytes_per_snapshot_session
GET /health
Readiness-oriented health response. Returns HTTP 200 when the service is healthy, and HTTP
503 when the service is unhealthy.
GET /status
Returns a lifecycle and operability snapshot including lifecycle, registered tables, feature flags, bundle visibility, retained-floor visibility, and error counters.