Tool Reference
All tools require a valid Authorization: Bearer sk_live_... header. Credits are deducted atomically before execution — if balance is insufficient, a 402 error is returned.
The gateway exposes 14 tools — creating a site, listing/analyzing sites, generating and managing A/B test ideas, generating variant code from natural-language prompts, creating / launching / inspecting experiments, and starting / controlling / inspecting always-on Continuous Loops.
Typical flows
Before calling any site-scoped tool (everything except list_sites and create_site) make sure the workspace has a registered site. A tool call against a missing site_id returns an actionable error telling the agent what to do next.
One-shot: prompt → running test (site already registered, 15 credits)
When speed matters more than iteration:
create_experiment({
site_id, name,
prompt: "Change hero price from $99 to $499 and update the Stripe link",
target_url: "https://mysite.com/pricing",
auto_launch: true
}) → 5 create + 10 inline generate = 15 credits
Server authors the variant, creates the experiment, and launches it in one call. If the page can't be resolved, returns needs_clarification and nothing is created.
Two-step: prompt → preview → create (site already registered, 17 credits)
When the agent wants to show the variant to the user before committing:
generate_variant({ site_id, prompt, target_url }) → 10 credits (mutations + hypothesis)
create_experiment({ site_id, name,
variants: [{ changes: variant.mutations }], goals, hypothesis })
→ 5 credits (draft)
launch_experiment({ experiment_id }) → 2 credits (running)
New-site path (22 credits, one-shot variant)
list_sites → 1 credit (confirm what's there)
create_site({ url, reuse_if_exists: true }) → 5 credits (idempotent for automation)
create_experiment({ site_id, name, prompt, target_url, auto_launch: true })
→ 15 credits
From a pre-generated idea (legacy path, 7 credits)
list_ideas({ site_id }) → 1 credit
create_experiment({ site_id, idea_id, name, goals }) → 5 credits (variant.mutations pulled from the idea automatically)
launch_experiment({ experiment_id }) → 2 credits
Continuous loops (always-on champion-challenger optimization)
A Continuous Loop is a long-running orchestrator that keeps the current winner as champion, authors a new challenger each round via LLM, runs a fresh A/B test for ≥7 days with ≥500 samples/arm, and promotes the challenger when P(challenger > champion) ≥ 0.95. Loops are paid-plan only (Free is blocked with plan_required); v1 only varies headline and copy (image / layout return coming_soon).
create_loop is two-phase — the agent first asks for seed-idea candidates, then calls again with the user's choice to actually launch round 1:
create_loop({ ... }) → returns seed ideas (15 credits, NOT debited if it errors out before launch)
create_loop({ ..., selected_idea_id: "..." }) → launches round 1 (debited)
get_loop_status({ loop_id }) → poll for decision (2 credits)
pause_loop / resume_loop → manual control (2 credits each)
After launch the loop ticks itself hourly via cron — agents don't drive it forward, they just poll. get_loop_status returns the live loop config, current round, last 10 rounds of history, and the loop's current decision (wait / hold / promote / inconclusive) including days_needed and samples_needed before the next decision can be made.
Guardrails the service enforces
- Missing
site_idon a site-scoped tool → 400site_id is required. - Unknown
site_id→ 404 with either "No sites registered in this workspace. Callcreate_sitefirst." (zero-sites case) or "Calllist_sites… orcreate_siteto add a new one." (some sites, just not this one). - Duplicate domain on
create_site→ 409 withexisting_siteincluded in the error so the agent can reuse it without a second round-trip. Passreuse_if_exists: trueto convert that to a 200 withreused: true. - Target page on a different domain than the registered site → the variant still returns, but
context.warningsincludes a mismatch notice so the agent can flag it to the user (most often a typo or wrongsite_id). - Launch preconditions — experiment needs ≥2 variants (control + ≥1 test) and ≥1 goal. Otherwise
launch_experimentreturns a 400 telling you exactly what's missing.
create_site
Register a new website in your workspace for A/B testing. Returns the site ID along with the SplitKit tracking snippet and install instructions — the snippet must be added to the <head> of the target site before experiments can run.
Subject to the workspace's plan-based site limit (Starter: 1, Pro: 10, Enterprise: 25). Duplicate domains return a conflict response that includes the existing site — the agent can reuse it without a second call. Pass reuse_if_exists: true to treat duplicates as successful no-ops (idempotent behaviour, recommended for automation).
Credits: 5 Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
url |
string | yes | The website URL (e.g. https://example.com). Protocol is added if missing. |
name |
string | no | Display name. Defaults to the domain (e.g. example.com). |
reuse_if_exists |
boolean | no | Default false. When true, a duplicate-domain call succeeds (200) with { reused: true, site } instead of returning a conflict. |
Duplicate response (conflict, default behaviour):
{
"error": "A site for mystore.com is already registered (ID: abc-123). Reuse it via list_sites / its id, or call create_site again with reuse_if_exists: true for idempotent behavior.",
"code": "conflict",
"existing_site": { "id": "abc-123", "name": "My Store", "url": "https://mystore.com/", "public_key": "39cfc..." }
}
Duplicate response (reuse_if_exists: true):
{
"reused": true,
"site": { "id": "abc-123", "name": "My Store", "url": "https://mystore.com/", "public_key": "39cfc..." },
"snippet": "<script src=\"https://splitkit.dev/s/39cfc...js\"></script>",
"instructions": "Site \"My Store\" (https://mystore.com/) is already registered (ID: abc-123). Reusing it."
}
Example (MCP):
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "create_site",
"arguments": { "url": "https://mystore.com", "name": "My Store" }
}
}
Response:
{
"site": {
"id": "ca7ee226-ebf1-4485-b993-a61c82464b35",
"name": "My Store",
"url": "https://mystore.com/",
"public_key": "39cfc3dd2e26d9452e545073b7fdb29f"
},
"snippet": "<script src=\"https://splitkit.dev/s/39cfc3dd2e26d9452e545073b7fdb29f.js\"></script>",
"instructions": "Site \"My Store\" created successfully (ID: ca7ee226-...).\n\nAdd this snippet to the <head> of https://mystore.com/, before any other scripts:\n\n<script src=\"https://splitkit.dev/s/39cfc3dd2e26d9452e545073b7fdb29f.js\"></script>\n\nThe script is synchronous and ~5KB gzipped — placing it first in <head> prevents A/B variant flicker.\nOnce installed, call generate_ideas to generate AI-powered test ideas for this site."
}
After creating a site, the typical flow is: call generate_ideas with the returned site.id to produce AI-informed test ideas, then create_experiment to launch a test from one of them.
list_sites
List all websites registered in your workspace.
Credits: 1
Parameters: none
Example (MCP):
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "list_sites",
"arguments": {}
}
}
Response:
{
"sites": [
{
"id": "uuid",
"name": "My Store",
"url": "https://mystore.com",
"platform": "shopify",
"public_key": "39cfc3dd2e26d9452e545073b7fdb29f",
"crawl_status": "complete",
"created_at": "2025-01-01T00:00:00Z"
}
]
}
get_site_intelligence
Get AI-analyzed intelligence for a site: industry, business model, target audience, value propositions, competitors, and top keywords.
Credits: 2
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
site_id |
string | yes | From list_sites |
Response:
{
"intelligence": {
"industry": "e-commerce",
"business_model": "DTC",
"target_audience": "...",
"value_propositions": ["..."],
"brand_tone": "friendly",
"pain_points": ["..."],
"updated_at": "..."
},
"keywords": [
{ "keyword": "natural skincare", "tier": "primary", "relevance_score": 0.95 }
],
"competitors": [
{ "domain": "competitor.com", "name": "Competitor", "confidence_score": 0.8 }
]
}
list_ideas
List A/B test ideas for a site.
Credits: 1
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
site_id |
string | yes | From list_sites |
status |
string | no | new | approved | implemented | rejected | archived |
limit |
number | no | Max results, default 20, max 100 |
Response:
{
"ideas": [
{
"id": "uuid",
"title": "Test CTA button color",
"hypothesis": "Changing from grey to green will increase CTR by 15%",
"category": "cta",
"priority": "high",
"status": "new",
"estimated_impact": "high",
"created_at": "..."
}
]
}
generate_ideas
Use AI to analyze a site and generate new CRO-informed A/B test ideas. Makes a live AI call — allow 15–30 seconds.
Credits: 10
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
site_id |
string | yes | From list_sites |
num_ideas |
number | no | Ideas to generate, default 5, max 10 |
Response:
{
"success": true,
"ideas": [
{
"id": "uuid",
"title": "...",
"hypothesis": "...",
"category": "layout",
"priority": "medium"
}
]
}
generate_variant
Generate A/B test variant code from a natural-language prompt. This is the path for creating experiments without going through the idea-generation flow — useful when you (or the user behind the agent) already know what you want to test.
The service:
- Resolves which page to target (from
target_url, or extracted from the prompt, or a homepage hint) - Fetches the live DOM (JS-rendered via our browser worker, cached for 7 days)
- Detects the tech stack (Shopify, Next.js, Webflow, Stripe Checkout, etc.)
- Pulls your site intelligence, top keywords, and competitors
- Calls an LLM in JSON mode with all of that context + your prompt
- Validates the output (selectors are checked against the fetched HTML; unknown selectors emit warnings)
Pass the returned variant.mutations directly into create_experiment as variants[0].changes.
Credits: 10 (same class as generate_ideas — includes HTML fetch + LLM call). Charged whether the response is a variant or a needs_clarification. Enterprise workspaces: unmetered.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
site_id |
string | yes | From list_sites or create_site |
prompt |
string | yes | Plain-English description of the test (min 10 chars). Include full URLs for any Stripe products, competitor references, etc. |
target_url |
string | no | Specific page URL the change applies to. If omitted, we try to extract a URL from the prompt; if still ambiguous, the response is needs_clarification instead of a variant. |
Example (MCP):
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "generate_variant",
"arguments": {
"site_id": "e0e3f741-3314-4e88-925f-327b08ecf9d2",
"target_url": "https://mysite.com/pricing",
"prompt": "Change the hero price from $99 to $499 and update the CTA link to the Stripe Pro-plan checkout at https://buy.stripe.com/pro_abc123"
}
}
}
Response (happy path):
{
"variant": {
"name": "Premium Pricing with Pro Stripe Link",
"mutations": [
{
"selector": ".plan--pro .price",
"action": "set_content",
"payload": "$499",
"description": "Replace the displayed price"
},
{
"selector": ".plan--pro a.cta",
"action": "set_attribute",
"attribute_name": "href",
"payload": "https://buy.stripe.com/pro_abc123",
"description": "Swap to the higher-tier Stripe product"
}
],
"redirect_url": null
},
"suggested_goals": [
{ "name": "Pro checkout click", "type": "click", "selector": ".plan--pro a.cta" }
],
"hypothesis": "Raising the price from $99 to $499 positions the product as premium; if the lift in ACV exceeds the conversion drop, revenue per visitor increases.",
"context": {
"page_analyzed": "https://mysite.com/pricing",
"page_fetched_at": "2026-04-24T...",
"tech_stack": "Next.js, Stripe",
"url_pattern": null,
"assumptions": ["Targeted .plan--pro as the primary pricing card"],
"warnings": []
}
}
context.url_pattern is the LLM's inferred glob scope. It's null when the test targets a single page. When the prompt implies a class of pages ("all report pages", "every /checkout/*", "the pricing page and its variants"), the LLM fills this — pass it through to create_experiment so the snippet only fires on matching pages. You can also override it explicitly when calling create_experiment.
Response (clarification needed — no URL, no homepage hint):
The clarification is a structured object so an agent can render a form field, dropdown, or suggestion chips without parsing English:
{
"needs_clarification": {
"retry_hint": "Re-send the same call with target_url set to one of the suggestions below (or any specific URL on your site where this change should apply).",
"questions": [
{
"field": "target_url",
"prompt": "Which page URL should this test target?",
"reason": "The prompt didn't include a URL and the service couldn't infer one from context (no 'homepage' / 'landing' hint either). We need the exact page so we can fetch its DOM and author selectors that actually exist.",
"input_type": "url",
"required": true,
"suggestions": [
"https://mysite.com/",
"https://mysite.com/pricing",
"https://mysite.com/checkout",
"https://mysite.com/about"
]
}
]
}
}
ClarificationQuestion shape:
| Field | Type | Description |
|---|---|---|
field |
string | The API parameter the answer should be set on when retrying (e.g. target_url). |
prompt |
string | Human-readable question — safe to show as-is in a chat bubble. |
reason |
string | Why the server is asking — render as secondary/muted text. |
input_type |
"url" | "text" | "select" | "boolean" |
Hint for rendering the input control. |
required |
boolean | Whether an answer is strictly needed to proceed. |
suggestions |
string[] |
Optional pre-computed options the user can pick from. For target_url, these are mined from the site's cached homepage links plus common CRO paths. |
When you get needs_clarification, ask the user (or pick a suggestion yourself) and call generate_variant again with the field set.
Chaining into create_experiment:
{
"name": "create_experiment",
"arguments": {
"site_id": "<uuid>",
"name": "Pro pricing $499",
"hypothesis": "<paste from generate_variant.hypothesis>",
"variants": [
{
"name": "<paste variant.name>",
"changes": [/* paste variant.mutations verbatim */],
"redirect_url": null
}
],
"goals": [/* paste suggested_goals verbatim */]
}
}
list_experiments
List A/B experiments for a site.
Credits: 1
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
site_id |
string | yes | From list_sites |
status |
string | no | draft | active | paused | completed | archived |
limit |
number | no | Max results, default 20 |
get_experiment
Get full details for an experiment including all variants and goals.
Credits: 2
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
experiment_id |
string | yes | From list_experiments |
create_experiment
Create a new A/B experiment. There are four ways to supply the variant:
- One-shot from a prompt — pass
prompt(and optionaltarget_url). The server runsgenerate_variantinternally, authors the variant, and creates the experiment in one call. Extra 10 credits on top of the 5 for create. Returnsneeds_clarification(no experiment created) if the target page can't be resolved. - From a previous
generate_variantcall — passvariants[0].changes. Lets the agent preview / edit the mutations before committing. - From an existing idea — pass
idea_id(the server pullsmockup_data.mutationsfrom that idea). - Draft — pass none of the above; create an empty experiment to fill in via the dashboard.
Set auto_launch: true to transition the experiment to running right after creation (requires ≥2 variants and ≥1 goal; the response includes a launch field with the outcome). The control variant is always created automatically; traffic_split controls how much traffic goes to the variant (default 50).
Credits: 5 base (10 more if prompt is used). Enterprise plan: unmetered.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
site_id |
string | yes | From list_sites |
name |
string | yes | Experiment name |
prompt |
string | no | Plain-English description. Mutually exclusive with idea_id and variants[].changes. |
target_url |
string | no | Used with prompt to tell the LLM which DOM to analyze, and stored on the experiment to build preview URLs. |
url_pattern |
string | no | Glob scoping the experiment to specific pages. Omit for site-wide. See syntax below. When prompt is used and phrases like "all report pages" / "every product page" appear, the LLM populates this automatically. |
auto_launch |
boolean | no | Default false. If true, launch on create when preconditions pass. |
url_pattern syntax (used here and on goals):
| Pattern | Matches | Doesn't match |
|---|---|---|
| (omit) | every page | — |
/checkout |
/checkout, /checkout/step-2 |
/checkouts |
/report/* |
/report/42, /report/abc |
/report, /report/42/details |
/product/*/reviews |
/product/x/reviews |
/product/reviews |
/blog/** |
anything under /blog/ at any depth |
/blogs |
* = one path segment, ** = any depth. Evaluated in the browser against location.pathname — scheme/host stripped. The snippet skips the experiment entirely on pages that don't match, so you can run homepage tests and /report/* tests side-by-side without them interfering.
| idea_id | string | no | ID of an idea to source mutations from |
| hypothesis | string | no | One-line statement of what you expect and why |
| traffic_split | number | no | % of traffic on variant (default 50) |
| variants | array | no | Up to one variant: [{ name, changes?, redirect_url? }] |
| variants[].changes | array | no | VariantMutation[] — pass generate_variant's variant.mutations verbatim |
| variants[].redirect_url | string | no | For split-URL tests — send variant traffic here instead of mutating DOM |
| goals | array | no | [{ name, type, selector?, url_pattern? }] — first is primary |
Example — one-shot prompt + auto-launch (Claude Desktop):
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "create_experiment",
"arguments": {
"site_id": "e0e3f741-...",
"name": "Pro pricing $499",
"prompt": "Change hero price from $99 to $499 and link the CTA to https://buy.stripe.com/pro_abc123",
"target_url": "https://mysite.com/pricing",
"auto_launch": true
}
}
}
One-shot response when the prompt needs clarification:
Same structured shape as generate_variant — each question has field, prompt, reason, input_type, required, and optional suggestions. Example:
{
"needs_clarification": {
"retry_hint": "Re-send the same call with target_url set to one of the suggestions below (or any specific URL on your site where this change should apply).",
"questions": [
{
"field": "target_url",
"prompt": "Which page URL should this test target?",
"reason": "The prompt didn't include a URL and the service couldn't infer one from context.",
"input_type": "url",
"required": true,
"suggestions": ["https://mysite.com/", "https://mysite.com/pricing"]
}
]
}
}
No experiment is created; 10 credits are still consumed for the HTML fetch + preflight. Retry with the field set to one of the suggestions.
Auto-launch response when preconditions aren't met:
{
"experiment": { "id": "...", "status": "draft", ... },
"launch": { "status": "draft", "error": "auto_launch skipped: experiment has 2 variant(s) and 0 goal(s); launch requires ≥2 variants (control + ≥1 test) and ≥1 goal." }
}
Preview URLs + activation
Every create response includes:
preview_urls[]— one entry per non-control variant, shape{ variant_id, variant_name, url }. The URL is<target_url>?sk_preview=<variant_id>; opening it in a browser applies that variant's mutations to the live page without counting as a tracked visit. Perfect for sharing with a stakeholder before launch.activation_hint— a short string telling the agent how to move the experiment from draft → running.
{
"experiment": { "id": "...", "status": "draft", "target_url": "https://mysite.com/pricing", "variants": [...], "goals": [...] },
"preview_urls": [
{ "variant_id": "var-b-uuid", "variant_name": "Variant B", "url": "https://mysite.com/pricing?sk_preview=var-b-uuid" }
],
"activation_hint": "To launch: pass `auto_launch: true` on create, OR call launch_experiment({ experiment_id }) ... after reviewing the preview URLs below."
}
get_experiment also returns preview_urls so you can fetch them later.
Three ways to activate (draft → running):
| Path | Tool call | When to use |
|---|---|---|
| One-shot | create_experiment({ ..., auto_launch: true }) |
Agent is confident enough to ship directly. If preconditions fail, experiment stays draft and the reason is in launch.error. |
| Review then launch | create_experiment(...) → share preview_urls[0].url with the user → launch_experiment({ experiment_id }) |
Standard agent flow — lets the user see the change live before real traffic splits. |
| Dashboard | Open https://abtestbot.com/experiments/{id} |
Human review + manual launch. |
VariantMutation shape:
{
"selector": "<CSS selector>",
"action": "set_style" | "set_content" | "set_attribute" | "insert_before" | "insert_after" | "add_class" | "remove_class",
"payload": "<value — text, CSS, class name, or attribute value>",
"attribute_name": "<required when action=set_attribute>",
"description": "<optional human-readable note>"
}
Example (chaining from generate_variant):
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "create_experiment",
"arguments": {
"site_id": "<uuid>",
"name": "Premium pricing $499",
"hypothesis": "Raising the price signals quality; lift in ACV exceeds conversion drop.",
"variants": [
{
"name": "Premium Pricing with Pro Stripe Link",
"changes": [
{ "selector": ".plan--pro .price", "action": "set_content", "payload": "$499" },
{ "selector": ".plan--pro a.cta", "action": "set_attribute", "attribute_name": "href", "payload": "https://buy.stripe.com/pro_abc123" }
]
}
],
"goals": [
{ "name": "Pro checkout click", "type": "click", "selector": ".plan--pro a.cta" }
]
}
}
}
Response: full experiment object with generated variants[] (control + your variant) and goals[]. Experiment is created in draft status — call launch_experiment next to start splitting traffic, or let the user launch from the dashboard.
launch_experiment
Transition a draft experiment to running — pushes the variant config to SplitKit's edge KV so the tracking snippet starts serving split traffic immediately.
Credits: 2 Preconditions:
- Experiment must have at least 2 variants (control + ≥1 test variant)
- Experiment must have at least 1 goal
- Experiment must currently be in
draftstatus
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
experiment_id |
string | yes | ID returned by create_experiment |
Example (MCP):
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "launch_experiment",
"arguments": { "experiment_id": "86773746-b2ff-486d-8846-3957c45fc625" }
}
}
Response:
{
"experiment_id": "86773746-b2ff-486d-8846-3957c45fc625",
"status": "running",
"message": "Experiment launched successfully"
}
Typical three-call flow for prompt-driven testing:
generate_variant(site_id, prompt, target_url)— 10 creditscreate_experiment(site_id, name, variants:[{name, changes: variant.mutations}], goals, hypothesis)— 5 creditslaunch_experiment(experiment_id)— 2 credits
Total: 17 credits from prompt → live test.
create_loop
Start a Continuous Loop on a page — an always-on champion-challenger optimizer that runs forever until paused. Each round is a real A/B test with a ≥7-day minimum and ≥500 samples/arm before any winner is declared, and the LLM authors each new challenger using the current champion + every prior loser as context.
Two-phase behavior. This tool is called twice for a single loop launch:
- Phase 1 — seed_ideas (
selected_idea_idomitted): the server callsgenerate-ideasinternally, filtered to the loop'selement_types, and returns 3-5 candidate ideas. No loop row is created — the agent shows the candidates to the user and lets them pick one. - Phase 2 — launch (
selected_idea_idprovided, along withnameandprimary_goal): the server creates theexperiment_loopsrow, callscreate_experimentwith the chosen idea's mutations, callslaunch_experiment, insertsloop_roundsrow #1, and returns the live loop + round + apreview_url.
Both phases cost the same 15 credits. No credits are debited on validation errors that fail before the upstream call (e.g. coming_soon element types, plan_required Free workspace, missing required field). The Phase 1 → Phase 2 hand-off is two separate tool calls and is billed as two separate calls.
Element-type constraint. v1 only supports headline and copy. Passing image or layout returns a coming_soon error (HTTP 400) with no credits charged. The constraint is on the loop config — every challenger the LLM authors over the loop's lifetime stays within these element types.
Plan gate. Free-plan workspaces get a plan_required error (HTTP 402). Loops require Starter / Growth / Scale / Enterprise.
Credits: 15 Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
site_id |
string | yes | The site ID (from list_sites) |
element_types |
array | yes | Element types the loop is allowed to vary. v1 supports headline and copy only; image / layout are coming soon and rejected with coming_soon error. |
name |
string | no in Phase 1, yes in Phase 2 | Display name for the loop. Required when launching. |
target_url |
string | no | Page the loop optimizes (e.g. https://example.com/pricing). Falls back to the site root. |
url_pattern |
string | no | Optional glob constraining which pages the loop runs on. Same syntax as create_experiment.url_pattern. |
selector_hints |
object | no | Optional CSS-selector hints (e.g. { "headline": ".hero h1", "copy": ".hero p" }) used by the LLM challenger author. Omit to let the loop infer from the page. |
primary_goal |
object | no in Phase 1, yes in Phase 2 | Conversion goal to optimize. Same shape as create_experiment.goals[0] — { name, type, selector?, url_pattern? }. |
selected_idea_id |
string | no | OPTIONAL. Phase 1 (omit): server returns 3-5 seed-idea candidates and does NOT create the loop. Phase 2 (provide an idea id returned from phase 1): server creates the loop row + round-1 experiment and launches. |
Example (MCP) — Phase 1, ask for candidates:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "create_loop",
"arguments": {
"site_id": "e0e3f741-3314-4e88-925f-327b08ecf9d2",
"target_url": "https://mysite.com/pricing",
"element_types": ["headline", "copy"]
}
}
}
Phase 1 response:
{
"phase": "seed_ideas",
"ideas": [
{
"id": "7b9d2a3f-1e4b-4a0c-9c8b-1f2a3b4c5d6e",
"title": "Lead with the outcome, not the feature",
"hypothesis": "Replacing the feature-led H1 with an outcome-led one ('Ship 3x more A/B tests') will lift CTA clicks by ~12%.",
"mockup_data": { "mutations": [{ "selector": ".hero h1", "action": "set_content", "payload": "Ship 3x more A/B tests" }] }
},
{
"id": "8c0e3b40-2f5c-4b1d-ad9c-2g3b4c5d6e7f",
"title": "Add quantified social proof to subhead",
"hypothesis": "...",
"mockup_data": { "mutations": [...] }
}
],
"next_step": "Call create_loop again with selected_idea_id, name, and primary_goal to launch."
}
Example (MCP) — Phase 2, launch:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "create_loop",
"arguments": {
"site_id": "e0e3f741-3314-4e88-925f-327b08ecf9d2",
"name": "Pricing page headline loop",
"target_url": "https://mysite.com/pricing",
"element_types": ["headline", "copy"],
"primary_goal": { "name": "Pro checkout click", "type": "click", "selector": ".plan--pro a.cta" },
"selected_idea_id": "7b9d2a3f-1e4b-4a0c-9c8b-1f2a3b4c5d6e"
}
}
}
Phase 2 response:
{
"phase": "launched",
"loop": {
"id": "4a1f5e2c-9b8d-4c3a-bb7e-1d2e3f4a5b6c",
"workspace_id": "...",
"site_id": "e0e3f741-3314-4e88-925f-327b08ecf9d2",
"name": "Pricing page headline loop",
"target_url": "https://mysite.com/pricing",
"element_types": ["headline", "copy"],
"primary_goal": { "name": "Pro checkout click", "type": "click", "selector": ".plan--pro a.cta" },
"status": "running",
"current_round_id": "9d4b7e1a-3c2f-4d5b-8a6e-2f3a4b5c6d7e",
"current_champion_variant_id": "c1a2b3c4-d5e6-7f80-9123-456789abcdef",
"min_round_days": 7,
"max_round_days": 28,
"min_samples_per_arm": 500,
"decision_threshold": 0.95,
"consecutive_inconclusive": 0,
"next_tick_at": "2026-05-23T13:00:00Z",
"created_at": "2026-05-23T12:00:00Z"
},
"round": {
"id": "9d4b7e1a-3c2f-4d5b-8a6e-2f3a4b5c6d7e",
"loop_id": "4a1f5e2c-9b8d-4c3a-bb7e-1d2e3f4a5b6c",
"round_number": 1,
"experiment_id": "86773746-b2ff-486d-8846-3957c45fc625",
"champion_variant_id": "c1a2b3c4-d5e6-7f80-9123-456789abcdef",
"challenger_variant_id": "d2b3c4d5-e6f7-8091-2345-6789abcdef01",
"is_regression_check": false,
"outcome": "pending",
"started_at": "2026-05-23T12:00:00Z"
},
"preview_url": "https://mysite.com/pricing?sk_preview=d2b3c4d5-e6f7-8091-2345-6789abcdef01"
}
The preview_url opens the challenger variant on the live page without counting as a tracked visit — share it with the user so they can see what the loop is testing before any traffic splits.
pause_loop
Pause a running Continuous Loop. The current round's experiment is also paused (KV stops serving split traffic). Idempotent — pausing an already-paused loop is a successful no-op with no_op: true.
Loops in completed or archived status can't be paused (HTTP 400). Pausing does NOT lose round history — resume picks up exactly where it left off.
Credits: 2 Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
loop_id |
string | yes | The loop ID (from create_loop or get_loop_status) |
Example (MCP):
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "pause_loop",
"arguments": { "loop_id": "4a1f5e2c-9b8d-4c3a-bb7e-1d2e3f4a5b6c" }
}
}
Response:
{
"loop": {
"id": "4a1f5e2c-9b8d-4c3a-bb7e-1d2e3f4a5b6c",
"status": "paused",
"current_round_id": "9d4b7e1a-3c2f-4d5b-8a6e-2f3a4b5c6d7e",
"updated_at": "2026-05-23T15:30:00Z"
},
"paused_experiment_id": "86773746-b2ff-486d-8846-3957c45fc625",
"no_op": false,
"message": "Loop paused"
}
Pausing the underlying experiment is best-effort — if the experiment was already completed (e.g. the loop just promoted a winner), paused_experiment_id may be null. The loop is still paused.
resume_loop
Resume a paused Continuous Loop. Sets next_tick_at = now() so the very next cron pass picks it up immediately; also resumes the current round's experiment so KV serves it again. Idempotent — resuming an already-running loop is a successful no-op with no_op: true.
Loops in completed or archived status can't be resumed (HTTP 400). Resuming a loop that was auto-paused by the 3-strikes inconclusive guard also resets consecutive_inconclusive to 0 — the streak doesn't carry across a manual resume.
Credits: 2 Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
loop_id |
string | yes | The loop ID |
Example (MCP):
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "resume_loop",
"arguments": { "loop_id": "4a1f5e2c-9b8d-4c3a-bb7e-1d2e3f4a5b6c" }
}
}
Response:
{
"loop": {
"id": "4a1f5e2c-9b8d-4c3a-bb7e-1d2e3f4a5b6c",
"status": "running",
"current_round_id": "9d4b7e1a-3c2f-4d5b-8a6e-2f3a4b5c6d7e",
"next_tick_at": "2026-05-23T16:00:00Z",
"consecutive_inconclusive": 0,
"updated_at": "2026-05-23T16:00:00Z"
},
"resumed_experiment_id": "86773746-b2ff-486d-8846-3957c45fc625",
"no_op": false,
"message": "Loop resumed"
}
get_loop_status
Read full status for a Continuous Loop without burning credits on a full tick. Returns the loop config, the current round, the last 10 rounds of history, and the live loop decision (wait / hold / promote / inconclusive) including how many more samples and days are needed before the next decision can be made.
The loop's state machine runs on its own hourly cron — get_loop_status only reads that state; it doesn't advance it. Polling this tool every few minutes is the recommended pattern for an agent that wants to react to round transitions.
next_decision_eta.decision mirrors what the loop will do on its next tick:
wait— not enough data yet;samples_needed/days_neededshow what's missinghold— stats favor the champion at the decision threshold; current round will end with the challenger held offpromote— challenger has crossed the decision threshold; next tick swaps the champion and launches round N+1inconclusive— round hitmax_round_dayswithout crossing the threshold; next tick retires the round and authors a fresh challenger
If the current round has no events yet (just-launched loop), next_decision_eta.days_needed and samples_needed may be null — the evaluator needs at least one row of data to compute.
Credits: 2 Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
loop_id |
string | yes | The loop ID |
Example (MCP):
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_loop_status",
"arguments": { "loop_id": "4a1f5e2c-9b8d-4c3a-bb7e-1d2e3f4a5b6c" }
}
}
Response:
{
"loop": {
"id": "4a1f5e2c-9b8d-4c3a-bb7e-1d2e3f4a5b6c",
"workspace_id": "...",
"site_id": "e0e3f741-3314-4e88-925f-327b08ecf9d2",
"name": "Pricing page headline loop",
"target_url": "https://mysite.com/pricing",
"element_types": ["headline", "copy"],
"primary_goal": { "name": "Pro checkout click", "type": "click", "selector": ".plan--pro a.cta" },
"status": "running",
"current_round_id": "9d4b7e1a-3c2f-4d5b-8a6e-2f3a4b5c6d7e",
"current_champion_variant_id": "c1a2b3c4-d5e6-7f80-9123-456789abcdef",
"min_round_days": 7,
"max_round_days": 28,
"min_samples_per_arm": 500,
"decision_threshold": 0.95,
"consecutive_inconclusive": 0,
"rounds_since_check": 2,
"next_tick_at": "2026-05-23T17:00:00Z",
"created_at": "2026-05-15T12:00:00Z"
},
"current_round": {
"id": "9d4b7e1a-3c2f-4d5b-8a6e-2f3a4b5c6d7e",
"loop_id": "4a1f5e2c-9b8d-4c3a-bb7e-1d2e3f4a5b6c",
"round_number": 3,
"experiment_id": "86773746-b2ff-486d-8846-3957c45fc625",
"champion_variant_id": "c1a2b3c4-d5e6-7f80-9123-456789abcdef",
"challenger_variant_id": "d2b3c4d5-e6f7-8091-2345-6789abcdef01",
"is_regression_check": false,
"outcome": "pending",
"started_at": "2026-05-20T12:00:00Z"
},
"history": [
{
"id": "9d4b7e1a-3c2f-4d5b-8a6e-2f3a4b5c6d7e",
"round_number": 3,
"outcome": "pending",
"started_at": "2026-05-20T12:00:00Z"
},
{
"id": "ab12cd34-ef56-7890-abcd-ef1234567890",
"round_number": 2,
"outcome": "challenger_promoted",
"final_prob_to_win": 0.971,
"final_lift": 0.142,
"decided_at": "2026-05-20T11:30:00Z",
"started_at": "2026-05-13T09:00:00Z"
},
{
"id": "cd34ef56-7890-abcd-ef12-34567890abcd",
"round_number": 1,
"outcome": "challenger_promoted",
"final_prob_to_win": 0.962,
"final_lift": 0.089,
"decided_at": "2026-05-13T08:45:00Z",
"started_at": "2026-05-15T12:00:00Z"
}
],
"next_decision_eta": {
"decision": "wait",
"reason": "Needs 240 more samples on the challenger arm and 2 more days of runtime before the decision threshold can be evaluated.",
"samples_needed": 240,
"days_needed": 2
}
}
Error Responses
| Status | Meaning |
|---|---|
401 |
Invalid or missing API key |
402 |
Insufficient credits |
403 |
API access not enabled for workspace |
404 |
Tool or resource not found |
500 |
Upstream error |
For MCP, errors are returned as isError: true in the tool result content. For A2A, status.state is "failed".
SplitKit Runtime Integration
Once a site is created and experiments are running, the SplitKit snippet exposes a synchronous JavaScript API that lets any code on the page read which variant the current visitor was assigned to.
How the snippet works
The snippet (<script src="https://splitkit.dev/s/{public_key}.js"></script>) runs synchronously in <head> before first paint. By the time DOMContentLoaded fires, the window.__sk object is fully populated:
window.__sk = {
v: 'visitor-uuid', // persistent visitor ID (localStorage)
s: 'session-uuid', // session ID (sessionStorage)
dt: 'desktop', // 'desktop' | 'tablet' | 'mobile'
sk: 'site-public-key',
a: [ // one entry per running experiment
{
eid: 'experiment-uuid', // experiment ID
vid: 'variant-uuid', // assigned variant ID
ic: false, // true if this is the control variant
gs: [...], // goals
ch: [...] // DOM changes (mutations)
}
],
preview: false // true when ?sk_preview=<variantId> is in the URL
}
Reading the assigned variant
Use window.__sk.getVariant(experimentId) — it returns { variantId, isControl } or null if the experiment isn't running:
var assignment = window.__sk && window.__sk.getVariant('YOUR_EXPERIMENT_UUID')
// { variantId: 'abc123...', isControl: false } — or null
Find your experiment UUID in the dashboard URL (/experiments/{uuid}) or via the get_experiment tool.
Recipe: dynamic price text + Stripe checkout link
This is the standard pattern for a pricing page where Variant B shows a discounted price with a different Stripe payment link:
<!-- 1. SplitKit snippet — must be first script in <head> -->
<script src="https://splitkit.dev/s/YOUR_PUBLIC_KEY.js"></script>
<!-- 2. Price text and buy button in your markup -->
<span id="sk-price">$29/mo</span>
<a id="sk-buy-btn" href="https://buy.stripe.com/CONTROL_LINK">Get started</a>
<!-- 3. Variant bridge — reads assignment, updates DOM before paint -->
<script>
(function () {
var EXP_ID = 'YOUR_EXPERIMENT_UUID'
var VARIANTS = {
'CONTROL_VARIANT_UUID': {
price: '$29/mo',
href: 'https://buy.stripe.com/CONTROL_LINK'
},
'VARIANT_B_UUID': {
price: '$19/mo',
href: 'https://buy.stripe.com/VARIANT_B_LINK'
}
}
var assignment = window.__sk && window.__sk.getVariant(EXP_ID)
var v = (assignment && VARIANTS[assignment.variantId]) || VARIANTS['CONTROL_VARIANT_UUID']
document.getElementById('sk-price').textContent = v.price
document.getElementById('sk-buy-btn').href = v.href
})()
</script>
Place the bridge <script> immediately after any element it reads — since the SplitKit snippet already ran synchronously, window.__sk is available inline.
Recipe: show/hide entire sections
var assignment = window.__sk && window.__sk.getVariant('YOUR_EXPERIMENT_UUID')
var isVariantB = assignment && !assignment.isControl
document.getElementById('section-a').style.display = isVariantB ? 'none' : ''
document.getElementById('section-b').style.display = isVariantB ? '' : 'none'
Preview mode
Append ?sk_preview=VARIANT_UUID to any URL to force a specific variant without being tracked. Useful when testing your bridge code before the experiment is live.
Finding your IDs
| Value | Where to find it |
|---|---|
| Experiment UUID | Dashboard URL /experiments/{uuid} or get_experiment tool response |
| Variant UUIDs | get_experiment → variants[].id |
| Public key | create_site or list_sites → sites[].public_key |