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. PostgreSQL SMALLINT,
INTEGER, BIGINT, NUMERIC, DECIMAL, REAL, and DOUBLE PRECISION business values are JSON
strings on every wire surface. Floating strings use the RFC 8785 shortest finite binary64 spelling;
negative zero becomes "0". SQLite-affinity clients may upload PostgreSQL Boolean values as the
strict strings "0"/"1"; committed rows, pulls, conflicts, and snapshots use JSON Booleans.
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_expired503 commit_outcome_unknown
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_expired503 commit_outcome_unknown
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_expired503 commit_outcome_unknown
commit_outcome_unknown means the server cannot prove whether the create, chunk, or commit
transaction committed. Clients must keep the frozen source bundle ID and canonical request hash and
restart POST /sync/push-sessions; they must not infer rollback from an absent or stale staging
push_id.
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_initializing409 snapshot_session_limit_exceededwith requireddimension,actual, andlimitfields429 snapshot_build_capacitywith required positive delta-secondsRetry-After
snapshot_session_limit_exceeded applies to row_count, byte_count, and row_byte_count.
Snapshot byte limits count complete encoded SnapshotRow UTF-8 JSON bytes, including schema,
table, key, row version, payload, and per-row JSON framing; they exclude inter-row commas and the
enclosing response object.
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)max_bytes(optional exact full-SnapshotRowJSON byte budget, default 4 MiB, capped at 16 MiB)
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,
"byte_count": 241
}
If the first eligible row does not fit max_bytes, the server returns
400 snapshot_chunk_too_small with its exact required_byte_count. Excess concurrent chunk reads
return 429 snapshot_chunk_capacity with required positive delta-seconds Retry-After.
The Go client handles these two exact capacity codes with a dedicated elapsed-time policy (enabled by default, 30-second budget, one-second missing/invalid-header fallback) rather than consuming the generic transport retry-attempt budget.
DELETE /sync/snapshot-sessions/{snapshot_id}
Logically retire a completed or abandoned snapshot session. The bounded cleanup worker removes its rows and then its empty parent asynchronously.
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.
registered_table_specs is required and canonical. Every item requires non-blank schema and
table strings plus exactly one non-blank sync_key_columns entry; duplicate schema/table specs are
invalid. registered_tables is an optional legacy summary. Compatible clients compare the exact
advertised schema/table and ordered key list with their validated local sync configuration before
connect or data movement. This is table/key compatibility checking, not wire-profile or projection
negotiation.
The breaking-development server protocol version is exactly v1. Updated clients must reject any other,
empty, or unknown value before connect, outbox freeze, or remote synchronization work.
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_sessiondefault_bytes_per_snapshot_chunkmax_bytes_per_snapshot_chunkmax_bytes_per_snapshot_rowsnapshot_materialization_batch_rowssnapshot_materialization_batch_bytesmax_concurrent_snapshot_buildsmax_concurrent_snapshot_chunk_requests
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.