Address book
Background
On Shukah the app keeps the client's saved delivery addresses with five calls on
/maeda/address-book (the path has no /api/ part): list, get, create, update and delete. The auth
service has the same five routes, but it is not a drop-in swap. Four things need care: the address
id is a 24-character string instead of an integer, the list and create answers use a different
shape from the get and update answers, create answers inside a {status, address} wrapper, and the
create call does not fill in the street, city and other address parts that Shukah derives itself.
Feature
The app lets a logged-in client keep several saved addresses and choose one as the default. In the app snapshot of 2026-02-26 the calls happen here:
| Call | Where in the app |
|---|---|
| List | When a logged-in client opens a screen that shows saved addresses (the location drawer and the user-location bar) and the list is not loaded yet, when the app sets up the default delivery address after login, on pull-to-refresh in the saved-addresses list, and again after every create, update or delete. |
| Get | When the client opens the Edit address screen. |
| Create | When the client saves a location from the map with "save for later" on. If a saved address with the same name already exists, the app first deletes it and then creates the new one. |
| Update | When the client saves the Edit address screen. The app sends location_name, area, is_default and image_urls. |
| Delete | When the client deletes an address on the Edit address screen. |
All five calls need the Locate JWT (see "Getting a test token" on the module overview). The rules that apply to all of them:
- Ids. Every response gives the id as a Mongo ObjectId: a 24-character hex string. In the path of get, update and delete, a value made only of digits is first tried as the old Shukah address id, then as an ObjectId. Any other value is tried only as an ObjectId. Only addresses that came from Shukah's address book carry a Shukah id, and no response ever shows it.
- Two response shapes. List and create use the list shape:
is_defaultis a boolean,latandlngare numbers, a missing text field isnull, and there aresource,created_atandupdated_atkeys. Get and update use the detail shape:is_defaultis0or1,latandlngare strings, a missing text field is"", and those three keys are absent. - Blank values. On create and update, a text field is trimmed, and a blank one is stored as
null. A blank or non-numericlatorlngis stored asnull. Nothing is validated: an empty body creates an address with every field empty, and there is no limit on the number of addresses. - Partial update. Update changes only the keys that are in the body. A key that is present with
null,""or only spaces clears the field (image_urlsbecomes[]).source, the Shukah id andcreated_atnever change. - Own addresses only. The id is looked up among the caller's addresses only. An id that belongs
to someone else, or to nobody, gives
404withAddress not found. - One default. The service keeps one default address when the app creates, updates or deletes:
| Call | Rule |
|---|---|
| Create | is_default true clears the flag on every other address. Otherwise the new address is the default only when the book is empty or has no default. |
| Update | Only when the body contains is_default. Set to true: every other address loses the flag. Set to false while no other address is default: the first other address becomes the default. If it is the only address, it stays the default. |
| Delete | If no default is left and the book is not empty, the first address in storage order becomes the default. The list is sorted by date, so this is not always the first address the app sees. |
This guarantee holds only for these three calls. Addresses copied from Shukah are not checked, and on dev all 10 addresses in the captured list were default (see the list block).
GET /api/v1/clients/address-book
GET /maeda/address-book✅ Replaced- 🔴 Shukah token gets 401 "Unauthorization"; send a Locate JWT (from exchange-token; keep the Shukah token)
- 🔴 Locate-made addresses have null formatted_address/street/city; the app sends formatted_address as c_address
- 🟡 id is a 24-hex string (Shukah int); a saved Shukah id no longer matches (initLocation.ts:78)
- 🟡 is_default is boolean, true on 28 of 30 (Shukah: 0 on all 15); initLocation takes the first truthy
- 🟡 30 addresses vs 15 on Shukah: Shukah deletes never reach Locate, and "Saihat" appears 3 times
- 🟡 image_urls is [] on Locate for all 4 Shukah addresses that have a photo (Shukah returns the URL)
- 🟡 Locate lists newest created_at first; Shukah orders by position (0..14), so the list can reorder
Locate returns is_default as a boolean and id as a 24-hex string, where Shukah returns 0 or 1 and an integer id (lat and lng are numbers on both); the app default check accepts the boolean, but anything that treats the id as a number must change.
Request
| Field | In | Type | Required | Constraints | Description |
|---|---|---|---|---|---|
Authorization | header | string | yes | Bearer <locate-jwt>. A Shukah token gets 401. | The Locate JWT. |
| Any query key or other header | query, header | — | no | Accepted and ignored, for example language. | — |
There is no paging, no filter and no sort option.
Response
200 with an array in the list shape. It is [] when the client has no address. The newest
created_at comes first, and addresses without a created_at come last.
| Field | Type | Nullable | Format / constraints | Description |
|---|---|---|---|---|
id | string | no | A 24-character hex ObjectId. Never the Shukah integer id. | The address id for get, update and delete. |
source | string | yes | mt_client_inline, mt_address_book or locate. | Where the address came from: locate for an address created through this API. |
is_default | boolean | no | true or false. More than one address can be true. | The default address. |
location_name, street, area, area_name, city, state, zipcode, country, country_code, formatted_address | string | yes | null when missing. An empty string that is already stored stays "" (the capture has state: ""). | The address parts. |
lat, lng | number | yes | JSON numbers. No range check. | Coordinates. |
image_urls | array of string | no | Always an array, possibly empty. | Photo URLs. |
created_at, updated_at | string | yes | ISO 8601 in UTC, for example 2026-09-29T17:01:10.000Z. | When the address was created and last changed. |
[
{
"id": "6abbeed8a492ed4862500b74",
"source": "mt_address_book",
"is_default": true,
"location_name": "Saihat",
"street": "King Fahd Rd",
"area": "Eastern Province",
"area_name": "A12345",
"city": "Saihat",
"state": "Al-Qatif",
"zipcode": "32461",
"country": "Saudi Arabia",
"country_code": "SA",
"lat": 24.7136,
"lng": 46.6753,
"formatted_address": "King Fahd Rd, Al Olaya, Riyadh 12211",
"image_urls": [],
"created_at": "2026-09-29T17:01:10.000Z",
"updated_at": "2026-09-29T17:01:10.000Z"
}
]
Captured on dev, 2026-10-01 (client 356259): the first of 28 items, with personal fields and coordinates replaced.
Errors
| HTTP | Body | When |
|---|---|---|
401 | {"status":401,"message":"Unauthorization"} | There is no token, the token is not a Locate JWT, or the client is missing or not active. Same guard as Verify token. |
401 | Same body. | The token is valid but the client record cannot be loaded. |
500 | On dev: {"status":500,"message":"<raw error text>","stack":"<stack trace>"}. In production: {"status":500,"message":"Internal server error"}. | An unexpected failure. Not captured. On dev the message is the raw error text and a stack field is added; Internal server error appears only in production (UAT and production: Not documented yet). |
The 401 body was captured on dev on 2026-10-01 with no token.
Try it
export AUTH_BASE_URL=https://dev.authentication.locate.sa
export TOKEN='<locate-jwt>' # see "Getting a test token" on the module overview
curl -s "$AUTH_BASE_URL/api/v1/clients/address-book" -H "Authorization: Bearer $TOKEN"
Expected: 200 and an array like the one shown under Response. With no token you get the 401 shown
under Errors.
Differences from Shukah
| Aspect | Shukah | Locate | Impact on the app |
|---|---|---|---|
| URL | GET $SHUKAH_BASE_URL/maeda/address-book | GET $AUTH_BASE_URL/api/v1/clients/address-book | Change the base URL and the path. |
| Token | Shukah opaque token. | Locate JWT only. | Get the JWT with exchange-token first, and keep the Shukah token for order-v2. Do not move login to Locate yet. |
id | An integer, for example 162115 (captured on dev, 2026-10-01). | A 24-character string, for example 6abbeed8a492ed4862500b74. | Anything that stores or compares the id as a number must change. The app types it as a number. |
is_default | 0 or 1 (captured on dev, 2026-10-01). | true or false. | The app finds the default with a truthiness test, which works for both. A strict compare with 1 would not. |
| Extra keys | position (an integer). | source, created_at, updated_at; no position. | None: the app's address type has none of these four keys. |
image_urls | An array or null. Four of the client's 15 addresses have a photo URL (captured on dev, 2026-10-02). | Always an array. The Locate copies of those four addresses have []. | The photos of addresses copied from Shukah are lost. The cause: Not documented yet. |
| Order | By position, 0 to 14, while the ids are not in order (162115, 161996, 162101, …) (captured on dev, 2026-10-02). | Newest created_at first. | If the app shows the list in server order, the order changes. |
| Count for client 356259 | 15 addresses (captured on dev, 2026-10-02; 13 on 2026-10-01). | 30 addresses, same client, same day (28 on 2026-10-01). | Addresses deleted on Shukah stay on Locate, and one address appears three times on Locate. Check for duplicates before the switch. |
| Default addresses | is_default 0 on all 15 (captured on dev, 2026-10-02). | true on 28 of 30. | The app takes the first default it finds, so the selected default address can differ. |
GET /api/v1/clients/address-book/:id
GET /maeda/address-book/:id✅ Replaced- 🔴 Shukah token gets 401 "Unauthorization"; send a Locate JWT (from exchange-token; keep the Shukah token)
- 🟡 id is a 24-hex string; a numeric Shukah id (162115) still resolves in the path
- 🟡 lat/lng are strings (Shukah numbers); the app uses Number() (locationPresetToSelectedAddress.ts:14)
- 🟡 Same address: is_default 1 on Locate vs 0 on Shukah, and image_urls [] vs a photo URL (162115)
- 🟡 Foreign id: Locate 404 "Address not found"; Shukah 403 "You can only view address that belong to you."
- 🟡 Locate 404 body carries a server stack (get.js:39:17) on dev
Request
| Field | In | Type | Required | Constraints | Description |
|---|---|---|---|---|---|
Authorization | header | string | yes | Bearer <locate-jwt>. | The Locate JWT. |
id | path | string | yes | A value of only digits is tried first as the Shukah address id, then as an ObjectId. Anything else is tried as an ObjectId only. | The address id. |
| Any query key or other header | query, header | — | no | Accepted and ignored. | — |
Response
200 with one address in the detail shape, with no wrapper:
| Field | Type | Nullable | Format / constraints | Description |
|---|---|---|---|---|
id | string | no | The ObjectId, even when a numeric Shukah id was used in the path. | The address id. |
location_name, formatted_address, area_name, street, city, area, state, zipcode, country, country_code | string | no | "" when missing, never null. | The address parts. |
is_default | integer | no | 0 or 1. | The default address. |
lat, lng | string | no | The number as text, for example "24.7136". "" when missing. | Coordinates. |
image_urls | array of string | no | Possibly empty. | Photo URLs. |
There is no source, created_at or updated_at.
{
"id": "6abe570b9ac75c8c65a5fa1a",
"location_name": "Docs capture",
"is_default": 0,
"formatted_address": "King Fahd Rd, Al Olaya, Riyadh 12211",
"area_name": "Olaya",
"street": "King Fahd Rd",
"city": "Riyadh",
"area": "Al Olaya",
"state": "",
"zipcode": "12211",
"country": "Saudi Arabia",
"country_code": "SA",
"lat": "24.7136",
"lng": "46.6753",
"image_urls": []
}
Captured on dev, 2026-10-01 (client 356259): the address made by the create-address sample below, read right after it was created and deleted at the end of the run. Address text was already illustrative.
The same address came back from create with state: null, and here with state: "": that is the
difference between the two shapes.
Errors
| HTTP | Body | When |
|---|---|---|
401 | {"status":401,"message":"Unauthorization"} | No token, a token that is not a Locate JWT, an inactive client, or a client record that cannot be loaded. |
404 | {"status":404,"message":"Address not found"} | No address of this client matches the id. This includes an address that was deleted, and an id that belongs to another client. |
The 404 was captured on dev on 2026-10-01 by reading the sample address after it was deleted, and
on 2026-10-02 for an unknown ObjectId. An id that belongs to another client gets the same answer
because the lookup only searches the caller's addresses (code reading; a foreign id was not tried live
on Locate). Shukah answers someone else's id with 403 instead (captured on dev, 2026-10-02; see
Differences from Shukah). On dev the error bodies also carry a stack field with a server stack trace
as text. The samples on this page leave it out.
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 ADDRESS_ID='<id-from-the-list>'
curl -s "$AUTH_BASE_URL/api/v1/clients/address-book/$ADDRESS_ID" -H "Authorization: Bearer $TOKEN"
Expected: 200 and the address in the detail shape. An unknown id gives the 404 shown under Errors.
Differences from Shukah
| Aspect | Shukah | Locate | Impact on the app |
|---|---|---|---|
| URL | GET $SHUKAH_BASE_URL/maeda/address-book/{id} | GET $AUTH_BASE_URL/api/v1/clients/address-book/{id} | Change the base URL and the path. |
id | An integer (captured on dev, 2026-10-01). | A 24-character string. A numeric Shukah id still works in the path for addresses that came from Shukah's address book. | The list now returns ObjectIds. The app compares its stored last-selected address id with the list ids as strings, so a saved Shukah integer no longer matches and the selection is dropped. |
lat, lng | JSON numbers, for example 24.7136 (captured on dev, 2026-10-01). | Strings, for example "24.7136". | None: the app accepts both and converts with Number() before it uses them. |
is_default | 0 or 1. Address 162115 is 0 (captured on dev, 2026-10-02). | 0 or 1. The same address read through its Shukah id is 1 (captured on dev, 2026-10-02). | The type is the same, the value is not: the default the app shows can change. |
image_urls | An array or null. Address 162115 has one photo URL (captured on dev, 2026-10-02). | Always an array. The same address has []. | The photo of an address copied from Shukah is lost. |
| Extra key | position. | None. | None: the app does not read it. |
| Someone else's address | 403 {"name":"Forbidden","message":"You can only view address that belong to you.","code":0,"status":403,"type":"Symfony\\Component\\HttpKernel\\Exception\\AccessDeniedHttpException"} (captured on dev, 2026-10-02, id 1). | 404 Address not found, the same as an unknown id (code reading). | The status and the text the app shows change. |
404 body | {"name":"Not Found","message":"Object not found: 162119","code":0,"status":404,"type":"Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException"} (captured on dev, 2026-10-01). | {"status":404,"message":"Address not found"} | The app shows message, so the text it shows changes. |
POST /api/v1/clients/address-book
POST /maeda/address-book✅ Replaced- 🔴 Shukah token gets 401 "Unauthorization"; send a Locate JWT (from exchange-token; keep the Shukah token)
- 🔴 Answer is {status,address:{...}}; the app builds the preset from the top level, so id and lat/lng are NaN
- 🔴 No geocoding: street, city, area, state, zipcode, country, formatted_address are null (Shukah fills them)
- 🟡 address.is_default is boolean (Shukah 0); lat/lng are numbers here but strings on get and update
- 🟡 The Locate-made address never appeared on Shukah (the push targets /maeda/api/address-book, which 404s)
Request
Every body field is optional and nothing is validated. Keys that are not listed are ignored. The
server sets source to locate and the timestamps to now, so the body cannot set them.
| Field | In | Type | Required | Constraints | Description |
|---|---|---|---|---|---|
Authorization | header | string | yes | Bearer <locate-jwt>. | The Locate JWT. |
location_name, street, area, area_name, city, state, zipcode, country, country_code, formatted_address | body | string | no | Trimmed. Blank or null is stored as null. No length limit. | The address parts. |
lat, lng | body | number or numeric string | no | null, "" and values that are not numbers are stored as null. No range check. | Coordinates. |
image_urls | body | array of string | no | Strings are trimmed and blank ones dropped. null gives []. A JSON-array string or a comma-separated string is also accepted. | Photo URLs. |
is_default | body | boolean, 0 or 1 | no | Only 1, "1" and true mean default. Every other value, including "true" and 2, means not default. | Make this the default address. |
What the app sends in the snapshot of 2026-02-26 is only location_name, area_name, lat and
lng (both as strings), is_default (0 or 1) and image_urls (an array or null).
A request used for the capture below:
{
"location_name": "Docs capture",
"street": "King Fahd Rd",
"area": "Al Olaya",
"area_name": "Olaya",
"city": "Riyadh",
"zipcode": "12211",
"country": "Saudi Arabia",
"country_code": "SA",
"lat": 24.7136,
"lng": 46.6753,
"formatted_address": "King Fahd Rd, Al Olaya, Riyadh 12211",
"image_urls": [],
"is_default": false
}
Response
200 with a wrapper that holds the new address in the list shape:
| Field | Type | Nullable | Format / constraints | Description |
|---|---|---|---|---|
status | integer | no | Always 200. | Status echo. |
address | object | no | The list shape, with the fields listed under the list block. source is locate. is_default is true when the request asked for it or when the client had no default. | The address that was created. |
{
"status": 200,
"address": {
"id": "6abe570b9ac75c8c65a5fa1a",
"source": "locate",
"is_default": false,
"location_name": "Docs capture",
"street": "King Fahd Rd",
"area": "Al Olaya",
"area_name": "Olaya",
"city": "Riyadh",
"state": null,
"zipcode": "12211",
"country": "Saudi Arabia",
"country_code": "SA",
"lat": 24.7136,
"lng": 46.6753,
"formatted_address": "King Fahd Rd, Al Olaya, Riyadh 12211",
"image_urls": [],
"created_at": "2026-10-01T12:50:19.661Z",
"updated_at": "2026-10-01T12:50:19.661Z"
}
}
Captured on dev, 2026-10-01 (client 356259, Task 9 live write). The address was deleted at the end
of the run. Address text was already illustrative. is_default is false because the client
already had default addresses.
Errors
| HTTP | Body | When |
|---|---|---|
401 | {"status":401,"message":"Unauthorization"} | No token, a token that is not a Locate JWT, an inactive client, or a client record that cannot be loaded. |
500 | On dev: {"status":500,"message":"<raw error text>","stack":"<stack trace>"}. In production: {"status":500,"message":"Internal server error"}. | An unexpected failure, for example a save that conflicts with a copy of the client's data from Shukah arriving at the same moment. Not captured. On dev the message is the raw error text and a stack field is added; Internal server error appears only in production (UAT and production: Not documented yet). |
There is no 400: bad values are cleaned or stored as null. The 401 body was captured on dev on
2026-10-01 with no token.
Try it
This call creates a real address on dev. Delete it afterwards with the delete call below.
export AUTH_BASE_URL=https://dev.authentication.locate.sa
export TOKEN='<locate-jwt>' # see "Getting a test token" on the module overview
curl -s -X POST "$AUTH_BASE_URL/api/v1/clients/address-book" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{"location_name":"Docs test","area_name":"Olaya","lat":"24.7136","lng":"46.6753","is_default":0,"image_urls":[]}'
Expected: 200 and {"status":200,"address":{...}}. This body is the one the app sends, so street,
city, area, state, zipcode, country, country_code and formatted_address come back as
null (captured on dev, 2026-10-02, with this body). Keep address.id for the other calls.
Differences from Shukah
| Aspect | Shukah | Locate | Impact on the app |
|---|---|---|---|
| URL | POST $SHUKAH_BASE_URL/maeda/address-book | POST $AUTH_BASE_URL/api/v1/clients/address-book | Change the base URL and the path. |
| Response | The address object itself, flat, with an integer id and a position key (captured on dev, 2026-10-01). | {"status":200,"address":{...}} with a 24-character id. | The app reads id, lat, lng and the rest from the top level of the answer. With the wrapper they are all missing, and the coordinates it builds for the selected address become NaN. Read address instead. |
| Missing address parts | A request with only lat, lng, area_name, location_name, is_default and image_urls came back with street, area, state, zipcode, country, country_code and formatted_address filled in (captured on dev, 2026-10-01, and again on 2026-10-02 with position 15). How Shukah derives them: Not documented yet. | Stores only what is sent. Anything not sent is null: the same request came back with all eight null (captured on dev, 2026-10-02). | An address created with the app's current body has no street, city or formatted address. The map screen has these values, so the app can send them, or the backend must fill them in. Whether that is planned: Not documented yet. |
| Same-name address | The app deletes an existing address with the same name before it creates the new one. | The same two calls work. The delete uses the id from the list, which is a string. | None for this step. See the delete block for the explicit delete. |
| Where the address is stored | In Shukah. | In Locate, then a best-effort copy to Shukah. | On dev the copy did not reach Shukah: Shukah's list still had 13 addresses after the create, update and delete of 2026-10-01, and on 2026-10-02 the address made through Locate never appeared on Shukah either. The delete is never copied. An address created through Locate cannot be assumed to exist on Shukah. |
PUT /api/v1/clients/address-book/:id
PUT /maeda/address-book/:id✅ Replaced- 🔴 Shukah token gets 401 "Unauthorization"; send a Locate JWT (from exchange-token; keep the Shukah token)
- 🟡 id is a 24-hex string and lat/lng are strings; the app reads the same keys, so no change is needed
- 🟡 Unset address parts come back "" on Locate (city, area, state ...); Shukah returns the geocoded values
- 🟡 An edit made on Shukah never reaches Locate ("S (edited)" stayed "Parity test S" there)
- 🟡 Locate keeps exactly one default on update; Shukah returned is_default 0 throughout
- 🟡 A 404 body carries a server stack on dev
Flat Shukah-style body; the id may be a 24-hex id or a numeric Shukah id.
Request
A partial update: only the keys that are in the body change.
| Field | In | Type | Required | Constraints | Description |
|---|---|---|---|---|---|
Authorization | header | string | yes | Bearer <locate-jwt>. | The Locate JWT. |
id | path | string | yes | Digits only: the Shukah address id first, then the ObjectId. Anything else: the ObjectId only. | The address to change. |
location_name, street, area, area_name, city, state, zipcode, country, country_code, formatted_address | body | string or null | no | Same cleaning as create. A present key with null, "" or spaces sets the field to null. | The address parts. |
lat, lng | body | number, numeric string or null | no | Same cleaning as create. | Coordinates. |
image_urls | body | array of string or null | no | Same cleaning as create. null gives []. | Photo URLs. |
is_default | body | boolean, 0 or 1 | no | Only 1, "1" and true mean default. See the default rules under Feature. | Make this the default address, or not. |
source, created_at, any other key | body | — | no | Ignored. | — |
What the app sends in the snapshot of 2026-02-26: location_name, area, is_default (0 or 1)
and image_urls.
{
"location_name": "Docs capture (edited)"
}
Response
200 with the address in the detail shape, the same as get: no wrapper, is_default
0 or 1, lat and lng as strings.
{
"id": "6abe570b9ac75c8c65a5fa1a",
"location_name": "Docs capture (edited)",
"is_default": 0,
"formatted_address": "King Fahd Rd, Al Olaya, Riyadh 12211",
"area_name": "Olaya",
"street": "King Fahd Rd",
"city": "Riyadh",
"area": "Al Olaya",
"state": "",
"zipcode": "12211",
"country": "Saudi Arabia",
"country_code": "SA",
"lat": "24.7136",
"lng": "46.6753",
"image_urls": []
}
Captured on dev, 2026-10-01 (client 356259, Task 9 live write): the request above, sent to the address from the create sample. Address text was already illustrative.
Errors
| HTTP | Body | When |
|---|---|---|
401 | {"status":401,"message":"Unauthorization"} | No token, a token that is not a Locate JWT, an inactive client, or a client record that cannot be loaded. |
404 | {"status":404,"message":"Address not found"} | No address of this client matches the id, including an id that belongs to another client. |
500 | On dev: {"status":500,"message":"<raw error text>","stack":"<stack trace>"}. In production: {"status":500,"message":"Internal server error"}. | An unexpected failure, as for create. Not captured. On dev the message is the raw error text and a stack field is added; Internal server error appears only in production (UAT and production: Not documented yet). |
There is no 400. The 401 body was captured on dev on 2026-10-01 with no token. The 404 body is
the one captured for get-address.
Try it
This call changes an address on dev. Use an address you created for the test.
export AUTH_BASE_URL=https://dev.authentication.locate.sa
export TOKEN='<locate-jwt>' # see "Getting a test token" on the module overview
export ADDRESS_ID='<id-from-the-create-call>'
curl -s -X PUT "$AUTH_BASE_URL/api/v1/clients/address-book/$ADDRESS_ID" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{"location_name":"Office"}'
Expected: 200 and the address in the detail shape with the new location_name.
Differences from Shukah
| Aspect | Shukah | Locate | Impact on the app |
|---|---|---|---|
| URL | PUT $SHUKAH_BASE_URL/maeda/address-book/{id} | PUT $AUTH_BASE_URL/api/v1/clients/address-book/{id} | Change the base URL and the path. |
| Response | The address, flat, with an integer id, numbers for lat and lng, and a position key (captured on dev, 2026-10-01 and 2026-10-02). | The address, flat, with a 24-character id, strings for lat and lng, and no position. Address parts that were never set come back "" (captured on dev, 2026-10-02). | None for the keys the app reads (confirmed on 2026-10-02). The app builds its saved address from the top-level keys, which are all there. |
| Default rules | Not documented yet. | The rules under Feature. | If the app relies on Shukah's way of picking a default, compare before switching. |
| Copy to Shukah | Not applicable. | A best-effort copy of the edited address goes to Shukah. When the address has no Shukah id, the copy creates a new address there, which can duplicate it. | On dev the copy did not show up in Shukah's list on 2026-10-01 or on 2026-10-02 (see create). An edit made on Shukah did not reach Locate either (2026-10-02). |
DELETE /api/v1/clients/address-book/:id
DELETE /maeda/address-book/:id✅ Replaced- 🔴 Shukah token gets 401 "Unauthorization"; send a Locate JWT (from exchange-token; keep the Shukah token)
- 🔴 The app sends +id, so a 24-hex id becomes NaN and delete answers 404; the slice compares +id too
- 🟡 Answer is 200 {"status":200}; Shukah is 204 with no body; the app ignores the body
- 🟡 A Locate delete is never pushed to Shukah, and a Shukah delete never reaches Locate (ghost addresses)
Request
| Field | In | Type | Required | Constraints | Description |
|---|---|---|---|---|---|
Authorization | header | string | yes | Bearer <locate-jwt>. | The Locate JWT. |
id | path | string | yes | Digits only: the Shukah address id first, then the ObjectId. Anything else: the ObjectId only. | The address to delete. |
There is no body.
Response
200 with one key:
| Field | Type | Nullable | Format / constraints | Description |
|---|---|---|---|---|
status | integer | no | Always 200. No other keys. | Status echo. |
{
"status": 200
}
Captured on dev, 2026-10-01 (client 356259, Task 9 live write): the delete of the sample address.
The delete is permanent. If the deleted address was the default and others remain, the first address in storage order becomes the default.
Errors
| HTTP | Body | When |
|---|---|---|
401 | {"status":401,"message":"Unauthorization"} | No token, a token that is not a Locate JWT, an inactive client, or a client record that cannot be loaded. |
404 | {"status":404,"message":"Address not found"} | No address of this client matches the id. A second delete of the same id also gets this, so the call is not idempotent. |
500 | On dev: {"status":500,"message":"<raw error text>","stack":"<stack trace>"}. In production: {"status":500,"message":"Internal server error"}. | An unexpected failure. Not captured. On dev the message is the raw error text and a stack field is added; Internal server error appears only in production (UAT and production: Not documented yet). |
The 401 body was captured on dev on 2026-10-01 with no token. The 404 body is the one captured
for get-address after this delete.
Try it
This call deletes an address on dev. Use an address you created for the test.
export AUTH_BASE_URL=https://dev.authentication.locate.sa
export TOKEN='<locate-jwt>' # see "Getting a test token" on the module overview
export ADDRESS_ID='<id-from-the-create-call>'
curl -s -X DELETE "$AUTH_BASE_URL/api/v1/clients/address-book/$ADDRESS_ID" -H "Authorization: Bearer $TOKEN"
Expected: 200 and {"status":200}. Running it again gives the 404 shown under Errors.
Differences from Shukah
| Aspect | Shukah | Locate | Impact on the app |
|---|---|---|---|
| URL | DELETE $SHUKAH_BASE_URL/maeda/address-book/{id} | DELETE $AUTH_BASE_URL/api/v1/clients/address-book/{id} | Change the base URL and the path. |
| Success answer | 204 with an empty body (captured on dev, 2026-10-01). | 200 with {"status":200}. | None: the app does not read the body. |
| Id the app sends | The app turns the id into a number with a unary plus before the call. That works for Shukah's integers. | A 24-character id becomes NaN, so the app calls .../address-book/NaN. That is not all digits, so the service looks for an ObjectId NaN and answers 404 Address not found (code reading, not tried live). | The explicit delete on the Edit address screen fails until the app stops converting the id. The delete of a same-name address inside create does not convert it. |
404 body | {"name":"Not Found","message":"Object not found: 162119",...} (captured on dev, 2026-10-01). | {"status":404,"message":"Address not found"} | The app shows message, so the text it shows changes. |
| Copy to Shukah | Not applicable. | The delete is never copied to Shukah. | An address created or edited through Locate and then deleted through Locate can stay on Shukah. |
Watch out
- The app turns an address id into a number before it deletes, so a Locate id (24 hex characters) becomes
NaNand the delete fails with404. - Create answers
{status, address}instead of the address itself, and it does not fill in street, city or formatted address from the coordinates the way Shukah does. - List and create return booleans and numbers, get and update return
0/1and string coordinates, and the list can hold several default addresses.
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. Error bodies on dev carry a stack field (captured
on 2026-10-01 on the 404 for get-address and on the profile and setting 400s), so dev does not
run with NODE_ENV=production; the handler adds stack only outside production
(src/middlewares/error-handler.js:12-35). The same handler makes an unexpected 500 carry the raw error text and a stack on dev, and Internal server error only in production (src/middlewares/error-handler.js:28-33).
Id lookup
findAddressIndexByApiId turns the path id into text. If it matches ^\d+$ it looks for an address
whose shukah_address_id equals that number, and if none matches it falls back to the sub-document
_id (src/controllers/client/address-book/lookup.js:13-25). Get, update and delete all use it. The
lookup runs over the caller's own addresses array, which is why another client's id gives 404
(src/controllers/client/address-book/get.js:11-14; the service doc lists the same 404 for an id
that is not the current user's, docs/api/client-auth-and-address-book.md:259-263). The Shukah id is
copied from mt_address_book.id for addresses that came from Shukah's address book, and is null for
addresses made through the API and for the inline address built from the client row
(src/models/client.js:19, src/sync/mappers/client-mapper.js:32-47,50-53).
Write rules
- Create reads only the keys in its request table, so unknown keys,
source,shukah_address_idand the timestamps in a body are ignored. It setssource: 'locate',shukah_address_id: nulland both timestamps to now (src/controllers/client/address-book/parse-payload.js:4-21,src/controllers/client/address-book/create.js:13,18). - The casts:
toStrtrims and turns blank ornullintonull;toNumgivesnullfornull,""and non-finite numbers;toBoolis true only for1,"1"andtrue;toImageUrlsaccepts an array, a JSON-array string or a comma-separated string, trims, and drops blanks (src/sync/lib/casts.js:1,56-81). - Update applies a key only when the body has it (
hasOwnProperty), so a presentnullclears the field (src/controllers/client/address-book/parse-payload.js:39-57). It runs the default rule only when the body hasis_default(src/controllers/client/address-book/update.js:22-24,src/controllers/client/address-book/parse-payload.js:60-75). - Create's default rule is in
resolveDefaultOnCreate(src/controllers/client/address-book/parse-payload.js:24-37), and delete's inresolveDefaultOnDelete, which promotesaddresses[0](parse-payload.js:78-82,src/controllers/client/address-book/remove.js:15-17). - The handlers reload the client without the query cache, so a read or write never uses the cached
300-second copy (
src/controllers/client/address-book/load-fresh-client.js:7-8). - A save that races with an inbound change from Shukah can fail with a Mongoose
VersionErrorand give500(code reading; the inbound push bumps the document version on purpose,docs/plans/2026-07-13-shukah-address-push-api.md:58-59).
Shapes
The list and create answers come from formatAddressForResponse, which uses ?? null for text and
Boolean(is_default) (src/controllers/client/address-book/mapper.js:4-28). Get and update come from
formatAddressDetailForResponse, which turns every missing text into "", is_default into 0 or
1, and lat and lng into text (src/controllers/client/address-book/mapper.js:30-51). The list is
sorted by created_at, newest first, with addresses that have no date last
(src/controllers/client/address-book/handler.js:9-15).
Default addresses in the data
The "one default" rule is enforced only by these three app calls. The ETL picks one default per
client when it builds the document (src/sync/mappers/client-mapper.js:80-87), but the inbound
Shukah-to-Locate address push does no election (src/controllers/admin/client-addresses/handler.js:67-68).
That is how all 10 captured list items can be default (2026-10-01), and 28 of 30 (2026-10-02). On
2026-10-02 the copy of a Shukah-made address arrived with is_default true, although Shukah sent 0
(cause: Not documented yet). The service doc still says the server always
keeps exactly one default (docs/api/client-auth-and-address-book.md:291).
Push to Shukah
After a local save, create and update push the address to Shukah, best effort and awaited. Create
stores the returned id as shukah_address_id. Update pushes to {shukah_address_id} when the address
has one, and otherwise falls back to the create push. Delete is not pushed
(src/services/shukah-sync.js:238-415; docs/plans/2026-09-22-shukah-address-outbound-push.md:106-110,149-151).
The push does nothing when sync is off or the host or admin token is not set. Whether it is on for
dev: Not documented yet. The 2026-10-01 captures suggest it was not working for this client: after
the Locate create, update and delete, Shukah's list still had 13 addresses, while a copied create with
no copied delete would have left 14.
On 2026-10-02 the cause was found in the code: the create push posts to /maeda/api/address-book
(src/services/shukah-sync.js:310), which dev Shukah answers with 404; Shukah serves
/maeda/address-book. The failure is swallowed and only written to sync_logs (:335). The update
push uses /maeda/address-book/{id} (:356) but only for an address that already has a Shukah id;
others fall back to the broken create. The other direction is broken too: a Shukah create reached
Locate within seconds, but a Shukah update and delete never did. Locate has the receivers
(src/routes/admin.js:52-54), so Shukah's Laravel port is not calling them (inference). Locate also
lost the photo URLs of the 4 Shukah addresses that have one; toImageUrls handles arrays and JSON
strings (src/sync/lib/casts.js:67-81), so the loss is upstream of it. Cause: Not documented yet.
Where the docs and the code disagree (code wins)
- The service doc says an address
sourceis"locate"or"shukah"(docs/api/client-auth-and-address-book.md:224). The model allowsmt_client_inline,mt_address_bookandlocate(src/models/client.js:18), and the capture showsmt_address_book. - The service doc says the server guarantees exactly one default
(
docs/api/client-auth-and-address-book.md:291). That holds only for the app calls, as above. - An early design doc says get returns
{status, address}(docs/plans/2026-07-07-shukah-address-book-api-local.md:13-14), and corrects it to a flat object at lines 23-24. The code returns the flat object. - The design docs write the Shukah paths as
/maeda/api/address-book(docs/plans/2026-07-07-shukah-address-book-api-local.md:49). The app calls/maeda/address-book, and dev Shukah answers404for the/api/form, so the pages use the app's path. - The scrubbed captures show Shukah returning
latandlngas numbers, while the app's own type declares strings (src/entities/location/model/types/location.server.ts:3-19). - 2026-10-02: this page gave 13 Shukah and 28 Locate addresses, no default on Shukah and all 10 default
on Locate, and an unknown Shukah order. The 2026-10-02 captures show 15 and 30, Shukah
0on all 15, Locatetrueon 28 of 30, and Shukah ordered byposition. - 2026-10-02: this page expected no
image_urlsdifference on list and get. Locate lost the photo URLs of the 4 Shukah addresses that have one. - 2026-10-02: this page gave get's
is_defaultas the same on both sides. The same address is0on Shukah and1on Locate. - 2026-10-02: this page said a foreign id gets the same answer on both sides. Shukah answers
403; Locate answers404(code reading). - 2026-10-02: this page said the
nulladdress parts on create were code reading. They are captured now. - 2026-10-02: the create and update "best-effort copy to Shukah" is in the code
(
src/controllers/client/address-book/create.js:23,src/controllers/client/address-book/update.js:31), but no copy reached dev Shukah on 2026-10-02 either (see Push to Shukah).
Sources
locate-authentication-service@6757aa4:src/routes/client.js:17-21,src/controllers/client/address-book/handler.js:6-17,src/controllers/client/address-book/get.js:9-22,src/controllers/client/address-book/create.js:10-28,src/controllers/client/address-book/update.js:12-37,src/controllers/client/address-book/remove.js:8-21,src/controllers/client/address-book/lookup.js:13-25,src/controllers/client/address-book/load-fresh-client.js:7-8,src/controllers/client/address-book/mapper.js:4-51,src/controllers/client/address-book/parse-payload.js:4-82,src/sync/lib/casts.js:1,56-81,src/models/client.js:16-34,src/middlewares/error-handler.js:12-35,src/sync/mappers/client-mapper.js:32-87,src/controllers/admin/client-addresses/handler.js:67-68,src/services/shukah-sync.js:238-415locate-authentication-service@6757aa4docs:docs/api/client-auth-and-address-book.md§4 to §8 (lines 187-370, 224, 259-263, 291; last changed 2026-07-27),docs/plans/2026-07-07-shukah-address-book-api-local.md:13-24,49,226,docs/plans/2026-07-13-shukah-address-push-api.md:58-59,docs/plans/2026-09-22-shukah-address-outbound-push.md:106-110,149-151- Mobile snapshot
mobile/locate-client@e960c3a(branchdevelopment, 2026-02-26; the app's own code, not Locate):src/entities/location/model/api/services/location.service.ts:21-24,37-41,55-59,73-78,92-96,src/entities/location/model/api/controllers/location.controller.ts:38-110,src/entities/location/model/api/dto/location.dto.ts:38-86,105-126,144-199,src/entities/location/model/types/location.server.ts:3-19,src/entities/location/model/constants/location.ts:15-18,src/entities/location/model/helpers/locationPresetToSelectedAddress.ts:3-20,src/entities/location/model/services/fetchLocationPresets/fetchLocationPresets.ts:17,src/entities/location/model/services/fetchLocationPresetById/fetchLocationPresetById.ts:17-19,src/entities/location/model/services/createLocationPreset/createLocationPreset.ts:50-92,src/entities/location/model/services/updateLocationPreset/updateLocationPreset.ts:18-20,src/entities/location/model/services/deleteLocationPreset/deleteLocationPreset.ts:20-21,src/entities/location/model/services/initLocation/initLocation.ts:33-38,78,src/entities/location/model/slice/location.slice.ts:56-58,src/features/SavedLocationsList/ui/SavedLocationsList/SavedLocationsList.tsx:70,src/features/UserLocationInfo/ui/UserLocationInfo/UserLocationInfo.container.tsx:44-46,src/widgets/LocationDrawer/ui/LocationDrawer/LocationDrawer.tsx:42-44,src/features/MapDrawer/model/services/createLocationPreset/saveLocation.ts:69-80,src/widgets/Map/ui/Map/Map.tsx:105,src/features/EditSavedAddress/model/services/init.ts:13-17,src/features/EditSavedAddress/model/services/updatePreset.ts:62-70,src/features/EditSavedAddress/model/services/deletePreset.ts:19,src/shared/api/reqError.ts:52-62 locate-authentication-service@6757aa4, push and sync:src/services/shukah-sync.js:310,335,356,src/routes/admin.js:52-54,src/sync/lib/casts.js:67-81- Dev captures, 2026-10-02 (parity audit):
addr-list-before--shukah,addr-list-before--locate-jwt,address-book-get--shukah,address-book-get--locate-jwt,address-book-get-shukahid--shukah,address-book-get-shukahid--locate-jwt,address-book-get-unknown--shukah,address-book-get-unknown--locate-jwt,address-create--shukah,address-create--locate-jwt,address-update--shukah,address-update--locate-jwt,address-delete--shukah,address-delete--locate-jwt,addr-list-after-create--shukah,addr-list-after-update--shukah,addr-list-after-delete--shukah,addr-list-after-delete--locate-jwt - Dev captures, 2026-10-01:
locate-address-list,locate-address-get,locate-address-create,locate-address-get-created,locate-address-update,locate-address-delete,locate-address-get-deleted,locate-address-list-after-shukah-writes,shukah-address-list,shukah-address-get,shukah-address-create,shukah-address-update,shukah-address-delete,shukah-address-get-deleted,shukah-address-list-after-locate-writes,probe-locate-address-list,probe-locate-address-get,probe-locate-address-create,probe-locate-address-update,probe-locate-address-delete