Product Management

The Product Management API provides offline-first access to product catalog data with comprehensive filtering, sorting, and pagination capabilities.

Table of contents

  1. Get Products (Paginated)
    1. Parameters
    2. ZyncProductSortAndFilter Options
    3. Return Value: GetProductsResult
  2. Get Product Categories (Paginated)
    1. Parameters
    2. Return Value: GetProductCategoriesResult
  3. Get Product Detail
    1. Parameters
    2. ZyncFetchType Options
    3. Return Value: ProductDetailResult
  4. Product Data Models
    1. ZyncProduct (List Item)
    2. ZyncProductDetail (Full Detail)
    3. ZyncProductCategory
    4. ZyncProductType Enum
  5. Best Practices
    1. Pagination
    2. Filtering & Sorting
    3. Offline-First Behavior
    4. Error Handling
  6. Common Use Cases
    1. Search Products by Keyword
    2. Filter by Category and Type
    3. Get Bundle Product with Components
    4. Scan Barcode to Find Product

Get Products (Paginated)

Retrieves a paginated list of products with advanced filtering and sorting options.

import zync.api.product.models.GetProductsResult
import zync.api.product.models.ZyncProductSortAndFilter
import zync.api.product.models.ZyncProductSortBy
import zync.api.product.models.ZyncProductType
import zync.api.common.filter.ZyncSortType

val sortAndFilter = ZyncProductSortAndFilter(
    sortType = ZyncSortType.Descending,
    sortBy = ZyncProductSortBy.PRODUCT_NAME,
    keyword = "hammer",
    isAvailable = true,
    type = listOf(ZyncProductType.PRODUCT, ZyncProductType.PART)
)

when (val result = zync.product.getProducts(
    sortAndFilter = sortAndFilter,
    page = 1,
    pageSize = 20
)) {
    is GetProductsResult.Success -> {
        val products = result.data
        val currentPage = result.currentPage
        val totalPages = result.totalPages
        val totalRecords = result.totalRecords
        val isPartialData = result.isPartialData

        println("Fetched ${products.size} products (page $currentPage of $totalPages)")
        if (isPartialData) {
            println("Data from cache - may be incomplete")
        }
    }
    is GetProductsResult.Failure -> {
        println("Error: ${result.error.message}")
    }
}
import ZuperSync

let sortAndFilter = ZyncProductSortAndFilter(
    sortType: .descending,
    sortBy: .productName,
    keyword: "hammer",
    isAvailable: true,
    type: [.product, .part]
)

switch onEnum(of: await zync.product.getProducts(
    sortAndFilter: sortAndFilter,
    page: 1,
    pageSize: 20
)) {
case .success(let success):
    let products = success.data
    let currentPage = success.currentPage
    let totalPages = success.totalPages
    let isPartialData = success.isPartialData

    print("Fetched \(products.count) products (page \(currentPage) of \(totalPages))")
    if isPartialData {
        print("Data from cache - may be incomplete")
    }

case .failure(let failure):
    print("Error: \(failure.error.message)")
}

Parameters

ParameterTypeDescription
sortAndFilterZyncProductSortAndFilterComprehensive filter and sort options
pageIntPage number (1-based)
pageSizeIntNumber of products per page

ZyncProductSortAndFilter Options

PropertyTypeDescription
sortTypeZyncSortTypeSort direction (Ascending/Descending/DEFAULT)
sortByZyncProductSortByField to sort by (PRODUCT_ID, PRODUCT_NAME, CREATED_DATE, PRODUCT_NUMBER, QUANTITY)
keywordString?Search keyword for product name or ID
categoryString?Filter by product category UID
locationZyncFilterModule?Filter by location UID
typeList<ZyncProductType>?Filter by product types (PRODUCT, PART, SERVICE, BUNDLE, LABOR, CUSTOM_PRODUCT)
isAvailableBoolean?Filter by availability status
brandString?Filter by brand name
specificationString?Filter by specification
customFieldZyncFilterByCustomField?Filter by custom field values
uidList<String>?Filter by specific product UIDs
productIdString?Filter by product ID
priceListUidString?Filter by price list UID
scannedCodeString?Filter by scanned barcode

Return Value: GetProductsResult

Success Case:

  • data: List of ZyncProduct objects
  • currentPage: Current page number
  • totalPages: Total number of pages available
  • totalRecords: Total number of products across all pages
  • isPartialData: true if data is from cache (may be incomplete), false if from API

Failure Case:

  • error: ZyncError with error details

Get Product Categories (Paginated)

Retrieves a paginated list of product categories.

import zync.api.product.models.GetProductCategoriesResult

when (val result = zync.product.getProductCategories(
    page = 1,
    pageSize = 50
)) {
    is GetProductCategoriesResult.Success -> {
        val categories = result.data
        val currentPage = result.currentPage
        val totalPages = result.totalPages
        val isPartialData = result.isPartialData

        println("Fetched ${categories.size} categories (page $currentPage of $totalPages)")
        categories.forEach { category ->
            println("${category.name} (${category.categoryUid})")
        }
    }
    is GetProductCategoriesResult.Failure -> {
        println("Error: ${result.error.message}")
    }
}
import ZuperSync

switch onEnum(of: await zync.product.getProductCategories(
    page: 1,
    pageSize: 50
)) {
case .success(let success):
    let categories = success.data
    let currentPage = success.currentPage
    let totalPages = success.totalPages
    let isPartialData = success.isPartialData

    print("Fetched \(categories.count) categories (page \(currentPage) of \(totalPages))")
    categories.forEach { category in
        print("\(category.name) (\(category.categoryUid))")
    }

case .failure(let failure):
    print("Error: \(failure.error.message)")
}

Parameters

ParameterTypeDescription
pageIntPage number (1-based)
pageSizeIntNumber of categories per page

Return Value: GetProductCategoriesResult

Success Case:

  • data: List of ZyncProductCategory objects
  • currentPage: Current page number
  • totalPages: Total number of pages available
  • totalRecords: Total number of categories across all pages
  • isPartialData: true if data is from cache (may be incomplete), false if from API

Failure Case:

  • error: ZyncError with error details

Get Product Detail

Retrieves comprehensive details for a specific product, including pricing, inventory locations, bundle items, attachments, and custom fields.

import zync.api.product.models.ProductDetailResult
import zync.api.common.filter.ZyncFetchType

when (val result = zync.product.getProductDetail(
    productUid = "550e8400-e29b-41d4-a716-446655440000",
    fetchType = ZyncFetchType.NONE
)) {
    is ProductDetailResult.Success -> {
        val product = result.data
        val syncStatus = result.syncStatus
        val lastSynced = result.lastSyncedAt

        println("Product: ${product.productName}")
        println("ID: ${product.productId}")
        println("Type: ${product.productType}")
        println("Price: ${product.price} ${product.currency}")
        println("Available: ${product.isAvailable}")
        println("Quantity: ${product.quantity}")
        println("Category: ${product.category?.name}")
        println("Bundle Items: ${product.bundleItems.size}")
        println("Locations: ${product.locationItems.size}")
        println("Attachments: ${product.attachments.size}")

        if (syncStatus == ZyncDataSyncStatus.AGED) {
            println("Note: Data is older than 5 minutes. Last synced: $lastSynced")
        }
    }
    is ProductDetailResult.Failure -> {
        println("Error: ${result.error.message}")
    }
}
import ZuperSync

switch onEnum(of: await zync.product.getProductDetail(
    productUid: "550e8400-e29b-41d4-a716-446655440000",
    fetchType: .none
)) {
case .success(let success):
    let product = success.data
    let syncStatus = success.syncStatus
    let lastSynced = success.lastSyncedAt

    print("Product: \(product.productName)")
    print("ID: \(product.productId ?? "N/A")")
    print("Type: \(product.productType.rawValue)")
    print("Price: \(product.price ?? 0) \(product.currency ?? "")")
    print("Available: \(product.isAvailable)")
    print("Quantity: \(product.quantity)")
    print("Category: \(product.category?.name ?? "N/A")")
    print("Bundle Items: \(product.bundleItems.count)")
    print("Locations: \(product.locationItems.count)")
    print("Attachments: \(product.attachments.count)")

    if syncStatus == .aged {
        print("Note: Data is older than 5 minutes. Last synced: \(lastSynced)")
    }

case .failure(let failure):
    print("Error: \(failure.error.message)")
}

Parameters

ParameterTypeDescription
productUidStringUnique identifier of the product
fetchTypeZyncFetchTypeData freshness strategy (default: NONE)

ZyncFetchType Options

ValueDescription
NONEUse cached data with background sync (default)
MARK_STALEMark current cache as stale and fetch with 3s timeout
FORCE_REFRESHForce immediate API call without timeout, fallback to cache on failure

Return Value: ProductDetailResult

Success Case:

  • data: ZyncProductDetail object with comprehensive product information
  • syncStatus: ZyncDataSyncStatus - Indicates the data freshness (NONE, AGED - older than 5 minutes, PARTIAL_RECORD, OUTDATED_RECORD)
  • lastSyncedAt: String - ISO-8601 formatted timestamp of when this record was last synced from the server

Failure Case:

  • error: ZyncError with error details

Product Data Models

ZyncProduct (List Item)

Basic product information returned in paginated lists:

PropertyTypeDescription
productUidStringUnique identifier
productNameStringProduct name
productIdString?Product ID/code
prefixString?Product prefix
productDescriptionString?Product description
productImageString?Product image URL
productTypeZyncProductTypeProduct type enum
brandString?Brand name
specificationString?Product specification
locationsList<ZyncProductLocationAvailability>?Inventory locations
quantityDoubleAvailable quantity
minQuantityDoubleMinimum quantity threshold
priceDouble?Selling price
purchasePriceDouble?Purchase price
uomString?Unit of measure
currencyString?Currency code
isAvailableBooleanAvailability status
bundlePricingLevelZyncBundlePricingLevelBundle pricing level
bundleItemsList<ZyncProductBundleItem>Bundle component items
taxZyncLineItemCustomTax?Tax information
markupZyncProductMarkup?Markup configuration
productCategoryZyncProductCategory?Product category
isBillableBooleanBillable status

ZyncProductDetail (Full Detail)

Comprehensive product information including all related data:

PropertyTypeDescription
productUidStringUnique identifier
productNameStringProduct name
productIdString?Product ID/code
prefixString?Product prefix
productDescriptionString?Product description
productImageString?Product image URL
productBarCodeString?Barcode/SKU
productTypeZyncProductTypeProduct type enum
brandString?Brand name
specificationString?Product specification
priceDouble?Selling price
purchasePriceDouble?Purchase price
currencyString?Currency code
hasCustomTaxBooleanCustom tax enabled
taxZyncLineItemCustomTax?Tax information
isBillableBooleanBillable status
quantityDoubleAvailable quantity
minQuantityDoubleMinimum quantity threshold
uomString?Unit of measure
isAvailableBooleanAvailability status
pricingLevelString?Pricing level
markupZyncProductMarkup?Markup configuration
attachmentsList<ZyncAttachment>Product attachments
bundleItemsList<ZyncProductBundleItem>Bundle component items
locationItemsList<ZyncProductLocationAvailability>Inventory by location
categoryZyncProductCategory?Product category
createdAtString?Creation timestamp
updatedAtString?Last update timestamp
isDeletedBooleanDeletion status
customFieldsList<ZyncFormField>Custom field values

ZyncProductCategory

Product category information:

PropertyTypeDescription
categoryUidStringUnique identifier
nameStringCategory name
iconString?Category icon URL

ZyncProductType Enum

Product type values:

ValueDescription
PRODUCTStandard product
PARTProduct part/component
SERVICEService item
CUSTOM_PRODUCTCustom product
BUNDLEProduct bundle
LABORLabor item

Best Practices

Pagination

Start with page 1 and use consistent page sizes across requests. Monitor the isPartialData flag to determine if data is from cache.

suspend fun loadAllProducts() {
    val filter = ZyncProductSortAndFilter(isAvailable = true)
    var currentPage = 1
    val pageSize = 50

    do {
        when (val result = zync.product.getProducts(filter, currentPage, pageSize)) {
            is GetProductsResult.Success -> {
                processProducts(result.data)
                currentPage++

                if (result.isPartialData) {
                    println("Warning: Showing cached data")
                }

                if (currentPage > result.totalPages) break
            }
            is GetProductsResult.Failure -> {
                println("Error loading page $currentPage: ${result.error.message}")
                break
            }
        }
    } while (true)
}
func loadAllProducts() async {
    let filter = ZyncProductSortAndFilter(isAvailable: true)
    var currentPage = 1
    let pageSize = 50

    while true {
        switch onEnum(of: await zync.product.getProducts(
            sortAndFilter: filter,
            page: currentPage,
            pageSize: pageSize
        )) {
        case .success(let success):
            processProducts(success.data)
            currentPage += 1

            if success.isPartialData {
                print("Warning: Showing cached data")
            }

            if currentPage > success.totalPages { break }

        case .failure(let failure):
            print("Error loading page \(currentPage): \(failure.error.message)")
            break
        }
    }
}

Filtering & Sorting

Combine filters to find products by category, type, brand, or location. Use keyword search for name or ID.

val filter = ZyncProductSortAndFilter(
    sortType = ZyncSortType.Ascending,
    sortBy = ZyncProductSortBy.PRODUCT_NAME,
    keyword = "wrench",
    category = "tool-category-uid",
    type = listOf(ZyncProductType.PRODUCT),
    isAvailable = true,
    brand = "Craftsman"
)

val result = zync.product.getProducts(filter, 1, 50)
let filter = ZyncProductSortAndFilter(
    sortType: .ascending,
    sortBy: .productName,
    keyword: "wrench",
    category: "tool-category-uid",
    type: [.product],
    isAvailable: true,
    brand: "Craftsman"
)

let result = await zync.product.getProducts(
    sortAndFilter: filter,
    page: 1,
    pageSize: 50
)

Offline-First Behavior

The Product Management API follows an offline-first approach. Data is immediately available from cache, with background synchronization when online. Always check the isPartialData flag to determine data freshness.

The SDK returns cached data when offline and syncs in the background when online.

Error Handling

Always handle both Success and Failure cases. Use error.message for user-friendly messages and error.httpStatusCode for specific error handling.

when (val result = zync.product.getProductDetail(productUid)) {
    is ProductDetailResult.Success -> {
        // Handle success
    }
    is ProductDetailResult.Failure -> {
        when (result.error.httpStatusCode) {
            404 -> println("Product not found")
            401 -> println("Authentication required")
            else -> println("Error: ${result.error.message}")
        }
    }
}
switch onEnum(of: await zync.product.getProductDetail(productUid: productUid)) {
case .success(let success):
    // Handle success

case .failure(let failure):
    switch failure.error.httpStatusCode {
    case 404:
        print("Product not found")
    case 401:
        print("Authentication required")
    default:
        print("Error: \(failure.error.message)")
    }
}

Common Use Cases

Search Products by Keyword

val searchFilter = ZyncProductSortAndFilter(
    keyword = "hammer",
    isAvailable = true
)

when (val result = zync.product.getProducts(searchFilter, 1, 20)) {
    is GetProductsResult.Success -> {
        println("Found ${result.totalRecords} products matching 'hammer'")
        result.data.forEach { product ->
            println("${product.productName} - ${product.price} ${product.currency}")
        }
    }
    is GetProductsResult.Failure -> {
        println("Search failed: ${result.error.message}")
    }
}
let searchFilter = ZyncProductSortAndFilter(
    keyword: "hammer",
    isAvailable: true
)

switch onEnum(of: await zync.product.getProducts(
    sortAndFilter: searchFilter,
    page: 1,
    pageSize: 20
)) {
case .success(let success):
    print("Found \(success.totalRecords) products matching 'hammer'")
    success.data.forEach { product in
        print("\(product.productName) - \(product.price ?? 0) \(product.currency ?? "")")
    }

case .failure(let failure):
    print("Search failed: \(failure.error.message)")
}

Filter by Category and Type

val categoryFilter = ZyncProductSortAndFilter(
    category = "tools-category-uid",
    type = listOf(ZyncProductType.PRODUCT, ZyncProductType.PART),
    isAvailable = true
)

val result = zync.product.getProducts(categoryFilter, 1, 50)
let categoryFilter = ZyncProductSortAndFilter(
    category: "tools-category-uid",
    type: [.product, .part],
    isAvailable: true
)

let result = await zync.product.getProducts(
    sortAndFilter: categoryFilter,
    page: 1,
    pageSize: 50
)

Get Bundle Product with Components

when (val result = zync.product.getProductDetail(productUid)) {
    is ProductDetailResult.Success -> {
        val product = result.data
        val syncStatus = result.syncStatus
        val lastSynced = result.lastSyncedAt

        println("Product: ${product.productName}")
        println("Type: ${product.productType}")
        println("Price: ${product.price} ${product.currency}")

        if (syncStatus == ZyncDataSyncStatus.AGED) {
            println("Note: Data is older than 5 minutes. Last synced: $lastSynced")
        }

        // Access bundle items
        if (product.productType == ZyncProductType.BUNDLE) {
            println("\nBundle Components (${product.bundleItems.size}):")
            product.bundleItems.forEach { item ->
                println("  - ${item.productName}")
                println("    Quantity: ${item.quantity}")
                println("    Price: ${item.price}")
            }
        }

        // Access inventory locations
        println("\nInventory Locations (${product.locationItems.size}):")
        product.locationItems.forEach { location ->
            println("  - ${location.locationName}: ${location.quantity} units")
        }

        // Access attachments
        println("\nAttachments (${product.attachments.size}):")
        product.attachments.forEach { attachment ->
            println("  - ${attachment.fileName}")
        }

        // Access custom fields
        println("\nCustom Fields (${product.customFields.size}):")
        product.customFields.forEach { field ->
            println("  ${field.fieldName}: ${field.fieldValue}")
        }
    }
    is ProductDetailResult.Failure -> {
        println("Error: ${result.error.message}")
    }
}
switch onEnum(of: await zync.product.getProductDetail(productUid: productUid)) {
case .success(let success):
    let product = success.data
    let syncStatus = success.syncStatus
    let lastSynced = success.lastSyncedAt

    print("Product: \(product.productName)")
    print("Type: \(product.productType.rawValue)")
    print("Price: \(product.price ?? 0) \(product.currency ?? "")")

    if syncStatus == .aged {
        print("Note: Data is older than 5 minutes. Last synced: \(lastSynced)")
    }

    // Access bundle items
    if product.productType == .bundle {
        print("\nBundle Components (\(product.bundleItems.count)):")
        product.bundleItems.forEach { item in
            print("  - \(item.productName)")
            print("    Quantity: \(item.quantity)")
            print("    Price: \(item.price ?? 0)")
        }
    }

    // Access inventory locations
    print("\nInventory Locations (\(product.locationItems.count)):")
    product.locationItems.forEach { location in
        print("  - \(location.locationName): \(location.quantity) units")
    }

    // Access attachments
    print("\nAttachments (\(product.attachments.count)):")
    product.attachments.forEach { attachment in
        print("  - \(attachment.fileName)")
    }

    // Access custom fields
    print("\nCustom Fields (\(product.customFields.count)):")
    product.customFields.forEach { field in
        print("  \(field.fieldName): \(field.fieldValue ?? "N/A")")
    }

case .failure(let failure):
    print("Error: \(failure.error.message)")
}

Scan Barcode to Find Product

val barcodeFilter = ZyncProductSortAndFilter(
    scannedCode = "1234567890123"
)

when (val result = zync.product.getProducts(barcodeFilter, 1, 1)) {
    is GetProductsResult.Success -> {
        if (result.data.isNotEmpty()) {
            val product = result.data.first()
            println("Found product: ${product.productName}")
            println("Price: ${product.price} ${product.currency}")
            println("Available: ${product.quantity} ${product.uom}")
        } else {
            println("No product found with this barcode")
        }
    }
    is GetProductsResult.Failure -> {
        println("Error: ${result.error.message}")
    }
}
let barcodeFilter = ZyncProductSortAndFilter(
    scannedCode: "1234567890123"
)

switch onEnum(of: await zync.product.getProducts(
    sortAndFilter: barcodeFilter,
    page: 1,
    pageSize: 1
)) {
case .success(let success):
    if let product = success.data.first {
        print("Found product: \(product.productName)")
        print("Price: \(product.price ?? 0) \(product.currency ?? "")")
        print("Available: \(product.quantity) \(product.uom ?? "")")
    } else {
        print("No product found with this barcode")
    }

case .failure(let failure):
    print("Error: \(failure.error.message)")
}

Copyright © 2025 Zuper Inc. All rights reserved. This software is proprietary and confidential.