IAP API Setup Guide
Setting up server-side in-app purchase verification with the Unveily IAP API — no SDK required.
At a Glance
The Unveily IAP API verifies Google Play and App Store purchase receipts from your backend server. Your app handles billing directly via Google Play Billing / StoreKit 2, and your server calls our API to validate each purchase.
Android App ──purchaseToken──▶ Your Server ──googleAccessToken──▶ Unveily API ──▶ Google Play
iOS App ──signedTransaction(JWS)──▶ Your Server ─────────────────────▶ Unveily API ──▶ App Store| Requirement | Detail |
|---|---|
| Unveily plan | IAP API subscription |
| Google Play | Developer account with billing profile set up |
| Your infrastructure | A backend server (any language / any cloud) |
Before You Begin
Service Account Key Security
Your service account key is a master credential
The JSON key grants read access to all order data in your Google Play Console. Treat it like a password — never commit it to source control or share it in plain text.
Store the key securely in the secrets manager that fits your infrastructure:
| Infrastructure | Recommended storage |
|---|---|
| AWS | AWS Secrets Manager or Systems Manager Parameter Store (SecureString) |
| Google Cloud | GCP Secret Manager |
| Azure | Azure Key Vault |
| Self-hosted / On-premises | HashiCorp Vault (recommended) — or OS environment variables as a minimum |
| Any environment | Never hardcode in source code, never commit to Git, never use plain .env in production |
Mission 1 — Create a Google Cloud Service Account
1-1. Enable the Google Play Android Developer API
- Open Google Cloud Console → select the project linked to your Play account
- APIs & Services → Enable APIs and Services
- Search for Google Play Android Developer API → Enable
1-2. Create the service account
- IAM & Admin → Service Accounts → Create Service Account
- Enter a name (e.g.,
unveily-iap-verifier) → Create and Continue - Skip role assignment at this step → Done
1-3. Download the JSON key
- Click the service account you just created → Keys tab
- Add Key → Create new key → JSON → Create
- The JSON file downloads automatically — store it in your secrets manager immediately
One-time download
Google only lets you download the key once at creation time. If you lose it, delete the key and create a new one.
Mission 2 — Link to Google Play Console
2-1. Grant access
- Google Play Console → Setup → API access
- Link to your Google Cloud project (if not already linked)
- Under Service accounts, find the account you just created → Grant access
2-2. Set minimum permissions
Principle of least privilege
Grant only what is required for purchase verification — nothing more.
| Permission | Required | Reason |
|---|---|---|
| View financial data | Yes | Read purchase and subscription status |
| Manage orders and subscriptions | Yes | Required to call the purchases API |
| All other permissions | No | Do not enable |
- Save → allow up to 24 hours for permissions to propagate
Mission 3 — Generate an Access Token on Your Server
Your server reads the JSON key from its secrets store and generates a short-lived Google OAuth2 Access Token (valid 1 hour). This token — not the JSON key itself — is what you send to Unveily.
Why send a token, not the key?
The JSON key is a permanent credential. The access token expires in 1 hour. Even if intercepted over HTTPS, a token's exposure window is strictly limited. Unveily never stores the token — it is used once per request and discarded.
Node.js
import { GoogleAuth } from 'google-auth-library';
const auth = new GoogleAuth({
// Load key from your secrets manager — never hardcode
credentials: JSON.parse(process.env.GOOGLE_SERVICE_ACCOUNT_JSON),
scopes: ['https://www.googleapis.com/auth/androidpublisher'],
});
async function getGoogleAccessToken() {
const client = await auth.getClient();
const tokenResponse = await client.getAccessToken();
return tokenResponse.token; // "ya29.xxxx..."
}npm install google-auth-libraryPython
from google.oauth2 import service_account
import google.auth.transport.requests
import json
import os
def get_google_access_token() -> str:
key_data = json.loads(os.environ["GOOGLE_SERVICE_ACCOUNT_JSON"])
credentials = service_account.Credentials.from_service_account_info(
key_data,
scopes=["https://www.googleapis.com/auth/androidpublisher"],
)
request = google.auth.transport.requests.Request()
credentials.refresh(request)
return credentials.token # "ya29.xxxx..."pip install google-authJava
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.AccessToken;
import java.io.ByteArrayInputStream;
import java.util.Collections;
public String getGoogleAccessToken() throws Exception {
String keyJson = System.getenv("GOOGLE_SERVICE_ACCOUNT_JSON");
GoogleCredentials credentials = GoogleCredentials
.fromStream(new ByteArrayInputStream(keyJson.getBytes()))
.createScoped(Collections.singletonList(
"https://www.googleapis.com/auth/androidpublisher"
));
credentials.refreshIfExpired();
return credentials.getAccessToken().getTokenValue(); // "ya29.xxxx..."
}<!-- pom.xml -->
<dependency>
<groupId>com.google.auth</groupId>
<artifactId>google-auth-library-oauth2-http</artifactId>
<version>1.23.0</version>
</dependency>.NET (C#)
using Google.Apis.Auth.OAuth2;
public async Task<string> GetGoogleAccessTokenAsync()
{
var keyJson = Environment.GetEnvironmentVariable("GOOGLE_SERVICE_ACCOUNT_JSON")
?? throw new InvalidOperationException("GOOGLE_SERVICE_ACCOUNT_JSON not set");
var credential = GoogleCredential
.FromJson(keyJson)
.CreateScoped("https://www.googleapis.com/auth/androidpublisher");
var token = await credential.UnderlyingCredential
.GetAccessTokenForRequestAsync();
return token; // "ya29.xxxx..."
}dotnet add package Google.Apis.AuthMission 4 — Call the Verify Endpoint
Once you have the access token, call the Unveily IAP API from your server:
POST https://api.theunveily.com/api/iap/verify
Authorization: Bearer {YOUR_UNVEILY_LICENSE_KEY}
Content-Type: application/json
{
"googleAccessToken": "ya29.xxxx...",
"purchaseToken": "AO-J1OxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxBxxxxxx",
"packageName": "com.example.myapp",
"productId": "premium_monthly",
"productType": "subs",
"platform": "android"
}| Field | Required | Description |
|---|---|---|
Authorization header | Yes | Bearer {YOUR_UNVEILY_LICENSE_KEY} |
googleAccessToken | Yes | Short-lived OAuth2 token from your service account |
purchaseToken | Yes | Token returned by Google Play Billing on the device |
packageName | Yes | Your app's package name (e.g. com.example.myapp) |
productId | Yes | Product ID as registered in Play Console |
productType | Yes | inapp or subs |
platform | No | Defaults to android |
Successful response
{
"success": true,
"data": {
"success": true,
"platform": "android",
"productId": "premium_monthly",
"orderId": "GPA.1234-5678-9012-34567",
"purchaseTime": "2025-05-01T09:23:11Z",
"purchaseState": 0,
"isAcknowledged": false,
"storeApiVerified": true
}
}Error responses
| HTTP | Error | Action |
|---|---|---|
401 | Missing or malformed Authorization header | Check that Bearer {licenseKey} is set correctly |
400 | googleAccessToken missing | Generate a token before calling |
400 | Play API verification failed | Token may be expired or service account lacks Play Console permission |
400 | Already verified receipt | This purchaseToken was already processed — safe to ignore |
400 | Invalid license key or plan | Check your license key and subscription status in the dashboard |
iOS / App Store variant
iOS needs no Google service account and no googleAccessToken. Forward the Apple-signed JWS (signedTransaction) that StoreKit 2 returns, and the Unveily server verifies the signature against Apple's public keys. Set platform to ios.
POST https://api.theunveily.com/api/iap/verify
Authorization: Bearer {YOUR_UNVEILY_LICENSE_KEY}
Content-Type: application/json
{
"platform": "ios",
"signedTransaction": "eyJhbGciOiJFUzI1NiIsIng1YyI6...",
"productId": "premium_monthly",
"productType": "subs"
}| Field | Required | Description |
|---|---|---|
Authorization header | Yes | Bearer {YOUR_UNVEILY_LICENSE_KEY} |
platform | Yes | Must be ios |
signedTransaction | Yes | Apple-signed JWS returned by StoreKit 2 (replaces purchaseToken + googleAccessToken) |
productId | Yes | Product ID as registered in App Store Connect |
productType | Yes | inapp or subs |
No googleAccessToken or packageName on iOS
The JWS itself carries a signed bundle ID and product information, so iOS requests omit googleAccessToken and packageName. Unveily verifies the signature directly against Apple's public keys.
iOS successful response
{
"success": true,
"data": {
"success": true,
"platform": "ios",
"productId": "premium_monthly",
"transactionId": "2000000012345678",
"purchaseTime": "2025-05-01T09:23:11Z",
"storeApiVerified": true
}
}Mission 5 — Complete the Full Flow
// Example: Node.js / Express
import axios from 'axios';
import { getGoogleAccessToken } from './googleAuth'; // your token helper
app.post('/purchase/verify', async (req, res) => {
const { purchaseToken, productId, productType } = req.body;
// 1. Get a fresh Google Access Token
const googleAccessToken = await getGoogleAccessToken();
// 2. Call Unveily IAP API
const response = await axios.post(
'https://api.theunveily.com/api/iap/verify',
{
googleAccessToken,
purchaseToken,
packageName: 'com.example.myapp',
productId,
productType,
platform: 'android',
},
{
headers: {
Authorization: `Bearer ${process.env.UNVEILY_LICENSE_KEY}`,
'Content-Type': 'application/json',
},
}
);
if (response.data.success) {
// 3. Activate the purchased feature for the user
await activateFeature(req.user.id, productId);
res.json({ success: true });
} else {
res.status(400).json({ success: false, error: response.data.error });
}
});Final Check
- Service account JSON key stored in secrets manager (not in code or
.env) - Service account linked to Play Console with View financial data + Manage orders only
- Access token generated server-side immediately before each verify call
-
Authorization: Bearer {licenseKey}header included in every request -
purchaseTokensent from device to your server over HTTPS - Response
storeApiVerified: trueconfirms Google Play validation succeeded - Duplicate receipts (
400 Already verified) handled gracefully
What's Next
- IAP Bridge API — SDK-based IAP for Pro plan users
- License Setup — Configure your license key