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:
Drift workflow lifecycle events (
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
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: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.
Delivery guarantees and failures
Nuon webhooks are best-effort notifications, not a durable event stream.- A final
2xxresponse is required for a delivery to succeed. - Nuon waits up to 5 seconds for a response. Endpoints should acknowledge promptly and perform long-running processing asynchronously.
- Network errors, timeouts, and non-
2xxresponses are treated as delivery failures. Nuon does not retry these failures or provide delivery history or replay. - A failed webhook does not interrupt the workflow that produced the event or prevent delivery attempts to other webhook subscriptions.
- Rare infrastructure interruptions can result in duplicate deliveries when Nuon cannot determine whether an earlier
request completed. A duplicate logical event may have a different CloudEvents
id.
2xx response, and derive an
idempotency key from the relevant workflow, step, resource, and transition fields rather than relying only on the
CloudEvents id.
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
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, such as when the secrets do not match, is a delivery failure. See Delivery guarantees and failures. -
When you are done, remove the webhook: