Feature flags and A/B tests
Read feature flags in your app or on your server, and measure A/B tests on paywalls.
A feature flag turns a feature on or off, or picks a variant, for each person without a release. An A/B test splits the people who reach a paywall location between several paywalls and measures which converts better. Your code reads flags and decides what to do; A/B tests on paywalls need no code at all.
Feature flags and A/B tests are in early access. Ask us on Discord, or email support@voidhash.com on a paid plan, to turn them on for your team.
| Use | When you want to |
|---|---|
| A feature flag | Roll out a feature gradually, switch it off remotely, or try variants of your own screens. |
| An A/B test | Compare paywalls at a location and read conversion and revenue per arm in Studio. |
Don't gate paid access on a flag. Check perks for that.
Evaluate flags in your app
Each SDK evaluates flags for the current person, caches the results on the device and keeps answering from the cache while Voidhash is unreachable. A flag that is off, unknown or not evaluated yet reads as off, or as the default you set, so write your code so that off is the safe behavior. Changes made in Studio reach the app at its next refresh.
See the SDK pages for Swift, Kotlin, React Native and Web. Flags work without analytics consent.
Evaluate flags on your server
Call POST /projects/{projectId}/feature-flags/evaluate
with a secret key. Name the person by any of personId,
distinctId, email or externalIds, and list the flag keys you need, or omit them for every
flag:
curl -X POST "https://api.voidhash.com/api/v1/projects/$VOIDHASH_PROJECT_ID/feature-flags/evaluate" \
-H "Authorization: Bearer $VOIDHASH_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "distinctId": "user_123", "keys": ["new-checkout"] }'Each entry in flags has key, isEnabled, variantKey, the variant's value, the reason for
the answer from the rules below, and the ruleId when a rule decided. The call changes nothing and
is safe to repeat. It answers as the person's app would in the key's own
environment; send environment to ask for the other.
How a flag decides
A flag has one configuration per environment. For each person, Voidhash reads it from the top and stops at the first line that names them:
| Order | Line | Result | reason |
|---|---|---|---|
| 1 | The environment's switch is off. | Off, overrides included. | disabled |
| 2 | The person has an override. | What the override says: on, off or one variant. | override |
| 3 | A rule names the person. Rules are read in order. | The flag for the rule's share of its people. | rule |
| 4 | Everyone else. | The flag for the fallthrough's share. | default |
A rule and the fallthrough each serve the flag to a share of their people, from 0% to 100%, and the
rest get it off. A flag with variants gives the variant a line names, or splits people evenly across
its variants when the line names none. A person no identity is known for can be given no share, so
they get the flag off with the reason no_identity; an override that names their email still
reaches them. Archived flags aren't evaluated at all, and apps fall back to their defaults.
Environments
Every API key belongs to production or to test, and a flag answers a key from that environment's configuration.
- Test follows production until you give test a configuration of its own. Until then a test key gets exactly what a production key gets, and a change to production changes both.
- A test configuration starts as a copy of production's. From then on the two are separate: turn a flag on in test and leave it off in production, or give one person an override in test only.
- Applying test to production copies test's configuration over production's. Resetting test removes its configuration, so it follows production again.
An A/B test's own flag has one configuration, which both environments read.
Change a flag from the API
Studio edits all of this on the flag's page. From the API, CLI or MCP server:
- Set who gets a flag: send an environment's whole configuration to
PUT …/feature-flags/{featureFlagId}/environments/{environment}.rolloutPercentageis 0–100 in 0.01 steps. - Make test follow production again:
DELETE …/environments/test. - Fix one person's answer: send
isEnabled, an optionalvariantKeyand an optionalnotetoPUT …/feature-flags/{featureFlagId}/overrides/{identity}, whereidentityisdistinct_id:user_123or another identity type, and?environment=testfor test. An override with a variant turns the flag on.
curl -X PUT "https://api.voidhash.com/api/v1/projects/$VOIDHASH_PROJECT_ID/feature-flags/$FLAG_ID/environments/production" \
-H "Authorization: Bearer $VOIDHASH_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "isEnabled": true, "fallthrough": { "rolloutPercentage": 25 } }'How assignment stays stable
Voidhash assigns flags and A/B test arms by the person, not at random on each request:
- The same person gets the same answer on every visit and, once identified, on every device where your app identifies them with the same user ID.
- Raising a share only adds people. Nobody who has a flag loses it when you go from 10% to 20%, and a person keeps their place when they move from one rule to another.
- A flag stores nothing per person, so identifying can change its answer: a person who starts anonymous and then signs in is placed again as the identified person, and their share or variant of a flag may change once.
- An A/B test remembers the arm it first gave someone. They keep it after signing in, and editing the split only changes where people assigned from then on go.
A person's place in a share is the same in production and in test; what differs is the configuration each environment reads.
A/B tests on paywalls
An A/B test runs at one or more locations. Your app keeps calling presentFlow with the same slug,
and Voidhash picks the person's arm when the location resolves. There is no SDK method for A/B tests
and nothing to release. Each arm shows its paywall's current deployed build, so an app whose SDK
can't draw an arm's paywall gets FLOW_NOT_ASSIGNED and isn't counted.
Exposures
When a location resolves to an arm, Voidhash records a $experiment.exposed event for the person,
with the test in experimentId and the arm in variantKey. Repeated exposures to the same arm count
once. No exposure is recorded while the test is paused, or when the person has withheld analytics
consent; they still see a paywall. Turning off the experiment exposure event under the project's
event settings breaks results.
Conversions
A conversion is the test's goal event, sent any time after the person's first exposure. Set it explicitly when you create the test:
$subscription.createdfor a paywall that sells subscriptions. It fires when a subscription starts, including a free trial.$purchase.completedfor one-time products, or a mix. It fires on every new purchase, but not on renewals.- A custom event your app captures, for any other goal.
Without a goal, a test counts purchase_completed, an event Voidhash never sends, so every arm
shows 0 conversions. The goal locks once the test starts.
From the API, set primaryMetricEventName when you create the experiment, or update it while it is
a draft. Revenue per arm counts what exposed people paid after their first exposure; it needs no
extra events.
Results
GET …/experiments/{experimentId}/results
returns each arm's exposures, conversions, conversionRate and revenue (USD cents) since the
test last started or resumed. A person counts in the arm of their first exposure. Results are per
environment: exposures and purchases made through a test key appear only when you read results with
a test secret key, never in Studio or with a production key. Studio, the CLI and the MCP server read
these counts the same way: each arm is compared with the control by a two-sided, pooled
two-proportion z-test at 5% significance, split between the arms when there are several (a
Bonferroni correction). An arm is too early to tell while it or the control has fewer than 100
exposed people, or both together have fewer than 10 conversions. Secondary events are recorded but
not analyzed.
Compare variants on any metric
The results cover one goal. To see what a test did to anything else, such as MRR, retention or an event of your own, use a variant's id wherever the analytics API takes a cohort id: it stands for the people the test gave that variant. Split a metric by the test's variant ids to compare them, or filter by one to read a single variant. In Studio, Compare in Explore on a test's Results tab opens that split.
A variant's people are everyone the test assigned to it, including visitors who haven't signed in, and they count with everything they did in the range you read, so start the range when the test started. People you fixed to a variant as testers aren't included.
Experiments on your website
The web SDK doesn't present paywalls. To test something on your website, use a flag with variants and track your own event with the variant when the visitor sees it; the web SDK records no exposure when a flag is read.
Next steps
- Manage A/B tests and manage feature flags in the Help center.
- Paywalls in your app: locations, results and fallbacks.
- Events: built-in revenue events and custom events.
- Manage flags and tests from the CLI or an AI agent.
Something wrong or missing on this page? Tell us by email or on Discord.