Appearance
Rollouts
The REST surface for rollouts and rollout plans. The conceptual model — rule vs rollout, percentage bucketing, plans, safety primitives — lives on the Rollouts concept page.
Three resource families:
- Rollout state — env-scoped, 1:1 with a flag or config. Create / read / pause / resume / cancel / advance.
- Target IDs — the rollout's allow-list. Bulk add / remove / replace / list / contains.
- Rollout plans — project-scoped ramp templates. CRUD plus a "used by" count.
Every flag endpoint below has a symmetric config endpoint at /configs/{key} instead of /flags/{key}. Only the flag form is shown to keep the page short.
The Rollout object:
json
{
"id": "9f7a32b5-…",
"environmentId": "5678ef01-…",
"primitiveKind": "flag",
"primitiveKey": "checkout.new-flow",
"status": "active",
"percent": 25.0,
"pausedAtPercent": null,
"pausedReason": null,
"seed": "checkout.new-flow:production",
"bucketField": "userId",
"newValue": true,
"planKey": "standard-1d",
"planRampStepsSnapshot": [
{ "percent": 1, "holdForSeconds": 3600 },
{ "percent": 10, "holdForSeconds": 7200 },
{ "percent": 50, "holdForSeconds": 14400, "requiresApproval": true },
{ "percent": 100, "holdForSeconds": 0 }
],
"cadence": "auto",
"planBlackoutDaysOfWeek": [5, 6],
"planBlackoutTimezone": "America/Los_Angeles",
"rampStepIdx": 1,
"nextStepAt": "2026-05-24T03:00:00Z",
"targetIdsCount": 142,
"createdAt": "2026-05-23T10:00:00Z",
"updatedAt": "2026-05-23T11:00:00Z"
}status is one of active, paused, completed, cancelled. pausedReason is one of user, approval_gate, auto_rollback when status is paused; null otherwise.
Rollout state
Get the rollout
http
GET /api/v1/envs/{envId}/flags/{key}/rolloutBearer scope: read action.
Response: 200 → Rollout | 404 not_found if no rollout exists for this (flag, env).
Create or update a rollout
http
PUT /api/v1/envs/{envId}/flags/{key}/rollout
Content-Type: application/json
{
"percent": 1.0,
"planKey": "standard-1d",
"seed": "checkout-rollout-v2",
"bucketField": "tenantId",
"newValue": true
}Bearer scope: write action. Idempotent — re-PUTting the same body returns the existing rollout unchanged. The first PUT creates the rollout; subsequent PUTs edit it.
Optional fields:
planKey— attach a rollout plan. Snapshots the plan's ramp steps and blackout config onto the rollout. Omit for manual rollouts driven by explicit PUTs.seed— override the bucketing seed. Defaults to<primitiveKey>:<envKey>. Locked oncepercent > 0; subsequent PUTs that change the seed return400 rollout_seed_locked.bucketField— override the project's target ID field. Defaults to the project's configured target ID.newValue— the candidate value to roll out. Required when the flag has no matching rule; if the flag has rules, the rule'svalueis used andnewValuemay be omitted.
Response: 200 → RolloutErrors: 400 invalid_request, 400 rollout_seed_locked, 403 scope_denied, 404 not_found.
Pause a rollout
http
POST /api/v1/envs/{envId}/flags/{key}/rollout/pauseBearer scope: write action. Sets status: "paused", pausedAtPercent: <current percent>, pausedReason: "user". Idempotent.
Response: 200 → Rollout
Resume a rollout
http
POST /api/v1/envs/{envId}/flags/{key}/rollout/resumeBearer scope: write action. Dispatches on pausedReason:
user— continues the ramp from the held percent.approval_gate— atomically approves the gate and advances to the gated step.auto_rollback— returns the rollout toactiveat the percent it was rolled back from. The operator is taking explicit ownership of resuming.
Response: 200 → RolloutErrors: 409 rollout_not_paused if the rollout is already active.
Cancel a rollout
http
POST /api/v1/envs/{envId}/flags/{key}/rollout/cancel
DELETE /api/v1/envs/{envId}/flags/{key}/rolloutBearer scope: write action. Sets status: "cancelled" and stops admission. The two endpoints are equivalent — DELETE is provided for clients that prefer REST verbs. Idempotent. The row is preserved for audit; the flag returns to its previous value.
Response: 200 → Rollout (POST) | 204 No Content (DELETE)
Advance a rollout (manual)
http
POST /api/v1/envs/{envId}/flags/{key}/rollout/advanceBearer scope: write action. Available on plans with cadence: "manual", or as an operator override on cadence: "auto" rollouts. Advances to the next ramp step regardless of holdForSeconds, blackout windows, or approval gates. Use sparingly.
Response: 200 → RolloutErrors: 409 rollout_already_at_final_step if there is no next step.
Target IDs
The rollout's allow-list. Target IDs are matched against the rollout's bucketField (e.g. userId). Hits short-circuit the percent gate and always receive the candidate value.
Add target IDs
http
POST /api/v1/envs/{envId}/flags/{key}/rollout/target-ids/add
Content-Type: application/json
{ "targetIds": ["u_42", "u_43", "u_44"] }Bearer scope: write action. Up to 100,000 IDs per request. Duplicate IDs are ignored; the response reports the actual number added.
Response: 200 → { "added": 3, "count": 145 }
count is the new total size of the allow-list.
Remove target IDs
http
POST /api/v1/envs/{envId}/flags/{key}/rollout/target-ids/remove
Content-Type: application/json
{ "targetIds": ["u_42"] }Bearer scope: write action. Up to 100,000 IDs per request. IDs not present are skipped.
Response: 200 → { "removed": 1, "count": 144 }
Replace target IDs
http
POST /api/v1/envs/{envId}/flags/{key}/rollout/target-ids/replace
Content-Type: application/json
{ "targetIds": ["u_100", "u_101"] }Bearer scope: write action. Atomically replaces the entire allow-list with the supplied set. Use for snapshot syncs from an external source of truth.
Response: 200 → { "count": 2 }
List target IDs
http
GET /api/v1/envs/{envId}/flags/{key}/rollout/target-ids?cursor=…&limit=1000Bearer scope: read action. Cursor-paginated, default page size 1000.
Response: 200 → { "items": ["u_42", "u_43", …], "nextCursor": "eyJ…" }
Check membership
http
GET /api/v1/envs/{envId}/flags/{key}/rollout/target-ids/contains/{targetId}Bearer scope: read action. O(1) lookup against the in-memory mirror.
Response: 200 → { "contains": true }
Rollout plans
Project-scoped templates. A plan supplies the ramp steps and cadence; a rollout attaches via planKey.
The RolloutPlan object:
json
{
"id": "1234abcd-…",
"projectId": "5678ef01-…",
"key": "standard-1d",
"name": "Standard 1-day ramp",
"description": "1% → 10% → 50% (approval) → 100% over ~6 hours.",
"rampSteps": [
{ "percent": 1, "holdForSeconds": 3600 },
{ "percent": 10, "holdForSeconds": 7200 },
{ "percent": 50, "holdForSeconds": 14400, "requiresApproval": true },
{ "percent": 100, "holdForSeconds": 0 }
],
"cadence": "auto",
"blackoutDaysOfWeek": [5, 6],
"blackoutTimezone": "America/Los_Angeles",
"createdAt": "2026-05-01T10:00:00Z",
"updatedAt": "2026-05-01T10:00:00Z"
}List plans
http
GET /api/v1/projects/{id}/rollout-plansBearer scope: read action.
Response: 200 → RolloutPlan[]
Create a plan
http
POST /api/v1/projects/{id}/rollout-plans
Content-Type: application/json
{
"key": "standard-1d",
"name": "Standard 1-day ramp",
"rampSteps": [
{ "percent": 1, "holdForSeconds": 3600 },
{ "percent": 10, "holdForSeconds": 7200 },
{ "percent": 50, "holdForSeconds": 14400, "requiresApproval": true },
{ "percent": 100, "holdForSeconds": 0 }
],
"cadence": "auto",
"blackoutDaysOfWeek": [5, 6],
"blackoutTimezone": "America/Los_Angeles"
}Bearer scope: write action.
Validation:
keyis required, unique within the project, ≤ 64 characters.rampStepsis required, 1–32 entries, percents strictly increasing, eachholdForSecondsbetween 60 and 2,592,000 (30 days).requiresApproval: trueis only valid whencadence: "auto".blackoutTimezonemust be a valid IANA timezone ifblackoutDaysOfWeekis non-empty.
Response: 201 → RolloutPlanErrors: 400 invalid_request, 403 scope_denied, 404 not_found, 409 rollout_plan_key_conflict.
Get a plan
http
GET /api/v1/projects/{id}/rollout-plans/{planKey}Bearer scope: read action.
Response: 200 → RolloutPlan
Update a plan
http
PATCH /api/v1/projects/{id}/rollout-plans/{planKey}
Content-Type: application/json
{ "rampSteps": [...] }Bearer scope: write action. Any field on RolloutPlan is patchable.
Edits do not cascade to live rollouts. Every active rollout that attached to this plan keeps the snapshot it was created with. New rollouts attached after the edit pick up the new shape.
Response: 200 → RolloutPlan
Delete a plan
http
DELETE /api/v1/projects/{id}/rollout-plans/{planKey}Bearer scope: write action. Idempotent on already-deleted plans.
Response: 204 No ContentErrors: 409 rollout_plan_in_use if any active rollout still references this plan. Cancel or complete those rollouts first.
Migration warnings
http
GET /api/v1/orgs/{slug}/rollouts/migration-warningsSession-authenticated, admin or owner. Surfaces rollouts that were affected by the May 2026 migration of per-rule rolloutPercent to the env-scoped rollout system. Each warning names the flag, the original rule indices, and which rule's rollout was hoisted. Use this to audit your post-migration state.
Response: 200 → RolloutMigrationWarning[]
Errors
| Code | HTTP | When |
|---|---|---|
rollout_seed_locked | 400 | seed changed on a PUT while percent > 0. Seed is immutable once admission begins. |
rollout_not_paused | 409 | /resume called on an active rollout. |
rollout_already_at_final_step | 409 | /advance called on a rollout already at 100%. |
rollout_plan_key_conflict | 409 | A plan with this key already exists in the project. |
rollout_plan_in_use | 409 | Plan delete blocked because at least one rollout still references it. |
rollout_plan_invalid_ramp | 400 | Ramp steps fail validation (count, ordering, hold range, approval-on-manual). |
The full error envelope (code, message, details) is the same as the rest of the API — see Errors.
Audit log actions
Every rollout transition writes an audit row. Action names:
rollout.createdrollout.updated(PUT with non-state changes)rollout.paused(operator)rollout.resumedrollout.advanced(manual or scheduler-driven step)rollout.approval_resolved(approval gate cleared)rollout.cancelledrollout.completedrollout.auto_rollback(webhook-driven pause — see Automatic rollback)
Plan CRUD writes parallel actions: rollout_plan.created, rollout_plan.updated, rollout_plan.deleted.
Query the audit log via the Audit endpoint, filtered by resourceType: "rollout" or resourceType: "rollout_plan".