The config JSON, in the open.
What the SDK downloads and exactly how it evaluates it, for authors of SDKs in other languages and anyone who wants to check our work.
1
Download it
One public route serves the config of one SDK key. It answers 200 with a weak ETag and the X-PC-Config-Revision and X-PC-Config-Generation headers, 304 to a matching If-None-Match, 404 SDK_KEY_NOT_FOUND for an unknown, revoked or expired key, and 429 with Retry-After above 20 requests a second per key and client IP. CORS allows any origin.
curl --compressed -i https://api.flags.productcraft.co/v1/configs/$PRODUCTCRAFT_FLAGS_SDK_KEY/config.json2
Schema 1
snake_case keys; no names, descriptions, tags or anything about your workspace. Unknown fields must be ignored. An unknown schema number means: keep the config you have. A breaking change would be schema 2 at a new path, served alongside. The JSON Schema is published at https://productcraft.co/docs/flags/config-json/schema-1.json and ships in @productcraft/flags-engine.
{
"schema": 1,
"revision": 42,
"generation": 1,
"published_at": "2026-11-08T14:02:11Z",
"config_id": "11111111-1111-4111-8111-111111111111",
"environment_id": "22222222-2222-4222-8222-222222222222",
"salt": "c2FsdC1mb3ItdmVjdG9ycw",
"delivery": {
"base_urls": [
"https://api.flags.productcraft.co"
],
"mode": "hint"
},
"segments": {
"acme_staff": {
"seed": "acme_staff_old",
"conditions": [
{
"user": {
"attribute": "email",
"comparator": "ends_with_any",
"values": [
"@acme.com"
]
}
}
]
}
},
"settings": {
"new_checkout": {
"type": "boolean",
"value": false,
"seed": "checkout_v2",
"percentage_attribute": "account_id",
"rules": [
{
"conditions": [
{
"segment": {
"segment": "acme_staff",
"comparator": "in"
}
}
],
"serve": true
},
{
"conditions": [
{
"user": {
"attribute": "app_version",
"comparator": "semver_gte",
"values": [
"2.0.0"
]
}
}
],
"percentages": [
{
"percentage": 10,
"value": true
},
{
"percentage": 90,
"value": false
}
]
},
{
"conditions": [
{
"prerequisite": {
"setting": "kill_all",
"comparator": "eq",
"value": false
}
}
],
"serve": true
}
],
"percentages": [
{
"percentage": 50,
"value": true
},
{
"percentage": 50,
"value": false
}
]
},
"banner_text": {
"type": "string",
"value": "Welcome"
},
"limits": {
"type": "json",
"value": "{\"max\":5}"
},
"kill_all": {
"type": "boolean",
"value": false
}
}
}3
The evaluation spec and test vectors
The canonical text is SPEC.md in the engine package: the user object, outcomes (match, soft miss, hard error), the rule algorithm, every comparator and its coercion, bucketing, confidential hashing and evaluation details. The test vectors (format 1) pin all of it: hash buckets, comparator cases, whole-config suites, configs an SDK must reject, and HTTP transcripts for the client layer. An SDK that passes them evaluates exactly as ours does.
The two hashes, in short:
- Bucket:
parseInt(hex(sha1(utf8(seed + value))).slice(0, 7), 16) % 100; the first option whose cumulative percentage is above it wins. - Confidential:
hex(sha256(utf8(value) + salt + context)), with the setting's or segment's seed as context; starts/ends-with hash the first or last n UTF-8 bytes and prefix"n_".
4
The engine on its own
@productcraft/flags-engine is the evaluator with no network code and no dependencies, synchronous on every runtime. Use it to compare another implementation case by case.
import { evaluate, evaluateAll, parseConfig } from "@productcraft/flags-engine";
// The text of GET https://api.flags.productcraft.co/v1/configs/{sdk_key}/config.json
declare const body: string;
// Throws ConfigParseError (PARSE_ERROR / UNSUPPORTED_SCHEMA):
// keep the config you had.
const config = parseConfig(body);
const d = evaluate(config, "new_checkout", false, { identifier: "user-42", custom: { plan: "pro" } });
console.log(d.value, d.reason, d.variant, d.warnings);
for (const each of evaluateAll(config, { identifier: "user-42" })) console.log(each.key, each.value);import { bucket, hashConfidential, isNewerConfig } from "@productcraft/flags-engine";
console.log(bucket("new_checkout", "user-1")); // 85
console.log(bucket("new_checkout", "user-2")); // 7
console.log(hashConfidential("ada@example.com", "c2FsdA", "beta_testers"));
console.log(isNewerConfig({ revision: 41, generation: 1 }, { revision: 42, generation: 1 })); // true