IAP Gateway — Android
This page covers the Android-specific steps only. For what IAP Gateway is, the standard flow, HMAC authentication, common errors and the shared checklist, see the IAP Gateway overview.
Android-specific APIs/sections on this page:
- Step 1 — retrieving the purchase token from Billing
POST /iap/gateway/verify— full request/response example for AndroidPOST /iap/gateway/lookup-receipt— re-check a purchase token directly against Google Play (support tool)
Unlike iOS, Alogame does not send a refund callback for Android purchases — Google Play has no Apple-equivalent server-to-server refund notification wired into this Gateway today. Reconcile refunds/chargebacks against Google Play's own Voided Purchases reports in the meantime.
Step 1 — Android app retrieves the purchase token
Use the Google Play Billing Library to initiate a purchase and retrieve the purchaseToken:
// build.gradle (app)
implementation("com.android.billingclient:billing:8.0.0")
It exposes billing-ktx:8.0.0 as an api dependency, so Billing is already on your classpath and this line is redundant. Declaring an older version has no effect either — Gradle resolves to the highest version in the graph.
import com.android.billingclient.api.*
class PurchaseManager(private val activity: Activity) : PurchasesUpdatedListener {
private var billingClient: BillingClient = BillingClient.newBuilder(activity)
.setListener(this)
.enablePendingPurchases(
PendingPurchasesParams.newBuilder().enableOneTimeProducts().build()
)
.build()
fun launchPurchaseFlow(productId: String) {
val productDetails = ... // obtained via queryProductDetailsAsync()
val productDetailsParams = BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
.build()
val params = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(listOf(productDetailsParams))
.build()
billingClient.launchBillingFlow(activity, params)
}
override fun onPurchasesUpdated(result: BillingResult, purchases: List<Purchase>?) {
if (result.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
for (purchase in purchases) {
val purchaseToken = purchase.purchaseToken
// Send purchaseToken to your game server
sendToGameServer(purchaseToken, purchase.products.first())
}
}
}
}
purchaseToken is the receipt you pass to Alogame Gateway. It is a long opaque string issued by Google Play — do not parse or truncate it.
Step 2 — App sends purchase token to your game server
This is an internal API between your Android app and your game server — design it however you like. Example:
data class VerifyRequest(
val purchaseToken: String,
val productId: String,
val playerId: String,
val serverId: String
)
suspend fun sendToGameServer(purchaseToken: String, productId: String) {
val body = VerifyRequest(
purchaseToken = purchaseToken,
productId = productId,
playerId = currentUser.id,
serverId = currentServer.id
)
// POST to your game server — your server calls OEG IAP Gateway
}
Step 3 — Game server calls Alogame IAP Gateway
Your game server uses the client_id and client_secret received from OEG.
Endpoint
POST {base_url}/iap/gateway/verify
See the Base URL table for the {base_url} value per environment.
Request signing
Signed the same way for every platform — see Authentication on the overview page for the header list, signature algorithm and replay-protection window.
Node.js example (Android request body):
import { createHmac, createHash } from 'crypto';
const body = JSON.stringify({
receipt: '<purchaseToken from Google Play>',
product_id: 'gems100',
os_id: 1, // 1 = Android, 2 = iOS
package: 'com.yourcompany.yourgame',
player_id: 'player-001',
character_id: 'char-001',
server_id: 'server-1',
game_version: '1.0.0',
});
const timestamp = String(Math.floor(Date.now() / 1000));
const bodyHash = createHash('sha256').update(body).digest('hex');
const message = `${CLIENT_ID}.${timestamp}.${bodyHash}`;
const signature = createHmac('sha256', CLIENT_SECRET).update(message).digest('hex');
const res = await fetch('https://api-sdk.alogame.vn/iap/gateway/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Client-ID': CLIENT_ID,
'X-Timestamp': timestamp,
'X-Signature': signature,
},
body,
});
Request body fields
| Field | Type | Required | Description |
|---|---|---|---|
receipt | string | Yes | purchaseToken from Google Play Billing |
product_id | string | Yes | Product ID (e.g. gems100) — required as the lookup key for the Google Play Developer API; Alogame cross-validates this against the Google response |
os_id | integer | Yes | 1 = Android, 2 = iOS |
package | string | Yes | Android package name (e.g. com.yourcompany.yourgame) |
player_id | string | No | Player ID for your records |
character_id | string | No | Character ID |
server_id | string | No | Game server ID |
game_version | string | No | App version string |
package is required for AndroidAlogame passes package to the Google Play Developer API to locate the purchase. Requests without a package field will be rejected.
Response
Success
{
"success": true,
"transaction_id": "GPA.1234-5678-9012-34567",
"product_id": "gems100",
"receipt_info": {
"platform": "android",
"environment": null,
"quantity": 1,
"purchase_date_ms": 1716389123000,
"purchase_state": 0,
"region_code": "VN",
"acknowledgement_state": 0,
"order_id": "GPA.1234-5678-9012-34567"
}
}
receipt_info fields (Android)
| Field | Type | Description |
|---|---|---|
platform | string | Always "android" |
environment | null | Always null — Google does not distinguish sandbox/production in this API |
quantity | integer | Number of units purchased |
purchase_date_ms | integer | Purchase timestamp in milliseconds (Unix epoch) |
purchase_state | integer | 0 = PURCHASED, 1 = CANCELED, 2 = PENDING |
region_code | string | ISO 3166-1 alpha-2 country code where the purchase was made (e.g. "VN") |
acknowledgement_state | integer | 0 = not acknowledged, 1 = acknowledged |
order_id | string | Google Play order ID — same as transaction_id |
Error
{ "success": false, "error": "ERROR_CODE", "message": "human-readable detail" }
| Case | HTTP | error |
|---|---|---|
| Verification successful | 200 | — |
| Receipt already verified | 409 | DUPLICATE_TRANSACTION |
product_id in request doesn't match Google receipt | 422 | PRODUCT_MISMATCH |
| Receipt rejected by Google | 422 | VERIFICATION_FAILED |
| Google server unreachable / unexpected error | 502 | VERIFIER_ERROR |
| Missing required fields | 400 | MISSING_FIELDS |
os_id value not 1 or 2 | 400 | INVALID_OS_ID |
| Invalid credentials or signature | 401 | UNAUTHORIZED |
success: true→ grant the item. Consumable items should then be consumed viaBillingClient.consumeAsync()to allow re-purchase.4xx→ request was wrong (bad fields, missingpackage, auth failure). Fix the request; do not retry blindly.409 DUPLICATE_TRANSACTION→ Alogame already verified this purchase token (common on retry after crash). Check if the item was already granted; do not re-grant.422 PRODUCT_MISMATCH→ theproduct_idyou sent does not match the product recorded by Google. This is a sign of a receipt swap attack or a client bug — do not grant the item.422 VERIFICATION_FAILED→ Google rejected the purchase token (purchaseState != 0, package mismatch, SA not configured). Do not grant the item.5xx→ upstream or server error. Safe to retry with exponential back-off.
Looking up a purchase directly (support tool)
For when your support team needs to re-confirm a purchase directly against Google Play — e.g. a player disputes whether an item was granted, or you suspect a /verify call never landed. Unlike iOS, Google Play's Developer API has no lookup by a bare order ID — purchases.products.get always requires the same product_id + package + purchase token that /verify itself needs. So this endpoint takes the same inputs, re-runs the read-only Google Play check, and additionally tells you whether Alogame's gateway ever recorded it via /verify.
If you never captured the purchaseToken (Step 1) or it's been consumed/lost, this endpoint cannot help — there is no Apple-style "Order ID" you can hand over instead. Always persist purchaseToken on your side until your server confirms the item was granted.
Endpoint
POST {base_url}/iap/gateway/lookup-receipt
See the Base URL table for the {base_url} value per environment.
Same auth as Step 3 — HMAC-SHA256 with your client_id/client_secret, X-Client-ID/X-Timestamp/X-Signature headers. game_id is your gateway client's own game — there is no game_id field in the request body; you can only look up purchases for your own game.
Request body
{
"os_id": 1,
"product_id": "gems100",
"package": "com.yourcompany.yourgame",
"receipt": "<purchaseToken from Google Play>"
}
os_id must be 1 — omitting it defaults to iOS's order_id-based contract, which will reject this body as missing fields.
Response
{
"order_id": "GPA.1234-5678-9012-34567",
"game_id": 42,
"found": true,
"environment": null,
"transactions": [
{
"platform": "android",
"environment": null,
"quantity": 1,
"purchase_date_ms": 1716389123000,
"purchase_state": 0,
"region_code": "VN",
"acknowledgement_state": 0,
"order_id": "GPA.1234-5678-9012-34567",
"transaction_id": "GPA.1234-5678-9012-34567",
"product_id": "gems100",
"gateway": {
"found": true,
"tx_mgmt_id": 991,
"current_state": "gateway_verified",
"last_error_code": null,
"last_error_message": null,
"recorded_for_this_game": true
}
}
]
}
If Google rejects the purchase token (bad token, wrong package, not purchased), the response looks like a normal /verify failure instead — found: false plus the same error/status Google returned:
{ "order_id": null, "game_id": 42, "found": false, "status": 1, "error": "Invalid purchase state: 1" }
When Google confirms the purchase but Alogame never recorded a matching /verify call, gateway.found is false:
gateway.reason (when gateway.found is false) | Meaning |
|---|---|
NEVER_RECEIVED_BY_GATEWAY | Google confirms the purchase happened, but no /verify call for it was ever recorded — check whether your client/server actually sent it |
gateway.found: false is informational, not a verification failure — Google has already confirmed the purchase is real by the time you see it. It only means the /verify call for this transaction never reached Alogame, which can be entirely normal (the game didn't route this purchase through the Gateway, or hit an issue before the call went out).
Integration checklist
- Received
client_idandclient_secretfrom Alogame — stored securely server-side - Alogame configured with Google service account credentials for this game (required to call Google Play Developer API)
- Android: app retrieves
purchaseTokenfromPurchase.getPurchaseToken() - Android: consumable items consumed via
consumeAsync()after server confirms success - Game server: signs every request with HMAC-SHA256 (see Step 3)
- Game server: includes
packagefield (Android package name) in every request - Game server: checks
successfield — not just HTTP status — before granting item - Game server: treats
409 DUPLICATE_TRANSACTIONas "already verified" — checks local state, does not re-grant - Game server: treats
422 VERIFICATION_FAILEDas "do not grant" — purchase was rejected by Google - Game server: retries with exponential back-off on
5xx(VERIFIER_ERROR) - Game server: stores
transaction_id(= Googleorder_id) for idempotent delivery — prevents double-granting - Game server: persists
purchaseTokenuntil the item is confirmed granted — it's the only key/lookup-receiptcan use for Android (no Apple-style Order ID lookup) - (Optional) Support team knows about
/iap/gateway/lookup-receiptfor re-confirming a disputed purchase directly against Google Play