Summary
wuzapi can resolve a phone number to a LID (GET /user/lid/{phone}), but there is no way to do the reverse. The underlying mapping is already available inside wuzapi — getCachedPNForLID() in handlers.go already calls client.Store.LIDs.GetPNForLID() — but it is only used internally by the blocklist code path and is never exposed over HTTP.
Without it, any contact that WhatsApp only identifies by LID can be messaged, but can never be linked to a real phone number.
Current behaviour
GET /user/lid/{phone} maps PN → LID and rejects a LID as input:
curl -H 'Token: <instance>' https://<host>/user/lid/20719022960729
# 404 {"code":404,"error":"LID not found for this number","success":false}
curl -H 'Token: <instance>' https://<host>/user/lid/20719022960729@lid
# 404 {"code":404,"error":"LID not found for this number:
# invalid GetLIDForPN call with non-PN JID 20719022960729@lid","success":false}
Other endpoints do not fill the gap either:
GET /user/contacts returns LID-keyed entries with RedactedPhone empty:
"10123405705452@lid": {
"BusinessName": "", "FirstName": "", "Found": true,
"FullName": "Gabrielzin CSGO", "PushName": "", "RedactedPhone": ""
}
HistorySync conversations expose pnJID (field 39) and lidJID (field 42), but pnJID is frequently absent — in a recent full history sync on our instance, 110 of 122 conversations arrived with a LID and no pnJID at all.
- Live
Message events expose Info.SenderAlt, which carries the PN when WhatsApp chooses to reveal it — but it is empty for a significant share of contacts.
So for those contacts, the phone number is simply unreachable through the API, even though whatsmeow has it in whatsmeow_lid_map.
Expected behaviour
A way to ask wuzapi for the PN behind a LID, mirroring the existing PN → LID endpoint.
Why this matters
We use wuzapi to power a customer-service inbox. When a conversation arrives, we try to match it to a customer record, which is keyed by phone number. For LID-only contacts we can chat normally (wuzapi routes by LID just fine — that part works well), but we cannot:
- link the conversation to an existing customer;
- create a customer record from the conversation, since we have no number to store;
- reconcile the same person across channels (a phone order and a WhatsApp chat).
The LID digits are not a phone number, so anything that stores them as one produces records that cannot receive messages and do not match anything.
Proposed solution
Expose the existing internal helper. Since getCachedPNForLID() is already implemented and used, this looks like a small addition — one handler and one route.
Either of these would work for us:
Option A — a new endpoint (preferred, keeps each direction explicit):
GET /user/pn/{lid} → { "code": 200, "success": true, "data": { "lid": "...@lid", "pn": "...@s.whatsapp.net" } }
Option B — make the existing endpoint bidirectional:
GET /user/lid/{jid} accepts either a PN or a LID and returns both, dispatching on types.ParseJID(...).Server (types.HiddenUserServer → GetPNForLID, otherwise GetLIDForPN).
Rough sketch, following the shape of the existing blocklist code:
func (s *server) GetUserPN() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
txtid := r.Context().Value("userinfo").(Values).Get("Id")
client := clientManager.GetWhatsmeowClient(txtid)
if client == nil {
s.Respond(w, r, http.StatusInternalServerError, errors.New("no session"))
return
}
lid, err := types.ParseJID(mux.Vars(r)["lid"])
if err != nil || lid.Server != types.HiddenUserServer {
s.Respond(w, r, http.StatusBadRequest, errors.New("expected a LID JID"))
return
}
pn, err := getCachedPNForLID(r.Context(), client, lid) // already exists
if err != nil {
s.Respond(w, r, http.StatusNotFound, err)
return
}
responseJson, _ := json.Marshal(map[string]string{"lid": lid.String(), "pn": pn.String()})
s.Respond(w, r, http.StatusOK, string(responseJson))
}
}
A 404 when the mapping is not known yet is fine for our use — we would fall back to asking the customer for their number.
Environment
- wuzapi: latest as of 2026-09
- Deployment: Docker
- Client: REST API (Node.js)
- Relevant endpoints:
/user/lid/{phone}, /user/contacts, /session/history, HistorySync webhook
Related
Same limitation is being discussed in other WhatsApp API wrappers, which suggests it is a common gap rather than something specific to wuzapi:
Happy to test a build or open a PR if you would rather point me at how you'd like it shaped.
Summary
wuzapi can resolve a phone number to a LID (
GET /user/lid/{phone}), but there is no way to do the reverse. The underlying mapping is already available inside wuzapi —getCachedPNForLID()inhandlers.goalready callsclient.Store.LIDs.GetPNForLID()— but it is only used internally by the blocklist code path and is never exposed over HTTP.Without it, any contact that WhatsApp only identifies by LID can be messaged, but can never be linked to a real phone number.
Current behaviour
GET /user/lid/{phone}maps PN → LID and rejects a LID as input:Other endpoints do not fill the gap either:
GET /user/contactsreturns LID-keyed entries withRedactedPhoneempty:HistorySyncconversations exposepnJID(field 39) andlidJID(field 42), butpnJIDis frequently absent — in a recent full history sync on our instance, 110 of 122 conversations arrived with a LID and nopnJIDat all.Messageevents exposeInfo.SenderAlt, which carries the PN when WhatsApp chooses to reveal it — but it is empty for a significant share of contacts.So for those contacts, the phone number is simply unreachable through the API, even though whatsmeow has it in
whatsmeow_lid_map.Expected behaviour
A way to ask wuzapi for the PN behind a LID, mirroring the existing PN → LID endpoint.
Why this matters
We use wuzapi to power a customer-service inbox. When a conversation arrives, we try to match it to a customer record, which is keyed by phone number. For LID-only contacts we can chat normally (wuzapi routes by LID just fine — that part works well), but we cannot:
The LID digits are not a phone number, so anything that stores them as one produces records that cannot receive messages and do not match anything.
Proposed solution
Expose the existing internal helper. Since
getCachedPNForLID()is already implemented and used, this looks like a small addition — one handler and one route.Either of these would work for us:
Option A — a new endpoint (preferred, keeps each direction explicit):
Option B — make the existing endpoint bidirectional:
GET /user/lid/{jid}accepts either a PN or a LID and returns both, dispatching ontypes.ParseJID(...).Server(types.HiddenUserServer→GetPNForLID, otherwiseGetLIDForPN).Rough sketch, following the shape of the existing blocklist code:
A 404 when the mapping is not known yet is fine for our use — we would fall back to asking the customer for their number.
Environment
/user/lid/{phone},/user/contacts,/session/history,HistorySyncwebhookRelated
Same limitation is being discussed in other WhatsApp API wrappers, which suggests it is a common gap rather than something specific to wuzapi:
Happy to test a build or open a PR if you would rather point me at how you'd like it shaped.