Dashboard

API Documentation

The Experiment Flow API lets you run A/B tests and multi-armed bandits programmatically.

Authentication

All API requests require authentication. Include your API key in one of these ways:

X-API-Key: YOUR_API_KEY
Authorization: Bearer YOUR_API_KEY
?api_key=YOUR_API_KEY

Quick Start

Create an experiment and start tracking in 3 steps:

# 1. Create experiment
curl -X POST https://experimentflow.com/api/experiments \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "CTA Test", "variants": ["control", "variant_a"]}'

# 2. Get variant for visitor
curl "https://experimentflow.com/api/decide?experiment_id=1&visitor_id=user123&api_key=YOUR_API_KEY"

# 3. Track conversion
curl -X POST https://experimentflow.com/api/convert \
  -H "X-API-Key: YOUR_API_KEY" \
  -d '{"experiment_id": 1, "visitor_id": "user123"}'

Storage & Privacy

Experiment Flow is designed for privacy compliance. Here's how data is stored:

Client-Side (JavaScript SDK)

StorageKeyPurposeDuration
localStorageexperimentflow_visitor_idAnonymous visitor ID for consistent variant assignmentPersistent

No cookies are used for tracking. The SDK uses localStorage only, making it GDPR/CCPA compliant without cookie consent banners for A/B testing.

Dashboard Authentication

CookiePurposeTypeDuration
optimize_sessionDashboard login sessionHTTP-only, Secure, SameSite=LaxSession or 30 days (remember me)

This cookie is only set when you log into the dashboard. It's not used for visitor tracking.

Backend SDK

When using the Backend SDK, you manage user IDs yourself. No cookies or localStorage are involved - you pass the user_id directly in API calls.

JavaScript SDK Installation

Add the SDK to your website with a single script tag:

<script src="https://experimentflow.com/sdk.js"
        data-optimize-key="YOUR_API_KEY"></script>

Or initialize manually for more control:

<script src="https://experimentflow.com/sdk.js"></script>
<script>
  var ef = new ExperimentFlow({
    apiKey: 'YOUR_API_KEY',
    debug: true  // Enable console logging
  });
</script>

JavaScript SDK Usage

Get Variant

// Get variant for an experiment
ef.getVariant(1, function(variant) {
  if (variant === 'control') {
    // Show original
  } else if (variant === 'variant_a') {
    // Show variation
  }
});

// Or use the helper
ef.onVariant(1, {
  'control': function() { /* original */ },
  'variant_a': function() { document.querySelector('.cta').textContent = 'Buy Now!'; }
});

Track Conversion

// Track conversion for specific experiment
ef.convert(1);

// Track with value (e.g., order amount)
ef.convert(1, 49.99);

// Track with goal name
ef.convert(1, 49.99, 'purchase');

// Convert all active experiments
ef.convertAll(49.99, 'purchase');

DOM Modifications

// Automatically modify DOM based on variant
ef.activate(1, {
  'control': {},
  'variant_a': {
    '.hero-title': { text: 'New Headline!' },
    '.cta-button': { style: { backgroundColor: 'green' } },
    '.old-section': { hide: true }
  }
});

Set User ID (Cross-Device)

// After user logs in
ef.setUserId('user_12345');

// Now experiments sync across devices

Shopify Integration

The SDK includes built-in Shopify support with automatic event tracking:

// Initialize with Shopify support
ef.initShopify();

// Automatically tracks:
// - Add to cart (with product value)
// - Checkout start
// - Page context (product type, collection, etc.)

// Manual tracking on thank you page
ef.trackPurchase({ total_price: 9999 }); // Price in cents

// Test add-to-cart button variants
ef.testAddToCart(1, {
  'control': { text: 'Add to Cart' },
  'variant_a': { text: 'Buy Now - Free Shipping!', style: { backgroundColor: '#00aa00' } }
});

Backend SDK Overview

Run A/B tests entirely on your server without any client-side code. Perfect for:

The Backend SDK uses your user_id for consistent assignment. No cookies or localStorage needed.

Python Example

import requests

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://experimentflow.com"

def get_variant(experiment_key: str, user_id: str) -> str:
    """Get variant assignment for a user."""
    resp = requests.post(
        BASE_URL + "/api/backend/decide",
        headers={"X-API-Key": API_KEY},
        json={"experiment_key": experiment_key, "user_id": user_id}
    )
    return resp.json().get("variant", "control")

def track_conversion(experiment_key: str, user_id: str, revenue: float = None):
    """Track a conversion event."""
    requests.post(
        BASE_URL + "/api/backend/convert",
        headers={"X-API-Key": API_KEY},
        json={
            "experiment_key": experiment_key,
            "user_id": user_id,
            "revenue": revenue
        }
    )

# Usage in your app
@app.route("/checkout")
def checkout():
    user_id = get_current_user_id()

    variant = get_variant("checkout_flow", user_id)

    if variant == "new_checkout":
        return render_template("checkout_v2.html")
    return render_template("checkout.html")

@app.route("/purchase", methods=["POST"])
def purchase():
    user_id = get_current_user_id()
    order_total = calculate_order_total()

    track_conversion("checkout_flow", user_id, revenue=order_total)
    return jsonify({"status": "ok"})

Node.js Example

const API_KEY = 'YOUR_API_KEY';
const BASE_URL = 'https://experimentflow.com';

async function getVariant(experimentKey, userId) {
  const resp = await fetch(BASE_URL + '/api/backend/decide', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-API-Key': API_KEY
    },
    body: JSON.stringify({ experiment_key: experimentKey, user_id: userId })
  });
  const data = await resp.json();
  return data.variant || 'control';
}

async function trackConversion(experimentKey, userId, revenue) {
  await fetch(BASE_URL + '/api/backend/convert', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-API-Key': API_KEY
    },
    body: JSON.stringify({
      experiment_key: experimentKey,
      user_id: userId,
      revenue: revenue
    })
  });
}

// Express.js example
app.get('/api/pricing', async (req, res) => {
  const userId = req.user.id;
  const variant = await getVariant('pricing_test', userId);

  const prices = {
    control: { monthly: 29, annual: 290 },
    annual_first: { monthly: 29, annual: 249 }
  };

  res.json(prices[variant] || prices.control);
});

app.post('/api/subscribe', async (req, res) => {
  const { plan, amount } = req.body;
  await trackConversion('pricing_test', req.user.id, amount);
  // ... process subscription
});

Go Example

package main

import (
    "bytes"
    "encoding/json"
    "net/http"
)

const apiKey = "YOUR_API_KEY"
const baseURL = "https://experimentflow.com"

type DecideResponse struct {
    Variant string
}

func getVariant(experimentKey, userID string) (string, error) {
    body, _ := json.Marshal(map[string]string{
        "experiment_key": experimentKey,
        "user_id":        userID,
    })

    req, _ := http.NewRequest("POST", baseURL+"/api/backend/decide", bytes.NewBuffer(body))
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("X-API-Key", apiKey)

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return "control", err
    }
    defer resp.Body.Close()

    var result DecideResponse
    json.NewDecoder(resp.Body).Decode(&result)
    return result.Variant, nil
}

func trackConversion(experimentKey, userID string, revenue float64) error {
    body, _ := json.Marshal(map[string]interface{}{
        "experiment_key": experimentKey,
        "user_id":        userID,
        "revenue":        revenue,
    })

    req, _ := http.NewRequest("POST", baseURL+"/api/backend/convert", bytes.NewBuffer(body))
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("X-API-Key", apiKey)

    _, err := http.DefaultClient.Do(req)
    return err
}

// Usage
func handleCheckout(w http.ResponseWriter, r *http.Request) {
    userID := getUserID(r)
    variant, _ := getVariant("checkout_flow", userID)

    switch variant {
    case "streamlined":
        renderTemplate(w, "checkout_v2.html")
    default:
        renderTemplate(w, "checkout.html")
    }
}

Get Current User

GET /api/me

Get the current authenticated user's information including team details and plan.

{
  "id": 1,
  "email": "[email protected]",
  "team_id": 1,
  "team_name": "My Team",
  "role": "owner",
  "plan": "pro"
}

Try it

Regenerate API Key

POST /api/apikey

Generate a new API key. The old key will immediately stop working.

{
  "api_key": "opt_new_key_here"
}

Try it

List Experiments

GET /api/experiments

List all experiments for your team.

ParameterTypeDescription
archivedstringSet to "1" to list archived experiments

Try it

Create Experiment

POST /api/experiments

Create a new A/B test experiment.

FieldTypeDescription
name requiredstringExperiment name
variants requiredarrayList of variant names (min 2)
descriptionstringOptional description
traffic_percentnumber% of traffic to include (default: 100)
auto_promotebooleanAuto-promote winner when significant
auto_promote_thresholdnumberConfidence threshold (default: 0.95)

Try it

Get Experiment Statistics

GET /api/stats/{experiment_id}

Get detailed statistics including conversion rates, statistical significance, and winner.

{
  "experiment_id": 1,
  "experiment_name": "CTA Test",
  "status": "running",
  "total_visitors": 1000,
  "total_conversions": 120,
  "variants": [
    {"variant": "control", "visitors": 500, "conversions": 50, "conversion_rate": 0.10},
    {"variant": "variant_a", "visitors": 500, "conversions": 70, "conversion_rate": 0.14}
  ],
  "significance": {
    "is_significant": true,
    "confidence": 95.2,
    "p_value": 0.048,
    "z_score": 1.98
  },
  "winner": "variant_a",
  "improvement": 40.0
}

Try it

Promote Winner

POST /api/experiments/{id}/promote

Manually promote a variant as the winner and end the experiment.

Try it

Archive Experiment

POST /api/experiments/{id}/archive

Archive an experiment. Archived experiments are hidden from the main list but data is preserved.

Try it

Delete Experiment

DELETE /api/experiments/{id}

Permanently delete an experiment and all associated data. This action cannot be undone.

Try it

Get Variant Assignment

POST /api/decide

Get variant assignment for a visitor. Automatically tracks exposure.

FieldTypeDescription
experiment_id requiredintegerExperiment ID
visitor_id requiredstringUnique visitor identifier
user_idstringOptional user ID for cross-device tracking
contextobjectOptional context/features for targeting
{
  "variant": "variant_a",
  "visitor_id": "user_abc123",
  "in_test": true
}

Try it

Batch Decide (Multiple Experiments)

POST /api/decide/batch

Get variant assignments for multiple experiments in a single request. Ideal for pages with multiple active tests.

FieldTypeDescription
experiment_ids requiredarrayArray of experiment IDs
visitor_id requiredstringUnique visitor identifier
user_idstringOptional user ID for cross-device tracking
contextobjectOptional context/features for targeting
{
  "variants": {
    "1": "control",
    "2": "variant_a",
    "3": "variant_b"
  },
  "visitor_id": "user_abc123"
}

Try it

Track Conversion

POST /api/convert

Record a conversion event for a visitor.

FieldTypeDescription
experiment_id requiredintegerExperiment ID
visitor_id requiredstringUnique visitor identifier
valuenumberConversion value (default: 1)
goal_namestringOptional goal name for multiple goals

Try it

Track Custom Event

POST /api/track

Track custom events for detailed analytics. Useful for tracking intermediate steps, engagement metrics, or custom goals.

FieldTypeDescription
experiment_id requiredintegerExperiment ID
visitor_id requiredstringUnique visitor identifier
variant requiredstringThe variant the visitor was assigned
event_type requiredstringType of event (e.g., "click", "scroll", "video_play")
valuenumberOptional numeric value for the event
contextobjectOptional additional context data

Try it

Feature Flag (Backend SDK)

GET POST /api/feature

Feature flag style API for server-side experiments. Returns whether a feature is enabled and which variant to show. Ideal for backend A/B testing in Python, Go, Node.js, etc.

FieldTypeDescription
experiment_keystringExperiment name (alternative to experiment_id)
experiment_idintegerExperiment ID
user_id requiredstringUnique user identifier
attributesobjectUser attributes for targeting
{
  "enabled": true,
  "variant": "new_checkout",
  "experiment_id": 1,
  "in_test": true
}

Try it

Backend Decide

POST /api/backend/decide

Server-side variant assignment for backend experiments. Use experiment_key (name) or experiment_id to identify experiments.

FieldTypeDescription
experiment_keystringExperiment name (alternative to experiment_id)
experiment_idintegerExperiment ID
user_id requiredstringUnique user identifier
attributesobjectUser attributes for targeting
{
  "variant": "treatment_a",
  "experiment_id": 1,
  "user_id": "user_12345",
  "in_test": true
}

Try it

Backend Batch Decide

POST /api/backend/decide/batch

Get variant assignments for multiple experiments in a single request. Supports both experiment keys (names) and IDs.

FieldTypeDescription
experiment_keysarrayList of experiment names
experiment_idsarrayList of experiment IDs
user_id requiredstringUnique user identifier
attributesobjectUser attributes for targeting
{
  "variants": {
    "checkout_flow": {"variant": "new_checkout", "in_test": true, "experiment_id": 1},
    "pricing_test": {"variant": "annual_first", "in_test": true, "experiment_id": 2}
  },
  "user_id": "user_12345"
}

Try it

Backend Convert

POST /api/backend/convert

Track a conversion from your backend. Automatically looks up the user's variant assignment.

FieldTypeDescription
experiment_keystringExperiment name (alternative to experiment_id)
experiment_idintegerExperiment ID
user_id requiredstringUser who converted
goal_namestringOptional goal name (e.g., "purchase", "signup")
valuenumberConversion value (default: 1)
revenuenumberRevenue amount (takes precedence over value)
{
  "status": "ok",
  "experiment_id": 1,
  "user_id": "user_12345",
  "variant": "treatment_a",
  "value": 99.99
}

Try it

Experiment Status

GET /api/experiments/{id}/status

Get detailed experiment status including visitors, conversions, conversion rate, and statistical significance.

{
  "id": 1,
  "name": "Checkout Flow",
  "status": "running",
  "variants": ["control", "new_checkout"],
  "total_visitors": 1500,
  "total_conversions": 180,
  "conversion_rate": 0.12,
  "is_significant": true,
  "confidence": 95.2,
  "traffic_percent": 100,
  "auto_promote": true,
  "auto_promote_threshold": 0.95
}

Try it

Experiment by Key

GET /api/experiments/by-key/{name}

Look up an experiment by its name (key). Useful when you don't know the experiment ID.

{
  "id": 1,
  "name": "checkout_flow",
  "status": "running",
  "variants": ["control", "new_checkout"],
  "winner": ""
}

Try it

List Bandits

GET /api/bandits

List all multi-armed bandits for your team.

Try it

Create Bandit

POST /api/bandits

Create a new multi-armed bandit for automatic traffic optimization.

FieldTypeDescription
name requiredstringBandit name
arms requiredarrayList of arm names (min 2)
algorithmstringAlgorithm: "thompson" (default), "ucb1", or "epsilon"

Try it

Select Bandit Arm

GET /api/bandits/{id}/select

Select the best arm using Thompson Sampling. Returns the arm to show and records the selection.

Try it

Record Bandit Reward

POST /api/bandits/{id}/reward

Record a reward (conversion) for a bandit arm selection.

Try it

Get Team

GET /api/team

Get team details and members.

Try it

Invite Team Member

POST /api/team/invite

Invite a new member to your team (owner/admin only).

Try it

List Personalizers

GET /api/personalizers

List all personalizers for your team. Personalizers use neural networks for real-time action ranking.

Try it

Create Personalizer

POST /api/personalizers

Create a new personalizer for AI-powered action ranking.

FieldTypeDescription
name requiredstringPersonalizer name
exploration_ratenumberExploration rate 0-1 (default: 0.2)
learning_ratenumberLearning rate (default: 0.01)

Try it

Rank Actions

POST /api/personalize/{id}/rank

Rank a set of actions for a user based on context. Returns actions sorted by predicted reward. Costs $0.01 per call.

FieldTypeDescription
visitor_id requiredstringUnique visitor identifier
context requiredobjectContext features (e.g., user attributes, page info)
actions requiredarrayList of action objects with features
{
  "ranked_actions": [
    {"id": "product_3", "score": 0.85},
    {"id": "product_1", "score": 0.72},
    {"id": "product_2", "score": 0.45}
  ],
  "event_id": "evt_abc123"
}

Try it

Record Personalizer Reward

POST /api/personalize/{id}/reward

Record a reward for a previous rank call. Used to train the model.

FieldTypeDescription
event_id requiredstringEvent ID from the rank response
reward requirednumberReward value (0-1 recommended)

Try it

Get Credit Balance

GET /api/credits

Get current credit balance for personalization API calls.

{
  "balance": 50.00,
  "currency": "USD"
}

Try it

Top Up Credits

POST /api/credits/topup

Purchase additional credits for personalization API calls.

FieldTypeDescription
amount requirednumberAmount in USD ($5, $20, $50, $100, $500)
success_urlstringRedirect URL after successful payment
cancel_urlstringRedirect URL if payment cancelled

Try it

Get Subscription

GET /api/subscription

Get current subscription details and usage.

{
  "plan": "pro",
  "seats": 3,
  "experiments_used": 5,
  "experiments_limit": 0,
  "events_used": 15000,
  "events_limit": 1000000
}

Try it

Start Checkout

POST /api/checkout

Start a Stripe checkout session to upgrade to Pro plan.

FieldTypeDescription
seatsnumberNumber of seats (default: 1)
embeddedbooleanUse embedded checkout (default: false)
billing_periodstring"monthly" ($29/mo) or "yearly" ($290/year, save 17%)
{
  "checkout_url": "https://checkout.stripe.com/...",
  "client_secret": "cs_..."
}

Try it