Key points
- Invoking answers 202 when it queues a run, and 200 when a replayed idempotency key returns the original run.
- The idempotency key is scoped by trigger and caller, so two systems using the same key do not collide.
- A key currently in flight answers 409 rather than starting a second run.
rate_limit.per_minuteis enforced per trigger and answers 429 when exceeded.payload_schemais checked before a run is created, so a malformed call costs nothing.- Every call, accepted or rejected, is recorded in the invocation history with its caller and reason.
- The JSON body and the GitOps YAML document carry the same fields, so a trigger can move between the two unchanged.
Operations
GET/v1/external-triggersAuthorized
Lists external triggers the caller can see.
Call it
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/external-triggers" | jq '.[] | {id, name, pipeline, enabled}'Responses
Triggers visible to the caller.
[{
"id": "3d2b1f88-77aa-4c19-9f3d-52b0c4e7a901",
"name": "start-first-pipeline",
"enabled": true,
"pipeline": "platform/release-service",
"scope": "platform/production",
"run_team_path": "platform/payments",
"allowed_callers": [{ "service_account": "release-bot" }],
"variable_mapping": { "RELEASE_CHANNEL": "payload.channel" },
"rate_limit": { "per_minute": 10 }
}]When it fails
| Status | Cause | What to do |
|---|---|---|
| 405 | A method other than GET. | Use GET. |
| 503 | Authorization is unavailable, so the list cannot be filtered. | Check AAA. |
| 500 | The query failed. | Platform fault. |
Side effects
- None.
Proven by
services/nopsai/external_triggers_test.goservices/nopsai/external_triggers.go
POST/v1/external-triggersAuthorized
Creates an external trigger.
Notes
The JSON body and the GitOps YAML document carry the same fields, so a trigger created here can be exported into a configuration repository unchanged.
Call it
curl -sX POST "$NOPSAI_URL/v1/external-triggers" \
-H "Authorization: Bearer $NOPSAI_TOKEN" \
-H "Content-Type: application/json" \
--data @trigger.json | jq -r .idResponses
Trigger created.
{
"id": "3d2b1f88-77aa-4c19-9f3d-52b0c4e7a901",
"name": "start-first-pipeline",
"enabled": true,
"pipeline": "platform/release-service",
"scope": "platform/production",
"run_team_path": "platform/payments",
"allowed_callers": [{ "service_account": "release-bot" }],
"variable_mapping": { "RELEASE_CHANNEL": "payload.channel" },
"rate_limit": { "per_minute": 10 }
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | The document is invalid: a missing pipeline, an unusable caller entry, or a malformed mapping. | The message names the field that failed normalisation. |
| 401 | The caller identity could not be resolved for the created-by record. | Use a user or service account token, not an anonymous request. |
| 405 | A method other than POST. | Use POST. |
| 500 | The trigger could not be stored. | Retry. |
Side effects
- Creates an entry point that can start runs.
- Records who created it.
- Writes an audit record.
Proven by
services/nopsai/external_triggers_test.goservices/nopsai/external_triggers_schema_test.goservices/nopsai/external_triggers.goservices/nopsai/external_triggers_gitops.go
GET/v1/external-triggers/{id}Authorized
Reads one external trigger.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | uuid | Required | Trigger identifier. |
Call it
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/external-triggers/$TRIGGER_ID" | jqResponses
The trigger.
{
"id": "3d2b1f88-77aa-4c19-9f3d-52b0c4e7a901",
"name": "start-first-pipeline",
"enabled": true,
"pipeline": "platform/release-service",
"scope": "platform/production",
"run_team_path": "platform/payments",
"allowed_callers": [{ "service_account": "release-bot" }],
"variable_mapping": { "RELEASE_CHANNEL": "payload.channel" },
"rate_limit": { "per_minute": 10 }
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 404 | No trigger with that id. | Confirm the id from the list route. |
| 405 | A method other than GET. | Use GET. |
| 500 | The trigger could not be loaded. | Platform fault. |
Side effects
- None.
Proven by
services/nopsai/external_triggers_test.goservices/nopsai/external_triggers.go
PUT/v1/external-triggers/{id}Authorized
Replaces an external trigger definition.
Notes
Disabling with enabled: false is safer than deleting while you investigate a misbehaving caller: the invocation history stays attached.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | uuid | Required | Trigger to replace. |
Call it
curl -sX PUT "$NOPSAI_URL/v1/external-triggers/$TRIGGER_ID" \
-H "Authorization: Bearer $NOPSAI_TOKEN" \
-H "Content-Type: application/json" \
--data @trigger.json | jqResponses
Trigger replaced.
{
"id": "3d2b1f88-77aa-4c19-9f3d-52b0c4e7a901",
"name": "start-first-pipeline",
"enabled": true,
"pipeline": "platform/release-service",
"scope": "platform/production",
"run_team_path": "platform/payments",
"allowed_callers": [{ "service_account": "release-bot" }],
"variable_mapping": { "RELEASE_CHANNEL": "payload.channel" },
"rate_limit": { "per_minute": 10 }
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | The document is invalid. | The message names the field. |
| 404 | No trigger with that id. | Create it instead. |
| 405 | A method other than PUT or PATCH. | Use PUT to replace or PATCH to merge. |
| 500 | The update could not be persisted. | Retry. |
Side effects
- Changes what callers may invoke and what the trigger starts.
- Writes an audit record.
Proven by
services/nopsai/external_triggers_test.goservices/nopsai/external_triggers.go
PATCH/v1/external-triggers/{id}Authorized
Partially updates an external trigger.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | uuid | Required | Trigger to update. |
Call it
curl -sX PATCH "$NOPSAI_URL/v1/external-triggers/$TRIGGER_ID" \
-H "Authorization: Bearer $NOPSAI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"enabled":false}' | jq '{id, enabled}'Responses
Trigger updated.
{
"id": "3d2b1f88-77aa-4c19-9f3d-52b0c4e7a901",
"name": "start-first-pipeline",
"enabled": true,
"pipeline": "platform/release-service",
"scope": "platform/production",
"run_team_path": "platform/payments",
"allowed_callers": [{ "service_account": "release-bot" }],
"variable_mapping": { "RELEASE_CHANNEL": "payload.channel" },
"rate_limit": { "per_minute": 10 }
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | A supplied field is invalid. | The message names the field. |
| 404 | No trigger with that id. | Confirm the id. |
| 500 | The update could not be persisted. | Retry. |
Side effects
- Writes an audit record.
Proven by
services/nopsai/external_triggers_test.goservices/nopsai/external_triggers.go
DELETE/v1/external-triggers/{id}Authorized
Deletes an external trigger.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | uuid | Required | Trigger to delete. |
Call it
curl -sX DELETE -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/external-triggers/$TRIGGER_ID" -w "%{http_code}\n"Responses
Deleted.
When it fails
| Status | Cause | What to do |
|---|---|---|
| 404 | No trigger with that id. | It may already be deleted. |
| 405 | A method other than DELETE. | Use DELETE. |
| 500 | The delete could not be persisted. | Retry. |
Side effects
- Removes the entry point. Runs it already started are unaffected.
- Writes an audit record.
Proven by
services/nopsai/external_triggers_test.goservices/nopsai/external_triggers.go
POST/v1/external-triggers/{id}/invokeAuthorized
Starts a run through the trigger.
Notes
The idempotency key is scoped by trigger and caller. Two systems using the same key against the same trigger do not collide.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | uuid | Required | Trigger to invoke. |
Call it
curl -sX POST "$NOPSAI_URL/v1/external-triggers/$TRIGGER_ID/invoke" \
-H "Authorization: Bearer $CALLER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"idempotency_key":"change-4821","payload":{"channel":"stable"}}' | jqResponses
Accepted: a run was queued from this invocation.
{
"run_id": "9c1f7a5e-2b44-4d2f-8f2a-2c9f0b6d4e11",
"trigger_event_id": "b0f1...",
"status": "queued"
}The idempotency key was already used and its run is returned instead of starting a second one.
{
"run_id": "9c1f7a5e-2b44-4d2f-8f2a-2c9f0b6d4e11",
"trigger_event_id": "b0f1...",
"status": "queued"
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | The payload fails payload_schema, or the body is malformed. | The schema is checked before a run is created, so a rejected payload costs nothing. |
| 401 | No usable caller identity on the request. | Send a token for an identity in allowed_callers. |
| 403 | The caller is authenticated but not permitted to invoke this trigger. | allowed_callers narrows an already-authorized set; AAA must also allow the caller on the trigger resource. |
| 404 | No trigger with that id, or it is disabled. | Check enabled on the trigger. |
| 409 | The same idempotency key is currently being processed. | Wait and retry: the first call is still in flight. |
| 429 | rate_limit.per_minute was exceeded over the previous minute. | Back off. The limit is per trigger, not per caller. |
| 503 | Authorization is unavailable. | Check AAA; the invocation is refused rather than run unauthorized. |
| 500 | The run could not be created. | Retry with the same idempotency key. |
Side effects
- Records an invocation, accepted or rejected, with the caller and the reason.
- Creates a run and maps payload values into run variables through
variable_mapping. - Updates
last_used_aton the trigger.
Proven by
services/nopsai/external_triggers_test.goservices/nopsai/external_triggers.go
GET/v1/external-triggers/{id}/invocationsAuthorized
Lists invocation history for a trigger.
Notes
This is the fastest answer to "my system says it is triggering runs and nothing happens": a rejected call is recorded here with its reason.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | uuid | Required | Trigger identifier. |
limit | query | integer | Optional | Page size for the history. |
Call it
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/external-triggers/$TRIGGER_ID/invocations" | jqResponses
Invocation records, newest first.
[
{
"id": "6a3c...",
"trigger_id": "3d2b1f88-77aa-4c19-9f3d-52b0c4e7a901",
"caller_type": "service_account",
"caller_id": "release-bot",
"status": "queued",
"run_id": "9c1f7a5e-2b44-4d2f-8f2a-2c9f0b6d4e11"
}
]When it fails
| Status | Cause | What to do |
|---|---|---|
| 405 | A method other than GET. | Use GET. |
| 500 | The history query failed. | Platform fault. |
Side effects
- None.
Proven by
services/nopsai/external_triggers_test.goservices/nopsai/external_triggers.go
How it works
Read the status codes as a state machine: 202 means this call created work, 200 means an earlier identical call did, 409 means one is happening right now, and 429 means you are asking too often. A client that treats all four as success will double-run; one that treats them all as failure will retry forever.
allowed_callers narrows rather than grants. AAA still authorizes the caller against the trigger resource first, which is why an empty list does not open the trigger to everyone.
When a caller insists it is triggering runs and nothing appears, read the invocation history before anything else — a 403 or a schema rejection is recorded there with the reason.
Implementation evidence
services/nopsai/external_triggers.goInvocation, idempotency, and rate-limit handling.
services/nopsai/external_triggers_gitops.goThe trigger document schema shared with GitOps.

