[{"data":1,"prerenderedAt":1762},["ShallowReactive",2],{"blog:post:whatsapp-lids-explained":3},{"id":4,"title":5,"author":6,"body":7,"date":1744,"description":1745,"extension":1746,"links_to":1747,"meta":1752,"navigation":476,"path":1753,"seo":1754,"stem":1755,"tags":1756,"updated":1760,"__hash__":1761},"blog\u002Fblog\u002Fwhatsapp-lids-explained.md","WhatsApp LIDs Explained for Developers","Wapito team",{"type":8,"value":9,"toc":1734},"minimark",[10,29,34,45,64,71,75,78,81,84,88,91,141,144,283,304,308,311,327,333,347,353,364,368,371,415,424,913,920,939,943,946,1045,1048,1296,1299,1326,1343,1668,1672,1679,1707,1711,1714,1730],[11,12,13,14,18,19,23,24,28],"p",{},"For most of WhatsApp's life, a person ",[15,16,17],"em",{},"was"," a phone number. Every message, every group\nparticipant, every read receipt named its sender as ",[20,21,22],"code",{},"\u003Cdigits>@s.whatsapp.net",", and every bot,\nCRM sync and support tool ever written against WhatsApp keyed its database on those digits. That\nassumption is now wrong, and it has been failing quietly in production systems for the better\npart of two years. This post explains the identifier that replaced it — the ",[25,26,27],"strong",{},"LID"," — why\nWhatsApp introduced it, exactly where it shows up, what it breaks, and how to change your data\nmodel so it stops breaking.",[30,31,33],"h2",{"id":32},"what-a-lid-is","What a LID is",[11,35,36,37,40,41,44],{},"A LID is a second identifier for a WhatsApp account. Where the classic address is built from the\nphone number (",[20,38,39],{},"15551234567@s.whatsapp.net","), the LID is an opaque number WhatsApp's servers\nassign to the account, written with its own suffix: ",[20,42,43],{},"187264518273645@lid",". It is stable — the same\naccount has the same LID tomorrow — but it is not derived from the phone number, cannot be turned\nback into one by any arithmetic, and cannot be dialled.",[11,46,47,48,51,52,55,56,59,60,63],{},"Every account has one, including yours: ",[20,49,50],{},"GET \u002Fchannel"," on a Wapito channel returns both ",[20,53,54],{},"phone","\nand ",[20,57,58],{},"lid"," for the linked number. The account did not gain a LID when you noticed it; it always\nhad one. What changed is where WhatsApp ",[15,61,62],{},"uses"," it.",[11,65,66,67,70],{},"The name is usually expanded as ",[15,68,69],{},"linked identity"," — an identity linked to the account rather than\nto the SIM. Meta has never published a specification for it; what follows is what the open-source\nprotocol implementations (whatsmeow, which Wapito's default engine is built on, and Baileys) have\nestablished by observation, and what our own traffic confirms.",[30,72,74],{"id":73},"why-whatsapp-introduced-it","Why WhatsApp introduced it",[11,76,77],{},"WhatsApp spent its first fifteen years exposing every group member's phone number to every other\nmember. Join a neighbourhood group of three hundred people and three hundred strangers can save,\ncall and message you. That was a privacy problem WhatsApp was under growing pressure to fix, and\nit was also an obstacle to two features the product wanted: usernames, so that people can be\nreached without handing out a number at all, and large public surfaces — Channels, community\nannouncement groups — where showing numbers is unthinkable.",[11,79,80],{},"Fixing it required an address that is not the number. Once a participant can be represented by\na LID, WhatsApp can show you a name and a profile photo in a group without ever transmitting the\ndigits behind them, and can let the two of you message each other through the LID alone. Whether\nyou see the number then becomes a privacy setting — roughly, you see it if the person is already\nin your contacts, or has chosen to show it — instead of a fact of the protocol.",[11,82,83],{},"The rollout has been gradual and uneven, which is part of why it hurt. Newer clients started\naddressing group traffic by LID first; individual chats followed for accounts that opted into\nnumber privacy; the servers began returning LIDs in places that used to return numbers. A\nlibrary that worked on Monday started receiving unfamiliar senders on Tuesday, with no\nannouncement.",[30,85,87],{"id":86},"where-it-shows-up","Where it shows up",[11,89,90],{},"Concretely, in the traffic a Wapito channel sees today:",[92,93,94,113,123,129,135],"ul",{},[95,96,97,100,101,104,105,108,109,112],"li",{},[25,98,99],{},"Group messages."," The ",[20,102,103],{},"participant"," of a message in a group — the person who actually sent\nit — frequently arrives as ",[20,106,107],{},"…@lid",". The group itself is still ",[20,110,111],{},"…@g.us",".",[95,114,115,118,119,122],{},[25,116,117],{},"Group participant lists and events."," Listing participants, and the ",[20,120,121],{},"groups.participants","\nevent when someone joins, leaves or is promoted, can name people by LID.",[95,124,125,128],{},[25,126,127],{},"Channel (newsletter) activity."," Reactions and comments on Channel posts are LID-addressed\nby design; there is no number to show.",[95,130,131,134],{},[25,132,133],{},"Individual chats, sometimes."," A person who has switched on number privacy can message you\nfrom a LID even one-to-one. You can reply to it — the LID is a perfectly good address — but you\nmay never learn the number.",[95,136,137,140],{},[25,138,139],{},"Your own contact list."," The engine's contact store now carries both identifiers for the\ncontacts it knows, and the mapping between them fills in as traffic flows.",[11,142,143],{},"And in the Wapito message object, that becomes three fields instead of one:",[145,146,151],"pre",{"className":147,"code":148,"language":149,"meta":150,"style":150},"language-json shiki shiki-themes github-light-default github-dark-default","{\n  \"id\": \"false_120363012345678901@g.us_3EB0C767D82B0A1E4F2B\",\n  \"chat_id\": \"120363012345678901@g.us\",\n  \"from\": \"15551234567\",\n  \"from_lid\": \"187264518273645@lid\",\n  \"participant\": \"187264518273645@lid\",\n  \"from_me\": false,\n  \"type\": \"text\",\n  \"text\": { \"body\": \"Is the venue confirmed for Saturday?\" }\n}\n","json","",[20,152,153,162,179,192,205,218,230,244,257,277],{"__ignoreMap":150},[154,155,158],"span",{"class":156,"line":157},"line",1,[154,159,161],{"class":160},"s4rv2","{\n",[154,163,165,169,172,176],{"class":156,"line":164},2,[154,166,168],{"class":167},"sjgCt","  \"id\"",[154,170,171],{"class":160},": ",[154,173,175],{"class":174},"sSVrQ","\"false_120363012345678901@g.us_3EB0C767D82B0A1E4F2B\"",[154,177,178],{"class":160},",\n",[154,180,182,185,187,190],{"class":156,"line":181},3,[154,183,184],{"class":167},"  \"chat_id\"",[154,186,171],{"class":160},[154,188,189],{"class":174},"\"120363012345678901@g.us\"",[154,191,178],{"class":160},[154,193,195,198,200,203],{"class":156,"line":194},4,[154,196,197],{"class":167},"  \"from\"",[154,199,171],{"class":160},[154,201,202],{"class":174},"\"15551234567\"",[154,204,178],{"class":160},[154,206,208,211,213,216],{"class":156,"line":207},5,[154,209,210],{"class":167},"  \"from_lid\"",[154,212,171],{"class":160},[154,214,215],{"class":174},"\"187264518273645@lid\"",[154,217,178],{"class":160},[154,219,221,224,226,228],{"class":156,"line":220},6,[154,222,223],{"class":167},"  \"participant\"",[154,225,171],{"class":160},[154,227,215],{"class":174},[154,229,178],{"class":160},[154,231,233,236,238,242],{"class":156,"line":232},7,[154,234,235],{"class":167},"  \"from_me\"",[154,237,171],{"class":160},[154,239,241],{"class":240},"sHrmB","false",[154,243,178],{"class":160},[154,245,247,250,252,255],{"class":156,"line":246},8,[154,248,249],{"class":167},"  \"type\"",[154,251,171],{"class":160},[154,253,254],{"class":174},"\"text\"",[154,256,178],{"class":160},[154,258,260,263,266,269,271,274],{"class":156,"line":259},9,[154,261,262],{"class":167},"  \"text\"",[154,264,265],{"class":160},": { ",[154,267,268],{"class":167},"\"body\"",[154,270,171],{"class":160},[154,272,273],{"class":174},"\"Is the venue confirmed for Saturday?\"",[154,275,276],{"class":160}," }\n",[154,278,280],{"class":156,"line":279},10,[154,281,282],{"class":160},"}\n",[11,284,285,287,288,291,292,295,296,299,300,303],{},[20,286,103],{}," is the address the message arrived with. ",[20,289,290],{},"from_lid"," is that address when it was a\nLID. ",[20,293,294],{},"from"," is the phone number in digits — ",[25,297,298],{},"when Wapito could resolve it",", and ",[20,301,302],{},"null"," when it\ncould not. That last clause is the whole migration.",[30,305,307],{"id":306},"what-breaks","What breaks",[11,309,310],{},"If your system stores WhatsApp users by phone number, four things go wrong, in roughly this order\nof how quickly you notice them.",[11,312,313,316,317,319,320,322,323,326],{},[25,314,315],{},"Lookups miss."," A ",[20,318,121],{}," event says ",[20,321,43],{}," joined your customer\ngroup. Your ",[20,324,325],{},"SELECT … WHERE phone = ?"," finds nothing, and the welcome flow, the CRM tag or the\nseat count never fires. Nothing errors; the row simply is not there.",[11,328,329,332],{},[25,330,331],{},"People appear twice."," The same customer messages you one-to-one (you see the number) and posts\nin your group (you see the LID). Two rows, two histories, two \"first seen\" dates, and a support\nagent who cannot see that they are one person.",[11,334,335,338,339,342,343,346],{},[25,336,337],{},"Replies go to the wrong key."," Code that takes a sender, strips the suffix and treats the rest\nas a phone number will try to message ",[20,340,341],{},"187264518273645"," — which is not a phone number — or, worse,\nwill pass it to a number-formatting library that \"corrects\" it into a real number belonging to\nsomeone else. Wapito refuses that send with ",[20,344,345],{},"400 invalid_recipient",", but a library that does not\nvalidate will happily try.",[11,348,349,352],{},[25,350,351],{},"Reports and dedupe drift."," Anything counted per person — unique senders, opt-outs, rate\nlimits you keep on your side — over-counts, and an opt-out recorded against a number does not\nmatch a later message from the same person's LID.",[11,354,355,356,359,360,363],{},"The pattern under all four: the phone number was doing two jobs, ",[15,357,358],{},"identity"," and ",[15,361,362],{},"address",", and\nWhatsApp has split them. The LID is now the durable identity; the phone number is an attribute of\nit that may or may not be visible to you.",[30,365,367],{"id":366},"how-wapito-maps-them","How Wapito maps them",[11,369,370],{},"Wapito's job is to hand you a phone number whenever one can honestly be known, and to say so\nclearly when it cannot. Every inbound message goes through the same resolution chain:",[372,373,374,383,397,403],"ol",{},[95,375,376,379,380,382],{},[25,377,378],{},"The engine's own answer."," Recent versions of whatsmeow expose a sender-alternate field: the\nphone-number address of a LID sender, when the server included it. When it is there, ",[20,381,294],{}," is\nfilled from it and nothing else is needed.",[95,384,385,388,389,392,393,396],{},[25,386,387],{},"The cached mapping."," Every resolved pair is remembered per channel. A sender seen once is\nknown from then on, and the ",[20,390,391],{},"contacts"," webhook event fires the first time a LID is resolved —\n",[20,394,395],{},"{ \"action\": \"…\", \"id\": \"15551234567@s.whatsapp.net\", \"phone\": \"15551234567\", \"lid\": \"187264518273645@lid\", \"name\": \"Dana\" }","\n— so your side can learn the mapping at the same moment we do.",[95,398,399,402],{},[25,400,401],{},"A live lookup."," If neither is available, Wapito asks the engine, which asks WhatsApp, and\ncaches the result.",[95,404,405,408,409,359,412,414],{},[25,406,407],{},"Honesty."," If all three fail — typically because the account hides its number — the event\nships with ",[20,410,411],{},"from: null",[20,413,290],{}," set. The message is not delayed and nothing is\ninvented.",[11,416,417,418,423],{},"The same mapping is exposed as endpoints, so you can resolve on demand rather than waiting for\ntraffic. Both are covered on the ",[419,420,422],"a",{"href":421},"\u002Fwhatsapp-api\u002Flid-to-phone\u002F","LID to phone"," pillar:",[425,426,427,502,676],"code-group",{},[145,428,433],{"className":429,"code":430,"filename":431,"language":432,"meta":150,"style":150},"language-bash shiki shiki-themes github-light-default github-dark-default","# Phone number → LID\ncurl https:\u002F\u002Fapi.wapito.com\u002Fv1\u002Fcontacts\u002F+15551234567\u002Flid \\\n  -H \"Authorization: Bearer $WAPITO_TOKEN\"\n# {\"phone\": \"15551234567\", \"lid\": \"187264518273645@lid\"}\n\n# LID → contact (with the phone number when it can be known)\ncurl https:\u002F\u002Fapi.wapito.com\u002Fv1\u002Fcontacts\u002Flid\u002F187264518273645@lid \\\n  -H \"Authorization: Bearer $WAPITO_TOKEN\"\n","curl","bash",[20,434,435,441,453,467,472,478,483,492],{"__ignoreMap":150},[154,436,437],{"class":156,"line":157},[154,438,440],{"class":439},"sU953","# Phone number → LID\n",[154,442,443,446,449],{"class":156,"line":164},[154,444,431],{"class":445},"sTDnQ",[154,447,448],{"class":174}," https:\u002F\u002Fapi.wapito.com\u002Fv1\u002Fcontacts\u002F+15551234567\u002Flid",[154,450,452],{"class":451},"sjeE4"," \\\n",[154,454,455,458,461,464],{"class":156,"line":181},[154,456,457],{"class":240},"  -H",[154,459,460],{"class":174}," \"Authorization: Bearer ",[154,462,463],{"class":160},"$WAPITO_TOKEN",[154,465,466],{"class":174},"\"\n",[154,468,469],{"class":156,"line":194},[154,470,471],{"class":439},"# {\"phone\": \"15551234567\", \"lid\": \"187264518273645@lid\"}\n",[154,473,474],{"class":156,"line":207},[154,475,477],{"emptyLinePlaceholder":476},true,"\n",[154,479,480],{"class":156,"line":220},[154,481,482],{"class":439},"# LID → contact (with the phone number when it can be known)\n",[154,484,485,487,490],{"class":156,"line":232},[154,486,431],{"class":445},[154,488,489],{"class":174}," https:\u002F\u002Fapi.wapito.com\u002Fv1\u002Fcontacts\u002Flid\u002F187264518273645@lid",[154,491,452],{"class":451},[154,493,494,496,498,500],{"class":156,"line":246},[154,495,457],{"class":240},[154,497,460],{"class":174},[154,499,463],{"class":160},[154,501,466],{"class":174},[145,503,508],{"className":504,"code":505,"filename":506,"language":507,"meta":150,"style":150},"language-javascript shiki shiki-themes github-light-default github-dark-default","const headers = { Authorization: `Bearer ${process.env.WAPITO_TOKEN}` };\n\nconst byPhone = await fetch('https:\u002F\u002Fapi.wapito.com\u002Fv1\u002Fcontacts\u002F+15551234567\u002Flid', { headers });\nconsole.log(await byPhone.json()); \u002F\u002F { phone: '15551234567', lid: '187264518273645@lid' }\n\nconst byLid = await fetch('https:\u002F\u002Fapi.wapito.com\u002Fv1\u002Fcontacts\u002Flid\u002F187264518273645@lid', { headers });\nif (byLid.status === 404) console.log('this account hides its number'); \u002F\u002F keep the LID as the key\nelse console.log((await byLid.json()).phone);\n","Node","javascript",[20,509,510,546,550,575,599,603,623,653],{"__ignoreMap":150},[154,511,512,515,518,521,524,527,530,532,535,537,540,543],{"class":156,"line":157},[154,513,514],{"class":451},"const",[154,516,517],{"class":240}," headers",[154,519,520],{"class":451}," =",[154,522,523],{"class":160}," { Authorization: ",[154,525,526],{"class":174},"`Bearer ${",[154,528,529],{"class":160},"process",[154,531,112],{"class":174},[154,533,534],{"class":160},"env",[154,536,112],{"class":174},[154,538,539],{"class":240},"WAPITO_TOKEN",[154,541,542],{"class":174},"}`",[154,544,545],{"class":160}," };\n",[154,547,548],{"class":156,"line":164},[154,549,477],{"emptyLinePlaceholder":476},[154,551,552,554,557,559,562,566,569,572],{"class":156,"line":181},[154,553,514],{"class":451},[154,555,556],{"class":240}," byPhone",[154,558,520],{"class":451},[154,560,561],{"class":451}," await",[154,563,565],{"class":564},"sbjLL"," fetch",[154,567,568],{"class":160},"(",[154,570,571],{"class":174},"'https:\u002F\u002Fapi.wapito.com\u002Fv1\u002Fcontacts\u002F+15551234567\u002Flid'",[154,573,574],{"class":160},", { headers });\n",[154,576,577,580,583,585,588,591,593,596],{"class":156,"line":194},[154,578,579],{"class":160},"console.",[154,581,582],{"class":564},"log",[154,584,568],{"class":160},[154,586,587],{"class":451},"await",[154,589,590],{"class":160}," byPhone.",[154,592,149],{"class":564},[154,594,595],{"class":160},"()); ",[154,597,598],{"class":439},"\u002F\u002F { phone: '15551234567', lid: '187264518273645@lid' }\n",[154,600,601],{"class":156,"line":207},[154,602,477],{"emptyLinePlaceholder":476},[154,604,605,607,610,612,614,616,618,621],{"class":156,"line":220},[154,606,514],{"class":451},[154,608,609],{"class":240}," byLid",[154,611,520],{"class":451},[154,613,561],{"class":451},[154,615,565],{"class":564},[154,617,568],{"class":160},[154,619,620],{"class":174},"'https:\u002F\u002Fapi.wapito.com\u002Fv1\u002Fcontacts\u002Flid\u002F187264518273645@lid'",[154,622,574],{"class":160},[154,624,625,628,631,634,637,640,642,644,647,650],{"class":156,"line":232},[154,626,627],{"class":451},"if",[154,629,630],{"class":160}," (byLid.status ",[154,632,633],{"class":451},"===",[154,635,636],{"class":240}," 404",[154,638,639],{"class":160},") console.",[154,641,582],{"class":564},[154,643,568],{"class":160},[154,645,646],{"class":174},"'this account hides its number'",[154,648,649],{"class":160},"); ",[154,651,652],{"class":439},"\u002F\u002F keep the LID as the key\n",[154,654,655,658,661,663,666,668,671,673],{"class":156,"line":246},[154,656,657],{"class":451},"else",[154,659,660],{"class":160}," console.",[154,662,582],{"class":564},[154,664,665],{"class":160},"((",[154,667,587],{"class":451},[154,669,670],{"class":160}," byLid.",[154,672,149],{"class":564},[154,674,675],{"class":160},"()).phone);\n",[145,677,682],{"className":678,"code":679,"filename":680,"language":681,"meta":150,"style":150},"language-python shiki shiki-themes github-light-default github-dark-default","import os\nimport requests\n\nheaders = {\"Authorization\": f\"Bearer {os.environ['WAPITO_TOKEN']}\"}\nbase = \"https:\u002F\u002Fapi.wapito.com\u002Fv1\"\n\nprint(requests.get(f\"{base}\u002Fcontacts\u002F+15551234567\u002Flid\", headers=headers, timeout=(5, 30)).json())\n\nres = requests.get(f\"{base}\u002Fcontacts\u002Flid\u002F187264518273645@lid\", headers=headers, timeout=(5, 30))\nif res.status_code == 404:\n    print(\"this account hides its number\")  # keep the LID as the key\nelse:\n    print(res.json()[\"phone\"])\n","Python","python",[20,683,684,692,699,703,745,755,759,810,814,860,875,892,899],{"__ignoreMap":150},[154,685,686,689],{"class":156,"line":157},[154,687,688],{"class":451},"import",[154,690,691],{"class":160}," os\n",[154,693,694,696],{"class":156,"line":164},[154,695,688],{"class":451},[154,697,698],{"class":160}," requests\n",[154,700,701],{"class":156,"line":181},[154,702,477],{"emptyLinePlaceholder":476},[154,704,705,708,711,714,717,719,722,725,728,731,734,737,740,743],{"class":156,"line":194},[154,706,707],{"class":160},"headers ",[154,709,710],{"class":451},"=",[154,712,713],{"class":160}," {",[154,715,716],{"class":174},"\"Authorization\"",[154,718,171],{"class":160},[154,720,721],{"class":451},"f",[154,723,724],{"class":174},"\"Bearer ",[154,726,727],{"class":451},"{",[154,729,730],{"class":160},"os.environ[",[154,732,733],{"class":174},"'WAPITO_TOKEN'",[154,735,736],{"class":160},"]",[154,738,739],{"class":451},"}",[154,741,742],{"class":174},"\"",[154,744,282],{"class":160},[154,746,747,750,752],{"class":156,"line":207},[154,748,749],{"class":160},"base ",[154,751,710],{"class":451},[154,753,754],{"class":174}," \"https:\u002F\u002Fapi.wapito.com\u002Fv1\"\n",[154,756,757],{"class":156,"line":220},[154,758,477],{"emptyLinePlaceholder":476},[154,760,761,764,767,769,771,773,776,778,781,784,787,789,792,795,797,799,802,804,807],{"class":156,"line":232},[154,762,763],{"class":240},"print",[154,765,766],{"class":160},"(requests.get(",[154,768,721],{"class":451},[154,770,742],{"class":174},[154,772,727],{"class":451},[154,774,775],{"class":160},"base",[154,777,739],{"class":451},[154,779,780],{"class":174},"\u002Fcontacts\u002F+15551234567\u002Flid\"",[154,782,783],{"class":160},", ",[154,785,786],{"class":445},"headers",[154,788,710],{"class":451},[154,790,791],{"class":160},"headers, ",[154,793,794],{"class":445},"timeout",[154,796,710],{"class":451},[154,798,568],{"class":160},[154,800,801],{"class":240},"5",[154,803,783],{"class":160},[154,805,806],{"class":240},"30",[154,808,809],{"class":160},")).json())\n",[154,811,812],{"class":156,"line":246},[154,813,477],{"emptyLinePlaceholder":476},[154,815,816,819,821,824,826,828,830,832,834,837,839,841,843,845,847,849,851,853,855,857],{"class":156,"line":259},[154,817,818],{"class":160},"res ",[154,820,710],{"class":451},[154,822,823],{"class":160}," requests.get(",[154,825,721],{"class":451},[154,827,742],{"class":174},[154,829,727],{"class":451},[154,831,775],{"class":160},[154,833,739],{"class":451},[154,835,836],{"class":174},"\u002Fcontacts\u002Flid\u002F187264518273645@lid\"",[154,838,783],{"class":160},[154,840,786],{"class":445},[154,842,710],{"class":451},[154,844,791],{"class":160},[154,846,794],{"class":445},[154,848,710],{"class":451},[154,850,568],{"class":160},[154,852,801],{"class":240},[154,854,783],{"class":160},[154,856,806],{"class":240},[154,858,859],{"class":160},"))\n",[154,861,862,864,867,870,872],{"class":156,"line":279},[154,863,627],{"class":451},[154,865,866],{"class":160}," res.status_code ",[154,868,869],{"class":451},"==",[154,871,636],{"class":240},[154,873,874],{"class":160},":\n",[154,876,878,881,883,886,889],{"class":156,"line":877},11,[154,879,880],{"class":240},"    print",[154,882,568],{"class":160},[154,884,885],{"class":174},"\"this account hides its number\"",[154,887,888],{"class":160},")  ",[154,890,891],{"class":439},"# keep the LID as the key\n",[154,893,895,897],{"class":156,"line":894},12,[154,896,657],{"class":451},[154,898,874],{"class":160},[154,900,902,904,907,910],{"class":156,"line":901},13,[154,903,880],{"class":240},[154,905,906],{"class":160},"(res.json()[",[154,908,909],{"class":174},"\"phone\"",[154,911,912],{"class":160},"])\n",[11,914,915,916,919],{},"A LID-to-phone lookup that answers ",[20,917,918],{},"404 not_found"," is not an error in your code. It is the\nprotocol telling you that this person has chosen not to be a phone number to you, and the right\nresponse is to carry on with the LID.",[11,921,922,923,927,928,930,931,934,935,938],{},"Two more places the mapping surfaces: the bulk ",[419,924,926],{"href":925},"\u002Fwhatsapp-api\u002Fcheck-number\u002F","number check","\nreturns a ",[20,929,58],{}," beside each ",[20,932,933],{},"jid",", so a contact import can learn both identifiers in one paced\nrequest; and every send endpoint accepts a LID in ",[20,936,937],{},"to",", so replying to a LID-only sender is the\nsame call as replying to anyone else.",[30,940,942],{"id":941},"re-keying-your-data","Re-keying your data",[11,944,945],{},"The fix is a schema change, not a code patch. Treat the WhatsApp account as the entity and both\nidentifiers as nullable, unique attributes of it:",[145,947,951],{"className":948,"code":949,"language":950,"meta":150,"style":150},"language-sql shiki shiki-themes github-light-default github-dark-default","CREATE TABLE whatsapp_contacts (\n  id          BIGSERIAL PRIMARY KEY,\n  lid         TEXT UNIQUE,          -- '187264518273645@lid', durable\n  phone       TEXT UNIQUE,          -- '15551234567', may be unknown\n  display_name TEXT,\n  first_seen  TIMESTAMPTZ NOT NULL DEFAULT now()\n);\n","sql",[20,952,953,967,980,997,1011,1020,1040],{"__ignoreMap":150},[154,954,955,958,961,964],{"class":156,"line":157},[154,956,957],{"class":451},"CREATE",[154,959,960],{"class":451}," TABLE",[154,962,963],{"class":564}," whatsapp_contacts",[154,965,966],{"class":160}," (\n",[154,968,969,972,975,978],{"class":156,"line":164},[154,970,971],{"class":160},"  id          ",[154,973,974],{"class":451},"BIGSERIAL",[154,976,977],{"class":451}," PRIMARY KEY",[154,979,178],{"class":160},[154,981,982,985,988,991,994],{"class":156,"line":181},[154,983,984],{"class":160},"  lid         ",[154,986,987],{"class":451},"TEXT",[154,989,990],{"class":451}," UNIQUE",[154,992,993],{"class":160},",          ",[154,995,996],{"class":439},"-- '187264518273645@lid', durable\n",[154,998,999,1002,1004,1006,1008],{"class":156,"line":194},[154,1000,1001],{"class":160},"  phone       ",[154,1003,987],{"class":451},[154,1005,990],{"class":451},[154,1007,993],{"class":160},[154,1009,1010],{"class":439},"-- '15551234567', may be unknown\n",[154,1012,1013,1016,1018],{"class":156,"line":207},[154,1014,1015],{"class":160},"  display_name ",[154,1017,987],{"class":451},[154,1019,178],{"class":160},[154,1021,1022,1025,1028,1031,1034,1037],{"class":156,"line":220},[154,1023,1024],{"class":160},"  first_seen  ",[154,1026,1027],{"class":451},"TIMESTAMPTZ",[154,1029,1030],{"class":451}," NOT NULL",[154,1032,1033],{"class":451}," DEFAULT",[154,1035,1036],{"class":451}," now",[154,1038,1039],{"class":160},"()\n",[154,1041,1042],{"class":156,"line":232},[154,1043,1044],{"class":160},");\n",[11,1046,1047],{},"Then, on every inbound event, upsert by whichever identifiers are present and merge when a new\nevent supplies the half you were missing:",[145,1049,1051],{"className":504,"code":1050,"language":507,"meta":150,"style":150},"\u002F\u002F Runs from the webhook worker, after the signature check (see \u002Fdocs\u002Fwebhooks\u002F).\nasync function upsertContact(db, message) {\n  const lid = message.from_lid ?? null;\n  const phone = message.from ?? null;\n\n  \u002F\u002F A message that carries both is the moment two half-rows become one person.\n  const existing = await db.oneOrNone(\n    'SELECT * FROM whatsapp_contacts WHERE lid = $1 OR phone = $2 ORDER BY first_seen LIMIT 1',\n    [lid, phone],\n  );\n\n  if (!existing) {\n    return db.one(\n      'INSERT INTO whatsapp_contacts (lid, phone, display_name) VALUES ($1, $2, $3) RETURNING *',\n      [lid, phone, message.from_name ?? null],\n    );\n  }\n\n  return db.one(\n    `UPDATE whatsapp_contacts\n        SET lid = COALESCE(lid, $2), phone = COALESCE(phone, $3),\n            display_name = COALESCE($4, display_name)\n      WHERE id = $1 RETURNING *`,\n    [existing.id, lid, phone, message.from_name ?? null],\n  );\n}\n",[20,1052,1053,1058,1082,1104,1122,1126,1131,1151,1158,1163,1168,1172,1186,1198,1206,1219,1225,1231,1236,1248,1254,1260,1266,1274,1286,1291],{"__ignoreMap":150},[154,1054,1055],{"class":156,"line":157},[154,1056,1057],{"class":439},"\u002F\u002F Runs from the webhook worker, after the signature check (see \u002Fdocs\u002Fwebhooks\u002F).\n",[154,1059,1060,1063,1066,1069,1071,1074,1076,1079],{"class":156,"line":164},[154,1061,1062],{"class":451},"async",[154,1064,1065],{"class":451}," function",[154,1067,1068],{"class":564}," upsertContact",[154,1070,568],{"class":160},[154,1072,1073],{"class":445},"db",[154,1075,783],{"class":160},[154,1077,1078],{"class":445},"message",[154,1080,1081],{"class":160},") {\n",[154,1083,1084,1087,1090,1092,1095,1098,1101],{"class":156,"line":181},[154,1085,1086],{"class":451},"  const",[154,1088,1089],{"class":240}," lid",[154,1091,520],{"class":451},[154,1093,1094],{"class":160}," message.from_lid ",[154,1096,1097],{"class":451},"??",[154,1099,1100],{"class":240}," null",[154,1102,1103],{"class":160},";\n",[154,1105,1106,1108,1111,1113,1116,1118,1120],{"class":156,"line":194},[154,1107,1086],{"class":451},[154,1109,1110],{"class":240}," phone",[154,1112,520],{"class":451},[154,1114,1115],{"class":160}," message.from ",[154,1117,1097],{"class":451},[154,1119,1100],{"class":240},[154,1121,1103],{"class":160},[154,1123,1124],{"class":156,"line":207},[154,1125,477],{"emptyLinePlaceholder":476},[154,1127,1128],{"class":156,"line":220},[154,1129,1130],{"class":439},"  \u002F\u002F A message that carries both is the moment two half-rows become one person.\n",[154,1132,1133,1135,1138,1140,1142,1145,1148],{"class":156,"line":232},[154,1134,1086],{"class":451},[154,1136,1137],{"class":240}," existing",[154,1139,520],{"class":451},[154,1141,561],{"class":451},[154,1143,1144],{"class":160}," db.",[154,1146,1147],{"class":564},"oneOrNone",[154,1149,1150],{"class":160},"(\n",[154,1152,1153,1156],{"class":156,"line":246},[154,1154,1155],{"class":174},"    'SELECT * FROM whatsapp_contacts WHERE lid = $1 OR phone = $2 ORDER BY first_seen LIMIT 1'",[154,1157,178],{"class":160},[154,1159,1160],{"class":156,"line":259},[154,1161,1162],{"class":160},"    [lid, phone],\n",[154,1164,1165],{"class":156,"line":279},[154,1166,1167],{"class":160},"  );\n",[154,1169,1170],{"class":156,"line":877},[154,1171,477],{"emptyLinePlaceholder":476},[154,1173,1174,1177,1180,1183],{"class":156,"line":894},[154,1175,1176],{"class":451},"  if",[154,1178,1179],{"class":160}," (",[154,1181,1182],{"class":451},"!",[154,1184,1185],{"class":160},"existing) {\n",[154,1187,1188,1191,1193,1196],{"class":156,"line":901},[154,1189,1190],{"class":451},"    return",[154,1192,1144],{"class":160},[154,1194,1195],{"class":564},"one",[154,1197,1150],{"class":160},[154,1199,1201,1204],{"class":156,"line":1200},14,[154,1202,1203],{"class":174},"      'INSERT INTO whatsapp_contacts (lid, phone, display_name) VALUES ($1, $2, $3) RETURNING *'",[154,1205,178],{"class":160},[154,1207,1209,1212,1214,1216],{"class":156,"line":1208},15,[154,1210,1211],{"class":160},"      [lid, phone, message.from_name ",[154,1213,1097],{"class":451},[154,1215,1100],{"class":240},[154,1217,1218],{"class":160},"],\n",[154,1220,1222],{"class":156,"line":1221},16,[154,1223,1224],{"class":160},"    );\n",[154,1226,1228],{"class":156,"line":1227},17,[154,1229,1230],{"class":160},"  }\n",[154,1232,1234],{"class":156,"line":1233},18,[154,1235,477],{"emptyLinePlaceholder":476},[154,1237,1239,1242,1244,1246],{"class":156,"line":1238},19,[154,1240,1241],{"class":451},"  return",[154,1243,1144],{"class":160},[154,1245,1195],{"class":564},[154,1247,1150],{"class":160},[154,1249,1251],{"class":156,"line":1250},20,[154,1252,1253],{"class":174},"    `UPDATE whatsapp_contacts\n",[154,1255,1257],{"class":156,"line":1256},21,[154,1258,1259],{"class":174},"        SET lid = COALESCE(lid, $2), phone = COALESCE(phone, $3),\n",[154,1261,1263],{"class":156,"line":1262},22,[154,1264,1265],{"class":174},"            display_name = COALESCE($4, display_name)\n",[154,1267,1269,1272],{"class":156,"line":1268},23,[154,1270,1271],{"class":174},"      WHERE id = $1 RETURNING *`",[154,1273,178],{"class":160},[154,1275,1277,1280,1282,1284],{"class":156,"line":1276},24,[154,1278,1279],{"class":160},"    [existing.id, lid, phone, message.from_name ",[154,1281,1097],{"class":451},[154,1283,1100],{"class":240},[154,1285,1218],{"class":160},[154,1287,1289],{"class":156,"line":1288},25,[154,1290,1167],{"class":160},[154,1292,1294],{"class":156,"line":1293},26,[154,1295,282],{"class":160},[11,1297,1298],{},"Three rules make this hold up:",[92,1300,1301,1307,1313],{},[95,1302,1303,1306],{},[25,1304,1305],{},"The LID is the primary identity when you have it."," It is the one that will still be there\nwhen the number is hidden. Key opt-outs, rate limits and conversation history on the account\nrow, never on the phone column.",[95,1308,1309,1312],{},[25,1310,1311],{},"Never derive one identifier from the other."," No arithmetic, no \"strip the suffix\". A LID is\na string that happens to contain digits.",[95,1314,1315,1318,1319,1321,1322,1325],{},[25,1316,1317],{},"Store the address you will reply to."," Keep the exact ",[20,1320,103],{}," or ",[20,1323,1324],{},"chat_id"," you received.\nSending to it works whether it is a number or a LID; reconstructing it does not.",[11,1327,1328,1329,1332,1333,1336,1337,1339,1340,1342],{},"For the backfill, walk your existing phone numbers through ",[20,1330,1331],{},"POST \u002Fcontacts\u002Fcheck"," in batches of\nfifty — it costs one check-quota unit per number, so read ",[20,1334,1335],{},"GET \u002Fchannel\u002Flimits"," first and spread\nthe run over days on Sandbox — and store the ",[20,1338,58],{}," that comes back. Then subscribe to the\n",[20,1341,391],{}," event so the table keeps itself current.",[145,1344,1346],{"className":678,"code":1345,"language":681,"meta":150,"style":150},"# Backfill LIDs for numbers you already hold, fifty at a time, inside the daily check quota.\nimport os, time, requests\n\nheaders = {\"Authorization\": f\"Bearer {os.environ['WAPITO_TOKEN']}\"}\nlimits = requests.get(\"https:\u002F\u002Fapi.wapito.com\u002Fv1\u002Fchannel\u002Flimits\", headers=headers, timeout=(5, 30)).json()\nbudget = (limits[\"number_checks\"][\"cap\"] or 10**9) - limits[\"number_checks\"][\"used\"]\n\nfor batch in chunks(phones_without_lid, 50):\n    if budget \u003C len(batch):\n        break  # tomorrow's run picks up here\n    res = requests.post(\"https:\u002F\u002Fapi.wapito.com\u002Fv1\u002Fcontacts\u002Fcheck\", headers=headers,\n                        json={\"phones\": batch}, timeout=(5, 60))\n    res.raise_for_status()\n    budget -= len(batch)\n    for row in res.json()[\"results\"]:\n        if row[\"exists\"] and row[\"lid\"]:\n            save_lid(row[\"phone\"], row[\"lid\"])\n    time.sleep(2)\n",[20,1347,1348,1353,1360,1364,1394,1429,1482,1486,1506,1523,1531,1553,1583,1588,1601,1620,1643,1657],{"__ignoreMap":150},[154,1349,1350],{"class":156,"line":157},[154,1351,1352],{"class":439},"# Backfill LIDs for numbers you already hold, fifty at a time, inside the daily check quota.\n",[154,1354,1355,1357],{"class":156,"line":164},[154,1356,688],{"class":451},[154,1358,1359],{"class":160}," os, time, requests\n",[154,1361,1362],{"class":156,"line":181},[154,1363,477],{"emptyLinePlaceholder":476},[154,1365,1366,1368,1370,1372,1374,1376,1378,1380,1382,1384,1386,1388,1390,1392],{"class":156,"line":194},[154,1367,707],{"class":160},[154,1369,710],{"class":451},[154,1371,713],{"class":160},[154,1373,716],{"class":174},[154,1375,171],{"class":160},[154,1377,721],{"class":451},[154,1379,724],{"class":174},[154,1381,727],{"class":451},[154,1383,730],{"class":160},[154,1385,733],{"class":174},[154,1387,736],{"class":160},[154,1389,739],{"class":451},[154,1391,742],{"class":174},[154,1393,282],{"class":160},[154,1395,1396,1399,1401,1403,1406,1408,1410,1412,1414,1416,1418,1420,1422,1424,1426],{"class":156,"line":207},[154,1397,1398],{"class":160},"limits ",[154,1400,710],{"class":451},[154,1402,823],{"class":160},[154,1404,1405],{"class":174},"\"https:\u002F\u002Fapi.wapito.com\u002Fv1\u002Fchannel\u002Flimits\"",[154,1407,783],{"class":160},[154,1409,786],{"class":445},[154,1411,710],{"class":451},[154,1413,791],{"class":160},[154,1415,794],{"class":445},[154,1417,710],{"class":451},[154,1419,568],{"class":160},[154,1421,801],{"class":240},[154,1423,783],{"class":160},[154,1425,806],{"class":240},[154,1427,1428],{"class":160},")).json()\n",[154,1430,1431,1434,1436,1439,1442,1445,1448,1451,1454,1457,1460,1463,1466,1469,1472,1474,1476,1479],{"class":156,"line":220},[154,1432,1433],{"class":160},"budget ",[154,1435,710],{"class":451},[154,1437,1438],{"class":160}," (limits[",[154,1440,1441],{"class":174},"\"number_checks\"",[154,1443,1444],{"class":160},"][",[154,1446,1447],{"class":174},"\"cap\"",[154,1449,1450],{"class":160},"] ",[154,1452,1453],{"class":451},"or",[154,1455,1456],{"class":240}," 10",[154,1458,1459],{"class":451},"**",[154,1461,1462],{"class":240},"9",[154,1464,1465],{"class":160},") ",[154,1467,1468],{"class":451},"-",[154,1470,1471],{"class":160}," limits[",[154,1473,1441],{"class":174},[154,1475,1444],{"class":160},[154,1477,1478],{"class":174},"\"used\"",[154,1480,1481],{"class":160},"]\n",[154,1483,1484],{"class":156,"line":232},[154,1485,477],{"emptyLinePlaceholder":476},[154,1487,1488,1491,1494,1497,1500,1503],{"class":156,"line":246},[154,1489,1490],{"class":451},"for",[154,1492,1493],{"class":160}," batch ",[154,1495,1496],{"class":451},"in",[154,1498,1499],{"class":160}," chunks(phones_without_lid, ",[154,1501,1502],{"class":240},"50",[154,1504,1505],{"class":160},"):\n",[154,1507,1508,1511,1514,1517,1520],{"class":156,"line":259},[154,1509,1510],{"class":451},"    if",[154,1512,1513],{"class":160}," budget ",[154,1515,1516],{"class":451},"\u003C",[154,1518,1519],{"class":240}," len",[154,1521,1522],{"class":160},"(batch):\n",[154,1524,1525,1528],{"class":156,"line":279},[154,1526,1527],{"class":451},"        break",[154,1529,1530],{"class":439},"  # tomorrow's run picks up here\n",[154,1532,1533,1536,1538,1541,1544,1546,1548,1550],{"class":156,"line":877},[154,1534,1535],{"class":160},"    res ",[154,1537,710],{"class":451},[154,1539,1540],{"class":160}," requests.post(",[154,1542,1543],{"class":174},"\"https:\u002F\u002Fapi.wapito.com\u002Fv1\u002Fcontacts\u002Fcheck\"",[154,1545,783],{"class":160},[154,1547,786],{"class":445},[154,1549,710],{"class":451},[154,1551,1552],{"class":160},"headers,\n",[154,1554,1555,1558,1560,1562,1565,1568,1570,1572,1574,1576,1578,1581],{"class":156,"line":894},[154,1556,1557],{"class":445},"                        json",[154,1559,710],{"class":451},[154,1561,727],{"class":160},[154,1563,1564],{"class":174},"\"phones\"",[154,1566,1567],{"class":160},": batch}, ",[154,1569,794],{"class":445},[154,1571,710],{"class":451},[154,1573,568],{"class":160},[154,1575,801],{"class":240},[154,1577,783],{"class":160},[154,1579,1580],{"class":240},"60",[154,1582,859],{"class":160},[154,1584,1585],{"class":156,"line":901},[154,1586,1587],{"class":160},"    res.raise_for_status()\n",[154,1589,1590,1593,1596,1598],{"class":156,"line":1200},[154,1591,1592],{"class":160},"    budget ",[154,1594,1595],{"class":451},"-=",[154,1597,1519],{"class":240},[154,1599,1600],{"class":160},"(batch)\n",[154,1602,1603,1606,1609,1611,1614,1617],{"class":156,"line":1208},[154,1604,1605],{"class":451},"    for",[154,1607,1608],{"class":160}," row ",[154,1610,1496],{"class":451},[154,1612,1613],{"class":160}," res.json()[",[154,1615,1616],{"class":174},"\"results\"",[154,1618,1619],{"class":160},"]:\n",[154,1621,1622,1625,1628,1631,1633,1636,1638,1641],{"class":156,"line":1221},[154,1623,1624],{"class":451},"        if",[154,1626,1627],{"class":160}," row[",[154,1629,1630],{"class":174},"\"exists\"",[154,1632,1450],{"class":160},[154,1634,1635],{"class":451},"and",[154,1637,1627],{"class":160},[154,1639,1640],{"class":174},"\"lid\"",[154,1642,1619],{"class":160},[154,1644,1645,1648,1650,1653,1655],{"class":156,"line":1227},[154,1646,1647],{"class":160},"            save_lid(row[",[154,1649,909],{"class":174},[154,1651,1652],{"class":160},"], row[",[154,1654,1640],{"class":174},[154,1656,912],{"class":160},[154,1658,1659,1662,1665],{"class":156,"line":1233},[154,1660,1661],{"class":160},"    time.sleep(",[154,1663,1664],{"class":240},"2",[154,1666,1667],{"class":160},")\n",[30,1669,1671],{"id":1670},"group-automations-in-particular","Group automations in particular",[11,1673,1674,1675,1678],{},"Groups are where LIDs arrived first and bite hardest, because group code is all about ",[15,1676,1677],{},"who",": who\njoined, who should be removed, who asked to join. Three specific adjustments:",[92,1680,1681,1689,1704],{},[95,1682,1683,1684,1688],{},"When you ",[419,1685,1687],{"href":1686},"\u002Fwhatsapp-api\u002Fgroup-participants\u002F","add or remove participants",", pass the identifier\nyou have. Both forms are accepted, but a removal by phone number of a member the group only\nknows by LID names an address that is not in the group, and it is the participant list — not\nyour CRM — that decides which form is in there. Read the list first and remove what it returns.",[95,1690,1683,1691,1695,1696,1699,1700,1703],{},[419,1692,1694],{"href":1693},"\u002Fwhatsapp-api\u002Fsend-group-message\u002F","send to a group"," and mention someone, mentions\nare still expressed as phone digits in the ",[20,1697,1698],{},"mentions"," array and as ",[20,1701,1702],{},"@digits"," in the body. A\nmember you know only by LID cannot yet be mentioned that way; write their name instead.",[95,1705,1706],{},"When you moderate — a welcome message keyed on the joiner, a rule that removes a member after\na third strike — key the state on the LID. A member who leaves and rejoins keeps their LID;\ntheir number may be visible to you one time and hidden the next.",[30,1708,1710],{"id":1709},"what-to-expect-next","What to expect next",[11,1712,1713],{},"The direction is clear even if the schedule is not: WhatsApp will keep moving toward the LID as\nthe address you see, and the phone number as something a person chooses to share. Every\nidentifier you cannot resolve today is a preview of the default. Build for it now — LID as the\nkey, phone as an attribute, the exact address stored for replies — and the next protocol change\nis a column that fills in more often, not an outage.",[11,1715,1716,1717,1721,1722,1726,1727,1729],{},"The ",[419,1718,1720],{"href":1719},"\u002Fglossary\u002Flid\u002F","glossary entry for LID"," and its neighbour on ",[419,1723,1725],{"href":1724},"\u002Fglossary\u002Fjid\u002F","JIDs"," are the\nshort versions of this post; the ",[419,1728,422],{"href":421}," pillar has every\nendpoint, in twelve languages.",[1731,1732,1733],"style",{},"html pre.shiki code .s4rv2, html code.shiki .s4rv2{--shiki-default:#1F2328;--shiki-dark:#E6EDF3}html pre.shiki code .sjgCt, html code.shiki .sjgCt{--shiki-default:#116329;--shiki-dark:#7EE787}html pre.shiki code .sSVrQ, html code.shiki .sSVrQ{--shiki-default:#0A3069;--shiki-dark:#A5D6FF}html pre.shiki code .sHrmB, html code.shiki .sHrmB{--shiki-default:#0550AE;--shiki-dark:#79C0FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sU953, html code.shiki .sU953{--shiki-default:#6E7781;--shiki-dark:#8B949E}html pre.shiki code .sTDnQ, html code.shiki .sTDnQ{--shiki-default:#953800;--shiki-dark:#FFA657}html pre.shiki code .sjeE4, html code.shiki .sjeE4{--shiki-default:#CF222E;--shiki-dark:#FF7B72}html pre.shiki code .sbjLL, html code.shiki .sbjLL{--shiki-default:#8250DF;--shiki-dark:#D2A8FF}",{"title":150,"searchDepth":164,"depth":164,"links":1735},[1736,1737,1738,1739,1740,1741,1742,1743],{"id":32,"depth":164,"text":33},{"id":73,"depth":164,"text":74},{"id":86,"depth":164,"text":87},{"id":306,"depth":164,"text":307},{"id":366,"depth":164,"text":367},{"id":941,"depth":164,"text":942},{"id":1670,"depth":164,"text":1671},{"id":1709,"depth":164,"text":1710},"2026-09-15","Why group and channel events now carry a linked identity instead of a phone number, what breaks in existing automations, and how to re-key your data.","md",[1748,1749,1750,1751],"lid-to-phone","group-participants","send-group-message","check-number",{},"\u002Fblog\u002Fwhatsapp-lids-explained",{"title":5,"description":1745},"blog\u002Fwhatsapp-lids-explained",[1757,1758,1759],"protocol","groups","migration",null,"FQwFy4iVVjp46CAYOSB51Rb3XtETjbGT_ABshYJF8Sc",1790464894221]