How Renewals and Restores Affect Transaction IDs in StoreKit 2

When working with in-app purchases using StoreKit 2, many developers assume that the transaction ID uniquely represents a purchase forever. In reality, transaction IDs can change when purchases are restored or subscriptions renew.

This is expected behavior in Apple’s purchase system. The key to implementing reliable purchase logic is understanding the difference between transaction IDs and original transaction IDs.

StoreKit 2 Transaction Model

In StoreKit 2, purchases are represented by the Transaction object.

Each Transaction represents a specific purchase event.

These events include:

  • Initial purchase
  • Subscription renewal
  • Restoring purchases on a new device
  • Family sharing access

Each event can generate a new transaction ID.

Transaction ID vs Original Transaction ID

StoreKit 2 provides two key identifiers for managing purchases:

  1. Transaction ID
transaction.id

Represents the specific transaction event.

Examples of events that produce new transaction IDs:

  • subscription renewal
  • restoring purchases
  • purchasing again after expiration

Apple documentation states that a transaction represents an individual purchase event processed by the App Store.

Source: Apple StoreKit documentation – Transaction

  1. Original Transaction ID
   transaction.originalID

Represents the first purchase of the product. This value remains stable across renewals and restores.

Apple states that the original transaction identifier identifies the first transaction in a chain of related transactions.

Source: Apple StoreKit documentation – Original Transaction Id

How to Retrieve Transaction ID and Original Transaction ID

In StoreKit 2, both the transaction ID and original transaction ID are available directly from the Transaction object after a purchase or when listening for updates.

Getting IDs After a Purchase

When a user completes a purchase, you typically receive a Transaction from the purchase result:

let result = try await product.purchase()

switch result {
case .success(let verificationResult):
    guard case .verified(let transaction) = verificationResult else {
        return
    }

    let transactionId = transaction.id
    let originalTransactionId = transaction.originalID

    // Use these values in your logic or send to backend

    await transaction.finish()

default:
    break
}
  • transaction.id → unique for this specific purchase event

  • transaction.originalID → stable identifier for the purchase chain

    Getting IDs from Transaction Updates

    StoreKit 2 can deliver transactions at any time (renewals, restores, etc.). You should always listen for updates:

  for await result in Transaction.updates {
      guard case .verified(let transaction) = result else {
          continue
      }

      let transactionId = transaction.id
      let originalTransactionId = transaction.originalID

      // Handle updated transaction

      await transaction.finish()
  }

This is especially important for:

  • Subscription renewals

  • Restored purchases

  • Background updates

    Getting IDs from Current Entitlements

    If you want to know what the user currently owns (for example, on app launch), use:

  for await result in Transaction.currentEntitlements {
      guard case .verified(let transaction) = result else {
          continue
      }

      let transactionId = transaction.id
      let originalTransactionId = transaction.originalID

      // Unlock features based on active entitlement
  }

What Happens During Restore Purchases

When a user reinstalls your app or taps a Restore Purchases button, your app needs to re-sync with the App Store to retrieve that user’s purchase history.

Apple recommends that apps provide a visible “Restore Purchases” button, especially for apps with non-consumables and subscriptions. This gives users a clear way to manually recover purchases if something doesn’t sync automatically.

In StoreKit 2, restoring is explicitly triggered by calling:

try await AppStore.sync()

The App Store then re-delivers previous purchases to the device.

These restored purchases appear as transactions referencing the original purchase, but the transaction event itself may have a new transaction ID.

Apple describes restore as replaying the user’s transaction history from the App Store.

Source: Apple StoreKit documentation – Restoring Purchased Products

Correct Backend Design

A common mistake is using only the transaction ID to identify purchases. These fields are ideal to use in our backend.

| Field | Purpose |
|—-|—-|
| originaltransactionid | stable identifier for the purchase |
| transactionid | specific purchase event |
| product
id | purchased item |
| expiration_date | subscription management |

Apple recommends using the original transaction identifier when tracking subscriptions and purchase history.

Source: Apple App Store Server API – Transaction History

Practical Examples

Consider a user who subscribes to a monthly plan. The first purchase generates:

  • transaction.id = 1001
  • transaction.originalID = 1001

On the next month’s renewal:

  • transaction.id = 1002
  • transaction.originalID = 1001

If the user restores purchases on a new device:

  • transaction.id = 1003 (if it treats as a separate transaction)
  • transaction.originalID = 1001

Notice that all transactions point back to the same original transaction ID, making it the most reliable identifier for tracking entitlements.

Summary

To summarize:

  • transaction.id represents a specific purchase event
  • transaction.originalID represents the original purchase
  • Restoring purchases or renewing subscriptions can create new transaction IDs
  • The original transaction ID remains constant
  • Always associate entitlements with the original transaction ID, not the individual transaction event

By understanding these nuances and implementing backend logic accordingly, developers can create robust, user-friendly in-app purchase experiences that work across devices and maintain subscription continuity.


Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.