- Docs
- Integrations
- API reference
API reference
Search, save, and organize your Stashr library over HTTPS from any language, script, or agent with the Stashr REST API.
The Stashr API is a small JSON API over HTTPS. It is the same API the official CLI and MCP server are built on, so anything they can do, you can do directly. All endpoints live under:
https://stashr.me/v1API access is part of Pro and included in every free trial. On Hobby, every endpoint except /v1/me returns a payment-required error.
A machine-readable OpenAPI 3.1 description of everything on this page is served at /openapi.json.
Authentication
Every request authenticates with a bearer token:
curl https://stashr.me/v1/me \
-H "Authorization: Bearer $STASHR_API_KEY"API keys
Create keys under Settings → API keys in the web app. Keys start with stashr_, are shown once at creation, and carry one of three access levels:
| Access level | Scopes | Allows |
|---|---|---|
| Read | read | Reading and searching |
| Write | read, write | Plus saving, updating, archiving, and restoring |
| Full | read, write, destructive | Plus collection deletion |
Use the smallest level your integration needs: read access for lookup-only automation, write for saving and organizing, and full only when collection deletion is required.
OAuth
Interactive clients can use OAuth instead of a long-lived key. The CLI and MCP connections handle this automatically with stashr login or your AI tool's connection flow, so you rarely need to implement it yourself. If you do, Stashr publishes standard discovery metadata at /.well-known/oauth-authorization-server and /.well-known/oauth-protected-resource/v1, and access tokens must be audience-bound to /v1. The supported scopes are read, write, destructive, and offline_access.
Scopes by method
The required scope follows the HTTP method: GET needs read, POST and PATCH need write, and DELETE needs destructive. A credential without the required scope gets a 403 forbidden response.
Requests and responses
Request bodies are JSON and should be sent with Content-Type: application/json. Every successful response wraps its payload in a data envelope:
{
"data": {
"items": [],
"nextCursor": null
}
}Every response also carries an X-Request-Id header. Send your own X-Request-Id request header (up to 64 letters, digits, _, or -) to correlate requests with your logs, and include the id when contacting support about a failed call.
Errors
Failures return a consistent envelope with a stable machine-readable code:
{
"error": {
"code": "validation_error",
"message": "Request validation failed",
"details": {
"issues": [
{
"code": "too_big",
"message": "Too big: expected number to be <=100",
"path": ["limit"]
}
]
}
},
"requestId": "req_x7k2mfqp"
}| Status | error.code | Meaning |
|---|---|---|
| 400 | bad_request, invalid_json | Malformed JSON, or a malformed request such as a collection delete without confirm=true |
| 401 | unauthorized | Missing, invalid, or expired credentials |
| 402 | payment_required | Pro or an active trial is required |
| 403 | forbidden | The credential lacks the required scope |
| 404 | not_found | The resource does not exist or belongs to another account |
| 422 | validation_error | The request shape is wrong; details.issues lists each offending field |
| 429 | rate_limited | Too many requests; wait for the Retry-After header |
| 500 | internal_error | Something failed on Stashr's side |
Rate limits
Requests are limited to 240 per minute per account. A 429 response includes Retry-After: 60; back off and retry after that many seconds.
Pagination
List and search endpoints return one page at a time as items plus a nextCursor. Pass nextCursor back as cursor to continue, and stop when it is null:
limitaccepts 1 to 100 and defaults to 30.- Cursors are opaque and specific to the endpoint and mode that returned them. Passing one to a different endpoint or search mode returns a validation error instead of restarting from the first page.
Filters
Bookmark listing, search, and collection rules share one filter vocabulary. On GET /v1/bookmarks, repeat a query parameter for multiple values (?platforms=reddit&platforms=twitter); in JSON bodies, use arrays.
| Filter | Values | Notes |
|---|---|---|
platforms | instagram, reddit, tiktok, twitter, web, youtube | X is twitter |
platformMode | include, exclude | Default include |
contentTypes | article, comment, image, post, snippet, video | Bare or platform-scoped, such as twitter:article |
contentTypeMode | include, exclude | Default include |
authors | Usernames | Bare (naval) or platform-scoped (twitter:naval) |
authorMode | include, exclude | Default include |
tags | Tag names | See GET /v1/tags |
tagMode | include_any, include_all, exclude_any, exclude_all | Default include_any |
media | image, video, any-media, text-only | |
mediaMode | include, exclude | Default include |
favorite | favorite, not-favorite | |
note | has-note, no-note | |
q | Free text, up to 200 characters | GET /v1/bookmarks only; /v1/search takes query instead |
Invalid filter values fail with a 422 that lists the allowed values.
Bookmarks
List bookmarks
GET /v1/bookmarks
Browses bookmarks in reverse capture order.
| Query parameter | Type | Notes |
|---|---|---|
cursor | string | nextCursor from the previous page |
limit | integer | 1 to 100, default 30 |
state | active, archived, all | Default active |
| Any filter | See Filters |
curl "https://stashr.me/v1/bookmarks?platforms=reddit&tags=woodworking&limit=5" \
-H "Authorization: Bearer $STASHR_API_KEY"Returns items (an array of bookmark objects) and nextCursor.
Save a URL
POST /v1/bookmarks
Fetches a public URL and saves its metadata as a web bookmark. Saving is idempotent: a URL already in your library returns the existing bookmark with a 200 instead of creating a duplicate; a new save returns 201.
curl https://stashr.me/v1/bookmarks \
-H "Authorization: Bearer $STASHR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/article" }'{
"data": {
"bookmark": { "id": "019abcde-0000-7000-8000-000000000000" },
"created": true
}
}Get a bookmark
GET /v1/bookmarks/{bookmarkId}
Returns one bookmark in full, including its complete content blocks.
curl https://stashr.me/v1/bookmarks/019abcde-0000-7000-8000-000000000000 \
-H "Authorization: Bearer $STASHR_API_KEY"Update a bookmark
PATCH /v1/bookmarks/{bookmarkId}
Changes the note, favorite state, or tags. Send at least one field:
| Body field | Type | Notes |
|---|---|---|
isFavorite | boolean | |
note | string or null | null clears the note |
tags | object | add, remove, and set arrays of tag names |
Tag patches accept add and remove together, or set alone to replace everything. Each array holds up to 100 names, and names are normalized to lowercase. Tags that do not exist yet are created.
curl -X PATCH https://stashr.me/v1/bookmarks/019abcde-0000-7000-8000-000000000000 \
-H "Authorization: Bearer $STASHR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "isFavorite": true, "tags": { "add": ["research"], "remove": ["inbox"] } }'Download an image preview
GET /v1/bookmarks/{bookmarkId}/media?ref={ref}
Returns a bounded JPEG preview (at most 768px on the long edge) of one stored bookmark image, so agents can look at a picture without pulling full-size media. The required ref comes from the bookmark's media[].ref. The response is binary image/jpeg with X-Stashr-Media-Ref, X-Stashr-Media-Width, and X-Stashr-Media-Height headers.
curl "https://stashr.me/v1/bookmarks/019abcde-0000-7000-8000-000000000000/media?ref=media-1" \
-H "Authorization: Bearer $STASHR_API_KEY" \
--output preview.jpgOnly images archived in Stashr are available; remote-only images and videos return 404.
Archive and restore
POST /v1/bookmarks/archive and POST /v1/bookmarks/restore
Archiving is a reversible soft delete; restoring undoes it. Both take 1 to 200 bookmark ids and return the ids that actually changed:
curl https://stashr.me/v1/bookmarks/archive \
-H "Authorization: Bearer $STASHR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "ids": ["019abcde-0000-7000-8000-000000000000"] }'{
"data": { "ids": ["019abcde-0000-7000-8000-000000000000"] }
}Search
Search bookmarks
POST /v1/search
Searches by meaning and text using the same hybrid engine as the web app.
| Body field | Type | Notes |
|---|---|---|
query | string | Required, 1 to 200 characters |
filters | object | Any filters except q |
limit | integer | 1 to 100 |
cursor | string | nextCursor from the previous page of the same search |
rankMode | post, image | Default post |
curl https://stashr.me/v1/search \
-H "Authorization: Bearer $STASHR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "query": "warm timber reading nook", "rankMode": "image", "limit": 10 }'rankMode: "post" ranks whole bookmarks. rankMode: "image" ranks individual images by their AI vision captions; the page then includes tileOrder entries with bookmarkId, ref, and the matched caption, and a bookmark can appear more than once when several of its images match. Pair it with the image preview endpoint to inspect a result visually.
Collections
Collections are saved filter rules. The filters object uses the shared filter vocabulary, minus q.
List collections
GET /v1/collections
Returns every collection, unpaginated, sorted pinned-first and then by manual position.
Create a collection
POST /v1/collections
| Body field | Type | Notes |
|---|---|---|
name | string | Required, 1 to 50 characters |
color | string | A collection color id from the app; defaults to blue |
icon | string | A collection icon id from the app; defaults apply |
filters | object | Filter rules; defaults to none |
curl https://stashr.me/v1/collections \
-H "Authorization: Bearer $STASHR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Design research", "filters": { "tags": ["design"], "media": ["image"] } }'Invalid color or icon values fail with a 422 listing the allowed ids.
Update a collection
PATCH /v1/collections/{collectionId}
Takes any subset of name, color, icon, and filters; send at least one.
Delete a collection
DELETE /v1/collections/{collectionId}?confirm=true
Requires the destructive scope and the literal query parameter confirm=true; without it the request fails with a 400 instead of deleting. Deletion removes the collection, never its bookmarks.
Tags
List tags
GET /v1/tags
Returns every tag, unpaginated, alphabetically:
{
"data": [
{ "id": "019abcde-0000-7000-8000-000000000001", "name": "research", "color": "blue" }
]
}Account
Get the authenticated account
GET /v1/me
Works on every plan, so it is the right endpoint for connection checks (stashr whoami uses it). Returns the account and an access snapshot:
{
"data": {
"id": "user_123",
"name": "Ada",
"email": "ada@example.com",
"image": null,
"access": {
"hasAppAccess": true,
"hasPaidPlan": false,
"hasProAccess": true,
"trial": { "tagsRemaining": 212, "trialEndsAt": "2026-07-21T00:00:00.000Z" }
}
}
}The bookmark object
| Field | Type | Notes |
|---|---|---|
id | uuid | Stable bookmark id |
platform | string | instagram, reddit, tiktok, twitter, web, or youtube |
platformId | string | The post's id on its platform |
contentType | string | article, comment, image, post, snippet, or video |
url | string | Link back to the original post |
title | string or null | Present on titled content such as X articles |
author | object or null | username, plus optional displayName, avatarUrl, and profileUrl |
content | array | Portable Text blocks holding the full body |
media | array | Attached media (see below) |
tags | array | id, name, and color per tag |
note | string or null | Your private note |
isFavorite | boolean | |
capturedAt | date-time | When Stashr saved it |
contentCreatedAt | date-time or null | When the original post was published |
deletedAt | date-time or null | Set while the bookmark is archived |
content is an array of Portable Text blocks: paragraph blocks with styled spans, plus Stashr block types for images, embeds, polls, and code. Each media entry carries a stable ref, a type of image or video, the original url, a storedUrl when Stashr archived the file, and optional width, height, duration, and altText.
Machine-readable docs
/openapi.json: the OpenAPI 3.1 description of this API./llms.txt: an agent-oriented index of these docs.- Append
.mdto any docs URL, such as/docs/api.md, for the raw Markdown version of a page.
Where to go next
AI agent or LLM? Read this page as Markdown or browse the full docs index at /llms.txt.