Docs
Search docs

Begin claiming a GitHub organization's slug by proof

POST/api/orgs/claims

beginOrganizationClaim

The first half of the org claim (`plans/github-app-login-and-org-claim-2026-09.md`, Track C1). `slug` must be a GitHub **organisation**'s login: `GET /users/{slug}` is asked, and a handle that is a GitHub user (`400`) or is not on GitHub at all (`400` — create the org directly, nothing reserves it) is refused, as is a slug that is already a workspace here (`409 already_exists`). Every begin then runs the live admin proof — the same `GET /user/memberships/orgs/{org}` read the completion runs, under the caller's own GitHub App user token — before anything else is asked of GitHub. The answer is tagged on `next`. **`install`**: a single-use, 30-minute consent nonce is minted that remembers the slug, and `install_url` carries it to GitHub's App install page pinned to that organisation (`installations/new/permissions?target_id=…`). Open it in a browser — do not fetch it. **`complete`**: the caller has just proved they administer the organisation, and the App is *already installed* on it (from github.com, or after an earlier consent expired) — which this route reads, under the App's own credential, only behind a passing proof. GitHub's page for an installed App is its Configure page, which never sends the browser back, so no nonce is minted and nothing is installed: complete the claim with `POST /api/orgs/claims/{installation_id}` straight away. Anyone the proof does not admit — a member, a stranger, an admin under an App the organisation has not granted *Members: read* — gets `install` for an installed organisation exactly as for one without the App, with no installation read made; and a proof that fails for a reason that is no verdict (GitHub rejecting the caller's token, `401 unauthenticated`; rate-limiting it, `502 upstream`) fails the same way whether or not the App is installed. So this route never says which organisations installed the App, and what it spends on GitHub is fixed by the caller, not by the organisation: one membership read for everyone, the App-JWT installation read only for a proven admin — all bounded by its per-account rate limit. Two refusals the callback would make are made here instead, before the browser leaves — and before GitHub is asked anything, since both are facts about the caller's own account: `github_identity_required` (403, this account has no linked GitHub identity) and `github_app_login_required` (403, the GitHub link is a classic OAuth-App login, which cannot prove an organisation — reconnect GitHub). Installing the App there brings the browser back through the App installation callback, which proves the caller administers the organisation and sends them to `/new/organization/claim?installation_id=…`. **Nothing is created by this route or by the callback**; the workspace is created by `POST /api/orgs/claims/{installation_id}`, after the proof is run again. **Browser sessions only.** A PAT or workspace token is refused at the gate: the flow ends in a workspace the caller owns and a durable App installation tied to their account, neither of which an attenuated token may mint. 404 when this deployment has no GitHub App configured.

Requires authentication using a session cookie — see tokens and scopes.

curl

curl -X POST 'https://neosource.dev/api/orgs/claims' \
  -b 'ns_session=$NEOSOURCE_SESSION' \
  -H 'Content-Type: application/json' \
  -d '{"slug":"string"}'

fetch

fetch("https://neosource.dev/api/orgs/claims", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  credentials: "include",
  body: JSON.stringify({"slug":"string"}),
});

Request bodyrequired

application/json

OrgClaimBeginRequest

object

`POST /api/orgs/claims` — start claiming a GitHub organisation's slug by proof (`plans/github-app-login-and-org-claim-2026-09.md`, Track C1).

slugrequired

string

The slug to claim — a GitHub **organisation**'s login. A handle that is a GitHub user, or that is not on GitHub at all, is refused: the first is claimed by signing in with that account, the second needs no claim (create the org directly).

Responses

200Where to send the browser

application/json

OrgClaimBeginResponse

What `POST /api/orgs/claims` decided, tagged on `next`. `install`: the App is not on the organisation yet — where to send the browser to install it, and the single-use nonce riding on that URL. `complete`: the App is **already installed** on the organisation (from github.com, or after an earlier consent expired) and the caller has just proved, live, that they administer it — GitHub's page for an installed App is its Configure page, which never sends the browser back, so there is nothing to install and no nonce; complete the claim with `POST /api/orgs/claims/{installation_id}` straight away, whose live admin proof is the lock either way. Only a proven admin is told "already installed": everyone else gets `install` whether the App is there or not, so the route never says which organisations installed it.

one of
  • object

    install_urlrequired

    string

    `https://github.com/apps/{slug}/installations/new/permissions?target_id={org}&state=…` — open it in the browser, do not fetch it. `target_id` pins GitHub's install page to the organisation the slug names (only the `/permissions` page honours it). GitHub redirects back to the App installation callback, which sends the browser on to `/new/organization/claim?installation_id=…`.

    nextrequired

    string

    "install"

    staterequired

    string

    The nonce embedded in `install_url`; single-use, expires in 30 minutes.

  • object

    installation_idrequired

    integerint64

    GitHub's id for the App's installation on the organisation — the `{installation_id}` to complete with.

    nextrequired

    string

    "complete"

Standard errors

Bodies documented once for the whole API — see standard errors.

  • 400Bad Request — one of: invalid_input
  • 401Authentication required
  • 403Forbidden — one of: forbidden, github_app_login_required, github_app_permission_missing, github_identity_required, not_an_org_admin, org_installations_unsupported
  • 404Not Found — one of: not_found
  • 409Conflict — one of: already_exists, conflict, not_an_org_installation
  • 429Rate limited — retry after the `Retry-After` header
  • 500Internal server error
  • 502Bad Gateway — one of: upstream
  • 503Service temporarily unavailable / at capacity — retry after the `Retry-After` header
  • 504Gateway timeout — the request exceeded the server's handling budget