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 |
No comments to display
No comments to display