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:
| Field | Type | Required | Description |
|---|---|---|---|
uid | string | Yes | User ID (MongoDB ObjectId as string) |
pid | string | Yes | Plan ID (MongoDB ObjectId as string) |
receipt | string | Yes | Base64-encoded App Store receipt data |
status | string | No | Purchase status: PAID, FAILED, PENDING (default: PAID) |
sandbox | boolean | No | Whether 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:
| Field | Type | Description |
|---|---|---|
status | string | Response status: success or error |
message | string | Human-readable message |
success | boolean | Request success indicator |
data | object | Response data (see below) |
data.points | number | Number of points credited to user |
data.isDuplicate | boolean | Whether this was a duplicate purchase |
data.transactionId | string/null | Original transaction ID from Apple |
data.country | string | Country of the user's wallet |
data.purchaseStatus | string | Final 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:
| Field | Type | Required | Description |
|---|---|---|---|
receipt | string | Yes | Base64-encoded App Store receipt data |
sandbox | boolean | No | Whether 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:
- Go to App Store Connect
- Select your app
- Go to "In-App Purchases"
- Click "App-Specific Shared Secret"
- 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:
- Accept App Store Server Notifications
- Verify signature using Apple's public certificates
- 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 Code | Error | Cause | Solution |
|---|---|---|---|
| 400 | uid (User ID) is required | Missing user ID | Pass uid in request body |
| 400 | pid (Plan ID) is required | Missing plan ID | Pass pid in request body |
| 400 | receipt is required | Missing receipt | Pass receipt in request body |
| 400 | Invalid status | Invalid purchase status | Use PAID, FAILED, or PENDING |
| 400 | Receipt verification failed | Apple rejects receipt | Verify receipt is valid and not expired |
| 404 | User not found | User ID doesn't exist | Ensure user is registered before purchase |
| 404 | Plan not found | Plan ID doesn't exist | Verify plan exists in database |
| 500 | iOS App Shared Secret not configured | Environment variable missing | Add 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:
- When a PAID purchase is recorded, the original transaction ID is extracted from the receipt
- The transaction ID is stored in the user's
iosProcessedReceiptsarray - If the same transaction ID is submitted again, the system returns a successful response with
isDuplicate: true - 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:
- Set
sandbox: truein your request - Use a sandbox receipt from your testing
- The system will validate against Apple's sandbox servers
- 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
}'
Related Documentation
- Plans API - Plan management
- Wallet API - Wallet and point system
- Ledger API - Transaction history
- Payments API - Payment processing overview
- Subscription Flow - Razorpay subscription details