Skip to main content

Client lookup

Background​

On Shukah the app looked up a client by phone number with GET /maeda/client?contact_phone=…. The auth service has the same lookup at GET /api/v1/clients. It returns the same four fields, but it needs a Locate JWT and it masks the last name.

Feature​

Send a phone number and get back who has it: the client's id, phone, first name and a masked last name. The answer is an array with one row, or an empty array when no active client has that phone.

In the app snapshot of 2026-02-26, the app uses it only to find the recipient of a point gift, from the gift form and again at gift checkout. It reads only the first row, and an empty array becomes a "User not found" error in the app.

The call needs a signed-in session, but the phone you look up does not have to be your own. The token only identifies the caller. The phone in the query decides who comes back.

GET /api/v1/clients​

Replaces ShukahGET /maeda/client✅ Replaced
Adapt1 breaking · 3 to watch
  • 🔴 Shukah token gets 401 "Unauthorization"; send a Locate JWT (from exchange-token; keep the Shukah token)
  • 🟡 Locate masks last_name ("User" becomes "U**r"); Shukah's masking is unknown (capture is scrubbed)
  • 🟡 Unknown phone is 200 [] on Locate (from code); the app turns [] into "User not found" (user.service.ts:49-51)
  • 🟡 Locate 401 body is {status,message}, not the Laravel body; the app reads message only

Request​

FieldInTypeRequiredConstraintsDescription
AuthorizationheaderstringyesBearer <locate-jwt>, the caller's token. A Shukah token is refused with 401.The caller's session.
contact_phonequerystringyesWhitespace is removed. It is matched to the stored phone with or without a leading +. Blank or missing gives 400. A literal + in a URL is read as a space and removed, so an unencoded +966… still matches.The phone to look up. It can belong to anyone, not only the caller.
key, language and any other query keyquerystringnoAccepted and ignored.Kept so the old Shukah query string still works.

Response​

200 with an array of zero or one row:

FieldTypeNullableFormat / constraintsDescription
client_idintegerno—The id of the client found.
contact_phonestringyesThe phone as stored, not masked.The client's phone.
first_namestringyesTrimmed. Blank becomes null. Not masked.First name.
last_namestringyesMasked: the first and last character are kept and every character between them becomes * (User becomes U**r). A name of one or two characters is returned as it is. Blank becomes null.Last name, masked.

When no active, not-deleted client has the phone, the answer is 200 with []. A client that is suspended, blocked, pending or expired gets [] too.

[
{
"client_id": 356259,
"contact_phone": "+966500000000",
"first_name": "Test",
"last_name": "User"
}
]

Captured on dev, 2026-10-01 (client 356259); personal fields replaced. The capture replaced last_name with User, so the masked form of the real value is not visible here. The mask rule comes from the code and its unit test: User would become U**r.

Errors​

HTTPBodyWhen
400{"name":"Bad Request","message":"Phone number cannot be blank.","code":0,"status":400,"type":"yii\\web\\BadRequestHttpException"}contact_phone is missing or blank.
401{"status":401,"message":"Unauthorization"}The token is missing, malformed or not a Locate JWT, the caller's client does not exist, or the caller's client is not active.

The 401 body was captured on dev on 2026-10-01 with no token. The 400 body has the same shape as the one captured for reset-otp (see OTP login). It was not captured on this route, because the capture run made no lookup call with a token and an empty phone.

Try it​

export AUTH_BASE_URL=https://dev.authentication.locate.sa
export TOKEN='<locate-jwt>' # see "Getting a test token" on the module overview
export PHONE='<phone-to-look-up>' # the answer shows that client's first name and phone
curl -s -G "$AUTH_BASE_URL/api/v1/clients" --data-urlencode "contact_phone=$PHONE" -H "Authorization: Bearer $TOKEN"

Expected: 200 and an array with one row, as shown under Response. A phone nobody has gives [].

Differences from Shukah​

The 200 body has the same four keys on both sides: on 2026-10-01 both services returned client_id, contact_phone, first_name and last_name for client 356259.

AspectShukahLocateImpact on the app
TokenA Shukah token (the capture used one).A Locate JWT only. A Shukah token gets 401 at the same guard as verify-token.Exchange the Shukah token for the JWT before switching, and keep the Shukah token for order-v2.
URLGET $SHUKAH_BASE_URL/maeda/client?contact_phone=…&key=maeda&language=en-GB (the captured call).GET $AUTH_BASE_URL/api/v1/clients?contact_phone=…. Locate ignores key and language.Change the base URL and the path. The app can keep sending key and language.
last_nameNot documented yet: the dev capture had last_name replaced, so masking cannot be seen.Masked: the first and last character are kept and the middle becomes *.The app copies last_name from the first row, and Locate gives it masked. Check how the gift flow uses the name.
Unknown phoneNot documented yet.200 with [], also for a client that is not active.The app already turns an empty array into "User not found", so [] needs no change.
401 body{"name":"Unauthorized","message":"Your request was made with invalid credentials.","code":0,"status":401,"type":"Illuminate\\Auth\\AuthenticationException"} (no token, captured on dev, 2026-10-01).{"status":401,"message":"Unauthorization"} (no token, captured on dev, 2026-10-01).Anything that matches on the error text, name or type must change. status is 401 in both.

Watch out​

  • last_name comes back masked (User becomes U**r), but first_name and contact_phone come back in full.
  • An unknown phone and a client that is not active both get 200 with [], which the app turns into a "User not found" error, so it cannot tell them apart.
  • The token and the phone are independent: any signed-in caller can look up any phone and get that client's first name and phone.

Details (for backend)​

Only dev was checked: host https://dev.authentication.locate.sa, prefix /api/v1. The UAT and production hosts and prefix: Not documented yet.

Route and lookup​

  • GET /clients is registered before any parameter route, behind the client middleware, so it needs an active client's Locate JWT like verify-token does (src/routes/client.js:14-16, src/middlewares/client.js:8-42).
  • The handler reads contact_phone and answers 400 through the Yii error when it is blank (src/controllers/client/lookup/index.js:33-34).
  • The lookup is findClientByPhone, the same as reset-otp: the stored phone with an optional +, not deleted, status ACTIVE, newest client_id first, not cached (src/controllers/client/reset-otp/handler.js:24-30, src/shared/helpers/filter.js:1-4). It returns the single best match, so the array has at most one row (src/controllers/client/lookup/index.js:36-40).
  • A literal + in a URL decodes to a space, and normalizePhone then strips it, so an unencoded +966… still matches (src/sync/lib/casts.js:40-41; code reading).
  • The design doc names merchant flows that hold a Locate client JWT as the use case (docs/plans/2026-08-04-client-lookup-by-phone-api-local.md:9-11). The app snapshot calls it only to resolve a gift recipient (see Sources).
  • The call is read-only, with no cache of its own, no pagination and no side effects (docs/plans/2026-08-04-client-lookup-by-phone-api-local.md:25-28).

Row mapping and masking​

toLookupRow builds each row (src/controllers/client/lookup/index.js:23-28):

  • client_id is the stored client_id. contact_phone is the stored phone, or null.
  • first_name is trimmed, and blank becomes null (src/controllers/client/lookup/index.js:17-21).
  • last_name goes through maskLastName: trimmed, null or blank gives null, a length of two or less is unchanged, and otherwise the first character, length - 2 stars and the last character (src/controllers/client/lookup/index.js:8-15). The unit test pins User to U**r (src/controllers/client/__tests__/lookup.test.js:32).
  • Masking first_name or contact_phone is out of scope in the design doc (docs/plans/2026-08-04-client-lookup-by-phone-api-local.md:93). Any signed-in caller therefore gets them in full for any phone, and the call cannot tell an unknown phone from one that is not active.

Not documented yet​

  • Whether Shukah masks last_name: the capture replaced the value.
  • What Shukah answers for an unknown phone or a blank phone: neither was captured.
  • Whether Shukah's lookup also takes list filters. The design doc lists "Pagination / status / page list filters from upstream" as out of scope for Locate (docs/plans/2026-08-04-client-lookup-by-phone-api-local.md:91), and the capture used only contact_phone, key and language.
  • Whether any environment has this route: origin/production at 4f3bc96 (2026-09-21) does not register it (its src/routes/client.js imports no lookup), and whether anything runs from that ref is unknown.

Where the docs and the code disagree (code wins)​

  • The design doc's mask example has 13 stars (U*************) for User (500…000)) (docs/plans/2026-08-04-client-lookup-by-phone-api-local.md:30). The code and the unit test give 12 (src/controllers/client/__tests__/lookup.test.js:33), because the rule is length - 2. The difference is cosmetic.

Sources​

  • locate-authentication-service@6757aa4: src/routes/client.js:14-16, src/middlewares/client.js:8-42, src/controllers/client/lookup/index.js:8-41, src/controllers/client/__tests__/lookup.test.js:32-33, src/controllers/client/reset-otp/handler.js:24-30, src/shared/helpers/filter.js:1-4, src/sync/lib/casts.js:40-41, src/shared/errors/yii-bad-request-error.js:12-23, src/middlewares/error-handler.js:10, src/shared/constant/error-message.js:59
  • locate-authentication-service@6757aa4 docs: docs/plans/2026-08-04-client-lookup-by-phone-api-local.md:9-11,25-30,56-59,90-93 (2026-08-04)
  • Mobile snapshot mobile/locate-client@e960c3a (branch development, 2026-02-26; the app's own code, not Locate): src/entities/user/model/api/services/user.service.ts:38-47,49-51,58-60, src/entities/user/model/api/dto/user.dto.ts:126-155, src/widgets/SendPointGiftForm/model/hooks/sendPointGiftForm.ts:64, src/widgets/PointGiftsCheckout/model/hooks/usePointGiftsCheckout.ts:376
  • Dev captures, 2026-10-01: locate-client-lookup, shukah-client-lookup, probe-locate-client-lookup, probe-shukah-client-lookup, probe-locate-reset-otp
  • Secondary evidence, not the pinned sha: locate-authentication-service origin/production at 4f3bc96 (2026-09-21), src/routes/client.js