BETA
Skip to content

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 belongsYour server, a secret storeAnywhere — bundle, app binary, page source
ScopeThe whole teamOne project, one environment
Evaluate / bootstrapYesYes
Track / identifyYes (flags:track)Yes
Read rules, segments, definitionsYesNo
Create or change anythingYes (flags:write)No
Chooses the environmentThe request doesThe 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-keys

Required 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-keys

Required scope: flags:write

FieldTypeRequiredDescription
namestringYesHow you will recognise it — "Web bundle", "iOS 4.2".
environmentstringYesMust 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/rotate

Required 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_id

Required 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/evaluate

Neither project_id nor environment is read from the request — both come from the key. Sending them is harmless and ignored.

Request Body ​

FieldTypeRequiredDescription
contextobjectNoAttributes 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/bootstrap
bash
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
FieldTypeRequiredDescription
eventstringYesEvent name, matched against your experiment metrics.
user_idstringOne of the twoIdentified visitor.
anonymous_idstringOne of the twoAnonymous visitor.
valuenumberNoNumeric value (revenue, duration). Defaults to 0.
propertiesobjectNoArbitrary event properties.
event_idstringNoYour 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
FieldTypeRequired
anonymous_idstringYes
user_idstringYes
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:

LayerLimitKeyed by
Per source120 req / 60sClient IP
Failed authentication20 refusals / 60sClient IP
Per credentialyour plan's API rate × 60sThe 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 ​

StatusMeaning
401Missing, unknown or revoked key. The body is identical in all three cases.
422A required field is missing or malformed (event, an identity, a numeric value).
429Either 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.

Built by Krafter Studio