Use this page with AI

Copy this into your coding assistant and add your request.

Read https://openiap.dev/docs/setup/store/horizon and https://openiap.dev/llms.txt. Follow the reading instructions, detailed reference, and linked guides relevant to my task before making changes.
Inspect my existing project and reuse its framework and conventions. Ask me for missing product decisions. Implement the requested behavior and run the applicable checks.
Show the working result, the commands and actual test results, and any remaining limitations. Keep your explanation brief.

My request: [describe what customers should be able to do]
See an example request →

Horizon OS Store Setup

Horizon OS is Meta's operating system for Quest headsets, and OpenIAP treats it as an Android build target. A debug build follows a connected Quest and a release build is pinned to horizon; 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.

ValueWhere to get itWhere OpenIAP reads it
Horizon app idMeta 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; Godot reads the openiap/horizon_app_id export option. Each writes Android manifest meta-data com.meta.horizon.platform.HORIZON_APP_ID.
Product SKUsMeta Horizon Developer Hub monetization products.The SKU values passed to fetchProducts, requestPurchase, and Horizon verification.
Verification credentialsYour 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:

  1. Register the app in Meta Horizon Developer Hub.
  2. Create your products and subscriptions with stable SKUs — the same strings you will pass to fetchProducts and requestPurchase.
  3. Copy the Horizon app id so the Android manifest can read it at build time.
  4. 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 ships Quest support through the same Horizon build of openiap-google; only the switch location differs. Find your framework here, then follow the matching section below for full snippets.

FrameworkHow Horizon is selectedApp id location
Native AndroidThe OpenIAP Gradle plugin: a connected Quest on a debug build; openiapStore=horizon pins it.Android manifest meta-data.
ExpoResolved at build time; an EAS profile pins a release with ORG_GRADLE_PROJECT_openiapStore=horizon.android.horizon.appId; the config plugin writes manifest meta-data.
React NativeResolved at build time by the shared Gradle resolver; openiapStore=horizon pins it.The app writes Android manifest meta-data directly.
FlutterResolved at build time by the shared Gradle resolver; openiapStore=horizon pins it.Gradle manifest placeholder, usually from local properties.
KMPThe OpenIAP Gradle plugin, as for native Android.The Android host app owns manifest meta-data.
MAUIA connected Quest on a Debug build; OpenIapStore=horizon pins it.The Android manifest in the MAUI app owns the app id.
GodotLeft at auto, a debug export follows a connected Quest; for release, set openiap/android_store to horizon.The openiap/horizon_app_id export option.

Native Android#

Depend on openiap-google and apply the OpenIAP Gradle plugin; it links openiap-google-horizon instead when a debug build finds a Quest, or when openiapStore=horizon pins a release.

kt
// settings.gradle.kts — keep mavenCentral() in pluginManagement.repositories
plugins {
    id("io.github.hyochan.openiap") version "3.6.3"
}

// app/build.gradle.kts
dependencies {
    implementation("io.github.hyochan.openiap:openiap-google:3.6.3")
}

Provide the app id in the Android manifest:

xml
<meta-data
    android:name="com.meta.horizon.platform.HORIZON_APP_ID"
    android:value="YOUR_HORIZON_APP_ID" />

Expo#

Keep the app id in the config plugin; the plugin writes the manifest meta-data on every prebuild and the Gradle build picks the store. An EAS build has no Quest to follow and a release build never looks, so pin every EAS profile that must target Horizon in its env. The Meta Horizon Store takes APK uploads, and EAS builds an AAB unless android.buildType is apk.

json
{
  "build": {
    "quest": {
      "android": { "buildType": "apk" },
      "env": { "ORG_GRADLE_PROJECT_openiapStore": "horizon" }
    }
  }
}
ts
plugins: [
  [
    'expo-iap',
    {
      android: {
        horizon: {
          appId: 'YOUR_HORIZON_APP_ID',
        },
      },
    },
  ],
]

React Native#

react-native-iap picks the store itself when Gradle runs, so the app only writes the Horizon app id into its manifest. Nothing below changes between Play and Quest builds: a connected Quest on a debug build, a horizon flavor, or -PopeniapStore=horizon selects Horizon.

props
# android/gradle.properties
horizonAppId=YOUR_HORIZON_APP_ID
groovy
// android/app/build.gradle
android {
    defaultConfig {
        manifestPlaceholders = [
            HORIZON_APP_ID: project.findProperty('horizonAppId') ?: ''
        ]
    }
}
xml
<meta-data
    android:name="com.meta.horizon.platform.HORIZON_APP_ID"
    android:value="${HORIZON_APP_ID}" />

Flutter#

flutter_inapp_purchase picks the store itself, so the app only injects the Horizon app id through a manifest placeholder. Pin a release build with ORG_GRADLE_PROJECT_openiapStore=horizon flutter build apk.

props
# android/local.properties
HORIZON_APP_ID=YOUR_HORIZON_APP_ID
kt
// android/app/build.gradle.kts
import java.util.Properties

val localProperties = Properties().apply {
    rootProject.file("local.properties").takeIf { it.isFile }?.inputStream()?.use { load(it) }
}

android {
    defaultConfig {
        manifestPlaceholders["HORIZON_APP_ID"] =
            localProperties.getProperty("HORIZON_APP_ID") ?: ""
    }
}

An older Groovy build.gradle already defines localProperties; set the same placeholder there.

Reference the placeholder in the Android manifest:

xml
<meta-data
    android:name="com.meta.horizon.platform.HORIZON_APP_ID"
    android:value="${HORIZON_APP_ID}" />

KMP and MAUI#

KMP apps apply the same plugin in settings.gradle.kts, which links kmp-iap's Horizon build, and keep the app id in the Android host app's manifest exactly as in the Native Android section above.

A MAUI Debug build follows a connected Quest; pin a release with the MSBuild property. Unlike the other frameworks, every MAUI build also carries the libraries the other stores need (MAUI Setup).

sh
dotnet publish -f net10.0-android -c Release -p:OpenIapStore=horizon

Godot#

Left at auto, the openiap/android_store export option exports the Horizon artifact for a debug export when one Quest is connected, or the one ANDROID_SERIAL names. A release export ignores the device, so set the option to horizon for release. Put the app id in the openiap/horizon_app_id export option; the plugin writes the manifest meta-data.

[preset.1.options]
openiap/android_store="horizon"
openiap/horizon_app_id="YOUR_HORIZON_APP_ID"

Verification#

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:

ts
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.