Approvals, scheduling, triggers and webhooks
Require a second pair of eyes in production, schedule a change for Monday morning, let your alerting turn a flag off, and tell Slack or your own systems about every change.
Everything on this page is on the Team and Business plans. A plan that’s moved down keeps what it already set up working; it just can’t add more.
Required comments
An environment can require a comment on every change to it, so history always says why. Turn it on in the environment’s settings. In the API, the comment travels in the Featflags-Comment header of any request that changes something, the CLI takes --comment, and the Terraform provider a change_comment.
Approvals
An environment can require changes to its flags’ targeting to be approved before they go live: turning a flag on or off, its rules, its individual targets. Set it in Environment → Approvals:
- Approvals needed: 1 to 5 people, other than the author unless you allow self-approval.
- Admins may bypass: whether admins and owners apply their own changes directly. A bypass is recorded on the change.
A change in that environment is then held instead of applied: nothing reaches SDKs, and the project’s Approvals page lists it until reviewers approve it (it applies with the approval that reaches the number), reject it (one rejection is enough), or someone who can edit the flag cancels it. When it’s approved, it applies only if the flag is still as it was when the change was requested; if someone changed it meanwhile, the held change fails rather than overwrite their work.
Uploaded key lists can’t wait for approval, since their keys aren’t kept for review, so an environment with approvals refuses them. Changes to the environment itself, which only admins can make, aren’t held.
Scheduled changes
Any change to a flag, its targeting or a segment can be scheduled for later: choose Schedule… when you save it in the app, or send Featflags-Apply-At with the time in the API.
curl -X PATCH -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \
-H 'Featflags-Apply-At: 2026-11-02T09:00:00Z' -H 'Featflags-Comment: launch for the Monday newsletter' \
"$API/projects/acme/flags/new-checkout/environments/production" -d '{"enabled":true}'
It applies at that time, as its author, once it’s approved where approval is required; if its author can no longer make that change by then, it fails instead. Until then you can apply it early or cancel it. Like held changes, it applies only if the flag is still as it was.
Triggers
A trigger is a secret URL that turns one flag on or off in one environment, for tools that can call a URL but not the API: point a Datadog, New Relic or PagerDuty alert at it and the alert flips your kill switch.
Create one on the flag’s Triggers tab. Its URL is shown once; regenerate it if it leaks, which stops the old one at once. Calling it needs no token:
curl -X POST "https://app.featflags.io/triggers/trg_…" -d '{"comment":"error rate over 5%"}'
A trigger’s change is recorded in history like any other, with the comment if the call sent one as JSON (any other body, such as the alerting tool’s own payload, is ignored). It applies even where a comment is required, and keeps working when the organisation’s account is read-only after a failed payment. In an environment with approvals, it can’t wait for reviewers, so only admins can create or enable triggers there, and only when admins may bypass approvals. A trigger takes 10 calls a minute; disable or delete it to stop it.
Copy between environments
To promote a flag from staging to production, open its Compare tab: it shows each part side by side (on or off, off variant, prerequisites, targets, each rule, default), lets you tick what to copy, previews the result and applies it as one change to the target environment, held or commented as that environment requires. featflags flags copy does the same from a terminal. Uploaded key lists aren’t copied; the comparison tells you whether they match.
Webhooks
A project’s webhooks hear about every change that passes their filter. Create them in Settings → Webhooks, with Terraform (featflags_webhook) or the API, and filter by:
- Environments: only changes in these, plus changes to flags and segments themselves, which affect every environment.
- What changed: flag on/off and targeting, rules, segments, keys, triggers and so on.
- Flag key prefix: only flags whose key starts with it.
Each change sends a POST with JSON like this:
{
"event": "applied",
"change": {
"id": 412,
"actor": "user:ana@acme.com",
"target_type": "flag_state",
"target_key": "new-checkout/production",
"before": { "enabled": false, "off_variant": "off", "prerequisites": [], "targets": [], "rules": [], "default": { "variant": "off" } },
"after": { "enabled": true, "off_variant": "off", "prerequisites": [], "targets": [], "rules": [], "default": { "variant": "off" } },
"comment": "launch",
"status": "applied",
"created_at": "2026-10-10T09:00:00Z",
"applied_at": "2026-10-10T09:00:00Z",
"apply_at": null,
"reviews": [],
"error": null,
"bypassed": []
},
"project": { "key": "acme", "name": "Acme" },
"environment": "production",
"versions": { "production": 1791367260000 }
}
event is applied for a change that went live, and requested, scheduled, approved, rejected, cancelled or failed for the steps of a held one. environment is null for changes to a flag or segment as a whole. Uploaded key lists appear as their size and a checksum, never the keys.
Check the signature before you trust a delivery. Each one carries Featflags-Signature: t=<unix seconds>,v1=<signature>, where the signature is the hex HMAC-SHA256 of t, a ., and the raw body, under the webhook’s secret (whsec_…, shown once when you create it). Check that t is recent too, so an old delivery can’t be replayed:
import { createHmac, timingSafeEqual } from 'node:crypto'
function verify(body: string, header: string, secret: string): boolean {
const { t, v1 } = Object.fromEntries(header.split(',').map((p) => p.split('=')))
const expected = createHmac('sha256', secret).update(`${t}.${body}`).digest('hex')
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300
return fresh && expected.length === v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(v1))
}
Featflags-Delivery is the same on every attempt at one delivery, so you can drop repeats. Anything but a 2xx within 10 seconds is retried, after 5 seconds, then 10, 20 and so on, at most an hour apart, for 24 hours. Settings → Webhooks shows each webhook’s recent deliveries with every attempt.
Deliveries only go to public internet addresses: a URL that is, or resolves to, a private or internal address is refused.
Slack
A Slack webhook posts each change to a channel in plain words, such as [Acme] user:ana@acme.com turned on new-checkout in production, with the comment and a link to the flag. In Slack, create an incoming webhook for the channel, then add it in Settings → Webhooks as a Slack webhook, with the same filters as any other.