# How the ipatool purchase Command Acquires Licenses for Free Apps

> Discover how the ipatool purchase command gets free app licenses. Learn the five-step workflow and API interaction that creates a license record for $0 apps.

- Repository: [Majd/ipatool](https://github.com/majd/ipatool)
- Tags: internals
- Published: 2026-09-06

---

**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](https://github.com/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`](https://github.com/majd/ipatool/blob/main/cmd/purchase.go), the tool calls `store.AccountInfo()` to fetch the Apple ID email, password, and refresh token.

```go
// 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`](https://github.com/majd/ipatool/blob/main/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()`.

```go
// 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`](https://github.com/majd/ipatool/blob/main/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`](https://github.com/majd/ipatool/blob/main/pkg/appstore/appstore_lookup.go) performs this resolution.

```go
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`](https://github.com/majd/ipatool/blob/main/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.

```go
_, 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.

```go
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

```bash

# Acquire license for TestFlight

ipatool purchase -b com.apple.testflight --platform iphone

```

### CLI: Purchase a Free macOS App

```bash

# Acquire license for Xcode (Mac App Store version)

ipatool purchase -b com.apple.Xcode --platform macos

```

### Programmatic Go Implementation

```go
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`](https://github.com/majd/ipatool/blob/main/cmd/purchase.go) | CLI command definition, argument parsing, retry logic, and orchestration of the purchase workflow |
| [`pkg/appstore/appstore_purchase.go`](https://github.com/majd/ipatool/blob/main/pkg/appstore/appstore_purchase.go) | Core `Purchase()` method that calls Apple's App Store purchase endpoint |
| [`pkg/appstore/appstore_login.go`](https://github.com/majd/ipatool/blob/main/pkg/appstore/appstore_login.go) | Session management, token refresh, and Apple ID authentication |
| [`pkg/appstore/appstore_lookup.go`](https://github.com/majd/ipatool/blob/main/pkg/appstore/appstore_lookup.go) | Bundle ID resolution to internal App Store identifiers |
| [`pkg/appstore/account.go`](https://github.com/majd/ipatool/blob/main/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.