# Vend16: full integration guide Vend16 verifies App Store and Google Play in-app purchases, tracks subscriptions and refunds, and tells your server when anything changes. Base URL: https://vend16.com ## Agent quick start An agent can do the whole setup itself: sign in as the person once, create the app, get the keys and check the store connections. No keys need pasting. **MCP (Claude Code, Cursor, any MCP client).** Run: ``` claude mcp add --transport http vend16 https://vend16.com/mcp ``` The first call opens a browser for the person to sign in (or sign up) and approve. Tools: whoami, list_apps, create_app, update_app, get_app_keys, create_secret_key, setup_status, upload_play_service_account, set_webhook, send_test_webhook, list_recent_purchases, get_integration_guide. **Plain HTTP (any agent or CLI).** Device sign-in, then a token: ``` curl -s -X POST https://vend16.com/v1/device/code -d client_name="My agent" # -> {"device_code":"dc_...","user_code":"BCDF-GHJK","verification_uri_complete":"https://vend16.com/activate?code=BCDF-GHJK","interval":5,...} # Show the person the link and code. Then poll every interval seconds: curl -s -X POST https://vend16.com/v1/device/token -d device_code=dc_... # -> {"error":"authorization_pending"} until they approve, then {"access_token":"at_...","token_type":"Bearer"} ``` With `Authorization: Bearer at_...`: `GET /v1/me`, `GET /v1/apps`, `POST /v1/apps` {name, bundle_id, package_name} (returns the public key), `PATCH /v1/apps/:id`, `GET /v1/apps/:id/keys`, `POST /v1/apps/:id/secret-key` (shown once), `GET /v1/apps/:id/setup`, `POST /v1/apps/:id/google-service-account` {service_account_json}, `PUT /v1/apps/:id/webhook` {url}, `POST /v1/apps/:id/webhook/test`, `GET /v1/apps/:id/purchases?env=sandbox|live`. **The loop.** 1. whoami. If the email is not verified, ask the person to click the link in their inbox. 2. list_apps; reuse an app whose bundle ID or package matches the project, otherwise create_app with the IDs read from the project. 3. Put the public key (pk_) in the app. Wire in purchases with get_integration_guide for the project type (capacitor, ios-swift, android-kotlin, react-native, flutter, server). 4. Only if the project has its own server: create_secret_key and write it straight into that server's environment as VEND16_SECRET_KEY. Never print it, commit it or put it in the mobile app. 5. Call setup_status (GET /v1/apps/:id/setup) and do its `next` step, until `ready` is true. Steps in App Store Connect, Play Console and Google Cloud come with the exact URLs and values to paste: tell the person, or do them if you have tools for those consoles. 6. After a sandbox test purchase, list_recent_purchases confirms it arrived. ## How it works 1. A user buys in your app. Your app receives a token from the store: on iOS the StoreKit 2 `jwsRepresentation`, on Android the Play `purchaseToken`. 2. Your app (or your server) sends it to `POST /v1/receipts`. Vend16 checks it with Apple's signing certificates or the Google Play Developer API, records it, and on Android acknowledges it so Google never auto-refunds it after three days. 3. Vend16 replies with the purchase and the user's current access. Only then should your app call the store's finish/consume. 4. Apple and Google notify Vend16 of renewals, cancellations and refunds. Vend16 updates the user and, if you set a webhook, POSTs a signed event to your server. ### Do I need my own server? No. A standalone app talks to Vend16 directly with the public key (`pk_...`) and asks `GET /v1/subscribers/:appUserId` what to unlock. The secret key (`sk_...`) and webhooks are only for apps that have their own backend. ## Setup 1. Create an account at https://vend16.com/signup and create an app with your iOS bundle ID and Android package name. Copy the public and secret keys (shown once). An agent can do this step itself (Agent quick start, above). 2. App Store: in App Store Connect, App Information, App Store Server Notifications, set both Production and Sandbox URL to `https://vend16.com/v1/notifications/apple/` (Version 2). No Apple API key is needed. 3. Google Play: enable the Google Play Android Developer API in a Google Cloud project, create a service account with a JSON key, invite it in Play Console (Users and permissions) with "View financial data" and "Manage orders and subscriptions", and paste the JSON on the app's Setup page. For renewals and refunds, create a Pub/Sub topic, grant google-play-developer-notifications@system.gserviceaccount.com Publisher, add a push subscription to the Play RTDN URL shown on the Setup page, and select the topic in Play Console, Monetisation setup. ## Capacitor Install the open-source plugin: ``` npm install github:spruikco/capacitor-iap npx cap sync ``` ```ts import { SpruikIap } from '@spruik/capacitor-iap'; import { createVend16 } from '@spruik/capacitor-iap/vend16'; const vend16 = createVend16({ apiKey: 'pk_...' }); // Listen BEFORE initialize: stores replay unfinished purchases on connect. await SpruikIap.addListener('transactionUpdated', (tx) => vend16.handle(tx, user.id, SpruikIap)); await SpruikIap.initialize({ productIds: ['coins_100', 'pro_monthly'] }); const { status, transaction } = await SpruikIap.purchase({ productId: 'pro_monthly' }); if (status === 'purchased') await vend16.handle(transaction, user.id, SpruikIap); const { active_product_ids } = await vend16.subscriber(user.id); ``` `vend16.handle` records the purchase and only then calls `finish()` (consuming consumables). ## Any other platform (HTTP) ``` POST https://vend16.com/v1/receipts Authorization: Bearer pk_... Content-Type: application/json {"platform":"ios","token":"","appUserId":"user_42"} {"platform":"android","token":"","productId":"coins_100","productType":"consumable","appUserId":"user_42","price":{"micros":4990000,"currency":"AUD"}} ``` Fields: `platform` ios|android; `token`; `appUserId` (your stable user ID, not an email); `productId` (required on Android); optional `productType` consumable|non_consumable|subscription (send it on Android: Google does not say, and a one-off product without it is recorded as consumable); optional `price` {micros, currency} for revenue reporting (Google does not report prices). Response: `status` recorded|duplicate|pending|revoked, `purchase`, `subscriber`, and `store` {account_token, quantity, revoked}. `recorded` is the app's first report of a purchase (also when a store notification told Vend16 first, while its event is still held, see Webhooks); `duplicate` is any later one. `store.account_token` is the iOS appAccountToken in lower case, or the Android obfuscatedExternalAccountId as Play returned it. If Google's notification reaches Vend16 before the app's report, a one-off Play purchase is recorded with `product_type` `unknown` (never guessed); the app's report then fills in the type and price. `product_type` in purchases and webhooks is consumable|non_consumable|subscription|non_renewing|unknown. A purchase a store notification announces first (Apple ONE_TIME_CHARGE, Play RTDN) is filed under the store's account ID (iOS appAccountToken in lower case, Android obfuscatedExternalAccountId) until the app reports it; the app's report then moves it to the app's `appUserId`. A one-off purchase an app has reported stays with that user. Apple's ONE_TIME_CHARGE is recorded only once the app's iOS bundle ID is set, and a Family Sharing grant (nobody paid) is not recorded from it. ``` GET https://vend16.com/v1/subscribers/user_42 Authorization: Bearer pk_... ``` Returns `active_product_ids`, `subscriptions` (status, is_active, expires_at, will_renew), `non_consumables`, `recent_one_time_purchases`. Unlock from `active_product_ids`. Secret-key endpoints: `GET /v1/purchases?app_user_id=&limit=`, `GET /v1/events?since=`, `GET /v1/metrics?days=` (MRR, revenue, active subscriptions, churn, refunds). ## Deleting a user's data (privacy requests) `DELETE /v1/subscribers/:appUserId` or `DELETE /v1/purchases/:transactionId[?platform=ios|android]` with the secret key. Deletes the purchases (for a subscription, every renewal), subscription records and events, including unsent webhooks. Returns `{"deleted":{"purchases","subscriptions","events"}}`; repeating it returns zeros. Add `?dry_run=1` for `{"dry_run":true,"would_delete":{...}}` with nothing changed. `dry_run` takes 1, true, 0 or false; any other value, or any other query parameter (`platform` aside), gets 400 and deletes nothing. A UUID user ID matches in any case. Rows are deleted, not anonymised, so dashboard numbers stay consistent. Deleted live purchases still count towards the Free plan allowance (only their number is kept). Nightly backups roll off within 14 days. ## Rotating keys Rotate keys on the app's Setup page. The old secret key stops working straight away. The old public key keeps working for 30 days (so app builds already on phones keep reporting) or until you click "Stop it now" on the Setup page. ## Errors `{"error":{"code","message"}}`. `422 invalid_receipt`: the store says it is not real; finish it and unlock nothing. `422 bundle_mismatch`: wrong app. `502 store_unavailable`: Apple or Google did not answer (Vend16 waits up to 15 s for Google); do not finish; the store will redeliver. `401/403`: wrong or missing key. ## Webhooks Events: initial_purchase, renewal, non_subscription_purchase, cancellation, uncancellation, billing_issue, billing_recovered, paused, expiration, refund, revocation, refund_reversed. Header `Vend16-Signature: t=,v1=`, hex = HMAC-SHA256 of `t + "." + rawBody` with the webhook secret. Reject if older than 5 minutes. Reply 2xx; retries back off from 1 minute to 6 hours for about 3 days. A one-off purchase a store notification announces before the app reports it holds its non_subscription_purchase event for the app's report (up to 72 hours on iOS, 10 minutes on Google Play), so the event carries the app's user, type and price as if the app had reported first. After that it is sent as it stands, within 5 minutes: `app_user_id` is then the store's account ID (or null) and, on Google Play, `product_type` may be `unknown`. A later report still corrects the record, without a second event. Sent once either way. ```js import { createHmac, timingSafeEqual } from 'node:crypto'; function verify(rawBody, header, secret) { const { t, v1 } = Object.fromEntries(header.split(',').map((p) => p.split('='))); if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; const expected = createHmac('sha256', secret).update(t + '.' + rawBody).digest('hex'); return v1.length === expected.length && timingSafeEqual(Buffer.from(v1), Buffer.from(expected)); } ``` ## Plans Free: first 100 live purchases, 1 app, unlimited sandbox. Pro: US$29 / month, 10 apps, unlimited. No revenue share. Passing the Free allowance never blocks customers' purchases; the dashboard asks to upgrade. ## Using an AI coding agent Connect the MCP server (`claude mcp add --transport http vend16 https://vend16.com/mcp`) and ask the agent to add in-app purchases with Vend16: it signs in through the browser and does the setup itself (Agent quick start, above). Claude Code plugin: `/plugin marketplace add spruikco/vend16-skill`, `/plugin install vend16@vend16`, then ask it to add in-app purchases with Vend16. Any other agent (Codex, Cursor, Copilot): paste this in your app's repo: > Integrate Vend16 in-app purchase verification into this app. Read https://vend16.com/llms-full.txt first and follow it exactly. Use my public key pk_REPLACE_ME. Report every completed purchase to POST /v1/receipts with the store token and my user ID, only call finish() after Vend16 replies, and unlock features from GET /v1/subscribers/:appUserId, never from the device. If this is a Capacitor app, install the plugin with: npm install github:spruikco/capacitor-iap. Tell me which App Store Connect and Play Console settings I still need to change.