Skip to main content

Product Management

Author: Josh S. Sakweli, Backend Lead Team
Last Updated: 2026-07-07
Version: v3.0

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:

  1. Client calls POST api/v1/files/request-upload with a context (PRODUCT_IMAGE, PRODUCT_VIDEO, DIGITAL_PRODUCT, PRODUCT_PREVIEW_IMAGE/_VIDEO/_DOCUMENT) → gets back a presigned upload URL and a fileId
  2. Client PUTs the raw file bytes directly to that URL (not through this API)
  3. Client references that fileId in the relevant product field (mediaFileIds, colors[].imageFileIds, digitalFileId, preview's ftFileId, ...) — the file is still processing (virus scan + variant generation) at this point, so validation only checks Redis-cached ownership/context, not readiness
  4. FileThunder finishes processing asynchronously and webhooks back into the product/preview/digital-file "updater" services, which upgrade the stored record to READY with resolved variants (a map of variant name → CDN URL)
  • Responses never return raw productImages/previewUrl strings — image-bearing fields are ProductMediaItem objects (fileId, mediaType, status, variants, mimeType, order) or similarly-shaped preview/file objects, and URLs are assembled fresh on every read via FileThunderUrlAssembler
  • Known gap: color imageFileIds are stored as raw file IDs and are not validated/resolved through FileThunder like top-level product media — the colors[].images field 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
  • comparePrice must be greater than price
  • groupPrice must be less than price
  • All group buying settings (groupMaxSize, groupPrice, groupTimeLimitHours) required when groupBuyingEnabled=true
  • maxOrderQuantity must be ≥ minOrderQuantity
  • Digital download fields (downloadExpiryDays, maxDownloadsPerBuyer, maxQuantityForDigital) only apply to DIGITAL products
  • mediaFileIds are resolved from the FileThunder Redis cache at request time — each file must have been requested via POST api/v1/files/request-upload (context PRODUCT_IMAGE/PRODUCT_VIDEO), be owned by the caller, and be at least PROCESSING (a still-uploading or expired/never-started file is rejected). Files that finish processing after the product is created are silently upgraded to READY in place via a FileThunder webhook
  • DIGITAL products require digitalFileId — 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 violations
  • 401: Authentication required
  • 403: Insufficient permissions
  • 404: Shop or category not found, or a referenced mediaFileIds/digitalFileId/previewFileId was never uploaded (upload expired or never started)
  • 409: Product with same name already exists in shop
  • 422: 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/previewFileId field 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 errors
  • 401: Authentication required
  • 403: Insufficient permissions
  • 404: Shop or product not found, or a referenced mediaFileIds entry was never uploaded
  • 409: 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 fields
  • 401: Authentication required
  • 403: Insufficient permissions
  • 404: 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 required
  • 403: Insufficient permissions
  • 404: 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 deleted
  • 401: Authentication required
  • 403: Insufficient permissions
  • 404: 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:

  • productMedia items may be status: "PROCESSING" with variants: null right after creation — poll this endpoint (or re-fetch) until status flips to "READY"
  • colors[].images currently 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 doc
  • preview.type is always null here regardless of whether a preview exists — it's wired to a legacy field the current preview pipeline never sets. Use GET .../products/{productId}/preview (17b) for the authoritative preview context/status/variants

Error Responses:

  • 401: Authentication required
  • 403: Insufficient permissions
  • 404: 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 ProductSummaryResponseproductPrimaryMedia 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 required
  • 403: Insufficient permissions
  • 404: 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 required
  • 403: Insufficient permissions
  • 404: 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:

  • previewType and previewUrl are wired to legacy fields that the current preview pipeline never populates — they will always be null, even when the product has an active, ready preview. There is currently no public field exposing the resolved preview media/variants; previewDownloadable is the one real preview field here. To surface preview media publicly, the frontend must call the seller-facing GET .../preview endpoint (17b) — note that endpoint currently requires shop-owner/admin auth, so a fully public preview-viewing path doesn't exist yet
  • colors[].images are raw FileThunder file ID strings, not resolved URLs (same gap as the detailed response)
  • isInCart/cartQuantity/isInWishlist/wishlistItemId/wishlistGroupId/wishlistGroupName are 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 long
  • 404: 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 error
  • 404: 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 errors
  • 404: 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.


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

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:

  1. POST api/v1/files/request-upload with context: "DIGITAL_PRODUCT" → receive uploadUrl and fileId
  2. PUT {uploadUrl} with the raw file bytes (client-to-storage, not through this API)
  3. POST .../digital-file/register with { "ftFileId": fileId } — do this immediately, you don't need to wait for the upload/scan to finish
  4. Poll GET .../digital-file until status (or pending.status) is READY; FAILED means re-upload and register again

Notes:

  • The top-level fileName/contentType/fileSize/status/uploadedAt describe the active (already-promoted) file; pending describes 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 digitalFileId field — same underlying registration logic

Error Responses:

  • 400: File not found in Redis (upload expired/never started), or file does not belong to the caller
  • 401: Authentication required
  • 403: Insufficient permissions, or product is not DIGITAL
  • 404: 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 required
  • 403: Insufficient permissions
  • 404: 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 required
  • 403: Insufficient permissions
  • 404: 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

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:

  1. POST api/v1/files/request-upload with context: "PRODUCT_PREVIEW_VIDEO" (or _IMAGE/_DOCUMENT) → receive uploadUrl and fileId
  2. PUT {uploadUrl} with the raw file bytes (client-to-storage, not through this API)
  3. POST .../preview/register with { "ftFileId": fileId }
  4. Poll GET .../preview (17b) until previewStatus is READYpreviewVariants will 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 caller
  • 401: Authentication required
  • 403: Insufficient permissions
  • 404: 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 required
  • 403: Insufficient permissions
  • 404: 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 required
  • 403: Insufficient permissions
  • 404: 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 not READY yet, or previewDownloadable is false for this product
  • 401: Authentication required
  • 404: 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