REST API
Bearer-token authenticated JSON API. All endpoints are version-flat: the same server build serves both hosted (syncbins.com) and self-hosted boxes. Payloads are validated against the zod schemas in shared/src/api.ts — that file is the source of truth.
Conventions
Section titled “Conventions”- Base URL:
https://<your-box>/api. Hosted ishttps://syncbins.com/api. - Auth:
Authorization: Bearer <session_token>. Tokens are issued at pair time and rotated on revocation. - IDs: ULIDs (Crockford Base32, 26 chars). Lexicographically sortable by creation time.
- Timestamps: milliseconds since epoch, integer.
- Binary fields: base64 (standard, no URL-safe).
- Errors: JSON envelope under
error(see below). HTTP status reflects the failure class.
curl https://box.yourdomain.com/api/items?since=4218 \ -H "Authorization: Bearer ${SB_TOKEN}" \ -H "Accept: application/json"const res = await fetch(`${origin}/api/items?since=${since}`, { headers: { 'Authorization': `Bearer ${token}`, },});if (!res.ok) throw new SyncBinsError(await res.json());The core of the API. Items are append-mostly — you create them, fetch them since a version cursor, and soft-delete them.
/items auth: device Create or replace an item. Idempotent on the client-supplied id.
idstringrequired — Client-generated ULID.bin_idstringrequired — Target bin.typeContentTyperequired — One of the 10 content types.tsintegerrequired — Client timestamp (ms).noncebase64(24)required — XChaCha20 nonce.ctbase64required — Ciphertext. Max 64 KB inline; useblob_reffor larger.wrappedWrappedKey[]required — K_item wrapped per active device pubkey.blob_refstring | null— Storage backend reference when ct lives in blob storage.
{ "id": "01JC4M9KJ7XAHQ3RT8VFNPE2QH", "bin_id": "01JC4ABCDEFGH", "type": "password", "ts": 1747843261000, "nonce": "base64(24 bytes)", "ct": "base64(ciphertext)", "wrapped": [ { "device_id": "…", "key": "base64(48 bytes)" } ], "blob_ref": null}{ "id": "01JC4M9KJ7XAHQ3RT8VFNPE2QH", "version": 4218, "server_ts": 1747843261103}/items auth: device Page items since a version cursor. Used for catch-up after reconnect.
sinceintegerrequired — Last version the client saw.limitinteger— Default 200, max 1000.bin_idstring— Filter to a single bin.
{ "items": [ { "id": "…", "version": 4218, "bin_id": "…", "type": "link", "ct": "…", "wrapped": [], "ts": 1747843001000 } ], "head_version": 4218, "has_more": false}/items/:id auth: device Soft delete. Hard delete runs nightly after 30 days.
idstringrequired — ULID of the item.
For items larger than 64 KB. Two-step flow: request a short-lived SAS URL, upload the ciphertext directly to your storage backend, then create the item with a blob_ref pointing at it.
/blobs/upload-url auth: device Get a pre-signed upload URL (Azure SAS / S3 / R2). Expires in 15 min.
sizeintegerrequired — Ciphertext size in bytes. Server enforces tenant cap.content_typestring— Defaults toapplication/octet-stream.
/blobs/:ref/url auth: device Short-lived download URL (5 min). Refresh on 403.
Devices & pairing
Section titled “Devices & pairing”Multi-step handshake described in How encryption works → Pairing. Each step is its own endpoint so the existing device can stay in control of when to release the wrapped master key.
/devices/pair-init auth: device Existing device starts a pair session, returns the 6-word code lookup id.
/devices/pair-fetch-epk auth: none (rate-limited) New device redeems the 6-word code for the existing device's ephemeral pubkey.
codestringrequired — BIP39 6-word string entered by the user.
/devices/pair-fetch-npk auth: none New device submits its pubkey + attestation. Server forwards to existing device over WS.
/devices/pair-wrap auth: device Existing device wraps the master key to the new device's pubkey.
/devices auth: device List paired devices. Used by Settings → Devices.
/devices/:id/revoke auth: device Revoke a device. Token is invalidated immediately; bundle of wrapped keys is purged on next item put.
Bootstrap (first device)
Section titled “Bootstrap (first device)”A brand-new server has no devices and no auth. The first device claims it through a one-shot bootstrap. After this returns, every subsequent device must go through pairing.
/devices/bootstrap auth: none · one-shot Claim an empty server. 410 Gone once a device exists.
/devices/bootstrap/restart auth: admin · self-host only Re-arm bootstrap. Wipes all device records. Items are kept but unrecoverable.
Account password
Section titled “Account password”/password/set auth: device Set or change the account password (second factor).
/password/verify auth: none Verify before unsealing the master key on a new device.
/password/exists auth: none Probe — is a password configured? Used by the unlock screen.
Multi-tenant (hosted)
Section titled “Multi-tenant (hosted)”Only mounted when TENANT_MODE=multi. Self-hosted boxes can ignore these.
/signup auth: none Allocate a new tenant + subdomain.
/tls-check auth: none Has Caddy provisioned a certificate for this subdomain yet?
/billing/webhook auth: stripe Stripe webhook receiver (subscription events).
/billing/portal auth: device Issues a Stripe customer portal session URL.
/me auth: device Current device info, server version, tenant stats.
/syncbin/status auth: none Liveness + bootstrap state. Used by deploy scripts.
Error envelope
Section titled “Error envelope”Every non-2xx response carries a JSON body with a stable error.code. See Error codes for the full catalogue and recovery guidance.
{ "error": { "code": "item_too_large", "message": "Inline payload limit is 64 KB. Use POST /blobs/upload-url for larger items.", "hint": "See /reference/errors#item_too_large" }}| Status | Class | Common codes |
|---|---|---|
400 | Bad request | invalid_payload, item_too_large |
401 | Unauthenticated | missing_token, expired_token |
403 | Forbidden | device_revoked, tenant_suspended |
404 | Not found | item_not_found, bin_not_found |
409 | Conflict | bootstrap_already_done, device_exists |
413 | Too large | blob_too_large |
429 | Rate limited | rate_limited — backoff in Retry-After |
500 | Server | internal — open a ticket |