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 17 tools — creating a site, listing/analyzing sites, generating and managing A/B test ideas (with rendered mockup images), 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 }
]
}
Mockup images (the `mockup` object)
list_ideas, get_idea, and generate_ideas each return a compact mockup object per idea, so an agent can show the user a rendered preview of the change without parsing the raw mockup_data blob:
"mockup": {
"status": "ready",
"image_url": "https://.../variant-abc.png",
"base_screenshot_url": "https://.../base.png",
"diff_percent": 3.4,
"vision_confirmed": true
}
| Field | Type | Description |
|---|---|---|
status |
string | none | rendering | ready | unconfirmed | review_required | failed (see below) |
image_url |
string|null | The displayable mockup image (rendered variant or regenerated hero image). null until status is ready. |
base_screenshot_url |
string|null | The unmodified base page screenshot — context only, never the variant. |
diff_percent |
number|null | Visual diff vs. the base page. null until rendered. |
vision_confirmed |
bool|null | AI vision verdict that the change is real and on-intent. null when not evaluated. |
failed_reason |
string | Present only when status is failed. |
status lifecycle — mockups render asynchronously after generate_ideas returns:
rendering— render still in flight. The idea exists and is usable; the image just isn't ready yet. Polllist_ideasorget_ideauntil it flips.ready—image_urlis a vision-confirmed mockup, safe to show the user.unconfirmed— render finished but the change wasn't visually confirmed (too small a diff / vision doubt); no image is surfaced.review_required— held for human review (regulated copy or a deceptive nav relabel).failed— un-renderable (selector matched nothing / no visible change).list_ideaswithholds these;get_ideastill returns them with afailed_reason.none— the idea has no DOM mutations, so there's nothing to render.
Typical agent flow:
generate_ideas→ ideas come backrendering→ polllist_ideas(orget_ideaper idea) every few seconds → present eachmockup.image_urloncestatusisready.
list_ideas
List A/B test ideas for a site. Each idea includes a mockup object carrying the rendered mockup image and its render status. Un-renderable ideas (mockup.status: failed) are withheld.
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",
"user_focus": null,
"created_at": "...",
"updated_at": "...",
"mockup": {
"status": "ready",
"image_url": "https://.../variant-abc.png",
"base_screenshot_url": "https://.../base.png",
"diff_percent": 3.4,
"vision_confirmed": true
}
}
]
}
get_idea
Get a single A/B test idea by ID, including its rendered mockup. This is the tool to re-poll an idea after generate_ideas: an idea starts at mockup.status: "rendering" and flips to "ready" (with mockup.image_url set) once its variant screenshot is rendered and vision-confirmed.
Credits: 1
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
idea_id |
string | yes | From list_ideas or generate_ideas |
Example (MCP):
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_idea",
"arguments": { "idea_id": "44444444-4444-4444-4444-444444444444" }
}
}
Response:
{
"idea": {
"id": "44444444-4444-4444-4444-444444444444",
"title": "Lead with the outcome, not the feature",
"hypothesis": "...",
"category": "copy",
"priority": "high",
"status": "new",
"mockup_data": { "mutations": [/* ... */], "variant_screenshot_url": "https://.../variant-abc.png", "...": "..." },
"mockup": {
"status": "ready",
"image_url": "https://.../variant-abc.png",
"base_screenshot_url": "https://.../base.png",
"diff_percent": 3.4,
"vision_confirmed": true
}
}
}
Returns not_found (404) if the idea doesn't exist in the caller's workspace.
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.
Mockup images render asynchronously after this call returns: ideas come back with mockup.status: "rendering" and no image_url yet. Poll list_ideas or get_idea until mockup.status: "ready" to retrieve mockup.image_url.
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 |
focus |
string | no | "I have an idea" text: the concept or part of the site to build ideas around, e.g. "shipping info on product pages". Every idea will be about it. Max 500 characters |
When the user says what they want tested ("ideas for the cart drawer", "something about free shipping"), pass their words as focus. Leave it out to get a spread of ideas across the page.
Response:
{
"success": true,
"ideas": [
{
"id": "uuid",
"title": "...",
"hypothesis": "...",
"category": "layout",
"priority": "medium",
"mockup": { "status": "rendering", "image_url": null, "base_screenshot_url": "https://.../base.png", "diff_percent": null, "vision_confirmed": null }
}
],
"mockups_note": "Mockup images render asynchronously. Poll list_ideas or get_idea until each idea's mockup.status is \"ready\" to retrieve mockup.image_url."
}
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 |
| device_types | string[] | no | Which visitors enter the test: any of mobile, tablet, desktop. Visitors on other devices are not assigned and see the original page. Omit to derive it from the changes: when every change has a media query, only the devices it reaches are enrolled. |
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://variflask.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>",
"media": "<optional CSS media query, e.g. (max-width: 749px)>",
"description": "<optional human-readable note>"
}
media limits a change to screen sizes where the query matches, for example (max-width: 749px) for phones or (min-width: 990px) for desktops. The snippet checks it in the visitor's browser and undoes the change if the screen crosses the breakpoint. Leave it out to apply the change at every size.
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. |
device_types |
string[] | no | Which visitors every round enrolls. Same meaning as create_experiment.device_types. Omit to let each round derive it from its own changes. |
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 |