apple-receipt-verify
v1.0.1
Published
A modern JavaScript module for Apple In-App Purchase receipt validation.
Maintainers
Readme
apple-receipt-verify
A modern JavaScript module for Apple In-App Purchase receipt validation.
Features
- ✅ Promise-based API - Modern async/await support
- 🔒 Type-safe - Built with Zod schema validation
- 🪶 Lightweight - Minimal dependencies, only
zod - 🚀 Fast - Uses native
fetchandstructuredClone - 📦 ESM & CJS - Supports both module systems
- 🔄 Auto-retry - Handles production/sandbox environment switching
- ⚡ Edge Computing Ready - Works in Cloudflare Workers, Vercel Edge, Deno, Bun, and other modern runtimes
StoreKit Versions
Note: This module works with the original StoreKit API. For StoreKit 2, Apple uses JWT tokens which can be verified without an API call to Apple servers.
Installation
npm install apple-receipt-verify
# or
pnpm add apple-receipt-verify
# or
yarn add apple-receipt-verifyRequirements
- Node.js >= 20
- Or any modern JavaScript runtime with
fetchandstructuredClonesupport:- Cloudflare Workers
- Vercel Edge Functions
- Deno
- Bun
- Modern browsers (for client-side validation)
Quick Start
import * as appleReceiptVerify from 'apple-receipt-verify'
// Initialize with your shared secret
appleReceiptVerify.config({
secret: 'your-apple-shared-secret',
environment: ['production']
})
// Validate a receipt
try {
const products = await appleReceiptVerify.validate({
receipt: 'base64-encoded-receipt-data'
})
console.log('Purchased products:', products)
} catch (error) {
console.error('Validation failed:', error)
}API Reference
config(options)
Initialize or reconfigure the module. Can be called multiple times.
Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| secret | string | - | Required. Apple shared secret from App Store Connect |
| environment | string \| string[] | ['production'] | Validation environments: 'production', 'sandbox', or both |
| verbose | boolean | false | Enable debug logging |
| extended | boolean | false | Include extended purchase information |
| ignoreExpired | boolean | true | Skip expired purchases in results |
| ignoreExpiredError | boolean | false | Don't throw error for expired receipts |
| excludeOldTransactions | boolean | false | Only include latest renewal transaction for subscriptions |
| doNotRemoveNonSubscriptions | boolean | false | Include lifetime purchases in results |
Example
appleReceiptVerify.config({
secret: 'your-shared-secret',
environment: ['production', 'sandbox'], // Try production first, then sandbox
verbose: true,
extended: true
})config()
Returns the current configuration.
const currentConfig = appleReceiptVerify.config()
console.log(currentConfig)validate(options)
Validates an App Store receipt and returns purchased products.
Options
| Option | Type | Required | Description |
|--------|------|----------|-------------|
| receipt | string | ✅ | Base64-encoded receipt data |
| device | string | ❌ | iOS vendor identifier (deprecated) |
You can also override any config() options per request.
Returns
Returns a Promise that resolves to an array of purchased products:
Promise<Array<{
bundleId: string
transactionId: string
productId: string
purchaseDate: number
quantity: number
expirationDate?: number
// Extended fields (when extended: true)
isTrialPeriod?: boolean
isInIntroOfferPeriod?: boolean
environment?: string
originalPurchaseDate?: number
applicationVersion?: string
originalApplicationVersion?: string
}>>Example
const products = await appleReceiptVerify.validate({
receipt: 'base64-encoded-receipt-data',
extended: true
})
console.log(products)
// [
// {
// bundleId: 'com.example.app',
// transactionId: '1000000123456789',
// productId: 'com.example.product.monthly',
// purchaseDate: 1609459200000,
// quantity: 1,
// expirationDate: 1612137600000,
// isTrialPeriod: false,
// isInIntroOfferPeriod: false,
// environment: 'Production',
// originalPurchaseDate: 1609459200000,
// applicationVersion: '1.0',
// originalApplicationVersion: '1.0'
// }
// ]Usage Examples
Basic Validation
import * as appleReceiptVerify from 'apple-receipt-verify'
// Initialize once
appleReceiptVerify.config({
secret: 'your-shared-secret'
})
// Validate receipt
try {
const products = await appleReceiptVerify.validate({
receipt: receiptData
})
if (products.length > 0) {
console.log('Valid purchase:', products[0].productId)
}
} catch (error) {
console.error('Invalid receipt:', error.message)
}Sandbox Testing
appleReceiptVerify.config({
secret: 'your-shared-secret',
environment: ['sandbox']
})
const products = await appleReceiptVerify.validate({
receipt: testReceiptData
})Auto-Retry (Production → Sandbox)
// Try production first, fallback to sandbox if needed
appleReceiptVerify.config({
secret: 'your-shared-secret',
environment: ['production', 'sandbox']
})
const products = await appleReceiptVerify.validate({
receipt: receiptData
})Override Configuration Per Request
// Global config
appleReceiptVerify.config({
secret: 'your-shared-secret',
environment: ['production']
})
// Override for specific request
const products = await appleReceiptVerify.validate({
receipt: receiptData,
environment: ['sandbox'], // Override to sandbox
extended: true // Override to get extended info
})Handle Expired Subscriptions
appleReceiptVerify.config({
secret: 'your-shared-secret',
ignoreExpiredError: true // Don't throw error for expired receipts
})
try {
const products = await appleReceiptVerify.validate({
receipt: receiptData
})
// Products may be empty if all subscriptions expired
} catch (error) {
console.error('Validation error:', error)
}Extended Purchase Information
const products = await appleReceiptVerify.validate({
receipt: receiptData,
extended: true
})
products.forEach(product => {
console.log('Product:', product.productId)
console.log('Is trial?', product.isTrialPeriod)
console.log('Is intro offer?', product.isInIntroOfferPeriod)
console.log('Environment:', product.environment)
})Error Handling
Error Types
The module exports two custom error classes:
EmptyError
Thrown when the receipt is valid but contains no purchases.
import { EmptyError } from 'apple-receipt-verify'
try {
const products = await appleReceiptVerify.validate({ receipt })
} catch (error) {
if (error instanceof EmptyError) {
console.log('Receipt is valid but empty')
}
}ServiceUnavailableError
Thrown when Apple's validation service is unavailable (5xx errors).
import { ServiceUnavailableError } from 'apple-receipt-verify'
try {
const products = await appleReceiptVerify.validate({ receipt })
} catch (error) {
if (error instanceof ServiceUnavailableError) {
console.log('Apple service unavailable, retry later')
// error.isRetryable === true
}
}Error Properties
All errors may include additional properties:
| Property | Type | Description |
|----------|------|-------------|
| isRetryable | boolean | true if Apple recommends retrying the request |
| appleStatus | number | Status code returned by Apple's validation service |
Complete Error Handling Example
import * as appleReceiptVerify from 'apple-receipt-verify'
appleReceiptVerify.config({
secret: 'your-shared-secret',
environment: ['production', 'sandbox']
})
try {
const products = await appleReceiptVerify.validate({
receipt: receiptData
})
console.log('Valid products:', products)
} catch (error) {
if (error instanceof appleReceiptVerify.EmptyError) {
console.log('Receipt is valid but contains no purchases')
} else if (error instanceof appleReceiptVerify.ServiceUnavailableError) {
console.log('Apple service unavailable, retry later')
if (error.isRetryable) {
// Implement retry logic
}
} else {
console.error('Validation failed:', error.message)
console.error('Apple status:', error.appleStatus)
}
}Error Codes
The module exports Apple's status codes as ERROR_CODES:
import { ERROR_CODES } from 'apple-receipt-verify'
console.log(ERROR_CODES.SUCCESS) // 0
console.log(ERROR_CODES.INVALID_JSON) // 21000
console.log(ERROR_CODES.INVALID_RECEIPT_DATA) // 21002
console.log(ERROR_CODES.COULD_NOT_AUTHENTICATE) // 21003
console.log(ERROR_CODES.INVALID_SECRET) // 21004
console.log(ERROR_CODES.UNAVAILABLE) // 21005
console.log(ERROR_CODES.EXPIRED_SUBSCRIPTION) // 21006
console.log(ERROR_CODES.TEST_RECEIPT) // 21007
console.log(ERROR_CODES.PROD_RECEIPT) // 21008Debug Logging
Enable verbose logging to see detailed validation information:
appleReceiptVerify.config({
secret: 'your-shared-secret',
verbose: true // Enable debug logs
})
// Logs will show:
// - Validation requests
// - Apple's responses
// - Environment switching
// - Error detailsCommonJS Usage (Node.js)
const appleReceiptVerify = require('apple-receipt-verify')
appleReceiptVerify.config({
secret: 'your-shared-secret'
})
// Use with async/await or .then()
appleReceiptVerify.validate({ receipt })
.then(products => console.log(products))
.catch(error => console.error(error))Edge Computing Examples
Cloudflare Workers
import * as appleReceiptVerify from 'apple-receipt-verify'
export default {
async fetch(request, env) {
appleReceiptVerify.config({
secret: env.APPLE_SHARED_SECRET
})
const { receipt } = await request.json()
try {
const products = await appleReceiptVerify.validate({ receipt })
return Response.json({ success: true, products })
} catch (error) {
return Response.json({ success: false, error: error.message }, { status: 400 })
}
}
}Vercel Edge Functions
import * as appleReceiptVerify from 'apple-receipt-verify'
export const config = {
runtime: 'edge'
}
export default async function handler(request) {
appleReceiptVerify.config({
secret: process.env.APPLE_SHARED_SECRET
})
const { receipt } = await request.json()
try {
const products = await appleReceiptVerify.validate({ receipt })
return Response.json({ success: true, products })
} catch (error) {
return Response.json({ success: false, error: error.message }, { status: 400 })
}
}Deno
import * as appleReceiptVerify from 'npm:apple-receipt-verify'
appleReceiptVerify.config({
secret: Deno.env.get('APPLE_SHARED_SECRET')!
})
Deno.serve(async (req) => {
const { receipt } = await req.json()
try {
const products = await appleReceiptVerify.validate({ receipt })
return Response.json({ success: true, products })
} catch (error) {
return Response.json({ success: false, error: error.message }, { status: 400 })
}
})Bun
import * as appleReceiptVerify from 'apple-receipt-verify'
appleReceiptVerify.config({
secret: process.env.APPLE_SHARED_SECRET
})
Bun.serve({
async fetch(req) {
const { receipt } = await req.json()
try {
const products = await appleReceiptVerify.validate({ receipt })
return Response.json({ success: true, products })
} catch (error) {
return Response.json({ success: false, error: error.message }, { status: 400 })
}
}
})Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Changelog
See CHANGELOG.md for release history.
License
[LICENSE]
Credits
Originally based on node-apple-receipt-verify by Siarhei Ladzeika.
Refactored and modernized by nswbmw.
