Shopping
Location & store discovery
How Tote places customers, stores and drivers on the map, and how the customer app finds stores and offers nearby.
Everything in Tote is located by map coordinates (lat, lng). There are no cities or neighbourhoods in the location flow: a customer address, a store and a driver are each a point on the map, and "nearby" is a straight-line distance between points.
The customer's default address drives discovery#
The discovery endpoints do not take the device's GPS position. They search around the customer's default address.
So the first thing a new customer must do is save an address with is_default: true. Until they have one, nearby-stores and the offer feeds return 404 with a message asking for a default address. Treat that 404 as "send the user to the address picker", not as an error.
To show stores around a different place, save or switch the default address and call discovery again.
Saving an address#
POST profile/address takes:
| Field | Rule |
|---|---|
lat, lng |
required, decimal degrees |
type |
gps when taken from the device location, manually when picked on the map |
address |
optional free text shown to the store and driver |
is_default |
optional; true makes it the default and clears the flag on every other address |
The response is the customer's full address list, so the app can replace its local copy.
The allowed area#
Tote only delivers inside an area drawn by the operations team. An address outside it is refused with 422, the message under both errors.lat and errors.lng.
To stop the user before they submit, fetch the area from GET location-boundary and draw it on the map. It is a GeoJSON Polygon:
{
"type": "Polygon",
"coordinates": [[[46.60, 24.60], [46.85, 24.60], [46.85, 24.85], [46.60, 24.85], [46.60, 24.60]]]
}
- Points are
[lng, lat], in GeoJSON order — the reverse of thelat,lngfields everywhere else. - The first ring is the area; any further rings are holes cut out of it.
- An empty
coordinatesarray means no area is configured, and every location is accepted.
Search radius#
Proximity endpoints accept an optional ?radius= in kilometres. The server applies the platform default when it is missing and caps it at the platform maximum; both are set by Tote. Every proximity response echoes the radius it actually used as radius_km, so the app can say "stores within 5 km".
Finding stores#
| Endpoint | Returns |
|---|---|
nearby-stores |
Active stores around the default address, nearest first, with distance (for example "1.25 km"). Pass ?limit= for a short unpaginated list, for a home screen row |
stores/popular |
Stores Tote has marked as featured, nearest first |
stores/{id} |
One store: min_order_price, working_hours, is_closed, return policy and return_window_days |
search |
Stores, products and categories matching query (at least 2 characters). Narrow it with type=store, product or category |
Open and closed#
is_closed is computed by the server from the store's working_hours and the current time in Riyadh. Use it rather than reading working_hours yourself: the device may be in another time zone. A store is closed when:
- today is not enabled in its hours, or the current time is outside them (overnight hours such as 22:00–02:00 are supported);
- it has no working hours at all;
- Tote has taken it offline temporarily.
Offer feeds#
Three feeds list discounted products in stores near the default address:
- best offers — products on an active offer;
- most bought — offer products customers have actually ordered while the offer ran;
- limited — offers with a usage limit, which disappear once it is used up.
How an offer changes the price is explained in Cart & checkout.