herro API
A small, stable REST API for reading your boards and creating or updating initiatives — plus inbound and outbound webhooks.
Overview
All requests go to the API base URL. Every response is JSON, and CORS is enabled so you can call it from a browser app.
https://twsow4mkgdvwkmsdfo3q6igdwm0xzdhg.lambda-url.us-east-1.on.aws
The current version is v1. Endpoints are additive — the response shapes documented here are a stable whitelist and won't change without a version bump.
Authentication
Create a personal access token in the app under Account → API tokens. The token is shown once at creation (only its hash is stored), and can be revoked from the same screen. Send it on every request:
Authorization: Bearer hro_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
curl -H "Authorization: Bearer hro_…" \
https://twsow4mkgdvwkmsdfo3q6igdwm0xzdhg.lambda-url.us-east-1.on.aws/v1/me
A missing, malformed, or revoked token returns 401. Tokens act on the data owned by the account that created them.
Token scopes
Each token has an access level, chosen when you create it — so a script that only needs to read can never mutate anything:
| Scope | Allows |
|---|---|
read | GET endpoints only — any mutation returns 403. |
write | Reads plus create and update. No deletes, no invites. (Default.) |
full | Everything, including DELETE endpoints and POST /v1/invites. |
Me
GET/v1/me
Identifies the token's account. Useful as a connectivity check.
{ "name": "Ada Lovelace", "email": "ada@example.com", "tokenName": "CI token" }
Workspaces
GET/v1/workspaces
{ "workspaces": [ { "id": "ws-main", "name": "Main", "archived": false } ] }
POST/v1/workspaces
Body: { "name": "..." }. Creates a workspace in your organization; returns 201.
PATCH/v1/workspaces/{id}
Body: name and/or archived (boolean).
Boards
GET/v1/boards
{ "boards": [ { "id": "board-home", "name": "Homebase", "workspaceId": "ws-main", "archived": false } ] }
POST/v1/boards
Body: { "name": "...", "workspaceId": "ws-main", "icon"?: "home" } → 201.
PATCH/v1/boards/{id}
Body: name and/or archived.
DELETE/v1/boards/{id}
Deletes the board and everything on it (focus areas, their groups, their initiatives). The response reports what was cascaded — this is not reversible.
Focus areas
GET/v1/focus-areas
| Query param | Description |
|---|---|
board | Only focus areas on this board id. |
{ "focusAreas": [ { "id": "fa-1", "title": "Now", "boardId": "board-home", "status": null, "archived": false } ] }
POST/v1/focus-areas
Body: { "title": "...", "boardId": "board-home", "color"?: "oklch(…)" } → 201.
PATCH/v1/focus-areas/{id}
Body: title and/or archived.
DELETE/v1/focus-areas/{id}
Deletes the column and every initiative in it (cascade reported in the response). Not reversible.
Initiatives
GET/v1/initiatives
| Query param | Description |
|---|---|
board | Only initiatives whose focus area is on this board. |
focus | Only initiatives in this focus area id. |
GET/v1/initiatives/{id}
Returns one initiative, or 404 if the id doesn't exist. The initiative shape:
{
"id": "HB-3F2K",
"title": "Ship the Q3 landing page",
"focusAreaId": "fa-1",
"priority": "P2",
"due": "2026-09-01",
"status": "doing",
"completed": false,
"completedAt": null,
"archived": false,
"labels": ["web"],
"createdAt": "2026-08-20T14:03:00.000Z"
}
POST/v1/initiatives
| Body field | Description |
|---|---|
title required | The initiative title (capped at 300 characters). |
focusAreaId required | Where the card lands — list ids with GET /v1/focus-areas. |
priority | P0–P4; anything else is stored as no priority. |
due | Due date string, e.g. 2026-09-01 (capped at 64 characters). |
status | One of none · backlog · todo · doing · review · done · blocked. |
description | Long-form text (capped at 20,000 characters). |
labels | Array of strings (max 20, each ≤40 chars). |
assignees | Array of member ids from GET /v1/members (max 20). |
Returns 201 with the created initiative. Ids follow the app's style (a board-derived prefix plus a short code, like HB-3F2K).
curl -X POST \
-H "Authorization: Bearer hro_…" -H "Content-Type: application/json" \
-d '{"title":"Fix login copy","focusAreaId":"fa-1","priority":"P2"}' \
https://twsow4mkgdvwkmsdfo3q6igdwm0xzdhg.lambda-url.us-east-1.on.aws/v1/initiatives
PATCH/v1/initiatives/{id}
| Body field | Description |
|---|---|
title | New title. |
priority | P0–P4, or null to clear. |
due | New due date, or null to clear. |
completed | true/false — completing stamps completedAt. |
status / description / labels / assignees / archived | Same semantics as create; archived is a boolean. |
Only the fields you send are changed. Returns the updated initiative.
DELETE/v1/initiatives/{id}
Permanently deletes the initiative.
Comments
GET/v1/initiatives/{id}/comments
{ "comments": [ { "id": "c1756…", "author": "Ada", "text": "On it", "ts": 1756150000000 } ] }
POST/v1/initiatives/{id}/comments
Body: { "text": "…", "author"?: "Bot name" } (text ≤4,000 chars) → 201. Comments posted by the API are attributed to the token (e.g. API (CI token)) unless author is given.
Attachments
GET/v1/initiatives/{id}/attachments
Lists attachments with fresh, short-lived downloadUrls (valid 1 hour).
POST/v1/initiatives/{id}/attachments
Two-step upload. Body: { "name": "report.pdf", "type": "application/pdf", "size": 12345 } (max 25MB). The response registers the attachment on the card and returns a presigned uploadUrl — PUT the file bytes there within 10 minutes, with the same Content-Type:
curl -X PUT -H "Content-Type: application/pdf" --data-binary @report.pdf "<uploadUrl>"
Members & invites
GET/v1/members
Your workspace roster — ids here are what assignees accepts.
POST/v1/invites
Body: { "email"?: "person@co.com", "role"?: "member|admin|viewer", "boardIds"?: ["board-…"] } — an empty boardIds array means workspace-only (no boards). Returns the invite link; when email is set, only that address can accept it. (The API creates the invite — it does not send the email; share the link yourself.)
Shared workspaces
Your token also sees organizations you've been invited to: boards, focus areas, and initiatives shared with you appear in the same lists, and you can write to them with the same endpoints. Two rules carry over from the app: viewers are read-only (writes return 403), and you can only create invites for your own organization.
Webhooks & deploy cards
Inbound — GitHub pushes
Connect a repository from Settings → Integrations → GitHub feed: herro gives you a webhook URL (POST /v1/github-webhook?k=…) and a secret for the repo's webhook settings. Deliveries are verified against the raw body with X-Hub-Signature-256 (HMAC SHA-256) — unsigned or mis-signed deliveries are rejected.
HB-3F2K: fix header — links that commit onto the card as a comment.Auto deploy-cards
Turn on Auto-create deploy cards on the GitHub feed and pick a focus area. From then on, work tracks itself:
- Push → one card per repo+branch is created in that column (titled from the commit), status In Progress, with every commit logged as a comment. Further pushes to the same branch update the same card.
- Deploy succeeds → the card is marked complete automatically. Send GitHub's
workflow_run(GitHub Actions) ordeployment_statusevents to the same webhook — or ping the endpoint below from any CI. - Deploy fails → the card flips to Blocked with a failure comment.
POST/v1/deploys/complete
Universal completion ping for pipelines GitHub can't see (Amplify, Jenkins, bare scripts). Bearer-token authenticated; needs a read & write token. Body: { "repo": "org/name", "branch": "main", "status"?: "success"|"failure", "note"?: "…" } — closes (or flags) the open deploy card for that repo+branch.
# last line of any deploy script:
curl -X POST -H "Authorization: Bearer hro_…" -H "Content-Type: application/json" \
-d '{"repo":"acme/site","branch":"main","note":"deploy #342"}' \
https://twsow4mkgdvwkmsdfo3q6igdwm0xzdhg.lambda-url.us-east-1.on.aws/v1/deploys/complete
POST/v1/deploys/ingest
The no-webhook path: report a finished deploy and herro pulls the story itself — the commit range since the last successful deploy is read from the GitHub API using your Settings → Integrations GitHub token (needs read access to the repo's contents), classified by changed files through your per-repo rules, and the card is created already completed. Failed deploys create nothing — their commits roll into the next successful deploy's card. Body: { "repo", "branch", "status"?, "commitId"? | ("appId" + "jobId"), "note"? } — with appId+jobId, herro resolves the deployed commit from the Amplify job itself.
For AWS Amplify specifically: an EventBridge rule on aws.amplify deployment-status events targeting an API destination that calls this endpoint gives you fully automatic completion with no pipeline changes.
POST/v1/deploys/slack-digest
Writes the day's deploy rundown — the same one-line-per-focus-area summary as the board's ✨ Summarize (All) — and posts it to your Slack incoming webhook from Settings → Integrations. Meant to be fired nightly by a scheduler. Body: { "board"?, "dryRun"? } — board scopes the digest to one board's focus areas, dryRun: true returns the message without posting. Days with no deploys post nothing.
Outbound — Slack, Teams, Zapier
Point herro at a Slack incoming webhook, a Teams webhook, or a Zapier catch hook from Settings → Integrations, and workspace activity is delivered there as it happens.
Errors
Errors are JSON with an error message:
400— invalid JSON body, or a missing required field401— missing, malformed, or revoked token / bad webhook signature403— you have viewer (read-only) access to that row404— unknown route or id (unknown routes list the valid ones)413— request body over 100KB500— something failed on our side