xbook exposes a REST API at /api/v1/ for programmatic access. The CLI uses this same API.
No authentication required. All requests are treated as the local user.
All API endpoints are rate-limited per client IP.
| Endpoint | Limit | Window |
|---|---|---|
General API (/api/v1/*) |
100 requests | 60 seconds |
Sync (POST /api/v1/sync) |
5 requests | 60 seconds |
When a rate limit is exceeded, the API returns 429 Too Many Requests. Rate limit state is tracked in-memory.
All endpoints return errors in this format:
{ "error": "Error message here" }| Status | Meaning |
|---|---|
| 400 | Bad request (invalid parameters or body) |
| 404 | Resource not found |
| 429 | Rate limit exceeded (check Retry-After header) |
| 500 | Server error |
Health check. Returns server status.
Authentication: None required.
Response:
{
"status": "ok",
"mode": "local",
"version": "0.1.0",
"timestamp": "2026-01-01T00:00:00.000Z",
"database": "ok"
}| Field | Type | Description |
|---|---|---|
status |
string | "ok" or "degraded" (returns 503 when degraded) |
mode |
string | "local" (SQLite) |
version |
string | Server version |
timestamp |
string | Current server time (ISO 8601) |
database |
string | "ok" or "error" |
databaseMessage |
string? | Error details (only present when database is "error") |
Returns the local user's profile. Used by xbook login to validate connections.
Response:
{
"userId": "local",
"name": "Local User",
"mode": "local"
}List and search bookmarks with pagination and filtering.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
search |
string | — | Free-text search |
folder |
string | — | Filter by folder ID |
author |
string | — | Filter by author username |
starred |
boolean | — | Filter starred bookmarks (true/false) |
need_to_read |
boolean | — | Filter need-to-read bookmarks |
tags |
string | — | Comma-separated tag list |
page |
integer | 1 | Page number |
page_size |
integer | 20 | Results per page (max 100) |
order_by |
string | — | Sort field |
order_dir |
string | — | Sort direction (asc or desc) |
Response:
{
"items": [
{
"tweet_id": "1234567890",
"text": "Great thread on...",
"author_username": "johndoe",
"author_name": "John Doe",
"starred": false,
"folder_id": "folder_abc",
"folder_name": "Tech",
"created_at": "2026-01-01T00:00:00.000Z"
}
],
"total": 42,
"page": 1,
"totalPages": 3
}Get a single bookmark by tweet ID.
Response: The bookmark object (same shape as items in the list response), or 404 if not found.
Update a bookmark's metadata (notes, tags, starred, folder).
Request Body (all fields optional):
{
"starred": true,
"need_to_read": true,
"notes": "Remember to revisit this",
"add_tags": ["ai", "must-read"],
"remove_tags": ["old-tag"],
"folder_id": "folder_abc",
"folder_name": "Tech"
}| Field | Type | Description |
|---|---|---|
starred |
boolean | Set starred status |
need_to_read |
boolean | Set need-to-read status |
notes |
string or null | Set bookmark notes (max 10,000 chars) |
add_tags |
string[] | Tags to add |
remove_tags |
string[] | Tags to remove |
folder_id |
string or null | Move to folder (null to remove from folder) |
folder_name |
string or null | Folder display name (used with folder_id) |
Response:
{ "success": true }Delete a bookmark.
Response:
{ "success": true }List all bookmark folders with counts.
Response:
{
"folders": [
{ "id": "folder_abc", "name": "Tech", "count": 15 },
{ "id": "folder_def", "name": "Design", "count": 8 }
]
}Dashboard statistics and sync history.
Response:
{
"stats": {
"totalBookmarks": 142,
"folderCount": 5,
"lastSyncAt": "2026-01-01T09:00:00.000Z",
"lastNewsletterAt": "2026-01-01T10:00:00.000Z",
"bookmarksByFolder": [
{ "folder": "Tech", "count": 42 },
{ "folder": "Design", "count": 18 }
]
},
"syncHistory": [
{
"synced_at": "2026-01-01T09:00:00.000Z",
"bookmarks_fetched": 50,
"bookmarks_new": 3
}
]
}Trigger a bookmark sync from X. Requires X OAuth tokens to be configured.
Request Body: None.
Response:
{
"success": true,
"fetched": 50,
"newCount": 3,
"foldersFound": 2
}Errors:
401-- Not authenticated (X account not connected)409-- Sync already in progress
Send or preview the weekly bookmark newsletter digest.
Request Body:
{
"dry_run": true
}| Field | Type | Default | Description |
|---|---|---|---|
dry_run |
boolean | false |
Preview without sending |
Response (dry run):
{
"success": true,
"subject": "Your xbook digest — Jan 1",
"count": 5,
"html": "<html>..."
}Response (send):
{
"success": true,
"message": "Newsletter sent with 5 bookmarks",
"count": 5
}Response (no new bookmarks):
{
"success": true,
"message": "No new bookmarks to send",
"count": 0
}Errors:
400-- No newsletter email configured500--RESEND_API_KEYnot set
Import bookmarks from a JSON or CSV file.
Accepts three input formats:
- Multipart file upload (
Content-Type: multipart/form-data) -- file field namedfile - Raw content (
Content-Type: application/json) --{ "content": "...", "filename": "bookmarks.json" } - Pre-parsed data (
Content-Type: application/json) --{ "tweets": [...], "users": {...} }
File size limit: 10 MB.
Response:
{
"success": true,
"total": 100,
"imported": 85,
"skipped": 15,
"errors": 0,
"format": "json-flat",
"warnings": []
}| Field | Type | Description |
|---|---|---|
total |
integer | Total items processed |
imported |
integer | New bookmarks added |
skipped |
integer | Duplicates skipped |
errors |
integer | Items that failed |
format |
string | Detected format (json-flat, json-twitter-exporter, csv) |
warnings |
string[] | Parse warnings |
See Import Formats for file format details.