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_xxxxxxxxxxxx

Keys 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/flags

A single polymorphic route. The request shape determines the operation:

Body fields presentOperation
flag + userEvaluate one flag
keys + userEvaluate many flags
event + userRecord 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

FieldTypeRequiredDescription
flagstringyesFlag key to evaluate
userobjectyesUser context. Must include id; any extra fields are available to targeting rules
environmentstringnoOverride the key's environment (development, staging, production)

Response 200

FieldTypeDescription
valueboolean / string / number / objectResolved flag value. Type depends on flag kind
variantstringVariation key that was served
reasonstringWhy 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

FieldTypeRequiredDescription
keysstring[]yesFlag keys to evaluate (max 100)
userobjectyesUser context (same shape as above)
environmentstringnoEnvironment 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 map

Java

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

FieldTypeRequiredDescription
eventstringyesMetric name — must match an experiment's primary metric
userobjectyesMust include the same id used when evaluating the flag
valuenumbernoOptional 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

FieldTypeDescription
keystringImmutable flag key
namestringDisplay name
kindstringboolean, string, number, or json
variationsobject[]Each variation: vkey, name, value, color
tagsstring[]Tags attached to this flag
archivedbooleanWhether 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:

ValueMeaning
rule_matchA targeting rule matched the user
fallthroughNo rule matched; the rollout percentage was used
fallthrough_offNo rule matched; the flag has no active rollout
flag_offThe flag is disabled in this environment
prerequisite_failedA prerequisite flag did not serve its required variation

Errors

All errors use the same envelope:

{
  "error": { "code": "unauthorized", "message": "Invalid or missing API key." }
}
StatusCodeMeaning
400ambiguous_requestMore than one of event, keys, flag in the body
400invalid_jsonRequest body is not valid JSON
400missing_keyskeys array is empty
400too_many_keyskeys contains more than 100 entries
400missing_useruser or user.id not provided
400env_mismatchenvironment in body does not match the key's environment
401unauthorizedMissing or invalid API key
402usage_exceededMonthly request cap reached (free plan)
404flag_not_foundFlag key does not exist
429rate_limitedPer-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.