Skip to main content

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:

No refund callback for Android

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")
Already using the Alogame SDK?

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())
}
}
}
}
note

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​

FieldTypeRequiredDescription
receiptstringYespurchaseToken from Google Play Billing
product_idstringYesProduct ID (e.g. gems100) — required as the lookup key for the Google Play Developer API; Alogame cross-validates this against the Google response
os_idintegerYes1 = Android, 2 = iOS
packagestringYesAndroid package name (e.g. com.yourcompany.yourgame)
player_idstringNoPlayer ID for your records
character_idstringNoCharacter ID
server_idstringNoGame server ID
game_versionstringNoApp version string
package is required for Android

Alogame 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)​

FieldTypeDescription
platformstringAlways "android"
environmentnullAlways null — Google does not distinguish sandbox/production in this API
quantityintegerNumber of units purchased
purchase_date_msintegerPurchase timestamp in milliseconds (Unix epoch)
purchase_stateinteger0 = PURCHASED, 1 = CANCELED, 2 = PENDING
region_codestringISO 3166-1 alpha-2 country code where the purchase was made (e.g. "VN")
acknowledgement_stateinteger0 = not acknowledged, 1 = acknowledged
order_idstringGoogle Play order ID — same as transaction_id

Error​

{ "success": false, "error": "ERROR_CODE", "message": "human-readable detail" }
CaseHTTPerror
Verification successful200—
Receipt already verified409DUPLICATE_TRANSACTION
product_id in request doesn't match Google receipt422PRODUCT_MISMATCH
Receipt rejected by Google422VERIFICATION_FAILED
Google server unreachable / unexpected error502VERIFIER_ERROR
Missing required fields400MISSING_FIELDS
os_id value not 1 or 2400INVALID_OS_ID
Invalid credentials or signature401UNAUTHORIZED
How to handle errors by category
  • success: true → grant the item. Consumable items should then be consumed via BillingClient.consumeAsync() to allow re-purchase.
  • 4xx → request was wrong (bad fields, missing package, 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 → the product_id you 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.

This is not a way to "recover" a lost purchaseToken

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_GATEWAYGoogle confirms the purchase happened, but no /verify call for it was ever recorded — check whether your client/server actually sent it
This lookup never fails a purchase

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_id and client_secret from 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 purchaseToken from Purchase.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 package field (Android package name) in every request
  • Game server: checks success field — not just HTTP status — before granting item
  • Game server: treats 409 DUPLICATE_TRANSACTION as "already verified" — checks local state, does not re-grant
  • Game server: treats 422 VERIFICATION_FAILED as "do not grant" — purchase was rejected by Google
  • Game server: retries with exponential back-off on 5xx (VERIFIER_ERROR)
  • Game server: stores transaction_id (= Google order_id) for idempotent delivery — prevents double-granting
  • Game server: persists purchaseToken until the item is confirmed granted — it's the only key /lookup-receipt can use for Android (no Apple-style Order ID lookup)
  • (Optional) Support team knows about /iap/gateway/lookup-receipt for re-confirming a disputed purchase directly against Google Play