Developer documentation
Your team's priorities, from your own tools
A personal API token lets your scripts and AI assistants read your lists, suggest items and request done, and, where you hold the pen, add, order and finish. A token can do what you can do in the app, and nothing more.
API tokens
- Open your profile, then API tokens, then Create a token.
- Name it after what will use it, so you know which one to revoke later.
- Copy it straight away. It is shown once; Penholder keeps only a hash of it.
Calling the API
The base URL is https://penholder.app/api. Send the token as a bearer token; token calls need no other header. Bodies are JSON and every id is a string. The examples below keep the token in PENHOLDER_TOKEN.
export PENHOLDER_TOKEN="ph_YOUR_TOKEN"AI assistants (MCP)
Penholder is also a Model Context Protocol server, so assistants like OpenCode, Claude and Cursor can use your lists directly. Connect to https://penholder.app/api/mcp/ with the same token. Each tool calls the API above as you, so the pen holder rules apply exactly as they do in the app.
| Tool | Who | What it does |
|---|---|---|
list_teams | Anyone | Your teams |
get_team | Anyone on the team | Members, the pen holder, projects |
get_project | Anyone on the team | A project and its topics |
get_topic | Anyone on the team | The ordered list, fields, suggestions |
list_assigned | Anyone | Open items assigned to you |
suggest_item | Anyone on the team | Suggest an item |
flag_item | Anyone on the team | Ask the pen holder to look again |
request_done | Anyone on the team | Say an item is done, with a note |
add_item | Pen holder | Add an item at the bottom |
move_item | Pen holder | Move an item to a position |
finish_item | Pen holder | Finish, or confirm done |
accept_suggestion | Pen holder | Put a suggestion on the list |
decline_suggestion | Pen holder | Decline a suggestion |
OpenCode
Save the token in a file of its own, then add Penholder to opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"penholder": {
"type": "remote",
"url": "https://penholder.app/api/mcp/",
"headers": { "Authorization": "Bearer {file:~/.penholder/token}" }
}
}
}Claude Code
claude mcp add --transport http penholder https://penholder.app/api/mcp/ \
--header "Authorization: Bearer ph_YOUR_TOKEN"Cursor and other clients
{
"mcpServers": {
"penholder": {
"url": "https://penholder.app/api/mcp/",
"headers": { "Authorization": "Bearer ph_YOUR_TOKEN" }
}
}
}Postman
The Postman collection holds every call on this page with the answer it gave. Import it, set api_token and run the Reading folder: it fills in the ids of your first team, project and topic, and every request checks the status it answers. The other folders change real data, so try them on a team made for it.
Reading
GET/api/meAnyone
You
The person the token belongs to.
curl -s https://penholder.app/api/me \
-H "Authorization: Bearer $PENHOLDER_TOKEN"const res = await fetch("https://penholder.app/api/me", {
method: "GET",
headers: { Authorization: "Bearer " + process.env.PENHOLDER_TOKEN },
});
console.log(res.status, await res.json());import os
import httpx
r = httpx.get(
"https://penholder.app/api/me",
headers={"Authorization": f"Bearer {os.environ['PENHOLDER_TOKEN']}"},
)
print(r.status_code, r.json()){
"user": {
"id": "2c539862-0ceb-4095-836d-0ebd94711798",
"full_name": "Rob Mills",
"initials": "RM",
"avatar_url": null,
"email": "rob@example.com",
"pending_email": null
}
}GET/api/teamsAnyone
Your teams
Your teams, with their ids and plans.
curl -s https://penholder.app/api/teams \
-H "Authorization: Bearer $PENHOLDER_TOKEN"const res = await fetch("https://penholder.app/api/teams", {
method: "GET",
headers: { Authorization: "Bearer " + process.env.PENHOLDER_TOKEN },
});
console.log(res.status, await res.json());import os
import httpx
r = httpx.get(
"https://penholder.app/api/teams",
headers={"Authorization": f"Bearer {os.environ['PENHOLDER_TOKEN']}"},
)
print(r.status_code, r.json()){
"teams": [
{
"id": "62c99d17-5c7f-443c-b4ea-46df198b20b5",
"name": "Northwind",
"initials": "N",
"logo_url": null,
"role": "member",
"member_count": 2,
"project_count": 1,
"faces": [
{
"id": "72b37a72-42a5-4529-8cff-928a5db6757e",
"full_name": "Dana Kim",
"initials": "DK",
"avatar_url": null
},
{
"id": "2c539862-0ceb-4095-836d-0ebd94711798",
"full_name": "Rob Mills",
"initials": "RM",
"avatar_url": null
}
],
"plan_status": "beta",
"locked": false,
"plan": null
}
],
"billing_configured": false
}GET/api/teams/TEAM_IDAnyone on the team
A team
A team: members, who holds the pen, projects and the first page of history. More history: GET /api/teams/TEAM_ID/history?offset=20.
curl -s https://penholder.app/api/teams/TEAM_ID \
-H "Authorization: Bearer $PENHOLDER_TOKEN"const res = await fetch("https://penholder.app/api/teams/TEAM_ID", {
method: "GET",
headers: { Authorization: "Bearer " + process.env.PENHOLDER_TOKEN },
});
console.log(res.status, await res.json());import os
import httpx
r = httpx.get(
"https://penholder.app/api/teams/TEAM_ID",
headers={"Authorization": f"Bearer {os.environ['PENHOLDER_TOKEN']}"},
)
print(r.status_code, r.json()){
"team": {
"id": "62c99d17-5c7f-443c-b4ea-46df198b20b5",
"name": "Northwind",
"initials": "N",
"logo_url": null,
"created_at": "2026-09-15T17:40:30Z"
},
"role": "member",
"is_owner": false,
"you": "2c539862-0ceb-4095-836d-0ebd94711798",
"pen": {
"id": "72b37a72-42a5-4529-8cff-928a5db6757e",
"full_name": "Dana Kim",
"initials": "DK",
"avatar_url": null
},
"you_hold_pen": false,
"members": [
{
"id": "72b37a72-42a5-4529-8cff-928a5db6757e",
"full_name": "Dana Kim",
"initials": "DK",
"avatar_url": null,
"email": "dana@example.com",
"role": "owner",
"joined_at": "2026-09-15T17:40:30Z",
"holds_pen": true
},
{
"id": "2c539862-0ceb-4095-836d-0ebd94711798",
"full_name": "Rob Mills",
"initials": "RM",
"avatar_url": null,
"email": "rob@example.com",
"role": "member",
"joined_at": "2026-09-15T17:40:30Z",
"holds_pen": false
}
],
"invites": [],
"projects": [
{
"id": "00e14274-52d0-4a09-a32b-83cba87de735",
"name": "Q4 launch",
"description": "Everything that ships before the quarter closes.",
"image_url": null,
"topic_count": 1
}
],
"live_total": 2,
"waiting": 0,
"plan": {
"status": "beta",
"interval": null,
"current_period_end": null,
"billing_configured": false,
"price_month": 5,
"price_year": 50,
"locked": false,
"ending": false
},
"history": [
{
"id": "666ff3c2-7722-4bed-aa6b-8884ed60bf86",
"who": {
"id": "2c539862-0ceb-4095-836d-0ebd94711798",
"full_name": "Rob Mills",
"initials": "RM",
"avatar_url": null
},
"action": "joined the team",
"subject": "Northwind",
"details": null,
"at": "2026-09-15T17:40:30Z"
},
{
"id": "e8955d1e-5fe7-494c-b213-eef7059f5b82",
"who": {
"id": "72b37a72-42a5-4529-8cff-928a5db6757e",
"full_name": "Dana Kim",
"initials": "DK",
"avatar_url": null
},
"action": "invited",
"subject": "rob@example.com",
"details": null,
"at": "2026-09-15T17:40:30Z"
}
],
"history_more": false
}GET/api/projects/PROJECT_IDAnyone on the team
A project
A project and its topics.
curl -s https://penholder.app/api/projects/PROJECT_ID \
-H "Authorization: Bearer $PENHOLDER_TOKEN"const res = await fetch("https://penholder.app/api/projects/PROJECT_ID", {
method: "GET",
headers: { Authorization: "Bearer " + process.env.PENHOLDER_TOKEN },
});
console.log(res.status, await res.json());import os
import httpx
r = httpx.get(
"https://penholder.app/api/projects/PROJECT_ID",
headers={"Authorization": f"Bearer {os.environ['PENHOLDER_TOKEN']}"},
)
print(r.status_code, r.json()){
"project": {
"id": "00e14274-52d0-4a09-a32b-83cba87de735",
"name": "Q4 launch",
"description": "Everything that ships before the quarter closes.",
"image_url": null
},
"team": {
"id": "62c99d17-5c7f-443c-b4ea-46df198b20b5",
"name": "Northwind",
"initials": "N",
"logo_url": null
},
"you": "2c539862-0ceb-4095-836d-0ebd94711798",
"you_hold_pen": false,
"is_owner": false,
"pen": {
"id": "72b37a72-42a5-4529-8cff-928a5db6757e",
"full_name": "Dana Kim",
"initials": "DK",
"avatar_url": null
},
"members": [
{
"id": "72b37a72-42a5-4529-8cff-928a5db6757e",
"full_name": "Dana Kim",
"initials": "DK",
"avatar_url": null
},
{
"id": "2c539862-0ceb-4095-836d-0ebd94711798",
"full_name": "Rob Mills",
"initials": "RM",
"avatar_url": null
}
],
"topics": [
{
"id": "e751eacf-7f0c-45f5-a9c5-74b6af8cbf53",
"name": "Revenue",
"description": null,
"line": 5,
"image_url": null,
"above": 2,
"below": 0,
"done": 0,
"filters": 1,
"flagged": 0,
"waiting": 0
}
]
}GET/api/topics/TOPIC_IDAnyone on the team
A topic's ordered list
A topic's ordered list: above the line (the shortlist), below it, recently done, its fields, waiting suggestions and recent history. More history: GET /api/topics/TOPIC_ID/history?offset=20.
curl -s https://penholder.app/api/topics/TOPIC_ID \
-H "Authorization: Bearer $PENHOLDER_TOKEN"const res = await fetch("https://penholder.app/api/topics/TOPIC_ID", {
method: "GET",
headers: { Authorization: "Bearer " + process.env.PENHOLDER_TOKEN },
});
console.log(res.status, await res.json());import os
import httpx
r = httpx.get(
"https://penholder.app/api/topics/TOPIC_ID",
headers={"Authorization": f"Bearer {os.environ['PENHOLDER_TOKEN']}"},
)
print(r.status_code, r.json()){
"topic": {
"id": "e751eacf-7f0c-45f5-a9c5-74b6af8cbf53",
"name": "Revenue",
"description": null,
"line": 5,
"created_at": "2026-09-15T17:40:30Z",
"image_url": null
},
"project": {
"id": "00e14274-52d0-4a09-a32b-83cba87de735",
"name": "Q4 launch"
},
"team": {
"id": "62c99d17-5c7f-443c-b4ea-46df198b20b5",
"name": "Northwind",
"initials": "N",
"logo_url": null
},
"you": "2c539862-0ceb-4095-836d-0ebd94711798",
"you_hold_pen": false,
"is_owner": false,
"pen": {
"id": "72b37a72-42a5-4529-8cff-928a5db6757e",
"full_name": "Dana Kim",
"initials": "DK",
"avatar_url": null
},
"members": [
{
"id": "72b37a72-42a5-4529-8cff-928a5db6757e",
"full_name": "Dana Kim",
"initials": "DK",
"avatar_url": null,
"holds_pen": true,
"joined_at": "2026-09-15T17:40:30Z"
},
{
"id": "2c539862-0ceb-4095-836d-0ebd94711798",
"full_name": "Rob Mills",
"initials": "RM",
"avatar_url": null,
"holds_pen": false,
"joined_at": "2026-09-15T17:40:30Z"
}
],
"above": [
{
"id": "6d217ccd-7a0e-4e47-99c4-be6b28840a9e",
"title": "Invoice the retainer",
"reason": "It pays **next month's** payroll.",
"status": "open",
"position": 1,
"assignee": null,
"image_url": null,
"thumb_url": null,
"stale": false,
"last_moved_at": "2026-09-15T17:40:30Z",
"created_at": "2026-09-15T17:40:30Z",
"solved_at": null,
"flag": null,
"values": {},
"done_request": null,
"done_proof": null
},
{
"id": "aea0d9b2-2ac8-4578-a39f-a2e0fbf5165d",
"title": "Chase the late client",
"reason": "It pays **next month's** payroll.",
"status": "open",
"position": 2,
"assignee": null,
"image_url": null,
"thumb_url": null,
"stale": false,
"last_moved_at": "2026-09-15T17:40:30Z",
"created_at": "2026-09-15T17:40:30Z",
"solved_at": null,
"flag": null,
"values": {},
"done_request": null,
"done_proof": null
}
],
"below": [],
"done": [],
"suggestions": [],
"filters": [
{
"id": "76bd080c-2b7c-46f8-b8f0-60349c60347b",
"name": "Brings money?",
"kind": "yes_no",
"created_at": "2026-09-15T17:40:30Z",
"born_from": null
}
],
"activity": [
{
"id": "575b6335-d0f9-4617-af4d-1aca563c7b5d",
"who": {
"id": "72b37a72-42a5-4529-8cff-928a5db6757e",
"full_name": "Dana Kim",
"initials": "DK",
"avatar_url": null
},
"action": "put on the list",
"subject": "Chase the late client",
"details": "It pays **next month's** payroll.",
"at": "2026-09-15T17:40:30Z"
},
{
"id": "3a52aab4-4d88-4a6e-a102-8df9e0c0184c",
"who": {
"id": "72b37a72-42a5-4529-8cff-928a5db6757e",
"full_name": "Dana Kim",
"initials": "DK",
"avatar_url": null
},
"action": "put on the list",
"subject": "Invoice the retainer",
"details": "It pays **next month's** payroll.",
"at": "2026-09-15T17:40:30Z"
}
],
"activity_more": false,
"last_ordered_at": null,
"flagged": 0,
"park_days": 30,
"line_min": 1,
"line_max": 50
}GET/api/assignedAnyone
Assigned to you
Open items assigned to you, across all your teams.
curl -s https://penholder.app/api/assigned \
-H "Authorization: Bearer $PENHOLDER_TOKEN"const res = await fetch("https://penholder.app/api/assigned", {
method: "GET",
headers: { Authorization: "Bearer " + process.env.PENHOLDER_TOKEN },
});
console.log(res.status, await res.json());import os
import httpx
r = httpx.get(
"https://penholder.app/api/assigned",
headers={"Authorization": f"Bearer {os.environ['PENHOLDER_TOKEN']}"},
)
print(r.status_code, r.json()){
"items": [],
"total": 0
}Anyone on the team
POST/api/suggestionsAnyone on the team
Suggest an item
Suggest an item; it waits for the pen holder to accept or decline. reason is Markdown, assigned_to a member's id, values maps field ids to answers. Edit it while it waits with PATCH /api/suggestions/SUGGESTION_ID, or take it back with POST /api/suggestions/SUGGESTION_ID/withdraw.
curl -s -X POST https://penholder.app/api/suggestions \
-H "Authorization: Bearer $PENHOLDER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"topic_id": "TOPIC_ID",
"title": "Renew the domain",
"reason": "It expires on the **30th**.",
"assigned_to": "MEMBER_ID",
"values": {
"FIELD_ID": "no"
}
}'const res = await fetch("https://penholder.app/api/suggestions", {
method: "POST",
headers: { Authorization: "Bearer " + process.env.PENHOLDER_TOKEN, "Content-Type": "application/json" },
body: JSON.stringify({
"topic_id": "TOPIC_ID",
"title": "Renew the domain",
"reason": "It expires on the **30th**.",
"assigned_to": "MEMBER_ID",
"values": {
"FIELD_ID": "no"
}
}),
});
console.log(res.status, await res.json());import os
import httpx
r = httpx.post(
"https://penholder.app/api/suggestions",
headers={"Authorization": f"Bearer {os.environ['PENHOLDER_TOKEN']}"},
json={
"topic_id": "TOPIC_ID",
"title": "Renew the domain",
"reason": "It expires on the **30th**.",
"assigned_to": "MEMBER_ID",
"values": {
"FIELD_ID": "no"
}
},
)
print(r.status_code, r.json()){
"suggestion": {
"id": "47ac734d-527a-4202-ac28-6c030198ad62",
"title": "Renew the domain",
"reason": "It expires on the **30th**.",
"assignee": {
"id": "2c539862-0ceb-4095-836d-0ebd94711798",
"full_name": "Rob Mills",
"initials": "RM",
"avatar_url": null
},
"by": {
"id": "2c539862-0ceb-4095-836d-0ebd94711798",
"full_name": "Rob Mills",
"initials": "RM",
"avatar_url": null
},
"created_at": "2026-09-15T17:40:31Z",
"values": {
"76bd080c-2b7c-46f8-b8f0-60349c60347b": "no"
}
}
}POST/api/priorities/ITEM_ID/flagAnyone on the team
Ask for another look
Ask the pen holder to look at an item again. Nothing moves. POST /api/priorities/ITEM_ID/unflag removes your flag.
curl -s -X POST https://penholder.app/api/priorities/ITEM_ID/flag \
-H "Authorization: Bearer $PENHOLDER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"note": "The client moved the deadline."
}'const res = await fetch("https://penholder.app/api/priorities/ITEM_ID/flag", {
method: "POST",
headers: { Authorization: "Bearer " + process.env.PENHOLDER_TOKEN, "Content-Type": "application/json" },
body: JSON.stringify({
"note": "The client moved the deadline."
}),
});
console.log(res.status, await res.json());import os
import httpx
r = httpx.post(
"https://penholder.app/api/priorities/ITEM_ID/flag",
headers={"Authorization": f"Bearer {os.environ['PENHOLDER_TOKEN']}"},
json={
"note": "The client moved the deadline."
},
)
print(r.status_code, r.json()){
"ok": true
}POST/api/priorities/ITEM_ID/request-doneAnyone on the team
Request done
Say an item is done, as a form with note and an optional file image. It stays on the list until the pen holder confirms. POST /api/priorities/ITEM_ID/withdraw-done takes it back.
curl -s -X POST https://penholder.app/api/priorities/ITEM_ID/request-done \
-H "Authorization: Bearer $PENHOLDER_TOKEN" \
-F "note=Sent and paid on Friday."const res = await fetch("https://penholder.app/api/priorities/ITEM_ID/request-done", {
method: "POST",
headers: { Authorization: "Bearer " + process.env.PENHOLDER_TOKEN },
body: new URLSearchParams({"note": "Sent and paid on Friday."}),
});
console.log(res.status, await res.json());import os
import httpx
r = httpx.post(
"https://penholder.app/api/priorities/ITEM_ID/request-done",
headers={"Authorization": f"Bearer {os.environ['PENHOLDER_TOKEN']}"},
data={"note": "Sent and paid on Friday."},
)
print(r.status_code, r.json()){
"ok": true
}Pen holder
POST /api/projects, POST /api/topics and POST /api/topics/TOPIC_ID/filters with {"name", "kind"}, where kind is yes_no or number.POST/api/prioritiesPen holder
Add an item
Put an item at the bottom of a topic's list. similar lists open items with close titles, so you can spot a duplicate. Edit with PATCH /api/priorities/ITEM_ID.
curl -s -X POST https://penholder.app/api/priorities \
-H "Authorization: Bearer $PENHOLDER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"topic_id": "TOPIC_ID",
"title": "Plan the offsite",
"reason": "- book the venue\n- send the agenda",
"assigned_to": "MEMBER_ID"
}'const res = await fetch("https://penholder.app/api/priorities", {
method: "POST",
headers: { Authorization: "Bearer " + process.env.PENHOLDER_TOKEN, "Content-Type": "application/json" },
body: JSON.stringify({
"topic_id": "TOPIC_ID",
"title": "Plan the offsite",
"reason": "- book the venue\n- send the agenda",
"assigned_to": "MEMBER_ID"
}),
});
console.log(res.status, await res.json());import os
import httpx
r = httpx.post(
"https://penholder.app/api/priorities",
headers={"Authorization": f"Bearer {os.environ['PENHOLDER_TOKEN']}"},
json={
"topic_id": "TOPIC_ID",
"title": "Plan the offsite",
"reason": "- book the venue\n- send the agenda",
"assigned_to": "MEMBER_ID"
},
)
print(r.status_code, r.json()){
"item": {
"id": "73908bca-7f88-4430-a3ea-05b2d708e3af",
"title": "Plan the offsite",
"reason": "- book the venue\n- send the agenda",
"status": "open",
"position": 3,
"assignee": {
"id": "2c539862-0ceb-4095-836d-0ebd94711798",
"full_name": "Rob Mills",
"initials": "RM",
"avatar_url": null
},
"image_url": null,
"thumb_url": null,
"stale": false,
"last_moved_at": "2026-09-15T17:40:31Z",
"created_at": "2026-09-15T17:40:31Z",
"solved_at": null,
"flag": null,
"values": {},
"done_request": null,
"done_proof": null
},
"similar": false
}POST/api/priorities/ITEM_ID/movePen holder
Move an item
Move an open item to a position, 1 being the top. POST /api/topics/TOPIC_ID/order with {"ids": [...]} sets the whole order at once, and POST /api/topics/TOPIC_ID/line with {"line": 5} moves the line.
curl -s -X POST https://penholder.app/api/priorities/ITEM_ID/move \
-H "Authorization: Bearer $PENHOLDER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"position": 1,
"why": "The venue fills up."
}'const res = await fetch("https://penholder.app/api/priorities/ITEM_ID/move", {
method: "POST",
headers: { Authorization: "Bearer " + process.env.PENHOLDER_TOKEN, "Content-Type": "application/json" },
body: JSON.stringify({
"position": 1,
"why": "The venue fills up."
}),
});
console.log(res.status, await res.json());import os
import httpx
r = httpx.post(
"https://penholder.app/api/priorities/ITEM_ID/move",
headers={"Authorization": f"Bearer {os.environ['PENHOLDER_TOKEN']}"},
json={
"position": 1,
"why": "The venue fills up."
},
)
print(r.status_code, r.json()){
"ok": true
}POST/api/priorities/ITEM_ID/donePen holder
Finish, or confirm done
Finish an item, or confirm someone's done request. POST /api/priorities/ITEM_ID/restore reopens it, and POST /api/priorities/ITEM_ID/send-back with {"why"} sends a done request back.
curl -s -X POST https://penholder.app/api/priorities/ITEM_ID/done \
-H "Authorization: Bearer $PENHOLDER_TOKEN"const res = await fetch("https://penholder.app/api/priorities/ITEM_ID/done", {
method: "POST",
headers: { Authorization: "Bearer " + process.env.PENHOLDER_TOKEN },
});
console.log(res.status, await res.json());import os
import httpx
r = httpx.post(
"https://penholder.app/api/priorities/ITEM_ID/done",
headers={"Authorization": f"Bearer {os.environ['PENHOLDER_TOKEN']}"},
)
print(r.status_code, r.json()){
"ok": true
}POST/api/suggestions/SUGGESTION_ID/acceptPen holder
Accept a suggestion
Put a waiting suggestion on the list. POST /api/suggestions/SUGGESTION_ID/decline with {"why"} declines it.
curl -s -X POST https://penholder.app/api/suggestions/SUGGESTION_ID/accept \
-H "Authorization: Bearer $PENHOLDER_TOKEN"const res = await fetch("https://penholder.app/api/suggestions/SUGGESTION_ID/accept", {
method: "POST",
headers: { Authorization: "Bearer " + process.env.PENHOLDER_TOKEN },
});
console.log(res.status, await res.json());import os
import httpx
r = httpx.post(
"https://penholder.app/api/suggestions/SUGGESTION_ID/accept",
headers={"Authorization": f"Bearer {os.environ['PENHOLDER_TOKEN']}"},
)
print(r.status_code, r.json()){
"item": {
"id": "bcbb1e63-bdf6-44c9-9927-2f624d99e4b5",
"title": "Renew the domain",
"reason": "It expires on the **30th**.",
"status": "open",
"position": 3,
"assignee": {
"id": "2c539862-0ceb-4095-836d-0ebd94711798",
"full_name": "Rob Mills",
"initials": "RM",
"avatar_url": null
},
"image_url": null,
"thumb_url": null,
"stale": false,
"last_moved_at": "2026-09-15T17:40:31Z",
"created_at": "2026-09-15T17:40:31Z",
"solved_at": null,
"flag": null,
"values": {
"76bd080c-2b7c-46f8-b8f0-60349c60347b": "no"
},
"done_request": null,
"done_proof": null
}
}Browser only
A token gets 403 on these, which take a signed-in browser: signing out, changing or resetting the password, changing the email, deleting the account or a team, billing checkout and the billing page, and listing, creating or revoking tokens.
Errors
Errors are JSON with a detail sentence you can show to a person.
| Status | Meaning |
|---|---|
400 | Something in the body is missing or not allowed |
401 | No token, an unknown one, or a revoked one |
402 | The team is not paid up: it has no plan, or a payment is due. Everything in it answers 402 until it is paid |
403 | A browser-only call, or it needs the pen |
404 | Not found, or not on a team you are in |
409 | A conflict, like an eleventh token |
429 | Too many requests; wait the seconds in Retry-After |
POST/api/priorities
A member cannot add
Without the pen, writes that change the list are refused, and the answer says what to do instead.
curl -s -X POST https://penholder.app/api/priorities \
-H "Authorization: Bearer $PENHOLDER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"topic_id": "TOPIC_ID",
"title": "Straight on the list"
}'const res = await fetch("https://penholder.app/api/priorities", {
method: "POST",
headers: { Authorization: "Bearer " + process.env.PENHOLDER_TOKEN, "Content-Type": "application/json" },
body: JSON.stringify({
"topic_id": "TOPIC_ID",
"title": "Straight on the list"
}),
});
console.log(res.status, await res.json());import os
import httpx
r = httpx.post(
"https://penholder.app/api/priorities",
headers={"Authorization": f"Bearer {os.environ['PENHOLDER_TOKEN']}"},
json={
"topic_id": "TOPIC_ID",
"title": "Straight on the list"
},
)
print(r.status_code, r.json()){
"detail": "Only the person holding the pen adds to the list. Suggest it instead."
}GET/api/teams
An unknown or revoked token
No token, an unknown one, or a revoked one.
curl -s https://penholder.app/api/teams \
-H "Authorization: Bearer $PENHOLDER_TOKEN"const res = await fetch("https://penholder.app/api/teams", {
method: "GET",
headers: { Authorization: "Bearer " + process.env.PENHOLDER_TOKEN },
});
console.log(res.status, await res.json());import os
import httpx
r = httpx.get(
"https://penholder.app/api/teams",
headers={"Authorization": f"Bearer {os.environ['PENHOLDER_TOKEN']}"},
)
print(r.status_code, r.json()){
"detail": "That API token is not valid. It may have been revoked. Create a new one in your profile."
}Limits
Each token has its own request budget, wherever it calls from. Reasons and descriptions hold up to 5,000 characters of Markdown, titles 200, and notes 300.