Contents

Resolve from your app

Send the visitor’s facts and a ruleset. The React helper takes a rulesetPublishedKey the way it already takes a widget published key. In Development, a dev. token evaluates the live draft. In Staging and Production, a published-key token on widget-cache evaluates the snapshot and returns the widget in one hop.

What you can call today

The homepage hero from the overview is four widgets and a ruleset. Your app should not pick among those published keys. It should send the visitor's locale, plan, and campaign, and render whichever widget comes back.

There are two URLs. The token picks which one you use.

DevelopmentStaging / Production
Hosthttps://rulecms.comhttps://widget-cache.rulecms.com
PathPOST /api/v1/c/dev/widget/resolvePOST /api/v1/c/widget/resolve
TokenStarts with dev.A Staging or Production token — no dev. prefix
You sendThe ruleset key (ruleset-…)The ruleset published key ({env}---ruleset-…)
It evaluatesThe live draft. Save, call again, see it.The last published snapshot. Unpublished heads are not found.
You get backThe draft widget, plus a resolution objectThe published widget payload, plus a selection object

If the site already uses @rulecms/widget-react, pass the ruleset published key the way you used to pass a widget published key. The helper POSTs for you and renders whichever widget won. On a WordPress or plain HTML page, the HTML embed takes the same pair as attributes. If you are on neither, POST the same body yourself — the payload you already render for a widget get is the same shape, with one extra field that says why this widget won.

Do not send a dev. token to widget-cache, and do not send a Staging token to the Development path. Each host will refuse the other. Create tokens on the environment under Projects, Environments & Tokens.

Staging and Production

Publish the ruleset first — see Publish, promote, and history. Copy the published key from View published. It looks like abc123---ruleset-…: the environment id, three dashes, then the same key you see in the editor.

POST https://widget-cache.rulecms.com/api/v1/c/widget/resolve. The body is JSON. Send it as Content-Type: text/plain so a browser does not have to preflight, or as application/json from a server. No other custom headers. The body must stay under 16 KiB.

POST https://widget-cache.rulecms.com/api/v1/c/widget/resolve
Content-Type: text/plain

{
  "token": "…",
  "rulesetPublishedKey": "{envId}---ruleset-…",
  "params": {
    "locale": "de-DE",
    "user": { "plan": "pro" },
    "campaign": "spring-sale"
  }
}

params is optional. Omit it when you want the default widget — a useful smoke test that the token and published key work. Nested objects flatten on the server: user.plan is what you declared, and { "user": { "plan": "pro" } } is what you send.

mode is optional. Leave it off (or send "payload") to get the widget. Send "key" when you only want the decision — a server that already caches payloads, or an analytics check.

Why text/plain is listed first: a cross-origin POST with application/json makes the browser send OPTIONS first. text/plain does not. The characters inside are still JSON. A different content type is 415. A body that is not JSON is 400. Larger than 16 KiB is 413, before the server even reads it.

What comes back (Staging / Production)

A 200 looks like the widget get you already render, plus data.selection.

FieldWhat it is
selection.outcomerule when a rule matched, default when nothing did, or fallback-default when the matched widget's payload was missing and the default was served instead.
selection.ruleIdThe winning rule's id (r_…), or empty when the default fired.
selection.widgetKeyThe widget key that was selected.
selection.widgetPublishedKeyThat widget's published key in this environment — the same key a hard-coded get would have used.
data.widgetThe published payload, same shape as GET /api/v1/c/widget/get. Omitted when mode is key.

Responses are not cached (Cache-Control: private, no-store). The decision can change when you publish, and it can depend on time. A header X-Selection repeats the winning rule id (or default) so you can see it without opening the body.

fallback-default means a rule won and then the widget it named could not be fetched — unpublished or deleted after the ruleset was published. The visitor still sees the default. Fix the widget or the rule; do not treat this as “nothing matched.”

When Staging / Production does not resolve

You seeWhat it usually means
401 missing tokentoken is absent or empty.
404 invalid tokenUnknown or disabled. A token you just created can look unknown for about a minute if something already asked about that string — try again shortly.
403 environment mismatchThe token's environment is not the one in the published key. A Staging token cannot resolve a Production ruleset.
404 ruleset not foundNo active published snapshot. Publish, or you unpublished it.
404 widget not foundThe chosen widget and the default are both missing as published payloads.
405You GET'd. This route is POST only.

Development

If you already preview a Development widget with a dev. token and a widget key, you know the shape: Development integration returns the live draft, not a published snapshot. Ruleset resolve is the same idea with one extra step. You send the token, the ruleset key, and the visitor parameters. RuleCMS evaluates the draft and returns the chosen Development widget in the same payload you already render.

You do not publish the ruleset for this path. Save the draft, keep the widgets in Development, and call resolve. Composer edits on the chosen widget show up the same way they do for a direct Development widget get.

You need the ruleset key from the editor header — ruleset-…, not the dashboard id and not a published key.

POST https://rulecms.com/api/v1/c/dev/widget/resolve
Content-Type: text/plain

{
  "token": "dev.…",
  "rulesetKey": "ruleset-…",
  "params": {
    "locale": "de-DE",
    "user": { "plan": "pro" },
    "campaign": "spring-sale"
  }
}

now is optional here (and on dry-run). Pass an ISO-8601 string when you are replaying a sale window in a test. Leave it off in the real app so the engine uses the current clock. Staging / Production ignore a client now.

A 200 looks like a Development widget get, plus a resolution object: outcome (rule or the default), ruleId, widgetKey, and a trace of the first-match walk. Rules after the winner are not in the trace. Responses are not cached (Cache-Control: no-store).

You seeWhat it usually means
400 dev_onlyThe token is not a dev. token. Staging and Production tokens belong on widget-cache, not here.
403The token is bound to Staging or Production. Use a Development token.
404 not_foundUnknown token, unknown ruleset key, or the draft is gone.
404 widget_not_foundThe winning key does not exist as a draft widget in this environment.
422 ruleset_invalidThe draft does not compile — missing default, bad operator, broken shape. The body includes the issue list.

Development evaluates the draft, not the last published snapshot. A teammate can save a broken default and your next resolve fails even though last week's publish still looks fine on the published page. Dry-run in the editor before you share a token.

With the React helper

@rulecms/widget-react 22.27.0 and later takes a ruleset the same way it takes a widget. Keep the token on RuleCMSWidgetProvider (or pass it on the widget). Swap the published widget key for the ruleset published key, and send the facts the rules read.

import { RuleCMSWidget } from "@rulecms/widget-react";

<RuleCMSWidget
  rulesetPublishedKey={process.env.NEXT_PUBLIC_HERO_RULESET}
  params={{ locale, path: location.pathname, user: { plan } }}
  onSelection={(selection) => analytics.track("widget_selected", selection)}
/>

params is any JSON object. The helper sends it as-is; the server flattens nested keys the way dry-run does (user.plan). A new object with the same values does not refetch. onSelection is optional — it tells you which rule won, or that the default did.

A dev. token still goes to rulecms.com and evaluates the live draft. You can pass the bare ruleset-… key or the published key; the helper strips the environment prefix for that path.

Unless you pass params.anonId or anonymousId={false}, the helper writes a UUID to first-party localStorage under rulecms_anon_id and sends it as params.anonId. That is how experiments will bucket a visitor later. It is not sent anywhere else, but it is a persistent identifier: if your consent banner requires opt-in, render with anonymousId={false} until they agree, and clear that key when they withdraw. Server helpers never invent an id — pass your own if you want the same visitor on the server and in the browser.

fallbackPublishedKey is optional. The helper fetches that widget with the ordinary GET only when resolve fails on the network or with a 5xx. A 4xx — bad token, unknown ruleset — stays an error. The server default already covers “no rule matched.”

On the server, fetchRuleCMSWidget and RuleCMSWidgetServer take the same rulesetPublishedKey + params pair. The request is a POST, so Next.js will not cache it and fetchOptions.next.revalidate does nothing on that call.

With the HTML embed

<rulecms-widget> 0.4.0 and later takes a ruleset the same way it takes a widget. Pin the versioned script from the HTML custom element page, then swap published-key for the ruleset published key. params is a JSON object in the attribute — same facts you would send from React.

<rulecms-widget
  token="YOUR_STAGING_OR_PRODUCTION_TOKEN"
  ruleset-published-key="ENVIRONMENT_ID---ruleset-…"
  params='{"locale":"de-DE","user":{"plan":"pro"}}'
></rulecms-widget>

Do not set both published-key and ruleset-published-key. A params value that is not a JSON object shows an alert instead of fetching. Optional: fallback-published-key (GET only on network / 5xx) and anonymous-id="false" if your consent banner must opt in before the helper writes rulecms_anon_id.

RuleCMS.mount takes the same union: publishedKey or rulesetPublishedKey plus an object params. Details and host-specific wiring live on HTML custom element.

A tiny client loop

If you are not on the React helper, POST yourself. In Staging or Production, treat the published key as the slot name you used to hard-code a widget published key.

async function resolveHero(params) {
  const res = await fetch("https://widget-cache.rulecms.com/api/v1/c/widget/resolve", {
    method: "POST",
    headers: { "Content-Type": "text/plain" },
    body: JSON.stringify({
      token: process.env.NEXT_PUBLIC_RULECMS_TOKEN,
      rulesetPublishedKey: process.env.NEXT_PUBLIC_HERO_RULESET,
      params,
    }),
  });
  if (!res.ok) throw new Error(await res.text());
  const body = await res.json();
  return body.data.widget;
}

In Development, swap the host to https://rulecms.com, the path to /api/v1/c/dev/widget/resolve, the token to a dev. token, and rulesetPublishedKey to rulesetKey. The widget you hand to the renderer is data.widget on widget-cache and widget on the Development path.

Pass { locale, user: { plan }, campaign } from the same place you already know those facts — the router, the session, a query string. Then hand the widget to the same renderer you use for a direct Development get or a published widget get.