Endpoint Reference
Base URL: https://api.abtestbot.com
Auth: Authorization: Bearer sk_live_...
All endpoints return the standard {data, meta} wrapper on success and {error: {code, message}} on failure. workspace_id is derived from the API key — you never pass it explicitly.
Sites
`GET /v1/sites`
List all sites registered in your workspace, ordered most-recent first.
Response:
{
"data": [
{
"id": "uuid",
"name": "mystore",
"url": "https://mystore.com",
"platform": "shopify",
"public_key": "39cfc3dd2e26d9452e545073b7fdb29f",
"crawl_status": "ok",
"created_at": "2026-04-15T11:40:06.712852+00:00",
"updated_at": "2026-04-15T11:40:06.712852+00:00"
}
],
"meta": { "workspace_id": "uuid" }
}
public_key is the SplitKit snippet identifier — use it to construct the tracking script URL (https://splitkit.dev/s/{public_key}.js) or call the client-side runtime (window.__sk.getVariant(experimentId)).
`POST /v1/sites`
Register a new site. Subject to your plan's site limit (Starter: 1, Pro: 10, Enterprise: 25).
Body:
| Field | Type | Required | Notes |
|---|---|---|---|
name |
string | yes | Display name |
url |
string | yes | Fully-qualified URL with scheme |
Response: 201 Created with the full site object (same shape as GET, plus public_key used by the SplitKit tracking snippet).
`GET /v1/sites/:id`
Get one site by ID. Returns 404 not_found if it doesn't exist or belongs to another workspace.
Ideas
`GET /v1/ideas`
List A/B test ideas. Ordered by creation date (newest first).
Query parameters:
| Param | Type | Notes |
|---|---|---|
site_id |
uuid | Filter to one site |
status |
string | new | approved | implemented | rejected | archived |
category |
string | e.g. cta, headline, layout |
limit |
integer | Default 20, max 100 |
offset |
integer | Default 0 |
Response:
{
"data": [
{
"id": "uuid",
"title": "Test CTA button color",
"hypothesis": "Changing from grey to green will increase CTR",
"category": "cta",
"priority": "high",
"status": "new",
"output_type": "html",
"estimated_impact": "high",
"mockup_data": { ... },
"site_id": "uuid",
"sites": { "name": "mystore", "url": "..." },
"created_at": "...",
"updated_at": "..."
}
],
"meta": { "workspace_id": "uuid" }
}
`POST /v1/ideas/generate`
Kick off AI-powered idea generation for a site. Returns 202 Accepted — the work runs asynchronously and emits the ideas.generated webhook when complete (15–30s typical).
Body:
| Field | Type | Required | Notes |
|---|---|---|---|
site_id |
uuid | yes | |
num_ideas |
integer | no | Default 5, max 10 |
output_type |
string | no | html (default) or screenshot |
Response:
{
"data": {
"success": true,
"ideas": [ { "id": "uuid", "title": "...", "hypothesis": "...", ... } ]
},
"meta": { "workspace_id": "uuid" }
}
`GET /v1/ideas/:id`
Get one idea in full, including the parent site summary and mockup data.
`PATCH /v1/ideas/:id`
Update an idea's status — typically after reviewing or implementing it.
Body:
{ "status": "approved" }
Valid status values: new, approved, implemented, rejected, archived.
Experiments
`GET /v1/experiments`
List experiments. Shares the standard pagination contract.
Query parameters:
| Param | Type | Notes |
|---|---|---|
site_id |
uuid | Filter to one site |
status |
string | draft | active | paused | completed | archived |
limit |
integer | Default 20, max 100 |
Response: list of experiments with id, name, status, traffic_split, started_at, ended_at, site_id, plus a nested sites summary.
`POST /v1/experiments`
Create an experiment. There are four ways to supply the variant:
prompt(one-shot) — plain-English description. Server runsgenerate-variant-from-promptinternally, authors the variant, and creates the experiment in one call. An extra 10 credits are deducted on top of the 5 for create. If the target page can't be resolved from the prompt ortarget_url, the response is{ needs_clarification: { questions: [...] } }and no experiment is created (10 credits are still consumed — same as calling/v1/variants/generatedirectly).variants[0].changes— DOM mutations from a previousPOST /v1/variants/generatecall. The agent gets to preview and edit before committing.idea_id— pulls mutations from an existing AI-generated idea.- None of the above — draft experiment to fill in via the dashboard.
These are mutually exclusive — pick one. Pass auto_launch: true to transition the experiment to running right after creation (requires ≥2 variants + ≥1 goal); the response includes a launch field with the outcome.
Body:
| Field | Type | Required | Notes |
|---|---|---|---|
site_id |
uuid | yes | |
name |
string | yes | |
prompt |
string | no | One-shot natural-language create. Mutually exclusive with idea_id and variants[].changes. |
target_url |
string | no | Page URL the test runs on. Used (1) with prompt to tell the LLM which DOM to fetch, and (2) to build preview URLs in the response. Falls back to the idea's source_url (idea path) or the site's root. |
url_pattern |
string | no | Glob pattern constraining which pages the experiment runs on at runtime. Omit = site-wide (default). See the syntax table below. When prompt is used and the user's description implies a class of pages ("all report pages", "every /checkout/*"), the LLM fills this automatically — override by passing it explicitly. |
auto_launch |
boolean | no | Default false. If true, the experiment is launched immediately on create when preconditions pass. |
url_pattern glob syntax:
| Pattern | Matches | Doesn't match |
|---|---|---|
| (omitted / null) | every page of the site | — |
/checkout |
/checkout, /checkout/, /checkout/step-2 |
/checkouts, /cart/checkout |
/report/* |
/report/42, /report/annual-2026 |
/report, /report/42/details, /reports/summary |
/product/*/reviews |
/product/abc/reviews |
/product/abc/reviews/5, /product/reviews |
/blog/** |
/blog, /blog/post, /blog/2026/04/post |
/blogs, /news |
* matches a single path segment; ** matches any depth. Applied against location.pathname in the browser — scheme and host are stripped. Experiments with no pattern continue to run site-wide (existing behaviour preserved). Goals' existing url_pattern field uses the same matcher.
| idea_id | uuid | no | Source mutations from an existing idea |
| hypothesis | string | no | One-line expectation |
| traffic_split | integer | no | 1-99, default 50 (percent on variant) |
| variants | array | no | Up to one variant — control is auto-created |
| variants[].name | string | yes (if variants) | |
| variants[].changes | array | no | VariantMutation[] — see shape below |
| variants[].redirect_url | string | no | For split-URL tests |
| goals | array | no | [{ name, type, selector?, url_pattern? }] — first is primary |
Example — one-shot from prompt + auto-launch:
curl https://api.abtestbot.com/v1/experiments \
-X POST \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"site_id": "e0e3f741-...",
"name": "Pro pricing test",
"prompt": "Change the hero price from $99 to $499 and update the CTA to https://buy.stripe.com/pro_abc123",
"target_url": "https://mysite.com/pricing",
"auto_launch": true
}'
Credits: 15 (5 create + 10 generate) in draft, 15 for the same call even if auto_launch succeeds (launch is free on this path). Enterprise plans: all unmetered.
Clarification response (no experiment created):
{
"data": {
"needs_clarification": {
"questions": ["Which page URL should this test target? e.g. https://mysite.com/pricing ..."]
}
}
}
Auto-launch failure response (experiment still created as draft):
{
"data": {
"experiment": { "id": "...", "status": "draft", ... },
"launch": { "status": "draft", "error": "auto_launch skipped: experiment has 2 variant(s) and 0 goal(s); launch requires ≥2 variants and ≥1 goal." }
}
}
VariantMutation:
{
"selector": "<CSS selector>",
"action": "set_style" | "set_content" | "set_attribute" | "insert_before" | "insert_after" | "add_class" | "remove_class",
"payload": "<text/CSS/class/attr value>",
"attribute_name": "<required when action=set_attribute>",
"description": "<optional>"
}
Response shape (201 Created):
{
"data": {
"experiment": {
"id": "86773746-...",
"status": "draft",
"target_url": "https://mysite.com/pricing",
"variants": [
{ "id": "var-control-uuid", "is_control": true, "name": "Control (A)", "changes": [] },
{ "id": "var-b-uuid", "is_control": false, "name": "Variant B", "changes": [...] }
],
"goals": [ { "id": "...", "name": "...", "type": "click", ... } ]
},
"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 }) / PATCH /v1/experiments/:id { \"action\": \"launch\" } after reviewing the preview URLs below. Requires at least 1 non-control variant and 1 goal."
}
}
Previewing + activating the experiment
Every non-control variant gets a preview URL of the form <target_url>?sk_preview=<variant_id>. Opening it in a browser applies that variant's mutations to the live page without being tracked as a real visitor — perfect for sharing with a stakeholder before launching. GET /v1/experiments/:id returns the same preview_urls array so you can retrieve them later.
Three ways to move an experiment from draft → running:
| Path | When to use |
|---|---|
Pass auto_launch: true on POST /v1/experiments |
One call, zero review — agent is confident enough to ship directly. Skipped if preconditions fail (≥2 variants, ≥1 goal) and the error is in the launch field. |
PATCH /v1/experiments/:id with { "action": "launch" } |
Create → review preview URL → decide → launch. Standard agent flow. |
| Dashboard | Visit the experiment detail page and click Launch. |
All three paths run the same launch-experiment pipeline: validates preconditions, pushes variant config to SplitKit's edge KV, and marks status='running'.
Response: 201 Created with the envelope shown above.
`POST /v1/variants/generate`
Generate A/B test variant code from a natural-language prompt. Fetches the target page DOM, uses your site intelligence, and returns VariantMutation[] ready to pass into POST /v1/experiments.
Body:
| Field | Type | Required | Notes |
|---|---|---|---|
site_id |
uuid | yes | |
prompt |
string | yes | Plain-English description, min 10 chars. Include full URLs (e.g. Stripe checkout links) where relevant. |
target_url |
string | no | Specific page to target. If omitted, we try to extract a URL from the prompt or fall back to the homepage; if still ambiguous, response is { needs_clarification: { questions: [...] } } (status still 200). |
Response (success):
{
"data": {
"variant": { "name": "...", "mutations": [...], "redirect_url": null },
"suggested_goals": [{ "type": "click", "selector": "...", "name": "..." }],
"hypothesis": "...",
"context": {
"page_analyzed": "https://mysite.com/pricing",
"tech_stack": "Next.js, Stripe",
"url_pattern": null,
"assumptions": [...],
"warnings": [...]
}
},
"meta": { "workspace_id": "uuid" }
}
context.url_pattern is the LLM's inferred glob scope (same syntax as POST /v1/experiments — see the url_pattern glob syntax table). null means a single-page test. When the prompt implies a class of pages ("all report pages", "every /checkout/*"), the LLM fills this — pass it through to POST /v1/experiments as url_pattern so the snippet only fires on matching pages. Override it explicitly by setting url_pattern on the create call.
Response (needs clarification):
Status is still 200 on clarification — parse the body to detect the two shapes. Each questions[] entry is a structured object, not a free-text string, so programmatic callers can render the right input and list suggestions:
{
"data": {
"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).",
"input_type": "url",
"required": true,
"suggestions": [
"https://mysite.com/",
"https://mysite.com/pricing",
"https://mysite.com/checkout"
]
}
]
}
},
"meta": { "workspace_id": "uuid" }
}
ClarificationQuestion fields:
| Field | Type | Description |
|---|---|---|
field |
string | The request-body parameter the answer should be set on when retrying. |
prompt |
string | Human-readable question. |
reason |
string | Why the service is asking — useful as secondary/muted text. |
input_type |
"url" | "text" | "select" | "boolean" |
Hint for rendering the input. |
required |
boolean | Whether an answer is strictly needed. |
suggestions |
string[] |
Pre-computed options. For target_url, mined from the site's cached homepage links plus common CRO paths. |
Same shape is used by the prompt field on POST /v1/experiments when it needs clarification before authoring the variant.
`GET /v1/experiments/:id`
Get one experiment with all nested variants, goals, and parent site summary.
`PATCH /v1/experiments/:id`
Transition an experiment's lifecycle.
Body:
{ "action": "launch" }
Valid action values:
| Action | From → To | Notes |
|---|---|---|
launch |
draft → running |
Requires ≥2 variants (control + ≥1) and ≥1 goal. Pushes the variant config to SplitKit edge KV so traffic starts splitting immediately. |
pause |
running → paused |
|
resume |
paused → running |
|
complete |
running/paused → completed |
Sets ended_at. |
archive |
completed/draft → archived |
Response: { "data": { "id": "...", "status": "running" } }
Schedules
Recurring idea-generation runs.
`GET /v1/schedules`
List all schedules in your workspace.
`POST /v1/schedules`
Create a schedule.
Body:
| Field | Type | Required | Default |
|---|---|---|---|
site_id |
uuid | yes | — |
enabled |
boolean | no | false |
frequency |
string | no | weekly (also daily, monthly) |
day_of_week |
integer | no | 1 (Monday = 1, Sunday = 7) |
ideas_per_run |
integer | no | 2 |
output_type |
string | no | html |
target_pages |
array | no | [] — specific URLs to target |
`PATCH /v1/schedules/:id`
Update a schedule. Any of the fields above can be partially updated.
Webhooks
Subscribe to events to react to API-driven or UI-driven changes in real time.
`GET /v1/webhooks`
List active webhook endpoints.
Response:
{
"data": [
{
"id": "uuid",
"url": "https://your-app.com/webhooks/abtestbot",
"events": ["ideas.generated", "experiment.launched"],
"is_active": true,
"created_at": "..."
}
]
}
`POST /v1/webhooks`
Register a new webhook endpoint. The 201 response includes a one-time signing secret — store it. Use it to verify incoming webhooks via the X-abTestBot-Signature header (HMAC-SHA256).
Body:
| Field | Type | Required | Notes |
|---|---|---|---|
url |
string | yes | HTTPS endpoint |
events |
array | yes | One or more event types from the list below |
Supported event types:
ideas.generated— a scheduled or manualgeneraterun completedidea.status_changed— an idea moved toapproved/implemented/etc.experiment.launched— an experiment transitioned toactiveexperiment.completed— an experiment concluded (manually or auto)
`DELETE /v1/webhooks/:id`
Deactivate a webhook (soft-delete — flips is_active to false). Returns { "data": { "deleted": true } }.
Versioning
All endpoints live under /v1/. Breaking changes will ship as /v2/ — we don't modify /v1/ contracts once published.