E_commerce-nexgate-service(7)
- Cart Management
- Shops Management Service
- Products Management Service
- Checkout Session
- Group Purchase
- Installment Purchase
- Installment Purchase - Customer Endpoints
- Installment Purchase - Public & Plan Endpoints
- Installment Plan Management - Admin/Shop Endpoints
- Order Management
- Marketplace
Cart Management
Short Description: The Cart Management API provides shopping cart functionality for the NextGate e-commerce platform. It supports adding products to cart, updating quantities, removing items, and real-time stock validation.
Base URL: api/v1/e-commerce/cart
Hints:
- All cart operations require user authentication via Bearer token
- Each user has one persistent cart that maintains state across sessions
- Real-time stock validation prevents overselling
- Adding an existing product updates its quantity (additive)
- Cart persistence survives user logout/login cycles
Endpoints
1. Add Product to Cart
Purpose: Adds a product to the user's cart or updates quantity if already exists
Endpoint: POST {base}/add
Access Level: ๐ Protected (Requires authentication)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
| Content-Type | string | Yes | application/json |
Request JSON Sample:
{
"productId": "456e7890-e89b-12d3-a456-426614174001",
"quantity": 2
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| productId | UUID | Yes | ID of the product to add | Not null, product must exist and be active |
| quantity | integer | Yes | Quantity to add to cart | Min: 1 |
Response JSON Sample (New Item):
{
"success": true,
"message": "Product added to cart successfully",
"data": null
}
Response JSON Sample (Updated Existing):
{
"success": true,
"message": "Product quantity updated in cart successfully",
"data": null
}
Business Logic:
- Creates cart automatically if one doesn't exist
- If product already in cart, adds to existing quantity
- Validates total quantity doesn't exceed available stock
- Only allows active products to be added
Error Responses:
400 Bad Request: Invalid productId or quantity validation error401 Unauthorized: Authentication required404 Not Found: Product not found or not active422 Unprocessable Entity: Insufficient stock available
Error Examples:
{
"success": false,
"message": "Insufficient stock for 'iPhone 15 Pro'. Only 3 units available",
"data": null
}
{
"success": false,
"message": "Cannot add more items. Total quantity (8) would exceed available stock (5) for 'MacBook Pro'",
"data": null
}
2. Get Shopping Cart
Purpose: Retrieves the complete shopping cart with all items and pricing
Endpoint: GET {base}
Access Level: ๐ Protected (Requires authentication)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
Response JSON Sample:
{
"success": true,
"message": "Shopping cart retrieved successfully",
"data": {
"user": {
"userId": "111e2222-e89b-12d3-a456-426614174003",
"userName": "john.doe",
"name": "John Doe"
},
"cartSummary": {
"totalItems": 2,
"totalQuantity": 3,
"subtotal": 2198.00,
"totalDiscount": 0.00,
"totalAmount": 2198.00
},
"cartItems": [
{
"itemId": "cart001-e89b-12d3-a456-426614174004",
"productId": "456e7890-e89b-12d3-a456-426614174001",
"productName": "iPhone 15 Pro Max 512GB",
"productSlug": "iphone-15-pro-max-512gb",
"productPrimaryMedia": {
"fileId": "9c1a2b3d-4e5f-6789-abcd-ef0123456789",
"mediaType": "IMAGE",
"status": "READY",
"thumbUrl": "https://example.com/products/iphone15-pro-max/thumb.webp",
"mimeType": "image/webp"
},
"productType": "PHYSICAL",
"unitPrice": 1199.00,
"quantity": 2,
"itemSubtotal": 2398.00,
"totalPrice": 2398.00,
"shop": {
"shopId": "123e4567-e89b-12d3-a456-426614174000",
"shopName": "TechStore Pro",
"shopSlug": "techstore-pro",
"shopLogoMedia": {
"fileId": "1d2e3f4a-5b6c-7890-defa-bc1234567890",
"status": "READY",
"thumbUrl": "https://example.com/shops/techstore-pro/thumb.webp",
"mimeType": "image/png"
}
},
"availability": {
"inStock": true,
"availableQuantity": 25,
"maxPerCustomer": 5
},
"addedAt": "2025-09-23T10:30:00Z"
},
{
"itemId": "cart002-e89b-12d3-a456-426614174005",
"productId": "567e8901-e89b-12d3-a456-426614174002",
"productName": "MacBook Air M3",
"productSlug": "macbook-air-m3",
"productPrimaryMedia": {
"fileId": "2a3b4c5d-6e7f-8901-bcde-f01234567890",
"mediaType": "IMAGE",
"status": "READY",
"thumbUrl": "https://example.com/products/macbook-air-m3/thumb.webp",
"mimeType": "image/webp"
},
"productType": "PHYSICAL",
"unitPrice": 999.00,
"quantity": 1,
"itemSubtotal": 999.00,
"totalPrice": 999.00,
"shop": {
"shopId": "123e4567-e89b-12d3-a456-426614174000",
"shopName": "TechStore Pro",
"shopSlug": "techstore-pro",
"shopLogoMedia": {
"fileId": "1d2e3f4a-5b6c-7890-defa-bc1234567890",
"status": "READY",
"thumbUrl": "https://example.com/shops/techstore-pro/thumb.webp",
"mimeType": "image/png"
}
},
"availability": {
"inStock": true,
"availableQuantity": 8,
"maxPerCustomer": null
},
"addedAt": "2025-09-23T11:15:00Z"
}
],
"updatedAt": "2025-09-23T14:20:00Z"
}
}
Response Structure:
User Summary
| Field | Description |
|---|---|
| userId | Unique identifier of the cart owner |
| userName | User's login username |
| name | User's full name (firstName + lastName) |
Cart Summary
| Field | Description |
|---|---|
| totalItems | Number of distinct products in cart |
| totalQuantity | Total quantity of all items combined |
| subtotal | Total price before discounts |
| totalDiscount | Total discount amount applied |
| totalAmount | Final amount after discounts |
Cart Item Details
| Field | Description |
|---|---|
| itemId | Unique identifier for the cart item |
| productId | Product identifier |
| productName | Current product name |
| productSlug | URL-friendly product identifier |
| productPrimaryMedia | Resolved primary media of the product โ {fileId, mediaType, status, thumbUrl, mimeType}. null if the product has no media yet |
| productType | PHYSICAL or DIGITAL |
| unitPrice | Current product price per unit |
| quantity | Quantity of this product in cart |
| itemSubtotal | unitPrice ร quantity |
| totalPrice | itemSubtotal after discounts |
| shop | Shop information โ {shopId, shopName, shopSlug, shopLogoMedia}, where shopLogoMedia is {fileId, status, thumbUrl, mimeType} or null if the shop has no logo |
| availability | Real-time stock information |
| addedAt | When item was first added to cart |
Product Availability
| Field | Description |
|---|---|
| inStock | Whether the product is currently in stock |
| availableQuantity | Current available inventory |
| maxPerCustomer | Maximum quantity a single customer can purchase (null if no limit) |
Error Responses:
3. Update Cart Item Quantity
Purpose: Updates the quantity of a specific item in the cart
Endpoint: PUT {base}/items/{itemId}
Access Level: ๐ Protected (Requires authentication)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
| Content-Type | string | Yes | application/json |
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| itemId | UUID | Yes | ID of the cart item to update |
Request JSON Sample:
{
"quantity": 3
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| quantity | integer | Yes | New quantity for the cart item | Min: 1 |
Response JSON Sample:
{
"success": true,
"message": "Product quantity updated successfully",
"data": null
}
Business Logic:
- Updates quantity to the exact specified value (not additive)
- Validates new quantity doesn't exceed current stock
- Only allows updating items in the authenticated user's own cart
Error Responses:
400 Bad Request: Invalid quantity value401 Unauthorized: Authentication required404 Not Found: Cart item not found in user's cart422 Unprocessable Entity: Insufficient stock for requested quantity
Error Example:
{
"success": false,
"message": "Insufficient stock for 'iPhone 15 Pro'. Only 2 units available",
"data": null
}
4. Remove Cart Item
Purpose: Removes a specific item completely from the cart
Endpoint: DELETE {base}/items/{itemId}
Access Level: ๐ Protected (Requires authentication)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| itemId | UUID | Yes | ID of the cart item to remove |
Response JSON Sample:
{
"success": true,
"message": "Product removed from cart successfully",
"data": null
}
Business Logic:
- Completely removes the cart item and all its quantity
- Only allows removing items from the authenticated user's own cart
Error Responses:
5. Clear Shopping Cart
Purpose: Removes all items from the user's cart
Endpoint: DELETE {base}/clear
Access Level: ๐ Protected (Requires authentication)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
Response JSON Sample:
{
"success": true,
"message": "Shopping cart cleared successfully",
"data": null
}
Business Logic:
- Removes all cart items for the authenticated user
- Cart entity remains but becomes empty
Error Responses:
Shops Management Service
The Shops Management Service provides comprehensive shop management capabilities for the NextGate social commerce platform. This service handles shop categories, shop creation and management, rating systems, and review functionality with role-based access controls and business logic enforcement.
Hints:
- Shop Approval: All shops are auto-approved upon creation (isApproved: true)
- Business Rules: Shop owners cannot rate/review their own shops
- One Per User: Each user can submit only one rating and one review per shop
- Admin Categories: Shop categories require SUPER_ADMIN role for management
- Soft Deletion: Shops use soft delete (isDeleted flag) to maintain data integrity
- Slug Generation: Shop slugs are auto-generated from names with uniqueness validation
- Rating Scale: Ratings use 1-5 star system with summary statistics
- Review Moderation: Reviews support status management (ACTIVE, HIDDEN, FLAGGED, UNDER_REVIEW)
Shop Management
Short Description: The Shop Management API provides endpoints for creating, managing, and retrieving shop information on the NextGate platform. Covers shop registration, updates, approvals, WABA (WhatsApp Business) integration, AI chatbot toggling, and conversation history.
Hints:
- All shop endpoints use the prefix
api/v1/e-commerce/shops - Shop creation requires authentication; public users can only read
- Pagination uses 1-based page numbering
- Shop approval operations require
ROLE_SUPER_ADMINorROLE_STAFF_ADMIN - Featured shops are randomized on each request
- Rating and review data is automatically included in shop responses (read-only โ managed by separate review service)
- WABA = WhatsApp Business Account integration for the shop's AI-powered chatbot
Shop media (logo, banner) goes through FileThunder โ no raw URLs are sent by the client, same pipeline used by product media elsewhere in the API:
- Client calls
POST api/v1/files/request-uploadwithcontext: "SHOP_LOGO"or"SHOP_BANNER"โ gets back a presigned upload URL and afileId - Client
PUTs the raw file bytes directly to that URL - Client sends that
fileIdaslogoFileId/bannerFileIdwhen creating/updating the shop โ validated at request time against the FileThunder Redis cache (ownership + expected context), not necessarily fully processed yet - FileThunder finishes processing (virus scan + variant generation) asynchronously and webhooks back into
ShopMediaUpdater.onFileReady, which upgrades the storedshopLogoMedia/shopBannerMediatostatus: READYwith resolvedvariants
- There is no shop image gallery (
shopImages) field โ only a singleshopLogoMediaand singleshopBannerMedia, each aShopMediaobject (fileId,status,variants,mimeType). URLs are assembled fresh on every read viaFileThunderUrlAssembler, never stored as raw strings - Owner/user avatars follow the same rule:
ownerProfileMediaonShopResponse(single-shop detail view) is a fullProfileMediaobject (fileId,status,variants,mimeType) resolved viaFileThunderUrlAssembler.assembleProfileMedia().userProfileMediaon review responses and on subscriber responses (list/card contexts) is instead the leanUserProfilePrimaryMedia(fileId,status,thumbUrl,mimeType) resolved viaFileThunderUrlAssembler.getUserProfilePrimaryMedia()โ a single resolved thumbnail URL instead of the full variants map. Neither is ever a rawavatarUrl/userAvatarUrlstring
Standard Response Format
{
"success": true,
"httpStatus": "OK",
"message": "Operation completed successfully",
"action_time": "2026-05-19T10:30:45",
"data": { }
}
Error responses follow the same envelope with "success": false and "data" set to the error message string.
Endpoints
1. Create Shop
Endpoint: POST api/v1/e-commerce/shops
Access Level: ๐ Protected
Request Body:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| shopName | string | Yes | Name of the shop | Min: 2, Max: 100 chars |
| shopDescription | string | Yes | Detailed description | Max: 1000 chars |
| logoFileId | UUID | Yes | FileThunder file ID for the shop logo | Uploaded via POST api/v1/files/request-upload with context SHOP_LOGO, owned by the caller |
| phoneNumber | string | Yes | Contact phone | Pattern: ^\+?[0-9]{10,15}$ |
| city | string | Yes | City | Min: 2, Max: 50 chars |
| region | string | Yes | Region/state | Min: 2, Max: 50 chars |
| bannerFileId | UUID | No | FileThunder file ID for the shop banner | Uploaded with context SHOP_BANNER |
| string | No | Contact email | Valid email, max 100 chars | |
| countryCode | string | No | Country code | Max: 3 chars, Default: "TZ" |
| streetAddress | string | No | Street address | Max: 255 chars |
| landmark | string | No | Landmark / location notes | Max: 300 chars |
| latitude | decimal | No | GPS latitude | Range: -90.0 to 90.0 |
| longitude | decimal | No | GPS longitude | Range: -180.0 to 180.0 |
Note: there is no shopImages field โ shops only ever have a shopLogoMedia and a shopBannerMedia, never a gallery.
Request JSON Sample:
{
"shopName": "Mama Lucy's Restaurant",
"shopDescription": "Authentic Tanzanian cuisine in the heart of Dar es Salaam",
"logoFileId": "f77e4567-e89b-12d3-a456-426614174777",
"phoneNumber": "+255123456789",
"city": "Dar es Salaam",
"region": "Dar es Salaam",
"bannerFileId": "f88e4567-e89b-12d3-a456-426614174888",
"email": "info@mamalucy.co.tz",
"countryCode": "TZ",
"streetAddress": "Msimbazi Street, Block 45",
"landmark": "Near the main bus stop",
"latitude": -6.7924,
"longitude": 39.2083
}
Response JSON Sample (ShopResponse):
{
"success": true,
"httpStatus": "OK",
"message": "Shop created successfully",
"action_time": "2026-05-19T10:30:45",
"data": {
"shopId": "123e4567-e89b-12d3-a456-426614174000",
"shopName": "Mama Lucy's Restaurant",
"shopSlug": "mama-lucys-restaurant",
"shopDescription": "Authentic Tanzanian cuisine...",
"shopLogoMedia": {
"fileId": "f77e4567-e89b-12d3-a456-426614174777",
"status": "PROCESSING",
"variants": null,
"mimeType": "image/jpeg"
},
"shopBannerMedia": {
"fileId": "f88e4567-e89b-12d3-a456-426614174888",
"status": "PROCESSING",
"variants": null,
"mimeType": "image/jpeg"
},
"ownerId": "456e7890-e89b-12d3-a456-426614174001",
"ownerName": "Lucy Mwalimu",
"ownerProfileMedia": {
"fileId": "p11e4567-e89b-12d3-a456-426614174p11",
"status": "READY",
"variants": {
"medium": "https://cdn.example.com/profiles/owner-456/medium.jpg"
},
"mimeType": "image/jpeg"
},
"status": "ACTIVE",
"phoneNumber": "+255123456789",
"email": "info@mamalucy.co.tz",
"streetAddress": "Msimbazi Street, Block 45",
"city": "Dar es Salaam",
"region": "Dar es Salaam",
"countryCode": "TZ",
"latitude": -6.7924,
"longitude": 39.2083,
"landmark": "Near the main bus stop",
"isVerified": false,
"verificationBadge": null,
"trustScore": 0.00,
"lastSeenTime": null,
"isApproved": true,
"createdAt": "2026-05-19T10:30:45",
"updatedAt": "2026-05-19T10:30:45",
"approvedAt": null,
"averageRating": null,
"totalRatings": 0,
"totalActiveReviews": 0,
"reviews": [],
"isSubscribed": false,
"subscriberCount": 0,
"productCount": 0
}
}
Notes:
shopLogoMedia/shopBannerMediaare resolved from the FileThunder Redis cache at request time โ the file must have been requested viaPOST api/v1/files/request-upload(matching context), be owned by the caller, and be at leastPROCESSING. They start out withvariants: nulland get upgraded tostatus: "READY"with resolved CDN URLs invariantsasynchronously once FileThunder finishes processing, via a webhook intoShopMediaUpdater.onFileReadyโ re-fetch the shop to see the upgrade- Shops are created directly as
status: "ACTIVE"andisApproved: trueโ there's currently noPENDING-on-create/manual-approval-required flow in code, despite 9. Approve/Reject Shop existing for admins to flipisApproved
Error Responses:
400: Shop name already exists, orlogoFileId/bannerFileIdwas never uploaded (upload expired or never started), or does not belong to the caller401: Authentication required422: Validation errors
2. Get All Shops
Endpoint: GET api/v1/e-commerce/shops/all
Access Level: ๐ Public
Returns a list of all shops with summary info and top 5 reviews per shop.
3. Get All Shops (Paginated)
Endpoint: GET api/v1/e-commerce/shops/all-paged
Access Level: ๐ Public
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
| page | integer | 1 | Page number (1-based) |
| size | integer | 10 | Items per page (max: 100) |
Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Shops retrieved successfully",
"action_time": "2026-05-19T10:30:45",
"data": {
"shops": [ "...ShopSummary fields..." ],
"currentPage": 1,
"pageSize": 10,
"totalElements": 150,
"totalPages": 15,
"hasNext": true,
"hasPrevious": false,
"isFirst": true,
"isLast": false
}
}
4. Update Shop
Endpoint: PUT api/v1/e-commerce/shops/{shopId}
Access Level: ๐ Protected (Shop Owner only)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
| shopId | UUID | ID of the shop |
All request body fields are optional โ only provided fields are updated:
| Parameter | Type | Validation |
|---|---|---|
| shopName | string | Min: 2, Max: 100 chars โ slug is regenerated only if the name actually changed |
| shopDescription | string | Max: 1000 chars |
| logoFileId | UUID | Replaces shopLogoMedia โ same FileThunder validation as Create (context SHOP_LOGO, owned by caller) |
| bannerFileId | UUID | Replaces shopBannerMedia โ context SHOP_BANNER |
| phoneNumber | string | ^\+?[0-9]{10,15}$ |
| string | Valid email | |
| streetAddress | string | Max: 255 chars |
| city | string | Min: 2, Max: 50 chars |
| region | string | Min: 2, Max: 50 chars |
| countryCode | string | Max: 3 chars |
| latitude | decimal | -90.0 to 90.0 |
| longitude | decimal | -180.0 to 180.0 |
| landmark | string | Max: 300 chars |
Notes:
- There is no
shopImagesfield here either, and no way to remove a logo/banner via this endpoint (noclearLogo/clearBannerflag) โ sending a newlogoFileId/bannerFileIdreplaces the existing one, but there's no null-out path - Unlike product media, a replaced
shopLogoMedia/shopBannerMediais not merged/preserved โ it's a straight overwrite with a freshly Redis-resolvedShopMedia
Response: Full ShopResponse (same shape as Create Shop response)
Error Responses:
400: Not the shop owner, shop is deleted, orlogoFileId/bannerFileIdwas never uploaded/doesn't belong to the caller401: Authentication required404: Shop not found
5. Get Shop by ID (Summary)
Endpoint: GET api/v1/e-commerce/shops/{shopId}
Access Level: ๐ Public
Returns ShopSummaryListResponse โ public-facing fields including top 5 reviews and rating.
6. Get Shop by ID (Detailed)
Endpoint: GET api/v1/e-commerce/shops/{shopId}/detailed
Access Level: ๐ Protected (Shop Owner or Admin)
Returns full ShopResponse including all reviews and management fields.
7. Get My Shops
Endpoint: GET api/v1/e-commerce/shops/my-shops
Access Level: ๐ Protected
Returns all shops owned by the authenticated user (ShopSummaryListResponse list).
8. Get My Shops (Paginated)
Endpoint: GET api/v1/e-commerce/shops/my-shops-paged
Access Level: ๐ Protected
Query Parameters:
| Parameter | Default | Description |
|---|---|---|
| page | 1 | Page number (1-based) |
| size | 10 | Items per page (max: 100) |
9. Approve / Reject Shop
Endpoint: PATCH api/v1/e-commerce/shops/{shopId}/approve-shop
Access Level: ๐ Protected (ROLE_SUPER_ADMIN or ROLE_STAFF_ADMIN)
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| approve | boolean | Yes | true to approve, false to reject |
Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Shop approval status changed successfully",
"action_time": "2026-05-19T10:30:45",
"data": {
"shopId": "123e4567-e89b-12d3-a456-426614174000",
"shopName": "Mama Lucy's Restaurant",
"isApproved": true,
"approvedAt": "2026-05-19T10:30:45"
}
}
10. Get Featured Shops
Endpoint: GET api/v1/e-commerce/shops/featured
Access Level: ๐ Public
Returns up to 20 randomly selected featured shops (ShopSummaryListResponse list).
11. Get Featured Shops (Paginated)
Endpoint: GET api/v1/e-commerce/shops/featured-paged
Access Level: ๐ Public
Query Parameters:
| Parameter | Default | Description |
|---|---|---|
| page | 1 | Page number |
| size | 10 | Items per page (max: 100) |
12. Search Shops
Endpoint: GET api/v1/e-commerce/shops/search
Access Level: ๐ Public
Query Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| q | string | No | โ | Search query |
| page | integer | No | 1 | Page number |
| size | integer | No | 10 | Items per page |
Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Shops retrieved successfully",
"action_time": "2026-05-19T10:30:45",
"data": {
"content": [ "...ShopSearchResponse fields..." ],
"totalElements": 8,
"totalPages": 1,
"currentPage": 1,
"pageSize": 10,
"hasNext": false,
"hasPrevious": false
}
}
13. Get Shop Summary Stats
Endpoint: GET api/v1/e-commerce/shops/{shopId}/summary-stats
Access Level: ๐ Public
Returns aggregated review and rating statistics for a shop, including per-user activity.
Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Shop summary stats retrieved successfully",
"data": {
"shopId": "123e4567-e89b-12d3-a456-426614174000",
"shopName": "Mama Lucy's Restaurant",
"averageRating": 4.5,
"totalRatings": 25,
"ratingDistribution": { "1": 1, "2": 2, "3": 5, "4": 7, "5": 10 },
"totalReviews": 15,
"activeReviews": 12,
"hiddenReviews": 2,
"flaggedReviews": 1,
"userActivities": [
{
"userId": "user-123",
"userName": "John Doe",
"feedbackId": "rev-123",
"reviewText": "Amazing food and service!",
"reviewStatus": "ACTIVE",
"ratingValue": 5,
"date": "2026-05-19T14:30:00",
"hasReview": true,
"hasRating": true
}
]
}
}
14. WABA (WhatsApp Business) Integration
WABA allows shops to receive and respond to WhatsApp customer messages via an AI-powered chatbot. The flow is: shop registers a WABA โ admin approves with Meta credentials โ shop toggles AI on/off and monitors conversation history.
Base path for all WABA endpoints: api/v1/e-commerce/shops/{shopId}/waba
| Parameter | Type | Description |
|---|---|---|
| shopId | UUID | ID of the shop |
14a. Register WABA
Purpose: Shop owner submits a WhatsApp number and display name to begin WABA registration.
Endpoint: POST api/v1/e-commerce/shops/{shopId}/waba/register
Access Level: ๐ Protected (Shop Owner)
Request Body:
| Parameter | Type | Required | Validation |
|---|---|---|---|
| phoneNumber | string | Yes | Max: 20 chars |
| displayName | string | Yes | Max: 100 chars |
Request JSON Sample:
{
"phoneNumber": "+255712345678",
"displayName": "Mama Lucy's Restaurant"
}
14b. Approve WABA
Purpose: Admin approves a pending WABA registration by supplying Meta WABA credentials.
Endpoint: PATCH api/v1/e-commerce/shops/{shopId}/waba/approve
Access Level: ๐ Protected (ROLE_SUPER_ADMIN or ROLE_STAFF_ADMIN)
Request Body:
| Parameter | Type | Required | Validation |
|---|---|---|---|
| wabaId | string | Yes | Max: 64 chars (Meta WABA ID) |
| phoneNumberId | string | Yes | Max: 64 chars (Meta Phone Number ID) |
| phoneNumber | string | Yes | Max: 20 chars |
14c. Resubmit WABA
Purpose: Shop owner resubmits a rejected or pending WABA with corrected info.
Endpoint: PATCH api/v1/e-commerce/shops/{shopId}/waba/resubmit
Access Level: ๐ Protected (Shop Owner)
Request Body:
| Parameter | Type | Validation |
|---|---|---|
| phoneNumber | string | Max: 20 chars |
| displayName | string | Max: 100 chars |
14d. Admin Update WABA
Purpose: Admin updates Meta credentials on an existing WABA account.
Endpoint: PATCH api/v1/e-commerce/shops/{shopId}/waba/admin-update
Access Level: ๐ Protected (ROLE_SUPER_ADMIN or ROLE_STAFF_ADMIN)
Same request body as Resubmit WABA.
14e. Update WABA Status
Purpose: Admin changes the status of a WABA account (e.g., suspend or reactivate).
Endpoint: PATCH api/v1/e-commerce/shops/{shopId}/waba/status
Access Level: ๐ Protected (ROLE_SUPER_ADMIN or ROLE_STAFF_ADMIN)
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| status | ShopWabaStatus | Yes | PENDING, ACTIVE, SUSPENDED, REJECTED |
14f. Toggle AI Chatbot
Purpose: Shop owner enables or disables the AI chatbot for their WABA.
Endpoint: PATCH api/v1/e-commerce/shops/{shopId}/waba/toggle-ai
Access Level: ๐ Protected (Shop Owner)
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| enabled | boolean | Yes | true to enable, false to disable |
Response JSON Sample:
{
"success": true,
"message": "AI enabled successfully",
"data": {
"shopId": "...",
"aiEnabled": true,
"updatedAt": "2026-05-19T11:00:00"
}
}
14g. Get WABA Conversations
Purpose: Retrieves paginated WhatsApp conversation sessions for the shop.
Endpoint: GET api/v1/e-commerce/shops/{shopId}/waba/conversations
Access Level: ๐ Protected (Shop Owner or Admin)
Query Parameters:
| Parameter | Default | Description |
|---|---|---|
| page | 1 | Page number |
| size | 10 | Items per page |
Response JSON Sample:
{
"success": true,
"message": "Conversations retrieved",
"data": {
"content": [ "...WabaSessionResponse fields..." ],
"currentPage": 1,
"pageSize": 10,
"totalElements": 42,
"totalPages": 5,
"hasNext": true,
"hasPrevious": false
}
}
14h. Get Session Messages
Purpose: Retrieves paginated messages within a specific conversation session.
Endpoint: GET api/v1/e-commerce/shops/{shopId}/waba/conversations/{sessionId}/messages
Access Level: ๐ Protected (Shop Owner or Admin)
Additional Path Parameter:
| Parameter | Type | Description |
|---|---|---|
| sessionId | UUID | ID of the conversation session |
Query Parameters:
| Parameter | Default | Description |
|---|---|---|
| page | 1 | Page number |
| size | 20 | Items per page |
14i. Get Session Messages by Date Range
Purpose: Retrieves messages in a session filtered by date range.
Endpoint: GET api/v1/e-commerce/shops/{shopId}/waba/conversations/{sessionId}/messages/by-date
Access Level: ๐ Protected (Shop Owner or Admin)
Additional Path Parameter:
| Parameter | Type | Description |
|---|---|---|
| sessionId | UUID | ID of the conversation session |
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| from | LocalDateTime | Yes | Start datetime (ISO 8601) |
| to | LocalDateTime | Yes | End datetime (ISO 8601) |
| page | integer | No | Default: 1 |
| size | integer | No | Default: 20 |
Quick Reference
Enums
ShopStatus: PENDING, ACTIVE, SUSPENDED, CLOSED, UNDER_REVIEW
ShopType: PHYSICAL, ONLINE, HYBRID
VerificationBadge: BRONZE, SILVER, GOLD, PREMIUM
ShopWabaStatus: PENDING, ACTIVE, SUSPENDED, REJECTED
Error Response Codes
| Code | Meaning |
|---|---|
400 |
Business logic violation or item already exists |
401 |
Authentication required or token invalid/expired |
403 |
Insufficient permissions |
404 |
Resource not found |
422 |
Field-level validation errors |
Validation Error (422):
{
"success": false,
"httpStatus": "UNPROCESSABLE_ENTITY",
"message": "Validation failed",
"data": {
"shopName": "Shop name must be between 2 and 100 characters",
"phoneNumber": "Phone number must be between 10-15 digits and may start with +"
}
}
Access Control Summary
| Role | Capabilities |
|---|---|
| Public | Read shops, search, featured, summary stats |
| Authenticated user | All public + create shop, view own shops |
| Shop Owner | All authenticated + update own shop, WABA management, view conversations |
ROLE_SUPER_ADMIN / ROLE_STAFF_ADMIN |
All + approve/reject shops, approve/update/status WABA |
WABA Registration Flow
1. POST /{shopId}/waba/register โ shop owner submits phone + display name
2. PATCH /{shopId}/waba/approve โ admin supplies Meta wabaId + phoneNumberId
3. PATCH /{shopId}/waba/toggle-ai โ shop owner enables AI chatbot
4. GET /{shopId}/waba/conversations โ shop owner monitors customer chats
Shop Review
Short Description: The Shop Review API allows users to write, manage, and retrieve reviews (text + star rating) for shops. Supports create, update, delete, listing with pagination, and summary statistics.
Hints:
- All endpoints use the prefix
api/v1/e-commerce/shops/reviews/{shopId} - All operations require Bearer token authentication
- One review per user per shop โ use PUT to update an existing one
- Shop owners cannot review their own shops
reviewTextandratingValueare both optional fields, but at least one should be providedratingValuemust be 1โ5 if providedreviewTextmust be 10โ1000 characters if providedisMyReviewflag is only populated when the authenticated user is included in the response builder
Standard Response Format
{
"success": true,
"httpStatus": "OK",
"message": "...",
"action_time": "2026-05-19T10:30:45",
"data": { }
}
Endpoints
1. Create Review (Feedback)
Endpoint: POST api/v1/e-commerce/shops/reviews/{shopId}
Access Level: ๐ Protected
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
| shopId | UUID | ID of the shop to review |
Request Body:
| Parameter | Type | Required | Validation |
|---|---|---|---|
| reviewText | string | No | Min: 10, Max: 1000 chars |
| ratingValue | integer | No | Min: 1, Max: 5 |
Request JSON Sample:
{
"reviewText": "Amazing food and excellent service! Highly recommend the local dishes.",
"ratingValue": 5
}
Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Feedback submitted successfully",
"action_time": "2026-05-19T10:30:45",
"data": {
"reviewId": "123e4567-e89b-12d3-a456-426614174000",
"shopId": "456e7890-e89b-12d3-a456-426614174001",
"shopName": "Mama Lucy's Restaurant",
"userId": "789e0123-e89b-12d3-a456-426614174002",
"userName": "John Doe",
"reviewText": "Amazing food and excellent service! Highly recommend the local dishes.",
"ratingValue": 5,
"status": "ACTIVE",
"createdAt": "2026-05-19T10:30:45",
"updatedAt": "2026-05-19T10:30:45",
"isMyReview": false
}
}
Error Responses:
400: Already reviewed this shop, or shop owner reviewing own shop401: Authentication required404: Shop not found
2. Update Review (Feedback)
Endpoint: PUT api/v1/e-commerce/shops/reviews/{shopId}
Access Level: ๐ Protected (Review Owner only)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
| shopId | UUID | ID of the reviewed shop |
Request Body (same as Create โ all fields optional):
| Parameter | Type | Validation |
|---|---|---|
| reviewText | string | Min: 10, Max: 1000 chars |
| ratingValue | integer | Min: 1, Max: 5 |
Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Feedback updated successfully",
"action_time": "2026-05-19T11:15:30",
"data": {
"reviewId": "123e4567-e89b-12d3-a456-426614174000",
"shopId": "456e7890-e89b-12d3-a456-426614174001",
"shopName": "Mama Lucy's Restaurant",
"userId": "789e0123-e89b-12d3-a456-426614174002",
"userName": "John Doe",
"reviewText": "Still amazing food. Local dishes are worth it.",
"ratingValue": 4,
"status": "ACTIVE",
"createdAt": "2026-05-19T10:30:45",
"updatedAt": "2026-05-19T11:15:30",
"isMyReview": false
}
}
Error Responses:
401: Authentication required404: Review not found (create one first)
3. Delete Review (Feedback)
Endpoint: DELETE api/v1/e-commerce/shops/reviews/{shopId}
Access Level: ๐ Protected (Review Owner only)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
| shopId | UUID | ID of the reviewed shop |
Soft-deletes the review. Deleted reviews no longer appear in listings or counts.
Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Feedback deleted successfully",
"action_time": "2026-05-19T10:30:45",
"data": null
}
Error Responses:
401: Authentication required404: Review not found
4. Get Active Reviews for Shop
Endpoint: GET api/v1/e-commerce/shops/reviews/{shopId}
Access Level: ๐ Protected (auth required to populate isMyReview)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
| shopId | UUID | ID of the shop |
Returns all ACTIVE reviews. isMyReview is set to true for the authenticated user's own review.
Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Reviews retrieved successfully",
"action_time": "2026-05-19T10:30:45",
"data": [
{
"reviewId": "123e4567-e89b-12d3-a456-426614174000",
"shopId": "456e7890-e89b-12d3-a456-426614174001",
"shopName": "Mama Lucy's Restaurant",
"userId": "789e0123-e89b-12d3-a456-426614174002",
"userName": "John Doe",
"reviewText": "Amazing food!",
"ratingValue": 5,
"status": "ACTIVE",
"createdAt": "2026-05-19T10:30:45",
"updatedAt": "2026-05-19T10:30:45",
"isMyReview": true
}
]
}
Error Responses:
401: Authentication required404: Shop not found
5. Get Active Reviews (Paginated)
Endpoint: GET api/v1/e-commerce/shops/reviews/{shopId}/paged
Access Level: ๐ Protected
Query Parameters:
| Parameter | Default | Description |
|---|---|---|
| page | 1 | Page number (1-based) |
| size | 10 | Items per page (max: 100) |
Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Reviews retrieved successfully",
"action_time": "2026-05-19T10:30:45",
"data": {
"reviews": [
{
"reviewId": "123e4567-e89b-12d3-a456-426614174000",
"shopName": "Mama Lucy's Restaurant",
"userName": "John Doe",
"reviewText": "Amazing food!",
"ratingValue": 5,
"status": "ACTIVE",
"createdAt": "2026-05-19T10:30:45"
}
],
"currentPage": 1,
"pageSize": 10,
"totalElements": 25,
"totalPages": 3,
"hasNext": true,
"hasPrevious": false,
"isFirst": true,
"isLast": false
}
}
Error Responses:
401: Authentication required404: Shop not found
6. Get My Review for Shop
Endpoint: GET api/v1/e-commerce/shops/reviews/{shopId}/my-review
Access Level: ๐ Protected
Returns the authenticated user's review for the shop, or null if they haven't reviewed it yet.
Response JSON Sample (has review):
{
"success": true,
"httpStatus": "OK",
"message": "Your feedback retrieved successfully",
"data": {
"reviewId": "123e4567-e89b-12d3-a456-426614174000",
"shopId": "456e7890-e89b-12d3-a456-426614174001",
"shopName": "Mama Lucy's Restaurant",
"userId": "789e0123-e89b-12d3-a456-426614174002",
"userName": "John Doe",
"reviewText": "Amazing food!",
"ratingValue": 5,
"status": "ACTIVE",
"createdAt": "2026-05-19T10:30:45",
"updatedAt": "2026-05-19T10:30:45",
"isMyReview": false
}
}
Response when no review exists:
{
"success": true,
"httpStatus": "OK",
"message": "Your feedback retrieved successfully",
"data": null
}
7. Get Shop Review Summary
Endpoint: GET api/v1/e-commerce/shops/reviews/{shopId}/summary
Access Level: ๐ Public
Returns aggregated rating and review counts for a shop.
Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Shop feedback summary retrieved successfully",
"data": {
"shopId": "456e7890-e89b-12d3-a456-426614174001",
"shopName": "Mama Lucy's Restaurant",
"averageRating": 4.5,
"totalRatings": 25,
"ratingDistribution": { "1": 1, "2": 2, "3": 5, "4": 7, "5": 10 },
"totalReviews": 15,
"activeReviews": 12,
"hiddenReviews": 2,
"flaggedReviews": 1
}
}
Quick Reference
ReviewStatus Enum
| Value | Description |
|---|---|
ACTIVE |
Visible in public listings |
HIDDEN |
Hidden from public view |
FLAGGED |
Flagged for admin review |
UNDER_REVIEW |
Under admin review |
ReviewResponse Fields
| Field | Type | Description |
|---|---|---|
| reviewId | UUID | Unique review ID |
| shopId | UUID | Shop that was reviewed |
| shopName | string | Shop name |
| userId | UUID | Reviewer's user ID |
| userName | string | Reviewer's display name |
| reviewText | string | Review text content |
| ratingValue | integer | Star rating (1โ5), nullable |
| status | ReviewStatus | Current review status |
| createdAt | LocalDateTime | Creation timestamp |
| updatedAt | LocalDateTime | Last update timestamp |
| isMyReview | boolean | True if this is the current user's review |
Error Codes
| Code | Meaning |
|---|---|
400 |
Business rule violation (duplicate review, owner self-review) |
401 |
Authentication required |
404 |
Shop or review not found |
422 |
Field validation errors |
Shop Subscription
Base URL: api/v1/e-commerce/shops
Short Description: The Shop Subscription API allows users to subscribe and unsubscribe from shops on the Nexgate marketplace. Subscriptions drive personalised product discovery โ subscribed shops receive a ranking boost in the Trending and For You feeds, and a dedicated "from your shops" signal in the relevance formula. Shop owners can view who has subscribed to their shop.
Hints:
- The action is called Subscribe โ not Follow. This is intentional to distinguish from the social module's userโuser Follow feature. Users follow people, they subscribe to shops.
POST /{shopId}/subscribeis a toggle โ calling it once subscribes, calling it again unsubscribes. No separate unsubscribe endpoint is needed.- Subscription status (
isSubscribed) is automatically included inShopResponsefor all shop detail endpoints. Authenticated users see their real status; anonymous users always receivefalse. - Subscriptions directly feed into the Marketplace
FOR_YOUandTRENDINGpersonalisation โ seemarketplace_api_doc.mdfor formula details. - Pages are 1-based (
page=1returns the first page).
Standard Response Format
All API responses follow a consistent structure using the Globe Response Builder pattern:
Success Response Structure
{
"success": true,
"httpStatus": "OK",
"message": "Operation completed successfully",
"action_time": "2026-06-04T10:30:45",
"data": { }
}
Error Response Structure
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Error description",
"action_time": "2026-06-04T10:30:45",
"data": "Error description"
}
Standard Response Fields
| Field | Type | Description |
|---|---|---|
success |
boolean | true for success, false for errors |
httpStatus |
string | HTTP status name (OK, BAD_REQUEST, NOT_FOUND, etc.) |
message |
string | Human-readable operation result |
action_time |
string | ISO 8601 timestamp of response generation |
data |
object | Response payload for success, error detail for failures |
HTTP Method Badge Standards
- GET โ GET Green
- POST โ POST Blue
Subscription State Reference
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ SUBSCRIPTION STATE FLOW โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ User not subscribed โ
โ โ โ
โ โผ POST /{shopId}/subscribe โ
โ User subscribed โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโบ โ
โ โ subscriberCount + 1 โ
โ โ subscribed: true โ
โ โ โ
โ โผ POST /{shopId}/subscribe (toggle again) โ
โ User not subscribed โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ subscriberCount - 1 โ
โ subscribed: false โ
โ โ
โ Side effects: โ
โ - Shop's subscriberCount is updated atomically โ
โ - Marketplace FOR_YOU feed recalculates on next request โ
โ - Marketplace TRENDING feed gives +0.25 boost to subscribed shops โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
How Subscriptions Affect the Marketplace
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ SUBSCRIPTION โ MARKETPLACE SIGNALS โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ TRENDING feed: โ
โ personalizedScore = trendingScore + 0.25 โ
โ (applied to every product from a subscribed shop) โ
โ โ
โ FOR YOU feed: โ
โ relevanceScore = categoryMatch ร 0.40 โ
โ + favShopBoost ร 0.40 โ subscription signal โ
โ + trendingScore ร 0.20 โ
โ favShopBoost = 1.0 if subscribed, 0.0 otherwise โ
โ โ
โ Result: products from subscribed shops surface near the top of โ
โ both feeds while still allowing genuinely trending products to โ
โ compete (soft priority, not guaranteed top position). โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Endpoints
1. Toggle Subscribe / Unsubscribe
Purpose: Subscribes the authenticated user to a shop if they are not already subscribed, or unsubscribes them if they are. Returns the new subscription state and the updated subscriber count.
Endpoint: POST api/v1/e-commerce/shops/{shopId}/subscribe
Access Level: ๐ Protected (authentication required)
Authentication: Bearer Token (required)
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
Authorization |
string | Yes | Bearer <token> |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
shopId |
UUID | Yes | The shop to subscribe or unsubscribe from | Must be a valid UUID of an active, non-deleted shop |
Success Response JSON Sample โ Subscribe:
{
"success": true,
"httpStatus": "OK",
"message": "Subscribed to shop successfully",
"action_time": "2026-06-04T10:30:45",
"data": {
"subscribed": true,
"subscriberCount": 1284
}
}
Success Response JSON Sample โ Unsubscribe (same endpoint, called again):
{
"success": true,
"httpStatus": "OK",
"message": "Unsubscribed from shop successfully",
"action_time": "2026-06-04T10:30:45",
"data": {
"subscribed": false,
"subscriberCount": 1283
}
}
Success Response Fields:
| Field | Type | Description |
|---|---|---|
subscribed |
boolean | Current subscription state after the toggle โ true if now subscribed, false if now unsubscribed |
subscriberCount |
long | Updated total number of subscribers for this shop |
Error Response JSON Samples:
{
"success": false,
"httpStatus": "UNAUTHORIZED",
"message": "Token has expired",
"action_time": "2026-06-04T10:30:45",
"data": "Token has expired"
}
Shop Not Found (404):
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "Shop not found",
"action_time": "2026-06-04T10:30:45",
"data": "Shop not found"
}
Standard Error Types:
2. My Subscriptions
Purpose: Returns a paginated list of all shops the authenticated user is currently subscribed to, ordered by most recently subscribed first. Each entry includes shop details and the current subscriber count.
Endpoint: GET api/v1/e-commerce/shops/my-subscriptions
Access Level: ๐ Protected (authentication required)
Authentication: Bearer Token (required)
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
Authorization |
string | Yes | Bearer <token> |
Query Parameters:
| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
page |
integer | No | Page number (1-based) | 1 |
size |
integer | No | Items per page | 10 |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "My subscriptions retrieved successfully",
"action_time": "2026-06-04T10:30:45",
"data": {
"content": [
{
"subscriptionId": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"shopId": "7cb3a812-1234-4abc-b3fc-9d84f55bce12",
"shopName": "TechStore Tanzania",
"shopSlug": "techstore-tanzania",
"logoUrl": "https://cdn.nexgate.com/shops/techstore-logo.jpg",
"bannerUrl": "https://cdn.nexgate.com/shops/techstore-banner.jpg",
"status": "ACTIVE",
"isVerified": true,
"verificationBadge": "GOLD",
"trustScore": 4.80,
"subscriberCount": 1284,
"subscribedAt": "2026-05-20T14:32:00"
}
],
"currentPage": 1,
"pageSize": 10,
"totalElements": 7,
"totalPages": 1,
"hasNext": false,
"hasPrevious": false
}
}
Success Response Fields:
| Field | Description |
|---|---|
content[].subscriptionId |
Unique identifier of this subscription record |
content[].shopId |
Shop's unique identifier |
content[].shopName |
Shop display name |
content[].shopSlug |
Shop URL slug โ use for navigating to the shop page |
content[].logoUrl |
Shop logo image URL |
content[].bannerUrl |
Shop banner image URL |
content[].status |
Shop status โ ACTIVE, TEMPORARILY_OFFLINE, etc. |
content[].isVerified |
Whether the shop has passed platform verification |
content[].verificationBadge |
Verification badge level โ BRONZE, SILVER, GOLD |
content[].trustScore |
Shop trust rating 0.00โ5.00 |
content[].subscriberCount |
Total subscribers this shop currently has |
content[].subscribedAt |
ISO 8601 timestamp of when the user subscribed |
currentPage |
Current page number (1-based) |
totalElements |
Total shops the user is subscribed to |
hasNext / hasPrevious |
Pagination navigation flags |
Error Response JSON Samples:
{
"success": false,
"httpStatus": "UNAUTHORIZED",
"message": "Token has expired",
"action_time": "2026-06-04T10:30:45",
"data": "Token has expired"
}
Standard Error Types:
3. Shop Subscribers (Owner Only)
Purpose: Returns a paginated list of users who have subscribed to a specific shop, ordered by most recently subscribed first. Only the shop owner can access this endpoint โ calling it for a shop you do not own returns a 403 error.
Endpoint: GET api/v1/e-commerce/shops/{shopId}/subscribers
Access Level: ๐ Protected (shop owner only)
Authentication: Bearer Token (required โ must be the owner of the shop)
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
Authorization |
string | Yes | Bearer <token> |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
shopId |
UUID | Yes | The shop whose subscriber list to retrieve | Caller must own this shop |
Query Parameters:
| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
page |
integer | No | Page number (1-based) | 1 |
size |
integer | No | Items per page | 10 |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Subscribers retrieved successfully",
"action_time": "2026-06-04T10:30:45",
"data": {
"content": [
{
"userId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"fullName": "Amina Hassan",
"userName": "amina.h",
"avatarUrl": "https://cdn.nexgate.com/avatars/amina-h.jpg",
"subscribedAt": "2026-06-01T09:45:00"
},
{
"userId": "1cb2e847-3f1a-4bcd-a3fc-9e84f22bce99",
"fullName": "John Mwangi",
"userName": "jmwangi",
"avatarUrl": null,
"subscribedAt": "2026-05-29T16:12:00"
}
],
"currentPage": 1,
"pageSize": 10,
"totalElements": 1284,
"totalPages": 129,
"hasNext": true,
"hasPrevious": false
}
}
Success Response Fields:
| Field | Description |
|---|---|
content[].userId |
Subscriber's unique account identifier |
content[].fullName |
Subscriber's first + last name |
content[].userName |
Subscriber's public username |
content[].avatarUrl |
Subscriber's profile picture URL โ null if no avatar set |
content[].subscribedAt |
ISO 8601 timestamp of when this user subscribed |
currentPage |
Current page number (1-based) |
totalElements |
Total subscriber count for this shop |
hasNext / hasPrevious |
Pagination navigation flags |
Error Response JSON Samples:
{
"success": false,
"httpStatus": "UNAUTHORIZED",
"message": "Token has expired",
"action_time": "2026-06-04T10:30:45",
"data": "Token has expired"
}
Forbidden โ Not the shop owner (403):
{
"success": false,
"httpStatus": "FORBIDDEN",
"message": "You do not own this shop",
"action_time": "2026-06-04T10:30:45",
"data": "You do not own this shop"
}
Shop Not Found (404):
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "Shop not found",
"action_time": "2026-06-04T10:30:45",
"data": "Shop not found"
}
Standard Error Types:
isSubscribed & subscriberCount in ShopResponse
Subscription state is automatically embedded in the standard ShopResponse returned by all shop detail endpoints โ no separate call needed.
{
"shopId": "7cb3a812-...",
"shopName": "TechStore Tanzania",
"isVerified": true,
"trustScore": 4.80,
"isSubscribed": true,
"subscriberCount": 1284,
"productCount": 47
}
| Field | Type | Description |
|---|---|---|
isSubscribed |
boolean | true if the requesting authenticated user is subscribed to this shop. Always false for anonymous users |
subscriberCount |
long | Total number of subscribers this shop currently has |
productCount |
long | Number of active (published) products in this shop |
Quick Reference โ All Subscription Endpoints
| # | Endpoint | Method | Auth | Who can call |
|---|---|---|---|---|
| 1 | api/v1/e-commerce/shops/{shopId}/subscribe |
POST | Required | Any authenticated user |
| 2 | api/v1/e-commerce/shops/my-subscriptions |
GET | Required | Any authenticated user |
| 3 | api/v1/e-commerce/shops/{shopId}/subscribers |
GET | Required | Shop owner only |
Products Management Service
Product Management
Short Description: The Product Management API provides comprehensive functionality for managing products within shops on the NextGate platform. It supports group buying, installment payment plans, color variations, specifications, digital product file delivery, product preview media (video, PDF, 3D, image), and comprehensive search/filter capabilities with role-based access control.
Hints:
All product media (product images/videos, color images, digital files, previews) go through FileThunder โ no raw URLs are ever sent by the client:
- Client calls
POST api/v1/files/request-uploadwith acontext(PRODUCT_IMAGE,PRODUCT_VIDEO,DIGITAL_PRODUCT,PRODUCT_PREVIEW_IMAGE/_VIDEO/_DOCUMENT) โ gets back a presigned upload URL and afileId - Client
PUTs the raw file bytes directly to that URL (not through this API) - Client references that
fileIdin the relevant product field (mediaFileIds,colors[].imageFileIds,digitalFileId, preview'sftFileId, ...) โ the file is still processing (virus scan + variant generation) at this point, so validation only checks Redis-cached ownership/context, not readiness - FileThunder finishes processing asynchronously and webhooks back into the product/preview/digital-file "updater" services, which upgrade the stored record to
READYwith resolvedvariants(a map of variant name โ CDN URL)
- Responses never return raw
productImages/previewUrlstrings โ image-bearing fields areProductMediaItemobjects (fileId,mediaType,status,variants,mimeType,order) or similarly-shaped preview/file objects, and URLs are assembled fresh on every read viaFileThunderUrlAssembler - Known gap: color
imageFileIdsare stored as raw file IDs and are not validated/resolved through FileThunder like top-level product media โ thecolors[].imagesfield in responses currently echoes back the file ID strings as-is, not resolved variant URLs
Endpoints
1. Create Product
Purpose: Creates a new product in a shop, supporting group buying, color variations, specifications, and digital product rules.
Endpoint: POST api/v1/e-commerce/shops/{shopId}/products
Access Level: ๐ Protected (Requires shop owner or system admin role)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token |
| Content-Type | string | Yes | application/json |
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| shopId | UUID | Yes | ID of the shop |
Query Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| action | ReqAction | Yes | โ | SAVE_DRAFT or SAVE_PUBLISH |
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| productType | ProductType | Yes | PHYSICAL or DIGITAL |
Required |
| productName | string | Yes | Unique name within shop | Min: 2, Max: 100 chars |
| productDescription | string | Yes | Detailed description | Min: 10, Max: 1000 chars |
| price | decimal | Yes | Selling price | Min: 0.01, max 8 digits + 2 decimal places |
| stockQuantity | integer | Yes | Available stock | Min: 0 |
| categoryId | UUID | Yes | Product category | Must exist and be active |
| comparePrice | decimal | No | Original price for discount display | Must be > price if provided |
| lowStockThreshold | integer | No | Low stock alert threshold | Min: 1, Max: 1000, Default: 5 |
| condition | ProductCondition | No | Product condition | NEW, USED_LIKE_NEW, USED_GOOD, USED_FAIR, REFURBISHED, FOR_PARTS |
| status | ProductStatus | No | Initial status (overridden by action) |
Default: ACTIVE |
| mediaFileIds | array<UUID> | Yes | FileThunder file IDs for product images/videos, in display order | Not null; each ID must belong to the caller and have been uploaded with context PRODUCT_IMAGE or PRODUCT_VIDEO |
| specifications | object | No | Key-value specs | Key max 100 chars, Value max 500 chars |
| colors | array | No | Color variations | See Color object below |
| minOrderQuantity | integer | No | Minimum order qty | Min: 1, Default: 1 |
| maxOrderQuantity | integer | No | Maximum order qty per order | Min: 1, must be โฅ minOrderQuantity |
| groupBuyingEnabled | boolean | No | Enable group buying | Default: false |
| groupMaxSize | integer | No | Maximum group participants | Min: 2, required if groupBuyingEnabled |
| groupPrice | decimal | No | Discounted group price | Must be < price |
| groupTimeLimitHours | integer | No | Group formation time limit | Min: 1, Max: 8760 |
| digitalFileId | UUID | Conditional | FileThunder file ID of the downloadable file | Required when productType=DIGITAL; uploaded with context DIGITAL_PRODUCT |
| previewFileId | UUID | No | FileThunder file ID of a public preview (DIGITAL only) | Uploaded with context PRODUCT_PREVIEW_IMAGE/_VIDEO/_DOCUMENT |
| previewDownloadable | boolean | No | Whether buyers can download the preview file itself | Default: false |
| downloadExpiryDays | integer | No | Download link expiry (DIGITAL only) | Min: 1, Default: 7 |
| maxDownloadsPerBuyer | integer | No | Download attempts per buyer (DIGITAL only) | Min: 1 |
| maxQuantityForDigital | integer | No | Purchase cap per buyer (DIGITAL only) | Min: 1 |
Color Object:
| Field | Type | Required | Validation |
|---|---|---|---|
| name | string | Yes | Max: 50 chars |
| hex | string | Yes | Valid #RRGGBB format |
| imageFileIds | array<UUID> | No | FileThunder file IDs for this color's images โ stored as-is, not validated/resolved against FileThunder (see top-of-doc note) |
| priceAdjustment | decimal | No | Min: 0.0, Default: 0 |
Request JSON Sample (PHYSICAL):
{
"productType": "PHYSICAL",
"productName": "iPhone 15 Pro Max 256GB",
"productDescription": "The most advanced iPhone featuring the A17 Pro chip and titanium design.",
"price": 1199.00,
"comparePrice": 1299.00,
"stockQuantity": 25,
"lowStockThreshold": 5,
"categoryId": "123e4567-e89b-12d3-a456-426614174000",
"condition": "NEW",
"mediaFileIds": [
"f11e4567-e89b-12d3-a456-426614174111",
"f22e4567-e89b-12d3-a456-426614174222"
],
"specifications": {
"Display": "6.7-inch Super Retina XDR OLED",
"Chip": "A17 Pro"
},
"colors": [
{
"name": "Natural Titanium",
"hex": "#F5F5DC",
"imageFileIds": ["f33e4567-e89b-12d3-a456-426614174333"],
"priceAdjustment": 0.00
}
],
"minOrderQuantity": 1,
"maxOrderQuantity": 3,
"groupBuyingEnabled": true,
"groupMaxSize": 50,
"groupPrice": 1099.00,
"groupTimeLimitHours": 72
}
Request JSON Sample (DIGITAL):
{
"productType": "DIGITAL",
"productName": "UI Design Kit Pro",
"productDescription": "A comprehensive Figma component library with 500+ components.",
"price": 49.00,
"stockQuantity": 1000,
"categoryId": "123e4567-e89b-12d3-a456-426614174000",
"mediaFileIds": ["f44e4567-e89b-12d3-a456-426614174444"],
"digitalFileId": "f55e4567-e89b-12d3-a456-426614174555",
"previewFileId": "f66e4567-e89b-12d3-a456-426614174666",
"previewDownloadable": false,
"downloadExpiryDays": 30,
"maxDownloadsPerBuyer": 5,
"maxQuantityForDigital": 1
}
Response JSON Sample:
{
"success": true,
"message": "Product created successfully",
"data": {
"productId": "456e7890-e89b-12d3-a456-426614174001",
"productSlug": "iphone-15-pro-max-256gb",
"productName": "iPhone 15 Pro Max 256GB",
"status": "ACTIVE",
"shopSlug": "techstore-pro"
}
}
Business Rules:
- Product name must be unique within the shop
comparePricemust be greater thanpricegroupPricemust be less thanprice- All group buying settings (
groupMaxSize,groupPrice,groupTimeLimitHours) required whengroupBuyingEnabled=true maxOrderQuantitymust be โฅminOrderQuantity- Digital download fields (
downloadExpiryDays,maxDownloadsPerBuyer,maxQuantityForDigital) only apply toDIGITALproducts mediaFileIdsare resolved from the FileThunder Redis cache at request time โ each file must have been requested viaPOST api/v1/files/request-upload(contextPRODUCT_IMAGE/PRODUCT_VIDEO), be owned by the caller, and be at leastPROCESSING(a still-uploading or expired/never-started file is rejected). Files that finish processing after the product is created are silently upgraded toREADYin place via a FileThunder webhookDIGITALproducts requiredigitalFileIdโ creation fails immediately if it's missing- After creation, add installment plans via Installment Plan Config; digital files and previews can also be (re)managed later via Digital File Management and Product Preview Management
Error Responses:
400: Validation errors or business rule violations401: Authentication required403: Insufficient permissions404: Shop or category not found, or a referencedmediaFileIds/digitalFileId/previewFileIdwas never uploaded (upload expired or never started)409: Product with same name already exists in shop422: Field-level validation errors
2. Update Product
Purpose: Updates an existing product. Only provided fields are updated.
Endpoint: PUT api/v1/e-commerce/shops/{shopId}/products/{productId}
Access Level: ๐ Protected (Requires shop owner or system admin role)
Authentication: Bearer Token
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| shopId | UUID | Yes | ID of the shop |
| productId | UUID | Yes | ID of the product |
Query Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| action | ReqAction | Yes | โ | SAVE_DRAFT or SAVE_PUBLISH |
Request Body Parameters (all optional):
| Parameter | Type | Description | Validation |
|---|---|---|---|
| productName | string | Updated name | Min: 2, Max: 100 chars |
| productDescription | string | Updated description | Min: 10, Max: 1000 chars |
| price | decimal | Updated selling price | Min: 0.01 |
| comparePrice | decimal | Updated compare price | Must be > price |
| stockQuantity | integer | Updated stock | Min: 0 |
| lowStockThreshold | integer | Updated low stock threshold | Min: 1, Max: 1000 |
| condition | ProductCondition | Updated condition | See enum values |
| status | ProductStatus | Updated status | Overridden by action |
| urgencyTag | UrgencyTag | Urgency badge on product | NONE, NEW_ARRIVAL, LIMITED_EDITION, LIMITED_OFFER, FEW_REMAINS |
| categoryId | UUID | Updated category | Must exist and be active |
| mediaFileIds | array<UUID> | Updated list of FileThunder file IDs for product images/videos, in order | Same validation as Create; merged, not replaced โ see note below |
| specifications | object | Updated specifications | Completely replaces existing |
| colors | array | Updated color variations | Completely replaces existing โ see Color object in Create Product (same imageFileIds shape) |
| minOrderQuantity | integer | Updated min order qty | Min: 1 |
| maxOrderQuantity | integer | Updated max order qty | Min: 1, must be โฅ min |
| groupBuyingEnabled | boolean | Enable/disable group buying | โ |
| groupMaxSize | integer | Updated group max | Min: 2 |
| groupPrice | decimal | Updated group price | Must be < price |
| groupTimeLimitHours | integer | Updated group time limit | Min: 1, Max: 8760 |
| installmentEnabled | boolean | Enable/disable installment feature toggle | โ |
| maxQuantityForInstallment | integer | Max qty a buyer can purchase on installment | Min: 1 |
| showStockAvailableToPublic | boolean | Show available stock count publicly | โ |
| showSoldCountToPublic | boolean | Show sold count publicly | โ |
| clearPreview | boolean | โ ๏ธ Currently only clears legacy preview fields โ does not remove an active preview (see note below) | โ |
| previewDownloadable | boolean | Allow/disallow viewers from downloading the preview file | โ |
Notes on mediaFileIds:
- Unlike Create, Update merges by fileId: any ID already present in the product's stored media that has finished processing (has resolved
variants) is kept as-is and just re-ordered; any new ID is resolved fresh from the FileThunder Redis cache (same ownership/context checks as Create) - To remove an image, omit its fileId from the array โ there is no separate "delete image" endpoint
- Digital file and preview are not updatable through this endpoint โ there is no
digitalFileId/previewFileIdfield here. Manage them via Digital File Management and Product Preview Management
Note on clearPreview: this flag only nulls the legacy previewType/previewUrl entity fields, which are already dead (never populated by the current preview pipeline). It does not clear previewFtFileId/previewContext/previewStatus/previewVariants. To actually remove a product's preview, call DELETE .../products/{productId}/preview (17c).
Response JSON Sample:
{
"success": true,
"message": "Product updated successfully and published",
"data": {
"...": "full ProductDetailedResponse โ same shape as Get Product Detailed (#6), rebuilt from the saved product"
}
}
Error Responses:
400: Invalid update data or validation errors401: Authentication required403: Insufficient permissions404: Shop or product not found, or a referencedmediaFileIdsentry was never uploaded409: Updated product name already exists
3. Publish Product
Purpose: Publishes a draft product making it active and publicly available.
Endpoint: PATCH api/v1/e-commerce/shops/{shopId}/products/{productId}/publish
Access Level: ๐ Protected (Requires shop owner or system admin role)
Authentication: Bearer Token
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| shopId | UUID | Yes | ID of the shop |
| productId | UUID | Yes | ID of the draft product |
Response JSON Sample:
{
"success": true,
"message": "Product 'iPhone 15 Pro Max' published successfully",
"data": {
"productId": "456e7890-e89b-12d3-a456-426614174001",
"productName": "iPhone 15 Pro Max",
"status": "ACTIVE",
"publishedAt": "2026-05-19T14:45:00Z"
}
}
Publishing Requirements:
- Product name, description, price, stock quantity, category, and at least one image must be present
- Group buying settings complete if enabled
- Installment plans present if installment is enabled
Error Responses:
400: Product already published or missing required publish fields401: Authentication required403: Insufficient permissions404: Shop or product not found
4. Delete Product
Purpose: Deletes a product. Draft products are hard-deleted; published products are soft-deleted.
Endpoint: DELETE api/v1/e-commerce/shops/{shopId}/products/{productId}
Access Level: ๐ Protected (Requires shop owner or system admin role)
Authentication: Bearer Token
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| shopId | UUID | Yes | ID of the shop |
| productId | UUID | Yes | ID of the product |
Response JSON Sample (Soft Delete):
{
"success": true,
"message": "Product 'iPhone 15 Pro Max' has been deleted and will be permanently removed after 30 days",
"data": {
"productName": "iPhone 15 Pro Max",
"productId": "456e7890-e89b-12d3-a456-426614174001",
"previousStatus": "ACTIVE",
"deletedAt": "2026-05-19T15:00:00Z",
"deletionType": "SOFT_DELETE"
}
}
Response JSON Sample (Hard Delete):
{
"success": true,
"message": "Draft product 'iPhone 15 Pro Max' has been permanently deleted",
"data": null
}
Deletion Logic:
- Draft products: Hard delete (permanently removed)
- Published products: Soft delete (status โ ARCHIVED, 30-day recovery window)
Error Responses:
401: Authentication required403: Insufficient permissions404: Shop or product not found
5. Restore Product
Purpose: Restores a soft-deleted product back to draft status.
Endpoint: PATCH api/v1/e-commerce/shops/{shopId}/products/{productId}/restore
Access Level: ๐ Protected (Requires shop owner or system admin role)
Authentication: Bearer Token
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| shopId | UUID | Yes | ID of the shop |
| productId | UUID | Yes | ID of the soft-deleted product |
Response JSON Sample:
{
"success": true,
"message": "Product 'iPhone 15 Pro Max' has been restored successfully",
"data": {
"productId": "456e7890-e89b-12d3-a456-426614174001",
"productName": "iPhone 15 Pro Max",
"status": "DRAFT",
"restoredAt": "2026-05-19T15:30:00Z",
"note": "Product restored as draft. Publish to make it active again."
}
}
Error Responses:
400: Product is not deleted401: Authentication required403: Insufficient permissions404: Shop or product not found
6. Get Product Detailed (Owner/Admin View)
Purpose: Retrieves comprehensive product details including all management information.
Endpoint: GET api/v1/e-commerce/shops/{shopId}/products/{productId}/detailed
Access Level: ๐ Protected (Requires shop owner or system admin role)
Authentication: Bearer Token
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| shopId | UUID | Yes | ID of the shop |
| productId | UUID | Yes | ID of the product |
Response JSON Sample (ProductDetailedResponse):
{
"success": true,
"message": "Product details retrieved successfully",
"data": {
"productId": "456e7890-e89b-12d3-a456-426614174001",
"productName": "iPhone 15 Pro Max 512GB",
"productType": "PHYSICAL",
"productSlug": "iphone-15-pro-max-512gb",
"productDescription": "The most advanced iPhone ever...",
"productMedia": [
{
"fileId": "f11e4567-e89b-12d3-a456-426614174111",
"mediaType": "IMAGE",
"status": "READY",
"variants": {
"original": "https://cdn.example.com/products/f11e4567/original.jpg",
"thumb": "https://cdn.example.com/products/f11e4567/thumb.jpg"
},
"mimeType": "image/jpeg",
"order": 0
}
],
"price": 1199.00,
"comparePrice": 1299.00,
"discountPercentage": 7.69,
"isOnSale": true,
"stockQuantity": 25,
"lowStockThreshold": 5,
"isInStock": true,
"isLowStock": false,
"stockInfo": {
"stockTotal": 25,
"stockHeld": 2,
"stockAvailable": 23,
"soldCount": 140,
"holdBreakdown": {
"heldForCheckout": 1,
"heldForGroupPurchase": 1,
"heldForInstallment": 0,
"nearestCheckoutExpiry": "2026-07-08T16:00:00"
},
"showStockAvailableToPublic": true,
"showSoldCountToPublic": true
},
"condition": "NEW",
"status": "ACTIVE",
"urgencyTag": "NONE",
"shopId": "123e4567-e89b-12d3-a456-426614174000",
"shopName": "TechStore Pro",
"shopSlug": "techstore-pro",
"categoryId": "789e0123-e89b-12d3-a456-426614174002",
"categoryName": "Smartphones",
"createdAt": "2026-05-19T10:30:00",
"updatedAt": "2026-05-19T14:30:00",
"createdBy": "111e4567-e89b-12d3-a456-426614174aaa",
"editedBy": "111e4567-e89b-12d3-a456-426614174aaa",
"specifications": {
"Display": "6.7-inch Super Retina XDR OLED",
"Chip": "A17 Pro"
},
"hasSpecifications": true,
"specificationCount": 2,
"colors": [
{
"name": "Natural Titanium",
"hex": "#F5F5DC",
"images": ["f33e4567-e89b-12d3-a456-426614174333"],
"priceAdjustment": 0.00,
"finalPrice": 1199.00,
"hasExtraFee": false,
"extraFeeReason": null
}
],
"hasMultipleColors": false,
"colorCount": 1,
"priceRange": {
"minPrice": 1199.00,
"maxPrice": 1199.00,
"priceStartsFrom": 1199.00,
"hasPriceVariations": false
},
"orderingLimits": {
"minOrderQuantity": 1,
"maxOrderQuantity": 3,
"canOrderQuantity": 3,
"maxAllowedQuantity": 3,
"hasOrderingLimits": true
},
"groupBuying": {
"isEnabled": true,
"maxGroupSize": 50,
"groupPrice": 1099.00,
"groupDiscount": 100.00,
"groupDiscountPercentage": 8.34,
"timeLimitHours": 72,
"canJoinGroup": true
},
"installmentOptions": {
"isEnabled": true,
"isAvailable": true,
"downPaymentRequired": true,
"minDownPaymentPercentage": 20.00,
"plans": [],
"eligibilityStatus": "ELIGIBLE",
"creditCheckRequired": false
},
"purchaseOptions": {
"canBuyNow": true,
"canJoinGroup": true,
"canPayInstallment": true,
"recommendedOption": "GROUP_BUYING",
"bestDeal": { "option": "GROUP_BUYING", "savings": 100.00, "finalPrice": 1099.00 }
},
"preview": {
"type": null,
"variants": { "thumb": "https://cdn.example.com/preview/f66e4567/thumb.jpg" },
"downloadable": false
}
}
}
Notes on media/preview in this response:
productMediaitems may bestatus: "PROCESSING"withvariants: nullright after creation โ poll this endpoint (or re-fetch) untilstatusflips to"READY"colors[].imagescurrently holds the raw FileThunder file ID strings supplied at create/update time, not resolved URLs โ see the "known gap" note at the top of this docpreview.typeis alwaysnullhere regardless of whether a preview exists โ it's wired to a legacy field the current preview pipeline never sets. UseGET .../products/{productId}/preview(17b) for the authoritative previewcontext/status/variants
Error Responses:
401: Authentication required403: Insufficient permissions404: Shop or product not found
7. Get Shop Products (Management View)
Purpose: Retrieves all products for a shop with summary statistics.
Endpoint: GET api/v1/e-commerce/shops/{shopId}/products/all
Access Level: ๐ Protected (Requires shop owner or system admin role)
Authentication: Bearer Token
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| shopId | UUID | Yes | ID of the shop |
Response JSON Sample:
{
"success": true,
"message": "Retrieved 47 products from shop: TechStore Pro",
"data": {
"shop": {
"shopId": "123e4567-e89b-12d3-a456-426614174000",
"shopName": "TechStore Pro",
"isVerified": true,
"isMyShop": true
},
"summary": {
"totalProducts": 47,
"activeProducts": 35,
"draftProducts": 8,
"outOfStockProducts": 4,
"lowStockProducts": 6,
"productsWithGroupBuying": 18,
"productsWithInstallments": 25
},
"products": [
{
"productId": "456e7890-e89b-12d3-a456-426614174001",
"productName": "iPhone 15 Pro Max 512GB",
"productType": "PHYSICAL",
"productSlug": "iphone-15-pro-max-512gb",
"productPrimaryMedia": {
"fileId": "f11e4567-e89b-12d3-a456-426614174111",
"mediaType": "IMAGE",
"status": "READY",
"thumbUrl": "https://cdn.example.com/products/f11e4567/thumb.jpg",
"mimeType": "image/jpeg"
},
"price": 1199.00,
"stockQuantity": 25,
"status": "ACTIVE",
"isInStock": true,
"hasGroupBuying": true,
"hasInstallments": true,
"createdAt": "2026-05-19T10:30:00Z"
}
],
"totalProducts": 47
}
}
Note: each product card is a ProductSummaryResponse โ productPrimaryMedia is a PrimaryMediaResponse (fileId, mediaType, status, thumbUrl, mimeType), or null if no media was uploaded at all. It resolves to the product's first IMAGE item; if the product has no image (e.g. video-only), it falls back to that video's poster/thumb frame instead โ so mediaType here can be VIDEO even though thumbUrl is a still image. thumbUrl is only populated once the underlying file's status is READY; while PROCESSING, productPrimaryMedia still comes back (so the client can show a placeholder) but thumbUrl is null. Every response type that surfaces a card thumbnail (ProductSummaryResponse, MarketplaceProductResponse, cart/wishlist items, installment plan's ProductBasicInfo) uses this same shape. Similarly, the accompanying shop logo is always ShopLogoPrimaryMedia (fileId, status, thumbUrl, mimeType) under the field name shopLogoMedia in these same list/card responses โ full ShopMedia (with the raw variants map) is reserved for single-entity detail views only (ShopResponse, ProductPublicResponse).
Error Responses:
401: Authentication required403: Insufficient permissions404: Shop not found
8. Get Shop Products Paginated (Management View)
Purpose: Retrieves shop products with pagination for management dashboard.
Endpoint: GET api/v1/e-commerce/shops/{shopId}/products/all-paged
Access Level: ๐ Protected (Requires shop owner or system admin role)
Authentication: Bearer Token
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| shopId | UUID | Yes | ID of the shop |
Query Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| page | integer | No | 1 | Page number (1-indexed) |
| size | integer | No | 10 | Items per page (max: 100) |
Response JSON Sample:
{
"success": true,
"message": "Retrieved 10 products from shop: TechStore Pro (Page 1 of 5)",
"data": {
"contents": { "...same structure as /all..." },
"currentPage": 1,
"pageSize": 10,
"totalElements": 47,
"totalPages": 5,
"hasNext": true,
"hasPrevious": false
}
}
Error Responses:
401: Authentication required403: Insufficient permissions404: Shop not found
9. Get Public Product by ID
Purpose: Retrieves a single active product for public viewing.
Endpoint: GET api/v1/e-commerce/shops/{shopId}/products/{productId}
Access Level: ๐ Public
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| shopId | UUID | Yes | Shop must be active and approved |
| productId | UUID | Yes | Product must be active |
Response JSON Sample (ProductPublicResponse):
{
"success": true,
"message": "Product retrieved successfully",
"data": {
"productId": "456e7890-e89b-12d3-a456-426614174001",
"productName": "iPhone 15 Pro Max 512GB",
"productSlug": "iphone-15-pro-max-512gb",
"productDescription": "The most advanced iPhone ever...",
"productMedia": [
{
"fileId": "f11e4567-e89b-12d3-a456-426614174111",
"mediaType": "IMAGE",
"status": "READY",
"variants": {
"original": "https://cdn.example.com/products/f11e4567/original.jpg",
"thumb": "https://cdn.example.com/products/f11e4567/thumb.jpg"
},
"mimeType": "image/jpeg",
"order": 0
}
],
"price": 1199.00,
"comparePrice": 1299.00,
"discountPercentage": 7.69,
"isOnSale": true,
"groupPurchasePrice": 1099.00,
"isInStock": true,
"isLowStock": false,
"stockInfo": {
"stockAvailable": 23,
"soldCount": 140
},
"condition": "NEW",
"urgencyTag": "NONE",
"shopId": "123e4567-e89b-12d3-a456-426614174000",
"shopName": "TechStore Pro",
"shopSlug": "techstore-pro",
"shopLogoMedia": {
"fileId": "s11e4567-e89b-12d3-a456-426614174001",
"status": "READY",
"variants": { "thumb": "https://cdn.example.com/shops/s11e4567/thumb.jpg" },
"mimeType": "image/png"
},
"categoryId": "789e0123-e89b-12d3-a456-426614174002",
"categoryName": "Smartphones",
"specifications": { "Display": "6.7-inch OLED" },
"hasSpecifications": true,
"colors": [
{
"name": "Natural Titanium",
"hex": "#F5F5DC",
"images": ["f33e4567-e89b-12d3-a456-426614174333"],
"priceAdjustment": 0.00,
"finalPrice": 1199.00
}
],
"hasMultipleColors": false,
"priceRange": {
"minPrice": 1199.00,
"maxPrice": 1199.00,
"hasPriceVariations": false
},
"groupBuying": {
"isAvailable": true,
"maxGroupSize": 50,
"groupPrice": 1099.00,
"groupDiscount": 100.00,
"groupDiscountPercentage": 8.34,
"timeLimitHours": 72
},
"installmentOptions": {
"isAvailable": true,
"downPaymentRequired": true,
"minDownPaymentPercentage": 20.00,
"plans": [
{
"installmentPlanId": "aa1e4567-e89b-12d3-a456-426614174aaa",
"duration": 6,
"interval": "MONTHLY",
"interestRate": 0.00,
"description": "6-Month Interest-Free"
}
]
},
"previewType": null,
"previewUrl": null,
"previewDownloadable": true,
"isInCart": null,
"cartQuantity": null,
"isInWishlist": null,
"wishlistItemId": null,
"wishlistGroupId": null,
"wishlistGroupName": null,
"createdAt": "2026-05-19T10:30:00"
}
}
Notes:
previewTypeandpreviewUrlare wired to legacy fields that the current preview pipeline never populates โ they will always benull, even when the product has an active, ready preview. There is currently no public field exposing the resolved preview media/variants;previewDownloadableis the one real preview field here. To surface preview media publicly, the frontend must call the seller-facingGET .../previewendpoint (17b) โ note that endpoint currently requires shop-owner/admin auth, so a fully public preview-viewing path doesn't exist yetcolors[].imagesare raw FileThunder file ID strings, not resolved URLs (same gap as the detailed response)isInCart/cartQuantity/isInWishlist/wishlistItemId/wishlistGroupId/wishlistGroupNameare only populated on this single-product view (not on list responses like #10/#11), and only when the request is authenticated
Error Responses:
404: Shop not found/not approved, or product not found/not active
10. Get Public Shop Products
Purpose: Retrieves all active products from a shop for public browsing.
Endpoint: GET api/v1/e-commerce/shops/{shopId}/products/public-view/all
Access Level: ๐ Public
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| shopId | UUID | Yes | Shop must be active and approved |
Response JSON Sample:
{
"success": true,
"message": "Retrieved 23 products from TechStore Pro",
"data": {
"shop": {
"shopId": "123e4567-e89b-12d3-a456-426614174000",
"shopName": "TechStore Pro",
"isVerified": true
},
"products": [
{
"productId": "456e7890-e89b-12d3-a456-426614174001",
"productName": "iPhone 15 Pro Max",
"productPrimaryMedia": {
"fileId": "f11e4567-e89b-12d3-a456-426614174111",
"mediaType": "IMAGE",
"status": "READY",
"thumbUrl": "https://cdn.example.com/products/f11e4567/thumb.jpg",
"mimeType": "image/jpeg"
},
"price": 1199.00,
"isOnSale": true,
"isInStock": true,
"hasGroupBuying": true,
"hasInstallments": true
}
],
"totalProducts": 23
}
}
Error Responses:
404: Shop not found, not approved, or not active
11. Get Public Shop Products Paginated
Purpose: Retrieves active products from a shop with pagination for public browsing.
Endpoint: GET api/v1/e-commerce/shops/{shopId}/products/public-view/all-paged
Access Level: ๐ Public
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| shopId | UUID | Yes | Shop must be active and approved |
Query Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| page | integer | No | 1 | Page number (1-indexed) |
| size | integer | No | 10 | Items per page (max: 50) |
Error Responses:
404: Shop not found, not approved, or not active
12. Search Products
Purpose: Searches products within a shop using multi-word query matching across multiple fields.
Endpoint: GET api/v1/e-commerce/shops/{shopId}/products/search
Access Level: ๐ Public (Enhanced features for authenticated users)
Authentication: Optional Bearer Token
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| shopId | UUID | Yes | ID of the shop |
Query Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| q | string | Yes | โ | Search query (min: 2, max: 100 chars) |
| status | ProductStatus[] | No | ACTIVE | Statuses to search (owners/admins only for non-ACTIVE) |
| page | integer | No | 1 | Page number |
| size | integer | No | 10 | Items per page (max: 50) |
| sortBy | string | No | relevance | relevance, createdAt, updatedAt, productName, price, stockQuantity, brand |
| sortDir | string | No | desc | asc or desc |
Search Behavior:
| Feature | Description |
|---|---|
| Multi-word | Searches for products containing ALL words |
| Partial match | "iph" matches "iPhone" |
| Cross-field | Matches against name, description, brand, tags, specifications |
| Case-insensitive | "APPLE" matches "apple" |
User Access:
| User Type | Searchable Statuses |
|---|---|
| Public / Authenticated | ACTIVE only |
| Shop Owner / Admin | All statuses |
Response JSON Sample:
{
"success": true,
"message": "Found 12 products matching 'iphone'",
"data": {
"contents": {
"shop": { "shopId": "...", "shopName": "TechStore Pro" },
"products": [ { "...product summary fields..." } ],
"totalProducts": 12,
"searchMetadata": {
"searchQuery": "iphone",
"searchedStatuses": ["ACTIVE"],
"userType": "PUBLIC"
}
},
"currentPage": 1,
"pageSize": 10,
"totalElements": 12,
"totalPages": 2,
"hasNext": true,
"hasPrevious": false
}
}
Error Responses:
400: Query too short or too long404: Shop not found or not accessible
13. Advanced Product Filter
Purpose: Filters products using multiple criteria with combined AND/OR logic.
Endpoint: GET api/v1/e-commerce/shops/{shopId}/products/advanced-filter
Access Level: ๐ Public (Enhanced features for authenticated users)
Authentication: Optional Bearer Token
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| shopId | UUID | Yes | ID of the shop |
Query Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| minPrice | decimal | No | โ | Minimum price |
| maxPrice | decimal | No | โ | Maximum price (must be โฅ minPrice) |
| condition | ProductCondition | No | โ | Condition filter |
| categoryId | UUID | No | โ | Category filter |
| inStock | boolean | No | โ | Filter by availability |
| onSale | boolean | No | โ | Filter by sale status |
| hasGroupBuying | boolean | No | โ | Filter by group buying |
| hasInstallments | boolean | No | โ | Filter by installments |
| hasMultipleColors | boolean | No | โ | Filter by color variations |
| status | ProductStatus[] | No | ACTIVE | Status filter (owners/admins for non-ACTIVE) |
| page | integer | No | 1 | Page number |
| size | integer | No | 10 | Items per page (max: 50) |
| sortBy | string | No | createdAt | createdAt, updatedAt, productName, price, stockQuantity |
| sortDir | string | No | desc | asc or desc |
Filter Logic:
| Filter Type | Logic |
|---|---|
| Price range | AND (minPrice AND maxPrice) |
| Feature flags | AND (all must match) |
| Multiple statuses | OR |
Error Responses:
400: Invalid filter values or price range error404: Shop or category not found
14. Get Public Product by Slug
Purpose: Retrieves a single active product by its slug (same response shape as Get Public Product by ID).
Endpoint: GET api/v1/e-commerce/shops/{shopId}/products/find-by-slug/{slug}
Access Level: ๐ Public
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| shopId | UUID | Yes | Shop must be active and approved |
| slug | string | Yes | Product slug |
Error Responses:
404: Shop not found/not approved, or product not found/not active
15. Installment Plan Config
Purpose: CRUD for installment plans attached to a product. Plans are created separately after the product, and linked to it by productId.
Base URL: api/v1/e-commerce/products/{shopId}/{productId}/installment-plans
Access Level: ๐ Protected (Requires shop owner or system admin role)
Authentication: Bearer Token
Path Parameters (shared):
| Parameter | Type | Required | Description |
|---|---|---|---|
| shopId | UUID | Yes | ID of the shop |
| productId | UUID | Yes | ID of the product |
15a. Create Installment Plan
Endpoint: POST api/v1/e-commerce/products/{shopId}/{productId}/installment-plans
Request Body:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| planName | string | Yes | Display name for the plan | Min: 3, Max: 100 chars |
| paymentFrequency | PaymentFrequency | Yes | Payment interval | DAILY, WEEKLY, BI_WEEKLY, SEMI_MONTHLY, MONTHLY, QUARTERLY, CUSTOM_DAYS |
| customFrequencyDays | integer | Conditional | Days between payments | Required when paymentFrequency=CUSTOM_DAYS, min: 1 |
| numberOfPayments | integer | Yes | Total number of payments | Min: 2, Max: 120 |
| apr | decimal | Yes | Annual percentage rate | Min: 0.0, Max: 36.0, 2 decimal places |
| minDownPaymentPercent | integer | Yes | Minimum down payment % | Min: 10, Max: 50 |
| fulfillmentTiming | FulfillmentTiming | Yes | When to ship | IMMEDIATE (ship after down payment), AFTER_PAYMENT (layaway โ ship after final payment) |
| displayOrder | integer | No | Sort order in UI | Min: 0, Default: 0 |
| isFeatured | boolean | No | Highlight as recommended plan | Default: false |
| isActive | boolean | No | Plan is available to buyers | Default: true |
Request JSON Sample:
{
"planName": "6-Month Interest-Free",
"paymentFrequency": "MONTHLY",
"numberOfPayments": 6,
"apr": 0.00,
"minDownPaymentPercent": 20,
"fulfillmentTiming": "IMMEDIATE",
"displayOrder": 1,
"isFeatured": true,
"isActive": true
}
Error Responses:
400: Validation errors404: Shop or product not found
15b. Get All Installment Plans
Endpoint: GET api/v1/e-commerce/products/{shopId}/{productId}/installment-plans
Returns a list of all installment plans for the product.
Error Responses:
404: Shop or product not found
15c. Get Installment Plan by ID
Endpoint: GET api/v1/e-commerce/products/{shopId}/{productId}/installment-plans/{planId}
Additional Path Parameter:
| Parameter | Type | Description |
|---|---|---|
| planId | UUID | ID of the installment plan |
15d. Update Installment Plan
Endpoint: PUT api/v1/e-commerce/products/{shopId}/{productId}/installment-plans/{planId}
All fields are optional โ only provided fields are updated. Same field structure as Create.
15e. Delete Installment Plan
Endpoint: DELETE api/v1/e-commerce/products/{shopId}/{productId}/installment-plans/{planId}
15f. Activate / Deactivate Plan
Activate: PATCH api/v1/e-commerce/products/{shopId}/{productId}/installment-plans/{planId}/activate
Deactivate: PATCH api/v1/e-commerce/products/{shopId}/{productId}/installment-plans/{planId}/deactivate
Toggles isActive on the plan without changing any other fields.
15g. Set Featured Plan
Endpoint: PATCH api/v1/e-commerce/products/{shopId}/{productId}/installment-plans/{planId}/set-featured
Marks the specified plan as the featured (recommended) plan for this product.
16. Digital File Management
Purpose: Manages the single downloadable file for a DIGITAL product. The client uploads through the generic FileThunder pipeline first (see the file/media note at the top of this doc), then registers the resulting ftFileId with the product here. There is exactly one file per product โ registering a new ftFileId replaces the current pending/active file.
Base URL: api/v1/e-commerce/shops/{shopId}/products/{productId}/digital-file
Access Level: ๐ Protected (Requires shop owner or system admin role)
Authentication: Bearer Token
Path Parameters (shared):
| Parameter | Type | Required | Description |
|---|---|---|---|
| shopId | UUID | Yes | ID of the shop |
| productId | UUID | Yes | ID of the digital product |
16a. Register File
Purpose: Links a FileThunder file (already uploaded with context DIGITAL_PRODUCT via POST api/v1/files/request-upload) to this product. The file starts in a "pending" shadow state and is promoted to the live/active file once FileThunder finishes virus-scanning it (webhook-driven) โ or immediately, if it was already READY by the time this call lands (race-condition-safe).
Endpoint: POST api/v1/e-commerce/shops/{shopId}/products/{productId}/digital-file/register
Request Body:
| Parameter | Type | Required | Description |
|---|---|---|---|
| ftFileId | UUID | Yes | FileThunder file ID from POST api/v1/files/request-upload (context DIGITAL_PRODUCT), owned by the caller |
Request JSON Sample:
{
"ftFileId": "f55e4567-e89b-12d3-a456-426614174555"
}
Response JSON Sample (DigitalFileResponse):
{
"success": true,
"message": "File registered โ scanning in progress",
"data": {
"productId": "456e7890-e89b-12d3-a456-426614174001",
"fileName": null,
"contentType": null,
"fileSize": null,
"fileVersion": 0,
"status": null,
"uploadedAt": null,
"pending": {
"status": "PROCESSING",
"failureReason": null,
"uploadedAt": "2026-07-07T10:45:00"
}
}
}
Upload Flow:
POST api/v1/files/request-uploadwithcontext: "DIGITAL_PRODUCT"โ receiveuploadUrlandfileIdPUT {uploadUrl}with the raw file bytes (client-to-storage, not through this API)POST .../digital-file/registerwith{ "ftFileId": fileId }โ do this immediately, you don't need to wait for the upload/scan to finish- Poll
GET .../digital-fileuntilstatus(orpending.status) isREADY;FAILEDmeans re-upload and register again
Notes:
- The top-level
fileName/contentType/fileSize/status/uploadedAtdescribe the active (already-promoted) file;pendingdescribes a file that was just registered and hasn't finished processing/replacing the active one yet. Both can be non-null at once while a replacement is in flight - Can also happen implicitly during Create Product via the
digitalFileIdfield โ same underlying registration logic
Error Responses:
400: File not found in Redis (upload expired/never started), or file does not belong to the caller401: Authentication required403: Insufficient permissions, or product is notDIGITAL404: Shop or product not found
16b. Get Product File
Purpose: Retrieves the current state of the product's digital file (active + any pending replacement).
Endpoint: GET api/v1/e-commerce/shops/{shopId}/products/{productId}/digital-file
Returns the same DigitalFileResponse shape as 16a.
Error Responses:
401: Authentication required403: Insufficient permissions404: Shop/product not found, or no digital file has ever been registered for this product
16c. Delete File
Purpose: Removes the digital file record from the product entirely (both active and pending state).
Endpoint: DELETE api/v1/e-commerce/shops/{shopId}/products/{productId}/digital-file
Response JSON Sample:
{
"success": true,
"message": "Digital file removed",
"data": null
}
Error Responses:
401: Authentication required403: Insufficient permissions404: Shop/product not found, or no digital file exists for this product
17. Product Preview Management
Purpose: Manages a single preview file per product โ a teaser shown to buyers before purchase (works for any productType, not just DIGITAL). Distinct from the private Digital File. Supports VIDEO, PDF, 3D model, and IMAGE previews. Like all product media, previews go through the generic FileThunder upload pipeline (see the note at the top of this doc) โ there is no preview-specific presign/confirm flow anymore.
Base URL: api/v1/e-commerce/shops/{shopId}/products/{productId}/preview
Access Level: ๐ Protected (Requires shop owner or system admin role) โ except 17d, which only requires authentication (any buyer)
Authentication: Bearer Token
Path Parameters (shared):
| Parameter | Type | Required | Description |
|---|---|---|---|
| shopId | UUID | Yes | ID of the shop |
| productId | UUID | Yes | ID of the product (any type โ PHYSICAL or DIGITAL) |
17a. Register Preview
Purpose: Links a FileThunder file (already uploaded via POST api/v1/files/request-upload with context PRODUCT_PREVIEW_IMAGE, PRODUCT_PREVIEW_VIDEO, or PRODUCT_PREVIEW_DOCUMENT) to this product as its preview. The preview's context/type is inferred server-side from the file's MIME type โ there is no client-supplied previewType anymore. If the product already has a preview, it's replaced. Processing is async and webhook-driven, with the same immediate-promotion race-condition guard as the digital file.
Endpoint: POST api/v1/e-commerce/shops/{shopId}/products/{productId}/preview/register
Request Body (RegisterPreviewRequest):
| Parameter | Type | Required | Description |
|---|---|---|---|
| ftFileId | UUID | Yes | FileThunder file ID, owned by the caller |
| previewDownloadable | boolean | No | Whether buyers can download the raw preview file itself. Default: false (view/stream only) |
Request JSON Sample:
{
"ftFileId": "f66e4567-e89b-12d3-a456-426614174666",
"previewDownloadable": false
}
Response JSON Sample (PreviewResponse):
{
"success": true,
"message": "Preview registered โ processing in background",
"data": {
"previewFtFileId": "f66e4567-e89b-12d3-a456-426614174666",
"previewContext": "PRODUCT_PREVIEW_VIDEO",
"previewStatus": "PROCESSING",
"previewFailureReason": null,
"previewVariants": null,
"previewDownloadable": false
}
}
Upload Flow:
POST api/v1/files/request-uploadwithcontext: "PRODUCT_PREVIEW_VIDEO"(or_IMAGE/_DOCUMENT) โ receiveuploadUrlandfileIdPUT {uploadUrl}with the raw file bytes (client-to-storage, not through this API)POST .../preview/registerwith{ "ftFileId": fileId }- Poll
GET .../preview(17b) untilpreviewStatusisREADYโpreviewVariantswill then hold the resolved, playable CDN URLs (image/video previews only; documents have no variants โ see 17d)
Can also happen implicitly during Create Product via the previewFileId/previewDownloadable fields (DIGITAL products only there) โ same underlying registration logic.
Error Responses:
400: File not found in Redis (upload expired/never started), or file does not belong to the caller401: Authentication required403: Insufficient permissions404: Shop or product not found
17b. Get Preview
Purpose: Retrieves the current preview state for the product.
Endpoint: GET api/v1/e-commerce/shops/{shopId}/products/{productId}/preview
Returns the same PreviewResponse shape as 17a. All fields are null if no preview has ever been registered.
Error Responses:
401: Authentication required403: Insufficient permissions404: Shop or product not found
17c. Remove Preview
Purpose: Fully clears the product's preview โ previewFtFileId, previewContext, previewStatus, previewFailureReason, previewVariants, and previewDownloadable are all reset. This is the only way to actually clear a preview (the clearPreview flag on Update Product does not do this โ see the note there).
Endpoint: DELETE api/v1/e-commerce/shops/{shopId}/products/{productId}/preview
Response JSON Sample:
{
"success": true,
"message": "Preview removed from product",
"data": null
}
Error Responses:
401: Authentication required403: Insufficient permissions404: Shop or product not found
17d. Get Preview Document URL
Purpose: For document previews only (previewContext = PRODUCT_PREVIEW_DOCUMENT, e.g. a PDF) โ generates a short-lived, direct download URL. Documents don't get resolved variants like images/videos do, so this is the only way to actually fetch a document preview's bytes. Access differs from the rest of this section: any authenticated user can call it (no shop-owner/admin check) โ it's meant for buyers evaluating the product.
Endpoint: GET api/v1/e-commerce/shops/{shopId}/products/{productId}/preview/document-url
Response JSON Sample (PreviewDocumentUrlResponse):
{
"success": true,
"message": "Preview document URL generated โ valid for 15 minutes",
"data": {
"url": "https://cdn.example.com/preview/f66e4567/sample.pdf?X-Amz-Signature=...",
"expiresInSeconds": 900
}
}
Error Responses:
400: No preview registered, preview is not a document, preview is notREADYyet, orpreviewDownloadableisfalsefor this product401: Authentication required404: Product not found
Quick Reference
Common HTTP Status Codes
| Code | Meaning |
|---|---|
200 OK |
Successful GET/PUT/PATCH |
201 Created |
Successful POST (resource created) |
400 Bad Request |
Invalid data, validation errors, business rule violations |
401 Unauthorized |
Authentication required or invalid token |
403 Forbidden |
Insufficient permissions |
404 Not Found |
Resource not found or not accessible |
409 Conflict |
Duplicate product name or business constraint violation |
422 Unprocessable Entity |
Field-level validation errors |
500 Internal Server Error |
Server error |
User Access Levels
| User Type | Product Management | Status Access |
|---|---|---|
| Public | View active products only | ACTIVE only |
| Authenticated | View active products only | ACTIVE only |
| Shop Owner | Full CRUD on own shop | All statuses |
| System Admin | Full CRUD on all shops | All statuses |
Product Type Fulfillment Flows
| Type | Flow |
|---|---|
PHYSICAL |
Payment โ PENDING_SHIPMENT โ Seller ships โ Buyer confirms with 6-digit code โ Escrow releases โ COMPLETED |
DIGITAL |
Payment โ COMPLETED immediately โ Escrow released โ DigitalDownloadAccess records created โ Buyer downloads |
Product Status Lifecycle
DRAFT โ ACTIVE โ INACTIVE โ ARCHIVED
โ โ
โโโโโโโโโโ RESTORE โโโโโโโโโโโ
OUT_OF_STOCK โโ ACTIVE (automatic based on inventory)
| Status | Public Visibility | Available Actions |
|---|---|---|
DRAFT |
Hidden | Edit, Publish, Hard Delete |
ACTIVE |
Visible | Edit, Deactivate, Soft Delete |
INACTIVE |
Hidden | Edit, Activate, Soft Delete |
OUT_OF_STOCK |
Visible (out of stock badge) | Restock (auto-activates) |
ARCHIVED |
Hidden | Restore |
Enums Reference
ProductType: PHYSICAL, DIGITAL
PreviewType: VIDEO, PDF, THREE_D, IMAGE โ legacy enum, no longer populated by the preview pipeline (always null in responses). The real preview media type is inferred from FileThunder's FtMediaContext: PRODUCT_PREVIEW_IMAGE, PRODUCT_PREVIEW_VIDEO, PRODUCT_PREVIEW_DOCUMENT (exposed as previewContext โ see 17b)
FtFileStatus (file/media processing status โ mediaFileIds, digital file, and preview all use this): PENDING, UPLOADING, UPLOADED, SCANNING, PROCESSING, LIVE_PARTIAL, READY, FAILED
ProductMediaType (top-level product media only): IMAGE, VIDEO
ProductCondition: NEW, USED_LIKE_NEW, USED_GOOD, USED_FAIR, REFURBISHED, FOR_PARTS
ReqAction: SAVE_DRAFT (โ DRAFT status), SAVE_PUBLISH (โ ACTIVE status)
UrgencyTag: NONE, NEW_ARRIVAL, LIMITED_EDITION, LIMITED_OFFER, FEW_REMAINS
PaymentFrequency: DAILY, WEEKLY, BI_WEEKLY, SEMI_MONTHLY, MONTHLY, QUARTERLY, CUSTOM_DAYS
FulfillmentTiming: IMMEDIATE (ship after down payment), AFTER_PAYMENT (layaway โ ship after final payment)
SKU Format
SHP[8-CHAR-UUID]-[CATEGORY-3]-[BRAND-3]-[ATTRIBUTE-3]-[SEQUENCE-4]
Example: SHP12345678-ELE-APP-512-0001
Data Format Standards
- Dates: ISO 8601 (
2026-05-19T14:30:00Z) - Prices: Decimal with 2 decimal places, stored as BigDecimal
- UUIDs: Standard UUID format
- Pagination: 1-indexed page parameter
- Colors: Hex format
#RRGGBB - Percentages: Decimal format (
20.00= 20%)
Error Response Format
{
"success": false,
"message": "Human-readable error message",
"error": {
"code": "ERROR_CODE",
"details": "Detailed information",
"field": "fieldName (if field-specific)",
"timestamp": "2026-05-19T14:30:00Z"
}
}
Product Creation Flow
0. POST /api/v1/files/request-upload (context=PRODUCT_IMAGE/PRODUCT_VIDEO, one call per file)
PUT {uploadUrl} (direct to storage, one per file)
โ repeat for the digital file (context=DIGITAL_PRODUCT) and preview (context=PRODUCT_PREVIEW_*) if needed
โ collect the resulting fileIds; no need to wait for processing to finish
1. POST /shops/{shopId}/products?action=SAVE_DRAFT
body includes mediaFileIds, colors[].imageFileIds, digitalFileId, previewFileId
โ create the product shell; product/digital-file/preview media all start as PROCESSING
and flip to READY asynchronously once FileThunder finishes scanning/transcoding
2. POST /products/{shopId}/{productId}/installment-plans
โ add installment plans (if installmentEnabled)
3. (optional, if not supplied at create, or to replace later)
POST /shops/{shopId}/products/{productId}/digital-file/register { "ftFileId": ... }
โ attach/replace the private digital file (DIGITAL products only)
4. (optional, if not supplied at create, or to replace later)
POST /shops/{shopId}/products/{productId}/preview/register { "ftFileId": ..., "previewDownloadable": false }
โ attach/replace the preview teaser (any product type)
5. PATCH /shops/{shopId}/products/{productId}/publish
โ publish when ready
Preview vs Digital File
| Aspect | Preview | Digital File |
|---|---|---|
| Who sees it | Everyone (before purchase) | Buyers only (after payment) |
| Upload context | PRODUCT_PREVIEW_IMAGE/_VIDEO/_DOCUMENT |
DIGITAL_PRODUCT |
| Access | previewDownloadable gates raw download; documents get a 15-min presigned URL via 17d |
Buyer-only download, expires per downloadExpiryDays (see order/download endpoints) |
| Count | One per product | One per product |
| Products | PHYSICAL or DIGITAL | DIGITAL only |
| Registration endpoint | POST .../preview/register |
POST .../digital-file/register |
| Purpose | Teaser/sample before buying | Actual purchased content |
Wishlist Management
Base URL: {base_url}/api/v1/e-commerce/wishlist
Short Description: The Wishlist API lets authenticated users save products for later, organize them into named groups, transfer items between groups, and move items directly to the cart. Every wishlist operation is strictly private โ users can only read and modify their own wishlist.
Hints:
- All endpoints require a valid JWT Bearer token โ there are no public wishlist endpoints
- When adding a product, pass either
groupId(existing group) orgroupName(creates a new group) โ passing both returns400 - Group names are unique per user โ attempting to create a duplicate name returns
400 - Deleting a group with
?deleteProducts=false(default) moves all items in that group to Ungrouped;?deleteProducts=truepermanently removes those items from the wishlist PATCH /{itemId}/groupwithgroupId: nullmoves an item to Ungrouped without deleting itisInWishlist,wishlistItemId,wishlistGroupId, andwishlistGroupNameare populated on the single product detail response (GET /api/v1/e-commerce/products/{slug}) for authenticated users
Standard Response Format
Success Response Structure
{
"success": true,
"httpStatus": "OK",
"message": "Operation completed successfully",
"action_time": "2025-09-23T10:30:45",
"data": {}
}
Error Response Structure
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Error description",
"action_time": "2025-09-23T10:30:45",
"data": "Error description"
}
Standard Response Fields
| Field | Type | Description |
|---|---|---|
success |
boolean | true for successful operations, false for errors |
httpStatus |
string | HTTP status name (OK, BAD_REQUEST, NOT_FOUND, etc.) |
message |
string | Human-readable message describing the result |
action_time |
string | ISO 8601 timestamp of when the response was generated |
data |
object/string | Response payload on success, error details on failure |
Standard Error Types
400 BAD_REQUEST: Business rule violation (duplicate product, duplicate group name, ambiguous group fields)401 UNAUTHORIZED: Missing, expired, or invalid JWT token404 NOT_FOUND: Product, wishlist item, or group not found422 UNPROCESSABLE_ENTITY: Validation errors with field-level detail500 INTERNAL_SERVER_ERROR: Unexpected server error
Endpoints
1. Add Product to Wishlist
Purpose: Adds a product to the authenticated user's wishlist. Optionally places it in an existing group (via groupId) or creates a new group on the fly (via groupName). If neither is provided the item lands in Ungrouped.
Endpoint: POST {base_url}/api/v1/e-commerce/wishlist/add
Access Level: ๐ Protected (Requires valid JWT)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
Authorization |
string | Yes | Bearer <token> |
Request JSON Sample โ add to Ungrouped (no group):
{
"productId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
Request JSON Sample โ add to an existing group:
{
"productId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"groupId": "f9e8d7c6-b5a4-3210-fedc-ba9876543210"
}
Request JSON Sample โ add and create a new group:
{
"productId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"groupName": "Birthday Gifts"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
productId |
UUID | Yes | ID of the product to add | Must be a valid, non-deleted product |
groupId |
UUID | No | Add to an existing group | Must belong to the authenticated user. Cannot be combined with groupName |
groupName |
string | No | Create a new group with this name and add the product to it | Must not already exist for this user. Cannot be combined with groupId |
Success Response JSON Sample โ added to Ungrouped:
{
"success": true,
"httpStatus": "OK",
"message": "Product added to wishlist successfully",
"action_time": "2026-06-04T14:22:10",
"data": null
}
Success Response JSON Sample โ added to existing group:
{
"success": true,
"httpStatus": "OK",
"message": "Product added to wishlist in group 'Electronics'",
"action_time": "2026-06-04T14:22:10",
"data": null
}
Success Response JSON Sample โ new group created and product added:
{
"success": true,
"httpStatus": "OK",
"message": "Product added to wishlist in group 'Birthday Gifts'",
"action_time": "2026-06-04T14:22:10",
"data": null
}
Success Response Fields:
| Field | Description |
|---|---|
message |
Confirms the product was added; includes group name when a group is involved |
Error Response JSON Sample:
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "'Sony WH-1000XM5' is already in your wishlist",
"action_time": "2026-06-04T14:22:10",
"data": "'Sony WH-1000XM5' is already in your wishlist"
}
2. Get Wishlist (Flat)
Purpose: Returns the authenticated user's full wishlist as a flat list. Each item includes its group ID and group name (or null if Ungrouped). Useful for list views where grouping is handled client-side.
Endpoint: GET {base_url}/api/v1/e-commerce/wishlist
Access Level: ๐ Protected (Requires valid JWT)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
Authorization |
string | Yes | Bearer <token> |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Wishlist retrieved successfully",
"action_time": "2026-06-04T14:22:10",
"data": {
"user": {
"userId": "uuid",
"userName": "josh_dev",
"name": "Josh Sakweli"
},
"wishlistSummary": {
"totalItems": 3,
"totalValue": 749.97,
"inStockItems": 2,
"outOfStockItems": 1
},
"wishlistItems": [
{
"wishlistId": "uuid",
"productId": "uuid",
"productName": "Sony WH-1000XM5",
"productSlug": "sony-wh-1000xm5",
"productPrimaryMedia": {
"fileId": "9c1a2b3d-4e5f-6789-abcd-ef0123456789",
"mediaType": "IMAGE",
"status": "READY",
"thumbUrl": "https://cdn.example.com/products/sony-wh-1000xm5/thumb.webp",
"mimeType": "image/webp"
},
"unitPrice": 349.99,
"isOnSale": false,
"shop": {
"shopId": "uuid",
"shopName": "Tech Haven",
"shopSlug": "tech-haven",
"shopLogoMedia": {
"fileId": "1d2e3f4a-5b6c-7890-defa-bc1234567890",
"status": "READY",
"thumbUrl": "https://cdn.example.com/shops/tech-haven/thumb.webp",
"mimeType": "image/jpeg"
}
},
"availability": {
"inStock": true,
"stockQuantity": 12
},
"groupId": "uuid",
"groupName": "Birthday Gifts",
"addedAt": "2026-06-04T10:00:00"
}
],
"updatedAt": "2026-06-04T10:00:00"
}
}
Success Response Fields:
| Field | Description |
|---|---|
user.userId |
UUID of the authenticated user |
user.userName |
System username |
user.name |
Full name |
wishlistSummary.totalItems |
Total number of items in the wishlist |
wishlistSummary.totalValue |
Sum of unit prices of all items |
wishlistSummary.inStockItems |
Count of items currently in stock |
wishlistSummary.outOfStockItems |
Count of items out of stock |
wishlistItems[].wishlistId |
UUID of the wishlist entry (used for remove/transfer) |
wishlistItems[].productId |
UUID of the product |
wishlistItems[].productName |
Product display name |
wishlistItems[].productSlug |
URL-friendly product identifier |
wishlistItems[].productPrimaryMedia |
Resolved primary media of the product โ {fileId, mediaType, status, thumbUrl, mimeType}. null if the product has no media yet |
wishlistItems[].unitPrice |
Current price of the product |
wishlistItems[].isOnSale |
Whether the product is currently on sale |
wishlistItems[].shop |
Shop that sells the product โ {shopId, shopName, shopSlug, shopLogoMedia}, where shopLogoMedia is {fileId, status, thumbUrl, mimeType} or null if the shop has no logo |
wishlistItems[].availability.inStock |
Whether the product is in stock |
wishlistItems[].availability.stockQuantity |
Current stock count |
wishlistItems[].groupId |
UUID of the group this item belongs to (null if Ungrouped) |
wishlistItems[].groupName |
Name of the group (null if Ungrouped) |
wishlistItems[].addedAt |
Timestamp when the product was added |
updatedAt |
Timestamp of the most recently added item |
3. Get Wishlist (Grouped)
Purpose: Returns the wishlist organized into sections โ one section per named group plus a separate Ungrouped section. Useful for grouped display views.
Endpoint: GET {base_url}/api/v1/e-commerce/wishlist/grouped
Access Level: ๐ Protected (Requires valid JWT)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
Authorization |
string | Yes | Bearer <token> |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Grouped wishlist retrieved successfully",
"action_time": "2026-06-04T14:22:10",
"data": {
"user": {
"userId": "uuid",
"userName": "josh_dev",
"name": "Josh Sakweli"
},
"wishlistSummary": {
"totalItems": 3,
"totalValue": 749.97,
"inStockItems": 2,
"outOfStockItems": 1
},
"groups": [
{
"groupId": "uuid",
"groupName": "Birthday Gifts",
"itemCount": 2,
"items": [ ]
}
],
"ungrouped": {
"groupId": null,
"groupName": "Ungrouped",
"itemCount": 1,
"items": [ ]
},
"updatedAt": "2026-06-04T10:00:00"
}
}
Success Response Fields:
| Field | Description |
|---|---|
user |
Same user summary as flat response |
wishlistSummary |
Same summary totals across all items |
groups |
Array of named group sections, ordered by creation date (oldest first) |
groups[].groupId |
UUID of the group |
groups[].groupName |
Name of the group |
groups[].itemCount |
Number of items in this group |
groups[].items |
Array of wishlist item responses (same shape as flat list items) |
ungrouped |
Section for items with no group assigned |
ungrouped.groupId |
Always null |
ungrouped.groupName |
Always "Ungrouped" |
ungrouped.itemCount |
Count of items with no group |
ungrouped.items |
Array of ungrouped wishlist items |
updatedAt |
Timestamp of the most recently added item |
4. Remove Item from Wishlist
Purpose: Permanently removes a specific item from the authenticated user's wishlist.
Endpoint: DELETE {base_url}/api/v1/e-commerce/wishlist/{itemId}
Access Level: ๐ Protected (Requires valid JWT โ owns the item)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
Authorization |
string | Yes | Bearer <token> |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
itemId |
UUID | Yes | The wishlistId of the item to remove |
Must belong to the authenticated user |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Product removed from wishlist successfully",
"action_time": "2026-06-04T14:22:10",
"data": null
}
5. Clear Wishlist
Purpose: Permanently removes all items from the authenticated user's wishlist. Groups are not deleted โ only the items inside them.
Endpoint: DELETE {base_url}/api/v1/e-commerce/wishlist/clear
Access Level: ๐ Protected (Requires valid JWT)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
Authorization |
string | Yes | Bearer <token> |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Wishlist cleared successfully",
"action_time": "2026-06-04T14:22:10",
"data": null
}
6. Move Item to Cart
Purpose: Adds a wishlist item to the user's cart at the specified quantity. The item remains in the wishlist โ it is not automatically removed.
Endpoint: POST {base_url}/api/v1/e-commerce/wishlist/move-to-cart/{itemId}
Access Level: ๐ Protected (Requires valid JWT)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
Authorization |
string | Yes | Bearer <token> |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
itemId |
UUID | Yes | The wishlistId of the item to move |
Must belong to the authenticated user |
Query Parameters:
| Parameter | Type | Required | Description | Validation | Default |
|---|---|---|---|---|---|
quantity |
integer | No | Quantity to add to cart | Min: 1 | 1 |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Product moved to cart successfully",
"action_time": "2026-06-04T14:22:10",
"data": null
}
7. Transfer Item to Group
Purpose: Moves a wishlist item to a different group, or removes it from its current group by setting groupId to null (moves to Ungrouped). The item stays in the wishlist โ only its group assignment changes.
Endpoint: PATCH {base_url}/api/v1/e-commerce/wishlist/{itemId}/group
Access Level: ๐ Protected (Requires valid JWT โ owns the item)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
Authorization |
string | Yes | Bearer <token> |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
itemId |
UUID | Yes | The wishlistId of the item to transfer |
Must belong to the authenticated user |
Request JSON Sample โ move to a group:
{
"groupId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
Request JSON Sample โ remove from group (move to Ungrouped):
{
"groupId": null
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
groupId |
UUID or null | Yes | Target group UUID, or null to move to Ungrouped |
When a UUID, must be a group that belongs to the authenticated user |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Item moved to group 'Electronics'",
"action_time": "2026-06-04T14:22:10",
"data": null
}
8. Create Group
Purpose: Creates a new named wishlist group for the authenticated user. Group names must be unique per user.
Endpoint: POST {base_url}/api/v1/e-commerce/wishlist/groups
Access Level: ๐ Protected (Requires valid JWT)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
Authorization |
string | Yes | Bearer <token> |
Request JSON Sample:
{
"name": "Electronics"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
name |
string | Yes | Display name for the group | Must not be blank. Must be unique for the authenticated user |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Wishlist group created successfully",
"action_time": "2026-06-04T14:22:10",
"data": {
"groupId": "uuid",
"name": "Electronics",
"itemCount": 0,
"createdAt": "2026-06-04T14:22:10"
}
}
Success Response Fields:
| Field | Description |
|---|---|
groupId |
UUID of the newly created group |
name |
Name of the group as saved |
itemCount |
Always 0 on creation |
createdAt |
Timestamp of group creation |
Error Response JSON Sample:
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "A group named 'Electronics' already exists",
"action_time": "2026-06-04T14:22:10",
"data": "A group named 'Electronics' already exists"
}
9. Get Groups
Purpose: Returns all wishlist groups created by the authenticated user, ordered by creation date (oldest first). Each group includes a live item count.
Endpoint: GET {base_url}/api/v1/e-commerce/wishlist/groups
Access Level: ๐ Protected (Requires valid JWT)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
Authorization |
string | Yes | Bearer <token> |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Wishlist groups retrieved successfully",
"action_time": "2026-06-04T14:22:10",
"data": [
{
"groupId": "uuid",
"name": "Birthday Gifts",
"itemCount": 3,
"createdAt": "2026-06-01T09:00:00"
},
{
"groupId": "uuid",
"name": "Electronics",
"itemCount": 1,
"createdAt": "2026-06-03T11:30:00"
}
]
}
Success Response Fields:
| Field | Description |
|---|---|
[].groupId |
UUID of the group |
[].name |
Display name of the group |
[].itemCount |
Current number of wishlist items in this group |
[].createdAt |
Timestamp of group creation |
10. Delete Group
Purpose: Deletes a wishlist group. Controls what happens to the items inside via the deleteProducts query parameter.
Endpoint: DELETE {base_url}/api/v1/e-commerce/wishlist/groups/{groupId}
Access Level: ๐ Protected (Requires valid JWT โ owns the group)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
Authorization |
string | Yes | Bearer <token> |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
groupId |
UUID | Yes | UUID of the group to delete | Must belong to the authenticated user |
Query Parameters:
| Parameter | Type | Required | Description | Validation | Default |
|---|---|---|---|---|---|
deleteProducts |
boolean | No | true โ permanently delete all items in the group from wishlist. false โ move items to Ungrouped, then delete group |
true or false |
false |
Success Response JSON Sample โ items moved to Ungrouped:
{
"success": true,
"httpStatus": "OK",
"message": "Group deleted, products moved to Ungrouped",
"action_time": "2026-06-04T14:22:10",
"data": null
}
Success Response JSON Sample โ items permanently deleted:
{
"success": true,
"httpStatus": "OK",
"message": "Group and all its products deleted from wishlist",
"action_time": "2026-06-04T14:22:10",
"data": null
}
Quick Reference
All Endpoints Summary
| # | Method | Path | Description |
|---|---|---|---|
| 1 | POST | /wishlist/add |
Add product to wishlist |
| 2 | GET | /wishlist |
Get flat wishlist |
| 3 | GET | /wishlist/grouped |
Get grouped wishlist |
| 4 | DELETE | /wishlist/{itemId} |
Remove item from wishlist |
| 5 | DELETE | /wishlist/clear |
Clear entire wishlist |
| 6 | POST | /wishlist/move-to-cart/{itemId} |
Move item to cart |
| 7 | PATCH | /wishlist/{itemId}/group |
Transfer item to group |
| 8 | POST | /wishlist/groups |
Create group |
| 9 | GET | /wishlist/groups |
Get all groups |
| 10 | DELETE | /wishlist/groups/{groupId} |
Delete group |
Group Behavior Rules
| Scenario | Behavior |
|---|---|
| Add with no group | Item lands in Ungrouped |
Add with groupId |
Item added to that existing group |
Add with groupName (new) |
New group created, item added to it |
Add with groupName (exists) |
400 โ duplicate group name |
Add with both groupId and groupName |
400 โ ambiguous request |
Delete group ?deleteProducts=false |
Items move to Ungrouped, group deleted |
Delete group ?deleteProducts=true |
Items permanently removed, group deleted |
Transfer item with groupId: null |
Item moved to Ungrouped |
NextGate Product Ecosystem โ Architecture & Purchase Flows
Overview
NextGate's e-commerce layer handles two fundamentally different kinds of products: physical products (tangible goods that require shipping and delivery confirmation) and digital products (files that a buyer downloads). Both types share the same payment infrastructure, checkout session system, financial rails, and inventory system โ but diverge completely at the fulfillment stage.
1. Product Types
Physical Products
A physical product has real-world stock. The platform tracks inventory, holds it during checkout, ships it through a seller, and releases escrow only after the buyer confirms physical receipt. The entire lifecycle can take days or weeks.
Digital Products
A digital product is a file (or collection of files) that the seller uploads to a private, access-controlled storage bucket. There is no shipping. There is no confirmation code. The moment payment clears, the buyer can download. Escrow releases immediately. The lifecycle is measured in seconds.
A single product can be either physical or digital โ never both. The productType field on the product record is the authoritative signal that drives every downstream decision.
2. Inventory โ Shared by Both Product Types
Key rule: Every product has a
stockQuantity. Digital and physical products are treated identically by the inventory system. The only difference is what happens after payment.
Every product โ physical or digital โ requires a stockQuantity set by the seller at creation time. The seller decides what that number is:
10 โ limited edition digital art, exclusive release
500 โ cohort-based course intake
1,000,000 โ effectively open โ seller still sets a real number
There is no "unlimited" mode and no trackInventory toggle. All inventory mechanics apply to both product types without exception:
- stockQuantity check on add-to-cart
- Inventory hold at checkout session creation
- Hold released on session expiry or cancellation
- stockQuantity decremented on successful payment
- Low stock threshold warnings apply to both
- isInStock() and canOrderQuantity() run identically
This means zero special-casing in the codebase. Every product behaves the same through cart, checkout, and payment. The divergence only begins at fulfillment.
Why sellers set quantity for digital products
The business reasons vary:
Exclusivity / scarcity โ "Only 500 copies ever sold" โ creates urgency and perceived value
License seat control โ Software licensed for exactly N seats
Cohort control โ Course intake capped at N students for direct purchase
Business guardrail โ Seller wants a hard ceiling as a safety net
Even if a seller sets 1,000,000, the system enforces it as a real number. The seller always sees and manages a stock figure.
3. The Only Real Difference Between Physical and Digital
Physical Digital
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
stockQuantity โ โ
Inventory hold at checkout โ โ
Stock decrements on buy โ โ
Low stock warnings โ โ
Requires shipping โ โ
Delivery confirmation code โ โ
Escrow held until delivery โ โ
Order โ PENDING_SHIPMENT โ โ
Escrow released immediatelyโ โ
Order โ COMPLETED โ โ
DigitalDownloadAccess โ โ
Everything above the dividing line is shared. Everything below is where the paths split.
4. How Sellers Prepare Digital Products
Before a digital product can be purchased, the seller must upload its files through a two-step process designed to handle large files without routing them through the API server.
Seller โ Request presigned upload URL
โ Platform generates time-limited URL (MinIO private bucket)
โ Seller uploads file DIRECTLY to MinIO (API server bypassed)
โ Seller confirms upload to API
โ Platform records file metadata and links to product
A product can have multiple files. Each gets its own record. Buyers get access to all files on purchase.
Sellers configure per product:
stockQuantity โ required ยท how many units can ever be sold
lowStockThreshold โ when to trigger low stock warning
maxQuantityForDigital โ max a single buyer can purchase in one order
(1 for personal-use content, higher for licenses/gifts)
Download rules:
expiryDays โ days download links stay active after purchase (default: 7)
downloadCap โ max downloads per buyer (default: unlimited)
clockStart โ expiry from purchase time OR from first download
5. The Cart
The cart is a persistent bag of products. It does not distinguish between physical and digital items โ both coexist freely. Stock availability is checked on add-to-cart for both types.
The physical/digital split only becomes relevant at fulfillment โ not at cart, not at payment.
6. Checkout Session Types
Every purchase flows through a checkout session โ a short-lived record holding purchase intent before payment. All four session types work for both physical and digital products.
| Session Type | Description |
|---|---|
REGULAR_DIRECTLY |
Single-item purchase from the product page |
REGULAR_CART |
Multi-item purchase from a cart |
GROUP_PURCHASE |
Coordinated group buy at a discounted group price |
INSTALLMENT |
Down payment now, remainder paid over time |
Sessions expire (typically 15โ30 minutes). During this window, inventory is held for both physical and digital products โ preventing overselling while the buyer completes payment.
7. Shipping Logic
All items digital? โ Skip shipping entirely. Cost = 0.
Inventory hold still applies.
Any item physical? โ Shipping address + method required. Inventory held.
Mixed cart (physical + digital, same shop)?
โ Engine splits into TWO sessions automatically:
Session 1: physical items โ shipping lifecycle
Session 2: digital items โ download lifecycle
8. Payment Infrastructure (Shared by All Types)
Payment works identically regardless of product type:
Buyer wallet โ debited
Escrow โ credited (money held, not yet with seller)
Ledger entry โ recorded (double-entry, full audit trail)
Tx history โ updated for buyer
For installment purchases, only the down payment moves at session time. Each subsequent payment creates its own ledger entry.
Escrow is the control point. Physical โ held until buyer confirms delivery. Digital โ released immediately at order creation.
9. Post-Payment Fulfillment โ The Core Split
Physical path
Payment completes
โ stockQuantity decremented
โ Inventory hold released
โ Order created [PENDING_SHIPMENT]
โ Seller notified
โ Seller ships โ marks order SHIPPED
โ 6-digit confirmation code generated โ sent to buyer
โ Buyer enters code in app
โ Escrow releases to seller
โ Order [COMPLETED]
Escrow is held the entire shipping period. The code is the handshake โ seller cannot claim money without buyer confirming receipt.
Digital path
Payment completes
โ stockQuantity decremented
โ Inventory hold released
โ Order created [COMPLETED immediately]
โ Escrow released to seller immediately
โ DigitalDownloadAccess records created (one per file)
โ Buyer notified with download link
โ Buyer downloads within expiry window
No shipping. No confirmation code. Delivery = access records created.
10. Group Purchase โ Physical vs Digital
Both product types support group purchase. The financial hold (escrow per participant) and inventory hold are identical in both cases during the waiting period. What differs is post-completion fulfillment.
Physical group purchase
Each participant pays โ Escrow held + Inventory held per participant
Group fills โ COMPLETED
โ Physical orders created for all participants [PENDING_SHIPMENT]
โ stockQuantity decremented for all participants
โ Each participant goes through shipping lifecycle individually
โ Each escrow releases on individual delivery confirmation
Group expires without filling:
โ All escrows refunded
โ All inventory holds released
Digital group purchase
Each participant pays โ Escrow held + Inventory held per participant
Group fills โ COMPLETED
โ Orders created for all participants [COMPLETED immediately]
โ stockQuantity decremented for all participants
โ All escrows released immediately
โ DigitalDownloadAccess records created for every participant
โ All buyers can download immediately
Group expires without filling:
โ All escrows refunded
โ All inventory holds released (identical to physical)
The seller's participant cap on the group instance is separate from
stockQuantity. Both are enforced โ a buyer cannot join if either the group is full or the product stock is exhausted.
11. Installment Purchase โ Physical vs Digital
Both product types support installment. The payment schedule, ledger entries, agreement lifecycle, early payoff, and flexible payment features are identical. What differs is fulfillment timing.
Fulfillment timing options
IMMEDIATE โ order and access created after the down payment. Buyer gets the product now, pays over time.
- Physical: seller ships after down payment, takes risk on future payments.
- Digital: buyer downloads immediately. If buyer defaults, downloaded files cannot be revoked. Seller has no recourse.
AFTER_PAYMENT โ order and access created only after the final payment clears.
- Physical: layaway โ seller holds stock, ships at the end.
- Digital: safest model โ access never opens until fully paid. On default, access records simply never created.
Platform recommendation: AFTER_PAYMENT is the default for digital installment products. Sellers must explicitly opt into IMMEDIATE with acknowledgment that pre-delivery means no recourse on default.
On default โ digital IMMEDIATE
Buyer stops paying โ Agreement DEFAULTED
โ No new access records created for future files
โ Already-downloaded files cannot be revoked โ known limitation, seller accepts this
On default โ digital AFTER_PAYMENT
Buyer stops paying โ Agreement DEFAULTED
โ Access records never created
โ No content ever delivered โ clean outcome
12. The Download System
On every successful digital purchase (any session type, any fulfillment trigger):
Platform creates DigitalDownloadAccess record per file:
- buyer ID + order ID + file ID
- downloadCount (starts at 0)
- maxDownloads (null = unlimited, or seller-set cap)
- accessExpiresAt (now + expiry window)
- firstDownloadAt (set on first use)
On every download request:
Buyer hits authenticated endpoint
โ Check 1: Does buyer own an active access record for this file?
โ Check 2: Has expiry window passed?
โ Check 3: Has download cap been reached?
All pass โ Platform generates presigned GET URL (5-min TTL)
โ Buyer browser downloads DIRECTLY from MinIO private bucket
โ Access record downloadCount incremented
โ API server is NOT in the file transfer path
The 5-minute TTL means a leaked URL is useless within minutes. The raw MinIO object key is never exposed to the buyer.
13. Scenario Walkthrough โ All Combinations
Scenario A โ Direct purchase, physical product
Buyer finds a T-shirt. Clicks "Buy Now". Quantity: 1.
REGULAR_DIRECTLY session created
โ Stock check passes ยท Inventory held
โ Shipping address + method required
โ Buyer pays โ Escrow funded
โ stockQuantity decremented ยท hold released
โ Order created [PENDING_SHIPMENT]
โ Seller ships โ marks SHIPPED
โ 6-digit code sent to buyer
โ Buyer confirms โ Escrow released
โ Order [COMPLETED]
Scenario B โ Direct purchase, digital product
Buyer finds a PDF course. Clicks "Buy Now". Quantity: 1.
REGULAR_DIRECTLY session created
โ Stock check passes ยท Inventory held
โ No shipping fields collected
โ Buyer pays โ Escrow funded and immediately released
โ stockQuantity decremented ยท hold released
โ Order created [COMPLETED]
โ 3 DigitalDownloadAccess records created (one per chapter PDF)
โ Buyer receives download link ยท downloads within 7 days
Scenario C โ Direct purchase, digital product, quantity 3
Buyer wants 3 software licenses to distribute to colleagues.
REGULAR_DIRECTLY session created (quantity: 3)
โ Stock check: stockQuantity >= 3? passes ยท 3 units held
โ No shipping fields collected
โ Buyer pays (unitPrice ร 3) โ Escrow funded and immediately released
โ stockQuantity decremented by 3 ยท hold released
โ Order created [COMPLETED]
โ 6 DigitalDownloadAccess records created (3 sets ร 2 files per product)
โ Each set is independent โ buyer can share with colleagues
Scenario D โ Cart, physical only, multiple shops
Phone case from Shop A + charger from Shop B.
REGULAR_CART session created
โ Inventory held for both items
โ Buyer pays once
โ Order engine groups by shop:
Order 1: Shop A [PENDING_SHIPMENT]
Order 2: Shop B [PENDING_SHIPMENT]
โ Each seller ships independently
โ Each escrow releases on individual buyer confirmation
Scenario E โ Cart, digital only
Video course + design template pack.
REGULAR_CART session created
โ Stock check + inventory hold for both digital products
โ No shipping.
โ Buyer pays once
โ Order 1: video course [COMPLETED] โ 4 access records
โ Order 2: template pack [COMPLETED] โ 2 access records
โ Buyer downloads all 6 files within 7 days
Scenario F โ Cart, mixed physical + digital, same shop
Printed book (physical) + PDF supplement (digital), same shop.
Engine detects mixed cart โ splits automatically:
Session 1: printed book โ shipping lifecycle ยท inventory held
Session 2: PDF โ download lifecycle ยท inventory held
Buyer sees one checkout flow, gets two orders in history:
Order 1 (book): [PENDING_SHIPMENT] โ shipping โ confirmation โ escrow releases
Order 2 (PDF): [COMPLETED] โ download access immediate
Scenario G โ Group purchase, physical product
5 buyers collectively buy a speaker.
Buyer 1 initiates โ GROUP_PURCHASE session โ pays โ group instance created (timer starts)
Buyers 2โ5 join โ each pays โ inventory held + escrow held per participant
Group fills โ COMPLETED
โ Physical orders created for all 5 [PENDING_SHIPMENT]
โ stockQuantity decremented for all 5
โ Each participant ships independently
โ Each escrow releases on individual delivery confirmation
Timer expires before filling:
โ All 5 escrows refunded ยท all inventory holds released
Scenario H โ Group purchase, digital product
30 buyers join a cohort-based online course. Seller stock: 30.
Buyers 1โ30 join โ each pays โ inventory held + escrow held per participant
Group fills (30th seat) โ COMPLETED
โ Orders created for all 30 [COMPLETED immediately]
โ stockQuantity decremented by 30 (now 0 โ sold out)
โ All 30 escrows released immediately
โ DigitalDownloadAccess records created for every participant
โ All 30 buyers can download immediately
Timer expires before filling:
โ All escrows refunded ยท all inventory holds released
Scenario I โ Installment, physical, IMMEDIATE fulfillment
Laptop, 6-month plan, 20% down, seller ships immediately.
INSTALLMENT session created
โ Stock check ยท inventory held
โ Down payment charged (20%)
โ Agreement created (6 scheduled payments)
โ Order created [PENDING_SHIPMENT] โ immediately after down payment
โ stockQuantity decremented ยท hold released
โ Seller ships
โ Monthly payments auto-process via scheduled jobs
Scenario J โ Installment, physical, AFTER_PAYMENT fulfillment
Same laptop, layaway model.
INSTALLMENT session created
โ Stock check ยท inventory held
โ Down payment charged
โ Agreement created
โ NO order created yet ยท inventory held until final payment
โ Monthly payments auto-process
โ Final payment clears โ Agreement COMPLETED
โ Order created [PENDING_SHIPMENT] โ only now
โ stockQuantity decremented ยท hold released
โ Shipping lifecycle begins
Scenario K โ Installment, digital, AFTER_PAYMENT (recommended)
Video course library, 500,000 TZS, 3-month plan.
INSTALLMENT session created
โ Stock check ยท inventory held
โ Down payment charged (30%)
โ Agreement created (3 scheduled payments)
โ NO order, NO download access yet ยท inventory held
โ Monthly payments auto-process
โ Final payment clears โ Agreement COMPLETED
โ stockQuantity decremented ยท hold released
โ Order created [COMPLETED]
โ DigitalDownloadAccess records created โ only now
โ Buyer downloads
If buyer defaults:
โ Agreement DEFAULTED
โ Inventory hold released ยท stockQuantity restored
โ Access records never created ยท no content delivered
Scenario L โ Installment, digital, IMMEDIATE fulfillment
Same course, seller opts into IMMEDIATE.
INSTALLMENT session created
โ Stock check ยท inventory held
โ Down payment charged (30%)
โ Agreement created
โ stockQuantity decremented ยท hold released
โ Order created [COMPLETED] โ immediately
โ DigitalDownloadAccess records created โ immediately
โ Buyer downloads now
Monthly payments continue auto-processing.
If buyer defaults:
โ Agreement DEFAULTED
โ Already-downloaded files CANNOT be revoked โ seller accepted this risk
14. Escrow Behavior Summary
| Scenario | Inventory Held | Escrow Released |
|---|---|---|
| Physical โ direct | Yes, at session creation | On delivery confirmation |
| Digital โ direct | Yes, at session creation | Immediately at order creation |
| Physical โ group | Yes, per participant | Per delivery confirmation |
| Digital โ group | Yes, per participant | Immediately when group completes |
| Group โ expired / failed | Released to stock | Refunded to all participants |
| Physical โ installment IMMEDIATE | Yes, until down pmt | Per installment via direct ledger |
| Physical โ installment AFTER_PAYMENT | Yes, until final pmt | After final payment |
| Digital โ installment IMMEDIATE | Yes, until down pmt | After down payment |
| Digital โ installment AFTER_PAYMENT | Yes, until final pmt | After final payment |
15. MinIO Bucket Architecture
nextgate-public (world-readable)
โ Product images ยท shop logos ยท avatars ยท category images
โ URLs are permanent ยท no expiry
nextgate-digital-content (private ยท no public access)
โ Paid digital product files only
โ Accessible only via presigned URLs (5-min TTL)
โ Generated by API after access verification
โ Raw object key never exposed to buyers
This separation ensures a seller can never accidentally expose a paid file by uploading to the wrong location.
16. Key Invariants
- Every money movement goes through the double-entry ledger.
No direct wallet balance adjustments exist.
- Escrow receives the full payment before any fulfillment action begins.
- Every product has a stockQuantity โ physical and digital alike.
There is no "unlimited" mode. Sellers set a real number.
- Inventory is held for both physical and digital products during the
checkout session window. Released on expiry, cancellation, or payment.
- Orders are never created before:
regular purchase โ payment completes
group purchase โ group fills
installment โ fulfillment trigger (down pmt or final pmt)
- Download links never expose the raw MinIO object key.
Only opaque access identifiers resolve to presigned URLs via authenticated API.
- A checkout session produces orders exactly once.
Idempotency checks prevent duplicates under retry or failure.
- Session expiry is enforced before payment processing.
An expired session cannot be paid.
- For digital installment IMMEDIATE: already-downloaded files cannot be
revoked on default. Sellers must explicitly accept this risk.
- For digital installment AFTER_PAYMENT: default means access records
are never created and stockQuantity is restored. Clean outcome.
Checkout Session
Base URL: https://apinexgate.glueauth.com/api/v1/
Short Description: The Checkout Session API manages the complete checkout process for both e-commerce and event transactions. It handles session creation (with upfront wallet balance validation), inventory holds, payment processing via Wallet, Cash, and Free flows, group purchasing, and installment payments. Each session maintains state, inventory holds, and payment attempt tracking with automatic expiration handling.
Hints:
- Wallet balance is validated at session creation time โ if insufficient, no session is created and a rich balance response is returned so the frontend can guide the user to top up
- All checkout sessions expire after 15 minutes by default
- Inventory is automatically held during active sessions and released upon expiration or cancellation
- Maximum 5 payment retry attempts allowed per session
- Group purchase sessions require WALLET payment method only
- Sessions in PAYMENT_PROCESSING status cannot be modified
- Installment payment only charges the down payment at checkout; monthly payments are handled by the scheduler
- All monetary values are in TZS (Tanzanian Shillings)
- Use ISO 8601 format for all datetime fields
- Sessions can only be updated when in PENDING_PAYMENT or PAYMENT_FAILED status
Full Checkout Flow Diagram
CLIENT BACKEND SYSTEMS
| | |
| POST /checkout-sessions | |
|------------------------------>| |
| |-- Validate request |
| |-- Calculate pricing |
| | |
| |-- assertSufficientBalance() |
| | | |
| | |-- GET wallet balance -|-> Ledger
| | | |
| | ....................... |
| | . BALANCE INSUFFICIENT . |
| | ....................... |
| 422 + rich balance data | | |
|<------------------------------|<-------' |
| { | |
| walletBalance: 5000, | |
| sessionTotal: 12000, | |
| shortfall: 7000, | |
| recommendedTopUp: 7000 | |
| } | |
| | |
| | ....................... |
| | . BALANCE SUFFICIENT . |
| | ....................... |
| | | |
| |-- Hold inventory ------------>|-> Inventory
| |-- Save session |
| |-- Schedule expiry job ------->|-> JobRunr
| | |
| 201 + session data | |
|<------------------------------| |
| { sessionId, status: | |
| PENDING_PAYMENT, ... } | |
| | |
| ...15 min window... | |
| | |
| POST /{sessionId}/payment | |
|------------------------------>| |
| |-- Validate status |
| |-- Check expiry |
| | |
| | ....................... |
| | . FREE checkout . |
| | ....................... |
| | | |
| |-- Create booking/order |
| |-- Track free transaction |
| 200 SUCCESS | |
|<------------------------------| |
| | |
| | ....................... |
| | . CASH checkout . |
| | ....................... |
| | | |
| |-- Create booking/order |
| |-- Track cash transaction |
| 200 SUCCESS | |
|<------------------------------| |
| | |
| | ....................... |
| | . WALLET checkout . |
| | ....................... |
| | | |
| |-- Set PAYMENT_PROCESSING |
| |-- holdMoney() --------------->|-> Escrow
| | |
| | ....................... |
| | . SUCCESS . |
| | ....................... |
| | | |
| |-- Set PAYMENT_COMPLETED |
| |-- Create order/booking |
| |-- Commit inventory hold ----->|-> Inventory
| |-- Publish payment event ----->|-> Notifications
| 200 SUCCESS | |
|<------------------------------| |
| { orderId, escrowId, | |
| amountPaid, ... } | |
| | |
| | ....................... |
| | . FAILED . |
| | ....................... |
| | | |
| |-- recordFailedAttempt() |
| |-- Release reservation |
| | (events only) |
| 200 FAILED | |
|<------------------------------| |
| { success: false, | |
| canRetry: true/false } | |
| | |
| POST /{sessionId}/retry | |
|------------------------------>| |
| |-- Check status = FAILED |
| |-- Check attempts < 5 |
| |-- Re-validate inventory |
| |-- Check wallet balance |
| |-- Re-hold inventory |
| |-- Reset to PENDING_PAYMENT |
| |-- processPayment() ... |
| | (same wallet flow above) |
Session Type Routing
POST /checkout-sessions
sessionType?
.
โโ REGULAR_DIRECTLY โโ> validate 1 item โโ> balance check โโ> hold inventory โโ> session
|
โโ REGULAR_CART โโโโโโ> validate cart โโโ> balance check โโ> hold inventory โโ> session
|
โโ GROUP_PURCHASE โโโโ> validate group โโ> balance check โโ> session (no hold, group-level)
|
โโ INSTALLMENT โโโโโโโ> calculate down payment โโ> balance check โโ> hold inventory โโ> session
POST /e-events/checkout
ticketPricingType?
.
โโ PAID โโ> balance check โโ> create payment intent โโ> reserve ticket โโ> session
|
โโ FREE โโ> (no balance check) โโ> reserve ticket โโ> session
Standard Response Format
All API responses follow a consistent structure using our Globe Response Builder pattern:
Success Response Structure
{
"success": true,
"httpStatus": "OK",
"message": "Operation completed successfully",
"action_time": "2025-10-02T10:30:45",
"data": {}
}
Error Response Structure
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Error description",
"action_time": "2025-10-02T10:30:45",
"data": "Error description"
}
Insufficient Balance Error Structure (422)
This is a special structured error returned when wallet balance is insufficient at session creation time. The data field contains rich balance details so the frontend can guide the user to top up.
{
"success": false,
"httpStatus": "UNPROCESSABLE_ENTITY",
"message": "Insufficient wallet balance to complete checkout",
"action_time": "2025-10-02T10:30:45",
"data": {
"walletBalance": 5000.00,
"sessionTotal": 12000.00,
"shortfall": 7000.00,
"hasSufficientBalance": false,
"recommendedTopUp": 7000.00,
"pspMinimum": 500.00,
"currency": "TZS"
}
}
| Field | Description |
|---|---|
walletBalance |
Current wallet balance of the user |
sessionTotal |
Total amount required for this checkout |
shortfall |
How much is missing (sessionTotal - walletBalance) |
hasSufficientBalance |
Always false when this error is returned |
recommendedTopUp |
Suggested top-up amount (at least shortfall, rounded up to PSP minimum) |
pspMinimum |
Minimum top-up amount accepted by the payment provider |
currency |
Always TZS |
Standard Response Fields
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true for successful operations, false for errors |
httpStatus |
string | HTTP status name (OK, CREATED, BAD_REQUEST, NOT_FOUND, etc.) |
message |
string | Human-readable message describing the operation result |
action_time |
string | ISO 8601 timestamp of when the response was generated |
data |
object/string | Response payload for success, error details for failures |
HTTP Method Badge Standards
For better visual clarity, all endpoints use colored badges for HTTP methods with the following standard colors:
- GET - GET - Green (Safe, read-only operations)
- POST - POST - Blue (Create new resources)
- PATCH - PATCH - Orange (Partial updates)
- DELETE - DELETE - Red (Remove resources)
Endpoints
1. Create Checkout Session
Purpose: Creates a new checkout session for processing a purchase. Wallet balance is validated upfront โ if insufficient, no session is created and the caller receives rich balance data to guide the user. Supports multiple checkout types: direct product purchase, cart checkout, group purchasing, and installment payments.
Endpoint: POST {base_url}/checkout-sessions
Access Level: ๐ Protected (Requires Authentication)
Authentication: Bearer Token required in Authorization header
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authenticated user |
| Content-Type | string | Yes | Must be application/json |
Request JSON Sample:
{
"sessionType": "REGULAR_DIRECTLY",
"items": [
{
"productId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"quantity": 2
}
],
"shippingAddressId": "f1e2d3c4-b5a6-7890-cdef-123456789abc",
"shippingMethodId": "standard-shipping",
"metadata": {
"couponCode": "SAVE20",
"referralCode": "REF123",
"notes": "Please handle with care"
},
"installmentPlanId": null,
"groupInstanceId": null,
"downPaymentPercent": 20
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| sessionType | string | Yes | Type of checkout session | enum: REGULAR_DIRECTLY, REGULAR_CART, GROUP_PURCHASE, INSTALLMENT |
| items | array | Conditional | Array of items to checkout. Required for REGULAR_DIRECTLY and GROUP_PURCHASE | For REGULAR_DIRECTLY: exactly 1 item. For GROUP_PURCHASE: exactly 1 item. Not used for REGULAR_CART |
| items[].productId | string (UUID) | Yes (if items provided) | Product identifier | Valid UUID format |
| items[].quantity | integer | Yes (if items provided) | Quantity to purchase | Min: 1, must not exceed product constraints |
| shippingAddressId | string (UUID) | Yes | User's shipping address identifier | Valid UUID format, must belong to authenticated user |
| shippingMethodId | string | Yes | Selected shipping method identifier | Must be valid shipping method |
| metadata | object | No | Additional metadata for the session | Key-value pairs for coupons, referrals, notes, etc. |
| installmentPlanId | string (UUID) | Conditional | Installment plan identifier (INSTALLMENT type only) | Valid UUID format |
| downPaymentPercent | integer | Conditional | Down payment percentage (INSTALLMENT type only) | Must satisfy plan's min/max constraints |
| groupInstanceId | string (UUID) | No | Group instance to join (GROUP_PURCHASE type only) | Valid UUID format, group must be joinable. Null = create new group |
| groupName | string | Conditional | Name for new group (GROUP_PURCHASE, groupInstanceId null) | Must be unique per product |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "CREATED",
"message": "Checkout session created successfully",
"action_time": "2025-10-02T14:30:45",
"data": {
"sessionId": "c1d2e3f4-a5b6-7890-cdef-123456789abc",
"sessionType": "REGULAR_DIRECTLY",
"status": "PENDING_PAYMENT",
"customerId": "u1s2e3r4-i5d6-7890-abcd-ef1234567890",
"customerUserName": "john_doe",
"items": [
{
"productId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"productName": "Premium Wireless Headphones",
"productSlug": "premium-wireless-headphones",
"productPrimaryMedia": {
"fileId": "8f2a1c3d-1234-4a5b-9c6d-abcdef012345",
"mediaType": "IMAGE",
"status": "READY",
"thumbUrl": "https://cdn.nextgate.com/products/headphones-001/thumb.webp",
"mimeType": "image/webp"
},
"quantity": 2,
"unitPrice": 150000.00,
"subtotal": 300000.00,
"tax": 0.00,
"total": 300000.00,
"shopId": "s1h2o3p4-i5d6-7890-abcd-ef1234567890",
"shopName": "TechWorld Electronics",
"availableForCheckout": true,
"availableQuantity": 50
}
],
"pricing": {
"subtotal": 300000.00,
"shippingCost": 5000.00,
"tax": 0.00,
"total": 305000.00,
"currency": "TZS"
},
"shippingAddress": {
"fullName": "John Doe",
"addressLine1": "123 Main Street",
"addressLine2": "Apartment 4B",
"city": "Dar es Salaam",
"state": "Dar es Salaam Region",
"postalCode": "12345",
"country": "Tanzania",
"phone": "+255123456789"
},
"shippingMethod": {
"id": "standard-shipping",
"name": "Standard Shipping",
"carrier": "DHL",
"cost": 5000.00,
"estimatedDays": "3-5 business days",
"estimatedDelivery": "2025-10-07T14:30:45"
},
"paymentAttempts": [],
"inventoryHeld": true,
"inventoryHoldExpiresAt": "2025-10-02T14:45:45",
"metadata": {
"couponCode": "SAVE20",
"referralCode": "REF123",
"notes": "Please handle with care"
},
"expiresAt": "2025-10-02T14:45:45",
"createdAt": "2025-10-02T14:30:45",
"updatedAt": "2025-10-02T14:30:45",
"completedAt": null,
"createdOrderIds": [],
"cartId": null
}
}
Success Response Fields:
| Field | Description |
|---|---|
| sessionId | Unique identifier for the checkout session |
| sessionType | Type of checkout (REGULAR_DIRECTLY, REGULAR_CART, GROUP_PURCHASE, INSTALLMENT) |
| status | Current status of the session (always PENDING_PAYMENT on creation) |
| customerId | User ID who created the session |
| customerUserName | Username of the customer |
| items | Array of checkout items with product details, pricing, and availability. Each item's image is productPrimaryMedia ({fileId, mediaType, status, thumbUrl, mimeType}) โ the lean, resolved primary-media shape, same as cart/wishlist/marketplace |
| pricing | Summary of all pricing calculations |
| pricing.total | Final amount to be charged (what was validated against wallet balance) |
| pricing.currency | Always TZS |
| shippingAddress | Complete shipping address details |
| shippingMethod | Selected shipping method details |
| paymentIntent | Payment processing details. Field is declared but not currently populated by any checkout flow โ omitted (null) in real responses today |
| paymentAttempts | Empty array on creation |
| inventoryHeld | Whether inventory is currently held (false for GROUP_PURCHASE) |
| inventoryHoldExpiresAt | When the inventory hold will be released |
| expiresAt | When this checkout session expires (15 minutes from creation) |
| createdOrderIds | Empty array until payment succeeds; populated with the created order ID(s) after |
| cartId | Cart ID if session was created from cart, null otherwise |
Error Responses:
Insufficient Wallet Balance (422) โ includes rich top-up data:
{
"success": false,
"httpStatus": "UNPROCESSABLE_ENTITY",
"message": "Insufficient wallet balance to complete checkout",
"action_time": "2025-10-02T14:30:45",
"data": {
"walletBalance": 150000.00,
"sessionTotal": 285000.00,
"shortfall": 135000.00,
"hasSufficientBalance": false,
"recommendedTopUp": 135000.00,
"pspMinimum": 500.00,
"currency": "TZS"
}
}
Bad Request - Invalid Session Type (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "REGULAR_DIRECTLY checkout supports only 1 item. Use REGULAR_CART for multiple items.",
"action_time": "2025-10-02T14:30:45",
"data": "REGULAR_DIRECTLY checkout supports only 1 item. Use REGULAR_CART for multiple items."
}
Bad Request - Insufficient Inventory (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Insufficient stock. Available: 3, Requested: 5",
"action_time": "2025-10-02T14:30:45",
"data": "Insufficient stock. Available: 3, Requested: 5"
}
Validation Error (422):
{
"success": false,
"httpStatus": "UNPROCESSABLE_ENTITY",
"message": "Validation failed",
"action_time": "2025-10-02T14:30:45",
"data": {
"sessionType": "must not be null",
"items[0].quantity": "must be greater than or equal to 1",
"shippingAddressId": "must not be null"
}
}
Not Found - Product Not Found (404):
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "Product not found",
"action_time": "2025-10-02T14:30:45",
"data": "Product not found"
}
{
"success": false,
"httpStatus": "UNAUTHORIZED",
"message": "Authentication token is required",
"action_time": "2025-10-02T14:30:45",
"data": "Authentication token is required"
}
2. Get Checkout Session by ID
Purpose: Retrieves detailed information about a specific checkout session by its ID. Only the session owner can access their session.
Endpoint: GET {base_url}/checkout-sessions/{sessionId}
Access Level: ๐ Protected (Requires Authentication and Ownership)
Authentication: Bearer Token required in Authorization header
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| sessionId | string (UUID) | Yes | Unique identifier of the checkout session | Valid UUID format |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Checkout session retrieved successfully",
"action_time": "2025-10-02T14:35:45",
"data": {
"sessionId": "c1d2e3f4-a5b6-7890-cdef-123456789abc",
"sessionType": "REGULAR_DIRECTLY",
"status": "PENDING_PAYMENT",
"customerId": "u1s2e3r4-i5d6-7890-abcd-ef1234567890",
"customerUserName": "john_doe",
"items": [],
"pricing": {},
"shippingAddress": {},
"shippingMethod": {},
"paymentIntent": {},
"paymentAttempts": [],
"inventoryHeld": true,
"inventoryHoldExpiresAt": "2025-10-02T14:45:45",
"metadata": { "couponCode": "SAVE20" },
"expiresAt": "2025-10-02T14:45:45",
"createdAt": "2025-10-02T14:30:45",
"updatedAt": "2025-10-02T14:30:45",
"completedAt": null,
"createdOrderId": null,
"cartId": null
}
}
Error Responses:
Not Found - Session Not Found or No Permission (404):
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "Checkout session not found or you don't have permission to access it",
"action_time": "2025-10-02T14:35:45",
"data": "Checkout session not found or you don't have permission to access it"
}
3. Get My Checkout Sessions
Purpose: Retrieves all checkout sessions belonging to the authenticated user, ordered by creation date (newest first).
Endpoint: GET {base_url}/checkout-sessions
Access Level: ๐ Protected (Requires Authentication)
Success Response Fields:
| Field | Description |
|---|---|
| sessionId | Unique identifier for the checkout session |
| sessionType | Type of checkout session |
| status | Current status of the session |
| itemCount | Number of items in the checkout |
| totalAmount | Total amount to be paid in TZS |
| currency | Currency code (TZS) |
| expiresAt | When this session expires |
| createdAt | When this session was created |
| isExpired | Whether the session has expired |
| canRetryPayment | true only if status is PAYMENT_FAILED and not expired and attempts < 5 |
| itemPreviews | Array of preview information for items |
4. Get My Active Checkout Sessions
Purpose: Retrieves only active checkout sessions (PENDING_PAYMENT or PAYMENT_FAILED status) that haven't expired yet, ordered by creation date (newest first).
Endpoint: GET {base_url}/checkout-sessions/active
Access Level: ๐ Protected (Requires Authentication)
Notes: Response structure is the same as Get My Checkout Sessions but filtered to active sessions only. isExpired is always false in this response.
5. Update Checkout Session
Purpose: Updates an existing checkout session. Can modify shipping address, shipping method, or metadata. Only sessions in PENDING_PAYMENT or PAYMENT_FAILED status can be updated.
Endpoint: PATCH {base_url}/checkout-sessions/{sessionId}
Access Level: ๐ Protected (Requires Authentication and Ownership)
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| sessionId | string (UUID) | Yes | Unique identifier of the checkout session |
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| shippingAddressId | string (UUID) | No | New shipping address identifier |
| shippingMethodId | string | No | New shipping method (triggers pricing recalculation) |
| metadata | object | No | Key-value pairs, merged with existing metadata |
Error Responses:
Cannot Update Completed Session (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Cannot update a completed checkout session",
"action_time": "2025-10-02T14:50:45",
"data": "Cannot update a completed checkout session"
}
6. Cancel Checkout Session
Purpose: Cancels an existing checkout session and releases held inventory. Cannot cancel sessions that are completed or have successful payment.
Endpoint: DELETE {base_url}/checkout-sessions/{sessionId}/cancel
Access Level: ๐ Protected (Requires Authentication and Ownership)
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "Checkout session cancelled successfully",
"action_time": "2025-10-02T14:55:45",
"data": null
}
Error Responses:
Cannot Cancel Completed (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Cannot cancel - payment has been completed. Please contact support.",
"action_time": "2025-10-02T14:55:45",
"data": "Cannot cancel - payment has been completed. Please contact support."
}
Already Cancelled (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Checkout session is already cancelled",
"action_time": "2025-10-02T14:55:45",
"data": "Checkout session is already cancelled"
}
7. Process Payment
Purpose: Initiates payment processing for a checkout session in PENDING_PAYMENT status. Routes to the appropriate payment processor (WALLET, CASH, FREE) based on the session amount and payment method.
Endpoint: POST {base_url}/checkout-sessions/{sessionId}/process-payment
Access Level: ๐ Protected (Requires Authentication and Ownership)
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| sessionId | string (UUID) | Yes | Unique identifier of the checkout session |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Payment completed successfully. Your order is being processed.",
"action_time": "2025-10-02T15:00:45",
"data": {
"success": true,
"status": "SUCCESS",
"message": "Payment completed successfully. Your order is being processed.",
"checkoutSessionId": "c1d2e3f4-a5b6-7890-cdef-123456789abc",
"escrowId": "esc-uuid",
"escrowNumber": "ESC-20251002-001",
"orderId": "ord-uuid",
"paymentMethod": "WALLET",
"amountPaid": 285000.00,
"platformFee": 5700.00,
"sellerAmount": 279300.00,
"currency": "TZS"
}
}
Success Response Fields:
| Field | Description |
|---|---|
| success | Whether the payment was successful |
| status | SUCCESS, PENDING, or FAILED |
| checkoutSessionId | The session that was paid |
| escrowId | Escrow account holding the funds |
| escrowNumber | Human-readable escrow reference |
| orderId | Order or booking ID created after payment |
| paymentMethod | WALLET, CASH, or FREE |
| amountPaid | Total amount charged in TZS |
| platformFee | Platform fee deducted |
| sellerAmount | Amount the seller receives |
| currency | Always TZS |
Error Responses:
Session Not in PENDING_PAYMENT status (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Cannot process payment - session is not pending: PAYMENT_COMPLETED",
"action_time": "2025-10-02T15:00:45",
"data": "Cannot process payment - session is not pending: PAYMENT_COMPLETED"
}
Session Expired (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Checkout session has expired",
"action_time": "2025-10-02T15:00:45",
"data": "Checkout session has expired"
}
8. Retry Payment
Purpose: Retries payment for a session in PAYMENT_FAILED status. Re-validates inventory availability, checks wallet balance, re-holds inventory, and extends session expiration before retrying. Maximum 5 total attempts.
Endpoint: POST {base_url}/checkout-sessions/{sessionId}/retry-payment
Access Level: ๐ Protected (Requires Authentication and Ownership)
Error Responses:
Invalid Status (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Cannot retry payment - session status: PENDING_PAYMENT. Expected: PAYMENT_FAILED",
"action_time": "2025-10-02T15:10:45",
"data": "Cannot retry payment - session status: PENDING_PAYMENT. Expected: PAYMENT_FAILED"
}
Max Attempts Exceeded (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Maximum payment attempts (5) exceeded. Please create a new checkout session.",
"action_time": "2025-10-02T15:10:45",
"data": "Maximum payment attempts (5) exceeded. Please create a new checkout session."
}
Insufficient Wallet Balance on Retry (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Insufficient wallet balance. Required: 285000 TZS, Available: 150000 TZS. Please top up your wallet.",
"action_time": "2025-10-02T15:10:45",
"data": "Insufficient wallet balance. Required: 285000 TZS, Available: 150000 TZS. Please top up your wallet."
}
Product No Longer Available (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Product 'Premium Wireless Headphones' is no longer available in requested quantity. Please create a new checkout session.",
"action_time": "2025-10-02T15:10:45",
"data": "Product 'Premium Wireless Headphones' is no longer available in requested quantity. Please create a new checkout session."
}
Checkout Session Types
REGULAR_DIRECTLY
Direct product purchase without using a cart. Must include exactly 1 item.
- Best for "Buy Now" buttons
- Balance is checked against
pricing.total(includes shipping) - Inventory is held immediately on session creation
REGULAR_CART
Checkout from existing shopping cart. Items are fetched automatically from the user's active cart.
- Multi-item purchase flow
- Items array not required in request
- Balance is checked against
pricing.total(includes shipping) - Inventory is held for all cart items
GROUP_PURCHASE
Group buying checkout where multiple users purchase the same product at a discounted group price.
- Single item at
product.groupPrice - Balance is checked against
groupPrice ร quantity - WALLET payment method only
- Can join existing group (provide
groupInstanceId) or create new group (providegroupName) - No session-level inventory hold โ inventory is held at the group level after payment
Create New Group:
{
"sessionType": "GROUP_PURCHASE",
"items": [{ "productId": "prod-uuid", "quantity": 2 }],
"shippingAddressId": "addr-uuid",
"shippingMethodId": "standard",
"groupName": "My Winning Group"
}
Join Existing Group:
{
"sessionType": "GROUP_PURCHASE",
"items": [{ "productId": "prod-uuid", "quantity": 3 }],
"shippingAddressId": "addr-uuid",
"shippingMethodId": "standard",
"groupInstanceId": "group-uuid-to-join"
}
INSTALLMENT
Split payment over multiple months. Only the down payment is charged at checkout; remaining monthly payments are processed automatically by the scheduler.
- Balance is checked against
downPaymentAmountonly (not the full product price) installmentPlanIdanddownPaymentPercentare requiredpricing.totalin the session response reflects the down payment only- Inventory is held immediately on session creation
{
"sessionType": "INSTALLMENT",
"items": [{ "productId": "prod-uuid", "quantity": 1 }],
"shippingAddressId": "addr-uuid",
"shippingMethodId": "standard",
"installmentPlanId": "plan-uuid",
"downPaymentPercent": 20
}
Checkout Session Status Flow
Status Definitions
| Status | Description | Can Update? | Can Cancel? | Can Pay? | Can Retry? |
|---|---|---|---|---|---|
| PENDING_PAYMENT | Session created, awaiting payment | Yes | Yes | Yes | No |
| PAYMENT_PROCESSING | Payment in progress | No | No | No | No |
| PAYMENT_FAILED | Payment failed, can retry | Yes | Yes | No | Yes |
| PAYMENT_COMPLETED | Payment successful | No | No | No | No |
| EXPIRED | Session expired | No | No | No | No |
| CANCELLED | User cancelled | No | No | No | No |
| COMPLETED | Free/cash order completed | No | No | No | No |
Status Transition Flow
[Session Creation]
|
| balance check passes
v
PENDING_PAYMENT
|
|-- user cancels โโโโโโโโโโโโโโโโโโ> CANCELLED
| (inventory released)
|
|-- 15 min timeout โโโโโโโโโโโโโโโโ> EXPIRED
| (inventory released)
|
|-- processPayment()
|
|-- amount = 0 โโโโโโโโโโโโโโโโ> COMPLETED (free)
|
|-- CASH โโโโโโโโโโโโโโโโโโโโโโ> COMPLETED (cash)
|
|-- WALLET โโโโโโโโโโโโโโโโโโโโ> PAYMENT_PROCESSING
|
|-- success โโ> PAYMENT_COMPLETED
| (order/booking created,
| inventory committed)
|
|-- failure โโ> PAYMENT_FAILED
(inventory released for events,
max 5 retries)
|
|-- retryPayment() โโ> PENDING_PAYMENT
| (inventory re-held)
|
|-- 5 attempts โโโโโโ> EXPIRED
Wallet Balance Check Endpoint
For cases where the frontend wants to proactively check balance against an existing session (e.g., before showing the "Pay Now" button), use:
Endpoint: GET {base_url}/wallet/checkout-balance-check?sessionId={id}&domain={PRODUCT|EVENT}
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "Checkout balance check completed",
"data": {
"walletBalance": 150000.00,
"sessionTotal": 285000.00,
"shortfall": 135000.00,
"hasSufficientBalance": false,
"recommendedTopUp": 135000.00,
"pspMinimum": 500.00,
"currency": "TZS"
}
}
Note: This endpoint always returns 200 โ it never throws. Use hasSufficientBalance to determine if the user can pay. This is the "soft check" for an existing session; the "hard check" (which blocks session creation) happens automatically inside POST /checkout-sessions.
Payment Methods Supported
WALLET (Default)
Internal wallet system. Default if no payment method is specified.
- Balance validated at session creation (hard block) and again at payment time (safety net)
- Instant processing via escrow
- Funds held in escrow until order is confirmed/delivered
CASH
Pay in cash on delivery or at the point of event check-in.
- No pre-payment required
- Order/booking created immediately
- Applicable to both product and event checkouts
FREE
Zero-amount checkout (free products or free event tickets).
- Handled automatically when
pricing.total = 0 - No payment method required
- Order/booking created immediately
CREDIT_CARD / MOBILE_MONEY
Status: Planned โ not yet implemented.
Inventory Management
Hold Mechanism
| Session Type | Hold Created At | Hold Released At |
|---|---|---|
| REGULAR_DIRECTLY | Session creation | Expiry, cancellation, or payment success (committed) |
| REGULAR_CART | Session creation | Expiry, cancellation, or payment success (committed) |
| GROUP_PURCHASE | After payment, at group level | Group expiry or group failure |
| INSTALLMENT | Session creation | Expiry, cancellation, or payment success (committed) |
On successful payment, holds are committed (stock permanently deducted). On failure/expiry/cancellation, holds are released (stock returned to available).
Session Expiration
Default: 15 minutes from creation.
Extended when: Payment retry is initiated (adds 15 minutes).
On expiry:
- Status โ EXPIRED
- Held inventory released
- Session cannot be updated, paid, or cancelled
- User must create a new checkout session
Payment Attempts Tracking
Maximum 5 attempts per session. Each attempt records:
- Attempt number (1โ5)
- Payment method used
- Status: SUCCESS, FAILED, or RETRY_INITIATED
- Error message (if failed)
- Timestamp and transaction ID
After 5 failed attempts, session status moves to EXPIRED and inventory is released.
Error Handling Best Practices
Frontend Checklist
Before calling POST /checkout-sessions:
- Ensure the user has a shipping address saved
- No need to pre-check balance โ the API returns rich balance data if insufficient
On 422 Insufficient Balance response:
- Read
data.shortfallto show how much the user is short - Read
data.recommendedTopUpto pre-fill a top-up amount - Navigate the user to the wallet top-up screen
- Once topped up, retry
POST /checkout-sessionsโ do not store the failed session
During active session (PENDING_PAYMENT):
- Show a countdown timer using
expiresAt - On expiry, prompt user to create a new session
On payment failure:
- Show
canRetryPaymentto decide whether to show a retry button - Show remaining attempts (
5 - paymentAttemptCount) - On retry, call
POST /{sessionId}/retry-paymentโ no need to create a new session
Integration Examples
Example 1: Direct Product Purchase
1. POST /checkout-sessions
โ 422 if balance insufficient (show top-up screen with data.recommendedTopUp)
โ 201 with sessionId if balance OK
2. POST /checkout-sessions/{sessionId}/process-payment
โ 200 with orderId on success
Example 2: Cart Checkout
1. POST /checkout-sessions { sessionType: REGULAR_CART, ... }
โ 422 or 201
2. (optional) PATCH /checkout-sessions/{sessionId} { shippingMethodId: "express" }
3. POST /checkout-sessions/{sessionId}/process-payment
Example 3: Group Purchase
1. POST /checkout-sessions { sessionType: GROUP_PURCHASE, groupName: "...", ... }
โ 422 if balance < groupPrice ร qty
โ 201 with sessionId
2. POST /checkout-sessions/{sessionId}/process-payment
(WALLET only)
Example 4: Installment
1. POST /checkout-sessions { sessionType: INSTALLMENT, installmentPlanId: "...", downPaymentPercent: 20 }
โ 422 if balance < downPaymentAmount
โ 201 with sessionId (pricing.total = down payment only)
2. POST /checkout-sessions/{sessionId}/process-payment
โ charges down payment only; monthly payments handled by scheduler
Example 5: Payment Retry
1. GET /checkout-sessions/active
โ find session with status: PAYMENT_FAILED, canRetryPayment: true
2. POST /checkout-sessions/{sessionId}/retry-payment
โ re-validates inventory + balance
โ re-holds inventory
โ processes payment
Rate Limiting
| Endpoint | Limit |
|---|---|
| Create Checkout | 20 req/min per user |
| Get Sessions | 60 req/min per user |
| Process / Retry Payment | 10 req/min per user |
| Update / Cancel | 30 req/min per user |
Group Purchase
Base URL: https://apinexgate.glueauth.com/api/v1/
Short Description: The Group Purchase API enables collaborative buying where multiple users join together to purchase products at discounted group prices. Users can create new groups, join existing groups, transfer between groups, and track their participations. The system automatically handles seat management, expiration, and order creation when groups are completed.
Hints:
- Groups automatically expire based on product's
groupTimeLimitHourssetting - Groups complete automatically when all seats are filled
- Users can join the same group multiple times to buy more seats (Hybrid Approach)
- Transfer between groups only allowed for same product, shop, and price
- Empty groups are automatically soft-deleted after all participants transfer out
- Group codes are auto-generated with format: GP-XXXXXX (6 random characters)
- Only WALLET payment method supported for group purchases
- Purchase and transfer history tracked for each participant
- Seats are released when users transfer out of groups
Standard Response Format
All API responses follow a consistent structure using our Globe Response Builder pattern:
Success Response Structure
{
"success": true,
"httpStatus": "OK",
"message": "Operation completed successfully",
"action_time": "2025-10-02T10:30:45",
"data": {
// Actual response data goes here
}
}
Error Response Structure
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Error description",
"action_time": "2025-10-02T10:30:45",
"data": "Error description"
}
Standard Response Fields
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true for successful operations, false for errors |
httpStatus |
string | HTTP status name (OK, BAD_REQUEST, NOT_FOUND, etc.) |
message |
string | Human-readable message describing the operation result |
action_time |
string | ISO 8601 timestamp of when the response was generated |
data |
object/string | Response payload for success, error details for failures |
HTTP Method Badge Standards
- GET - GET - Green (Safe, read-only operations)
- POST - POST - Blue (Create new resources)
Endpoints
1. Get Available Groups for Product
Purpose: Retrieves all available (open, not expired, not full) group purchase instances for a specific product.
Endpoint: GET {base_url}/group-purchases/product/{productId}/available
Access Level: ๐ Public (No Authentication Required)
Authentication: None
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authenticated user |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| productId | string (UUID) | Yes | Unique identifier of the product | Valid UUID format |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Available groups retrieved successfully",
"action_time": "2025-10-02T14:30:45",
"data": [
{
"groupInstanceId": "gp123456-7890-abcd-ef12-345678901234",
"groupCode": "GP-A3X7K9",
"productName": "Premium Wireless Headphones",
"productPrimaryMedia": {
"fileId": "9c1a2b3d-4e5f-6789-abcd-ef0123456789",
"mediaType": "IMAGE",
"status": "READY",
"thumbUrl": "https://cdn.nextgate.com/products/headphones-001/thumb.webp",
"mimeType": "image/webp"
},
"shopName": "TechWorld Electronics",
"groupPrice": 80000.00,
"savingsPercentage": 46.67,
"currency": "TZS",
"totalSeats": 10,
"seatsOccupied": 4,
"seatsRemaining": 6,
"totalParticipants": 3,
"progressPercentage": 40.00,
"status": "OPEN",
"expiresAt": "2025-10-02T20:30:45",
"isExpired": false,
"isUserMember": false,
"participants": [
{
"userId": "user1234-5678-90ab-cdef-123456789012",
"userName": "john_doe",
"userProfileMedia": {
"fileId": "3f4a5b6c-7d8e-9f01-abcd-ef2345678901",
"status": "READY",
"thumbUrl": "https://cdn.nextgate.com/profiles/john/thumb.webp",
"mimeType": "image/jpeg"
},
"quantity": 2,
"contributionPercentage": 50.00
},
{
"userId": "user2345-6789-01bc-def1-234567890123",
"userName": "jane_smith",
"userProfileMedia": {
"fileId": "4a5b6c7d-8e9f-0123-bcde-f34567890123",
"status": "READY",
"thumbUrl": "https://cdn.nextgate.com/profiles/jane/thumb.webp",
"mimeType": "image/jpeg"
},
"quantity": 1,
"contributionPercentage": 25.00
},
{
"userId": "user3456-7890-12cd-ef12-345678901234",
"userName": "bob_wilson",
"userProfileMedia": null,
"quantity": 1,
"contributionPercentage": 25.00
}
]
},
{
"groupInstanceId": "gp234567-8901-bcde-f123-456789012345",
"groupCode": "GP-B2Y8M5",
"productName": "Premium Wireless Headphones",
"productPrimaryMedia": {
"fileId": "9c1a2b3d-4e5f-6789-abcd-ef0123456789",
"mediaType": "IMAGE",
"status": "READY",
"thumbUrl": "https://cdn.nextgate.com/products/headphones-001/thumb.webp",
"mimeType": "image/webp"
},
"shopName": "TechWorld Electronics",
"groupPrice": 80000.00,
"savingsPercentage": 46.67,
"currency": "TZS",
"totalSeats": 10,
"seatsOccupied": 7,
"seatsRemaining": 3,
"totalParticipants": 5,
"progressPercentage": 70.00,
"status": "OPEN",
"expiresAt": "2025-10-02T18:45:30",
"isExpired": false,
"isUserMember": true,
"participants": [
{
"userId": "user4567-8901-23de-f234-567890123456",
"userName": "alice_brown",
"userProfileMedia": {
"fileId": "5b6c7d8e-9f01-2345-cdef-456789012345",
"status": "READY",
"thumbUrl": "https://cdn.nextgate.com/profiles/alice/thumb.webp",
"mimeType": "image/jpeg"
},
"quantity": 3,
"contributionPercentage": 42.86
}
]
}
]
}
Success Response Fields:
| Field | Description |
|---|---|
| groupInstanceId | Unique identifier for the group |
| groupCode | Human-readable group code (e.g., GP-A3X7K9) |
| productName | Name of the product in this group |
| productPrimaryMedia | Resolved primary media of the product โ {fileId, mediaType, status, thumbUrl, mimeType}. null if the product has no media yet |
| shopName | Shop selling this product |
| groupPrice | Discounted price per unit in TZS |
| savingsPercentage | Percentage saved vs regular price |
| currency | Currency code (TZS) |
| totalSeats | Maximum number of seats in this group |
| seatsOccupied | Number of seats currently filled |
| seatsRemaining | Available seats remaining |
| totalParticipants | Number of unique participants |
| progressPercentage | Group completion percentage (0-100) |
| status | Group status (OPEN, COMPLETED, FAILED, DELETED) |
| expiresAt | When the group expires |
| isExpired | Whether the group has expired |
| isUserMember | Whether authenticated user is in this group |
| participants | Array of participant previews |
| participants[].userId | Participant's user ID |
| participants[].userName | Participant's username |
| participants[].userProfileMedia | Resolved profile media โ {fileId, status, thumbUrl, mimeType}. null if no profile picture set |
| participants[].quantity | Number of seats this participant holds |
| participants[].contributionPercentage | Percentage of total seats this participant holds |
Error Response Examples:
Not Found - Product Not Found (404):
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "Product not found",
"action_time": "2025-10-02T14:30:45",
"data": "Product not found"
}
2. Get Group by ID
Purpose: Retrieves detailed information about a specific group purchase instance including all participants and their histories.
Endpoint: GET {base_url}/group-purchases/{groupId}
Access Level: ๐ Protected (Requires Authentication)
Authentication: Bearer Token required in Authorization header
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authenticated user |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| groupId | string (UUID) | Yes | Unique identifier of the group | Valid UUID format |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Group retrieved successfully",
"action_time": "2025-10-02T14:35:45",
"data": {
"groupInstanceId": "gp123456-7890-abcd-ef12-345678901234",
"groupCode": "GP-A3X7K9",
"productId": "prod1234-5678-90ab-cdef-123456789012",
"productName": "Premium Wireless Headphones",
"productPrimaryMedia": {
"fileId": "9c1a2b3d-4e5f-6789-abcd-ef0123456789",
"mediaType": "IMAGE",
"status": "READY",
"thumbUrl": "https://cdn.nextgate.com/products/headphones-001/thumb.webp",
"mimeType": "image/webp"
},
"shopId": "shop1234-5678-90ab-cdef-123456789012",
"shopName": "TechWorld Electronics",
"shopLogoMedia": {
"fileId": "1d2e3f4a-5b6c-7890-defa-bc1234567890",
"status": "READY",
"thumbUrl": "https://cdn.nextgate.com/shops/techworld/thumb.webp",
"mimeType": "image/jpeg"
},
"regularPrice": 150000.00,
"groupPrice": 80000.00,
"savingsAmount": 70000.00,
"savingsPercentage": 46.67,
"currency": "TZS",
"totalSeats": 10,
"seatsOccupied": 4,
"seatsRemaining": 6,
"totalParticipants": 3,
"progressPercentage": 40.00,
"status": "OPEN",
"isExpired": false,
"isFull": false,
"initiatorId": "user1234-5678-90ab-cdef-123456789012",
"initiatorName": "john_doe",
"durationHours": 24,
"createdAt": "2025-10-01T20:30:45",
"expiresAt": "2025-10-02T20:30:45",
"completedAt": null,
"maxPerCustomer": 5,
"isUserMember": true,
"myParticipantId": "part1234-5678-90ab-cdef-123456789012",
"myQuantity": 2,
"participants": [
{
"participantId": "part1234-5678-90ab-cdef-123456789012",
"userId": "user1234-5678-90ab-cdef-123456789012",
"userName": "john_doe",
"userProfileMedia": {
"fileId": "3f4a5b6c-7d8e-9f01-abcd-ef2345678901",
"status": "READY",
"thumbUrl": "https://cdn.nextgate.com/profiles/john/thumb.webp",
"mimeType": "image/jpeg"
},
"quantity": 2,
"totalPaid": 160000.00,
"status": "ACTIVE",
"joinedAt": "2025-10-01T20:30:45",
"contributionPercentage": 50.00,
"purchaseCount": 1,
"hasTransferred": false,
"purchaseHistory": [
{
"checkoutSessionId": "checkout1-2345-6789-0abc-def123456789",
"quantity": 2,
"amountPaid": 160000.00,
"purchasedAt": "2025-10-01T20:30:45",
"transactionId": "txn_1234567890abcdef"
}
],
"transferHistory": []
},
{
"participantId": "part2345-6789-01bc-def1-234567890123",
"userId": "user2345-6789-01bc-def1-234567890123",
"userName": "jane_smith",
"userProfileMedia": {
"fileId": "4a5b6c7d-8e9f-0123-bcde-f34567890123",
"status": "READY",
"thumbUrl": "https://cdn.nextgate.com/profiles/jane/thumb.webp",
"mimeType": "image/jpeg"
},
"quantity": 1,
"totalPaid": 80000.00,
"status": "ACTIVE",
"joinedAt": "2025-10-01T21:15:20",
"contributionPercentage": 25.00,
"purchaseCount": 1,
"hasTransferred": false
},
{
"participantId": "part3456-7890-12cd-ef12-345678901234",
"userId": "user3456-7890-12cd-ef12-345678901234",
"userName": "bob_wilson",
"userProfileMedia": null,
"quantity": 1,
"totalPaid": 80000.00,
"status": "ACTIVE",
"joinedAt": "2025-10-02T08:45:10",
"contributionPercentage": 25.00,
"purchaseCount": 1,
"hasTransferred": false
}
]
}
}
Success Response Fields:
| Field | Description |
|---|---|
| groupInstanceId | Unique identifier for the group |
| groupCode | Human-readable group code |
| productId | Product unique identifier |
| productName | Product name |
| productPrimaryMedia | Resolved primary media of the product โ {fileId, mediaType, status, thumbUrl, mimeType}. null if the product has no media yet |
| shopId | Shop identifier |
| shopName | Shop name |
| shopLogoMedia | Resolved shop logo โ {fileId, status, thumbUrl, mimeType}. null if the shop has no logo |
| regularPrice | Original product price in TZS |
| groupPrice | Discounted group price in TZS |
| savingsAmount | Amount saved per unit (regularPrice - groupPrice) |
| savingsPercentage | Percentage saved |
| currency | Currency code (TZS) |
| totalSeats | Maximum seats in group |
| seatsOccupied | Filled seats |
| seatsRemaining | Available seats |
| totalParticipants | Unique participants count |
| progressPercentage | Completion percentage |
| status | OPEN, COMPLETED, FAILED, or DELETED |
| isExpired | Whether group expired |
| isFull | Whether all seats filled |
| initiatorId | User who created the group |
| initiatorName | Initiator's username |
| durationHours | Group duration in hours |
| createdAt | Group creation timestamp |
| expiresAt | Expiration timestamp |
| completedAt | Completion timestamp (null if not completed) |
| maxPerCustomer | Maximum seats per customer |
| isUserMember | Whether authenticated user is member |
| myParticipantId | User's participant ID (if member) |
| myQuantity | User's seat quantity (if member) |
| participants | Array of detailed participant information |
| participants[].participantId | Participant unique identifier |
| participants[].userId | User ID |
| participants[].userName | Username |
| participants[].userProfileMedia | Resolved profile media โ {fileId, status, thumbUrl, mimeType}. null if no profile picture set |
| participants[].quantity | Number of seats held |
| participants[].totalPaid | Total amount paid in TZS |
| participants[].status | ACTIVE, TRANSFERRED_OUT, or REFUNDED |
| participants[].joinedAt | Join timestamp |
| participants[].contributionPercentage | Percentage of total seats |
| participants[].purchaseCount | Number of purchases made |
| participants[].hasTransferred | Whether participated in transfers |
| participants[].purchaseHistory | Purchase records (only shown to participant owner) |
| participants[].transferHistory | Transfer records (only shown to participant owner) |
Error Response Examples:
Not Found (404):
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "Group not found with ID: gp123456-7890-abcd-ef12-345678901234",
"action_time": "2025-10-02T14:35:45",
"data": "Group not found with ID: gp123456-7890-abcd-ef12-345678901234"
}
3. Get Group by Code
Purpose: Retrieves group information using the human-readable group code instead of UUID.
Endpoint: GET {base_url}/group-purchases/code/{groupCode}
Access Level: ๐ Protected (Requires Authentication)
Authentication: Bearer Token required in Authorization header
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authenticated user |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| groupCode | string | Yes | Group code (e.g., GP-A3X7K9) | Format: GP-XXXXXX (6 alphanumeric characters) |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Group retrieved successfully",
"action_time": "2025-10-02T14:40:45",
"data": {
"groupInstanceId": "gp123456-7890-abcd-ef12-345678901234",
"groupCode": "GP-A3X7K9",
"productId": "prod1234-5678-90ab-cdef-123456789012",
"productName": "Premium Wireless Headphones",
"productPrimaryMedia": {
"fileId": "9c1a2b3d-4e5f-6789-abcd-ef0123456789",
"mediaType": "IMAGE",
"status": "READY",
"thumbUrl": "https://cdn.nextgate.com/products/headphones-001/thumb.webp",
"mimeType": "image/webp"
},
"shopId": "shop1234-5678-90ab-cdef-123456789012",
"shopName": "TechWorld Electronics",
"shopLogoMedia": {
"fileId": "1d2e3f4a-5b6c-7890-defa-bc1234567890",
"status": "READY",
"thumbUrl": "https://cdn.nextgate.com/shops/techworld/thumb.webp",
"mimeType": "image/jpeg"
},
"regularPrice": 150000.00,
"groupPrice": 80000.00,
"savingsAmount": 70000.00,
"savingsPercentage": 46.67,
"currency": "TZS",
"totalSeats": 10,
"seatsOccupied": 4,
"seatsRemaining": 6,
"totalParticipants": 3,
"progressPercentage": 40.00,
"status": "OPEN",
"isExpired": false,
"isFull": false,
"initiatorId": "user1234-5678-90ab-cdef-123456789012",
"initiatorName": "john_doe",
"durationHours": 24,
"createdAt": "2025-10-01T20:30:45",
"expiresAt": "2025-10-02T20:30:45",
"completedAt": null,
"maxPerCustomer": 5,
"isUserMember": false,
"myParticipantId": null,
"myQuantity": null,
"participants": []
}
}
Success Response Fields:
| Field | Description |
|---|---|
| All fields | Same as "Get Group by ID" response |
Error Response Examples:
Not Found (404):
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "Group not found with code: GP-INVALID",
"action_time": "2025-10-02T14:40:45",
"data": "Group not found with code: GP-INVALID"
}
4. Get My Groups
Purpose: Retrieves all groups that the authenticated user is a member of, optionally filtered by status.
Endpoint: GET {base_url}/group-purchases/my-groups
Access Level: ๐ Protected (Requires Authentication)
Authentication: Bearer Token required in Authorization header
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authenticated user |
Query Parameters:
| Parameter | Type | Required | Description | Validation | Default |
|---|---|---|---|---|---|
| status | string | No | Filter by group status | enum: OPEN, COMPLETED, FAILED, DELETED | null (all statuses) |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "My groups retrieved successfully",
"action_time": "2025-10-02T14:45:45",
"data": [
{
"groupInstanceId": "gp123456-7890-abcd-ef12-345678901234",
"groupCode": "GP-A3X7K9",
"productName": "Premium Wireless Headphones",
"productPrimaryMedia": {
"fileId": "9c1a2b3d-4e5f-6789-abcd-ef0123456789",
"mediaType": "IMAGE",
"status": "READY",
"thumbUrl": "https://cdn.nextgate.com/products/headphones-001/thumb.webp",
"mimeType": "image/webp"
},
"shopName": "TechWorld Electronics",
"groupPrice": 80000.00,
"savingsPercentage": 46.67,
"currency": "TZS",
"totalSeats": 10,
"seatsOccupied": 8,
"seatsRemaining": 2,
"totalParticipants": 5,
"progressPercentage": 80.00,
"status": "OPEN",
"expiresAt": "2025-10-02T20:30:45",
"isExpired": false,
"isUserMember": true,
"participants": [
{
"userId": "user1234-5678-90ab-cdef-123456789012",
"userName": "john_doe",
"userProfileMedia": {
"fileId": "3f4a5b6c-7d8e-9f01-abcd-ef2345678901",
"status": "READY",
"thumbUrl": "https://cdn.nextgate.com/profiles/john/thumb.webp",
"mimeType": "image/jpeg"
},
"quantity": 2,
"contributionPercentage": 25.00
}
]
},
{
"groupInstanceId": "gp234567-8901-bcde-f123-456789012345",
"groupCode": "GP-B2Y8M5",
"productName": "Smart Watch Series 5",
"productPrimaryMedia": {
"fileId": "2a3b4c5d-6e7f-8901-bcde-f01234567890",
"mediaType": "IMAGE",
"status": "READY",
"thumbUrl": "https://cdn.nextgate.com/products/watch-005/thumb.webp",
"mimeType": "image/webp"
},
"shopName": "Gadget Hub",
"groupPrice": 250000.00,
"savingsPercentage": 28.57,
"currency": "TZS",
"totalSeats": 15,
"seatsOccupied": 15,
"seatsRemaining": 0,
"totalParticipants": 8,
"progressPercentage": 100.00,
"status": "COMPLETED",
"expiresAt": "2025-10-01T18:20:30",
"isExpired": false,
"isUserMember": true,
"participants": []
}
]
}
Success Response Fields:
| Field | Description |
|---|---|
| All fields | Same as "Get Available Groups for Product" response |
5. Get My Participations
Purpose: Retrieves all active participations of the authenticated user across all groups with detailed participant information.
Endpoint: GET {base_url}/group-purchases/my-participations
Access Level: ๐ Protected (Requires Authentication)
Authentication: Bearer Token required in Authorization header
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authenticated user |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "My participations retrieved successfully",
"action_time": "2025-10-02T14:50:45",
"data": [
{
"participantId": "part1234-5678-90ab-cdef-123456789012",
"userId": "user1234-5678-90ab-cdef-123456789012",
"userName": "john_doe",
"userProfileMedia": {
"fileId": "3f4a5b6c-7d8e-9f01-abcd-ef2345678901",
"status": "READY",
"thumbUrl": "https://cdn.nextgate.com/profiles/john/thumb.webp",
"mimeType": "image/jpeg"
},
"quantity": 2,
"totalPaid": 160000.00,
"status": "ACTIVE",
"joinedAt": "2025-10-01T20:30:45",
"purchaseCount": 1,
"hasTransferred": false,
"checkoutSessionId": "checkout1-2345-6789-0abc-def123456789",
"purchaseHistory": [
{
"checkoutSessionId": "checkout1-2345-6789-0abc-def123456789",
"quantity": 2,
"amountPaid": 160000.00,
"purchasedAt": "2025-10-01T20:30:45",
"transactionId": "txn_1234567890abcdef"
}
],
"transferHistory": []
},
{
"participantId": "part2345-6789-01bc-def1-234567890123",
"userId": "user1234-5678-90ab-cdef-123456789012",
"userName": "john_doe",
"userProfileMedia": {
"fileId": "3f4a5b6c-7d8e-9f01-abcd-ef2345678901",
"status": "READY",
"thumbUrl": "https://cdn.nextgate.com/profiles/john/thumb.webp",
"mimeType": "image/jpeg"
},
"quantity": 3,
"totalPaid": 750000.00,
"status": "ACTIVE",
"joinedAt": "2025-10-02T10:15:20",
"purchaseCount": 2,
"hasTransferred": true,
"checkoutSessionId": "checkout2-3456-7890-1bcd-ef2345678901",
"purchaseHistory": [
{
"checkoutSessionId": "checkout2-3456-7890-1bcd-ef2345678901",
"quantity": 1,
"amountPaid": 250000.00,
"purchasedAt": "2025-10-02T10:15:20",
"transactionId": "txn_2345678901bcdef0"
},
{
"checkoutSessionId": "checkout3-4567-8901-2cde-f34567890123",
"quantity": 2,
"amountPaid": 500000.00,
"purchasedAt": "2025-10-02T12:30:15",
"transactionId": "txn_3456789012cdef01"
}
],
"transferHistory": [
{
"fromGroupId": "gp345678-9012-cdef-1234-567890123456",
"fromGroupCode": null,
"toGroupId": "gp234567-8901-bcde-f123-456789012345",
"toGroupCode": null,
"transferredAt": "2025-10-02T11:45:30",
"reason": "Transferred 1 seats from group GP-C3Z9N7"
}
]
}
]
}
Success Response Fields:
| Field | Description |
|---|---|
| participantId | Participant unique identifier |
| userId | User ID of the participant |
| userName | Username |
| userProfileMedia | Resolved profile media โ {fileId, status, thumbUrl, mimeType}. null if no profile picture set |
| quantity | Total seats held |
| totalPaid | Total amount paid in TZS |
| status | ACTIVE, TRANSFERRED_OUT, or REFUNDED |
| joinedAt | When user joined this group |
| purchaseCount | Number of purchases made in this group |
| hasTransferred | Whether user has transfer history |
| checkoutSessionId | Original checkout session ID |
| purchaseHistory | Array of all purchases in this group |
| purchaseHistory[].checkoutSessionId | Checkout session for this purchase |
| purchaseHistory[].quantity | Seats purchased |
| purchaseHistory[].amountPaid | Amount paid for this purchase |
| purchaseHistory[].purchasedAt | Purchase timestamp |
| purchaseHistory[].transactionId | Payment transaction ID |
| transferHistory | Array of all transfers involving this participation |
| transferHistory[].fromGroupId | Source group ID |
| transferHistory[].toGroupId | Target group ID |
| transferHistory[].transferredAt | Transfer timestamp |
| transferHistory[].reason | Transfer reason/description |
6. Transfer Seats Between Groups
Purpose: Transfers seats from one group to another. Allows users to move their purchases between compatible groups (same product, shop, and price).
Endpoint: POST {base_url}/group-purchases/transfer
Access Level: ๐ Protected (Requires Authentication)
Authentication: Bearer Token required in Authorization header
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authenticated user |
| Content-Type | string | Yes | Must be application/json |
Request JSON Sample:
{
"sourceGroupId": "gp123456-7890-abcd-ef12-345678901234",
"targetGroupId": "gp234567-8901-bcde-f123-456789012345",
"quantity": 2
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| sourceGroupId | string (UUID) | Yes | Group to transfer from | Valid UUID, user must be active member |
| targetGroupId | string (UUID) | Yes | Group to transfer to | Valid UUID, must be different from source |
| quantity | integer | Yes | Number of seats to transfer | Min: 1, cannot exceed user's quantity in source group |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Seats transferred successfully",
"action_time": "2025-10-02T15:00:45",
"data": {
"participantId": "part2345-6789-01bc-def1-234567890123",
"userId": "user1234-5678-90ab-cdef-123456789012",
"userName": "john_doe",
"userProfileMedia": {
"fileId": "3f4a5b6c-7d8e-9f01-abcd-ef2345678901",
"status": "READY",
"thumbUrl": "https://cdn.nextgate.com/profiles/john/thumb.webp",
"mimeType": "image/jpeg"
},
"quantity": 3,
"totalPaid": 0.00,
"status": "ACTIVE",
"joinedAt": "2025-10-02T15:00:45",
"purchaseCount": 0,
"hasTransferred": true,
"checkoutSessionId": "checkout1-2345-6789-0abc-def123456789",
"purchaseHistory": [],
"transferHistory": [
{
"fromGroupId": "gp123456-7890-abcd-ef12-345678901234",
"fromGroupCode": null,
"toGroupId": "gp234567-8901-bcde-f123-456789012345",
"toGroupCode": null,
"transferredAt": "2025-10-02T15:00:45",
"reason": "Transferred 2 seats from group GP-A3X7K9"
}
]
}
}
Success Response Fields:
| Field | Description |
|---|---|
| participantId | Updated participant ID in target group |
| userId | User ID |
| userName | Username |
| userProfileMedia | Resolved profile media โ {fileId, status, thumbUrl, mimeType}. null if no profile picture set |
| quantity | New total quantity in target group |
| totalPaid | Total paid (0 for transfers) |
| status | Participant status (ACTIVE) |
| joinedAt | Join timestamp (current time if new to target) |
| purchaseCount | Purchase count (0 for pure transfers) |
| hasTransferred | Always true for transferred participants |
| checkoutSessionId | Original checkout session ID |
| purchaseHistory | Purchase history (shown only to owner) |
| transferHistory | Transfer history including this transfer |
Error Response Examples:
Bad Request - Same Source and Target (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Source and target groups must be different",
"action_time": "2025-10-02T15:00:45",
"data": "Source and target groups must be different"
}
Bad Request - Insufficient Seats (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Not enough seats to transfer. You have: 1, requested: 2",
"action_time": "2025-10-02T15:00:45",
"data": "Not enough seats to transfer. You have: 1, requested: 2"
}
Bad Request - Target Group Full (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Not enough seats available. Requested: 2, Available: 1",
"action_time": "2025-10-02T15:00:45",
"data": "Not enough seats available. Requested: 2, Available: 1"
}
Bad Request - Product Mismatch (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Cannot transfer between groups with different products",
"action_time": "2025-10-02T15:00:45",
"data": "Cannot transfer between groups with different products"
}
Bad Request - Price Mismatch (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Cannot transfer. Price mismatch: 80000.00 vs 75000.00",
"action_time": "2025-10-02T15:00:45",
"data": "Cannot transfer. Price mismatch: 80000.00 vs 75000.00"
}
Not Found - Not a Participant (404):
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "You are not a participant in the source group",
"action_time": "2025-10-02T15:00:45",
"data": "You are not a participant in the source group"
}
7. Update Group Name
Purpose: Allow the group initiator to update the group name. Group names must be unique among active (OPEN) groups.
Endpoint: PATCH {base_url}/group-purchases/{groupId}/name
Access Level: ๐ Protected (Requires Authentication - Group Initiator Only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
| Content-Type | string | Yes | application/json |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| groupId | UUID | Yes | Unique identifier of the group | Valid UUID format |
Request Body:
| Field | Type | Required | Description | Validation |
|---|---|---|---|---|
| groupName | string | Yes | New name for the group | 3-100 characters, unique among active groups |
Request JSON Sample:
{
"groupName": "iPhone 15 Pro - Dar es Salaam Deal"
}
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Group name updated successfully",
"action_time": "2025-12-30T10:00:00",
"data": null
}
Success Response Fields:
| Field | Description |
|---|---|
data |
Always null โ the endpoint confirms the rename via message only. Fetch Get Group by ID to see the updated groupName |
Error Response JSON Samples:
Group not found:
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "Group not found",
"action_time": "2025-12-30T10:00:00",
"data": null
}
Not the initiator:
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Only the group initiator can change the group name",
"action_time": "2025-12-30T10:00:00",
"data": null
}
Group not active:
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Cannot rename group with status: COMPLETED",
"action_time": "2025-12-30T10:00:00",
"data": null
}
Group expired:
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Cannot rename expired group",
"action_time": "2025-12-30T10:00:00",
"data": null
}
Invalid name length:
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Group name must be between 3 and 100 characters",
"action_time": "2025-12-30T10:00:00",
"data": null
}
Name already taken:
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Group name already taken: iPhone 15 Pro - Dar es Salaam Deal",
"action_time": "2025-12-30T10:00:00",
"data": null
}
Standard Error Types:
| Status | Message | Cause |
|---|---|---|
400 BAD_REQUEST |
Only the group initiator can change the group name | User is not the group initiator |
400 BAD_REQUEST |
Cannot rename group with status: {status} | Group is not OPEN |
400 BAD_REQUEST |
Cannot rename deleted group | Group has been deleted |
400 BAD_REQUEST |
Cannot rename expired group | Group has expired |
400 BAD_REQUEST |
Group name must be between 3 and 100 characters | Invalid name length |
400 BAD_REQUEST |
Group name already taken: {name} | Name exists on another active group |
401 UNAUTHORIZED |
User not authenticated | Missing or invalid token |
404 NOT_FOUND |
Group not found | Invalid group ID |
Business Rules:
| Rule | Description |
|---|---|
| Initiator only | Only the user who created the group can rename it |
| Active groups only | Group must have status OPEN |
| Not expired | Group must not be past its expiration time |
| Not deleted | Group must not be soft-deleted |
| Unique name | Name must be unique among all active (OPEN) groups |
| Name length | Must be between 3 and 100 characters |
| Trimmed | Leading/trailing whitespace is automatically removed |
Default Group Name: When a group is created, the name is auto-generated as:
{groupCode}-{productName}
Example: GP-ABC123-iPhone 15 Pro
Usage Example:
curl -X PATCH \
'https://api.nexgate.com/api/v1/groups/550e8400-e29b-41d4-a716-446655440000/name' \
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIs...' \
-H 'Content-Type: application/json' \
-d '{
"groupName": "iPhone 15 Pro - Dar es Salaam Deal"
}'
Group Purchase Workflow
1. Creating a New Group
When a user makes a GROUP_PURCHASE checkout and no groupInstanceId is provided in metadata:
Flow:
- User creates checkout session with
sessionType: GROUP_PURCHASE - User processes payment (WALLET only)
- Payment completes successfully
- System automatically creates new group instance
- User becomes first participant (initiator)
- Group gets unique code (e.g., GP-A3X7K9)
- Group status set to OPEN
- Expiration set based on product's
groupTimeLimitHours
2. Joining an Existing Group
When a user makes a GROUP_PURCHASE checkout with groupInstanceId in metadata:
Flow:
- User finds available group (via product page or group code)
- User creates checkout session with
sessionType: GROUP_PURCHASE - User includes
groupInstanceIdin checkout metadata - User processes payment (WALLET only)
- Payment completes successfully
- System adds user to existing group
- Group's
seatsOccupiedincreases - Group's
totalParticipantsincreases (if new member)
Note: User can join same group multiple times to buy more seats (Hybrid Approach)
3. Group Completion
When a group fills all seats:
Automatic Actions:
- Group status changes to COMPLETED
completedAttimestamp recorded- Orders created for all participants
- Inventory permanently deducted
- No new participants allowed
4. Group Expiration
When a group reaches expiration time without filling:
Automatic Actions:
- Group status changes to FAILED
- All participants refunded
- Inventory holds released
- Participant status changes to REFUNDED
5. Transferring Between Groups
Users can transfer seats between compatible groups:
Compatibility Requirements:
- Same product
- Same shop
- Same group price
- Target group must be OPEN
- Target group not expired
- Target group has available seats
Transfer Types:
Partial Transfer:
- Transfer some seats, keep some in source
- User remains ACTIVE in both groups
- Source group seats reduced
- Target group seats increased
Full Transfer:
- Transfer all seats from source
- Source participant status โ TRANSFERRED_OUT
- Source group participants count decreased
- If source group becomes empty โ soft deleted
Group Status Definitions
| Status | Description | Can Join? | Can Transfer From? | Can Transfer To? |
|---|---|---|---|---|
| OPEN | Active, accepting participants | Yes | Yes | Yes |
| COMPLETED | All seats filled, orders created | No | No | No |
| FAILED | Expired without filling, refunds issued | No | No | No |
| DELETED | Soft deleted (empty or admin action) | No | No | No |
Participant Status Definitions
| Status | Description | Still in Group? | Can Buy More? | Can Transfer? |
|---|---|---|---|---|
| ACTIVE | Currently participating in group | Yes | Yes | Yes |
| TRANSFERRED_OUT | Left this group via transfer | No | No | No |
| REFUNDED | Group failed, money refunded | No | No | No |
Purchase History Tracking
Each participant maintains detailed purchase history:
Tracked Information:
- Checkout session ID
- Quantity purchased
- Amount paid
- Purchase timestamp
- Transaction ID
Use Cases:
- User buys 2 seats initially
- User joins same group again, buys 3 more seats
- Purchase history shows 2 separate purchases
- Total quantity: 5 seats
- Total paid: sum of both purchases
Transfer History Tracking
Each transfer is recorded in participant history:
Tracked Information:
- Source group ID and code
- Target group ID and code
- Transfer timestamp
- Transfer reason/description
Transfer Scenarios:
Scenario 1: Partial Transfer
- User has 5 seats in Group A
- Transfers 2 seats to Group B
- Group A participant: 3 seats, ACTIVE status
- Group B participant: 2 seats, ACTIVE status, transfer history added
Scenario 2: Full Transfer
- User has 3 seats in Group A
- Transfers all 3 seats to Group B
- Group A participant: 0 seats, TRANSFERRED_OUT status
- Group B participant: 3 seats, ACTIVE status, transfer history added
Scenario 3: Multiple Transfers
- User transfers from Group A to Group B
- Later transfers from Group B to Group C
- Full transfer history maintained in each participation
Group Expiration and Cleanup
Automatic Expiration
Scheduled Job runs periodically to:
- Find groups with status=OPEN and expiresAt < now
- Change status to FAILED
- Initiate refunds for all participants
- Update participant status to REFUNDED
- Release inventory holds
Soft Deletion
Groups are soft-deleted when:
- All participants transfer out (empty group)
- Admin manually deletes group
Soft Delete Actions:
isDeletedset to truestatuschanged to DELETEDdeletedAttimestamp recordeddeletedByuser ID recordeddeleteReasonstored- Group still queryable but excluded from active lists
Business Rules
Maximum Seats Per Customer
If product has maxPerCustomer limit:
- Single purchase cannot exceed limit
- Multiple purchases in same group respect limit
- Transfer to group validates combined quantity
Example:
- Product has maxPerCustomer = 5
- User has 3 seats in Group A
- Tries to transfer to Group B where they have 3 seats
- Transfer rejected (3 + 3 = 6 > 5)
Group Price Lock
Group price is snapshot at creation:
- Price stored in group instance
- Transfers validate price match
- Product price changes don't affect existing groups
Inventory Management
During Group Lifecycle:
- Seats held in inventory when purchased
- Holds maintained until group completes or fails
- Completed: inventory permanently deducted
- Failed: inventory holds released
Transfer Impact:
- No inventory change during transfer
- Total inventory hold remains same
- Just moves between groups
Integration with Checkout Sessions
Creating New Group
Checkout Session Requirements:
sessionType: GROUP_PURCHASE- Exactly 1 item
- WALLET payment only
- No
groupInstanceIdin metadata - Status: PAYMENT_COMPLETED
After Payment Success:
GroupPurchaseInstanceEntity group = groupPurchaseService.createGroupInstance(checkoutSession);
Joining Existing Group
Checkout Session Requirements:
sessionType: GROUP_PURCHASE- Exactly 1 item
- WALLET payment only
groupInstanceIdin metadata- Status: PAYMENT_COMPLETED
After Payment Success:
UUID groupId = (UUID) checkoutSession.getMetadata().get("groupInstanceId");
GroupPurchaseInstanceEntity group = groupPurchaseService.joinGroup(groupId, checkoutSession);
Error Handling Best Practices
Common Error Scenarios
Product Not Available for Group Buying:
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Group buying is not enabled for this product"
}
Action: Check product has groupBuyingEnabled: true
Group Expired:
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Group has expired at: 2025-10-02T20:30:45"
}
Action: Find another available group or create new group
Group Full:
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Group is full. Seats occupied: 10/10"
}
Action: Find another available group or create new group
Quantity Exceeds Group Size:
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Quantity (8) exceeds group max size (5)"
}
Action: Reduce quantity or create multiple purchases
Transfer Between Incompatible Groups:
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Cannot transfer between groups from different shops"
}
Action: Only transfer between compatible groups
Quick Reference Guide
Endpoint Summary
| Endpoint | Method | Purpose |
|---|---|---|
/group-purchases/product/{productId}/available |
GET | Get available groups for product |
/group-purchases/{groupId} |
GET | Get group details by ID |
/group-purchases/code/{groupCode} |
GET | Get group details by code |
/group-purchases/my-groups |
GET | Get user's groups |
/group-purchases/my-participations |
GET | Get user's participations |
/group-purchases/transfer |
POST | Transfer seats between groups |
Common HTTP Status Codes
200 OK: Successful operation400 Bad Request: Validation error or business rule violation401 Unauthorized: Authentication required404 Not Found: Group, product, or participation not found
Group Code Format
- Pattern:
GP-XXXXXX - Length: 9 characters (GP- + 6 alphanumeric)
- Example:
GP-A3X7K9,GP-B2Y8M5 - Auto-generated at group creation
Participant Contribution Calculation
contributionPercentage = (participantQuantity / totalSeatsOccupied) ร 100
Progress Calculation
progressPercentage = (seatsOccupied / totalSeats) ร 100
Savings Calculation
savingsAmount = regularPrice - groupPrice
savingsPercentage = (savingsAmount / regularPrice) ร 100
Testing
Test Scenarios
Scenario 1: Create and Complete Group
- User A creates group (2 seats)
- User B joins group (3 seats)
- User C joins group (5 seats)
- Group auto-completes
- Orders created for all participants
Scenario 2: Transfer Between Groups
- User A in Group 1 (3 seats)
- User A transfers 2 seats to Group 2
- User A remains in Group 1 (1 seat)
- User A now in Group 2 (2 seats)
Scenario 3: Group Expiration
- Create group with 1-minute expiration
- Wait for expiration
- Scheduled job processes
- Status โ FAILED
- Participants refunded
Scenario 4: Hybrid Approach - Multiple Purchases
- User A creates group (2 seats)
- User A joins same group again (3 seats)
- User A total: 5 seats
- Purchase history shows 2 records
Installment Purchase
Flexible payment plans with 7+ frequencies, custom intervals, and 2-4 plans per product. Features transparent amortization schedules, 75% early payoff interest discount, and two fulfillment options (immediate or after payment). Includes configurable 0-60 day grace periods, automated payment processing via JobRunr, up to 5 retry attempts for failed payments, real-time tracking of payment history and agreement status, and full admin control for plan management.
Installment Purchase - Customer Endpoints
Base URL: https://api.nextgate.com/api/v1/installments
Short Description: This API provides endpoints for customers to manage their installment purchase agreements. Customers can view their agreements, track payment schedules, make manual payments, retry failed payments, calculate and process early payoffs, and cancel agreements. All endpoints require authentication and operate on the principle that customers can only access their own agreement data.
Hints:
- All customer endpoints require authentication via Bearer token
- Customers can only access their own agreements (ownership validation applied)
- Payment processing uses wallet-based transactions
- Early payoff provides 75% discount on remaining interest
- Agreements can only be cancelled before the first payment is completed
- Failed payments can be retried up to 5 times
- Grace periods apply before first payment due date
- All amounts are in TZS (Tanzanian Shillings)
Standard Response Format
All API responses follow a consistent structure using our Globe Response Builder pattern:
Success Response Structure
{
"success": true,
"httpStatus": "OK",
"message": "Operation completed successfully",
"action_time": "2025-10-18T10:30:45",
"data": {
// Actual response data goes here
}
}
Error Response Structure
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Error description",
"action_time": "2025-10-18T10:30:45",
"data": "Error description"
}
Standard Response Fields
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true for successful operations, false for errors |
httpStatus |
string | HTTP status name (OK, BAD_REQUEST, NOT_FOUND, etc.) |
message |
string | Human-readable message describing the operation result |
action_time |
string | ISO 8601 timestamp of when the response was generated |
data |
object/string | Response payload for success, error details for failures |
HTTP Method Badge Standards
For better visual clarity, all endpoints use colored badges for HTTP methods with the following standard colors:
- GET - GET - Green (Safe, read-only operations)
- POST - POST - Blue (Create new resources)
- DELETE - DELETE - Red (Remove resources)
Endpoints
1. Get My Agreements
Purpose: Retrieve all installment agreements for the authenticated customer, optionally filtered by status
Endpoint: GET {base_url}/my-agreements
Access Level: ๐ Protected (Requires Authentication)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
Query Parameters:
| Parameter | Type | Required | Description | Validation | Default |
|---|---|---|---|---|---|
| status | string | No | Filter by agreement status | enum: PENDING_FIRST_PAYMENT, ACTIVE, COMPLETED, DEFAULTED, CANCELLED | null (all statuses) |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Agreements retrieved successfully",
"action_time": "2025-10-18T10:30:45",
"data": [
{
"agreementId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"agreementNumber": "INST-2025-12345",
"productId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"productName": "Samsung Galaxy S24 Ultra",
"productImage": "https://cdn.example.com/products/samsung-s24.jpg",
"shopId": "8d3a7b12-9c4e-4f8a-b5d2-3e6f7a8b9c0d",
"shopName": "Tech World Store",
"totalAmount": 2500000.00,
"amountPaid": 500000.00,
"amountRemaining": 2000000.00,
"currency": "TZS",
"paymentsCompleted": 1,
"paymentsRemaining": 11,
"totalPayments": 12,
"progressPercentage": 20.0,
"nextPaymentDate": "2025-11-18T00:00:00",
"nextPaymentAmount": 166666.67,
"agreementStatus": "ACTIVE",
"agreementStatusDisplay": "ACTIVE",
"createdAt": "2025-10-18T09:00:00",
"completedAt": null,
"canMakeEarlyPayment": true,
"canCancel": false
}
]
}
Success Response Fields:
| Field | Description |
|---|---|
| agreementId | Unique identifier for the agreement |
| agreementNumber | Human-readable agreement number (format: INST-YYYY-NNNNN) |
| productId | ID of the product being purchased |
| productName | Name of the product |
| productImage | URL to product image |
| shopId | ID of the shop selling the product |
| shopName | Name of the shop |
| totalAmount | Total amount to be paid including interest |
| amountPaid | Amount paid so far |
| amountRemaining | Amount still owed |
| currency | Currency code (TZS) |
| paymentsCompleted | Number of payments made |
| paymentsRemaining | Number of payments left |
| totalPayments | Total number of scheduled payments |
| progressPercentage | Percentage of payments completed |
| nextPaymentDate | Date when next payment is due |
| nextPaymentAmount | Amount of next payment |
| agreementStatus | Current status of the agreement |
| agreementStatusDisplay | Human-readable status |
| createdAt | When agreement was created |
| completedAt | When agreement was completed (null if not completed) |
| canMakeEarlyPayment | Whether early payoff is allowed |
| canCancel | Whether agreement can be cancelled |
Error Response JSON Sample:
{
"success": false,
"httpStatus": "UNAUTHORIZED",
"message": "Token has expired",
"action_time": "2025-10-18T10:30:45",
"data": "Token has expired"
}
Standard Error Types:
Application-Level Exceptions (400-499)
Error Response Examples:
{
"success": false,
"httpStatus": "UNAUTHORIZED",
"message": "Token has expired",
"action_time": "2025-10-18T10:30:45",
"data": "Token has expired"
}
Not Found (404):
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "User not found",
"action_time": "2025-10-18T10:30:45",
"data": "User not found"
}
2. Get My Active Agreements
Purpose: Retrieve only active installment agreements for the authenticated customer
Endpoint: GET {base_url}/my-agreements/active
Access Level: ๐ Protected (Requires Authentication)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Active agreements retrieved successfully",
"action_time": "2025-10-18T10:30:45",
"data": [
{
"agreementId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"agreementNumber": "INST-2025-12345",
"productId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"productName": "Samsung Galaxy S24 Ultra",
"productImage": "https://cdn.example.com/products/samsung-s24.jpg",
"shopId": "8d3a7b12-9c4e-4f8a-b5d2-3e6f7a8b9c0d",
"shopName": "Tech World Store",
"totalAmount": 2500000.00,
"amountPaid": 500000.00,
"amountRemaining": 2000000.00,
"currency": "TZS",
"paymentsCompleted": 1,
"paymentsRemaining": 11,
"totalPayments": 12,
"progressPercentage": 20.0,
"nextPaymentDate": "2025-11-18T00:00:00",
"nextPaymentAmount": 166666.67,
"agreementStatus": "ACTIVE",
"agreementStatusDisplay": "ACTIVE",
"createdAt": "2025-10-18T09:00:00",
"completedAt": null,
"canMakeEarlyPayment": true,
"canCancel": false
}
]
}
Success Response Fields: Same as Get My Agreements endpoint
Error Response Examples: Same error types as Get My Agreements endpoint
3. Get Agreement By ID
Purpose: Retrieve detailed information about a specific installment agreement by its ID
Endpoint: GET {base_url}/agreements/{agreementId}
Access Level: ๐ Protected (Requires Authentication, Owner Only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| agreementId | UUID | Yes | Unique identifier of the agreement | Valid UUID format |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Agreement details retrieved successfully",
"action_time": "2025-10-18T10:30:45",
"data": {
"agreementId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"agreementNumber": "INST-2025-12345",
"customerId": "9b2e4d56-7c8a-4f9b-a3d1-5e6f7a8b9c0d",
"customerName": "John Doe",
"customerEmail": "john.doe@example.com",
"productId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"productName": "Samsung Galaxy S24 Ultra",
"productImage": "https://cdn.example.com/products/samsung-s24.jpg",
"productPrice": 2000000.00,
"quantity": 1,
"shopId": "8d3a7b12-9c4e-4f8a-b5d2-3e6f7a8b9c0d",
"shopName": "Tech World Store",
"selectedPlanId": "4b5c6d7e-8f9a-4b1c-9d2e-3f4a5b6c7d8e",
"planName": "12 Month Standard Plan",
"paymentFrequency": "MONTHLY",
"paymentFrequencyDisplay": "Monthly",
"customFrequencyDays": null,
"numberOfPayments": 12,
"duration": "12 months",
"apr": 15.00,
"gracePeriodDays": 30,
"downPaymentAmount": 400000.00,
"financedAmount": 1600000.00,
"monthlyPaymentAmount": 166666.67,
"totalInterestAmount": 400000.00,
"totalAmount": 2400000.00,
"currency": "TZS",
"paymentsCompleted": 1,
"paymentsRemaining": 11,
"amountPaid": 566666.67,
"amountRemaining": 1833333.33,
"progressPercentage": 8.33,
"nextPaymentDate": "2025-11-18T00:00:00",
"nextPaymentAmount": 166666.67,
"agreementStatus": "ACTIVE",
"defaultCount": 0,
"createdAt": "2025-10-18T09:00:00",
"firstPaymentDate": "2025-11-18T00:00:00",
"lastPaymentDate": "2026-10-18T00:00:00",
"completedAt": null,
"fulfillmentTiming": "IMMEDIATE",
"shippedAt": "2025-10-18T14:00:00",
"deliveredAt": null,
"orderId": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"shippingAddress": {
"fullName": "John Doe",
"phoneNumber": "+255712345678",
"street": "123 Main Street",
"city": "Dar es Salaam",
"state": "Dar es Salaam",
"postalCode": "12345",
"country": "Tanzania"
},
"billingAddress": {
"fullName": "John Doe",
"phoneNumber": "+255712345678",
"street": "123 Main Street",
"city": "Dar es Salaam",
"state": "Dar es Salaam",
"postalCode": "12345",
"country": "Tanzania"
},
"payments": [
{
"paymentId": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
"paymentNumber": 1,
"scheduledAmount": 166666.67,
"paidAmount": 166666.67,
"principalPortion": 146666.67,
"interestPortion": 20000.00,
"remainingBalance": 1453333.33,
"lateFee": null,
"currency": "TZS",
"paymentStatus": "COMPLETED",
"paymentStatusDisplay": "COMPLETED",
"dueDate": "2025-11-18T00:00:00",
"paidAt": "2025-11-18T10:30:00",
"attemptedAt": "2025-11-18T10:29:45",
"paymentMethod": "WALLET",
"transactionId": "TXN-2025-67890",
"failureReason": null,
"retryCount": 0,
"daysUntilDue": null,
"daysOverdue": null,
"canPay": false,
"canRetry": false
}
],
"canMakeEarlyPayment": true,
"canCancel": false,
"canUpdatePaymentMethod": false
}
}
Success Response Fields:
| Field | Description |
|---|---|
| agreementId | Unique identifier for the agreement |
| agreementNumber | Human-readable agreement number |
| customerId | ID of the customer |
| customerName | Customer's full name |
| customerEmail | Customer's email address |
| productId | ID of the product |
| productName | Name of the product |
| productImage | URL to product image |
| productPrice | Original product price |
| quantity | Quantity purchased |
| shopId | ID of the shop |
| shopName | Name of the shop |
| selectedPlanId | ID of the selected installment plan |
| planName | Name of the installment plan |
| paymentFrequency | Payment frequency enum value |
| paymentFrequencyDisplay | Human-readable payment frequency |
| customFrequencyDays | Custom frequency in days (if applicable) |
| numberOfPayments | Total number of payments |
| duration | Human-readable duration |
| apr | Annual Percentage Rate |
| gracePeriodDays | Grace period before first payment |
| downPaymentAmount | Down payment amount |
| financedAmount | Amount being financed |
| monthlyPaymentAmount | Amount per payment |
| totalInterestAmount | Total interest to be paid |
| totalAmount | Grand total (product + interest) |
| currency | Currency code |
| paymentsCompleted | Number of completed payments |
| paymentsRemaining | Number of remaining payments |
| amountPaid | Total amount paid so far |
| amountRemaining | Total amount remaining |
| progressPercentage | Percentage of completion |
| nextPaymentDate | Next payment due date |
| nextPaymentAmount | Next payment amount |
| agreementStatus | Current agreement status |
| defaultCount | Number of missed payments |
| createdAt | Agreement creation timestamp |
| firstPaymentDate | First payment due date |
| lastPaymentDate | Last payment due date |
| completedAt | Completion timestamp (null if not completed) |
| fulfillmentTiming | When product is shipped (IMMEDIATE or AFTER_PAYMENT) |
| shippedAt | Shipment timestamp |
| deliveredAt | Delivery timestamp |
| orderId | Associated order ID |
| shippingAddress | Shipping address object |
| billingAddress | Billing address object |
| payments | Array of payment objects with full details |
| canMakeEarlyPayment | Whether early payoff is allowed |
| canCancel | Whether agreement can be cancelled |
| canUpdatePaymentMethod | Whether payment method can be updated |
Error Response Examples:
Bad Request (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "You do not have access to this agreement",
"action_time": "2025-10-18T10:30:45",
"data": "You do not have access to this agreement"
}
Not Found (404):
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "Agreement not found with ID: 3fa85f64-5717-4562-b3fc-2c963f66afa6",
"action_time": "2025-10-18T10:30:45",
"data": "Agreement not found with ID: 3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
4. Get Agreement By Number
Purpose: Retrieve detailed information about a specific installment agreement by its agreement number
Endpoint: GET {base_url}/agreements/number/{agreementNumber}
Access Level: ๐ Protected (Requires Authentication, Owner Only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| agreementNumber | string | Yes | Agreement number (format: INST-YYYY-NNNNN) | Pattern: INST-\d{4}-\d{5} |
Success Response JSON Sample: Same as Get Agreement By ID endpoint
Success Response Fields: Same as Get Agreement By ID endpoint
Error Response Examples: Same as Get Agreement By ID endpoint, with agreement number in error messages instead of ID
5. Get Agreement Payments
Purpose: Retrieve all payment records for a specific installment agreement
Endpoint: GET {base_url}/agreements/{agreementId}/payments
Access Level: ๐ Protected (Requires Authentication, Owner Only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| agreementId | UUID | Yes | Unique identifier of the agreement | Valid UUID format |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Payment history retrieved successfully",
"action_time": "2025-10-18T10:30:45",
"data": [
{
"paymentId": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
"paymentNumber": 1,
"scheduledAmount": 166666.67,
"paidAmount": 166666.67,
"principalPortion": 146666.67,
"interestPortion": 20000.00,
"remainingBalance": 1453333.33,
"lateFee": null,
"currency": "TZS",
"paymentStatus": "COMPLETED",
"paymentStatusDisplay": "COMPLETED",
"dueDate": "2025-11-18T00:00:00",
"paidAt": "2025-11-18T10:30:00",
"attemptedAt": "2025-11-18T10:29:45",
"paymentMethod": "WALLET",
"transactionId": "TXN-2025-67890",
"failureReason": null,
"retryCount": 0,
"daysUntilDue": null,
"daysOverdue": null,
"canPay": false,
"canRetry": false
},
{
"paymentId": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a",
"paymentNumber": 2,
"scheduledAmount": 166666.67,
"paidAmount": null,
"principalPortion": 148533.33,
"interestPortion": 18133.34,
"remainingBalance": 1304800.00,
"lateFee": null,
"currency": "TZS",
"paymentStatus": "SCHEDULED",
"paymentStatusDisplay": "SCHEDULED",
"dueDate": "2025-12-18T00:00:00",
"paidAt": null,
"attemptedAt": null,
"paymentMethod": null,
"transactionId": null,
"failureReason": null,
"retryCount": 0,
"daysUntilDue": 61,
"daysOverdue": null,
"canPay": false,
"canRetry": false
}
]
}
Success Response Fields:
| Field | Description |
|---|---|
| paymentId | Unique identifier for the payment |
| paymentNumber | Sequential payment number (1, 2, 3...) |
| scheduledAmount | Amount scheduled to be paid |
| paidAmount | Amount actually paid (null if not paid) |
| principalPortion | Amount going toward principal |
| interestPortion | Amount going toward interest |
| remainingBalance | Balance after this payment |
| lateFee | Late fee if applicable |
| currency | Currency code |
| paymentStatus | Current payment status |
| paymentStatusDisplay | Human-readable status |
| dueDate | When payment is due |
| paidAt | When payment was made (null if not paid) |
| attemptedAt | Last payment attempt timestamp |
| paymentMethod | Payment method used |
| transactionId | Transaction reference ID |
| status | Payment status after processing |
| processedAt | When payment was processed |
| message | Success message |
| agreementUpdate | Updated agreement information |
| agreementUpdate.paymentsCompleted | Updated number of completed payments |
| agreementUpdate.paymentsRemaining | Updated number of remaining payments |
| agreementUpdate.amountPaid | Updated total amount paid |
| agreementUpdate.amountRemaining | Updated remaining amount |
| agreementUpdate.nextPaymentDate | Next payment due date |
| agreementUpdate.nextPaymentAmount | Next payment amount |
| agreementUpdate.agreementStatus | Updated agreement status |
| agreementUpdate.isCompleted | Whether agreement is now completed |
Error Response Examples:
Bad Request - Insufficient Balance (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Insufficient wallet balance. Required: 166666.67 TZS, Available: 50000.00 TZS. Please top up your wallet before the next payment attempt.",
"action_time": "2025-10-18T10:30:45",
"data": "Insufficient wallet balance. Required: 166666.67 TZS, Available: 50000.00 TZS. Please top up your wallet before the next payment attempt."
}
Bad Request - Payment Already Completed (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Payment is already completed",
"action_time": "2025-10-18T10:30:45",
"data": "Payment is already completed"
}
Bad Request - Payment Not Due (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Payment is not due yet. Due date: 2025-12-18T00:00:00",
"action_time": "2025-10-18T10:30:45",
"data": "Payment is not due yet. Due date: 2025-12-18T00:00:00"
}
6. Get Upcoming Payments
Purpose: Retrieve all upcoming payments across all active agreements for the authenticated customer
Endpoint: GET {base_url}/upcoming-payments
Access Level: ๐ Protected (Requires Authentication)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Upcoming payments retrieved successfully",
"action_time": "2025-10-18T10:30:45",
"data": [
{
"paymentId": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a",
"paymentNumber": 2,
"scheduledAmount": 166666.67,
"paidAmount": null,
"principalPortion": 148533.33,
"interestPortion": 18133.34,
"remainingBalance": 1304800.00,
"lateFee": null,
"currency": "TZS",
"paymentStatus": "SCHEDULED",
"paymentStatusDisplay": "SCHEDULED",
"dueDate": "2025-12-18T00:00:00",
"paidAt": null,
"attemptedAt": null,
"paymentMethod": null,
"transactionId": null,
"failureReason": null,
"retryCount": 0,
"daysUntilDue": 61,
"daysOverdue": null,
"canPay": false,
"canRetry": false
}
]
}
Success Response Fields: Same as Get Agreement Payments endpoint
Error Response Examples: Standard authentication errors only
7. Make Manual Payment
Purpose: Process a manual payment for a specific scheduled installment
Endpoint: POST {base_url}/agreements/{agreementId}/payments/{paymentId}/pay
Access Level: ๐ Protected (Requires Authentication, Owner Only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| agreementId | UUID | Yes | Unique identifier of the agreement | Valid UUID format |
| paymentId | UUID | Yes | Unique identifier of the payment | Valid UUID format |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Payment processed successfully",
"action_time": "2025-10-18T10:30:45",
"data": {
"paymentId": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a",
"agreementId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"agreementNumber": "INST-2025-12345",
"amount": 166666.67,
"currency": "TZS",
"paymentMethod": "WALLET",
"transactionId": "TXN-2025-67891",
"status": "COMPLETED",
"processedAt": "2025-10-18T10:30:45",
"message": "Payment processed successfully",
"agreementUpdate": {
"paymentsCompleted": 2,
"paymentsRemaining": 10,
"amountPaid": 733333.34,
"amountRemaining": 1666666.66,
"nextPaymentDate": "2026-01-18T00:00:00",
"nextPaymentAmount": 166666.67,
"agreementStatus": "ACTIVE",
"isCompleted": false
}
}
}
Success Response Fields:
| Field | Description |
|---|---|
| paymentId | ID of the processed payment |
| agreementId | ID of the agreement |
| agreementNumber | Agreement number |
| amount | Amount paid |
| currency | Currency code |
| paymentMethod | Payment method used |
| transaction |
8. Retry Failed Payment
Purpose: Retry a failed payment that has not exceeded maximum retry attempts
Endpoint: POST {base_url}/payments/{paymentId}/retry
Access Level: ๐ Protected (Requires Authentication, Owner Only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| paymentId | UUID | Yes | Unique identifier of the payment to retry | Valid UUID format |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Payment retry processed successfully",
"action_time": "2025-10-18T10:30:45",
"data": {
"paymentId": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a",
"agreementId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"agreementNumber": "INST-2025-12345",
"amount": 166666.67,
"currency": "TZS",
"paymentMethod": "WALLET",
"transactionId": "TXN-2025-67892",
"status": "COMPLETED",
"processedAt": "2025-10-18T10:30:45",
"message": "Payment retry successful",
"agreementUpdate": {
"paymentsCompleted": 2,
"paymentsRemaining": 10,
"amountPaid": 733333.34,
"amountRemaining": 1666666.66,
"nextPaymentDate": "2026-01-18T00:00:00",
"nextPaymentAmount": 166666.67,
"agreementStatus": "ACTIVE",
"isCompleted": false
}
}
}
Success Response Fields: Same as Make Manual Payment endpoint
Error Response Examples:
Bad Request - Cannot Retry (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Payment cannot be retried",
"action_time": "2025-10-18T10:30:45",
"data": "Payment cannot be retried"
}
Bad Request - Max Retries Exceeded (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Maximum retry attempts (5) exceeded",
"action_time": "2025-10-18T10:30:45",
"data": "Maximum retry attempts (5) exceeded"
}
9. Preview Flexible Payment
Purpose: Calculate and preview how a flexible payment amount will be distributed across installment payments before processing
Endpoint: POST {base_url}/installments/agreements/{agreementId}/early-flexible-payment/preview
Access Level: ๐ Protected (Requires Authentication)
Authentication: Bearer Token (JWT)
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
| Content-Type | string | Yes | Must be application/json |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| agreementId | UUID | Yes | The ID of the installment agreement | Must be a valid UUID, agreement must belong to authenticated user |
Request JSON Sample:
{
"amount": 500000.00
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| amount | decimal | Yes | Amount the customer wants to pay | Must be > 0, cannot exceed remaining balance |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Flexible payment preview calculated successfully",
"action_time": "2025-11-06T10:30:45",
"data": {
"requestedAmount": 500000.00,
"minimumRequired": 150000.00,
"maximumAllowed": 1200000.00,
"isValid": true,
"validationMessage": null,
"impactedPayments": [
{
"paymentNumber": 3,
"dueDate": "2025-12-01T00:00:00",
"scheduledAmount": 200000.00,
"currentPaid": 50000.00,
"willApply": 150000.00,
"willRemain": 0.00,
"resultStatus": "Will be COMPLETED"
},
{
"paymentNumber": 4,
"dueDate": "2026-01-01T00:00:00",
"scheduledAmount": 200000.00,
"currentPaid": 0.00,
"willApply": 200000.00,
"willRemain": 0.00,
"resultStatus": "Will be COMPLETED"
},
{
"paymentNumber": 5,
"dueDate": "2026-02-01T00:00:00",
"scheduledAmount": 200000.00,
"currentPaid": 0.00,
"willApply": 150000.00,
"willRemain": 50000.00,
"resultStatus": "Will be PARTIALLY_PAID"
}
],
"paymentsWillComplete": 2,
"paymentsWillBePartial": 1,
"remainingAfter": 700000.00
}
}
Success Response Fields:
| Field | Description |
|---|---|
| requestedAmount | The amount customer wants to pay |
| minimumRequired | Minimum payment required (next incomplete payment amount) |
| maximumAllowed | Maximum payment allowed (total remaining balance) |
| isValid | Whether the requested amount is valid |
| validationMessage | Error message if amount is invalid, null if valid |
| impactedPayments | Array of payments that will be affected by this payment |
| impactedPayments[].paymentNumber | Sequential payment number |
| impactedPayments[].dueDate | When this payment is due |
| impactedPayments[].scheduledAmount | Original scheduled amount for this payment |
| impactedPayments[].currentPaid | Amount already paid towards this payment |
| impactedPayments[].willApply | How much of the flexible payment will apply here |
| impactedPayments[].willRemain | How much will remain unpaid after applying |
| impactedPayments[].resultStatus | Final status: "Will be COMPLETED" or "Will be PARTIALLY_PAID" |
| paymentsWillComplete | Number of payments that will be fully completed |
| paymentsWillBePartial | Number of payments that will be partially paid |
| remainingAfter | Total remaining balance after this payment |
Error Response Examples:
Bad Request - Amount Too Low (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Minimum payment required: 150000.00 TZS",
"action_time": "2025-11-06T10:30:45",
"data": "Minimum payment required: 150000.00 TZS"
}
Bad Request - Amount Too High (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Payment amount exceeds remaining balance. Use early payoff endpoint if paying off completely.",
"action_time": "2025-11-06T10:30:45",
"data": "Payment amount exceeds remaining balance. Use early payoff endpoint if paying off completely."
}
Forbidden - Not Agreement Owner (403):
{
"success": false,
"httpStatus": "FORBIDDEN",
"message": "You do not have access to this agreement",
"action_time": "2025-11-06T10:30:45",
"data": "You do not have access to this agreement"
}
Not Found - Agreement Not Found (404):
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "Agreement not found",
"action_time": "2025-11-06T10:30:45",
"data": "Agreement not found"
}
Validation Error - Invalid Amount (422):
{
"success": false,
"httpStatus": "UNPROCESSABLE_ENTITY",
"message": "Validation failed",
"action_time": "2025-11-06T10:30:45",
"data": {
"amount": "must be greater than 0"
}
}
10. Process Flexible Payment
Purpose: Process a flexible payment that can pay multiple installments or partially pay upcoming installments
Endpoint: POST {base_url}/installments/agreements/{agreementId}/early-flexible-payment
Access Level: ๐ Protected (Requires Authentication, Sufficient Wallet Balance)
Authentication: Bearer Token (JWT)
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
| Content-Type | string | Yes | Must be application/json |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| agreementId | UUID | Yes | The ID of the installment agreement | Must be a valid UUID, agreement must belong to authenticated user |
Request JSON Sample:
{
"amount": 500000.00,
"note": "Paying 3 months ahead"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| amount | decimal | Yes | Amount to pay | Must be > 0, between minimumRequired and maximumAllowed |
| note | string | No | Optional note about the payment | Max 500 characters |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Successfully paid 2 installments and partially paid 1 more",
"action_time": "2025-11-06T10:30:45",
"data": {
"agreementId": "a8b3c4d5-e6f7-8901-2345-6789abcdef01",
"agreementNumber": "INST-2025-12345",
"totalAmountPaid": 500000.00,
"currency": "TZS",
"transactionId": "TXN-2025-67890",
"processedAt": "2025-11-06T10:30:45",
"paymentsAffected": [
{
"paymentId": "p1a2b3c4-d5e6-f789-0123-456789abcdef",
"paymentNumber": 3,
"dueDate": "2025-12-01T00:00:00",
"scheduledAmount": 200000.00,
"amountApplied": 150000.00,
"previouslyPaid": 50000.00,
"newPaidAmount": 200000.00,
"remaining": 0.00,
"status": "COMPLETED",
"wasCompleted": true
},
{
"paymentId": "p2a3b4c5-d6e7-f890-1234-56789abcdef0",
"paymentNumber": 4,
"dueDate": "2026-01-01T00:00:00",
"scheduledAmount": 200000.00,
"amountApplied": 200000.00,
"previouslyPaid": 0.00,
"newPaidAmount": 200000.00,
"remaining": 0.00,
"status": "COMPLETED",
"wasCompleted": true
},
{
"paymentId": "p3a4b5c6-d7e8-f901-2345-6789abcdef01",
"paymentNumber": 5,
"dueDate": "2026-02-01T00:00:00",
"scheduledAmount": 200000.00,
"amountApplied": 150000.00,
"previouslyPaid": 0.00,
"newPaidAmount": 150000.00,
"remaining": 50000.00,
"status": "PARTIALLY_PAID",
"wasCompleted": false
}
],
"agreementUpdate": {
"paymentsCompleted": 4,
"paymentsPartial": 1,
"paymentsRemaining": 8,
"amountPaid": 1300000.00,
"amountRemaining": 700000.00,
"nextPaymentDate": "2026-02-01T00:00:00",
"nextPaymentAmount": 50000.00,
"agreementStatus": "ACTIVE",
"isCompleted": false
},
"message": "Successfully paid 2 installments and partially paid 1 more"
}
}
Success Response Fields:
| Field | Description |
|---|---|
| agreementId | ID of the installment agreement |
| agreementNumber | Human-readable agreement number |
| totalAmountPaid | Total amount paid in this transaction |
| currency | Currency code (TZS) |
| transactionId | Unique transaction identifier from ledger system |
| processedAt | Timestamp when payment was processed |
| paymentsAffected | Array of payments that were affected |
| paymentsAffected[].paymentId | ID of the affected payment |
| paymentsAffected[].paymentNumber | Sequential payment number |
| paymentsAffected[].dueDate | When this payment is due |
| paymentsAffected[].scheduledAmount | Original scheduled amount |
| paymentsAffected[].amountApplied | How much of flexible payment was applied here |
| paymentsAffected[].previouslyPaid | Amount that was previously paid |
| paymentsAffected[].newPaidAmount | Total amount now paid for this payment |
| paymentsAffected[].remaining | Amount still remaining for this payment |
| paymentsAffected[].status | New payment status (COMPLETED or PARTIALLY_PAID) |
| paymentsAffected[].wasCompleted | Whether this payment became fully completed |
| agreementUpdate | Updated agreement summary |
| agreementUpdate.paymentsCompleted | Total number of completed payments |
| agreementUpdate.paymentsPartial | Number of partially paid payments |
| agreementUpdate.paymentsRemaining | Number of payments still remaining |
| agreementUpdate.amountPaid | Total amount paid on agreement |
| agreementUpdate.amountRemaining | Total amount still remaining |
| agreementUpdate.nextPaymentDate | When next payment is due |
| agreementUpdate.nextPaymentAmount | Amount of next payment (remaining amount) |
| agreementUpdate.agreementStatus | Current agreement status |
| agreementUpdate.isCompleted | Whether agreement is fully completed |
| message | Human-readable success message |
Error Response Examples:
Bad Request - Insufficient Balance (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Insufficient wallet balance. Required: 500000.00 TZS, Available: 300000.00 TZS",
"action_time": "2025-11-06T10:30:45",
"data": "Insufficient wallet balance. Required: 500000.00 TZS, Available: 300000.00 TZS"
}
Bad Request - Agreement Not Active (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Cannot make payment on inactive agreement. Status: COMPLETED",
"action_time": "2025-11-06T10:30:45",
"data": "Cannot make payment on inactive agreement. Status: COMPLETED"
}
Bad Request - Wallet Not Active (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Wallet is not active",
"action_time": "2025-11-06T10:30:45",
"data": "Wallet is not active"
}
Validation Error (422):
{
"success": false,
"httpStatus": "UNPROCESSABLE_ENTITY",
"message": "Validation failed",
"action_time": "2025-11-06T10:30:45",
"data": {
"amount": "must be greater than 0",
"note": "size must be between 0 and 500"
}
}
11. Calculate Early Payoff
Purpose: Calculate the amount required to pay off the entire agreement early with interest discount
Endpoint: GET {base_url}/agreements/{agreementId}/early-payoff
Access Level: ๐ Protected (Requires Authentication, Owner Only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| agreementId | UUID | Yes | Unique identifier of the agreement | Valid UUID format |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Early payoff calculation completed",
"action_time": "2025-10-18T10:30:45",
"data": {
"agreementId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"paymentsCompleted": 2,
"paymentsRemaining": 10,
"amountPaid": 733333.34,
"remainingPrincipal": 1320000.00,
"unaccruedInterest": 346666.66,
"interestRebate": 260000.00,
"payoffWithRebate": 1406666.66,
"payoffWithoutRebate": 1666666.66,
"savingsVsScheduled": 260000.00,
"rebatePolicy": "75% discount on remaining interest for early payoff",
"calculatedAt": "2025-10-18T10:30:45"
}
}
Success Response Fields:
| Field | Description |
|---|---|
| agreementId | ID of the agreement |
| paymentsCompleted | Number of payments already made |
| paymentsRemaining | Number of payments left |
| amountPaid | Total amount paid so far |
| remainingPrincipal | Principal amount remaining |
| unaccruedInterest | Interest not yet charged |
| interestRebate | Interest discount amount (75% of remaining interest) |
| payoffWithRebate | Early payoff amount with discount applied |
| payoffWithoutRebate | Full remaining amount if paid on schedule |
| savingsVsScheduled | Amount saved by paying off early |
| rebatePolicy | Description of rebate policy |
| calculatedAt | When calculation was performed |
Error Response Examples:
Bad Request - Not Eligible (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Early payoff not available for this agreement",
"action_time": "2025-10-18T10:30:45",
"data": "Early payoff not available for this agreement"
}
Bad Request - Agreement Not Active (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Agreement is not active. Status: COMPLETED",
"action_time": "2025-10-18T10:30:45",
"data": "Agreement is not active. Status: COMPLETED"
}
12. Process Early Payoff
Purpose: Process full early payment of the agreement with interest discount applied
Endpoint: POST {base_url}/agreements/{agreementId}/early-payoff
Access Level: ๐ Protected (Requires Authentication, Owner Only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| agreementId | UUID | Yes | Unique identifier of the agreement | Valid UUID format |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Early payoff processed successfully",
"action_time": "2025-10-18T10:30:45",
"data": {
"paymentId": null,
"agreementId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"agreementNumber": "INST-2025-12345",
"amount": 1406666.66,
"currency": "TZS",
"paymentMethod": "WALLET",
"transactionId": null,
"status": "COMPLETED",
"processedAt": "2025-10-18T10:30:45",
"message": "Early payoff processed successfully",
"agreementUpdate": {
"paymentsCompleted": 12,
"paymentsRemaining": 0,
"amountPaid": 2400000.00,
"amountRemaining": 0.00,
"nextPaymentDate": null,
"nextPaymentAmount": null,
"agreementStatus": "COMPLETED",
"isCompleted": true
}
}
}
Success Response Fields:
| Field | Description |
|---|---|
| paymentId | Payment ID (null for early payoff) |
| agreementId | ID of the agreement |
| agreementNumber | Agreement number |
| amount | Total early payoff amount paid |
| currency | Currency code |
| paymentMethod | Payment method used |
| transactionId | Transaction reference (null for early payoff) |
| status | Payment status (COMPLETED) |
| processedAt | When payoff was processed |
| message | Success message |
| agreementUpdate | Updated agreement information |
| agreementUpdate.paymentsCompleted | Updated to total number of payments |
| agreementUpdate.paymentsRemaining | Set to 0 |
| agreementUpdate.amountPaid | Updated to total amount |
| agreementUpdate.amountRemaining | Set to 0 |
| agreementUpdate.nextPaymentDate | Set to null |
| agreementUpdate.nextPaymentAmount | Set to null |
| agreementUpdate.agreementStatus | Set to COMPLETED |
| agreementUpdate.isCompleted | Set to true |
Error Response Examples:
Bad Request - Insufficient Balance (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Insufficient wallet balance for early payoff. Required: 1406666.66 TZS, Available: 500000.00 TZS",
"action_time": "2025-10-18T10:30:45",
"data": "Insufficient wallet balance for early payoff. Required: 1406666.66 TZS, Available: 500000.00 TZS"
}
Bad Request - Not Eligible (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Agreement is not active. Status: DEFAULTED",
"action_time": "2025-10-18T10:30:45",
"data": "Agreement is not active. Status: DEFAULTED"
}
13. Cancel Agreement
Purpose: Cancel an installment agreement before any payments have been completed
Endpoint: POST {base_url}/agreements/{agreementId}/cancel
Access Level: ๐ Protected (Requires Authentication, Owner Only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
| Content-Type | string | Yes | application/json |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| agreementId | UUID | Yes | Unique identifier of the agreement | Valid UUID format |
Request JSON Sample:
{
"reason": "Changed my mind about the purchase. Found a better deal elsewhere."
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| reason | string | Yes | Reason for cancellation | Min: 1, Max: 500 characters |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Agreement cancelled successfully",
"action_time": "2025-10-18T10:30:45",
"data": null
}
Success Response Fields: No data returned on successful cancellation
Error Response Examples:
Bad Request - Cannot Cancel (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Agreement cannot be cancelled. Only agreements with no completed payments can be cancelled.",
"action_time": "2025-10-18T10:30:45",
"data": "Agreement cannot be cancelled. Only agreements with no completed payments can be cancelled."
}
Validation Error (422):
{
"success": false,
"httpStatus": "UNPROCESSABLE_ENTITY",
"message": "Validation failed",
"action_time": "2025-10-18T10:30:45",
"data": {
"reason": "Cancellation reason is required"
}
}
Quick Reference Guide
HTTP Method Badge Code Templates
GET Badge:
<span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span>
POST Badge:
<span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span>
Common HTTP Status Codes
200 OK: Successful GET/POST request400 Bad Request: Invalid request data or business logic violation401 Unauthorized: Authentication required/failed403 Forbidden: Insufficient permissions404 Not Found: Resource not found422 Unprocessable Entity: Validation errors500 Internal Server Error: Server error
Authentication
- Bearer Token: Include
Authorization: Bearer your_tokenin headers - All customer endpoints require valid authentication
- Token must belong to the customer who owns the agreement
Agreement Status Flow
- PENDING_FIRST_PAYMENT: Down payment made, waiting for grace period
- ACTIVE: Currently paying installments
- COMPLETED: Fully paid off
- DEFAULTED: Missed 2+ payments, in collections
- CANCELLED: User cancelled before first payment
Payment Status Types
- SCHEDULED: Not due yet
- PENDING: Due today, awaiting payment
- PROCESSING: Payment in progress
- COMPLETED: Successfully paid
- FAILED: Payment attempt failed
- LATE: Past due date
- SKIPPED: Missed completely
- WAIVED: Forgiven (special cases)
Business Rules
- Early Payoff Discount: 75% off remaining interest
- Maximum Retries: 5 attempts per payment
- Default Threshold: 2 missed payments triggers DEFAULTED status
- Cancellation Window: Only before first payment completed
- Payment Method: Wallet payments only
- Currency: All amounts in TZS (Tanzanian Shillings)
Common Error Scenarios
- Insufficient Wallet Balance: Top up wallet before payment
- Payment Not Due: Wait until due date or make early payoff
- Agreement Defaulted: Contact support for resolution
- Max Retries Exceeded: Contact support
- Ownership Validation Failed: Can only access own agreements
Notes for Developers
Idempotency
- Payment processing endpoints are idempotent
- Duplicate payment requests will return existing payment status
- Use transaction IDs to track payment processing
Rate Limiting
- Standard rate limits apply: 100 requests per hour per user
- Payment processing has additional throttling for security
Webhooks
- Payment success/failure events trigger notifications
- Agreement completion triggers order creation (for AFTER_PAYMENT fulfillment)
- Integration with notification service for email/SMS alerts
Testing
- Use sandbox environment for testing:
https://sandbox-api.nextgate.com - Test user accounts available with pre-loaded wallets
- Mock payment processing for integration testing
Support
- For API issues: api-support@nextgate.com
- For business logic questions: product@nextgate.com
- Emergency contact: +255-XXX-XXX-XXXId | Transaction reference ID | | failureReason | Reason if payment failed | | retryCount | Number of retry attempts | | daysUntilDue | Days until payment is due (negative if overdue) | | daysOverdue | Days payment is overdue (null if not overdue) | | canPay | Whether payment can be made now | | canRetry | Whether payment can be retried |
Installment Purchase - Public & Plan Endpoints
Base URL: https://api.nextgate.com/api/v1/installments
Short Description: This API provides public endpoints for browsing installment plans and calculating installment previews before checkout. These endpoints do not require authentication and are designed to help customers explore installment options, understand payment breakdowns, and make informed purchase decisions. The endpoints return detailed financial calculations including amortization schedules, interest breakdowns, and payment timelines.
Hints:
- Public endpoints do not require authentication
- Plans are filtered to show only active plans to customers
- Preview calculations use amortization formula for accurate payment schedules
- Early payoff discount (75% off remaining interest) is standard across all plans
- Down payment range: minimum set by plan (typically 10-20%), maximum 50% (platform limit)
- All financial calculations rounded to 2 decimal places
- Grace periods determine when first payment is due
- Fulfillment timing (IMMEDIATE vs AFTER_PAYMENT) affects when product ships
- APR (Annual Percentage Rate) converted to period rate based on payment frequency
- All amounts are in TZS (Tanzanian Shillings)
Standard Response Format
All API responses follow a consistent structure using our Globe Response Builder pattern:
Success Response Structure
{
"success": true,
"httpStatus": "OK",
"message": "Operation completed successfully",
"action_time": "2025-10-18T10:30:45",
"data": {
// Actual response data goes here
}
}
Error Response Structure
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Error description",
"action_time": "2025-10-18T10:30:45",
"data": "Error description"
}
Standard Response Fields
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true for successful operations, false for errors |
httpStatus |
string | HTTP status name (OK, BAD_REQUEST, NOT_FOUND, etc.) |
message |
string | Human-readable message describing the operation result |
action_time |
string | ISO 8601 timestamp of when the response was generated |
data |
object/string | Response payload for success, error details for failures |
HTTP Method Badge Standards
For better visual clarity, all endpoints use colored badges for HTTP methods with the following standard colors:
- GET - GET - Green (Safe, read-only operations)
- POST - POST - Blue (Create new resources)
Endpoints
1. Get Available Plans for Product
Purpose: Retrieve all active installment plans available for a specific product
Endpoint: GET {base_url}/products/{productId}/plans
Access Level: ๐ Public (No Authentication Required)
Authentication: None
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| productId | UUID | Yes | Unique identifier of the product | Valid UUID format |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Available installment plans retrieved successfully",
"action_time": "2025-10-18T10:30:45",
"data": [
{
"planId": "4b5c6d7e-8f9a-4b1c-9d2e-3f4a5b6c7d8e",
"planName": "Quick Payment Plan",
"paymentFrequency": "WEEKLY",
"paymentFrequencyDisplay": "Weekly",
"customFrequencyDays": null,
"numberOfPayments": 8,
"duration": "8 weeks",
"apr": 10.00,
"minDownPaymentPercent": 20,
"gracePeriodDays": 7,
"fulfillmentTiming": "IMMEDIATE",
"isActive": true,
"isFeatured": false,
"displayOrder": 1,
"preview": {
"productPrice": 2000000.00,
"minDownPaymentAmount": 400000.00,
"maxDownPaymentAmount": 1000000.00,
"financedAmountExample": 1600000.00,
"paymentAmountExample": 210000.00,
"totalInterestExample": 80000.00,
"totalCostExample": 2080000.00,
"firstPaymentDateExample": "2025-10-25T00:00:00",
"lastPaymentDateExample": "2025-12-13T00:00:00"
}
},
{
"planId": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
"planName": "Standard Monthly Plan",
"paymentFrequency": "MONTHLY",
"paymentFrequencyDisplay": "Monthly",
"customFrequencyDays": null,
"numberOfPayments": 12,
"duration": "12 months",
"apr": 15.00,
"minDownPaymentPercent": 15,
"gracePeriodDays": 30,
"fulfillmentTiming": "IMMEDIATE",
"isActive": true,
"isFeatured": true,
"displayOrder": 2,
"preview": {
"productPrice": 2000000.00,
"minDownPaymentAmount": 300000.00,
"maxDownPaymentAmount": 1000000.00,
"financedAmountExample": 1700000.00,
"paymentAmountExample": 156250.00,
"totalInterestExample": 175000.00,
"totalCostExample": 2175000.00,
"firstPaymentDateExample": "2025-11-17T00:00:00",
"lastPaymentDateExample": "2026-10-17T00:00:00"
}
},
{
"planId": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a",
"planName": "Budget Friendly Plan",
"paymentFrequency": "MONTHLY",
"paymentFrequencyDisplay": "Monthly",
"customFrequencyDays": null,
"numberOfPayments": 24,
"duration": "24 months",
"apr": 18.00,
"minDownPaymentPercent": 10,
"gracePeriodDays": 30,
"fulfillmentTiming": "AFTER_PAYMENT",
"isActive": true,
"isFeatured": false,
"displayOrder": 3,
"preview": {
"productPrice": 2000000.00,
"minDownPaymentAmount": 200000.00,
"maxDownPaymentAmount": 1000000.00,
"financedAmountExample": 1800000.00,
"paymentAmountExample": 87500.00,
"totalInterestExample": 300000.00,
"totalCostExample": 2300000.00,
"firstPaymentDateExample": "2025-11-17T00:00:00",
"lastPaymentDateExample": "2027-10-17T00:00:00"
}
}
]
}
Success Response Fields:
| Field | Description |
|---|---|
| planId | Unique identifier for the plan |
| planName | Display name of the plan |
| paymentFrequency | Payment frequency enum (DAILY, WEEKLY, BI_WEEKLY, SEMI_MONTHLY, MONTHLY, QUARTERLY, CUSTOM_DAYS) |
| paymentFrequencyDisplay | Human-readable payment frequency |
| customFrequencyDays | Custom frequency in days (only for CUSTOM_DAYS type) |
| numberOfPayments | Total number of payments in the plan |
| duration | Human-readable duration (e.g., "12 months", "8 weeks") |
| apr | Annual Percentage Rate |
| minDownPaymentPercent | Minimum down payment percentage required by this plan |
| gracePeriodDays | Days before first payment is due |
| fulfillmentTiming | When product ships (IMMEDIATE = after down payment, AFTER_PAYMENT = after final payment) |
| isActive | Whether plan is currently active |
| isFeatured | Whether plan is featured/recommended (shows "Most Popular" badge) |
| displayOrder | Order for display on product page |
| preview | Preview calculations object |
| preview.productPrice | Product price used for calculations |
| preview.minDownPaymentAmount | Minimum down payment amount |
| preview.maxDownPaymentAmount | Maximum down payment amount (platform limit: 50%) |
| preview.financedAmountExample | Amount being financed (at minimum down payment) |
| preview.paymentAmountExample | Each installment amount (at minimum down payment) |
| preview.totalInterestExample | Total interest to be paid (at minimum down payment) |
| preview.totalCostExample | Grand total cost including interest (at minimum down payment) |
| preview.firstPaymentDateExample | When first payment would be due |
| preview.lastPaymentDateExample | When last payment would be due |
Error Response JSON Sample:
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "Product not found with ID: 7c9e6679-7425-40de-944b-e07fc1f90ae7",
"action_time": "2025-10-18T10:30:45",
"data": "Product not found with ID: 7c9e6679-7425-40de-944b-e07fc1f90ae7"
}
Standard Error Types:
Application-Level Exceptions (400-499)
404 NOT_FOUND: Product not found or product does not have installment enabled
Error Response Examples:
Not Found - Product (404):
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "Product not found with ID: 7c9e6679-7425-40de-944b-e07fc1f90ae7",
"action_time": "2025-10-18T10:30:45",
"data": "Product not found with ID: 7c9e6679-7425-40de-944b-e07fc1f90ae7"
}
Special Cases:
- If product has installment disabled, returns empty array
- If product has no active plans, returns empty array
- Plans are automatically sorted by displayOrder
2. Calculate Installment Preview
Purpose: Calculate detailed payment breakdown and schedule for a specific plan with customer's chosen down payment percentage
Endpoint: POST {base_url}/calculate-preview
Access Level: ๐ Public (No Authentication Required)
Authentication: None
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Content-Type | string | Yes | application/json |
Request JSON Sample:
{
"planId": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
"productPrice": 2000000.00,
"quantity": 1,
"downPaymentPercent": 20
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| planId | UUID | Yes | ID of the installment plan to calculate | Valid UUID format, plan must exist and be active |
| productPrice | decimal | Yes | Price of the product | Min: 0.01, Max: 999999999.99 |
| quantity | integer | Yes | Quantity of product | Min: 1, Max: 1 (installment limited to 1 item) |
| downPaymentPercent | integer | Yes | Down payment percentage customer chooses | Min: 10, Max: 50, must be >= plan's minDownPaymentPercent |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Installment preview calculated successfully",
"action_time": "2025-10-18T10:30:45",
"data": {
"planId": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
"planName": "Standard Monthly Plan",
"planDescription": "Pay in 12 monthly installments at 15.0% APR",
"paymentFrequency": "Monthly",
"numberOfPayments": 12,
"durationDisplay": "12 months",
"apr": 15.00,
"gracePeriodDays": 30,
"productPrice": 2000000.00,
"quantity": 1,
"totalProductCost": 2000000.00,
"downPaymentPercent": 20,
"downPaymentAmount": 400000.00,
"minDownPaymentPercent": 15,
"maxDownPaymentPercent": 50,
"minDownPaymentAmount": 300000.00,
"maxDownPaymentAmount": 1000000.00,
"financedAmount": 1600000.00,
"monthlyPaymentAmount": 146666.67,
"totalInterestAmount": 160000.00,
"totalAmount": 2160000.00,
"currency": "TZS",
"firstPaymentDate": "2025-11-17T00:00:00",
"lastPaymentDate": "2026-10-17T00:00:00",
"schedule": [
{
"paymentNumber": 1,
"dueDate": "2025-11-17T00:00:00",
"amount": 146666.67,
"principalPortion": 126666.67,
"interestPortion": 20000.00,
"remainingBalance": 1473333.33,
"description": "Month 1 payment"
},
{
"paymentNumber": 2,
"dueDate": "2025-12-17T00:00:00",
"amount": 146666.67,
"principalPortion": 128250.00,
"interestPortion": 18416.67,
"remainingBalance": 1345083.33,
"description": "Month 2 payment"
},
{
"paymentNumber": 3,
"dueDate": "2026-01-17T00:00:00",
"amount": 146666.67,
"principalPortion": 129853.13,
"interestPortion": 16813.54,
"remainingBalance": 1215230.20,
"description": "Month 3 payment"
},
{
"paymentNumber": 12,
"dueDate": "2026-10-17T00:00:00",
"amount": 146666.67,
"principalPortion": 144833.33,
"interestPortion": 1833.34,
"remainingBalance": 0.00,
"description": "Final payment"
}
],
"comparison": {
"payingUpfront": 2000000.00,
"payingWithInstallment": 2160000.00,
"additionalCost": 160000.00,
"additionalCostPercent": 8.00
},
"fulfillmentTiming": "IMMEDIATE",
"fulfillmentDescription": "Product ships immediately after down payment"
}
}
Success Response Fields:
| Field | Description |
|---|---|
| planId | ID of the plan used |
| planName | Name of the plan |
| planDescription | Description of the plan |
| paymentFrequency | Human-readable payment frequency |
| numberOfPayments | Total number of payments |
| durationDisplay | Human-readable duration |
| apr | Annual Percentage Rate |
| gracePeriodDays | Grace period before first payment |
| productPrice | Per-unit product price |
| quantity | Quantity purchased |
| totalProductCost | Total product cost (price ร quantity) |
| downPaymentPercent | Chosen down payment percentage |
| downPaymentAmount | Calculated down payment amount |
| minDownPaymentPercent | Minimum allowed down payment % |
| maxDownPaymentPercent | Maximum allowed down payment % (50%) |
| minDownPaymentAmount | Minimum down payment amount |
| maxDownPaymentAmount | Maximum down payment amount |
| financedAmount | Amount being financed (after down payment) |
| monthlyPaymentAmount | Each installment payment amount |
| totalInterestAmount | Total interest to be paid |
| totalAmount | Grand total (product + interest) |
| currency | Currency code (TZS) |
| firstPaymentDate | When first payment is due |
| lastPaymentDate | When last payment is due |
| schedule | Array of payment schedule items |
| schedule[].paymentNumber | Payment number (1, 2, 3...) |
| schedule[].dueDate | When payment is due |
| schedule[].amount | Payment amount |
| schedule[].principalPortion | Amount going to principal |
| schedule[].interestPortion | Amount going to interest |
| schedule[].remainingBalance | Balance after this payment |
| schedule[].description | Payment description |
| comparison | Comparison information object |
| comparison.payingUpfront | Cost if paying full price now |
| comparison.payingWithInstallment | Total cost with installment |
| comparison.additionalCost | Extra cost (interest) |
| comparison.additionalCostPercent | Interest as % of product price |
| fulfillmentTiming | When product ships |
| fulfillmentDescription | Description of fulfillment timing |
Error Response JSON Sample:
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Down payment must be at least 15% for this plan",
"action_time": "2025-10-18T10:30:45",
"data": "Down payment must be at least 15% for this plan"
}
Standard Error Types:
Application-Level Exceptions (400-499)
400 BAD_REQUEST: Invalid down payment percentage, inactive plan, or business rule violation404 NOT_FOUND: Plan not found422 UNPROCESSABLE_ENTITY: Validation errors on request parameters
Error Response Examples:
Bad Request - Down Payment Too Low (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Down payment must be at least 15% for this plan",
"action_time": "2025-10-18T10:30:45",
"data": "Down payment must be at least 15% for this plan"
}
Bad Request - Down Payment Too High (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Down payment cannot exceed 50%",
"action_time": "2025-10-18T10:30:45",
"data": "Down payment cannot exceed 50%"
}
Bad Request - Plan Not Active (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "This installment plan is not currently available",
"action_time": "2025-10-18T10:30:45",
"data": "This installment plan is not currently available"
}
Not Found - Plan (404):
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "Installment plan not found with ID: 5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
"action_time": "2025-10-18T10:30:45",
"data": "Installment plan not found with ID: 5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f"
}
Validation Error (422):
{
"success": false,
"httpStatus": "UNPROCESSABLE_ENTITY",
"message": "Validation failed",
"action_time": "2025-10-18T10:30:45",
"data": {
"planId": "Plan ID is required",
"productPrice": "Product price must be greater than 0",
"downPaymentPercent": "must be between 10 and 50"
}
}
Quick Reference Guide
HTTP Method Badge Code Templates
GET Badge:
<span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span>
POST Badge:
<span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span>
Common HTTP Status Codes
200 OK: Successful GET/POST request400 Bad Request: Invalid request data or business logic violation404 Not Found: Resource not found422 Unprocessable Entity: Validation errors500 Internal Server Error: Server error
Payment Frequency Types
- DAILY: Payment every day
- WEEKLY: Payment every week (7 days)
- BI_WEEKLY: Payment every 2 weeks (14 days)
- SEMI_MONTHLY: Payment twice per month (1st and 15th)
- MONTHLY: Payment every month (30 days)
- QUARTERLY: Payment every 3 months (90 days)
- CUSTOM_DAYS: Payment at custom interval (specified in customFrequencyDays)
Fulfillment Timing Options
- IMMEDIATE: Product ships immediately after down payment is made. Customer gets product while paying installments.
- AFTER_PAYMENT: Product ships only after final payment is completed (layaway model). Inventory is held during payment period.
Down Payment Rules
- Minimum: Set by individual plan (typically 10-20%)
- Maximum: 50% (platform-wide limit)
- Validation: Customer's choice must be within min-max range
- Purpose: Reduces financed amount and total interest paid
APR (Annual Percentage Rate)
- Expressed as percentage (e.g., 15.00 = 15%)
- Range: 0% to 36% (platform limits)
- Converted to period rate based on payment frequency
- Used in amortization calculation for each payment
Financial Calculations
Amortization Formula
Monthly Payment = P ร [r(1+r)^n] / [(1+r)^n - 1]
Where:
- P = Principal (financed amount)
- r = Period rate (APR / periods per year)
- n = Number of payments
Period Rate Calculation
- Daily: APR / 365
- Weekly: APR / 52
- Bi-weekly: APR / 26
- Semi-monthly: APR / 24
- Monthly: APR / 12
- Quarterly: APR / 4
- Custom: APR / (365 / customDays)
Payment Breakdown
Each payment consists of:
- Interest Portion: Remaining balance ร period rate
- Principal Portion: Payment amount - interest portion
- Remaining Balance: Previous balance - principal portion
Schedule Generation
- First payment due after grace period
- Subsequent payments calculated based on frequency
- Last payment adjusted for rounding differences
- All dates in ISO 8601 format
Comparison Information
Shows cost difference between:
- Paying upfront: Original product price
- Paying with installment: Product price + total interest
- Additional cost: Total interest amount
- Additional cost %: Interest as percentage of product price
Common Use Cases
1. Display Plans on Product Page
GET /api/v1/installments/products/{productId}/plans
- Shows all active plans for product
- Displays preview calculations at minimum down payment
- Highlights featured plans
- Sorted by displayOrder
2. Calculate Custom Preview
POST /api/v1/installments/calculate-preview
{
"planId": "...",
"productPrice": 2000000.00,
"quantity": 1,
"downPaymentPercent": 25
}
- Customer adjusts down payment slider
- Real-time calculation of payments
- Shows full payment schedule
- Displays savings comparison
3. Pre-Checkout Validation
POST /api/v1/installments/calculate-preview
- Validates customer's choices before checkout
- Ensures plan is still active
- Confirms down payment within range
- Generates schedule for agreement creation
Integration Examples
Frontend Flow
- Product page loads โ Call GET /products/{id}/plans
- Display plan options with previews
- User selects plan and adjusts down payment
- Call POST /calculate-preview on down payment change
- Show detailed breakdown and schedule
- User proceeds to checkout with selected configuration
Mobile App Flow
- Fetch plans when user taps "Installment Options"
- Show plan cards with key metrics
- Tapped plan shows full preview
- Slider for down payment percentage
- Real-time preview updates
- "Continue to Checkout" with configuration
Error Handling Best Practices
Client-Side Validation
- Validate down payment range before API call
- Check quantity = 1 for installment
- Ensure product price > 0
- Validate UUID formats
Server-Side Errors
- 400: Show user-friendly message, allow retry
- 404: Product/plan not found, redirect or show alternatives
- 422: Display field-specific validation errors
- 500: Generic error message, log for investigation
Performance Considerations
- Cache plan data (TTL: 1 hour)
- Debounce preview calculations (300ms)
- Lazy load full schedules
- Optimize for mobile networks
Testing Scenarios
Happy Path
- Get plans for valid product with installment enabled
- Calculate preview with valid parameters
- Verify calculations match expected values
Edge Cases
- Product with no active plans โ empty array
- Product with installment disabled โ empty array
- Down payment at minimum boundary
- Down payment at maximum boundary (50%)
- Inactive plan in preview request โ error
Error Cases
- Invalid product ID โ 404
- Invalid plan ID โ 404
- Down payment too low โ 400
- Down payment too high โ 400
- Missing required fields โ 422
- Invalid data types โ 422
Data Format Standards
- Dates: ISO 8601 format (2025-10-18T14:30:00Z)
- Currency: TZS (Tanzanian Shillings), no currency symbol in API
- Decimals: 2 decimal places for all monetary values
- Percentages: Whole numbers (15 = 15%, not 0.15)
- UUIDs: Standard UUID v4 format with hyphens
Business Rules Summary
- One Item Per Agreement: Installment limited to single product
- Active Plans Only: Only active plans returned in public endpoints
- Down Payment Range: 10-50% (plan minimum to platform maximum)
- APR Limits: 0-36% (platform enforced)
- Grace Period: 0-60 days (plan-specific)
- Payment Frequency: All standard frequencies supported
- Fulfillment Options: IMMEDIATE or AFTER_PAYMENT
- Currency: TZS only (Tanzania market)
- Rounding: All calculations rounded to 2 decimal places
- Early Payoff: 75% discount on remaining interest (not shown in preview)
Notes for Developers
Calculation Accuracy
- Use decimal/numeric types for currency calculations
- Avoid floating-point arithmetic
- Round to 2 decimals at final step only
- Last payment absorbs rounding differences
Caching Strategy
- Cache plan data per product (1 hour TTL)
- Invalidate cache when plans updated
- Preview calculations should not be cached
- Consider CDN for plan endpoints
Mobile Optimization
- Minimize payload size (exclude schedule if not needed)
- Compress responses (gzip/brotli)
- Use pagination for long schedules
- Implement request debouncing
Security Considerations
- No authentication required for public endpoints
- Rate limiting: 1000 requests/hour per IP
- Input validation on all parameters
- SQL injection prevention (parameterized queries)
- XSS prevention (sanitize all inputs)
Support & Resources
- API Documentation: https://docs.nextgate.com/api/installments
- Developer Portal: https://developers.nextgate.com
- Support Email: api-support@nextgate.com
- Status Page: https://status.nextgate.com
- Changelog: https://docs.nextgate.com/changelog
Documentation Checklist
Before using this API, ensure you understand:
- Public Access: No authentication required
- Plan Filtering: Only active plans returned
- Down Payment Rules: Min (plan-specific) to Max (50%)
- APR Calculation: Converted to period rate automatically
- Amortization: Principal + interest breakdown per payment
- Schedule Generation: All payment dates calculated
- Fulfillment Options: IMMEDIATE vs AFTER_PAYMENT
- Currency: TZS only
- Rounding: 2 decimal places
- Error Handling: Proper validation and error messages
- Rate Limiting: 1000 requests/hour per IP
- Caching: Recommended for plan data
- Testing: Sandbox environment available
Installment Plan Management - Admin/Shop Endpoints
Base URL: https://api.nextgate.com/api/v1/products/{shopId}/{productId}/installment-plans
Short Description: This API provides comprehensive endpoints for shop owners and administrators to create, manage, and configure installment plans for their products. Shop owners can define flexible payment terms including payment frequencies, interest rates, down payment requirements, grace periods, and fulfillment options. The API supports full CRUD operations, plan activation/deactivation, featured plan selection, and product-level installment enablement. All endpoints require authentication and validate shop ownership.
Hints:
- All endpoints require authentication and shop ownership validation
- Shop owners can only manage plans for their own products
- Multiple plans can be created per product for different customer segments
- Only one plan per product can be featured at a time
- Plans can be deactivated without deletion to preserve historical data
- Active plans are automatically available to customers on product pages
- Inactive plans are hidden from customers but preserved for existing agreements
- Payment frequencies support daily, weekly, bi-weekly, semi-monthly, monthly, quarterly, and custom intervals
- APR (Annual Percentage Rate) must be between 0% and 36%
- Down payment requirements: 10-50% range
- Grace periods: 0-60 days before first payment
- Fulfillment options: IMMEDIATE (ship after down payment) or AFTER_PAYMENT (ship after completion)
- Display order determines plan sorting on product pages
- Featured plans get "Most Popular" or "Recommended" badge
- Enabling/disabling installments at product level affects all plans
Standard Response Format
All API responses follow a consistent structure using our Globe Response Builder pattern:
Success Response Structure
{
"success": true,
"httpStatus": "OK",
"message": "Operation completed successfully",
"action_time": "2025-10-18T10:30:45",
"data": {
// Actual response data goes here
}
}
Error Response Structure
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Error description",
"action_time": "2025-10-18T10:30:45",
"data": "Error description"
}
Standard Response Fields
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true for successful operations, false for errors |
httpStatus |
string | HTTP status name (OK, BAD_REQUEST, NOT_FOUND, etc.) |
message |
string | Human-readable message describing the operation result |
action_time |
string | ISO 8601 timestamp of when the response was generated |
data |
object/string | Response payload for success, error details for failures |
HTTP Method Badge Standards
For better visual clarity, all endpoints use colored badges for HTTP methods with the following standard colors:
- GET - GET - Green (Safe, read-only operations)
- POST - POST - Blue (Create new resources)
- PUT - PUT - Yellow (Update/replace entire resource)
- PATCH - PATCH - Orange (Partial updates)
- DELETE - DELETE - Red (Remove resources)
Endpoints
1. Create Installment Plan
Purpose: Create a new installment plan for a specific product
Endpoint: POST {base_url}
Access Level: ๐ Protected (Requires Authentication, Shop Owner Only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| shopId | UUID | Yes | Unique identifier of the shop | Valid UUID format, must be owned by authenticated user |
| productId | UUID | Yes | Unique identifier of the product | Valid UUID format, must belong to specified shop |
| planId | UUID | Yes | Unique identifier of the plan to deactivate | Valid UUID format, must belong to specified product |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Installment plan deactivated successfully",
"action_time": "2025-10-18T10:30:45",
"data": {
"planId": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a",
"planName": "Budget Friendly Plan",
"isActive": false,
"updatedAt": "2025-10-18T10:30:45"
}
}
Success Response Fields:
| Field | Description |
|---|---|
| planId | ID of the deactivated plan |
| planName | Name of the plan |
| isActive | Active status (false) |
| updatedAt | Update timestamp |
Error Response Examples: Same as Activate Installment Plan endpoint
Important Notes:
- Deactivated plans are hidden from customers on product pages
- Existing agreements continue to use deactivated plans
- Plan can be reactivated at any time
- Deactivation is preferred over deletion for plans with existing agreements
2. Get Product Installment Plans
Purpose: Retrieve all installment plans for a specific product (including inactive plans for admin view)
Endpoint: GET {base_url}
Access Level: ๐ Protected (Requires Authentication, Shop Owner Only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| shopId | UUID | Yes | Unique identifier of the shop | Valid UUID format, must be owned by authenticated user |
| productId | UUID | Yes | Unique identifier of the product | Valid UUID format, must belong to specified shop |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Installment plans retrieved successfully",
"action_time": "2025-10-18T10:30:45",
"data": [
{
"planId": "4b5c6d7e-8f9a-4b1c-9d2e-3f4a5b6c7d8e",
"planName": "Quick Payment Plan",
"paymentFrequency": "WEEKLY",
"paymentFrequencyDisplay": "Weekly",
"customFrequencyDays": null,
"numberOfPayments": 8,
"calculatedDurationDays": 56,
"calculatedDurationDisplay": "8 weeks",
"apr": 10.00,
"minDownPaymentPercent": 20,
"gracePeriodDays": 7,
"fulfillmentTiming": "IMMEDIATE",
"isActive": true,
"isFeatured": false,
"displayOrder": 1,
"productId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"productName": "Samsung Galaxy S24 Ultra",
"shopId": "8d3a7b12-9c4e-4f8a-b5d2-3e6f7a8b9c0d",
"shopName": "Tech World Store",
"createdAt": "2025-10-15T09:00:00",
"updatedAt": "2025-10-15T09:00:00"
},
{
"planId": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
"planName": "12 Month Standard Plan",
"paymentFrequency": "MONTHLY",
"paymentFrequencyDisplay": "Monthly",
"customFrequencyDays": null,
"numberOfPayments": 12,
"calculatedDurationDays": 360,
"calculatedDurationDisplay": "12 months",
"apr": 15.00,
"minDownPaymentPercent": 20,
"gracePeriodDays": 30,
"fulfillmentTiming": "IMMEDIATE",
"isActive": true,
"isFeatured": true,
"displayOrder": 2,
"productId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"productName": "Samsung Galaxy S24 Ultra",
"shopId": "8d3a7b12-9c4e-4f8a-b5d2-3e6f7a8b9c0d",
"shopName": "Tech World Store",
"createdAt": "2025-10-16T10:30:00",
"updatedAt": "2025-10-16T10:30:00"
},
{
"planId": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a",
"planName": "Budget Friendly Plan",
"paymentFrequency": "MONTHLY",
"paymentFrequencyDisplay": "Monthly",
"customFrequencyDays": null,
"numberOfPayments": 24,
"calculatedDurationDays": 720,
"calculatedDurationDisplay": "24 months",
"apr": 18.00,
"minDownPaymentPercent": 10,
"gracePeriodDays": 30,
"fulfillmentTiming": "AFTER_PAYMENT",
"isActive": false,
"isFeatured": false,
"displayOrder": 3,
"productId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"productName": "Samsung Galaxy S24 Ultra",
"shopId": "8d3a7b12-9c4e-4f8a-b5d2-3e6f7a8b9c0d",
"shopName": "Tech World Store",
"createdAt": "2025-10-17T14:00:00",
"updatedAt": "2025-10-18T08:00:00"
}
]
}
Success Response Fields: Same as Create Installment Plan endpoint, returned as array
Error Response Examples:
Forbidden (403):
{
"success": false,
"httpStatus": "FORBIDDEN",
"message": "You do not have permission to view this shop's products",
"action_time": "2025-10-18T10:30:45",
"data": "You do not have permission to view this shop's products"
}
Not Found (404):
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "Product not found",
"action_time": "2025-10-18T10:30:45",
"data": "Product not found"
}
3. Get Installment Plan By ID
Purpose: Retrieve detailed information about a specific installment plan
Endpoint: GET {base_url}/{planId}
Access Level: ๐ Protected (Requires Authentication, Shop Owner Only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| shopId | UUID | Yes | Unique identifier of the shop | Valid UUID format, must be owned by authenticated user |
| productId | UUID | Yes | Unique identifier of the product | Valid UUID format, must belong to specified shop |
| planId | UUID | Yes | Unique identifier of the plan | Valid UUID format, must belong to specified product |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Installment plan retrieved successfully",
"action_time": "2025-10-18T10:30:45",
"data": {
"planId": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
"planName": "12 Month Standard Plan",
"paymentFrequency": "MONTHLY",
"paymentFrequencyDisplay": "Monthly",
"customFrequencyDays": null,
"numberOfPayments": 12,
"calculatedDurationDays": 360,
"calculatedDurationDisplay": "12 months",
"apr": 15.00,
"minDownPaymentPercent": 20,
"gracePeriodDays": 30,
"fulfillmentTiming": "IMMEDIATE",
"isActive": true,
"isFeatured": true,
"displayOrder": 2,
"productId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"productName": "Samsung Galaxy S24 Ultra",
"shopId": "8d3a7b12-9c4e-4f8a-b5d2-3e6f7a8b9c0d",
"shopName": "Tech World Store",
"createdAt": "2025-10-16T10:30:00",
"updatedAt": "2025-10-16T10:30:00",
"metadata": {}
}
}
Success Response Fields: Same as Create Installment Plan endpoint
Error Response Examples: Same as Get Product Installment Plans endpoint, plus:
Not Found - Plan (404):
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "Installment plan not found",
"action_time": "2025-10-18T10:30:45",
"data": "Installment plan not found"
}
4. Update Installment Plan
Purpose: Update an existing installment plan's configuration
Endpoint: PUT {base_url}/{planId}
Access Level: ๐ Protected (Requires Authentication, Shop Owner Only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
| Content-Type | string | Yes | application/json |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| shopId | UUID | Yes | Unique identifier of the shop | Valid UUID format, must be owned by authenticated user |
| productId | UUID | Yes | Unique identifier of the product | Valid UUID format, must belong to specified shop |
| planId | UUID | Yes | Unique identifier of the plan to update | Valid UUID format, must belong to specified product |
Request JSON Sample:
{
"planName": "12 Month Premium Plan",
"paymentFrequency": "MONTHLY",
"customFrequencyDays": null,
"numberOfPayments": 12,
"apr": 12.00,
"minDownPaymentPercent": 25,
"gracePeriodDays": 45,
"fulfillmentTiming": "IMMEDIATE",
"displayOrder": 1
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| planName | string | No | Updated display name | Min: 3, Max: 100 characters |
| paymentFrequency | string | No | Updated payment frequency | enum: DAILY, WEEKLY, BI_WEEKLY, SEMI_MONTHLY, MONTHLY, QUARTERLY, CUSTOM_DAYS |
| customFrequencyDays | integer | Conditional | Updated custom frequency | Required if paymentFrequency is CUSTOM_DAYS, Min: 1, Max: 365 |
| numberOfPayments | integer | No | Updated number of payments | Min: 2, Max: 120 |
| apr | decimal | No | Updated APR | Min: 0.00, Max: 36.00 |
| minDownPaymentPercent | integer | No | Updated minimum down payment | Min: 10, Max: 50 |
| gracePeriodDays | integer | No | Updated grace period | Min: 0, Max: 60 |
| fulfillmentTiming | string | No | Updated fulfillment option | enum: IMMEDIATE, AFTER_PAYMENT |
| displayOrder | integer | No | Updated display order | Min: 0 |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Installment plan updated successfully",
"action_time": "2025-10-18T10:30:45",
"data": {
"planId": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
"planName": "12 Month Premium Plan",
"paymentFrequency": "MONTHLY",
"paymentFrequencyDisplay": "Monthly",
"customFrequencyDays": null,
"numberOfPayments": 12,
"calculatedDurationDays": 360,
"calculatedDurationDisplay": "12 months",
"apr": 12.00,
"minDownPaymentPercent": 25,
"gracePeriodDays": 45,
"fulfillmentTiming": "IMMEDIATE",
"isActive": true,
"isFeatured": true,
"displayOrder": 1,
"productId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"productName": "Samsung Galaxy S24 Ultra",
"shopId": "8d3a7b12-9c4e-4f8a-b5d2-3e6f7a8b9c0d",
"shopName": "Tech World Store",
"createdAt": "2025-10-16T10:30:00",
"updatedAt": "2025-10-18T10:30:45"
}
}
Success Response Fields: Same as Create Installment Plan endpoint
Error Response Examples: Same as Create Installment Plan endpoint
Important Notes:
- Updating a plan only affects new agreements created after the update
- Existing agreements continue using the original terms
- Cannot update isActive or isFeatured via this endpoint (use dedicated endpoints)
5. Delete Installment Plan
Purpose: Permanently delete an installment plan (only if no active agreements exist)
Endpoint: DELETE {base_url}/{planId}
Access Level: ๐ Protected (Requires Authentication, Shop Owner Only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| shopId | UUID | Yes | Unique identifier of the shop | Valid UUID format, must be owned by authenticated user |
| productId | UUID | Yes | Unique identifier of the product | Valid UUID format, must belong to specified shop |
| planId | UUID | Yes | Unique identifier of the plan to delete | Valid UUID format, must belong to specified product |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Installment plan deleted successfully",
"action_time": "2025-10-18T10:30:45",
"data": null
}
Success Response Fields: No data returned on successful deletion
Error Response Examples:
Bad Request - Has Active Agreements (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Cannot delete plan with active agreements. Deactivate the plan instead.",
"action_time": "2025-10-18T10:30:45",
"data": "Cannot delete plan with active agreements. Deactivate the plan instead."
}
Forbidden (403):
{
"success": false,
"httpStatus": "FORBIDDEN",
"message": "You do not have permission to delete this plan",
"action_time": "2025-10-18T10:30:45",
"data": "You do not have permission to delete this plan"
}
Not Found (404):
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "Installment plan not found",
"action_time": "2025-10-18T10:30:45",
"data": "Installment plan not found"
}
6. Activate Installment Plan
Purpose: Activate an installment plan to make it available to customers
Endpoint: PATCH {base_url}/{planId}/activate
Access Level: ๐ Protected (Requires Authentication, Shop Owner Only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| shopId | UUID | Yes | Unique identifier of the shop | Valid UUID format, must be owned by authenticated user |
| productId | UUID | Yes | Unique identifier of the product | Valid UUID format, must belong to specified shop |
| planId | UUID | Yes | Unique identifier of the plan to activate | Valid UUID format, must belong to specified product |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Installment plan activated successfully",
"action_time": "2025-10-18T10:30:45",
"data": {
"planId": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a",
"planName": "Budget Friendly Plan",
"isActive": true,
"updatedAt": "2025-10-18T10:30:45"
}
}
Success Response Fields:
| Field | Description |
|---|---|
| planId | ID of the activated plan |
| planName | Name of the plan |
| isActive | Active status (true) |
| updatedAt | Update timestamp |
Error Response Examples: Same as Delete Installment Plan endpoint
7. Deactivate Installment Plan
Purpose: Deactivate an installment plan to hide it from customers (preserves data for existing agreements)
Endpoint: PATCH {base_url}/{planId}/deactivate
Access Level: ๐ Protected (Requires Authentication, Shop Owner Only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token |
8. Set Featured Plan
Purpose: Mark a plan as featured (recommended/most popular) - only one plan per product can be featured
Endpoint: PATCH {base_url}/{planId}/set-featured
Access Level: ๐ Protected (Requires Authentication, Shop Owner Only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| shopId | UUID | Yes | Unique identifier of the shop | Valid UUID format, must be owned by authenticated user |
| productId | UUID | Yes | Unique identifier of the product | Valid UUID format, must belong to specified shop |
| planId | UUID | Yes | Unique identifier of the plan to feature | Valid UUID format, must belong to specified product |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Featured plan set successfully",
"action_time": "2025-10-18T10:30:45",
"data": {
"planId": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
"planName": "12 Month Premium Plan",
"isFeatured": true,
"previousFeaturedPlanId": "4b5c6d7e-8f9a-4b1c-9d2e-3f4a5b6c7d8e",
"updatedAt": "2025-10-18T10:30:45"
}
}
Success Response Fields:
| Field | Description |
|---|---|
| planId | ID of the newly featured plan |
| planName | Name of the plan |
| isFeatured | Featured status (true) |
| previousFeaturedPlanId | ID of previously featured plan (null if none) |
| updatedAt | Update timestamp |
Error Response Examples: Same as Activate Installment Plan endpoint
Important Notes:
- Setting a plan as featured automatically un-features any previously featured plan
- Only active plans should be featured
- Featured plans typically display "Most Popular" or "Recommended" badge
- Featured plans often appear first in plan listings
9. Enable Installments for Product
Purpose: Enable installment payment option for a product (makes all active plans available)
Endpoint: PATCH {base_url}/enable-installments
Access Level: ๐ Protected (Requires Authentication, Shop Owner Only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| shopId | UUID | Yes | Unique identifier of the shop | Valid UUID format, must be owned by authenticated user |
| productId | UUID | Yes | Unique identifier of the product | Valid UUID format, must belong to specified shop |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Installments enabled successfully for product",
"action_time": "2025-10-18T10:30:45",
"data": {
"productId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"productName": "Samsung Galaxy S24 Ultra",
"installmentAvailable": true,
"activePlansCount": 2,
"updatedAt": "2025-10-18T10:30:45"
}
}
Success Response Fields:
| Field | Description |
|---|---|
| productId | ID of the product |
| productName | Name of the product |
| installmentAvailable | Installment availability status (true) |
| activePlansCount | Number of active plans for this product |
| updatedAt | Update timestamp |
Error Response Examples:
Bad Request - No Plans (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Cannot enable installments: No installment plans created for this product",
"action_time": "2025-10-18T10:30:45",
"data": "Cannot enable installments: No installment plans created for this product"
}
Forbidden (403):
{
"success": false,
"httpStatus": "FORBIDDEN",
"message": "You do not have permission to modify this product",
"action_time": "2025-10-18T10:30:45",
"data": "You do not have permission to modify this product"
}
Not Found (404):
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "Product not found",
"action_time": "2025-10-18T10:30:45",
"data": "Product not found"
}
10. Disable Installments for Product
Purpose: Disable installment payment option for a product (hides all plans from customers)
Endpoint: PATCH {base_url}/disable-installments
Access Level: ๐ Protected (Requires Authentication, Shop Owner Only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| shopId | UUID | Yes | Unique identifier of the shop | Valid UUID format, must be owned by authenticated user |
| productId | UUID | Yes | Unique identifier of the product | Valid UUID format, must belong to specified shop |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Installments disabled successfully for product",
"action_time": "2025-10-18T10:30:45",
"data": {
"productId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"productName": "Samsung Galaxy S24 Ultra",
"installmentAvailable": false,
"activePlansCount": 2,
"updatedAt": "2025-10-18T10:30:45"
}
}
Success Response Fields: Same as Enable Installments endpoint
Error Response Examples: Same as Enable Installments endpoint
Important Notes:
- Disabling installments hides the option from product page
- Existing agreements continue to function normally
- Plans are preserved and can be reactivated
- Customers with active agreements can still make payments
Common HTTP Status Codes
200 OK: Successful request400 Bad Request: Invalid request data or business logic violation401 Unauthorized: Authentication required/failed403 Forbidden: Insufficient permissions or not shop owner404 Not Found: Resource not found422 Unprocessable Entity: Validation errors500 Internal Server Error: Server error
Payment Frequency Options
- DAILY: Payment every day (1 day)
- WEEKLY: Payment every week (7 days)
- BI_WEEKLY: Payment every 2 weeks (14 days)
- SEMI_MONTHLY: Payment twice per month (~15 days)
- MONTHLY: Payment every month (~30 days)
- QUARTERLY: Payment every 3 months (~90 days)
- CUSTOM_DAYS: Custom interval (1-365 days)
Fulfillment Timing Options
- IMMEDIATE: Product ships after down payment, customer receives product while paying installments
- AFTER_PAYMENT: Product ships after final payment (layaway model), inventory held during payment period
Plan Configuration Constraints
| Parameter | Minimum | Maximum | Notes |
|---|---|---|---|
| APR | 0% | 36% | Platform-enforced limit |
| Down Payment | 10% | 50% | Minimum by plan, maximum platform-wide |
| Number of Payments | 2 | 120 | Typical range: 4-24 payments |
| Grace Period | 0 days | 60 days | Days before first payment |
| Custom Frequency | 1 day | 365 days | Only for CUSTOM_DAYS type |
| Plan Name | 3 chars | 100 chars | Must be unique per product |
Plan States and Lifecycle
Active Plan
- Visible to customers on product page
- Can be selected during checkout
- Creates new agreements
- Appears in public API responses
Inactive Plan
Featured Plan
- Displays "Most Popular" or "Recommended" badge
- Only one plan per product can be featured
- Typically listed first in plan options
- Used for promoting preferred payment terms
Deleted Plan
- Permanently removed from system
- Only possible if no agreements exist
- Cannot be recovered
- Use deactivation instead for preservation
Business Rules Summary
- One Featured Plan: Only one plan per product can be featured at a time
- Shop Ownership: Only shop owners can manage their product plans
- Active Plans Only: Customers only see active plans for products with installments enabled
- No Deletion with Agreements: Plans with existing agreements cannot be deleted, only deactivated
- Update Impact: Plan updates only affect new agreements, not existing ones
- Product-Level Control: Disabling installments at product level hides all plans
- Duration Calculation: System automatically calculates duration based on frequency and payment count
- Validation: All parameters validated against platform constraints
- Plan Minimum: At least one active plan required to enable installments on product
- Historical Preservation: Deactivated plans preserved for agreement history
Common Workflows
Creating a New Installment Plan
- Ensure product exists and you own the shop
- POST /installment-plans with plan configuration
- Verify plan appears in GET /installment-plans
- Optionally set as featured via PATCH /set-featured
- Enable installments on product if not already enabled
Managing Multiple Plans
- Create multiple plans for different customer segments
- Quick payment plan (8 weeks, 10% APR)
- Standard plan (12 months, 15% APR)
- Budget plan (24 months, 18% APR)
- Set most balanced plan as featured
- Order by display priority (displayOrder field)
- Monitor customer preferences and adjust
Temporarily Disabling Plans
- PATCH /deactivate for specific plan
- OR PATCH /disable-installments for all plans
- Existing agreements continue normally
- Reactivate when ready with PATCH /activate or /enable-installments
Updating Plan Terms
- Analyze current agreement performance
- PUT /installment-plans/{planId} with new terms
- New terms apply only to future agreements
- Consider creating new plan for major changes
Deleting Unused Plans
- Check if plan has any agreements (attempt delete will fail if so)
- DELETE /installment-plans/{planId}
- Plan permanently removed
- If has agreements, use deactivate instead
Client-Side Validation
- Validate shop/product ownership before API call
- Check APR within 0-36% range
- Verify down payment within 10-50% range
- Validate payment count between 2-120
- Ensure customFrequencyDays present for CUSTOM_DAYS
- Check plan name length (3-100 characters)
Server-Side Error Handling
- 400: Display user-friendly message, allow retry
- 403: Show "Insufficient permissions" message
- 404: Redirect to product list or show error
- 422: Display field-specific validation errors
- 500: Generic error message, log for investigation
Testing Scenarios
Happy Path
- Create plan with valid parameters
- Retrieve plan and verify all fields
- Update plan configuration
- Set as featured plan
- Activate/deactivate plan
- Enable/disable product installments
Edge Cases
- Create multiple plans with same parameters
- Set featured when no featured plan exists
- Set featured when another plan is featured
- Update plan to use custom frequency
- Deactivate only active plan
- Enable installments with no active plans
Error Cases
- Invalid shop/product IDs โ 404
- Non-owner attempting management โ 403
- APR outside 0-36% range โ 422
- Down payment outside 10-50% โ 422
- Delete plan with agreements โ 400
- Missing required fields โ 422
- Invalid payment frequency โ 422
- Custom days without CUSTOM_DAYS โ 422
Performance Considerations
- Cache plan data at product level (TTL: 30 minutes)
- Invalidate cache on plan create/update/delete
- Batch operations when managing multiple plans
- Use pagination for products with many plans
- Index on productId for fast retrieval
Security Considerations
- Always validate shop ownership
- Verify product belongs to shop
- Rate limiting: 100 requests/hour per user
- Input validation on all parameters
- SQL injection prevention (parameterized queries)
- XSS prevention (sanitize all inputs)
- Audit log all plan changes
Monitoring and Analytics
Track the following metrics:
- Plans created per shop
- Plan activation/deactivation frequency
- Featured plan changes
- Plans by payment frequency
- Average APR across plans
- Popular plan configurations
- Plans with most agreements
Best Practices for Shop Owners
Plan Design Strategy
- Offer 2-3 plans for different budgets
- Quick plan: Short term, low interest
- Standard plan: Medium term, moderate interest
- Budget plan: Long term, higher interest
- Use clear naming: "8-Week Quick Pay", "12-Month Standard"
- Set appropriate featured plan: Balance of affordability and profit
- Adjust based on data: Monitor which plans customers choose
APR Configuration
Down Payment Strategy
- High-value items: Higher down payment (25-40%)
- Impulse purchases: Lower down payment (10-20%)
- Luxury items: Flexible range (15-50%)
- Clearance items: Fixed minimum (10-15%)
Grace Period Guidelines
- Weekly payments: 7-14 days grace
- Monthly payments: 30-45 days grace
- Seasonal products: Align with customer cash flow
- New customers: Shorter grace (7-15 days)
Common Mistakes to Avoid
- Too many plans (confuses customers) - stick to 2-4
- Too high APR (customers avoid) - keep competitive
- Too low down payment (higher default risk) - minimum 15-20%
- Deleting active plans - deactivate instead
- Not setting featured plan - customers want guidance
- Inconsistent plan naming - use clear conventions
- Not updating based on performance - review quarterly
- Enabling without active plans - create plans first
Support & Resources
- API Documentation: https://docs.nextgate.com/api/installment-plans
- Shop Owner Guide: https://help.nextgate.com/shop/installments
- Developer Portal: https://developers.nextgate.com
- Support Email: shop-support@nextgate.com
- Status Page: https://status.nextgate.com
- Video Tutorials: https://academy.nextgate.com/installments
Version 1.0 (2025-10-18)
- Initial release
- Full CRUD operations for installment plans
- Plan activation/deactivation
- Featured plan management
- Product-level installment control
- Shop ownership validation
- Comprehensive error handling
- Audit logging support for authentication | | Content-Type | string | Yes | application/json |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| shopId | UUID | Yes | Unique identifier of the shop | Valid UUID format, must be owned by authenticated user |
| productId | UUID | Yes | Unique identifier of the product | Valid UUID format, must belong to specified shop |
Request JSON Sample:
{
"planName": "12 Month Standard Plan",
"paymentFrequency": "MONTHLY",
"customFrequencyDays": null,
"numberOfPayments": 12,
"apr": 15.00,
"minDownPaymentPercent": 20,
"gracePeriodDays": 30,
"fulfillmentTiming": "IMMEDIATE",
"isActive": true,
"isFeatured": false,
"displayOrder": 1
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| planName | string | Yes | Display name for the plan | Min: 3, Max: 100 characters |
| paymentFrequency | string | Yes | Payment frequency type | enum: DAILY, WEEKLY, BI_WEEKLY, SEMI_MONTHLY, MONTHLY, QUARTERLY, CUSTOM_DAYS |
| customFrequencyDays | integer | Conditional | Custom frequency in days | Required if paymentFrequency is CUSTOM_DAYS, Min: 1, Max: 365 |
| numberOfPayments | integer | Yes | Total number of payments | Min: 2, Max: 120 |
| apr | decimal | Yes | Annual Percentage Rate | Min: 0.00, Max: 36.00 |
| minDownPaymentPercent | integer | Yes | Minimum down payment percentage | Min: 10, Max: 50 |
| gracePeriodDays | integer | Yes | Days before first payment due | Min: 0, Max: 60 |
| fulfillmentTiming | string | Yes | When to ship product | enum: IMMEDIATE, AFTER_PAYMENT |
| isActive | boolean | No | Whether plan is active | Default: true |
| isFeatured | boolean | No | Whether plan is featured | Default: false |
| displayOrder | integer | No | Display order on product page | Min: 0, Default: 0 |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Installment plan created successfully",
"action_time": "2025-10-18T10:30:45",
"data": {
"planId": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
"planName": "12 Month Standard Plan",
"paymentFrequency": "MONTHLY",
"paymentFrequencyDisplay": "Monthly",
"customFrequencyDays": null,
"numberOfPayments": 12,
"calculatedDurationDays": 360,
"calculatedDurationDisplay": "12 months",
"apr": 15.00,
"minDownPaymentPercent": 20,
"gracePeriodDays": 30,
"fulfillmentTiming": "IMMEDIATE",
"isActive": true,
"isFeatured": false,
"displayOrder": 1,
"productId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"productName": "Samsung Galaxy S24 Ultra",
"shopId": "8d3a7b12-9c4e-4f8a-b5d2-3e6f7a8b9c0d",
"shopName": "Tech World Store",
"createdAt": "2025-10-18T10:30:45",
"updatedAt": "2025-10-18T10:30:45"
}
}
Success Response Fields:
| Field | Description |
|---|---|
| planId | Unique identifier for the created plan |
| planName | Name of the plan |
| paymentFrequency | Payment frequency enum value |
| paymentFrequencyDisplay | Human-readable frequency |
| customFrequencyDays | Custom days (if CUSTOM_DAYS type) |
| numberOfPayments | Total number of payments |
| calculatedDurationDays | Auto-calculated total duration in days |
| calculatedDurationDisplay | Human-readable duration |
| apr | Annual Percentage Rate |
| minDownPaymentPercent | Minimum down payment percentage |
| gracePeriodDays | Grace period in days |
| fulfillmentTiming | Fulfillment option |
| isActive | Active status |
| isFeatured | Featured status |
| displayOrder | Display order |
| productId | Associated product ID |
| productName | Associated product name |
| shopId | Associated shop ID |
| shopName | Associated shop name |
| createdAt | Creation timestamp |
| updatedAt | Last update timestamp |
Error Response Examples:
Bad Request - Invalid APR (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "APR must be between 0% and 36%",
"action_time": "2025-10-18T10:30:45",
"data": "APR must be between 0% and 36%"
}
Bad Request - Invalid Payment Count (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Number of payments must be between 2 and 120",
"action_time": "2025-10-18T10:30:45",
"data": "Number of payments must be between 2 and 120"
}
Validation Error (422):
{
"success": false,
"httpStatus": "UNPROCESSABLE_ENTITY",
"message": "Validation failed",
"action_time": "2025-10-18T10:30:45",
"data": {
"planName": "Plan name is required",
"apr": "must be between 0 and 36",
"minDownPaymentPercent": "must be between 10 and 50"
}
}
Forbidden - Not Shop Owner (403):
{
"success": false,
"httpStatus": "FORBIDDEN",
"message": "You do not have permission to manage this shop's products",
"action_time": "2025-10-18T10:30:45",
"data": "You do not have permission to manage this shop's products"
}
Not Found - Product (404):
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "Product not found",
"action_time": "2025-10-18T10:30:45",
"data": "Product not found"
}
Order Management
Base URL: api/v1/e-commerce/orders
Short Description: The Order Management API handles the complete order lifecycle for the NextGate e-commerce platform. It supports multiple purchase types, order tracking, shipping management, delivery confirmation with 6-digit codes, escrow integration, and digital product downloads.
Hints:
- All endpoints require Bearer token authentication
- Order sources: DIRECT_PURCHASE, CART_PURCHASE, DIGITAL_PURCHASE, INSTALLMENT, GROUP_PURCHASE
- Delivery confirmation uses a 6-digit code (SHA-256 hashed with salt, expires in 30 days, max 5 attempts)
- Digital orders have
deliveryStatus: NOT_APPLICABLE - Every digital order contains exactly one product โ a checkout with several digital products returns one order ID per product, not one order per shop (see "Order Grouping Rules")
- Digital file downloads are a two-step, different-base-path flow: list via
{base}/{orderId}/downloads(or/api/v1/e-commerce/digital-files/my-downloadsfor all orders), then fetch the actual URL via/api/v1/e-commerce/digital-files/my-downloads/{accessId}/url(endpoints 15-17) - Confirm-delivery response is returned directly (not wrapped in the standard response envelope)
- Every order detail response includes a
timelinearray โ ordered list of status steps with timestamps. Steps not yet reached havetimestamp: nullandisCompleted: false
Endpoints
1. Get Order by ID
Purpose: Retrieve detailed information about a specific order.
Endpoint: GET {base}/{orderId}
Access Level: ๐ Protected (Buyer or Seller only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| orderId | UUID | Yes | Unique identifier of the order |
Response JSON Sample:
{
"success": true,
"message": "Order retrieved successfully",
"data": {
"orderId": "550e8400-e29b-41d4-a716-446655440000",
"orderNumber": "ORD-2025-12345",
"buyer": {
"accountId": "123e4567-e89b-12d3-a456-426614174000",
"userName": "johndoe",
"email": "john@example.com",
"firstName": "John",
"lastName": "Doe"
},
"seller": {
"shopId": "789e0123-e45b-67d8-a901-234567890abc",
"shopName": "TechStore",
"shopLogoMedia": {
"fileId": "9f8e7d6c-5b4a-3210-9876-543210fedcba",
"status": "READY",
"thumbUrl": "https://cdn.example.com/shops/techstore-thumb.webp",
"mimeType": "image/png"
},
"shopSlug": "techstore"
},
"productOrderStatus": "SHIPPED",
"deliveryStatus": "IN_TRANSIT",
"productOrderSource": "DIRECT_PURCHASE",
"items": [
{
"orderItemId": "111e2222-e33b-44d5-a666-777788889999",
"productId": "abc12345-def6-7890-ghij-klmnopqrstuv",
"productName": "Wireless Headphones",
"productSlug": "wireless-headphones",
"productPrimaryMedia": {
"fileId": "9f8e7d6c-5b4a-3210-9876-543210fedcba",
"mediaType": "IMAGE",
"status": "READY",
"thumbUrl": "https://cdn.example.com/products/headphones-thumb.webp",
"mimeType": "image/webp"
},
"productType": "PHYSICAL",
"fileIds": null,
"quantity": 2,
"unitPrice": 85000.00,
"subtotal": 170000.00,
"tax": 0.00,
"total": 170000.00
}
],
"subtotal": 170000.00,
"shippingFee": 5000.00,
"tax": 0.00,
"totalAmount": 175000.00,
"platformFee": 8750.00,
"sellerAmount": 166250.00,
"currency": "TZS",
"paymentMethod": "MPESA",
"amountPaid": 175000.00,
"amountRemaining": 0.00,
"deliveryAddress": "123 Main St, Dar es Salaam, Tanzania",
"trackingNumber": "TRACK-550E8400",
"carrier": "NextGate Shipping",
"isDeliveryConfirmed": false,
"deliveryConfirmedAt": null,
"orderedAt": "2025-10-20T14:30:00",
"shippedAt": "2025-10-21T09:15:00",
"deliveredAt": null,
"cancelledAt": null,
"cancellationReason": null,
"timeline": [
{
"status": "ORDER_PLACED",
"label": "Order Placed",
"timestamp": "2025-10-20T14:30:00",
"isCompleted": true,
"note": null
},
{
"status": "SHIPPED",
"label": "Shipped",
"timestamp": "2025-10-21T09:15:00",
"isCompleted": true,
"note": "NextGate Shipping ยท TRACK-550E8400"
},
{
"status": "DELIVERED",
"label": "Delivered",
"timestamp": null,
"isCompleted": false,
"note": null
},
{
"status": "COMPLETED",
"label": "Order Completed",
"timestamp": null,
"isCompleted": false,
"note": null
}
]
}
}
Response Fields:
| Field | Description |
|---|---|
| orderId | Unique identifier of the order |
| orderNumber | Human-readable order number |
| buyer | Buyer account info (accountId, userName, email, firstName, lastName) |
| seller | Shop info (shopId, shopName, shopLogoMedia, shopSlug) |
| productOrderStatus | PENDING_PAYMENT, PENDING_SHIPMENT, SHIPPED, DELIVERED, AWAITING_BUYER_CONFIRM, COMPLETED, DISPUTED, CANCELLED, REFUNDED |
| deliveryStatus | PENDING, SHIPPED, IN_TRANSIT, DELIVERED, CONFIRMED, NOT_APPLICABLE |
| productOrderSource | DIRECT_PURCHASE, CART_PURCHASE, DIGITAL_PURCHASE, INSTALLMENT, GROUP_PURCHASE |
| items | Array of order items โ see item fields below |
| items[].productPrimaryMedia | Same lean media object used by cart, shop listings, and marketplace: { fileId, mediaType, status, thumbUrl, mimeType }. status lets the frontend show a "processing" placeholder instead of a broken image if the file wasn't READY yet at purchase time. null if the product had no media |
| items[].productType | PHYSICAL or DIGITAL โ frontend uses this to show tracking UI vs download UI |
| items[].fileIds | List of file UUIDs for the item โ populated only when productType is DIGITAL, null for physical. Informational only โ don't use these to download; call endpoint 15 (GET {base}/{orderId}/downloads) to get the accessId needed for the actual download flow (see "Digital Download Flow" below) |
| subtotal | Sum of all item totals before shipping and tax |
| shippingFee | Shipping cost |
| tax | Tax amount |
| totalAmount | Final amount (subtotal + shipping + tax) |
| platformFee | Platform commission โ currently a flat 5% of totalAmount, applied the same way across all purchase types (direct, cart, installment, group, physical, digital) |
| sellerAmount | Amount seller receives after platform fee |
| currency | Currency code (TZS) |
| paymentMethod | Payment method used |
| amountPaid | Amount already paid |
| amountRemaining | Remaining balance (installment orders) |
| deliveryAddress | Shipping address |
| trackingNumber | Shipping tracking number (null until shipped) |
| carrier | Shipping carrier (null until shipped). Currently always the hardcoded string "NexGate Shipping" โ no real carrier integration yet. Note: this is a typo of the platform name "NextGate" baked into the code; flagged as a candidate one-line fix. |
| isDeliveryConfirmed | Whether buyer confirmed delivery |
| deliveryConfirmedAt | Timestamp of delivery confirmation (null if not confirmed) |
| orderedAt | Order creation timestamp |
| shippedAt | Shipping timestamp (null until shipped) |
| deliveredAt | Delivery timestamp (null until delivered) |
| cancelledAt | Cancellation timestamp (null if not cancelled) |
| cancellationReason | Reason for cancellation (null if not cancelled) |
| timeline | Ordered list of status steps โ see Timeline Fields below |
| timeline[].status | Step identifier: ORDER_PLACED, SHIPPED, DELIVERED, COMPLETED, FILES_AVAILABLE (digital), CANCELLED, DISPUTED, REFUNDED |
| timeline[].label | Human-readable step label |
| timeline[].timestamp | When this step occurred (null if not yet reached) |
| timeline[].isCompleted | true if this step has been reached |
| timeline[].note | Optional context โ shipping carrier + tracking on SHIPPED, cancellation reason on CANCELLED, "Confirmed by buyer" or "Auto-confirmed" on COMPLETED, null otherwise |
Error Responses:
400 Bad Request: Access denied โ user is not buyer or seller of this order401 Unauthorized: Authentication required404 Not Found: Order not found
2. Get Order by Order Number
Purpose: Retrieve order details using the human-readable order number.
Endpoint: GET {base}/number/{orderNumber}
Access Level: ๐ Protected (Buyer or Seller only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| orderNumber | string | Yes | Human-readable order number (e.g. ORD-2025-12345) |
Response: Same structure as Get Order by ID.
Error Responses:
400 Bad Request: Access denied โ user is not buyer or seller of this order401 Unauthorized: Authentication required404 Not Found: Order not found
Order Summary Object (used by list endpoints 3-10)
Every list/paged endpoint below (my-orders, shop/{shopId}/orders, and their status-filtered/paged variants) returns this lighter shape instead of the full order object from endpoint 1 โ no items[] array, no delivery/cancellation fields. timeline[] is included (same shape as endpoint 1) so list/card views can render a mini progress indicator without a second request. Fetch the full object via endpoint 1 or 2 once the buyer/seller opens a specific order.
{
"orderId": "550e8400-e29b-41d4-a716-446655440000",
"orderNumber": "ORD-2025-12345",
"productOrderStatus": "SHIPPED",
"deliveryStatus": "IN_TRANSIT",
"productOrderSource": "DIRECT_PURCHASE",
"buyer": {
"accountId": "123e4567-e89b-12d3-a456-426614174000",
"userName": "johndoe"
},
"seller": {
"shopId": "789e0123-e45b-67d8-a901-234567890abc",
"shopName": "TechStore",
"shopLogoMedia": {
"fileId": "9f8e7d6c-5b4a-3210-9876-543210fedcba",
"status": "READY",
"thumbUrl": "https://cdn.example.com/shops/techstore-thumb.webp",
"mimeType": "image/png"
},
"shopSlug": "techstore"
},
"itemCount": 1,
"firstItemProductName": "Wireless Headphones",
"firstItemProductPrimaryMedia": {
"fileId": "9f8e7d6c-5b4a-3210-9876-543210fedcba",
"mediaType": "IMAGE",
"status": "READY",
"thumbUrl": "https://cdn.example.com/products/headphones-thumb.webp",
"mimeType": "image/webp"
},
"totalAmount": 175000.00,
"currency": "TZS",
"orderedAt": "2025-10-20T14:30:00",
"timeline": [
{
"status": "ORDER_PLACED",
"label": "Order Placed",
"timestamp": "2025-10-20T14:30:00",
"isCompleted": true,
"note": null
},
{
"status": "SHIPPED",
"label": "Shipped",
"timestamp": "2025-10-21T09:15:00",
"isCompleted": true,
"note": "NextGate Shipping ยท TRACK-550E8400"
},
{
"status": "DELIVERED",
"label": "Delivered",
"timestamp": null,
"isCompleted": false,
"note": null
},
{
"status": "COMPLETED",
"label": "Order Completed",
"timestamp": null,
"isCompleted": false,
"note": null
}
]
}
| Field | Description |
|---|---|
| orderId, orderNumber | Same as endpoint 1 |
| productOrderStatus, deliveryStatus, productOrderSource | Same enums as endpoint 1 |
| buyer | { accountId, userName } โ lean, unlike endpoint 1's full BuyerInfo (no email/firstName/lastName) |
| seller | { shopId, shopName, shopLogoMedia, shopSlug } โ same shape as endpoint 1 |
| itemCount | Total number of line items on this order. Always 1 for digital orders (see "Order Grouping Rules") |
| firstItemProductName | Name of the first item โ enough for a list-row label. For multi-item physical orders, pair with itemCount to render "Wireless Headphones + 2 more" |
| firstItemProductPrimaryMedia | Same lean media object as items[].productPrimaryMedia on endpoint 1, for the first item only |
| totalAmount, currency | Same as endpoint 1 |
| orderedAt | Same as endpoint 1 |
| timeline | Identical shape and rules as endpoint 1's timeline โ see "Timeline Reference" below |
3. Get My Orders
Purpose: Retrieve all orders for the authenticated customer.
Endpoint: GET {base}/my-orders
Access Level: ๐ Protected (Customer only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
Response JSON Sample:
{
"success": true,
"message": "Orders retrieved successfully",
"data": [ ...array of Order Summary Objects (see above)... ]
}
Error Responses:
4. Get My Orders by Status
Purpose: Retrieve customer orders filtered by order status.
Endpoint: GET {base}/my-orders/status/{status}
Access Level: ๐ Protected (Customer only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| status | enum | Yes | PENDING_PAYMENT, PENDING_SHIPMENT, SHIPPED, DELIVERED, AWAITING_BUYER_CONFIRM, COMPLETED, DISPUTED, CANCELLED, REFUNDED |
Response: Array of Order Summary Objects (see above).
Error Responses:
400 Bad Request: Invalid status value401 Unauthorized: Authentication required404 Not Found: User account not found
5. Get My Orders (Paginated)
Purpose: Retrieve customer orders with pagination.
Endpoint: GET {base}/my-orders/paged
Access Level: ๐ Protected (Customer only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
Query Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| page | integer | No | 1 | Page number (1-based) |
| size | integer | No | 10 | Number of items per page |
Response JSON Sample:
{
"success": true,
"message": "Orders retrieved successfully",
"data": {
"orders": [ "...Order Summary Objects (see above)..." ],
"currentPage": 1,
"pageSize": 10,
"totalElements": 25,
"totalPages": 3,
"hasNext": true,
"hasPrevious": false,
"isFirst": true,
"isLast": false
}
}
Error Responses:
6. Get My Orders by Status (Paginated)
Purpose: Retrieve customer orders filtered by status with pagination.
Endpoint: GET {base}/my-orders/status/{status}/paged
Access Level: ๐ Protected (Customer only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| status | enum | Yes | PENDING_PAYMENT, PENDING_SHIPMENT, SHIPPED, DELIVERED, AWAITING_BUYER_CONFIRM, COMPLETED, DISPUTED, CANCELLED, REFUNDED |
Query Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| page | integer | No | 1 | Page number (1-based) |
| size | integer | No | 10 | Number of items per page |
Response: Same paginated structure as endpoint 5.
Error Responses:
400 Bad Request: Invalid status value401 Unauthorized: Authentication required404 Not Found: User account not found
7. Get Shop Orders
Purpose: Retrieve all orders for a specific shop.
Endpoint: GET {base}/shop/{shopId}/orders
Access Level: ๐ Protected (Shop Owner only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| shopId | UUID | Yes | Unique identifier of the shop |
Response: Array of Order Summary Objects (see above).
Error Responses:
400 Bad Request: User is not the owner of this shop401 Unauthorized: Authentication required404 Not Found: Shop not found
8. Get Shop Orders by Status
Purpose: Retrieve shop orders filtered by order status.
Endpoint: GET {base}/shop/{shopId}/orders/status/{status}
Access Level: ๐ Protected (Shop Owner only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| shopId | UUID | Yes | Unique identifier of the shop |
| status | enum | Yes | PENDING_PAYMENT, PENDING_SHIPMENT, SHIPPED, DELIVERED, AWAITING_BUYER_CONFIRM, COMPLETED, DISPUTED, CANCELLED, REFUNDED |
Response: Array of Order Summary Objects (see above).
Error Responses:
400 Bad Request: Invalid status value or user is not shop owner401 Unauthorized: Authentication required404 Not Found: Shop not found
9. Get Shop Orders (Paginated)
Purpose: Retrieve shop orders with pagination.
Endpoint: GET {base}/shop/{shopId}/orders/paged
Access Level: ๐ Protected (Shop Owner only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| shopId | UUID | Yes | Unique identifier of the shop |
Query Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| page | integer | No | 1 | Page number (1-based) |
| size | integer | No | 10 | Number of items per page |
Response: Same paginated structure as endpoint 5.
Error Responses:
400 Bad Request: User is not the owner of this shop401 Unauthorized: Authentication required404 Not Found: Shop not found
10. Get Shop Orders by Status (Paginated)
Purpose: Retrieve shop orders filtered by status with pagination.
Endpoint: GET {base}/shop/{shopId}/orders/status/{status}/paged
Access Level: ๐ Protected (Shop Owner only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| shopId | UUID | Yes | Unique identifier of the shop |
| status | enum | Yes | PENDING_PAYMENT, PENDING_SHIPMENT, SHIPPED, DELIVERED, AWAITING_BUYER_CONFIRM, COMPLETED, DISPUTED, CANCELLED, REFUNDED |
Query Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| page | integer | No | 1 | Page number (1-based) |
| size | integer | No | 10 | Number of items per page |
Response: Same paginated structure as endpoint 5.
Error Responses:
400 Bad Request: Invalid status value or user is not shop owner401 Unauthorized: Authentication required404 Not Found: Shop not found
11. Mark Order as Shipped
Purpose: Seller marks an order as shipped. Generates a delivery confirmation code and sends it to the buyer.
Endpoint: POST {base}/{orderId}/ship
Access Level: ๐ Protected (Shop Owner/Seller only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| orderId | UUID | Yes | Unique identifier of the order |
Response JSON Sample:
{
"success": true,
"message": "Order marked as shipped",
"data": {
"orderId": "550e8400-e29b-41d4-a716-446655440000",
"orderNumber": "ORD-2025-12345",
"shippedAt": "2025-10-25T10:30:45",
"message": "Order marked as shipped. Confirmation code sent to customer.",
"confirmationCodeSent": true,
"codeExpiresAt": "2025-11-24T10:30:45",
"maxVerificationAttempts": 5
}
}
Response Fields:
| Field | Description |
|---|---|
| orderId | UUID of the shipped order |
| orderNumber | Human-readable order number |
| shippedAt | Timestamp when order was marked as shipped |
| message | Confirmation message |
| confirmationCodeSent | Whether confirmation code was sent to customer |
| codeExpiresAt | When the confirmation code expires (30 days from generation) |
| maxVerificationAttempts | Maximum number of code verification attempts allowed |
Error Responses:
400 Bad Request: Order is a digital order (does not require shipping), order status is not PENDING_SHIPMENT, or user is not the seller401 Unauthorized: Authentication required404 Not Found: Order not found
12. Confirm Delivery
Purpose: Customer confirms order delivery using the 6-digit confirmation code. Releases escrow to seller.
Endpoint: POST {base}/{orderId}/confirm-delivery
Access Level: ๐ Protected (Buyer/Customer only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
| User-Agent | string | No | Device info for verification tracking |
| X-Forwarded-For | string | No | Client IP address (if behind proxy) |
| X-Real-IP | string | No | Real client IP address |
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| orderId | UUID | Yes | Unique identifier of the order |
Request JSON Sample:
{
"confirmationCode": "123456"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| confirmationCode | string | Yes | 6-digit delivery confirmation code | Exactly 6 digits (0-9) |
Response JSON Sample:
{
"orderId": "550e8400-e29b-41d4-a716-446655440000",
"orderNumber": "ORD-2025-12345",
"deliveredAt": "2025-10-25T10:30:45",
"confirmedAt": "2025-10-25T10:30:45",
"escrowReleased": true,
"sellerAmount": 166250.00,
"currency": "TZS",
"message": "Delivery confirmed successfully. Order completed!"
}
Note: This response is returned directly without the standard success envelope.
Response Fields:
| Field | Description |
|---|---|
| orderId | UUID of the confirmed order |
| orderNumber | Human-readable order number |
| deliveredAt | Timestamp when order was marked as delivered |
| confirmedAt | Timestamp when delivery was confirmed |
| escrowReleased | Whether escrow funds were released to seller |
| sellerAmount | Amount released to seller after platform fee |
| currency | Currency code |
| message | Confirmation message |
Error Responses:
400 Bad Request: Order is a digital order (does not use delivery-confirmation codes โ see endpoint 14, "Confirm Digital Delivery"), invalid confirmation code, order not SHIPPED, user is not buyer, max attempts exceeded, code expired, or escrow already released401 Unauthorized: Authentication required404 Not Found: Order not found or no active confirmation code422 Unprocessable Entity: Confirmation code format invalid
13. Regenerate Confirmation Code
Purpose: Customer requests a new delivery confirmation code if the previous one was lost or expired.
Endpoint: POST {base}/{orderId}/regenerate-code
Access Level: ๐ Protected (Buyer/Customer only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| orderId | UUID | Yes | Unique identifier of the order |
Response JSON Sample:
{
"success": true,
"message": "Confirmation code regenerated successfully",
"data": {
"orderId": "550e8400-e29b-41d4-a716-446655440000",
"orderNumber": "ORD-2025-12345",
"codeSent": true,
"destination": "email",
"codeExpiresAt": "2025-11-24T10:30:45",
"maxAttempts": 5,
"message": "New confirmation code sent to your email"
}
}
Response Fields:
| Field | Description |
|---|---|
| orderId | UUID of the order |
| orderNumber | Human-readable order number |
| codeSent | Whether new code was successfully sent |
| destination | Where the code was sent (email) |
| codeExpiresAt | When the new code expires (30 days from generation) |
| maxAttempts | Maximum number of verification attempts allowed |
| message | Confirmation message |
Error Responses:
400 Bad Request: Order is a digital order (does not use delivery confirmation codes), order status is not SHIPPED, user is not the buyer, or delivery already confirmed401 Unauthorized: Authentication required404 Not Found: Order not found
14. Confirm Digital Delivery
Purpose: Buyer confirms receipt of a digital order. Releases escrow to the seller and completes the order immediately, instead of waiting for the automatic confirmation window (see "Digital Download Flow" below). This is the digital-order equivalent of endpoint 12 ("Confirm Delivery") โ physical orders use a 6-digit code, digital orders use this no-body endpoint instead.
Endpoint: POST {base}/{orderId}/confirm-digital
Access Level: ๐ Protected (Buyer/Customer only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| orderId | UUID | Yes | Unique identifier of the order |
Request Body: None.
Response JSON Sample:
{
"success": true,
"message": "Digital product confirmed. Payment released to seller.",
"data": null
}
Error Responses:
400 Bad Request: Order is not a digital order, user is not the buyer, or order is not inAWAITING_BUYER_CONFIRMstatus (already confirmed, still processing, or disputed)401 Unauthorized: Authentication required404 Not Found: Order not found
15. List Order Downloads
Purpose: Returns all digital files the buyer has access to for a given order, including the accessId needed to generate a download URL. Call this first, then pass each item's accessId to endpoint 16.
Endpoint: GET {base}/{orderId}/downloads
Access Level: ๐ Protected (Buyer only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| orderId | UUID | Yes | Unique identifier of the order |
Response JSON Sample:
{
"success": true,
"message": "1 file(s) available for download. Use each item's accessId with GET /api/v1/e-commerce/digital-files/my-downloads/{accessId}/url to get the download link.",
"data": [
{
"accessId": "d4e5f6a7-1234-5678-9abc-def012345678",
"fileId": "f1a2b3c4-def5-6789-ghij-klmnopqrstuv",
"fileName": "spring-boot-course.zip",
"contentType": "application/zip",
"fileSize": 524288000,
"downloadCount": 1,
"downloadsRemaining": 4,
"accessExpiresAt": "2026-06-18T10:00:00",
"canDownload": true
}
]
}
Note: A digital order always contains exactly one file (see "Order Grouping Rules" below โ each digital product gets its own order).
Response Fields:
| Field | Description |
|---|---|
| accessId | Use this in endpoint 16 to get the actual download URL |
| fileId | Unique identifier of the digital file (informational) |
| fileName | Display name of the file |
| contentType | MIME type of the file |
| fileSize | File size in bytes |
| downloadCount | How many times this buyer has downloaded this file |
| downloadsRemaining | Downloads left before cap is hit (null = unlimited). Scales with quantity purchased โ see "Digital Download Flow" below |
| accessExpiresAt | When this buyer's access to this file expires |
| canDownload | false if access is revoked, expired, or download cap reached |
Error Responses:
16. Get Digital Download URL
Purpose: Generates a presigned download URL for a digital file, identified by the accessId obtained from endpoint 15 (or endpoint 17). This is the single endpoint used to actually fetch a file, regardless of which order or how many times the buyer has purchased the underlying product.
โ ๏ธ Different base path โ this endpoint is NOT under {base} (/api/v1/e-commerce/orders); it lives under /api/v1/e-commerce/digital-files/my-downloads.
Endpoint: GET /api/v1/e-commerce/digital-files/my-downloads/{accessId}/url
Access Level: ๐ Protected (Buyer only โ must own the access record)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authentication |
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| accessId | UUID | Yes | Unique identifier of the download-access record (from endpoint 15 or 17) |
Response JSON Sample:
{
"success": true,
"message": "Download URL generated โ valid for 15 minutes",
"data": {
"url": "https://storage.example.com/files/...",
"expiresInSeconds": 900
}
}
Response Fields:
| Field | Description |
|---|---|
| url | Presigned URL for downloading the file directly |
| expiresInSeconds | How long url stays valid, in seconds. This is the real TTL from the storage layer (FileThunder) โ treat the human-readable text in message as a rough convention, not a guarantee |
Error Responses:
17. Get My Digital Downloads (All Orders)
Purpose: Returns every digital download-access record the authenticated buyer owns, across all orders and all shops โ the data source for a "My Digital Library" page.
โ ๏ธ Different base path โ /api/v1/e-commerce/digital-files/my-downloads, not {base}.
Endpoint: GET /api/v1/e-commerce/digital-files/my-downloads
Access Level: ๐ Protected (Buyer only)
Authentication: Bearer Token
Response JSON Sample:
{
"success": true,
"message": "Downloads retrieved",
"data": [
{
"accessId": "d4e5f6a7-1234-5678-9abc-def012345678",
"orderId": "550e8400-e29b-41d4-a716-446655440000",
"productId": "abc12345-def6-7890-ghij-klmnopqrstuv",
"productName": "Spring Boot Course",
"productSlug": "spring-boot-course",
"shopName": "TechStore",
"downloadCount": 1,
"maxDownloads": 5,
"accessExpiresAt": "2026-06-18T10:00:00",
"firstDownloadAt": "2026-06-11T09:00:00",
"lastDownloadAt": "2026-06-11T09:00:00",
"isActive": true,
"expired": false,
"canDownload": true
}
]
}
Response Fields:
| Field | Description |
|---|---|
| accessId | Use this in endpoint 16 to get the download URL |
| orderId | The order this access grant came from |
| productId, productName, productSlug, shopName | Product/shop info for display |
| downloadCount | Downloads used so far |
| maxDownloads | Cap for this access record (null = unlimited) โ see quantity scaling in "Digital Download Flow" |
| accessExpiresAt | When access expires |
| firstDownloadAt / lastDownloadAt | null until first download |
| isActive | false if access was manually revoked |
| expired | true once past accessExpiresAt |
| canDownload | Convenience flag โ false if inactive, expired, or capped |
Error Responses:
Order Creation โ How Orders Are Generated
Orders are never created manually. They are generated automatically when a checkout session moves to PAYMENT_COMPLETED. The system reads the session, applies grouping rules, and creates one or more orders depending on the cart contents and purchase type.
Order Grouping Rules
Physical items are grouped by (shop + product type) โ every physical item from the same shop lands in one order (one shipment, one tracking number, one delivery confirmation).
Digital items are grouped per product โ every distinct digital product becomes its own order, even if bought in the same checkout from the same shop. Each digital file gets independent escrow, independent buyer confirmation, and its own auto-confirm timer, so one file being unavailable never blocks or hides delivery of another.
| Scenario | Result |
|---|---|
| Same shop, same type (both PHYSICAL) | 1 order |
| Same shop, two different DIGITAL products | 2 orders โ one per product |
| Same shop, same DIGITAL product, quantity 2+ | 1 order (quantity lives on the single line item, capped by the seller's maxQuantityForDigital) |
| Same shop, PHYSICAL + DIGITAL mixed | 2+ orders โ physical bundle stays together, each digital product is separate |
| Different shops, same type | 1 order per shop (physical) / 1 order per product (digital) |
Why split digital by product, not just by shop? Digital orders skip shipping and are downloadable immediately, then complete via buyer confirmation or an automatic timeout (see "Digital Download Flow" below). Bundling several digital products into one order would mean one buyer-confirm action and one escrow release cover all of them at once โ if one file in the bundle isn't ready, it silently blocks (or falsely completes) delivery of the others. Splitting by product keeps every file's fulfillment, escrow, and confirmation fully independent. Physical items keep bundling per shop since they already share one shipment and one delivery confirmation regardless of how many products are in the box.
Installment and group-purchase orders are always for exactly one product, so this grouping question doesn't apply to them โ see Scenarios 3-5.
Scenario 1 โ Direct Purchase (Buy Now)
Buyer clicks Buy Now on a single product. Always produces exactly one order.
Physical product:
Buyer โ Buy Now โ Payment
โ 1 order created (source: DIRECT_PURCHASE)
โ status: PENDING_SHIPMENT
โ escrow held until buyer confirms delivery
โ seller ships โ buyer confirms with 6-digit code โ escrow released โ COMPLETED
Digital product:
Buyer โ Buy Now โ Payment
โ 1 order created (source: DIGITAL_PURCHASE)
โ status: AWAITING_BUYER_CONFIRM immediately
โ DigitalDownloadAccess record created for the product's file
โ buyer can download right away (escrow still held)
โ order completes + escrow releases when buyer calls
POST {base}/{orderId}/confirm-digital, OR automatically after
app.digital.order.auto-confirm-days (default: 3 days)
Scenario 2 โ Cart Purchase
Buyer checks out a cart with multiple items. Physical items are grouped per shop; each digital product becomes its own order (see "Order Grouping Rules" above).
Example cart:
| Item | Shop | Type |
|---|---|---|
| Wireless Headphones | TechStore | PHYSICAL |
| Spring Boot Course (ZIP) | TechStore | DIGITAL |
| Kubernetes Guide (PDF) | TechStore | DIGITAL |
| Running Shoes | SportShop | PHYSICAL |
Result: 4 orders created โ one physical order for TechStore's headphones, one order per digital product (even though both are from TechStore), one physical order for SportShop:
Order #1 โ TechStore | PHYSICAL (Wireless Headphones)
source: CART_PURCHASE
status: PENDING_SHIPMENT
shipping: split equally across shops (if multi-shop)
Order #2 โ TechStore | DIGITAL (Spring Boot Course)
source: DIGITAL_PURCHASE
status: AWAITING_BUYER_CONFIRM immediately
shipping: TZS 0
โ DigitalDownloadAccess created for the Spring Boot Course file
โ buyer can download immediately (escrow still held)
โ completes via POST /confirm-digital or auto-confirm after 3 days (default)
Order #3 โ TechStore | DIGITAL (Kubernetes Guide)
source: DIGITAL_PURCHASE
status: AWAITING_BUYER_CONFIRM immediately
shipping: TZS 0
โ DigitalDownloadAccess created for the Kubernetes Guide file
โ independent from Order #2 โ confirming/downloading one has no effect on the other
โ completes via POST /confirm-digital or auto-confirm after 3 days (default)
Order #4 โ SportShop | PHYSICAL (Running Shoes)
source: CART_PURCHASE
status: PENDING_SHIPMENT
shipping: split equally across shops
Shipping split rule: If the cart has items from multiple shops, the total shipping cost is divided equally across the number of distinct shops with physical items (not proportional to each shop's item count or value). Each physical order gets its share; digital orders never carry shipping cost.
Frontend implication: a single checkout can return more orderIds than the number of distinct shops in the cart. Always render one confirmation/tracking card per returned order ID โ never assume "1 shop = 1 order."
Scenario 3 โ Installment Purchase (IMMEDIATE fulfillment)
Buyer pays in installments but gets the product after the first payment.
Physical product:
First payment โ order created (source: INSTALLMENT)
โ status: PENDING_SHIPMENT
โ seller ships after first payment
โ buyer confirms delivery โ escrow released proportionally as payments come in
Remaining payments โ collected without creating new orders
Digital product:
First payment โ order created (source: INSTALLMENT โ detected as DIGITAL_PURCHASE)
โ status: AWAITING_BUYER_CONFIRM immediately
โ DigitalDownloadAccess created
โ buyer can download after first payment (escrow still held)
โ completes via POST /confirm-digital or auto-confirm after 3 days (default)
Remaining payments โ collected, no new order needed
Scenario 4 โ Installment Purchase (AFTER_PAYMENT fulfillment)
Buyer pays all installments first, gets the product only after full payment.
Physical product:
First payment โ no order created yet, agreement tracked only
...
Final payment โ order created (source: INSTALLMENT)
โ status: PENDING_SHIPMENT
โ seller ships โ buyer confirms โ COMPLETED
Digital product:
First payment โ no order created yet
...
Final payment โ order created (source: INSTALLMENT โ detected as DIGITAL_PURCHASE)
โ status: AWAITING_BUYER_CONFIRM immediately
โ DigitalDownloadAccess created
โ buyer can download only after all installments are paid (escrow still held)
โ completes via POST /confirm-digital or auto-confirm after 3 days (default)
Scenario 5 โ Group Purchase
Multiple buyers join a group for a discounted price. When the group reaches its participant goal, an order is created for every participant simultaneously.
Physical product:
Group goal reached โ
For each participant:
โ 1 order created (source: GROUP_PURCHASE)
โ status: PENDING_SHIPMENT
โ seller ships to each buyer individually
โ each buyer confirms delivery independently
Digital product:
Group goal reached โ
For each participant:
โ 1 order created (source: GROUP_PURCHASE โ detected as DIGITAL_PURCHASE)
โ status: AWAITING_BUYER_CONFIRM immediately
โ DigitalDownloadAccess created per participant
โ all buyers can download simultaneously (escrow still held, per buyer)
โ each buyer's order completes via their own POST /confirm-digital
or auto-confirm after 3 days (default)
Group metadata stored on each order: groupInstanceId, groupPrice, regularPrice, savings.
Digital Download Flow (after any purchase)
Once an order with source DIGITAL_PURCHASE is created, the fulfillment service creates one DigitalDownloadAccess record for the order's file (every digital order has exactly one, per "Order Grouping Rules" above), and the order status is set to AWAITING_BUYER_CONFIRM. The file is downloadable at this point, but escrow is not yet released and the order is not yet COMPLETED. The order finishes one of two ways:
- Buyer calls
POST {base}/{orderId}/confirm-digital(endpoint 14) any time, or - A scheduled job auto-confirms the order after
app.digital.order.auto-confirm-days(default: 3 days) โ a 4-hourly cleanup sweep also catches any order whose individual job was missed (e.g. server restart).
Either path releases escrow to the seller and sets the order to COMPLETED.
DigitalDownloadAccess records enforce:
| Rule | Configured by |
|---|---|
| Access expiry | product.downloadExpiryDays (default: 7 days if the seller doesn't set one) |
| Max downloads | product.maxDownloadsPerBuyer ร quantity purchased (null cap = unlimited, regardless of quantity). Example: cap of 5 with quantity 3 โ buyer gets 15 total downloads on that access record |
| Per-download URL TTL | Determined by FileThunder's own presigned-URL response (expiresInSeconds), not a hardcoded constant โ the human-readable text in the endpoint 16 response message is a rough convention, treat expiresInSeconds as the source of truth |
Frontend download flow โ two steps, using endpoints 15 and 16:
Step 1 โ List available files for an order (endpoint 15):
GET api/v1/e-commerce/orders/{orderId}/downloads
Response:
[
{
"accessId": "d4e5f6a7-...",
"fileId": "f1a2b3c4-...",
"fileName": "spring-boot-course.zip",
"contentType": "application/zip",
"fileSize": 524288000,
"downloadCount": 0,
"downloadsRemaining": 5,
"accessExpiresAt": "2026-06-18T10:00:00",
"canDownload": true
}
]
Step 2 โ Get a short-lived download link using the accessId from step 1 (endpoint 16):
GET api/v1/e-commerce/digital-files/my-downloads/{accessId}/url
Response:
{
"url": "https://storage.../...?X-Amz-Expires=900&...",
"expiresInSeconds": 900
}
Step 3 โ Buyer hits url directly. Each call to step 2 increments downloadCount on the access record.
Buyer's full digital library across all orders/shops: use endpoint 17 (GET /api/v1/e-commerce/digital-files/my-downloads) instead of step 1 when building a "My Purchases" page that isn't scoped to one order โ it returns every access record the buyer owns, each with its own accessId ready for step 2.
Order Status Reference
| Status | Applies to | Meaning |
|---|---|---|
PENDING_PAYMENT |
โ | Declared on the enum but not currently used โ orders are only ever created once checkout reaches PAYMENT_COMPLETED, so a persisted order never starts in this status |
PENDING_SHIPMENT |
Physical | Order paid, waiting for seller to ship |
SHIPPED |
Physical | Seller marked as shipped, waiting for buyer confirmation |
DELIVERED |
Physical | Declared on the enum but not currently assigned by any code path โ confirmDelivery() jumps straight from SHIPPED to COMPLETED (it does set the deliveredAt timestamp, which is what actually drives the DELIVERED timeline step) |
AWAITING_BUYER_CONFIRM |
Digital | Order created, files downloadable, escrow still held โ waiting for buyer confirmation (POST /confirm-digital) or the auto-confirm window (default 3 days) |
COMPLETED |
Both | Physical: buyer confirmed delivery via 6-digit code. Digital: buyer confirmed via /confirm-digital, or auto-confirmed |
DISPUTED |
Digital only (currently) | Buyer raised a dispute while AWAITING_BUYER_CONFIRM โ escrow frozen pending admin review. No code path currently raises a dispute on a physical (SHIPPED) order, despite the timeline reference below implying both are possible |
CANCELLED |
Both | Declared and mapped in the timeline builder, but order cancellation itself is not implemented anywhere yet (CancelProductOrderRequest/ProductOrderCancelledResponse DTOs exist but are unused) โ this status is currently unreachable in practice |
REFUNDED |
Both | Payment refunded |
| Delivery Status | Applies to | Meaning |
|---|---|---|
PENDING |
Physical | Not yet shipped |
IN_TRANSIT |
Physical | Seller marked as shipped |
CONFIRMED |
Physical | Buyer confirmed receipt |
NOT_APPLICABLE |
Digital | No physical delivery involved |
Timeline Reference
The timeline field is embedded in every order detail response. It is a sequential list of steps representing the order's lifecycle. Steps not yet reached have timestamp: null and isCompleted: false โ the frontend renders these as pending/greyed-out.
Physical order steps (DIRECT_PURCHASE, CART_PURCHASE, INSTALLMENT, GROUP_PURCHASE):
ORDER_PLACED โ SHIPPED โ DELIVERED โ COMPLETED
Digital order steps (DIGITAL_PURCHASE):
ORDER_PLACED โ FILES_AVAILABLE โ COMPLETED
Terminal branches (replace remaining steps when reached):
CANCELLED โ appears after ORDER_PLACED if cancelled before shipping
(mapper supports rendering this branch, but order cancellation
itself isn't implemented yet โ currently unreachable)
DISPUTED โ appears after FILES_AVAILABLE if buyer raises a dispute
(digital orders only, via the DigitalProductDispute subsystem โ
no code path currently raises a dispute on a SHIPPED physical order)
REFUNDED โ appears after DISPUTED if resolved in buyer's favour
Step notes:
| Step | Note value |
|---|---|
SHIPPED |
"<Carrier> ยท <TrackingNumber>" if tracking info is set, otherwise null |
CANCELLED |
Cancellation reason if provided, otherwise null |
COMPLETED |
"Confirmed by buyer" (buyer used the 6-digit code, or /confirm-digital) or "Auto-confirmed" (3-day digital timeout job) โ driven by buyerConfirmedAt/autoConfirmedAt |
| All others | null |
Marketplace
Base URL: api/v1/e-commerce/marketplace
Short Description: The Marketplace API is the primary product discovery layer of the Nexgate e-commerce platform. It exposes personalised product feeds, trending rankings, hot deals, live group purchases, and a powerful advanced filter with keyword search โ all driven by a scoring formula built on real purchase, view, and cart signals.
Hints:
- All endpoints work for anonymous users (no token required). Authenticated users automatically receive personalised results where applicable โ no extra parameter needed.
TRENDINGandFOR_YOUfeeds re-rank within each page using the full scoring formula. Other sort modes (NEWEST,PRICE_ASC, etc.) are sorted at the database level and return exact ordering.hasMultipleColors,maxGroupSeatsLeft, andminGroupDiscountPercentfilters are applied after the database query, so thetotalElementscount in the response reflects the pre-filtered DB count โ not the post-filtered count.viewCountincrements each timeGET /shops/{shopId}/products/{productId}is called.cartAddCountincrements when a product is added to a cart for the first time (not on quantity updates).- Pages are 1-based (
page=1returns the first page).
Standard Response Format
All API responses follow a consistent structure using the Globe Response Builder pattern:
Success Response Structure
{
"success": true,
"httpStatus": "OK",
"message": "Operation completed successfully",
"action_time": "2026-06-04T10:30:45",
"data": {
"content": [...],
"currentPage": 1,
"pageSize": 20,
"totalElements": 158,
"totalPages": 8,
"hasNext": true,
"hasPrevious": false
}
}
Error Response Structure
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Error description",
"action_time": "2026-06-04T10:30:45",
"data": "Error description"
}
Standard Response Fields
| Field | Type | Description |
|---|---|---|
success |
boolean | true for success, false for errors |
httpStatus |
string | HTTP status name (OK, BAD_REQUEST, NOT_FOUND, etc.) |
message |
string | Human-readable operation result |
action_time |
string | ISO 8601 timestamp of response generation |
data |
object | Paginated payload for success, error detail for failures |
Scoring Formulas
These formulas are the engine behind TRENDING, FOR_YOU, and BEST_DEAL sort modes. Understanding them helps predict how products are ranked.
Log Normalization
All count-based signals (soldQuantity, viewCount, cartAddCount) are log-normalized before applying weights. This prevents one product with 100,000 sales from dominating everything else.
normalize(value) = min(1.0, log(1 + value) / log(1 + 10,000))
Examples:
0 sales โ 0.000
100 sales โ 0.501
1,000 sales โ 0.750
10,000 sales โ 1.000 โ reference ceiling
50,000 sales โ 1.000 โ capped at 1.0
Formula 1 โ Trending Score (global, same for everyone)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ TRENDING SCORE FORMULA โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ trendingScore = โ
โ normalize(soldQuantity) ร 0.30 โ real money paid โ
โ + normalize(viewCount) ร 0.25 โ real browsing intent โ
โ + groupHeat ร 0.20 โ social urgency โ
โ + normalize(cartAddCount) ร 0.15 โ purchase intent โ
โ + discountStrength ร 0.07 โ deal attractiveness โ
โ + recencyBonus ร 0.03 โ freshness tiebreaker โ
โ โ
โ groupHeat = seatsOccupied / totalSeats (hottest live group) โ
โ discountStrength= (comparePrice - price) / comparePrice โ
โ recencyBonus = 1.0 if โค 7 days old โ
โ 0.5 if โค 30 days old โ
โ 0.0 otherwise โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Formula 2 โ Personalized Trending Score (authenticated users)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ PERSONALIZED TRENDING SCORE FORMULA โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ personalizedScore = โ
โ trendingScore โ
โ + 0.25 (if product's shop is in user's subscriptions) โ
โ + 0.00 (otherwise) โ
โ โ
โ This is Option A โ soft priority. Subscribed-shop products float โ
โ near the top but a genuinely viral product still beats them. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Formula 3 โ Relevance Score (For You feed)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ RELEVANCE SCORE FORMULA โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ relevanceScore = โ
โ categoryMatch ร 0.40 โ product's category is in cart โ
โ + favShopBoost ร 0.40 โ shop is in user's subscriptions โ
โ + trendingScore ร 0.20 โ global popularity tiebreaker โ
โ โ
โ categoryMatch = 1.0 if product's category matches any category โ
โ currently in the user's cart, else 0.0 โ
โ favShopBoost = 1.0 if shop is subscribed, else 0.0 โ
โ โ
โ Fallback: if user has no cart items and no subscriptions, โ
โ the endpoint falls back to the Personalized Trending feed. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Advanced Filter โ UI Reference
The advanced filter groups its parameters into logical sections. Below is a reference layout showing how a frontend filter panel would be structured:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ MARKETPLACE FILTERS โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ ๐ SEARCH โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ ?q= keyword search across name & description... โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ ๐ฐ PRICE RANGE โ
โ Min [ minPrice _________ ] โ
โ Max [ maxPrice _________ ] โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ ๐ท๏ธ PRODUCT โ
โ Category [ categoryId โผ ] โ
โ Condition โ NEW โ USED โ REFURBISHED โ
โ Type โ Physical โ Digital โ
โ Urgency โ NONE โ LIMITED_TIME โ LOW_STOCK โ
โ โ FLASH_SALE โ
โ [ ] hasMultipleColors โ show colour variants only โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ ๐ฆ AVAILABILITY โ
โ [ ] inStock โ in-stock only โ
โ Min stock [ minStockQuantity ] โ bulk buyers โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ ๐ค GROUP DEALS โ
โ [ ] hasGroupBuying โ supports group buying โ
โ [ ] hasActiveGroup โ live OPEN group right now โ
โ Max seats left [ maxGroupSeatsLeft ] โ urgency โ
โ Min group disc. [ minGroupDiscountPercent ]% โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ ๐ณ PAYMENT โ
โ [ ] onSale โ currently discounted โ
โ [ ] hasInstallments โ instalment plans available โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ ๐ช SHOP TRUST โ
โ [ ] shopVerified โ verified shops only โ
โ Min trust score [ minTrustScore ] / 5.00 โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ ๐ POPULARITY โ
โ Min sold count [ minSoldCount ] โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ๏ธ SORT BY โ
โ โ TRENDING โ formula-ranked โ
โ โ FOR_YOU โ personalised relevance โ
โ โ NEWEST โ createdAt DESC โ
โ โ PRICE_ASC โ cheapest first โ
โ โ PRICE_DESC โ most expensive first โ
โ โ MOST_SOLD โ soldQuantity DESC โ
โ โ BEST_DEAL โ highest discount % first โ
โ โ MOST_VIEWED โ viewCount DESC โ
โ โ MOST_CARTED โ cartAddCount DESC โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Endpoints
1. Main Discovery Feed
Purpose: Returns the primary marketplace product feed with optional filters and all sort strategies. Works for both anonymous and authenticated users โ authenticated users receive personalised scoring automatically.
Endpoint: GET api/v1/e-commerce/marketplace/feed
Access Level: ๐ Public (personalised when authenticated)
Authentication: Bearer Token (optional โ improves results when provided)
Query Parameters:
| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
sortBy |
enum | No | Sort strategy. Values: TRENDING, FOR_YOU, NEWEST, PRICE_ASC, PRICE_DESC, MOST_SOLD, BEST_DEAL, MOST_VIEWED, MOST_CARTED |
TRENDING |
page |
integer | No | Page number (1-based) | 1 |
size |
integer | No | Items per page | 20 |
minPrice |
decimal | No | Minimum product price | โ |
maxPrice |
decimal | No | Maximum product price | โ |
categoryId |
UUID | No | Filter by product category | โ |
condition |
enum | No | NEW, USED, REFURBISHED |
โ |
productType |
enum | No | PHYSICAL, DIGITAL |
โ |
inStock |
boolean | No | true = in-stock products only |
โ |
onSale |
boolean | No | true = discounted products only |
โ |
hasActiveGroup |
boolean | No | true = products with a live OPEN group right now |
โ |
shopVerified |
boolean | No | true = verified shops only |
โ |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Feed retrieved successfully",
"action_time": "2026-06-04T10:30:45",
"data": {
"content": [
{
"productId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"productName": "Samsung Galaxy S24",
"productSlug": "samsung-galaxy-s24-techstore",
"productPrimaryMedia": {
"fileId": "9c1a2b3d-4e5f-6789-abcd-ef0123456789",
"mediaType": "IMAGE",
"status": "READY",
"thumbUrl": "https://cdn.nexgate.com/products/galaxy-s24/thumb.webp",
"mimeType": "image/webp"
},
"productType": "PHYSICAL",
"price": 850000.00,
"comparePrice": 1050000.00,
"discountPercentage": 19.05,
"effectiveDiscountPercentage": 31.43,
"stockQuantity": 42,
"soldQuantity": 318,
"viewCount": 2741,
"cartAddCount": 195,
"urgencyTag": "LOW_STOCK",
"condition": "NEW",
"inStock": true,
"onSale": true,
"hasInstallments": true,
"shopId": "7cb3a812-1234-4abc-b3fc-9d84f55bce12",
"shopName": "TechStore Tanzania",
"shopSlug": "techstore-tanzania",
"shopLogoMedia": {
"fileId": "1d2e3f4a-5b6c-7890-defa-bc1234567890",
"status": "READY",
"thumbUrl": "https://cdn.nexgate.com/shops/techstore/thumb.webp",
"mimeType": "image/jpeg"
},
"shopVerified": true,
"shopTrustScore": 4.80,
"categoryId": "a1b2c3d4-1234-5678-abcd-ef0123456789",
"categoryName": "Smartphones",
"hasActiveGroup": true,
"activeGroupHeat": 0.78,
"activeGroupPrice": 720000.00,
"activeGroupSeatsLeft": 4,
"activeGroupExpiresAt": "2026-06-04T18:00:00",
"createdAt": "2026-05-28T09:15:00"
}
],
"currentPage": 1,
"pageSize": 20,
"totalElements": 1284,
"totalPages": 65,
"hasNext": true,
"hasPrevious": false
}
}
Success Response Fields:
| Field | Description |
|---|---|
content |
Array of product cards for this page |
content[].productId |
Unique product identifier |
content[].productPrimaryMedia |
Resolved primary media of the product โ {fileId, mediaType, status, thumbUrl, mimeType}. null if the product has no media yet |
content[].price |
Current selling price |
content[].comparePrice |
Original price (present only if product is on sale) |
content[].discountPercentage |
Regular sale discount % โ null if no comparePrice |
content[].effectiveDiscountPercentage |
Best available deal across ALL discount types โ max(salePct, activeGroupPct) ร 100. null if product has no discount of any kind. This is what the Hot Deals feed sorts by |
content[].soldQuantity |
Total units sold โ visible only if shop has enabled this |
content[].viewCount |
Cumulative public views since tracking began |
content[].cartAddCount |
Cumulative times added to any cart |
content[].urgencyTag |
NONE, LIMITED_TIME, LOW_STOCK, FLASH_SALE |
content[].hasActiveGroup |
true if there is a live OPEN group purchase right now |
content[].activeGroupHeat |
Group fill ratio โ 0.0 (empty) to 1.0 (full). null if no active group |
content[].activeGroupPrice |
Discounted price available inside the active group |
content[].activeGroupSeatsLeft |
Remaining seats in the active group |
content[].activeGroupExpiresAt |
When the active group purchase expires |
content[].shopTrustScore |
Shop trust rating from 0.00 to 5.00 |
currentPage |
Current page number (1-based) |
totalElements |
Total matching products across all pages |
hasNext / hasPrevious |
Pagination navigation flags |
Standard Error Types:
500 INTERNAL_SERVER_ERROR: Unexpected server failure
2. Trending Products
Purpose: Returns products ranked by the trending score formula. Anonymous users receive the global trending score. Authenticated users receive a personalised ranking โ products from subscribed shops receive a +0.25 score boost so they float near the top.
Endpoint: GET api/v1/e-commerce/marketplace/trending
Access Level: ๐ Public (personalised when authenticated)
Authentication: Bearer Token (optional)
Scoring applied:
Anonymous โ trendingScore
Authenticated โ trendingScore + 0.25 (if product's shop is subscribed)
Note: The database pre-sorts by
soldQuantity DESCas an approximation, then the full formula re-ranks within each page. This means the absolute order across pages may differ slightly from a pure formula sort โ which is intentional (pagination stability).
Query Parameters:
| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
page |
integer | No | Page number (1-based) | 1 |
size |
integer | No | Items per page | 20 |
categoryId |
UUID | No | Filter by category | โ |
minPrice |
decimal | No | Minimum price | โ |
maxPrice |
decimal | No | Maximum price | โ |
inStock |
boolean | No | In-stock only | โ |
onSale |
boolean | No | On sale only | โ |
shopVerified |
boolean | No | Verified shops only | โ |
Success Response: Same structure as the Main Feed endpoint.
Standard Error Types:
500 INTERNAL_SERVER_ERROR: Unexpected server failure
3. For You โ Personalised Recommendations
Purpose: Returns products ranked by relevance to the authenticated user based on their cart categories and shop subscriptions. Falls back to the trending feed for unauthenticated users or users with no cart items and no subscriptions.
Endpoint: GET api/v1/e-commerce/marketplace/for-you
Access Level: ๐ Public (best results when authenticated)
Authentication: Bearer Token (optional โ falls back to trending if absent)
Scoring applied:
relevanceScore =
categoryMatch ร 0.40 (product's category is in user's cart)
+ favShopBoost ร 0.40 (shop is in user's subscriptions)
+ trendingScore ร 0.20 (global popularity tiebreaker)
Pool strategy: fetches 3ร the requested page size (max 150), scores in Java,
then slices the requested page from the ranked result.
Query Parameters:
| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
page |
integer | No | Page number (1-based) | 1 |
size |
integer | No | Items per page | 20 |
categoryId |
UUID | No | Narrow recommendations to a specific category | โ |
inStock |
boolean | No | In-stock products only | โ |
shopVerified |
boolean | No | Verified shops only | โ |
Success Response: Same structure as the Main Feed endpoint.
Standard Error Types:
500 INTERNAL_SERVER_ERROR: Unexpected server failure
4. Hot Deals
Purpose: Returns products with any form of discount โ regular sale price or an active group purchase discount โ ranked by the best available saving. A product with a 35% group discount ranks higher than one with a 15% regular sale, even if it has no comparePrice set.
Endpoint: GET api/v1/e-commerce/marketplace/hot-deals
Access Level: ๐ Public
Authentication: None required
Scoring applied:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ EFFECTIVE DISCOUNT FORMULA โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ salePct = (comparePrice - price) / comparePrice โ
โ 0.0 if no comparePrice or comparePrice โค price โ
โ โ
โ activeGroupPct = (regularPrice - groupPrice) / regularPrice โ
โ 0.0 if no live OPEN group exists for the product โ
โ โ
โ effectiveDiscountPct = max(salePct, activeGroupPct) โ
โ โ
โ Products included: onSale = true OR hasActiveGroup = true โ
โ Sorted by: effectiveDiscountPct DESC (within each page) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Example:
Product A โ regular sale 15% off, no group โ effectivePct = 15%
Product B โ no sale, group discount 35% off โ effectivePct = 35% โ ranks first
Product C โ sale 20% off + group 40% off โ effectivePct = 40% โ ranks first
Query Parameters:
| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
page |
integer | No | Page number (1-based) | 1 |
size |
integer | No | Items per page | 20 |
categoryId |
UUID | No | Filter by category | โ |
minPrice |
decimal | No | Minimum price | โ |
maxPrice |
decimal | No | Maximum price | โ |
shopVerified |
boolean | No | Verified shops only | โ |
inStock |
boolean | No | In-stock only | โ |
Success Response: Same structure as the Main Feed endpoint. effectiveDiscountPercentage is always present and non-null in this feed.
Standard Error Types:
500 INTERNAL_SERVER_ERROR: Unexpected server failure
5. New Arrivals
Purpose: Returns recently published products, newest first. Ideal for users who want to discover what just dropped.
Endpoint: GET api/v1/e-commerce/marketplace/new-arrivals
Access Level: ๐ Public
Authentication: None required
Sorting: createdAt DESC โ exact database-level sort, no formula applied.
Query Parameters:
| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
page |
integer | No | Page number (1-based) | 1 |
size |
integer | No | Items per page | 20 |
categoryId |
UUID | No | Filter by category | โ |
productType |
enum | No | PHYSICAL or DIGITAL |
โ |
shopVerified |
boolean | No | Verified shops only | โ |
Success Response: Same structure as the Main Feed endpoint.
Standard Error Types:
500 INTERNAL_SERVER_ERROR: Unexpected server failure
6. Live Group Purchases
Purpose: Returns products that currently have an active OPEN group purchase, sorted by group heat (most seats filled = most urgent = shown first). Useful for showing users time-sensitive social buying opportunities.
Endpoint: GET api/v1/e-commerce/marketplace/live-groups
Access Level: ๐ Public
Authentication: None required
Sorting applied:
groupHeat = seatsOccupied / totalSeats
Products sorted by groupHeat DESC โ a group at 90% capacity appears
before one at 30%, creating urgency awareness for the user.
Query Parameters:
| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
page |
integer | No | Page number (1-based) | 1 |
size |
integer | No | Items per page | 20 |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Live group purchases retrieved successfully",
"action_time": "2026-06-04T10:30:45",
"data": {
"content": [
{
"productId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"productName": "Samsung Galaxy S24",
"price": 850000.00,
"hasActiveGroup": true,
"activeGroupHeat": 0.92,
"activeGroupPrice": 720000.00,
"activeGroupSeatsLeft": 2,
"activeGroupExpiresAt": "2026-06-04T18:00:00"
}
],
"currentPage": 1,
"pageSize": 20,
"totalElements": 37,
"totalPages": 2,
"hasNext": true,
"hasPrevious": false
}
}
Standard Error Types:
500 INTERNAL_SERVER_ERROR: Unexpected server failure
7. Advanced Filter
Purpose: The most powerful endpoint in the marketplace. Combines every available filter with an optional keyword search (?q=) so users can express highly specific queries like: "show me NEW Samsung smartphones under 1M from verified shops with an active group deal saving at least 20%, sorted by trending."
Endpoint: GET api/v1/e-commerce/marketplace/advanced-filter
Access Level: ๐ Public (personalised scoring when authenticated)
Authentication: Bearer Token (optional โ enables FOR_YOU and personalised TRENDING)
Scoring applied per sortBy:
โโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ sortBy โ How it works โ
โโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ TRENDING โ Full formula re-rank in Java after DB fetch โ
โ FOR_YOU โ Relevance formula re-rank in Java after DB fetch โ
โ NEWEST โ createdAt DESC โ exact DB sort โ
โ PRICE_ASC โ price ASC โ exact DB sort โ
โ PRICE_DESC โ price DESC โ exact DB sort โ
โ MOST_SOLD โ soldQuantity DESC โ exact DB sort โ
โ BEST_DEAL โ discount % DESC โ re-ranked in Java after DB fetch โ
โ MOST_VIEWED โ viewCount DESC โ exact DB sort โ
โ MOST_CARTED โ cartAddCount DESC โ exact DB sort โ
โโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Post-fetch Java filters (applied after DB query โ totalElements reflects pre-filter count):
hasMultipleColorsโ checkscolors.size() > 1in memorymaxGroupSeatsLeftโ checks active group seats remaining in memoryminGroupDiscountPercentโ calculates group discount % in memory
Query Parameters:
| Group | Parameter | Type | Required | Description | Default |
|---|---|---|---|---|---|
| Sort & Page | sortBy |
enum | No | Sort strategy (see table above) | TRENDING |
page |
integer | No | Page number (1-based) | 1 |
|
size |
integer | No | Items per page | 20 |
|
| Search | q |
string | No | Keyword โ searches product name and description (case-insensitive LIKE) | โ |
| Price | minPrice |
decimal | No | Minimum price | โ |
maxPrice |
decimal | No | Maximum price | โ | |
| Product | categoryId |
UUID | No | Filter by product category | โ |
condition |
enum | No | NEW, USED, REFURBISHED |
โ | |
productType |
enum | No | PHYSICAL, DIGITAL |
โ | |
urgencyTag |
enum | No | NONE, LIMITED_TIME, LOW_STOCK, FLASH_SALE |
โ | |
hasMultipleColors |
boolean | No | true = colour variant products only (post-fetch) |
โ | |
| Availability | inStock |
boolean | No | true = in-stock only |
โ |
minStockQuantity |
integer | No | Minimum stock units (bulk buyers) | โ | |
| Deals | onSale |
boolean | No | true = discounted only |
โ |
hasGroupBuying |
boolean | No | true = group buying enabled on product |
โ | |
hasActiveGroup |
boolean | No | true = live OPEN group right now |
โ | |
maxGroupSeatsLeft |
integer | No | Max seats remaining in active group (post-fetch) | โ | |
minGroupDiscountPercent |
integer | No | Min group discount % e.g. 20 (post-fetch) |
โ | |
hasInstallments |
boolean | No | true = instalment plans available |
โ | |
| Shop Trust | shopVerified |
boolean | No | true = verified shops only |
โ |
minTrustScore |
decimal | No | Min shop trust score e.g. 4.00 (0.00โ5.00) |
โ | |
| Popularity | minSoldCount |
integer | No | Min total units sold | โ |
Example Requests:
Find trending Samsung phones on sale from verified shops:
GET /marketplace/advanced-filter
?q=samsung
&categoryId=a1b2c3d4-...
&onSale=true
&shopVerified=true
&sortBy=TRENDING
Find live group deals saving at least 25% with fewer than 5 seats left:
GET /marketplace/advanced-filter
?hasActiveGroup=true
&minGroupDiscountPercent=25
&maxGroupSeatsLeft=5
&sortBy=BEST_DEAL
Find digital products under 50,000 newly published, for anonymous browsing:
GET /marketplace/advanced-filter
?productType=DIGITAL
&maxPrice=50000
&sortBy=NEWEST
Success Response: Same structure as the Main Feed endpoint.
Standard Error Types:
500 INTERNAL_SERVER_ERROR: Unexpected server failure
MarketplaceProductResponse โ Full Field Reference
| Field | Type | Nullable | Description |
|---|---|---|---|
productId |
UUID | No | Unique product identifier |
productName |
string | No | Product display name |
productSlug |
string | No | URL-safe unique slug |
productPrimaryMedia |
object | Yes | Resolved primary media โ {fileId, mediaType, status, thumbUrl, mimeType}. Same lean shape used in cart/wishlist/checkout. null if no media |
productType |
enum | No | PHYSICAL or DIGITAL |
price |
decimal | No | Current selling price |
comparePrice |
decimal | Yes | Original price โ present only when product is on sale |
discountPercentage |
decimal | Yes | Regular sale discount % โ (comparePrice - price) / comparePrice ร 100. null if not on sale |
effectiveDiscountPercentage |
decimal | Yes | Best available deal: max(salePct, activeGroupPct) ร 100. Covers both regular sale AND live group discounts. This is the field the Hot Deals feed sorts by. null if no discount of any kind |
stockQuantity |
integer | No | Available stock units |
soldQuantity |
integer | No | Total units sold |
viewCount |
long | No | Cumulative public views |
cartAddCount |
long | No | Cumulative first-time cart adds |
urgencyTag |
enum | No | NONE, LIMITED_TIME, LOW_STOCK, FLASH_SALE |
condition |
enum | No | NEW, USED, REFURBISHED |
inStock |
boolean | No | true if stockQuantity > 0 |
onSale |
boolean | No | true if comparePrice > price |
hasInstallments |
boolean | No | true if instalment plans exist |
shopId |
UUID | No | Owning shop identifier |
shopName |
string | No | Shop display name |
shopSlug |
string | No | Shop URL slug |
shopLogoMedia |
object | Yes | Resolved shop logo โ {fileId, status, thumbUrl, mimeType}. null if shop has no logo |
shopVerified |
boolean | No | Whether shop has passed verification |
shopTrustScore |
decimal | No | Shop trust rating 0.00โ5.00 |
categoryId |
UUID | Yes | Product category identifier |
categoryName |
string | Yes | Product category display name |
hasActiveGroup |
boolean | No | true if a live OPEN group exists |
activeGroupHeat |
decimal | Yes | Fill ratio 0.0โ1.0 of hottest live group |
activeGroupPrice |
decimal | Yes | Discounted group price |
activeGroupSeatsLeft |
integer | Yes | Remaining seats in active group |
activeGroupExpiresAt |
datetime | Yes | Expiry of active group purchase |
createdAt |
datetime | No | When the product was published |
Quick Reference โ All Marketplace Endpoints
| # | Endpoint | Auth | Sort Mode |
|---|---|---|---|
| 1 | GET /marketplace/feed |
Optional | All MarketplaceSortBy values |
| 2 | GET /marketplace/trending |
Optional | Formula-ranked |
| 3 | GET /marketplace/for-you |
Optional | Relevance-ranked |
| 4 | GET /marketplace/hot-deals |
None | effectiveDiscountPercentage DESC (sale + group) |
| 5 | GET /marketplace/new-arrivals |
None | createdAt DESC |
| 6 | GET /marketplace/live-groups |
None | Group heat DESC |
| 7 | GET /marketplace/advanced-filter |
Optional | All MarketplaceSortBy values |