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:
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:read | List projects and environments. |
| flags:read | List and read flags and flag values, and evaluate a flag's resolved value. |
| flags:write | Create, update, and delete flags and flag values. |
| targets:read | List and read targets and targeting rules. |
| targets:write | Create, update, publish, and delete targets, rollouts, and targeting rules — implies targets:read. |
| audit:read | Read 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 |
|---|---|
| standard | 60 |
| elevated | 300 |
| unlimited | No 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:
{
"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:
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"key": "new-checkout",
"name": "New Checkout",
"type": "boolean",
"description": "Rolls out the redesigned checkout flow."
}
{
"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.
{
"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.
{ "status": "success" }
POST /projects/{projectKey}/flags/bulk-delete
flags:write
Delete multiple flags by key in one request — max 100 keys, no duplicates.
{ "flag_keys": ["new-checkout", "old-checkout"] }
{
"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.
{
"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.
{ "display": "true" }
{
"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.
{ "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.
{ "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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{ "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.
{ "comment": "Rolling out to 25% of staging." }
{
"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.
{ "rollout_mode": "manual", "rollout_percentage": 10 }
{
"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.
{ "rollout_percentage": 25 }
{
"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.
{
"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.
{
"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.
{
"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.
{
"value_ulid": "val_01hzy9m2qj1q1jz3sd4xg9n1vb",
"description": "US enterprise accounts",
"criteria": {
"selector": "attribute",
"sub_selector": "country",
"comparer": "equal",
"values": ["US"]
}
}
{
"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.
{
"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.
{ "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.
{
"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.
{
"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.
{
"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.