Ask for a decision
Your site asks for features by name, up to 32 in one request, and gets one decision for each. A feature never runs until a request names it, so saving or deploying a feature changes nothing on a page that does not ask for it.
There are two routes, and each takes one kind of credential:
| Route | Who calls it | Credential | Who the decision is for |
|---|---|---|---|
POST /edgekit/v1/decide |
Your pages, through /edgekit/ on your own domain |
None. The page’s origin must be in the site’s allowed origins | The user the sign-in cookie names, else the anonymous device |
POST /v1/decide |
Your servers | An API key pinned to the site | The userId your server sends, else an anonymous device |
Both routes take the site in the x-edgekit-site-id header and answer the same way. The full schema is in the
decide API reference.
From a page
Section titled “From a page”The browser client, createClient, sends this request for
you with its decide method. Without it, post to /edgekit/v1/decide on your own domain, so the sign-in cookie goes
with the request:
const response = await fetch('/edgekit/v1/decide', { method: 'POST', credentials: 'include', headers: { 'content-type': 'application/json', 'x-edgekit-site-id': 'site_example', }, body: JSON.stringify({ features: ['paywall', 'subscribe-prompt'], context: { article_id: 'a-1', section: 'news' }, }),});
const { decisions, unknown } = await response.json();The same request with cURL, against your tenant host. It carries no cookies, so it is decided for an anonymous device:
curl -X POST https://your-tenant-host/edgekit/v1/decide \ -H 'content-type: application/json' \ -H 'x-edgekit-site-id: site_example' \ -H 'Origin: https://www.example.com' \ -d '{"features":["paywall","subscribe-prompt"],"context":{"article_id":"a-1","section":"news"}}'This route refuses a userId with 403 forbidden, in the body or in the query string. The user comes from the
sign-in cookie and nowhere else. Setting up /edgekit/ on your domain is covered in
Mount EdgeKit on your site’s own domain.
From your server
Section titled “From your server”Send an API key pinned to the site, as Authorization: Bearer <key> or in x-api-key. To decide for a signed-in
user, send their id as userId:
curl -X POST https://your-tenant-host/v1/decide \ -H 'authorization: Bearer YOUR_API_KEY' \ -H 'content-type: application/json' \ -H 'x-edgekit-site-id: site_example' \ -d '{"features":["paywall"],"userId":"0199a4c2-7b1e-7c3d-9a4f-2e8b5d6c1a90"}'const response = await fetch('https://your-tenant-host/v1/decide', { method: 'POST', headers: { authorization: `Bearer ${process.env.EDGEKIT_API_KEY}`, 'content-type': 'application/json', 'x-edgekit-site-id': 'site_example', }, body: JSON.stringify({ features: ['paywall'], userId }),});EdgeKit takes userId at face value and reads that user’s current grants. Take it from a sign-in your server has
checked, never from a value on the incoming request. Without userId, the decision is for an anonymous device.
createServerClient is the server client that sends
this request.
At a site where users sign in only through your own identity provider, a userId is refused with
422 unprocessable_entity, This site cannot take an asserted userId.
The request
Section titled “The request”| Field | Required | What it does |
|---|---|---|
features |
Yes | 1 to 32 feature names. A name asked twice is decided once |
context |
No | An object of facts about the page. See Page facts in context |
content |
No | Signed content facts. No rule can test them, and a request carrying them answers 503 unless EdgeKit has set them up for your organization. See Troubleshooting |
userId |
No | Server route only. Who the decision is for |
giftLink |
No | The page URL’s _gt value, as it is. See Gift links |
The body may be at most 64 KB. A body that does not match this shape gets 422 unprocessable_entity,
Request validation failed.
The response
Section titled “The response”{ "decisions": { "paywall": { "outcome": "metered", "type": "allow", "reason": "meter", "meters": { "articles": { "count": 2, "allowance": 3 } } }, "subscribe-prompt": { "outcome": "promote", "reason": "paywall", "value": { "label": "Read in full" } } }, "unknown": [], "preview": false, "exposures": []}decisionshas one entry for each feature that answered:outcomeis theidyour page switches on. It isnull, withreasonerror, when the slot that answered returns no outcome.typeis the outcome’s access type, when it has one:allowordenysay whether the page opens, anddataandredirectare not about access. Treat a value you do not know as no type.reasonis the outcome’s reason, from a fixed set your reporting can group on.reasonDetailis your own word for it, when the outcome has one.valueholds the outcome’s fields. It is absent for an outcome with no fields. See Fields.metersholds each meter the feature’s rules read, with thecountand theallowanceas read before this request counted anything.allowance - countis what was left. It is absent when no rule read a meter.
unknownlists the names that got no decision: not deployed, left out of the deployment, or not running on this site. All three look the same, so a typo in one name never fails the others.previewisfalsefor every request your site makes.exposureslists the experiment arms this request exposed, one per experiment. Send each as an analytics event. The browser and server clients’decideresults leave it out, so read it from the response itself. See Exposures.staleIdentityis present andtruewhen the request had a sign-in session but no usable access cookie, so it was decided with no grants. CallGET /edgekit/v1/id/get-sessiononce to refresh the cookie, and the next decide uses it. The browser client does this for you in the background, at most once per client.
Every call counts
Section titled “Every call counts”Each decide request counts on the meters its winning rules count. A retry of a request that already got an answer
counts again on a count meter. A seen-set meter counts one key once, so asking again about the same article
costs nothing. See How a meter counts.
By default, a user who holds any live grant counts on no meter except a gift link’s.
Limits
Section titled “Limits”| Route | Limit |
|---|---|
POST /v1/decide |
2,000 requests per 10 seconds per API key |
POST /edgekit/v1/decide |
200 requests per 10 seconds per client per site |
GET /v1/decide/forward |
200 requests per 10 seconds per client per site |
The browser route, /decide/forward on either path and creating a gift link from a page share one count per client
per site. The API key routes, creating a gift link included, share one count per key.
Past the limit, decide answers 429 too_many_requests, Too many decide requests., with Retry-After: 10. The
browser client waits 2 seconds for an answer and the server client 5. Neither retries for you, and neither throws for
a failed decision: every feature you asked for answers unavailable, and your page shows what it shows for that
state. See Troubleshooting.
Page facts in context
Section titled “Page facts in context”context is the only way a browser can describe the page it is asking about, such as the article’s id or section.
Rules read it as context.*, exactly as sent:
- Anyone can set any value. A rule that opens a page because of a
contextvalue can be opened by anyone who sends that value. Stage warns on every rule that reads one. - It never mixes with signed content facts.
context.sectionandsigned.sectionare two different facts. - The console’s rule builder does not list
context.*facts. A feature you author in the console tests only the facts in Revenue › Decision Engine › Facts.
No rule can test a signed content fact either, so no rule has a fact about the page that a user cannot change. See
content in The request.
Grants are as fresh as the access cookie
Section titled “Grants are as fresh as the access cookie”From a page, decide reads the user’s grants from a signed access cookie, not from your organization’s database. The grants in the cookie are read again whenever the page reads the sign-in session. Each decide extends the cookie by 15 minutes. It is refused 15 minutes after it was last written or extended, and in any case an hour after its grants were read. A grant that changes elsewhere, such as a purchase on another device or a revoked grant, reaches decisions in that browser the next time the page reads the sign-in session, and at most an hour later.
From your server, and at a site where users sign in only through your own identity provider, every request reads the user’s current grants.
Journey features and redirects
Section titled “Journey features and redirects”A journey feature returns only redirect outcomes. Instead of asking for it with POST, your page sends the user to:
curl -i 'https://your-tenant-host/v1/decide/forward?feature=registered&site=site_example'The route answers 302 with the destination in Location. Use it as a link or with location.assign, never with
fetch. It is served under /edgekit/v1/decide/forward too, takes no API key and no origin check, and reads the user
from the sign-in cookie. It refuses a userId with 403 forbidden. A journey feature counts on no meter, so
following the link again costs nothing.
The destination must be on one of the site’s redirect origins. The console does not create journey features or
redirect outcomes. A feature that is not a deployed journey feature on the site answers 404 not_found.