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
Maintainers
Readme
Apple StoreKit API
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-apiSee 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. AppleStoreKituses 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
.p8format (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
Go to App Store Connect:
- Visit App Store Connect
- Navigate to "Users and Access" > "Keys"
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
Generate and Download Key:
- Click "Generate" to create the key
- Your browser will download a
.p8file - Important: Save this file securely. You can only download it once!
- Note the Key ID (visible in the keys list)
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
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:
File Path: Provide the path to your
.p8fileprivateKey: '/absolute/path/to/private_key.p8' // or privateKey: './relative/path/to/private_key.p8'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:
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
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, and504, and Apple's retryable4040002,4040004, and4040006errors use bounded exponential backoff - HTTP
429honors Apple'sRetry-Aftervalue when it is withinmaxRetryDelayMs - 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 APIgetTransactionHistory(anyTransactionId, request?, options?): Get and verify bounded V2 transaction historyiterateTransactionHistory(anyTransactionId, request?, options?): Stream verified transactionsgetTransactionHistoryPage(anyTransactionId, request?, revision?, environment?): Get one V2 history pagegetAppTransactionInfo(anyTransactionId): Get the signed app transactiongetVerifiedAppTransactionInfo(anyTransactionId): Get the verified app transactionfinishTransaction(transactionId): Mark server-side transaction processing as finishedlookupOrder(orderId: string): Look up order detailsgetRefundHistory(anyTransactionId, options?): Get bounded V2 refund history pagesiterateRefundHistoryPages(anyTransactionId, options?): Stream V2 refund history pagesgetRefundHistoryPage(anyTransactionId, revision?, environment?): Get one V2 refund pagerefundLookup(anyTransactionId): Deprecated alias forgetRefundHistorysetAppAccountToken(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 filtersgetSubscriptionStatus(originalTransactionId): Return an exact matching status, or fail if the response is ambiguousextendSubscriptionRenewalDate(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 pagegetAllNotificationHistory(request, options?): Get all notification-history pagesiterateNotificationHistoryPages(request, options?): Stream notification-history pagesrequestTestNotification(options?): Request a test notificationgetTestNotificationStatus(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), withConsumptionRequest- Required fields:
customerConsented,deliveryStatus, andsampleContentProvided - Optional fields:
consumptionPercentageandrefundPreference
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 transactiongetCurrentEnvironment(): Deprecated alias forgetConfiguredEnvironment()getAccountTenure(date: Date): Calculate the account tenure enum value based on account creation date
Security Best Practices
Private Key Storage:
- Never commit your
.p8file 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) };
- Never commit your
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
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'feat: amazing new feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Support
For issues and feature requests, please use the GitHub issue tracker.
