HTTP API
All /sync/* endpoints are expected to be mounted behind authentication. The handlers require an
authenticated oversync.Actor{UserID, SourceID} in request context. Auxiliary GET /syncx/health
and GET /syncx/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.
Wire-facing row_version and base_row_version fields are bundle sequence values. The server
stores the current version internally as sync.row_state.bundle_seq and returns it through the
existing wire field names.
Oversync uses RFC 8785 JSON Canonicalization Scheme (JCS) for canonical bytes. Exact BIGINT
and arbitrary-precision decimal columns are schema-typed JSON strings; approximate floating-point
columns remain finite binary64 JSON numbers. bundle_hash authenticates the committed logical row
stream, while canonical_request_hash associates that commit with the immutable original upload.
Hash-only protocol counters are decimal strings inside the logical hash model. byte_count is the
length of those canonical logical bytes. This is standard JCS plus a typed wire schema, not an
arbitrary-precision extension to JCS.
Oversync does not migrate incompatible stored hashes, server history, staged sessions, or durable client state. To adopt this contract, deploy compatible server and client versions together and recreate the affected server and client databases. Mixed-version operation is unsupported.
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,
"canonical_request_hash": "f9952b89b7b7ce1f4a2b9a3ea31ee7e8ca05d19bb6cb2cd400c8d18a1043fc83"
}
Response:
{
"push_id": "6df0d8dd-a84b-43b6-bbca-8de70432922a",
"status": "staging",
"planned_row_count": 1,
"next_expected_row_ordinal": 0,
"canonical_request_hash": "f9952b89b7b7ce1f4a2b9a3ea31ee7e8ca05d19bb6cb2cd400c8d18a1043fc83"
}
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 - the server recomputes
canonical_request_hashfrom separately staged original rows before any server-side payload mutation; a different hash for an existing source tuple is a sequence error - session creation is serialized by
(user_id, source_id, source_bundle_id) - source bundle ids must be contiguous per source
- if the exact tuple is no longer retained but the server can prove it was already committed,
session creation returns
history_pruned
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",
"canonical_request_hash": "f9952b89b7b7ce1f4a2b9a3ea31ee7e8ca05d19bb6cb2cd400c8d18a1043fc83"
}
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",
"canonical_request_hash": "f9952b89b7b7ce1f4a2b9a3ea31ee7e8ca05d19bb6cb2cd400c8d18a1043fc83",
"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 retained_bundle_flooris the highest discarded bundle; a checkpoint equal to the floor is valid and returns bundles strictly above it- when the floor is positive, lower checkpoints (including zero) return HTTP
409witherror=history_pruned - a checkpoint or positive target above current committed history returns HTTP
409witherror=checkpoint_ahead; the server does not clamp it into a durable stable checkpoint - target zero captures current committed history; a positive target below
after_bundle_seqreturns HTTP400witherror=invalid_request
Failure contract:
400 invalid_request409 history_pruned409 checkpoint_ahead409 scope_uninitialized409 scope_initializing
GET /sync/watch
Open an optional Server-Sent Events stream for metadata-only bundle wakeups.
Query:
after_bundle_seq(optional, defaults to0)
Event:
event: bundle
data: {"bundle_seq":157,"source_id":"device-a","source_bundle_id":9}
Notes:
/sync/watchis only a wake-up hint; clients must still call/sync/pullor existing sync operations to receive authoritative data- heartbeat comments such as
: heartbeatkeep idle connections observable and clean up stale subscribers - events can coalesce; clients must not expect one event per committed bundle
Failure contract:
400 invalid_request401 authentication_failed409 scope_uninitialized409 scope_initializing503 bundle_change_watch_disabled
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_errorsbundle_change_watch
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 /syncx/health
Readiness-oriented health response. Returns HTTP 200 when the service is healthy, and HTTP
503 when the service is unhealthy. A newly constructed runtime remains unhealthy and reports
accepting_operations=false until Bootstrap() has committed schema validation, trigger
installation, and any populated-table adoption.
GET /syncx/status
Returns a lifecycle and operability snapshot including lifecycle, registered tables, feature flags, bundle visibility, retained-floor visibility, and error counters. Unlike sync operations, status is available while bootstrap is incomplete so operators can observe the readiness gate.