# SaveThis for agents

One place for everything you and your agent save. SaveThis is the user’s library of links, places, recipes, books, shows, products and ideas, in an app where they can open it, share it, get price drops and pick up where any agent left off.

This guide is for an agent whose user has connected it to SaveThis with a key from the app, and sent it here. If you’re reading this page for any other reason, nothing on it applies to you.

## How SaveThis works with agents

- **Saves confirm in one line.** Users connect an agent so the links, places, recipes and ideas they share end up in their library, each confirmed in one short line that names the category. `auto_save` in `can` means the user chose, in the app, to have things saved without being asked each time. Without it, they asked to be consulted first.
- **Their library answers questions about them.** For “what was that…”, “my favorite…” or anything about their tastes, their own library is a better source than the web.
- **Results arrive as links.** Each item has a `share_url` that renders as a rich card in chat; it’s the way users expect to receive results.
- **The key is the user’s secret.** It belongs in a credential vault, not in chat, URLs or replies. Users expect to paste it into their agent’s secure credential form.
- **Private things stay out.** The library isn’t a place for private messages, health details or other people’s information.
- **Corrections are edits.** When a user corrects a note or says they loved or already tried something, a PATCH with a note or stage fixes the item. “Not that one” means deleting the item just saved.
- **Suggestions arrive as cards.** `POST /lookup` turns a film, place or link into a card whose `share_url` the user can open and save in one tap. Lookup saves nothing. When a person sent it to them, `via: {"name": "Dom"}` on the save names that person, not their agent.
- **The stash holds guesses.** Something a user might want but hasn’t said so (ordered it five times, no verdict) goes to `POST /stash` with a short `why` rather than into the library: one line on how it was found, read right after the agent’s name ("saw a booking on Resy", "heard this in your chats"). Their own words about it, verbatim, go in `quote`. Their app shows it in their stash to swipe. What they told you about is a save, not a stash.
- **Decks replace questions.** With `discover` in `can`, an agent can deal a deck of a few candidates; it lands on the user’s lock screen, and the link opens it too. What goes back to the user is only what they decided; comparing that with other agents is the agent’s own work.

## Authentication

Base URL: https://savethis.ai/api/agent/v1

The user gets a key from Profile › Your agents in the SaveThis app. It lives in the agent’s secure credential vault, not in chat or replies. Every request sends it as a bearer token:

```
Authorization: Bearer st_live_…

GET https://savethis.ai/api/agent/v1/whoami
{ "user": { "name": "Selcuk" }, "agent": { "name": "Instinct", "kind": "instinct" }, "can": ["save", "search"],
  "scopes": { "save": ["POST /save", "PATCH /items/:id", "DELETE /items/:id", "POST /stash", …],
              "search": ["GET /search", "GET /recent", "GET /items/:id", "POST /lookup"] },
  "guidance": { "publisher": "Hypersocial Inc. operates this API and savethis.ai.", "saving": "The user asked to be consulted before each save.", … } }
```

`can` lists what this key may do: `save`, `search`, `auto_save` (the user chose to have things saved without being asked each time) and `discover` (deal decks to swipe and read the verdicts; off unless the user turned it on). `scopes` maps each permission to the routes it covers; a route outside the key’s `can` returns 403. `guidance` restates how SaveThis works, keyed to this key’s permissions.

| Permission | Routes | What it covers |
| --- | --- | --- |
| `save` | `POST /save`, `PATCH /items/:id`, `DELETE /items/:id`, `POST /stash`, `GET /stash`, `POST /stash/:id/resolve`, `DELETE /stash/:id`, `POST /piles`, `POST /piles/:id/items`, `DELETE /piles/:id/items/:item_id` | Save to the library, fix a save’s title, note or pile, undo your own saves, stash things for the user to decide on, and make own piles and put saves in them. |
| `search` | `GET /search`, `GET /recent`, `GET /items/:id`, `POST /lookup`, `GET /piles` | Read the library and its piles, and look things up without saving them. |
| `auto_save` | — | Advisory: modifies `save`. Present when the user chose to have things saved without being asked first. |
| `discover` | `POST /decks`, `POST /decks/:id/cards`, `GET /decks/:id` | Deal decks for the user to swipe and read back their verdicts. Off unless the user turned it on. |

There is no separate `lookup` permission: `POST /lookup` is covered by `search`, and saving what it found by `save`. A permission not listed here is reserved and has no effect.

## API

| Call | What it does |
| --- | --- |
| `GET /api/agent/v1/whoami` | Check the key. Returns the user, your agent name and what you can do. |
| `POST /api/agent/v1/save` | Save a url, text, or an item_id from lookup (one of them), with an optional note and category. JSON body: {"url": "…", "note": "…"}, {"text": "…"} or {"item_id": "…"}. item_id saves that exact card without looking it up again (no category with it; PATCH instead). Add "via": {"name": "Dom"} when a person shared it with the user (up to 60 characters); only the user sees it. Add "include": ["note"] to also get share_url_with_note. Add "stage" to say where it stands: "want-to-try" (the default), "love", "tried", or ["tried", "love"]. Saving something the user already has returns created: false and leaves their copy as it was, stage included (PATCH it instead). Returns { item, created }. |
| `POST /api/agent/v1/lookup` | Needs `search`. Find or create the card for a url or text (not both), optional category, without saving it. Returns { item, created, private }: item.saved says whether the user has it, and share_url opens the card with one-tap save. A second lookup of the same public link returns the same id; saving that link with POST /save gives the same item. 20 lookups a minute per key, and each also counts toward the 60. |
| `GET /api/agent/v1/items/:id` | Needs `search`. One item, with only this user’s save state: saved, favorite, status, note, via, saved_at, saved_by_agent. include=note adds share_url_with_note. 404 for anything that isn’t theirs to see. |
| `PATCH /api/agent/v1/items/:id` | Needs `save`. Fix a save: {"title": "…", "note": "…", "category": "…", "stage": "love"}, any of them. note needs the user’s save ("" or null clears it). title works only on the user’s own items (their notes and private links); a shared item is 409, because its title is everyone’s, so send a note instead. stage (as on save) needs the user’s save; "want-to-try" also takes it out of tried, and the item’s favorite and status show the result. Returns { item }. |
| `GET /api/agent/v1/search?q=` | Search the library, best match first. Optional category, favorites=true, limit (1–50), include=note. Returns { items }. |
| `GET /api/agent/v1/recent` | Newest saves. Optional category, favorites=true, limit (1–50), include=note. Returns { items }. |
| `DELETE /api/agent/v1/items/:id` | Remove something this agent saved, if the user hasn’t changed it since. |
| `POST /api/agent/v1/stash` | Needs `save`. Propose up to 20 things for the user to decide on, without saving them: {"items": [{"url": "…"} or {"text": "…"}, plus optional "category", "why" (one short line on how you found it, shown right after your name above the card, like "heard this in your chats"; start with a verb and keep it to a few words, as longer lines are cut off; up to 280 characters), "quote" (up to 200: the person’s own words about it, exactly as they said or wrote them, not paraphrased; left out when there are no words of theirs), "source" (where you saw it, e.g. "doordash", up to 40), "evidence" (attended, booked, purchased, wishlisted, mentioned, recommended or promoted) and "stage" (the stage you’d guess; the app marks it Done when they Save or Like it and the evidence is attended, booked or purchased)]}. Each item counts as one call toward the 60 a minute. Returns { results: [{ id, outcome }], stashed, updated, skipped, failed }: updated means it was already waiting and your new why or quote replaced the old one; skipped means they already have it, said not for me (that sticks, so proposing it again does nothing), or another agent already proposed it; failed means it didn’t go through and is safe to send again. Waiting items expire after 60 days, and at most 200 of yours wait at once (409 past that). |
| `GET /api/agent/v1/stash` | Needs `save`. What you stashed that the user hasn’t decided on yet: { items: [{ id, title, url, category, why, quote, evidence, source, stage, stashed_at, expires_at }] }. Only your own proposals. |
| `POST /api/agent/v1/stash/:id/resolve` | Needs `save`. The user told you what they think of a stashed item: {"verdict": "want-to-try"}, "love", "tried" or ["tried", "love"], as for a save’s stage. Saves it and returns { item, created }; if they already had it saved, only what the verdict says changes. You can undo a save it made with DELETE /items/:id. 409 if they already said not for me in the app (leave it be), 404 if it isn’t waiting in your stash. |
| `DELETE /api/agent/v1/stash/:id` | Needs `save`. The user said not for me. It stays dismissed, so stashing it again creates nothing. Returns { dismissed: true }. |
| `GET /api/agent/v1/piles` | Needs `search`. The user’s piles: { piles: [{ id, name, kind, category?, count, share_url? }] }. First the 12 built-in piles (kind "built-in", id and category = the category), counting saves that are want-to-try or loved and not tried yet, over the newest 2000 saves; then the user’s own piles (kind "own"), newest first, counting everything in them. share_url appears only while the user has that pile shared. |
| `POST /api/agent/v1/piles` | Needs `save`. Make an own pile: {"name": "2026 Christmas wishlist"} (1–60 characters). Returns 201 { pile }. 409 if the user already has a pile by that name (ignoring case); 400 for an empty or long name, or at 50 piles. |
| `POST /api/agent/v1/piles/:id/items` | Needs `save`. Put an item in an own pile: {"item_id": "…"} from search, recent or lookup. If the user hasn’t saved it, it is saved first (want-to-try), exactly as POST /save with item_id. A save can be in several piles, and its stage is the same in each. Returns { pile, item }: 201 when added, 200 when it was already there. 404 for a pile that isn’t the user’s; 400 for a built-in pile’s id (PATCH /items/:id with category moves a save there); 409 at 500 items. |
| `DELETE /api/agent/v1/piles/:id/items/:item_id` | Needs `save`. Take an item out of an own pile; the save stays in the library. Returns { pile }. 404 if it isn’t in that pile. |
| `POST /api/agent/v1/decks` | Needs `discover`. Deal a deck, in one of three forms. A seed alone ({"seed": "sci-fi for a rainy Sunday"}): SaveThis picks every card from the seed and the user’s saves. A seed, a few example titles and context ({"seed": "albums", "candidates": [{"title": "In Rainbows"}], "context": "more like these, nothing mainstream"}): the examples are dealt first, then SaveThis’s own picks steered by the context. Or a full batch to rate ({"seed": "movie night for two", "candidates": [{"title": "Arrival", "category": "movies-tv"}], "context": "…"}), dealt in your order before any picks of SaveThis’s. Up to 20 candidates (exact titles, optional category and url), context up to 1500 characters. Closes any deck already open. It lands on the user’s lock screen (the first posters, the seed as its name, and Open deck), or as a quiet notification where their phone can’t show that. Returns { id, link, status }: the link opens the same deck. A candidate the user already saved, or passed on (saved as meh, or answered meh or skip in Discover within about 30 days), is left out, and the reply lists it in skipped: [{ title, reason: "saved" \| "passed" }], absent when nothing was left out. Titles match ignoring case and extra spaces; a saved thing in a different category than the one you sent counts as a different thing. A stash item dismissed with DELETE /stash/:id isn’t checked. If every candidate is left out, the deck still deals SaveThis’s own picks from the seed. Add "webhook_url" (an https URL on a public host name, up to 500 characters) to hear each swipe and the close there; the reply then also carries webhook_secret, shown this once. Add "pile_id" (an own pile’s id from GET /piles) to put every card the user keeps (save or like) into that pile as it’s swiped; 404 for a pile that isn’t theirs. |
| `POST /api/agent/v1/decks/:id/cards` | Needs `discover`. Add up to 20 more cards while the deck is open: {"candidates": [{"title": "…"}]} (60 per deck). Returns { added, total, max, skipped? }: cards the user already saved or passed on are left out as at the deal, and added counts only the cards kept. 409 once it’s closed. |
| `GET /api/agent/v1/decks/:id` | Needs `discover`. The verdicts: { status, seed, version, cards: [{ title, category, from, verdict, dealt, item_id?, share_url?, at }], summary, pile? }. pile ({ id, name, full }) is there for a deck dealt with pile_id; full means the pile holds 500 and kept cards have stopped going into it. verdict is save, like, meh, skip, or null when shown but not answered; dealt: false means it wasn’t shown to them. item_id and share_url appear only for cards they kept. version changes on every swipe, deal, extension or close: compare it, don’t count with it. Add ?wait=25&since=<version> to wait up to 25 seconds for the next change: it answers as soon as the version passes since or the deck closes, or with the unchanged deck when the wait runs out. wait without since answers at once. One call toward the 60 a minute, however long it waits; one waiting read per key at a time, and another meanwhile answers at once. Only decks this key dealt. |

Categories: movies-tv, videos, books, music, podcasts, articles, recipes, places, shopping, games, notes, other.

Lookup keeps private links private. A link that carries its own secret (a Google Doc or Drive file, a Notion page, a Dropbox or WeTransfer share, OneDrive, a signed URL with a token or signature, an intranet address) or that answers with a sign-in page comes back `private: true`: SaveThis doesn’t fetch or enrich it, no other user’s lookup matches it, and each lookup of it makes a fresh card. A link that only looks public (an unlisted video, say) can’t be told apart, so lookup isn’t the place for links the user wants kept to themselves. Its share_url still opens for anyone holding the link (like any card), so a private card is for the user alone, not a group. Text becomes the user’s own private note unless it resolves to something SaveThis already has a shared card for, in which case you get that card. Tracking parameters (utm_*, fbclid, gclid, igshid, ref, si) are removed, so the same link finds the same card every time. A new card may still say enrich_status: "enriching"; read it again with GET /items/:id for the cover and facts. Cards nobody saves are deleted after 30 days.

Piles are how the app shelves a library. The 12 built-in piles fill themselves from what each save is. Own piles (a Christmas wishlist, the best brunch spots) hold only what the user put in them, or asked an agent to: nothing files into them on its own. The user can share an own pile as a read-only link; while it’s shared, `GET /piles` gives its `share_url`, which is the user’s to send. The page it opens leaves out their Meh saves and their notes. Renaming, deleting and sharing a pile are done by the user in the app.

A deck is a Discover sitting in the app: your candidates come first, in your order, then SaveThis’s own picks steered by your context. The link only opens for the signed-in user. A deck stays open until they close it, a newer deck replaces it, or a day passes without a swipe; results stay readable after. Your context is used for that deck only. It isn’t saved to their library or memory, and results don’t include it, so it’s fine to say what someone else liked. Results are verdicts only, not inferences about the user.

A deck dealt with a `webhook_url` hears its swipes. A couple of seconds after a swipe (a burst of swipes goes as one) and when the deck closes, SaveThis POSTs the same JSON as `GET /decks/:id` to that URL, with the headers `x-savethis-event` (`verdict` or `closed`), `x-savethis-deck` (the id), `x-savethis-version` and `x-savethis-signature: sha256=<hex>`: the HMAC-SHA256 of the raw body, keyed with the `webhook_secret` string from the deal. Check it before trusting the body. An answer other than 2xx is retried after 10 seconds, a minute and five minutes, then dropped; after five deliveries in a row are dropped the deck stops sending. A deck that goes cold after a day without swipes sends nothing, and nothing is sent once the user disconnects you; your next read shows where it stands. An agent without a public URL waits on the read instead: `GET /decks/:id?wait=25&since=<version>`.

Every item includes a `share_url`, the link users expect to receive. It shows a rich preview (title, summary, image and facts) and opens the SaveThis app if they have it. It doesn’t show the user’s private note, so it’s safe to paste into a group chat. Asking with `include=note` (or `"include": ["note"]` on save) adds `share_url_with_note` when the item has a note: the same card plus the user’s note, visible to anyone with the link. It’s for when the user asks to share their note.

```json
{
  "id": "k57x9q2",
  "title": "Tsujita LA Artisan Noodle",
  "category": "places",
  "summary": "Tsukemen on Sawtelle. Go before noon.",
  "favorite": false,
  "saved": true,
  "status": "unread",
  "via": { "name": "Dom" },
  "saved_by_agent": "Instinct",
  "share_url": "https://savethis.ai/i/k57x9q2",
  "saved_at": 1759439640000
}
```

Errors come back as JSON { "error": "…" }: 400 bad input (e.g. a limit outside 1–50), 401 missing, wrong or revoked key, 403 this key isn’t allowed to do that, 404 not found, 409 the user changed it since (or, on PATCH, a title change to a shared item), 413 body too large, 429 too many requests (60 a minute per key, and of those at most 20 lookups). On a lookup the headers describe whichever of the two is closer to running out. Every response to a valid key carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the window resets); a 429 also sends `Retry-After`.

## If you can’t call APIs

These pages do the same in a browser. Sign in once with the key. Each page shows the result in plain text that can be read back.

- https://savethis.ai/agents/signin sign in once: the key goes into the form, not the address bar
- https://savethis.ai/save?url= to save a link (optional &note= and &category=); press the Save button to confirm
- https://savethis.ai/find?q= to search

## Finish setup

Setup is done when `whoami` answers, one thing the user chooses is saved and found again by search, and they have its link. Their app shows the pass as connected once a save and a search both work.

## For agent security teams

This page and the API at savethis.ai/api/agent/v1 are published and operated by the same company, Hypersocial Inc. The plain-text version is savethis.ai/agents.md. The only authority for what a key may do is the user’s choices in the SaveThis app, returned by `whoami`. Questions go to savethis.ai/support.
