App Fixes — Change Log
Author : Josh S. Sakweli, Backend Lead Team
Last Updated : 2026-10-25
Version : v1.0
Backend version / branch : 1.2.4 · fet/location_service (+ notification server fet/location_service , File Thunder fet/shipping )
Deployed to : not yet — staging first
Compared with : the staging branch as deployed ( 448f06a0 , 2026-10-06) — "Before" means what the app sees on staging today
Base URL : https://api.nexgate.co/api/v1 (staging: https://dev.api.nexgate.co/api/v1 )
Short Description : Shops now have a real place on the map, buyers keep an address book, and every shop in a checkout is
either pickup at the shop or delivery by a shipper with a real price. This changes existing shop, product, cart,
checkout and order endpoints — the old fake shipping fields ( shippingAddressId , shippingMethodId , trackingNumber ,
carrier ) are gone. The release also carries app fixes: digital downloads, chat avatars, profile cover, donation target,
the sign-in alert. Android, iOS and web must update before this goes to production; the admin dashboard has a few changes.
Audience : mobile team · web team · admin dashboard
Related docs — the new endpoints are not repeated here, they live in their own docs:
docs/location/location_api_doc.md — pin → area, area lists, shop location, my addresses, recent pins
docs/shipping/shipping_api_doc.md — delivery options, cart delivery, every pickup / delivery order action, shippers, admin
docs/shipping/campaigns/campaigns_api_doc.md — delivery discounts run by shops, shippers and NexGate
notifications_inapp_bell_api_doc.md — the bell list, targetType / targetId
Also new since staging, each with its own doc (already shared, not repeated here):
event_group_tickets_api_doc.md — event tickets "buy together": groupBuying on tickets, group on POST /e-events/checkout ,
ticket groups (open, by code, mine, transfer, withdraw), 4 notifications
product_group_purchase_api_doc.md — product group buys: whole-payment transfer, withdraw, orders always created,
true savings, 15-minute expiry
This log covers what changed in endpoints, fields, rules and notifications that already existed , plus the few new
endpoints that sit inside an existing service and have no other doc.
How to read this
Change types
Badge
Means
App must act?
NEW
a new endpoint, field or notification
only to use the new feature
CHANGED
an existing endpoint gained fields or behaves differently; old calls still work
should update
BREAKING
old calls fail or give a different meaning — a field removed or renamed, a rule tightened
must update before the deploy date
REMOVED
the endpoint / field / notification is gone
must stop using it
FIX
a bug fixed; same shape, now correct values
check screens that relied on the wrong value
HTTP methods
GET - GET
POST - POST
PUT - PUT
PATCH - PATCH
DELETE - DELETE
Rules this log follows
Grouped by service . Inside a service: what changed, the flow (when the order of steps changed), the endpoint table,
then one block per endpoint.
Every changed endpoint shows Before and After for the request and the response — only the parts that changed.
Samples are short: the changed fields and one or two around them.
Coded refusals are HTTP 409 with data.code (and now, sometimes, data.details ). New codes are listed per endpoint
and again in the index (§16).
0. At a glance
#
Service
New
Changed
Breaking
Removed
App work
1
Shops
4
6
2
6 fields
location step after create; opening hours + pause screens; read location , availability ; show review replies
2
Products
—
3
—
—
Directions on the product page; "Bulky" switch
3
Cart
1
4
—
—
group by shop, delivery per shop, ticks, selectedTotal
4
Checkout & payment
—
4
1
2 request + 2 response fields
send fulfillment ; handle price-changed codes
5
Orders
—
6
2
3 fields
read fulfillment ; pickup orders use the new order actions
6
Installments
—
3
—
1 field
delivery for "ships now" plans; cancel via the agreement
7
Group purchase
—
1
—
—
pickup / delivery in the group sheet
8
Wallet & transactions
—
1
—
—
new history titles
9
Events
—
4
1
—
venue pin required to publish; donation progress
10
Profile & account
—
2
—
—
cover photo
11
Interests
—
1
—
—
(admin) only admins edit categories
12
Chat
—
2
—
—
avatars are now real links
13
Digital downloads
—
1
—
—
save the file as received
14
Admin (existing screens)
1
2
—
—
review moderation, fee type, reconciliation lines
15
Notifications
26
9
—
—
handle new types and 4 new targets
0.1 Breaking changes — do these first
Service
Endpoint / item
What breaks
Do this
Section
Shops
create / update shop
city , region , streetAddress , landmark , latitude , longitude no longer exist
stop sending them; add the Shop location step
§1.2
Shops
every shop response, shop search
the same fields are gone from responses
read hasLocation + location
§1.3, §1.4
Checkout
POST /e-commerce/checkout-sessions , PATCH …/{id}
shippingAddressId / shippingMethodId removed; a session with physical items needs fulfillment
send fulfillment
§4.2
Checkout
session response
shippingAddress / shippingMethod removed
read fulfillment
§4.3
Orders
order detail
deliveryAddress , trackingNumber , carrier removed
read fulfillment
§5.2
Orders
/orders/{id}/ship , /confirm-delivery , /regenerate-code
refused for pickup orders and for orders a shipper delivers
use the shipping order actions
§5.4
Orders
delivery code
6 digits → 4 digits , expires after 24 h, wrong code is a coded 409
4-digit input, handle the codes
§5.5
Events
publish / reveal-location
an in-person or hybrid event needs a venue pin
pin screen on the venue step
§9.2
0.2 Removed
Service
Endpoint / field / notification
Use instead
Section
Shops
request + response fields city , region , streetAddress , landmark , latitude , longitude
location (shop location)
§1.2–§1.4
Checkout
shippingAddressId , shippingMethodId (request) · shippingAddress , shippingMethod (response)
fulfillment
§4.2, §4.3
Orders
deliveryAddress , trackingNumber , carrier (detail) · trackingNumber on /ship
fulfillment
§5.2
Installments
shippingAddress on the agreement
fulfillment
§6.1
1. Shops
What changed in this service : a shop's place is no longer typed text (city, region, landmark). It is a pin on the
map , turned by the backend into ward / district / region, set with the shop location endpoint (location doc). Every shop
response now carries that location , and an availability block (open now, paused, taking orders). Paused shops stay
visible. Shop reviews finally show the owner's reply.
1.0 Flow — before and after
Before
flowchart LR
A[Create shop
type city, region, street, landmark, lat/lng] --> B[Shop live]
After
flowchart LR
A[Create shop
no location fields] --> B[Shop location step
drop a pin: PUT /shops/id/location] --> C[Shop can sell physical items]
A -. no location yet .-> D[Physical items refused at checkout:
SHOP_LOCATION_REQUIRED]
What the user notices : the seller drops a pin instead of typing an address; buyers see "Kariakoo, Ilala, Dar es
Salaam" with a Directions and a Call button; shops show "Open now · closes 18:00" or "Paused until …".
1.1 Endpoints in this service
Change
Method
Endpoint
One line
App
BREAKING
POST
/e-commerce/shops
location fields removed
must update
BREAKING
PUT
/e-commerce/shops/{shopId}
location fields removed
must update
BREAKING
GET
every shop response (profile, detailed, lists, create / update / approve)
old location fields out, hasLocation + location + availability in
must update
BREAKING
GET
/e-commerce/shops/search
city → location
must update
CHANGED
GET
/e-commerce/search (global)
shop.location is now the area name
check display
CHANGED
—
product pages, search, subscribe, event / post / clip shop links
paused shops are accepted, not 404
no change
CHANGED
GET
shop reviews (list, paged, my-review, create / update)
gain replyText , repliedAt
should update
FIX
GET
shop rating, /summary
counts only ACTIVE reviews
check
NEW
PUT
/e-commerce/shops/{shopId}/hours
set the shop's opening hours
to use
NEW
GET
/e-commerce/shops/{shopId}/hours
read them
to use
NEW
PUT
/e-commerce/shops/{shopId}/pause
pause the shop
to use
NEW
DELETE
/e-commerce/shops/{shopId}/pause
reopen it
to use
New, documented elsewhere: shop location PUT/GET /e-commerce/shops/{shopId}/location → location doc §6–§7.
1.2 BREAKING Create / update shop — location fields removed
Endpoint : POST {base_url}/e-commerce/shops · PUT {base_url}/e-commerce/shops/{shopId}
Change
BREAKING in meaning — old bodies are still accepted (the fields are ignored) but nothing is saved from them
Impact
Must update
Reference
commerce_logistics_changes.md §5
In one line : the shop's place is no longer part of create / update; set it with the shop location step.
Request
Field
Before
After
Note
city , region
required on create
removed
streetAddress , landmark , latitude , longitude
optional
removed
landmark + pin now live in the shop location
phoneNumber , countryCode , the rest
unchanged
unchanged
phoneNumber is also the shop location's phone
Before After
{
"shopName": "Mama Yoyo Shop",
"phoneNumber": "+255744742944",
"city": "Dar es Salaam",
"region": "Dar es Salaam",
"landmark": "Opposite the clock tower"
}
{
"shopName": "Mama Yoyo Shop",
"phoneNumber": "+255744742944"
}
App to do
Remove city / region / street / landmark / lat / lng inputs from the shop form.
After create, open the Shop location step ( PUT …/location , location doc §6); editing uses the same screen.
1.3 BREAKING Shop responses — location , hasLocation , availability
Endpoint : every shop response — GET /e-commerce/shops/{shopId} , /{shopId}/detailed , /shops/all , /featured ,
/my-shops (and paged / category forms), create / update / approve.
Change
BREAKING — fields removed; new blocks added
Impact
Must update
Reference
§6, §21
Response
Field
Before
After
Note
streetAddress , city , region
present
removed
latitude , longitude , landmark
present
removed
hasLocation
—
added
true / false
location
—
added
same object as GET …/location (location doc §7), or null
availability
—
added
open now / paused / taking orders
Before After
{
"shopName": "Mama Yoyo Shop",
"city": "Dar es Salaam",
"region": "Dar es Salaam",
"landmark": "Opposite the clock tower",
"latitude": -6.819, "longitude": 39.2768
}
{
"shopName": "Mama Yoyo Shop",
"hasLocation": true,
"location": { "lat": -6.819, "lng": 39.2768,
"areaLabel": "Kariakoo, Ilala, Dar es Salaam",
"landmark": "Opposite the clock tower",
"phone": "+255744742944", "…": "…" },
"availability": { "status": "ACTIVE", "acceptingOrders": true,
"paused": false, "pauseMode": null, "pausedUntil": null, "pauseReason": null,
"hours": { "hasHours": true, "openNow": true,
"closesAt": "2026-10-25T18:00:00+03:00", "nextOpensAt": null } }
}
Behaviour
availability.acceptingOrders is false when the shop is paused with STOP_ORDERS , suspended or closed → Buy is off.
pauseMode : KEEP_SELLING (shows "paused" but orders still come in, accepted later) or STOP_ORDERS .
hours.openNow is null when the shop has set no hours. All times are East Africa Time with the offset.
Paused shops stay visible. Product pages, product and shop search, subscribe, profile shop counts, linking a shop to
an event and shop tags on posts / clips now accept a paused ( TEMPORARILY_OFFLINE ) shop — before, its products 404'd.
App to do
Show area / map / Directions ( lat,lng → Google / Apple Maps) / Call ( phone ) from location .
Owner: when hasLocation is false show "Add your shop location to start receiving orders".
Shop page: "Open now · closes 18:00" / "Opens Mon 08:00" / "Paused until …"; Buy off when acceptingOrders is false.
1.4 BREAKING Shop search and global search
Endpoint : GET {base_url}/e-commerce/shops/search · GET {base_url}/e-commerce/search?query=…
Response
Field
Before
After
shop search items
city
owner-typed text
removed
shop search items
location
—
added (location object or null )
global search, type: "SHOP"
shop.location (string)
owner-typed landmark
the area name, e.g. "Ubungo, Ubungo, Dar es Salaam" , or null
Reference : §7, §8
1.5 CHANGED Shop reviews — the owner's reply is returned
Endpoint : shop review list, /paged , /my-review , create / update responses.
Change
CHANGED (+ FIX on the rating)
Impact
Should update
Reference
§31
In one line : the reply the owner saved with POST /shops/reviews/{shopId}/{reviewId}/reply was never returned; now it is.
Field
Before
After
replyText , repliedAt
—
added (null when no reply)
FIX : the shop's average, rating count and the 5★…1★ breakdown ( /summary , and the rating on shop responses) counted
every review whatever its status; now only ACTIVE reviews count, so a hidden review leaves the stars at once.
App to do
Show the shop's reply under a review when replyText is set.
1.6 NEW Shop opening hours
Purpose : the hours that drive availability.hours ("Open now · closes 18:00"), the seller's 24-opening-hour accept
window and the buyer's pickup times.
Endpoint : PUT {base_url}/e-commerce/shops/{shopId}/hours · GET {base_url}/e-commerce/shops/{shopId}/hours
Access Level : 🔒 PUT: shop owner, or a super / staff admin · GET: any logged-in user
Authentication : Bearer token
Request JSON Sample (PUT — send the whole week every time; it replaces what was there):
{
"weekly": [
{ "day": "MONDAY", "open": "08:00", "close": "18:00" },
{ "day": "SATURDAY", "open": "09:00", "close": "13:00" },
{ "day": "SATURDAY", "open": "14:00", "close": "17:00" }
],
"specialDays": [
{ "date": "2026-12-25", "closed": true, "ranges": [], "note": "Christmas" },
{ "date": "2026-12-31", "closed": false, "ranges": [ { "open": "08:00", "close": "12:00" } ] }
]
}
Request Body Parameters :
Parameter
Type
Required
Description
Validation
weekly
array
Yes
{ day, open, close } ; several ranges per day allowed; a day not listed = closed
max 21; day MONDAY … SUNDAY ; times HH:mm
weekly[].close
time
Yes
earlier than open = runs past midnight (e.g. 20:00 → 02:00)
specialDays
array
No
one date that differs from the week: closed: true , or its own ranges
max 60; each date once; not closed → at least one range (max 3)
specialDays[].note
string
No
e.g. "Christmas"
max 100
sameDayCutoff
time
No
not used for shops (shippers only)
Success Response JSON Sample (PUT and GET):
{ "success": true, "httpStatus": "OK", "message": "Opening hours saved",
"data": { "weekly": [ { "day": "MONDAY", "open": "08:00:00", "close": "18:00:00" } ],
"specialDays": [ { "date": "2026-12-25", "closed": true, "ranges": [], "note": "Christmas" } ],
"sameDayCutoff": null, "timeZone": "Africa/Dar_es_Salaam", "updatedAt": "2026-10-25T10:12:00" } }
Success Response Fields :
Field
Description
weekly, specialDays
as saved, weekly sorted by day then open time
timeZone
always East Africa Time
Errors :
Code / message
Status
When
HOURS_INVALID
409
a special day listed twice, or not closed and with no hours
"Only the shop owner can change the shop's hours or pause it"
403
not the owner
"This shop has not set its opening hours yet"
404
GET before any hours were saved
Rules : a shop with no hours counts as always open ( availability.hours.hasHours: false , openNow: null ). The status
on shop responses updates at once after a save.
App to do :
Seller: "Opening hours" screen (a row per day, add a second range, special days) → PUT the whole thing.
1.7 NEW Pause / reopen the shop
Purpose : the seller takes a break — with or without still taking orders.
Endpoint : PUT {base_url}/e-commerce/shops/{shopId}/pause · DELETE {base_url}/e-commerce/shops/{shopId}/pause
Access Level : 🔒 shop owner, or a super / staff admin
Authentication : Bearer token
Request JSON Sample (PUT):
{ "mode": "STOP_ORDERS", "until": "2026-10-28T09:00:00+03:00", "reason": "Stock taking" }
Request Body Parameters :
Parameter
Type
Required
Description
Validation
mode
enum
Yes
STOP_ORDERS — checkout refused with SHOP_CLOSED ; KEEP_SELLING — shows "paused" but orders still come in and wait
until
datetime with offset
No
reopens by itself at that time (checked every 5 minutes); leave out = until reopened by hand
must be in the future
reason
string
No
shown to buyers ("Paused: Stock taking")
max 255
Success Response JSON Sample (PUT "Shop paused", DELETE "Shop reopened") — the availability block (§1.3):
{ "success": true, "httpStatus": "OK", "message": "Shop paused",
"data": { "status": "TEMPORARILY_OFFLINE", "acceptingOrders": false, "paused": true,
"pauseMode": "STOP_ORDERS", "pausedUntil": "2026-10-28T09:00:00+03:00", "pauseReason": "Stock taking",
"hours": { "hasHours": true, "openNow": true, "closesAt": "2026-10-25T18:00:00+03:00", "nextOpensAt": null } } }
Errors :
Code / message
Status
When
SHOP_NOT_PAUSABLE
409
the shop is neither ACTIVE nor already paused (e.g. SUSPENDED, PENDING, closed)
"Only the shop owner can change the shop's hours or pause it"
403
not the owner
Rules :
Pausing a paused shop replaces its mode / until / reason. DELETE on a shop that is not paused changes nothing and returns
its availability.
A paused shop stays visible everywhere (§1.3).
KEEP_SELLING : orders are not auto-cancelled while paused; their 24-opening-hour accept deadline restarts when the
pause ends.
App to do :
Seller: "Pause shop" sheet (stop orders / keep selling, optional until, optional reason) and a "Reopen" button.
Buyer: "Paused until …" on the shop page from availability .
2. Products
What changed in this service : product responses carry the shop's location (for "Pickup at …" and Directions), a new
bulky flag tells shippers a parcel is big or heavy, and a refused save no longer half-saves.
2.1 Endpoints in this service
Change
Method
Endpoint
One line
App
CHANGED
GET
product detail, public view, by slug, shop product lists / filter / search
gain shopLocation / shop.location
should update
CHANGED
POST / PUT / PATCH
create / update product, every product response
new bulky
should update (seller form)
FIX
POST / PUT / PATCH
create / update / publish / delete / restore
a refused request changes nothing
no change
FIX
DELETE
/e-commerce/products/{shopId}/{productId}/installment-plans/{planId}
readable refusal for a used plan
no change
2.2 CHANGED Product responses — shop location
Response
Field added
GET …/products/{productId}/detailed (owner)
shopLocation
GET …/products/{productId} , by slug (public)
shopLocation
shop product lists / filter / search ( shop summary block)
shop.location
Same location object as the shop's, or null when the shop has none. Reference : §9
"shopLocation": { "lat": -6.7889, "lng": 39.2083, "areaLabel": "Ubungo, Ubungo, Dar es Salaam", "phone": "+255700111222", "…": "…" }
App to do
Product page: "Pickup at Ubungo, Ubungo" + Directions; optionally disable Buy for a physical product when shopLocation is null .
2.3 CHANGED Products — bulky
Before
After
Create ( POST )
—
optional "bulky": true (default false )
Update ( PUT / PATCH )
—
"bulky" changes it; leaving it out keeps it
Detailed, summary, public responses
—
"bulky": false | true (next to condition )
Shippers that do not carry bulky parcels are left out of its delivery options. The backend also ticks it by itself after
two different shippers report the product too big. Reference : §19, §45
App to do
Seller product form: "Bulky / heavy item" switch.
2.4 FIX Refused product saves / used installment plans
Before (wrong)
After (correct)
update + publish with no image → refused "At least one product image is required", but the new name / price were saved
a refused create / update / publish / delete / restore changes nothing
deleting an installment plan buyers used → raw database error
"Buyers have already used this plan, so it can't be deleted. Deactivate it instead."
Reference : §27, §71
3. Cart
What changed in this service : the cart now knows about delivery. It keeps one delivery address and, per shop with
physical items, pickup or delivery with a held price. Each item has a tick : checkout buys only ticked items.
Adding, changing or removing items answers with what that did to delivery.
3.0 Flow — before and after
Before
flowchart LR
A[Add to cart] --> B[Cart: items only] --> C[Checkout: pick address + DHL placeholder] --> D[Pay the whole cart]
After
flowchart LR
A[Add to cart
+ pickup / delivery for that shop] --> B[Cart: one address,
a choice per shop, held prices, ticks]
B --> C[Checkout: takes the cart's choices,
ticked items only] --> D[Pay: items + delivery]
D --> E[Bought items leave the cart;
unticked ones stay]
What the user notices : the cart is grouped by shop, each shop shows "Pickup at the shop" or "Delivery by Dar Cargo ·
TZS 3,500 · Tue–Wed", the total includes delivery, and a change of address re-prices every shop and says what changed.
3.1 Endpoints in this service
Change
Method
Endpoint
One line
App
CHANGED
POST
/e-commerce/cart/add
optional fulfillment + dropoff ; answers with the delivery change
should update
CHANGED
PUT
/e-commerce/cart/items/{itemId}
answers with the delivery change
should update
CHANGED
DELETE
/e-commerce/cart/items/{itemId}
answers with the delivery change
should update
CHANGED
GET
/e-commerce/cart
gains destination , shops , ticks, delivery totals
should update
CHANGED
DELETE
/e-commerce/cart/clear
keeps the delivery address
no change
NEW
PUT
/e-commerce/cart/selection
tick / untick items
to use
FIX
POST
/e-commerce/cart/add
two taps at once no longer fail one of them
no change
FIX
—
after a paid cart checkout
the bought items now leave the cart
re-read the cart
New, documented elsewhere: PUT /e-commerce/cart/destination , POST /e-commerce/cart/destination/change-back ,
PUT /e-commerce/cart/shops/{shopId}/fulfillment → shipping doc §B.
3.2 CHANGED Add to cart / change / remove an item
Endpoint : POST {base_url}/e-commerce/cart/add · PUT {base_url}/e-commerce/cart/items/{itemId} · DELETE {base_url}/e-commerce/cart/items/{itemId}
Change
CHANGED — old bodies still work
Impact
Should update
Reference
§32
In one line : the product sheet can send the shop's pickup / delivery choice, and every cart change says what it did to delivery.
Request (add only)
Field
Before
After
Note
productId , quantity
required
unchanged
fulfillment
—
added , optional: { type: PICKUP | DELIVERY, quotationId, optionId }
the choice from the product sheet
dropoff
—
added , optional
same shape as checkout; only when the buyer changed the address in the sheet — it becomes the cart's address
Before After
{
"productId": "5f0e…",
"quantity": 2
}
{
"productId": "5f0e…",
"quantity": 2,
"fulfillment": { "type": "DELIVERY", "quotationId": "3c1f…", "optionId": "o-1" },
"dropoff": { "addressId": "9b1d…" }
}
Response (add, change, remove)
Field
Before
After
Note
data
null
the delivery change
{ deliveryBefore, deliveryAfter, switchedToPickup, canChangeBack, message, changes }
Before After
{ "success": true,
"message": "Product added to cart successfully",
"data": null }
{ "success": true,
"message": "Product added to cart successfully",
"data": { "deliveryBefore": 10500, "deliveryAfter": 13000,
"switchedToPickup": 1, "canChangeBack": false,
"message": "Delivery 10,500 → 13,000, 1 shop switched to pickup",
"changes": [ { "shopId": "cfb6…", "shopName": "My Shop",
"before": { "type": "DELIVERY", "businessName": "Dar Cargo", "total": 3500 },
"after": { "type": "PICKUP", "businessName": null, "total": 0 },
"message": "…" } ] } }
Behaviour
A sheet quotation that no longer fits (other items, other address, expired) is not an error : the item is added and
the shop falls back to the cart rules.
FIX : two add-to-cart taps at the same moment for a new product used to fail one of them; now both succeed.
App to do
Send fulfillment (and dropoff when changed) from the product sheet.
Show data.message after add / change / remove.
3.3 CHANGED Get the cart
Endpoint : GET {base_url}/e-commerce/cart
Change
CHANGED — fields added; cartItems and totalAmount unchanged (items only)
Impact
Should update
Reference
§32, §48, §49, §65, §68
Response
Field
Before
After
Note
destination
—
added
the cart's delivery place (pin, areaLabel , label) or null
shops[]
—
added
one per shop with ticked physical items (below)
cartItems[].selected
—
added
the tick
cartSummary.deliveryTotal
—
added
delivery of ticked items, what the buyer pays
cartSummary.totalWithDelivery
—
added
cartSummary.selectedItems , selectedQuantity , selectedAmount
—
added
ticked items only
cartSummary.selectedTotal
—
added
ticked items + their delivery — the amount to pay
shops[] :
{ "shopId": "cfb6…", "shopName": "My Shop", "itemIds": ["…"],
"type": "DELIVERY", "chosenBy": "BUYER",
"quotationId": "3c1f…",
"selected": { "optionId": "o-1", "businessId": "c4bc…", "name": "Dar Cargo", "logoUrl": "…",
"reason": "BUYER_CHOICE", "cost": { "total": 7000 }, "eta": { "…": "…" },
"offer": { "listPrice": 7000, "price": 7000, "buyerPays": 3500, "shares": [ … ] } },
"pricesHeldUntil": "2026-10-25T14:30:00+03:00",
"notice": { "code": "NO_ADDRESS", "message": "Add a delivery address to see delivery prices" },
"nudge": { "campaignId": "…", "funder": "SELLER", "kind": "FREE", "value": null, "addMore": 4995.05,
"message": "Add 4,995 more from My Shop for free delivery" } }
Behaviour
Reading the cart refreshes expired or outdated quotations.
No address → pickup + "Add a delivery address…"; an address nobody covers → pickup + "Delivery is not available to X yet".
A pickup the buyer chose never changes by itself; a pickup set by default turns into delivery once a shipper covers.
chosenBy: "BUYER" ↔ selected.reason: "BUYER_CHOICE" now always agree. A shipper picked on an earlier cart shows with
the option label USED_BEFORE (not selected). (§49)
selected.offer (delivery campaigns, campaigns doc): show buyerPays , not cost.total . deliveryTotal and the change
messages count what the buyer pays.
nudge appears only while a campaign's minimum order is not reached; it follows the ticked items.
New items come in ticked; adding more of an unticked item ticks it again.
DELETE /cart/clear keeps destination .
FIX : after a paid cart checkout the bought quantities now leave the cart (before: nothing was removed). Units added
after the checkout stay; unticked items stay.
App to do
Group the cart by shops ; show each shop's choice, notice , nudge , and "Used before" for USED_BEFORE .
A checkbox per item, per shop and "select all" (§3.4); the pay button shows cartSummary.selectedTotal .
After a cart payment, re-read the cart instead of clearing it locally.
3.4 NEW Tick / untick cart items
Purpose : choose which cart items the next checkout buys.
Endpoint : PUT {base_url}/e-commerce/cart/selection
Access Level : 🔒 Protected (cart owner)
Authentication : Bearer token
Request JSON Sample :
{ "itemIds": ["a1b2…", "c3d4…"], "selected": false }
Request Body Parameters :
Parameter
Type
Required
Description
Validation
itemIds
array of uuid
Yes
one item, a whole shop's items, or every item
items of my cart
selected
boolean
Yes
tick ( true ) or untick ( false )
Success Response : the same body as GET /e-commerce/cart (§3.3).
Errors : unknown item → 404 "Cart item not found".
Rules : cart checkout ( REGULAR_CART ) takes ticked items only; nothing ticked → "Tick at least one item to check out."
A shop with nothing ticked leaves shops and keeps its pickup / delivery choice for when it is ticked again. Reference : §48
App to do :
Checkbox per item, per shop, "select all" → one call each.
4. Checkout & payment
What changed in this service : every shop with physical items in a checkout is PICKUP or DELIVERY . The fake
address and DHL method are gone; fulfillment replaces them. Delivery has a real price from a real shipper, held from the
quotation; if the price ran out and changed, checkout refuses instead of charging a new price silently. Group purchases and
"ships now" installments can now be delivered. The payment response finally separates the seller's money from delivery.
4.0 Flow — before and after
Before
flowchart LR
A[Create session
shippingAddressId + shippingMethodId: fake] --> B[Shipping = 0] --> C[Pay: one amount]
After
flowchart LR
A[Create session
fulfillment: per shop PICKUP / DELIVERY
+ dropoff + chosen option] --> B{Held price still valid?}
B -- yes --> C[Pay: items part + delivery part per shop]
B -- changed / gone / campaign ended --> D[409 with before → after
re-quote, PATCH, pay]
D --> C
What the user notices : the buyer picks pickup or delivery per shop, an address, who receives, a note; the total
includes delivery; if a price moved they see "was X, now Y" and confirm.
4.1 Endpoints in this service
Change
Method
Endpoint
One line
App
BREAKING
POST
/e-commerce/checkout-sessions
fulfillment replaces shippingAddressId / shippingMethodId
must update
BREAKING
PATCH
/e-commerce/checkout-sessions/{id}
same
must update
BREAKING
GET
checkout session response
fulfillment replaces shippingAddress / shippingMethod ; real delivery in pricing
must update
CHANGED
POST
/e-commerce/checkout-sessions/{id}/process-payment , /retry-payment
new refusals; deliveryAmount + parts
should update
FIX
POST
same
sellerAmount no longer includes the shipper's money
check
4.2 BREAKING Create / update a checkout session — fulfillment
Endpoint : POST {base_url}/e-commerce/checkout-sessions · PATCH {base_url}/e-commerce/checkout-sessions/{id}
Change
BREAKING — two fields removed; fulfillment required when the session has physical items
Impact
Must update before the deploy
Reference
§11c, §33, §55, §56
In one line : tell the backend, per shop, pickup or delivery — and where, who receives, and which shipper option.
Request
Field
Before
After
Note
shippingAddressId
fake: any uuid gave "John Doe, 123 Main Street"
removed
shippingMethodId
fake: always "DHL, Standard Shipping, 0"
removed
fulfillment.byShop
—
added
shopId → PICKUP | DELIVERY , one per shop with physical items
fulfillment.deliveries
—
added
shopId → { quotationId, optionId } — the option from the sheet / cart; missing → today's default shipper
fulfillment.dropoff
—
added
required if any shop is DELIVERY — exactly one of addressId / recentId / lat + lng (+ saveAs , customLabel , directions )
fulfillment.receiver
—
added , optional
{ name, phone } ; omit = me
fulfillment.note
—
added , optional
this order only, max 300
Before After
{
"sessionType": "REGULAR_DIRECTLY",
"items": [ { "productId": "5f0e…", "quantity": 1 } ],
"shippingAddressId": "1111…",
"shippingMethodId": "2222…"
}
{
"sessionType": "REGULAR_DIRECTLY",
"items": [ { "productId": "5f0e…", "quantity": 1 } ],
"fulfillment": {
"byShop": { "cfb6…": "DELIVERY" },
"deliveries": { "cfb6…": { "quotationId": "3c1f…", "optionId": "o-1" } },
"dropoff": { "addressId": "9b1d…", "directions": "Blue gate" },
"receiver": { "name": "Mama Json", "phone": "0712345678" },
"note": "Call when you arrive"
}
}
Behaviour
Digital-only checkouts need no fulfillment .
Cart checkout ( REGULAR_CART ) may send no fulfillment : pickup / delivery, the address and the held shipper come
from the cart. Sending fulfillment.dropoff overrides the cart's address. Only ticked items are bought.
Group purchase accepts DELIVERY (same shape). The group's member totals count items + tax only; each member's delivery
is on their own session / order. (§55)
Installment : a plan that ships after the down payment ( IMMEDIATE ) accepts DELIVERY — pricing = down payment + the
whole delivery. A layaway plan ( AFTER_PAYMENT ) refuses delivery at checkout: LAYAWAY_DELIVERY_LATER . (§56)
A new pin becomes a recent place (and a saved / recent one is marked used) only after payment ; saveAs saves the
address at once.
The fulfillment is frozen when the session is created — later edits to the shop location or the address never change it.
Errors
Code
Status
When
Before / After
FULFILLMENT_REQUIRED
409
a shop with physical items has no PICKUP / DELIVERY choice
NEW
DROPOFF_REQUIRED
409
a shop is DELIVERY but no dropoff
NEW
DROPOFF_INVALID
409
not exactly one of addressId / recentId / pin, or it isn't yours
NEW
RECEIVER_INCOMPLETE
409
receiver name without phone, or the reverse, or a bad phone
NEW
DELIVERY_NOT_AVAILABLE
409
DELIVERY to an address no shipper covers
NEW
SHOP_LOCATION_REQUIRED
409
a physical product from a shop with no location (any session type)
NEW
SHOP_CLOSED
409
an item from a shop paused with STOP_ORDERS, suspended or closed; details { shopId, shopName, reopensAt }
NEW — before, checkout never checked the shop
DELIVERY_PRICE_CHANGED / DELIVERY_OPTION_GONE
409
the held price ran out and changed / the shipper can't take it now; details { shopId, businessId, businessName, before, after }
NEW
LAYAWAY_DELIVERY_LATER
409
DELIVERY on a layaway installment
NEW
OUTSIDE_COVERAGE , LABEL_ALREADY_USED , ADDRESS_LIMIT_REACHED
409
from a saveAs pin
NEW
App to do
Per shop "Pickup at shop / Delivery"; when any delivery: saved address / recent / new pin (+ "Save as"), receiver
"Me / Someone else", optional note.
Send fulfillment (with deliveries from the sheet / cart); for cart checkout it can be left out.
Stop sending shippingAddressId / shippingMethodId .
On DELIVERY_PRICE_CHANGED / DELIVERY_OPTION_GONE : show before → after, get a fresh quotation (sheet: ask again;
cart: re-read the cart), retry.
4.3 BREAKING Checkout session response — fulfillment and real delivery prices
Endpoint : the checkout session (create, get, patch).
Change
BREAKING — two fake blocks removed
Impact
Must update
Reference
§1, §11c, §33, §53, §65
Response
Field
Before
After
Note
shippingAddress
fake John Doe block
removed
shippingMethod
fake DHL block
removed
fulfillment
—
added
shopId → that shop's fulfillment (below)
pricing.shippingByShop
—
added
shopId → delivery , DELIVERY shops only, what the buyer pays
pricing.shippingCost , total
shipping always 0
real
sum of shippingByShop
The fulfillment object (same on the session, the order and the installment agreement):
{ "type": "DELIVERY",
"pickup": { "source": "SHOP_LOCATION", "label": "Yoyo Test Shop", "lat": -6.7889, "lng": 39.2083,
"areaLabel": "Ubungo, Ubungo, Dar es Salaam", "phone": "+255700111222", "…": "…" },
"dropoff": { "source": "SAVED_ADDRESS", "label": "Home", "lat": -6.77, "lng": 39.25,
"areaLabel": "Mikocheni, Kinondoni, Dar es Salaam", "directions": "Blue gate", "…": "…" },
"receiver": { "name": "Mama Json", "phone": "+255712345678" },
"note": "Call when you arrive",
"delivery": { "quotationId": "3c1f…", "optionId": "o-1", "businessId": "c4bc…", "businessName": "Dar Cargo",
"total": 7000, "minMinutes": 120, "maxMinutes": 240, "arrivalFrom": "…", "arrivalTo": "…",
"pricesHeldUntil": "…", "route": null,
"offer": { "listPrice": 7000, "price": 7000, "buyerPays": 3500, "shares": [ … ] } } }
dropoff and delivery are absent for PICKUP. delivery.total is what the shipper is paid; offer shows who paid
the rest (campaigns). route is set for station routes (shipping doc §1.8).
4.4 CHANGED Process / retry payment
Endpoint : POST {base_url}/e-commerce/checkout-sessions/{id}/process-payment · /retry-payment
Change
CHANGED + FIX
Impact
Should update
Reference
§33, §34, §47, §65, §67
Response
Field
Before
After
Note
sellerAmount
every held part added together (seller + shipper)
the seller's share only
FIX
deliveryAmount
—
added
the delivery parts' full value
parts
—
added
[{ escrowId, escrowNumber, part: ITEMS | LEG, orderId, amount }] — one per held part, every shop's order
escrowId , escrowNumber , orderId , amountPaid
unchanged
Before (wrong) After (correct)
{ "escrowId": "e1…",
"amountPaid": 16000.99,
"sellerAmount": 16000.99 }
{ "escrowId": "e1…",
"amountPaid": 16000.99,
"sellerAmount": 1000.99,
"deliveryAmount": 15000,
"parts": [ { "part": "ITEMS", "amount": 1000.99, "orderId": "…", "…": "…" },
{ "part": "LEG", "amount": 15000, "orderId": "…", "…": "…" } ] }
Behaviour
The money is held per shop in parts: ITEMS (the seller; platform fee on items + tax only) and LEG (the shipper,
one per leg). A free delivery has no LEG part.
Buyer transaction history for a split payment reads "N parts" instead of "N sellers".
Errors
Code
Status
When
Before / After
DELIVERY_OPTION_GONE
409
the chosen shipper stopped taking jobs after the session was made — the session stays open: change the shipper with PATCH , pay
NEW
CAMPAIGN_ENDED
409
a delivery campaign ended / ran out since checkout; details { shopId, businessId, businessName, before, after } . Nothing charged: PATCH the session (same pick) for today's price, then pay
NEW
DELIVERY_PRICE_CHANGED
409
now also when only the buyer's part changed
CHANGED
App to do
Show items from sellerAmount and delivery from deliveryAmount ; list each shop's order from parts .
Handle CAMPAIGN_ENDED like DELIVERY_PRICE_CHANGED (before → after, PATCH, pay).
5. Orders
What changed in this service : every physical order has a fulfillment — pickup or delivery — with its own status
ladder, steps and times. Pickup orders and orders a shipper delivers no longer use the old ship / confirm-delivery /
regenerate-code endpoints: they use the shipping order actions (shipping doc §C). The delivery code is 4 digits, comes by
its own SMS and lasts 24 h. A buyer's cancel follows clearer rules.
5.0 Flow — before and after
Before
flowchart LR
A[Paid] --> B[Seller: /ship
fake tracking number] --> C[Buyer types 6-digit code:
/confirm-delivery] --> D[Money to seller]
After — pickup
flowchart LR
A[PLACED] --> B[Seller accepts
ACCEPTED] --> C[Packed & ready
READY + pickup code to buyer] --> D[Seller enters buyer's code
COLLECTED] --> E[Buyer: It's what I ordered
COMPLETED, money released]
After — delivery
flowchart LR
A[PLACED] --> B[ACCEPTED] --> C[READY
job offer to shipper] --> D[SHIPPER_ACCEPTED] --> E[HEADING_TO_SHOP] --> F[Hand-over at the shop
ON_THE_WAY, code to receiver] --> G[Rider enters code
DELIVERED] --> H[Buyer confirms
COMPLETED: seller + shipper paid]
What the user notices : order screens show where the parcel is, the shipper's name and phone, "Code sent by SMS · valid
until …", and buttons that depend on the step (Accept, Ready, Hand over, Collect, "It's what I ordered", Report a problem).
5.1 Endpoints in this service
Change
Method
Endpoint
One line
App
BREAKING
GET
/e-commerce/orders/{orderId} (buyer + seller)
deliveryAddress / trackingNumber / carrier → fulfillment
must update
CHANGED
GET
order lists (buyer /my-orders… , shop /shop/{shopId}/orders… )
gain fulfillmentType , fulfillmentStatus
should update
BREAKING
POST
/orders/{orderId}/ship , /confirm-delivery , /regenerate-code
refused for pickup orders and shipper orders; 4-digit code
must update
CHANGED
POST
/orders/{orderId}/regenerate-code
"I lost my code": new fields, code shown once
should update
CHANGED
POST
/orders/{orderId}/cancel
pickup / shipper orders follow the new cancel rules; installments refused
should update
CHANGED
—
shippingFee , totalAmount on an order
can change after payment
check
New, documented elsewhere: every pickup / delivery order action under /shipping/orders/{orderId}/… (accept, ready,
pickup-time, code, hand-over, hand-over-rider, collect, confirm, report-problem, delivery-options, switch-to-delivery,
change-destination, switch-to-pickup, delivery-decision, cancel) → shipping doc §C.
5.2 BREAKING Order detail and lists — fulfillment
Endpoint : GET {base_url}/e-commerce/orders/{orderId} and the order lists
Change
BREAKING — three fields removed
Impact
Must update
Reference
§11c, §24, §26, §37–§41, §45, §53, §59, §63
Response
Field
Before
After
Note
deliveryAddress , trackingNumber , carrier
text / fake
removed
fulfillment
—
added (detail)
the object from §4.3 plus status, steps and times (below)
fulfillmentType
—
added (lists)
PICKUP / DELIVERY , null for digital
fulfillmentStatus
—
added (lists)
productOrderStatus
kept
still works; for pickup / shipper orders it follows the fulfillment steps
Before After
{ "orderNumber": "ORD-2026-94070",
"productOrderStatus": "SHIPPED",
"deliveryAddress": "123 Main Street",
"trackingNumber": "TRACK-8F2A",
"carrier": "DHL" }
{ "orderNumber": "ORD-2026-94070",
"productOrderStatus": "SHIPPED",
"fulfillment": {
"type": "DELIVERY", "status": "ON_THE_WAY",
"parcelNumber": "NX-48213",
"placedAt": "…", "acceptedAt": "…", "readyAt": "…", "pickedUpAt": "…",
"code": { "status": "ACTIVE", "expiresAt": "2026-10-26T09:12:00" },
"shippers": [ { "leg": 1, "name": "Dar Cargo", "phone": "+255745051250" } ],
"delivery": { "businessName": "Dar Cargo", "total": 7000, "…": "…" },
"decision": null, "needsShipperSince": null, "…": "…" } }
fulfillment keys added beyond §4.3
Key
Meaning
status
pickup: PLACED → ACCEPTED → READY → COLLECTED → COMPLETED ; delivery: PLACED → ACCEPTED → READY → SHIPPER_ACCEPTED → HEADING_TO_SHOP → ON_THE_WAY → (AT_STATION) → DELIVERED → COMPLETED ; or CANCELLED / EXPIRED
parcelNumber , pickupTime
parcel number from Accept; buyer's planned pickup time
placedAt , acceptedAt , readyAt , collectedAt , headingAt , pickedUpAt , deliveredAt , completedAt , closedAt
step times, absent until set
acceptBy , collectBy
deadlines (24 opening hours to accept; 14 days to collect)
closeReason
e.g. NOT_ACCEPTED_IN_TIME , NOT_COLLECTED_IN_TIME , ADMIN_REFUNDED , ADMIN_CANCELLED
code
{ status: ACTIVE | USED | LOCKED | EXPIRED | REPLACED, expiresAt } — detail only, never the code itself
packedConfirmedAt , labelConfirmedAt
the seller's Ready ticks
needsShipperSince
the shipper said no and a new one is being found
decision
the buyer must choose (new shipper costs more, nobody found, …) — shipping doc §1.11
refusal
the latest shipper refusal at the shop ( TOO_BIG / BADLY_PACKED )
shippers
one per leg whose shipper accepted: { leg, businessId, name, phone } (not for Bolt)
rider
Bolt only: driverName , driverPhone , driverPicture , plate , vehicleModel , trackingUrl , etaToShopMinutes , state
Behaviour
Existing orders were copied over: PENDING_SHIPMENT → PLACED , SHIPPED / DELIVERED / DISPUTED → READY , COMPLETED →
COMPLETED , CANCELLED / REFUNDED → CANCELLED .
Automatic closing: a pickup or delivery order not accepted within 24 opening hours is cancelled; a pickup not
collected 14 days after Ready becomes EXPIRED . Both refund in full; the order shows CANCELLED with no cancelledBy .
shippingFee and totalAmount can change after payment (another shipper at another price; 0 after switching to
pickup). shippingFee is what the buyer paid for delivery.
For now the seller sees the full drop-off (pin, directions, receiver phone).
App to do
Read fulfillment instead of deliveryAddress / trackingNumber / carrier ; pickup orders show the shop's place + Directions.
Show fulfillment.status and the step times; lists show the fulfillmentType badge and fulfillmentStatus .
A Call button per entry in shippers ; a rider card (photo, plate, Track) for Bolt.
5.3 CHANGED Cancel an order
Endpoint : POST {base_url}/e-commerce/orders/{orderId}/cancel
Change
CHANGED — rules for pickup orders and orders a shipper delivers
Impact
Should update
Reference
§25, §34, §43, §69
Who
Before
After (pickup orders and shipper orders)
Buyer
any time before shipped
only before the seller accepts ( PLACED ), or while the delivery has no shipper ( needsShipperSince / a decision waiting)
Seller
any time before delivered
pickup: until collected; delivery: only before Ready. Body { "informedBuyer": true, "reason"? }
Refund
the order's single escrow
every part (items + delivery), stock back
Old orders without a shipper keep the old rules.
Errors
Code
Status
When
Before / After
CANCEL_NOT_ALLOWED
409
after those windows ("You can switch to pickup instead" / "Contact support to cancel")
NEW (was FULFILLMENT_STATE )
INFORM_BUYER_FIRST
409
seller without informedBuyer: true
NEW
INSTALLMENT_ORDER
409
an installment order — cancel the agreement instead
NEW
App to do
Buyer: show Cancel only while PLACED or while "We're finding a new shipper" / a decision shows; otherwise "Collect it myself instead".
Seller: hide Cancel on delivery orders from Ready on; send informedBuyer .
Installment order: the Cancel button calls the agreement cancel (§6.2).
5.4 BREAKING Old ship / confirm-delivery / regenerate-code
Endpoint : POST {base_url}/e-commerce/orders/{orderId}/ship · /confirm-delivery · /regenerate-code
Change
BREAKING — refused for most new orders
Impact
Must update
Reference
§25, §40
Order
Before
After
Pickup order
seller ships, buyer confirms with the code
409 USE_PICKUP_FLOW — use the shipping order actions
Order a shipper delivers
—
409 SHIPPER_DELIVERS
Old order without a shipper
unchanged (old flow)
POST /ship no longer takes or returns trackingNumber (it was a fake "TRACK-…").
App to do
For pickup and shipper orders, stop calling these three; use the buttons from shipping doc §C.
5.5 BREAKING Delivery code — 4 digits, own SMS, 24 h, "I lost my code"
Endpoint : /orders/{orderId}/ship , /confirm-delivery , /regenerate-code (old orders) — and the same code rules for
the shipping …/code action.
Change
BREAKING
Impact
Must update
Reference
§20, §26
Before
After
Length
6 digits
4 digits ( confirmationCode must match ^[0-9]{4}$ )
Lifetime
30 days
24 hours ; a new code kills the old one; max 5 per order per 24 h
Delivery of the code
inside the "Order shipped" email / SMS / push
its own SMS to the receiver; the bell says "Your delivery code was sent by SMS", never the code
Wrong code
HTTP 400 with prose
409 coded refusal, details.triesLeft
Old codes
gone — a buyer with a SHIPPED order taps "I lost my code"
POST /regenerate-code response — fields added (old ones stay):
Before After
{ "orderId": "…",
"orderNumber": "ORD-2026-94070",
"codeSent": true,
"destination": "email",
"codeExpiresAt": null,
"maxAttempts": 5 }
{ "orderId": "…",
"orderNumber": "ORD-2026-94070",
"codeSent": true,
"destination": "sms",
"codeExpiresAt": "2026-10-18T05:03:05",
"maxAttempts": 5,
"message": "New code sent by SMS to +255*****2944",
"code": "4821",
"sentTo": "+255*****2944" }
code is shown once , on that screen; it is never stored readable, so it cannot be shown again.
Errors
Code
Status
When
Before / After
CODE_WRONG
409
wrong code; details.triesLeft
NEW (was 400)
CODE_LOCKED
409
after 5 wrong tries — even the right code, until a new one is asked for
NEW
CODE_NOT_ACTIVE
409
no active code, or already used
NEW
CODE_EXPIRED
409
older than 24 h
NEW
CODE_LIMIT
409
more than 5 new codes in 24 h
NEW
App to do
4-digit input; show triesLeft ; on CODE_LOCKED / CODE_EXPIRED offer "I lost my code".
Order screen: "Code sent by SMS to +255•••2944 · valid until …" from fulfillment.code .
"I lost my code" → show code big, once. Never cache it.
6. Installments
What changed in this service : the agreement carries the fulfillment like an order; "ships now" plans can be delivered;
installment orders are cancelled only through their agreement; and two money bugs are fixed.
Change
Method
Endpoint
One line
App
CHANGED
GET
installment agreement
shippingAddress → fulfillment
should update
CHANGED
POST
/e-commerce/installments/agreements/{id}/cancel
also cancels the order and refunds every part
no change
FIX
POST
/installments/agreements/{id}/payments/{paymentId}/pay
instalments after the down payment can be paid
no change
FIX
GET / POST
/installments/agreements/{id}/early-payoff
no "/ by zero" on 0 % plans
no change
FIX
—
confirming delivery
releases every instalment held so far to the seller
no change
6.1 CHANGED Agreement — fulfillment , layaway, cancel
Field / rule
Before
After
shippingAddress
always empty
removed
fulfillment
—
added (same object as orders)
Layaway ( AFTER_PAYMENT )
refuses delivery at checkout ( LAYAWAY_DELIVERY_LATER ); when paid off the order is pickup and the buyer may choose delivery from the order
INSTALLMENT_AGREEMENT_COMPLETED
gains nextStep ("Collect it at the shop, or choose delivery in your order." for layaway)
Agreement cancel
when the order exists it is cancelled too and everything refunded (down payment, delivery, instalments) — refused once the parcel left the shop
Order cancel on an installment order
allowed (left the agreement ACTIVE)
refused INSTALLMENT_ORDER everywhere
Reference : §17, §56, §69
6.2 FIX Installment money
Before (wrong)
After (correct)
every instalment after the down payment failed to pay (database error)
pays normally
confirming delivery released only the down payment + delivery; held instalments stayed held for good
every instalment held so far is released; one paid after confirmation goes to the seller at once
early payoff on a 0 % plan crashed "/ by zero"
works
a "ships now" plan sometimes never got its order
always gets it
Reference : §56, §57, §70, §71
App to do
Offer pickup / delivery in the installment sheet for "ships now" plans; layaway: "choose delivery after the last payment".
Installment order: Cancel = POST /e-commerce/installments/agreements/{agreementId}/cancel .
7. Group purchase
7.1 CHANGED Group checkout with delivery
Rule
Before
After
GROUP_PURCHASE with DELIVERY
not possible
accepted — byShop , deliveries , dropoff like a direct buy
Member totals ( totalPaid , amountPaid )
items + tax only — the same for every member; each member's delivery is on their own session / order
Leaving ( POST /group-purchases/{id}/withdraw ) / group runs out of time
refund the items
refund items and delivery
Moving a payment to another group
keeps its delivery choice
Changing the delivery while the group fills
not possible; once the order exists the usual changes apply
Reference : §55
App to do
The group checkout sheet offers pickup / delivery like a direct buy; the group screen keeps showing the group price.
8. Wallet & transactions
8.1 CHANGED Transaction history titles
Where
Before
After
Buyer, split payment
"N sellers"
"N parts"
Shipper payout
—
"Delivery Earnings" ("Payment received for delivery"); type still SALE
Extra delivery payment after ordering
—
"Delivery Payment"
Money back (cheaper shipper, switch to pickup)
—
"Refund Received"
Known, not changed: a refund also writes "Sale Refund Issued" (debit) into the payee's history although the payee never
received that money. Reference : §34, §40, §41
9. Events
What changed in this service : a venue is a pin turned into ward / district / region (like a shop); an in-person
event needs that pin to publish. Donation tickets can have a target and show progress.
Change
Method
Endpoint
One line
App
CHANGED
PATCH
/e-events/drafts/{draftId}/location
pin outside Tanzania → OUTSIDE_COVERAGE
should update
BREAKING
PATCH
/e-events/{eventId}/publish , /e-events/{eventId}/published/reveal-location
in-person / hybrid needs a pin
must update
CHANGED
GET
event responses
venue gains ward , areaLabel , matchType
should update
CHANGED
POST / PUT / PATCH
ticket create / draft update / published update
optional donationTarget
to use
CHANGED
GET
ticket details, ticket list, event tickets
donation progress on donation tickets
should update
9.1 CHANGED Venue — area from the pin
Endpoint : PATCH {base_url}/e-events/drafts/{draftId}/location · event responses
Field
Before
After
request venue
unchanged; venue.coordinates outside Tanzania's wards → OUTSIDE_COVERAGE
response venue.ward , venue.areaLabel , venue.matchType
—
added (null without a pin)
locationSummary , share preview, ticket / booking location, event on a post or clip
typed address
the area when no address was typed
"venue": { "name": "Mlimani City Hall", "address": null, "coordinates": { "latitude": -6.77, "longitude": 39.22 },
"areaLabel": "Mikocheni, Kinondoni, Dar es Salaam", "matchType": "CONTAINED", "ward": { "…": "…" } }
Reference : §78
9.2 BREAKING Publish / reveal location needs a pin
Endpoint : PATCH {base_url}/e-events/{eventId}/publish · {base_url}/e-events/{eventId}/published/reveal-location
Before
After
an IN_PERSON / HYBRID event published with a venue name only
refused 422 "Drop a pin on the map for the venue" ( details.stage LOCATION_DETAILS ) without coordinates
TBA and online events unchanged; drafts can still be saved without a pin.
App to do
Venue step: the same pin screen as the shop location (resolve, show areaLabel , Confirm off when not covered);
the address field stays optional ("directions").
9.3 CHANGED Donation target and progress
Endpoint : POST {base_url}/e-events/tickets/{eventId} · PUT {base_url}/e-events/tickets/{ticketId} · PATCH {base_url}/e-events/tickets/{ticketId}/published
Change
CHANGED — optional field in, optional block out
Impact
To use (the progress bar)
Reference
§79
Request
Field
Before
After
Note
donationTarget
—
added , optional
> 0, at most 10 digits + 2 decimals; donation tickets only
Response (ticket details, ticket list, tickets in the event response)
Field
Before
After
Note
donation
—
added
{ target, raised, donations } on donation tickets; null on others; target null when not set
Before After
{ "name": "Support the choir",
"ticketPricingType": "DONATION",
"price": 0.00 }
{ "name": "Support the choir",
"ticketPricingType": "DONATION",
"price": 0.00,
"donation": { "target": 500000.00,
"raised": 210000.00,
"donations": 11 } }
raised = confirmed donations only (cancelled / refunded bookings do not count). The organiser can set or raise the
target after publishing.
Errors
Message
Status
When
"A donation target can only be set on a donation ticket"
422
donationTarget on a paid / free ticket
App to do
Organiser: optional "Target" field on donation tickets.
Event page: progress bar from donation.raised / donation.target (hide the bar when target is null; still show raised ).
10. Profile & account
10.1 CHANGED Profile cover photo
Endpoint : PUT {base_url}/profile/update · GET {base_url}/profile/me , /profile/id/{userId} , /profile/u/{username}
Change
CHANGED — field in, field out
Impact
To use
Reference
§79
Where
Field
Before
After
PUT /profile/update
coverPhotoFileId
—
added , optional — a File Thunder upload with context COVER_PHOTO
profile responses
coverMedia
—
added — same shape as profileMedia ( fileId , status , variants , mimeType ), null when not set
Before After
{ "userName": "joshuasakweli",
"profileMedia": { "fileId": "…", "status": "READY",
"variants": { "thumb": "https://…/thumb.webp" } } }
{ "userName": "joshuasakweli",
"profileMedia": { "fileId": "…", "status": "READY",
"variants": { "thumb": "https://…/thumb.webp" } },
"coverMedia": { "fileId": "…", "status": "READY",
"variants": { "medium": "https://…/medium.webp",
"large": "https://…/large.webp" } } }
Errors : a file uploaded with another context → "File was not uploaded as a cover photo"; someone else's file → 403.
App to do
Profile banner: upload as COVER_PHOTO , send coverPhotoFileId , show coverMedia .
10.2 CHANGED New-device sign-in alert
AUTH_NEW_DEVICE_LOGIN is now sent by password, SMS-code and Google sign-in (was password only) and never for an
account's first device. Its data gains the details the app can show in the bell item:
Key
Before
After
device.name , device.platform , ipAddress
yes
unchanged
device.type
—
Phone / Tablet / Computer
device.browser
—
e.g. Chrome , Safari , NexGate app
device.os
—
Windows , macOS , iOS , Android , Linux , ChromeOS
device.location , location
—
e.g. "Dar es Salaam, TZ" ( "Unknown location" when not found)
timestamp
—
e.g. "Oct 25, 2026 at 8:15 AM EAT"
Browser and system come from the User-Agent header — no new request fields. Email, push and SMS now show them.
Reference : §79
11. Interests
11.1 CHANGED Category management is admin only
Endpoint
Before
After
POST /interests/categories , PUT /{categoryId} , DELETE /{categoryId} , PATCH /{categoryId}/toggle-active , GET /{categoryId}/usage
any signed-in user
SUPER_ADMIN / STAFF_ADMIN only (403 for others)
GET /interests/categories/all
public
unchanged
Picking interests (unchanged, for the onboarding screen): load GET /interests/categories/all , then save at least 3 of
those ids with POST /onboarding/secondary/interests { "interestIds": [ … ] } . One unknown or switched-off id → 404
"Some categories are inactive or do not exist" and nothing is saved — always send ids from the list you just loaded.
The categories are created by the system at startup. Reference : §79
12. Chat
12.1 FIX Chat avatars and group icons are real links
Endpoint : GET {base_url}/chat/conversations ( display.avatar ) · group responses ( iconMedia : create, get, PUT /chat/groups/{conversationId}/icon )
Before (wrong)
After (correct)
variants held storage paths — "thumb": "messages/4a8c…/thumb.webp" — which no image can load
"thumb": "https://…/thumb.webp" — full links; same object shape
Applies to groups, people and shops in the chat list. An icon set while its upload was still processing now upgrades to its
final images (it stayed stuck before). A failed icon upload now says "Upload failed" instead of "still in progress".
The chat doc said avatar was a plain link string — it is the object { fileId, status, mimeType, variants } .
Reference : §80
App to do
Read the image from display.avatar.variants.thumb / iconMedia.variants.thumb (no prefixing).
On CHAT_GROUP_UPDATED (§15) refresh the chat list row and the group header.
13. Digital downloads
13.1 FIX Files keep their name and type
Endpoint : GET {base_url}/e-commerce/digital-files/my-downloads/{accessId}/url — then the app downloads the returned url
Before (wrong)
After (correct)
the file arrived named original , type application/octet-stream — devices could not open it, and reading it as text corrupted it (and roughly doubled its size)
the link answers Content-Disposition: attachment; filename="notes pedi.zip" and the real Content-Type ( application/zip , video/mp4 , …)
Response body unchanged ( url , expiresInSeconds ). Works for files already uploaded. Reference : §80
App to do
Download the url as bytes and stream it to a file; never read the body as text / string.
Use the filename from Content-Disposition (or fileName from the order downloads list).
14. Admin dashboard (existing screens)
Change
Method
Endpoint
One line
NEW
PUT
/admin/shops/reviews/{reviewId}/status
moderate a shop review: { "status": "HIDDEN", "reason": "…" } (SUPER_ADMIN, reason required, audit-logged) → { reviewId, shopId, status }
CHANGED
GET
fees, current + history
lists a new fee type DELIVERY_COMMISSION (PERCENTAGE)
CHANGED
GET
/api/admin/reconciliation/position
gains owedToProviders , campaignMoney , ownerTopUps ; expectedSelcomFloat includes them
CHANGED
GET
/api/admin/reconciliation/trial-balance
new lines PROVIDER , CAMPAIGN_BUDGET , CAMPAIGN_POT
CHANGED
—
interest categories
management is admin only (§11)
Reference : §30, §31, §58, §64
New admin endpoints for shipping, Bolt, campaigns, the operations board, reports and the audit log → shipping doc §K and
campaigns doc §7–§8.
15. Notifications
New types are listed so the bell list and push taps can handle them; each opens the screen in its targetType /
targetId . Four new targets : SHIPPING_JOB , SHIPPER_BUSINESS , SHIPPING_CAMPAIGN , ADMIN_SHIPPING_BOARD — an app
that does not know a target yet opens the bell list.
Change
Type
To
When
Opens ( targetType )
App
NEW
HANDOVER_CODE
receiver
a pickup / delivery code was sent (in-app + SMS; never the code in-app)
ORDER_TRACKING
bell item
NEW
ORDER_ACCEPTED
buyer
seller accepted (parcel number)
ORDER_TRACKING
refresh the order
NEW
ORDER_ACCEPT_REMINDER
seller
2 h before the accept deadline
SHOP_ORDER
NEW
PICKUP_READY
buyer
pickup order ready (place, collect-by)
ORDER_TRACKING
NEW
PICKUP_REMINDER
buyer
not collected: day 7 and 12
ORDER_TRACKING
NEW
PICKUP_LAST_DAY
buyer (+ SMS)
24 h before the collect deadline
ORDER_TRACKING
NEW
SHIPPER_FOUND
buyer
every shipper accepted / Bolt rider assigned
ORDER_TRACKING
refresh
NEW
SHIPPER_CHANGED
buyer
automatic swap to an equal or cheaper shipper
ORDER_TRACKING
refresh
NEW
DELIVERY_DECISION_NEEDED
buyer
the buyer must choose (costs more, nobody found, …)
ORDER_TRACKING
open the decision
NEW
PARCEL_AT_STATION
buyer
the parcel reached the buyer's station
ORDER_TRACKING
NEW
RIDER_COMING
seller (+ SMS)
shipper heading to the shop / Bolt rider (plate to check)
SHOP_ORDER
NEW
PARCEL_REFUSED
seller
badly packed / Bolt left without the parcel
SHOP_ORDER
refresh
NEW
JOB_OFFER
shipper
a new job offer
SHIPPING_JOB
NEW
JOB_CANCELLED
shipper
an offer or accepted job was taken away
SHIPPING_JOB
refresh
NEW
DELIVERY_EARNINGS
shipper
delivery money released
TRANSACTION
refresh
NEW
SHIPPER_NEW_REVIEW
shipper
new review
SHIPPER_BUSINESS
NEW
SHIPPER_REVIEW_REPLY
reviewer
the shipper replied (in-app)
SHIPPER_BUSINESS
NEW
SHIPPER_STANDING
shipper owner
warning / automatic pause / suspension
SHIPPER_BUSINESS
banner
NEW
SHIPPER_DOCUMENT_EXPIRING
shipper owner
a KYC document expires in 14 / 3 days
SHIPPER_BUSINESS
NEW
SHIPPING_CAMPAIGN_RAN_OUT
campaign runner
a campaign ran out
SHIPPING_CAMPAIGN
NEW
SHIPPING_CAMPAIGN_ENDED
campaign runner
a campaign ended (what it paid)
SHIPPING_CAMPAIGN
NEW
SHIPPING_ADMIN_DIGEST
staff + super admins
07:30 when anything is stuck
ADMIN_SHIPPING_BOARD
admin app
NEW
CHAT_GROUP_UPDATED
other group members (in-app only)
the group photo changed; data conversationId , group.name , change: "ICON"
CONVERSATION
refresh the chat row + header
CHANGED
ORDER_AWAITING_CONFIRM
buyer
now sent when a delivery is delivered : "tap Received so the seller gets paid"
ORDER
CHANGED
ORDER_DELIVERED
was: buyer at completion → now: seller at delivery ("You are paid when they confirm")
SHOP_ORDER
move the handler
CHANGED
ORDER_SHIPPED
buyer
no carrier, tracking number or code; no SMS
ORDER_TRACKING
CHANGED
AUTH_NEW_DEVICE_LOGIN
user
all sign-in methods; new data keys (§10.2)
ACTIVE_SESSIONS
show the details
CHANGED
INSTALLMENT_AGREEMENT_COMPLETED
buyer
gains nextStep for layaway
AGREEMENT
FIX
ESCROW_RELEASED_TO_SELLER
seller
was blank (" released to your wallet / From order "); now amount + order number
TRANSACTION
FIX
ESCROW_REFUNDED , ORDER_CANCELLED
buyer
blank amount / order number / reason filled
FIX
DIGITAL_PRODUCT_READY
buyer
said only " is ready"; now the product name ("Notes Pedi and 2 more") and days the link lasts; sent only after access exists
DOWNLOAD
FIX
order "delivered" to the buyer
buyer
crashed when the buyer had no email
16. Error codes index
All HTTP 409, data.code ; some carry data.details .
Code
Change
Service
Endpoint(s)
Meaning
SHOP_LOCATION_REQUIRED
NEW
Checkout
create session
a physical product from a shop with no location
SHOP_CLOSED
NEW
Checkout
create session
shop paused (stop orders), suspended or closed; details.reopensAt
FULFILLMENT_REQUIRED / DROPOFF_REQUIRED / DROPOFF_INVALID
NEW
Checkout
create / patch session
missing choice / missing drop-off / not exactly one drop-off source
RECEIVER_INCOMPLETE
NEW
Checkout
create / patch session
receiver name and phone go together
DELIVERY_NOT_AVAILABLE
NEW
Checkout
create session
no shipper covers the address
DELIVERY_PRICE_CHANGED / DELIVERY_OPTION_GONE
NEW
Checkout
create session, process / retry payment
held price changed / shipper can't take it; show before → after
CAMPAIGN_ENDED
NEW
Checkout
process payment
a delivery campaign ended since checkout
LAYAWAY_DELIVERY_LATER
NEW
Checkout
create session (installment)
layaway: choose delivery after the last payment
HOURS_INVALID
NEW
Shops
PUT /shops/{shopId}/hours
a special day twice, or open with no hours
SHOP_NOT_PAUSABLE
NEW
Shops
PUT /shops/{shopId}/pause
only an ACTIVE (or already paused) shop can be paused
OUTSIDE_COVERAGE
NEW
Checkout, Events
saveAs pin, venue pin
pin outside Tanzania's wards
LABEL_ALREADY_USED / ADDRESS_LIMIT_REACHED
NEW
Checkout
saveAs pin
second HOME / WORK; 11th address
USE_PICKUP_FLOW
NEW
Orders
ship / confirm-delivery / regenerate-code
pickup order — use the shipping actions
SHIPPER_DELIVERS
NEW
Orders
ship / confirm-delivery / regenerate-code
a shipper delivers this order
CODE_WRONG / CODE_LOCKED / CODE_NOT_ACTIVE
NEW (was 400)
Orders
confirm-delivery
wrong code ( details.triesLeft ) / locked / none active
CODE_EXPIRED / CODE_LIMIT
NEW
Orders
confirm-delivery / regenerate-code
older than 24 h / more than 5 in 24 h
CANCEL_NOT_ALLOWED
NEW (was FULFILLMENT_STATE )
Orders
cancel
outside the cancel window
INFORM_BUYER_FIRST
NEW
Orders
cancel (seller)
send informedBuyer: true
INSTALLMENT_ORDER
NEW
Orders
cancel
cancel the installment agreement instead
The codes of the new location, shipping and campaign endpoints are in their docs (location §Part 3, shipping §4.5,
campaigns §Part 3).
17. App checklist by screen
Screen
What to change
Sections
Shop create / edit (seller)
drop location inputs; add the Shop location (pin) step
§1.2
Shop page / lists / search
location , Directions, Call; availability (open now / paused); Buy off when not accepting
§1.3, §1.4
Shop settings (seller)
opening hours screen; pause / reopen
§1.6, §1.7
Shop reviews
show replyText
§1.5
Product page
"Pickup at …" + Directions from shopLocation
§2.2
Product form (seller)
"Bulky / heavy item" switch
§2.3
Cart
group by shops , delivery per shop, notice , nudge , "Used before", ticks, selectedTotal , change messages
§3.2–§3.4
Checkout
pickup / delivery per shop, drop-off, receiver, note; send fulfillment ; price-changed codes
§4.2–§4.4
Payment result
sellerAmount + deliveryAmount , parts
§4.4
Order detail (buyer + seller)
fulfillment status, steps, times, shippers' phones, Bolt rider, code status, "I lost my code"
§5.2, §5.5
Order lists
fulfillmentType badge, fulfillmentStatus
§5.2
Order cancel
new windows; installment → agreement cancel
§5.3, §6.1
Delivery code entry
4 digits, triesLeft , locked / expired
§5.5
Installment sheet / agreement
delivery for "ships now"; layaway text; fulfillment
§6.1
Group purchase sheet
pickup / delivery
§7.1
Wallet history
new titles
§8.1
Event venue step (organiser)
pin screen; pin required to publish
§9.1, §9.2
Donation ticket (organiser + event page)
donationTarget ; progress bar from donation
§9.3
Profile
cover banner ( COVER_PHOTO , coverMedia )
§10.1
Interests (onboarding)
send ids from the loaded list
§11.1
Chat list / group header
avatars from variants ; refresh on CHAT_GROUP_UPDATED
§12.1
Digital downloads
download as bytes, keep the filename
§13.1
Bell list / push
the new types and four new targets
§15
Admin dashboard
review moderation, DELIVERY_COMMISSION , reconciliation lines, admin-only interests
§14