Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
13 KiB
API Reference
Base URL: http://localhost:3000
Auth: GET endpoints are public. POST, PUT, PATCH, and DELETE require authentication via session cookie or X-API-Key header. See authentication.md for details.
Prices are stored and returned in cents (e.g., 2500 = $25.00).
Weights are in grams.
Timestamps are unix epoch integers.
Table of Contents
Health
GET /api/health
Returns server status. Always public.
Response:
{ "status": "ok" }
Items
GET /api/items
List all items in the collection.
Response:
[
{
"id": 1,
"name": "Revelate Tangle Frame Bag",
"categoryId": 2,
"weightGrams": 185,
"priceCents": 12000,
"notes": "Medium size, fits 2020 Surly Straggler",
"productUrl": "https://revelatedesigns.com/...",
"imageFilename": "1710000000000-uuid.jpg",
"imageSourceUrl": "https://example.com/image.jpg",
"createdAt": 1710000000,
"updatedAt": 1710000000
}
]
GET /api/items/:id
Get a single item by ID.
Response: item object (see above), or 404 { "error": "Item not found" }.
POST /api/items
Create a new item. Auth required.
Request:
{
"name": "Revelate Tangle Frame Bag",
"categoryId": 2,
"weightGrams": 185,
"priceCents": 12000,
"notes": "Medium size",
"productUrl": "https://revelatedesigns.com/...",
"imageFilename": "1710000000000-uuid.jpg",
"imageSourceUrl": "https://example.com/image.jpg"
}
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Item name |
categoryId |
integer | yes | ID of an existing category |
weightGrams |
number | no | Weight in grams (non-negative) |
priceCents |
integer | no | Price in cents (non-negative) |
notes |
string | no | Free-text notes |
productUrl |
string | no | URL to product page |
imageFilename |
string | no | Filename from a prior image upload |
imageSourceUrl |
string | no | Original URL the image came from |
Response: 201 — created item object.
PUT /api/items/:id
Update an existing item. All fields are optional. Auth required.
Request: same fields as POST, all optional.
Response: updated item object, or 404.
DELETE /api/items/:id
Delete an item and clean up its image file if one exists. Auth required.
Response:
{ "success": true }
Returns 404 if item not found.
Categories
GET /api/categories
List all categories.
Response:
[
{ "id": 1, "name": "Uncategorized", "icon": "package" },
{ "id": 2, "name": "Bags", "icon": "backpack" }
]
POST /api/categories
Create a new category. Auth required.
Request:
{ "name": "Bags", "icon": "backpack" }
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Category name |
icon |
string | no | Icon name (defaults to "package") |
Response: 201 — created category object.
PUT /api/categories/:id
Update a category. Auth required.
Request:
{ "name": "Carry", "icon": "bag" }
Response: updated category object, or 404.
DELETE /api/categories/:id
Delete a category. Auth required.
Response:
{ "success": true }
Threads
Research threads track multiple candidate options for a single gear slot before committing to a purchase.
GET /api/threads
List threads. By default only active (unresolved) threads are returned.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
includeResolved |
boolean | false |
Set to true to include resolved threads |
Example: GET /api/threads?includeResolved=true
Response:
[
{
"id": 1,
"name": "Handlebar bag",
"categoryId": 2,
"status": "active",
"resolvedCandidateId": null,
"createdAt": 1710000000,
"updatedAt": 1710000000
}
]
POST /api/threads
Create a new research thread. Auth required.
Request:
{ "name": "Handlebar bag", "categoryId": 2 }
Response: 201 — created thread object.
GET /api/threads/:id
Get a thread with all its candidates.
Response:
{
"id": 1,
"name": "Handlebar bag",
"categoryId": 2,
"status": "active",
"resolvedCandidateId": null,
"candidates": [
{
"id": 10,
"threadId": 1,
"name": "Revelate Sweetroll",
"categoryId": 2,
"weightGrams": 290,
"priceCents": 13500,
"notes": "16L, fits most drop bars",
"productUrl": "https://revelatedesigns.com/...",
"imageFilename": null,
"imageSourceUrl": null,
"status": "researching",
"pros": "Waterproof, large capacity",
"cons": "Pricey",
"sortOrder": 0,
"createdAt": 1710000000,
"updatedAt": 1710000000
}
]
}
Returns 404 if thread not found.
PUT /api/threads/:id
Update a thread's name or category. Auth required.
Request:
{ "name": "Bar bag", "categoryId": 2 }
Response: updated thread object, or 404.
DELETE /api/threads/:id
Delete a thread and all its candidates. Cleans up any candidate image files. Auth required.
Response:
{ "success": true }
POST /api/threads/:id/candidates
Add a candidate to a thread. Auth required.
Request:
{
"name": "Revelate Sweetroll",
"categoryId": 2,
"weightGrams": 290,
"priceCents": 13500,
"notes": "16L capacity",
"productUrl": "https://revelatedesigns.com/...",
"imageFilename": "1710000000000-uuid.jpg",
"imageSourceUrl": "https://example.com/image.jpg",
"status": "researching",
"pros": "Waterproof, large capacity",
"cons": "Expensive"
}
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Candidate name |
categoryId |
integer | yes | Category ID |
weightGrams |
number | no | Weight in grams |
priceCents |
integer | no | Price in cents |
notes |
string | no | Notes |
productUrl |
string | no | Product URL |
imageFilename |
string | no | Filename from a prior image upload |
imageSourceUrl |
string | no | Original image URL |
status |
string | no | "researching", "ordered", or "arrived" |
pros |
string | no | Pros of this option |
cons |
string | no | Cons of this option |
Response: 201 — created candidate object. Returns 404 if thread not found.
PUT /api/threads/:threadId/candidates/:candidateId
Update a candidate. All fields optional. Auth required.
Request: same fields as POST, all optional.
Response: updated candidate object, or 404.
DELETE /api/threads/:threadId/candidates/:candidateId
Delete a candidate and clean up its image. Auth required.
Response:
{ "success": true }
PATCH /api/threads/:id/candidates/reorder
Reorder candidates within a thread. Auth required.
Request:
{ "orderedIds": [12, 10, 11] }
orderedIds is an array of candidate IDs in the desired display order. All IDs must belong to the thread.
Response:
{ "success": true }
Returns 400 with an error message if validation fails.
POST /api/threads/:id/resolve
Resolve a thread by selecting the winning candidate. Auth required.
The winning candidate's data is copied into a new item in the collection. The thread status is set to "resolved" and resolvedCandidateId is set.
Request:
{ "candidateId": 10 }
Response:
{
"success": true,
"item": {
"id": 42,
"name": "Revelate Sweetroll",
"categoryId": 2,
"weightGrams": 290,
"priceCents": 13500
}
}
Returns 400 if the candidate does not belong to the thread or another error occurs.
Setups
Setups are named collections of items (e.g., "Bikepacking weekend", "Commute loadout").
GET /api/setups
List all setups with item counts and weight/cost totals.
Response:
[
{
"id": 1,
"name": "Bikepacking weekend",
"itemCount": 12,
"totalWeightGrams": 4800,
"totalPriceCents": 285000,
"createdAt": 1710000000,
"updatedAt": 1710000000
}
]
POST /api/setups
Create a new setup. Auth required.
Request:
{ "name": "Bikepacking weekend" }
Response: 201 — created setup object.
GET /api/setups/:id
Get a setup with its full item list.
Response:
{
"id": 1,
"name": "Bikepacking weekend",
"items": [
{
"id": 1,
"name": "Revelate Tangle Frame Bag",
"weightGrams": 185,
"priceCents": 12000,
"categoryId": 2,
"classification": "base"
}
],
"totalWeightGrams": 185,
"totalPriceCents": 12000
}
Returns 404 if setup not found.
PUT /api/setups/:id
Update a setup's name. Auth required.
Request:
{ "name": "Bikepacking overnighter" }
Response: updated setup object, or 404.
PUT /api/setups/:id/items
Replace all items in a setup atomically. Deletes all existing setup-item associations and re-inserts with the provided IDs. Auth required.
Request:
{ "itemIds": [1, 5, 12, 17] }
Pass an empty array to remove all items from the setup.
Response:
{ "success": true }
DELETE /api/setups/:id
Delete a setup. Does not delete the items themselves. Auth required.
Response:
{ "success": true }
Images
POST /api/images
Upload an image file. Auth required.
Request: multipart/form-data with an image field.
POST /api/images
Content-Type: multipart/form-data; boundary=...
--boundary
Content-Disposition: form-data; name="image"; filename="bag.jpg"
Content-Type: image/jpeg
<binary data>
--boundary--
Constraints:
- Accepted types:
image/jpeg,image/png,image/webp - Maximum size: 5MB
- Files are saved to
./uploads/with a UUID-based filename
Response: 201
{ "filename": "1710000000000-550e8400-e29b-41d4-a716-446655440000.jpg" }
Use the returned filename as imageFilename when creating or updating items or candidates.
POST /api/images/from-url
Fetch an image from a remote URL and save it locally. Auth required.
Request:
{ "url": "https://example.com/product-image.jpg" }
Response: 201
{
"filename": "1710000000000-uuid.jpg",
"sourceUrl": "https://example.com/product-image.jpg"
}
Returns 400 for invalid URLs, unsupported content types, files over 5MB, or non-200 HTTP responses.
Settings
Key-value store for application settings.
GET /api/settings/:key
Get a setting by key. Public.
Response:
{ "key": "collectionName", "value": "My Bikepacking Gear" }
Returns 404 if the key does not exist.
PUT /api/settings/:key
Create or update a setting. Auth required.
Request:
{ "value": "My Bikepacking Gear" }
Response: updated setting object.
Totals
GET /api/totals
Global and per-category weight and cost totals. Computed from current item data. Public.
Response:
{
"global": {
"totalWeightGrams": 12500,
"totalPriceCents": 450000,
"itemCount": 34
},
"categories": [
{
"categoryId": 2,
"name": "Bags",
"icon": "backpack",
"totalWeightGrams": 1200,
"totalPriceCents": 78000,
"itemCount": 5
}
]
}
Auth
Authentication endpoints are documented in authentication.md.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/auth/me |
public | Current session state |
| POST | /api/auth/setup |
public | Create first admin user |
| POST | /api/auth/login |
public | Log in, receive cookie |
| POST | /api/auth/logout |
public | Clear session |
| PUT | /api/auth/password |
session | Change password |
| GET | /api/auth/keys |
required | List API keys |
| POST | /api/auth/keys |
required | Create API key |
| DELETE | /api/auth/keys/:id |
required | Revoke API key |