There are 3 steps to implement feature flags using the PostHog API:
flags is the endpoint used to determine if a given flag is enabled for a certain user or not.
# Basic request (flags only)
curl -v -L --header "Content-Type: application/json" -d ' {
"api_key": "<ph_project_token>",
"distinct_id": "distinct_id_of_your_user",
"groups" : {
"group_type": "group_id"
}
}' "<ph_client_api_host>/flags?v=2"
# With configuration (flags + PostHog config)
curl -v -L --header "Content-Type: application/json" -d ' {
"api_key": "<ph_project_token>",
"distinct_id": "distinct_id_of_your_user",
"groups" : {
"group_type": "group_id"
}
}' "<ph_client_api_host>/flags?v=2&config=true"import requests
import json
# Basic request (flags only)
url = "<ph_client_api_host>/flags?v=2"
headers = {
"Content-Type": "application/json"
}
payload = {
"api_key": "<ph_project_token>",
"distinct_id": "user distinct id",
"groups": {
"group_type": "group_id"
}
}
response = requests.post(url, headers=headers, data=json.dumps(payload))
print(response.json())
# With configuration (flags + PostHog config)
url_with_config = "<ph_client_api_host>/flags?v=2&config=true"
response_with_config = requests.post(url_with_config, headers=headers, data=json.dumps(payload))
print(response_with_config.json())import fetch from "node-fetch";
async function sendFlagsRequest() {
const headers = {
"Content-Type": "application/json",
};
const payload = {
api_key: "<ph_project_token>",
distinct_id: "user distinct id",
groups: {
group_type: "group_id",
},
};
// Basic request (flags only)
const url = "<ph_client_api_host>/flags?v=2";
const response = await fetch(url, {
method: "POST",
headers: headers,
body: JSON.stringify(payload),
});
const data = await response.json();
console.log(data);
// With configuration (flags + PostHog config)
const urlWithConfig = "<ph_client_api_host>/flags?v=2&config=true";
const responseWithConfig = await fetch(urlWithConfig, {
method: "POST",
headers: headers,
body: JSON.stringify(payload),
});
const dataWithConfig = await responseWithConfig.json();
console.log(dataWithConfig);
}
sendFlagsRequest();Note: The
groupskey is only required for group-based feature flags. If you use it, replacegroup_typeandgroup_idwith the values for your group such ascompany: "Twitter".
When making direct API calls to the /flags endpoint, you can control which flags are evaluated using evaluation context tags and runtime filtering.
To filter flags by evaluation context, include the evaluation_contexts field in your request body:
Note: The legacy parameter
evaluation_environmentsis also supported for backward compatibility.
curl -v -L --header "Content-Type: application/json" -d ' {
"api_key": "<ph_project_token>",
"distinct_id": "distinct_id_of_your_user",
"evaluation_contexts": ["production", "web"]
}' "<ph_client_api_host>/flags?v=2"import requests
import json
url = "<ph_client_api_host>/flags?v=2"
headers = {
"Content-Type": "application/json"
}
payload = {
"api_key": "<ph_project_token>",
"distinct_id": "user distinct id",
"evaluation_contexts": ["production", "web"]
}
response = requests.post(url, headers=headers, data=json.dumps(payload))
print(response.json())const response = await fetch("<ph_client_api_host>/flags?v=2", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
api_key: "<ph_project_token>",
distinct_id: "user-distinct-id",
evaluation_contexts: ["production", "web"]
}),
});
const data = await response.json();Only flags where at least one evaluation tag matches (or flags with no tags at all) will be returned. For example:
- Flag with evaluation context tags
["production", "api", "backend"]+ request with["production", "web"]= ✅ Flag evaluates ("production" matches) - Flag with evaluation context tags
["staging", "api"]+ request with["production", "web"]= ❌ Flag doesn't evaluate (no tags match) - Flag with evaluation context tags
["web", "mobile"]+ request with["production", "web"]= ✅ Flag evaluates ("web" matches) - Flag with no evaluation context tags = ✅ Always evaluates (backward compatibility)
Evaluation runtime (server vs. client) is automatically detected based on your request headers and user-agent. This determines which flags are available based on their runtime setting (server-only, client-only, or all).
How runtime is detected:
-
User-Agent patterns - The system analyzes the User-Agent header:
- Client-side patterns:
Mozilla/,Chrome/,Safari/,Firefox/,Edge/(browsers), or mobile SDKs likeposthog-android/,posthog-ios/,posthog-react-native/,posthog-flutter/ - Server-side patterns:
posthog-python/,posthog-ruby/,posthog-php/,posthog-java/,posthog-go/,posthog-node/,posthog-dotnet/,posthog-elixir/,python-requests/,curl/
- Client-side patterns:
-
Browser-specific headers - Presence of these headers indicates client-side:
OriginheaderRefererheaderSec-Fetch-ModeheaderSec-Fetch-Siteheader
-
Default behavior - If runtime can't be determined, the system includes flags with no runtime requirement and those set to "all"
Examples of runtime detection:
// Browser fetch - Detected as CLIENT runtime
// Will receive: client-only flags + "all" flags
// Won't receive: server-only flags
const response = await fetch("<ph_client_api_host>/flags?v=2", {
method: "POST",
headers: {
"Content-Type": "application/json",
// Browser automatically adds Origin, Referer, Sec-Fetch-* headers
},
body: JSON.stringify({
api_key: "<ph_project_token>",
distinct_id: "user-id"
})
});# Python requests - Detected as SERVER runtime
# Will receive: server-only flags + "all" flags
# Won't receive: client-only flags
import requests
response = requests.post(
"<ph_client_api_host>/flags?v=2",
json={
"api_key": "<ph_project_token>",
"distinct_id": "user-id"
}
# python-requests/ in User-Agent indicates server-side
)# curl - Detected as SERVER runtime
# Will receive: server-only flags + "all" flags
# Won't receive: client-only flags
curl -v -L --header "Content-Type: application/json" -d '{
"api_key": "<ph_project_token>",
"distinct_id": "user-id"
}' "<ph_client_api_host>/flags?v=2"
# curl/ in User-Agent indicates server-side// Node.js with custom User-Agent - Control runtime detection
const response = await fetch("<ph_client_api_host>/flags?v=2", {
method: "POST",
headers: {
"Content-Type": "application/json",
"User-Agent": "posthog-node/3.0.0" // Explicitly indicates server-side
},
body: JSON.stringify({
api_key: "<ph_project_token>",
distinct_id: "user-id"
})
});Both features work together as sequential filters:
// Example: Production web client
const response = await fetch("<ph_client_api_host>/flags?v=2", {
method: "POST",
headers: {
"Content-Type": "application/json",
// Browser headers will trigger client runtime detection
},
body: JSON.stringify({
api_key: "<ph_project_token>",
distinct_id: "user-id",
evaluation_contexts: ["production", "web"]
})
});
// This request will only receive flags that:
// 1. Have runtime set to "client" OR "all" (due to browser headers)
// AND
// 2. Have evaluation context tags matching "production" OR "web" (or no tags)
// Note: You can also use the legacy "evaluation_environments" parameterThis allows precise control over which flags are evaluated in different contexts, helping optimize costs and improve security by ensuring flags only evaluate where intended.
The response varies depending on whether you include the config=true query parameter:
Use this endpoint when you only need to evaluate feature flags. It returns a response with just the flag evaluation results.
Note: If a feature flag is associated with an experiment that has a holdout group, users in the holdout receive a variant value in the format
holdout-{holdout_id}(e.g.,holdout-727). You can detect holdout users by checking if the variant starts withholdout-.
{
"flags": {
"my-awesome-flag": {
"key": "my-awesome-flag",
"enabled": true,
"reason": {
"code": "condition_match",
"condition_index": 0,
"description": "Condition set 1 matched"
},
"metadata": {
"id": 1,
"version": 1,
"payload": "{\"example\": \"json\", \"payload\": \"value\"}"
}
},
"my-multivariate-flag" :{
"key":"my-multivariate-flag",
"enabled": true,
"variant": "some-string-value",
"reason": {
"code": "condition_match",
"condition_index": 1,
"description": "Condition set 2 matched"
},
"metadata": {
"id": 2,
"version": 42,
}
},
"flag-thats-not-on": {
"key": "flag-thats-not-on",
"enabled": false,
"reason": {
"code": "no_condition_match",
"condition_index": 0,
"description": "No condition sets matched"
},
"metadata": {
"id": 3,
"version": 1
}
}
},
"errorsWhileComputingFlags": false,
"requestId": "550e8400-e29b-41d4-a716-446655440000"
}Use this endpoint when you need both feature flag evaluation and PostHog configuration information (useful for client-side SDKs that need to initialize PostHog):
{
"config": {
"enable_collect_everything": true
},
"toolbarParams": {},
"errorsWhileComputingFlags": false,
"isAuthenticated": false,
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"supportedCompression": [
"gzip",
"lz64"
],
"flags": {
"my-awesome-flag": {
"key": "my-awesome-flag",
"enabled": true,
"reason": {
"code": "condition_match",
"condition_index": 0,
"description": "Condition set 1 matched"
},
"metadata": {
"id": 1,
"version": 1,
"payload": "{\"example\": \"json\", \"payload\": \"value\"}"
}
},
"my-multivariate-flag" :{
"key":"my-multivariate-flag",
"enabled": true,
"variant": "some-string-value",
"reason": {
"code": "condition_match",
"condition_index": 1,
"description": "Condition set 2 matched"
},
"metadata": {
"id": 2,
"version": 42,
}
},
"flag-thats-not-on": {
"key": "flag-thats-not-on",
"enabled": false,
"reason": {
"code": "no_condition_match",
"condition_index": 0,
"description": "No condition sets matched"
},
"metadata": {
"id": 3,
"version": 1
}
}
}
}Note:
errorsWhileComputingFlagswill returntrueif we didn't manage to compute some flags (for example, if there's an ongoing incident involving flag evaluation).This enables partial updates to currently active flags in your clients.
If your organization exceeds its feature flag quota, the /flags endpoint will return a modified response with quotaLimited.
For basic response (/flags?v=2):
{
"flags": {},
"errorsWhileComputingFlags": false,
"quotaLimited": ["feature_flags"],
"requestId": "d4d89b14-9619-4627-adf2-01b761691c2e"
}For full response with configuration (/flags?v=2&config=true):
{
"config": {
"enable_collect_everything": true
},
"toolbarParams": {},
"isAuthenticated": false,
"supportedCompression": [
"gzip",
"lz64"
],
"flags": {},
"errorsWhileComputingFlags": false,
"quotaLimited": ["feature_flags"],
"requestId": "d4d89b14-9619-4627-adf2-01b761691c2e"
// ... other fields, not relevant to feature flags
}When you receive a response with quotaLimited containing "feature_flags", it means:
- Your feature flag evaluations have been temporarily paused because you've exceeded your feature flag quota
- If you want to continue evaluating feature flags, you can increase your quota in your billing settings under Feature flags & Experiments or contact support
import IncludePropertyInEvents from "./include-feature-flag-property-in-backend-events.mdx"
To do this, include the $feature/feature_flag_name property in your event:
curl -v -L --header "Content-Type: application/json" -d ' {
"api_key": "<ph_project_token>",
"event": "your_event_name",
"distinct_id": "distinct_id_of_your_user",
"properties": {
"$feature/feature-flag-key": "variant-key" # Replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant
}
}' <ph_client_api_host>/i/v0/e/ import requests
import json
url = "<ph_client_api_host>/i/v0/e/"
headers = {
"Content-Type": "application/json"
}
payload = {
"api_key": "<ph_project_token>",
"event": "your_event_name",
"distinct_id": "distinct_id_of_your_user",
"properties": {
"$feature/feature-flag-key": "variant-key" # Replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant
}
}
response = requests.post(url, headers=headers, data=json.dumps(payload))
print(response)To track usage of your feature flag and view related analytics in PostHog, submit the $feature_flag_called event whenever you check a feature flag value in your code.
You need to include two properties with this event:
$feature_flag_response: This is the name of the variant the user has been assigned to e.g., "control" or "test"$feature_flag: This is the key of the feature flag in your experiment.
curl -v -L --header "Content-Type: application/json" -d ' {
"api_key": "<ph_project_token>",
"event": "$feature_flag_called",
"distinct_id": "distinct_id_of_your_user",
"properties": {
"$feature_flag": "feature-flag-key",
"$feature_flag_response": "variant-name"
}
}' <ph_client_api_host>/i/v0/e/ import requests
import json
url = "<ph_client_api_host>/i/v0/e/"
headers = {
"Content-Type": "application/json"
}
payload = {
"api_key": "<ph_project_token>",
"event": "feature_flag_called",
"distinct_id": "distinct_id_of_your_user",
"properties": {
"$feature_flag": "feature-flag-key",
"$feature_flag_response": "variant-name"
}
}
response = requests.post(url, headers=headers, data=json.dumps(payload))
print(response)import APIOverrideServerProperties from './override-server-properties/api.mdx'