Decide a batch of features (browser)
const url = 'https://your-tenant-host/edgekit/v1/decide';const options = { method: 'POST', headers: {'x-edgekit-site-id': 'site_example', 'Content-Type': 'application/json'}, body: '{"features":["paywall","newsletter-prompt"],"content":{"payload":"{\"article_id\":\"a-1\",\"section\":\"news\",\"paywall_level\":\"2\"}","signature":"example"},"context":{"additionalProperty":"example"},"userId":"0199a4c2-7b1e-7c3d-9a4f-2e8b5d6c1a90","giftLink":"example"}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url https://your-tenant-host/edgekit/v1/decide \ --header 'Content-Type: application/json' \ --header 'x-edgekit-site-id: site_example' \ --data '{ "features": [ "paywall", "newsletter-prompt" ], "content": { "payload": "{\"article_id\":\"a-1\",\"section\":\"news\",\"paywall_level\":\"2\"}", "signature": "example" }, "context": { "additionalProperty": "example" }, "userId": "0199a4c2-7b1e-7c3d-9a4f-2e8b5d6c1a90", "giftLink": "example" }'One request carries the facts once and a list of feature names. Every feature evaluates against one immutable fact snapshot, and effects are deduped and applied after every feature has resolved — so a batch is order-independent and replayable from the request alone. Rate limited to 2,000 requests per 10 seconds per API key on /v1/decide, or 200 per 10 seconds per client per site on /edgekit/v1/decide — one flat window, no separate burst. Exceeding it returns 429 with Retry-After. A retry is not free: every call ticks the meters its rules charge (a seen-set meter counts one article once, a count meter every call).
Parameters
Section titled “Parameters”Header Parameters
Section titled “Header Parameters”The site this request is for, within this tenant. Every official EdgeKit SDK sets it from the client you configured; set it yourself only when calling this API directly.
Example
site_exampleThe site this request is for, within this tenant. Every official EdgeKit SDK sets it from the client you configured; set it yourself only when calling this API directly.
A short-lived preview token for this site. When valid, the decision evaluates against the staged preview document instead of the live one, and the response’s preview field is true. An absent, expired or invalid token falls back to the live document, with no error.
A short-lived preview token for this site. When valid, the decision evaluates against the staged preview document instead of the live one, and the response’s preview field is true. An absent, expired or invalid token falls back to the live document, with no error.
Request Bodyrequired
Section titled “Request Bodyrequired”object
The features to evaluate, named by the caller. A feature enabled in the console does not start evaluating — or ticking meters — because somebody saved a draft.
Example
[ "paywall", "newsletter-prompt"]The signed content descriptor. Absent means the decision reads no content facts, and a feature that reads one then routes to its error slot — which may not allow.
object
The content descriptor, serialised. Signed as these exact bytes — there is no canonicalisation step, because a canonicalising scheme is a second parser that has to agree with the first.
Example
{"article_id":"a-1","section":"news","paywall_level":"2"}Base64url HMAC-SHA256 of payload under this site’s content key.
Unverified passthrough. Never merged with the signed facts, and a rule that allows on one of these is a bypass the organization wrote itself.
object
Who this decision is for. Accepted on the keyed route only, and refused outright (403) on /edgekit/v1/decide — a site-pinned key is a server credential and asserting an identity is what one is for, while a browser asserting one would let anyone on the page decide as any user. On the browser route the identity comes from the signed access cookie and nowhere else.
Example
0199a4c2-7b1e-7c3d-9a4f-2e8b5d6c1a90The page’s ?_gt gift-link token, verbatim. Verified here with no read; an invalid, forged, expired or overlong token reads as no gift and the answer is still 200.
Responses
Section titled “Responses”One entry per feature that resolved.
object
object
object
The id the site switches on.
The winning outcome’s own named fields, flattened to plain JSON — never a stringified blob. Absent for a plain outcome, where the id is the whole answer. A ref field resolves to a curated catalog object (never a bare id) carrying exactly the keys its select named.
Present iff the matched outcome declared one. allow/deny is an access verdict; redirect names a destination (see GET /decide/forward); data is neither. Open: accept a member not listed.
The meter reads this feature’s rules made, keyed by meter name, as read — before this request’s own ticks. allowance - count is what was left going in. Absent when no rule read a meter.
object
object
Names that were asked for and not answered — unpublished, disabled, or not running on this site. Reported rather than thrown: one typo must not fail the other five in the batch.
Whether this answer came from the preview document rather than the live one.
Present and true when this request carried a session cookie but no usable signed access cookie, so the answer above was decided with no grants. Call GET /v1/id/get-session to refresh the access cookie, at most once per decide: the flag only checks that a session cookie is present and never verifies it, so refetching without a limit on a forged cookie never ends. Absent means nothing is wrong.
This batch’s deduped, edgekit-owned experiment exposures — one per experiment, not per feature, and never a vendor-assigned one (the vendor’s own SDK already recorded that). Collection stays client-side: fire each of these as a trackSelfDescribingEvent against iglu:cloud.edgekit/experiment_exposure/jsonschema/1-0-0, tagged preview from this same response’s preview field if you need to exclude it at analysis time — nothing here writes it for you.
object
Example
{ "decisions": { "additionalProperty": { "type": "allow" } }}Invalid request.
object
object
Example
{ "error": { "code": "unauthorized", "message": "Invalid or missing API key." }}Not allowed.
object
object
Example
{ "error": { "code": "unauthorized", "message": "Invalid or missing API key." }}Resource not found.
object
object
Example
{ "error": { "code": "unauthorized", "message": "Invalid or missing API key." }}Request body too large.
object
object
Example
{ "error": { "code": "unauthorized", "message": "Invalid or missing API key." }}Request validation failed.
object
object
Example
{ "error": { "code": "unauthorized", "message": "Invalid or missing API key." }}Too many requests. Retry-After says when to retry.
object
object
Example
{ "error": { "code": "unauthorized", "message": "Invalid or missing API key." }}Unexpected error.
object
object
Example
{ "error": { "code": "unauthorized", "message": "Invalid or missing API key." }}With Retry-After, briefly unavailable; without it, not configured.
object
object
Example
{ "error": { "code": "unauthorized", "message": "Invalid or missing API key." }}