A/B Testing Checkout
Before replacing your native checkout for every shopper, you can send only a share of them to Simpler Checkout and compare how each group converts on real traffic.
Each visitor is assigned to one of two groups:
| Variant | What the shopper sees |
|---|---|
SIMPLER | Your server redirects them to Simpler Checkout, as described in Replacing Checkout. |
NATIVE | Your native checkout, unchanged. |
Simpler decides the split and assigns each visitor. Your integration asks which group a visitor is in, routes them accordingly, and reports checkout entries and orders for both groups. The results appear in your Simpler dashboard.
The split is configured on our side. Talk to your Simpler account manager to start a test, change the share of shoppers sent to Simpler, or end it.
Prerequisites
You have implemented Replacing Checkout. The SIMPLER group uses exactly that flow.
Overview
The order of calls matters: the session id returned by proceed_to_checkout must be carried into both the cart request and the order_saved_to_remote event, so Simpler can tie each order back to the checkout that produced it.
Step 1: Get the Assignment
When the shopper proceeds to checkout, ask Simpler which group they belong to. Run steps 1–3 where your server handles the request for the checkout page, not in individual buttons, so every entry point — cart page, mini cart, quick view — is covered. A checkout flow that bypasses that request is not part of the test.
GET https://button.simpler.so/stores/{appId}/rollouts?viewerId={viewerId}
No authentication is required. Omit viewerId the first time you see a visitor, and Simpler generates one.
{
"viewerId": "0b7f6a52-3c1e-4a8e-9f7d-2c4b1e6a9d10",
"rollouts": [
{ "id": "5d2c8e14-7a3b-4f61-b0c9-8e1f2a3d4b5c", "name": "simpler_redirect", "enabled": true }
]
}
- Store
viewerIdfor the visitor (in a cookie or their session) and send it on every later call. The assignment is derived fromviewerId, so the same visitor always lands in the same group. - Decide the variant. Find the rollout named
simpler_redirect(compare case-insensitively). If itsenabledistrue, the variant isSIMPLER. In every other case —enabledisfalse, the rollout is missing, or the request fails or times out — the variant isNATIVE. - Keep
idof that rollout as therolloutId.
Because the assignment is deterministic, you may cache the response for a visitor. Keep the cache short (the plugins use 10 minutes) so changes we make to the split reach your shoppers.
Step 2: Report Proceed to Checkout
In both groups, send a proceed_to_checkout event once per cart:
POST https://analytics.simpler.so/remote/event/{appId}
Content-Type: application/json
{
"events": [
{
"name": "proceed_to_checkout",
"payload": {
"variant": "SIMPLER",
"viewerId": "0b7f6a52-3c1e-4a8e-9f7d-2c4b1e6a9d10",
"rolloutId": "5d2c8e14-7a3b-4f61-b0c9-8e1f2a3d4b5c"
},
"triggered_at": 1759132800000
}
]
}
triggered_at is a Unix timestamp in milliseconds. The endpoint responds with 204 No Content.
The response sets a cookie named simplerCheckoutAnalyticsSessionId whose value is <sessionId>_<storeId>. Read it from the Set-Cookie response header and keep the part before the _ as the session id for this checkout. If the cookie is absent, continue without a session id.
Step 3: Route the Shopper
SIMPLER — send the cart request and add the three identifiers to its metadata:
| Key | Value |
|---|---|
viewerId | From step 1. |
rolloutId | From step 1. |
remoteEventSessionId | The session id from step 2. |
NATIVE — render your native checkout as usual.
If the cart request does not return a 303, you can either:
- redirect to a checkout URL, passing the same three identifiers in its
metadataparameter. The order still goes through Simpler and is reported asSIMPLER; or - render your native checkout. The order goes through your checkout, so report it as
NATIVE.
Step 4: Report Every Order
Send an order_saved_to_remote event once for every order placed, whichever group the shopper was in:
{
"session_id": "01923f4e-8b7a-7c3d-9e2f-4a5b6c7d8e9f",
"events": [
{
"name": "order_saved_to_remote",
"payload": {
"variant": "NATIVE",
"viewerId": "0b7f6a52-3c1e-4a8e-9f7d-2c4b1e6a9d10",
"rolloutId": "5d2c8e14-7a3b-4f61-b0c9-8e1f2a3d4b5c",
"orderAmountInCents": 12345
},
"triggered_at": 1759133100000
}
]
}
orderAmountInCents is the order's grand total in minor units. Where the values come from depends on the group:
- Native orders — use the
viewerId,rolloutIdand session id you stored for the visitor, withvariantset toNATIVE— including orders fromSIMPLERshoppers who fell back to your native checkout. - Simpler orders — Simpler places the order by calling your
/submitendpoint from its servers, so there is no shopper cookie or session to read. TakeviewerId,rolloutIdandremoteEventSessionIdfrom theorder.metadatalist in the request body, which echoes back the metadata you sent in step 3:
"metadata": [
{ "key": "remoteEventSessionId", "value": "01923f4e-8b7a-7c3d-9e2f-4a5b6c7d8e9f" },
{ "key": "rolloutId", "value": "5d2c8e14-7a3b-4f61-b0c9-8e1f2a3d4b5c" },
{ "key": "viewerId", "value": "0b7f6a52-3c1e-4a8e-9f7d-2c4b1e6a9d10" }
]
The NATIVE group is the baseline the SIMPLER group is measured against. If native orders are not reported, the test has nothing to compare against and cannot produce a result.
PHP Example
const ROLLOUTS_URL = 'https://button.simpler.so/stores/%s/rollouts';
const EVENTS_URL = 'https://analytics.simpler.so/remote/event/%s';
function get_assignment(string $app_id, ?string $viewer_id): array
{
$url = sprintf(ROLLOUTS_URL, $app_id) . ($viewer_id ? '?viewerId=' . urlencode($viewer_id) : '');
$ch = curl_init($url);
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 3]);
$config = json_decode((string) curl_exec($ch), true) ?: [];
curl_close($ch);
foreach ($config['rollouts'] ?? [] as $rollout) {
if (strcasecmp($rollout['name'], 'simpler_redirect') === 0) {
return [
'viewerId' => $config['viewerId'],
'rolloutId' => $rollout['id'],
'variant' => $rollout['enabled'] === true ? 'SIMPLER' : 'NATIVE',
];
}
}
return ['viewerId' => $config['viewerId'] ?? $viewer_id, 'rolloutId' => null, 'variant' => 'NATIVE'];
}
/** Returns the session id set by the Events API, if any. */
function send_event(string $app_id, string $name, array $payload, ?string $session_id = null): ?string
{
$body = ['events' => [['name' => $name, 'payload' => $payload, 'triggered_at' => (int) (microtime(true) * 1000)]]];
if ($session_id) {
$body['session_id'] = $session_id;
}
$ch = curl_init(sprintf(EVENTS_URL, $app_id));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_TIMEOUT => 3,
]);
$response = (string) curl_exec($ch);
curl_close($ch);
if (preg_match('/^Set-Cookie:\s*simplerCheckoutAnalyticsSessionId=([^_;]+)/mi', $response, $m)) {
return $m[1];
}
return null;
}
On proceed to checkout:
$assignment = get_assignment($app_id, $_SESSION['simpler_viewer_id'] ?? null);
$_SESSION['simpler_viewer_id'] = $assignment['viewerId'];
$_SESSION['simpler_assignment'] = $assignment;
$_SESSION['simpler_session_id'] = send_event($app_id, 'proceed_to_checkout', $assignment);
if ($assignment['variant'] === 'SIMPLER') {
$cart_request['metadata'] = json_encode([
'viewerId' => $assignment['viewerId'],
'rolloutId' => $assignment['rolloutId'],
'remoteEventSessionId' => $_SESSION['simpler_session_id'],
]);
// POST $cart_request to /v1/carts and forward the 303 — see Replacing Checkout.
}
When a native order is placed:
send_event($app_id, 'order_saved_to_remote', [
'variant' => 'NATIVE',
'viewerId' => $_SESSION['simpler_assignment']['viewerId'],
'rolloutId' => $_SESSION['simpler_assignment']['rolloutId'],
'orderAmountInCents' => (int) round($order->grand_total * 100),
], $_SESSION['simpler_session_id']);
When Simpler submits an order to your /submit endpoint:
$metadata = array_column($request['order']['metadata'], 'value', 'key');
send_event($app_id, 'order_saved_to_remote', [
'variant' => 'SIMPLER',
'viewerId' => $metadata['viewerId'] ?? null,
'rolloutId' => $metadata['rolloutId'] ?? null,
'orderAmountInCents' => (int) round($order->grand_total * 100),
], $metadata['remoteEventSessionId'] ?? null);