Environment approval webhooks
Vetrix emits three webhook events around the environment-protection
approval flow. External systems filter on the
X-Vetrix-Event request header value to route each delivery.
| Event identifier | When it fires |
|---|---|
environment.approved |
On every approval AND on the threshold-crossing vote. |
environment.rejected |
On the first rejection. Kills the deployment. |
deployment.review_requested |
When a deployment enters queued and addresses a reviewer who has not yet voted. Triggers the per-reviewer email. |
Every delivery carries the standard headers:
X-Vetrix-Event— one of the identifiers above.X-Vetrix-Signature-256— HMAC-SHA256 over the raw JSON body, keyed with the webhook's signing secret.Content-Type: application/json.
Payload shapes
environment.approved
{
"deployment_id": "9d7f7b8e-…",
"environment": "production",
"approver_id": "5a1a6b5e-…",
"comment": "looks good, shipping",
"approvals": [
{ "reviewer_id": "5a1a6b5e-…", "action": "approved", "decided_at": "2026-04-19T18:02:11Z" },
{ "reviewer_id": "21d1ae2f-…", "action": "approved", "decided_at": "2026-04-19T18:03:44Z" }
],
"required_count": 2,
"approved_count": 2,
"threshold_met": true,
"delivered_at": "2026-04-19T18:03:44Z"
}
Receivers filter on threshold_met to decide whether the approval
unblocked the run (the gate promotes the deployment to in_progress).
environment.rejected
{
"deployment_id": "9d7f7b8e-…",
"environment": "production",
"rejector_id": "5a1a6b5e-…",
"comment": "needs changelog entry",
"delivered_at": "2026-04-19T18:03:44Z"
}
A single rejection kills the deployment regardless of how many other
reviewers already approved — the notifier only ever delivers one
environment.rejected per deployment.
deployment.review_requested
{
"deployment_id": "9d7f7b8e-…",
"environment": "production",
"reviewer_id": "21d1ae2f-…",
"delivered_at": "2026-04-19T18:01:03Z"
}
Delivered once per reviewer per deployment; used to trigger the per-reviewer email via the notification pipeline. The dedupe guarantee (no duplicate emails on re-request) lives at the notifier layer; the payload above is pure and idempotent at the webhook layer.
Eligibility helper (engine-side)
internal/environments/approval_webhook.go ships two pure helpers
callers share across the HTTP handler + frontend eligibility check:
IsApproverEligible(actor, reviewers, prior)— returns true iff actor is in the reviewer allowlist and has not yet submitted an approval/rejection.IsThresholdCrossing(prior, required)— returns true exactly once, on the vote that transitions the tally from below-required to at /above-required. Callers use this to decide whether to publish theenvironment.approvedcrossing event.
Deferred follow-ups
- Frontend
ApprovalInline.tsxcomponent wired on the run-detail and environment-detail pages. - WebSocket broadcast at
/ws/deployments/:idso a second reviewer's approval updates the UI within 1 s. - Playwright e2e harness.
- MailCatcher fixture for the email dedupe guarantee.
- Signed-payload receiver fixture.
The webhook event identifiers, payload shapes, and eligibility and threshold helpers shipping today are the stable contract the deferred frontend and e2e layers will consume.