# ClipJot > Personal bookmark manager with web UI, REST API, browser extension, and mobile apps. Save links from anywhere, access them everywhere. ClipJot is a self-hosted bookmark manager built with FastHTML and HTMX. It stores bookmarks in SQLite, supports tagging and full-text search, and authenticates via Google or GitHub OAuth. Clients include a web interface, a Chrome extension, and iOS/Android apps. ## Authentication All API endpoints except `POST /api/v1/auth/invite` require a Bearer token in the Authorization header. ``` Authorization: Bearer YOUR_TOKEN ``` Tokens are obtained via: - OAuth login (Google or GitHub) which creates a session token - API tokens created in Settings > API Tokens with scope `read` or `write` - Invite code exchange via `POST /api/v1/auth/invite` Token scopes: - `read`: List, search, and export bookmarks and tags - `write`: All read permissions plus create, edit, delete, and import ## API Endpoints ### Authentication #### POST /api/v1/user/profile Returns the authenticated user's profile information including email, account type, and creation date. - Scope: `read` Response: - `email` (string): User's email address - `is_premium` (boolean): Whether user has premium account - `created_at` (string): ISO 8601 timestamp of account creation Example: ```bash curl -X POST https://your-domain.com/api/v1/user/profile \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` #### POST /api/v1/auth/invite Exchange an invite code for a session token. This is a public endpoint that does not require authentication. - Scope: none (public endpoint) Request body (JSON): - `code` (string (required)): 8-character invite code (uppercase alphanumeric) Response: - `token` (string): Session token for API authentication - `user` (object): User object with id and email Example: ```bash curl -X POST https://your-domain.com/api/v1/auth/invite \ -H "Content-Type: application/json" \ -d '{"code": "ABC12345"}' ``` #### POST /api/v1/logout Revokes the current session token, logging out the user. - Scope: `read` Response: - `logged_out` (boolean): Always true on success - `message` (string): Confirmation message Example: ```bash curl -X POST https://your-domain.com/api/v1/logout \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` ### Bookmarks #### POST /api/v1/bookmarks/add Add a new bookmark with optional title, comment, and tags. Tags are created automatically if they don't exist. - Scope: `write` Request body (JSON): - `url` (string (required)): The URL to bookmark - `title` (string): Display title (optional) - `comment` (string): Personal note or comment (optional) - `tags` (array): List of tag names (optional) Response: - `id` (integer): Unique bookmark ID - `url` (string): The bookmarked URL - `title` (string): Display title - `comment` (string): Personal note - `tags` (array): List of tag objects - `created_at` (string): ISO 8601 timestamp - `updated_at` (string): ISO 8601 timestamp Example: ```bash curl -X POST https://your-domain.com/api/v1/bookmarks/add \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/article", "title": "Interesting Article", "comment": "Read this later", "tags": ["reading", "tech"] }' ``` #### POST /api/v1/bookmarks/edit Update the title, comment, or tags of an existing bookmark. Only provided fields are updated. - Scope: `write` Request body (JSON): - `id` (integer (required)): Bookmark ID to update - `title` (string): New title (optional) - `comment` (string): New comment (optional) - `tags` (array): New tag list (replaces existing tags) Response: - `id` (integer): Bookmark ID - `url` (string): The bookmarked URL - `title` (string): Updated title - `comment` (string): Updated comment - `tags` (array): Updated tag list - `updated_at` (string): ISO 8601 timestamp Example: ```bash curl -X POST https://your-domain.com/api/v1/bookmarks/edit \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "id": 123, "title": "Updated Title", "tags": ["important", "work"] }' ``` #### POST /api/v1/bookmarks/delete Permanently delete a bookmark by ID. - Scope: `write` Request body (JSON): - `id` (integer (required)): Bookmark ID to delete Response: - `deleted` (boolean): Always true on success Example: ```bash curl -X POST https://your-domain.com/api/v1/bookmarks/delete \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"id": 123}' ``` #### POST /api/v1/bookmarks/search Search bookmarks by query string with pagination. Searches URL, title, and comment fields. - Scope: `read` Request body (JSON): - `query` (string): Search query (searches URL, title, comment) - `tag` (string): Filter by tag name - `page` (integer): Page number (default: 1) - `per_page` (integer): Results per page (default: 50, max: 100) Response: - `bookmarks` (array): List of bookmark objects - `total` (integer): Total matching bookmarks - `page` (integer): Current page number - `per_page` (integer): Results per page - `has_more` (boolean): Whether more results exist Example: ```bash curl -X POST https://your-domain.com/api/v1/bookmarks/search \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "query": "python tutorial", "page": 1, "per_page": 20 }' ``` #### POST /api/v1/bookmarks/list Get all bookmarks with cursor-based pagination. Use for full sync or bulk operations. - Scope: `read` Request body (JSON): - `cursor` (string): Pagination cursor from previous response - `limit` (integer): Max results (default: 100, max: 500) Response: - `bookmarks` (array): List of bookmark objects - `has_more` (boolean): Whether more results exist - `next_cursor` (string): Cursor for next page (if has_more is true) Example: ```bash curl -X POST https://your-domain.com/api/v1/bookmarks/list \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"limit": 100}' ``` #### POST /api/v1/bookmarks/sync Get new bookmarks since a cursor position. Supports long polling to wait for changes. - Scope: `read` Request body (JSON): - `cursor` (integer): Last seen bookmark ID (0 for initial sync) - `limit` (integer): Max results (default: 50, max: 100) - `wait` (boolean): Enable long polling (wait up to 30s for new data) Response: - `bookmarks` (array): New bookmarks since cursor - `cursor` (integer): New cursor position - `has_more` (boolean): Whether more results exist - `waited` (boolean): Whether request waited (long polling) Example: ```bash # Initial sync curl -X POST https://your-domain.com/api/v1/bookmarks/sync \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"cursor": 0}' # Long polling for changes curl -X POST https://your-domain.com/api/v1/bookmarks/sync \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"cursor": 123, "wait": true}' ``` ### Tags #### POST /api/v1/tags/list Get all tags for the current user with bookmark counts. - Scope: `read` Response: - `tags` (array): List of tag objects with id, name, and bookmark_count Example: ```bash curl -X POST https://your-domain.com/api/v1/tags/list \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` #### POST /api/v1/tags/create Create a new tag. Returns error if tag name already exists. - Scope: `write` Request body (JSON): - `name` (string (required)): Tag name Response: - `id` (integer): Tag ID - `name` (string): Tag name - `created_at` (string): ISO 8601 timestamp Example: ```bash curl -X POST https://your-domain.com/api/v1/tags/create \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "recipes"}' ``` #### POST /api/v1/tags/update Update the name of an existing tag. - Scope: `write` Request body (JSON): - `id` (integer (required)): Tag ID to update - `name` (string (required)): New tag name Response: - `id` (integer): Tag ID - `name` (string): Updated tag name Example: ```bash curl -X POST https://your-domain.com/api/v1/tags/update \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"id": 5, "name": "cooking"}' ``` #### POST /api/v1/tags/delete Delete a tag. The tag is removed from all bookmarks but bookmarks are not deleted. - Scope: `write` Request body (JSON): - `id` (integer (required)): Tag ID to delete Response: - `deleted` (boolean): Always true on success Example: ```bash curl -X POST https://your-domain.com/api/v1/tags/delete \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"id": 5}' ``` ### Import & Export #### POST /api/v1/export Export all bookmarks as JSON. Includes tags for each bookmark. - Scope: `read` Response: - `bookmarks` (array): All bookmarks with tags - `exported_at` (string): ISO 8601 timestamp - `count` (integer): Total bookmark count Example: ```bash curl -X POST https://your-domain.com/api/v1/export \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{}' > bookmarks.json ``` #### POST /api/v1/import Import bookmarks from JSON. Supports merge (skip duplicates) or replace mode. - Scope: `write` Request body (JSON): - `bookmarks` (array (required)): Array of bookmark objects to import - `mode` (string): 'merge' (default) or 'replace' Response: - `imported` (integer): Number of bookmarks imported - `skipped` (integer): Number of duplicates skipped - `errors` (array): List of import errors Example: ```bash curl -X POST https://your-domain.com/api/v1/import \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "bookmarks": [ {"url": "https://example.com", "title": "Example", "tags": ["test"]} ], "mode": "merge" }' ``` ## Error Responses All errors return JSON: ```json {"error": "Human-readable message", "code": "ERROR_CODE"} ``` - `INVALID_TOKEN` (HTTP 401): The provided token is invalid, expired, or missing. - `PERMISSION_DENIED` (HTTP 403): The token does not have the required scope for this operation. - `VALIDATION_ERROR` (HTTP 400): The request body is missing required fields or contains invalid data. - `NOT_FOUND` (HTTP 404): The requested resource (bookmark, tag, etc.) was not found. - `RATE_LIMITED` (HTTP 429): Too many requests. Check the Retry-After header. - `LIMIT_EXCEEDED` (HTTP 403): Account limit reached (e.g., max bookmarks for free tier). ## Rate Limits Default: 100 requests per 60 seconds per token. Exceeding the limit returns HTTP 429 with a `Retry-After` header.