APIs & automation

Management API

Create and update flags, targets, and targeting rules, and read the audit trail programmatically with scoped Bearer tokens.

Overview

The Management API lets you create and update feature flags, read and roll out targets, manage targeting rules, and read your account's audit trail — all at https://api.zenmanage.com under the /management/v1 path.

This is a distinct API from the public evaluation API: it authenticates with Bearer tokens instead of platform keys, and it manages configuration rather than evaluating flags for a context. Use it to build internal tooling, CI/CD gates, or automation around your flag configuration.

Prerequisites

  • A personal access token or a management API token, with whichever scopes your integration needs.
  • The project (and environment, for target/rule endpoints) key you're operating on.
  • A plan for handling 429s and 422s — write traffic should back off and re-validate rather than retry blindly.

Connecting an AI agent?

Skip the raw HTTP calls

The hosted Zenmanage MCP server wraps this API in tools an AI assistant can call directly — reads and writes, one tool per endpoint, with the same token and scopes described below.

Authentication and scopes

Every request must include a token as a Bearer token:

Authorization header http

GET /management/v1/projects/your-project/flags HTTP/1.1
Host: api.zenmanage.com
Authorization: Bearer mgt_your_token_here
  

A personal access token (pat_...) has no scopes — it can call anything your own account access allows. A management API token (mgt_...) is scoped explicitly. A token missing the scope an operation requires gets a 403, even if the token itself is valid.

Scope Grants
projects:readList projects and environments.
flags:readList and read flags and flag values, and evaluate a flag's resolved value.
flags:writeCreate, update, and delete flags and flag values.
targets:readList and read targets and targeting rules.
targets:writeCreate, update, publish, and delete targets, rollouts, and targeting rules — implies targets:read.
audit:readRead the account and public API audit trails.

A config:write scope also exists and can be granted, but doesn't back any endpoint yet.

Rate limits

Each token is rate limited per minute; the limit depends on the token's tier.

Tier Requests / minute
standard60
elevated300
unlimitedNo enforced ceiling

Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers. Exceeding the limit returns a 429 with a Retry-After header and body {"status":"error","message":"Rate limit exceeded."}. Contact hello@zenmanage.com if your token needs a higher tier.

Error envelope

Most errors — auth failures, missing scopes, not-found resources, rate limits — share one envelope:

Standard error json

{
  "status": "error",
  "message": "Flag not found."
}
  

A missing-scope 403 adds a required_scope field alongside status/message.

422s on flag and targeting-rule bodies are the exception — those are request-validation failures, and instead return Laravel's field-keyed shape:

Validation error json

{
  "message": "The key field is required.",
  "errors": {
    "key": ["The key field is required."]
  }
}
  

Two operations don't follow that split: audit log query-parameter errors always use the standard envelope, and rollout updates can return either shape depending on what failed — a bad rollout_percentage uses the field-keyed shape, while "target isn't published" or "target has no active rollout" use the standard one.

Idempotency keys

POST and PUT requests accept an optional Idempotency-Key header, up to 255 characters. Replaying the same key with the same request body and token returns the original response instead of repeating the operation, marked with X-Idempotent-Replayed: true. Replays are honored for 24 hours; only successful and client-error responses are cached, so a 500 is never replayed.

Pagination

The flags list, audit logs, and projects/environments lists are cursor-paginated: pass the previous response's meta.next_cursor as the cursor query parameter to fetch the next page. A null cursor means there are no more results. per_page defaults to 25 and is capped at 100. Targets and targeting rules are returned as full, unpaginated arrays — each is a bounded set scoped to one target.

OpenAPI specification

Zenmanage publishes an OpenAPI v3 document for the Management API at https://api.zenmanage.com/management/v1/api-docs. Use it for client generation, schema validation, and endpoint exploration. This page mirrors that spec — if the two ever disagree, treat the spec as authoritative.

Prefer trying it interactively? The Swagger UI explorer opens with the Management API spec selected — use the Authorize button to set a Bearer token and try-it against real endpoints. It also covers the Public API, one tab over.

Endpoints

All paths are relative to https://api.zenmanage.com/management/v1. Target and target-rule paths are all prefixed with /projects/{projectKey}/flags/{flagKey}/environments/{environmentKey}, shortened to ... below. Click an endpoint to see its scope, description, and an example request/response.

Projects

GET /projects

projects:read

List projects the token has access to, paginated.

200 response json

{
  "status": "success",
  "data": [
    { "ulid": "prj_01hqz3k1e9v8j2n8k9v8j2n8k9", "key": "your-project", "name": "My App", "description": "The main product.", "created_at": "2026-07-28T12:00:00+00:00", "updated_at": "2026-07-28T12:00:00+00:00" }
  ],
  "meta": { "per_page": 25, "next_cursor": null, "prev_cursor": null }
}
        
GET /projects/{projectKey}/environments

projects:read

List environments for a project, paginated.

200 response json

{
  "status": "success",
  "data": [
    { "ulid": "env_01hqz3k1e9v8j2n8k9v8j2n8k9", "key": "staging", "name": "Staging", "description": null, "color": "#f59e0b", "comments_required": false, "confirmation_required": false, "created_at": "2026-07-28T12:00:00+00:00", "updated_at": "2026-07-28T12:00:00+00:00" }
  ],
  "meta": { "per_page": 25, "next_cursor": null, "prev_cursor": null }
}
        

Flags

GET /projects/{projectKey}/flags

flags:read

List flags in a project, paginated. Supports type and search filters.

200 response json

{
  "status": "success",
  "data": [
    { "ulid": "flg_01hzy9m2qj1q1jz3sd4xg9n1vb", "key": "new-checkout", "name": "New Checkout", "type": "boolean", "description": null, "permanent": false, "include_in_mobile": true, "include_in_client": true, "created_at": "2026-07-28T12:00:00+00:00", "updated_at": "2026-07-28T12:00:00+00:00" }
  ],
  "meta": { "per_page": 25, "next_cursor": null, "prev_cursor": null }
}
        
GET /projects/{projectKey}/flags/{flagKey}

flags:read

Read a single flag.

200 response json

{
  "status": "success",
  "data": {
    "ulid": "flg_01hzy9m2qj1q1jz3sd4xg9n1vb",
    "key": "new-checkout",
    "name": "New Checkout",
    "type": "boolean",
    "description": "Rolls out the redesigned checkout flow.",
    "permanent": false,
    "include_in_mobile": true,
    "include_in_client": true,
    "created_at": "2026-07-28T12:00:00+00:00",
    "updated_at": "2026-07-28T12:00:00+00:00"
  }
}
        
GET .../flags/{flagKey}/environments/{environmentKey}/evaluate

flags:read

Evaluate a flag's current resolved value in an environment — the same way a live SDK client would see it, accounting for rollout state and default targeting.

200 response json

{
  "status": "success",
  "data": {
    "key": "new-checkout",
    "name": "New Checkout",
    "type": "boolean",
    "value": false,
    "resolved_by": "rollout"
  }
}
        
POST /projects/{projectKey}/flags

flags:write

Create a flag. key must be lowercase, unique within the project, and can't be changed afterward — nor can type. include_in_mobile/include_in_client both default to true.

Request body json

{
  "key": "new-checkout",
  "name": "New Checkout",
  "type": "boolean",
  "description": "Rolls out the redesigned checkout flow."
}
        
201 response json

{
  "status": "success",
  "data": {
    "ulid": "flg_01hzy9m2qj1q1jz3sd4xg9n1vb",
    "key": "new-checkout",
    "name": "New Checkout",
    "type": "boolean",
    "description": "Rolls out the redesigned checkout flow.",
    "permanent": false,
    "include_in_mobile": true,
    "include_in_client": true,
    "created_at": "2026-07-28T12:00:00+00:00",
    "updated_at": "2026-07-28T12:00:00+00:00"
  }
}
          
PUT /projects/{projectKey}/flags/{flagKey}

flags:write

Update a flag's name, description, permanence, and inclusion settings. name and permanent are required on every call, even if unchanged.

Request body json

{
  "name": "New Checkout",
  "description": "Rolls out the redesigned checkout flow.",
  "permanent": false,
  "include_in_mobile": true,
  "include_in_client": true
}
        
DELETE /projects/{projectKey}/flags/{flagKey}

flags:write

Delete a flag and its targeting configuration. This cannot be undone.

200 response json

{ "status": "success" }
        
POST /projects/{projectKey}/flags/bulk-delete

flags:write

Delete multiple flags by key in one request — max 100 keys, no duplicates.

Request body json

{ "flag_keys": ["new-checkout", "old-checkout"] }
        
200 response json

{
  "status": "success",
  "data": {
    "deleted": ["new-checkout"],
    "not_found": ["old-checkout"]
  }
}
          

Flag values

GET /projects/{projectKey}/flags/{flagKey}/values

flags:read

List all values defined for a flag. Not paginated — each flag has a bounded set of values.

200 response json

{
  "status": "success",
  "data": [
    { "ulid": "val_01hzy9m2qj1q1jz3sd4xg9n1vb", "display": "true", "value": true, "default": true, "created_at": "2026-07-28T12:00:00+00:00", "updated_at": "2026-07-28T12:00:00+00:00" },
    { "ulid": "val_01hzy9m2qj1q1jz3sd4xg9n1vc", "display": "false", "value": false, "default": false, "created_at": "2026-07-28T12:00:00+00:00", "updated_at": "2026-07-28T12:00:00+00:00" }
  ]
}
        
POST /projects/{projectKey}/flags/{flagKey}/values

flags:write

Create a new value for a flag. display must match the flag's type: "true"/"false" for boolean, a numeric string for number, any string for string.

Request body json

{ "display": "true" }
        
201 response json

{
  "status": "success",
  "data": {
    "ulid": "val_01hzy9m2qj1q1jz3sd4xg9n1vb",
    "display": "true",
    "value": true,
    "default": false,
    "created_at": "2026-07-28T12:00:00+00:00",
    "updated_at": "2026-07-28T12:00:00+00:00"
  }
}
          
PUT .../flags/{flagKey}/values/{valueUlid}

flags:write

Replace a flag value's display label.

Request body json

{ "display": "false" }
        
DELETE .../flags/{flagKey}/values/{valueUlid}

flags:write

Delete a flag value. Fails if it's currently published to an environment, referenced by a targeting rule, or the flag's default.

200 response json

{ "status": "success" }
        
PATCH .../values/{valueUlid}/default

flags:write

Set a flag value as the flag's default, clearing the default flag on any other value for the flag.

200 response json

{
  "status": "success",
  "data": {
    "ulid": "val_01hzy9m2qj1q1jz3sd4xg9n1vb",
    "display": "true",
    "value": true,
    "default": true,
    "created_at": "2026-07-28T12:00:00+00:00",
    "updated_at": "2026-07-28T12:00:00+00:00"
  }
}
        

Targets

GET .../targets

targets:read

List all targets for a flag/environment, newest first. Not paginated.

200 response json

{
  "status": "success",
  "data": [
    {
      "ulid": "tgt_01hzy9m2qj1q1jz3sd4xg9n1vb",
      "flag_ulid": "flg_01hzy9m2qj1q1jz3sd4xg9n1vb",
      "environment_ulid": "env_01hqz3k1e9v8j2n8k9v8j2n8k9",
      "value_ulid": "val_01hzy9m2qj1q1jz3sd4xg9n1vc",
      "status": "published",
      "comment": null,
      "rollout_percentage": 25,
      "rollout_mode": "manual",
      "rollout_status": "active",
      "published_at": "2026-07-28T12:00:00+00:00",
      "created_at": "2026-07-28T12:00:00+00:00",
      "updated_at": "2026-07-28T12:00:00+00:00"
    }
  ]
}
        
GET .../targets/{targetUlid}

targets:read

Read a single target by ULID.

200 response json

{
  "status": "success",
  "data": {
    "ulid": "tgt_01hzy9m2qj1q1jz3sd4xg9n1vb",
    "flag_ulid": "flg_01hzy9m2qj1q1jz3sd4xg9n1vb",
    "environment_ulid": "env_01hqz3k1e9v8j2n8k9v8j2n8k9",
    "value_ulid": "val_01hzy9m2qj1q1jz3sd4xg9n1vc",
    "status": "published",
    "comment": null,
    "rollout_percentage": 25,
    "rollout_mode": "manual",
    "rollout_status": "active",
    "published_at": "2026-07-28T12:00:00+00:00",
    "created_at": "2026-07-28T12:00:00+00:00",
    "updated_at": "2026-07-28T12:00:00+00:00"
  }
}
        
POST .../targets

targets:write

Start a new draft target by cloning the currently-published target's rules, criteria, and value links. No request body.

201 response json

{
  "status": "success",
  "data": {
    "ulid": "tgt_01hzy9m2qj1q1jz3sd4xg9n1vc",
    "flag_ulid": "flg_01hzy9m2qj1q1jz3sd4xg9n1vb",
    "environment_ulid": "env_01hqz3k1e9v8j2n8k9v8j2n8k9",
    "value_ulid": "val_01hzy9m2qj1q1jz3sd4xg9n1vc",
    "status": "draft",
    "comment": null,
    "created_at": "2026-07-28T12:10:00+00:00",
    "updated_at": "2026-07-28T12:10:00+00:00"
  }
}
        
PUT .../targets/{targetUlid}

targets:write

Edit a draft target's base/fallthrough value and/or comment before publishing. At least one field must be provided.

Request body json

{
  "value_ulid": "val_01hzy9m2qj1q1jz3sd4xg9n1vb",
  "comment": "Enabling for 25% of staging traffic."
}
        
DELETE .../targets/{targetUlid}

targets:write

Cancel a draft or scheduled target without publishing it. This cannot be undone.

200 response json

{ "status": "success" }
        
PUT .../targets/{targetUlid}/publish

targets:write

Publish a draft or scheduled target, replacing the currently-served value. Provide scheduled_at to schedule a future publish instead. If the environment requires a typed confirmation, this request is rejected with a 422 — the Management API has no way to satisfy that step.

Request body json

{ "comment": "Rolling out to 25% of staging." }
        
200 response json

{
  "status": "success",
  "data": {
    "ulid": "tgt_01hzy9m2qj1q1jz3sd4xg9n1vc",
    "status": "published",
    "published_at": "2026-07-28T12:15:00+00:00"
  }
}
          
POST .../targets/{targetUlid}/rollout

targets:write

Start a percentage-based rollout on a draft target. The target must not already have a rollout configured. rollout_percentage is required in manual mode and prohibited in automatic mode.

Request body json

{ "rollout_mode": "manual", "rollout_percentage": 10 }
        
200 response json

{
  "status": "success",
  "data": {
    "ulid": "tgt_01hzy9m2qj1q1jz3sd4xg9n1vc",
    "rollout_mode": "manual",
    "rollout_percentage": 10,
    "rollout_status": "active"
  }
}
          
PUT .../targets/{targetUlid}/rollout

targets:write

Update a published target's active rollout percentage, or pause/resume it. Provide exactly one of action or rollout_percentage — updating the percentage additionally requires manual mode.

Request body json

{ "rollout_percentage": 25 }
        
422 — target not published json

{
  "status": "error",
  "message": "Rollout can only be updated on published targets."
}
          
DELETE .../targets/{targetUlid}/rollout

targets:write

Remove all rollout configuration from a target, reverting it to a plain base/fallthrough value. The target must have a rollout configured.

200 response json

{
  "status": "success",
  "data": {
    "ulid": "tgt_01hzy9m2qj1q1jz3sd4xg9n1vc",
    "rollout_percentage": null,
    "rollout_mode": null,
    "rollout_status": null
  }
}
        
POST .../targets/{targetUlid}/rollout/complete

targets:write

Complete a target's rollout immediately, setting it to 100% and expiring the previously-published target(s) for the same flag/environment. No request body.

200 response json

{
  "status": "success",
  "data": {
    "ulid": "tgt_01hzy9m2qj1q1jz3sd4xg9n1vc",
    "rollout_percentage": 100,
    "rollout_status": "completed"
  }
}
        

Targeting rules

GET .../targets/{targetUlid}/rules

targets:read

List a target's targeting rules, in evaluation order. Not paginated.

200 response json

{
  "status": "success",
  "data": [
    {
      "ulid": "rul_01hzy9m2qj1q1jz3sd4xg9n1vb",
      "target_ulid": "tgt_01hzy9m2qj1q1jz3sd4xg9n1vb",
      "value_ulid": "val_01hzy9m2qj1q1jz3sd4xg9n1vb",
      "description": "US enterprise accounts",
      "position": 0,
      "criteria": {
        "data": [
          { "ulid": "crt_01hzy9m2qj1q1jz3sd4xg9n1vb", "selector": "attribute", "sub_selector": "country", "comparer": "equal", "position": 0, "values": { "data": [{ "value": "US" }] } }
        ]
      },
      "created_at": "2026-07-28T12:00:00+00:00",
      "updated_at": "2026-07-28T12:00:00+00:00"
    }
  ]
}
        
POST .../targets/{targetUlid}/rules

targets:write

Create a targeting rule, appended after the target's existing rules. comparer depends on selector: context/segment only accept equal, notequal, in, notin; attribute additionally accepts gt, gte, lt, lte, isnull, notnull, contains, notcontains, startswith, endswith, notstartswith, notendswith. values is required (min 1) unless the comparer is isnull/notnull.

Request body json

{
  "value_ulid": "val_01hzy9m2qj1q1jz3sd4xg9n1vb",
  "description": "US enterprise accounts",
  "criteria": {
    "selector": "attribute",
    "sub_selector": "country",
    "comparer": "equal",
    "values": ["US"]
  }
}
        
201 response json

{
  "status": "success",
  "data": {
    "ulid": "rul_01hzy9m2qj1q1jz3sd4xg9n1vb",
    "target_ulid": "tgt_01hzy9m2qj1q1jz3sd4xg9n1vb",
    "value_ulid": "val_01hzy9m2qj1q1jz3sd4xg9n1vb",
    "description": "US enterprise accounts",
    "position": 0,
    "created_at": "2026-07-28T12:00:00+00:00",
    "updated_at": "2026-07-28T12:00:00+00:00"
  }
}
          
PUT .../targets/{targetUlid}/rules/{ruleUlid}

targets:write

Replace a targeting rule's value, description, and criteria in full — not a partial patch, so pass every field even for a small change.

Request body json

{
  "value_ulid": "val_01hzy9m2qj1q1jz3sd4xg9n1vb",
  "description": "US and Canada enterprise accounts",
  "criteria": {
    "selector": "attribute",
    "sub_selector": "country",
    "comparer": "in",
    "values": ["US", "CA"]
  }
}
        
DELETE .../targets/{targetUlid}/rules/{ruleUlid}

targets:write

Delete a targeting rule. This cannot be undone.

200 response json

{ "status": "success" }
        
PUT .../targets/{targetUlid}/rules-order

targets:write

Set the evaluation order of a target's rules — lowest order is matched first. Rules not included keep their current position; rules belonging to another target are ignored.

Request body json

{
  "rules": [
    { "ulid": "rul_01hzy9m2qj1q1jz3sd4xg9n1vc", "order": 0 },
    { "ulid": "rul_01hzy9m2qj1q1jz3sd4xg9n1vb", "order": 1 }
  ]
}
        

Audit logs

GET /audit-logs

audit:read

List Management API audit log entries for the account, newest first by default, paginated. Filterable by from/to, result, subject_type/subject_ulid, management_token_ulid, and actor_ulid. action filtering currently only covers auth and flag actions — target/target-rule actions still appear in unfiltered results.

200 response json

{
  "status": "success",
  "data": [
    {
      "ulid": "aud_01hzy9m2qj1q1jz3sd4xg9n1vb",
      "actor": { "type": "user", "ulid": "usr_...", "name": "Jane Doe", "email": "jane@example.com" },
      "token": { "ulid": "mgt_...", "name": "CI deploy token" },
      "http_method": "PUT",
      "http_path": "/management/v1/projects/your-project/flags/new-checkout/environments/staging/targets/.../rollout",
      "action": "target.rollout_updated",
      "subject_type": "target",
      "subject_ulid": "tgt_...",
      "subject_label": "new-checkout / staging",
      "result": "success",
      "status_code": 200,
      "created_at": "2026-07-28T12:05:00+00:00"
    }
  ],
  "meta": { "per_page": 25, "sort": "created_at_desc", "next_cursor": null }
}
        
GET /api-public-audit-logs

audit:read

List public (SDK/client-facing) API audit log entries for the account, newest first by default, paginated and filterable the same way as the Management API audit log.

200 response json

{
  "status": "success",
  "data": [
    {
      "ulid": "aud_01hzy9m2qj1q1jz3sd4xg9n1vc",
      "actor_type": "sdk_key",
      "environment": { "ulid": "env_01hqz3k1e9v8j2n8k9v8j2n8k9", "key": "staging" },
      "action": "flag.evaluated",
      "subject_type": "flag",
      "subject_ulid": "flg_01hzy9m2qj1q1jz3sd4xg9n1vb",
      "subject_label": "new-checkout",
      "result": "success",
      "status_code": 200,
      "created_at": "2026-07-28T12:05:00+00:00"
    }
  ],
  "meta": { "per_page": 25, "sort": "created_at_desc", "next_cursor": null }
}
        

Common errors

400 Bad Request: the Idempotency-Key header exceeds 255 characters.

401 Unauthorized: the Bearer token is missing, invalid, revoked, or expired.

403 Forbidden: the token is valid but lacks the required scope, or lacks a grant for the project/environment.

404 Not Found: the project, environment, flag, target, or rule in the path doesn't exist, or the token has no grant for it.

422 Unprocessable Entity: request body or query parameters failed validation — see the error envelope section above for the exact shape.

429 Too Many Requests: the token's rate limit was exceeded — back off using the Retry-After header.