VoidhashDocs
DevelopersHelp centerGuidesAPI reference
Dashboard
Get started
Introduction
Quickstart
How Voidhash works
Glossary
Versions and releases
Concepts
Teams, projects and environments
API keys
Products and perks
Offers
Subscription lifecycle
People and identity
Groups
Events
Features
Paywalls in your app
Feature flags and A/B tests
Paywall components
Push notifications
Ad attribution in your app
Client SDKs
React Native
Swift
Kotlin
Web
Your backend
Node.js
PHP
Go
Rust
Other languages
Payment sources
Forward store notifications
Developer tools
CLI
MCP server
Query analytics with VoidQL

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.

Early access

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.

UseWhen you want to
A feature flagRoll out a feature gradually, switch it off remotely, or try variants of your own screens.
An A/B testCompare 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:

OrderLineResultreason
1The environment's switch is off.Off, overrides included.disabled
2The person has an override.What the override says: on, off or one variant.override
3A rule names the person. Rules are read in order.The flag for the rule's share of its people.rule
4Everyone 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}. rolloutPercentage is 0–100 in 0.01 steps.
  • Make test follow production again: DELETE …/environments/test.
  • Fix one person's answer: send isEnabled, an optional variantKey and an optional note to PUT …/feature-flags/{featureFlagId}/overrides/{identity}, where identity is distinct_id:user_123 or another identity type, and ?environment=test for 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.created for a paywall that sells subscriptions. It fires when a subscription starts, including a free trial.
  • $purchase.completed for 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.
Always set the 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.

PreviousPaywalls in your appNextComponents

On this page

Evaluate flags in your appEvaluate flags on your serverHow a flag decidesEnvironmentsChange a flag from the APIHow assignment stays stableA/B tests on paywallsExposuresConversionsResultsCompare variants on any metricExperiments on your websiteNext steps