I Built My Own iMessage Wrapped (And So Can You)
My daughter was joking that someone should build a Spotify Wrapped, but for iMessage β your year in emoji and reactions. My wife said my brother-in-law should build it since he works at Apple. (Every Apple problem is his fault in our house.)
From the other room, my daughter yelled: βI bet Dad could write this!β
Challenge accepted.
The Result
Thirty minutes later, I had a working script. Hereβs my 2025:
π iMessage Wrapped 2025
π± Messages
Total: 35,768
Sent: 12,191
Received: 23,577
π¬ Reactions
Given: 1,974
Received: 5,014
π Your Reaction Style
π 982
β€οΈ 398
π 275
βΌοΈ 126
β 19
π 16
π Messages by Month
Jan βββββββββββββββ 3,052
Feb βββββββββββββ 2,621
Mar βββββββββββββββββ 3,422
Apr ββββββββββββββββββ 3,696
May ββββββββββββββββββ 3,640
Jun ββββββββββββββββββββ 3,904
Jul βββββββββββββ 2,561
Aug ββββββββββββ 2,448
Sep ββββββββββββββ 2,790
Oct βββββββββββββββββ 3,466
Nov βββββββββββ 2,206
Dec ββββββββββ 1,962
Some things I learned about myself:
Iβm a listener. I receive almost 2x as many messages as I send.
People love reacting to me. 5,014 reactions received vs 1,974 given.
Iβm a thumbs-up guy. 
accounts for half my reactions. Apparently Iβm very agreeable.
June was my chattiest month. December was quietest (holiday break mode).How It Works
Your iMessage history lives in a SQLite database at ~/Library/Messages/chat.db. Itβs just sitting there, queryable.
The tricky bits:
Dates are weird. Apple stores timestamps as nanoseconds since January 1, 2001 (because of course they do).
Reactions are messages. When you tapback a

οΈ, itβs stored as a separate message with associated_message_type set to a magic number (2000 = loved, 2001 = liked, etc.).
Custom emoji reactions landed in iOS 17 and are stored in associated_message_emoji.
Once you know the schema, itβs just SQL.
The Script
Hereβs the full thing β about 100 lines of Python, no dependencies beyond the standard library:
#!/usr/bin/env python3
"""
iMessage Wrapped β Your year in emoji and reactions
Usage: python3 imessage-wrapped.py [year]
Requires: Full Disk Access for Terminal
"""
import sqlite3
import os
import sys
from datetime import datetime
from pathlib import Path
YEAR = int(sys.argv[1]) if len(sys.argv) > 1 else 2025
DB_PATH = Path.home() / "Library/Messages/chat.db"
APPLE_EPOCH_OFFSET = 978307200
TAPBACKS = {
2000: "β€οΈ", # Loved
2001: "π", # Liked
2002: "π", # Disliked
2003: "π", # Laughed
2004: "βΌοΈ", # Emphasized
2005: "β", # Questioned
}
def get_db():
if not DB_PATH.exists():
print(f"β Database not found at {DB_PATH}")
sys.exit(1)
return sqlite3.connect(f"file:{DB_PATH}?mode=ro", uri=True)
def date_filter(year):
return f"""
datetime(date/1000000000 + {APPLE_EPOCH_OFFSET}, 'unixepoch') >= '{year}-01-01'
AND datetime(date/1000000000 + {APPLE_EPOCH_OFFSET}, 'unixepoch') < '{year + 1}-01-01'
"""
def main():
print(f"\nπ iMessage Wrapped {YEAR}\n")
db = get_db()
cur = db.cursor()
# Message counts
cur.execute(f"""
SELECT COUNT(*),
SUM(CASE WHEN is_from_me = 1 THEN 1 ELSE 0 END),
SUM(CASE WHEN is_from_me = 0 THEN 1 ELSE 0 END)
FROM message WHERE {date_filter(YEAR)} AND associated_message_type = 0
""")
total, sent, received = cur.fetchone()
print(f"π± Messages: {total:,} ({sent:,} sent, {received:,} received)")
# Reaction counts
cur.execute(f"""
SELECT SUM(CASE WHEN is_from_me = 1 THEN 1 ELSE 0 END),
SUM(CASE WHEN is_from_me = 0 THEN 1 ELSE 0 END)
FROM message WHERE {date_filter(YEAR)} AND associated_message_type >= 2000
""")
given, got = cur.fetchone()
print(f"π¬ Reactions: {given + got:,} ({given:,} given, {got:,} received)")
# Your tapback style
print(f"\nπ Your Reaction Style")
cur.execute(f"""
SELECT associated_message_type, COUNT(*) FROM message
WHERE {date_filter(YEAR)} AND associated_message_type BETWEEN 2000 AND 2005 AND is_from_me = 1
GROUP BY associated_message_type ORDER BY COUNT(*) DESC
""")
for type_id, cnt in cur.fetchall():
print(f" {TAPBACKS.get(type_id, '?')} {cnt:,}")
# Custom emoji
cur.execute(f"""
SELECT associated_message_emoji, COUNT(*) FROM message
WHERE {date_filter(YEAR)} AND associated_message_emoji IS NOT NULL AND is_from_me = 1
GROUP BY associated_message_emoji ORDER BY COUNT(*) DESC LIMIT 5
""")
customs = cur.fetchall()
if customs:
print(f"\nπ― Custom Reactions: {', '.join(f'{e} ({c})' for e, c in customs)}")
# Monthly volume
print(f"\nπ By Month")
cur.execute(f"""
SELECT strftime('%m', datetime(date/1000000000 + {APPLE_EPOCH_OFFSET}, 'unixepoch')), COUNT(*)
FROM message WHERE {date_filter(YEAR)} AND associated_message_type = 0
GROUP BY 1 ORDER BY 1
""")
months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]
data = {row[0]: row[1] for row in cur.fetchall()}
max_cnt = max(data.values()) if data else 1
for i, name in enumerate(months, 1):
cnt = data.get(f"{i:02d}", 0)
bar = "β" * int(20 * cnt / max_cnt)
print(f" {name} {bar} {cnt:,}")
db.close()
if __name__ == "__main__":
main()Running ItSave the script as imessage-wrapped.pyGrant Full Disk Access to your terminal (System Settings β Privacy & Security β Full Disk Access)Run it:python3 imessage-wrapped.py # defaults to 2025
python3 imessage-wrapped.py 2024 # or any year
Thatβs it. Your data never leaves your machine.
What Iβd Add Next
If I turn this into a proper app:
Shareable cards β export your stats as an image
Conversation breakdown β who do you text the most?
Time of day patterns β are you a morning texter or a midnight scroller?
Streak tracking β longest daily conversation streak
But honestly? The script is fun enough. Sometimes a quick hack that makes your daughter laugh is the whole point.
The script and a slightly more polished version are on GitHub if you want to grab it.
#Apple #iMessage #Messages #NerdyStuff #Python