Getting Old Transactions on Test Flight Apple Account
Summary
In the Apple Sandbox environment, when a test user subscribes to an auto-renewable subscription, the received JWT transactionInfo contains stale dates — purchaseDate and expiresDate reflect a previous subscription period, even though the JWT itself was signed today. This creates a confusing scenario where the signature is valid and current, but the underlying transaction data is outdated.
Key Takeaway: Apple Sandbox reuses existing transaction records for test subscriptions, returning old transaction metadata signed with fresh certificates. This is a known Sandbox behavior, not a bug in your implementation.
Root Cause
The root cause lies in how Apple’s App Store Server handles transaction record persistence in the Sandbox environment:
- Apple reuses originalTransactionId: When a test user subscribes again (even after expiration or cancellation), Apple does not create a brand new transaction record. Instead, it reuses the existing transaction tied to the originalTransactionId.
- Sandbox transaction deduplication: Apple’s servers optimize for consistency with production behavior — in production, renewals and lapses maintain the same originalTransactionId.
- Date fields are pulled from the existing record: The purchaseDate and expiresDate in the JWT payload come from the database record associated with that originalTransactionId, which may be months old.
- Signature is regenerated for each request: The JWT is freshly signed using today’s certificate, but the payload data is pulled from the existing transaction record, not created anew.
Key Takeaway: The signature being “today” does not mean the transaction data is new — Apple signs whatever data exists in the transaction record at request time.
Why This Happens in Real Systems
This behavior manifests in real systems for several reasons:
- Subscription continuity modeling: Apple treats all subscription events (initial purchase, renewal, lapse, re-subscribe) as updates to a single transaction chain, identified by originalTransactionId.
- Sandbox data persistence: Unlike what developers expect, Sandbox environments retain transaction data indefinitely, unlike the sometimes-reset receipt data in older test approaches.
- Server-to-server notification alignment: The same transaction record feeds both the /transactionHistory endpoint and server-to-server notifications, ensuring consistency — but this means old data surfaces when the same user re-subscribes.
- No “fresh start” for test accounts: Apple does not provide a mechanism to create truly fresh subscription records for the same sandbox test user account.
Key Takeaway: This is architectural — Apple prioritizes transaction chain integrity over “fresh” test data, which aligns with production behavior but confuses testing workflows.
Real-World Impact
This behavior affects production systems in specific ways:
- Subscription validation logic breaks: If your backend checks
purchaseDate > some_recent_thresholdto determine if a subscription is “new,” it will incorrectly reject valid re-subscriptions. - Grace period calculations fail: Using stale expiresDate for grace period logic can cause premature access revocation.
- Analytics and attribution issues: Marketing teams cannot accurately attribute new subscriptions to campaigns when dates are misaligned.
- Refund and dispute handling: Old transaction dates combined with new signatures can cause confusion in fraud detection systems.
- Migration and audit failures: When migrating subscription data or auditing transaction logs, the mismatch between signedDate and transaction dates creates reconciliation problems.
Key Takeaway: Relying on purchaseDate for business logic without accounting for transaction reuse is a common source of subscription management bugs.
Example or Code (if necessary and relevant)
When decoding the JWT payload, you might see something like this:
{
"signedDate": 1706745600000,
"originalTransactionId": "2000000000000000",
"transactionId": "2000000123456789",
"productId": "premium.monthly",
"purchaseDate": 1696118400000,
"expiresDate": 1696204800000,
"quantity": 1
}
Note: signedDate is “today” but purchaseDate and expiresDate are from months ago — this is the expected behavior in Sandbox, not an error.
How Senior Engineers Fix It
Senior engineers address this with robust validation strategies:
- Use transactionId, not originalTransactionId, for new purchase detection: The transactionId changes for each purchase/renewal event, while originalTransactionId stays constant.
- Implement signedDate-based freshness checks: Use the JWT’s signedDate field as the authority for “when this data was retrieved,” not purchaseDate.
- Track subscription state changes event-driven: Rather than relying on date comparisons, track the actual state transitions (ACTIVE → EXPIRED → ACTIVE) via server-to-server notifications.
- Add sandbox-specific test fallbacks: Implement test mode logic that acknowledges known Sandbox behaviors and adjusts validation accordingly.
- Store transactionId chains: Maintain your own mapping of transactionId → status, updating on each new transactionId for the same originalTransactionId.
Key Takeaway: Never assume purchaseDate represents a new subscription — always use transactionId for freshness and signedDate for retrieval timing.
Why Juniors Miss It
Junior engineers commonly overlook this issue because:
- Documentation assumes production patterns: Apple’s documentation focuses on production flows where transaction reuse is expected and correct; Sandbox differences are not explicitly highlighted.
- Intuitive expectations vs. reality: Developers expect “new subscription = new dates,” which is logically sound but architecturally incorrect in Apple’s system.
- Testing only happy paths: Initial sandbox tests often use brand-new test users, masking the behavior until existing test accounts are reused.
- JWT validation focus: Most debugging focuses on signature verification, which passes — leading to false confidence that the data is correct.
- Lack of domain knowledge: Understanding subscription transaction chains, originalTransactionId semantics, and receipt lifecycle requires experience with Apple’s specific implementation details.
- No visible error: Unlike a 400/500 HTTP error, this returns 200 with “wrong” data, making it appear functional until business logic fails.
Key Takeaway: This is a subtle semantic issue, not a visible error — it passes all validation checks while delivering incorrect data for naive business logic.