Appearance
Automatic rollback
Automatic rollback pauses a live rollout the moment your alerting tool says something is wrong. You mint a webhook secret in Actuator, paste the URL into Datadog / Grafana / PagerDuty (or any tool that can POST), and when the alert fires, Actuator drops the rollout to 0% and all matching contexts fall back to the previous value.
Automatic rollback is product-level: it acts on a single flag or config rollout, not your service as a whole. Rolling back a bad binary deploy is a separate operator concern.
How it works
Each flag or config has a stable webhook URL per environment:
POST /api/v1/webhooks/rollback/envs/{envId}/flags/{key}
POST /api/v1/webhooks/rollback/envs/{envId}/configs/{key}The URL is stable: it doesn't change when you pause, resume, or restart a rollout. You wire it once into your alerting tool, and it works for every future rollout of that flag.
A fire transitions the bound rollout from active (or paused) to paused with pausedAtPercent: 0 and pausedReason: "auto_rollback". The state machine has no new states — only one new discriminator on the existing pause:
| Current state | After a fire |
|---|---|
active | paused(auto_rollback, 0) |
paused(user) | paused(auto_rollback, 0) |
paused(approval_gate) | paused(auto_rollback, 0) |
paused(auto_rollback) | unchanged (idempotent) |
completed / canceled | unchanged (terminal — fire records but does nothing) |
The fire endpoint is idempotent: re-firing on an already-rolled-back rollout returns 200 with noop: true and no state change. Re-running your alerting tool's webhook test is safe.
Once paused, the rollout stays paused until a human resumes it or starts a new rollout. Auto-resume is intentionally not part of this feature — the operator who's diagnosing the incident decides when traffic is safe to re-admit.
Set up
Mint a webhook secret
Webhook secrets are env-scoped bearer tokens with a single capability: fire any rollback in this environment. They are separate from API keys (which can read and write flags) — the third party that holds your alerting webhook only needs to fire rollbacks, never to read your flag configuration.
http
POST /api/v1/envs/{envId}/rollback-webhook-secrets
Cookie: actuator_session=…
Content-Type: application/json
{
"name": "datadog-prod"
}Session-authenticated, admin or owner. The raw token is returned once; store it immediately in your alerting tool's secret store. Actuator keeps only a SHA-256 hash.
Response: 201 → RollbackWebhookSecretMintResponse
json
{
"id": "9f7a32b5-…",
"name": "datadog-prod",
"tokenPrefix": "actrb_a1b2",
"createdAt": "2026-05-23T10:00:00Z",
"lastFiredAt": null,
"revokedAt": null,
"token": "actrb_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"warning": "This is the only time the raw token is shown. Save it now."
}The token format is actrb_<32 chars>. It does not expire — revoke it explicitly when you rotate.
Wire it to your alerting tool
The fire endpoint accepts an optional JSON body containing the alert's monitor URL. Actuator parses the URL out of the payload and surfaces it in the dashboard banner and fire history so the on-call engineer can jump straight to the alert that caused the rollback.
Datadog
In Datadog, set up a webhook integration per environment:
- URL: the fire endpoint for the rollout you want bound.
- Custom headers:
Authorization: Bearer actrb_…. - Payload: Datadog's default payload works — Actuator extracts
alert_url(or the legacylinkfield) automatically.
Then add the webhook to the monitor's notification list (@webhook-actuator-rollback-checkout-flow).
Grafana
Add a webhook contact point:
- URL: the fire endpoint.
- HTTP Method: POST.
- Authorization header:
Bearer actrb_….
Grafana's default alert payload includes alerts[0].generatorURL, which Actuator parses to surface the originating alert in the dashboard banner.
PagerDuty
Add a webhook v3 extension on the service whose incidents should trip the rollback:
- URL: the fire endpoint.
- Authorization header:
Bearer actrb_….
Actuator extracts event.data.html_url from PagerDuty's v3 payload.
curl (testing)
bash
curl -X POST https://useactuator.ai/api/v1/webhooks/rollback/envs/$ENV_ID/flags/checkout-flow \
-H "Authorization: Bearer actrb_…"The body is optional — the URL and bearer header carry everything load-bearing. When a body is present, Actuator best-effort parses Datadog / Grafana / PagerDuty payload shapes (alert_url, alerts[0].generatorURL, event.data.html_url) and surfaces the extracted URL in the dashboard banner. Other body shapes are accepted but the parsed-URL column is left null.
What happens when it fires
Every fire writes a RollbackFire record — regardless of whether it actually changed state. Seven outcomes are possible:
| Outcome | Meaning |
|---|---|
transitioned | The fire moved an active or user-paused rollout to paused(auto_rollback, 0). |
noop_already_rolled_back | The rollout was already auto-rolled-back. No state change. |
noop_terminal_state | The rollout was completed or canceled. No state change. |
error_unauthorized | Missing, invalid, revoked, or env-mismatched token. |
error_not_found | No rollout exists for the (env, kind, key) tuple. |
error_rate_limited | Per-secret or per-rollout cap exceeded. The response carries Retry-After. |
error_invalid_request | Request body exceeded the 16 KiB cap or was unreadable. Logged so the dashboard can surface misconfigured integrations sending oversized payloads. |
State transitions also write an audit_log row with actorType: "rollback_webhook", actorId set to the secret's id, and reason: "rollout:auto_rollback". Noop fires record in the fire log only — they don't pollute the audit log with non-events.
The dashboard surfaces a banner on the flag/config page for the hour following any transitioned fire, naming the secret and (if parsed) the alert URL. The fire history is queryable per environment, per secret, and per rollout (see Endpoint reference below).
Endpoint reference
Mint a rollback webhook secret
http
POST /api/v1/envs/{envId}/rollback-webhook-secrets
Cookie: actuator_session=…
Content-Type: application/json
{
"name": "datadog-prod"
}Session-authenticated, admin or owner.
name is required, ≤ 64 characters, unique among live (non-revoked) secrets in the environment. A revoked secret's name is freed and can be reused.
Response: 201 → RollbackWebhookSecretMintResponse (see above; includes one-shot token). Errors: 400 invalid_request, 403 insufficient_role (non-admin), 403 quota_exceeded with dimension: "rollback_webhook_secrets_per_env" (20-secret cap reached), 404 not_found, 409 key_in_use (name collides with another live secret in this env).
List rollback webhook secrets
http
GET /api/v1/envs/{envId}/rollback-webhook-secretsReturns every secret minted in the environment, including revoked ones (kept for history). The response strips the token hash; only the prefix and metadata are returned.
Response: 200 → RollbackWebhookSecretSummary[]
json
[
{
"id": "9f7a32b5-…",
"name": "datadog-prod",
"tokenPrefix": "actrb_a1b2",
"createdAt": "2026-05-23T10:00:00Z",
"lastFiredAt": "2026-05-24T02:14:00Z",
"revokedAt": null
}
]lastFiredAt is throttled to at most once per minute per secret, to avoid write amplification during alert storms.
Revoke a rollback webhook secret
http
DELETE /api/v1/envs/{envId}/rollback-webhook-secrets/{id}
Cookie: actuator_session=…Session-authenticated, admin or owner. Idempotent — re-revoking returns 204 either way. Subsequent fires with the revoked token return 401 rollback_unauthorized.
Response: 204 No ContentErrors: 403 insufficient_role, 404 not_found.
Fire a rollback
http
POST /api/v1/webhooks/rollback/envs/{envId}/flags/{key}
Authorization: Bearer actrb_…http
POST /api/v1/webhooks/rollback/envs/{envId}/configs/{key}
Authorization: Bearer actrb_…Bearer-authenticated with a rollback webhook secret (the actrb_ token from minting).
The body is optional. If present, Actuator parses Datadog / Grafana / PagerDuty payload shapes for the originating alert URL and surfaces it on the dashboard. Unknown shapes are accepted but the URL field is left empty. Body is capped at 16 KiB.
Response: 200 → RollbackFireResponse
json
{
"noop": false,
"currentStatus": "paused",
"newStatus": "paused"
}When noop is true, newStatus is omitted.
Errors: 401 rollback_unauthorized, 404 rollback_rollout_not_found, 429 rollback_rate_limited (with Retry-After).
List fire history
Four scopes, all cursor-paginated. The per-rollout variant uses the rollout's primitive key in the path (not a rollout id) so the URL stays stable across cancel-and-recreate.
http
GET /api/v1/envs/{envId}/rollback-fires?cursor=…&limit=…
GET /api/v1/envs/{envId}/rollback-webhook-secrets/{id}/fires?cursor=…&limit=…
GET /api/v1/envs/{envId}/flags/{flagKey}/rollout/fires?cursor=…&limit=…
GET /api/v1/envs/{envId}/configs/{configKey}/rollout/fires?cursor=…&limit=…Each returns a flat list of RollbackFireEvent records:
json
{
"fires": [
{
"id": "019e56bf-…",
"rolloutId": "rol_xyz",
"secretId": "9f7a32b5-…",
"secretName": "datadog-prod",
"primitiveKind": "flag",
"primitiveKey": "checkout-flow",
"outcome": "transitioned",
"sourceIp": "203.0.113.42",
"parsedMonitorUrl": "https://app.datadoghq.com/monitors/12345",
"payloadFingerprint": "7f3a8d…",
"firedAt": "2026-05-23T10:14:22Z"
}
],
"nextCursor": "eyJ…"
}secretId, secretName, and rolloutId are nullable: an unauthorized fire has no secret, and a not-found fire has no rollout. They're preserved either way so you can audit who tried to fire what. parsedMonitorUrl is populated when Actuator recognized the alerting tool's payload (Datadog alert_url / Grafana alerts[0].generatorURL / PagerDuty event.data.html_url); unknown payload shapes leave it null without affecting the rollback itself. payloadFingerprint is SHA-256(raw body) hex-encoded — useful for "is my monitor sending the same notification over and over?" investigations.
Default limit=50, max 200. The response includes nextCursor only when the page filled to limit; a short page means you've reached the end.
Errors
| Code | HTTP | When |
|---|---|---|
rollback_unauthorized | 401 | Missing, invalid, revoked, or wrong-env token. Collapsed deliberately so an attacker can't probe which envs a token belongs to. |
rollback_rollout_not_found | 404 | No flag or config exists at the given key, or no rollout is bound to it. |
rollback_rate_limited | 429 | Per-secret or per-rollout cap exceeded. Response includes Retry-After in seconds. |
quota_exceeded (dimension: "rollback_webhook_secrets_per_env") | 403 | Environment already has 20 live secrets. Revoke an unused one first. |
key_in_use (fields.name) | 409 | A live secret in this env already uses that name. Revoke it or pick a different name. |
The full error envelope (code, message, details) is the same as the rest of the API — see Errors.
Limits
| Limit | Default | Scope |
|---|---|---|
| Fires per minute, per rollout | 60 | Catches flapping alerts on a single rollout. |
| Fires per minute, per secret | 300 | Catches misconfigured floods before they hit rollout resolution. |
| Live webhook secrets per environment | 20 | Mint a new one when you rotate; revoke the old one. |
| Fire request body | 16 KiB | Larger payloads are rejected with 400 invalid_request. |
There is no org-wide auto-rollback cap. The subsystem exists to handle widespread incidents, so a thousand simultaneous fires across a thousand rollouts is supported by design.