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 purchase acquires 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 ErrLicenseAlreadyExists error is treated as success.
  • The workflow requires valid Apple ID credentials stored via ipatool auth login before 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →