Skip to content

Feature request: expose LID → phone number (PN) resolution over the API #373

Description

@lucascampuus

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions