Shelf API v1

Shelf HTTP API v1

Developer reference

The complete contract for automating a running Shelf desktop app over local HTTP.

Default base URLhttp://localhost:9876/api/v1

From off to automated in three steps.

01

Enable the server

Open Settings → API server, enable it, choose the bind interface and port, then save.

02

Set the base URL

BASE='http://localhost:9876/api/v1'

03

Check health

curl "$BASE/health" should return {"status":"ok","version":"v1","locked":false}.

Local and unauthenticated

The API uses plain HTTP, has no API key or token, and does not enable browser CORS. Keep it bound to localhost unless every device and user that can reach it is trusted.

Conventions

  • JSON properties use camelCase. JSON request bodies require content-type: application/json.
  • Successful endpoints currently return 200 OK.
  • Most mutations return the complete current AppSnapshot.
  • Timestamps are RFC 3339 UTC strings. Optional response fields are present as null.
  • Bookmark URLs are limited to HTTP/HTTPS, normalized, and stripped of fragments.
  • POST creates ignore body IDs; PUT routes use the path ID.
  • Protected routes return 423 Locked while Shelf is locked.
  • Filesystem paths are paths on the computer running Shelf.

Shared schemas

BookmarkDraft

Used by bookmark create and replace requests.

PropertyTypeRequiredBehavior
idstring | nullNoIgnored by HTTP create/replace routing.
urlstringYesNormalized; only HTTP and HTTPS.
titlestring | nullNoURL domain when absent or blank.
descriptionstring | nullNoBlank becomes null.
imageUrl, faviconUrlstring | nullNoPage image and favicon URLs.
siteName, author, publishedAtstring | nullNoOptional source metadata.
tagsstring[]NoDefaults to []; normalized and deduplicated.
collectionIdstring | nullNoUnknown IDs become null.
favorite, archivedbooleanNoEach defaults to false.
notesstring | nullNoBlank becomes null.
archiveMode"none" | "html"NoDefaults to "none"; html queues capture.
fetchMetadatabooleanNoCreate only; defaults to false.
metadataBehavior"add-new-only" | "override"NoCreate only; defaults to add-new-only.

Bookmark

{ "id": string, "url": string, "title": string, "description": string | null, "imageUrl": string | null, "faviconUrl": string | null, "siteName": string | null, "author": string | null, "publishedAt": string | null, "domain": string, "tags": string[], "collectionId": string | null, "favorite": boolean, "archived": boolean, "notes": string | null, "createdAt": RFC3339, "updatedAt": RFC3339, "lastOpenedAt": RFC3339 | null, "openCount": number, "archive": { "mode": "none" | "html", "folder": string | null, "updatedAt": RFC3339 | null, "htmlAvailable": boolean, "textAvailable": boolean } }

PaginatedBookmarks

Returned by the bookmark list endpoint.

{ "bookmarks": Bookmark[], "offset": number, "limit": number, "total": number }

Collection

{ "id": string, "name": string, "color": string, "createdAt": RFC3339 }

SavedQuery

{ "id": string, "name": string, "query": string, "createdAt": RFC3339, "updatedAt": RFC3339 }

AppSnapshot

{ "version": number, "bookmarks": Bookmark[], "collections": Collection[], "savedQueries": SavedQuery[], "tags": [{ "name": string, "count": number }], "dataPath": string, "dataDirectory": string, "dataFileName": string, "archiveDirectory": string, "pendingArchives": [{ "id": string, "mode": "html", "queuedAt": RFC3339 }] }

Discovery & health

GET

/

Discover the API from the listener root. Available while locked.

Request

No body or parameters.

Response · 200

{ "name": "Shelf API", "version": "v1", "basePath": "/api/v1", "note": string, "resources": string[] }

Example

curl 'http://localhost:9876/'

Errors: Standard routing errors only.

GET

/api/v1

Return the same discovery document from the versioned base path.

Request

No body or parameters.

Response · 200

Discovery document (same shape as GET /).

Example

curl "$BASE"

Errors: Standard routing errors only.

GET

/api/v1/health

Check the listener and current app-lock state. Available while locked.

Request

No body or parameters.

Response · 200

{ "status": "ok", "version": "v1", "locked": boolean }

Example

curl "$BASE/health"

Errors: Standard routing errors only.

Snapshot

GET

/api/v1/snapshot

Return the complete library, saved queries, tag counts, storage paths, archive path, and pending archive queue.

Request

No body or parameters.

Response · 200

AppSnapshot

Example

curl "$BASE/snapshot"

Errors: 423 Shelf is locked.

Bookmarks

GET

/api/v1/bookmarks?offset=0&limit=100

List bookmarks, including archived bookmarks, using zero-based offset pagination. Shelf’s desktop filter row is evaluated locally against its current snapshot; these controls are not additional query properties on this endpoint.

Desktop filterBookmark data and behavior
Date addedCompare the local calendar date derived from createdAt with an inclusive From/To range. Either boundary may be omitted.
DomainMatch domain against any selected domain. The desktop multi-select is searchable; multiple selections use OR.
Has saved copyRequire archive.htmlAvailable to be true.
FavoriteRequire favorite to be true.
Never openedRequire openCount to be 0 and lastOpenedAt to be null.
Has notesRequire non-empty notes.
No tagsRequire an empty tags array.
Combined filters: Active desktop criteria are combined with AND. Use GET /api/v1/snapshot for a complete client-side filter pass, or paginate through this endpoint before filtering.

Query parameters

offset?: non-negative integer // defaults to 0; bookmarks to skip limit?: positive integer // defaults to 100; 1–1,000 inclusive

Response · 200

PaginatedBookmarks // bookmarks: Bookmark[] // offset, limit, total: number
Paging: total is the complete count at request time, independent of offset and limit. Request the next page with offset + limit while that offset is less than total. An offset at or beyond total returns an empty bookmarks array. Pages may shift if bookmarks change between requests; restart for a consistent traversal.

Example

curl "$BASE/bookmarks?offset=0&limit=100"

Errors: 400 malformed offset/limit or limit outside 1–1,000; 423 Shelf is locked.

POST

/api/v1/bookmarks

Create a bookmark. Metadata is fetched only when requested; HTML mode queues background capture.

Request body

BookmarkDraft & { "fetchMetadata"?: boolean, "metadataBehavior"?: "add-new-only" | "override" }

Response · 200

AppSnapshot // includes the generated ID and normalized URL

Example

curl -X POST "$BASE/bookmarks" \
  -H 'content-type: application/json' \
  -d '{
    "url":"https://example.com/article#intro",
    "tags":["reading","example"],
    "favorite":true,
    "fetchMetadata":true,
    "metadataBehavior":"add-new-only",
    "archiveMode":"none"
  }'

Errors: 400 invalid input; 409 duplicate URL; 423 locked; 502 requested metadata fetch failed.

POST

/api/v1/bookmarks/batch

Atomically create 1–1,000 bookmarks. Validation and requested metadata fetches complete before anything is inserted.

Request body

{ "bookmarks": Array<BookmarkDraft & { "fetchMetadata"?: boolean, "metadataBehavior"?: "add-new-only" | "override" }> }

Response · 200

AppSnapshot // includes every inserted bookmark

Example

curl -X POST "$BASE/bookmarks/batch" \
  -H 'content-type: application/json' \
  -d '{"bookmarks":[
    {"url":"https://example.com/a","tags":["imported"]},
    {"url":"https://example.org/b","fetchMetadata":true,
     "metadataBehavior":"override"}
  ]}'

Errors: 400 empty/oversized batch or invalid item; 409 duplicate existing/in-batch URL; 423 locked; 502 any requested metadata fetch failed. No items are inserted on failure.

GET

/api/v1/bookmarks/:id

Return one bookmark.

Path

id: UUID string

Response · 200

Bookmark

Example

curl "$BASE/bookmarks/$ID"

Errors: 404 bookmark missing; 423 locked.

PUT

/api/v1/bookmarks/:id

Replace all editable fields. Omitted defaulted fields reset to their defaults; send the complete desired state.

Path + body

id: UUID string body: BookmarkDraft // path id overrides body id

Response · 200

AppSnapshot

Example

curl -X PUT "$BASE/bookmarks/$ID" \
  -H 'content-type: application/json' \
  -d '{
    "url":"https://example.com/article",
    "title":"Updated article",
    "tags":["reading","updated"],
    "collectionId":null,
    "favorite":true,
    "archived":false,
    "notes":"Review later",
    "archiveMode":"html"
  }'

Errors: 400 invalid input; 404 missing; 409 duplicate URL; 423 locked.

DELETE

/api/v1/bookmarks/:id

Delete the bookmark, pending archive work, and saved-copy folder.

Path

id: UUID string

Response · 200

AppSnapshot

Example

curl -X DELETE "$BASE/bookmarks/$ID"

Errors: 404 missing; 423 locked.

PATCH

/api/v1/bookmarks/:id/status

Update either or both status flags. Omitted flags remain unchanged.

Request body

{ "favorite"?: boolean, "archived"?: boolean }

Response · 200

AppSnapshot

Example

curl -X PATCH "$BASE/bookmarks/$ID/status" \
  -H 'content-type: application/json' \
  -d '{"favorite":true,"archived":false}'

Errors: 404 missing; 423 locked.

POST

/api/v1/bookmarks/:id/opened

Increment openCount and set lastOpenedAt. Does not open a browser.

Path

id: UUID string No request body.

Response · 200

AppSnapshot

Example

curl -X POST "$BASE/bookmarks/$ID/opened"

Errors: 404 missing; 423 locked.

Metadata

POST

/api/v1/metadata

Fetch and parse metadata without changing the library. Available while locked.

Request body

{ "url": string }

Response · 200

{ "url": string, "title": string, "description": string | null, "imageUrl": string | null, "faviconUrl": string | null, "siteName": string | null, "author": string | null, "publishedAt": string | null, "domain": string }

Example

curl -X POST "$BASE/metadata" \
  -H 'content-type: application/json' \
  -d '{"url":"https://example.com/article"}'

Errors: 400 invalid URL/page or response over 3 MB; 502 mapped fetch/timeout failure.

POST

/api/v1/bookmarks/:id/metadata

Refresh stored URL and metadata. Tags, collection, flags, and notes remain unchanged.

Path

id: UUID string No request body.

Response · 200

AppSnapshot

Example

curl -X POST "$BASE/bookmarks/$ID/metadata"

Errors: 400 invalid/concurrently changed data or response over 3 MB; 404 missing; 409 final URL duplicates another bookmark; 423 locked; 502 fetch failure.

Saved copies & archive search

Set archiveMode: "html" during create/replace to enable capture. The PUT route refreshes an enabled copy; DELETE removes and disables it.

PUT

/api/v1/bookmarks/:id/archive

Capture or refresh saved HTML. A JSON body is required; mode defaults to html when omitted.

Request body

{ "mode"?: "html" }

Response · 200

AppSnapshot

Example

curl -X PUT "$BASE/bookmarks/$ID/archive" \
  -H 'content-type: application/json' \
  -d '{"mode":"html"}'

Errors: 400 capture/filesystem/page failure; 404 missing; 423 locked; 502 fetch failure.

DELETE

/api/v1/bookmarks/:id/archive

Remove the saved-copy folder, clear pending work, and reset mode to none.

Path

id: UUID string No request body.

Response · 200

AppSnapshot

Example

curl -X DELETE "$BASE/bookmarks/$ID/archive"

Errors: 400 filesystem failure; 404 missing; 423 locked.

GET

/api/v1/bookmarks/:id/archive/html

Return saved raw HTML and the captured URL used as its render base.

Path

id: UUID string

Response · 200

{ "html": string, "baseUrl": string }

Example

curl "$BASE/bookmarks/$ID/archive/html"

Errors: 400 no copy configured; 404 bookmark/copy missing; 423 locked.

POST

/api/v1/bookmarks/:id/archive/export

Write saved raw HTML to a path on the Shelf host.

Request body

{ "path": string }

Response · 200

{ "exported": true }

Example

curl -X POST "$BASE/bookmarks/$ID/archive/export" \
  -H 'content-type: application/json' \
  -d '{"path":"/Users/example/Exports/page.html"}'

Errors: 400 no configured copy/filesystem failure; 404 bookmark/copy missing; 423 locked.

GET

/api/v1/archives/search?q=...

Search saved-copy text using exact, prefix, typo-tolerant, and subsequence scoring. Blank queries return [].

Query

q?: string

Response · 200

[{ "id": string, "score": number }]

Example

curl --get "$BASE/archives/search" \
  --data-urlencode 'q=rust archive'

Errors: 400 archive read failure; 423 locked.

POST

/api/v1/archives/process-pending

Process each currently queued job at most once. Retryable failures remain queued.

Request

No body or parameters.

Response · 200

AppSnapshot

Example

curl -X POST "$BASE/archives/process-pending"

Errors: 400 queue/state failure; 423 locked. Individual capture failures stay queued instead of failing the request.

Saved queries

Saved queries store search text, not bookmark membership or cached result IDs. Selecting one reruns the existing search against the latest bookmark metadata and saved-page text.

GET

/api/v1/saved-queries

List saved queries in creation order.

Request

No body or parameters.

Response · 200

SavedQuery[]

Example

curl "$BASE/saved-queries"

Errors: 423 locked.

POST

/api/v1/saved-queries

Save a named query. Names are unique without regard to case; query text must use supported Boolean operators, non-empty terms, and balanced parentheses.

Request body

{ "name": string, "query": string }

Response · 200

AppSnapshot

Example

curl -X POST "$BASE/saved-queries" \
  -H 'content-type: application/json' \
  -d '{"name":"Rust tutorials","query":"#rust AND tutorial -video"}'

Errors: 400 invalid input; 409 duplicate name; 423 locked.

PUT

/api/v1/saved-queries/:id

Replace a saved query’s name and query text. The path ID is authoritative.

Request body

{ "name": string, "query": string }

Response · 200

AppSnapshot

Example

curl -X PUT "$BASE/saved-queries/$SAVED_QUERY_ID" \
  -H 'content-type: application/json' \
  -d '{"name":"Rust learning","query":"#rust AND (tutorial OR guide) -video"}'

Errors: 400 invalid input; 404 missing; 409 duplicate name; 423 locked.

DELETE

/api/v1/saved-queries/:id

Delete a saved query without changing any bookmarks.

Path

id: UUID string

Response · 200

AppSnapshot

Example

curl -X DELETE "$BASE/saved-queries/$SAVED_QUERY_ID"

Errors: 404 missing; 423 locked.

Collections

GET

/api/v1/collections

List collections.

Request

No body or parameters.

Response · 200

Collection[]

Example

curl "$BASE/collections"

Errors: 423 locked.

POST

/api/v1/collections

Create a collection. Blank/omitted color defaults to #e87752.

Request body

{ "name": string, "color"?: string }

Response · 200

AppSnapshot

Example

curl -X POST "$BASE/collections" \
  -H 'content-type: application/json' \
  -d '{"name":"Reading","color":"#e46f4b"}'

Errors: 400 missing/blank name; 409 duplicate case-insensitive name; 423 locked.

PUT

/api/v1/collections/:id

Replace a collection’s name and color. The path ID wins over any body ID.

Request body

{ "name": string, "color": string }

Response · 200

AppSnapshot

Example

curl -X PUT "$BASE/collections/$COLLECTION_ID" \
  -H 'content-type: application/json' \
  -d '{"name":"Long reads","color":"#788b6b"}'

Errors: 400 invalid input; 404 missing; 409 duplicate name; 423 locked.

DELETE

/api/v1/collections/:id

Delete a collection and set collectionId to null on affected bookmarks.

Path

id: UUID string

Response · 200

AppSnapshot

Example

curl -X DELETE "$BASE/collections/$COLLECTION_ID"

Errors: 404 missing; 423 locked.

App lock

GET

/api/v1/security

Return app-lock status. Available while locked.

Request

No body or parameters.

Response · 200

{ "passwordConfigured": boolean, "timeoutMinutes": number, "locked": boolean }

Example

curl "$BASE/security"

Errors: Standard routing errors only.

PUT

/api/v1/security

Enable, change, or remove the password and set the inactivity timeout. Passwords must be 8–1,024 Unicode characters; timeout must be 1–1,440 minutes.

Request body

{ "currentPassword"?: string | null, "newPassword"?: string | null, "timeoutMinutes": number, "removePassword": boolean }

Response · 200

{ "passwordConfigured": boolean, "timeoutMinutes": number, "locked": boolean }

Example · enable

curl -X PUT "$BASE/security" \
  -H 'content-type: application/json' \
  -d '{
    "newPassword":"correct horse battery staple",
    "timeoutMinutes":15,
    "removePassword":false
  }'
To change or remove an existing password, include currentPassword. For removal, set removePassword: true. To change only the timeout, omit newPassword.

Errors: 400 invalid timeout/password/current credentials/removal request; 423 existing lock is currently locked.

POST

/api/v1/security/lock

Lock Shelf immediately when a password exists. Available while locked.

Request

No body or parameters.

Response · 200

{ "passwordConfigured": boolean, "timeoutMinutes": number, "locked": boolean }

Example

curl -X POST "$BASE/security/lock"

Errors: Standard routing errors only.

POST

/api/v1/security/unlock

Unlock Shelf. With no configured password, returns the current snapshot. Available while locked.

Request body

{ "password": string }

Response · 200

AppSnapshot

Example

curl -X POST "$BASE/security/unlock" \
  -H 'content-type: application/json' \
  -d '{"password":"correct horse battery staple"}'

Errors: 400 incorrect password or invalid stored password settings.

Storage & CSV

These routes use paths on the Shelf host and never open native file dialogs.

PUT

/api/v1/storage

Copy the SQLite library and archives to a new location, activate that database, and persist the preference. Directory must be absolute; a .sqlite3 extension is added when absent.

Request body

{ "directory": string, "fileName": string, "overwrite": boolean }

Response · 200

AppSnapshot

Example

curl -X PUT "$BASE/storage" \
  -H 'content-type: application/json' \
  -d '{
    "directory":"/Users/example/Documents/Shelf",
    "fileName":"bookmarks.sqlite3",
    "overwrite":false
  }'

Errors: 400 invalid path/name or copy/write failure; 409 target exists without overwrite; 423 locked.

POST

/api/v1/import/csv

Import a native Shelf CSV from a host path. The complete file is validated before mutation; invalid rows are skipped.

Request body

{ "path": string }

Response · 200

{ "imported": number, "updated": number, "skipped": number }

Example

curl -X POST "$BASE/import/csv" \
  -H 'content-type: application/json' \
  -d '{"path":"/Users/example/Imports/bookmarks.csv"}'
A URL column is required. Accepted aliases include url/link/uri, title/name, collection/folder/group, and tags/labels/keywords. Raindrop source selection is desktop-only.

Errors: 400 file/CSV parsing failure; 423 locked.

POST

/api/v1/export/csv

Export all bookmarks to a CSV file on the host. Collections use names and tags are joined with |.

Request body

{ "path": string }

Response · 200

{ "exported": number }

Example

curl -X POST "$BASE/export/csv" \
  -H 'content-type: application/json' \
  -d '{"path":"/Users/example/Exports/shelf.csv"}'

Errors: 400 host file/write/CSV failure; 423 locked.

Error format & status codes

Shelf application errors use this JSON shape:

{ "error": "That bookmark no longer exists" }
StatusMeaning
200Request completed successfully.
400Validation, credentials, URL, storage, CSV, archive, or general operation failure; also malformed JSON.
404Resource or route not found.
409Duplicate bookmark URL, collection name, saved-query name, or existing storage target.
413Request body exceeded the framework limit.
415JSON body sent without application/json.
422JSON properties or types do not match the request schema.
423Protected library data was requested while Shelf was locked.
502Mapped remote-page fetch or timeout failure.

Framework-generated routing, unsupported method, query, or JSON-extraction failures may be plain text. Unsupported methods normally return 405 Method Not Allowed.

Operational notes

  • Listener enablement/interface/port are configured in Shelf Settings, not over HTTP.
  • Listener reconfiguration restores the previous listener if the new bind or preference write fails.
  • Bookmark writes requesting HTML capture can return before capture completes; inspect pendingArchives.
  • Archive availability is derived from the files currently present on disk.
  • Metadata and archive capture contact remote URLs from the Shelf host.
  • Storage/import/export paths are constrained by the Shelf process’s host filesystem permissions.