Creates a feature flag in the resolved project.
const url = 'https://api.featureflip.io/api/v1/orgs/example/projects/example/flags';const options = { method: 'POST', headers: {Authorization: '<Authorization>', 'Content-Type': 'application/json'}, body: '{"key":"new-checkout-flow","name":"New Checkout Flow","type":"Boolean","description":"Gates the redesigned checkout experience.","tags":["checkout","q3-launch"],"initialVariations":[{"key":"true","name":"Enabled","value":"true","description":"Serve the new checkout."},{"key":"false","name":"Disabled","value":"false","description":"Serve the legacy checkout."}],"clientSideVisible":false}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url https://api.featureflip.io/api/v1/orgs/example/projects/example/flags \ --header 'Authorization: <Authorization>' \ --header 'Content-Type: application/json' \ --data '{ "key": "new-checkout-flow", "name": "New Checkout Flow", "type": "Boolean", "description": "Gates the redesigned checkout experience.", "tags": [ "checkout", "q3-launch" ], "initialVariations": [ { "key": "true", "name": "Enabled", "value": "true", "description": "Serve the new checkout." }, { "key": "false", "name": "Disabled", "value": "false", "description": "Serve the legacy checkout." } ], "clientSideVisible": false }'Requires at least Member.
Type is passed through verbatim to
Type (a bare string) — an invalid value surfaces as
the handler’s own validation failure. Re-fetches via GetFeatureFlagQuery so
the response reflects exactly what was persisted (e.g. server-assigned variation ids). A flag
past the plan’s per-project cap on active flags is a 403 plan_limit_reached.
Authorizations
Section titled “ Authorizations ”Parameters
Section titled “ Parameters ”Path Parameters
Section titled “ Path Parameters ”Request Body
Section titled “ Request Body ”Request body for POST /api/v1/orgs/{org}/projects/{project}/flags. Named distinctly
from the dashboard’s own CreateFeatureFlagRequest
— see PublicCreateEnvironmentRequest for the
schemaId-collision rationale (both controllers share one Swagger document). Type
is passed through verbatim to Type, which itself takes a
bare string (Boolean/String/Number/Json) rather than the domain enum — invalid values surface
as the handler’s own validation failure, not a controller-level 400. ExpiresAtUtc is
optional and advisory; a bare date (2026-12-01) means the end of that day in UTC, and a full
timestamp is stored exactly. It must be in the future and requires the flag-expiration feature
(400 EXPIRY_IN_PAST / FEATURE_NOT_ENABLED). OwnerEmail optionally names
the owner (an active member, matched case-insensitively; requires a plan that includes flag owners,
Pro and above — 400 OWNER_NOT_MEMBER / PLAN_FEATURE_UNAVAILABLE). Omitted, the owner is the caller when
the caller is a member, so a service token creates an unowned flag. IsPermanent marks
the flag permanent (a kill switch, ops toggle or entitlement that is meant to stay; never reported for
cleanup). It requires the flag-permanence feature and can’t be combined with ExpiresAtUtc
(400 FEATURE_NOT_ENABLED / PERMANENT_FLAG_HAS_EXPIRY).
object
Allowed values: Boolean, String, Number, Json.
A variation supplied at flag-creation time. Maps 1:1 onto CreateVariationDto.
object
Example
{ "key": "new-checkout-flow", "name": "New Checkout Flow", "type": "Boolean", "description": "Gates the redesigned checkout experience.", "tags": [ "checkout", "q3-launch" ], "initialVariations": [ { "key": "true", "name": "Enabled", "value": "true", "description": "Serve the new checkout." }, { "key": "false", "name": "Disabled", "value": "false", "description": "Serve the legacy checkout." } ], "clientSideVisible": false}Request body for POST /api/v1/orgs/{org}/projects/{project}/flags. Named distinctly
from the dashboard’s own CreateFeatureFlagRequest
— see PublicCreateEnvironmentRequest for the
schemaId-collision rationale (both controllers share one Swagger document). Type
is passed through verbatim to Type, which itself takes a
bare string (Boolean/String/Number/Json) rather than the domain enum — invalid values surface
as the handler’s own validation failure, not a controller-level 400. ExpiresAtUtc is
optional and advisory; a bare date (2026-12-01) means the end of that day in UTC, and a full
timestamp is stored exactly. It must be in the future and requires the flag-expiration feature
(400 EXPIRY_IN_PAST / FEATURE_NOT_ENABLED). OwnerEmail optionally names
the owner (an active member, matched case-insensitively; requires a plan that includes flag owners,
Pro and above — 400 OWNER_NOT_MEMBER / PLAN_FEATURE_UNAVAILABLE). Omitted, the owner is the caller when
the caller is a member, so a service token creates an unowned flag. IsPermanent marks
the flag permanent (a kill switch, ops toggle or entitlement that is meant to stay; never reported for
cleanup). It requires the flag-permanence feature and can’t be combined with ExpiresAtUtc
(400 FEATURE_NOT_ENABLED / PERMANENT_FLAG_HAS_EXPIRY).
object
Allowed values: Boolean, String, Number, Json.
A variation supplied at flag-creation time. Maps 1:1 onto CreateVariationDto.
object
Example
{ "key": "new-checkout-flow", "name": "New Checkout Flow", "type": "Boolean", "description": "Gates the redesigned checkout experience.", "tags": [ "checkout", "q3-launch" ], "initialVariations": [ { "key": "true", "name": "Enabled", "value": "true", "description": "Serve the new checkout." }, { "key": "false", "name": "Disabled", "value": "false", "description": "Serve the legacy checkout." } ], "clientSideVisible": false}Request body for POST /api/v1/orgs/{org}/projects/{project}/flags. Named distinctly
from the dashboard’s own CreateFeatureFlagRequest
— see PublicCreateEnvironmentRequest for the
schemaId-collision rationale (both controllers share one Swagger document). Type
is passed through verbatim to Type, which itself takes a
bare string (Boolean/String/Number/Json) rather than the domain enum — invalid values surface
as the handler’s own validation failure, not a controller-level 400. ExpiresAtUtc is
optional and advisory; a bare date (2026-12-01) means the end of that day in UTC, and a full
timestamp is stored exactly. It must be in the future and requires the flag-expiration feature
(400 EXPIRY_IN_PAST / FEATURE_NOT_ENABLED). OwnerEmail optionally names
the owner (an active member, matched case-insensitively; requires a plan that includes flag owners,
Pro and above — 400 OWNER_NOT_MEMBER / PLAN_FEATURE_UNAVAILABLE). Omitted, the owner is the caller when
the caller is a member, so a service token creates an unowned flag. IsPermanent marks
the flag permanent (a kill switch, ops toggle or entitlement that is meant to stay; never reported for
cleanup). It requires the flag-permanence feature and can’t be combined with ExpiresAtUtc
(400 FEATURE_NOT_ENABLED / PERMANENT_FLAG_HAS_EXPIRY).
object
Allowed values: Boolean, String, Number, Json.
A variation supplied at flag-creation time. Maps 1:1 onto CreateVariationDto.
object
Example
{ "key": "new-checkout-flow", "name": "New Checkout Flow", "type": "Boolean", "description": "Gates the redesigned checkout experience.", "tags": [ "checkout", "q3-launch" ], "initialVariations": [ { "key": "true", "name": "Enabled", "value": "true", "description": "Serve the new checkout." }, { "key": "false", "name": "Disabled", "value": "false", "description": "Serve the legacy checkout." } ], "clientSideVisible": false}Responses
Section titled “ Responses ”Created
Public detail representation of a feature flag, returned by
GET .../flags/{flag} and the body of a successful create. Drops internal/dashboard-only
fields present on FeatureFlagDto — notably ProjectId (implicit in the
route) and Staleness (an internal analytics signal, not part of the public contract).
EffectiveExpiresAtUtc is when the flag counts as past expiry, from its own ExpiresAtUtc
or its project’s default lifetime; ExpirySource says which (explicit / project-default),
both null when neither applies.
object
Allowed values: Boolean, String, Number, Json.
A single variation on a flag, as returned in Variations.
object
Capability hints: which operations the authenticated caller may perform on this resource, each with allowed + optional reason. Computed from the caller’s role and the resource’s state. Injected at runtime; safe to ignore. Present only on top-level resource responses — nested occurrences (e.g. a rule inside a targeting-config response) do not carry it.
object
One entry in a resource’s _actions block: may the caller perform it, and if not, why.
object
A flag’s owner as returned on flag reads. Null means the flag is unowned.
object
Capability hints: which operations the authenticated caller may perform on this resource, each with allowed + optional reason. Computed from the caller’s role and the resource’s state. Injected at runtime; safe to ignore. Present only on top-level resource responses — nested occurrences (e.g. a rule inside a targeting-config response) do not carry it.
object
One entry in a resource’s _actions block: may the caller perform it, and if not, why.
object
Example
{ "id": "0197b6a0-5f2a-7c3e-9b4d-1e2f3a4b5c6d", "key": "new-checkout-flow", "name": "New Checkout Flow", "description": "Gates the redesigned checkout experience.", "type": "Boolean", "isArchived": false, "clientSideVisible": false, "tags": [ "checkout", "q3-launch" ], "variations": [ { "id": "0197b6a0-6a1b-7d4f-8c2e-2f3a4b5c6d7e", "key": "true", "name": "Enabled", "value": "true", "description": "Serve the new checkout." }, { "id": "0197b6a0-6a1b-7d4f-8c2e-3a4b5c6d7e8f", "key": "false", "name": "Disabled", "value": "false", "description": "Serve the legacy checkout." } ], "createdAt": "2026-06-01T12:00:00Z", "updatedAt": "2026-06-15T08:30:00Z", "isPermanent": false}Headers
Section titled “ Headers ”The maximum number of requests permitted per rate-limit window for this caller.
The number of requests remaining in the current rate-limit window.
The UTC time at which the current rate-limit window resets, as a Unix timestamp in seconds.
The request is malformed, or it carries an If-Match header, which this endpoint can’t evaluate. Send no If-Match, or *.
The frozen public-API error contract. error codes are stable snake_case strings. Wire keys
are frozen snake_case too (error, message, docs_url, fields,
retry_after) — the global camelCase naming policy would otherwise emit
docsUrl/retryAfter, breaking the published spec. !:JsonPropertyName
always wins over the policy, so these are pinned explicitly rather than relying on the
property names already being lowercase for the single-word ones.
did_you_mean, next_actions and connection_id are ADDITIVE optional keys
(null → omitted via the global DefaultIgnoreCondition = WhenWritingNull), so
pre-existing error bodies are byte-for-byte unchanged when they’re absent.
connection_id is present only on the sso_required error: the organization
requires single sign-on, and this credential didn’t come through its identity provider.
connection_id identifies which SSO connection to sign in with.
plan_limit is present only on the plan_limit_reached error: which plan cap the
request would have exceeded, so a client can branch on it without parsing message.
object
object
object
The cap behind a plan_limit_reached error. resource is the capped collection
(projects, environments, flags, webhooks, members);
plan is the plan’s wire name, as GET /api/v1/orgs/{org} returns it; max is
the cap. The environment and flag caps are per project.
object
Example
{ "error": "not_found", "message": "Flag 'new-checkout-flw' was not found in project 'checkout'.", "docs_url": "https://featureflip.io/docs/management-api/errors/not_found", "did_you_mean": [ "new-checkout-flow" ], "next_actions": [ { "method": "GET", "path": "/api/v1/orgs/acme/projects/checkout/flags" } ]}Headers
Section titled “ Headers ”The maximum number of requests permitted per rate-limit window for this caller.
The number of requests remaining in the current rate-limit window.
The UTC time at which the current rate-limit window resets, as a Unix timestamp in seconds.
Authentication is required, or the supplied API token is invalid or expired.
The frozen public-API error contract. error codes are stable snake_case strings. Wire keys
are frozen snake_case too (error, message, docs_url, fields,
retry_after) — the global camelCase naming policy would otherwise emit
docsUrl/retryAfter, breaking the published spec. !:JsonPropertyName
always wins over the policy, so these are pinned explicitly rather than relying on the
property names already being lowercase for the single-word ones.
did_you_mean, next_actions and connection_id are ADDITIVE optional keys
(null → omitted via the global DefaultIgnoreCondition = WhenWritingNull), so
pre-existing error bodies are byte-for-byte unchanged when they’re absent.
connection_id is present only on the sso_required error: the organization
requires single sign-on, and this credential didn’t come through its identity provider.
connection_id identifies which SSO connection to sign in with.
plan_limit is present only on the plan_limit_reached error: which plan cap the
request would have exceeded, so a client can branch on it without parsing message.
object
object
object
The cap behind a plan_limit_reached error. resource is the capped collection
(projects, environments, flags, webhooks, members);
plan is the plan’s wire name, as GET /api/v1/orgs/{org} returns it; max is
the cap. The environment and flag caps are per project.
object
Example
{ "error": "not_found", "message": "Flag 'new-checkout-flw' was not found in project 'checkout'.", "docs_url": "https://featureflip.io/docs/management-api/errors/not_found", "did_you_mean": [ "new-checkout-flow" ], "next_actions": [ { "method": "GET", "path": "/api/v1/orgs/acme/projects/checkout/flags" } ]}Headers
Section titled “ Headers ”The maximum number of requests permitted per rate-limit window for this caller.
The number of requests remaining in the current rate-limit window.
The UTC time at which the current rate-limit window resets, as a Unix timestamp in seconds.
Forbidden
The frozen public-API error contract. error codes are stable snake_case strings. Wire keys
are frozen snake_case too (error, message, docs_url, fields,
retry_after) — the global camelCase naming policy would otherwise emit
docsUrl/retryAfter, breaking the published spec. !:JsonPropertyName
always wins over the policy, so these are pinned explicitly rather than relying on the
property names already being lowercase for the single-word ones.
did_you_mean, next_actions and connection_id are ADDITIVE optional keys
(null → omitted via the global DefaultIgnoreCondition = WhenWritingNull), so
pre-existing error bodies are byte-for-byte unchanged when they’re absent.
connection_id is present only on the sso_required error: the organization
requires single sign-on, and this credential didn’t come through its identity provider.
connection_id identifies which SSO connection to sign in with.
plan_limit is present only on the plan_limit_reached error: which plan cap the
request would have exceeded, so a client can branch on it without parsing message.
object
object
object
The cap behind a plan_limit_reached error. resource is the capped collection
(projects, environments, flags, webhooks, members);
plan is the plan’s wire name, as GET /api/v1/orgs/{org} returns it; max is
the cap. The environment and flag caps are per project.
object
Example
{ "error": "not_found", "message": "Flag 'new-checkout-flw' was not found in project 'checkout'.", "docs_url": "https://featureflip.io/docs/management-api/errors/not_found", "did_you_mean": [ "new-checkout-flow" ], "next_actions": [ { "method": "GET", "path": "/api/v1/orgs/acme/projects/checkout/flags" } ]}Headers
Section titled “ Headers ”The maximum number of requests permitted per rate-limit window for this caller.
The number of requests remaining in the current rate-limit window.
The UTC time at which the current rate-limit window resets, as a Unix timestamp in seconds.
Not Found
The frozen public-API error contract. error codes are stable snake_case strings. Wire keys
are frozen snake_case too (error, message, docs_url, fields,
retry_after) — the global camelCase naming policy would otherwise emit
docsUrl/retryAfter, breaking the published spec. !:JsonPropertyName
always wins over the policy, so these are pinned explicitly rather than relying on the
property names already being lowercase for the single-word ones.
did_you_mean, next_actions and connection_id are ADDITIVE optional keys
(null → omitted via the global DefaultIgnoreCondition = WhenWritingNull), so
pre-existing error bodies are byte-for-byte unchanged when they’re absent.
connection_id is present only on the sso_required error: the organization
requires single sign-on, and this credential didn’t come through its identity provider.
connection_id identifies which SSO connection to sign in with.
plan_limit is present only on the plan_limit_reached error: which plan cap the
request would have exceeded, so a client can branch on it without parsing message.
object
object
object
The cap behind a plan_limit_reached error. resource is the capped collection
(projects, environments, flags, webhooks, members);
plan is the plan’s wire name, as GET /api/v1/orgs/{org} returns it; max is
the cap. The environment and flag caps are per project.
object
Example
{ "error": "not_found", "message": "Flag 'new-checkout-flw' was not found in project 'checkout'.", "docs_url": "https://featureflip.io/docs/management-api/errors/not_found", "did_you_mean": [ "new-checkout-flow" ], "next_actions": [ { "method": "GET", "path": "/api/v1/orgs/acme/projects/checkout/flags" } ]}Headers
Section titled “ Headers ”The maximum number of requests permitted per rate-limit window for this caller.
The number of requests remaining in the current rate-limit window.
The UTC time at which the current rate-limit window resets, as a Unix timestamp in seconds.
Rate limit exceeded. Retry after the interval indicated by the Retry-After header.
The frozen public-API error contract. error codes are stable snake_case strings. Wire keys
are frozen snake_case too (error, message, docs_url, fields,
retry_after) — the global camelCase naming policy would otherwise emit
docsUrl/retryAfter, breaking the published spec. !:JsonPropertyName
always wins over the policy, so these are pinned explicitly rather than relying on the
property names already being lowercase for the single-word ones.
did_you_mean, next_actions and connection_id are ADDITIVE optional keys
(null → omitted via the global DefaultIgnoreCondition = WhenWritingNull), so
pre-existing error bodies are byte-for-byte unchanged when they’re absent.
connection_id is present only on the sso_required error: the organization
requires single sign-on, and this credential didn’t come through its identity provider.
connection_id identifies which SSO connection to sign in with.
plan_limit is present only on the plan_limit_reached error: which plan cap the
request would have exceeded, so a client can branch on it without parsing message.
object
object
object
The cap behind a plan_limit_reached error. resource is the capped collection
(projects, environments, flags, webhooks, members);
plan is the plan’s wire name, as GET /api/v1/orgs/{org} returns it; max is
the cap. The environment and flag caps are per project.
object
Example
{ "error": "not_found", "message": "Flag 'new-checkout-flw' was not found in project 'checkout'.", "docs_url": "https://featureflip.io/docs/management-api/errors/not_found", "did_you_mean": [ "new-checkout-flow" ], "next_actions": [ { "method": "GET", "path": "/api/v1/orgs/acme/projects/checkout/flags" } ]}Headers
Section titled “ Headers ”Number of seconds to wait before retrying the request.
The maximum number of requests permitted per rate-limit window for this caller.
The number of requests remaining in the current rate-limit window.
The UTC time at which the current rate-limit window resets, as a Unix timestamp in seconds.