npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

apple-receipt-verify

v1.0.1

Published

A modern JavaScript module for Apple In-App Purchase receipt validation.

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 fetch and structuredClone
  • 📦 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-verify

Requirements

  • Node.js >= 20
  • Or any modern JavaScript runtime with fetch and structuredClone support:
    • 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)             // 21008

Debug 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 details

CommonJS 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.