Skip to main content
Unveilydocs

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
RequirementDetail
Unveily planIAP API subscription
Google PlayDeveloper account with billing profile set up
Your infrastructureA 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:

InfrastructureRecommended storage
AWSAWS Secrets Manager or Systems Manager Parameter Store (SecureString)
Google CloudGCP Secret Manager
AzureAzure Key Vault
Self-hosted / On-premisesHashiCorp Vault (recommended) — or OS environment variables as a minimum
Any environmentNever 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

  1. Open Google Cloud Console → select the project linked to your Play account
  2. APIs & ServicesEnable APIs and Services
  3. Search for Google Play Android Developer APIEnable

1-2. Create the service account

  1. IAM & AdminService AccountsCreate Service Account
  2. Enter a name (e.g., unveily-iap-verifier) → Create and Continue
  3. Skip role assignment at this step → Done

1-3. Download the JSON key

  1. Click the service account you just created → Keys tab
  2. Add KeyCreate new keyJSONCreate
  3. 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.


2-1. Grant access

  1. Google Play ConsoleSetupAPI access
  2. Link to your Google Cloud project (if not already linked)
  3. 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.

PermissionRequiredReason
View financial dataYesRead purchase and subscription status
Manage orders and subscriptionsYesRequired to call the purchases API
All other permissionsNoDo not enable
  1. 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-library

Python

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-auth

Java

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

Mission 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"
}
FieldRequiredDescription
Authorization headerYesBearer {YOUR_UNVEILY_LICENSE_KEY}
googleAccessTokenYesShort-lived OAuth2 token from your service account
purchaseTokenYesToken returned by Google Play Billing on the device
packageNameYesYour app's package name (e.g. com.example.myapp)
productIdYesProduct ID as registered in Play Console
productTypeYesinapp or subs
platformNoDefaults 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

HTTPErrorAction
401Missing or malformed Authorization headerCheck that Bearer {licenseKey} is set correctly
400googleAccessToken missingGenerate a token before calling
400Play API verification failedToken may be expired or service account lacks Play Console permission
400Already verified receiptThis purchaseToken was already processed — safe to ignore
400Invalid license key or planCheck 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"
}
FieldRequiredDescription
Authorization headerYesBearer {YOUR_UNVEILY_LICENSE_KEY}
platformYesMust be ios
signedTransactionYesApple-signed JWS returned by StoreKit 2 (replaces purchaseToken + googleAccessToken)
productIdYesProduct ID as registered in App Store Connect
productTypeYesinapp 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
  • purchaseToken sent from device to your server over HTTPS
  • Response storeApiVerified: true confirms Google Play validation succeeded
  • Duplicate receipts (400 Already verified) handled gracefully

What's Next

On this page