---
name: bookie
description: Search, save, and organize bookmarks in Bookie via its REST API. Use when the user asks to save or bookmark a link, find something they saved, look up a URL from their bookmarks, tag or retag bookmarks, or tidy up their bookmark collection.
---

# Bookie

Bookie is a bookmark manager with a REST API. This skill drives that API with `curl`.

## Setup

Two environment variables:

| Variable         | Required | Default                       |
| ---------------- | -------- | ----------------------------- |
| `BOOKIE_API_KEY` | Yes      | none                          |
| `BOOKIE_URL`     | No       | `https://bookie.edwardes.xyz` |

Check the key before the first request:

```bash
test -n "$BOOKIE_API_KEY" || echo "BOOKIE_API_KEY is not set"
```

If it is unset, stop and tell the user to create a key at `https://bookie.edwardes.xyz/app/settings/api-keys` and export it. Do not guess a key or read one out of a file the user did not point you at.

Every request needs the `x-api-key` header. Set a base for the rest of the session:

```bash
BOOKIE_URL="${BOOKIE_URL:-https://bookie.edwardes.xyz}"
```

## Confirm before writing

Run `GET` requests freely.

**Ask the user before every `POST`, `PUT`, or `DELETE`.** Show the exact request you intend to send and wait for a yes. Deleting a bookmark is permanent and there is no undo.

## Endpoints

Seven endpoints. Everything below is the whole API.

### GET /api/bookmarks: search and list

```bash
curl -s -H "x-api-key: $BOOKIE_API_KEY" \
  "$BOOKIE_URL/api/bookmarks?q=svelte&page=1&limit=20"
```

| Param   | Default | Notes                                                     |
| ------- | ------- | --------------------------------------------------------- |
| `q`     | `""`    | Matches alias, title, url, and description. Empty = all.  |
| `page`  | `1`     | Positive integer.                                         |
| `limit` | `50`    | Positive integer, **max 500**. Above 500 returns a `500`. |

Response:

```json
{
  "bookmarks": [
    {
      "id": 42,
      "url": "https://svelte.dev",
      "title": "Svelte",
      "description": null,
      "alias": "svelte",
      "isFavorite": true,
      "isArchived": false,
      "clickCount": 7,
      "lastVisited": null,
      "createdAt": "2026-01-15T10:00:00.000Z",
      "updatedAt": "2026-01-20T08:30:00.000Z",
      "userId": "user_abc123",
      "bookmarkToTags": [{ "tag": { "id": 2, "name": "frontend", "color": null } }]
    }
  ],
  "pagination": { "page": 1, "limit": 50, "total": 142, "totalPages": 3, "hasMore": true }
}
```

Notes:

- Tags arrive nested under `bookmarkToTags[].tag`, not as a flat `tags` array.
- A `rank` field appears on each result only when `q` is non-empty.
- `favicon` and `screenshot` are omitted here. Fetch a single bookmark to get them.
- Page through with `pagination.hasMore`, not by guessing.

### POST /api/bookmarks: create

```bash
curl -s -X POST -H "x-api-key: $BOOKIE_API_KEY" -H "Content-Type: application/json" \
  "$BOOKIE_URL/api/bookmarks" \
  -d '{"url":"https://svelte.dev","title":"Svelte","tagIds":["frontend","docs"]}'
```

| Field         | Required | Notes                                           |
| ------------- | -------- | ----------------------------------------------- |
| `url`         | Yes      | Not format-validated. Send a real absolute URL. |
| `title`       | No       | Defaults to the URL with the protocol stripped. |
| `description` | No       |                                                 |
| `isFavorite`  | No       | Defaults to `false`.                            |
| `tagIds`      | No       | Numbers **or strings**. See below.              |
| `alias`       | No       | Short link slug. Only `a-z`, `0-9`, `/`, `-`.   |

`tagIds` accepts a mix of numeric tag IDs and tag names. A non-numeric string is looked up by name and **created if it does not exist**. So `"tagIds": ["frontend"]` just works; there is no need to call `POST /api/tags` first.

Returns `200` (not `201`) with the created bookmark. The response is the raw row, so `bookmarkToTags` is absent even when you sent tags. Re-fetch by ID if you need to confirm the tags landed.

Errors: `400` with a JSON array of Zod issues; `400 Alias is not available: <alias>` when the alias is taken.

### GET /api/bookmarks/:id: fetch one

```bash
curl -s -H "x-api-key: $BOOKIE_API_KEY" "$BOOKIE_URL/api/bookmarks/42"
```

Includes `bookmarkToTags[].tag`, `favicon`, and `screenshot`. Returns `404` when the bookmark does not exist or belongs to someone else.

### PUT /api/bookmarks/:id: partial update

```bash
curl -s -X PUT -H "x-api-key: $BOOKIE_API_KEY" -H "Content-Type: application/json" \
  "$BOOKIE_URL/api/bookmarks/42" -d '{"isFavorite":true}'
```

Only the fields you send change. Accepts `url`, `title`, `description`, `isFavorite`, `tagIds`, `alias`.

Tag semantics matter:

- `tagIds` present → **replaces** every existing tag association.
- `tagIds: []` → removes all tags.
- `tagIds` omitted → tags untouched.

To add one tag without losing the others, GET the bookmark first, then send the existing tag IDs plus the new one.

`description: null` and `alias: null` clear those fields. `title: ""` falls back to the URL-derived title.

### DELETE /api/bookmarks/:id

```bash
curl -s -X DELETE -H "x-api-key: $BOOKIE_API_KEY" "$BOOKIE_URL/api/bookmarks/42"
```

Returns `{"success": true, "message": "Bookmark deleted successfully"}`. Deleting an ID that does not exist **also** reports success, so confirm the bookmark exists with a GET before reporting a deletion to the user.

### GET /api/tags

```bash
curl -s -H "x-api-key: $BOOKIE_API_KEY" "$BOOKIE_URL/api/tags"
```

Returns a bare array, not an object: `[{"id":1,"name":"frontend","color":null,"userId":"...","createdAt":"..."}]`.

### POST /api/tags

```bash
curl -s -X POST -H "x-api-key: $BOOKIE_API_KEY" -H "Content-Type: application/json" \
  "$BOOKIE_URL/api/tags" -d '{"name":"frontend"}'
```

Returns `201`. The name is trimmed and lowercased, 1 to 50 characters. A duplicate name hits a unique index and fails with a `500`, so check `GET /api/tags` first. Usually you do not need this endpoint at all. Pass tag names in `tagIds` instead.

## Recipes

Find a bookmark and show just the useful fields:

```bash
curl -s -H "x-api-key: $BOOKIE_API_KEY" "$BOOKIE_URL/api/bookmarks?q=svelte&limit=10" \
  | jq -r '.bookmarks[] | "\(.id)\t\(.title)\t\(.url)"'
```

List every tag with its ID:

```bash
curl -s -H "x-api-key: $BOOKIE_API_KEY" "$BOOKIE_URL/api/tags" | jq -r '.[] | "\(.id)\t\(.name)"'
```

Add a tag to a bookmark without dropping its existing tags:

```bash
ID=42
EXISTING=$(curl -s -H "x-api-key: $BOOKIE_API_KEY" "$BOOKIE_URL/api/bookmarks/$ID" \
  | jq -c '[.bookmarkToTags[].tag.id]')
curl -s -X PUT -H "x-api-key: $BOOKIE_API_KEY" -H "Content-Type: application/json" \
  "$BOOKIE_URL/api/bookmarks/$ID" \
  -d "{\"tagIds\": $(echo "$EXISTING" | jq -c '. + ["reading-list"]')}"
```

Fetch every bookmark across all pages:

```bash
page=1
while :; do
  body=$(curl -s -H "x-api-key: $BOOKIE_API_KEY" "$BOOKIE_URL/api/bookmarks?page=$page&limit=500")
  echo "$body" | jq -c '.bookmarks[]'
  [ "$(echo "$body" | jq -r '.pagination.hasMore')" = "true" ] || break
  page=$((page + 1))
done
```

Find duplicate URLs:

```bash
curl -s -H "x-api-key: $BOOKIE_API_KEY" "$BOOKIE_URL/api/bookmarks?limit=500" \
  | jq -r '.bookmarks | group_by(.url) | map(select(length > 1)) | .[][] | "\(.id)\t\(.url)"'
```

## Limits

- **No bulk endpoints.** Bulk tagging, bulk delete, import, and duplicate detection exist only in the web UI. Over the API, loop one bookmark at a time and confirm with the user first.
- **No tag update or delete over HTTP.** Renaming or deleting a tag is web-UI only, at `/app/bookmarks/tags`.
- **Rate limit:** 10,000 requests per 24 hours per key.
- **`401` responses have an empty body.** That means a missing, revoked, or mistyped key, not a permissions problem with a specific bookmark.
- A bookmark you do not own is indistinguishable from one that does not exist; both return `404`.
