Appearance
Client Keys
Evaluate flags from a browser or a mobile app without shipping a platform API key.
A client key is a credential designed to be readable. It starts with krf_client_, it belongs to one project in one environment, and the only thing it can do is evaluate flags and record events. It unlocks no flag definitions, no rules, no segments, no changes — so putting it in a JavaScript bundle is its intended use, not a mistake.
Base URL: https://app.krafter.dev/api/v1
Why not just use an API key?
A platform kr_live_ key with flags:read is scoped to a team, not to a project. Anyone who reads it out of your bundle can list every project you have, every flag in them, every targeting rule and segment definition, and the full audit log — and can choose which environment to evaluate, so your unreleased development values are readable too.
That is why a browser SDK needs a different credential rather than a narrower scope.
Platform key (kr_live_) | Client key (krf_client_) | |
|---|---|---|
| Where it belongs | Your server, a secret store | Anywhere — bundle, app binary, page source |
| Scope | The whole team | One project, one environment |
| Evaluate / bootstrap | Yes | Yes |
| Track / identify | Yes (flags:track) | Yes |
| Read rules, segments, definitions | Yes | No |
| Create or change anything | Yes (flags:write) | No |
| Chooses the environment | The request does | The key does |
Managing keys
In the dashboard, under Flags → Client Keys for the selected project — or over the API, which is what the rest of this section documents.
The management endpoints need a platform kr_live_ key, never a client key. A client key that could mint another one would be an escalation path out of the browser, so it gets 401 on all four.
List keys
GET /flags/projects/:project_id/client-keysRequired scope: flags:read
bash
curl https://app.krafter.dev/api/v1/flags/projects/b1c2d3e4-.../client-keys \
-H "Authorization: Bearer kr_live_abc123def456"json
{
"data": [
{
"id": "7c1f...",
"name": "Web bundle",
"environment": "production",
"key_prefix": "krf_client_9fA2xQ",
"last_used_at": "2026-09-08T11:22:04Z",
"revoked_at": null,
"created_at": "2026-09-01T09:14:00Z",
"updated_at": "2026-09-01T09:14:00Z"
}
]
}The secret is not in this response, and there is no endpoint that returns it — only key_prefix, which is all the row holds. Revoked keys are listed too, with revoked_at set: after a leak, "this key existed and was turned off then" is the answer you want, and deleting the row erases it.
Create a key
POST /flags/projects/:project_id/client-keysRequired scope: flags:write
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | How you will recognise it — "Web bundle", "iOS 4.2". |
environment | string | Yes | Must be one of the project's own environments, or 422. |
bash
curl -X POST https://app.krafter.dev/api/v1/flags/projects/b1c2d3e4-.../client-keys \
-H "Authorization: Bearer kr_live_abc123def456" \
-H "Content-Type: application/json" \
-d '{"name": "Web bundle", "environment": "production"}'json
{
"data": {
"id": "7c1f...",
"name": "Web bundle",
"environment": "production",
"key": "krf_client_9fA2xQ...",
"key_prefix": "krf_client_9fA2xQ",
"revoked_at": null
}
}key appears only here and in the rotate response. Store it when you read it.
Rotate a key
POST /flags/projects/:project_id/client-keys/:key_id/rotateRequired scope: flags:write
Replaces the secret and keeps the key's id, name and environment. The response carries the new key, and the old value stops evaluating immediately — so rotate when your deploy can swap the bundle in one step.
If your bundle cannot be swapped atomically, create a second key, deploy it, then revoke the first once traffic has moved.
Revoke a key
DELETE /flags/projects/:project_id/client-keys/:key_idRequired scope: flags:write
Turns the key off for good and returns the row with revoked_at set. This is a revoke, not a delete: the row survives as evidence.
A key belonging to another project returns 404 on rotate and revoke, even within the same team, and a platform key restricted to particular projects (see per-project permissions) cannot manage keys for a project it is not authorised for.
A leaked client key is a smaller problem
It reads flag values for one environment of one project and nothing else. Rotate it and move on — no platform key is affected, and nothing else in your account is exposed.
A leaked client key is a smaller problem
It reads flag values for one environment of one project and nothing else. Rotate it and move on — no platform key is affected, and nothing else in your account is exposed.
Evaluate
Evaluate every flag in the key's project and environment, optionally with a targeting context.
POST /flags/client/evaluateNeither project_id nor environment is read from the request — both come from the key. Sending them is harmless and ignored.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
context | object | No | Attributes used by targeting rules — user_id, anonymous_id, and anything your rules reference. |
Example Request
bash
curl -X POST https://app.krafter.dev/api/v1/flags/client/evaluate \
-H "Authorization: Bearer krf_client_abc123def456" \
-H "Content-Type: application/json" \
-d '{
"context": {
"user_id": "usr_123",
"plan": "pro",
"country": "US"
}
}'Example Response
json
{
"flags": {
"new-checkout-flow": true,
"hero-banner-variant": "treatment",
"dark-mode": false
},
"environment": "production"
}Values only — no rule_id, no conditions, nothing that would describe why a flag resolved the way it did. The environment field echoes the key's environment so a misconfigured key is obvious in the response rather than silently returning the wrong values.
Bootstrap
The same evaluation with no context, for the first paint before you know who the visitor is.
GET /flags/client/bootstrapbash
curl https://app.krafter.dev/api/v1/flags/client/bootstrap \
-H "Authorization: Bearer krf_client_abc123def456"The response shape is identical to evaluate.
Track
Record a conversion event for an experiment.
POST /flags/client/track| Field | Type | Required | Description |
|---|---|---|---|
event | string | Yes | Event name, matched against your experiment metrics. |
user_id | string | One of the two | Identified visitor. |
anonymous_id | string | One of the two | Anonymous visitor. |
value | number | No | Numeric value (revenue, duration). Defaults to 0. |
properties | object | No | Arbitrary event properties. |
event_id | string | No | Your own idempotency key. |
bash
curl -X POST https://app.krafter.dev/api/v1/flags/client/track \
-H "Authorization: Bearer krf_client_abc123def456" \
-H "Content-Type: application/json" \
-d '{"anonymous_id": "anon_9f2", "event": "checkout_completed", "value": 49.90}'json
{ "tracked": true }Identify
Link an anonymous visitor to a user id, so experiment assignments made before sign-in follow them after it.
POST /flags/client/identify| Field | Type | Required |
|---|---|---|
anonymous_id | string | Yes |
user_id | string | Yes |
bash
curl -X POST https://app.krafter.dev/api/v1/flags/client/identify \
-H "Authorization: Bearer krf_client_abc123def456" \
-H "Content-Type: application/json" \
-d '{"anonymous_id": "anon_9f2", "user_id": "usr_123"}'json
{ "ok": true }There is no client-key snapshot
GET /flags/snapshot returns full flag definitions — rules, conditions, percentages, variants — so an SDK can evaluate locally without a network round trip. Those definitions are your targeting logic: internal segment names, customer email domains, plan tiers.
A credential that is expected to be read must not unlock them, so the endpoint has no client-key counterpart. /flags/client/snapshot returns 404. Local evaluation is a server-side SDK's job, with a platform key.
Metering
A browser evaluation costs exactly what a server evaluation costs. One call charges one evaluation per flag in the project, against the same monthly flags/evaluations quota your platform key spends, and is refused the same way once the quota is reached.
So the usual advice applies with more force in a browser: call bootstrap (or evaluate) once per session and keep the result in memory rather than re-evaluating per page view or per component render.
Rate limits
Three layers apply, and the request fails when any one of them is exceeded:
| Layer | Limit | Keyed by |
|---|---|---|
| Per source | 120 req / 60s | Client IP |
| Failed authentication | 20 refusals / 60s | Client IP |
| Per credential | your plan's API rate × 60s | The client key |
The per-credential bucket is keyed by the key, not by your team, so a client key under load cannot spend the request budget your server-side traffic depends on. Two client keys in the same project get separate budgets too — which is another reason to mint one per surface (web, iOS, Android) rather than sharing one.
See rate limits for headers and the 429 response shape.
CORS
The client endpoints answer any origin, because the caller is your own page. GET, POST and OPTIONS are allowed, with content-type and authorization request headers. Credentials (cookies) are not allowed — the key in the Authorization header is the whole authentication story.
Errors
| Status | Meaning |
|---|---|
401 | Missing, unknown or revoked key. The body is identical in all three cases. |
422 | A required field is missing or malformed (event, an identity, a numeric value). |
429 | Either a rate-limit bucket is exhausted — the body carries retry_after and the response a retry-after header — or your monthly evaluation quota is spent, in which case the body reads Monthly usage limit exceeded. and no retry-after is sent. Check the body before backing off: the second one will not clear until the month does. |