Skip to contentMenu Download OpenVireo

The HTTP API of openvireo-sync-server. It stores encrypted .kdbx files and never sees their contents. See docs/api.md for how the pieces fit together.

Which routes exist depends on SYNC_AUTH_MODE: standalone has the database and pairing routes, jwt the vault and internal routes, both all of them. /health and /events always exist. Routes the mode doesn’t have answer 404. The internal routes also need SYNC_INTERNAL_TOKEN to be set.

CORS: for origins in SYNC_ALLOWED_ORIGINS every route answers preflight with 204, allowing GET, HEAD, PUT, POST, DELETE and the headers Authorization, Content-Type, If-Match, If-None-Match, and responses expose ETag, X-Database-Revision, Last-Modified.

Every response has Cache-Control: no-store.

The machine-readable description is openapi.yaml (OpenAPI 3.1.0); import it into an HTTP client or a code generator. HTTP API explains how the routes fit together.

Authentication

Scheme
deviceA device token (self-hosted). From users add, devices add or a pairing claim.
accessAn account access token from the account server: EdDSA, aud containing SYNC_AUTH_AUDIENCE (default ov-sync), and a vlt claim mapping vault ids to read, write or manage.
internalThe SYNC_INTERNAL_TOKEN shared secret.

Health

Liveness

GET /api/v1/health

Liveness check (getHealth)

Token: none

Status
200The server is up (object)

Databases

A self-hosted user’s databases, with device tokens

GET /api/v1/databases

List the user’s databases (listDatabases)

Token: device

Status
200OK (object)
401Missing, invalid or expired token (Error)
403The token doesn’t grant this, or is the wrong kind of token for the route (Error)

GET /api/v1/databases/{id}

Download a database (getDatabase)

HEAD works the same and returns no body.

Token: device

ParameterInType
idpathstring
If-None-Match (optional)headerstring* on uploads (create only), an ETag on downloads (304 when unchanged)
Status
200The encrypted file (string (binary))
304The ETag in If-None-Match is still current
401Missing, invalid or expired token (Error)
404Not found (Error)

PUT /api/v1/databases/{id}

Upload a database (putDatabase)

If-None-Match: * creates it (412 if it exists). If-Match: "<etag>" replaces exactly that version (412 if another device uploaded in the meantime). With neither header the upload creates the database, or gets 428 if it already exists. Sends a database event to the user’s other streams.

Token: device

ParameterInType
idpathstring
If-Match (optional)headerstring
If-None-Match (optional)headerstring* on uploads (create only), an ETag on downloads (304 when unchanged)

Request body (application/octet-stream): The encrypted database, starting with the KDBX file signature

Status
200200 when it replaced a version, 201 when it created the database or vault (Meta)
201200 when it replaced a version, 201 when it created the database or vault (Meta)
400Invalid id, JSON or body (Error)
401Missing, invalid or expired token (Error)
412If-Match or If-None-Match didn’t hold; download, merge and try again (Error)
413Over SYNC_MAX_DB_BYTES; no body
415The body isn’t a KDBX file (Error)
428The upload needs If-Match (update) or If-None-Match (create) (Error)

DELETE /api/v1/databases/{id}

Delete a database and its history (deleteDatabase)

Token: device

ParameterInType
idpathstring
If-Match (optional)headerstring
Status
204Deleted
401Missing, invalid or expired token (Error)
404Not found (Error)
412If-Match or If-None-Match didn’t hold; download, merge and try again (Error)

GET /api/v1/databases/{id}/history

The current and kept versions, newest first (listDatabaseVersions)

Token: device

ParameterInType
idpathstring
Status
200OK (object)
401Missing, invalid or expired token (Error)
404Not found (Error)

GET /api/v1/databases/{id}/history/{revision}

Download one kept version (getDatabaseVersion)

Token: device

ParameterInType
idpathstring
revisionpathinteger
Status
200The encrypted file (string (binary))
401Missing, invalid or expired token (Error)
404Not found (Error)

Devices

Pairing new devices

POST /api/v1/devices/pairing

Create a pairing code for another device of the same user (createPairingCode)

Works once and for 10 minutes. A user has at most 5 codes waiting; a new one replaces the oldest.

Token: device

Status
201Created (object)
401Missing, invalid or expired token (Error)

POST /api/v1/devices/pairing/claim

Trade a pairing code for a device token (claimPairingCode)

Case, spaces and the dash in the code don’t matter. Wrong codes are limited to 10 per client address and 100 in all per 10 minutes; after that, claims get 429 with Retry-After.

Token: none

Request body (application/json)

FieldType
codestring
deviceNamestring
Status
201The new device and its token, shown only once (object)
400Invalid id, JSON or body (Error)
429Too many wrong codes (Error)

Vaults

Account vaults, with access tokens from the account server

GET /api/v1/vaults

The vaults in the token’s vlt claim that exist (any role) (listVaults)

Token: access

Status
200OK (object)
401Missing, invalid or expired token (Error)
403The token doesn’t grant this, or is the wrong kind of token for the route (Error)

GET /api/v1/vaults/{vault}

Download a vault (read) (getVault)

HEAD works the same and returns no body. A vault the token doesn’t list gets 403, whether it exists or not.

Token: access

ParameterInType
vaultpathstring (uuid)
If-None-Match (optional)headerstring* on uploads (create only), an ETag on downloads (304 when unchanged)
Status
200The encrypted file (string (binary))
304The ETag in If-None-Match is still current
400Invalid id, JSON or body (Error)
401Missing, invalid or expired token (Error)
403The token doesn’t grant this, or is the wrong kind of token for the route (Error)
404Not found (Error)

PUT /api/v1/vaults/{vault}

Create (manage) or update (write) a vault (putVault)

If-None-Match: * creates it and needs the manage role. If-Match: "<etag>" updates it and needs write. Without either header the upload always gets 428. Sends a vault event to the streams whose token lists the vault.

Token: access

ParameterInType
vaultpathstring (uuid)
If-Match (optional)headerstring
If-None-Match (optional)headerstring* on uploads (create only), an ETag on downloads (304 when unchanged)

Request body (application/octet-stream): The encrypted database, starting with the KDBX file signature

Status
200200 when it replaced a version, 201 when it created the database or vault (Meta)
201200 when it replaced a version, 201 when it created the database or vault (Meta)
400Invalid id, JSON or body (Error)
401Missing, invalid or expired token (Error)
403The token doesn’t grant this, or is the wrong kind of token for the route (Error)
412If-Match or If-None-Match didn’t hold; download, merge and try again (Error)
413Over SYNC_MAX_DB_BYTES; no body
415The body isn’t a KDBX file (Error)
428The upload needs If-Match (update) or If-None-Match (create) (Error)

DELETE /api/v1/vaults/{vault}

Delete a vault and its history (manage) (deleteVault)

Token: access

ParameterInType
vaultpathstring (uuid)
If-Match (optional)headerstring
Status
204Deleted; the vault’s streams get a vault event with deleted true
400Invalid id, JSON or body (Error)
401Missing, invalid or expired token (Error)
403The token doesn’t grant this, or is the wrong kind of token for the route (Error)
404Not found (Error)
412If-Match or If-None-Match didn’t hold; download, merge and try again (Error)

GET /api/v1/vaults/{vault}/history

The current and kept versions, newest first (read) (listVaultVersions)

Token: access

ParameterInType
vaultpathstring (uuid)
Status
200OK (object)
401Missing, invalid or expired token (Error)
403The token doesn’t grant this, or is the wrong kind of token for the route (Error)
404Not found (Error)

GET /api/v1/vaults/{vault}/history/{revision}

Download one kept version (read) (getVaultVersion)

Token: access

ParameterInType
vaultpathstring (uuid)
revisionpathinteger
Status
200The encrypted file (string (binary))
401Missing, invalid or expired token (Error)
403The token doesn’t grant this, or is the wrong kind of token for the route (Error)
404Not found (Error)

Events

Change notifications (server-sent events)

GET /api/v1/events

Change notifications as server-sent events (streamEvents)

Starts with retry: 5000; a : ping comment every 25 seconds keeps it open.

With a device token: event: database, data {"id", "etag", "revision"} or {"id", "deleted": true}, for every change to one of the user’s databases.

With an access token: event: vault (same data, for the vaults in the token) and event: access, data {"vault": uuid|null, "change": "granted"|"revoked"|"rotating"|"rotated"|"org"}, when the account server reports an access change for the token’s subject. The stream ends once the token’s exp has passed; the client reconnects with a fresh token, and also after any access event.

Token: device or access

Status
200The stream (string)
401Missing, invalid or expired token (Error)

Internal

Called by the account server only

POST /api/v1/internal/notify

Push an access event to the open streams of some subjects (notifyAccess)

Delivery is best effort. With Postgres, the event reaches streams on every instance.

Token: internal

Request body (application/json)

FieldType
subjectsarray of Principal
event"access"
dataobject
Status
204Sent
400Invalid id, JSON or body (Error)
401Missing, invalid or expired token (Error)

DELETE /api/v1/internal/vaults/{vault}

Delete a vault and its history when no client does, e.g. an expired recovery archive (deleteVaultInternal)

Token: internal

ParameterInType
vaultpathstring (uuid)
Status
204Deleted; the vault’s streams get a vault event with deleted true
400Invalid id, JSON or body (Error)
401Missing, invalid or expired token (Error)
404Not found (Error)

Schemas

Meta

FieldType
idstringThe database id or vault UUID
revisioninteger
etagstringThe quoted SHA-256 of the file
sizeintegerBytes
updatedAtstring (date-time)

Principal

string, matching ^(user|sa):[0-9a-f-]{36}$

Error

FieldType
errorstringA message for people
code (optional)invalid_request, unauthorized, forbidden, not_found, conflict, precondition_failed, precondition_required, rate_limited, internal_errorOnly on the vault and internal routes, and on /events when access tokens are accepted

The Sync Server docs cover OpenVireo Sync Server, develop (0d4f752), last updated 26 October 2026. Found a mistake? Edit this page on GitHub.