How the ipatool purchase Command Acquires Licenses for Free Apps
The ipatool purchase command acquires licenses for free apps by executing a five-step workflow against Apple's App Store API, culminating in a purchase request that creates a license record even when the price is $0.
The purchase sub-command in majd/ipatool automates the process of adding free apps to your Apple ID's Purchased list. This license is required before you can download the app's IPA file using the download command. Below is a complete breakdown of how this process works, including the source code implementation and practical examples.
The Five-Step License Acquisition Workflow
Step 1: Load Stored Apple ID Credentials
The command begins by retrieving cached account information from the local keychain. In cmd/purchase.go, the tool calls store.AccountInfo() to fetch the Apple ID email, password, and refresh token.
// From cmd/purchase.go
acc, err := store.AccountInfo()
if err != nil {
return fmt.Errorf("failed to get account info: %w", err)
}
The account structure is defined in pkg/appstore/account.go, which handles storage and retrieval of Apple ID credentials.
Step 2: Refresh Expired Sessions
If the stored token has expired, the App Store API returns appstore.ErrPasswordTokenExpired. The command detects this and immediately triggers a re-authentication via store.Login().
// Retry loop in cmd/purchase.go handles token expiration
for attempts := 0; attempts < maxRetries; attempts++ {
// ... attempt purchase ...
if errors.Is(err, appstore.ErrPasswordTokenExpired) {
// Re-login with stored credentials
acc, err = store.Login(appstore.LoginInput{
Email: acc.Email,
Password: acc.Password,
})
continue // Retry with fresh token
}
}
This logic is implemented in pkg/appstore/appstore_login.go.
Step 3: Resolve Bundle ID to App Store ID
Before purchasing, the tool must translate the human-readable bundle identifier (e.g., com.apple.testflight) into Apple's internal numeric identifier. The store.Lookup() function in pkg/appstore/appstore_lookup.go performs this resolution.
lookupResult, err := store.Lookup(appstore.LookupInput{
Account: acc,
BundleID: bundleID, // e.g., "com.apple.testflight"
Platform: platform, // e.g., appstore.PlatformiPhone
})
appID := lookupResult.App
Step 4: Send the Purchase Request
The core license acquisition happens in pkg/appstore/appstore_purchase.go. The store.Purchase() method sends an authenticated request to Apple's purchase endpoint. For free apps, no payment is processed, but Apple still creates a purchase record—this record is the license.
_, err := store.Purchase(appstore.PurchaseInput{
Account: acc,
App: appID,
Platform: platform,
})
The purchase endpoint treats free and paid apps identically at the protocol level; the only difference is the absence of a payment step for $0 items.
Step 5: Handle Already-Owned Apps
If the app already exists in the user's purchase history, Apple returns appstore.ErrLicenseAlreadyExists. The command treats this as success rather than failure, since the desired outcome—possession of a valid license—is already achieved.
if errors.Is(err, appstore.ErrLicenseAlreadyExists) {
fmt.Println("License already exists – no action needed.")
return nil
}
Complete Usage Examples
CLI: Purchase a Free iOS App
# Acquire license for TestFlight
ipatool purchase -b com.apple.testflight --platform iphone
CLI: Purchase a Free macOS App
# Acquire license for Xcode (Mac App Store version)
ipatool purchase -b com.apple.Xcode --platform macos
Programmatic Go Implementation
package main
import (
"errors"
"fmt"
"github.com/majd/ipatool/pkg/appstore"
)
func acquireLicense(store appstore.AppStore, bundleID string) error {
// Load account
acc, err := store.AccountInfo()
if err != nil {
return fmt.Errorf("load account: %w", err)
}
// Lookup app
lookup, err := store.Lookup(appstore.LookupInput{
Account: acc,
BundleID: bundleID,
Platform: appstore.PlatformiPhone,
})
if err != nil {
return fmt.Errorf("lookup failed: %w", err)
}
// Request license
_, err = store.Purchase(appstore.PurchaseInput{
Account: acc,
App: lookup.App,
Platform: appstore.PlatformiPhone,
})
// Handle already-owned case
if errors.Is(err, appstore.ErrLicenseAlreadyExists) {
fmt.Println("App already owned – existing license valid.")
return nil
}
if err != nil {
return fmt.Errorf("purchase failed: %w", err)
}
fmt.Println("License acquired successfully.")
return nil
}
Key Source Files and Their Roles
| File | Purpose |
|---|---|
cmd/purchase.go |
CLI command definition, argument parsing, retry logic, and orchestration of the purchase workflow |
pkg/appstore/appstore_purchase.go |
Core Purchase() method that calls Apple's App Store purchase endpoint |
pkg/appstore/appstore_login.go |
Session management, token refresh, and Apple ID authentication |
pkg/appstore/appstore_lookup.go |
Bundle ID resolution to internal App Store identifiers |
pkg/appstore/account.go |
Data structures for Apple ID credential storage |
Summary
ipatool purchaseacquires licenses by sending authenticated purchase requests to Apple's App Store API, which creates a license record for free apps without charging.- The command implements automatic retry with re-authentication when sessions expire, ensuring reliable operation.
- Already-owned apps are handled gracefully—the
ErrLicenseAlreadyExistserror is treated as success. - The workflow requires valid Apple ID credentials stored via
ipatool auth loginbefore purchase commands can succeed.
Frequently Asked Questions
Why does a free app need a "purchase" at all?
Apple's App Store infrastructure uses the same purchase mechanism regardless of price. A $0 purchase still creates a license record that ties the app to your Apple ID, enabling subsequent downloads and updates. The ipatool purchase command mirrors this behavior to remain compatible with Apple's API.
What happens if I run purchase on an app I already own?
The command receives appstore.ErrLicenseAlreadyExists from the App Store API, logs a success message, and exits cleanly. No duplicate license is created, and your existing purchase history remains unchanged.
Can I purchase apps without storing my Apple ID password?
No. The purchase command requires cached credentials obtained via ipatool auth login. The password is stored in your system's keychain and is used to refresh authentication tokens when they expire during the purchase workflow.
Does ipatool work with paid apps or in-app purchases?
The codebase shows the same purchase endpoint is used for all apps, but ipatool is designed for acquiring free app licenses to enable IPA downloads. Paid apps would require additional payment processing that is not implemented in the current tool.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →