Your app

React Native and Expo

Keep react-native-iap (or expo-iap) for the payment sheet. Send what it hands you to Vend16, finish once we’ve answered, and unlock from our answer.

1Get your public key

Create a free account, verify your email, and the setup steps ask for your app’s name, iOS bundle ID and Android package name. You get a public key (pk_…, it goes in the app) and a secret key (sk_…, only for a server, if you have one). Both are shown once, so copy them.

2Add it to your app

Install react-native-iap (version 14 or later, which needs React Native 0.79 or later and gives you the StoreKit 2 JWS on iPhone as purchaseToken). It needs react-native-nitro-modules alongside it.

Terminal
npm install react-native-iap react-native-nitro-modules
npx pod-install

Expo: use expo-iap instead (npx expo install expo-iap), in a development build. Neither library runs in Expo Go. The code below works with either; change the import.

Report every purchase, finish only after Vend16 answers, and read access back:

vend16.ts
import { useEffect } from 'react';
import { Platform } from 'react-native';
import {
  initConnection, purchaseUpdatedListener, getAvailablePurchases, finishTransaction, type Purchase,
} from 'react-native-iap';

const KEY = 'pk_your_public_key'; // public key, safe in the app
const BASE = 'https://vend16.com';
const TYPES: Record<string, 'consumable' | 'non_consumable' | 'subscription'> = {
  coins_100: 'consumable', pro_monthly: 'subscription',
};

async function report(p: Purchase, userId: string) {
  const res = await fetch(`${BASE}/v1/receipts`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({
      platform: Platform.OS === 'ios' ? 'ios' : 'android',
      token: p.purchaseToken, // iOS: the StoreKit 2 JWS. Android: the Play purchase token.
      productId: p.productId,
      productType: TYPES[p.productId],
      appUserId: userId,
    }),
  });
  if (res.status === 422) return 'finish-unlock-nothing';
  if (!res.ok) return 'try-later'; // 502 or offline: the store hands it back later
  const { status } = await res.json();
  return ['recorded', 'duplicate', 'revoked'].includes(status) ? 'finish' : 'try-later';
}

async function handle(p: Purchase, userId: string) {
  const outcome = await report(p, userId);
  if (outcome === 'try-later') return; // left unfinished, so it comes back
  try {
    await finishTransaction({ purchase: p, isConsumable: TYPES[p.productId] === 'consumable' });
  } catch { /* already finished: nothing to do */ }
}

export function useVend16(userId: string) {
  useEffect(() => {
    let sub: { remove(): void } | undefined;
    initConnection().then(async () => {
      // New purchases. On iPhone, ones left unfinished last time replay here too.
      sub = purchaseUpdatedListener((p) => { void handle(p, userId); });
      // Android doesn't replay them, so pick up anything paid for but not yet reported.
      if (Platform.OS === 'android') {
        for (const p of await getAvailablePurchases()) {
          if (TYPES[p.productId] === 'consumable' || !('isAcknowledgedAndroid' in p && p.isAcknowledgedAndroid)) {
            await handle(p, userId);
          }
        }
      }
    });
    return () => sub?.remove();
  }, [userId]);
}

export async function activeProducts(userId: string): Promise<string[]> {
  const res = await fetch(`${BASE}/v1/subscribers/${encodeURIComponent(userId)}`, {
    headers: { Authorization: `Bearer ${KEY}` },
  });
  return (await res.json()).active_product_ids;
}

Start purchases with requestPurchase as the react-native-iap docs show; the listener above does the rest.

3Keep the money safe

Three rules that protect your money
  1. Tell the store a purchase is finished only after Vend16 answers recorded, duplicate or revoked. If the answer is pending, or Vend16 can’t be reached (a 502 or no network), leave it unfinished. The store hands it back on the next launch and you try again.
  2. If Vend16 answers 422 invalid_receipt, finish it and unlock nothing. The store says it isn’t a real purchase.
  3. Unlock from active_product_ids in GET /v1/subscribers/:appUserId, never from a flag on the phone. Check it at launch and after every purchase.

Use a stable user ID from your own sign-in (or a UUID you keep on the device), never an email address.

4Connect the stores

Your code is done. The stores need to know about your products and about Vend16:

Stuck? Email hello@spruik.co and a real person replies.