Status: field notes / proof of concept
Scope: Android → iPhone, where the iPhone is already configured and must not be factory-reset
Approach: local, open-source tooling, no jailbreak, no root required for the successful path described here
Official WhatsApp migration from Android to iPhone is designed primarily for a new or factory-reset iPhone during initial setup. This repository documents a successful reverse-engineered migration into an already configured iPhone, while preserving the existing iPhone WhatsApp state.
This is not an official WhatsApp, Apple, or Meta procedure.
The goal of this document is not to provide a polished end-user utility. It is to preserve technical findings that may help future developers, researchers, maintainers of open-source tools, and AI coding agents avoid repeating the same reverse-engineering work.
This workflow modifies WhatsApp's private iOS database and uses a partial MobileBackup2 restore.
A mistake can corrupt WhatsApp data or force you to restore the phone.
Before doing anything:
- keep the original Android WhatsApp backup;
- keep the original Android media directory;
- make an encrypted local backup of the iPhone;
- never overwrite your only working backup;
- work on copies;
- keep WhatsApp closed during database/restore operations;
- do not publish real databases, media, phone numbers, LIDs, group JIDs, UDIDs, backup passwords, or WhatsApp encryption keys.
Use at your own risk.
The successful migration was tested with a contemporary 2026 stack roughly equivalent to:
- Android 16
- iOS 26.x
- current WhatsApp versions on both platforms
- Windows 11
- Python 3.12
- ADB
wa-crypt-toolsios-backup-decryptorpymobiledevice3
No Android root and no iPhone jailbreak were used.
Useful upstream projects:
wa-crypt-toolsios-backup-decryptorpymobiledevice3SwiftWABackupAPI- historical reference:
watoi
Android WhatsApp
│
▼
encrypted msgstore.db.crypt15
│
▼
decrypt locally
│
▼
Android msgstore.db
│
├── chats
├── messages
├── groups
├── participants
└── media metadata
│
▼
normalize / map identities
│
├── @s.whatsapp.net
├── @lid
└── @g.us
│
▼
merge into iOS ChatStorage.sqlite
│
├── ZWACHATSESSION
├── ZWAMESSAGE
├── ZWAMEDIAITEM
├── ZWAGROUPINFO
└── ZWAGROUPMEMBER
│
▼
stage Message/Media/*
│
▼
build partial encrypted iOS backup
│
├── Manifest.db
├── MBFile metadata
├── per-file encryption keys
└── hashed payload files
│
▼
MobileBackup2 partial restore
│
▼
reboot
│
▼
existing WhatsApp installation now contains migrated history
The problem is not just "copy a SQLite file".
Android and iOS WhatsApp use different internal databases, different identity representations, different media layouts, and different backup systems.
The difficult parts are:
- converting Android message data to the current iOS Core Data schema;
- resolving modern WhatsApp LIDs;
- merging history into already existing iPhone chats;
- recreating group participants correctly;
- preserving chronological message ordering;
- rebuilding media references;
- adding new files into an encrypted iOS backup;
- restoring only the WhatsApp App Group without factory-resetting the phone.
Typical Android location:
/sdcard/Android/media/com.whatsapp/WhatsApp/Databases/
Example:
adb pull `
"/sdcard/Android/media/com.whatsapp/WhatsApp/Databases/msgstore.db.crypt15" `
"C:\WA-transfer\msgstore.db.crypt15"A normal non-root ADB session cannot read WhatsApp's private app directory under /data/user/0/....
That is expected.
A practical route is to enable WhatsApp's end-to-end encrypted backup and use the locally known backup key.
Do not paste the raw backup key into chat logs, scripts, GitHub issues, shell history, or public repositories.
Decrypt locally using a tool such as wa-crypt-tools.
The result should be a valid SQLite database:
C:\WA-transfer\msgstore.db
Validate it:
PRAGMA integrity_check;Expected:
ok
Modern Android WhatsApp databases may contain tables including:
chat
message
message_text
message_media
jid
group_participant_user
Relevant identity information is commonly available through jid.
Useful fields include:
jid._id
jid.user
jid.server
jid.raw_string
Common server suffixes:
@s.whatsapp.net classic user JID
@lid modern LID identity
@g.us group
Do not assume every chat is represented by a classic phone-number JID.
Create an encrypted local iPhone backup.
With pymobiledevice3, a WhatsApp-focused backup can be created with a command similar to:
uvx --python 3.12 pymobiledevice3 backup2 backup `
--full `
--only whatsapp `
--udid "<IPHONE_UDID>" `
"C:\WA-transfer\iphone-backup"The important WhatsApp App Group domain is:
AppDomainGroup-group.net.whatsapp.WhatsApp.shared
The main database is:
ChatStorage.sqlite
A frequently observed MobileBackup file ID for this file is:
7c7fba66680ef796b916b067077cc246adacf01d
Do not assume a hard-coded file ID forever; resolving by domain + relative path is safer.
Current WhatsApp installations may use at least:
ChatStorage.sqlite
ContactsV2.sqlite
LID.sqlite
LID.sqlite is extremely important for modern identity mapping.
A useful relationship observed in current databases:
ZIDENTIFIER -> something@lid
ZPHONENUMBER -> normalized phone number
ContactsV2.sqlite may also contain fields such as:
ZLID
ZPHONENUMBER
ZWHATSAPPID
ZFULLNAME
Important Core Data tables include:
ZWACHATSESSION
ZWAMESSAGE
ZWAMEDIAITEM
ZWAMESSAGEINFO
ZWAGROUPINFO
ZWAGROUPMEMBER
Z_PRIMARYKEY
Observed Core Data entity IDs in the tested version:
WAChatSession -> 4
WAGroupInfo -> 5
WAGroupMember -> 6
WAMediaItem -> 8
WAMessage -> 9
WAMessageInfo -> 11
Never assume these IDs are permanent.
Always inspect:
SELECT Z_ENT, Z_NAME, Z_MAX
FROM Z_PRIMARYKEY;Android message timestamps are Unix milliseconds.
iOS WhatsApp stores timestamps relative to the Apple epoch.
Conversion:
ios_timestamp = android_timestamp_ms / 1000.0 - 978307200.0The reverse is:
android_timestamp_ms = (ios_timestamp + 978307200.0) * 1000.0A robust mapping strategy is:
Android classic JID
│
▼
normalized phone number
│
▼
LID.sqlite / ContactsV2.sqlite
│
▼
current iOS @lid
│
▼
ZWACHATSESSION
Do not blindly convert:
123456789@s.whatsapp.net
into some guessed:
123456789@lid
LIDs are separate identifiers.
For existing iOS chats, useful fields are:
ZWACHATSESSION.ZCONTACTJID
ZWACHATSESSION.ZCONTACTIDENTIFIER
In the tested environment, a chat could contain:
ZCONTACTJID = <contact-lid>@lid
ZCONTACTIDENTIFIER = <phone>@s.whatsapp.net
A useful discovery was that historical plain-text messages can work with a minimal ZWAMESSAGE row.
It was not necessary to fabricate:
ZWAMEDIAITEM
ZWAMESSAGEINFO
messageSecret
for ordinary imported historical text messages.
This greatly simplifies text migration.
Important fields include:
Z_PK
Z_ENT
Z_OPT
ZFLAGS
ZISFROMME
ZMESSAGESTATUS
ZMESSAGETYPE
ZSORT
ZCHATSESSION
ZMESSAGEDATE
ZSENTDATE
ZFROMJID
ZTOJID
ZSTANZAID
ZTEXT
For the tested version, native text flags were observed approximately as:
outgoing: 16777280
incoming: 16777216
Do not hard-code these without first checking a native message from the target version.
When inserting rows manually, update the corresponding Core Data high-water mark:
UPDATE Z_PRIMARYKEY
SET Z_MAX = <new max PK>
WHERE Z_NAME = 'WAMessage';Likewise for media:
UPDATE Z_PRIMARYKEY
SET Z_MAX = <new max PK>
WHERE Z_NAME = 'WAMediaItem';Failure to maintain these values can result in future collisions when WhatsApp inserts new records.
ZSORT is per-chat ordering.
Do not simply append imported historical messages to the end.
A safe approach is to build a combined chronology:
existing native messages
+
imported text messages
+
imported media messages
Then assign sequential ZSORT values.
For some group chats, the current iOS database may already contain system messages at early ZSORT positions. Preserve those when necessary.
Always validate:
unique sort positions
monotonic chronology
correct last message
correct message counter
Groups use exact @g.us identifiers.
If the same group already exists on iOS, map directly to its ZWACHATSESSION.
Incoming group messages require the correct:
ZWAMESSAGE.ZGROUPMEMBER
which references:
ZWAGROUPMEMBER.Z_PK
A message being in the correct group is not sufficient to show the correct author.
For incoming group messages, the tested structure looked roughly like:
ZCHATSESSION = <group chat>
ZGROUPMEMBER = <specific sender member PK>
ZFROMJID = <group>@g.us
ZTOJID = <local account>@s.whatsapp.net
ZLASTSESSION = <group chat>
Outgoing:
ZGROUPMEMBER = NULL
ZFROMJID = NULL
ZTOJID = <group>@g.us
Android media metadata is found in message_media.
Common Android message types observed:
1 image
2 audio / voice
3 video
9 document
13 GIF
20 sticker
Some special types such as view-once media should be treated separately.
Do not convert private app paths such as:
/data/user/0/com.whatsapp/files/ViewOnce/...
unless you explicitly understand the semantics and have legitimate access to the source file.
A normal non-root ADB session cannot read those paths.
Observed / externally corroborated iOS message types:
1 image
2 video
3 audio
8 document
11 GIF
15 sticker
Therefore a useful Android → iOS mapping is:
Android 1 -> iOS 1
Android 3 -> iOS 2
Android 2 -> iOS 3
Android 9 -> iOS 8
Android 13 -> iOS 11
Android 20 -> iOS 15
Validate against native messages from the target WhatsApp version whenever possible.
Observed schema included:
Z_PK
Z_ENT
Z_OPT
ZCLOUDSTATUS
ZFILESIZE
ZMEDIAORIGIN
ZMOVIEDURATION
ZMESSAGE
ZASPECTRATIO
ZHACCURACY
ZLATITUDE
ZLONGITUDE
ZMEDIAURLDATE
ZAUTHORNAME
ZCOLLECTIONNAME
ZMEDIALOCALPATH
ZMEDIAURL
ZTHUMBNAILLOCALPATH
ZTITLE
ZVCARDNAME
ZVCARDSTRING
ZXMPPTHUMBPATH
ZMEDIAKEY
ZMETADATA
The relationship is bidirectional:
ZWAMESSAGE.ZMEDIAITEM -> ZWAMEDIAITEM.Z_PK
ZWAMEDIAITEM.ZMESSAGE -> ZWAMESSAGE.Z_PK
Validate both directions after insertion.
For a native image, WhatsApp stored something like:
ZWAMEDIAITEM.ZMEDIALOCALPATH =
Media/<lid>/5/0/<uuid>.jpg
while the actual MobileBackup relative path was:
Message/Media/<lid>/5/0/<uuid>.jpg
This distinction matters.
In other words:
backup_relative_path = "Message/" + ZMEDIALOCALPATHA native thumbnail followed the same pattern:
ZWAMEDIAITEM.ZXMPPTHUMBPATH =
Media/<lid>/5/0/<uuid>.thumb
Backup path:
Message/Media/<lid>/5/0/<uuid>.thumb
For an image with a caption:
ZWAMESSAGE.ZTEXT = NULL
ZWAMEDIAITEM.ZTITLE = <caption>
ZWAMEDIAITEM.ZVCARDSTRING = image/jpeg
This is easy to miss if you search only ZWAMESSAGE.ZTEXT.
Adding a file into an encrypted iOS backup is not equivalent to dropping a JPEG into a folder.
A MobileBackup entry involves:
Manifest.db row
MBFile / NSKeyedArchiver metadata
fileID
relativePath
flags
file size
digest
protection class
per-file encryption key
wrapped encryption key
encrypted payload
The usual file ID formula is:
sha1(f"{domain}-{relative_path}".encode()).hexdigest()Physical payload location:
<backup>/<first two chars of fileID>/<fileID>
When cloning a native file entry as a template for a new file, updating only:
Size
InodeNumber
EncryptionKey
was not enough.
The archived MBFile object must also contain the correct internal:
RelativePath
Digest
A mismatch can cause MobileBackup2 restore errors.
This was one of the most important failure modes discovered during the migration.
For a minimal WhatsApp App Group restore, do not keep only:
ChatStorage.sqlite
The root App Group directory entry must also remain.
A successful minimal database-only restore required at least:
AppDomainGroup-group.net.whatsapp.WhatsApp.shared
relativePath=""
flags=2
AppDomainGroup-group.net.whatsapp.WhatsApp.shared
relativePath="ChatStorage.sqlite"
flags=1
Dropping the App Group root caused restore failure.
For media, retain all required directory entries such as:
Message
Message/Media
Message/Media/<jid>
Message/Media/<jid>/5
Message/Media/<jid>/5/0
plus each encrypted media file entry.
A partial restore can be initiated through pymobiledevice3.
Conceptually:
await service.restore(
backup_directory=BACKUP,
system=True,
reboot=True,
copy=False,
settings=True,
remove=False,
password=password,
source=UDID,
skip_apps=True,
)Important observations:
A restore performed without reboot appeared to stage the new files but did not make the modified WhatsApp database live.
The modified files became active after reboot.
Therefore the reboot was part of the successful commit path in the tested environment.
The restore may fail if Find My iPhone is enabled.
One observed failure was associated with:
MBErrorDomain/211
Temporarily disabling Find My allowed the restore to proceed.
Remember to enable Find My again after the migration.
A partial restore can cause iOS to boot into Setup Assistant even though the phone was not erased.
In the tested case, the assistant could be completed/skipped and the phone returned to its previous configured state.
This is still an unsupported workflow, so do not assume identical behavior on every iOS version.
Example:
Timeout waiting for SpringBoard notification from SpringBoard
that it's ready for a restore
This may happen when the phone is still stuck in a previous restore state.
A force restart returned the device to a usable state, after which the restore could be retried.
A failed file-transfer operation may be obscured by a secondary exception such as:
KeyError: 22
inside device_link.py.
Do not immediately assume this is the root cause.
Inspect the generated backup metadata and the device state.
Never restore immediately after generating the database.
At minimum:
PRAGMA integrity_check;
PRAGMA foreign_key_check;Also validate programmatically:
[ ] every imported message has the expected chat
[ ] every stanza ID is unique where expected
[ ] every media message points to a real ZWAMEDIAITEM
[ ] every ZWAMEDIAITEM points back to the correct message
[ ] every staged media path exists
[ ] every staged media file has a Manifest.db row
[ ] every Manifest.db file row has a physical encrypted payload
[ ] every required directory is in Manifest.db
[ ] Z_PRIMARYKEY high-water marks are correct
[ ] source backups were not modified
Use separate paths for every stage:
original Android files
│
├── read only
▼
working Android DB
│
▼
working iOS DB
│
▼
final iOS DB
│
▼
media staging
│
▼
new restore backup
Never modify the only copy of:
msgstore.db.crypt15
msgstore.db
original iPhone backup
A future open-source utility should ideally be:
100% local
no cloud processing
no telemetry containing message data
no raw WhatsApp key logging
no backup-password logging
no media uploads
Backup passwords should be entered using a hidden prompt such as Python's:
getpass.getpass()Never place them in command-line arguments or traceback output.
A useful future CLI could look like:
wabridge doctor
wabridge extract-android
wabridge analyze
wabridge build
wabridge verify
wabridge restore
Internally, it should avoid direct Android-SQL → iOS-SQL spaghetti.
A better design is:
Android adapter
│
▼
common internal model
│
├── Chat
├── Participant
├── Message
├── Attachment
├── Group
├── Reaction
└── Quote
│
▼
iOS adapter
This would also make an eventual iPhone → Android direction possible.
WhatsApp changes schemas frequently.
A robust tool should inspect schemas dynamically:
PRAGMA table_info(...)
SELECT * FROM sqlite_master;
SELECT * FROM Z_PRIMARYKEY;Then select a compatibility adapter based on observed columns and structures.
Do not build the entire program around one exact WhatsApp version.
The successful path intentionally did not attempt to perfectly migrate every WhatsApp feature.
Examples that need more work:
view-once media
some private/service media types
reactions
polls
stickers using newer container formats
edited-message history
advanced reply metadata
channels
status content
payments
live locations
every possible system/group event
The goal was preservation of normal chat history, groups, and common media.
This is the section most likely to save somebody time.
You need to understand @lid.
The correct group chat alone is not enough.
ZGROUPMEMBER must identify the sender.
A minimal ZWAMESSAGE worked without fabricating media shells or message-secret data.
Database:
Media/...
Backup:
Message/Media/...
Not in ZWAMESSAGE.ZTEXT.
The Manifest metadata matters just as much as the payload.
Changing only the SQL Files.relativePath is not sufficient.
Keeping only ChatStorage.sqlite can fail.
A reboot was required before WhatsApp used the modified database.
Once the schema is understood, prefer:
offline DB work
offline validation
one final restore
rather than repeated device backups after every experiment.
The proof of concept successfully demonstrated that it is technically possible to:
- decrypt a modern Android WhatsApp backup;
- merge historical chats into an already configured iPhone;
- preserve existing iPhone WhatsApp conversations;
- migrate individual chats;
- migrate group chats;
- preserve group senders;
- migrate common media;
- use an encrypted local iOS backup as the transport;
- perform a targeted restore;
- avoid a factory reset;
- avoid jailbreak/root.
This does not make the procedure officially supported or universally safe.
Useful project directions:
- package the workflow into a Python CLI;
- add schema adapters;
- write automated database validators;
- add synthetic test databases;
- support reactions/replies/polls;
- improve media metadata generation;
- support Android ↔ iOS in both directions;
- build a GUI only after the migration engine is reliable.
The most valuable contribution would probably be a heavily tested compatibility layer rather than a pretty interface.
If you publish your own findings, do not include:
real phone numbers
real LIDs
real group JIDs
device UDIDs
real message text
contact names
actual WhatsApp databases
actual media
WhatsApp E2E backup keys
iPhone backup passwords
decrypted Manifest.db files from real devices
Prefer placeholders:
<PHONE>@s.whatsapp.net
<CONTACT_LID>@lid
<GROUP_ID>@g.us
<IPHONE_UDID>
C:\Users\<user>\
These notes are intended to help open-source research.
If code from third-party projects is copied into a future implementation, follow the license of each upstream project. Some useful iOS/WhatsApp tooling is GPL-licensed while other projects use permissive licenses.
Do not assume that snippets from different projects can be combined into a closed-source product without checking their licenses.
The important conclusion is simple:
Android → already configured iPhone WhatsApp migration without factory reset is possible, but it currently requires understanding both WhatsApp database formats and the encrypted iOS MobileBackup2 format.
The procedure is complex mainly because there is no supported "merge Android WhatsApp history into my existing iPhone WhatsApp" API.
Hopefully these notes save the next person — or the next AI agent — several hours of archaeology.