Bookmarks
Create, read, update, search, and delete bookmarks
List Bookmarks
GET /bookmarksScope: bookmarks:read
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
limit | number | Max results to return (default: 50) |
collectionId | string | Filter by collection ID |
domain | string | Filter by domain (e.g., github.com) |
type | string | Filter by type: link, article, image, video, document |
isFavorite | boolean | Filter favorites only |
sortBy | string | Sort field: newest (default), oldest, title, domain |
cursor | string | Opaque pagination cursor from a previous response. Pass to fetch the next page |
Response:
{
"data": {
"data": [
{
"id": "abc123",
"url": "https://example.com",
"title": "Example Site",
"description": "An example website",
"excerpt": "",
"note": "My personal notes",
"type": "link",
"isFavorite": false,
"tags": ["dev", "reference"],
"collectionId": "col_xyz",
"domain": "example.com",
"favicon": "https://example.com/favicon.ico",
"coverImage": null,
"clickCount": 0,
"metadataStatus": "complete",
"linkStatus": "valid",
"lastCheckedAt": "2026-01-15T12:00:00Z",
"httpStatusCode": 200,
"createdAt": "2026-01-15T10:30:00Z"
}
],
"cursor": "eyJwb3MiOjUwfQ",
"hasMore": true,
"truncated": false
},
"meta": {
"requestId": "req_abc123"
}
}Pagination:
The response data object includes cursor-based pagination fields alongside the bookmark array:
| Field | Type | Description |
|---|---|---|
data.data | array | Array of bookmark objects |
data.cursor | string | null | Opaque cursor to pass as the cursor query parameter for the next page. null when there are no more results |
data.hasMore | boolean | Whether more results exist beyond this page |
data.truncated | boolean | true when the sort covered only the newest matches and more matches exist that cannot be fetched (see below) |
meta.requestId | string | Unique request identifier |
All sort modes (newest, oldest, title, domain) return the same envelope shape and page with cursor, with one exception: a title or domain sort combined with isFavorite=true (and no domain, collectionId or type filter). That combination sorts only a window of the most recently saved favorites and cannot continue, so cursor is null, hasMore is false, and truncated is true when more favorites exist beyond that window. Use newest or oldest to page through every favorite.
Get Bookmark
GET /bookmarks/:idScope: bookmarks:read
Response: single bookmark object (same shape as list items)
Create Bookmark
POST /bookmarksScope: bookmarks:write
Body:
{
"url": "https://example.com",
"title": "Example Site",
"description": "Optional description",
"note": "Optional personal note",
"tags": ["dev", "reference"],
"collectionId": "col_xyz",
"type": "link",
"isFavorite": false
}| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | The URL to bookmark |
title | string | No | Override the auto-detected title |
description | string | No | Override the auto-detected description |
note | string | No | Personal notes |
tags | string[] | No | Tags to apply |
collectionId | string | No | Collection to add to |
type | string | No | Content type override |
isFavorite | boolean | No | Mark as favorite (default: false) |
If the URL already exists in your trash, it will be restored instead of creating a duplicate.
Update Bookmark
PATCH /bookmarks/:idScope: bookmarks:write
Body: any subset of bookmark fields (partial update)
{
"title": "Updated Title",
"note": "New notes"
}Delete Bookmark (Soft)
DELETE /bookmarks/:idScope: bookmarks:write
Moves the bookmark to trash. It can be restored within 30 days.
Restore Bookmark
POST /bookmarks/:id/restoreScope: bookmarks:write
Restores a trashed bookmark.
Permanently Delete Bookmark
DELETE /bookmarks/:id/permanentScope: destructive
Permanently deletes the bookmark and all associated data (tags, highlights, uploads). This cannot be undone.
Set Favorite
PUT /bookmarks/:id/favoriteScope: bookmarks:write
Body:
{
"isFavorite": true
}Move Bookmark
POST /bookmarks/:id/moveScope: bookmarks:write
Body:
{
"collectionId": "col_xyz"
}Pass null for collectionId to move to Unsorted.
Search Bookmarks
GET /bookmarks/searchScope: bookmarks:read
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
q | string | Yes | Search query (alias: query). Searches title, description, and URL |
Response: array of matching bookmarks
List Domains
GET /bookmarks/domainsScope: bookmarks:read
Returns all domains with bookmark counts.
{
"data": [
{ "domain": "github.com", "count": 42 },
{ "domain": "stackoverflow.com", "count": 15 }
]
}Get Stats
GET /bookmarks/statsScope: bookmarks:read
{
"data": {
"total": 1234,
"favorites": 89,
"unsorted": 45,
"typeCounts": {
"link": 800,
"article": 300,
"video": 100,
"image": 30,
"document": 4
},
"noTags": 200
}
}Cover Image
Upload, replace, or remove a bookmark's cover image. Uses a three-step upload flow:
- Request an upload URL (creates a server-side reservation)
- Upload the image binary directly to the storage URL
- Confirm the upload to attach it to the bookmark
Requirements: A paid plan (Pro). Free accounts cannot upload cover images.
Limits: 10 MB per file, 500 MB total storage. Accepted types: JPEG, PNG, GIF, WebP.
Get Upload URL
POST /bookmarks/:id/cover/upload-urlScope: bookmarks:write
Response:
{
"data": {
"uploadUrl": "https://storage.example.com/upload?token=...",
"reservationId": "res_abc123"
}
}The uploadUrl is a temporary, single-use URL valid for 15 minutes. You may have up to 5 outstanding reservations at a time.
Upload the Image
Upload the binary file directly to the uploadUrl returned above. This request goes to the storage service, not the API:
curl -X POST "$UPLOAD_URL" \
-H "Content-Type: image/jpeg" \
--data-binary @photo.jpgResponse:
{
"storageId": "kg2abc123..."
}Save Cover Image
POST /bookmarks/:id/coverScope: bookmarks:write
Body:
{
"storageId": "kg2abc123...",
"reservationId": "res_abc123"
}| Field | Type | Required | Description |
|---|---|---|---|
storageId | string | Yes | Storage ID returned from the upload step |
reservationId | string | Yes | Reservation ID from the upload URL step |
The server validates the file type, size, and storage quota before accepting. If the bookmark already has a cover image, the old one is replaced and its storage is freed.
Response:
{
"data": {
"url": "https://storage.example.com/api/storage/abc-123"
}
}Remove Cover Image
DELETE /bookmarks/:id/coverScope: bookmarks:write
Removes the cover image and frees the storage quota.
Response:
{
"data": {
"success": true
}
}Cover Image Errors
| Code | Status | Description |
|---|---|---|
INVALID_FILE_TYPE | 422 | File is not JPEG, PNG, GIF, or WebP |
FILE_TOO_LARGE | 422 | File exceeds 10 MB |
STORAGE_LIMIT_REACHED | 422 | Total storage quota (500 MB) exceeded |
RESERVATION_INVALID | 400 | Reservation expired, already used, or doesn't match this bookmark |
TOO_MANY_PENDING_UPLOADS | 429 | 5 outstanding upload reservations already exist |
Bookmark Tags
List Tags on a Bookmark
GET /bookmarks/:id/tagsScope: bookmarks:read
Response:
{
"data": [
{
"id": "tag_abc",
"name": "development",
"color": "blue",
"parentId": null,
"count": 42,
"createdAt": "2026-01-10T08:00:00Z"
}
],
"meta": {
"requestId": "req_abc123"
}
}Add Tag to Bookmark
POST /bookmarks/:id/tagsScope: tags:write
Body:
{
"name": "important"
}Remove Tag from Bookmark
DELETE /bookmarks/:id/tags/:tagIdScope: tags:write
List Bookmarks by Tag Name
Also documented under Tags.
GET /tags/:name/bookmarksScope: bookmarks:read
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
limit | number | Max results to return (default: 50, max: 200) |
Response:
{
"data": {
"bookmarks": [ ... ],
"tag": {
"id": "tag_abc",
"name": "development",
"color": "blue",
"parentId": null,
"count": 42,
"createdAt": "2026-01-10T08:00:00Z"
}
},
"meta": {
"requestId": "req_abc123"
}
}The tag field is null if the tag name does not match any existing tag.
List Bookmarks by Tag ID
Also documented under Tags.
GET /tags/by-id/:id/bookmarksScope: bookmarks:read
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
limit | number | Max results to return (default: 50, max: 200) |
Same as above but uses the tag's ID instead of its name. Useful for hierarchical tags with / in their names.
Response: same shape as GET /tags/:name/bookmarks — { data: { bookmarks, tag }, meta }.