WhyTho

Every element is a decision somebody made.

Select anything on a page, on desktop or a phone, and record why it is that way. Notes carry the selector, the breakpoint, and your name automatically.

How it works API docs · Install · Demo page

WhyTho
Notes Inbox Workspace Setup Changes API Docs

Notes

Import a JSON export from somewhere else

Only needed to bring notes over from another account or an old export. Anything you write with the bookmarklet is already here. Paste it, or drop a .json file anywhere on this page.

Page

Inbox

Notes where you or one of your teams was tagged. Joining a team brings its history with it.

Changes

Notes you marked as a change request. Each one is treated as approved: Claude edits the repository mapped to that site and commits it. A request can reach one page, everything under a path, or the whole site, in which case the edit is worked out once and applied to every file where it matches exactly. Every change records what it replaced, so undo is one click, and a repository can be set to require review instead.

Workspace

An organization holds teams, and a team holds people. Notes written inside an organization are visible to everyone in it. Work outside one and they stay private to you.

Connections

Change requests run on your own credentials, never shared ones: your GitHub token decides which repositories can be written to, and your Anthropic key pays for the model that drafts the edit. Both are stored encrypted and neither is ever sent back to this page.

Model

Which model drafts your change requests. All of them read the whole file and return the same surgical edits; they differ in how much judgement they bring to a vague request, how fast they are, and what they cost you per change.

Repositories

Map a site to the repository its source lives in, and a change request against that site gets made. New mappings commit directly; switch one to require review and it opens a pull request instead. Without a mapping, requests are recorded and marked as skipped.

Setup

WhyTho runs inside the page you are annotating, so it works on staging, on production, and behind a login.

Browser extension

The cleanest way in. One click on the toolbar icon, or Alt Shift W, on any page. Your account connects itself, so there is no token to copy.

BookmarkletWorks in any browser, including Safari and mobile.

Your bookmarklet

This is how you start WhyTho on a page. Install it once per browser, then open it on any page you want to annotate.

Annotate with WhyTho ← drag me up to your bookmarks bar

Always on for a staging siteOptional. Only for a site you control.

The bookmarklet is per person: you click it when you need it. This script tag does the opposite, it loads WhyTho for anyone who opens the page, with no clicking. Useful on a staging environment your team lives in.

The token below is your account credential. Anyone who can view the source of that page can take it and write notes as you. Put it on private staging only, never on a production site.

Then

  1. Load the page you want to document and click the bookmarklet.
  2. Choose Select an element, then click it. On a phone, press and hold instead.
  3. Write the reasoning, tag it, save. A numbered pin sticks to the element.
  4. The note saves to your account as you write it, signed with your name.

API

A read only HTTP API over your notes, so they can feed a dashboard, a CI check, or a report instead of being copied by hand.

Your keys

A key reads one workspace: whichever one you are in when you create it. Only a hash is stored, so a key is shown once and cannot be recovered later.

Read only is the right default. A write key can create, edit and delete notes, and manage teams, people and invites, so treat one like a password with your name on it.

Use it from Claude

WhyTho is a connector. Add it once and Claude can search your notes, read what a page has recorded, and, if you allow it, write notes back. Anybody with a WhyTho account can add it with their own account, no key to copy.

Full endpoint reference: whytho.jakelabate.com/docs.html

Connector URL

  1. In Claude, open Customize, then Connectors.
  2. Press the + and choose Add custom connector. On Team or Enterprise an owner has to add it once for the organization first.
  3. Paste the URL above and press Add.
  4. Press Connect. You land on a WhyTho consent screen, choose read only or read and write, and press Allow.

Works on Claude, Claude Desktop, Cowork and mobile. Free accounts can hold one custom connector. Leave the detected defaults alone: authentication always required, and no client id, since the server registers Claude automatically.

What it is for

A note is only worth writing if it reaches the person about to change the element. The API is how that happens without anybody remembering to check an app.

  • Turn documentation into a guardrail. A build can ask whether anything marked do not change still exists on the page, and fail when it does not. That is the difference between a record of a decision and a decision that defends itself.
  • Brief a redesign from evidence. Pull every note under a template path and hand engineering the constraints before the work is scoped, rather than after the traffic moves.
  • Report without copying. Client updates, coverage checks and audit appendices read from the same source the annotator wrote to, so they cannot drift out of date.
  • Notice open questions. Notes with a question status are a queue. Query it daily and it stops being a queue nobody reads.

Quickstart

Base URL

Authenticate with your key on every request. Confirm it works:

curl -H "Authorization: Bearer why_live_..." \
  "/me"
{
  "ok": true,
  "key_name": "Looker dashboard",
  "scope": "organization",
  "workspace": "Acme Retail",
  "pages": 24
}

If that returns a workspace, everything below will work. A 401 means the key is wrong or revoked.

Endpoints

EndpointReturns
GET /meWhat the key can see: its workspace, and how many pages are in scope.
GET /pagesEvery annotated page in that workspace, with a note count each.
GET /notesThe notes themselves, filtered and paginated.

Writing

Any key created with read and write can do what you can do in the app. A read only key gets a 403 on anything that is not a GET.

EndpointDoes
POST /notesCreate a note. Needs url, selector and body. The page is created if it does not exist yet.
PATCH /notes/{id}Change the body, selector, category, status or applies_to of your own note.
DELETE /notes/{id}Remove your own note.
PATCH /pages/{id}Rename a page.
DELETE /pages/{id}Delete a page and its notes. Admins only, in an organization.
POST /teamsCreate a team. Admins only.
PATCH / DELETE /teams/{id}Rename or remove a team. Admins only.
POST /teams/{id}/membersAdd somebody to a team. Anyone can add themselves, admins can add others.
DELETE /teams/{id}/members/{user_id}Remove somebody from a team.
GET /membersEveryone in the organization, with roles.
PATCH / DELETE /members/{user_id}Change a role, or remove somebody. Admins only, and never the owner.
GET / POST /invitesList or mint an invite code, optionally bound to a team and a role.
DELETE /invites/{code}Revoke an invite.
GET /notificationsYour inbox. Add ?unread=true for only the new ones.
POST /notifications/readMark everything read, or pass ids for specific ones.
curl -X POST "/notes" \
  -H "Authorization: Bearer $WHYTHO_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/pricing/",
    "selector": "main > section.hero > h1",
    "body": "Service plus city stays, the phrase carries local demand. @Engineering keep it out of an image.",
    "category": "seo",
    "status": "do not change",
    "applies_to": ["all"]
  }'

Mentions work the same as in the app. The database parses the body against your organization on write, so an @ in an API created note reaches the same inboxes.

Filters on /notes

siteMatch part of the hostname, e.g. site=jimmiesplace.com
pathPath prefix, e.g. path=/products/
categoryseo, content, tech, a11y, perf, ux
statusdecided, proposed, question, do not change
authorExact display name, e.g. author=Jake Labate
teamExact team name
qFree text inside the note body
sinceISO date, returns notes created on or after it
limit / offsetPage through results. Default 50, maximum 200.
orderoldest to reverse the default newest first

A note looks like this

{
  "data": [
    {
      "id": "n_abc123",
      "url": "https://example.com/pricing/",
      "origin": "https://example.com",
      "path": "/pricing/",
      "page_title": "Pricing",
      "selector": "main > section.hero > h1",
      "fallback_selector": "main > section.hero > h1",
      "element": "h1",
      "element_text": "Same day plumbing repair",
      "body": "Service plus city stays, the phrase carries the local demand.",
      "category": "seo",
      "status": "do not change",
      "author": "Jake Labate",
      "team": "SEO",
      "mentions": [{ "type": "team", "id": "...", "label": "Engineering" }],
      "viewport": { "width": 390, "breakpoint": "mobile" },
      "created_at": "2026-09-09T13:00:00.000Z",
      "updated_at": "2026-09-09T13:00:00.000Z"
    }
  ],
  "pagination": { "limit": 50, "offset": 0, "returned": 1 }
}

Cookbook

1. Fail a build when a protected element disappears

The strongest reason this API exists. Every note marked do not change names a selector. If the selector is gone from the built page, somebody removed something deliberate.

// guard.mjs, run against your built HTML or a preview URL
const KEY = process.env.WHYTHO_KEY;
const res = await fetch(
  "/notes?status=do%20not%20change&limit=200",
  { headers: { Authorization: `Bearer ${KEY}` } }
);
const { data } = await res.json();

let failed = 0;
for (const note of data) {
  const html = await (await fetch(note.url)).text();
  // any selector matcher works here, this is the cheap version
  const stillThere = html.includes(note.element_text ?? "");
  if (!stillThere) {
    console.error(`MISSING  ${note.url}\n  ${note.selector}\n  why: ${note.body}\n  set by: ${note.author}`);
    failed++;
  }
}
process.exit(failed ? 1 : 0);
# .github/workflows/whytho.yml
- name: Check protected elements
  env:
    WHYTHO_KEY: ${{ secrets.WHYTHO_KEY }}
  run: node guard.mjs

2. Brief a redesign on one template

Everything anyone has recorded under a path, oldest first, as a readable list.

curl -s -H "Authorization: Bearer $WHYTHO_KEY" \
  "/notes?path=/products/&order=oldest&limit=200" \
| jq -r '.data[] | "## \(.path)\n\(.body)\n`\(.selector)`\n\(.status) · \(.author)\n"'

3. A daily digest of what changed

New notes since yesterday, ready to post into Slack or a standup.

SINCE=$(date -u -v-1d +%Y-%m-%dT%H:%M:%SZ)   # GNU: date -u -d '1 day ago' +...
curl -s -H "Authorization: Bearer $WHYTHO_KEY" \
  "/notes?since=$SINCE" \
| jq -r '.data[] | "\(.author) on \(.path): \(.body)"'

4. The open questions queue

Notes somebody could not resolve alone. Left unqueried, this is where knowledge goes to die.

curl -s -H "Authorization: Bearer $WHYTHO_KEY" \
  "/notes?status=question" \
| jq -r '.data[] | "\(.url)\n  \(.body)\n  asked by \(.author)\n"'

5. Coverage: which important pages have no reasoning recorded

Compare your sitemap against the pages that carry notes. The gap is your documentation debt.

import requests, xml.etree.ElementTree as ET

KEY = "why_live_..."
API = ""

annotated = {p["url"] for p in requests.get(
    f"{API}/pages", headers={"Authorization": f"Bearer {KEY}"}).json()["data"]}

sitemap = ET.fromstring(requests.get("https://example.com/sitemap.xml").content)
ns = {"s": "http://www.sitemaps.org/schemas/sitemap/0.9"}
live = {loc.text for loc in sitemap.findall(".//s:loc", ns)}

for url in sorted(live - annotated):
    print("no notes:", url)

6. Pull everything, for a spreadsheet or a warehouse

Page through in blocks of 200 until a short page comes back.

import requests, csv

KEY, API = "why_live_...", ""
rows, offset = [], 0
while True:
    r = requests.get(f"{API}/notes", params={"limit": 200, "offset": offset},
                     headers={"Authorization": f"Bearer {KEY}"}).json()
    rows += r["data"]
    if r["pagination"]["returned"] < 200:
        break
    offset += 200

with open("notes.csv", "w", newline="") as f:
    w = csv.DictWriter(f, fieldnames=["url", "selector", "category", "status", "author", "team", "body"], extrasaction="ignore")
    w.writeheader(); w.writerows(rows)

7. Migrate reasoning you already have

Audit findings in a spreadsheet, decisions buried in a Notion page, comments in a codebase. Anything with a URL and a selector can become a note.

import csv, requests

KEY, API = "why_live_...", ""
head = {"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"}

for row in csv.DictReader(open("audit-findings.csv")):
    requests.post(f"{API}/notes", headers=head, json={
        "url": row["url"],
        "selector": row["selector"],
        "body": row["recommendation"],
        "category": row.get("category", "seo"),
        "status": "proposed",
        "applies_to": ["all"],
    })

8. Close the loop after a fix ships

A proposal becomes a decision once the change is live, so let the deploy say so rather than waiting for somebody to remember.

curl -X PATCH "/notes/$NOTE_ID" \
  -H "Authorization: Bearer $WHYTHO_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "decided", "body": "Shipped in release 2026.09. Do not revert without asking."}'

9. Onboard a whole team in one script

# make a team, then an invite code bound to it
TEAM=$(curl -s -X POST "/teams" \
  -H "Authorization: Bearer $WHYTHO_KEY" -H "Content-Type: application/json" \
  -d '{"name":"Engineering"}' | jq -r '.data.id')

curl -s -X POST "/invites" \
  -H "Authorization: Bearer $WHYTHO_KEY" -H "Content-Type: application/json" \
  -d "{\"team_id\":\"$TEAM\",\"role\":\"member\"}" | jq -r '.data.code'

10. Show the reasoning inside your own tools

The API allows browser requests, so an internal dashboard or a CMS sidebar can show the notes for whatever page an editor is looking at. Use a key scoped to the organization, and remember that a key in front-end code is readable by anyone who loads it, so only do this behind your own login.

const notes = await fetch(
  `/notes?path=${encodeURIComponent(location.pathname)}`,
  { headers: { Authorization: `Bearer ${KEY}` } }
).then(r => r.json());

Behaviour worth knowing

  • Every note records where it came from. Notes written by the annotator were captured against a real element at a real width. Notes written through the API were not, so they carry "source": "api" and you can filter on it with ?source=annotator. Trust them accordingly.
  • Writes are pinned to the key. A key can only touch what its owner could touch in the app. Only the author edits or deletes a note, only admins change teams, people and invites, and the owner of an organization cannot be removed by a key at all.
  • One key, one workspace. A key made while you are in an organization reads that organization, including notes written by everyone in it. A key made outside one reads only your private notes.
  • Deleted notes disappear. Deletes are soft in the database but the API never returns them.
  • Filters combine. Every parameter narrows the result further, so ?site=x&status=question&since=... is one query, not three.
  • Keys are hashes. A lost key cannot be recovered, only revoked and replaced. Revoking takes effect on the next request.
  • No hard rate limit yet. Be reasonable, cache what you poll, and pull in pages of 200 rather than in a loop of single requests.

Account

Your name and picture come from however you signed in, and are used to attribute your notes.

· How WhyTho works · Source · Built by Jake Labate, SEO Consultant