Skip to main content

IAP Gateway — iOS

IAP Gateway is designed for co-published games that run their own game server and want full control over item delivery. Alogame only handles receipt verification with Apple — your game server decides when and how to grant items to the player.

APIs on this page:

When to use IAP Gateway instead of Alogame SDK IAP?
Alogame SDK IAP (standard)IAP Gateway
iOS clientOEGPayment.purchaseAndVerify()StoreKit directly
Receipt verificationAlogame Backend calls AppleAlogame Backend (triggered by your game server)
Item deliveryAlogame calls webhook on your serverYour game server handles it after receiving the result
Best forGames using the full Alogame SDKCo-pub games with their own backend that want full control

Before you start

Contact Alogame to receive:

  • client_id and client_secret — used by your game server to authenticate with Alogame IAP Gateway
  • Endpoints:
    • Development / Sandbox: https://api-sdk.dev.alogame.vn/iap/gateway/verify
    • Production: https://api-sdk.alogame.vn/iap/gateway/verify
warning

client_secret is issued only once. Save it securely immediately — it cannot be retrieved again.


How it works


Step 1 — iOS app retrieves the receipt

import StoreKit

func purchase(productId: String) async throws -> String {
guard let product = try await Product.products(for: [productId]).first else {
throw PurchaseError.productNotFound
}

let result = try await product.purchase()

switch result {
case .success(let verification):
switch verification {
case .verified(let transaction):
let receipt = transaction.jwsRepresentation
await transaction.finish()
return receipt

case .unverified:
throw PurchaseError.unverified
}

case .userCancelled:
throw PurchaseError.cancelled

case .pending:
throw PurchaseError.pending

@unknown default:
throw PurchaseError.unknown
}
}
note

jwsRepresentation returns a JWS token (three dot-separated parts). Send this string in the receipt field when calling Alogame Gateway — Alogame will detect SK2 automatically and verify it with Apple.

StoreKit 1 (SK1 — iOS 13+)

If your game is not yet on SK2, use appStoreReceiptURL:

import StoreKit

class PurchaseManager: NSObject, SKPaymentTransactionObserver {

func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for tx in transactions where tx.transactionState == .purchased {
if let receiptURL = Bundle.main.appStoreReceiptURL,
let receiptData = try? Data(contentsOf: receiptURL) {
let receipt = receiptData.base64EncodedString()
sendToGameServer(receipt: receipt, productId: tx.payment.productIdentifier)
}
queue.finishTransaction(tx)
}
}
}
SK1 and SK2 use the same field

Whether you use SK1 or SK2, pass the receipt string in the receipt field when calling Alogame Gateway. Alogame automatically detects the type and verifies accordingly.


Step 2 — App sends receipt to your game server

This is an internal API between your iOS app and your game server — design it however you like. Example:

struct VerifyRequest: Encodable {
let receipt: String
let productId: String
let playerId: String
let serverId: String
}

func sendToGameServer(receipt: String, productId: String) async throws {
let body = VerifyRequest(
receipt: receipt,
productId: productId,
playerId: currentUser.id,
serverId: currentServer.id
)

var req = URLRequest(url: URL(string: "https://your-game-server.com/iap/verify")!)
req.httpMethod = "POST"
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.httpBody = try JSONEncoder().encode(body)

let (data, _) = try await URLSession.shared.data(for: req)
// Handle response from your game server
}

Step 3 — Game server calls Alogame IAP Gateway

Your game server uses the client_id and client_secret received from OEG.

Endpoint

POST https://api-sdk.dev.alogame.vn/iap/gateway/verify   (development)
POST https://api-sdk.alogame.vn/iap/gateway/verify (production)

Request signing

Every request must include three authentication headers signed with HMAC-SHA256:

X-Client-ID: <client_id>
X-Timestamp: <unix_timestamp_seconds>
X-Signature: <hmac_hex>

Signature algorithm:

body_hash = SHA256(raw_request_body_bytes)   # hex string
message = "{client_id}.{timestamp}.{body_hash}"
signature = HMAC-SHA256(client_secret, message) # hex string

Replay protection: Alogame rejects requests where |server_time − X-Timestamp| > 300 seconds (5-minute window).

Node.js example:

import { createHmac, createHash } from 'crypto';

const body = JSON.stringify({
receipt: '<jws_or_sk1_string>',
product_id: 'gems100',
os_id: 2, // 2 = iOS, 1 = Android
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
receiptstringYesJWS token (SK2) or base64 blob (SK1) from StoreKit
product_idstringYesProduct ID (e.g. gems100) — Alogame cross-validates this against the Apple receipt; a mismatch returns 422 PRODUCT_MISMATCH
os_idintegerYes2 = iOS, 1 = Android
player_idstringNoPlayer ID for your records
character_idstringNoCharacter ID
server_idstringNoGame server ID
game_versionstringNoApp version string

Response

Success

{
"success": true,
"transaction_id": "2000000100000000",
"product_id": "gems100",
"receipt_info": {
"platform": "ios",
"environment": "Sandbox",
"quantity": 1,
"purchase_date_ms": 1716389123000,
"original_transaction_id": "2000000100000000",
"type": "Consumable",
"storefront": "VNM",
"price": 10000,
"currency": "VND",
"in_app_ownership_type": "PURCHASED"
}
}

receipt_info fields (iOS)

FieldTypeDescription
platformstringAlways "ios"
environmentstring"Sandbox" or "Production"
quantityintegerNumber of units purchased
purchase_date_msintegerPurchase timestamp in milliseconds (Unix epoch)
original_transaction_idstringOriginal transaction ID — stable across renewals / restores
typestring"Consumable", "Non-Consumable", "Auto-Renewable Subscription", or "Non-Renewing Subscription"
storefrontstringISO 3166-1 alpha-3 country code of the App Store storefront (e.g. "VNM")
priceintegerPrice in milliunits of currency (e.g. 10000 = 10 VND)
currencystringISO 4217 currency code (e.g. "VND")
in_app_ownership_typestring"PURCHASED" or "FAMILY_SHARED"
SK2 vs SK1

receipt_info is fully populated for SK2 JWS receipts. For SK1 base64 receipts, only environment, quantity, purchase_date_ms, and original_transaction_id are populated — type, storefront, price, currency, and in_app_ownership_type are null (SK1 does not expose these fields).

Error

{ "success": false, "error": "ERROR_CODE", "message": "human-readable detail" }
CaseHTTPerror
Verification successful200
Receipt already verified409DUPLICATE_TRANSACTION
product_id in request doesn't match Apple receipt422PRODUCT_MISMATCH
Sandbox receipt sent to production (or vice-versa)412ENVIRONMENT_MISMATCH
Receipt rejected by Apple422VERIFICATION_FAILED
Apple 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 and call transaction.finish().
  • 4xx → request was wrong (bad fields, invalid os_id, auth failure). Fix the request; do not retry blindly.
  • 409 DUPLICATE_TRANSACTION → Alogame already verified this receipt (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 in the Apple receipt. This is a sign of a receipt swap attack or a client bug — do not grant the item.
  • 412 ENVIRONMENT_MISMATCH → the receipt is from the wrong environment (a Sandbox/TestFlight receipt hitting the production verifier, or vice-versa). Returned with its own HTTP status (distinct from 422 VERIFICATION_FAILED) so you can branch on the status code alone and spot QA/test devices pointing at the wrong build. Do not grant the item; rebuild against the correct environment. If a game is approved to use Sandbox receipts in production, Alogame enables allow_sandbox for its client and these are accepted instead.
  • 422 VERIFICATION_FAILED → Apple rejected the receipt. Do not grant the item.
  • 5xx → upstream or server error. Safe to retry with exponential back-off.

Refunds

If Apple refunds a purchase after your game server already granted the item, Alogame notifies your server so you can claw it back. This is the reverse direction of Step 3 — same HMAC identity (client_id/client_secret), but Alogame is the sender this time.

Configure your callback URL

Set refund_callback_url for your client_id in the Alogame Console (IAP Gateway Clients panel), or ask Alogame to set it for you. If it's not set, refunds are still recorded on Alogame's side (visible to Alogame support) but never delivered to your server — you won't be notified automatically.

Optional, but recommended

Without a configured callback, a refunded purchase's item stays granted on your side indefinitely unless you separately reconcile against Apple's own refund reports.

What Alogame sends

POST <your refund_callback_url>

Signed with the same HMAC-SHA256 scheme as Step 3, just computed by Alogame using your client_secret:

X-Client-ID: <your client_id>
X-Timestamp: <unix_timestamp_seconds>
X-Signature: <hmac_hex>

Verify it the same way the Node.js example in Step 3 signs a request — recompute the signature server-side and compare:

import { createHmac, createHash, timingSafeEqual } from 'crypto';

function verifyAlogameSignature(req) {
const { 'x-client-id': clientId, 'x-timestamp': timestamp, 'x-signature': signature } = req.headers;
if (clientId !== CLIENT_ID) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; // same 5-min replay window

const bodyHash = createHash('sha256').update(req.rawBody).digest('hex');
const message = `${clientId}.${timestamp}.${bodyHash}`;
const expected = createHmac('sha256', CLIENT_SECRET).update(message).digest('hex');

return timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expected, 'hex'));
}

Request body

{
"transaction_id": "2000000100000000",
"product_id": "gems100",
"revocation_date": 1736510400000,
"revocation_reason": 0,
"game_id": 42
}
FieldTypeDescription
transaction_idstringSame transaction_id your server received back in Step 3's success response
product_idstringThe refunded product
revocation_dateinteger | nullEpoch ms — when Apple revoked the transaction
revocation_reasoninteger | nullApple's revocation reason code (0 = other/unspecified, 1 = app issue)
game_idintegerYour game's id, for servers that host multiple games

Your response

Return any 2xx to acknowledge. Alogame sends this once, on a best-effort basis — there is no automatic retry queue. If your endpoint is down or errors, Alogame support can retry manually from the Console once your server is back up, but nothing retries on its own, so treat a non-2xx/timeout on your side as something to alert on, not something Alogame will paper over.

What to do on receipt

Look up transaction_id in your own records and revoke whatever was granted — currency, items, entitlements. This callback fires regardless of whether the original purchase went through IAP Gateway's /verify or was recorded some other way, as long as it's the same game_id.


Looking up an order directly (support tool)

For when your own support team only has the human-readable Apple Order ID a player can see (e.g. "MTZ4Z8YFS7" from Report a Problem or order emails) — not a receipt blob, not a transaction_id. This asks Apple directly whether the order is genuine, then tells you whether Alogame's gateway ever received it via /verify.

Endpoint

POST https://api-sdk.dev.alogame.vn/iap/gateway/lookup-receipt   (development)
POST https://api-sdk.alogame.vn/iap/gateway/lookup-receipt (production)

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 orders for your own game.

Request body

{ "order_id": "MTZ4Z8YFS7" }

Response

{
"order_id": "MTZ4Z8YFS7",
"game_id": 42,
"found": true,
"environment": "Production",
"transactions": [
{
"transaction_id": "2000000100000000",
"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 Apple doesn't recognize the order at all (found: false), or if it's genuine but was never received by your /verify call (gateway.found: false, reason: "NEVER_RECEIVED_BY_GATEWAY"), that tells you where to look next — either the order really doesn't exist, or your game client/server never actually called Alogame for it.

This lookup never fails a receipt

gateway.found: false is informational, not a verification failure — Apple 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 (or arrived malformed), which can be entirely normal (the game didn't route this purchase through the Gateway, or hit an issue before the call went out). This endpoint doesn't verify anything itself, so nothing here is ever a "failed" result.

gateway.reason (when gateway.found is false)Meaning
NEVER_RECEIVED_BY_GATEWAYApple confirms the purchase happened, but no /verify call for it was ever recorded — check whether your client/server actually sent it
DECODE_FAILEDApple returned this transaction but Alogame couldn't decode its signed data — treat as inconclusive, not as proof the order is fake

Integration checklist

  • Received client_id and client_secret from Alogame — stored securely server-side
  • iOS: app retrieves the correct receipt type (SK2 jwsRepresentation or SK1 base64)
  • iOS: calls transaction.finish() only after the server confirms success
  • Game server: signs every request with HMAC-SHA256 (see Step 3)
  • 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" — receipt was rejected by Apple
  • Game server: treats 412 ENVIRONMENT_MISMATCH as "wrong build/environment" — do not grant; flag the device as pointing at the wrong (Sandbox vs Production) environment
  • Game server: retries with exponential back-off on 5xx (VERIFIER_ERROR)
  • Game server: stores transaction_id for idempotent delivery — prevents double-granting
  • (Optional) Set refund_callback_url for your client_id and implement the endpoint — verifies the same HMAC signature scheme, revokes the granted item on receipt
  • (Optional) Support team knows about /iap/gateway/lookup-receipt for investigating a player-reported Order ID directly