Docs
Use this when the decision must live in your code — paywalls, pricing, auth, personalization — not in page content. Two endpoints, no SDK, no client: your backend asks which variant a visitor is in, serves it, and reports the outcome.
Looking for the reasoning, not the API? Read the server-side A/B testing guide — this page is the API reference layer.
/api/assign from your backend with a stable visitor id (session id, user id, or a first-party cookie — never something that changes per request):curl -X POST https://a66e659b628ef51d9c6d61167f0a3809.ctonew.app/api/assign \
-H 'Content-Type: application/json' \
-d '{"testKey":"YOUR_TEST_KEY","visitorId":"user-1234"}'{
"testId": "…",
"testName": "Checkout paywall",
"testStatus": "running",
"variants": [{ "key": "A", "name": "A", "isControl": true }],
"variantKey": "B",
"variantName": "B",
"inTest": true,
"reason": "new"
}inTest:false with variantKey:null means serve your default (see failure semantics below).variantKey when inTest is true:const res = await fetch("https://a66e659b628ef51d9c6d61167f0a3809.ctonew.app/api/assign", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ testKey: "YOUR_TEST_KEY", visitorId }),
});
const a = await res.json();
const showPaywallB = a.inTest && a.variantKey === "B";curl -X POST https://a66e659b628ef51d9c6d61167f0a3809.ctonew.app/api/events \
-H 'Content-Type: application/json' \
-d '{"testKey":"YOUR_TEST_KEY","visitorId":"user-1234","type":"conversion"}'Returns 201 with {"event":{"eventId":"…","variantKey":"B","inTest":true,"assignmentCreated":false}}. An optional eventName (max 120 chars) labels the conversion in the dashboard's event breakdown — e.g. send "form_fill" for a form-fill goal.testKey + visitorId (SHA-256) against the configured traffic split. Same inputs, same answer — a returned visitor is in the same variant they were in.visitorId stable for the same human across requests. Losing it means a fresh bucket draw (and a new exposure).The reason field tells you why. Your safe default is always: serve the control / default behavior and never block the request.
| reason | Meaning | Your code should |
|---|---|---|
| new | First assignment for this visitor. | Serve the returned variant. |
| sticky | Returning visitor; stored assignment still holds. | Serve the same variant as before. |
| winner | A winner was declared — everyone in-test gets it. | Serve the winning variant. |
| holdout | Visitor fell outside the test's total traffic. | Serve your default. |
| draft / scheduled / ended / outside_schedule | Test isn't active (status or schedule window). | Serve your default; re-check later if you cache. |
testKey → 404. Malformed body → 400. Treat both as "no experiment" and serve your default.variantKey both times (stickiness).inTest:false, reason:"ended", and your code serves the default.Client-side testing (headlines, copy, layout) doesn't need any of this — install the snippet instead.