REST API 12 min read

Get your API key

Generate a sk_live_ key from Settings → API (Enterprise plan).

Go to Settings →

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:

  1. prompt (one-shot) — plain-English description. Server runs generate-variant-from-prompt internally, 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 or target_url, the response is { needs_clarification: { questions: [...] } } and no experiment is created (10 credits are still consumed — same as calling /v1/variants/generate directly).
  2. variants[0].changes — DOM mutations from a previous POST /v1/variants/generate call. The agent gets to preview and edit before committing.
  3. idea_id — pulls mutations from an existing AI-generated idea.
  4. 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 draftrunning:

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 draftrunning Requires ≥2 variants (control + ≥1) and ≥1 goal. Pushes the variant config to SplitKit edge KV so traffic starts splitting immediately.
pause runningpaused
resume pausedrunning
complete running/pausedcompleted Sets ended_at.
archive completed/draftarchived

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 manual generate run completed
  • idea.status_changed — an idea moved to approved/implemented/etc.
  • experiment.launched — an experiment transitioned to active
  • experiment.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.