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-storekit-api

v2.1.0

Published

Apple StoreKit 2 Server API - Verify purchases, manage subscriptions, handle refunds, and process In-App Purchases with TypeScript support

Readme

Apple StoreKit API

npm version npm downloads License: MIT TypeScript

A TypeScript/JavaScript library for Apple StoreKit API integration. Handles In-App Purchases and subscription management using the latest StoreKit 2 API.

Features

  • Subscription status verification
  • Purchase verification with Apple JWS certificate-chain and claim validation
  • Transaction history
  • Order information lookup
  • Refund status checking
  • Consumption information reporting
  • Flexible private key handling (file path or string content)
  • Auto environment detection (production/sandbox)
  • Bounded retries, request timeouts, cancellation, and streaming pagination

Installation

npm install apple-storekit-api

See CHANGES.md for release notes and migration guidance.

Version 2 migration notes

  • Signed payload helpers now verify asynchronously and require Apple root certificates.
  • Production signed-data verification requires appAppleId.
  • AppleStoreKit uses composition and no longer exposes low-level transport methods.
  • Endpoints without a transaction identifier require an explicit environment in auto mode.
  • The supported runtime is Node.js 22 or newer.
  • TypeScript consumers require TypeScript 5.2 or newer.
  • Package-root imports remain the supported API. An explicit compatibility allowlist preserves the dist/... paths that were shipped by v1.

Requirements

  • Node.js >= 22
  • TypeScript >= 5.2 when consuming the package from TypeScript
  • App Store Connect API access
  • Private key in .p8 format (file or content)
  • Issuer ID and Key ID
  • Apple root certificates from Apple PKI
  • App Apple ID for production signed-data verification

Imports and v1 deep-import compatibility

Import the facade, errors, and public interfaces from the package root:

import { AppleStoreKit, AppleStoreKitApiError } from 'apple-storekit-api';

Import public interfaces from the package root as well:

import type { AppleStoreKitConfig, TransactionInfo } from 'apple-storekit-api';

The exact dist/... paths published by v1 remain resolvable so existing applications can migrate incrementally. They are deprecated compatibility entrypoints, not a stable service-level API; new code should import from apple-storekit-api.

Usage

import { AppleStoreKit, type AppleStoreKitConfig } from 'apple-storekit-api';
import { readFileSync } from 'node:fs';

const appleRootCertificates = [
  readFileSync('/path/to/AppleRootCA-G3.cer')
];

// Example with file path
const configWithPath = {
  issuerId: 'YOUR_ISSUER_ID',
  keyId: 'YOUR_KEY_ID',
  privateKey: '/path/to/private_key.p8',
  bundleId: 'com.yourcompany.yourapp',
  appleRootCertificates,
  environment: 'sandbox' // or 'production'
} satisfies AppleStoreKitConfig;

// Example with key content
const configWithContent = {
  issuerId: 'YOUR_ISSUER_ID',
  keyId: 'YOUR_KEY_ID',
  privateKey: '-----BEGIN PRIVATE KEY-----\nYOUR_PRIVATE_KEY_CONTENT\n-----END PRIVATE KEY-----',
  bundleId: 'com.yourcompany.yourapp',
  appleRootCertificates,
  environment: 'sandbox' // or 'production'
} satisfies AppleStoreKitConfig;

// Example with environment variables (recommended for production)
const configWithEnv = {
  issuerId: process.env.APPLE_ISSUER_ID!,
  keyId: process.env.APPLE_KEY_ID!,
  privateKey: process.env.APPLE_PRIVATE_KEY!,
  bundleId: process.env.APPLE_BUNDLE_ID!,
  appleRootCertificates: [process.env.APPLE_ROOT_CA_PATH!],
  appAppleId: Number(process.env.APPLE_APP_ID),
  // environment is optional. Auto mode falls back to sandbox only when
  // Apple returns 4040010 (TransactionIdNotFoundError).
  maxRetries: 2,
  timeoutMs: 10_000
} satisfies AppleStoreKitConfig;

const storeKit = new AppleStoreKit(configWithPath); // or configWithContent or configWithEnv

// Check subscription status
const status = await storeKit.getSubscriptionStatus('original-transaction-id');


// Verify purchase
const purchase = await storeKit.verifyPurchase('transactionId');

// Get transaction history
const history = await storeKit.getTransactionHistory('transactionId');

// Look up order information
const order = await storeKit.lookupOrder('orderId');

// Check refund status
const refund = await storeKit.refundLookup('transactionId');

// Inspect the configured mode
const mode = storeKit.getConfiguredEnvironment(); // 'production', 'sandbox', or 'auto'

// Resolve the environment for a specific transaction
const transactionEnvironment = await storeKit.resolveTransactionEnvironment('transactionId');

Configuration

Generating API Credentials

  1. Go to App Store Connect:

  2. Create API Key:

    • Click the "+" button to create a new key
    • Enter a name for your key
    • Select "App Store Connect API" access
    • For In-App Purchases, ensure you have the following access rights:
      • App Access
      • Sales and Finance
      • In-App Purchase Management
  3. Generate and Download Key:

    • Click "Generate" to create the key
    • Your browser will download a .p8 file
    • Important: Save this file securely. You can only download it once!
    • Note the Key ID (visible in the keys list)
  4. Get Issuer ID:

    • The Issuer ID is shown at the top of the Keys page
    • It's the same for all keys in your organization
  5. Bundle ID:

    • This is your app's bundle identifier
    • Found in Xcode or App Store Connect under app settings
    • Format: com.yourcompany.yourapp

Private Key Handling

The library accepts the private key in two formats:

  1. File Path: Provide the path to your .p8 file

    privateKey: '/absolute/path/to/private_key.p8'
    // or
    privateKey: './relative/path/to/private_key.p8'
  2. Key Content: Provide the private key content directly

    privateKey: '-----BEGIN PRIVATE KEY-----\nYOUR_KEY_CONTENT\n-----END PRIVATE KEY-----'

Signed Data Verification

verifyPurchase, transaction history, order lookup, subscription helpers, and the verifyAndDecode* methods verify Apple's JWS signature and certificate chain before returning decoded data. Pass DER-encoded Apple root certificates as buffers or file paths. Production verification also requires appAppleId.

Certificate revocation and current-date checks are enabled by default. Set enableOnlineChecks: false only when your deployment cannot perform the required online checks and you accept the reduced verification guarantees.

Environment Detection

The library supports automatic environment detection:

  1. Auto Detection (recommended for development):

    const config = {
      // ... other config
      // environment not specified
    };
    • Transaction-based reads first try production
    • The request falls back to sandbox only when Apple returns error 4040010 (TransactionIdNotFoundError)
    • Authentication, validation, rate-limit, server, and network errors never cause an environment switch
    • Write operations resolve the transaction environment first, then send the write to one environment
    • Look Up Order ID never falls back because Apple doesn't provide that endpoint in sandbox
  2. Manual Setting:

    const config = {
      // ... other config
      environment: 'production' // or 'sandbox'
    };
    • Explicitly sets the environment
    • Values other than 'production' or 'sandbox' are rejected at runtime
    • No automatic switching
    • Recommended for production use

Retry Policy

Retries always stay in the same environment:

  • Selected transient network errors, HTTP 408, 429, 500, 502, 503, and 504, and Apple's retryable 4040002, 4040004, and 4040006 errors use bounded exponential backoff
  • HTTP 429 honors Apple's Retry-After value when it is within maxRetryDelayMs
  • HTTP 400, 401, 403, and other non-retryable responses fail immediately
  • GET requests retry by default; write endpoints opt in only when their semantics are idempotent
const config = {
  // ... credentials
  maxRetries: 2,       // default: 2
  retryBaseDelayMs: 250,
  maxRetryDelayMs: 5000,
  timeoutMs: 10_000
};

Every request also accepts an AbortSignal through its options where options are available. Pagination helpers stop at 100 pages or 20,000 items by default; override these bounds with maxPages and maxItems. Timeout values must be positive integers; timeout and retry-delay values may not exceed 2_147_483_647 milliseconds.

API Methods

Subscription Status

const status = await storeKit.getSubscriptionStatus('original-transaction-id');

This method returns the current status of a subscription, including:

  • Original transaction ID
  • Status (numeric) and statusType (string)
  • Expiration date
  • Transaction info
  • Renewal info

SubscriptionStatusType

{
  ACTIVE = 1,        // The auto-renewable subscription is active
  EXPIRED = 2,       // The auto-renewable subscription is expired
  BILLING_RETRY = 3, // The subscription is in a billing retry period
  GRACE_PERIOD = 4,  // The subscription is in a Billing Grace Period
  REVOKED = 5        // The subscription is revoked (refunded or removed from Family Sharing)
}

Purchases

  • verifyPurchase(transactionId: string): Verify a specific purchase using StoreKit 2 API
  • getTransactionHistory(anyTransactionId, request?, options?): Get and verify bounded V2 transaction history
  • iterateTransactionHistory(anyTransactionId, request?, options?): Stream verified transactions
  • getTransactionHistoryPage(anyTransactionId, request?, revision?, environment?): Get one V2 history page
  • getAppTransactionInfo(anyTransactionId): Get the signed app transaction
  • getVerifiedAppTransactionInfo(anyTransactionId): Get the verified app transaction
  • finishTransaction(transactionId): Mark server-side transaction processing as finished
  • lookupOrder(orderId: string): Look up order details
  • getRefundHistory(anyTransactionId, options?): Get bounded V2 refund history pages
  • iterateRefundHistoryPages(anyTransactionId, options?): Stream V2 refund history pages
  • getRefundHistoryPage(anyTransactionId, revision?, environment?): Get one V2 refund page
  • refundLookup(anyTransactionId): Deprecated alias for getRefundHistory
  • setAppAccountToken(originalTransactionId: string, appAccountToken: string): Set or update app account token for a transaction

Transaction history supports Apple filters:

const history = await storeKit.getTransactionHistory('transaction-id', {
  startDate: Date.now() - 30 * 24 * 60 * 60 * 1000,
  productIds: ['com.example.product'],
  productTypes: [TransactionProductType.AUTO_RENEWABLE],
  sort: TransactionHistoryOrder.DESCENDING,
  revoked: false
});

Subscription Status and Renewal Extensions

  • getAllSubscriptionStatuses(anyTransactionId, statuses?): Return Apple's complete status response with optional repeated status filters
  • getSubscriptionStatus(originalTransactionId): Return an exact matching status, or fail if the response is ambiguous
  • extendSubscriptionRenewalDate(originalTransactionId, request)
  • extendRenewalDateForAllActiveSubscribers(request, options?)
  • getStatusOfSubscriptionRenewalDateExtensions(requestIdentifier, productId, options?)

For endpoints without a transaction ID, pass an explicit environment. Auto mode throws instead of silently selecting production for these endpoints.

App Store Server Notifications

  • getNotificationHistory(request, paginationToken?, options?): Get one notification-history page
  • getAllNotificationHistory(request, options?): Get all notification-history pages
  • iterateNotificationHistoryPages(request, options?): Stream notification-history pages
  • requestTestNotification(options?): Request a test notification
  • getTestNotificationStatus(testNotificationToken, options?): Check a test notification
const notifications = await storeKit.getAllNotificationHistory(
  { startDate, endDate, onlyFailures: true },
  { environment: 'sandbox' }
);

The history window is limited to the past 180 days in production and 30 days in sandbox. Keep startDate before endDate and inside the selected environment's window.

Retention Messaging

Image endpoints:

  • uploadImage(imageIdentifier, image, imageSize?, options?)
  • deleteImage(imageIdentifier, options?)
  • getImageList(options?)

Message and default-configuration endpoints:

  • uploadMessage(messageIdentifier, request, options?)
  • deleteMessage(messageIdentifier, options?)
  • getMessageList(options?)
  • configureDefaultMessage(productId, locale, request, options?)
  • deleteDefaultMessage(productId, locale, options?)
  • getDefaultMessage(productId, locale, options?)

Realtime URL and sandbox performance-test endpoints:

  • configureRealtimeURL(request, options?)
  • deleteRealtimeURL(options?)
  • getRealtimeURL(options?)
  • initiatePerformanceTest(request)
  • getPerformanceTestResults(requestId)

Use verifyAndDecodeRealtimeRequest() to verify and decode signed realtime requests received at your configured URL before processing their payloads.

Performance tests always use Apple's sandbox environment. Image uploads use image/png. FULL_SIZE images must be 3840 pixels wide and 160–2160 pixels high; BULLET_POINT images must be 1024×1024. Transparent PNGs are rejected before upload. Repeated query parameters are encoded in Apple's expected format.

Consumption Information

Use the V2 endpoint for new integrations:

  • sendConsumptionInformationV2(transactionId, request), with ConsumptionRequest
  • Required fields: customerConsented, deliveryStatus, and sampleContentProvided
  • Optional fields: consumptionPercentage and refundPreference

consumptionPercentage is an integer in milliunits from 0 through 100000. It must be 0 for an undelivered item. When refundPreference is GRANT_PRORATED and a percentage is provided, it must be greater than 0 and less than 100000. Only these five fields are sent to Apple's V2 endpoint.

For an auto-renewable subscription, omit consumptionPercentage entirely. Apple calculates the consumed portion and rejects a supplied percentage with HTTP 400 (ConsumptionPercentageAutoRenewableSubscriptionError). The example below with consumptionPercentage is for a consumable, non-consumable, or non-renewing subscription. See Apple's consumption percentage rules.

import {
  ConsumptionRequest,
  DeliveryStatus,
  RefundPreference
} from 'apple-storekit-api';

const consumptionData: ConsumptionRequest = {
  customerConsented: true,
  deliveryStatus: DeliveryStatus.DELIVERED,
  sampleContentProvided: true,
  consumptionPercentage: 75_000,
  refundPreference: RefundPreference.GRANT_PRORATED
};

await storeKit.sendConsumptionInformationV2('transactionId', consumptionData);

For an auto-renewable subscription:

const subscriptionConsumption: ConsumptionRequest = {
  customerConsented: true,
  deliveryStatus: DeliveryStatus.DELIVERED,
  sampleContentProvided: true,
  refundPreference: RefundPreference.GRANT_PRORATED
};

await storeKit.sendConsumptionInformationV2('subscription-transaction-id', subscriptionConsumption);

The deprecated V1 endpoint has a different request schema and numeric enums. The legacy sendConsumptionInformation() method accepts ConsumptionRequestV1; all fields except refundPreference are required by Apple.

import {
  AccountTenure,
  ConsumptionRequestV1,
  ConsumptionStatus,
  DeliveryStatusV1,
  LifetimeDollars,
  Platform,
  PlayTime,
  RefundPreferenceV1,
  UserStatus
} from 'apple-storekit-api';

const legacyConsumptionData: ConsumptionRequestV1 = {
  accountTenure: AccountTenure.UNDECLARED,
  appAccountToken: '',
  consumptionStatus: ConsumptionStatus.FULLY_CONSUMED,
  customerConsented: true,
  deliveryStatus: DeliveryStatusV1.DELIVERED_AND_WORKING_PROPERLY,
  lifetimeDollarsPurchased: LifetimeDollars.UNDECLARED,
  lifetimeDollarsRefunded: LifetimeDollars.UNDECLARED,
  platform: Platform.APPLE,
  playTime: PlayTime.UNDECLARED,
  refundPreference: RefundPreferenceV1.NO_PREFERENCE,
  sampleContentProvided: true,
  userStatus: UserStatus.ACTIVE
};

await storeKit.sendConsumptionInformation('transactionId', legacyConsumptionData);

Both methods reject requests unless customerConsented is exactly true. Obtain valid consent before sharing the data, send the response within 12 hours of a CONSUMPTION_REQUEST notification, and update your app privacy disclosures. For V1 fields you don't provide, use the corresponding UNDECLARED value or an empty appAccountToken as required by Apple.

Utility

  • getConfiguredEnvironment(): Get 'production', 'sandbox', or 'auto'
  • resolveTransactionEnvironment(transactionId: string): Resolve the environment for one transaction
  • getCurrentEnvironment(): Deprecated alias for getConfiguredEnvironment()
  • getAccountTenure(date: Date): Calculate the account tenure enum value based on account creation date

Security Best Practices

  1. Private Key Storage:

    • Never commit your .p8 file to version control
    • Store the key securely (e.g., environment variables, secure key management service)
    • Consider using environment variables for all sensitive data:
      const config = {
        issuerId: process.env.APPLE_ISSUER_ID,
        keyId: process.env.APPLE_KEY_ID,
       privateKey: process.env.APPLE_PRIVATE_KEY,
       bundleId: process.env.APPLE_BUNDLE_ID,
       appleRootCertificates: [process.env.APPLE_ROOT_CA_PATH],
       appAppleId: Number(process.env.APPLE_APP_ID)
      };
  2. Environment Management:

    • Use 'sandbox' for development and testing
    • Use 'production' for live apps
    • Consider using different keys for sandbox and production

Compatibility

This library is compatible with:

  • Node.js versions 22 and 24
  • TypeScript 5.2 and above
  • All major Node.js frameworks (Express, Koa, Nest.js, etc.)
  • CommonJS packages and TypeScript declarations

Set App Account Token

Sets or updates the app account token for a transaction made outside of your app:

try {
  await storeKit.setAppAccountToken(
    'original-transaction-id',
    '00000000-0000-4000-8000-000000000001'
  );
  console.log('App account token updated successfully');
} catch (error) {
  console.error('Failed to update app account token:', error instanceof Error ? error.message : error);
}

Note: This method is available in App Store Server API 1.16+ and is useful for:

  • Linking transactions to specific user accounts
  • Updating account tokens for purchases made outside your app
  • Improving transaction tracking and analytics

Error Handling

The library includes comprehensive error handling for API responses. All methods throw descriptive errors that include the original Apple StoreKit API error message when available.

try {
  const status = await storeKit.getSubscriptionStatus('originalTransactionId');
} catch (error) {
  console.error('StoreKit API Error:', error instanceof Error ? error.message : error);
}

License

MIT

Contributing

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'feat: amazing new feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Support

For issues and feature requests, please use the GitHub issue tracker.