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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
sourceID | path | uuid | Required | The source the provider was configured against. |
X-Hub-Signature-256 | header | string | Optional | Provider signature. The header name depends on the provider; every delivery is verified against the source secret before it is accepted. |
Call it
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"Responses
Delivery accepted and normalised. Matching triggers start runs asynchronously.
{"status":"accepted"}Accepted but produced no run, which is normal for an event no trigger matches.
{"status":"ignored"}When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | The payload is not parseable as a provider event. | Check the provider content type; form-encoded deliveries must be configured as JSON. |
| 401 | The 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". |
| 403 | The repository is not in the source allowlist. | Add the owner/repository pattern to the source. |
| 404 | No source with that id, or the source is disabled. | Check enabled on the source. |
| 413 | The payload exceeds the platform body limit. | Nothing to do at the provider; very large events are rejected by design. |
| 422 | The event is understood but carries nothing runnable. | Usually a provider ping or an event type the platform does not map. |
| 429 | The source rate limit was exceeded. | Providers retry; a sustained 429 means the limit is too low for the repository traffic. |
| 503 | Authorization 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.goservices/nopsai/git_webhook_sources_model.godoc/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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Nopsai-Signature | header | string | Optional | Signature over the event body, verified before the event is accepted. |
Call it
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"Responses
Event accepted and dispatched to matching triggers.
{"status":"accepted"}Event accepted and a run was created directly from it.
{"run_id":"9c1f7a5e-2b44-4d2f-8f2a-2c9f0b6d4e11"}Accepted with nothing to run.
{"status":"ignored"}When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | The event body is malformed or missing required repository context. | Compare against doc/sample-git-event.json. |
| 401 | No bearer token. Unlike the webhook ingress, this route is not public. | Send a token; git-bot uses its service token. |
| 403 | The event is not permitted to start the pipeline it matched. | Check the trigger owner and the scope it runs in. |
| 500 | The 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.goservices/nopsai/routes.godoc/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
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/git-webhook-sources" | jq '.[] | {id, provider, enabled, connected_trigger_count}'Responses
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
| Status | Cause | What to do |
|---|---|---|
| 503 | Authorization is unavailable. | Check AAA. |
| 500 | The query failed. | Platform fault. |
Side effects
- None.
Proven by
services/nopsai/git_webhook_sources_test.goservices/nopsai/git_webhook_sources_model.go
POST/v1/git-webhook-sourcesAuthorized
Creates a managed webhook source.
Call it
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 .idResponses
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
| Status | Cause | What to do |
|---|---|---|
| 400 | An unknown provider, a missing allowlist, or an unusable credential reference. | Every source needs at least one owner/repository allowlist pattern. |
| 401 | The caller identity could not be resolved for the created-by record. | Use a user or service account token. |
| 409 | A source with that name already exists. | Pick another name. |
| 500 | The 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.goservices/nopsai/git_webhook_sources_schema_test.goservices/nopsai/git_webhook_sources_schema.go
GET/v1/git-webhook-sources/{sourceID}Authorized
Reads one webhook source.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
sourceID | path | uuid | Required | Source identifier. |
Call it
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/git-webhook-sources/$SOURCE_ID" | jqResponses
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
| Status | Cause | What to do |
|---|---|---|
| 404 | No source with that id. | Confirm the id from the list route. |
| 500 | The source could not be loaded. | Platform fault. |
Side effects
- None.
Proven by
services/nopsai/git_webhook_sources_test.goservices/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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
sourceID | path | uuid | Required | Source to replace. |
Call it
curl -sX PUT "$NOPSAI_URL/v1/git-webhook-sources/$SOURCE_ID" \
-H "Authorization: Bearer $NOPSAI_TOKEN" \
-H "Content-Type: application/json" \
--data @source.json | jqResponses
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
| Status | Cause | What to do |
|---|---|---|
| 400 | The document is invalid. | The message names the field. |
| 401 | The caller identity could not be resolved. | Use a user or service account token. |
| 404 | No source with that id. | Create it instead. |
| 500 | The 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.goservices/nopsai/git_webhook_sources_schema.go
PATCH/v1/git-webhook-sources/{sourceID}Authorized
Partially updates a webhook source.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
sourceID | path | uuid | Required | Source to update. |
Call it
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}'Responses
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
| Status | Cause | What to do |
|---|---|---|
| 400 | A supplied field is invalid. | The message names the field. |
| 404 | No source with that id. | Confirm the id. |
| 500 | The update could not be persisted. | Retry. |
Side effects
- Writes an audit record.
Proven by
services/nopsai/git_webhook_sources_test.goservices/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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
sourceID | path | uuid | Required | Source to delete. |
Call it
curl -sX DELETE -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/git-webhook-sources/$SOURCE_ID" -w "%{http_code}\n"Responses
Deleted.
When it fails
| Status | Cause | What to do |
|---|---|---|
| 404 | No source with that id. | It may already be deleted. |
| 500 | The 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.goservices/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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
sourceID | path | uuid | Required | Source identifier. |
limit | query | integer | Optional | How many deliveries to return. |
Call it
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/git-webhook-sources/$SOURCE_ID/deliveries" | jq '.[] | {received_at, event_type, result}'Responses
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
| Status | Cause | What to do |
|---|---|---|
| 500 | The delivery query failed. | Platform fault. |
Side effects
- None.
Proven by
services/nopsai/git_webhook_sources_test.goservices/nopsai/git_webhook_sources_model.godoc/git-webhook-sources.md
GET/v1/overridesAuthorized
Lists repository trigger overrides.
Call it
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/overrides" | jqResponses
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
| Status | Cause | What to do |
|---|---|---|
| 503 | Authorization is unavailable. | Check AAA. |
| 500 | The query failed. | Platform fault. |
Side effects
- None.
Proven by
services/nopsai/git_apps_handlers_test.goservices/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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
repoOwner | path | string | Required | Repository owner. |
repoName | path | string | Required | Repository name. |
Call it
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/overrides/acme/payments" | jqResponses
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.goservices/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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
repoOwner | path | string | Required | Repository owner. |
repoName | path | string | Required | Repository name. |
Call it
curl -sX PUT "$NOPSAI_URL/v1/overrides/acme/payments" \
-H "Authorization: Bearer $NOPSAI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"pipeline":"platform/release-service"}' | jqResponses
Override stored.
{
"repo_owner": "acme",
"repo_name": "payments",
"pipeline": "platform/release-service"
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | The document is invalid or names an unknown pipeline. | Validate with POST /v1/overrides/validate first. |
| 409 | The override conflicts with an existing one. | Read the current override before replacing it. |
| 500 | The 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.goservices/nopsai/routes.godoc/triggering.md
DELETE/v1/overrides/{repoOwner}/{repoName}Authorized
Removes a trigger override.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
repoOwner | path | string | Required | Repository owner. |
repoName | path | string | Required | Repository name. |
Call it
curl -sX DELETE -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/overrides/acme/payments" -w "%{http_code}\n"Responses
Override removed.
When it fails
| Status | Cause | What to do |
|---|---|---|
| 500 | The 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.goservices/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
curl -sX POST "$NOPSAI_URL/v1/overrides/validate" \
-H "Authorization: Bearer $NOPSAI_TOKEN" \
-H "Content-Type: application/yaml" \
--data-binary @override.yaml | jqResponses
Validation ran. Read valid rather than the status code.
{
"valid": true,
"errors": [],
"warnings": []
}Side effects
- None.
Proven by
services/nopsai/git_apps_handlers_test.goservices/nopsai/validation_contract.go
GET/v1/repositories/{repoOwner}/{repoName}/branchesAuthorized
Lists branches for a connected repository, used when choosing a revision.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
repoOwner | path | string | Required | Repository owner. |
repoName | path | string | Required | Repository name. |
Call it
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/repositories/acme/payments/branches" | jqResponses
Branches.
[
{ "name": "main", "commit_sha": "4f2a91c" }
]When it fails
| Status | Cause | What to do |
|---|---|---|
| 404 | The repository is not connected, or the installation cannot see it. | Refresh the installation; repository access granted at GitHub is not pushed to NopsAI. |
| 502 | The provider could not be reached. | Verify the installation. |
Side effects
- Calls the provider.
Proven by
services/nopsai/git_apps_handlers_test.goservices/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
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/git-apps/github" | jqResponses
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.goservices/nopsai/git_apps_handlers.godoc/git-apps.md
PUT/v1/git-apps/githubAuthorized
Updates GitHub App settings.
Call it
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}'Responses
Settings stored.
{
"app_id": "1284410",
"slug": "nopsai-platform",
"has_private_key": true
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | A malformed private key or a missing app id. | The key is a PEM block; paste it whole, newlines included. |
| 500 | Settings 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.goservices/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
curl -sX POST "$NOPSAI_URL/v1/git-apps/github/register/start" \
-H "Authorization: Bearer $NOPSAI_TOKEN" \
-H "Content-Type: application/json" \
--data @registration.json | jqResponses
The registration handoff for the browser.
{
"url": "https://github.com/organizations/acme/settings/apps/new",
"state": "9f2c...",
"manifest": { "name": "nopsai-platform" }
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | A missing organisation or an unusable callback URL. | The callback must be reachable from GitHub. |
| 412 | A GitHub App is already registered. | Remove or replace the existing settings first. |
| 500 | The registration could not be started. | Retry. |
Side effects
- Creates the pending registration state the callback validates.
Proven by
services/nopsai/git_apps_registration_test.goservices/nopsai/git_apps_handlers.godoc/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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
code | query | string | Required | Temporary code GitHub returns after the manifest is accepted. |
state | query | string | Required | Single-use state that must match the pending registration. |
Call it
GET /v1/git-apps/github/register/callback?code=<code>&state=<state>Responses
Redirect back to the System UI.
Location: https://nopsai.example.com/#/system/git-appsWhen it fails
| Status | Cause | What to do |
|---|---|---|
| 302 | A 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.goservices/nopsai/git_apps_handlers.goservices/nopsai/http_middleware.go
POST/v1/git-apps/github/install/startAuthorized
Begins a GitHub App installation flow.
Call it
curl -sX POST -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/git-apps/github/install/start" | jqResponses
The install handoff for the browser.
{
"url": "https://github.com/apps/nopsai-platform/installations/new",
"state": "4b81..."
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 412 | No 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.goservices/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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
installation_id | query | string | Required | Installation id GitHub assigns. |
state | query | string | Required | Single-use state that must match the pending install. |
Call it
GET /v1/git-apps/github/install/callback?installation_id=48210077&state=<state>Responses
Redirect back to the System UI.
Location: https://nopsai.example.com/#/system/git-appsWhen it fails
| Status | Cause | What to do |
|---|---|---|
| 302 | A 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.goservices/nopsai/git_apps_handlers.goservices/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
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/git-apps/github/installations" | jqResponses
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.goservices/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
curl -sX POST "$NOPSAI_URL/v1/git-apps/github/installations" \
-H "Authorization: Bearer $NOPSAI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"installation_id":"48210077"}' | jqResponses
Installation recorded.
{ "installation_id": "48210077", "account": "acme", "status": "active" }When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | A missing or unusable installation id. | The id comes from GitHub, not from NopsAI. |
| 500 | The 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.goservices/nopsai/git_apps_handlers.go
GET/v1/git-apps/github/installations/{installationID}Authorized
Reads one installation.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
installationID | path | string | Required | GitHub App installation id. |
Call it
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/git-apps/github/installations/$INSTALLATION_ID" | jqResponses
The installation.
{ "installation_id": "48210077", "account": "acme", "status": "active" }When it fails
| Status | Cause | What to do |
|---|---|---|
| 404 | No installation with that id. | List the installations. |
Side effects
- None.
Proven by
services/nopsai/git_apps_handlers_test.goservices/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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
installationID | path | string | Required | GitHub App installation id. |
Call it
curl -sX DELETE -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/git-apps/github/installations/$INSTALLATION_ID" -w "%{http_code}\n"Responses
Installation removed.
When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | The id is malformed. | Use an id from the list. |
| 404 | No installation with that id. | It may already be removed. |
| 500 | The 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.goservices/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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
installationID | path | string | Required | GitHub App installation id. |
Call it
curl -sX POST -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/git-apps/github/installations/$INSTALLATION_ID/verify" | jqResponses
The installation authenticated.
{ "installation_id": "48210077", "ok": true, "verified_at": "2026-08-19T13:40:02Z" }When it fails
| Status | Cause | What to do |
|---|---|---|
| 404 | No installation with that id. | List the installations. |
| 500 | Verification 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.goservices/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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
installationID | path | string | Required | GitHub App installation id. |
Call it
curl -sX POST -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/git-apps/github/installations/$INSTALLATION_ID/refresh" | jqResponses
Metadata refreshed.
{ "installation_id": "48210077", "account": "acme", "repository_count": 12 }When it fails
| Status | Cause | What to do |
|---|---|---|
| 404 | No installation with that id. | List the installations. |
| 502 | GitHub could not be reached or rejected the call. | Verify the installation first. |
| 500 | The 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.goservices/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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
installationID | path | string | Required | GitHub App installation id. |
Call it
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/git-apps/github/installations/$INSTALLATION_ID/repositories" | jqResponses
Reachable repositories.
[
{ "full_name": "acme/payments", "private": true }
]When it fails
| Status | Cause | What to do |
|---|---|---|
| 404 | No installation with that id. | List the installations. |
| 502 | GitHub could not be reached. | Verify the installation. |
Side effects
- Calls GitHub.
Proven by
services/nopsai/git_apps_handlers_test.goservices/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.goSource document, delivery records, and verification results.
doc/git-webhook-sources.mdProvider configuration, security, and payload normalisation.
doc/git-apps.mdGitHub App registration, installations, and git-bot routing.

