Customer Management

The Customer Management API provides offline-first access to customer data with comprehensive filtering, sorting, and pagination capabilities.

Table of contents

  1. Fetch Customers (Paginated)
    1. Parameters
    2. ZyncCustomerSortAndFilter Options
    3. Return Value: GetCustomersResult
  2. Get Customer Detail
    1. Parameters
    2. Return Value: CustomerDetailResult
  3. Customer Data Models
    1. ZyncCustomer (List Item)
    2. ZyncCustomerDetail (Full Detail)
  4. Best Practices
    1. Pagination
    2. Filtering & Sorting
    3. Offline-First Behavior
    4. Error Handling
  5. Common Use Cases
    1. Search Customers by Keyword
    2. Filter by Organization
    3. Get Customer with Related Data

Fetch Customers (Paginated)

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

import zync.api.customer.models.GetCustomersResult
import zync.api.customer.models.ZyncCustomerSortAndFilter
import zync.api.customer.models.ZyncCustomerSortBy
import zync.api.shared.models.ZyncSortType

val sortAndFilter = ZyncCustomerSortAndFilter(
    sortType = ZyncSortType.Descending,
    sortBy = ZyncCustomerSortBy.CUSTOMER_NAME,
    keyword = "john",
    isActive = true
)

when (val result = zync.customer.fetchCustomers(
    sortAndFilter = sortAndFilter,
    page = 1,
    pageSize = 20
)) {
    is GetCustomersResult.Success -> {
        val customers = result.data
        val currentPage = result.currentPage
        val totalPages = result.totalPages
        val totalRecords = result.totalRecords
        val isPartialData = result.isPartialData

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

let sortAndFilter = ZyncCustomerSortAndFilter(
    sortType: .descending,
    sortBy: .customerName,
    keyword: "john",
    isActive: true
)

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

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

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

Parameters

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

ZyncCustomerSortAndFilter Options

PropertyTypeDescription
sortTypeZyncSortTypeSort direction (Ascending/Descending)
sortByZyncCustomerSortByField to sort by (DEFAULT, CUSTOMER_NAME, CREATED_TIME, etc.)
keywordString?Search keyword for customer name, email, or phone
filterCategoryString?Filter by customer category UID
jobRangeZyncCustomerJobRange?Filter by number of jobs (min/max)
customerTagsList<String>?Filter by customer tag UIDs
createdDateRangeZyncFilterDateRange?Filter by creation date range
createdByUserZyncFilterModule?Filter by creator user UID
customFieldZyncFilterByCustomField?Filter by custom field values
isActiveBoolean?Filter by active status
accountManagerZyncFilterModule?Filter by account manager UID
organizationZyncFilterModule?Filter by organization UID
hasCardOnFileBoolean?Filter by payment card status
hasOrganizationBoolean?Filter customers with/without organization
uidsList<String>?Filter by specific customer UIDs

Return Value: GetCustomersResult

Success Case:

  • data: List of ZyncCustomer objects
  • currentPage: Current page number
  • totalPages: Total number of pages available
  • totalRecords: Total number of customers 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 Customer Detail

Retrieves comprehensive details for a specific customer, including related data like attachments, custom fields, and account information.

import zync.api.customer.models.CustomerDetailResult

when (val result = zync.customer.getCustomerDetail(
    customerUid = "550e8400-e29b-41d4-a716-446655440000"
)) {
    is CustomerDetailResult.Success -> {
        val customer = result.data
        val lastSynced = result.lastSyncedAt
        val syncStatus = result.syncStatus

        println("Customer: ${customer.firstName} ${customer.lastName ?: ""}")
        println("Email: ${customer.email}")
        println("Phone: ${customer.contactNo?.number}")
        println("Jobs: ${customer.noOfJobs}")
        println("Organization: ${customer.organization?.name}")
        println("Custom Fields: ${customer.customFields.size}")
        println("Attachments: ${customer.attachments.size}")

        if (customer.isSyncPending) {
            println("Has pending changes to sync")
        }

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

switch onEnum(of: await zync.customer.getCustomerDetail(
    customerUid: "550e8400-e29b-41d4-a716-446655440000"
)) {
case .success(let success):
    let customer = success.data
    let lastSynced = success.lastSyncedAt
    let syncStatus = success.syncStatus

    print("Customer: \(customer.firstName) \(customer.lastName ?? "")")
    print("Email: \(customer.email ?? "N/A")")
    print("Phone: \(customer.contactNo?.number ?? "N/A")")
    print("Jobs: \(customer.noOfJobs ?? 0)")
    print("Organization: \(customer.organization?.name ?? "N/A")")
    print("Custom Fields: \(customer.customFields.count)")
    print("Attachments: \(customer.attachments.count)")

    if customer.isSyncPending {
        print("Has pending changes to sync")
    }

    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
customerUidStringUnique identifier of the customer

Return Value: CustomerDetailResult

Success Case:

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

Failure Case:

  • error: ZyncError with error details

Customer Data Models

ZyncCustomer (List Item)

Basic customer information returned in paginated lists:

PropertyTypeDescription
uidStringUnique identifier
firstNameStringCustomer’s first name
lastNameString?Customer’s last name
emailString?Email address
contactNoZyncContactNumber?Phone number with country code
noOfJobsInt?Number of jobs associated with customer
companyNameString?Company name
organizationZyncOrganization?Associated organization
timezoneString?Customer’s timezone
categoryZyncCustomerCategory?Customer category
billingAddressZyncAddress?Billing address
addressZyncAddress?Primary address
allAddressesList<ZyncAddress>?All customer addresses
isActiveBooleanActive status
fullNameStringComputed full name (firstName + lastName)

ZyncCustomerDetail (Full Detail)

Comprehensive customer information including all related data:

PropertyTypeDescription
customerUidStringUnique identifier
firstNameStringCustomer’s first name
lastNameString?Customer’s last name
emailString?Email address
contactNoZyncContactNumber?Phone number with country code
timezoneString?Customer’s timezone
companyNameString?Company name
descriptionString?Customer description
noOfJobsInt?Number of jobs
isActiveBooleanActive status
organizationZyncOrganization?Associated organization
categoryZyncCustomerCategory?Customer category
addressZyncAddress?Primary address
billingAddressZyncAddress?Billing address
allAddressesList<ZyncAddress>All addresses
tagsList<String>Customer tags
accountsZyncCustomerAccounts?Account information
slaDurationZyncSlaDuration?SLA configuration
accountManagerUser?Assigned account manager
customFieldsList<ZyncFormField>Custom field values
attachmentsList<ZyncAttachment>Customer attachments
priceListZyncPriceList?Associated price list
customerTaxZyncCustomerTax?Tax information
hasSlaBooleanSLA enabled status
hasCardOnFileBooleanPayment card on file
createdAtStringCreation timestamp
updatedAtString?Last update timestamp
isSyncPendingBooleanHas pending changes to sync

Best Practices

Pagination

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

suspend fun loadAllCustomers() {
    val filter = ZyncCustomerSortAndFilter(isActive = true)
    var currentPage = 1
    val pageSize = 50

    do {
        when (val result = zync.customer.fetchCustomers(filter, currentPage, pageSize)) {
            is GetCustomersResult.Success -> {
                processCustomers(result.data)
                currentPage++

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

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

    while true {
        switch onEnum(of: await zync.customer.fetchCustomers(
            sortAndFilter: filter,
            page: currentPage,
            pageSize: pageSize
        )) {
        case .success(let success):
            processCustomers(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 multiple filter properties together for precise results. Use keyword to search across name, email, and phone fields.

val filter = ZyncCustomerSortAndFilter(
    sortType = ZyncSortType.Descending,
    sortBy = ZyncCustomerSortBy.CREATED_TIME,
    keyword = "acme",
    isActive = true,
    createdDateRange = ZyncFilterDateRange(
        fromDate = "2024-01-01",
        toDate = "2024-12-31"
    ),
    hasCardOnFile = true
)

val result = zync.customer.fetchCustomers(filter, 1, 50)
let filter = ZyncCustomerSortAndFilter(
    sortType: .descending,
    sortBy: .createdTime,
    keyword: "acme",
    isActive: true,
    createdDateRange: ZyncFilterDateRange(
        fromDate: "2024-01-01",
        toDate: "2024-12-31"
    ),
    hasCardOnFile: true
)

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

Offline-First Behavior

The Customer 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. Check isSyncPending in detail view for pending changes.

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.customer.getCustomerDetail(customerUid)) {
    is CustomerDetailResult.Success -> {
        // Handle success
    }
    is CustomerDetailResult.Failure -> {
        when (result.error.httpStatusCode) {
            404 -> println("Customer not found")
            401 -> println("Authentication required")
            else -> println("Error: ${result.error.message}")
        }
    }
}
switch onEnum(of: await zync.customer.getCustomerDetail(customerUid: customerUid)) {
case .success(let success):
    // Handle success

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

Common Use Cases

Search Customers by Keyword

val searchFilter = ZyncCustomerSortAndFilter(
    keyword = "john smith",
    isActive = true
)

when (val result = zync.customer.fetchCustomers(searchFilter, 1, 20)) {
    is GetCustomersResult.Success -> {
        println("Found ${result.totalRecords} customers matching 'john smith'")
        result.data.forEach { customer ->
            println("${customer.fullName} - ${customer.email}")
        }
    }
    is GetCustomersResult.Failure -> {
        println("Search failed: ${result.error.message}")
    }
}
let searchFilter = ZyncCustomerSortAndFilter(
    keyword: "john smith",
    isActive: true
)

switch onEnum(of: await zync.customer.fetchCustomers(
    sortAndFilter: searchFilter,
    page: 1,
    pageSize: 20
)) {
case .success(let success):
    print("Found \(success.totalRecords) customers matching 'john smith'")
    success.data.forEach { customer in
        print("\(customer.fullName) - \(customer.email ?? "N/A")")
    }

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

Filter by Organization

val orgFilter = ZyncCustomerSortAndFilter(
    organization = ZyncFilterModule(uid = "org-uid-123"),
    isActive = true,
    sortBy = ZyncCustomerSortBy.CUSTOMER_NAME
)

val result = zync.customer.fetchCustomers(orgFilter, 1, 50)
let orgFilter = ZyncCustomerSortAndFilter(
    organization: ZyncFilterModule(uid: "org-uid-123"),
    isActive: true,
    sortBy: .customerName
)

let result = await zync.customer.fetchCustomers(
    sortAndFilter: orgFilter,
    page: 1,
    pageSize: 50
)
when (val result = zync.customer.getCustomerDetail(customerUid)) {
    is CustomerDetailResult.Success -> {
        val customer = result.data

        // Access related data
        println("Addresses: ${customer.allAddresses.size}")
        println("Tags: ${customer.tags.joinToString()}")
        println("Account Manager: ${customer.accountManager?.fullName}")

        // Access custom fields
        customer.customFields.forEach { field ->
            println("${field.fieldName}: ${field.fieldValue}")
        }

        // Access attachments
        customer.attachments.forEach { attachment ->
            println("Attachment: ${attachment.fileName}")
        }
    }
    is CustomerDetailResult.Failure -> {
        println("Error: ${result.error.message}")
    }
}
switch onEnum(of: await zync.customer.getCustomerDetail(customerUid: customerUid)) {
case .success(let success):
    let customer = success.data

    // Access related data
    print("Addresses: \(customer.allAddresses.count)")
    print("Tags: \(customer.tags.joined(separator: ", "))")
    print("Account Manager: \(customer.accountManager?.fullName ?? "N/A")")

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

    // Access attachments
    customer.attachments.forEach { attachment in
        print("Attachment: \(attachment.fileName)")
    }

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

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