A Product Page Optimization test shows a share of App Store visitors one of up to three alternate product pages ("treatments") instead of the original, so you can measure which converts better. Apple's API calls these appStoreVersionExperiments; the CLI calls them experiments.
Hierarchy: App → experiment → experiment-treatment → experiment-treatment-localization.
Preconditions Apple enforces (the CLI surfaces them as 409 errors — don't retry, explain them):
- The app must have a version in READY_FOR_DISTRIBUTION (or pre-order ready) on the target platform. Otherwise
createfails withSTATE_ERROR.PPO_CREATE_ONLY_ALLOWED_WITH_APP_STORE_DISTRIBUTION. Check withasc versions list --app-id <id>first. - A test runs for at most 90 days or until stopped.
- More treatments → longer to reach significance;
--traffic-proportionis split evenly across treatments (30% with three treatments = 10% each).
Resolve App ID
See project-context.md — check .asc/project.json before asking the user or running asc apps list.
List / inspect
asc experiments list --app-id <APP_ID> [--state APPROVED] [--limit N] [--pretty]
asc experiments list --app-id <APP_ID> --output table
asc experiments get --experiment-id <EXP_ID>
--state accepts any raw state value (case-insensitive): PREPARE_FOR_SUBMISSION, READY_FOR_REVIEW, WAITING_FOR_REVIEW, IN_REVIEW, ACCEPTED, APPROVED, REJECTED, COMPLETED, STOPPED.
Create a test
asc experiments create --app-id <APP_ID> --name "Icon test" --traffic-proportion 30 [--platform ios|macos|tvos|visionos]
--name is the reference name shown in App Analytics. --traffic-proportion is validated to 1–100 before the request is sent. Platform defaults to ios.
Add treatments (max three)
asc experiment-treatments list --experiment-id <EXP_ID>
asc experiment-treatments create --experiment-id <EXP_ID> --name "Blue icon" [--app-icon-name AppIcon-Blue]
asc experiment-treatments update --treatment-id <TRT_ID> [--name NAME] [--app-icon-name ASSET]
asc experiment-treatments delete --treatment-id <TRT_ID>
--app-icon-name is the name of an alternate icon asset bundled in the app — the way to A/B test icons. Screenshot / preview sets for a treatment hang off its localizations (not yet exposed by the CLI; see the doc's "Extending" section).
Choose locales per treatment
asc experiment-treatment-localizations list --treatment-id <TRT_ID>
asc experiment-treatment-localizations create --treatment-id <TRT_ID> --locale en-US
asc experiment-treatment-localizations delete --localization-id <LOC_ID>
Users whose storefront locale isn't included in the treatment are excluded from the test.
Update, start, stop, delete
asc experiments update --experiment-id <EXP_ID> [--name NAME] [--traffic-proportion N] # at least one flag
asc experiments start --experiment-id <EXP_ID> # only once approved (state ACCEPTED/APPROVED)
asc experiments stop --experiment-id <EXP_ID> # ends a running test
asc experiments delete --experiment-id <EXP_ID>
start/stop both go through PATCH /v2/appStoreVersionExperiments/{id} with started: true|false.
State semantics
AppStoreVersionExperimentState exposes semantic booleans; the model adds two more from dates:
| Boolean | True when |
|---|---|
state.isEditable |
PREPARE_FOR_SUBMISSION, READY_FOR_REVIEW, REJECTED |
state.isPendingReview |
WAITING_FOR_REVIEW, IN_REVIEW |
state.isApproved |
ACCEPTED, APPROVED |
state.isFinished |
COMPLETED, STOPPED |
canStart (in JSON) |
approved and startDate is nil |
isRunning (in JSON) |
approved, startDate set, endDate nil |
State-aware affordances
Follow the affordances in the JSON output rather than guessing what's legal next — mutations Apple would reject are simply not offered:
| Situation | Offered |
|---|---|
editable (isEditable) |
createTreatment, update, delete |
approved, not started (canStart) |
start |
running (isRunning) |
stop |
| in review / finished | only listSiblings, listTreatments |
{
"affordances": {
"listSiblings": "asc experiments list --app-id <APP_ID>",
"listTreatments": "asc experiment-treatments list --experiment-id <ID>",
"createTreatment": "asc experiment-treatments create --experiment-id <ID> --name <name>", // editable only
"update": "asc experiments update --experiment-id <ID>", // editable only
"delete": "asc experiments delete --experiment-id <ID>", // editable only
"start": "asc experiments start --experiment-id <ID>", // canStart only
"stop": "asc experiments stop --experiment-id <ID>" // isRunning only
}
}
Typical Workflow
APP_ID=$(cat .asc/project.json 2>/dev/null | jq -r '.appId // empty')
# 0. Confirm a live version exists (otherwise create is rejected)
asc versions list --app-id "$APP_ID" --output table
# 1. Create the test
EXP_ID=$(asc experiments create --app-id "$APP_ID" --name "Icon test" --traffic-proportion 30 \
| jq -r '.data[0].id')
# 2. Add a treatment with an alternate icon, and the locales it applies to
TRT_ID=$(asc experiment-treatments create --experiment-id "$EXP_ID" --name "Blue icon" --app-icon-name AppIcon-Blue \
| jq -r '.data[0].id')
asc experiment-treatment-localizations create --treatment-id "$TRT_ID" --locale en-US
# 3. Submit for review (review-submissions), then poll until canStart is true
asc experiments get --experiment-id "$EXP_ID" | jq '.data[0] | {state, canStart, isRunning}'
# 4. Run it; stop once you have a winner
asc experiments start --experiment-id "$EXP_ID"
asc experiments stop --experiment-id "$EXP_ID"
REST
Every command is also served by asc web-server: GET/POST /api/v1/apps/:appId/experiments, GET/PATCH/DELETE /api/v1/experiments/:id, POST /api/v1/experiments/:id/start|stop, GET/POST /api/v1/experiments/:id/experiment-treatments, PATCH/DELETE /api/v1/experiment-treatments/:id, GET/POST /api/v1/experiment-treatments/:id/experiment-treatment-localizations, DELETE /api/v1/experiment-treatment-localizations/:id. Query params match CLI flags (?state=&limit=).
Reference
For full domain-model, REST and architecture detail see docs/features/product-page-optimization.md.