Skip to main content

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

  • 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<br/>type city, region, street, landmark, lat/lng] --> B[Shop live]

After

flowchart LR
    A[Create shop<br/>no location fields] --> B[Shop location step<br/>drop a pin: PUT /shops/id/location] --> C[Shop can sell physical items]
    A -. no location yet .-> D[Physical items refused at checkout:<br/>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
BeforeAfter
{
  "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
BeforeAfter
{
  "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<br/>+ pickup / delivery for that shop] --> B[Cart: one address,<br/>a choice per shop, held prices, ticks]
    B --> C[Checkout: takes the cart's choices,<br/>ticked items only] --> D[Pay: items + delivery]
    D --> E[Bought items leave the cart;<br/>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
BeforeAfter
{
  "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 }
BeforeAfter
{ "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<br/>shippingAddressId + shippingMethodId: fake] --> B[Shipping = 0] --> C[Pay: one amount]

After

flowchart LR
    A[Create session<br/>fulfillment: per shop PICKUP / DELIVERY<br/>+ 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<br/>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
BeforeAfter
{
  "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<br/>fake tracking number] --> C[Buyer types 6-digit code:<br/>/confirm-delivery] --> D[Money to seller]

After — pickup

flowchart LR
    A[PLACED] --> B[Seller accepts<br/>ACCEPTED] --> C[Packed & ready<br/>READY + pickup code to buyer] --> D[Seller enters buyer's code<br/>COLLECTED] --> E[Buyer: It's what I ordered<br/>COMPLETED, money released]

After — delivery

flowchart LR
    A[PLACED] --> B[ACCEPTED] --> C[READY<br/>job offer to shipper] --> D[SHIPPER_ACCEPTED] --> E[HEADING_TO_SHOP] --> F[Hand-over at the shop<br/>ON_THE_WAY, code to receiver] --> G[Rider enters code<br/>DELIVERED] --> H[Buyer confirms<br/>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
BeforeAfter
{ "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):

BeforeAfter
{ "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
BeforeAfter
{ "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
BeforeAfter
{ "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