Platform

Platform overviewArchitectureWorkflow orchestrationGitOps configurationGovernance and AAAAI and MCPKnowledge and contextRuntime and executionEvidence and monitoring

Use cases

All use casesProduction incidentRelease preparationHotfix to productionSecurity scan triage
Why NopsAIIntegrationsSecurity

Resources

All resourcesAI agent governanceMCP governanceMCP securitySelf-hosted platforms
PricingGitHub

Company

How a run worksAboutContactBook a demo

Git integration API

The two ingress routes providers call, the managed sources that verify them, and the overrides that decide what a repository runs.

ReferenceDeveloperAdministratorSecurity

Key points

  • The ingress routes are the only part of the platform that has to be reachable from the internet.
  • Public does not mean unauthenticated: every delivery is signature-verified against the source secret before it is accepted.
  • A delivery answers 202 when it produced work and 200 when it was accepted with nothing to run — an event no trigger matches is not an error.
  • GitHub App deliveries reach git-bot first, which verifies and forwards a normalised event to /v1/git/events. Other providers use a managed source.
  • Every delivery is recorded, accepted or rejected, with its verification result.
  • A repository with no override answers 200 with an empty document rather than 404.
  • An override outranks the repository’s own trigger manifest.

Operations

POST/v1/git/webhooks/{sourceID}Public

Receives a provider delivery for one managed webhook source.

Notes

Public by necessity — the provider calls it — but never unauthenticated in effect: every delivery is signature-verified against the source secret.

Parameters

NameInTypeRequiredDescription
sourceIDpathuuidRequiredThe source the provider was configured against.
X-Hub-Signature-256headerstringOptionalProvider signature. The header name depends on the provider; every delivery is verified against the source secret before it is accepted.

Call it

Replay a delivery by handapi-git-integration request
BODY=$(cat push-event.json)
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | sed 's/^.* //')
curl -sX POST "$NOPSAI_URL/v1/git/webhooks/$SOURCE_ID" \
  -H "Content-Type: application/json" \
  -H "X-Hub-Signature-256: sha256=$SIG" \
  --data "$BODY" -w "\n%{http_code}\n"
Result

202 when the delivery is accepted and normalised into an event.

Replace before running
  • push-event.json is a provider payload; $WEBHOOK_SECRET is the source signing secret.

Responses

202application/json

Delivery accepted and normalised. Matching triggers start runs asynchronously.

{"status":"accepted"}
200application/json

Accepted but produced no run, which is normal for an event no trigger matches.

{"status":"ignored"}

When it fails

StatusCauseWhat to do
400The payload is not parseable as a provider event.Check the provider content type; form-encoded deliveries must be configured as JSON.
401The signature is missing or does not match the source secret.Re-copy the secret into the provider. This is the common cause of "the webhook shows a red delivery".
403The repository is not in the source allowlist.Add the owner/repository pattern to the source.
404No source with that id, or the source is disabled.Check enabled on the source.
413The payload exceeds the platform body limit.Nothing to do at the provider; very large events are rejected by design.
422The event is understood but carries nothing runnable.Usually a provider ping or an event type the platform does not map.
429The source rate limit was exceeded.Providers retry; a sustained 429 means the limit is too low for the repository traffic.
503Authorization is unavailable, so the delivery cannot be evaluated.Deliveries fail closed rather than starting unauthorized runs.

Side effects

  • Records a delivery, accepted or rejected, with its verification result.
  • Normalises the payload into the shared event model, so trigger manifests stay provider-neutral.
  • Starts runs for every matching trigger.

Proven by

  • services/nopsai/git_webhook_sources_test.go
  • services/nopsai/git_webhook_sources_model.go
  • doc/git-webhook-sources.md
POST/v1/git/eventsAuthenticated

Receives a normalised Git event, used by git-bot for GitHub App deliveries.

Notes

git-bot is the intended caller and uses a service token, but the gate is authentication rather than a service token specifically: any authenticated caller may post an event. The public, signature-verified ingress is /v1/git/webhooks/{sourceID}.

Parameters

NameInTypeRequiredDescription
X-Nopsai-SignatureheaderstringOptionalSignature over the event body, verified before the event is accepted.

Call it

What git-bot forwardsapi-git-integration request
curl -sX POST "$NOPSAI_URL/v1/git/events" \
  -H "Authorization: Bearer $NOPSAI_TOKEN" \
  -H "Content-Type: application/json" \
  --data @doc/sample-git-event.json -w "\n%{http_code}\n"
Result

202 when the event is accepted. The repository ships a sample event at doc/sample-git-event.json.

Replace before running
  • In normal operation git-bot calls this route; calling it by hand is a testing aid.

Responses

202application/json

Event accepted and dispatched to matching triggers.

{"status":"accepted"}
201application/json

Event accepted and a run was created directly from it.

{"run_id":"9c1f7a5e-2b44-4d2f-8f2a-2c9f0b6d4e11"}
200application/json

Accepted with nothing to run.

{"status":"ignored"}

When it fails

StatusCauseWhat to do
400The event body is malformed or missing required repository context.Compare against doc/sample-git-event.json.
401No bearer token. Unlike the webhook ingress, this route is not public.Send a token; git-bot uses its service token.
403The event is not permitted to start the pipeline it matched.Check the trigger owner and the scope it runs in.
500The event could not be recorded.Retry; providers redeliver.

Side effects

  • Creates a trigger event record.
  • Starts runs for matching triggers.

Proven by

  • services/nopsai/git_webhook_sources_test.go
  • services/nopsai/routes.go
  • doc/triggering.md
GET/v1/git-webhook-sourcesAuthorized

Lists managed Git webhook sources.

Notes

connected_trigger_count is what makes a source safe to delete or not: zero means nothing depends on it.

Call it

List webhook sourcesapi-git-integration request
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/git-webhook-sources" | jq '.[] | {id, provider, enabled, connected_trigger_count}'
Result

One entry per source, with how many triggers currently depend on it.

Responses

200application/json

Sources visible to the caller.

[{
  "id": "c81f2b40-5e37-4a94-9b1e-1d2f6a0c7e33",
  "name": "gitlab-platform",
  "provider": "gitlab",
  "enabled": true,
  "team_path": "platform",
  "visibility": "team",
  "auth_mode": "signature",
  "credential_ref": "platform/gitlab-webhook-secret",
  "repository_allowlist": ["acme/payments"],
  "rate_limit": { "per_minute": 60 },
  "connected_trigger_count": 2
}]

When it fails

StatusCauseWhat to do
503Authorization is unavailable.Check AAA.
500The query failed.Platform fault.

Side effects

  • None.

Proven by

  • services/nopsai/git_webhook_sources_test.go
  • services/nopsai/git_webhook_sources_model.go
POST/v1/git-webhook-sourcesAuthorized

Creates a managed webhook source.

Call it

Create a GitLab sourceapi-git-integration request
curl -sX POST "$NOPSAI_URL/v1/git-webhook-sources" \
  -H "Authorization: Bearer $NOPSAI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"gitlab-platform","provider":"gitlab","team_path":"platform","auth_mode":"signature","credential_ref":"platform/gitlab-webhook-secret","repository_allowlist":["acme/payments"]}' | jq -r .id
Result

The source id, which becomes part of the delivery URL the provider is configured with.

Replace before running
  • credential_ref names a stored credential holding the signing secret.

Responses

201application/json

Source created.

{
  "id": "c81f2b40-5e37-4a94-9b1e-1d2f6a0c7e33",
  "name": "gitlab-platform",
  "provider": "gitlab",
  "enabled": true,
  "team_path": "platform",
  "visibility": "team",
  "auth_mode": "signature",
  "credential_ref": "platform/gitlab-webhook-secret",
  "repository_allowlist": ["acme/payments"],
  "rate_limit": { "per_minute": 60 },
  "connected_trigger_count": 2
}

When it fails

StatusCauseWhat to do
400An unknown provider, a missing allowlist, or an unusable credential reference.Every source needs at least one owner/repository allowlist pattern.
401The caller identity could not be resolved for the created-by record.Use a user or service account token.
409A source with that name already exists.Pick another name.
500The source could not be stored.Retry.

Side effects

  • Creates a public delivery endpoint at /v1/git/webhooks/{sourceID}.
  • Writes an audit record.

Proven by

  • services/nopsai/git_webhook_sources_test.go
  • services/nopsai/git_webhook_sources_schema_test.go
  • services/nopsai/git_webhook_sources_schema.go
GET/v1/git-webhook-sources/{sourceID}Authorized

Reads one webhook source.

Parameters

NameInTypeRequiredDescription
sourceIDpathuuidRequiredSource identifier.

Call it

Read a sourceapi-git-integration request
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/git-webhook-sources/$SOURCE_ID" | jq
Result

The source with its allowlist, auth mode, and connected triggers. The signing secret itself is a credential reference, not a value.

Responses

200application/json

The source.

{
  "id": "c81f2b40-5e37-4a94-9b1e-1d2f6a0c7e33",
  "name": "gitlab-platform",
  "provider": "gitlab",
  "enabled": true,
  "team_path": "platform",
  "visibility": "team",
  "auth_mode": "signature",
  "credential_ref": "platform/gitlab-webhook-secret",
  "repository_allowlist": ["acme/payments"],
  "rate_limit": { "per_minute": 60 },
  "connected_trigger_count": 2
}

When it fails

StatusCauseWhat to do
404No source with that id.Confirm the id from the list route.
500The source could not be loaded.Platform fault.

Side effects

  • None.

Proven by

  • services/nopsai/git_webhook_sources_test.go
  • services/nopsai/git_webhook_sources_model.go
PUT/v1/git-webhook-sources/{sourceID}Authorized

Replaces a webhook source definition.

Notes

Narrowing the allowlist takes effect on the next delivery. Runs already started are unaffected.

Parameters

NameInTypeRequiredDescription
sourceIDpathuuidRequiredSource to replace.

Call it

Replace a sourceapi-git-integration request
curl -sX PUT "$NOPSAI_URL/v1/git-webhook-sources/$SOURCE_ID" \
  -H "Authorization: Bearer $NOPSAI_TOKEN" \
  -H "Content-Type: application/json" \
  --data @source.json | jq
Result

The stored source after replacement.

Replace before running
  • A PUT replaces the whole document, including the allowlist.

Responses

200application/json

Source replaced.

{
  "id": "c81f2b40-5e37-4a94-9b1e-1d2f6a0c7e33",
  "name": "gitlab-platform",
  "provider": "gitlab",
  "enabled": true,
  "team_path": "platform",
  "visibility": "team",
  "auth_mode": "signature",
  "credential_ref": "platform/gitlab-webhook-secret",
  "repository_allowlist": ["acme/payments"],
  "rate_limit": { "per_minute": 60 },
  "connected_trigger_count": 2
}

When it fails

StatusCauseWhat to do
400The document is invalid.The message names the field.
401The caller identity could not be resolved.Use a user or service account token.
404No source with that id.Create it instead.
500The update could not be persisted.Retry.

Side effects

  • Changes which repositories may deliver through this source.
  • Writes an audit record.

Proven by

  • services/nopsai/git_webhook_sources_test.go
  • services/nopsai/git_webhook_sources_schema.go
PATCH/v1/git-webhook-sources/{sourceID}Authorized

Partially updates a webhook source.

Parameters

NameInTypeRequiredDescription
sourceIDpathuuidRequiredSource to update.

Call it

Disable a sourceapi-git-integration request
curl -sX PATCH "$NOPSAI_URL/v1/git-webhook-sources/$SOURCE_ID" \
  -H "Authorization: Bearer $NOPSAI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"enabled":false}' | jq '{id, enabled}'
Result

Deliveries to this source start answering 404 while it is disabled.

Responses

200application/json

Source updated.

{
  "id": "c81f2b40-5e37-4a94-9b1e-1d2f6a0c7e33",
  "name": "gitlab-platform",
  "provider": "gitlab",
  "enabled": true,
  "team_path": "platform",
  "visibility": "team",
  "auth_mode": "signature",
  "credential_ref": "platform/gitlab-webhook-secret",
  "repository_allowlist": ["acme/payments"],
  "rate_limit": { "per_minute": 60 },
  "connected_trigger_count": 2
}

When it fails

StatusCauseWhat to do
400A supplied field is invalid.The message names the field.
404No source with that id.Confirm the id.
500The update could not be persisted.Retry.

Side effects

  • Writes an audit record.

Proven by

  • services/nopsai/git_webhook_sources_test.go
  • services/nopsai/git_webhook_sources_schema.go
DELETE/v1/git-webhook-sources/{sourceID}Authorized

Deletes a webhook source.

Notes

Check connected_trigger_count first. Deleting a source with connected triggers silently stops them.

Parameters

NameInTypeRequiredDescription
sourceIDpathuuidRequiredSource to delete.

Call it

Delete a sourceapi-git-integration request
curl -sX DELETE -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/git-webhook-sources/$SOURCE_ID" -w "%{http_code}\n"
Result

204. Its delivery endpoint stops existing, and triggers bound to it stop firing.

Responses

204

Deleted.

When it fails

StatusCauseWhat to do
404No source with that id.It may already be deleted.
500The delete could not be persisted.Retry.

Side effects

  • Triggers that referenced the source stop receiving events.
  • Writes an audit record.

Proven by

  • services/nopsai/git_webhook_sources_test.go
  • services/nopsai/git_webhook_sources_model.go
GET/v1/git-webhook-sources/{sourceID}/deliveriesAuthorized

Lists recent deliveries for diagnosis.

Notes

First stop when a push produced no run: this separates "never arrived" from "arrived and was rejected".

Parameters

NameInTypeRequiredDescription
sourceIDpathuuidRequiredSource identifier.
limitqueryintegerOptionalHow many deliveries to return.

Call it

Read recent deliveriesapi-git-integration request
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/git-webhook-sources/$SOURCE_ID/deliveries" | jq '.[] | {received_at, event_type, result}'
Result

Accepted and rejected deliveries alike, with the verification result.

Responses

200application/json

Recent deliveries, newest first.

[
  {
    "id": "7d21...",
    "received_at": "2026-08-19T11:02:44Z",
    "event_type": "push",
    "repository": "acme/payments",
    "result": "accepted",
    "run_id": "9c1f7a5e-2b44-4d2f-8f2a-2c9f0b6d4e11"
  }
]

When it fails

StatusCauseWhat to do
500The delivery query failed.Platform fault.

Side effects

  • None.

Proven by

  • services/nopsai/git_webhook_sources_test.go
  • services/nopsai/git_webhook_sources_model.go
  • doc/git-webhook-sources.md
GET/v1/overridesAuthorized

Lists repository trigger overrides.

Call it

List overridesapi-git-integration request
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/overrides" | jq
Result

Every repository whose trigger resolution is pinned rather than taken from the repository.

Responses

200application/json

Overrides visible to the caller.

[
  {
    "repo_owner": "acme",
    "repo_name": "payments",
    "pipeline": "platform/release-service",
    "updated_at": "2026-08-18T14:20:00Z"
  }
]

When it fails

StatusCauseWhat to do
503Authorization is unavailable.Check AAA.
500The query failed.Platform fault.

Side effects

  • None.

Proven by

  • services/nopsai/git_apps_handlers_test.go
  • services/nopsai/routes.go
GET/v1/overrides/{repoOwner}/{repoName}Authorized

Reads the override for one repository.

Notes

A repository with no override answers 200 with an empty document rather than 404, so a client can read unconditionally.

Parameters

NameInTypeRequiredDescription
repoOwnerpathstringRequiredRepository owner.
repoNamepathstringRequiredRepository name.

Call it

Read one overrideapi-git-integration request
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/overrides/acme/payments" | jq
Result

The override, or an empty document when the repository has none.

Responses

200application/json

The override for that repository.

{
  "repo_owner": "acme",
  "repo_name": "payments",
  "pipeline": "platform/release-service"
}

Side effects

  • None.

Proven by

  • services/nopsai/git_apps_handlers_test.go
  • services/nopsai/routes.go
PUT/v1/overrides/{repoOwner}/{repoName}Authorized

Sets a trigger override for a repository.

Notes

An override outranks the repository’s own trigger manifest, which makes it powerful and easy to forget. List overrides when a repository runs something unexpected.

Parameters

NameInTypeRequiredDescription
repoOwnerpathstringRequiredRepository owner.
repoNamepathstringRequiredRepository name.

Call it

Pin a repository to a pipelineapi-git-integration request
curl -sX PUT "$NOPSAI_URL/v1/overrides/acme/payments" \
  -H "Authorization: Bearer $NOPSAI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"pipeline":"platform/release-service"}' | jq
Result

201. Events for that repository resolve to this pipeline regardless of what the repository declares.

Responses

201application/json

Override stored.

{
  "repo_owner": "acme",
  "repo_name": "payments",
  "pipeline": "platform/release-service"
}

When it fails

StatusCauseWhat to do
400The document is invalid or names an unknown pipeline.Validate with POST /v1/overrides/validate first.
409The override conflicts with an existing one.Read the current override before replacing it.
500The override could not be stored.Retry.

Side effects

  • Changes which pipeline the repository’s events resolve to.
  • Writes an audit record.

Proven by

  • services/nopsai/git_apps_handlers_test.go
  • services/nopsai/routes.go
  • doc/triggering.md
DELETE/v1/overrides/{repoOwner}/{repoName}Authorized

Removes a trigger override.

Parameters

NameInTypeRequiredDescription
repoOwnerpathstringRequiredRepository owner.
repoNamepathstringRequiredRepository name.

Call it

Remove an overrideapi-git-integration request
curl -sX DELETE -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/overrides/acme/payments" -w "%{http_code}\n"
Result

204. Trigger resolution returns to what the repository declares.

Responses

204

Override removed.

When it fails

StatusCauseWhat to do
500The delete could not be persisted.Retry.

Side effects

  • Repository events resolve from the repository again.
  • Writes an audit record.

Proven by

  • services/nopsai/git_apps_handlers_test.go
  • services/nopsai/routes.go
POST/v1/overrides/validateAuthenticated

Validates an override document without storing it.

Notes

Like every validation route, it answers 200 even when the document is rejected: the failure is in the body.

Call it

Validate an overrideapi-git-integration request
curl -sX POST "$NOPSAI_URL/v1/overrides/validate" \
  -H "Authorization: Bearer $NOPSAI_TOKEN" \
  -H "Content-Type: application/yaml" \
  --data-binary @override.yaml | jq
Result

The same valid, errors, warnings shape the pipeline validator returns.

Responses

200application/json

Validation ran. Read valid rather than the status code.

{
  "valid": true,
  "errors": [],
  "warnings": []
}

Side effects

  • None.

Proven by

  • services/nopsai/git_apps_handlers_test.go
  • services/nopsai/validation_contract.go
GET/v1/repositories/{repoOwner}/{repoName}/branchesAuthorized

Lists branches for a connected repository, used when choosing a revision.

Parameters

NameInTypeRequiredDescription
repoOwnerpathstringRequiredRepository owner.
repoNamepathstringRequiredRepository name.

Call it

List branchesapi-git-integration request
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/repositories/acme/payments/branches" | jq
Result

Branches the connected installation can see, which is what a revision picker renders from.

Responses

200application/json

Branches.

[
  { "name": "main", "commit_sha": "4f2a91c" }
]

When it fails

StatusCauseWhat to do
404The repository is not connected, or the installation cannot see it.Refresh the installation; repository access granted at GitHub is not pushed to NopsAI.
502The provider could not be reached.Verify the installation.

Side effects

  • Calls the provider.

Proven by

  • services/nopsai/git_apps_handlers_test.go
  • services/nopsai/git_apps_handlers.go
GET/v1/git-apps/githubAuthorized

Reads GitHub App settings.

Notes

Reachable while first-install setup is still locked, because the wizard connects GitHub before setup completes.

Call it

Read GitHub App settingsapi-git-integration request
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/git-apps/github" | jq
Result

App id, slug, and whether a private key is configured. The key itself is never returned.

Responses

200application/json

GitHub App settings, or an empty document when none is registered.

{
  "app_id": "1284410",
  "slug": "nopsai-platform",
  "has_private_key": true,
  "webhook_configured": true
}

Side effects

  • None.

Proven by

  • services/nopsai/git_apps_handlers_test.go
  • services/nopsai/git_apps_handlers.go
  • doc/git-apps.md
PUT/v1/git-apps/githubAuthorized

Updates GitHub App settings.

Call it

Configure an existing GitHub App by handapi-git-integration request
curl -sX PUT "$NOPSAI_URL/v1/git-apps/github" \
  -H "Authorization: Bearer $NOPSAI_TOKEN" \
  -H "Content-Type: application/json" \
  --data @github-app.json | jq '{app_id, has_private_key}'
Result

The stored settings. Use this when the App already exists; use the registration flow to create one.

Replace before running
  • github-app.json carries the app id, slug, private key, and webhook secret.

Responses

200application/json

Settings stored.

{
  "app_id": "1284410",
  "slug": "nopsai-platform",
  "has_private_key": true
}

When it fails

StatusCauseWhat to do
400A malformed private key or a missing app id.The key is a PEM block; paste it whole, newlines included.
500Settings could not be stored.Retry.

Side effects

  • git-bot picks up the new credentials for subsequent deliveries.
  • Writes an audit record.

Proven by

  • services/nopsai/git_apps_handlers_test.go
  • services/nopsai/git_apps_handlers.go
POST/v1/git-apps/github/register/startAuthorized

Begins GitHub App registration.

Notes

A browser flow driven by the System UI. The 412 is the guard against silently replacing a working App.

Call it

Start registering a GitHub Appapi-git-integration request
curl -sX POST "$NOPSAI_URL/v1/git-apps/github/register/start" \
  -H "Authorization: Bearer $NOPSAI_TOKEN" \
  -H "Content-Type: application/json" \
  --data @registration.json | jq
Result

A URL and manifest for the operator browser to post to GitHub. The flow is completed by the callback.

Replace before running
  • registration.json names the organisation and the public URL GitHub will call back on.

Responses

200application/json

The registration handoff for the browser.

{
  "url": "https://github.com/organizations/acme/settings/apps/new",
  "state": "9f2c...",
  "manifest": { "name": "nopsai-platform" }
}

When it fails

StatusCauseWhat to do
400A missing organisation or an unusable callback URL.The callback must be reachable from GitHub.
412A GitHub App is already registered.Remove or replace the existing settings first.
500The registration could not be started.Retry.

Side effects

  • Creates the pending registration state the callback validates.

Proven by

  • services/nopsai/git_apps_registration_test.go
  • services/nopsai/git_apps_handlers.go
  • doc/git-apps.md
GET/v1/git-apps/github/register/callbackPublic

Completes GitHub App registration and stores the generated credentials.

Notes

Public because GitHub redirects the operator browser here without a bearer token; it authorizes itself with the single-use state.

Parameters

NameInTypeRequiredDescription
codequerystringRequiredTemporary code GitHub returns after the manifest is accepted.
statequerystringRequiredSingle-use state that must match the pending registration.

Call it

What GitHub redirects the browser toapi-git-integration request
GET /v1/git-apps/github/register/callback?code=<code>&state=<state>
Result

A redirect back into the UI once the App id, private key, and webhook secret are stored.

Responses

302

Redirect back to the System UI.

Location: https://nopsai.example.com/#/system/git-apps

When it fails

StatusCauseWhat to do
302A state mismatch or an exchange failure redirects back with an error rather than rendering one.Restart the registration; a state works once.

Side effects

  • Stores the App id, private key, and webhook secret.
  • Consumes the pending registration state.
  • Writes an audit record.

Proven by

  • services/nopsai/git_apps_registration_test.go
  • services/nopsai/git_apps_handlers.go
  • services/nopsai/http_middleware.go
POST/v1/git-apps/github/install/startAuthorized

Begins a GitHub App installation flow.

Call it

Start installing the App on an organisationapi-git-integration request
curl -sX POST -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/git-apps/github/install/start" | jq
Result

The GitHub install URL for the browser, plus the state the callback will validate.

Responses

200application/json

The install handoff for the browser.

{
  "url": "https://github.com/apps/nopsai-platform/installations/new",
  "state": "4b81..."
}

When it fails

StatusCauseWhat to do
412No GitHub App is registered yet.Register the App before installing it.

Side effects

  • Creates the pending install state the callback validates.

Proven by

  • services/nopsai/git_apps_registration_test.go
  • services/nopsai/git_apps_handlers.go
GET/v1/git-apps/github/install/callbackPublic

Completes an installation and records it.

Notes

Public for the same reason as the registration callback: a browser redirect carries no bearer token, and the single-use state is the authorization.

Parameters

NameInTypeRequiredDescription
installation_idquerystringRequiredInstallation id GitHub assigns.
statequerystringRequiredSingle-use state that must match the pending install.

Call it

What GitHub redirects the browser toapi-git-integration request
GET /v1/git-apps/github/install/callback?installation_id=48210077&state=<state>
Result

A redirect back into the UI with the installation recorded.

Responses

302

Redirect back to the System UI.

Location: https://nopsai.example.com/#/system/git-apps

When it fails

StatusCauseWhat to do
302A state mismatch redirects back with an error.Restart the install flow.

Side effects

  • Records the installation so git-bot can act for it.
  • Consumes the pending install state.
  • Writes an audit record.

Proven by

  • services/nopsai/git_apps_registration_test.go
  • services/nopsai/git_apps_handlers.go
  • services/nopsai/http_middleware.go
GET/v1/git-apps/github/installationsAuthorized

Lists GitHub App installations.

Notes

Reachable while first-install setup is still locked, because the wizard connects GitHub before setup completes.

Call it

List installationsapi-git-integration request
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/git-apps/github/installations" | jq
Result

Each installation with the account it belongs to and when it was last verified.

Responses

200application/json

Installations.

[
  {
    "installation_id": "48210077",
    "account": "acme",
    "status": "active",
    "last_verified_at": "2026-08-19T08:02:11Z"
  }
]

Side effects

  • None.

Proven by

  • services/nopsai/git_apps_handlers_test.go
  • services/nopsai/git_apps_handlers.go
POST/v1/git-apps/github/installationsAuthorized

Registers an installation manually.

Notes

The escape hatch when the browser callback could not reach the platform. Verify immediately: a recorded installation that cannot authenticate looks healthy in a list.

Call it

Record an installation without the browser flowapi-git-integration request
curl -sX POST "$NOPSAI_URL/v1/git-apps/github/installations" \
  -H "Authorization: Bearer $NOPSAI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"installation_id":"48210077"}' | jq
Result

The installation record. Verify it afterwards to confirm the App can authenticate for it.

Responses

201application/json

Installation recorded.

{ "installation_id": "48210077", "account": "acme", "status": "active" }

When it fails

StatusCauseWhat to do
400A missing or unusable installation id.The id comes from GitHub, not from NopsAI.
500The installation could not be recorded.Retry.

Side effects

  • git-bot may act for the installation once it verifies.
  • Writes an audit record.

Proven by

  • services/nopsai/git_apps_handlers_test.go
  • services/nopsai/git_apps_handlers.go
GET/v1/git-apps/github/installations/{installationID}Authorized

Reads one installation.

Parameters

NameInTypeRequiredDescription
installationIDpathstringRequiredGitHub App installation id.

Call it

Read an installationapi-git-integration request
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/git-apps/github/installations/$INSTALLATION_ID" | jq
Result

The installation with its account and verification state.

Responses

200application/json

The installation.

{ "installation_id": "48210077", "account": "acme", "status": "active" }

When it fails

StatusCauseWhat to do
404No installation with that id.List the installations.

Side effects

  • None.

Proven by

  • services/nopsai/git_apps_handlers_test.go
  • services/nopsai/git_apps_handlers.go
DELETE/v1/git-apps/github/installations/{installationID}Authorized

Removes an installation.

Notes

Removing the record here does not uninstall the App at GitHub. Do both, or GitHub keeps delivering to an install that ignores it.

Parameters

NameInTypeRequiredDescription
installationIDpathstringRequiredGitHub App installation id.

Call it

Remove an installationapi-git-integration request
curl -sX DELETE -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/git-apps/github/installations/$INSTALLATION_ID" -w "%{http_code}\n"
Result

204. Deliveries for its repositories stop producing runs.

Responses

204

Installation removed.

When it fails

StatusCauseWhat to do
400The id is malformed.Use an id from the list.
404No installation with that id.It may already be removed.
500The delete failed.Retry.

Side effects

  • git-bot stops acting for the installation; its repository events stop producing runs.
  • Writes an audit record.

Proven by

  • services/nopsai/git_apps_handlers_test.go
  • services/nopsai/git_apps_handlers.go
POST/v1/git-apps/github/installations/{installationID}/verifyAuthorized

Verifies an installation can authenticate against GitHub.

Notes

Run this after rotating the App private key. A stale key fails only when a delivery arrives, which is the worst time to find out.

Parameters

NameInTypeRequiredDescription
installationIDpathstringRequiredGitHub App installation id.

Call it

Verify an installationapi-git-integration request
curl -sX POST -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/git-apps/github/installations/$INSTALLATION_ID/verify" | jq
Result

A real token exchange with GitHub — the difference between recorded and working.

Responses

200application/json

The installation authenticated.

{ "installation_id": "48210077", "ok": true, "verified_at": "2026-08-19T13:40:02Z" }

When it fails

StatusCauseWhat to do
404No installation with that id.List the installations.
500Verification could not run, usually a missing or malformed private key.Re-check the App settings.

Side effects

  • Exchanges a token with GitHub and records the verification time.

Proven by

  • services/nopsai/git_apps_handlers_test.go
  • services/nopsai/git_apps_handlers.go
POST/v1/git-apps/github/installations/{installationID}/refreshAuthorized

Refreshes cached installation metadata.

Notes

Repository access granted at GitHub is not pushed to NopsAI. Refresh after changing which repositories the App can see.

Parameters

NameInTypeRequiredDescription
installationIDpathstringRequiredGitHub App installation id.

Call it

Refresh installation metadataapi-git-integration request
curl -sX POST -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/git-apps/github/installations/$INSTALLATION_ID/refresh" | jq
Result

Account and repository access re-read from GitHub.

Responses

200application/json

Metadata refreshed.

{ "installation_id": "48210077", "account": "acme", "repository_count": 12 }

When it fails

StatusCauseWhat to do
404No installation with that id.List the installations.
502GitHub could not be reached or rejected the call.Verify the installation first.
500The refresh could not be stored.Retry.

Side effects

  • Calls GitHub and updates the cached account and repository list.

Proven by

  • services/nopsai/git_apps_handlers_test.go
  • services/nopsai/git_apps_handlers.go
GET/v1/git-apps/github/installations/{installationID}/repositoriesAuthorized

Lists repositories an installation can reach.

Notes

A repository missing here is why its pushes produce nothing: the App was never granted access to it.

Parameters

NameInTypeRequiredDescription
installationIDpathstringRequiredGitHub App installation id.

Call it

List reachable repositoriesapi-git-integration request
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/git-apps/github/installations/$INSTALLATION_ID/repositories" | jq
Result

The repositories the App may act on, which bounds what triggers can run.

Responses

200application/json

Reachable repositories.

[
  { "full_name": "acme/payments", "private": true }
]

When it fails

StatusCauseWhat to do
404No installation with that id.List the installations.
502GitHub could not be reached.Verify the installation.

Side effects

  • Calls GitHub.

Proven by

  • services/nopsai/git_apps_handlers_test.go
  • services/nopsai/git_apps_handlers.go

How it works

The delivery status codes separate three states an integrator otherwise conflates: 401 means the signature did not match, 403 means the repository is not in the allowlist, and 422 means the event was understood and carried nothing runnable. Only the first two are configuration mistakes.

When a push produces no run, read the delivery list before anything else. It distinguishes "the provider never called" from "the provider called and we rejected it", and those have entirely different fixes.

Overrides are the reason a repository can run something its own manifest does not mention. They are easy to set and easy to forget, so list them when trigger resolution surprises you.

The GitHub App registration and installation routes are a browser flow driven by the System UI. They carry index rows here; the flow itself is documented in GitHub App and installations.

Implementation evidence

  • services/nopsai/git_webhook_sources_model.go

    Source document, delivery records, and verification results.

  • doc/git-webhook-sources.md

    Provider configuration, security, and payload normalisation.

  • doc/git-apps.md

    GitHub App registration, installations, and git-bot routing.