Catalog Product — Detail (GraphQL)
GraphQL item query that returns a single catalog product by its IRI. Every nested block is a field-selectable Relay connection — sub-select exactly the fields you need with edges { node { … } }.
Operation
| Operation | Type |
|---|---|
adminCatalogProduct | Query (item) |
For product types, the two-step create flow, and the per-product sub-resources, see the Products overview.
Arguments
| Argument | Type | Required | Description |
|---|---|---|---|
id | ID! | Yes | API Platform IRI of the product (e.g. "/api/admin/catalog/products/42") |
Response Shape
Top-level scalars carry the same values as the listing, so the traps are the same:
- Booleans arrive as
"1"or the empty string"", nevertrue/false.status,visibleIndividually,featured,new, andinStockare all typed String, sostatus === "0"never matches andfeatured === falsenever matches. Compare against"1", or coerce. - Numbers arrive as strings.
price,quantity,imagesCount,weight, andcategoryIdare String; only_idandattributeFamilyIdare Int. taxCategoryIdandmanageStockare frequentlynulleven on the detail query — they resolve only when the product actually carries that attribute value.- Timestamps are ISO 8601 with offset,
2026-01-12T08:15:00+05:30.
Every nested block is a connection you sub-select with { edges { node { … } } }:
| Connection | Node fields | Present for |
|---|---|---|
images | _id, type, path, url, position | all |
videos | _id, type, path, url, position | all |
categories | _id, name, slug | all |
inventories | _id, sourceId, sourceCode, qty | all |
customerGroupPrices | _id, customerGroupId, qty, valueType, value, uniqueId | all |
translations | _id, locale, name, description, shortDescription, urlKey, metaTitle, metaDescription, metaKeywords | all |
channels | _id, code, name | all (the product's assigned channels) |
attributeValues | _id, attributeId, code, adminName, type, isRequired, groupCode, value | all (the stored EAV values) |
superAttributes | _id, code, type, adminName, options { edges { node { _id adminName swatchValue sortOrder } } } | configurable |
variants | _id, sku, name, price, formattedPrice, quantity, inStock, attributeValues { edges { node { code adminName value } } } | configurable |
bundleOptions | _id, label, type, position, isRequired, products { edges { node { _id productId sku name qty isDefault sortOrder } } } | bundle |
linkedProducts | _id, associatedProductId, sku, name, qty, sortOrder | grouped |
downloadableLinks | _id, sortOrder, downloads, price, formattedPrice, type, file, fileUrl, sampleFile, sampleFileUrl, sampleType, translations { edges { node { _id locale title } } } | downloadable |
downloadableSamples | _id, sortOrder, type, file, fileUrl, translations { edges { node { _id locale title } } } | downloadable |
customizableOptions | _id, type, isRequired, sortOrder, maxCharacters, supportedFileExtensions, translations { edges { node { _id locale label } } }, prices { edges { node { _id label price sortOrder } } } | all |
relatedProducts / upSells / crossSells | _id, sku, type, name | all |
Type-specific connections (superAttributes / variants, bundleOptions, linkedProducts, downloadableLinks / downloadableSamples) return empty edges on non-matching types — switch on type to know which to read. Selecting all of them regardless is safe.
Two connection details worth knowing:
- An image node's
typeis the string"images", plural, not"image"— it names the storage folder, not the media kind. Video nodes carry"videos"for the same reason. channelslists only the product's assigned channels, not every channel in the store. The REST detail'schannelsblock instead lists every channel with anassignedflag, so the two are not interchangeable.
Configurable Option Lists
A configurable exposes its option data twice, and the two answer different questions:
superAttributes { edges { node { options { edges { node } } } } }— every option the attribute defines, whether or not a variant uses it. This is the set to render in a picker.variants { edges { node { attributeValues { edges { node { code adminName value } } } } } }— the option value actually chosen for each existing variant.
Fields That Are REST-Only
Three fields exist in the schema but always resolve null over GraphQL — attributes, bookingProduct, and warnings. They are returned only by GET /api/admin/catalog/products/{id}.
attributesis the admin edit-screen field set, including family fields the product has not filled in. The GraphQL equivalent is theattributeValuesconnection, which carries one node per stored value — an unset field simply has no node, so you cannot tell "empty" from "not in this family" without the REST block.bookingProductcarries the booking sub-type, slots, and tickets. There is no GraphQL equivalent; read booking configuration over REST.warningsis populated only on the update response, never on a read.
Errors
An unknown or deleted id returns HTTP 200 with "Product not found." in the GraphQL errors[] array and null in data.adminCatalogProduct.
Working With This Query
- The
idargument is the IRI, not the numeric id. Build it as/api/admin/catalog/products/{_id}from a listing row's_id, or pass a listing edge'sidstraight through. - REST is the flat counterpart.
GET /api/admin/catalog/products/{id}returns the same data with every nested block as an inline array, and addsattributes,bookingProduct, and the fullsuperAttributes[].options.

