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:
- 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
- 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 chainGetting 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 |
| productid | 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 = 1001transaction.originalID = 1001
On the next month’s renewal:
transaction.id = 1002transaction.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.idrepresents a specific purchase eventtransaction.originalIDrepresents 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.
