POSTs a
CloudEvents v1.0 envelope to every webhook URL registered on the Org.
Webhooks are scoped to the current Org. Manage them with the Nuon CLI or the dashboard.
Manage webhooks
- The
--urlmust be an absolutehttporhttpsURL with a host. - Webhook URLs are unique per Org per scope. The uniqueness key is
(org_id, webhook_url, match)— the same URL can be registered multiple times in one Org as long as each registration uses a different match predicate (for example, once for “all installs” and again for “components in env=prod”). Registering the exact same(url, match)twice returns a conflict. - The
--secretis write-only. The API never returns it; responses includehas_secret: true|falseso you can tell whether one is configured. - The webhook URL is part of the
(org_id, webhook_url, match)unique index and cannot be changed in place. To rename it, delete the webhook and create a new one. Use the dashboard’s edit action (orPATCH /v1/orgs/current/webhooks/{webhook_id}) to rotate the signing secret or replace the interests filter / match predicate without recreating the webhook.
Filtering events with interests
By default a new webhook receives every supported workflow, deploy, sandbox, runner, and approval event for the Org. To narrow that down, attach aninterests filter on create or update. The filter is a structured object stored
on each webhook and persisted as JSONB on the webhook row.
If you omit interests entirely (and on legacy webhooks created before this filter shipped), Nuon treats the
configuration as {"all_events": true} so deliveries don’t silently stop. The list endpoint surfaces this same
effective shape so the CLI/dashboard/SDK stay in sync with what’s actually delivered.
Top-level shape
A configuration is one of two shapes:all_events: true is the new-subscription default and short-circuits everything else. To opt into a per-resource
configuration, omit all_events (or set it to false) and populate resources. A resource missing from resources
is off — no events of that kind are delivered.
Resource vocabulary
Each resource accepts a list of sub-ops drawn from a fixed vocabulary:| Resource | Supported ops |
|---|---|
installs | provision, deprovision, reprovision |
stacks | version_active |
components | deploy, teardown |
sandboxes | provision, reprovision, deprovision |
install_configurations | inputs, secrets |
runners | provision, reprovision, inactive |
actions | run |
drift_run, drift_run_reprovision_sandbox) are intentionally not part of the
ops vocabulary — every cron tick fires a started/completed pair, which is pure noise on a clean scan. To subscribe to
drift, set drift_detected: true on components and/or sandboxes (see below).
Per-resource fields
| Field | Type | Default | Description |
|---|---|---|---|
ops | []string | [] | Sub-ops to subscribe to. An empty (or omitted) list means every sub-op for the resource. |
outcome | string | "all" | One of all, completion, failures, or none. Filters lifecycle events on terminal status; none mutes lifecycle for this resource entirely. |
approval_requests | bool | false | Deliver workflow-step approval requests for this resource. |
approval_responses | bool | false | Deliver workflow-step approval responses (approved / rejected) for this resource. |
drift_detected | bool | false | Deliver a notification only when drift is actually detected during a drift scan. Only meaningful for components and sandboxes. |
outcome semantics:
all— everystarted+ terminal event for the matched ops.completion— terminal events only (succeeded,failed,cancelled); suppressstarted.failures— onlyfailedandcancelledterminal events.none— mutes lifecycle events entirely for this resource. Drift and approval events are gated independently and remain functional, sononeis the right choice when you want a drift-only or approvals-only subscription for this resource.
outcome by the approval_requests and approval_responses booleans.
Drift detection
To receive drift notifications, setdrift_detected: true on components and/or sandboxes. The
notification fires once per component (inside drift_run) or once per sandbox (inside
drift_run_reprovision_sandbox) whenever the plan-only check finds non-no-op changes. It works for
both manually triggered and scheduled drift scans, and is gated independently of outcome — the
same way approval_requests / approval_responses are.
Runner inactivity
Runners emit arunners.inactive lifecycle event when the control plane gives
up on a runner-process — currently this fires after roughly 5 minutes of
silence on the heartbeat channel for that process. It does not distinguish
a true outage from routine churn (process restart, version upgrade, deliberate
shutdown), so subscribers should expect to see it on normal operations as well.
Subscribe by including inactive in the runners ops list (or by leaving
ops empty to receive every runner sub-op).
Examples
Subscribe to every supported event (this is the default ifinterests is omitted):
Setting interests via the CLI
nuon orgs webhooks create and nuon orgs webhooks update take the full subscription — both the
interests filter and the optional match predicate
— as a single JSON document via --subscription-json (inline) or --subscription-file (path on disk). The two flags
are mutually exclusive. Omit both to fall back to the default: every supported event in the Org, no scope.
{"interests": {...}, "match": {...}} — both keys are optional. A missing interests defaults to
{"all_events": true} so you can scope a webhook with --subscription-json '{"match":{...}}' without restating the
events filter. A missing or null match is the org-wide (“every entity”) subscription.
update replaces both interests and match wholesale — it is not a deep merge. Pass --secret <new> to rotate the
signing secret at the same time; omit --secret to leave the existing secret unchanged. The webhook URL plus its
match predicate together form the (org_id, webhook_url, match) unique index — delete and recreate to rename, or
register a sibling row with a different match to deliver the same URL with a different scope.
Setting interests via the dashboard
The dashboard’s webhook create + edit forms expose the interests filter as an inline picker. Toggling Send all events off materializes the per-resource baseline shown above so you land on a sensible starting point instead of an empty config that silently drops every event. Saving the form persists the same JSON shape documented here.Match in the dashboard. The dashboard’s webhook forms do not yet expose the match predicate, and saving from the dashboard resets the webhook to org-wide. To create or preserve a scoped webhook, manage it from the CLI.
Scoping deliveries with match
By default a webhook is org-wide — it fires for every event the interests filter opts into, regardless of which install, component, or action triggered it. Attach amatch predicate to scope deliveries to specific entities or to entities carrying specific
labels.
match is an object with up to three optional kinds — installs, components, actions. For each kind, list the
ids you care about and/or a label selector:
- An event is delivered if any populated kind matches.
- Within a kind, the entity matches if its ID is in
idsor its labels satisfyselector. - A
selectorrequires everymatch_labelsentry to match (AND); use"*"as the value to require only that the key is present. - An empty filter (
{}) for a kind means “any entity of this kind”. - Omit
matchentirely (or sendnull) for the org-wide default.
Examples
Every event, but only for two specific installs:env=prod:
match predicates — for example, a noisy
“all events” subscription to a staging endpoint plus a focused “drift only on env=prod components” subscription to a
paging endpoint.
Two primitives: workflows and workflow steps
The webhook surface exposes exactly two primitives:- Workflow lifecycle (
com.nuon.workflow.lifecycle.v1) — fires for the workflow as a whole. Carries the workflow’stype(e.g.provision,reprovision,manual_deploy,action_workflow_run) and its owner (installs,apps,app_branches). - Workflow step lifecycle (
com.nuon.workflow_step.lifecycle.v1) — fires for each step within a workflow. Carries the step’sname,idx, andtarget_type(e.g.install_deploys,install_sandbox_runs,install_action_workflow_runs) along with denormalizedcomponent_id/sandbox_idwhen applicable.
data.workflow.type == "provision" and a step whose target_type == "install_sandbox_runs" succeeded.
To know “did this component deploy?”, look at a step whose target_type == "install_deploys" and inspect
data.step.component_id.
Transitions
Both primitives use the same transition vocabulary:| Transition | When it fires |
|---|---|
started | The workflow / step begins execution |
succeeded | Execution completed successfully |
failed | Execution errored or validation rejected the input |
cancelled | Execution was cancelled |
data.transition is the transition name. data.outcome.status mirrors it on *.succeeded, *.failed, and
*.cancelled events. Validation failures of the workflow / step wrappers surface as a failed outcome on the
following execute event — there is no separate validate event.
A typical Install provision produces, in order:
workflow.lifecycle startedfor the provision workflow.- For each step in the workflow:
workflow_step.lifecycle started→workflow_step.lifecycle succeeded(orfailed/cancelled). workflow.lifecycle succeededfor the provision workflow once every step terminates.
Payload format
The body is a CloudEvents v1.0 JSON envelope sent withContent-Type: application/cloudevents+json; charset=utf-8.
Nuon-specific extension attributes (nuonorgid, nuonkind, nuontransition) are mirrored on the envelope for
routing.
Workflow lifecycle event
Workflow step lifecycle event
Nested workflows: parent
When a workflow is launched from another workflow’s step (for example, an action workflow run launched from a
deploy step), the child workflow’s events include a data.parent block:
parent is omitted for top-level workflows.
Field reference
| Field | Description |
|---|---|
nuonorgid | Org id this event belongs to. Nuon CloudEvents extension; mirrors data.org_id. |
nuonkind | workflow or workflow_step. Mirrors data.kind. |
nuontransition | started, succeeded, failed, or cancelled. Mirrors data.transition. |
interests | Slug list produced by Nuon’s classifier for this event. Always contains a resource:<kind> and op:<kind>.<sub-op> slug, plus an event:<…> slug identifying the transition (e.g. event:lifecycle.succeeded, event:approval.request, event:drift.detected) and zero or more outcome:<…> slugs (outcome:completion on terminal events, plus outcome:failures on failed or cancelled transitions). Consumers can route by slug prefix without re-implementing the classifier. |
data.kind | workflow or workflow_step. |
data.transition | started, succeeded, failed, or cancelled. |
data.org_id | Org id. |
data.workflow.id | Workflow id. Stable across all events for the workflow. |
data.workflow.type | Workflow kind (provision, reprovision, manual_deploy, action_workflow_run, etc.). |
data.workflow.owner_id | Id of the entity that owns the workflow (an install, app, or app branch). |
data.workflow.owner_type | One of installs, apps, app_branches. |
data.step.id | Workflow step id (workflow_step events only). |
data.step.name | Human-readable step name (for example, "deploy api (apply)"). |
data.step.idx | Step index within the workflow. |
data.step.target_type | The kind of resource the step manipulates: install_deploys, install_sandbox_runs, etc. |
data.step.target_id | Id of the manipulated resource. |
data.step.component_id | Component id when target_type == "install_deploys". |
data.step.sandbox_id | Sandbox id when target_type == "install_sandbox_runs". |
data.step.execution_type | system, user, approval, skipped, or hidden. |
data.parent | Present when this workflow was launched from another workflow’s step. See above. |
data.outcome.status | Mirrors data.transition on terminal events. Omitted on started. |
data.outcome.error | Human-readable error message. Set only on failed events. |
data.outcome.duration_ms | How long the workflow / step took to run, in milliseconds. |
data.links | Dashboard URLs for the org, install, workflow, sandbox, and component (when applicable). |
Verifying signatures
When a webhook is created with--secret, Nuon signs the raw request body with HMAC-SHA256 using your secret and sends
the lowercase hex digest in the X-Nuon-Signature header. Reject any request whose signature does not match. When no
secret is configured, no signature header is sent.
End-to-end example
-
Start your receiver locally and expose it (for example with
ngrok http 8080). -
Register the webhook with your Org:
-
Trigger a workflow. The smallest reliable trigger is reprovisioning a Sandbox:
-
Your receiver should record one
workflow.lifecycle.started, a sequence ofworkflow_step.lifecycle.*deliveries (one started + one terminal per step), and finally aworkflow.lifecycle.succeeded(orfailed/cancelled). A401response from your receiver — for example, when the secrets do not match — surfaces in Nuon as a delivery failure and the event is not retried. -
When you are done, remove the webhook: