Skip to content

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}/rollout

Bearer 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 once percent > 0; subsequent PUTs that change the seed return 400 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's value is used and newValue may 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/pause

Bearer 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/resume

Bearer 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 to active at 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}/rollout

Bearer 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/advance

Bearer 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=1000

Bearer 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-plans

Bearer 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:

  • key is required, unique within the project, ≤ 64 characters.
  • rampSteps is required, 1–32 entries, percents strictly increasing, each holdForSeconds between 60 and 2,592,000 (30 days).
  • requiresApproval: true is only valid when cadence: "auto".
  • blackoutTimezone must be a valid IANA timezone if blackoutDaysOfWeek is 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-warnings

Session-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

CodeHTTPWhen
rollout_seed_locked400seed changed on a PUT while percent > 0. Seed is immutable once admission begins.
rollout_not_paused409/resume called on an active rollout.
rollout_already_at_final_step409/advance called on a rollout already at 100%.
rollout_plan_key_conflict409A plan with this key already exists in the project.
rollout_plan_in_use409Plan delete blocked because at least one rollout still references it.
rollout_plan_invalid_ramp400Ramp 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.created
  • rollout.updated (PUT with non-state changes)
  • rollout.paused (operator)
  • rollout.resumed
  • rollout.advanced (manual or scheduler-driven step)
  • rollout.approval_resolved (approval gate cleared)
  • rollout.cancelled
  • rollout.completed
  • rollout.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".