Docs
A quick reference for the exp REST API. The full contract — every endpoint, object and error — lives in the repository.
Read the full contract (CONTRACT.md) →Base URL
https://exp.3m4.net/apiAuthentication
Three kinds of credentials, each for one job:
- Dashboard sessions: JWT bearer tokens issued by /auth/login. Hold every scope.
- Management API: API keys with a role (viewer / editor / admin). Header X-API-Key: exp_live_… (or Authorization: Bearer).
- Evaluation API: environment client keys. Header X-Environment-Key: exp_env_…. They identify one (project, environment) and can only evaluate.
Projects & environments
A project owns environments and flags. Creating one seeds production, staging and development and returns their client keys once.
| Method | Path | Scope | Notes |
|---|---|---|---|
| GET | /projects?q= | projects:read | |
| POST | /projects | projects:write | {name, key?, description?} → 201 · 402 limit_reached |
| GET | /projects/:id | projects:read | |
| PATCH | /projects/:id | projects:write | {name?, description?} |
| DELETE | /projects/:id | projects:delete | cascades |
| GET | /projects/:id/export | flags:read | exp.project.v1 |
| GET | /projects/:id/environments | projects:read | |
| POST | /projects/:id/environments | projects:write | {key, name?} → client_key once |
| DELETE | /projects/:id/environments/:envId | projects:delete | last one: 400 last_environment |
| POST | /projects/:id/environments/:envId/rotate-key | projects:write | old key stops immediately |
curl -X POST https://exp.3m4.net/api/projects \
-H "X-API-Key: exp_live_…" -H "Content-Type: application/json" \
-d '{"name": "Web app"}'Flags
A flag has a key, a kind (boolean, string, number, json), a default value, tags and one state per environment. Archived flags are kept but never evaluated.
| Method | Path | Scope | Notes |
|---|---|---|---|
| GET | /projects/:id/flags?q=&page=&per_page=&include_archived= | flags:read | ordered by key |
| POST | /projects/:id/flags | flags:write | {key, name?, kind?, default_value?, tags?, states?} · 409 key_taken · 402 |
| GET | /projects/:id/flags/:flag | flags:read | :flag = UUID or key |
| PATCH | /projects/:id/flags/:flag | flags:write | changing kind resets state values |
| DELETE | /projects/:id/flags/:flag | flags:delete |
curl -X POST https://exp.3m4.net/api/projects/$PROJECT/flags \
-H "X-API-Key: exp_live_…" -H "Content-Type: application/json" \
-d '{"key": "new-checkout", "kind": "boolean", "tags": ["checkout"]}'States
A state is enabled + value + rollout_percent for one flag in one environment. Set it with PUT; the response is the whole flag.
| Method | Path | Scope | Notes |
|---|---|---|---|
| PUT | /projects/:id/flags/:flag/states/:envKey | flags:write | {enabled?, value?, rollout_percent?} → Flag |
| GET | /projects/:id/flags/evaluate?environment=&distinct_id= | flags:read | preview, not metered |
curl -X PUT https://exp.3m4.net/api/projects/$PROJECT/flags/new-checkout/states/staging \
-H "X-API-Key: exp_live_…" -H "Content-Type: application/json" \
-d '{"enabled": true, "rollout_percent": 25}'Copy flags
Copies flag definitions (and optionally their states) into another project of the same account. One transaction: if the target would exceed its flag limit nothing is copied.
| Method | Path | Scope | Notes |
|---|---|---|---|
| POST | /projects/:id/flags/copy | flags:write | {target_project_id, flag_keys?, include_states?, on_conflict?} → {copied, overwritten, skipped} |
Evaluation semantics
For one flag in one environment, the evaluation API answers like this:
- State enabled == false → off.
- rollout_percent < 100: bucket = first 8 hex chars of sha256("<flag_key>:<distinct_id>") as an integer, modulo 100. In when bucket < rollout_percent. Without distinct_id the request is out unless rollout_percent == 100.
- On → boolean: value = true; other kinds: value = state.value ?? flag.default_value.
- Off → boolean: value = false; other kinds: value = null.
The dashboard and the management API expose the same logic as a preview (GET /projects/:id/flags/evaluate), which is neither metered nor rate-limited as evaluation.
| Method | Path | Scope | Notes |
|---|---|---|---|
| GET | /eval/flags?distinct_id= | X-Environment-Key | metered +1 |
| GET | /eval/flags/:key?distinct_id= | X-Environment-Key | metered +1 · 404 for unknown/archived |
curl "https://exp.3m4.net/api/eval/flags?distinct_id=user-42" \
-H "X-Environment-Key: exp_env_…"Errors & limits
Errors are JSON: { "error": { "code", "message" } }. Creating past a plan limit answers 402 limit_reached with error.which = projects | environments | flags | api_keys. Evaluations over the monthly budget are still served.
MCP (AI agents)
The MCP server exposes the management API as tools, filtered by the role of the key you connect with. Destructive tools ask for confirmation.
claude mcp add exp --scope user --env EXP_API_KEY=exp_live_… -- npx -y exp-mcpclaude mcp add --transport http exp https://exp.3m4.net/mcp --header "Authorization: Bearer exp_live_…"