Docs
Search docs

Complete a GitHub organization claim: prove admin, create the org

POST/api/orgs/claims/{installation_id}

completeOrganizationClaim

The second half of the org claim, made by `/new/organization/claim` after the App installation callback lands there — or straight after a begin that answered `next: complete`. `installation_id` must be an **active** App installation on a GitHub **organisation** (`409 not_an_org_installation` otherwise — one answer whether the id is unknown to GitHub, suspended, an enterprise or a personal installation, so this is not an existence oracle). An installation this deployment has not recorded — the App was installed from github.com, or after an earlier consent expired — is read live under the App's credential and recorded **only once the proof below has passed**; a refused proof writes nothing. **The proof is live and it is the lock.** `GET /user/memberships/orgs/{org}` is asked of GitHub under the caller's own GitHub App user token, here and now, and only `role: admin, state: active` admits them. Who installed the App is never consulted, and a member who can *see* the installation is refused here exactly as a stranger is. Refusals name what stands in the way: `github_app_login_required` (403, the GitHub link is a classic OAuth-App login — reconnect GitHub), `github_app_permission_missing` (403, the App needs the organization permission *Members: read* and an org owner must approve it on GitHub), `not_an_org_admin` (403), `github_identity_required` (403, no GitHub link at all); none of them names the organisation. On proof the org workspace is created with the installation's login as its slug and the caller as Owner — the same creation `POST /api/orgs` performs, with the GitHub reservation satisfied by the proof instead of the directory probe — the installation is recorded as owned by that workspace, and the claim is written to the audit log (`201`). **Re-entrant.** A slug that is already a workspace is adopted rather than refused when the caller Owns it by the policy layer's decision — the shape a claim left in when it failed after creating the workspace — and the installation is marked and the claim audited then (`200`); an installation already marked with that same workspace answers `200` again with nothing written. A workspace the caller does not own is `409 already_exists` with nothing written; an installation already marked with a DIFFERENT workspace (a GitHub org renamed and claimed again) is `409 not_an_org_installation`, and no second workspace is created. **Browser sessions only**, for the same reason as the begin route. 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/INSTALLATION_ID' \
  -b 'ns_session=$NEOSOURCE_SESSION'

fetch

fetch("https://neosource.dev/api/orgs/claims/INSTALLATION_ID", {
  method: "POST",
  credentials: "include",
});

Path parameters

installation_idrequired

GitHub's installation id, as the callback's redirect carried it.

integerint64

Responses

200The claim was already complete, or its workspace already existed under the caller; nothing new was created

application/json

OrgResponse

object

avatar_url

string | null

Absolute URL of the organization's avatar on the cookieless content origin (`https://{content_origin}/avatars/{slug}?v={digest}`), or absent when it has none — the SPA renders initials then. Same shape as `UserResponse::avatar_url`; the `v` query is the stored digest.

created_atrequired

integerint64

descriptionrequired

string

display_namerequired

string

kindrequired

WorkspaceKind

string

The kind of workspace (namespace container).

"personal""organization"

slugrequired

string

updated_atrequired

integerint64

viewer_role
one of
  • WorkspaceRole

    string

    Workspace membership role. Ordered low → high (`Member < Admin < Owner`) so that `if actor_role >= WorkspaceRole::Admin` reads naturally.

    "member""admin""owner"

  • null

workspace_idrequired

string

201Org created; the caller is its Owner

application/json

OrgResponse

object

avatar_url

string | null

Absolute URL of the organization's avatar on the cookieless content origin (`https://{content_origin}/avatars/{slug}?v={digest}`), or absent when it has none — the SPA renders initials then. Same shape as `UserResponse::avatar_url`; the `v` query is the stored digest.

created_atrequired

integerint64

descriptionrequired

string

display_namerequired

string

kindrequired

WorkspaceKind

string

The kind of workspace (namespace container).

"personal""organization"

slugrequired

string

updated_atrequired

integerint64

viewer_role
one of
  • WorkspaceRole

    string

    Workspace membership role. Ordered low → high (`Member < Admin < Owner`) so that `if actor_role >= WorkspaceRole::Admin` reads naturally.

    "member""admin""owner"

  • null

workspace_idrequired

string

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