Listings Client
The Listings client provides synchronous and asynchronous operations to search and bulk upsert product listings. A listing represents an article that is offered in a specific facility, and includes details such as title, pricing, categories, and custom attributes.
Quick Start
Import the necessary classes and create a bulk upsert request:
import de.joesst.dev.fulfillmenttools.listings.*;
import de.joesst.dev.fulfillmenttools.id.FacilityId;
import de.joesst.dev.fulfillmenttools.id.TenantArticleId;
// Create a listing to upsert
ListingUpsertItem item = ListingUpsertItem.builder()
.facilityId(FacilityId.builder().value("fac-001").build())
.tenantArticleId(TenantArticleId.builder().value("art-123").build())
.title("Classic Widget")
.imageUrl("https://example.com/image.jpg")
.scannableCodes(List.of("123456789", "9876543210"))
.build();
// Bulk upsert
List<Listing> upserted = client.listings().bulkUpsert(
ListingBulkUpsertRequest.builder()
.listings(List.of(item))
.build()
);
System.out.println("Upserted: " + upserted.size() + " listings");Core Concepts
Listing Identity
Each listing is identified by a combination of facilityId and tenantArticleId. The tenantArticleId is your internal product identifier — it must be unique within your tenant and never changes for a given article.
Immutable Records
Listing and ListingUpsertItem are immutable and thread-safe. Use their respective builder() methods to construct instances.
Pagination
Search results are returned as Page<Listing> objects. Use page.hasMore() to check if more results are available, and page.nextCursor() to fetch the next page.
Bulk Upsert
Create or update multiple listings in a single operation. Both facilityId and tenantArticleId are required; all other fields are optional.
Synchronous Bulk Upsert
List<ListingUpsertItem> items = List.of(
ListingUpsertItem.builder()
.facilityId(FacilityId.builder().value("fac-001").build())
.tenantArticleId(TenantArticleId.builder().value("SKU-001").build())
.title("Widget Pro")
.categoryRefs(List.of("widgets", "bestsellers"))
.build(),
ListingUpsertItem.builder()
.facilityId(FacilityId.builder().value("fac-001").build())
.tenantArticleId(TenantArticleId.builder().value("SKU-002").build())
.title("Widget Basic")
.categoryRefs(List.of("widgets"))
.build()
);
List<Listing> result = client.listings().bulkUpsert(
ListingBulkUpsertRequest.builder()
.listings(items)
.build()
);Asynchronous Bulk Upsert
client.listings().bulkUpsertAsync(
ListingBulkUpsertRequest.builder()
.listings(items)
.build()
).thenAccept(listings -> {
System.out.println("Upserted " + listings.size() + " listings");
}).exceptionally(ex -> {
System.err.println("Bulk upsert failed: " + ex.getMessage());
return null;
});Searching Listings
Search for listings using flexible query criteria. Results are returned one page at a time; use searchAll() to automatically iterate through all pages.
Basic Search by Article ID
Page<Listing> page = client.listings().search(
ListingSearchRequest.builder()
.query(ListingSearchQuery.builder()
.tenantArticleIdEq("SKU-001")
.build())
.size(50)
.build()
);
for (Listing listing : page.items()) {
System.out.println("ID: " + listing.id() + ", Title: " + listing.title());
}Search with Multiple Filters
Page<Listing> page = client.listings().search(
ListingSearchRequest.builder()
.query(ListingSearchQuery.builder()
.statusEq("ACTIVE")
.facilityRefEq("fac-001")
.categoryRefsContains("electronics")
.build())
.size(100)
.build()
);Search Multiple Values (OR Condition)
Page<Listing> page = client.listings().search(
ListingSearchRequest.builder()
.query(ListingSearchQuery.builder()
.tenantArticleIdIn("SKU-001", "SKU-002", "SKU-003")
.statusIn("ACTIVE", "INACTIVE")
.build())
.size(50)
.build()
);Combined Filters (AND/OR Logic)
ListingSearchQuery activeInFac1 = ListingSearchQuery.builder()
.statusEq("ACTIVE")
.facilityRefEq("fac-001")
.build();
ListingSearchQuery activeInFac2 = ListingSearchQuery.builder()
.statusEq("ACTIVE")
.facilityRefEq("fac-002")
.build();
// Find active listings in either facility
Page<Listing> page = client.listings().search(
ListingSearchRequest.builder()
.query(ListingSearchQuery.builder()
.or(activeInFac1, activeInFac2)
.build())
.size(50)
.build()
);Pagination
Use after (cursor-based) or before parameters along with size to control pagination:
// First page
Page<Listing> page1 = client.listings().search(
ListingSearchRequest.builder()
.query(ListingSearchQuery.builder()
.statusEq("ACTIVE")
.build())
.size(50)
.build()
);
// Fetch next page if available
if (page1.hasMore()) {
Page<Listing> page2 = client.listings().search(
ListingSearchRequest.builder()
.query(ListingSearchQuery.builder()
.statusEq("ACTIVE")
.build())
.size(50)
.after(page1.nextCursor())
.build()
);
}Search All Results
Automatically iterate through all pages without manual cursor management:
Iterable<Listing> allListings = client.listings().searchAll(
ListingSearchRequest.builder()
.query(ListingSearchQuery.builder()
.statusEq("ACTIVE")
.build())
.size(100) // Page size; API fetches pages automatically
.build()
);
for (Listing listing : allListings) {
System.out.println(listing.tenantArticleId().value() + ": " + listing.title());
}Asynchronous Search
client.listings().searchAsync(
ListingSearchRequest.builder()
.query(ListingSearchQuery.builder()
.facilityRefEq("fac-001")
.statusEq("ACTIVE")
.build())
.size(50)
.build()
).thenAccept(page -> {
System.out.println("Found " + page.items().size() + " listings");
if (page.hasMore()) {
System.out.println("More results available; use nextCursor: " + page.nextCursor());
}
}).exceptionally(ex -> {
System.err.println("Search failed: " + ex.getMessage());
return null;
});Accessing Listing Fields
Once a listing is retrieved, access its properties via accessor methods:
Listing listing = upserted.get(0);
// Identity and status
ListingId listingId = listing.id();
String rawId = listingId.value(); // Extract the raw string from the typed ID
TenantArticleId articleId = listing.tenantArticleId();
FacilityId facilityId = listing.facilityId();
String status = listing.status(); // "ACTIVE" or "INACTIVE"
// Content
String title = listing.title();
Map<String, String> titlesByLocale = listing.titleLocalized();
String imageUrl = listing.imageUrl();
List<String> barcodes = listing.scannableCodes();
List<String> categories = listing.categoryRefs();
// Metadata
Integer version = listing.version();
Instant created = listing.created();
Instant lastModified = listing.lastModified();
// Custom metadata
CustomAttributes customAttrs = listing.customAttributes();
Map<String, Object> raw = customAttrs.attributes();Error Handling
All synchronous operations throw FulfillmenttoolsException on failure. This includes HTTP errors, network issues, and authentication failures.
Catching API Errors
import de.joesst.dev.fulfillmenttools.FulfillmenttoolsException;
import de.joesst.dev.fulfillmenttools.ApiError;
try {
List<Listing> upserted = client.listings().bulkUpsert(
ListingBulkUpsertRequest.builder()
.listings(items)
.build()
);
} catch (FulfillmenttoolsException ex) {
int status = ex.statusCode();
String requestId = ex.requestId();
List<ApiError> errors = ex.errors();
if (status == 400) {
System.err.println("Bad request:");
for (ApiError error : errors) {
System.err.println(" - " + error.summary());
}
} else if (status == 401) {
System.err.println("Authentication failed (request: " + requestId + ")");
} else if (status >= 500) {
System.err.println("Server error (request: " + requestId + ")");
}
}Handling Async Errors
Async methods return CompletableFuture and do not throw directly. Use exceptionally() or handle() to process errors:
client.listings().bulkUpsertAsync(request)
.exceptionally(throwable -> {
Throwable cause = throwable;
// Unwrap CompletionException if present
if (throwable instanceof java.util.concurrent.CompletionException) {
cause = throwable.getCause();
}
if (cause instanceof FulfillmenttoolsException ex) {
System.err.println("API error: " + ex.getMessage());
System.err.println("Request ID: " + ex.requestId());
} else {
System.err.println("Unexpected error: " + cause.getMessage());
}
return null;
});API Reference
bulkUpsert(ListingBulkUpsertRequest)
Bulk upserts product listings (creates or updates). Both facilityId and tenantArticleId are required on each item; matches are based on this combination.
Signature:
List<Listing> bulkUpsert(ListingBulkUpsertRequest request)Parameters:
request: ListingBulkUpsertRequest— Request containing a list of listings to upsert
Returns: List<Listing> — The upserted listings with their assigned IDs and versions
Throws: FulfillmenttoolsException if the request fails or contains invalid data
Example:
List<Listing> upserted = client.listings().bulkUpsert(
ListingBulkUpsertRequest.builder()
.listings(List.of(
ListingUpsertItem.builder()
.facilityId(FacilityId.builder().value("fac-001").build())
.tenantArticleId(TenantArticleId.builder().value("SKU-001").build())
.title("Product A")
.build()
))
.build()
);bulkUpsertAsync(ListingBulkUpsertRequest)
Asynchronously bulk upserts product listings.
Signature:
CompletableFuture<List<Listing>> bulkUpsertAsync(ListingBulkUpsertRequest request)Parameters:
request: ListingBulkUpsertRequest— Request containing a list of listings to upsert
Returns: CompletableFuture<List<Listing>> — Future that resolves to the upserted listings
Example:
client.listings().bulkUpsertAsync(request)
.thenAccept(listings -> System.out.println("Upserted: " + listings.size()))
.exceptionally(ex -> {
System.err.println("Failed: " + ex.getMessage());
return null;
});search(ListingSearchRequest)
Searches for product listings matching the specified criteria, returning one page of results.
Signature:
Page<Listing> search(ListingSearchRequest request)Parameters:
request: ListingSearchRequest— Search request with query and pagination parameters
Returns: Page<Listing> — A single page of matching listings; use page.hasMore() and page.nextCursor() to fetch additional pages
Throws: FulfillmenttoolsException if the request fails
Example:
Page<Listing> page = client.listings().search(
ListingSearchRequest.builder()
.query(ListingSearchQuery.builder()
.tenantArticleIdEq("SKU-001")
.statusEq("ACTIVE")
.build())
.size(50)
.build()
);
for (Listing listing : page.items()) {
System.out.println(listing.title());
}searchAll(ListingSearchRequest)
Searches for product listings and automatically iterates through all pages. Useful for batch operations where you need all matching results.
Signature:
Iterable<Listing> searchAll(ListingSearchRequest request)Parameters:
request: ListingSearchRequest— Search request with query and optional page size (defaults internally if not set)
Returns: Iterable<Listing> — An iterable that transparently fetches additional pages as needed
Example:
Iterable<Listing> allListings = client.listings().searchAll(
ListingSearchRequest.builder()
.query(ListingSearchQuery.builder()
.statusEq("ACTIVE")
.build())
.size(100)
.build()
);
// Iterate and process — pages are fetched automatically
for (Listing listing : allListings) {
System.out.println(listing.title());
}searchAsync(ListingSearchRequest)
Asynchronously searches for product listings, returning one page of results.
Signature:
CompletableFuture<Page<Listing>> searchAsync(ListingSearchRequest request)Parameters:
request: ListingSearchRequest— Search request with query and pagination parameters
Returns: CompletableFuture<Page<Listing>> — Future that resolves to a page of listings
Example:
client.listings().searchAsync(
ListingSearchRequest.builder()
.query(ListingSearchQuery.builder()
.facilityRefEq("fac-001")
.build())
.size(50)
.build()
).thenAccept(page -> {
System.out.println("Found " + page.items().size() + " listings");
}).exceptionally(ex -> {
System.err.println("Search failed: " + ex.getMessage());
return null;
});Query Builder Reference
ListingSearchQuery provides a fluent builder for constructing search filters:
Filtering by Listing ID
idEq(String)— Match a single listing IDidIn(String...)— Match any of the given listing IDsidNotEq(String)— Exclude a listing IDidNotIn(List<String>)— Exclude multiple listing IDs
Filtering by Tenant Article ID
tenantArticleIdEq(String)— Match a single tenant article IDtenantArticleIdIn(String...)— Match any of the given tenant article IDstenantArticleIdNotEq(String)— Exclude a tenant article ID
Filtering by Facility
facilityRefEq(String)— Match a single facility referencefacilityRefIn(String...)— Match any of the given facility referencesfacilityRefNotEq(String)— Exclude a facility reference
Filtering by Status
statusEq(String)— Match a specific status (e.g.,"ACTIVE","INACTIVE")statusIn(String...)— Match any of the given statusesstatusNotEq(String)— Exclude a status
Filtering by Price
priceEq(Number)— Match an exact pricepriceGt(Number)— Price greater thanpriceGte(Number)— Price greater than or equalpriceLt(Number)— Price less thanpriceLte(Number)— Price less than or equal
Filtering by Weight
weightEq(Number)— Match an exact weightweightGt(Number)— Weight greater thanweightGte(Number)— Weight greater than or equalweightLt(Number)— Weight less thanweightLte(Number)— Weight less than or equal
Filtering by Categories
categoryRefsContains(String)— Match listings that have a specific category
Filtering by Tags
tagsContainId(TagId)— Match listings that have a tag with a specific IDtagsContainValue(String)— Match listings that have a tag with a specific value
Filtering by Creation Date
createdEq(String)— Match an exact creation date (ISO 8601)createdGt(String)— Created after (ISO 8601)createdGte(String)— Created on or after (ISO 8601)createdLt(String)— Created before (ISO 8601)createdLte(String)— Created on or before (ISO 8601)
Custom Attributes
customAttribute(String key, Object filter)— Filter on a custom attribute by key
Combining Filters
and(ListingSearchQuery...)— Combine multiple queries with AND logicor(ListingSearchQuery...)— Combine multiple queries with OR logic
Example:
ListingSearchQuery query = ListingSearchQuery.builder()
.statusEq("ACTIVE")
.facilityRefIn("fac-001", "fac-002")
.categoryRefsContains("electronics")
.priceGte(10.0)
.priceLte(100.0)
.build();Troubleshooting
"facilityId must not be null" or "tenantArticleId must not be null"
Both fields are required when constructing a ListingUpsertItem. Ensure you provide both:
// Wrong: missing facilityId
ListingUpsertItem item = ListingUpsertItem.builder()
.tenantArticleId(TenantArticleId.builder().value("SKU-001").build())
.title("Product")
.build(); // NullPointerException during build()
// Correct
ListingUpsertItem item = ListingUpsertItem.builder()
.facilityId(FacilityId.builder().value("fac-001").build())
.tenantArticleId(TenantArticleId.builder().value("SKU-001").build())
.title("Product")
.build();Search returns no results
Verify that your query criteria match the actual data. Use a broad query to test:
// Broad search to verify data exists
Page<Listing> all = client.listings().search(
ListingSearchRequest.builder()
.query(ListingSearchQuery.builder().build()) // Empty query matches all
.size(10)
.build()
);
System.out.println("Total listings: " + all.items().size());CompletionException in async handlers
When using async methods, exceptions are wrapped in CompletionException. Unwrap them in error handlers:
future.exceptionally(throwable -> {
Throwable cause = throwable;
if (throwable instanceof java.util.concurrent.CompletionException) {
cause = throwable.getCause();
}
if (cause instanceof FulfillmenttoolsException ex) {
// Handle expected SDK exception
}
return null;
});Request timed out
Network timeouts indicate the API is slow or unavailable. Increase the client timeout or retry with exponential backoff:
// Retry logic with exponential backoff
int maxRetries = 3;
int delayMs = 1000;
for (int attempt = 0; attempt < maxRetries; attempt++) {
try {
return client.listings().search(request);
} catch (FulfillmenttoolsException ex) {
if (attempt < maxRetries - 1 && ex.statusCode() >= 500) {
Thread.sleep(delayMs);
delayMs *= 2;
} else {
throw ex;
}
}
}