App Fixes — Change Log
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 pinsdocs/shipping/shipping_api_doc.md— delivery options, cart delivery, every pickup / delivery order action, shippers, admindocs/shipping/campaigns/campaigns_api_doc.md— delivery discounts run by shops, shippers and NexGatenotifications_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":groupBuyingon tickets,grouponPOST /e-events/checkout, ticket groups (open, by code, mine, transfer, withdraw), 4 notificationsproduct_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 |
| Before | After |
|---|---|
|
|
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 |
|---|---|
|
|
Behaviour
availability.acceptingOrdersisfalsewhen the shop is paused withSTOP_ORDERS, suspended or closed → Buy is off.pauseMode:KEEP_SELLING(shows "paused" but orders still come in, accepted later) orSTOP_ORDERS.hours.openNowisnullwhen 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) fromlocation. - Owner: when
hasLocationis 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
acceptingOrdersis 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
replyTextis 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
shopLocationisnull.
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 |
| Before | After |
|---|---|
|
|
Response (add, change, remove)
| Field | Before | After | Note |
|---|---|---|---|
data |
null |
the delivery change | { deliveryBefore, deliveryAfter, switchedToPickup, canChangeBack, message, changes } |
| Before | After |
|---|---|
|
|
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(anddropoffwhen changed) from the product sheet. - Show
data.messageafter 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 labelUSED_BEFORE(not selected). (§49)selected.offer(delivery campaigns, campaigns doc): showbuyerPays, notcost.total.deliveryTotaland the change messages count what the buyer pays.nudgeappears 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/clearkeepsdestination.- 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" forUSED_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 |
| Before | After |
|---|---|
|
|
Behaviour
- Digital-only checkouts need no
fulfillment. - Cart checkout (
REGULAR_CART) may send nofulfillment: pickup / delivery, the address and the held shipper come from the cart. Sendingfulfillment.dropoffoverrides 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;
saveAssaves 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(withdeliveriesfrom 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": [ … ] } } }
dropoffanddeliveryare absent for PICKUP.delivery.totalis what the shipper is paid;offershows who paid the rest (campaigns).routeis 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) |
|---|---|
|
|
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
sellerAmountand delivery fromdeliveryAmount; list each shop's order fromparts. - Handle
CAMPAIGN_ENDEDlikeDELIVERY_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 |
| Before | After |
|---|---|
|
|
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 showsCANCELLEDwith nocancelledBy. shippingFeeandtotalAmountcan change after payment (another shipper at another price; 0 after switching to pickup).shippingFeeis what the buyer paid for delivery.- For now the seller sees the full drop-off (pin, directions, receiver phone).
App to do
- Read
fulfillmentinstead ofdeliveryAddress/trackingNumber/carrier; pickup orders show the shop's place + Directions. - Show
fulfillment.statusand the step times; lists show thefulfillmentTypebadge andfulfillmentStatus. - 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
PLACEDor 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 |
|---|---|
|
|
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; onCODE_LOCKED/CODE_EXPIREDoffer "I lost my code". - Order screen: "Code sent by SMS to +255•••2944 · valid until …" from
fulfillment.code. - "I lost my code" → show
codebig, 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 |
|---|---|
|
|
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 whentargetis null; still showraised).
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 |
|---|---|
|
|
Errors: a file uploaded with another context → "File was not uploaded as a cover photo"; someone else's file → 403.
App to do
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
urlas bytes and stream it to a file; never read the body as text / string. - Use the filename from
Content-Disposition(orfileNamefrom 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 |
No comments to display
No comments to display