v1 API reference
The v1 API is a single HTTP endpoint served by the RestFlags edge service. It handles flag evaluation, flag metadata reads, and conversion tracking.
Base URL: https://api.restflags.com
Authentication
Every request must carry an API key as a Bearer token:
Authorization: Bearer rf_live_xxxxxxxxxxxxKeys are scoped to an environment (development, staging, production). You
may also pass an explicit environment field in a POST body — it must match the
key's environment or the request is rejected with 400 env_mismatch.
See API keys for how to create and manage keys.
Endpoint
POST https://api.restflags.com/api/v1/flags
GET https://api.restflags.com/api/v1/flagsA single polymorphic route. The request shape determines the operation:
| Body fields present | Operation |
|---|---|
flag + user | Evaluate one flag |
keys + user | Evaluate many flags |
event + user | Record a conversion |
flag only (no user) | Get one flag's metadata |
| Empty body (or GET) | List all flags' metadata |
Providing more than one of event, keys, or flag returns 400 ambiguous_request.
Evaluate one flag
POST /api/v1/flags
Request body
| Field | Type | Required | Description |
|---|---|---|---|
flag | string | yes | Flag key to evaluate |
user | object | yes | User context. Must include id; any extra fields are available to targeting rules |
environment | string | no | Override the key's environment (development, staging, production) |
Response 200
| Field | Type | Description |
|---|---|---|
value | boolean / string / number / object | Resolved flag value. Type depends on flag kind |
variant | string | Variation key that was served |
reason | string | Why this value was chosen. See Reasons |
{ "value": true, "variant": "on", "reason": "rule_match" }Code examples
curl
curl https://api.restflags.com/api/v1/flags \
-H "Authorization: Bearer rf_live_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"flag": "new-checkout",
"user": { "id": "user-123", "plan": "pro", "country": "US" }
}'Node.js
const res = await fetch("https://api.restflags.com/api/v1/flags", {
method: "POST",
headers: {
Authorization: "Bearer rf_live_xxxxxxxxxxxx",
"Content-Type": "application/json",
},
body: JSON.stringify({
flag: "new-checkout",
user: { id: "user-123", plan: "pro", country: "US" },
}),
});
const { value, variant, reason } = await res.json();Python
import requests
res = requests.post(
"https://api.restflags.com/api/v1/flags",
headers={"Authorization": "Bearer rf_live_xxxxxxxxxxxx"},
json={
"flag": "new-checkout",
"user": {"id": "user-123", "plan": "pro", "country": "US"},
},
)
data = res.json()
value = data["value"]Go
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func main() {
body, _ := json.Marshal(map[string]any{
"flag": "new-checkout",
"user": map[string]string{"id": "user-123", "plan": "pro", "country": "US"},
})
req, _ := http.NewRequest(http.MethodPost, "https://api.restflags.com/api/v1/flags", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer rf_live_xxxxxxxxxxxx")
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
fmt.Println(result["value"])
}Ruby
require "net/http"
require "json"
require "uri"
uri = URI("https://api.restflags.com/api/v1/flags")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer rf_live_xxxxxxxxxxxx"
req["Content-Type"] = "application/json"
req.body = { flag: "new-checkout", user: { id: "user-123", plan: "pro" } }.to_json
res = http.request(req)
value = JSON.parse(res.body)["value"]PHP
<?php
$ch = curl_init("https://api.restflags.com/api/v1/flags");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer rf_live_xxxxxxxxxxxx",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"flag" => "new-checkout",
"user" => ["id" => "user-123", "plan" => "pro"],
]),
]);
$data = json_decode(curl_exec($ch), true);
$value = $data["value"];Java
import java.net.URI;
import java.net.http.*;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
var mapper = new ObjectMapper();
var body = mapper.writeValueAsString(Map.of(
"flag", "new-checkout",
"user", Map.of("id", "user-123", "plan", "pro")
));
var request = HttpRequest.newBuilder()
.uri(URI.create("https://api.restflags.com/api/v1/flags"))
.header("Authorization", "Bearer rf_live_xxxxxxxxxxxx")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
var response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
var result = mapper.readValue(response.body(), Map.class);
var value = result.get("value");Rust
use serde_json::json;
#[tokio::main]
async fn main() {
let client = reqwest::Client::new();
let res = client
.post("https://api.restflags.com/api/v1/flags")
.bearer_auth("rf_live_xxxxxxxxxxxx")
.json(&json!({
"flag": "new-checkout",
"user": { "id": "user-123", "plan": "pro" }
}))
.send().await.unwrap()
.json::<serde_json::Value>().await.unwrap();
println!("{}", res["value"]);
}C#
using System.Net.Http.Json;
using System.Text.Json;
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer rf_live_xxxxxxxxxxxx");
var payload = new {
flag = "new-checkout",
user = new { id = "user-123", plan = "pro" }
};
var response = await client.PostAsJsonAsync("https://api.restflags.com/api/v1/flags", payload);
var result = await response.Content.ReadFromJsonAsync<JsonElement>();
var value = result.GetProperty("value");Evaluate many flags
POST /api/v1/flags
Up to 100 flags in a single request. Prefer this over multiple single-flag calls on page load.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
keys | string[] | yes | Flag keys to evaluate (max 100) |
user | object | yes | User context (same shape as above) |
environment | string | no | Environment override |
Response 200
A results map keyed by flag key. Each entry is an eval result, or
{ "error": "not_found" } for keys that don't exist — the overall request
still returns 200.
{
"results": {
"new-checkout": { "value": true, "variant": "on", "reason": "rule_match" },
"dark-mode": { "value": false, "variant": "off", "reason": "flag_off" },
"unknown-flag": { "error": "not_found" }
}
}Code examples
curl
curl https://api.restflags.com/api/v1/flags \
-H "Authorization: Bearer rf_live_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"keys": ["new-checkout", "dark-mode"],
"user": { "id": "user-123" }
}'Node.js
const res = await fetch("https://api.restflags.com/api/v1/flags", {
method: "POST",
headers: {
Authorization: "Bearer rf_live_xxxxxxxxxxxx",
"Content-Type": "application/json",
},
body: JSON.stringify({
keys: ["new-checkout", "dark-mode"],
user: { id: "user-123" },
}),
});
const { results } = await res.json();Python
import requests
res = requests.post(
"https://api.restflags.com/api/v1/flags",
headers={"Authorization": "Bearer rf_live_xxxxxxxxxxxx"},
json={
"keys": ["new-checkout", "dark-mode"],
"user": {"id": "user-123"},
},
)
results = res.json()["results"]Go
body, _ := json.Marshal(map[string]any{
"keys": []string{"new-checkout", "dark-mode"},
"user": map[string]string{"id": "user-123"},
})
req, _ := http.NewRequest(http.MethodPost, "https://api.restflags.com/api/v1/flags", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer rf_live_xxxxxxxxxxxx")
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
var payload map[string]any
json.NewDecoder(resp.Body).Decode(&payload)
results := payload["results"].(map[string]any)Ruby
req.body = { keys: ["new-checkout", "dark-mode"], user: { id: "user-123" } }.to_json
res = http.request(req)
results = JSON.parse(res.body)["results"]PHP
CURLOPT_POSTFIELDS => json_encode([
"keys" => ["new-checkout", "dark-mode"],
"user" => ["id" => "user-123"],
]),
// $data["results"] contains the mapJava
var body = mapper.writeValueAsString(Map.of(
"keys", List.of("new-checkout", "dark-mode"),
"user", Map.of("id", "user-123")
));
// response body: { "results": { ... } }Rust
let res = client
.post("https://api.restflags.com/api/v1/flags")
.bearer_auth("rf_live_xxxxxxxxxxxx")
.json(&json!({
"keys": ["new-checkout", "dark-mode"],
"user": { "id": "user-123" }
}))
.send().await.unwrap()
.json::<serde_json::Value>().await.unwrap();
let results = &res["results"];C#
var payload = new {
keys = new[] { "new-checkout", "dark-mode" },
user = new { id = "user-123" }
};
var response = await client.PostAsJsonAsync("https://api.restflags.com/api/v1/flags", payload);
var result = await response.Content.ReadFromJsonAsync<JsonElement>();
var results = result.GetProperty("results");Record a conversion
POST /api/v1/flags
Records a metric event. Call this when a user completes the goal you are measuring.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
event | string | yes | Metric name — must match an experiment's primary metric |
user | object | yes | Must include the same id used when evaluating the flag |
value | number | no | Optional numeric value (e.g. revenue in cents) summed into experiment results |
Response 200
{ "recorded": true }Code examples
curl
curl https://api.restflags.com/api/v1/flags \
-H "Authorization: Bearer rf_live_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"event": "purchase",
"user": { "id": "user-123" },
"value": 4999
}'Node.js
await fetch("https://api.restflags.com/api/v1/flags", {
method: "POST",
headers: {
Authorization: "Bearer rf_live_xxxxxxxxxxxx",
"Content-Type": "application/json",
},
body: JSON.stringify({
event: "purchase",
user: { id: "user-123" },
value: 4999,
}),
});Python
requests.post(
"https://api.restflags.com/api/v1/flags",
headers={"Authorization": "Bearer rf_live_xxxxxxxxxxxx"},
json={"event": "purchase", "user": {"id": "user-123"}, "value": 4999},
)Go
body, _ := json.Marshal(map[string]any{
"event": "purchase",
"user": map[string]string{"id": "user-123"},
"value": 4999,
})
req, _ := http.NewRequest(http.MethodPost, "https://api.restflags.com/api/v1/flags", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer rf_live_xxxxxxxxxxxx")
req.Header.Set("Content-Type", "application/json")
http.DefaultClient.Do(req)Ruby
req.body = { event: "purchase", user: { id: "user-123" }, value: 4999 }.to_json
http.request(req)PHP
CURLOPT_POSTFIELDS => json_encode([
"event" => "purchase",
"user" => ["id" => "user-123"],
"value" => 4999,
]),Java
var body = mapper.writeValueAsString(Map.of(
"event", "purchase",
"user", Map.of("id", "user-123"),
"value", 4999
));Rust
client
.post("https://api.restflags.com/api/v1/flags")
.bearer_auth("rf_live_xxxxxxxxxxxx")
.json(&json!({ "event": "purchase", "user": { "id": "user-123" }, "value": 4999 }))
.send().await.unwrap();C#
await client.PostAsJsonAsync("https://api.restflags.com/api/v1/flags", new {
@event = "purchase",
user = new { id = "user-123" },
value = 4999
});Get one flag's metadata
GET /api/v1/flags?flag={key}
Returns metadata for a single flag. Targeting rules and rollout configuration are never included.
Response 200
| Field | Type | Description |
|---|---|---|
key | string | Immutable flag key |
name | string | Display name |
kind | string | boolean, string, number, or json |
variations | object[] | Each variation: vkey, name, value, color |
tags | string[] | Tags attached to this flag |
archived | boolean | Whether the flag is archived |
Returns 404 flag_not_found if the key does not exist.
Code examples
curl
curl "https://api.restflags.com/api/v1/flags?flag=new-checkout" \
-H "Authorization: Bearer rf_live_xxxxxxxxxxxx"Node.js
const res = await fetch(
"https://api.restflags.com/api/v1/flags?flag=new-checkout",
{
headers: { Authorization: "Bearer rf_live_xxxxxxxxxxxx" },
},
);
const flag = await res.json();Python
res = requests.get(
"https://api.restflags.com/api/v1/flags",
params={"flag": "new-checkout"},
headers={"Authorization": "Bearer rf_live_xxxxxxxxxxxx"},
)
flag = res.json()Go
req, _ := http.NewRequest(http.MethodGet,
"https://api.restflags.com/api/v1/flags?flag=new-checkout", nil)
req.Header.Set("Authorization", "Bearer rf_live_xxxxxxxxxxxx")
resp, _ := http.DefaultClient.Do(req)
var flag map[string]any
json.NewDecoder(resp.Body).Decode(&flag)Ruby
uri = URI("https://api.restflags.com/api/v1/flags?flag=new-checkout")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer rf_live_xxxxxxxxxxxx"
res = http.request(req)
flag = JSON.parse(res.body)PHP
$ch = curl_init("https://api.restflags.com/api/v1/flags?flag=new-checkout");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer rf_live_xxxxxxxxxxxx"],
]);
$flag = json_decode(curl_exec($ch), true);Java
var request = HttpRequest.newBuilder()
.uri(URI.create("https://api.restflags.com/api/v1/flags?flag=new-checkout"))
.header("Authorization", "Bearer rf_live_xxxxxxxxxxxx")
.GET().build();
var response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
var flag = mapper.readValue(response.body(), Map.class);Rust
let flag = client
.get("https://api.restflags.com/api/v1/flags")
.query(&[("flag", "new-checkout")])
.bearer_auth("rf_live_xxxxxxxxxxxx")
.send().await.unwrap()
.json::<serde_json::Value>().await.unwrap();C#
client.DefaultRequestHeaders.Add("Authorization", "Bearer rf_live_xxxxxxxxxxxx");
var flag = await client.GetFromJsonAsync<JsonElement>(
"https://api.restflags.com/api/v1/flags?flag=new-checkout");List all flags
GET /api/v1/flags
Returns metadata for every flag in the workspace. Each entry has the same shape
as the single-flag response above, wrapped in a flags array.
{
"flags": [
{
"key": "new-checkout",
"name": "New checkout",
"kind": "boolean",
"variations": [
{ "vkey": "on", "name": "On", "value": true, "color": "green" },
{ "vkey": "off", "name": "Off", "value": false, "color": "grey" }
],
"tags": ["payments"],
"archived": false
}
]
}Reasons
The reason field in an evaluation result explains the decision:
| Value | Meaning |
|---|---|
rule_match | A targeting rule matched the user |
fallthrough | No rule matched; the rollout percentage was used |
fallthrough_off | No rule matched; the flag has no active rollout |
flag_off | The flag is disabled in this environment |
prerequisite_failed | A prerequisite flag did not serve its required variation |
Errors
All errors use the same envelope:
{
"error": { "code": "unauthorized", "message": "Invalid or missing API key." }
}| Status | Code | Meaning |
|---|---|---|
400 | ambiguous_request | More than one of event, keys, flag in the body |
400 | invalid_json | Request body is not valid JSON |
400 | missing_keys | keys array is empty |
400 | too_many_keys | keys contains more than 100 entries |
400 | missing_user | user or user.id not provided |
400 | env_mismatch | environment in body does not match the key's environment |
401 | unauthorized | Missing or invalid API key |
402 | usage_exceeded | Monthly request cap reached (free plan) |
404 | flag_not_found | Flag key does not exist |
429 | rate_limited | Per-key (10,000/s) or per-IP (50/s) rate limit exceeded |
500 | — | Internal server error |
Rate limits
Two limits apply to every request — evaluations, conversion events and
metadata reads, on both GET and POST:
- 10,000 requests per second per API key.
- 50 requests per second per client IP, as front-line abuse protection.
Exceed either and every subsequent request returns 429 rate_limited until the
bucket refills (limits refill continuously, not in fixed windows). Cache
evaluation results briefly on your side to stay well under the limits.