Skip to main content

iOS In-App Purchase (IAP) Integration

Complete documentation for iOS in-app purchase handling in Zishes.

Overview

This API allows iOS apps to record and process in-app purchases, automatically crediting users with points based on their plan selection. The system handles receipt verification with Apple's App Store and prevents duplicate point crediting through idempotency tracking.

Key Features

  • Receipt Verification: Validates receipts with Apple's App Store servers
  • Sandbox Support: Automatically handles both sandbox and production receipts
  • Duplicate Prevention: Tracks processed transactions to prevent duplicate point crediting
  • Country-Specific Wallets: Credits points to the correct wallet based on user's country
  • Unified Response Format: Follows the standard Zishes API response format
  • Notifications: Sends purchase success notifications to users
  • Comprehensive Logging: Stores ledger entries for all transactions

API Endpoints

1. Record iOS Purchase

Endpoint: POST /api/v1/public/iap/ios

Description: Records an iOS in-app purchase and credits points to the user's wallet.

Request Body:

{
"uid": "user_id",
"pid": "plan_id",
"receipt": "base64_encoded_receipt",
"status": "PAID",
"sandbox": false
}

Request Fields:

FieldTypeRequiredDescription
uidstringYesUser ID (MongoDB ObjectId as string)
pidstringYesPlan ID (MongoDB ObjectId as string)
receiptstringYesBase64-encoded App Store receipt data
statusstringNoPurchase status: PAID, FAILED, PENDING (default: PAID)
sandboxbooleanNoWhether receipt is from sandbox environment (default: false)

Success Response (HTTP 200):

{
"status": "success",
"message": "Purchase processed successfully",
"success": true,
"data": {
"points": 500,
"isDuplicate": false,
"transactionId": "150000000000000",
"country": "India",
"purchaseStatus": "PAID"
}
}

Duplicate Purchase Response (HTTP 200):

{
"status": "success",
"message": "This purchase has already been processed",
"success": true,
"data": {
"points": 500,
"isDuplicate": true,
"transactionId": null,
"country": "India",
"purchaseStatus": "PAID"
}
}

Error Response (HTTP 400/500):

{
"status": "error",
"message": "uid (User ID) is required",
"success": false,
"data": null
}

Response Fields:

FieldTypeDescription
statusstringResponse status: success or error
messagestringHuman-readable message
successbooleanRequest success indicator
dataobjectResponse data (see below)
data.pointsnumberNumber of points credited to user
data.isDuplicatebooleanWhether this was a duplicate purchase
data.transactionIdstring/nullOriginal transaction ID from Apple
data.countrystringCountry of the user's wallet
data.purchaseStatusstringFinal purchase status

2. Verify iOS Receipt

Endpoint: POST /api/v1/public/iap/ios/verify

Description: Verifies an iOS App Store receipt without processing a purchase (read-only operation).

Request Body:

{
"receipt": "base64_encoded_receipt",
"sandbox": false
}

Request Fields:

FieldTypeRequiredDescription
receiptstringYesBase64-encoded App Store receipt data
sandboxbooleanNoWhether receipt is from sandbox environment (default: false)

Success Response (HTTP 200):

{
"status": "success",
"message": "Receipt verified successfully",
"success": true,
"data": {
"isValid": true,
"status": 0,
"bundleId": "com.example.app",
"inAppPurchases": [
{
"product_id": "plan_500_points",
"transaction_id": "1000000000000001",
"original_transaction_id": "150000000000000",
"purchase_date_ms": "1634567890000",
"expires_date_ms": "1634654290000"
}
]
}
}

Error Response (HTTP 400):

{
"status": "error",
"message": "Failed to verify receipt",
"success": false,
"data": null
}

Environment Configuration

Add the following to your .env file:

# iOS App Store Configuration
IOS_APP_SHARED_SECRET=your_shared_secret_from_app_store_connect

To find your Shared Secret:

  1. Go to App Store Connect
  2. Select your app
  3. Go to "In-App Purchases"
  4. Click "App-Specific Shared Secret"
  5. Copy the secret and add it to your .env

Data Model Changes

User Model Updates

Added the following fields to track iOS IAP:

lastValidIosReceipt?: string | null;          // Latest valid App Store receipt
iosLastPurchaseDate?: Date | null; // Timestamp of last iOS purchase
iosProcessedReceipts?: string[]; // Array of processed receipt IDs (max 50)

The iosProcessedReceipts array stores transaction IDs to prevent duplicate point crediting. The system automatically maintains only the last 50 receipts to avoid unbounded growth.


Flow Diagram

┌─────────────────────────────────────────────────────────────┐
│ iOS App: User completes in-app purchase via App Store │
└────────────────────┬────────────────────────────────────────┘

├─ Receive receipt from App Store

├─ Call POST /api/v1/public/iap/ios


┌─────────────────────────────────────────────────────────────┐
│ Backend: Receive and validate request │
│ ├─ Validate uid, pid, receipt │
│ └─ Check purchase status │
└────────────────────┬────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ For PAID Status Only: │
│ ├─ Verify receipt with Apple (production/sandbox) │
│ ├─ Extract transaction ID │
│ └─ Check if already processed (idempotency) │
└────────────────────┬────────────────────────────────────────┘

├─ Duplicate? → Return success (isDuplicate: true)

├─ New transaction? → Continue


┌─────────────────────────────────────────────────────────────┐
│ For All Statuses: │
│ ├─ Find user and plan │
│ ├─ Get/Create user's country-specific wallet │
│ └─ Create ledger entry │
└────────────────────┬────────────────────────────────────────┘

├─ PAID → Credit points to wallet
│ + Update user receipt tracking
│ + Send notification

├─ FAILED/PENDING → Log without crediting


┌─────────────────────────────────────────────────────────────┐
│ Return unified response with purchase details │
└─────────────────────────────────────────────────────────────┘

Implementation Example (iOS Client)

Swift/Objective-C Example

import StoreKit

// 1. Get receipt from App Store
guard let receiptURL = Bundle.main.appStoreReceiptURL,
let receiptData = try? Data(contentsOf: receiptURL) else {
print("Receipt not found")
return
}

let receiptString = receiptData.base64EncodedString()

// 2. Call backend endpoint
let url = URL(string: "https://api.zishes.com/api/v1/public/iap/ios")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")

let payload: [String: Any] = [
"uid": userID,
"pid": planID,
"receipt": receiptString,
"status": "PAID",
"sandbox": false // Set to true for sandbox testing
]

request.httpBody = try? JSONSerialization.data(withJSONObject: payload)

let session = URLSession.shared
let task = session.dataTask(with: request) { data, response, error in
if let data = data,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
print("Purchase recorded:", json)

if let dataDict = json["data"] as? [String: Any],
let points = dataDict["points"] as? Int {
print("points credited: \(points)")
}
}
}

task.resume()

Webhook Integration (Optional)

Currently, iOS purchases are processed synchronously through the /iap/ios endpoint. If you want to implement webhook-based processing similar to Razorpay/Stripe, you can extend the system to:

  1. Accept App Store Server Notifications
  2. Verify signature using Apple's public certificates
  3. Process purchase, renewal, and cancellation events

This would follow the same pattern as the existing Razorpay and Stripe webhook handlers.


Error Handling

Common Error Codes

Status CodeErrorCauseSolution
400uid (User ID) is requiredMissing user IDPass uid in request body
400pid (Plan ID) is requiredMissing plan IDPass pid in request body
400receipt is requiredMissing receiptPass receipt in request body
400Invalid statusInvalid purchase statusUse PAID, FAILED, or PENDING
400Receipt verification failedApple rejects receiptVerify receipt is valid and not expired
404User not foundUser ID doesn't existEnsure user is registered before purchase
404Plan not foundPlan ID doesn't existVerify plan exists in database
500iOS App Shared Secret not configuredEnvironment variable missingAdd IOS_APP_SHARED_SECRET to .env

Ledger Entries

Each iOS purchase creates a ledger entry with:

  • Type: TOPUP
  • Amount: Number of points from the plan
  • Country: User's country from their address
  • Note: iOS IAP ios:\{transactionId\} (for PAID purchases)
  • CreatedAt: Timestamp of the transaction

This allows for complete audit trails and financial reconciliation.


Idempotency

The system prevents duplicate point crediting through transaction ID tracking:

  1. When a PAID purchase is recorded, the original transaction ID is extracted from the receipt
  2. The transaction ID is stored in the user's iosProcessedReceipts array
  3. If the same transaction ID is submitted again, the system returns a successful response with isDuplicate: true
  4. No points are credited for duplicate submissions

Maximum tracked receipts: 50 per user (older receipts are automatically removed)


Testing

Test with Sandbox Receipts

For development/testing, Apple provides sandbox receipts. To test:

  1. Set sandbox: true in your request
  2. Use a sandbox receipt from your testing
  3. The system will validate against Apple's sandbox servers
  4. The system automatically detects and handles sandbox receipts

Test Endpoints

# Test successful payment
curl -X POST http://localhost:3000/api/v1/public/iap/ios \
-H "Content-Type: application/json" \
-d '{
"uid": "user123",
"pid": "plan456",
"receipt": "base64_encoded_receipt",
"status": "PAID"
}'

# Test receipt verification
curl -X POST http://localhost:3000/api/v1/public/iap/ios/verify \
-H "Content-Type: application/json" \
-d '{
"receipt": "base64_encoded_receipt",
"sandbox": true
}'