WhyTho API reference
Version 1.0.0. Base URL https://whytho-api.jakelabate.com
WhyTho records why the elements on a webpage are the way they are. A note is attached to a CSS selector rather than to a screenshot or a line number, and carries who wrote it, which viewport widths it applies to, and the context it was written in.
Authentication
Every request needs an API key in the Authorization header:
Authorization: Bearer why_live_...
Create one in the WhyTho app under API. A key is scoped to one workspace, personal or an organization, and is created either read only or read and write. A read only key is refused with 403 on anything that is not a GET. Keys are stored as hashes, so a lost key is replaced rather than recovered.
What a key can reach
Exactly what its owner could reach in the app, and nothing more. Only the author of a note can edit or delete it. Only organization admins can change teams, membership and invites. The owner of an organization cannot be removed through the API at all.
Provenance
Notes created through this API are stored with source: "api", because nothing verified that the selector was ever on the page. Notes captured by the annotator carry source: "annotator". Filter on it with ?source=annotator when the difference matters, which it does if you are using notes as a build guardrail.
Rate limits
120 requests a minute per key, counted at the Cloudflare edge, and 20 a minute per IP for anything without a key. Going over returns 429 with a Retry-After header. Cache what you poll, and page in blocks of 200 rather than looping single requests.
Built by Jake Labate.
Prefer to poke at it? The same spec is a
Postman collection,
or download the OpenAPI file.
Key
What the key can see.
What this key can see
GET/me
Confirms the key works and reports its workspace, role and scopes. Start here.
Example
curl "https://whytho-api.jakelabate.com/me" \
-H "Authorization: Bearer $WHYTHO_KEY"
Responses
200 | Key details |
401 | Missing, invalid or revoked key. |
Notes
The reasoning itself.
Search notes
GET/notes
Every filter is optional and they combine into one query.
Parameters
| Name | Type | Notes |
|---|
site | string | Match part of the hostname. Example: example.com |
path | string | Path prefix. Example: /products/ |
category | seo or content or tech or a11y or perf or ux | |
status | decided or proposed or question or do not change | |
applies_to | all or mobile or tablet or laptop or desktop | Only notes that apply at this width. |
source | annotator or api | |
author | string | Exact display name. Example: Jake Labate |
team | string | Exact team name. |
q | string | Free text inside the note body. |
since | string | Notes created on or after this. |
limit | integer | |
offset | integer | |
order | oldest | Reverses the default newest first. |
Example
curl "https://whytho-api.jakelabate.com/notes" \
-H "Authorization: Bearer $WHYTHO_KEY"
Responses
200 | Matching notes |
401 | Missing, invalid or revoked key. |
Create a note
POST/notes
Write scope required. The page is created if it does not exist yet. An @name or @team in the body is resolved against your organization and delivered to those inboxes. Stored with source: "api".
Body
| Field | Type | Notes |
|---|
url required | string | Example: https://example.com/pricing/ |
selector required | string | Example: main > section.hero > h1 |
body required | string | Example: Service plus city stays, the phrase carries local demand. @Engineering keep it out of an image. |
category | seo or content or tech or a11y or perf or ux | |
status | decided or proposed or question or do not change | |
applies_to | array of all or mobile or tablet or laptop or desktop | |
page_title | string | |
id | string | Your own id for the note. Sending the same one again updates it rather than duplicating. |
Example
curl -X POST "https://whytho-api.jakelabate.com/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.",
"category": "seo",
"status": "do not change",
"applies_to": [
"all"
]
}'
Responses
201 | Created |
400 | A note needs a full url, a selector and a body. |
403 | Read only key. |
Read one note
GET/notes/{id}
Parameters
| Name | Type | Notes |
|---|
id required | string | Example: n_mtvqtjrmx2lws |
Example
curl "https://whytho-api.jakelabate.com/notes/{id}" \
-H "Authorization: Bearer $WHYTHO_KEY"
Responses
200 | The note |
404 | No such note in this workspace. |
Update your own note
PATCH/notes/{id}
Write scope required, and only the author of a note can change it.
Parameters
| Name | Type | Notes |
|---|
id required | string | Example: n_mtvqtjrmx2lws |
Body
| Field | Type | Notes |
|---|
body | string | |
selector | string | |
category | seo or content or tech or a11y or perf or ux | |
status | decided or proposed or question or do not change | |
applies_to | array of string | |
Example
curl -X PATCH "https://whytho-api.jakelabate.com/notes/{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."
}'
Responses
200 | Updated |
403 | Read only key, or not the author. |
404 | No such note. |
Delete your own note
DELETE/notes/{id}
Write scope required, and only the author of a note can delete it.
Parameters
| Name | Type | Notes |
|---|
id required | string | Example: n_mtvqtjrmx2lws |
Example
curl -X DELETE "https://whytho-api.jakelabate.com/notes/{id}" \
-H "Authorization: Bearer $WHYTHO_KEY"
Responses
200 | Deleted |
403 | Read only key, or not the author. |
Pages
Pages that carry notes.
List annotated pages
GET/pages
Every page carrying notes in this workspace, with a note count each.
Example
curl "https://whytho-api.jakelabate.com/pages" \
-H "Authorization: Bearer $WHYTHO_KEY"
Responses
200 | Pages |
401 | Missing, invalid or revoked key. |
Rename a page
PATCH/pages/{id}
Write scope required.
Parameters
| Name | Type | Notes |
|---|
id required | string | Page id from /pages. |
Body
| Field | Type | Notes |
|---|
title required | string | |
Example
curl -X PATCH "https://whytho-api.jakelabate.com/pages/{id}" \
-H "Authorization: Bearer $WHYTHO_KEY" \
-H "Content-Type: application/json"
Responses
200 | Updated |
403 | Read only key. |
404 | No such page in this workspace. |
Delete a page and its notes
DELETE/pages/{id}
Write scope required. In an organization, admins only.
Parameters
| Name | Type | Notes |
|---|
id required | string | Page id from /pages. |
Example
curl -X DELETE "https://whytho-api.jakelabate.com/pages/{id}" \
-H "Authorization: Bearer $WHYTHO_KEY"
Responses
200 | Deleted |
403 | Read only key, or not an admin. |
404 | No such page. |
Teams
Teams inside an organization.
List teams
GET/teams
Example
curl "https://whytho-api.jakelabate.com/teams" \
-H "Authorization: Bearer $WHYTHO_KEY"
Responses
200 | Teams |
400 | This key is personal, so it has no teams. |
Create a team
POST/teams
Write scope required. Admins only.
Body
| Field | Type | Notes |
|---|
name required | string | Example: Engineering |
Example
curl -X POST "https://whytho-api.jakelabate.com/teams" \
-H "Authorization: Bearer $WHYTHO_KEY" \
-H "Content-Type: application/json"
Responses
201 | Created |
403 | Read only key, or not an admin. |
Rename a team
PATCH/teams/{id}
Write scope required. Admins only.
Parameters
| Name | Type | Notes |
|---|
id required | string | |
Body
| Field | Type | Notes |
|---|
name required | string | |
Example
curl -X PATCH "https://whytho-api.jakelabate.com/teams/{id}" \
-H "Authorization: Bearer $WHYTHO_KEY" \
-H "Content-Type: application/json"
Responses
200 | Renamed |
403 | Not an admin. |
Delete a team
DELETE/teams/{id}
Write scope required. Admins only.
Parameters
| Name | Type | Notes |
|---|
id required | string | |
Example
curl -X DELETE "https://whytho-api.jakelabate.com/teams/{id}" \
-H "Authorization: Bearer $WHYTHO_KEY"
Responses
200 | Deleted |
403 | Not an admin. |
List team members
GET/teams/{id}/members
Parameters
| Name | Type | Notes |
|---|
id required | string | |
Example
curl "https://whytho-api.jakelabate.com/teams/{id}/members" \
-H "Authorization: Bearer $WHYTHO_KEY"
Responses
Add somebody to a team
POST/teams/{id}/members
Write scope required. Anyone can add themselves, admins can add others.
Parameters
| Name | Type | Notes |
|---|
id required | string | |
Body
| Field | Type | Notes |
|---|
user_id | string | Omit to add yourself. |
Example
curl -X POST "https://whytho-api.jakelabate.com/teams/{id}/members" \
-H "Authorization: Bearer $WHYTHO_KEY" \
-H "Content-Type: application/json"
Responses
201 | Added |
400 | That person is not in this organization. |
403 | Not an admin. |
Remove somebody from a team
DELETE/teams/{id}/members/{user_id}
Write scope required. Anyone can remove themselves, admins can remove others.
Parameters
| Name | Type | Notes |
|---|
id required | string | |
user_id required | string | |
Example
curl -X DELETE "https://whytho-api.jakelabate.com/teams/{id}/members/{user_id}" \
-H "Authorization: Bearer $WHYTHO_KEY"
Responses
200 | Removed |
403 | Not an admin. |
People
Membership and invites.
List everyone in the organization
GET/members
Example
curl "https://whytho-api.jakelabate.com/members" \
-H "Authorization: Bearer $WHYTHO_KEY"
Responses
200 | Members |
400 | This key is personal, so it has no members. |
Change somebody's role
PATCH/members/{user_id}
Write scope required. Admins only, and never the owner.
Parameters
| Name | Type | Notes |
|---|
user_id required | string | |
Body
| Field | Type | Notes |
|---|
role required | admin or member | |
Example
curl -X PATCH "https://whytho-api.jakelabate.com/members/{user_id}" \
-H "Authorization: Bearer $WHYTHO_KEY" \
-H "Content-Type: application/json"
Responses
200 | Changed |
400 | You cannot change your own role. |
403 | Not an admin. |
Remove somebody from the organization
DELETE/members/{user_id}
Write scope required. Admins only. The owner cannot be removed.
Parameters
| Name | Type | Notes |
|---|
user_id required | string | |
Example
curl -X DELETE "https://whytho-api.jakelabate.com/members/{user_id}" \
-H "Authorization: Bearer $WHYTHO_KEY"
Responses
200 | Removed |
403 | Not an admin. |
List invite codes
GET/invites
Admins only.
Example
curl "https://whytho-api.jakelabate.com/invites" \
-H "Authorization: Bearer $WHYTHO_KEY"
Responses
Mint an invite code
POST/invites
Write scope required. Admins only. Codes last 30 days. People join by entering it in the app.
Body
| Field | Type | Notes |
|---|
role | admin or member | |
team_id | string | Optional. Joining with this code also joins that team. |
Example
curl -X POST "https://whytho-api.jakelabate.com/invites" \
-H "Authorization: Bearer $WHYTHO_KEY" \
-H "Content-Type: application/json" \
-d '{
"role": "member"
}'
Responses
201 | Created |
403 | Not an admin. |
Revoke an invite code
DELETE/invites/{code}
Write scope required. Admins only.
Parameters
| Name | Type | Notes |
|---|
code required | string | |
Example
curl -X DELETE "https://whytho-api.jakelabate.com/invites/{code}" \
-H "Authorization: Bearer $WHYTHO_KEY"
Responses
Inbox
Notifications from @ mentions.
Your inbox
GET/notifications
Notes where you or one of your teams was tagged.
Parameters
| Name | Type | Notes |
|---|
unread | boolean | true for unread only. |
Example
curl "https://whytho-api.jakelabate.com/notifications" \
-H "Authorization: Bearer $WHYTHO_KEY"
Responses
Mark notifications read
POST/notifications/read
Write scope required. Send ids for specific ones, or nothing to mark everything read.
Body
| Field | Type | Notes |
|---|
ids | array of string | |
Example
curl -X POST "https://whytho-api.jakelabate.com/notifications/read" \
-H "Authorization: Bearer $WHYTHO_KEY" \
-H "Content-Type: application/json"
Responses
200 | Marked |
403 | Read only key. |
Objects
Note
| Field | Type | Notes |
|---|
id | string | Stable id for the note. Use it on /notes/{id}. Example: n_mtvqtjrmx2lws |
url | string | Example: https://example.com/pricing/ |
origin | string | Example: https://example.com |
path | string | Example: /pricing/ |
page_title | string | |
selector | string | CSS selector for the element the note is about. Example: main > section.hero > h1 |
fallback_selector | string | |
element | string | Example: h1 |
element_text | string | Example: Same day plumbing repair |
body | string | Example: Service plus city stays, the phrase carries local demand. |
category | seo or content or tech or a11y or perf or ux | |
status | decided or proposed or question or do not change | |
applies_to | array of all or mobile or tablet or laptop or desktop | Which viewport widths the note is about, separate from the width it was taken at. |
author | string | Resolved from the account that wrote it. Never sent by a client. |
team | string | |
mentions | array of object | Resolved in the database from @names in the body, and delivered to those inboxes. |
viewport | object | The viewport the note was written at. |
source | annotator or api | annotator means a person selected a real element on a real page. api means it was asserted through this API and nothing verified it. |
created_at | string | |
updated_at | string | |
Page
| Field | Type | Notes |
|---|
id | string | |
url | string | |
origin | string | |
path | string | |
title | string | |
notes | integer | |
updated_at | string | |
Team
| Field | Type | Notes |
|---|
id | string | |
name | string | |
members | integer | |
created_at | string | |
Member
| Field | Type | Notes |
|---|
user_id | string | |
name | string | |
role | owner or admin or member | |
created_at | string | |
Error
| Field | Type | Notes |
|---|
error | string | |