# REST API

Everything the dashboard does goes through the REST API at `https://flag.nightroll.app/api/v1`. Paths below are
relative to it; `{org}` is your org's slug (or id) and must be the org your key belongs to.

## Authentication

Send an API key as a bearer token:

```sh
curl https://flag.nightroll.app/api/v1/orgs/$ORG/projects/default/flags \
  -H "Authorization: Bearer nr_flag_..."
```

Make keys under [Settings → API keys](/settings/org/keys). A key is bound to one org, acts with your role there, and
is limited by its scopes:

| Scope | Allows |
|---|---|
| `read` | reading projects, flags, segments, change requests, history and insights; evaluating |
| `write` | also changing flags and segments, and proposing change requests |
| `admin` | everything your role allows: projects, environments, SDK keys, org settings |

A key (or an MCP client connected with OAuth) acts as an **agent**: its changes to a critical environment become
change requests unless an admin turns that off in the dashboard's **Settings**, and it can never review a change
request. `GET /me` shows who and which org a key is for.

The dashboard uses a session cookie; state-changing cookie requests must come from Flag's own origin.

## Errors

Errors are JSON with a stable code and a message written for people:

```json
{"error": {"code": "conflict", "message": "Flag new-checkout already exists in default."}}
```

| Status | Code | When |
|---|---|---|
| 400 | `invalid_request` | the request is invalid (the message says how) |
| 401 | `unauthorized` | no or bad credentials |
| 403 | `forbidden`, `insufficient_scope` | your role or the key's scopes don't allow it |
| 404 | `not_found` | |
| 409 | `conflict` | the state doesn't allow it (a key in use, a request already decided...) |
| 503 | `unavailable` | |

## Dry runs

Every mutation accepts `?dryRun=true`: it runs in a transaction that is rolled back and answers with what would have
happened, plus `"dryRun": true`. Use it to preview a change, or to validate one.

```sh
curl -X PATCH 'https://flag.nightroll.app/api/v1/orgs/acme/projects/default/flags/new-checkout?dryRun=true' \
  -H "Authorization: Bearer $FLAG_TOKEN" -H 'content-type: application/json' \
  -d '{"environmentKey": "production", "instructions": [{"kind": "turnFlagOn"}]}'
# {"outcome": "applied", "dryRun": true, "diff": {"production": [{"path": "/on", "from": false, "to": true}]}, "flag": {...}}
```

## Organizations, projects and environments

Paths take the org's id or slug, and project, environment and flag keys.

| | |
|---|---|
| `GET /me` | you, the key's org and your role there |
| `GET /orgs/{org}` | the org, your role, its projects and environments |
| `GET\|POST /orgs/{org}/projects`, `GET\|PATCH\|DELETE .../projects/{proj}` | projects (DELETE archives and revokes SDK keys) |
| `GET\|POST .../projects/{proj}/environments`, `GET\|PATCH\|DELETE .../environments/{env}` | environments: `critical`, `requireApproval`, `minApprovals`, `allowSelfApproval` (owners and admins); deleting a critical one needs `?confirm=<key>` |
| `GET\|POST .../environments/{env}/keys`, `DELETE .../keys/{id}` | SDK keys; `{"kind": "server"}` returns the key once; `expireOthersInHours` rotates; `?expireInHours=` revokes later |
| `GET .../projects/{proj}/compare?from=&to=` | which flags and segments differ between two environments |
| `POST .../environments/{env}/copy` | copy targeting (`{"from", "flags"?, "includeSegments"?}`) into `{env}`, through its approvals |

## Flags

| | |
|---|---|
| `GET .../flags` | flags with a summary per environment; `?tag=`, `?q=`, `?archived=true`, `?full=true` |
| `POST .../flags` | create: `{"key", "name"?, "template"?, "kind"?, "variations"?, "tags"?, "clientSide"?, ...}` |
| `GET .../flags/{flag}` | the flag with its full targeting in every environment, dependents, pending change requests and schedules |
| `PATCH .../flags/{flag}` | change it with [semantic patch instructions](#semantic-patch) |
| `DELETE .../flags/{flag}` | archive (restore with `restoreFlag`) |
| `GET .../flags/{flag}/history?env=` | versions, newest first, with who changed what and the diff (omit `env` for flag-wide settings) |
| `POST .../flags/{flag}/restore` | `{"revisionId"}`: write an earlier version back as a new one |
| `POST .../flags/{flag}/evaluate` | `{"environmentKey", "context"}`: the result and every step that led to it |
| `GET .../flags/{flag}/insights?env=&hours=` | evaluations per hour and variation |
| `POST .../flags/{flag}/blast-radius` | `{"environmentKey", "instructions"?}`: traffic, dependents, segments, streams and a risk level |
| `GET\|POST /orgs/{org}/templates`, `DELETE .../templates/{key}` | flag templates (built-in and your own) |

## Semantic patch

A change is a list of instructions applied in order, all or nothing:

```json
{
  "environmentKey": "production",
  "instructions": [
    {"kind": "turnFlagOn"},
    {"kind": "addRule", "description": "Acme staff",
     "clauses": [{"contextKind": "user", "attribute": "email", "op": "endsWith", "values": ["@acme.com"]}],
     "variation": 0},
    {"kind": "updateFallthroughVariationOrRollout",
     "rollout": {"variations": [{"variation": 0, "weight": 10000}, {"variation": 1, "weight": 90000}]}}
  ],
  "comment": "Acme first, then 10%"
}
```

The answer has an `outcome`: `applied`, `unchanged`, or `changeRequest` when the environment requires approval (or an
agent writes to a critical environment); then `changeRequest` holds the request. It also has the updated `flag` and the
`diff` per scope (`"flag"` for flag-wide settings, the environment's key for targeting). Owners and admins may add
`?bypassApprovals=true` to apply directly where approval is required; it is audited.

**Targeting instructions** (need `environmentKey`):

| `kind` | Fields |
|---|---|
| `turnFlagOn`, `turnFlagOff` | |
| `updateOffVariation` | `variation` (index, or `null` to serve the SDK's default) |
| `updateFallthroughVariationOrRollout` | `variation` or `rollout` (the default rule) |
| `addTargets`, `removeTargets` | `variation`, `values` (keys), `contextKind` (default `user`) |
| `replaceTargets` | `targets`: `[{"contextKind", "variation", "values"}]` |
| `addRule` | `clauses`, `variation` or `rollout`, `description`?, `ruleId`?, `beforeRuleId`? |
| `removeRule` | `ruleId` |
| `updateRuleVariationOrRollout` | `ruleId`, `variation` or `rollout` |
| `updateRuleDescription` | `ruleId`, `description` |
| `addClauses` | `ruleId`, `clauses` |
| `removeClauses` | `ruleId`, `clauseIndexes` |
| `reorderRules` | `ruleIds` (every rule, in the new order) |
| `replaceRules` | `rules`: `[{"id"?, "description"?, "clauses", "variation" or "rollout", "trackEvents"?}]` |
| `addPrerequisite`, `updatePrerequisite` | `key` (another flag), `variation` it must serve |
| `removePrerequisite` | `key` |
| `updateTrackEvents` | `value`: send a full event per evaluation |
| `replaceConfig` | `config`: the whole environment configuration |

**Flag-wide instructions** (no `environmentKey`; in an environment that requires approval, send them separately):

| `kind` | Fields |
|---|---|
| `updateName`, `updateDescription` | `value` |
| `addTags`, `removeTags` | `values` |
| `addMaintainers`, `removeMaintainers` | `values` (user ids) |
| `makeFlagTemporary`, `makeFlagPermanent` | |
| `turnOnClientSideAvailability`, `turnOffClientSideAvailability` | |
| `addVariation` | `value`, `name`?, `description`? |
| `updateVariation` | `index`, and any of `value`, `name`, `description` |
| `updateDefaultVariations` | `onVariation`, `offVariation` (for new environments) |
| `updateJsonSchema` | `schema` (JSON flags; `null` removes it) |
| `archiveFlag`, `restoreFlag` | |
| `replaceMeta` | `meta`: every flag-wide field |

**Clauses**: `{"contextKind": "user", "attribute": "email", "op": "endsWith", "values": ["@acme.com"], "negate": false}`.
Operators: `in`, `startsWith`, `endsWith`, `contains`, `matches` (regular expression), `lessThan`, `lessThanOrEqual`,
`greaterThan`, `greaterThanOrEqual` (numbers), `before`, `after` (RFC 3339 or Unix milliseconds), `semVerEqual`,
`semVerLessThan`, `semVerGreaterThan`, and `segmentMatch` (values are segment keys; no attribute). A rule matches when
all its clauses do. Exact semantics: [evaluation spec](/docs/evaluation/).

**Rollouts**: `{"variations": [{"variation": 0, "weight": 25000}, {"variation": 1, "weight": 75000}], "bucketBy":
"key", "contextKind": "user"}`. Weights are thousandths of a percent and add up to `100000`. A context always lands in
the same bucket for the same flag.

## Segments

`GET|POST .../projects/{proj}/environments/{env}/segments`, `GET|PATCH|DELETE .../segments/{seg}`. Create with
`{"key", "name"?, "included"?, "excluded"?, "includedContexts"?, "excludedContexts"?, "rules"?}`; change with
`{"instructions", "comment"?}`:

| `kind` | Fields |
|---|---|
| `addIncludedTargets`, `removeIncludedTargets`, `addExcludedTargets`, `removeExcludedTargets` | `values`, `contextKind` (default `user`) |
| `addRule` | `clauses`, `weight`? (0 to 100000: only that share of matching contexts), `bucketBy`?, `rolloutContextKind`?, `ruleId`? |
| `removeRule` | `ruleId` |
| `replaceRules` | `rules` |
| `replaceBody` | `body`: `{"included", "excluded", "includedContexts", "excludedContexts", "rules"}` |
| `updateName`, `updateDescription` | `value` |
| `addTags`, `removeTags` | `values` |

Segments in environments that require approval change through change requests too. A segment in use can't be deleted.

## Change requests

`GET .../projects/{proj}/change-requests?status=&env=`, `POST` to propose (`{"kind": "flag"|"segment", "targetKey",
"environmentKey", "instructions", "comment"?, "runAt"?}`), `GET .../change-requests/{id}` (with a preview of what it
would change now), and `POST .../{id}/review` (`{"decision": "approve"|"decline"|"comment", "comment"?}`),
`.../{id}/apply`, `.../{id}/cancel`. A request applies once it has the environment's `minApprovals` approvals (at
`runAt` if it has one). People can't approve their own requests unless the environment allows it; agents and tokens
can propose but never review.

## Schedules and cleanup

| | |
|---|---|
| `GET\|POST .../flags/{flag}/schedules`, `DELETE .../schedules/{id}` | targeting instructions to apply at `runAt` (Unix ms or RFC 3339); a timed change request where approval is required |
| `GET .../projects/{proj}/stale-flags` | temporary flags that look ready to remove, and why |
| `GET .../projects/{proj}/kill-switches?env=` | flags tagged `kill-switch`, on or off per environment |

## Organization

| | |
|---|---|
| `GET\|PATCH /orgs/{org}/settings` | `agentProductionWrites`: `changeRequest` (default) or `direct` (owners and admins) |
| `GET /orgs/{org}/export` | everything the org configured, as JSON (owners and admins) |

Members, invites, API keys, SSO, the audit log and usage are the platform's, under [Settings](/settings) and its
JSON API at `/api/orgs/{org}/...`; your agent reaches them with the MCP tools listed on [AI & MCP](/ai).