Skip to content
Document DB v7.1: Temporal Support, Telemetry Collection, & Orleans Storage Providers! Feed The Machine Here

Server API Contracts

The default RestSyncTransport speaks plain HTTP + JSON. This page lists every request and response shape so you can wire up a backend in any language. If your server can’t (or shouldn’t) speak this protocol, swap the transport — see Custom Transports.

StreamMethodPathRequest bodyResponse body
Outbox — CreatePOST{Url}T (the entity)server-defined (passed to OnSent)
Outbox — UpdatePUT{Url}/{id}Tserver-defined
Outbox — DeleteDELETE{Url}/{id}
Outbox — BatchPOST{BatchUrl} or {Url}/batchSyncBatchRequestSyncBatchResponse
Inbox — PullGET{PullUrl} or {Url}SyncPullResponse
Inbox — TombstonesGET{TombstoneUrl}string[] or { cursor, ids }

Default headers on every request:

  • Content-Type: application/json on writes
  • Accept: application/json on reads
  • Anything added by ISyncInterceptor and SyncEndpoint.OnBeforeSend

The default cursor parameter is since (override with CursorParameter or omit it with null):

GET /todos?since=2026-06-01T00:00:00Z

Expected response (SyncPullResponse):

{
"cursor": "<opaque next cursor>",
"hasMore": false,
"items": [
{ "id": "abc", "verb": "Create", "payload": { "identifier": "abc", "title": "Buy milk", "completed": false } },
{ "id": "def", "verb": "Update", "payload": { "identifier": "def", "title": "Walk dog", "completed": true } },
{ "id": "ghi", "verb": "Delete" }
]
}

Notes:

  • verb is one of "Create", "Update", "Delete" (case-insensitive).
  • payload is required for Create / Update. For Delete it can be omitted, null, or {}.
  • When hasMore: true, the engine immediately re-pulls with the new cursor — one PullNow / PullAll call drains the full delta set.
  • HTTP 304 Not Modified is treated as “no changes” — LastPulledAt is bumped but the cursor is left alone.

The cursor is opaque — Shiny.Data.Sync persists whatever string you return and round-trips it on the next pull. Common patterns:

  • ISO-8601 timestamp of the last change you returned
  • Monotonic integer (database xmin, transaction id, snapshot lsn, etc.)
  • Combined “timestamp + entity id” tuple for tie-breaking

The engine emits one HTTP request per queued op:

POST /todos
Content-Type: application/json
{ "identifier": "abc", "title": "Buy milk", "completed": false }
PUT /todos/abc
Content-Type: application/json
{ "identifier": "abc", "title": "Buy milk", "completed": true }
DELETE /todos/abc

Any 2xx is treated as success. The full response body is passed to IDataSyncDelegate.OnSent(op, responseBody) so you can read a server-assigned id, ETag, or whatever you need.

When Batch = true, all queued ops for the endpoint are coalesced into a single request:

POST /todos/batch
Content-Type: application/json
{
"operations": [
{ "id": "<op-guid>", "entityId": "abc", "verb": "Create", "payload": { ... } },
{ "id": "<op-guid>", "entityId": "def", "verb": "Update", "payload": { ... } },
{ "id": "<op-guid>", "entityId": "ghi", "verb": "Delete" }
]
}

Coalescing happens client-side before the request — trailing Delete wins, Create + Update(s) collapses into a single Create with the latest payload, and Update + Update(s) collapses into a single Update.

Expected response (SyncBatchResponse):

{
"results": [
{ "id": "<op-guid>", "status": 200, "body": { /* server's serialized entity */ }, "error": null },
{ "id": "<op-guid>", "status": 409, "body": { /* current remote state */ }, "error": null },
{ "id": "<op-guid>", "status": 500, "body": null, "error": "Unexpected database error" }
]
}

Notes:

  • One result per op, keyed by the id the client sent.
  • status is the per-op HTTP status. The conflict / retry / error logic runs per result, exactly as if each op had been sent individually.
  • body for a successful op is passed to OnSent. For 409 / 412 it’s the remote payload passed to OnConflict.
  • error is a free-form server-side message attached to SyncOperation.LastError.

The Apple NSURLSession path does not batch — it sends one upload task per op by design. Everything else (Android, Windows, Linux, macOS, Blazor) honours the flag.

GET /todos/deleted?since=2026-06-01T00:00:00Z

Either of two shapes is accepted:

["abc", "def", "ghi"]
{ "cursor": "<opaque next cursor>", "ids": ["abc", "def", "ghi"] }

The second shape lets the tombstone stream advance its cursor independently from the main pull — useful when deletes pile up and need their own pagination.

The engine treats these as transient and schedules a retry with exponential backoff:

  • 0 — network down / connection error
  • 408 — request timeout
  • 429 — rate limited (respects Retry-After if you add it)
  • 5xx — server error

Everything else (4xx besides the above, or a 2xx with a parse error) is terminal:

  • 409 / 412 — handed to OnConflict per the endpoint’s DefaultConflictPolicy
  • everything else — handed to OnError after the retry budget is exhausted

Shiny.Data.Sync.Blazor runs in the browser, so your server must respond to preflight OPTIONS requests and include Access-Control-Allow-Origin. ASP.NET example:

app.UseCors(p => p
.WithOrigins("https://myapp.example.com")
.AllowAnyMethod()
.AllowAnyHeader()
);

A working ASP.NET reference server (using Shiny.DocumentDb.Sqlite for storage) lives in this repo at samples/Sample.Api. It covers every endpoint above plus a /{type}/current convenience route for inspecting state during development.