FlagrshipGuidesReference
esc

HTTP API

The CLI, the SDKs, and the dashboard are all clients of this API. Anything they can do, you can do with curl.

Base URL and authentication

https://api.flagrship.dev/api/v1

Every request under /api/v1 carries an API key as a bearer token. The key determines the organization, the environment, and what you are allowed to do. There is no environment parameter anywhere; the key is the environment.

$ curl https://api.flagrship.dev/api/v1/flags \
-H "Authorization: Bearer sk_test_your_staging_key"
ScopeGrantsKey prefixes
readGET on everything, including /evaluatesk_read_
writeread, plus POST on flags: create, enable, disable, rollout, rollbacksk_test_, sk_live_
adminwrite, plus lock, unlock, archive, and key managementsk_admin_

Errors

Every error has the same shape. The request ID is also in the response headers; quote it when asking for help.

1{
2 "error": {
3 "code": "VALIDATION_ERROR",
4 "message": "Rollout percentage must be between 0 and 100.",
5 "field": "percentage",
6 "request_id": "5608458f-925f-4a86-9d83-8d92a49b22c5"
7 }
8}
StatusCodeWhen
400VALIDATION_ERRORBad body, missing field, out-of-range value. field names the offender.
401UNAUTHORIZEDMissing, unknown, revoked, or expired key.
403FORBIDDENThe key lacks the scope this endpoint needs.
404NOT_FOUNDNo such flag, key, or environment in this organization.
409CONFLICTDuplicate flag key, locked flag, nothing to roll back, already revoked.
429RATE_LIMITEDToo many requests from your IP. The message says how long to wait.
500INTERNAL_ERROROur fault, and the message says nothing else. The request ID is in the body.

Flags

POST/flagsscope: write

Create a flag. It exists in every environment at once, disabled and at 0%.

{
"key": "new-checkout",
"name": "New Checkout",
"description": "optional",
"flagType": "BOOLEAN"
}

key: 3–120 lowercase letters, digits, hyphens; must start and end alphanumeric. Returns 201 with the flag as seen from the key's environment.

response
1{
2 "key": "new-checkout",
3 "name": "New Checkout",
4 "description": null,
5 "flagType": "BOOLEAN",
6 "environment": "staging",
7 "enabled": true,
8 "rolloutPercentage": 25,
9 "targetingRules": null,
10 "locked": false,
11 "lockReason": null,
12 "createdAt": "2026-09-15T14:40:54.203Z",
13 "updatedAt": "2026-09-15T23:04:44.330Z"
14}
GET/flagsscope: read

Every active flag in the environment, newest first. ?search= filters by key or name. Returns { environment, flags: [...] }.

GET/flags/:keyscope: read

One flag, with its config for this environment. 404 if it does not exist or is archived.

POST/flags/:key/enablescope: write

Sets enabled. Rollout percentage is preserved. No body.

POST/flags/:key/disablescope: write

Clears enabled and resets rollout to 0. The previous percentage survives in the audit log. No body.

POST/flags/:key/rolloutscope: write
{ "percentage": 25 }

Integer 0–100. The flag must be enabled first, or you get a 400 that says so.

POST/flags/:key/rollbackscope: write

Restore the state from before the most recent change in this environment, and record the rollback as a change of its own. A second call undoes the first. 409 if there is nothing to roll back to. No body.

POST/flags/:key/lockscope: admin
{ "reason": "CVE-2026-1234" }

Reason is required, 1–500 characters. While locked, enable, disable, rollout, and rollback return 409 with the reason in the message. 409 if already locked.

POST/flags/:key/unlockscope: admin

Clears the lock. 409 if not locked. No body.

DELETE/flags/:keyscope: admin

Archive (soft delete). The flag disappears from /flags and /evaluate immediately; its history stays. Refused with 409 while the flag is locked in any environment.

GET/flags/:key/historyscope: read

The last 50 audit entries for the flag, newest first.

1{
2 "flag": "new-checkout",
3 "entries": [
4 {
5 "action": "flag.rollout",
6 "environment": "staging",
7 "actor": { "name": "Nisarg Patel", "email": "nisarg@flagrship.dev", "via": "api_key" },
8 "before": { "enabled": true, "rolloutPercentage": 0, "locked": false, "lockReason": null },
9 "after": { "enabled": true, "rolloutPercentage": 25, "locked": false, "lockReason": null },
10 "metadata": { "keyPrefix": "sk_test_75GA", "ip": "127.0.0.1", "requestId": "…" },
11 "at": "2026-09-15T14:40:54.741Z"
12 }
13 ]
14}

SDK sync

GET/evaluatescope: read

Every active flag's config for the key's environment, in the shape the SDKs consume. This endpoint has no parameters. It never accepts a user identifier; bucketing happens in the SDK.

1{
2 "environment": "staging",
3 "flags": [
4 { "key": "dark-mode", "enabled": true, "rolloutPercentage": 100, "targetingRules": null },
5 { "key": "new-checkout", "enabled": true, "rolloutPercentage": 25, "targetingRules": null }
6 ]
7}

The response carries a strong ETag. Send it back as If-None-Match and you get a 304 with no body when nothing has changed.

$ curl -i https://api.flagrship.dev/api/v1/evaluate \
-H "Authorization: Bearer sk_read_…" \
-H 'If-None-Match: "kM3Qn8tYw2xZa1bC4dE5fG6hI7j"'
HTTP/1.1 304 Not Modified
etag: "kM3Qn8tYw2xZa1bC4dE5fG6hI7j"
cache-control: private, no-cache

API keys

All three require an admin key.

POST/keysscope: admin
{
"name": "CI reader",
"scopes": ["read"]
}

The new key lands in the calling key's environment. There is no way to name a different one: a staging admin key must not be a way to mint a production key. Scopes are hierarchical: admin implies write implies read. The response includes the raw key once; only its SHA-256 hash is stored.

response
1{
2 "id": "b4dd593e-efdc-4553-aecf-c3b0edb21ff2",
3 "name": "CI reader",
4 "environment": "staging",
5 "scopes": ["read"],
6 "keyPrefix": "sk_read_your",
7 "createdAt": "2026-09-15T22:20:46.831Z",
8 "key": "sk_read_your_new_key_shown_once"
9}
GET/keysscope: admin

Every key in the organization with prefix, scopes, environment, last use, and whether it is still active. Never the key itself.

DELETE/keys/:idscope: admin

Revoke. The key stops authenticating immediately. It is not deleted; revoked keys are evidence. 409 if already revoked.

Rate limits

600 requests per minute per IP address, across every endpoint including /health. Going over returns 429 with RATE_LIMITED and a wait time. An SDK polls twice a minute and a CLI command is one call, so this only bites abuse.

The limit is not what protects a key from being guessed - a key is 24 random bytes, so guessing is not a threat a rate limit meaningfully changes. It is there so one noisy client cannot take the API away from everyone else.

Health

GET/health

Public, no key needed. Runs a real query, so a dead database connection fails it. Path is /health, not under /api/v1.

{ "status": "ok", "uptime": 3.63 }
Flagrship · Ship without a release.