Skip to content
Hosted in Europe. Your test data is stored in the European Union.Hosted and stored in EuropeLearn more

Install Probant on your site.

The browser script, consent, conversions, server-side assignment, and the events API.

Two ways to assign visitors

Probant assigns each visitor to a variant, always the same one, then measures their exposures and conversions. Two tools handle this and work together.

  • The browser script (/sdk/v1.js, under 7 KB compressed) assigns visitors, applies the changes made in the editor, and measures.
  • The server helper (the @probant/edge package) assigns before rendering, on your server or at the edge, so the page shows with no flicker at all. The script is still needed to measure.

Both read the same cookie, probant_id, so they give the same visitor the same variant.

Install the browser script

Paste this code into the <head> of every page, with your workspace’s public key (pk_…). The secret key (sk_…) never belongs in a web page: the API rejects it.

Script installation code
<script>
  window.probant = window.probant || [];
  ["consent", "track"].forEach(function (m) {
    probant[m] = probant[m] || function () { probant.push([m].concat([].slice.call(arguments))); };
  });
</script>
<script async src="https://getprobant.com/sdk/v1.js" data-key="pk_…"></script>
  • data-key: the workspace’s public key, required.
  • data-endpoint: the API address, if it differs from the script’s. Defaults to the origin of src.

The first block lets you call probant.consent(…) and probant.track(…) before the script loads: calls wait in a queue and are replayed in order. It’s optional if you don’t call anything before loading. Included twice, the script starts only one instance.

The script doesn’t hide the page while it loads; only a redirect test hides it, while the browser leaves for the other URL (3 seconds at most). Each change applies as soon as its element exists, then again on every re-render (React, client-side navigation), before the next paint. In your selectors, target stable hooks ([data-add-to-cart], IDs) rather than theme classes. An invalid selector counts as not found and doesn’t block the changes that follow.

The app’s Overview shows your key once, with this code ready to copy.

Pass consent

If the workspace requires consent (the default setting), the script sets no cookie, assigns nothing, applies nothing, and sends nothing until the visitor accepts. Pass their choice on every page, as soon as your consent tool knows it:

Pass the visitor’s choice
probant.consent(true);  // accepted: assignment and measurement, without switching the page on screen
probant.consent(false); // refused or withdrawn: cookie and local traces erased

Calls made before consent, track included, wait and are replayed afterward. probant.consent(false) erases the cookie and local traces, empties the send queue, and stops observing the page, even if the workspace doesn’t require consent.

Nothing switches before the visitor’s eyes. If they accept once the page is showing, it stays the original, with no exposure: their variant starts on the next page, reloaded or not, and only conversions for a test they’ve already seen are sent. If they accepted on an earlier visit (the cookie exists), their variant applies as soon as the page loads, and nothing is sent until your tool confirms consent; a refusal erases everything. A test that targets landing pages therefore loses the first view of new visitors who accept mid-page, with no bias between variants: the test plan flags it.

A visitor can also opt out of measurement from your privacy policy, whatever the mode: put the opt-out link there. It sets an opt-out cookie that the script, the journey module, and the server helper respect, and requests deletion of the data linked to the visitor’s ID.

Opt-out link
<div data-probant-opposition></div>
<script async src="https://getprobant.com/sdk/opposition.js" data-key="pk_…"></script>

The probant_id cookie is first-party, SameSite=Lax, and lasts at most 390 days after the first visit, which is under 13 months. The script sets it once; the server helper sets it again with the same expiry (see Lasting identity on Safari): it’s never extended beyond that.

Measure conversions

Send a conversion
probant.track("commande", 89.9); // with an amount
probant.track("ajout-panier");   // no amount: counts as 1

The name (commande, ajout-panier…) is the “event name” that the workspace’s metrics refer to. A conversion is attributed to the tests the visitor has seen, with their variant. Exposure to a test is sent once per visitor, test, and session.

Events are batched and sent with navigator.sendBeacon, which completes the request even if the page closes; otherwise with fetch and keepalive.

Leads and CRM conversions

On a lead generation site, the form carries the Probant visitor ID in a hidden field, which your CRM stores with the lead. Once turned on in Probant (Leads page), the script fills it in marked forms, after the visitor agrees, and never for a visitor who opted out:

Marked form
<form action="/quote" method="post" data-probant-lead>…</form>

// JavaScript form, at submit time:
const id = typeof probant.leadId === "function" ? probant.leadId() : "";

Your CRM then sends back “qualified,” “meeting held,” or “signed” for that ID, server to server, with a management key that has the “Send delayed conversions” permission:

Send a delayed conversion
POST https://getprobant.com/api/v1/conversions
Authorization: Bearer sk_…

{ "visitorId": "0190f2c4-…", "metric": "lead-qualifie",
  "occurredAt": "2026-11-08T10:30:00Z", "externalId": "deal-1042",
  "value": 4800, "currency": "EUR" }
  • No emails, names, or phone numbers. A field that looks like an email address, a name, or a phone number gets the whole request rejected; the CRM ID is stored only as a keyed hash; the first conversion received for an ID stands; a conversion for an unknown visitor or one who opted out is ignored.
  • A window. A “CRM conversion” metric counts the conversion up to day N after the first exposure. A visitor whose window is still open waits outside the calculation, and the test plan adds N days to the duration.
  • By tool, according to their official pages as of November 8, 2026: HubSpot, the workflows “Send a webhook” action (Data Hub Professional or Enterprise plans); Pipedrive, automation webhook (“Advanced and higher” plans); Zapier, Webhooks by Zapier (Professional, Team, Enterprise plans); Salesforce, a Flow’s HTTP Callout action, and Make, HTTP module (check the required plan with the vendor); Stripe, connected directly (below). In every case, send only these fields, never the whole record. See also Lead generation sites.

Subscriptions: Stripe, MRR, retention

For a SaaS, Stripe connects directly: in Workbench → Webhooks, create a “Snapshot” destination pointing to your workspace’s address, select the events below, and paste its signing secret (“whsec_…”) into Probant’s Subscriptions page, which encrypts it.

Endpoint and events
POST https://getprobant.com/api/v1/stripe/<workspace ID>

checkout.session.completed      customer.subscription.created
customer.subscription.updated   customer.subscription.deleted
invoice.paid                    invoice_payment.paid
charge.refunded

At sign-up, your server puts the visitor ID (hidden field or probant.leadId()) in the Checkout session; Probant then finds the visitor through the Stripe customer:

Checkout session
stripe.checkout.sessions.create({
  mode: "subscription",
  client_reference_id: probantId,
  subscription_data: { metadata: { probant_id: probantId } },
  …
})
  • Signature verified the way Stripe describes it (HMAC-SHA256, v1 signatures, constant-time comparison), with the 5-minute tolerance of Stripe’s libraries: an intercepted event can’t be replayed. Each event counts only once.
  • No emails, names, or addresses. Amounts, dates, a status, and each Stripe ID as a keyed hash.
  • Net revenue, MRR, still subscribed. Refunds are subtracted from collected revenue; MRR brings an annual plan back to a monthly amount, as Stripe does; “still subscribed at day N” is a state at day N. Windows up to 365 days, with the same censoring.
  • Cohort follow-up. A stopped test’s report keeps reading its exposed visitors at day 30, 90, 180, and 365, and says when the decision reverses; it’s a descriptive estimate, with simultaneous intervals.
  • Without Stripe: POST /api/v1/subscriptions (status and monthly value) and refundOf on POST /api/v1/conversions. See also Subscriptions (SaaS).

Assign on the server, with no flicker

assignRequest has no framework dependency: Next.js proxy, worker, Express middleware… The variant is known before rendering, and the page arrives already changed.

assignRequest signature
assignRequest({ cookieHeader, url, userAgent, config, consent })
// → { visitorId, setCookie, assignments }
  • config: the response from GET /api/v1/config, to keep in memory for a minute rather than request on every call.
  • consent: the visitor’s choice, read from your consent tool’s cookie. If the workspace requires consent, nothing is assigned without true; an explicit false is always respected.
  • setCookie: add it to each response when it isn’t null: a new visitor’s cookie, the same cookie set again with its original expiry (lasting identity on Safari), or deletion after a refusal.
  • assignments: for each running test or rollout, the variant or the reason for exclusion (holdout, outside-layer-slice, url-mismatch, device-mismatch). assignedVariants(assignments) turns it into an object like { "date-livraison": "b" }; during a rollout, a visitor still on the original appears with a.

The server helper is the @probant/edge package, with no dependencies (see the API documentation).

Example: Next.js 16 proxy.ts

Next.js proxy example
// proxy.ts, at the root of your site
import { NextResponse, type NextRequest } from "next/server";
import { assignRequest, assignedVariants, createConfigLoader } from "@probant/edge";

// The configuration is kept in memory for one minute, and the previous one is
// served if Probant doesn't respond: with no configuration, the site shows the original.
const loadConfig = createConfigLoader({ key: process.env.NEXT_PUBLIC_PROBANT_KEY!, endpoint: "https://getprobant.com" });

export async function proxy(request: NextRequest) {
  const config = await loadConfig();
  if (!config) return NextResponse.next();

  const { setCookie, assignments } = assignRequest({
    cookieHeader: request.headers.get("cookie"),
    url: request.url,
    userAgent: request.headers.get("user-agent"),
    config,
    consent: request.cookies.get("consent")?.value === "yes", // your consent tool
  });

  // Variants reach rendering through a request header.
  const headers = new Headers(request.headers);
  headers.set("x-probant-variants", JSON.stringify(assignedVariants(assignments)));
  const response = NextResponse.next({ request: { headers } });
  if (setCookie) response.headers.append("set-cookie", setCookie);
  return response;
}

export const config = {
  matcher: ["/((?!_next/|api/|favicon.ico).*)"],
};

In the page, a server component reads the variant. With Cache Components, headers() makes the component dynamic: put it inside <Suspense>.

Server component that reads the variant
import { headers } from "next/headers";

export async function DeliveryEstimate() {
  const variants = JSON.parse((await headers()).get("x-probant-variants") ?? "{}");
  if (variants["date-livraison"] !== "b") return null;
  return <p>Estimated delivery: within 2 business days</p>;
}

Keep the browser script on the page: it finds the same variant through the cookie and sends the exposure and conversions. For a server-rendered test, leave the variant without changes in the editor: the script then applies nothing and only measures.

Lasting identity on Safari

Safari deletes a cookie written by a script after 7 days (Intelligent Tracking Prevention). Set by the script alone, probant_id therefore disappears for a Safari visitor who comes back after a week: they get a new ID and may switch variants.

The server helper closes this gap: on every page, setCookie sets the same cookie again, with the same attributes as the script, from your site’s response and with its original expiry (390 days after the first visit, read from the ID). Set by your server, it’s no longer subject to the 7-day limit, and the script reuses it without rewriting it. Set it on the page’s own response (proxy, worker, middleware), not from another domain.

Settings (Script and API tab) and the Overview show where your workspace stands, based on two measured signals: whether Probant sees the server helper when it reads the configuration, and the age of IDs received from Safari (an ID older than 8 days proves a server-set cookie).

Measure page speed

A separate module, /sdk/v1-vitesse.js, measures LCP, INP, and CLS on every page, following the Core Web Vitals definitions, and attributes them to each applied test with its variant. The script loads it only if the workspace measures speed on all pages, or if a running test has a speed metric, and always after consent when the workspace requires it.

Every workspace has three built-in metrics, read at the 75th percentile of pageviews like the Core Web Vitals, and offered as guardrails for every test: a variant that slows the page beyond the limit stops on its own. The module also measures what Probant costs your visitors: downloading v1.js and execution time in long animation frames, where the browser exposes them.

Send events through the API

Use it to send events from a server, for example a conversion confirmed by your back office. Orders themselves, with their line items and product costs, go through the orders API with the secret key, so margin is calculated on Probant’s server, never in the page. To send events:

Event request
POST /api/v1/events
x-probant-key: pk_…
Content-Type: application/json

{ "events": [
  { "type": "exposure", "visitorId": "…", "experiment": "barre-livraison", "variant": "b", "timestamp": 1794299400000 },
  { "type": "conversion", "visitorId": "…", "metric": "commande", "value": 89.9, "timestamp": "2026-11-10T08:30:00Z" },
  { "type": "metric", "visitorId": "…", "metric": "lcp", "value": 2.41, "experiment": "barre-livraison", "variant": "b", "timestamp": 1794299400000 }
] }
  • Up to 500 events and 256 kB per request.
  • timestamp: Unix milliseconds or an ISO 8601 date with a time zone; no more than 5 minutes in the future or 7 days in the past.
  • experiment and variant go together; a conversion or measurement can omit them (visitor outside any test).
  • The key can also go in a parameter (?key=pk_…), which is what the script does with sendBeacon, since it can’t set headers.

Responses

CodeMeaning
202{ accepted, rejected, errors }: each event is validated on its own, so one bad event doesn’t block the others.
401Key missing, unknown, revoked, or secret.
413Body too large.
503Service temporarily unavailable.

To manage tests from a server or an assistant (create, launch, stop, read results): Management API and MCP server.

Public configuration

GET /api/v1/config?key=pk_… returns the workspace’s public configuration: running tests, rollouts, holdout, and consent. The response is cached for 60 seconds.

Gradual rollouts

Once a test is decided, its winning variant can be rolled out in steps from Probant (25%, 50%, and 100% by default). Nothing to change on your site: the script and the server helper read the rollout from the configuration, where the experiment has a rollout field:

Experiment being rolled out, in the configuration
{
  "key": "barre-livraison",
  "status": "stopped",
  "rollout": { "variant": "b", "share": 0.25 },
  "variants": [ … ]
}
  • Who sees the variant. A draw specific to the rollout, independent of the test’s variant draw, puts each visitor in a bucket; they see the variant if their bucket is below the share. The share only goes up: a visitor exposed at one step stays exposed at the next, and nobody goes back to the original after seeing the new version.
  • Population. All visitors targeted by the test (URL, device). The workspace holdout stays out: it measures the cumulative impact of winners, including rolled-out ones.
  • Measurement. Visitors still on the original get the first variant (a); both groups send their exposure and conversions, so exposed visitors and the original can be compared on guardrails at each step.
  • Statuses. A running or paused rollout is served at its share; once completed, the variant is served to everyone (share: 1) with no further measurement; when rolled back to the original, the experiment disappears from the configuration.

An older script ignores stopped experiments: it then shows the original, never an unintended variant.

How assignment works

The browser script and the server helper share the same assignment code: the same visitor gets the same variant everywhere, with nothing stored on the server.

  • Hashing. MurmurHash3 (32-bit), checked against its published reference vectors.
  • Buckets. Each visitor lands in one of 10,000 buckets, based on the hash of their ID and a salt, so a traffic share can be set to a hundredth of a percent.
  • Three independent draws, each with its own salt: the holdout first, then the layer slice, then the weighted variant. Targeting (URL pattern with *, device list) comes last.
  • Layers. Tests in the same layer take separate slices, so they never see the same visitor.
  • Stable keys. A test’s key no longer changes once the test is launched. A stopped test doesn’t restart: you create a new one.

Data and retention

  • Database and compute in Amsterdam, with Scaleway, in the European Union.
  • Visitor events are kept for 13 months after they’re received, then deleted.
  • The probant_id cookie is first-party and lasts at most 390 days after the first visit, never extended beyond that.
  • API keys: only a hash, a non-secret prefix, and the last four characters are stored.

Public key

The public key is readable in the page, like any browser key. Fill in the list of allowed origins in the workspace settings to restrict it to your site’s domains. The API also rate-limits requests, per key and per IP address.

Conclusive Or not yet…

Create your workspace, or explore the demo first with data from a fictional store.

  • Open demo, no sign-up
  • Free to start, no overage charges
  • Database and compute in Amsterdam, with Scaleway