Horizon OS Store Setup
Horizon OS is Meta's operating system for Quest headsets, and OpenIAP treats it as an Android build target. Select the horizon platform flavor, provide the Horizon app id from Meta Horizon Developer Hub, and ship a Quest artifact that is separate from your Google Play or Amazon Fire OS artifacts.
Required Values#
Horizon setup has one OpenIAP-specific configuration value. It is the Meta app id for the Horizon app record, not the Android package name.
| Value | Where to get it | Where OpenIAP reads it |
|---|---|---|
| Horizon app id | Meta Horizon Developer Hub app record. Gradle projects commonly expose it through a placeholder named HORIZON_APP_ID. | Expo uses android.horizon.appId. Bare React Native reads a Gradle property named horizonAppId; Flutter reads HORIZON_APP_ID from android/local.properties. Both write Android manifest meta-data com.meta.horizon.platform.HORIZON_APP_ID. |
| Product SKUs | Meta Horizon Developer Hub monetization products. | The SKU values passed to fetchProducts, requestPurchase, and Horizon verification. |
| Verification credentials | Your backend or IAPKit project configuration. | Runtime verification payloads use horizon.sku, horizon.userId, and horizon.accessToken when verifying Horizon receipts. |
Store Prerequisites#
Complete these once per app in Meta Horizon Developer Hub before configuring any framework:
- Register the app in Meta Horizon Developer Hub.
- Create your products and subscriptions with stable SKUs — the same strings you will pass to
fetchProductsandrequestPurchase. - Copy the Horizon app id so the Android manifest can read it at build time.
- Prepare a Meta Quest device signed into an account that has access to the app, such as a release-channel member or registered test user.
Framework Setup#
Every framework except Godot ships Quest support through the same Android horizon flavor; only the switch location differs. Find your framework here, then follow the matching section below for full snippets.
| Framework | How Horizon is selected | App id location |
|---|---|---|
| Native Android | Depend on openiap-google-horizon or select platform=horizon. | Android manifest meta-data. |
| Expo | modules.horizon plus android.horizon.appId in the expo-iap config plugin. | The config plugin writes manifest meta-data. |
| React Native | horizonEnabled=true in Gradle properties. | The app writes Android manifest meta-data directly. |
| Flutter | horizonEnabled=true and app-level missingDimensionStrategy. | Gradle manifest placeholder, usually from local properties. |
| KMP | Build/publish the Android horizonRelease variant. | The Android host app owns manifest meta-data. |
| MAUI | Build Android with OpenIapAndroidStore=horizon, meta, or quest. | The Android manifest in the MAUI app owns the app id. |
| Godot | No dedicated Horizon flavor switch yet, so there is no Godot section below. | Not applicable. |
Native Android#
Use the Horizon artifact directly, or select the local Gradle flavor when building from source:
dependencies {
implementation("io.github.hyochan.openiap:openiap-google-horizon:3.5.0")
}
android {
defaultConfig {
missingDimensionStrategy("platform", "horizon")
}
}Provide the app id in the Android manifest:
<meta-data
android:name="com.meta.horizon.platform.HORIZON_APP_ID"
android:value="YOUR_HORIZON_APP_ID" />Expo#
Expo apps should let the expo-iap config plugin write the Gradle flavor, dependency, and manifest app id during prebuild:
plugins: [
[
'expo-iap',
{
modules: {
horizon: true,
},
android: {
horizon: {
appId: 'YOUR_HORIZON_APP_ID',
},
},
},
],
]React Native#
react-native-iap has no Expo config plugin, so select the Horizon flavor in the app Gradle build and write the app id in the manifest yourself. fireOsEnabled is the switch for Amazon Fire OS builds; the two flavors are mutually exclusive, so keep it false for Quest artifacts:
# android/gradle.properties
horizonEnabled=true
fireOsEnabled=false
horizonAppId=YOUR_HORIZON_APP_ID// android/app/build.gradle
android {
defaultConfig {
def horizonEnabled = project.findProperty('horizonEnabled')?.toBoolean() ?: false
def fireOsEnabled = project.findProperty('fireOsEnabled')?.toBoolean() ?: false
if (horizonEnabled && fireOsEnabled) {
throw new GradleException("horizonEnabled and fireOsEnabled cannot both be true")
}
def flavor = fireOsEnabled ? 'amazon' : (horizonEnabled ? 'horizon' : 'play')
missingDimensionStrategy "platform", flavor
manifestPlaceholders = [
HORIZON_APP_ID: project.findProperty('horizonAppId') ?: ''
]
}
}<meta-data
android:name="com.meta.horizon.platform.HORIZON_APP_ID"
android:value="${HORIZON_APP_ID}" />Flutter#
Flutter uses the same Gradle property model as bare React Native. The app module maps the property into the plugin flavor and injects the app id through a manifest placeholder; localProperties is the loader the Flutter Android template already defines in android/app/build.gradle:
# android/gradle.properties
horizonEnabled=true
fireOsEnabled=false# android/local.properties
HORIZON_APP_ID=YOUR_HORIZON_APP_IDdef horizonEnabled = project.findProperty('horizonEnabled')?.toBoolean() ?: false
def fireOsEnabled = project.findProperty('fireOsEnabled')?.toBoolean() ?: false
def flavor = fireOsEnabled ? 'amazon' : (horizonEnabled ? 'horizon' : 'play')
android {
defaultConfig {
missingDimensionStrategy 'platform', flavor
manifestPlaceholders = [
HORIZON_APP_ID: localProperties.getProperty("HORIZON_APP_ID") ?: ""
]
}
}Reference the placeholder in the Android manifest:
<meta-data
android:name="com.meta.horizon.platform.HORIZON_APP_ID"
android:value="${HORIZON_APP_ID}" />KMP and MAUI#
KMP publishes per-store Android variants of the library; Quest apps consume the horizonRelease variant and keep the app id in the Android host app's manifest, exactly as in the Native Android section above. When building the library from source, assemble the variant directly:
./gradlew :library:assembleHorizonReleaseMAUI selects the Horizon AAR flavor with an MSBuild property:
dotnet build -f net10.0-android -p:OpenIapAndroidStore=horizonVerification#
Client purchase calls stay the same on Quest — fetchProducts and requestPurchase work unchanged. For server validation, pass the Horizon receipt context to Validation or to IAPKit, OpenIAP's hosted verification backend, using the horizon payload:
await verifyPurchase({
horizon: {
sku: purchase.productId,
userId: metaUserId,
accessToken: horizonAccessToken, // Meta app credential
},
});The access token is a Meta app credential. Keep Horizon verification on trusted infrastructure — your backend or IAPKit — rather than embedding the token in the shipped app.