# Validate purchases directly with Apple and Google

> Use InfluTo without RevenueCat — add App Store Server API and Google Play credentials, let the SDK report purchases, and InfluTo verifies each one with the store.

Store-direct is for apps that don't use RevenueCat. The InfluTo SDK sends the store's signed proof of each purchase — a StoreKit 2 transaction on iOS, a Play Billing purchase token on Android — and InfluTo verifies it with Apple or Google before recording a commission. Afterwards InfluTo checks attributed subscriptions with the stores every hour to pick up renewals, cancellations and refunds. You don't need a backend of your own.

Using RevenueCat? Use the [RevenueCat path](https://docs.influ.to/integrations/revenuecat.md) instead — never both on the same platform.

<!-- steps -->

### Add your store credentials

Open **Settings → Validation** for the app. InfluTo only reads from the stores; it never changes anything in your accounts.

**Apple (App Store Server API)**

1. In App Store Connect, open **Users and Access → Integrations → In-App Purchase** and create a key. Download the `.p8` file — Apple lets you download it once.
2. Note the **Issuer ID** and the **Key ID** shown on that page.
3. In InfluTo, upload the `.p8` (drag and drop works; the Key ID is read from the file name when it's Apple's default), enter the Issuer ID and Key ID, and the app's numeric **App Store ID** (needed for production).
4. Choose the **Environment**: *Sandbox* while you test with TestFlight or sandbox testers, *Production* before you release. InfluTo verifies against exactly the environment you pick — a TestFlight purchase sent to an app set to Production fails verification.

The app also needs its bundle ID set (it comes from the store listing when you add the app).

**Google (Play Developer API)**

1. In Google Cloud, enable the **Google Play Android Developer API** and create a service account with a JSON key.
2. In the Play Console, open **Users and permissions**, invite the service account's email, and give it **View financial data, orders, and cancellation survey responses** for the app.
3. Upload the JSON key in InfluTo and press **Save & validate**.

New Google permissions can take up to 48 hours to reach the API. InfluTo shows the check as "propagating" meanwhile.

### Switch the platform to store-direct

When the credential check is green, pick **Apple — direct**, **Google — direct** or **Both stores — direct** as the validation provider. You can't switch before the check passes, and until you do, the SDK's purchase reports are answered with `400` — so it's safe to ship the SDK first.

The choice is per platform: iOS can be store-direct while Android stays on RevenueCat.

### Let the SDK report purchases

On store-direct platforms the SDK captures purchases on its own once you add the platform's capture code. At startup it asks InfluTo whether the platform is store-direct, and only then starts listening — so the same build is safe on RevenueCat apps.

| SDK | To capture automatically |
|---|---|
| iOS | Built in — the SDK observes StoreKit 2 transactions (also report right after each purchase, see below) |
| React Native | Install `expo-iap` or `react-native-iap` |
| Android | Add `to.influ:android-sdk-billing` |
| Flutter | Add `influto_iap` and call `InfluToPurchaseObserver().enable()` after `initialize` |
| Web / Capacitor | Pass a `purchaseSource` — `capgoNativePurchasesSource()`, `cordovaPurchaseSource()` or `digitalGoodsSource()` for Play in a TWA |

Automatic capture also back-fills purchases that happened before it started, and reports each purchase once. Selling one-time products on Android? List their product IDs in `oneTimeProductIds` (React Native, Android, web) so they're validated as one-time purchases rather than subscriptions.

<!-- /steps -->

## Reporting purchases yourself

You can call `reportPurchase` after each successful purchase — instead of automatic capture (set `autoCapture: false`), or alongside it. Reports are de-duplicated on the store's transaction identity, so a purchase reported twice is still counted once.

> [!TIP]
> On iOS, report right after `product.purchase()` succeeds. StoreKit hands purchases made inside your app back to the caller rather than to the background listener, so without the explicit report a purchase is only picked up when the app next starts.

<!-- tabs -->

```tsx title="React Native / Expo"
// Store-direct only (no RevenueCat). With expo-iap or react-native-iap installed,
// purchases are captured automatically on store-direct apps. To report
// manually instead, pass the store-signed proof:
await InfluTo.reportPurchase({
  platform: 'ios',                 // or 'android'
  signedTransaction: jwsString,    // iOS: StoreKit 2 JWS
  // purchaseToken: token,         // Android: Play Billing purchaseToken
});
```

```swift title="iOS (Swift)"
// Store-direct only (no RevenueCat). Report right after a purchase — StoreKit
// doesn't send purchases made in your app to the background listener, and
// reports are de-duplicated, so this is safe alongside auto-capture:
if case .success(let verification) = try await product.purchase() {
  let r = try await InfluTo.reportPurchase(verification: verification)
}
```

```kotlin title="Android (Kotlin)"
// Store-direct only (no RevenueCat). With the android-sdk-billing artifact on the
// classpath, purchases are captured AUTOMATICALLY. To report manually:
InfluTo.reportPurchase(
  purchaseToken = purchase.purchaseToken,
  // productId = "coins_100",  // one-time / consumable products only
)
```

```dart title="Flutter (Dart)"
// Store-direct only (no RevenueCat). For automatic capture add influto_iap and
// call InfluToPurchaseObserver().enable() after initialize(). Or report manually:
await InfluTo.instance.reportPurchase(
  platform: 'ios',                 // or 'android'
  signedTransaction: jws,          // iOS: StoreKit 2 JWS
  // purchaseToken: token,         // Android: Play Billing purchaseToken
);
```

```ts title="Web / Capacitor / PWA"
// Store-direct only (no RevenueCat). Attach a purchase source and purchases are
// captured automatically — TWA/PWA on Play: digitalGoodsSource(); Capacitor:
// capgoNativePurchasesSource(); Cordova: cordovaPurchaseSource(). Or report manually:
await InfluTo.reportPurchase({
  platform: 'android',             // or 'ios'
  purchaseToken: token,            // Android: Play Billing purchaseToken
  // signedTransaction: jws,       // iOS: StoreKit 2 JWS
});
```

<!-- /tabs -->

- `reportPurchase` throws on failure — handle it.
- A `503` means the currency rate wasn't available; retry later. Reports are idempotent, so retrying is safe.
- A `409` on Android means the one-time purchase isn't in the PURCHASED state yet (still pending).
- On iOS, set the purchase's `appAccountToken` with `InfluTo.appAccountToken(forUserID:)` if you want purchases tied to your user ID.

Purchases are de-duplicated on the store's identity — Apple's original transaction ID and Google's purchase token — so the same purchase is never counted twice, whoever reports it.

## What InfluTo handles on the server

- Verifies every reported purchase with Apple or Google before recording it.
- Converts the price to USD at the purchase date's exchange rate; commissions are calculated in USD.
- Checks attributed subscriptions every hour for renewals, cancellations, expirations and refunds.
- Reverses commission on refunded subscriptions and one-time purchases on both stores.

## Test it

1. Set the Apple environment to **Sandbox** and use a TestFlight or sandbox tester account, or a Google Play license tester.
2. Open a referral link, install, launch, buy.
3. Check **Real-Time Events** — the purchase shows as validated and attributed. Sandbox purchases never move money.

Switch Apple back to **Production** before you release. Full checklist: [Verify the integration](https://docs.influ.to/integrations/testing.md).

## With an AI agent

Agents can't receive your keys — credentials always go through a dashboard link you open yourself (`influto_get_credential_upload_link`). After that, `influto_validate_store_credentials` runs the green check and `influto_activate_store_direct` switches the platform over. See [AI agents](https://docs.influ.to/agents/index.md).
