Changing the catalog
Everything on this page requires the products:write scope, which is separate from
products:read on purpose: a read-only sync should not be one credential away from changing what
your site sells. Check what you hold with GET /v1/me.
Five endpoints:
| Endpoint | Changes |
|---|---|
PATCH /v1/products/{productId} |
Name, content, SEO, visibility, stock bounds, approval flag |
PATCH /v1/products/{productId}/variants/{variantId} |
One variant’s attribute choices, location, weight |
PUT /v1/products/{productId}/segments |
Which segments the product is shown to |
GET /v1/products/{productId}/images |
The product’s images and their metadata |
PATCH /v1/products/{productId}/images/{imageId} |
One image’s alt text, order, primary flag |
Inventory is read-only through this API. There is no endpoint to adjust stock, and that is a
deliberate decision rather than a gap. availableInventory and friends are reported by
GET /v1/products and cannot be written.
Read this first: four behaviors that will surprise you
Section titled “Read this first: four behaviors that will surprise you”These are not edge cases. Each one has bitten us during development, and none of them is visible
from a 200.
1. A product PATCH is not atomic
Section titled “1. A product PATCH is not atomic”PATCH /v1/products/{productId} fans out to up to four separate upstream operations depending on
which fields you send. Each one commits independently. If a later section fails, the earlier
ones stay applied - there is no rollback.
When that happens you get 409 product_write_partially_applied, and its detail names exactly
which sections did apply. Do not retry blindly: re-read the product, see what landed, and send only
what is still missing.
The order is fixed so that a half-applied patch fails safe: content is written before visibility, so a product never becomes visible while its content write is still outstanding.
2. A 200 does not always mean your field was applied
Section titled “2. A 200 does not always mean your field was applied”Two fields can be accepted upstream and then silently not applied, depending on permissions and
site configuration: requiresApproval and supplierReference. No status code reveals it.
So the product PATCH response carries unappliedFields: the fields you asked to change whose
value did not actually move.
{ "productId": "SLO_44431", "supplierReference": null, "unappliedFields": ["supplierReference"]}An empty array is a clean write. A non-empty one is not an error - the request was valid and the platform declined to apply that field. Check it rather than trusting the status.
3. The storefront may lag
Section titled “3. The storefront may lag”Nothing downstream is notified when you write. No search reindex, no cache eviction. A storefront can keep serving the previous values for a short time.
The response body of every write on this page is a re-read of current state, so it is authoritative immediately. If the API and the storefront disagree for a minute, that is expected - it is not a failed write, and retrying will not help.
4. Attribution lives in this API’s audit trail, not in the admin UI
Section titled “4. Attribution lives in this API’s audit trail, not in the admin UI”Every write here goes upstream as a shared service account. So Collaterate’s own “last updated by” column shows that service account, not your partner name - and a site owner reading the admin UI cannot tell which partner made a change, or distinguish a partner’s change from a staff member’s.
Per-partner attribution exists, but it lives in this API’s audit trail, which records the partner, the endpoint, the fields attempted, and the outcome for every mutating request. Ask us if you need a change traced. Do not expect the admin UI to answer that question.
Omitted, null, and the boolean rule
Section titled “Omitted, null, and the boolean rule”Every write body on this page is a partial update:
- Omit a field to leave it unchanged.
- Send
nullto clear a text field. - Never send
nullfor a boolean or an enum. It is refused with400 null_not_supported. Booleans and enums are set, not cleared -visibility,requiresApproval,overrideDisplay,minQuantity,maxQuantityandweightall follow this rule. Send a value, or omit the field.
An empty body ({}) is refused too: there is nothing to change.
Any field that is not writable is refused by name, with a reason. You will not get a generic
“unknown field” for something you can plainly see in a GET - if a field is read-only, the error
says so and says where it is set instead.
Product-level fields
Section titled “Product-level fields”PATCH /v1/products/SLO_44431Content-Type: application/json
{ "excerpt": "16pt uncoated, full color both sides", "visibility": "HIDDEN" }visibility takes HIDDEN, VISIBLE_TO_ALL or VISIBLE_TO_SOME.
minQuantity and maxQuantity are readable everywhere but writable only on STOCK. This is
the one field pair where read and write disagree, so it is worth a sentence rather than a rule.
Most POD products do have bounds - the large majority carry both - and this API publishes them.
But on POD the bounds belong to the print-job classification that prices the product, not to
the product itself, and one classification can govern thousands of products across many sites.
There is no per-product value to change. On DIGITAL there are no bounds at all.
So a write to either field on a POD or DIGITAL product is refused with
422 unsupported_field_for_service_type. It is refused rather than accepted because the upstream
field a write would land in is not the one this API reads back on those products - you would get a
200 and see nothing change, forever. Reading the bounds works normally on every service type.
defaultQuantity is not writable. It is null for every STOCK product (no such setting exists
upstream) and derived from pricing configuration on POD.
Not writable, and refused by name: excludeFromSearch, imagePath, attributes,
fulfillmentCenterName, urlPattern (it is derived from name upstream and changes as a side
effect of renaming), anything pricing-related, and inventory in any form.
Share-backed (SLS_) products and overrideDisplay
Section titled “Share-backed (SLS_) products and overrideDisplay”A share-backed product inherits its display fields from a master product. overrideDisplay is the
single switch that decides whether your site’s own values or the master’s are shown.
overrideDisplay: true- your share’s owndescription,metaDescription,metaKeywordsand thumbnail stand.overrideDisplay: false- all of them re-inherit from the master, together. It is one share-wide switch, not per-field.
overrideDisplay is null on site-owned (SLO_) products, which have nothing to inherit from, and
sending it there is refused.
Variants
Section titled “Variants”PATCH /v1/products/SLO_44431/variants/46001Content-Type: application/json
{ "attribute1Choice": "White", "location": "A-12-3" }Five fields: attribute1Choice, attribute2Choice, attribute3Choice, location, weight.
sequence and enabled are not writable here - the platform changes those through separate
operations this API does not expose.
Segments
Section titled “Segments”PUT /v1/products/SLO_44431/segmentsContent-Type: application/json
{ "segmentIds": [991, 992] }{"segmentIds": []} is legal and means “assigned to no segments”. On a VISIBLE_TO_SOME product
that makes it visible to nobody - a real operation, so it is accepted rather than refused.
Every id is checked against your own site before anything is written. If any id is not a segment
on that product’s site, the whole request is refused with 400 invalid_segment_ids naming the
offending ids, and nothing changes. Unknown ids are never silently skipped - on a replace, skipping
one would delete real assignments while reporting success. Use GET /v1/segments for the ids you can
assign - or, one site at a time, GET /v1/sites/{siteId}/segments. That is the same request as
GET /v1/segments?siteId=, not a separate lookup.
Duplicates are collapsed and order does not matter.
Images
Section titled “Images”Images are metadata-only through this API.
GET /v1/products/SLO_44431/imagesPATCH /v1/products/SLO_44431/images/12345Content-Type: application/json
{ "imageAlt": "Welcome folder, front cover" }Three writable fields: imageAlt, sequence, primary.
primary may only be set to true, which makes that image primary and demotes whichever image held
it before. There is no way to set false: that would leave the product with no primary image, and one
gets reassigned upstream anyway, so the request would not do what it said. To change which image is
primary, patch the one you want.
imagePath, attribution, imageWidth and imageHeight are returned on read but are not writable.
The dimensions are read from the file itself.
The PATCH response is the whole image list, not just the image you changed - because setting
primary changes another image too, and changing sequence reorders the rest.
Errors
Section titled “Errors”Every failure is an RFC 9457 problem document; branch on code. See
Errors for the full table. The ones specific to writing:
code |
Status | Meaning |
|---|---|---|
invalid_product_patch |
400 | A product field is not writable, missing, or the wrong type |
invalid_variant_patch |
400 | A variant field is not writable, or the wrong type |
invalid_image_patch |
400 | An image field is not writable, or the wrong type |
invalid_segments_body |
400 | segmentIds missing, not an array, or an unknown key was sent |
invalid_segment_ids |
400 | An id is not a segment on that product’s site. Nothing changed. |
null_not_supported |
400 | null sent for a boolean or enum |
sls_null_backfills_master |
400 | null sent for a share’s inherited content field |
monolith_rejected |
400 | The platform refused on its own validation grounds |
unsupported_field_for_service_type |
422 | STOCK-only field on a non-STOCK product |
variant_not_site_addressable |
422 | Share-backed or print-on-demand variant |
image_not_found |
404 | That image is not on that product |
product_write_partially_applied |
409 | Some sections applied, a later one failed |
audit_unavailable |
503 | We could not record the attempt, so we did not make it. Retry. |
audit_unavailable is worth understanding: we record every mutating request before sending it
upstream, and if that record cannot be written we refuse the request rather than making an
untraceable change. Nothing was written. Retry.