Appearance
Campaign A/B experiments
Español: Experimentos A/B de campaña.
An experiment compares two to five variants of one campaign. It runs on voice, SMS and email campaigns (see SMS and email campaigns). Create it as a draft, start it when the variants are ready, and stop it to make it terminal. The routes, request bodies, response envelopes and status codes below are copied from the Experiment schemas and the listCampaignExperiments, createCampaignExperiment, getCampaignExperiment, updateCampaignExperiment, deleteCampaignExperiment, startCampaignExperiment and stopCampaignExperiment operations in the vendored openapi/dialer-v1.yaml.
Create a draft
POST /v1/campaigns/{id}/experiments accepts name, primary_metric and variants. primary_metric is either contact_rate or promise_rate. The variants array has 2–5 entries, each with a name, a weight from 1 through 1,000, and exactly one is_control: true entry.
bash
curl -sS -X POST "$API/v1/campaigns/$CAMPAIGN_ID/experiments" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"name": "Window test",
"primary_metric": "contact_rate",
"variants": [
{"name": "Control", "weight": 50, "is_control": true, "overrides": {}},
{"name": "Afternoon window", "weight": 50, "overrides": {
"window": {"start": "10:00", "end": "16:00"}
}}
]
}'The response is 201 with { "experiment": ... }. A malformed body is 400; an invalid experiment is 422 with error.details, one entry per refused field. window and min_gap_seconds only apply to a voice campaign, and an SMS or email campaign cannot choose promise_rate (a promise points at a call, not at a message): all three are refused with 422 (createCampaignExperiment). A window must be a subset of the campaign window, and an SMS script must satisfy the campaign's own validators, including the STOP requirement.
What a variant may change
The overrides object is a closed list. Only these keys are accepted:
script:{ "field": "tts_prompt" | "voicemail_message" | "sms_body", "text": "..." };window:{ "start": "HH:MM", "end": "HH:MM" }, never wider than the campaign window;min_gap_seconds: an integer from 1 through 2,592,000 (voice campaigns only, see below);email_template_id: a UUID for an approved template belonging to the tenant.
Keys such as optout_digit, abandonment_threshold and max_dial_ratio are not experiment controls. They are refused rather than ignored. This closed list and the field constraints come from ExperimentVariantOverrides in the vendored spec; the compliance boundary is also recorded in core ADR #242.
Inspect, edit and delete
GET /v1/campaigns/{id}/experimentslists every state, newest first, with variants inpositionorder.GET /v1/campaigns/{id}/experiments/{experiment_id}returns one experiment.PATCH /v1/campaigns/{id}/experiments/{experiment_id}acceptsnameand/orvariants; replacingvariantsreplaces the whole set.primary_metricmay only be repeated, not changed.DELETE /v1/campaigns/{id}/experiments/{experiment_id}deletes only a draft and returns204.
Once an experiment is running or ended, its variants are frozen. Editing or deleting it returns 409. Cross-tenant resources and unknown identifiers return 404.
Start and stop
Start a draft with:
bash
curl -sS -X POST \
"$API/v1/campaigns/$CAMPAIGN_ID/experiments/$EXPERIMENT_ID/start" \
-H "Authorization: Bearer $TOKEN"POST .../start changes draft to running and validates the variants again against the current campaign. It returns 422 if the constraints no longer hold and 409 if another experiment is already running or the experiment is not a draft. Experiments run on voice, SMS and email campaigns whose messages or calls the platform sends itself. Starting one on a preview or manual campaign is 422 (details[].path = "campaign"), even though creating the draft was accepted (startCampaignExperiment).
Stop a running experiment with:
bash
curl -sS -X POST \
"$API/v1/campaigns/$CAMPAIGN_ID/experiments/$EXPERIMENT_ID/stop" \
-H "Authorization: Bearer $TOKEN"POST .../stop changes running to terminal ended; any other current state returns 409.
SMS and email campaigns
On an SMS or email campaign the experiment reads the campaign's message ledger instead of the call ledger (getCampaignExperimentResults):
- Attempts are the original outbound messages and their status transitions. A message the provider never took is
not_placed, and a contact is adeliveredmessage. promisesandabandonedare always 0, sopromise_rateandabandon_ratearenull. Useprimary_metric: contact_rate;promise_rateis refused at create time.- For an SMS campaign a variant can override
scriptwithfield: "sms_body"; the text must still satisfy the campaign's own validators, includingSTOP. For a variant'semail_template_id, the template must be an approved one of the tenant (ExperimentVariantOverrides). - The closed override list, the assignment and the verdict rules are the same as for voice.
Core's v0.105.0 release carries the SMS and email experiment commits tagged core#2057 (messages ledgers stamp the variant, the runners apply it, results and start accept the channel). The vendored spec of that release is the source for the lines above.
The separate read GET /v1/campaigns/{id}/email-skips (campaignEmailSkips) explains why an e-mail campaign did not send to a lead; it is a diagnostic of the e-mail runner and takes no experiment parameter, so it is not part of the experiment contract.
Stable assignment and compliance invariants
Core assigns a debt deterministically from sha256(experiment_id <> debt_id), using the first 64 bits and the variant weights in position order. The same experiment/debt pair therefore receives the same variant across retries; the experiment identifier is part of the input, so separate experiments do not share an assignment. This behavior is defined by core's Dialer.Campaigns.Experiments.Assignment implementation and its tests.
The experiment does not create a separate compliance lane. Every variant goes through the same compliance gates, and the legal disclosure is unchanged across variants. The closed override list is intentional: opt-out, abandonment and dial-ratio controls cannot be varied by an experiment. Core ADR #242 documents this boundary and the campaign runner keeps the full compliance path at origination.
Results and verdict
GET /v1/campaigns/{id}/experiments/{experiment_id}/results returns { "results": ... }. It is recomputed from the call ledger on every read (no counters), and a draft answers with zeros and insufficient_data. Errors are 401, 404 and 429.
bash
curl -sS \
"$API/v1/campaigns/$CAMPAIGN_ID/experiments/$EXPERIMENT_ID/results" \
-H "Authorization: Bearer $TOKEN"variants[]: per variantassigned_debts,attempts,not_placed,placed,contacts,promises,abandoned,successes, and the ratescontact_rate,promise_rateandabandon_rate(eachnullwhen its denominator is zero). Attempts that never left (not_placed) are outside every denominator.verdict.statusisinsufficient_data,no_significant_differenceorwinner. Awinnercarriesvariant_id,versus_variant_id,p_value,difference,ci_lowandci_high;comparisons[]has one entry per challenger andsample_floorstates the minimum sample.allocation_warningistruewhen the observed split of assigned debts deviates from the weights (chi-square, alpha 0.001); then nowinneris emitted.verdict.withheld_byisallocation_warningonly when a would-be winner was withheld (the status is thenno_significant_difference); otherwise it isnull, even withallocation_warning: true.unattributed_attemptsis non-zero when an attempt's experiment stamp and the experiment disagree.
The field definitions are ExperimentResults, ExperimentVariantResults and ExperimentVerdict in the vendored spec (core v0.105.0); the recomputation and verdict rules are recorded in core ADR #249.