app-store-server-api

repository·main·Indexed 42 days ago

https://github.com/agisboye/app-store-server-api

A Node.js ESM client for the Apple App Store Server API (v1.0.0) providing typed responses and automated authentication. It includes utilities to decode and verify JWS payloads for transactions, renewal info, and App Store Server Notifications V2, with support for both Apple's G3 root certificate and custom Xcode StoreKit testing fingerprints.

Tokens
7.9K
Snippets
27
Records
28
Agent score
52%

What's inside app-store-server-api

  1. Verify transactions signed by Xcode (StoreKit Testing)

    main

    When testing with StoreKit in Xcode, transactions are signed by a local CA instead of Apple. To verify these, you must pass the SHA256 fingerprint of the Xcode root certificate to the decoding functions.

    Use APPLE_ROOT_CA_G3_FINGERPRINT for production and your local fingerprint for development.

    import { decodeTransactions, APPLE_ROOT_CA_G3_FINGERPRINT } from "app-store-server-api"
    
    const LOCAL_ROOT_FINGERPRINT = "AA:BB:CC:DD:..."
    const fingerprint = process.env.NODE_ENV === "production" 
      ? APPLE_ROOT_CA_G3_FINGERPRINT 
      : LOCAL_ROOT_FINGERPRINT
    
    const transactions = await decodeTransactions(response.signedTransactions, fingerprint)
  2. Validate JWS certificate chains with custom fingerprints

    main

    When calling decoding functions, you can optionally pass a rootCertFingerprint string. This allows you to override the default Apple G3 CA fingerprint validation. This is useful for testing environments or when you need to validate against a specific root certificate fingerprint.

    If the certificate chain validation fails (e.g., expired certificates, invalid issuer chain, or mismatched root fingerprint), the functions will throw a CertificateValidationError.

  3. Decode and verify App Store Server Notifications V2

    main

    Use the provided decoding functions to parse the signedPayload sent by Apple. Because a notification can contain different types of data, use the provided type guards to safely access the payload properties.

    Supported payload types:

    • data (use isDecodedNotificationDataPayload)
    • summary (use isDecodedNotificationSummaryPayload)
    • externalPurchaseToken (use isDecodedNotificationExternalPurchaseTokenPayload)
    • appData (use isDecodedNotificationAppDataPayload)
    import {
      decodeNotificationPayload,
      isDecodedNotificationDataPayload,
      isDecodedNotificationSummaryPayload,
      isDecodedNotificationExternalPurchaseTokenPayload,
      isDecodedNotificationAppDataPayload
    } from "app-store-server-api"
    
    const payload = await decodeNotificationPayload(signedPayload)
    
    if (isDecodedNotificationDataPayload(payload)) {
      // payload is type DecodedNotificationDataPayload
      if (payload.data.bundleId === APP_BUNDLE_ID) {
        // Handle data payload
      }
    }
    
    if (isDecodedNotificationSummaryPayload(payload)) {
      // payload is type DecodedNotificationSummaryPayload
    }
    
    if (isDecodedNotificationExternalPurchaseTokenPayload(payload)) {
      // payload is type DecodedNotificationExternalPurchaseTokenPayload
    }
    
    if (isDecodedNotificationAppDataPayload(payload)) {
      // payload is type DecodedNotificationAppDataPayload
    }
  4. Manage test notifications

    main

    You can request a test notification from Apple and then check its status using the token provided in the request response.

    // Request a test notification
    const response = await api.requestTestNotification()
    const token = response.testNotificationToken
    
    // Check the status of that notification
    const status = await api.getTestNotificationStatus(token)
  5. Get subscription status and decode info

    main

    Retrieve the subscription statuses for an originalTransactionId. To access the actual transaction and renewal details, you must decode the signed information using decodeTransaction and decodeRenewalInfo.

    import { decodeTransaction, decodeRenewalInfo } from "app-store-server-api"
    
    const response = await api.getSubscriptionStatuses(originalTransactionId)
    
    // Find the specific transaction
    const item = response.data[0].lastTransactions.find(item => item.originalTransactionId === originalTransactionId)
    
    // Decode the signed info
    const transactionInfo = await decodeTransaction(item.signedTransactionInfo)
    const renewalInfo = await decodeRenewalInfo(item.signedRenewalInfo)
  6. Initialize the AppStoreServerAPI client

    main

    To use the API, create an instance of AppStoreServerAPI. You will need your private key, Key ID, Issuer ID, and App Bundle ID. You must also specify the Environment (e.g., Environment.Production).

    Note on Issuer ID: To obtain your Issuer ID, you must first create an API key for App Store Connect (not the App Store Server API). The ID will appear after the first key is created.

    import { AppStoreServerAPI, Environment } from "app-store-server-api"
    
    const KEY = `-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----`
    const KEY_ID = "ABCD123456"
    const ISSUER_ID = "91fa5999-7b54-4363-a2a8-265363fa6cbe"
    const APP_BUNDLE_ID = "com.yourcompany.app"
    
    const api = new AppStoreServerAPI(KEY, KEY_ID, ISSUER_ID, APP_BUNDLE_ID, Environment.Production)
  7. Perform order lookup

    main

    Check the status of an order using an orderId. Use the OrderLookupStatus enum to verify if the order is valid before processing the transactions.

    import { OrderLookupStatus, decodeTransactions } from "app-store-server-api"
    
    const response = await api.lookupOrder(orderId)
    
    if (response.status === OrderLookupStatus.Valid) {
      const transactions = await decodeTransactions(response.signedTransactions)
      // ... handle transactions
    }
  8. Retrieve notification history

    main

    Fetch a history of notifications. You must provide a startDate and endDate (in milliseconds). The earliest supported start date is June 6th, 2022. If hasMore is true, use history.paginationToken to fetch subsequent pages.

    const response = await api.getNotificationHistory({
      startDate: 1654466400000, // June 6th 2022
      endDate: new Date().getTime()
    })
    
    if (response.hasMore) {
      // Use response.paginationToken for the next request
    }
  9. Get transaction history and decode transactions

    main

    Retrieve the transaction history for a specific originalTransactionId. The response can be paginated using the revision field if hasMore is true.

    Use decodeTransactions to verify that the transactions were signed by Apple and to access their contents.

    import { decodeTransactions, ProductTypeParameter, SortParameter } from "app-store-server-api"
    
    // Basic lookup
    const response = await api.getTransactionHistory(originalTransactionId)
    const transactions = await decodeTransactions(response.signedTransactions)
    
    // Pagination
    if (response.hasMore) {
      const nextResponse = await api.getTransactionHistory(originalTransactionId, { revision: response.revision })
    }
    
    // Using filters and sort options
    const filteredResponse = await api.getTransactionHistory(originalTransactionId, {
      productType: ProductTypeParameter.AutoRenewable,
      sort: SortParameter.Descending
    })
  10. Handle App Store Server errors

    main

    When interacting with the API, you may encounter specific error types exported by the library:

    • AppStoreError: Represents errors returned by the App Store Server API.
    • CertificateValidationError: Represents errors occurring during the validation of Apple's certificates.
    import { AppStoreError, CertificateValidationError } from 'app-store-server-api';
    
    try {
      // API call
    } catch (error) {
      if (error instanceof AppStoreError) {
        // Handle API error
      } else if (error instanceof CertificateValidationError) {
        // Handle certificate error
      }
    }
  11. Get transaction history and info

    main

    Use these methods to retrieve details about specific transactions or a user's transaction history.

    • getTransactionHistory(transactionId, query): Retrieves the transaction history for a specific ID. Accepts an optional TransactionHistoryQuery object.
    • getTransactionInfo(transactionId): Retrieves detailed information for a single transaction.
    • getAppTransactionInfo(transactionId): Retrieves app-level transaction info using an originalTransactionId, transactionId, or appTransactionId.
    // Get history
    const history = await api.getTransactionHistory('12345', { /* query params */ });
    
    // Get specific transaction info
    const info = await api.getTransactionInfo('12345');
    
    // Get app transaction info
    const appInfo = await api.getAppTransactionInfo('12345');