Skip to content

Overview

Work management on GitHub Issues and Projects, with portals for the people who are not on GitHub.

GitHub is the system of record. Every issue, comment, label, milestone, project and field value this API returns exists in GitHub first, and every write goes to GitHub before it goes anywhere else — so a write this API reports is a write GitHub accepted.

Reads come from Pealboard’s cache of GitHub and cost the installation nothing. Writes cost one GitHub round trip and are serialised per installation, so a bulk edit cannot race itself into a secondary rate limit.

Every response carries X-Request-Id. Every failure has the same shape: { error: { code, message, detail?, remedy?, requestId } }. Lists are cursor-paged: send nextCursor back as cursor. Creates accept Idempotency-Key, and a retry with the same key returns the first answer rather than making a second object.

A workspace has a default repository, and an issue does not have to name one. POST /v1/issues takes repositoryId as optional: omitted, the issue is created in the repository the workspace set as its default with PATCH /v1/workspace, which is what makes filing an issue possible for somebody who does not know which of fifty repositories it belongs to. A workspace with no default refuses with 422 and says to set one; GET /v1/github/default-repository/suggestion proposes which repository that should be and links to GitHub’s own form for creating one, because Pealboard does not create repositories. A portal may name its own default for the request types that name none, and a request type resolves its repository once, when it is saved.

Engineering routes the work afterwards, not at intake. POST /v1/issues/{issueId}/sub-issues splits one issue into up to twenty children, each in a repository it names — a different repository from the parent is the ordinary case and is what the split is for — and links each one to the parent as a GitHub sub-issue. Each child says Part of <owner>/<repo>#<number> in its body. Creating and linking are separate GitHub calls, so a child past GitHub’s limits (a hundred sub-issues per parent, eight levels of nesting) comes back created with linked: false and GitHub’s own sentence. GET /v1/issues/{issueId}/sub-issues lists the children, and an issue that is one carries parent.

Sign-in failures do not use that envelope, because they are browser redirects and not API calls. Better Auth’s endpoints under /auth and /portal/auth redirect a failed sign-in to /auth/error?error=<code> on the origin the person is looking at — the workspace application for /auth, and the portal the request arrived on for /portal/auth. Both applications proxy that path back to this API, so this API answers it, with a second redirect to the sign-in screen each one really serves: /signin?error=<code> on the workspace application and /sign-in?error=<code> on a portal. The client reads the error query parameter there and says what to do about it. The values are Better Auth’s, not this API’s; the ones a person actually meets are email_not_found (a GitHub account with no address this App may read), account_not_linked, signup_disabled, please_restart_the_process (an expired or reused state) and unable_to_create_user. Treat an unrecognised value as a generic failure and offer the sign-in screen again.

A sign-in link can be built for the mobile app. POST /auth/sign-in/magic-link accepts an optional client of web or mobile, or an optional callbackURL — and the only values it accepts are the workspace application’s own origin and pealboard://, the app’s private-use scheme. A mobile link is pealboard://sign-in/link#token=…; a web one, and the default, is the same path on the application. Anything else is refused with invalid_request rather than silently sent to the default, because the link is a live credential and quietly delivering it elsewhere is an open redirect with a password in it. The token is in the fragment either way, so it is never in a server log or a Referer.

Signing up carries a Turnstile token. POST /auth/sign-up/email takes turnstileToken beside email, password and name: the widget’s response, from a Cloudflare Turnstile widget on the sign-up form. The API verifies it with Cloudflare before Better Auth sees the request, and checks the hostname the challenge was solved on as well as the token, because a token solved on somebody else’s page is a valid token for the wrong site. Every way of not having a solved challenge is a refusal, and they read differently on purpose: no token at all is 422 with "This form needs its challenge completed.", which is the one a client without the widget meets; a token that does not verify or was solved on another site is 403; a verifier that cannot be reached is 502, and the account is not created. A deployment with no TURNSTILE_SECRET does not check, and creates the account — which is what makes a local run and a preview usable, and is not true of api.pealboard.com. The portal’s two sign-in paths and POST /portal/requests on an open portal take the same field and answer the same way.

A session token, or an API key beginning pb_. A browser sends the session cookie instead and needs no header. X-Workspace-Id names the workspace when the credential belongs to more than one.

Security scheme type: http