Mr. Latte


Migrating Sign in with Apple users to another team

Overview

This walks through migrating Sign in with Apple users to a different team. When an app that includes Sign in with Apple moves to another team, the user records have to move with it. Moving an ios app itself to a different account is covered in an earlier post1.

Moving the sign-in records to another team

Generating client_secret

Generate a client_secret for both the current team and the receiving team.

require 'jwt'

key_file = 'key.p8'
team_id = 'TeamID'
client_id = 'AppID'
key_id = 'KeyID'

ecdsa_key = OpenSSL::PKey::EC.new IO.read key_file

headers = {
 'kid' => key_id
}

claims = {
'iss' => team_id,
'iat' => Time.now.to_i,
'exp' => Time.now.to_i + 86400*180,
'aud' => 'https://appleid.apple.com',
'sub' => client_id,
}

token = JWT.encode claims, ecdsa_key, 'ES256', headers

puts token

Getting an access_token

Use the current team’s client_id (the app’s bundle id) and client_secret to obtain an access_token. Send grant_type and scope exactly as shown below, unmodified.

  • client_id: the client_id used by the current team
  • client_secret: the current team’s client_secret

Request

POST /auth/token HTTP/1.1
Host: appleid.apple.com
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&scope=user.migration&client_id={client_id}&client_secret={client_secret}

Response

Keep the returned access_token somewhere safe.

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
  "access_token":"adg6105ed7a4de...0.nx.20LreF67Or9",
  "token_type":"Bearer",
  "expires_in":3600
}

Creating a transfer identifier

Now use that access_token to create a transfer_sub. Note that the token goes in the header.

  • sub: the existing user’s Provider Id
  • recipient_team_id: the id of the receiving team
  • client_id: the client_id used by the current team
  • client_secret: the current team’s client_secret

Request

POST /auth/usermigrationinfo HTTP/1.1
Host: appleid.apple.com
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer {access_token}

sub={sub}&target={recipient_team_id}&client_id={client_id}&client_secret={client_secret}

Response

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
  "transfer_sub":"760417.ebbf1c...ba852d492d8a.1827"
}

Creating the exchange identifier

Use the transfer identifier transfer_sub to create the exchange identifier. The same access_token from above continues to be used.

  • client_id: the new team’s client_id
  • client_secret: the new team’s client_secret

Request

POST /auth/usermigrationinfo HTTP/1.1
Host: appleid.apple.com
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer {access_token}

transfer_sub={transfer_sub}&client_id={client_id}&client_secret={client_secret}

Response

Update the provider Id in your database with the returned exchange identifier sub and the migration is complete.

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
  "sub":"820417.faa325...2d492d8a.0219",
  "email": "ep9...nph@privaterelay.appleid.com",
  "is_private_email" : true
}

Note that this step only succeeds for an app that was actually transferred from its owner. Otherwise the response comes back as invalid_client. The provider Id token stays valid for at most 60 days from the ownership transfer date, so this has to be done within that window2.

{
  "error": "invalid_client"
}

Conclusion

When an app goes through the normal transfer procedure, the method above does move the provider Id to the new team.

What I ran into was an unusual case. A misconfiguration meant the app belonged to team A while the key file sat with team B, and in that situation the method above cannot complete the migration.

For an app built purely natively, a mismatched client_id registration is not possible. But when a native app is mixed with web views and the Apple ID integration lives in the web view, a mistake can result in a client_id belonging to a different team than the app. That is the case where team A holds only the key file and team B holds only the app. Two possible routes out:

  • Ship an app under the same client_id from the current team all the way to release, then transfer it to the new team
  • Prompt users through the web view to sign in again under the new team, and on success replace the stored provider Id from the old team (this needs a user migration flow built on the web side)
Looking for a product partner? Founders, teams, businesses: from problem framing to launch.

Copyright © 2026 - present Mr. Latte. All Rights Reserved.

hello@mrlatte.net

v2026.08.25.0130