Skip to main content

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:

VariantWhat the shopper sees
SIMPLERYour server redirects them to Simpler Checkout, as described in Replacing Checkout.
NATIVEYour 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.

Enabled by Simpler

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 viewerId for the visitor (in a cookie or their session) and send it on every later call. The assignment is derived from viewerId, so the same visitor always lands in the same group.
  • Decide the variant. Find the rollout named simpler_redirect (compare case-insensitively). If its enabled is true, the variant is SIMPLER. In every other case — enabled is false, the rollout is missing, or the request fails or times out — the variant is NATIVE.
  • Keep id of that rollout as the rolloutId.

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:

KeyValue
viewerIdFrom step 1.
rolloutIdFrom step 1.
remoteEventSessionIdThe 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 metadata parameter. The order still goes through Simpler and is reported as SIMPLER; 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, rolloutId and session id you stored for the visitor, with variant set to NATIVE — including orders from SIMPLER shoppers who fell back to your native checkout.
  • Simpler orders — Simpler places the order by calling your /submit endpoint from its servers, so there is no shopper cookie or session to read. Take viewerId, rolloutId and remoteEventSessionId from the order.metadata list 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" }
]
Report native orders too

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);

See also​