# REST API Documentation

Full reference for the Otter REST API. Manage projects, create experiments, inspect results, and automate your testing workflows.

Manage API keys from your dashboard under **Settings → API & MCP**, or see the [MCP Integration Guide](https://www.otterab.com/mcp-docs).

## Authentication

All API requests require a bearer token. Generate API keys from your dashboard under **Settings → API & MCP**.

### Base URL

```
https://www.otterab.com/api/v1
```

### Token Format

```
oab_live_<token_id>_<secret>
```

Pick `oab_live_` or `oab_test_` when you create a key — it's an organizational label, not a behavioral split. Both prefixes authenticate against the same account data.

### Example Request

```bash
curl https://www.otterab.com/api/v1/projects \
  -H "Authorization: Bearer oab_live_abc123_9f8e7d..." \
  -H "Accept: application/json"
```

### Idempotency on Writes

Write endpoints support the optional `Idempotency-Key` header. Re-send the same method, path, and body with the same key to safely retry after a timeout or transport failure. Reusing a key for a different write request returns `409 Conflict`.

### Available Scopes

| Scope | Description |
| --- | --- |
| `projects:read` | List and view projects |
| `projects:write` | Create, update, delete projects |
| `experiments:read` | List and view experiments |
| `experiments:write` | Create, update, manage experiments |
| `results:read` | View experiment results |
| `account:read` | View account and usage info |
| `api_keys:write` | Manage API keys |
| `sdk:write` | Call SDK tracking endpoints (init, track, convert) from the server |

## Errors & Rate Limits

All errors follow a consistent envelope. Rate limits are enforced per account based on your plan.

### Error Response

```json
{
  "error": {
    "code": "validation_error",
    "message": "The request was invalid.",
    "details": [
      { "field": "name", "message": "can't be blank" }
    ],
    "request_id": "req_abc123"
  }
}
```

### Error Codes

| Status | Code | Description |
| --- | --- | --- |
| 401 | `invalid_api_key` | Missing or invalid bearer token |
| 402 | `account_inactive` | Subscription expired or inactive |
| 403 | `forbidden` | Missing required scope |
| 404 | `not_found` | Resource does not exist |
| 409 | `idempotency_key_reused` | Idempotency key was reused for a different write request |
| 422 | `validation_error` | Invalid request body |
| 429 | `rate_limited` | Too many requests |

### Rate Limits by Plan

| Plan | Requests / Minute |
| --- | --- |
| Starter | 100 |
| Growth | 500 |
| Scale | 2,000 |

### Pagination

List endpoints accept `page` and `per_page` query parameters. Responses include a `meta` object with page, per_page, and total count.

## Projects

Projects are containers for experiments. Each project maps to a website or app where the SDK snippet is installed.

```bash
# List all projects
curl https://www.otterab.com/api/v1/projects \
  -H "Authorization: Bearer oab_live_abc123_9f8e7d..."

# Create a project
curl -X POST https://www.otterab.com/api/v1/projects \
  -H "Authorization: Bearer oab_live_abc123_9f8e7d..." \
  -H "Content-Type: application/json" \
  -d '{"name": "Marketing Site", "url": "https://example.com", "platform": "custom_js"}'
```

### List projects

`GET /api/v1/projects`

Returns a paginated list of projects accessible to this API key.

Scopes: `projects:read`

Query Parameters:

| Name | Type | Description |
| --- | --- | --- |
| `page` | integer | Page number (default: 1) |
| `per_page` | integer | Items per page (default: 25, max: 100) |

Response 200:

```json
{
  "data": [
    {
      "id": "42",
      "name": "Marketing Site",
      "url": "https://example.com",
      "platform": "custom_js",
      "installation_guide": "custom_js",
      "reporting_timezone": "America/New_York",
      "integrations": { "ga4": { "enabled": false }, "gtm": { "enabled": false } },
      "environments": [],
      "settings": {},
      "snippet_verified": true,
      "running_experiments_count": 3,
      "created_at": "2026-03-01T12:00:00Z",
      "updated_at": "2026-03-28T09:15:00Z"
    }
  ],
  "meta": {
    "page": 1,
    "per_page": 25,
    "total": 1
  }
}
```

### Create project

`POST /api/v1/projects`

Creates a new project. Requires an unrestricted API key (not project-scoped).

Scopes: `projects:write`

Request Body:

```json
{
  "name": "Marketing Site",
  "url": "https://example.com",
  "platform": "custom_js",
  "installation_guide": "custom_js"
}
```

Response 201:

```json
{
  "data": {
    "id": "42",
    "name": "Marketing Site",
    "url": "https://example.com",
    "platform": "custom_js",
    "installation_guide": "custom_js",
    "reporting_timezone": null,
    "integrations": { "ga4": { "enabled": false }, "gtm": { "enabled": false } },
    "environments": [],
    "settings": {},
    "snippet_verified": false,
    "running_experiments_count": 0,
    "created_at": "2026-03-28T12:00:00Z",
    "updated_at": "2026-03-28T12:00:00Z"
  }
}
```

### Get project

`GET /api/v1/projects/:id`

Returns a single project by ID.

Scopes: `projects:read`

Response 200:

```json
{
  "data": {
    "id": "42",
    "name": "Marketing Site",
    "url": "https://example.com",
    "platform": "custom_js",
    "installation_guide": "custom_js",
    "reporting_timezone": "America/New_York",
    "integrations": { "ga4": { "enabled": true, "measurement_id": "G-ABC123" }, "gtm": { "enabled": false } },
    "environments": [],
    "settings": {},
    "snippet_verified": true,
    "running_experiments_count": 3,
    "created_at": "2026-03-01T12:00:00Z",
    "updated_at": "2026-03-28T09:15:00Z"
  }
}
```

### Update project

`PATCH /api/v1/projects/:id`

Updates a project. Only provided fields are changed.

Scopes: `projects:write`

Request Body:

```json
{
  "name": "Marketing Site (Prod)",
  "url": "https://www.example.com"
}
```

Response 200:

```json
{
  "data": {
    "id": "42",
    "name": "Marketing Site (Prod)",
    "url": "https://www.example.com",
    "platform": "custom_js",
    "installation_guide": "custom_js",
    "reporting_timezone": "America/New_York",
    "integrations": { "ga4": { "enabled": true, "measurement_id": "G-ABC123" }, "gtm": { "enabled": false } },
    "environments": [],
    "settings": {},
    "snippet_verified": true,
    "running_experiments_count": 3,
    "created_at": "2026-03-01T12:00:00Z",
    "updated_at": "2026-03-28T14:30:00Z"
  }
}
```

### Delete project

`DELETE /api/v1/projects/:id`

Permanently deletes a project and all its experiments.

Scopes: `projects:write`

Response 204: (no content)

> **Note:** This action cannot be undone. All experiments, variants, goals, and results will be deleted.

### Get project stats

`GET /api/v1/projects/:id/stats`

Returns aggregate statistics for a project.

Scopes: `projects:read`

Response 200:

```json
{
  "data": {
    "total_experiments": 12,
    "running_experiments": 3,
    "total_visitors": 45230,
    "total_conversions": 3812,
    "visitors_30d": 5140,
    "conversions_30d": 412
  }
}
```

### Get installation info

`GET /api/v1/projects/:id/installation`

Returns installation metadata and snippets for a project.

Scopes: `projects:read`

Response 200:

```json
{
  "data": {
    "platform": "custom_js",
    "installation_guide": "custom_js",
    "snippet_verified": true,
    "snippet_html": "<style id=\"optimo-hide\">body{opacity:0 !important}</style>\n<script src=\"https://www.otterab.com/sdk/optimo.js\" key=\"pk_abc123\" async></script>\n<script>window.optimoq=window.optimoq||[]</script>",
    "api_key": "pk_abc123",
    "tracking_snippet": "<style id=\"optimo-hide\">body{opacity:0 !important}</style>\n<script src=\"https://www.otterab.com/sdk/optimo.js\" key=\"pk_abc123\" async></script>\n<script>window.optimoq=window.optimoq||[]</script>",
    "shopify_pixel_code": null
  }
}
```

## Experiments

Experiments contain variants, goals, and targeting rules. Create and update the full experiment draft in a single request.

```bash
# List experiments for a project
curl https://www.otterab.com/api/v1/projects/42/experiments \
  -H "Authorization: Bearer oab_live_abc123_9f8e7d..."

# Create and immediately view an experiment draft
curl -X POST https://www.otterab.com/api/v1/projects/42/experiments \
  -H "Authorization: Bearer oab_live_abc123_9f8e7d..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Pricing Hero Test",
    "type": "visual",
    "url": "https://example.com/pricing",
    "url_match_type": "exact",
    "traffic_allocation": 100,
    "variants": [
      {"name": "Control", "is_control": true, "weight": 1, "changes": []},
      {"name": "New Headline", "is_control": false, "weight": 1, "changes": [
        {"selector": "h1", "action": "modify_text", "value": "Start winning."}
      ]}
    ],
    "goals": [
      {"name": "Signup", "goal_type": "pageview", "is_primary": true, "config": {"url": "https://example.com/success"}}
    ]
  }'

# Start an experiment
curl -X POST https://www.otterab.com/api/v1/projects/42/experiments/88/start \
  -H "Authorization: Bearer oab_live_abc123_9f8e7d..."
```

### List experiments

`GET /api/v1/projects/:project_id/experiments`

Returns a paginated list of experiments for a project.

Scopes: `experiments:read`

Query Parameters:

| Name | Type | Description |
| --- | --- | --- |
| `page` | integer | Page number (default: 1) |
| `per_page` | integer | Items per page (default: 25, max: 100) |
| `status` | string | Filter by status: draft, running, paused, completed, archived |

Response 200:

```json
{
  "data": [
    {
      "id": "88",
      "name": "Pricing Hero Test",
      "test_key": "pricing-hero-test",
      "type": "visual",
      "status": "running",
      "url": "https://example.com/pricing",
      "url_match_type": "exact",
      "url_rules": [],
      "traffic_allocation": 100,
      "trigger": "direct",
      "trigger_selector": null,
      "custom_css": null,
      "custom_js": null,
      "relay_params": false,
      "hypothesis": "A clearer value prop will increase signups.",
      "scheduled_start_at": null,
      "scheduled_end_at": null,
      "stop_conditions": { "visitor_limit": null, "conversion_limit": null, "auto_stop_on_winner": false },
      "confidence_level": 95,
      "analysis_method": "frequentist",
      "score_label": "Significance (1-p)",
      "score_description": "Significance (1-p) is the resolved decision score for this experiment.",
      "effective_confidence_threshold": 95,
      "created_at": "2026-03-15T10:00:00Z",
      "updated_at": "2026-03-28T08:00:00Z"
    }
  ],
  "meta": { "page": 1, "per_page": 25, "total": 1 }
}
```

### Create experiment

`POST /api/v1/projects/:project_id/experiments`

Creates a new experiment draft with variants, goals, and targeting in a single request.

Scopes: `experiments:write`

Request Body:

```json
{
  "name": "Pricing Hero Test",
  "type": "visual",
  "hypothesis": "A clearer value prop will increase signups.",
  "url": "https://example.com/pricing",
  "url_match_type": "exact",
  "traffic_allocation": 100,
  "trigger": "direct",
  "confidence_level": 95,
  "analysis_method": "frequentist",
  "targeting_rules": {
    "version": 2,
    "root": {
      "operator": "and",
      "children": [
        { "field": "country", "operator": "in", "values": ["US", "CA"] }
      ]
    }
  },
  "variants": [
    {
      "name": "Control",
      "is_control": true,
      "weight": 1,
      "changes": []
    },
    {
      "name": "Clearer Headline",
      "is_control": false,
      "weight": 1,
      "changes": [
        {
          "selector": "h1",
          "action": "modify_text",
          "value": "Stop guessing. Start winning."
        }
      ]
    }
  ],
  "goals": [
    {
      "name": "Signup Complete",
      "goal_type": "pageview",
      "is_primary": true,
      "config": { "url": "https://example.com/signup/success" }
    }
  ]
}
```

Response 201:

```json
{
  "data": {
    "id": "88",
    "name": "Pricing Hero Test",
    "test_key": "pricing-hero-test",
    "type": "visual",
    "status": "draft",
    "url": "https://example.com/pricing",
    "url_match_type": "exact",
    "url_rules": [],
    "traffic_allocation": 100,
    "trigger": "direct",
    "trigger_selector": null,
    "custom_css": null,
    "custom_js": null,
    "relay_params": false,
    "hypothesis": "A clearer value prop will increase signups.",
    "scheduled_start_at": null,
    "scheduled_end_at": null,
    "stop_conditions": { "visitor_limit": null, "conversion_limit": null, "auto_stop_on_winner": false },
    "confidence_level": 95,
    "analysis_method": "frequentist",
    "score_label": "Significance (1-p)",
    "score_description": "Significance (1-p) is the resolved decision score for this experiment.",
    "effective_confidence_threshold": 95,
    "targeting_rules": { "version": 2, "root": { "operator": "and", "children": [{ "field": "country", "operator": "in", "values": ["US", "CA"] }] } },
    "variants": [
      {
        "id": "201",
        "name": "Control",
        "weight": 1,
        "is_control": true,
        "changes": []
      },
      {
        "id": "202",
        "name": "Clearer Headline",
        "weight": 1,
        "is_control": false,
        "changes": [
          { "id": "301", "selector": "h1", "action": "modify_text", "value": "Stop guessing. Start winning." }
        ]
      }
    ],
    "goals": [
      {
        "id": "140",
        "name": "Signup Complete",
        "goal_type": "pageview",
        "is_primary": true,
        "config": { "url": "https://example.com/signup/success" }
      }
    ],
    "assigned_user_ids": [],
    "created_at": "2026-03-28T12:00:00Z",
    "updated_at": "2026-03-28T12:00:00Z"
  }
}
```

> **Note:** Draft experiments can be edited freely. Paused experiments can be edited with stable variant, change, and goal IDs so historical assignments and conversions stay attached.

### Get experiment

`GET /api/v1/projects/:project_id/experiments/:id`

Returns full experiment detail including variants, goals, and targeting rules.

Scopes: `experiments:read`

Response 200:

```json
{
  "data": {
    "id": "88",
    "name": "Pricing Hero Test",
    "test_key": "pricing-hero-test",
    "type": "visual",
    "status": "running",
    "url": "https://example.com/pricing",
    "url_match_type": "exact",
    "url_rules": [],
    "traffic_allocation": 100,
    "trigger": "direct",
    "trigger_selector": null,
    "custom_css": null,
    "custom_js": null,
    "relay_params": false,
    "hypothesis": "A clearer value prop will increase signups.",
    "scheduled_start_at": null,
    "scheduled_end_at": null,
    "stop_conditions": { "visitor_limit": 10000, "conversion_limit": null, "auto_stop_on_winner": true },
    "confidence_level": 95,
    "analysis_method": "frequentist",
    "score_label": "Significance (1-p)",
    "score_description": "Significance (1-p) is the resolved decision score for this experiment.",
    "effective_confidence_threshold": 95,
    "targeting_rules": { ... },
    "variants": [
      {
        "id": "201",
        "name": "Control",
        "weight": 1,
        "is_control": true,
        "changes": []
      },
      {
        "id": "202",
        "name": "Clearer Headline",
        "weight": 1,
        "is_control": false,
        "changes": [
          { "id": "301", "selector": "h1", "action": "modify_text", "value": "Stop guessing. Start winning." }
        ]
      }
    ],
    "goals": [
      { "id": "140", "name": "Signup Complete", "goal_type": "pageview", "is_primary": true, "config": { "url": "https://example.com/signup/success" } }
    ],
    "assigned_user_ids": ["12"],
    "created_at": "2026-03-15T10:00:00Z",
    "updated_at": "2026-03-28T08:00:00Z"
  }
}
```

### Update experiment

`PATCH /api/v1/projects/:project_id/experiments/:id`

Updates a draft experiment, or safely updates a paused experiment when existing variants, changes, and goals include their IDs.

Scopes: `experiments:write`

Request Body:

```json
{
  "name": "Pricing Hero Test v2",
  "hypothesis": "Updated hypothesis with more detail.",
  "variants": [
    { "name": "Control", "is_control": true, "weight": 1, "changes": [] },
    {
      "name": "Bold CTA",
      "is_control": false,
      "weight": 1,
      "changes": [
        { "selector": ".cta-button", "action": "modify_text", "value": "Start Free Trial" }
      ]
    }
  ],
  "goals": [
    { "name": "Signup Complete", "goal_type": "pageview", "is_primary": true, "config": { "url": "https://example.com/signup/success" } }
  ]
}
```

Response 200:

```json
{
  "data": {
    "id": "88",
    "name": "Pricing Hero Test v2",
    "status": "draft",
    ...
  }
}
```

> **Note:** Running experiments must be paused before editing. Paused updates preserve historical data, block URL/type changes and variant additions, and keep the primary goal tracking contract locked.

### Delete experiment

`DELETE /api/v1/projects/:project_id/experiments/:id`

Permanently deletes a draft experiment.

Scopes: `experiments:write`

Response 204: (no content)

> **Note:** Only draft experiments can be deleted.

### Start experiment

`POST /api/v1/projects/:project_id/experiments/:id/start`

Starts a draft experiment. Requires at least 2 variants and 1 goal.

Scopes: `experiments:write`

Response 200:

```json
{
  "data": {
    "id": "88",
    "name": "Pricing Hero Test",
    "status": "running",
    ...
  }
}
```

### Pause experiment

`POST /api/v1/projects/:project_id/experiments/:id/pause`

Pauses a running experiment. New visitors will not be assigned while paused.

Scopes: `experiments:write`

Response 200:

```json
{
  "data": {
    "id": "88",
    "status": "paused",
    ...
  }
}
```

### Resume experiment

`POST /api/v1/projects/:project_id/experiments/:id/resume`

Resumes a paused experiment.

Scopes: `experiments:write`

Response 200:

```json
{
  "data": {
    "id": "88",
    "status": "running",
    ...
  }
}
```

### Complete experiment

`POST /api/v1/projects/:project_id/experiments/:id/complete`

Completes a running or paused experiment. No more visitors will be assigned.

Scopes: `experiments:write`

Response 200:

```json
{
  "data": {
    "id": "88",
    "status": "completed",
    ...
  }
}
```

### Archive experiment

`POST /api/v1/projects/:project_id/experiments/:id/archive`

Archives a completed experiment.

Scopes: `experiments:write`

Response 200:

```json
{
  "data": {
    "id": "88",
    "status": "archived",
    ...
  }
}
```

## Results

Query experiment results with optional segment filters. The summary endpoint is ideal for dashboards and reporting bots.

```bash
# Get results summary for primary goal
curl https://www.otterab.com/api/v1/projects/42/experiments/88/results/summary \
  -H "Authorization: Bearer oab_live_abc123_9f8e7d..."

# Get results filtered by country
curl "https://www.otterab.com/api/v1/projects/42/experiments/88/results?country=US" \
  -H "Authorization: Bearer oab_live_abc123_9f8e7d..."

# Get time series for charting
curl https://www.otterab.com/api/v1/projects/42/experiments/88/results/timeseries \
  -H "Authorization: Bearer oab_live_abc123_9f8e7d..."
```

### Get full results

`GET /api/v1/projects/:project_id/experiments/:id/results`

Returns full experiment results across all goals, including time series data.

Scopes: `results:read`

Query Parameters:

| Name | Type | Description |
| --- | --- | --- |
| `traffic_source` | string | Filter by traffic source |
| `device_type` | string | Filter: desktop, mobile, tablet |
| `visitor_type` | string | Filter: new, returning |
| `day_of_week` | string | Filter: weekday, weekend |
| `country` | string | Filter by country code (e.g. US) |
| `region` | string | Filter by region |
| `os` | string | Filter by operating system |
| `browser` | string | Filter by browser |
| `utm_source` | string | Filter by UTM source |
| `utm_medium` | string | Filter by UTM medium |
| `utm_campaign` | string | Filter by UTM campaign |
| `visitor_identity` | string | Filter: identified, anonymous |
| `custom_dimension_key` | string | Custom visitor property key |
| `custom_dimension_value` | string | Custom visitor property value |

Response 200:

```json
{
  "data": {
    "experiment": {
      "id": "88",
      "name": "Checkout Revenue Test",
      "status": "running",
      "analysis_method": "frequentist",
      "confidence_level": 95,
      "score_label": "Significance (1-p)",
      "score_description": "Significance (1-p) is the resolved decision score for this experiment.",
      "effective_confidence_threshold": 95
    },
    "variants": {
      "140": [
        {
          "variant_id": "201",
          "variant_name": "Control",
          "is_control": true,
          "visitors": 1412,
          "conversions": 118,
          "conversion_rate": 8.36,
          "revenue_cents": 702340,
          "revenue_per_visitor_cents": 497.41,
          "improvement": null,
          "confidence": null,
          "score_value": null,
          "score_progress_percentage": null,
          "chance_to_beat": null,
          "status": "control"
        },
        {
          "variant_id": "202",
          "variant_name": "Clearer Headline",
          "is_control": false,
          "visitors": 1408,
          "conversions": 151,
          "conversion_rate": 10.72,
          "revenue_cents": 781120,
          "revenue_per_visitor_cents": 554.77,
          "improvement": 11.53,
          "confidence": 96.14,
          "score_value": 96.14,
          "score_progress_percentage": 100.0,
          "chance_to_beat": null,
          "rpv_lift": 11.53,
          "projected_annual_revenue_impact_cents": 1245000,
          "status": "winner"
        }
      ]
    },
    "goals": [
      { "id": "140", "name": "Purchase Revenue", "goal_type": "revenue", "is_primary": true }
    ],
    "goal_summaries": {
      "140": {
        "goal_id": "140",
        "goal_name": "Purchase Revenue",
        "goal_type": "revenue",
        "visitors": 2820,
        "conversions": 269,
        "conversion_rate": 9.54,
        "revenue_cents": 1483460,
        "revenue_per_visitor_cents": 525.34,
        "control_variant_name": "Control",
        "best_variant_name": "Clearer Headline",
        "winner_variant_name": "Clearer Headline",
        "improvement": 11.53,
        "confidence": 96.14,
        "score_value": 96.14,
        "score_progress_percentage": 100.0,
        "score_label": "Significance (1-p)",
        "score_description": "Significance (1-p) is the resolved decision score for this experiment.",
        "effective_confidence_threshold": 95,
        "rpv_lift": 11.53,
        "projected_annual_revenue_impact_cents": 1245000,
        "status": "winner"
      }
    },
    "decision_summary": {
      "goal_id": "140",
      "goal_name": "Purchase Revenue",
      "goal_type": "revenue",
      "recommendation": "ship",
      "recommendation_label": "Ship",
      "tone": "positive",
      "headline": "Ship Clearer Headline",
      "explanation": "Clearer Headline reached the resolved decision threshold for Purchase Revenue.",
      "next_step": "Promote the winning experience and keep monitoring downstream metrics.",
      "variant_name": "Clearer Headline",
      "control_variant_name": "Control",
      "estimated_uplift": 11.53,
      "score_label": "Significance (1-p)",
      "score_value": 96.14,
      "effective_confidence_threshold": 95,
      "confidence_assessment": "Significance (1-p) is 96.1%, above the 95% decision threshold.",
      "projected_annual_revenue_impact_cents": 1245000,
      "incremental_revenue_impact_cents": 1245000,
      "revenue_currency": "USD",
      "visitors": 2820,
      "conversions": 269,
      "conversion_rate": 9.54
    },
    "decision_summaries": {
      "140": {
        "goal_id": "140",
        "recommendation": "ship",
        "headline": "Ship Clearer Headline"
      }
    },
    "time_series": [ ... ],
    "available_segments": {
      "traffic_sources": ["direct", "organic_search"],
      "device_types": ["desktop", "mobile"],
      "visitor_types": ["new", "returning"],
      "operating_systems": ["ios", "macos"],
      "day_of_weeks": ["weekday", "weekend"],
      "countries": ["CA", "US"],
      "regions": ["BC", "CA"],
      "browsers": ["chrome", "safari"],
      "utm_sources": ["google", "newsletter"],
      "utm_mediums": ["cpc", "email"],
      "utm_campaigns": ["spring_sale"],
      "visitor_identities": ["anonymous", "identified"],
      "custom_dimensions": [
        { "key": "plan", "label": "Plan", "values": ["free", "pro"] }
      ]
    },
    "active_segment": null
  }
}
```

### Get results summary

`GET /api/v1/projects/:project_id/experiments/:id/results/summary`

Returns a simplified summary for the primary goal. Ideal for dashboards and bots.

Scopes: `results:read`

Query Parameters:

| Name | Type | Description |
| --- | --- | --- |
| `traffic_source` | string | Filter by traffic source |
| `device_type` | string | Filter: desktop, mobile, tablet |
| `visitor_type` | string | Filter: new, returning |
| `day_of_week` | string | Filter: weekday, weekend |
| `country` | string | Filter by country code (e.g. US) |
| `region` | string | Filter by region |
| `os` | string | Filter by operating system |
| `browser` | string | Filter by browser |
| `utm_source` | string | Filter by UTM source |
| `utm_medium` | string | Filter by UTM medium |
| `utm_campaign` | string | Filter by UTM campaign |
| `visitor_identity` | string | Filter: identified, anonymous |
| `custom_dimension_key` | string | Custom visitor property key |
| `custom_dimension_value` | string | Custom visitor property value |

Response 200:

```json
{
  "data": {
    "experiment": {
      "id": "88",
      "name": "Checkout Revenue Test",
      "status": "running",
      "analysis_method": "frequentist",
      "confidence_level": 95,
      "score_label": "Significance (1-p)",
      "score_description": "Significance (1-p) is the resolved decision score for this experiment.",
      "effective_confidence_threshold": 95
    },
    "primary_goal": {
      "id": "140",
      "name": "Purchase Revenue",
      "goal_type": "revenue",
      "is_primary": true
    },
    "variants": [
      {
        "variant_id": "201",
        "variant_name": "Control",
        "is_control": true,
        "visitors": 1412,
        "conversions": 118,
        "conversion_rate": 8.36,
        "revenue_cents": 702340,
        "revenue_per_visitor_cents": 497.41,
        "improvement": null,
        "confidence": null,
        "score_value": null,
        "score_progress_percentage": null,
        "chance_to_beat": null,
        "status": "control"
      },
      {
        "variant_id": "202",
        "variant_name": "Clearer Headline",
        "is_control": false,
        "visitors": 1408,
        "conversions": 151,
        "conversion_rate": 10.72,
        "revenue_cents": 781120,
        "revenue_per_visitor_cents": 554.77,
        "improvement": 11.53,
        "confidence": 96.14,
        "score_value": 96.14,
        "score_progress_percentage": 100.0,
        "chance_to_beat": null,
        "rpv_lift": 11.53,
        "projected_annual_revenue_impact_cents": 1245000,
        "status": "winner"
      }
    ],
    "primary_goal_summary": {
      "goal_id": "140",
      "goal_name": "Purchase Revenue",
      "goal_type": "revenue",
      "visitors": 2820,
      "conversions": 269,
      "conversion_rate": 9.54,
      "revenue_cents": 1483460,
      "revenue_per_visitor_cents": 525.34,
      "control_variant_name": "Control",
      "best_variant_name": "Clearer Headline",
      "winner_variant_name": "Clearer Headline",
      "improvement": 11.53,
      "confidence": 96.14,
      "score_value": 96.14,
      "score_progress_percentage": 100.0,
      "score_label": "Significance (1-p)",
      "score_description": "Significance (1-p) is the resolved decision score for this experiment.",
      "effective_confidence_threshold": 95,
      "rpv_lift": 11.53,
      "projected_annual_revenue_impact_cents": 1245000,
      "status": "winner"
    },
    "decision_summary": {
      "goal_id": "140",
      "goal_name": "Purchase Revenue",
      "goal_type": "revenue",
      "recommendation": "ship",
      "recommendation_label": "Ship",
      "tone": "positive",
      "headline": "Ship Clearer Headline",
      "explanation": "Clearer Headline reached the resolved decision threshold for Purchase Revenue.",
      "next_step": "Promote the winning experience and keep monitoring downstream metrics.",
      "variant_name": "Clearer Headline",
      "control_variant_name": "Control",
      "estimated_uplift": 11.53,
      "score_label": "Significance (1-p)",
      "score_value": 96.14,
      "effective_confidence_threshold": 95,
      "confidence_assessment": "Significance (1-p) is 96.1%, above the 95% decision threshold.",
      "projected_annual_revenue_impact_cents": 1245000,
      "incremental_revenue_impact_cents": 1245000,
      "revenue_currency": "USD",
      "visitors": 2820,
      "conversions": 269,
      "conversion_rate": 9.54
    }
  }
}
```

### Get results time series

`GET /api/v1/projects/:project_id/experiments/:id/results/timeseries`

Returns daily time series data for charting.

Scopes: `results:read`

Query Parameters:

| Name | Type | Description |
| --- | --- | --- |
| `traffic_source` | string | Filter by traffic source |
| `device_type` | string | Filter: desktop, mobile, tablet |
| `visitor_type` | string | Filter: new, returning |
| `day_of_week` | string | Filter: weekday, weekend |
| `country` | string | Filter by country code (e.g. US) |
| `region` | string | Filter by region |
| `os` | string | Filter by operating system |
| `browser` | string | Filter by browser |
| `utm_source` | string | Filter by UTM source |
| `utm_medium` | string | Filter by UTM medium |
| `utm_campaign` | string | Filter by UTM campaign |
| `visitor_identity` | string | Filter: identified, anonymous |
| `custom_dimension_key` | string | Custom visitor property key |
| `custom_dimension_value` | string | Custom visitor property value |
| `goal_id` | integer | Filter time series to a specific goal |

Response 200:

```json
{
  "data": [
    {
      "date": "2026-03-27",
      "variants": {
        "201": { "visitors": 142, "conversions": 12, "conversion_rate": 8.45, "revenue_cents": 70234, "revenue_per_visitor_cents": 494.61 },
        "202": { "visitors": 138, "conversions": 15, "conversion_rate": 10.87, "revenue_cents": 78112, "revenue_per_visitor_cents": 565.88 }
      }
    },
    {
      "date": "2026-03-28",
      "variants": {
        "201": { "visitors": 156, "conversions": 13, "conversion_rate": 8.33, "revenue_cents": 74580, "revenue_per_visitor_cents": 478.08 },
        "202": { "visitors": 149, "conversions": 17, "conversion_rate": 11.41, "revenue_cents": 82654, "revenue_per_visitor_cents": 554.72 }
      }
    }
  ]
}
```

## Account & Usage

View account details, team members, plan limits, and manage API keys programmatically.

### Get account

`GET /api/v1/account`

Returns account details and plan information.

Scopes: `account:read`

Response 200:

```json
{
  "data": {
    "id": "1",
    "name": "Acme Corp",
    "slug": "acme-corp",
    "plan": "growth",
    "plan_name": "Growth",
    "subscription_status": "active",
    "rate_limit_per_minute": 500
  }
}
```

### List members

`GET /api/v1/account/members`

Returns all team members on the account.

Scopes: `account:read`

Response 200:

```json
{
  "data": [
    { "id": "12", "name": "Jane Doe", "email_address": "jane@acme.com", "role": "owner" },
    { "id": "19", "name": "Bob Smith", "email_address": "bob@acme.com", "role": "member" }
  ]
}
```

### Get usage

`GET /api/v1/account/usage`

Returns plan limits and current usage.

Scopes: `account:read`

Response 200:

```json
{
  "data": {
    "plan": "growth",
    "plan_name": "Growth",
    "projects_count": 3,
    "projects_remaining": 2,
    "max_projects": 5,
    "rate_limit_per_minute": 500
  }
}
```

### List API keys

`GET /api/v1/account/api_keys`

Returns all API keys on the account. Only available to owners and admins.

Scopes: `api_keys:write`

Response 200:

```json
{
  "data": [
    {
      "id": "5",
      "name": "CI Pipeline",
      "environment": "live",
      "key_prefix": "oab_live_abc...",
      "scopes": ["projects:read", "experiments:read", "results:read"],
      "project_ids": [],
      "last_used_at": "2026-03-28T14:00:00Z",
      "revoked_at": null,
      "created_at": "2026-03-01T10:00:00Z"
    }
  ]
}
```

### Create API key

`POST /api/v1/account/api_keys`

Creates a new API key. The full token is returned only once.

Scopes: `api_keys:write`

Request Body:

```json
{
  "name": "CI Pipeline",
  "environment": "live",
  "scopes": ["projects:read", "experiments:read", "results:read"],
  "project_ids": []
}
```

Response 201:

```json
{
  "data": {
    "token": "oab_live_abc123_9f8e7d6c5b4a3210...",
    "api_key": {
      "id": "5",
      "name": "CI Pipeline",
      "environment": "live",
      "key_prefix": "oab_live_abc...",
      "scopes": ["projects:read", "experiments:read", "results:read"],
      "project_ids": [],
      "created_at": "2026-03-28T12:00:00Z"
    }
  }
}
```

> **Note:** The token field is only returned on creation. Store it immediately in your secrets manager.

### Revoke API key

`POST /api/v1/account/api_keys/:id/revoke`

Revokes an API key. It will immediately stop working.

Scopes: `api_keys:write`

Response 200:

```json
{
  "data": {
    "api_key": {
      "id": "5",
      "name": "CI Pipeline",
      "revoked_at": "2026-03-28T15:00:00Z",
      ...
    }
  }
}
```

## Capabilities

Discovery endpoints for building valid requests. Useful for MCP clients and automation that need to construct targeting rules or experiments dynamically.

### List targeting fields

`GET /api/v1/capabilities/targeting_fields`

Returns available targeting fields and their operators for building targeting rules.

Scopes: `account:read OR experiments:read`

Response 200:

```json
{
  "data": {
    "country": {
      "data_type": "string",
      "operators": ["equals", "not_equals", "in", "not_in"],
      "evaluation": "server_first",
      "widget": "country_picker",
      "requires_key": false
    },
    "device_type": {
      "data_type": "string",
      "operators": ["equals", "not_equals", "in", "not_in"],
      "evaluation": "server_and_client",
      "widget": "checkbox_list",
      "requires_key": false,
      "enum_values": ["desktop", "mobile", "tablet"]
    }
  }
}
```

### List goal types

`GET /api/v1/capabilities/goal_types`

Returns supported goal types.

Scopes: `account:read OR experiments:read`

Response 200:

```json
{
  "data": ["pageview", "click", "custom_event", "revenue", "ga4_event"]
}
```

### List experiment types

`GET /api/v1/capabilities/experiment_types`

Returns supported experiment types.

Scopes: `account:read OR experiments:read`

Response 200:

```json
{
  "data": ["redirect", "visual", "personalization"]
}
```

### List visual change actions

`GET /api/v1/capabilities/visual_change_actions`

Returns supported visual change actions for the visual editor.

Scopes: `account:read OR experiments:read`

Response 200:

```json
{
  "data": ["modify_text", "modify_html", "modify_attribute", "modify_style", "modify_class", "remove", "insert_before", "insert_after", "insert_inside", "replace"]
}
```

### List lifecycle actions

`GET /api/v1/capabilities/lifecycle`

Returns available experiment lifecycle transitions.

Scopes: `account:read OR experiments:read`

Response 200:

```json
{
  "data": ["start", "pause", "resume", "complete", "archive"]
}
```

## MCP Integration

Connect your AI assistant directly to Otter using the Model Context Protocol. Use the same API key — all scopes and project restrictions apply.

### Quick Start

```bash
# Claude Code (recommended)
claude mcp add --transport http otterab https://www.otterab.com/mcp/YOUR_API_KEY/v2/mcp

# Or run locally via npx
claude mcp add --transport stdio --env OTTERAB_API_KEY=oab_live_... otterab -- npx -y @otterab/mcp
```

Supported clients:

- **Claude Code & Desktop** — One-line setup via remote URL or local npx. Full tool support.
- **Cursor & VS Code** — Add to your MCP config with npx. Works with Copilot agent mode.
- **Windsurf & Others** — Standard MCP config. Any client supporting stdio or remote HTTP MCP servers.

See the [Full MCP Setup Guide](https://www.otterab.com/mcp-docs).
