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/v1

API 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 levelScopesAllows
ReadreadReading and searching
Writeread, writePlus saving, updating, archiving, and restoring
Fullread, write, destructivePlus 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"
}
Statuserror.codeMeaning
400bad_request, invalid_jsonMalformed JSON, or a malformed request such as a collection delete without confirm=true
401unauthorizedMissing, invalid, or expired credentials
402payment_requiredPro or an active trial is required
403forbiddenThe credential lacks the required scope
404not_foundThe resource does not exist or belongs to another account
422validation_errorThe request shape is wrong; details.issues lists each offending field
429rate_limitedToo many requests; wait for the Retry-After header
500internal_errorSomething 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:

  • limit accepts 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.

FilterValuesNotes
platformsinstagram, reddit, tiktok, twitter, web, youtubeX is twitter
platformModeinclude, excludeDefault include
contentTypesarticle, comment, image, post, snippet, videoBare or platform-scoped, such as twitter:article
contentTypeModeinclude, excludeDefault include
authorsUsernamesBare (naval) or platform-scoped (twitter:naval)
authorModeinclude, excludeDefault include
tagsTag namesSee GET /v1/tags
tagModeinclude_any, include_all, exclude_any, exclude_allDefault include_any
mediaimage, video, any-media, text-only
mediaModeinclude, excludeDefault include
favoritefavorite, not-favorite
notehas-note, no-note
qFree text, up to 200 charactersGET /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 parameterTypeNotes
cursorstringnextCursor from the previous page
limitinteger1 to 100, default 30
stateactive, archived, allDefault active
Any filterSee 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 fieldTypeNotes
isFavoriteboolean
notestring or nullnull clears the note
tagsobjectadd, 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.jpg

Only 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 bookmarks

POST /v1/search

Searches by meaning and text using the same hybrid engine as the web app.

Body fieldTypeNotes
querystringRequired, 1 to 200 characters
filtersobjectAny filters except q
limitinteger1 to 100
cursorstringnextCursor from the previous page of the same search
rankModepost, imageDefault 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 fieldTypeNotes
namestringRequired, 1 to 50 characters
colorstringA collection color id from the app; defaults to blue
iconstringA collection icon id from the app; defaults apply
filtersobjectFilter 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

FieldTypeNotes
iduuidStable bookmark id
platformstringinstagram, reddit, tiktok, twitter, web, or youtube
platformIdstringThe post's id on its platform
contentTypestringarticle, comment, image, post, snippet, or video
urlstringLink back to the original post
titlestring or nullPresent on titled content such as X articles
authorobject or nullusername, plus optional displayName, avatarUrl, and profileUrl
contentarrayPortable Text blocks holding the full body
mediaarrayAttached media (see below)
tagsarrayid, name, and color per tag
notestring or nullYour private note
isFavoriteboolean
capturedAtdate-timeWhen Stashr saved it
contentCreatedAtdate-time or nullWhen the original post was published
deletedAtdate-time or nullSet 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 .md to 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.