GitHub App installation callback
GET/api/github/app/installations/callback
githubAppInstallationCallback
Where GitHub sends the browser after an App installation. Consumes the single-use `state` nonce, proves the installation belongs to this App by minting an installation token against it, proves it belongs to **the caller** — the installation must be on their own linked GitHub user account — and only then records it against the consenting account. **An organisation's installation is the caller's when they administer the organisation**, proved live: `GET /user/memberships/orgs/{org}` under the caller's own GitHub App user token must answer `role: admin, state: active`. A classic OAuth-App login cannot ask that and is refused with `github_app_login_required`; GitHub's 403 there (the App lacks organization *Members: read*, or an org owner has not accepted the permission update) is `github_app_permission_missing`; anything short of an active admin is `not_an_org_admin`. Who installed the App is never the proof. **A consent begun by `POST /api/orgs/claims` lands on `/new/organization/claim`** — with `?installation_id=` on success, with `?error=` on a refusal reached after the nonce was consumed. The workspace is never created here; that page completes the claim with `POST /api/orgs/claims/{installation_id}`, which proves ownership again. **Browser sessions only.** A PAT or workspace token is refused: this route associates a durable third-party credential with an account, and a leaked scoped token must not be able to do that. **An installation started on GitHub arrives with no `state`.** Anyone can install a public App from `github.com/apps/{slug}`; that callback carries `installation_id` and `setup_action=install` and nothing this server minted. Nothing is written and nothing is asked of GitHub — the browser is sent to `/settings/integrations/github?claim={installation_id}`, signed in or not, where the user connects it explicitly through `POST /api/github/app/installations/{installation_id}/claim`. A callback with `state` but no `installation_id` — the install page was left without an installation being created for the caller — spends the nonce and is `?error=bad_request`, on `/new/organization/claim` when that nonce was an org claim's and on the integrations page otherwise; one with neither is `?error=bad_request` on the integrations page. Always answers `303`, never a JSON body — on success to the consent's `return_to` (or `/settings/integrations/github`), on failure to the same location with `?error=` set to one of `invalid_state`, `forbidden`, `unauthenticated`, `bad_request`, `already_connected`, `github_identity_required`, `github_app_login_required`, `github_app_permission_missing`, `not_an_org_admin`, `upstream`. `already_connected` means a personal installation is bound to a different neosource account and retrying will not help; `github_identity_required` means this account has no linked GitHub identity, so there is nothing to prove the installation is yours against — link GitHub, then install the App again; the three organisation tokens are described above. `org_installations_unsupported` is no longer produced and remains only as a documented value. The `state` value is never echoed back.
Authentication optional — this operation also accepts anonymous requests. When authenticating, use a session cookie.
curl
curl -X GET 'https://neosource.dev/api/github/app/installations/callback' \
-b 'ns_session=$NEOSOURCE_SESSION'fetch
fetch("https://neosource.dev/api/github/app/installations/callback", {
method: "GET",
credentials: "include",
});Query parameters
installation_idGitHub's installation id.
stateThe nonce this server minted at consent time.
setup_action`install` or `update`, per GitHub. **Accepted and ignored**: nothing branches on it server-side and it is not forwarded in the redirect — the SPA reads the resulting installation list, which states the same fact without trusting a query parameter. Declared so the callback's `Query` extractor does not have to be lenient about it.
Responses
303Redirect back into the app, with `?error=` on failure
404No GitHub App is configured on this deployment
Standard errors
Bodies documented once for the whole API — see standard errors.
- 429Rate limited — retry after the `Retry-After` header
- 500Internal server error
- 503Service temporarily unavailable / at capacity — retry after the `Retry-After` header
- 504Gateway timeout — the request exceeded the server's handling budget