Skip to main content

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:

CallWhere in the app
ListWhen 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.
GetWhen the client opens the Edit address screen.
CreateWhen 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.
UpdateWhen the client saves the Edit address screen. The app sends location_name, area, is_default and image_urls.
DeleteWhen 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_default is a boolean, lat and lng are numbers, a missing text field is null, and there are source, created_at and updated_at keys. Get and update use the detail shape: is_default is 0 or 1, lat and lng are 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-numeric lat or lng is stored as null. 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_urls becomes []). source, the Shukah id and created_at never 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 404 with Address not found.
  • One default. The service keeps one default address when the app creates, updates or deletes:
CallRule
Createis_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.
UpdateOnly 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.
DeleteIf 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​

Replaces ShukahGET /maeda/address-book✅ Replaced
Adapt2 breaking · 5 to watch
  • 🔴 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​

FieldInTypeRequiredConstraintsDescription
AuthorizationheaderstringyesBearer <locate-jwt>. A Shukah token gets 401.The Locate JWT.
Any query key or other headerquery, header—noAccepted 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.

FieldTypeNullableFormat / constraintsDescription
idstringnoA 24-character hex ObjectId. Never the Shukah integer id.The address id for get, update and delete.
sourcestringyesmt_client_inline, mt_address_book or locate.Where the address came from: locate for an address created through this API.
is_defaultbooleannotrue 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_addressstringyesnull when missing. An empty string that is already stored stays "" (the capture has state: "").The address parts.
lat, lngnumberyesJSON numbers. No range check.Coordinates.
image_urlsarray of stringnoAlways an array, possibly empty.Photo URLs.
created_at, updated_atstringyesISO 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​

HTTPBodyWhen
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.
401Same body.The token is valid but the client record cannot be loaded.
500On 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​

AspectShukahLocateImpact on the app
URLGET $SHUKAH_BASE_URL/maeda/address-bookGET $AUTH_BASE_URL/api/v1/clients/address-bookChange the base URL and the path.
TokenShukah 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.
idAn 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_default0 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 keysposition (an integer).source, created_at, updated_at; no position.None: the app's address type has none of these four keys.
image_urlsAn 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.
OrderBy 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 35625915 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 addressesis_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​

Replaces ShukahGET /maeda/address-book/:id✅ Replaced
Adapt1 breaking · 5 to watch
  • 🔴 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​

FieldInTypeRequiredConstraintsDescription
AuthorizationheaderstringyesBearer <locate-jwt>.The Locate JWT.
idpathstringyesA 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 headerquery, header—noAccepted and ignored.—

Response​

200 with one address in the detail shape, with no wrapper:

FieldTypeNullableFormat / constraintsDescription
idstringnoThe 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_codestringno"" when missing, never null.The address parts.
is_defaultintegerno0 or 1.The default address.
lat, lngstringnoThe number as text, for example "24.7136". "" when missing.Coordinates.
image_urlsarray of stringnoPossibly 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​

HTTPBodyWhen
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​

AspectShukahLocateImpact on the app
URLGET $SHUKAH_BASE_URL/maeda/address-book/{id}GET $AUTH_BASE_URL/api/v1/clients/address-book/{id}Change the base URL and the path.
idAn 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, lngJSON 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_default0 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_urlsAn 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 keyposition.None.None: the app does not read it.
Someone else's address403 {"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​

Replaces ShukahPOST /maeda/address-book✅ Replaced
Adapt3 breaking · 2 to watch
  • 🔴 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.

FieldInTypeRequiredConstraintsDescription
AuthorizationheaderstringyesBearer <locate-jwt>.The Locate JWT.
location_name, street, area, area_name, city, state, zipcode, country, country_code, formatted_addressbodystringnoTrimmed. Blank or null is stored as null. No length limit.The address parts.
lat, lngbodynumber or numeric stringnonull, "" and values that are not numbers are stored as null. No range check.Coordinates.
image_urlsbodyarray of stringnoStrings are trimmed and blank ones dropped. null gives []. A JSON-array string or a comma-separated string is also accepted.Photo URLs.
is_defaultbodyboolean, 0 or 1noOnly 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:

FieldTypeNullableFormat / constraintsDescription
statusintegernoAlways 200.Status echo.
addressobjectnoThe 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​

HTTPBodyWhen
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.
500On 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​

AspectShukahLocateImpact on the app
URLPOST $SHUKAH_BASE_URL/maeda/address-bookPOST $AUTH_BASE_URL/api/v1/clients/address-bookChange the base URL and the path.
ResponseThe 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 partsA 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 addressThe 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 storedIn 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​

Replaces ShukahPUT /maeda/address-book/:id✅ Replaced
Adapt1 breaking · 5 to watch
  • 🔴 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.

FieldInTypeRequiredConstraintsDescription
AuthorizationheaderstringyesBearer <locate-jwt>.The Locate JWT.
idpathstringyesDigits 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_addressbodystring or nullnoSame cleaning as create. A present key with null, "" or spaces sets the field to null.The address parts.
lat, lngbodynumber, numeric string or nullnoSame cleaning as create.Coordinates.
image_urlsbodyarray of string or nullnoSame cleaning as create. null gives [].Photo URLs.
is_defaultbodyboolean, 0 or 1noOnly 1, "1" and true mean default. See the default rules under Feature.Make this the default address, or not.
source, created_at, any other keybody—noIgnored.—

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​

HTTPBodyWhen
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.
500On 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​

AspectShukahLocateImpact on the app
URLPUT $SHUKAH_BASE_URL/maeda/address-book/{id}PUT $AUTH_BASE_URL/api/v1/clients/address-book/{id}Change the base URL and the path.
ResponseThe 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 rulesNot documented yet.The rules under Feature.If the app relies on Shukah's way of picking a default, compare before switching.
Copy to ShukahNot 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​

Replaces ShukahDELETE /maeda/address-book/:id✅ Replaced
Adapt2 breaking · 2 to watch
  • 🔴 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​

FieldInTypeRequiredConstraintsDescription
AuthorizationheaderstringyesBearer <locate-jwt>.The Locate JWT.
idpathstringyesDigits 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:

FieldTypeNullableFormat / constraintsDescription
statusintegernoAlways 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​

HTTPBodyWhen
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.
500On 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​

AspectShukahLocateImpact on the app
URLDELETE $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 answer204 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 sendsThe 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 ShukahNot 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 NaN and the delete fails with 404.
  • 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/1 and 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_id and the timestamps in a body are ignored. It sets source: 'locate', shukah_address_id: null and 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: toStr trims and turns blank or null into null; toNum gives null for null, "" and non-finite numbers; toBool is true only for 1, "1" and true; toImageUrls accepts 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 present null clears the field (src/controllers/client/address-book/parse-payload.js:39-57). It runs the default rule only when the body has is_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 in resolveDefaultOnDelete, which promotes addresses[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 VersionError and give 500 (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 source is "locate" or "shukah" (docs/api/client-auth-and-address-book.md:224). The model allows mt_client_inline, mt_address_book and locate (src/models/client.js:18), and the capture shows mt_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 answers 404 for the /api/ form, so the pages use the app's path.
  • The scrubbed captures show Shukah returning lat and lng as 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 0 on all 15, Locate true on 28 of 30, and Shukah ordered by position.
  • 2026-10-02: this page expected no image_urls difference 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_default as the same on both sides. The same address is 0 on Shukah and 1 on Locate.
  • 2026-10-02: this page said a foreign id gets the same answer on both sides. Shukah answers 403; Locate answers 404 (code reading).
  • 2026-10-02: this page said the null address 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-415
  • locate-authentication-service@6757aa4 docs: 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 (branch development, 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