Questions? Email [email protected] · All prices are one-time unless stated otherwise
D
Datapad Projects
Reference

API reference

The /v1 sync API — authentication, chunk transfer, manifests, the change feed, rename, and events.

Conventions

The sync API lives under /v1 and speaks JSON unless noted. Every endpoint except POST /v1/login and GET /healthz requires a bearer token:

Authorization: Bearer <token>

All data is scoped to the authenticated user. Anything that isn't yours reads as 404 — the API does not reveal whether something exists for another user. Errors come as {"error": "message"} with a matching status code.

Two ideas recur throughout. A file has a stable id that never changes across edits and renames, and a version — the id of its newest manifest — that increases with every write, globally across all of a user's files, which is what lets a version double as a change-feed cursor. Writes carry a base: the version the client last synced. If the file has moved past that base, the server answers 412 and the client keeps both copies; see Conflicts and versioning.

Authentication

POST /v1/login

  • Body: {"username": "...", "password": "..."}
  • Returns {"token": "..."}. Store it; there is no expiry yet (token lifecycle is roadmap).
  • 401 on bad credentials (the response does not distinguish unknown user from wrong password).

Chunks

File content is transferred as content-addressed chunks: the client cuts each file into pieces, hashes each with SHA-256, and only uploads pieces the server lacks.

POST /v1/chunks/missing

  • Body: {"hashes": ["sha256hex", ...]} — up to 10,000 per request; batch larger sets.
  • Returns {"missing": [...]} — the subset the server does not hold. Upload those, skip the rest.

PUT /v1/chunks/{hash}

  • Body: the raw chunk bytes (up to 16 MiB).
  • The server recomputes the hash and rejects a mismatch with 400 — a client-claimed hash is never trusted. 201 on success; re-uploading an existing chunk is a harmless no-op.

GET /v1/chunks/{hash}

  • Returns the chunk bytes. 404 unless one of your file versions references the chunk — storage is deduplicated globally, but reads are authorized per user.

Files and manifests

A manifest is one saved version of a file: an ordered list of chunk hashes plus metadata. Recording a manifest is what creates a new version.

PUT /v1/files/{path}

  • Body: {"device": "name", "deleted": false, "chunks": ["hash", ...], "base": 0}. Every referenced chunk must already be uploaded. A delete is the same call with "deleted": true and no chunks — versions are kept, so a delete is reversible history, not erasure.
  • base is the version you edited from (0 for a new file). Omitting it skips the concurrency check entirely — the sync client always sends it.
  • Returns 201 with the stored manifest (see below). 409 with {"missing": [...]} if chunks are absent — upload them and retry. 412 if base is stale: reconcile and keep both.

GET /v1/files/{path}

  • Returns the file's newest manifest: {"path", "file_id", "version", "size", "deleted", "device", "updated_at", "chunks": [{"hash", "size"}, ...]}. Download the chunks in order and concatenate to reassemble the file. 404 if the path does not exist (a renamed-away path stops resolving; a deleted file still resolves, with "deleted": true).

GET /v1/by-id/{id}

  • The same manifest, addressed by the file's stable id instead of its path — how an id-keyed client resolves an item after renames. 404 for an unknown id or another user's.

GET /v1/files

  • Returns {"files": [{"id", "path", "size", "deleted", "version", "updated_at"}, ...]} — every file at its latest version, deleted ones included with their tombstone flag.

Change feed

GET /v1/changes?since=<cursor>&limit=<n>

  • The incremental form of the listing: only files whose latest version advanced past the cursor, oldest first, capped at limit (default 1000, max 10,000).
  • Returns {"changes": [...], "cursor": <next>}. Entries are the listing shape plus "renamed_from" when that newest version was a rename. Pass the returned cursor back as since to page; an empty page means you are caught up. since=0 is a full enumeration equal to GET /v1/files.
  • A cursor is never invalidated — replaying from 0 is always safe and converges to the same state.

Rename

POST /v1/rename

  • Body: {"id": <stable id>, "to": "new/path", "base": <version>, "device": "name"}.
  • Moves the file in one operation, keeping its id and history; content is not re-uploaded. Returns 201 with the new manifest.
  • 404 unknown or deleted file · 409 destination already occupied · 412 stale base (the file changed since — reconcile first).

Events

GET /v1/events

  • A Server-Sent Events stream. Each time one of your files gains a version, one event arrives: data: {"path", "version", "device", "deleted"}. Comment lines (: ping) keep the connection alive through proxies.
  • Events are hints to re-sync, not a complete log — a client that misses one catches up from its change-feed cursor on the next pass.

Health

GET /healthz

  • Unauthenticated, outside /v1. 200 with body ok when the database responds, 503 otherwise — made for container health checks and probes.

Status codes at a glance

  • 200/201 — success (created, for uploads and writes).
  • 400 — malformed request: bad JSON, invalid path, hash mismatch, oversized body.
  • 401 — missing or invalid token (or bad login).
  • 404 — not found for you: absent, or another user's.
  • 409 — a precondition about content: manifest chunks not yet uploaded, or a rename destination already taken.
  • 412 — stale base: the file advanced past the version you edited from; reconcile and keep both.
  • 413 — chunk over the 16 MiB limit.