REST API Reference
Base URL
http://your-checkgate-server:3000Authentication
All protected endpoints require one of:
Authorization: Bearer sk_live_your_sdk_keyAuthorization: Bearer pat_your_personal_access_tokenor an active session cookie set by POST /api/auth/login.
An SDK key is always admin-equivalent, intended for the SDKs themselves. A personal access token (see Personal Access Tokens) instead acts as its owning user — same role, same project memberships — and can be capped to read-only, making it the right choice for CI/CD, Terraform, or any script that shouldn't hold blanket admin access.
Dashboard mutation requests also require:
X-Checkgate-Request: trueThis header is required for all state-changing requests from browser clients (CSRF protection). SDK clients using Authorization: Bearer are exempt.
Auth
Login
POST /api/auth/login
Content-Type: application/jsonRequest Body
{
"email": "admin@example.com",
"password": "your-password"
}Response 200 OK
{
"email": "admin@example.com",
"name": "Jane Smith",
"role": "admin",
"workspace_name": "Acme Corp",
"is_setup_complete": true
}Sets an HttpOnly SameSite=Strict session cookie valid for 7 days.
Response 401 Unauthorized
{
"error": "Incorrect email or password.",
"attempts_remaining": 4
}Response 429 Too Many Requests — account locked after 5 failures in 10 minutes.
{
"error": "Too many failed attempts. Account locked for 15 minutes.",
"retry_after_seconds": 900
}Logout
POST /api/auth/logoutClears the session cookie.
Response 204 No Content
Get Current User
GET /api/auth/meResponse 200 OK — returns the authenticated user (same shape as login response, includes is_setup_complete).
Response 401 Unauthorized — no valid session.
Workspace Info
GET /api/auth/workspacePublic endpoint — no auth required. Returns the workspace name for the login page.
Response 200 OK
{ "workspace_name": "Acme Corp" }Projects
Projects are the top-level namespace for environments, flags, and SDK keys. Workspace admins can manage all projects; other users see only projects they are members of.
List Projects
GET /api/projectsReturns all projects the caller has access to (all projects for workspace admins; only member projects for others).
Response 200 OK
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Mobile App",
"slug": "mobile-app",
"environment_count": 3,
"member_count": 4,
"created_at": "2026-04-18T00:00:00Z"
}
]Create Project
POST /api/projects
Content-Type: application/jsonAdmin only. Creates a new project and seeds it with Production, Staging, and Development environments plus one SDK key for the Production environment.
Request Body
{ "name": "Mobile App" }Response 200 OK — returns the created project.
Response 409 Conflict — slug derived from the name already exists.
Rename Project
PATCH /api/projects/{project_id}
Content-Type: application/json{ "name": "Mobile App v2" }Response 200 OK — returns the updated project.
Delete Project
DELETE /api/projects/{project_id}Admin only. Cascades to all environments, flags, SDK keys, and impressions in the project.
Response 204 No Content
Response 422 Unprocessable Entity — cannot delete the last project.
Project Members
List Members
GET /api/projects/{project_id}/membersResponse 200 OK
[
{
"user_id": 1,
"name": "Jane Smith",
"email": "jane@example.com",
"role": "admin"
}
]Add Member
POST /api/projects/{project_id}/members
Content-Type: application/json{ "user_id": 2, "role": "editor" }rolemust be"admin","editor", or"viewer"
Response 200 OK — returns the added member.
Response 409 Conflict — user is already a member.
Update Member Role
PATCH /api/projects/{project_id}/members/{user_id}
Content-Type: application/json{ "role": "viewer" }Response 200 OK — returns the updated member.
Remove Member
DELETE /api/projects/{project_id}/members/{user_id}Response 204 No Content
Environments
Environments are scoped to a project. All environment routes are nested under /api/projects/{project_id}/environments.
List Environments
GET /api/projects/{project_id}/environmentsResponse 200 OK
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Production",
"slug": "production",
"color": "#ef4444",
"is_default": false,
"created_at": "2026-04-10T00:00:00Z"
}
]Create Environment
POST /api/projects/{project_id}/environments
Content-Type: application/jsonRequest Body
{
"name": "Canary",
"slug": "canary",
"color": "#f59e0b"
}name— required, max 100 charactersslug— required, max 64 characters, lowercase alphanumerics and hyphens onlycolor— optional, must be a valid hex color (#rgbor#rrggbb); defaults to#6366f1
Response 200 OK — returns the created environment.
Response 409 Conflict — slug already exists in this project.
Update Environment
PATCH /api/projects/{project_id}/environments/{env_id}
Content-Type: application/json{ "name": "Canary East", "color": "#8b5cf6" }Response 200 OK — returns the updated environment.
Delete Environment
DELETE /api/projects/{project_id}/environments/{env_id}Response 204 No Content
Response 422 Unprocessable Entity — cannot delete the default or the last environment.
Set Default Environment
POST /api/projects/{project_id}/environments/{env_id}/defaultResponse 200 OK — returns the updated environment with is_default: true.
Set Approval Requirement
POST /api/projects/{project_id}/environments/{env_id}/require-approval
Content-Type: application/json{ "require_approval": true }When enabled, PATCH requests against flags in this environment no longer apply immediately — see Partially Update a Flag and Change Requests.
Response 200 OK — returns the updated environment.
Flags
Flag endpoints remain environment-scoped via {env_id} (UUID). The environment UUID can be obtained from the environments list.
List Flags
GET /api/environments/{env_id}/flagsResponse 200 OK
[
{
"key": "new-checkout-flow",
"is_enabled": true,
"rollout_percentage": 50,
"description": "New checkout UI rollout",
"rules": []
}
]Response 404 Not Found — environment does not exist.
Get a Flag
GET /api/environments/{env_id}/flags/{key}Response 200 OK — returns the flag object.
Response 404 Not Found — flag or environment does not exist.
Create or Replace a Flag
POST /api/environments/{env_id}/flags
Content-Type: application/jsonCreates a flag. If the key already exists in this environment, replaces it entirely.
Request Body
{
"key": "new-checkout-flow",
"is_enabled": true,
"rollout_percentage": 50,
"description": "New checkout UI rollout",
"rules": []
}Response 200 OK — returns the created flag.
Response 422 Unprocessable Entity — invalid flag key or rollout_percentage out of range.
Partially Update a Flag
PATCH /api/environments/{env_id}/flags/{key}
Content-Type: application/jsonApplies a JSON merge patch. Only the provided fields are updated; omitted fields retain their current values.
{ "rollout_percentage": 100 }{ "is_enabled": false }Response 200 OK — returns the updated flag (applied immediately).
Response 202 Accepted — the environment has require_approval set; the patch was validated and queued as a pending change request instead of applied. Returns the created change request, not the flag (which is unchanged).
Response 404 Not Found — flag does not exist.
Delete a Flag
DELETE /api/environments/{env_id}/flags/{key}Deletes the flag and broadcasts a DELETE event to connected SDK clients.
Response 204 No Content
Promote a Flag
POST /api/environments/{env_id}/flags/{key}/promote
Content-Type: application/jsonCopies the flag's configuration from {env_id} to another environment atomically.
Request Body
{ "target_env_id": "uuid-of-target-environment" }Response 200 OK — returns the flag as it now exists in the target environment.
Change Requests
Only relevant for environments with require_approval enabled. A change request captures a flag PATCH that hasn't been applied yet — see Partially Update a Flag for how one gets created. Someone other than the requester must approve it (self-approval is rejected); admins may cancel anyone's pending request, mirroring how workspace admins bypass project membership elsewhere.
List Change Requests
GET /api/environments/{env_id}/change-requests?status=pendingstatus is optional — one of pending, approved, rejected, cancelled. Omit to list all.
Response 200 OK
[
{
"id": 1,
"environment_id": "550e8400-e29b-41d4-a716-446655440000",
"flag_key": "dark_mode",
"patch": { "is_enabled": true },
"requested_by": "alice@acme.com",
"status": "pending",
"reviewed_by": null,
"reason": null,
"created_at": "2026-07-04T00:00:00Z",
"reviewed_at": null
}
]Approve Change Request
POST /api/environments/{env_id}/change-requests/{id}/approveApplies the stored patch to the flag's current state (not a stale snapshot from when the request was filed) and marks the request approved.
Response 200 OK — returns the updated flag.
Response 403 Forbidden — the caller is the original requester.
Response 409 Conflict — the request is no longer pending (already approved/rejected/cancelled).
Reject Change Request
POST /api/environments/{env_id}/change-requests/{id}/reject
Content-Type: application/json{ "reason": "Not ready for this rollout yet" }reason is optional. The flag is never touched.
Response 204 No Content
Cancel (Withdraw) Change Request
DELETE /api/environments/{env_id}/change-requests/{id}Only the original requester or a workspace admin may cancel, and only while still pending.
Response 204 No Content
Response 404 Not Found — no such pending request, or the caller doesn't own it.
Flag Schema
Flag Object
| Field | Type | Required | Description |
|---|---|---|---|
key | string | Yes | Unique within environment. Alphanumerics, underscores, hyphens. Max 100 chars. Immutable after creation. |
is_enabled | boolean | Yes | Master switch. false always evaluates to the disabled result. |
rollout_percentage | integer | null | No | 0–100. null is treated as 100%. |
description | string | null | No | Human-readable description. |
rules | TargetingRule[] | No | Targeting rules, evaluated in order (first match wins). Defaults to []. |
flag_type | "boolean" | "string" | "integer" | "json" | No | Variant type. Defaults to "boolean". |
default_value | any (matching flag_type) | No | Returned when enabled, inside the rollout, and no rule matched. |
disabled_value | any (matching flag_type) | No | Returned when disabled, outside the rollout, or a prerequisite fails. |
variants | WeightedVariant[] | No | Weighted multivariate distribution — see Weighted Variants. Defaults to []. |
prerequisites | Prerequisite[] | No | Other flags this flag depends on — see Prerequisite Flags. Defaults to []. |
TargetingRule Object
| Field | Type | Required | Description |
|---|---|---|---|
attribute | string | Yes* | User attribute name to match against. Not required when segment_key is set. |
operator | Operator | Yes* | Comparison operator. Not required when segment_key is set. |
values | string[] | Yes* | One or more values to match. At least one must match. Not required when segment_key is set. |
segment_key | string | null | No | References a named segment instead of a concrete attribute check. Expanded server-side before reaching SDKs. |
variant | any (matching flag_type) | No | Value returned when this rule matches (non-boolean flags). Falls back to default_value if unset. |
Operator Enum
| Value | Description |
|---|---|
equals | Attribute value equals any of the provided values |
not_equals | Attribute value does not equal any of the provided values |
contains | Attribute value contains any of the provided values as a substring |
starts_with | Attribute value starts with any of the provided values |
ends_with | Attribute value ends with any of the provided values |
greater_than | Attribute, parsed as a number, is greater than any of the provided values |
greater_than_or_equal | Attribute, parsed as a number, is greater than or equal to any of the provided values |
less_than | Attribute, parsed as a number, is less than any of the provided values |
less_than_or_equal | Attribute, parsed as a number, is less than or equal to any of the provided values |
Non-numeric attribute or rule values never match the four numeric operators (no error — the comparison is simply skipped).
WeightedVariant Object
| Field | Type | Required | Description |
|---|---|---|---|
weight | integer | Yes | Relative weight. Weights need not sum to 100 — normalized against their total. |
value | any (matching flag_type) | Yes | The variant value returned when this weighted bucket is selected. |
Prerequisite Object
| Field | Type | Required | Description |
|---|---|---|---|
flag_key | string | Yes | The prerequisite flag's key. |
required_value | any (matching the prerequisite's flag_type) | No | The value the prerequisite must resolve to. Omit to just require it be enabled. |
Impressions
Ingest Impressions
POST /api/environments/{env_id}/impressions
Authorization: Bearer sk_live_your_key
Content-Type: application/jsonReports a batch of flag evaluation events from an SDK client. Authenticated with an SDK key; does not require admin role. CSRF header not required for Bearer-authenticated requests.
Request Body — array of up to 500 impression objects
[
{
"flag_key": "checkout_v2",
"user_id": "user-123",
"value": "true",
"context": { "plan": "pro" },
"evaluated_at": "2026-04-18T10:00:00Z"
}
]| Field | Type | Required | Description |
|---|---|---|---|
flag_key | string | Yes | The evaluated flag key |
user_id | string | null | No | User identifier; null for anonymous |
value | string | Yes | Evaluation result (e.g. "true", "false") |
context | object | null | No | Evaluation context attributes |
evaluated_at | ISO-8601 string | No | Defaults to server receive time |
Response 204 No Content
Response 413 Payload Too Large — batch exceeds 500 items.
List Impressions
GET /api/environments/{env_id}/impressionsQuery Parameters
| Param | Default | Description |
|---|---|---|
flag_key | — | Filter by exact flag key |
user_id | — | Filter by exact user ID |
value | — | Filter by evaluated value (e.g. "true", "false") |
since_id | — | Return only rows with id > since_id — for live polling |
limit | 50 | Max results (1–200) |
offset | 0 | Pagination offset (ignored when since_id is set) |
total in the response always reflects the count matching the base filters (ignoring since_id), so it can be used for pagination display regardless of polling state.
Response 200 OK
{
"items": [
{
"id": 142,
"flag_key": "checkout_v2",
"user_id": "user-123",
"value": "true",
"context": { "plan": "pro" },
"evaluated_at": "2026-04-18T10:00:00Z"
}
],
"total": 142
}Impression Stats
GET /api/environments/{env_id}/impressions/statsReturns per-flag aggregate counts.
Response 200 OK
[
{
"flag_key": "checkout_v2",
"total": 1420,
"true_count": 750,
"false_count": 670,
"unique_users": 89,
"last_seen": "2026-04-18T10:30:00Z"
}
]SDK Keys
SDK keys are scoped to a project and bound to a specific environment. All key routes are nested under /api/projects/{project_id}/keys.
List Keys
GET /api/projects/{project_id}/keysReturns all SDK keys for the project. The full key value is never returned after creation.
Response 200 OK
[
{
"id": 1,
"name": "Default",
"prefix": "sk_live_a1b2c3…",
"environment_id": "550e8400-e29b-41d4-a716-446655440000",
"environment_name": "Production",
"created_at": "2026-04-10T00:00:00Z"
}
]Create Key
POST /api/projects/{project_id}/keys
Content-Type: application/json{
"name": "iOS Production",
"environment_id": "550e8400-e29b-41d4-a716-446655440000"
}environment_idmust belong to the project
Response 200 OK — returns the key including the full value (shown once only).
{
"id": 2,
"name": "iOS Production",
"key": "sk_live_a1b2c3d4e5f6...",
"prefix": "sk_live_a1b2c3…",
"environment_id": "550e8400-e29b-41d4-a716-446655440000",
"environment_name": "Production",
"created_at": "2026-04-18T12:00:00Z"
}Revoke Key
DELETE /api/projects/{project_id}/keys/{id}Response 204 No Content
Response 422 Unprocessable Entity — cannot revoke the last key in the project.
Personal Access Tokens
Personal access tokens are user-owned, not project-scoped — routes are flat under /api/tokens and always operate on the caller's own tokens. There is no admin override; revoking a departing or compromised user's automation access is done by deleting the user, which cascades to their tokens.
List Tokens
GET /api/tokensReturns the caller's own tokens. The full token value is never returned after creation.
Response 200 OK
[
{
"id": 1,
"name": "Terraform CI",
"prefix": "pat_a1b2c3d4e5f6…",
"scope": "read_only",
"created_at": "2026-07-04T00:00:00Z",
"last_used_at": "2026-07-04T09:02:09Z",
"expires_at": "2026-08-03T00:00:00Z"
}
]Create Token
POST /api/tokens
Content-Type: application/json{
"name": "Terraform CI",
"scope": "read_only",
"expires_in_days": 90
}scopeis"read_only"or"read_write". A token authenticated request that is itselfread_onlycannot create aread_writetoken — this would let a leaked read-only credential mint itself a more privileged replacement.expires_in_daysis optional (1–365); omit for a token that never expires.
Response 200 OK — returns the token including the full value (shown once only).
{
"id": 2,
"name": "Terraform CI",
"token": "pat_a1b2c3d4e5f6...",
"prefix": "pat_a1b2c3d4e5f6…",
"scope": "read_only",
"created_at": "2026-07-04T00:00:00Z",
"expires_at": "2026-10-02T00:00:00Z"
}Response 403 Forbidden — a read-only-scoped caller tried to create a read-write token.
Revoke Token
DELETE /api/tokens/{id}Response 204 No Content
Response 404 Not Found — no such token owned by the caller.
Users
Users are workspace-level. Role here is the workspace role; per-project access is managed via Project Members.
List Users
GET /api/usersResponse 200 OK
[
{
"id": 1,
"name": "Jane Smith",
"email": "jane@example.com",
"role": "admin",
"created_at": "2026-04-10T00:00:00Z"
}
]Create User
POST /api/users
Content-Type: application/json{
"name": "Bob Jones",
"email": "bob@example.com",
"role": "viewer",
"password": "their-initial-password"
}rolemust be"admin","editor", or"viewer"passwordmust be at least 8 charactersemailmust be unique
Response 200 OK — returns the created user (password hash is never returned).
Response 409 Conflict — email already in use.
{ "error": "Email address is already in use." }Response 422 Unprocessable Entity — invalid input or password too short.
Delete User
DELETE /api/users/{id}Response 204 No Content
Response 422 Unprocessable Entity — cannot delete yourself or the last admin.
SSE Stream
Connect
GET /stream
Authorization: Bearer sk_live_your_key
Accept: text/event-streamOr for browser EventSource:
GET /stream?sdk_key=sk_live_your_keyOpens a persistent SSE connection scoped to the environment associated with the SDK key. The server sends:
- A
connectedevent immediately - One
updateevent per existing flag in the key's environment (bootstrap) - A
readyevent once the bootstrap replay is complete updateevents as flags change in that environment in real time- Keep-alive comments every 15 seconds
Event: connected
event: connected
data: {"environment_id":"<uuid>"}Sent once when the connection is established. environment_id is null for session-cookie (dashboard) connections, which aren't scoped to a single environment. SDK clients should clear their local store on receiving this event to prepare for a clean bootstrap, and use environment_id to route impression reporting (POST /api/environments/{environment_id}/impressions).
Event: update
event: update
data: {"type":"UPSERT","env_id":"<uuid>","flag":{"key":"my-flag","is_enabled":true,...}}event: update
data: {"type":"DELETE","env_id":"<uuid>","key":"my-flag"}Event: ready
event: ready
data: "3"Sent once the full bootstrap replay (the update events above) is complete — data is the number of flags sent. SDK clients should resolve their connect() promise on this event rather than guessing when bootstrap has finished. Sent on every (re)connection, not just the first.
Keep-Alive
: keep-alive-textSent every 15 seconds to prevent proxy timeouts.
Reconnection
If the SSE connection drops, clients should reconnect with exponential backoff. On reconnect:
- Server sends
connected→ SDK clears local store - Server replays all current flags for the environment → SDK rebuilds from scratch
- Server sends
ready→ SDK resolvesconnect()again if it hadn't already
This guarantees consistency even after missed updates during the disconnected period.
Flags Snapshot (Poll Fallback)
GET /flags/snapshot
Authorization: Bearer sk_live_your_keyReturns the full, segment-expanded flag set for the environment associated with the SDK key, as a plain JSON array — the same shape each flag has in an SSE update event, but without the type/env_id envelope:
[
{ "key": "my-flag", "is_enabled": true, "rollout_percentage": null, "rules": [], "flag_type": "boolean", ... }
]Intended as a fallback for SDK clients when the SSE connection at /stream cannot be established at all — e.g. a corporate proxy or firewall blocking long-lived connections. Official SDKs poll this endpoint automatically after a configurable number of consecutive failed SSE reconnects (default: 3), and stop polling as soon as SSE reconnects successfully.
Unlike GET /api/environments/{env_id}/flags (session-cookie auth only, segment references left unexpanded — intended for the dashboard UI), this route is keyed by SDK key alone, exactly like /stream, and returns segment-expanded flags ready to evaluate against directly.
Returns 400 if called with session-cookie auth rather than an SDK key (the dashboard isn't scoped to a single environment the way an SDK key is), and 401 for missing/invalid credentials.
Health Check
GET /healthPublic endpoint — no authentication required.
Response 200 OK — body: OK
Use this for load balancer and container health checks.
