> ## Documentation Index
> Fetch the complete documentation index at: https://docs.privy.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Migrating Sign in with Apple users for an Apple team transfer

When transferring an iOS app between Apple Developer Teams (e.g., selling an app or moving to a new organization), Sign in with Apple user identifiers (`sub`) change because they are team-scoped. Users who chose "Hide My Email" also receive new private relay email addresses under the new team. Without migrating these identifiers in Privy, affected users will lose access to their existing accounts.

Throughout this guide, **Team A** refers to the current team that owns the app today, and **Team B** refers to the destination team receiving the app.

<Warning>
  You have **60 days** from the date of the app transfer to complete the migration. After that,
  Apple's migration endpoints become inactive. If you miss this window, the app must be transferred
  back and the process restarted. See [TN3159: Migrating users after the 60-day app transfer
  period](https://developer.apple.com/documentation/technotes/tn3159-migrating-sign-in-with-apple-users-for-an-app-transfer#Migrating-users-after-the-60-day-app-transfer-period).
</Warning>

## What changes during an Apple team transfer

| What changes                                                        | Impact on Privy                                                                         |
| ------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| Every user gets a new team-scoped `sub` (subject identifier)        | Privy matches users by subject — logins will fail if subjects aren't updated            |
| "Hide My Email" users get a new `@privaterelay.appleid.com` address | Email-based fallback matching fails, causing duplicate accounts                         |
| Both teams' credentials remain valid during the 60-day window       | No immediate login disruption, but credentials must be updated before the window closes |

## Before the transfer (Team A)

### Step 1: Export your Apple users from Privy

Use the Privy API to [fetch all users](/user-management/users/managing-users/querying-users) for your app, then filter for users with `apple_oauth` linked accounts. For each matching user, extract their Privy ID, Apple subject, and email.

For each Apple OAuth account, you will need:

* **Privy ID** (`privy_id`)
* **Apple subject** (the current `sub` stored in Privy)
* **Email** (the email currently stored for the Apple OAuth account)

Save this list — you will need each user's Apple `sub` in step 3 to generate transfer identifiers via Apple's API.

### Step 2: Disable Apple login (strongly recommended)

Temporarily remove Apple as a login method in the Privy dashboard configuration.

<Info>
  There is a race condition between when the app transfer completes (Apple starts issuing new `sub`
  values) and when Privy's subject migration is run. If a user signs in during this window, their
  new `sub` won't match any stored subject, and if they used "Hide My Email," their new relay email
  won't match either so we don't do any automatic account merging. This results in a **duplicate
  account** being created.
</Info>

This is especially important if:

* Your users have wallets, balances, or other critical state tied to their accounts
* A significant portion of your users use "Hide My Email" (private relay)

If your app has very few Apple sign-in users and you can execute the migration quickly, it may be acceptable to skip this step and accept the risk of needing to manually resolve duplicates.

### Step 3: Generate transfer identifiers

Using Team A's credentials, generate a `transfer_sub` for each user. Follow Apple's guide: [Transferring your apps and users to another team](https://developer.apple.com/documentation/sign_in_with_apple/transferring_your_apps_and_users_to_another_team).

**Obtain an access token for Team A:**

```bash theme={"system"}
curl -X POST "https://appleid.apple.com/auth/token" \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=client_credentials' \
  -d 'scope=user.migration' \
  -d 'client_id=YOUR_CLIENT_ID' \
  -d 'client_secret=CLIENT_SECRET_SIGNED_BY_TEAM_A'
```

**For each user, generate a transfer identifier:**

```bash theme={"system"}
curl -X POST "https://appleid.apple.com/auth/usermigrationinfo" \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -H 'Authorization: Bearer ACCESS_TOKEN_FOR_TEAM_A' \
  -d 'sub=USERS_CURRENT_APPLE_SUB' \
  -d 'target=TEAM_B_TEAM_ID' \
  -d 'client_id=YOUR_CLIENT_ID' \
  -d 'client_secret=CLIENT_SECRET_SIGNED_BY_TEAM_A'
```

Apple returns:

```json theme={"system"}
{
  "transfer_sub": "760417.ebbf12acbc78e1be1668ba852d492d8a.1827"
}
```

Save the `transfer_sub` alongside each user's `privy_id` and old `sub`.

### Step 4: Initiate the app transfer

Transfer the app in [App Store Connect](https://developer.apple.com/help/app-store-connect/transfer-an-app/overview-of-app-transfer). Once Team B accepts, Apple begins issuing Team B-scoped identifiers.

## After the transfer (Team B)

### Step 5: Exchange transfer identifiers for new identifiers

Using Team B's credentials, exchange each `transfer_sub` for the new team-scoped `sub` and (if applicable) the new private relay email. Follow Apple's guide: [Bringing new apps and users into your team](https://developer.apple.com/documentation/sign_in_with_apple/bringing_new_apps_and_users_into_your_team).

**Obtain an access token for Team B:**

```bash theme={"system"}
curl -X POST "https://appleid.apple.com/auth/token" \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=client_credentials' \
  -d 'scope=user.migration' \
  -d 'client_id=YOUR_CLIENT_ID' \
  -d 'client_secret=CLIENT_SECRET_SIGNED_BY_TEAM_B'
```

**For each user, exchange the transfer identifier:**

```bash theme={"system"}
curl -X POST "https://appleid.apple.com/auth/usermigrationinfo" \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -H 'Authorization: Bearer ACCESS_TOKEN_FOR_TEAM_B' \
  -d 'transfer_sub=TRANSFER_SUB_FROM_TEAM_A' \
  -d 'client_id=YOUR_CLIENT_ID' \
  -d 'client_secret=CLIENT_SECRET_SIGNED_BY_TEAM_B'
```

Apple returns the new team-scoped identifiers:

```json theme={"system"}
{
  "sub": "820417.faa325acbc78e1be1668ba852d492d8a.0219",
  "email": "ep9ks2tnph@privaterelay.appleid.com",
  "is_private_email": true
}
```

* **`sub`** — the new Team B-scoped user identifier (present for all users)
* **`email`** — the new private relay email (only present for users who used "Hide My Email")
* **`is_private_email`** — indicates this is a relay address

Save the new `sub` and `email` (when present) alongside each user's `privy_id`.

### Step 6: Build the migration CSV

Prepare a CSV with the following columns:

```csv theme={"system"}
privy_id,old_apple_sub,email,new_apple_sub,new_email
did:privy:user1,001234.old-sub-a.5678,oldrelay@privaterelay.appleid.com,820417.new-sub-b.0219,newrelay@privaterelay.appleid.com
did:privy:user2,001234.old-sub-c.9012,realuser@gmail.com,820417.new-sub-d.3456,
```

| Column          | Required | Description                                                                          |
| --------------- | -------- | ------------------------------------------------------------------------------------ |
| `privy_id`      | Yes      | The user's Privy ID (e.g., `did:privy:abc123`)                                       |
| `old_apple_sub` | Yes      | The current Apple subject stored in Privy (Team A's `sub`)                           |
| `email`         | No       | The email currently stored in Privy for this Apple account, used for verification    |
| `new_apple_sub` | Yes      | The new Apple subject from step 5 (Team B's `sub`)                                   |
| `new_email`     | No       | The new private relay email from step 5 (include when `is_private_email` was `true`) |

For users who shared their real email address (not "Hide My Email"), leave `new_email` blank. Real email addresses are not team-scoped and don't change during migration — Apple's exchange response won't include an `email` field for these users.

### Step 7: Submit the migration to Privy

Provide the CSV to Privy support to run the Apple subject migration. This updates each user's stored subject and, where provided, their stored email to the new Team B values. We'll reach out once the migration is complete on our end.

### Step 8: Update Apple OAuth credentials in Privy

This step can be done while waiting for the subject migration in step 7 to complete — they are independent.

Update the app's Apple OAuth configuration with Team B's credentials:

* **Key ID** — the Key ID for Team B's Sign in with Apple private key
* **Private key** — Team B's `.p8` private key file
* **Team ID** — Team B's 10-character Team ID
* **Client ID** — this is typically the bundle ID and stays the same after transfer

The **Key ID** and **private key** can be updated directly in the [Privy Dashboard](https://dashboard.privy.io). However, the **Team ID** and **Client ID** are read-only in the dashboard once users exist. Contact Privy support to update these fields.

<Info>
  Both teams' credentials remain valid during the 60-day migration window, so this step doesn't need
  to happen before the subject migration. However, it **must** be completed before the 60-day window
  closes or Apple login will stop working.
</Info>

### Step 9: Re-enable Apple login and verify

Once the subject migration (step 7) and credential update (step 8) are both complete, re-enable Apple as a login method in the Privy dashboard.

Then verify the migration by testing with a small number of users:

1. Have a user sign in with Apple through the app
2. Verify they land on their **existing account** — same Privy ID, same linked accounts, same wallets and data
3. If possible, also test with:
   * A user who used "Hide My Email" (private relay) — these are most likely to be affected
   * A user who shared their real email address
   * A brand new user signing up for the first time post-transfer

If anything is wrong, you can disable Apple login again while investigating.

## Troubleshooting

### Duplicate accounts were created

If users signed in during the migration window (between the transfer and the subject migration), they may have new duplicate accounts. Contact Privy support with the affected Privy IDs to resolve these.

### The 60-day window has passed

If more than 60 days have elapsed since the transfer, Apple's migration endpoints are no longer active. The app must be transferred back to Team A, and the process restarted from step 3. See [TN3159: Migrating users after the 60-day app transfer period](https://developer.apple.com/documentation/technotes/tn3159-migrating-sign-in-with-apple-users-for-an-app-transfer#Migrating-users-after-the-60-day-app-transfer-period).

## Apple documentation

* [TN3159: Migrating Sign in with Apple users for an app transfer](https://developer.apple.com/documentation/technotes/tn3159-migrating-sign-in-with-apple-users-for-an-app-transfer)
* [Transferring your apps and users to another team](https://developer.apple.com/documentation/sign_in_with_apple/transferring_your_apps_and_users_to_another_team)
* [Bringing new apps and users into your team](https://developer.apple.com/documentation/sign_in_with_apple/bringing_new_apps_and_users_into_your_team)
* [TN3107: Resolving Sign in with Apple response errors](https://developer.apple.com/documentation/technotes/tn3107-resolving-sign-in-with-apple-response-errors)
