# UXON API Keys Agent Playbook

> Maintainer note: when updating MCP/API/SKILLS capabilities, follow `docs/ai-chat-cro-working-agreement.md` and keep parity explicitly aligned.
> MCP parity note: API commands use dotted names in request payloads; Claude MCP tools use underscore names because Claude requires `^[a-zA-Z0-9_-]{1,64}$`.

This document is the operational contract for external agents that execute account setup and reporting actions in UXON.

Use this playbook when agents are running in Slack, custom apps, internal tools, or orchestration platforms.

## 1) Primary Outcomes

Use API Keys commands to:

- discover account and campaign state quickly,
- keep account setup synchronized across tools,
- run controlled setup changes,
- pull recurring performance reports,
- execute repeatable agency workflows with auditability.

## 2) Scope And Limits

Allowed in this API:

- read reporting and setup data,
- create and update approved resources,
- set status for landing pages and experiments,
- manage saved audiences, saved Page groups, and personalization drafts,
- delete only resources with explicit approved delete commands.

Blocked in this API:

- delete operations without a listed delete command,
- raw internal passthrough endpoints,
- commands not listed in this document.

## 3) Endpoint And Auth

Endpoint:

`POST https://uxon.ai/api/external/commands`

Latest skills URL:

`GET https://uxon.ai/api/skills`

Agents should fetch the latest skills from this URL when they need the current command playbook. This document contains instructions only; commands still require the bearer API key below.

Headers:

```
Authorization: Bearer uxon_live_...
Content-Type: application/json
```

## 3.1) Claude MCP Quickstart (Autopilot)

For Claude connector users (MCP), the fastest path is a single workflow start:

- Tool: `onboarding_first_client_setup`
- Purpose: run onboarding -> automatic brand/media import -> tracking -> domain setup in one guided flow.

What to expect in chat:

- Claude asks only missing inputs: client name, website URL, reporting currency, reporting timezone only when unknown, landing page feedback mode, tracking setup, and domain setup.
- After the client account is created, UXON automatically starts brand import and website media scraping in the background.
- Do not ask for brand colors, typography, brand voice, or logo URL during first-client setup unless the user explicitly wants a manual branding override.
- Use `branding_setup` separately when the user wants to override the automatic brand import.
- If DNS is still propagating, workflow pauses safely and resumes with `workflow_status`.
- Tracking safety rule is enforced:
  - GTM mode clears direct GA4/Meta IDs.
  - Direct mode disables managed GTM scripts for that domain.

Recommended first prompt in Claude:

- `Start first client setup for [brand name], website [url].`

## 3.2) Claude Support Q&A (Knowledge Articles)

When users ask product/how-to questions in chat:

1. Run `docs_search` with the user phrase.
2. Pick the best match.
3. Run `docs_get` on that article ID.
4. Reply using article facts and include the article URL when available.

## 3.3) Claude Landing Page Brief Workflow

For Claude connector users creating a landing page from brief:

- Tool: `landing_pages_create_from_brief`
- Collect or pass: `clientReference` or `siteId`, `lpName`, `language`, `funnelStrategy`, `buyerGroup`, `desiredAction`, `market`, `serviceOrProduct`, `idealClient`.
- Use the same brief labels as AI landing page generation: Brand name, Website URL, Language, Offer type, Buyer group, Desired action, Market, Promoted offer, Ideal customer.
- If any field is missing, the workflow asks the next missing question.
- The workflow submits the real UXON AI landing page generation job. Poll with `landing_pages_generation_status`, then read the page with `landing_pages_get`.

## 4) Request Envelope

Every request must use this exact envelope:

```json
{
  "command": "subaccounts.list",
  "input": {}
}
```

## 5) Response Envelope

Successful response shape:

```json
{
  "command": "subaccounts.list",
  "scope": {
    "teamId": "team-id",
    "siteId": null
  },
  "data": {}
}
```

## 6) Access, Scope, And Safety

- Keys are team-scoped.
- A key may be restricted to one `siteId`.
- If key scope is one site, using another `siteId` is rejected.
- Use only listed commands.
- Delete actions are allowed only for listed delete commands and require exact IDs.
- Unknown commands are rejected.
- Commands are rate-limited and logged per key.
- MCP high-impact write tools require explicit user approval. When the user has approved the action, retry the MCP tool with `confirm: true`.
- MCP OAuth sessions should have both `uxon:read` and `uxon:write`; read-only sessions cannot run write tools.
- MCP create-style tools must include a stable `idempotencyKey` so retries do not create duplicates.
- MCP `tools/list` is the source of truth for connector schemas, including one-of identifier requirements such as `landingPageId` or `id`.
- Direct External API commands do not use `confirm` or `idempotencyKey` in the request body unless the command schema lists them.

## 7) Operational Defaults

Client account defaults:

- `reportingCurrency` defaults to `USD`.
- `reportingTimezone` must be a valid IANA timezone, for example `UTC`, `Australia/Brisbane`, `America/New_York`, or `Europe/London`.
- `reportingTimezone` defaults to `UTC` when omitted through API/MCP.
- Browser onboarding silently sends the user's browser timezone when available. API/MCP agents do not have browser context, so pass the best known client timezone or ask once if unknown.
- `landingPageFeedbackMode` defaults to `free_comments`.

Landing page feedback mode values:

- `free_comments`: clients can place open comments on landing page previews.
- `ai_approvals`: the preview uses the AI-assisted approval workflow to review and moderate feedback.

Do not confuse client account feedback mode with landing-page approval settings:

- `landingPageFeedbackMode` is the client account default.
- A landing page can override the feedback mode in its own approval settings.
- A landing page can set an approval email override.
- A landing page can disable new comments while keeping existing comments visible.
- Optional client logos are handled by branding/logo workflows, not by bare `subaccounts.create`.

Date defaults:

- If date range is omitted on report commands, the API defaults to the last 30 days.
- Report commands with a `timezone` field expect the same IANA timezone format.
- If report `timezone` is omitted, UXON uses the sub-account reporting timezone, then `UTC`.

Pagination defaults:

- `page = 1`
- `perPage = 25`
- `perPage` max is `100`

Date format:

- Use ISO-8601 date strings, for example: `2026-04-19T00:00:00.000Z`

Report `dateRange` presets:

- `today`
- `yesterday`
- `last-3-days`
- `last-7-days`
- `last-14-days`
- `last-30-days`
- `last-90-days`
- `last-365-days`
- `custom`

Custom report windows:

- Recommended: use `dateRange: "custom"` with `from` and `to` as ISO-8601 strings.
- Backwards-compatible: some report commands also accept `dateRange` as `"fromISO,toISO"`.

Rate limits:

- Default API key limit is 60 requests per minute.
- Internal key configuration supports custom limits from 1 to 600 requests per minute.
- Responses include `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset`.
- On `429`, wait for `X-RateLimit-Reset` before retrying.

Bulk reporting behavior:

- For multi-account checks, start with `subaccounts.list`.
- Use snapshot counts to decide where detail is needed.
- Do not fetch every page, variant, experiment, channel, and contact unless the user asks for exhaustive detail.
- Prefer summary commands first, then details only for live/running/high-volume items.
- If `X-RateLimit-Remaining` is low, pause before continuing.

Agent contract:

- Treat listed command schemas as closed contracts. Do not send extra top-level fields.
- Use API dotted command names, for example `subaccounts.create`.
- Use MCP underscore tool names, for example `subaccounts_create`.
- Resolve IDs with list/get commands before write commands.
- Use `audiences.list` before assigning an `audienceId`; UXON snapshots the current audience rules into experiments and personalization experiences at save time.
- Use `page_groups.list` before assigning a saved Page group to personalization page scope.
- Personalization can be created, updated, and moved through `Draft`, `Running`, `Paused`, and `Ended` through API/MCP.
- For MCP write tools, check `tools/list`. If the schema includes `confirm`, ask the user for approval first and then send `confirm: true`.
- For MCP create tools, send a unique `idempotencyKey`, for example `client-page-2026-07-20-001`.
- For timezone, pass an IANA timezone if the user has supplied locality or timezone. Ask only when it affects reporting and cannot be inferred from the request.
- For client onboarding, do not ask for brand colors, typography, voice, or logo URL unless the user explicitly wants manual branding overrides.
- After write commands, read back the changed object and confirm the saved values.
- For branding work, use `branding.get` first. Use `branding.update` only for explicit adjustments, and send only fields the user wants changed.
- `branding.update` is partial: omitted `colorVariables`, `brandColors`, `logo`, `fonts`, `customFonts`, `paletteName`, and `brandVoice` are preserved.
- Branding logo roles are based on the logo artwork colour: `logo.light` is a light-coloured or white logo for dark backgrounds; `logo.dark` is a dark-coloured logo for light backgrounds; `logo.favicon` is the square browser/app icon.
- `branding.update` can replace the active URL for `logo.light`, `logo.dark`, and `logo.favicon`. Use the web app or dedicated logo workflows for binary file uploads when no direct image URL exists.
- `branding.update` can update `fonts.headings`, `fonts.bodyText`, and `fonts.buttons`. Use UXON weight labels such as `400 - Regular` or `700 - Bold`, or numeric weights such as `400` and `700`.
- `customFonts` imports must use direct `.woff2`, `.woff`, `.ttf`, or `.otf` URLs. UXON downloads, converts when needed, hosts the web font, and saves the `@font-face` CSS. Use the web app or upload workflow for binary font files.

## 8) Full Command Catalog

### Knowledge base articles
- `docs.list`: List Help Center articles available to AI chat tools.
- `docs.search`: Search Help Center articles by phrase.
- `docs.get`: Fetch one Help Center article by ID.

### Sub-accounts
- `subaccounts.list`: List accessible sub-accounts with a high-level operational snapshot.
- `subaccounts.get`: Fetch details and totals for a single sub-account.
- `subaccounts.create`: Create a new sub-account with website URL, reporting currency, reporting timezone, and feedback mode.
- `subaccounts.update`: Update sub-account profile fields, reporting currency, reporting timezone, or feedback mode.

### Branding
- `branding.get`: Fetch a sub-account branding overview including color variables, brand colors, logo URLs, fonts, brand voice, and readiness flags.
- `branding.update`: Update editable sub-account branding fields such as color variables, brand colors, logo URLs, fonts, custom font URL imports, palette name, and brand voice.

### Landing pages
- `landing_pages.list`: List landing pages for a sub-account.
- `landing_pages.get`: Get landing page metadata, variants, and preview/live URLs.
- `landing_pages.create`: Create a draft landing page with a blank builder variant.
- `landing_pages.update`: Update landing page metadata, settings, or knowledge base.
- `landing_pages.delete`: Delete a landing page.
- `landing_pages.duplicate`: Duplicate a landing page and its builder variants.
- `landing_pages.publish_changes`: Publish current draft variant changes to the live page.
- `landing_pages.review_link`: Create a landing page review link for preview comments or approvals.
- `landing_pages.generate_from_brief`: Start the real UXON AI landing page generation pipeline from an approved brief.
- `landing_pages.generation_status`: Poll a UXON AI landing page generation job.
- `landing_pages.variants.create`: Create a blank builder/code variant or duplicate from an existing variant.
- `landing_pages.variants.get`: Get one landing page variant.
- `landing_pages.variants.update`: Update one landing page variant.
- `landing_pages.variants.set_main`: Set the main variant for a landing page.
- `landing_pages.variants.duplicate`: Duplicate one builder variant.
- `landing_pages.variants.delete`: Delete one landing page variant.
- `landing_pages.code_variant.upload_init`: Create a signed upload URL for a custom code ZIP variant.
- `landing_pages.code_variant.upload_complete`: Import a custom code ZIP after signed upload completes.
- `landing_pages.code_variant.upload`: Upload or replace a custom code ZIP as a landing page variant.
- `landing_pages.set_status`: Set landing page status to draft or live.
- `reports.landing_pages.summary`: Return aggregate landing page totals and conversion metrics.
- `reports.landing_page.details`: Return detailed performance data for a landing page.
- `reports.landing_page.variant_metrics`: Return variant-level metrics for a landing page.
- `reports.landing_page.trends`: Return trend data for a landing page over time.
- `reports.landing_page.channels`: Return landing page performance by attribution channel.

### Audiences
- `audiences.list`: List saved system and custom audiences for a sub-account.
- `audiences.get`: Get one saved audience, including its rules and estimated size.
- `audiences.create`: Create a custom audience with the same rule fields used by the dashboard.
- `audiences.update`: Update a custom audience name, description, or rules.
- `audiences.delete`: Delete a custom audience. System audiences cannot be deleted.

### Page groups
- `page_groups.list`: List saved Page groups for Multi-page targeting.
- `page_groups.get`: Get one saved Page group, including URL rules and default preview URL.
- `page_groups.create`: Create a saved Page group for Multi-page targeting.
- `page_groups.update`: Update a saved Page group.
- `page_groups.delete`: Delete a saved Page group.

### Experiments
- `experiments.list`: List experiments for a sub-account.
- `experiments.get`: Get one experiment's setup, status, selected goals, audience snapshot, targeting, and live all-time pixel totals.
- `experiments.create`: Create a draft experiment with UXON, external, or Multi-page targets, selectedGoals, filters, and optional saved audience snapshot.
- `experiments.create_and_launch`: Create and launch a modern A/B/n experiment with one control and up to four redirect variants in one command.
- `experiments.update`: Update mutable experiment settings, including traffic, advanced audience targeting, and audience snapshot, without changing status.
- `experiments.set_status`: Set experiment status for run, pause, draft, or end actions.
- `reports.experiments.summary`: Return aggregate experiment totals and session split metrics.
- `reports.experiment.details`: Return detailed analytics and decision context for one experiment, with optional conversionGoalId.
- `reports.experiment.chart`: Return chart-ready time-series metrics for an experiment, with optional conversionGoalId.
- `reports.experiment.channels`: Return experiment performance segmented by channel, with optional conversionGoalId.

### Personalization
- `personalization.list`: List personalization campaigns for a sub-account.
- `personalization.get`: Get one personalization campaign with page scope, experiences, audiences, goals, and settings.
- `personalization.create`: Create a draft personalization campaign with page scope and targeted experiences.
- `personalization.create_and_launch`: Create and launch a personalization campaign with page scope and redirect experiences in one command.
- `personalization.update`: Update a draft personalization campaign's setup, page scope, experiences, goals, or redirect parameter behavior.
- `personalization.set_status`: Set personalization status to Draft, Running, Paused, or Ended.

### Reports
- `reports.account_health`: Return a practical account health snapshot across landing pages, experiments, personalization, and tracking setup.

### Domains
- `domains.list`: List domains for a sub-account.
- `domains.add`: Add a domain or subdomain to a sub-account.
- `domains.connect`: Run DNS verification and update domain connection status.

### Tracking
- `tracking.setup`: Configure tracking using GTM (head + body) or direct GA4/Meta with anti-duplication guardrails.

### Goals
- `goals.list`: List conversion goals for a sub-account.
- `goals.create`: Create a new conversion goal.
- `goals.update`: Update goal metadata or activation status.

### Contacts
- `reports.contacts.list`: List contacts with filters and pagination.
- `reports.contacts.detail`: Fetch contact profile and latest submission details.

## 9) Agent Execution Protocol (Mandatory)

For every action, follow this order:

1. Validate command and required fields locally before request.
2. Resolve IDs first (never guess IDs).
3. Send minimal payload containing only intended fields.
4. Parse response and verify expected keys are present.
5. Log command + input + response + timestamp for audit.
6. If write command changes state, read back to confirm.

## 10) ID Discovery Protocol

When you do not have IDs, use this sequence:

1. `subaccounts.list` to get `siteId`.
2. `branding.get` to inspect saved branding for a sub-account.
3. `goals.list` to get goal IDs for `selectedGoals`.
4. `landing_pages.list` for landing page IDs.
5. `reports.landing_page.details` for landing page variant IDs.
6. `experiments.list` for experiment IDs.
7. `audiences.list` for audience IDs.
8. `page_groups.list` for saved Multi-page Page group IDs.
9. `personalization.list` for personalization campaign IDs.
10. `domains.list` for domain IDs.
11. `reports.contacts.list` for contact IDs.

## 11) High-Value Agency Runbooks

### A) Account health check across LPs, experiments, and personalization

Sequence:

1. Use `reports.account_health` first.
2. If the user asks for all accounts, omit `siteId` and keep `maxSites` reasonable.
3. If one account needs deeper detail, use the returned `nextCommands`.
4. Use `reports.landing_page.details`, `reports.landing_page.channels`, `reports.experiment.details`, `reports.experiment.chart`, and `reports.experiment.channels` only for the accounts or campaigns that need investigation.
5. For personalization setup detail, use `personalization.get` until dedicated personalization report commands exist.
6. Produce one per-site summary: live LP count, running experiment count, running personalization count, domain setup, reporting summaries, and attention items.

Bulk behavior:

- Use `dateRange: "last-30-days"` unless the user asks for a different window.
- Process accounts in small batches and watch `X-RateLimit-Remaining`.
- On `429`, wait for `X-RateLimit-Reset` seconds before retrying.
- `subaccounts.list.snapshot.landingPages`, `snapshot.experiments`, and `snapshot.domains` are counts, not full objects.

### B) Generate a landing page from brief, review it, then prepare publishing

Sequence:

1. Resolve the client with `subaccounts.list` if `siteId` is not already known.
2. Start generation with `landing_pages.generate_from_brief`.
3. Poll `landing_pages.generation_status` until `status` is `complete` or `failed`.
4. Read the page with `landing_pages.get` and check generated variants/URLs.
5. If review is needed, create a review link with `landing_pages.review_link`.
6. To prepare publishing, update only `settings.publish` through `landing_pages.update`:
   - `domain`
   - `path`
   - `hideFromSearch`
7. After user approval, use `landing_pages.set_status` with `status: "live"`.

Important:

- Do not write `settings.published`.
- Do not construct builder state manually.
- Use `landing_pages.publish_changes` only for an already-live page with draft changes.

### C) Onboard a new client and prepare first setup

Sequence:

1. Ask for client name, website URL, reporting currency (default `USD`), reporting timezone only if the client timezone is not known (IANA format; default `UTC`), and landing page feedback mode (default `free_comments`).
2. `subaccounts.create`
3. UXON automatically starts brand import and website media scraping after create.
4. `branding.get` to inspect imported colors/logos/voice readiness.
5. Optional: `branding.update` only when the user wants manual color, logo, font, custom font, or voice adjustments.
6. `tracking.setup`
7. `domains.add`
8. `domains.connect`
9. Optional after setup: `goals.create`, `landing_pages.generate_from_brief` for an AI page, or `landing_pages.create` for a blank builder page, then `landing_pages.set_status` when ready.

### D) Create and launch a UXON-page experiment

Sequence:

1. `goals.list` or `goals.create` to resolve selected conversion goals.
2. `landing_pages.list` to find candidate Control and Variant pages.
3. `landing_pages.get` or `reports.landing_page.details` to get the exact landing page variant IDs.
4. Use `experiments.create_and_launch` with:
   - `control.source: "uxon"`
   - `control.landingPageId`
   - `control.landingPageVariantId`
   - `control.trafficWeight`
   - `variants[]` with UXON or external redirect rows
   - `selectedGoals`
5. Read back with `experiments.get` only if more detail is needed.
6. Monitor with `reports.experiment.details`, `reports.experiment.chart`, and `reports.experiment.channels`.

Important:

- `experiments.create_and_launch` launches redirect variants only. For edit-original page variants, create a draft first and use the Page editor flow before launch.
- Use `experiments.create` + `experiments.set_status` only when a legacy A/B-only integration needs the old shape.
- Never guess landing page variant IDs. Resolve them first.

### E) Create and launch an external URL experiment

Sequence:

1. `goals.list` or `goals.create` to resolve selected conversion goals.
2. Use `experiments.create_and_launch` with:
   - `control.source: "external"` and `control.pageUrl`, or UXON control with resolved LP IDs
   - `variants[]` rows using `experienceType: "redirect"` with `sourceType: "external"` or `sourceType: "uxon"`
   - traffic weights that total 100 across Control and all variants
   - `selectedGoals`
   - `preserveRedirectParams: true` unless the user says otherwise
3. Read back with `experiments.get` only if more detail is needed.
4. Monitor using `reports.experiment.details`, `reports.experiment.chart`, and `reports.experiment.channels`.

### F) Create an audience and use it in an experiment

Sequence:

1. Use `audiences.list` first to check whether a suitable system/custom audience already exists.
2. If needed, create a custom audience with `audiences.create`.
3. Create and launch the experiment with `experiments.create_and_launch` and `audienceId`, or update an existing draft with `experiments.update`.
4. Read back with `experiments.get` to confirm the audience snapshot.

Important:

- `system:<key>` audience IDs are valid, for example `system:all-visitors`.
- When `audienceId` is saved to a campaign, UXON snapshots the audience rules. Later audience edits do not silently update existing campaigns.

### G) Create a Page group and use it in personalization

Sequence:

1. Use `page_groups.list` to check whether the reusable Multi-page scope already exists.
2. If needed, create it with `page_groups.create`.
3. Use the returned `pageGroupId` in `personalization.create_and_launch`, `personalization.create`, or `personalization.update` with `pageScope.source: "page_group"`.
4. Add at least one active experience.
5. If using the wrapper, it launches and returns the created campaign. If using low-level commands, read back with `personalization.get` and launch with `personalization.set_status`.

Important:

- Page group rules require at least one `include` condition.
- `defaultPreviewUrl` is recommended so the editor knows which page to show first.

### H) Create a personalization campaign with targeted experiences

Sequence:

1. Resolve page scope:
   - use `landing_pages.list` / `landing_pages.get` for UXON page scope;
   - use a direct URL for external page scope;
   - use `page_groups.list` / `page_groups.create` for Multi-page scope.
2. Resolve audiences with `audiences.list` or create custom audiences with `audiences.create`.
3. Use `personalization.create_and_launch` for redirect experiences, or `personalization.create` for edit-original experiences that need Page editor changes before launch.
4. Include `experiences[]`; each experience should have:
   - `name`
   - `experienceType`: `redirect` for create-and-launch; `edit_original_page` only for draft/page-editor flows
   - `sourceType`: `uxon` or `external` for redirect; `original` only for edit-original drafts
   - `audienceId`
5. Use `measureImpactEnabled` and `originalComparisonPercentage` when the user wants original comparison.
6. Read back with `personalization.get` only if more detail is needed.

Important:

- `personalization.create_and_launch` launches redirect experiences only. Edit-original experiences should be drafted, edited in the Page editor, then launched after saved Page editor Changes exist.
- Sending `experiences` to `personalization.update` replaces the experience list.
- Use `personalization.create` + `personalization.set_status` only when an integration deliberately needs the low-level two-step flow.
- Use `preserveRedirectParams: true` when redirect experiences exist unless the user says otherwise.

### I) Weekly optimization cycle

Sequence:

1. `reports.landing_page.channels`
2. `reports.landing_page.trends`
3. `reports.experiment.channels`
4. `reports.experiment.chart`
5. `personalization.list` and `personalization.get` for running personalization setup state until dedicated personalization report commands exist.
6. `experiments.update`, `experiments.set_status`, `personalization.update`, `personalization.set_status`
7. `landing_pages.update`, `landing_pages.publish_changes`, or `landing_pages.set_status`

## 12) Command Reference (Detailed)

This section is command-by-command so agents can execute without assumptions.

---

## Sub-accounts

### `subaccounts.list`

Purpose:

- List all accessible sub-accounts and an operational snapshot per account.

Required input:

- none

Optional input:

- none

Request example:

```json
{
  "command": "subaccounts.list",
  "input": {}
}
```

Expected output keys:

- `data.subaccounts[]`
- `id`, `name`, `slug`, `websiteUrl`, `shortId`
- `snapshot.landingPages`, `snapshot.experiments`, `snapshot.domains`

Common failures:

- `401` invalid/missing key
- `403` API Keys not enabled for account

### `subaccounts.get`

Purpose:

- Fetch one sub-account with metadata and totals.

Required input:

- `siteId`

Request example:

```json
{
  "command": "subaccounts.get",
  "input": {
    "siteId": "site_uuid"
  }
}
```

Expected output keys:

- `data.subaccount`
- `snapshot` totals

Common failures:

- `400` missing `siteId`
- `404` sub-account not found

### `subaccounts.create`

Purpose:

- Create a new client sub-account.

Required input:

- `name`
- `websiteUrl`

Validation notes:

- `websiteUrl` must be valid.
- `reportingCurrency` defaults to `USD`; ask the user for the right 3-letter code if it is not obvious.
- `reportingTimezone` must be a valid IANA timezone such as `UTC`, `Australia/Brisbane`, `America/New_York`, or `Europe/London`.
- If a user says the client is in Brisbane, send `Australia/Brisbane`; if they say New York, send `America/New_York`.
- Ask for timezone only if locality/timezone is unknown and local reporting windows matter.
- If omitted, the API stores `UTC`.
- `landingPageFeedbackMode` defaults to `free_comments`.
- Use `free_comments` for open landing page comments.
- Use `ai_approvals` when the user wants the AI-assisted approval workflow to review and moderate feedback.
- `subaccounts.create` automatically starts background brand import and website media scraping.
- Do not ask for brand colors, typography, brand voice, or logo URL during normal client account creation.
- Optional account logos are handled separately through branding/logo tools, not by this command.
- Creation depends on plan limits.

Request example:

```json
{
  "command": "subaccounts.create",
  "input": {
    "name": "ACME Dental",
    "websiteUrl": "https://acmedental.com",
    "reportingCurrency": "USD",
    "reportingTimezone": "Australia/Brisbane",
    "landingPageFeedbackMode": "free_comments"
  }
}
```

Expected output keys:

- `data.subaccount.id`
- `data.subaccount.name`
- `data.subaccount.reportingCurrency`
- `data.subaccount.reportingTimezone`
- `data.subaccount.landingPageFeedbackMode`

Common failures:

- `400` invalid URL or missing required fields
- `403` plan cannot create more sub-accounts

### `subaccounts.update`

Purpose:

- Update sub-account profile fields.

Required input:

- `siteId`

Optional input:

- `name`
- `websiteUrl`
- `reportingCurrency`
- `reportingTimezone`
- `landingPageFeedbackMode`
- `featured`

Request example:

```json
{
  "command": "subaccounts.update",
  "input": {
    "siteId": "site_uuid",
    "name": "ACME Dental Group",
    "reportingCurrency": "AUD",
    "reportingTimezone": "Australia/Brisbane",
    "landingPageFeedbackMode": "ai_approvals"
  }
}
```

Expected output keys:

- `data.subaccount.id`
- `data.subaccount.updatedAt`

Common failures:

- `400` invalid `websiteUrl`
- `404` sub-account not found

---

## Branding

### `branding.get`

Purpose:

- Fetch the branding overview saved for a sub-account.
- Use this before building pages, triggering AI page generation, or making manual brand adjustments.
- This returns the persisted branding data UXON uses for landing page defaults.

Required input:

- `siteId`

Request example:

```json
{
  "command": "branding.get",
  "input": {
    "siteId": "site_uuid"
  }
}
```

Expected output keys:

- `data.branding.colorVariables`
- `data.branding.brandColors`
- `data.branding.logo`
- `data.branding.colorPalettes[]`
- `data.branding.logos[]`
- `data.branding.brandVoice`
- `data.branding.fonts`
- `data.branding.fontVariables`
- `data.branding.customFonts[]`
- `data.branding.readiness`

Example output shape:

```json
{
  "branding": {
    "colorVariables": {
      "pageBackground": "#FFFFFF",
      "headings": "#2F2217",
      "bodyText": "#343433",
      "button": "#F8C80B",
      "buttonText": "#111111",
      "lightBackground": "#F7F7E0",
      "darkBackground": "#2F2217",
      "starRating": "#FDBF01"
    },
    "brandColors": ["#F8C80B", "#343433", "#FFFFFF"],
    "logo": {
      "light": "https://example.com/logo-light.svg",
      "dark": "https://example.com/logo-dark.svg",
      "favicon": "https://example.com/favicon.png"
    },
    "logos": [],
    "brandVoice": "",
    "fonts": {
      "headings": {
        "fontFamily": "Inter",
        "fontWeight": "700 - Bold",
        "uppercase": false,
        "google": true
      },
      "bodyText": {
        "fontFamily": "Inter",
        "fontWeight": "400 - Regular",
        "uppercase": false,
        "google": true
      },
      "buttons": {
        "fontFamily": "",
        "fontWeight": "500 - Medium",
        "uppercase": false,
        "google": false,
        "source": "heading"
      }
    },
    "fontVariables": {
      "headings": {
        "fontFamily": "Inter",
        "fontWeight": "700 - Bold",
        "uppercase": false,
        "google": true
      },
      "bodyText": {
        "fontFamily": "Inter",
        "fontWeight": "400 - Regular",
        "uppercase": false,
        "google": true
      },
      "buttons": {
        "fontFamily": "",
        "fontWeight": "500 - Medium",
        "uppercase": false,
        "google": false,
        "source": "heading"
      },
      "updatedAt": "2026-07-21T00:00:00.000Z"
    },
    "customFonts": [],
    "readiness": {
      "hasColorVariables": true,
      "hasBrandColors": true,
      "hasLogo": false,
      "hasBrandVoice": false,
      "hasFonts": false,
      "isReady": false
    }
  }
}
```

Readiness notes:

- `isReady` means UXON has color variables, brand colors, and at least one logo.
- `hasBrandVoice` and `hasFonts` are useful quality signals, but they do not block `isReady`.
- `logo.light` is the light-coloured or white logo variant used on dark backgrounds.
- `logo.dark` is the dark-coloured logo variant used on light backgrounds.
- `logo.favicon` is the square icon used for browser/app metadata.

Common failures:

- `400` missing `siteId`
- `404` sub-account not found

### `branding.update`

Purpose:

- Update editable branding fields for a sub-account.
- Use this only when the user explicitly asks for branding adjustments.

Required input:

- `siteId`
- At least one of `colorVariables`, `brandColors`, `logo`, `fonts`, `customFonts`, `paletteName`, or `brandVoice`

Editable fields:

- `colorVariables`: named landing page colors. Preferred keys are `pageBackground`, `headings`, `bodyText`, `button`, `buttonText`, `lightBackground`, `darkBackground`, and `starRating`.
- `brandColors`: ordered palette array. Send this only when intentionally replacing the brand color palette.
- `logo`: direct HTTP/HTTPS image URLs by role.
- `fonts`: heading/body/button font variables. Supported slots are `headings`, `bodyText`, and `buttons`. Each slot accepts `fontFamily`, `fontWeight`, `uppercase`, `google`, and for buttons `source: "heading" | "body"`.
- `customFonts`: custom font URL imports. Each font needs a `name` and one or more direct `.woff2`, `.woff`, `.ttf`, or `.otf` URLs. UXON imports and hosts successful files.
- `paletteName`: current palette label.
- `brandVoice`: brand voice guidance text.

Validation notes:

- Colour values must be 3- or 6-digit hex values, with or without `#`.
- UXON normalizes saved hex values to uppercase `#RRGGBB`.
- `branding.update` is partial. Omitted fields are preserved.
- Font weights can use UXON labels such as `400 - Regular` and `700 - Bold`, or numeric weights such as `400` and `700`.
- Existing kebab-case color variable keys are also accepted: `page-background`, `body-text`, `button-background`, `button-text`, `light-background`, `dark-background`, `star-rating`.
- `button` maps to UXON's primary button background color.
- Logo values must be direct HTTP/HTTPS image URLs.
- `logo.light` should be a light-coloured or white logo for dark backgrounds.
- `logo.dark` should be a dark-coloured logo for light backgrounds.
- `logo.favicon` should be a square browser/app icon.
- Omitted logo roles are preserved.
- Omitted font slots are preserved.
- Use the web app or dedicated logo workflows for binary file uploads when no direct image URL exists.
- Use the web app or dedicated font upload workflow for binary font uploads when no direct font URL exists.

Request example:

```json
{
  "command": "branding.update",
  "input": {
    "siteId": "site_uuid",
    "colorVariables": {
      "pageBackground": "#FFFFFF",
      "headings": "#2F2217",
      "bodyText": "#343433",
      "button": "#F8C80B",
      "buttonText": "#111111",
      "lightBackground": "#F7F7E0",
      "darkBackground": "#2F2217",
      "starRating": "#FDBF01"
    },
    "brandColors": [
      "#F8C80B",
      "#947C1C",
      "#343433",
      "#AA7944",
      "#E4D3B2",
      "#2F2217",
      "#FFFFFF"
    ],
    "logo": {
      "light": "https://example.com/logo-light.svg",
      "dark": "https://example.com/logo-dark.svg",
      "favicon": "https://example.com/favicon.png"
    },
    "fonts": {
      "headings": {
        "fontFamily": "Inter",
        "fontWeight": "700 - Bold",
        "uppercase": false,
        "google": true
      },
      "bodyText": {
        "fontFamily": "Inter",
        "fontWeight": "400 - Regular",
        "uppercase": false,
        "google": true
      },
      "buttons": {
        "source": "heading",
        "fontWeight": "600 - Semi-Bold",
        "uppercase": false
      }
    },
    "customFonts": [
      {
        "name": "Brand Sans",
        "variants": [
          {
            "url": "https://cdn.example.com/fonts/brand-sans-400.woff2",
            "weight": "400",
            "style": "Normal"
          },
          {
            "url": "https://cdn.example.com/fonts/brand-sans-700.woff2",
            "weight": "700",
            "style": "Normal"
          }
        ]
      }
    ],
    "paletteName": "Brand colours",
    "brandVoice": "Clear, useful, confident, and specific. Avoid hype."
  }
}
```

Partial update example:

```json
{
  "command": "branding.update",
  "input": {
    "siteId": "site_uuid",
    "colorVariables": {
      "button": "#F8C80B",
      "buttonText": "#111111"
    },
    "logo": {
      "dark": "https://example.com/logo-dark.svg"
    },
    "fonts": {
      "buttons": {
        "source": "heading",
        "fontWeight": "600 - Semi-Bold"
      }
    }
  }
}
```

Expected output keys:

- `data.updatedFields[]`
- `data.branding`

Common failures:

- `400` invalid hex color, invalid logo URL, invalid font URL, or no editable field supplied
- `404` sub-account not found

---

## Landing pages

### `landing_pages.list`

Purpose:

- List landing pages for one sub-account.

Required input:

- `siteId`

Request example:

```json
{
  "command": "landing_pages.list",
  "input": {
    "siteId": "site_uuid"
  }
}
```

Expected output keys:

- `data.landingPages[]`
- `id`, `name`, `status`, `variantCount`, metrics summary fields

Common failures:

- `400` missing `siteId`

### `landing_pages.create`

Purpose:

- Create a draft landing page with one blank builder variant.

Required input:

- `siteId`
- `name`

Optional input:

- `settings`
- `knowledgeBase`

Important behavior:

- UXON creates one blank `Control` builder variant.
- The blank variant uses existing brand colors, fonts, logos, and quick-setup defaults when they exist.
- This does not run UXON AI landing page generation. Use `landing_pages.generate_from_brief` for that.
- `settings` is a JSON object. The agent-writable shape is `{ "publish": { "domain": "www.example.com", "path": "/offer", "hideFromSearch": true } }`.
- `settings.publish.domain` should be a connected domain hostname. `settings.publish.path` should be `/` or a published path such as `/offer`.
- `settings.publish.hideFromSearch` is a boolean. If `domain` or `path` is provided and `hideFromSearch` is omitted, UXON defaults it to `true`.
- Do not send `settings.published`. That is UXON's internal full page-builder/published snapshot.
- Do not construct page-builder state inside `settings`.
- `knowledgeBase` is a JSON object for notes/brief context. It is stored; it does not trigger AI research or generation.
- Recommended `knowledgeBase` shape: `{ "summary": "Short context", "notes": ["Important note"], "links": [{ "label": "Brief", "url": "https://example.com" }], "raw": {} }`.

Request example:

```json
{
  "command": "landing_pages.create",
  "input": {
    "siteId": "site_uuid",
    "name": "Summer Promo"
  }
}
```

Expected output keys:

- `data.landingPage.id`
- `data.landingPage.variants[]`

Common failures:

- `400` missing required fields
- `403` feature not in plan

### `landing_pages.update`

Purpose:

- Update landing page metadata, settings, or knowledge base.

Required input:

- `siteId`
- one of `landingPageId` or `id`

Optional input:

- `name`
- `settings`
- `knowledgeBase`

Important behavior:

- Do not send `status`; use `landing_pages.set_status`.
- Do not send variant changes; use `landing_pages.variants.*`.
- This does not run UXON AI landing page generation.
- `settings` and `knowledgeBase` use the same object shapes as `landing_pages.create`.
- If updating publish settings, send only the `settings.publish` object. Do not send `settings.published` or builder state.

Request example:

```json
{
  "command": "landing_pages.update",
  "input": {
    "siteId": "site_uuid",
    "landingPageId": "lp_uuid",
    "name": "Summer Promo - Rev 2"
  }
}
```

Common failures:

- `400` missing page id
- `404` landing page not found

### `landing_pages.get`

Purpose:

- Get landing page metadata, variants, and preview/live URLs.

Required input:

- `siteId`
- one of `landingPageId` or `id`

Request example:

```json
{
  "command": "landing_pages.get",
  "input": {
    "siteId": "site_uuid",
    "landingPageId": "lp_uuid"
  }
}
```

Expected output keys:

- `data.landingPage.id`
- `data.landingPage.urls.previewUrl`
- `data.landingPage.urls.liveUrl`
- `data.landingPage.variants[]`

### `landing_pages.generate_from_brief`

Purpose:

- Start the real UXON AI landing page generation pipeline from an approved brief.
- Use this when the user asks to create or generate an AI landing page.

Required input:

- `siteId`
- `brief`
- one of `landingPageId`, `pageId`, or `name`

Brief object fields:

- `brandName`
- `websiteUrl`
- `language`
- `funnelStrategy`
- `buyerGroup`
- `desiredAction`
- `market`
- `serviceOrProduct`
- `idealClient`
- optional `selectedProduct`

Important behavior:

- If `landingPageId` or `pageId` is provided, UXON adds an AI-generated variant to that landing page.
- If only `name` is provided, UXON first creates a draft landing page shell, then starts AI generation.
- The command returns immediately with a `jobId`. Poll `landing_pages.generation_status`.
- The page is not automatically published.
- Build requests are not exposed through API/MCP. Paid done-for-you landing page build requests require in-app checkout/payment.

Request example:

```json
{
  "command": "landing_pages.generate_from_brief",
  "input": {
    "siteId": "site_uuid",
    "name": "Spring Offer",
    "brief": {
      "brandName": "ACME Dental",
      "websiteUrl": "https://www.acmedental.com",
      "language": "English (US)",
      "funnelStrategy": "Service",
      "buyerGroup": "Consumers (B2C)",
      "desiredAction": "Submit a form",
      "market": "Brisbane, Queensland",
      "serviceOrProduct": "Emergency dental appointments for new patients",
      "idealClient": "Adults with urgent dental pain who need a same-day appointment"
    }
  }
}
```

Expected output keys:

- `data.landingPage.id`
- `data.generation.jobId`
- `data.generation.variantId`
- `data.generation.stage`

### `landing_pages.generation_status`

Purpose:

- Poll a UXON AI landing page generation job.

Required input:

- `siteId`
- `jobId`

Request example:

```json
{
  "command": "landing_pages.generation_status",
  "input": {
    "siteId": "site_uuid",
    "jobId": "job_uuid"
  }
}
```

Expected output keys:

- `data.generation.status`
- `data.generation.stage`
- `data.generation.progress`
- `data.generation.error`

### `landing_pages.variants.create`

Purpose:

- Create a blank builder variant, duplicate a builder variant, or create an empty code variant.

Required input:

- `siteId`
- `landingPageId`

Optional input:

- `name`
- `sourceVariantId` to duplicate an existing builder variant
- `variantType`: `builder` or `code`
- `makeMain`

### `landing_pages.variants.get`

Purpose:

- Get one landing page variant, including builder state when available.

Required input:

- `siteId`
- `landingPageId`
- `variantId`

### `landing_pages.variants.update`

Purpose:

- Update one variant's name, settings, or builder state.

Required input:

- `siteId`
- `landingPageId`
- `variantId`

Optional input:

- `name`
- `settings`
- `builderState`

### `landing_pages.variants.set_main`

Purpose:

- Set the main variant for a landing page.

Required input:

- `siteId`
- `landingPageId`
- `variantId`

### `landing_pages.variants.duplicate`

Purpose:

- Duplicate one builder variant.

Required input:

- `siteId`
- `landingPageId`
- `variantId`

Optional input:

- `name`
- `makeMain`

### `landing_pages.variants.delete`

Purpose:

- Delete one landing page variant.

Required input:

- `siteId`
- `landingPageId`
- `variantId`

### `landing_pages.duplicate`

Purpose:

- Duplicate a landing page and its builder variants as a new draft.

Required input:

- `siteId`
- one of `landingPageId` or `id`

Optional input:

- `name`

### `landing_pages.delete`

Purpose:

- Delete a landing page if it is not used by an experiment.

Required input:

- `siteId`
- one of `landingPageId` or `id`

### `landing_pages.publish_changes`

Purpose:

- Publish current draft changes for an already-live landing page.

Required input:

- `siteId`
- one of `landingPageId` or `id`

### `landing_pages.review_link`

Purpose:

- Create a review link for the main or specified variant.

Required input:

- `siteId`
- one of `landingPageId` or `id`

Optional input:

- `variantId`
- `variantKey`

### `landing_pages.code_variant.upload_init`

Purpose:

- Create a signed upload URL for a custom code ZIP variant.

Required input:

- `siteId`
- `landingPageId`
- `variantId`
- `fileName` ending in `.zip`
- `size` in bytes

Important behavior:

- Use after creating a code variant with `landing_pages.variants.create` and `variantType="code"`.
- Upload the ZIP to the returned `uploadUrl` using `contentType`.
- Then call `landing_pages.code_variant.upload_complete` with the returned `uploadToken`.

### `landing_pages.code_variant.upload_complete`

Purpose:

- Import a custom code ZIP after the signed upload completes.

Required input:

- `siteId`
- `landingPageId`
- `variantId`
- `uploadToken`

### `landing_pages.code_variant.upload`

Purpose:

- Upload or replace a custom externally coded landing page ZIP as a UXON variant.
- Use this for Claude Code, Codex, Cursor, or other tools that generate a static page bundle outside UXON.

Required input:

- `siteId`
- one of `landingPageId` or `id`
- exactly one of `zipBase64` or `zipUrl`

Optional input:

- `variantId` to replace an existing code variant ZIP
- `variantName` when creating a new code variant
- `fileName` ending in `.zip`
- `makeMain` to make the uploaded variant the main variant
- `publish` to publish the landing page after upload

ZIP format:

- Upload final built static output, not source project files.
- Include `index.html` and all referenced assets in the ZIP.
- Supported assets include CSS, JS, JSON, SVG, images, fonts, audio/video, manifest, XML, source maps, and WASM.
- Maximum ZIP size is 250MB.
- UXON injects pixels, Global Scripts, and page metadata automatically.

Important behavior:

- If `variantId` is omitted, UXON creates a new code variant.
- If `variantId` is provided, UXON replaces that variant's ZIP.
- `publish` defaults to `false`; use `landing_pages.set_status` later or pass `publish: true` when ready.
- `makeMain` defaults to `false`; pass `makeMain: true` only when this variant should receive main traffic.

Request example with `zipUrl`:

```json
{
  "command": "landing_pages.code_variant.upload",
  "input": {
    "siteId": "site_uuid",
    "landingPageId": "lp_uuid",
    "variantName": "Claude Code Variant",
    "zipUrl": "https://example.com/builds/landing-page.zip",
    "makeMain": false,
    "publish": false
  }
}
```

Request example with `zipBase64`:

```json
{
  "command": "landing_pages.code_variant.upload",
  "input": {
    "siteId": "site_uuid",
    "landingPageId": "lp_uuid",
    "variantId": "variant_uuid",
    "fileName": "landing-page.zip",
    "zipBase64": "UEsDB...",
    "makeMain": true,
    "publish": true
  }
}
```

Expected output keys:

- `data.variant.id`
- `data.variant.urlKey`
- `data.upload.status`
- `data.urls.previewUrl`
- `data.urls.liveUrl`

Common failures:

- `400` missing page id, missing ZIP source, unsupported ZIP format, or ZIP too large
- `403` custom code pages not included in plan
- `404` landing page or variant not found

### `landing_pages.set_status`

Purpose:

- Set page status to `draft` or `live`.

Required input:

- `siteId`
- one of `landingPageId` or `id`
- `status` (`draft` or `live`)

Request example:

```json
{
  "command": "landing_pages.set_status",
  "input": {
    "siteId": "site_uuid",
    "landingPageId": "lp_uuid",
    "status": "live"
  }
}
```

Common failures:

- `400` invalid status or missing id
- `404` landing page not found

### `reports.landing_pages.summary`

Purpose:

- Aggregate landing page KPIs for one site.
- Page counts are current landing-page inventory.
- Sessions, conversions, unique conversions, and revenue are calculated from pixel tracking data inside the selected reporting window.
- Conversion totals exclude `PageView` and `Bounce`.

Required input:

- `siteId`

Optional input:

- `dateRange`
- `from`
- `to`
- `timezone`

Request example:

```json
{
  "command": "reports.landing_pages.summary",
  "input": {
    "siteId": "site_uuid",
    "dateRange": "last-30-days"
  }
}
```

Expected output keys:

- `data.summary.totalPages`
- `data.summary.livePages`
- `data.summary.draftPages`
- `data.summary.sessions`
- `data.summary.conversions`
- `data.summary.uniqueConversions`
- `data.summary.revenue`
- `data.summary.dateRange`

### `reports.landing_page.details`

Purpose:

- Detailed landing page record with variants and comparison summary.

Required input:

- `siteId`
- one of `landingPageId` or `id`

Optional input:

- `dateRange`
- `from`
- `to`
- `timezone`

Important use:

- Use this command to get landing page variant IDs needed for experiments.

Request example:

```json
{
  "command": "reports.landing_page.details",
  "input": {
    "siteId": "site_uuid",
    "landingPageId": "lp_uuid"
  }
}
```

Expected output keys:

- `data.landingPage.id`
- `data.landingPage.name`
- `data.landingPage.slug`
- `data.landingPage.status`
- `data.landingPage.variants[]`
- `data.landingPage.variants[].id`
- `data.landingPage.variants[].name`
- `data.landingPage.variants[].isMain`
- `data.landingPage.variants[].urlKey`
- `data.summary.sessions`
- `data.summary.allEvents`
- `data.summary.uniqueConversions`
- `data.summary.conversionRate`
- `data.summary.revenue`

### `reports.landing_page.variant_metrics`

Purpose:

- Variant-level KPI rows for one landing page.

Required input:

- `siteId`
- one of `landingPageId` or `id`

Optional input:

- `dateRange`
- `from`
- `to`
- `timezone`

Request example:

```json
{
  "command": "reports.landing_page.variant_metrics",
  "input": {
    "siteId": "site_uuid",
    "landingPageId": "lp_uuid"
  }
}
```

Expected output keys:

- `data.metrics[]`
- `data.metrics[].variantId`
- `data.metrics[].variantName`
- `data.metrics[].sessions`
- `data.metrics[].conversions`
- `data.metrics[].uniqueConversions`
- `data.metrics[].conversionRate`
- `data.metrics[].revenue`

### `reports.landing_page.trends`

Purpose:

- Time-series trends for landing page metrics.

Required input:

- `siteId`
- one of `landingPageId` or `id`

Optional input:

- `dateRange`
- `from`
- `to`
- `timezone`
- `metric`
- `granularity` (`day`, `week`, `month`)

Request example:

```json
{
  "command": "reports.landing_page.trends",
  "input": {
    "siteId": "site_uuid",
    "landingPageId": "lp_uuid",
    "granularity": "day"
  }
}
```

Expected output keys:

- `data.granularity`
- `data.chartDataByMetric`
- `data.chartDataByMetric.sessions[]`
- `data.chartDataByMetric.conversions[]`
- `data.chartDataByMetric.revenue[]`
- `data.chartDataByMetric.conversion_rate[]`

### `reports.landing_page.channels`

Purpose:

- Channel attribution view for one landing page.

Required input:

- `siteId`
- one of `landingPageId` or `id`

Optional input:

- `eventId` (goal/event filter)
- `dateRange`
- `from`
- `to`
- `timezone`

Request example:

```json
{
  "command": "reports.landing_page.channels",
  "input": {
    "siteId": "site_uuid",
    "landingPageId": "lp_uuid",
    "dateRange": "last-30-days"
  }
}
```

Expected output keys:

- `data.rows[]`
- `data.rows[].channel`
- `data.rows[].sessions`
- `data.rows[].conversions`
- `data.rows[].conversionRate`
- `data.rows[].revenue`

---

## Audiences

Audience commands manage reusable targeting rules for a sub-account. Use them before experiments or personalization when the user wants targeting that can be reused in the dashboard.

Agent workflow:

- Use `audiences.list` first to find existing system and custom audiences.
- Use a returned `id` for custom audiences.
- Use `system:<systemKey>` for system audiences, for example `system:all-visitors`.
- When an `audienceId` is assigned to an experiment or personalization experience, UXON snapshots the current rules at save time. Later edits to the saved audience do not automatically change campaigns already using an older snapshot.
- To refresh a campaign snapshot, update that campaign or experience and send the `audienceId` again.

Audience rule shape:

```json
{
  "operator": "and",
  "conditions": [
    { "field": "country", "operator": "equals", "value": "US" },
    { "field": "hostname", "operator": "equals", "value": "www.example.com" }
  ]
}
```

Supported fields:

- `channel`
- `event_name`
- `utm_source`
- `utm_medium`
- `utm_campaign`
- `utm_content`
- `device`
- `visitor_type`
- `country`
- `region`
- `hostname`
- `browser_language`
- `viewport`
- `query_parameter`
- `url`
- `referrer`

Supported operators:

- `contains`
- `equals`
- `starts_with`
- `ends_with`

### `audiences.list`

Purpose:

- List saved system and custom audiences for one sub-account.
- Use before assigning `audienceId` to experiments or personalization experiences.

Required input:

- `siteId`

Optional input:

- `page`, `perPage`, `search`, `type`: `all`, `system`, or `custom`

Request example:

```json
{
  "command": "audiences.list",
  "input": {
    "siteId": "site_uuid",
    "search": "US",
    "type": "all",
    "page": 1,
    "perPage": 10
  }
}
```

Expected output keys:

- `data.audiences[]`
- `id`, `name`, `type`, `systemKey`, `rules`, `estimatedSize`
- `pagination`

Common failures:

- `403` experiments or personalization management is not included in the plan.
- `404` or scope errors when the API key cannot access the requested sub-account.

### `audiences.get`

Purpose:

- Get one saved audience with its current rules.
- Use this before changing an existing custom audience or before explaining what an audience targets.

Required input:

- `siteId`
- one of `audienceId` or `id`

Request example:

```json
{
  "command": "audiences.get",
  "input": {
    "siteId": "site_uuid",
    "audienceId": "audience_uuid"
  }
}
```

Expected output keys:

- `data.audience`
- `id`, `name`, `type`, `systemKey`, `description`
- `rules`
- `estimatedSize`
- `createdAt`, `updatedAt`

Common failures:

- `400` `audienceId` or `id` is missing.
- `404` audience not found or outside the API key scope.

### `audiences.create`

Purpose:

- Create a custom saved audience.

Required input:

- `siteId`
- `name`

Optional input:

- `description`
- `rules`

Request example:

```json
{
  "command": "audiences.create",
  "input": {
    "siteId": "site_uuid",
    "name": "US desktop visitors on pricing",
    "description": "Reusable audience for pricing tests.",
    "rules": {
      "operator": "and",
      "conditions": [
        { "field": "country", "operator": "equals", "value": "US" },
        { "field": "device", "operator": "equals", "value": "desktop" },
        { "field": "url", "operator": "contains", "value": "/pricing" }
      ]
    }
  }
}
```

Expected output keys:

- `data.audience`
- `id`, `name`, `type: "custom"`, `description`
- `rules`
- `estimatedSize`
- `createdAt`, `updatedAt`

Common failures:

- `400` name is missing.
- `400` invalid rule field, operator, or empty value.
- `400` invalid rule group shape.

### `audiences.update`

Purpose:

- Update a custom audience name, description, or rules.
- Use when the saved audience itself should change for future selections.

Required input:

- `siteId`
- one of `audienceId` or `id`

Optional input:

- `name`
- `description`
- `rules`

Request example:

```json
{
  "command": "audiences.update",
  "input": {
    "siteId": "site_uuid",
    "audienceId": "audience_uuid",
    "name": "US paid traffic",
    "rules": {
      "operator": "and",
      "conditions": [
        { "field": "country", "operator": "equals", "value": "US" },
        { "field": "utm_medium", "operator": "equals", "value": "cpc" }
      ]
    }
  }
}
```

Expected output keys:

- `data.audience`
- `id`, `name`, `type`, `systemKey`, `description`
- `rules`
- `estimatedSize`
- `createdAt`, `updatedAt`

Common failures:

- `400` no `audienceId` or `id`.
- `400` no valid name after trimming.
- `400` invalid rule field, operator, or value.
- `400` system audiences cannot be updated.
- `404` audience not found or outside scope.

### `audiences.delete`

Purpose:

- Delete a custom saved audience.
- Use only when the user clearly wants to remove the saved reusable audience.

Required input:

- `siteId`
- one of `audienceId` or `id`

Request example:

```json
{
  "command": "audiences.delete",
  "input": {
    "siteId": "site_uuid",
    "audienceId": "audience_uuid"
  }
}
```

Expected output keys:

- `data.success`
- `data.audience.id`
- `data.audience.deleted`

Common failures:

- `400` system audiences cannot be deleted.
- `400` no `audienceId` or `id`.
- `404` audience not found or outside scope.

---

## Page Groups

Page group commands manage reusable Multi-page URL targeting. Use them when the user wants a named page scope such as "Pricing pages", "Service pages", or "All blog articles".

Agent workflow:

- Use `page_groups.list` before creating a duplicate group.
- Use `page_groups.create` when the user defines a reusable group from URL rules.
- Use a returned `pageGroupId` in `personalization.pageScope` with `source: "page_group"`.
- For one-off URL matching that does not need to be saved, use `pageScope.source: "url_patterns"` on personalization instead.

Page group rule shape:

```json
{
  "operator": "and",
  "conditions": [
    { "type": "include", "operator": "contains", "value": "/pricing" },
    { "type": "exclude", "operator": "contains", "value": "/thank-you" }
  ]
}
```

Rules require at least one `include` condition. Supported operators are `is`, `contains`, `starts_with`, and `ends_with`.

### `page_groups.list`

Purpose:

- List saved Page groups for Multi-page targeting.

Required input:

- `siteId`

Optional input:

- `page`, `perPage`, `search`

Request example:

```json
{
  "command": "page_groups.list",
  "input": {
    "siteId": "site_uuid",
    "search": "pricing",
    "page": 1,
    "perPage": 10
  }
}
```

Expected output keys:

- `data.pageGroups[]`
- `id`, `siteId`, `name`, `description`
- `rules`
- `defaultPreviewUrl`
- `createdAt`, `updatedAt`
- `pagination`

Common failures:

- `403` experiments or personalization management is not included in the plan.
- `404` or scope errors when the API key cannot access the requested sub-account.

### `page_groups.get`

Purpose:

- Get one saved Page group with URL rules and default editor preview URL.

Required input:

- `siteId`
- one of `pageGroupId` or `id`

Request example:

```json
{
  "command": "page_groups.get",
  "input": {
    "siteId": "site_uuid",
    "pageGroupId": "page_group_uuid"
  }
}
```

Expected output keys:

- `data.pageGroup`
- `id`, `siteId`, `name`, `description`
- `rules`
- `defaultPreviewUrl`
- `createdAt`, `updatedAt`

Common failures:

- `400` `pageGroupId` or `id` is missing.
- `404` Page group not found or outside scope.

### `page_groups.create`

Purpose:

- Create a saved Page group.

Required input:

- `siteId`
- `name`
- `rules`

Optional input:

- `description`
- `defaultPreviewUrl`

Request example:

```json
{
  "command": "page_groups.create",
  "input": {
    "siteId": "site_uuid",
    "name": "Pricing pages",
    "description": "All public pricing URLs except thank-you pages.",
    "defaultPreviewUrl": "https://www.example.com/pricing",
    "rules": {
      "operator": "and",
      "conditions": [
        { "type": "include", "operator": "contains", "value": "/pricing" },
        { "type": "exclude", "operator": "contains", "value": "/thank-you" }
      ]
    }
  }
}
```

Expected output keys:

- `data.pageGroup`
- `id`, `siteId`, `name`, `description`
- `rules`
- `defaultPreviewUrl`
- `createdAt`, `updatedAt`

Common failures:

- `400` name is missing.
- `400` rules are missing or have no `include` condition.
- `400` invalid rule operator or empty value.
- `400` invalid `defaultPreviewUrl`.

### `page_groups.update`

Purpose:

- Update a saved Page group name, description, default preview URL, or URL rules.

Required input:

- `siteId`
- one of `pageGroupId` or `id`

Optional input:

- `name`
- `description`
- `defaultPreviewUrl`
- `rules`

Request example:

```json
{
  "command": "page_groups.update",
  "input": {
    "siteId": "site_uuid",
    "pageGroupId": "page_group_uuid",
    "defaultPreviewUrl": "https://www.example.com/pricing",
    "rules": {
      "operator": "and",
      "conditions": [
        { "type": "include", "operator": "starts_with", "value": "/pricing" },
        { "type": "exclude", "operator": "contains", "value": "/pricing/internal" }
      ]
    }
  }
}
```

Expected output keys:

- `data.pageGroup`
- `id`, `siteId`, `name`, `description`
- `rules`
- `defaultPreviewUrl`
- `createdAt`, `updatedAt`

Common failures:

- `400` no `pageGroupId` or `id`.
- `400` no valid name after trimming.
- `400` rules are invalid or have no `include` condition.
- `400` invalid `defaultPreviewUrl`.
- `404` Page group not found or outside scope.

### `page_groups.delete`

Purpose:

- Delete a saved Page group.
- Use only after the user confirms the reusable group is no longer needed.

Required input:

- `siteId`
- one of `pageGroupId` or `id`

Request example:

```json
{
  "command": "page_groups.delete",
  "input": {
    "siteId": "site_uuid",
    "pageGroupId": "page_group_uuid"
  }
}
```

Expected output keys:

- `data.success`
- `data.pageGroup.id`
- `data.pageGroup.deleted`

Common failures:

- `400` no `pageGroupId` or `id`.
- `404` Page group not found or outside scope.

---

## Experiments

### `experiments.list`

Purpose:

- List experiments for one sub-account.
- Includes live all-time control/variant session and conversion totals calculated from pixel tracking data.

Required input:

- `siteId`

Request example:

```json
{
  "command": "experiments.list",
  "input": {
    "siteId": "site_uuid"
  }
}
```

Expected output keys:

- `data.experiments[]`
- `id`, `nickname`, `status`, `goal`, `startedAt`, `endedAt`
- live totals: `sessions`, `controlSessions`, `variantSessions`, `controlLeads`, `variantLeads`

### `experiments.get`

Purpose:

- Get one experiment's saved setup and current status.
- Use this to inspect configuration before updating or launching.
- Use report commands for date-range performance metrics.
- `totals` are live all-time pixel totals, not stale saved counters.

Required input:

- `siteId`
- one of `experimentId` or `id`

Request example:

```json
{
  "command": "experiments.get",
  "input": {
    "siteId": "site_uuid",
    "experimentId": "exp_uuid"
  }
}
```

Expected output keys:

- `data.experiment`
- `id`, `nickname`, `status`, `hypothesis`
- `goals[]`
- control setup: `controlSource`, `controlLandingPage`, `controlVariant`, `controlPageUrl`
- variant setup: `variantSource`, `variantLandingPage`, `variantVariant`, `variantPageUrl`
- targeting setup: `trafficAllocation`, `testTrigger`, `visitorType`, `deviceType`
- filters: `trafficFilters`, `urlPatterns`, `geoFilters`, `preserveRedirectParams`
- live totals: `totals.controlSessions`, `totals.variantSessions`, `totals.controlConversions`, `totals.variantConversions`

### `experiments.create`

Purpose:

- Create a new experiment in `Draft` status.

Required input:

- `siteId`
- `nickname`
- goal selector:
  - recommended: `selectedGoals`
  - legacy-compatible: `goalId` or `goal` (goal name)
- target setup for control and variant:
  - If source is `uxon`, provide landing page and variant IDs.
  - If source is `external`, provide page URL.
  - If source is `url_patterns`, provide `urlPatterns`.

Conditional requirements:

- control side:
  - `controlSource = "uxon"` requires `controlLandingPage` + `controlVariant`
  - `controlSource = "external"` requires `controlPageUrl`
  - `controlSource = "url_patterns"` requires `urlPatterns.side = "control"`
- variant side:
  - `variantSource = "uxon"` requires `variantLandingPage` + `variantVariant`
  - `variantSource = "external"` requires `variantPageUrl`
  - `variantSource = "url_patterns"` requires `urlPatterns.side = "variant"`

Optional tuning fields:

- `experimentTitle`
- `hypothesis`
- `testTrigger`: `entry`, `visits`
- `visitorType`: `all-visitors`, `new-visitors`, `returning-visitors`
- `deviceType`: `all-devices`, `desktop-only`, `mobile-only`
- `controlLabel`, `variantLabel`
- `trafficAllocation` (or `trafficSplit`)
- `trafficFilters`
- `urlPatterns`
- `geoFilters`
- `audienceId`
- `audienceRules`
- `preserveRedirectParams` (default `true`)

Critical note:

- This command creates `Draft` only.
- Use `selectedGoals` for new agents. Keep `goalId` / `goal` only for old integrations.
- Bounce cannot be combined with another goal.
- URL Patterns can be used on Control or Variant, not both.
- URL Patterns support up to 5 total conditions.
- `preserveRedirectParams` defaults to `true`. When a redirect sends visitors to another URL, UXON keeps UTMs, click IDs, and other query parameters unless this is set to `false`.
- Use `experiments.set_status` with `Running` to launch.
- `trafficFilters` are exact string key-value filters. Runtime compares stored tracking value exactly to the provided value.
- If `audienceId` is sent, UXON snapshots that audience's current rules into the experiment. Later changes to the saved audience do not automatically change this experiment.
- Running experiments can use saved Audience rules evaluated by Pixel Core v6.
- `trafficFilters` do not support regex, operators, arrays, includes/contains matching, or country targeting.
- Common app fields: `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`.
- Other stored tracking keys that can be matched when present: `utm_term`, `utm_id`, `utm_source_platform`, `gclid`, `gbraid`, `wbraid`, `fbclid`, `msclkid`, `ttclid`, `li_fat_id`, `twclid`.
- Do not put country filters in `trafficFilters`; use `geoFilters`.
- `geoFilters.countries.type` is `include` or `exclude`.
- `urlPatterns.conditions[].operator` is `is`, `contains`, `starts_with`, or `ends_with`.

UXON-to-UXON request example:

```json
{
  "command": "experiments.create",
  "input": {
    "siteId": "site_uuid",
    "nickname": "Headline Test",
    "selectedGoals": [
      { "id": "lead_goal_uuid", "name": "Lead" },
      { "id": "call_goal_uuid", "name": "Phone call" }
    ],
    "controlSource": "uxon",
    "controlLandingPage": "lp_control_uuid",
    "controlVariant": "lpv_control_uuid",
    "variantSource": "uxon",
    "variantLandingPage": "lp_variant_uuid",
    "variantVariant": "lpv_variant_uuid",
    "testTrigger": "entry",
    "trafficAllocation": 100,
    "visitorType": "all-visitors",
    "deviceType": "all-devices"
  }
}
```

Cross-domain request example:

```json
{
  "command": "experiments.create",
  "input": {
    "siteId": "site_uuid",
    "nickname": "Checkout Hero Test",
    "selectedGoals": [
      { "id": "lead_goal_uuid", "name": "Lead" }
    ],
    "controlSource": "external",
    "controlPageUrl": "https://www.example.com/checkout-a",
    "variantSource": "external",
    "variantPageUrl": "https://www.example.com/checkout-b",
    "preserveRedirectParams": true
  }
}
```

URL Pattern request example:

```json
{
  "command": "experiments.create",
  "input": {
    "siteId": "site_uuid",
    "nickname": "Pricing Redirect Test",
    "selectedGoals": [
      { "id": "lead_goal_uuid", "name": "Lead" }
    ],
    "controlSource": "url_patterns",
    "variantSource": "external",
    "variantPageUrl": "https://www.example.com/pricing-variant",
    "urlPatterns": {
      "side": "control",
      "conditions": [
        { "type": "include", "operator": "contains", "value": "/pricing" },
        { "type": "exclude", "operator": "contains", "value": "/internal" }
      ]
    }
  }
}
```

Geo filters example:

```json
{
  "geoFilters": {
    "countries": {
      "type": "include",
      "countryCodes": ["AU", "NZ"],
      "includeUnknown": false
    }
  }
}
```

Common failures:

- `400` missing conditional source fields
- `400` selected goal not found
- `400` Bounce combined with another goal
- `400` invalid URL Pattern or Geo filter setup
- `403` external sources not allowed by plan
- `403` traffic or Geo filters not allowed by plan

### `experiments.create_and_launch`

Purpose:

- Create and launch a modern A/B/n experiment in one command.
- Use this when an agent has already resolved the Control page, destination pages, goals, and optional audience.
- Supports one Control plus up to four variants.

Required input:

- `siteId`
- `idempotencyKey`
- `nickname`
- `selectedGoals`, `goalId`, or `goal`
- `control`
- `variants[]`

Control:

- `source` or `sourceType`: `uxon`, `external`, or `url_patterns`
- `trafficWeight`
- For UXON: `landingPageId` and `landingPageVariantId`
- For external: `pageUrl`
- For Multi-page: `urlPatterns`

Variants:

- `experienceType`: `redirect`
- `trafficWeight`
- For redirect to UXON: `sourceType: "uxon"`, `landingPageId`, and `landingPageVariantId`
- For redirect to external: `sourceType: "external"` and `pageUrl`

Important:

- Control plus all variants must total `100` traffic weight.
- `experiments.create_and_launch` launches redirect variants only. For edit-original variants, create a draft, add Page editor Changes, then launch.
- Never guess UXON landing page variant IDs. Use `landing_pages.get` or `reports.landing_page.details` first.
- `preserveRedirectParams` defaults to `true`.
- Use the older `experiments.create` only when a legacy A/B-only integration needs the old shape.

Request example:

```json
{
  "command": "experiments.create_and_launch",
  "input": {
    "siteId": "site_uuid",
    "idempotencyKey": "homepage-headline-abn-2026-07-30-001",
    "nickname": "Homepage headline A/B/n",
    "selectedGoals": [{ "id": "lead_goal_uuid", "name": "Lead" }],
    "control": {
      "name": "Control",
      "source": "uxon",
      "landingPageId": "lp_uuid",
      "landingPageVariantId": "lpv_control_uuid",
      "trafficWeight": 40
    },
    "variants": [
      {
        "name": "Variant B",
        "experienceType": "redirect",
        "sourceType": "uxon",
        "landingPageId": "lp_uuid",
        "landingPageVariantId": "lpv_variant_b_uuid",
        "trafficWeight": 30
      },
      {
        "name": "Variant C",
        "experienceType": "redirect",
        "sourceType": "external",
        "pageUrl": "https://www.example.com/variant-c",
        "trafficWeight": 30
      }
    ],
    "preserveRedirectParams": true
  }
}
```

Expected output keys:

- `data.experiment.id`
- `data.experiment.status`
- `data.launch.experiment.status`
- `data.nextCommands`

Common failures:

- `400` traffic weights do not total 100
- `400` missing UXON landing page or variant IDs
- `400` missing external URL
- `400` invalid Multi-page rules
- `403` plan does not allow A/B/n, external pages, Multi-page, or advanced audience targeting

### `experiments.update`

Purpose:

- Update mutable experiment settings (not status).

Required input:

- `siteId`
- one of `experimentId` or `id`

Optional input:

- `nickname`
- `hypothesis`
- `testTrigger`: `entry`, `visits`
- `trafficAllocation`
- `visitorType`: `all-visitors`, `new-visitors`, `returning-visitors`
- `deviceType`: `all-devices`, `desktop-only`, `mobile-only`
- `trafficFilters`
- `geoFilters`
- `audienceId`
- `audienceRules`
- `preserveRedirectParams`

Important behavior:

- Status changes are blocked here.
- Use `experiments.set_status` for lifecycle transitions.
- Target pages, URL Patterns, selected goals, labels, and audience snapshots can be updated before launch. Running experiments still enforce domain, URL Pattern, Geo, and audience runtime checks.

Request example:

```json
{
  "command": "experiments.update",
  "input": {
    "siteId": "site_uuid",
    "experimentId": "exp_uuid",
    "trafficAllocation": 60,
    "visitorType": "new-visitors"
  }
}
```

### `experiments.set_status`

Purpose:

- Move experiment to `Draft`, `Running`, `Paused`, or `Ended`.

Required input:

- `siteId`
- one of `experimentId` or `id`
- `status`

Allowed values:

- `Draft`
- `Running`
- `Paused`
- `Ended`

Request example:

```json
{
  "command": "experiments.set_status",
  "input": {
    "siteId": "site_uuid",
    "experimentId": "exp_uuid",
    "status": "Running"
  }
}
```

Launch checks:

- Valid external URL sources when external pages are used.
- Plan support for cross-domain sources.

---

## Personalization

Personalization commands manage campaign setup for page scope, targeted experiences, impact measurement, goals, and redirect parameter behavior. They intentionally manage setup only until the personalization page editor and pixel runtime are wired.

Agent workflow:

- Use `personalization.list` to find campaigns.
- Use `personalization.get` before updating a campaign.
- Use `page_groups.list` before assigning `pageScope.source: "page_group"`.
- Use `audiences.list` before assigning an experience `audienceId`.
- Use `personalization.set_status` to move campaigns through `Draft`, `Running`, `Paused`, and `Ended`.
- Use `preserveRedirectParams` only when redirect experiences exist. It defaults to `true` and keeps UTMs, click IDs, and other query parameters on redirect destinations.

Page scope examples:

```json
{
  "source": "page_group",
  "pageGroupId": "page_group_uuid"
}
```

```json
{
  "source": "url_patterns",
  "urlPatterns": {
    "operator": "and",
    "conditions": [
      { "type": "include", "operator": "contains", "value": "/pricing" },
      { "type": "exclude", "operator": "contains", "value": "/thank-you" }
    ]
  }
}
```

```json
{
  "source": "external",
  "pageUrl": "https://www.example.com/pricing"
}
```

Experience examples:

```json
{
  "name": "US offer",
  "experienceType": "redirect",
  "sourceType": "external",
  "pageUrl": "https://www.example.com/us-offer",
  "audienceId": "audience_uuid"
}
```

```json
{
  "name": "Edit original page",
  "experienceType": "edit_original_page",
  "sourceType": "original",
  "audienceId": "system:all-visitors"
}
```

### `personalization.list`

Purpose:

- List personalization campaigns for one sub-account.

Required input:

- `siteId`

Optional input:

- `page`, `perPage`

Request example:

```json
{
  "command": "personalization.list",
  "input": {
    "siteId": "site_uuid",
    "page": 1,
    "perPage": 10
  }
}
```

Expected output keys:

- `data.personalizations[]`
- `id`, `nickname`, `status`, `goal`
- `experienceCount`
- `measureImpactEnabled`
- `originalComparisonPercentage`
- `preserveRedirectParams`
- `createdAt`, `updatedAt`, `startedAt`, `endedAt`
- `pagination`

Common failures:

- `403` personalization is not included in the plan.
- `404` or scope errors when the API key cannot access the requested sub-account.

### `personalization.get`

Purpose:

- Get one personalization campaign with page scope, experiences, audience snapshots, goals, and settings.

Required input:

- `siteId`
- one of `personalizationId`, `campaignId`, or `id`

Request example:

```json
{
  "command": "personalization.get",
  "input": {
    "siteId": "site_uuid",
    "personalizationId": "personalization_uuid"
  }
}
```

Expected output keys:

- `data.personalization`
- `id`, `nickname`, `status`, `campaignType`, `hypothesis`
- `goals[]`
- `pageScope.source`, `pageScope.pageGroup`, `pageScope.urlPatterns`, `pageScope.pageUrl`
- `experiences[]`
- `experiences[].experienceType`, `sourceType`, `pageUrl`, `landingPageId`, `landingPageVariantId`
- `experiences[].audience.id`, `audience.name`, `audience.rules`
- `measureImpactEnabled`
- `originalComparisonPercentage`
- `preserveRedirectParams`
- `createdAt`, `updatedAt`, `startedAt`, `endedAt`

Common failures:

- `400` `personalizationId`, `campaignId`, or `id` is missing.
- `403` personalization is not included in the plan.
- `404` personalization not found or outside scope.

### `personalization.create`

Purpose:

- Create a Draft personalization campaign.

Required input:

- `siteId`
- `nickname`

Optional input:

- `pageScope`
- `experiences`
- `selectedGoals`
- `measureImpactEnabled`
- `originalComparisonPercentage`
- `preserveRedirectParams`

Request example:

```json
{
  "command": "personalization.create",
  "input": {
    "siteId": "site_uuid",
    "nickname": "Geo offer personalization",
    "pageScope": {
      "source": "page_group",
      "pageGroupId": "page_group_uuid"
    },
    "experiences": [
      {
        "name": "US visitors",
        "experienceType": "redirect",
        "sourceType": "external",
        "pageUrl": "https://www.example.com/us-offer",
        "audienceId": "audience_uuid"
      }
    ],
    "measureImpactEnabled": true,
    "originalComparisonPercentage": 5,
    "preserveRedirectParams": true
  }
}
```

Expected output keys:

- `data.personalization`
- `id`, `nickname`, `status: "Draft"`, `campaignType: "personalization"`
- `goals[]`
- `pageScope`
- `experiences[]`
- `experiences[].audience.rules`
- `measureImpactEnabled`
- `originalComparisonPercentage`
- `preserveRedirectParams`
- `createdAt`, `updatedAt`

Common failures:

- `400` nickname is missing.
- `400` selected Page group was not found.
- `400` invalid page scope.
- `400` invalid experience setup.
- `400` selected audience was not found.
- `400` invalid goals or Bounce combined with another goal.
- `403` personalization is not included in the plan.

### `personalization.create_and_launch`

Purpose:

- Create and launch a personalization campaign in one command.
- Use this when page scope, targeted experiences, audiences, and goals are already resolved.

Required input:

- `siteId`
- `idempotencyKey`
- `nickname`
- `pageScope`
- `experiences[]`

Optional input:

- `selectedGoals`, `goalId`, or `goal`
- `measureImpactEnabled`
- `originalComparisonPercentage`
- `preserveRedirectParams`
- `campaignPriority`

Important:

- Each active experience must be complete before launch.
- `personalization.create_and_launch` launches redirect experiences only. For edit-original experiences, create a draft, add Page editor Changes, then launch.
- `pageScope.source` supports `uxon`, `external`, `url_patterns`, and `page_group`.
- Experience `audienceId` can be a saved audience UUID or `system:<key>`.
- `measureImpactEnabled` defaults to on; `originalComparisonPercentage` defaults through the campaign rules when omitted.
- Use `personalization.create` + `personalization.set_status` only when a legacy integration needs a two-step Draft then launch flow.

Request example:

```json
{
  "command": "personalization.create_and_launch",
  "input": {
    "siteId": "site_uuid",
    "idempotencyKey": "paid-search-homepage-2026-07-30-001",
    "nickname": "Paid search homepage message",
    "pageScope": {
      "source": "external",
      "pageUrl": "https://www.example.com/"
    },
    "experiences": [
      {
        "name": "Paid search message",
        "experienceType": "redirect",
        "sourceType": "external",
        "pageUrl": "https://www.example.com/paid-search-homepage",
        "audienceId": "system:paid-search"
      }
    ],
    "measureImpactEnabled": true,
    "originalComparisonPercentage": 5
  }
}
```

Expected output keys:

- `data.personalization.id`
- `data.personalization.status`
- `data.launch.personalization.status`
- `data.nextCommands`

Common failures:

- `400` invalid or incomplete page scope
- `400` missing active experience
- `400` selected Page group or audience was not found
- `400` invalid goals or Bounce combined with another goal
- `403` personalization, external pages, Multi-page, or advanced audience targeting is not included in the plan

### `personalization.update`

Purpose:

- Update a personalization Draft's setup, page scope, experiences, goals, measurement, or redirect parameter behavior.

Required input:

- `siteId`
- one of `personalizationId`, `campaignId`, or `id`

Optional input:

- `nickname` or `campaignTitle`
- `hypothesis`
- `pageScope`
- `experiences` or `personalizationExperiences`
- `selectedGoals`
- `goalId` or `goal`
- `measureImpactEnabled`
- `originalComparisonPercentage`
- `preserveRedirectParams`

Important behavior:

- Status changes are blocked here. Use `personalization.set_status`.
- When `experiences` is sent, UXON replaces the campaign's experience list with the provided list.
- Experience `audienceId` snapshots the saved audience rules at save time.
- Later audience edits do not automatically alter already-saved personalization experiences.
- `pageScope.source` supports `uxon`, `external`, `url_patterns`, and `page_group`.

Request example:

```json
{
  "command": "personalization.update",
  "input": {
    "siteId": "site_uuid",
    "personalizationId": "personalization_uuid",
    "nickname": "Pricing personalization",
    "pageScope": {
      "source": "page_group",
      "pageGroupId": "page_group_uuid"
    },
    "experiences": [
      {
        "name": "US visitors",
        "experienceType": "redirect",
        "sourceType": "external",
        "pageUrl": "https://www.example.com/us-offer",
        "audienceId": "audience_uuid"
      },
      {
        "name": "All other visitors",
        "experienceType": "edit_original_page",
        "sourceType": "original",
        "audienceId": "system:all-visitors"
      }
    ],
    "measureImpactEnabled": true,
    "originalComparisonPercentage": 5,
    "preserveRedirectParams": true
  }
}
```

Expected output keys:

- `data.personalization`
- `id`, `nickname`, `status`
- `goals[]`
- `pageScope`
- `experiences[]`
- `experiences[].audience.rules`
- `measureImpactEnabled`
- `originalComparisonPercentage`
- `preserveRedirectParams`
- `updatedAt`

Common failures:

- `400` no fields provided to update.
- `400` status was sent to update instead of `personalization.set_status`.
- `400` selected Page group was not found.
- `400` selected audience was not found.
- `400` invalid page scope, experience setup, goals, or measurement percentage.
- `403` personalization is not included in the plan.
- `404` personalization not found or outside scope.

### `personalization.set_status`

Purpose:

- Set personalization status to `Draft`, `Running`, `Paused`, or `Ended`.

Required input:

- `siteId`
- one of `personalizationId`, `campaignId`, or `id`
- `status`

Allowed values:

- `Draft`
- `Running`
- `Paused`
- `Ended`

Request example:

```json
{
  "command": "personalization.set_status",
  "input": {
    "siteId": "site_uuid",
    "personalizationId": "personalization_uuid",
    "status": "Paused"
  }
}
```

Expected output keys:

- `data.personalization.id`
- `data.personalization.status`
- `data.personalization.updatedAt`

Common failures:

- `400` `personalizationId`, `campaignId`, or `id` is missing.
- `403` personalization is not included in the plan.
- `404` personalization not found or outside scope.

## Reports

### `reports.account_health`

Purpose:

- Return a concise account health snapshot across landing pages, experiments, personalization, domains, and key reporting summaries.
- Use this as the first command for broad dashboard/account checks before drilling into specific pages or campaigns.

Optional input:

- `siteId`; omit to check accessible sub-accounts
- `maxSites`; default `10`, maximum `25`
- `dateRange`, `from`, `to`, `timezone`

Request example:

```json
{
  "command": "reports.account_health",
  "input": {
    "dateRange": "last-30-days",
    "maxSites": 10
  }
}
```

Expected output keys:

- `data.accountHealth.generatedAt`
- `data.accountHealth.returnedSites`
- `data.accountHealth.availableSites`
- `data.accountHealth.sites[]`
- `sites[].landingPages`
- `sites[].experiments`
- `sites[].personalization`
- `sites[].domains`
- `sites[].attentionItems[]`
- `data.accountHealth.nextCommands`

Common failures:

- `400` invalid date range
- `403` API key cannot access the requested sub-account

### `reports.experiments.summary`

Purpose:

- Aggregate experiment totals for a site.

Required input:

- `siteId`

Optional input:

- `dateRange`
- `from`
- `to`
- `timezone`

Request example:

```json
{
  "command": "reports.experiments.summary",
  "input": {
    "siteId": "site_uuid"
  }
}
```

Expected output keys:

- `data.summary.totalExperiments`
- `data.summary.runningExperiments`
- `data.summary.draftExperiments`
- `data.summary.endedExperiments`
- `data.summary.sessions`
- `data.summary.controlSessions`
- `data.summary.variantSessions`

### `reports.experiment.details`

Purpose:

- Detailed experiment record plus KPI comparison.

Required input:

- `siteId`
- one of `experimentId` or `id`

Optional input:

- `dateRange`
- `from`
- `to`
- `timezone`
- `conversionGoalId`

Conversion goal behavior:

- Omit `conversionGoalId` or use `"all"` for all selected conversions.
- Use a selected goal ID to report one conversion only.

Request example:

```json
{
  "command": "reports.experiment.details",
  "input": {
    "siteId": "site_uuid",
    "experimentId": "exp_uuid"
  }
}
```

Expected output keys:

- `data.experiment.id`
- `data.experiment.nickname`
- `data.experiment.status`
- `data.experiment.controlSource`
- `data.experiment.variantSource`
- `data.experiment.controlLandingPage`
- `data.experiment.controlVariant`
- `data.experiment.variantLandingPage`
- `data.experiment.variantVariant`
- `data.experiment.controlPageUrl`
- `data.experiment.variantPageUrl`
- `data.experiment.goals`
- `data.experiment.selectedConversionGoalId`
- `data.experiment.trafficAllocation`
- `data.experiment.testTrigger`
- `data.experiment.visitorType`
- `data.experiment.deviceType`
- `data.experiment.trafficFilters`
- `data.experiment.geoFilters`
- `data.metrics.sessions`
- `data.metrics.goals`
- `data.metrics.conversionRate`
- `data.metrics.revenue`
- `data.metrics.probabilityToWin`

### `reports.experiment.chart`

Purpose:

- Chart-ready time series for sessions, goals, revenue, conversion rate.

Required input:

- `siteId`
- one of `experimentId` or `id`

Optional input:

- `dateRange`
- `from`
- `to`
- `timezone`
- `conversionGoalId`

Conversion goal behavior:

- Omit `conversionGoalId` or use `"all"` for all selected conversions.
- Use a selected goal ID to chart one conversion only.

Request example:

```json
{
  "command": "reports.experiment.chart",
  "input": {
    "siteId": "site_uuid",
    "experimentId": "exp_uuid",
    "dateRange": "last-30-days"
  }
}
```

Expected output keys:

- `data.granularity`
- `data.experiment.goals`
- `data.experiment.selectedConversionGoalId`
- `data.sessions[]`
- `data.goals[]`
- `data.revenue[]`
- `data.conversionRate[]`

### `reports.experiment.channels`

Purpose:

- Channel-level performance split by control vs variant.

Required input:

- `siteId`
- one of `experimentId` or `id`

Optional input:

- `dateRange`
- `from`
- `to`
- `timezone`
- `conversionGoalId`

Conversion goal behavior:

- Omit `conversionGoalId` or use `"all"` for all selected conversions.
- Use a selected goal ID to segment one conversion only.

Request example:

```json
{
  "command": "reports.experiment.channels",
  "input": {
    "siteId": "site_uuid",
    "experimentId": "exp_uuid",
    "dateRange": "last-30-days"
  }
}
```

Expected output keys:

- `data.experiment.goals`
- `data.experiment.selectedConversionGoalId`
- `data.rows[]`
- `data.rows[].channel`
- `data.rows[].controlSessions`
- `data.rows[].variantSessions`
- `data.rows[].controlConversions`
- `data.rows[].variantConversions`
- `data.rows[].lift`
- `data.rows[].confidence`
- `controlSessions`, `variantSessions`, `controlConversions`, `variantConversions`, `lift`, `confidence`

---

## Domains

### `domains.list`

Purpose:

- List domains for one sub-account.

Required input:

- `siteId`

Request example:

```json
{
  "command": "domains.list",
  "input": {
    "siteId": "site_uuid"
  }
}
```

Expected output keys:

- `domains[]`
- `id`, `domain`, `subdomain`, `status`, `ssl`, `connectedAt`

### `domains.add`

Purpose:

- Add root domain or subdomain.

Required input:

- `siteId`
- `domain`

Optional input:

- `subdomain`

Validation notes:

- Domain format validated.
- Duplicate host/subdomain combinations are rejected.

Request example:

```json
{
  "command": "domains.add",
  "input": {
    "siteId": "site_uuid",
    "domain": "example.com",
    "subdomain": "www"
  }
}
```

### `domains.connect`

Purpose:

- Trigger verification/connect flow for existing domain record.

Required input:

- `domainId`

Request example:

```json
{
  "command": "domains.connect",
  "input": {
    "domainId": "domain_uuid"
  }
}
```

Common failures:

- `400` missing `domainId`
- `404` domain not found

---

## Tracking

### `tracking.setup`

Purpose:

- Configure tracking safely in one command.
- Install GTM managed snippets (`head` + `body-start`) or direct GA4/Meta IDs.
- Prevent duplicate loading by never keeping GTM and direct IDs active together.

Required input:

- `siteId`
- `mode` (`gtm` or `direct`)
- `domain` (host only, for example `www.example.com`)

Mode-specific required input:

- If `mode=gtm`: `gtmContainerId`
- If `mode=direct`: at least one of `ga4MeasurementId` or `metaPixelId`

Guardrails:

- In `gtm` mode:
  - managed GTM scripts are installed in `head` and `body-start`
  - direct GA4/Meta IDs are cleared to prevent duplicate firing
- In `direct` mode:
  - managed GTM scripts are disabled on the selected domain
  - direct GA4/Meta IDs are saved

Request examples:

```json
{
  "command": "tracking.setup",
  "input": {
    "siteId": "site_uuid",
    "mode": "gtm",
    "domain": "www.example.com",
    "gtmContainerId": "GTM-ABC1234"
  }
}
```

```json
{
  "command": "tracking.setup",
  "input": {
    "siteId": "site_uuid",
    "mode": "direct",
    "domain": "www.example.com",
    "ga4MeasurementId": "G-PSW1MY7HB4",
    "metaPixelId": "123456789012345"
  }
}
```

---

## Goals

### `goals.list`

Purpose:

- List goals for one sub-account.

Required input:

- `siteId`

Optional input:

- `page`
- `perPage`

Request example:

```json
{
  "command": "goals.list",
  "input": {
    "siteId": "site_uuid",
    "page": 1,
    "perPage": 25
  }
}
```

Expected output keys:

- `goals[]`
- `id`, `name`, `isActive`, `isSystemEvent`, `lastTriggered`

### `goals.create`

Purpose:

- Create a custom conversion goal.

Required input:

- `siteId`
- `name`

Optional input:

- `isActive`
- `googleAdsConversionId`
- `googleAdsConversionLabel`
- `googleAdsEnabled`

Request example:

```json
{
  "command": "goals.create",
  "input": {
    "siteId": "site_uuid",
    "name": "Qualified Lead",
    "isActive": true
  }
}
```

### `goals.update`

Purpose:

- Update goal attributes.

Required input:

- `siteId`
- one of `goalId` or `id`

Optional input:

- `name`
- `isActive`
- `googleAdsConversionId`
- `googleAdsConversionLabel`
- `googleAdsEnabled`

Guardrails:

- System-goal restrictions apply.
- Only Lead system goal can update Google Ads settings.

Request example:

```json
{
  "command": "goals.update",
  "input": {
    "siteId": "site_uuid",
    "goalId": "goal_uuid",
    "googleAdsEnabled": true
  }
}
```

---

## Contacts

### `reports.contacts.list`

Purpose:

- Paginated contacts list with optional filters.

Required input:

- `siteId`

Optional input:

- `page`
- `perPage`
- `search`
- `landingPageId`
- `startDate`
- `endDate`

Request example:

```json
{
  "command": "reports.contacts.list",
  "input": {
    "siteId": "site_uuid",
    "page": 1,
    "perPage": 25,
    "startDate": "2026-03-20T00:00:00.000Z",
    "endDate": "2026-04-19T23:59:59.999Z"
  }
}
```

Expected output keys:

- `data.contacts[]`
- `data.contacts[].id`
- `data.contacts[].firstName`
- `data.contacts[].lastName`
- `data.contacts[].email`
- `data.contacts[].phone`
- `data.contacts[].landingPageName`
- `data.contacts[].variantName`
- `data.contacts[].pageUrl`
- `data.contacts[].submittedAt`
- `data.contacts[].utmSource`
- `data.contacts[].utmMedium`
- `data.contacts[].utmCampaign`
- `data.contacts[].utmContent`
- `data.contacts[].userType`
- `data.contacts[].deviceType`
- `data.pagination.page`
- `data.pagination.perPage`
- `data.pagination.total`

### `reports.contacts.detail`

Purpose:

- Detailed profile for one contact.

Required input:

- `siteId`
- one of `contactId` or `id`

Request example:

```json
{
  "command": "reports.contacts.detail",
  "input": {
    "siteId": "site_uuid",
    "contactId": "contact_uuid"
  }
}
```

Expected output keys:

- `data.contact.id`
- `data.contact.firstName`
- `data.contact.lastName`
- `data.contact.email`
- `data.contact.phone`
- `data.contact.landingPageName`
- `data.contact.variantName`
- `data.contact.submittedAt`
- `data.contact.referrerUrl`
- `data.contact.utmSource`
- `data.contact.utmMedium`
- `data.contact.utmCampaign`
- `data.contact.utmContent`
- `data.contact.userType`
- `data.contact.deviceType`
- `data.latestSubmission`
- `data.latestSubmission.id`
- `data.latestSubmission.pageUrl`
- `data.latestSubmission.referrerUrl`
- `data.latestSubmission.deviceType`
- `data.latestSubmission.submittedAt`
- `data.latestSubmission.values`

---

## Knowledge Base Articles

### `docs.list`

Purpose:

- List Intercom Help Center articles for AI chat knowledge access.

Optional input:

- `page`
- `perPage` (max 50)
- `state` (`published` or `draft`)

Request example:

```json
{
  "command": "docs.list",
  "input": {
    "page": 1,
    "perPage": 20,
    "state": "published"
  }
}
```

Expected output keys:

- `docs[]`
- `pagination`

### `docs.search`

Purpose:

- Search Intercom Help Center articles by phrase.

Required input:

- `query`

Optional input:

- `page`
- `perPage` (max 50)
- `state` (`published` or `draft`)

Request example:

```json
{
  "command": "docs.search",
  "input": {
    "query": "connect domain",
    "page": 1,
    "perPage": 10,
    "state": "published"
  }
}
```

Expected output keys:

- `query`
- `docs[]`
- `pagination`

### `docs.get`

Purpose:

- Fetch one Intercom Help Center article by ID.

Required input:

- one of `articleId` or `id`

Request example:

```json
{
  "command": "docs.get",
  "input": {
    "articleId": "15284583"
  }
}
```

Expected output keys:

- `doc`

---

## 13) Copy-Paste cURL Templates

Replace values before execution.

### Template wrapper

```bash
curl -X POST 'https://uxon.ai/api/external/commands' \
  -H 'Authorization: Bearer uxon_live_REPLACE_WITH_REAL_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "command": "REPLACE_COMMAND",
    "input": {}
  }'
```

### cURL: subaccounts.list

```bash
curl -X POST 'https://uxon.ai/api/external/commands' \
  -H 'Authorization: Bearer uxon_live_REPLACE_WITH_REAL_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"command":"subaccounts.list","input":{}}'
```

### cURL: subaccounts.create

```bash
curl -X POST 'https://uxon.ai/api/external/commands' \
  -H 'Authorization: Bearer uxon_live_REPLACE_WITH_REAL_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "command":"subaccounts.create",
    "input":{"name":"ACME Dental","websiteUrl":"https://acmedental.com","reportingCurrency":"USD","reportingTimezone":"Australia/Brisbane","landingPageFeedbackMode":"free_comments"}
  }'
```

### cURL: landing_pages.create

```bash
curl -X POST 'https://uxon.ai/api/external/commands' \
  -H 'Authorization: Bearer uxon_live_REPLACE_WITH_REAL_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "command":"landing_pages.create",
    "input":{"siteId":"site_uuid","name":"Spring Offer"}
  }'
```

### cURL: landing_pages.set_status

```bash
curl -X POST 'https://uxon.ai/api/external/commands' \
  -H 'Authorization: Bearer uxon_live_REPLACE_WITH_REAL_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "command":"landing_pages.set_status",
    "input":{"siteId":"site_uuid","landingPageId":"lp_uuid","status":"live"}
  }'
```

### cURL: reports.landing_pages.summary

```bash
curl -X POST 'https://uxon.ai/api/external/commands' \
  -H 'Authorization: Bearer uxon_live_REPLACE_WITH_REAL_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "command":"reports.landing_pages.summary",
    "input":{"siteId":"site_uuid"}
  }'
```

### cURL: reports.landing_page.details

```bash
curl -X POST 'https://uxon.ai/api/external/commands' \
  -H 'Authorization: Bearer uxon_live_REPLACE_WITH_REAL_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "command":"reports.landing_page.details",
    "input":{"siteId":"site_uuid","landingPageId":"lp_uuid"}
  }'
```

### cURL: experiments.create (UXON source)

```bash
curl -X POST 'https://uxon.ai/api/external/commands' \
  -H 'Authorization: Bearer uxon_live_REPLACE_WITH_REAL_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "command":"experiments.create",
    "input":{
      "siteId":"site_uuid",
      "nickname":"Headline Test",
      "selectedGoals":[
        {"id":"lead_goal_uuid","name":"Lead"}
      ],
      "controlSource":"uxon",
      "controlLandingPage":"lp_control_uuid",
      "controlVariant":"lpv_control_uuid",
      "variantSource":"uxon",
      "variantLandingPage":"lp_variant_uuid",
      "variantVariant":"lpv_variant_uuid"
    }
  }'
```

### cURL: experiments.set_status

```bash
curl -X POST 'https://uxon.ai/api/external/commands' \
  -H 'Authorization: Bearer uxon_live_REPLACE_WITH_REAL_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "command":"experiments.set_status",
    "input":{"siteId":"site_uuid","experimentId":"exp_uuid","status":"Running"}
  }'
```

### cURL: experiments.get

```bash
curl -X POST 'https://uxon.ai/api/external/commands' \
  -H 'Authorization: Bearer uxon_live_REPLACE_WITH_REAL_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "command":"experiments.get",
    "input":{"siteId":"site_uuid","experimentId":"exp_uuid"}
  }'
```

### cURL: reports.experiment.details

```bash
curl -X POST 'https://uxon.ai/api/external/commands' \
  -H 'Authorization: Bearer uxon_live_REPLACE_WITH_REAL_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "command":"reports.experiment.details",
    "input":{"siteId":"site_uuid","experimentId":"exp_uuid","conversionGoalId":"all"}
  }'
```

### cURL: reports.experiment.chart

```bash
curl -X POST 'https://uxon.ai/api/external/commands' \
  -H 'Authorization: Bearer uxon_live_REPLACE_WITH_REAL_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "command":"reports.experiment.chart",
    "input":{"siteId":"site_uuid","experimentId":"exp_uuid","dateRange":"last-30-days","conversionGoalId":"all"}
  }'
```

### cURL: reports.experiment.channels

```bash
curl -X POST 'https://uxon.ai/api/external/commands' \
  -H 'Authorization: Bearer uxon_live_REPLACE_WITH_REAL_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "command":"reports.experiment.channels",
    "input":{"siteId":"site_uuid","experimentId":"exp_uuid","dateRange":"last-30-days","conversionGoalId":"all"}
  }'
```

### cURL: domains.add

```bash
curl -X POST 'https://uxon.ai/api/external/commands' \
  -H 'Authorization: Bearer uxon_live_REPLACE_WITH_REAL_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "command":"domains.add",
    "input":{"siteId":"site_uuid","domain":"example.com","subdomain":"www"}
  }'
```

### cURL: domains.connect

```bash
curl -X POST 'https://uxon.ai/api/external/commands' \
  -H 'Authorization: Bearer uxon_live_REPLACE_WITH_REAL_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"command":"domains.connect","input":{"domainId":"domain_uuid"}}'
```

### cURL: tracking.setup (GTM mode)

```bash
curl -X POST 'https://uxon.ai/api/external/commands' \
  -H 'Authorization: Bearer uxon_live_REPLACE_WITH_REAL_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "command":"tracking.setup",
    "input":{
      "siteId":"site_uuid",
      "mode":"gtm",
      "domain":"www.example.com",
      "gtmContainerId":"GTM-ABC1234"
    }
  }'
```

### cURL: tracking.setup (Direct GA4/Meta mode)

```bash
curl -X POST 'https://uxon.ai/api/external/commands' \
  -H 'Authorization: Bearer uxon_live_REPLACE_WITH_REAL_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "command":"tracking.setup",
    "input":{
      "siteId":"site_uuid",
      "mode":"direct",
      "domain":"www.example.com",
      "ga4MeasurementId":"G-PSW1MY7HB4",
      "metaPixelId":"123456789012345"
    }
  }'
```

### cURL: goals.list

```bash
curl -X POST 'https://uxon.ai/api/external/commands' \
  -H 'Authorization: Bearer uxon_live_REPLACE_WITH_REAL_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"command":"goals.list","input":{"siteId":"site_uuid"}}'
```

### cURL: goals.create

```bash
curl -X POST 'https://uxon.ai/api/external/commands' \
  -H 'Authorization: Bearer uxon_live_REPLACE_WITH_REAL_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"command":"goals.create","input":{"siteId":"site_uuid","name":"Qualified Lead"}}'
```

### cURL: reports.contacts.list

```bash
curl -X POST 'https://uxon.ai/api/external/commands' \
  -H 'Authorization: Bearer uxon_live_REPLACE_WITH_REAL_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"command":"reports.contacts.list","input":{"siteId":"site_uuid","page":1,"perPage":25}}'
```

### cURL: reports.contacts.detail

```bash
curl -X POST 'https://uxon.ai/api/external/commands' \
  -H 'Authorization: Bearer uxon_live_REPLACE_WITH_REAL_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"command":"reports.contacts.detail","input":{"siteId":"site_uuid","contactId":"contact_uuid"}}'
```

### cURL: docs.list

```bash
curl -X POST 'https://uxon.ai/api/external/commands' \
  -H 'Authorization: Bearer uxon_live_REPLACE_WITH_REAL_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"command":"docs.list","input":{"page":1,"perPage":20,"state":"published"}}'
```

### cURL: docs.search

```bash
curl -X POST 'https://uxon.ai/api/external/commands' \
  -H 'Authorization: Bearer uxon_live_REPLACE_WITH_REAL_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"command":"docs.search","input":{"query":"connect domain","page":1,"perPage":10,"state":"published"}}'
```

### cURL: docs.get

```bash
curl -X POST 'https://uxon.ai/api/external/commands' \
  -H 'Authorization: Bearer uxon_live_REPLACE_WITH_REAL_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"command":"docs.get","input":{"articleId":"15284583"}}'
```

## 14) Error Handling Contract

HTTP status guidance:

- `400`: invalid payload, missing required fields, invalid IDs/date ranges.
- `401`: invalid or missing key.
- `403`: blocked command, inaccessible scope, feature not enabled.
- `404`: resource not found.
- `429`: rate limit exceeded.
- `500`: unexpected server error.

Recommended retry policy:

- Retry `429` with backoff and jitter.
- Read `X-RateLimit-Reset` before retrying `429`; default limit is 60 requests per minute unless the key has a custom internal limit.
- Retry `500` up to 3 attempts.
- Do not retry `400`, `401`, `403`, `404` without correcting input or permissions.

## 15) Production Guardrails For Agent Builders

- Always keep a per-command audit log.
- Require human confirmation before status changes in production.
- Require human confirmation for domain operations.
- Use idempotency keys on your side for repeated workflows.
- Never store plaintext API keys in logs.

## 16) Final Checklist Before You Mark A Task Done

- Correct site selected?
- Required IDs resolved from list/read commands?
- Write operation succeeded with `200`?
- Read-back verification performed?
- Outcome summary posted for user?


## 17) Complete Payload Library (All Supported Commands)

Use these as copy-paste JSON payloads for `POST https://uxon.ai/api/external/commands`.

### Sub-accounts

#### `subaccounts.list`

```json
{
  "command": "subaccounts.list",
  "input": {}
}
```

#### `subaccounts.get`

```json
{
  "command": "subaccounts.get",
  "input": {
    "siteId": "site_uuid"
  }
}
```

#### `subaccounts.create`

```json
{
  "command": "subaccounts.create",
  "input": {
    "name": "ACME Dental",
    "websiteUrl": "https://acmedental.com",
    "reportingCurrency": "USD",
    "reportingTimezone": "Australia/Brisbane",
    "landingPageFeedbackMode": "free_comments"
  }
}
```

#### `subaccounts.update`

```json
{
  "command": "subaccounts.update",
  "input": {
    "siteId": "site_uuid",
    "name": "ACME Dental Group",
    "websiteUrl": "https://www.acmedental.com",
    "reportingCurrency": "AUD",
    "reportingTimezone": "Australia/Brisbane",
    "landingPageFeedbackMode": "ai_approvals"
  }
}
```

### Landing pages

#### `landing_pages.list`

```json
{
  "command": "landing_pages.list",
  "input": {
    "siteId": "site_uuid"
  }
}
```

#### `landing_pages.create`

```json
{
  "command": "landing_pages.create",
  "input": {
    "siteId": "site_uuid",
    "name": "Spring Offer",
    "settings": {
      "publish": {
        "domain": "www.example.com",
        "path": "/spring-offer",
        "hideFromSearch": true
      }
    },
    "knowledgeBase": {
      "summary": "Emergency dental appointment landing page for new patients.",
      "notes": ["Same-day availability is the key buyer trigger."],
      "links": [
        {
          "label": "Campaign brief",
          "url": "https://example.com/brief"
        }
      ],
      "raw": {}
    }
  }
}
```

#### `landing_pages.generate_from_brief`

```json
{
  "command": "landing_pages.generate_from_brief",
  "input": {
    "siteId": "site_uuid",
    "name": "Spring Offer",
    "brief": {
      "brandName": "ACME Dental",
      "websiteUrl": "https://www.acmedental.com",
      "language": "English (US)",
      "funnelStrategy": "Service",
      "buyerGroup": "Consumers (B2C)",
      "desiredAction": "Submit a form",
      "market": "Brisbane, Queensland",
      "serviceOrProduct": "Emergency dental appointments for new patients",
      "idealClient": "Adults with urgent dental pain who need a same-day appointment"
    }
  }
}
```

#### `landing_pages.generation_status`

```json
{
  "command": "landing_pages.generation_status",
  "input": {
    "siteId": "site_uuid",
    "jobId": "job_uuid"
  }
}
```

#### `landing_pages.update`

```json
{
  "command": "landing_pages.update",
  "input": {
    "siteId": "site_uuid",
    "landingPageId": "landing_page_uuid",
    "name": "Spring Offer Rev 2",
    "settings": {
      "publish": {
        "domain": "www.example.com",
        "path": "/spring-offer",
        "hideFromSearch": true
      }
    },
    "knowledgeBase": {
      "summary": "Updated page context for new visitors.",
      "notes": ["Keep the offer focused on fast appointment booking."],
      "links": [],
      "raw": {}
    }
  }
}
```

#### `landing_pages.code_variant.upload`

```json
{
  "command": "landing_pages.code_variant.upload",
  "input": {
    "siteId": "site_uuid",
    "landingPageId": "landing_page_uuid",
    "variantName": "External Code Variant",
    "zipUrl": "https://example.com/builds/landing-page.zip",
    "makeMain": false,
    "publish": false
  }
}
```

#### `landing_pages.set_status`

```json
{
  "command": "landing_pages.set_status",
  "input": {
    "siteId": "site_uuid",
    "landingPageId": "landing_page_uuid",
    "status": "live"
  }
}
```

#### `reports.landing_pages.summary`

```json
{
  "command": "reports.landing_pages.summary",
  "input": {
    "siteId": "site_uuid",
    "from": "2026-03-20T00:00:00.000Z",
    "to": "2026-04-19T23:59:59.999Z"
  }
}
```

#### `reports.landing_page.details`

```json
{
  "command": "reports.landing_page.details",
  "input": {
    "siteId": "site_uuid",
    "landingPageId": "landing_page_uuid",
    "from": "2026-03-20T00:00:00.000Z",
    "to": "2026-04-19T23:59:59.999Z"
  }
}
```

#### `reports.landing_page.variant_metrics`

```json
{
  "command": "reports.landing_page.variant_metrics",
  "input": {
    "siteId": "site_uuid",
    "landingPageId": "landing_page_uuid",
    "from": "2026-03-20T00:00:00.000Z",
    "to": "2026-04-19T23:59:59.999Z"
  }
}
```

#### `reports.landing_page.trends`

```json
{
  "command": "reports.landing_page.trends",
  "input": {
    "siteId": "site_uuid",
    "landingPageId": "landing_page_uuid",
    "from": "2026-03-20T00:00:00.000Z",
    "to": "2026-04-19T23:59:59.999Z",
    "granularity": "day",
    "metric": "sessions"
  }
}
```

#### `reports.landing_page.channels`

```json
{
  "command": "reports.landing_page.channels",
  "input": {
    "siteId": "site_uuid",
    "landingPageId": "landing_page_uuid",
    "dateRange": "last-30-days"
  }
}
```

### Experiments

#### `experiments.list`

```json
{
  "command": "experiments.list",
  "input": {
    "siteId": "site_uuid"
  }
}
```

#### `experiments.get`

```json
{
  "command": "experiments.get",
  "input": {
    "siteId": "site_uuid",
    "experimentId": "experiment_uuid"
  }
}
```

#### `experiments.create` (UXON vs UXON)

```json
{
  "command": "experiments.create",
  "input": {
    "siteId": "site_uuid",
    "nickname": "Headline Test",
    "selectedGoals": [
      { "id": "lead_goal_uuid", "name": "Lead" }
    ],
    "controlSource": "uxon",
    "controlLandingPage": "control_landing_page_uuid",
    "controlVariant": "control_variant_uuid",
    "variantSource": "uxon",
    "variantLandingPage": "variant_landing_page_uuid",
    "variantVariant": "variant_variant_uuid",
    "testTrigger": "entry",
    "visitorType": "all-visitors",
    "deviceType": "all-devices",
    "trafficAllocation": 100
  }
}
```

#### `experiments.create` (External vs External)

```json
{
  "command": "experiments.create",
  "input": {
    "siteId": "site_uuid",
    "nickname": "Checkout Hero Test",
    "selectedGoals": [
      { "id": "lead_goal_uuid", "name": "Lead" }
    ],
    "controlSource": "external",
    "controlPageUrl": "https://www.example.com/checkout-a",
    "variantSource": "external",
    "variantPageUrl": "https://www.example.com/checkout-b",
    "testTrigger": "entry",
    "visitorType": "all-visitors",
    "deviceType": "all-devices",
    "trafficAllocation": 100
  }
}
```

#### `experiments.create` (URL Pattern to External)

```json
{
  "command": "experiments.create",
  "input": {
    "siteId": "site_uuid",
    "nickname": "Pricing URL Pattern Test",
    "selectedGoals": [
      { "id": "lead_goal_uuid", "name": "Lead" },
      { "id": "call_goal_uuid", "name": "Phone call" }
    ],
    "controlSource": "url_patterns",
    "variantSource": "external",
    "variantPageUrl": "https://www.example.com/pricing-variant",
    "urlPatterns": {
      "side": "control",
      "conditions": [
        { "type": "include", "operator": "contains", "value": "/pricing" },
        { "type": "exclude", "operator": "contains", "value": "/thank-you" }
      ]
    },
    "geoFilters": {
      "countries": {
        "type": "include",
        "countryCodes": ["US", "CA"],
        "includeUnknown": false
      }
    },
    "testTrigger": "entry",
    "visitorType": "all-visitors",
    "deviceType": "all-devices",
    "trafficAllocation": 100
  }
}
```

#### `experiments.update`

```json
{
  "command": "experiments.update",
  "input": {
    "siteId": "site_uuid",
    "experimentId": "experiment_uuid",
    "nickname": "Headline Test Rev 2",
    "hypothesis": "Benefit-led headline increases lead conversion.",
    "testTrigger": "visits",
    "trafficAllocation": 75,
    "visitorType": "new-visitors",
    "deviceType": "desktop-only",
    "trafficFilters": {
      "utm_source": "google",
      "utm_campaign": "brand"
    },
    "geoFilters": {
      "countries": {
        "type": "exclude",
        "countryCodes": ["GB"],
        "includeUnknown": true
      }
    }
  }
}
```

#### `experiments.set_status`

```json
{
  "command": "experiments.set_status",
  "input": {
    "siteId": "site_uuid",
    "experimentId": "experiment_uuid",
    "status": "Running"
  }
}
```

#### `reports.experiments.summary`

```json
{
  "command": "reports.experiments.summary",
  "input": {
    "siteId": "site_uuid",
    "from": "2026-03-20T00:00:00.000Z",
    "to": "2026-04-19T23:59:59.999Z"
  }
}
```

#### `reports.experiment.details`

```json
{
  "command": "reports.experiment.details",
  "input": {
    "siteId": "site_uuid",
    "experimentId": "experiment_uuid",
    "from": "2026-03-20T00:00:00.000Z",
    "to": "2026-04-19T23:59:59.999Z",
    "timezone": "Australia/Brisbane",
    "conversionGoalId": "all"
  }
}
```

#### `reports.experiment.chart`

```json
{
  "command": "reports.experiment.chart",
  "input": {
    "siteId": "site_uuid",
    "experimentId": "experiment_uuid",
    "dateRange": "last-30-days",
    "timezone": "Australia/Brisbane",
    "conversionGoalId": "lead_goal_uuid"
  }
}
```

#### `reports.experiment.channels`

```json
{
  "command": "reports.experiment.channels",
  "input": {
    "siteId": "site_uuid",
    "experimentId": "experiment_uuid",
    "dateRange": "last-30-days",
    "timezone": "Australia/Brisbane",
    "conversionGoalId": "all"
  }
}
```

### Domains

#### `domains.list`

```json
{
  "command": "domains.list",
  "input": {
    "siteId": "site_uuid"
  }
}
```

#### `domains.add`

```json
{
  "command": "domains.add",
  "input": {
    "siteId": "site_uuid",
    "domain": "example.com",
    "subdomain": "www"
  }
}
```

#### `domains.connect`

```json
{
  "command": "domains.connect",
  "input": {
    "domainId": "domain_uuid"
  }
}
```

### Tracking

#### `tracking.setup`

```json
{
  "command": "tracking.setup",
  "input": {
    "siteId": "site_uuid",
    "mode": "gtm",
    "domain": "www.example.com",
    "gtmContainerId": "GTM-ABC1234"
  }
}
```

### Goals

#### `goals.list`

```json
{
  "command": "goals.list",
  "input": {
    "siteId": "site_uuid",
    "page": 1,
    "perPage": 25
  }
}
```

#### `goals.create`

```json
{
  "command": "goals.create",
  "input": {
    "siteId": "site_uuid",
    "name": "Qualified Lead",
    "isActive": true,
    "googleAdsConversionId": null,
    "googleAdsConversionLabel": null,
    "googleAdsEnabled": false
  }
}
```

#### `goals.update`

```json
{
  "command": "goals.update",
  "input": {
    "siteId": "site_uuid",
    "goalId": "goal_uuid",
    "isActive": true,
    "googleAdsEnabled": true
  }
}
```

### Contacts

#### `reports.contacts.list`

```json
{
  "command": "reports.contacts.list",
  "input": {
    "siteId": "site_uuid",
    "page": 1,
    "perPage": 25,
    "search": "john",
    "landingPageId": "landing_page_uuid",
    "startDate": "2026-03-20T00:00:00.000Z",
    "endDate": "2026-04-19T23:59:59.999Z"
  }
}
```

#### `reports.contacts.detail`

```json
{
  "command": "reports.contacts.detail",
  "input": {
    "siteId": "site_uuid",
    "contactId": "contact_uuid"
  }
}
```

### Knowledge base articles

#### `docs.list`

```json
{
  "command": "docs.list",
  "input": {
    "page": 1,
    "perPage": 20,
    "state": "published"
  }
}
```

#### `docs.search`

```json
{
  "command": "docs.search",
  "input": {
    "query": "connect domain",
    "page": 1,
    "perPage": 10,
    "state": "published"
  }
}
```

#### `docs.get`

```json
{
  "command": "docs.get",
  "input": {
    "articleId": "15284583"
  }
}
```

## 18) Experiment Launch Checklist (No Guesswork)

Use this exact process when creating real experiments.

1. Resolve the site: run `subaccounts.list` and capture `subaccounts[].id` as `siteId`.
2. Resolve goals: run `goals.list` and choose valid goal IDs for `selectedGoals`.
3. Resolve landing page IDs: run `landing_pages.list` and capture landing page IDs.
4. Resolve variant IDs: run `reports.landing_page.details` for each selected landing page and capture `landingPage.variants[].id`.
5. Build create payload:
   - For UXON target, pass landing page ID + variant ID.
   - For external target, pass URL field.
   - For URL Pattern target, pass `urlPatterns` on exactly one side.
6. Create draft: run `experiments.create`.
7. Confirm setup: run `experiments.get` and check sources, selected goals, targeting, filters, and `preserveRedirectParams`.
8. Launch: run `experiments.set_status` with `status = "Running"`.
9. Verify results: run `reports.experiment.details` and `reports.experiment.chart`; use `conversionGoalId` only when reporting one selected goal.

Required field matrix for `experiments.create`:

- Always required:
  - `siteId`
  - `nickname`
  - `selectedGoals` for new integrations
  - legacy fallback: one of `goalId` or `goal`
- Control side:
  - if `controlSource = "uxon"` -> `controlLandingPage` and `controlVariant`
  - if `controlSource = "external"` -> `controlPageUrl`
  - if `controlSource = "url_patterns"` -> `urlPatterns.side = "control"`
- Variant side:
  - if `variantSource = "uxon"` -> `variantLandingPage` and `variantVariant`
  - if `variantSource = "external"` -> `variantPageUrl`
  - if `variantSource = "url_patterns"` -> `urlPatterns.side = "variant"`

Experiment setup rules:

- Use `selectedGoals` for one or more conversion goals.
- Do not combine Bounce with any other goal.
- URL Patterns can be selected by Control or Variant, not both.
- URL Patterns require at least one Include URLs condition and support up to 5 total conditions.
- Traffic filters are exact stored tracking key matches, not operator-based rules.
- Geo filters use `geoFilters.countries.type`, `countryCodes`, and `includeUnknown`.
- `preserveRedirectParams` defaults to `true`. When a redirect sends visitors to another URL, UXON keeps UTMs, click IDs, and other query parameters unless this is set to `false`.

Status value rules:

- Landing pages: `draft` or `live` (lowercase)
- Experiments: `Draft`, `Running`, `Paused`, `Ended` (title case)

## 19) Error Message To Action Map

These are real server-side messages and the correct fix path.

### Access and scope

- `API Keys access is available on Advance and Enterprise plans.`
  - Upgrade the plan to Advance or Enterprise.
- `API Keys access is disabled for this enterprise account. Ask your admin to enable it in Operations settings.`
  - Enterprise admin must enable Operations -> API Keys in superadmin.
- `This API key is restricted to a different site.`
  - Use the key's allowed site or use an unrestricted/team-level key.
- `Requested siteId is not part of this team.`
  - Resolve valid site IDs from `subaccounts.list`.

### General validation

- `Invalid command payload`
  - Ensure JSON is valid and includes `command` and `input`.
- `Invalid input for <command>...`
  - Fix fields exactly as described in this playbook.
- `siteId is required for this command.`
  - Include `siteId` unless command is `domains.connect`.
- `Invalid from.` / `Invalid to.` / `Invalid startDate.` / `Invalid endDate.`
  - Use ISO date strings.
- `Invalid date range.`
  - Ensure from <= to, or use a supported `dateRange` token.

### ID resolution

- `landingPageId is required.`
  - Provide `landingPageId` or `id`.
- `experimentId is required.`
  - Provide `experimentId` or `id`.
- `audienceId is required.`
  - Provide `audienceId` or `id`.
- `pageGroupId is required.`
  - Provide `pageGroupId` or `id`.
- `personalizationId is required.`
  - Provide `personalizationId`, `campaignId`, or `id`.
- `goalId is required.`
  - Provide `goalId` or `id`.
- `contactId is required.`
  - Provide `contactId` or `id`.
- `domainId is required.`
  - Provide `domainId` (for `domains.connect`).

### Resource not found

- `Sub-account not found.`
- `Landing page not found.`
- `Experiment not found.`
- `Audience not found.`
- `Page group not found.`
- `Personalization not found.`
- `Domain not found.`
- `Goal not found.`
- `Contact not found.`
- `Site not found.`

Action for all not-found errors:

- Re-resolve IDs using list/read commands and retry with exact UUID.

### Write safety and lifecycle rules

- `Use landing_pages.set_status for status changes.`
  - Do not send `status` to `landing_pages.update`.
- `Use experiments.set_status for status changes.`
  - Do not send `status` to `experiments.update`.
- `Use personalization.set_status for status changes.`
  - Do not send `status` to `personalization.update`.
- `No fields provided to update.`
  - Send at least one mutable field.

### Landing page publish validations

- `A connected domain and URL path are required to publish.`
  - Provide complete publish settings before setting status to `live`.
- `Selected domain must belong to this workspace and be connected.`
  - Attach/connect domain for the same site first.
- `This domain and path are already used by another page.`
  - Choose a unique domain/path pair.
- `Please create a variant before publishing.`
  - Ensure page has at least one variant.
- `Unable to determine the main variant.`
  - Recreate variants and retry publish.
- `Provide exactly one of zipBase64 or zipUrl`
  - Send either inline base64 ZIP content or an HTTPS ZIP URL, not both.
- `Format not accepted. Include index.html at the root of the ZIP.`
  - Upload built static output with `index.html` and referenced assets included.
- `ZIP is too large. Please upload a bundle under 250MB.`
  - Keep custom code page ZIPs under 250MB.
- `Upgrade required: Custom code pages are not included in your current plan.`
  - Upgrade plan or use a normal UXON builder variant.

### Experiment setup validations

- `nickname is required.`
  - Provide non-empty nickname.
- `Selected goal not found.`
  - Use valid goal IDs from `goals.list`, preferably in `selectedGoals`.
- `selectedGoals or goalId or goal is required.`
  - Send `selectedGoals` for new integrations, or legacy `goalId` / `goal`.
- `Bounce cannot be combined with another goal.`
  - Use Bounce by itself or choose non-Bounce conversion goals.
- `controlPageUrl is required for external control source.`
  - Add URL when control source is external.
- `controlLandingPage and controlVariant are required.`
  - Add both IDs when control source is UXON.
- `variantPageUrl is required for external variant source.`
  - Add URL when variant source is external.
- `variantLandingPage and variantVariant are required.`
  - Add both IDs when variant source is UXON.
- `Selected audience was not found.`
  - Re-run `audiences.list` and pass a valid saved audience UUID or `system:<key>`.
- `URL Patterns can be selected on Control or Variant, not both.`
  - Use `url_patterns` on only one side.
- `Add URL Pattern conditions.`
  - Add `urlPatterns.side` and at least one Include URLs condition.
- `URL Pattern side does not match the selected target.`
  - If Control uses `url_patterns`, set `urlPatterns.side` to `control`; if Variant uses it, set it to `variant`.
- `System audiences cannot be updated.`
  - Create a custom audience instead.
- `System audiences cannot be deleted.`
  - Do not delete built-in audiences.
- `Selected Page group was not found.`
  - Re-run `page_groups.list` and pass a valid Page group UUID.
- `URL Patterns support up to 5 conditions.`
  - Reduce total include/exclude URL conditions to 5 or fewer.
- `Selected conversion goal is not part of this experiment.`
  - Use `"all"` or one of the experiment's selected goal IDs for `conversionGoalId`.
- `Invalid Geo filter country condition.`
  - Use `include` or `exclude`.
- `Select at least one country to use Geo filters.`
  - Add country codes or clear `geoFilters`.
- `External page sources are not included in your current plan.`
  - Upgrade plan or switch to UXON sources.
- `Traffic Filters are not included in your current plan.`
  - Remove `trafficFilters` or upgrade.
- `Geo filters are not included in your current plan.`
  - Remove `geoFilters` or upgrade.

### Goals-specific rules

- `System goals cannot update name or active status.`
  - Only custom goals can change name/active.
- `Only the Lead system goal can update Google Ads settings.`
  - Restrict Google Ads fields to the Lead system goal.
- `name is required.`
  - Goal/sub-account/landing page names cannot be blank.

### Domains

- `domain is required.`
  - Provide root domain.
- `Invalid domain or subdomain.`
  - Use valid host format.
- `This domain or subdomain has already been added.`
  - Use a different host or existing domain ID.
- `Failed to create domain.`
  - Retry later; verify domain service config.
- `Domain is missing SaaS reference UUID.`
  - Domain record is incomplete; recreate or repair it.

### Rate limiting and retries

- `Rate limit exceeded`
  - Default limit is 60 requests per minute unless the key has a custom internal limit.
  - Read `X-RateLimit-Reset`, wait, then retry.
- `Failed to process external command`
  - Treat as transient server error; retry with backoff.
