Skip to content

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.

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.

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.

Every write body on this page is a partial update:

  • Omit a field to leave it unchanged.
  • Send null to clear a text field.
  • Never send null for a boolean or an enum. It is refused with 400 null_not_supported. Booleans and enums are set, not cleared - visibility, requiresApproval, overrideDisplay, minQuantity, maxQuantity and weight all 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.

PATCH /v1/products/SLO_44431
Content-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 own description, metaDescription, metaKeywords and 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.

PATCH /v1/products/SLO_44431/variants/46001
Content-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.

PUT /v1/products/SLO_44431/segments
Content-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 are metadata-only through this API.

GET /v1/products/SLO_44431/images
PATCH /v1/products/SLO_44431/images/12345
Content-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.

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.