Products API guide
This guide describes how to obtain products from the Saleor GraphQL API.
You can either retrieve a single product or a list of products. You may require a list of products in many situations, for example, when you need to display the catalog in your storefront or to provide a third-party service with a list of products available in your store.
Multiple channels and products
Customers can only fetch products from available channels. Because listing channels is only available for staff users, you will need to provide channel slugs to your storefront. Besides availability, you can define more parameters on a channel basis using ProductChannelListing:
publicationDate
isPublished
visibleInListings
availableForPurchase
isAvailableForPurchase
For variants, there's ProductVariantChannelListing:
price
costPrice
margin
Learn more about using multiple channels.
Retrieving a list of products
To fetch a product list, you need to run the products
query. The products
field is a paginated collection, see Pagination for more information.
Let's take a look at an example query to fetch a list of products:
{
products(first: 2, channel: "default-channel") {
edges {
node {
id
name
}
}
}
}
Response:
{
"data": {
"products": {
"edges": [
{
"node": {
"id": "UHJvZHVjdDo3Mg==",
"name": "Apple Juice"
}
},
{
"node": {
"id": "UHJvZHVjdDo3NA==",
"name": "Banana Juice"
}
}
]
}
}
}
In this example, for each product, we want to return the following fields:
id
: the unique product ID, most operations will require one.name
: the name of the product.
The channel
argument can be skipped only if a user has is_staff
flag.
Filtering
The products
query gives the ability to filter the results. Use the optional filter
argument. Some of the filters that are available here are:
search: String
: search by name or part of the description.price: ...
: filter by base price:price: {lte: Float}
: price lower than or equal to a given value.price: {gte: Float}
: price greater than or equal to a given value.
minimalPrice: ...
: filter by minimal variant price:minimalPrice: {lte: Float}
: price lower than or equal to a given value.minimalPrice: {gte: Float}
: price greater than or equal to a given value.
stockAvailability: ...
: filter by available stock:stockAvailability: IN_STOCK
: only available products.stockAvailability: OUT_OF_STOCK
: only out-of-stock products.
Here is an example query that looks for the first three products containing the term "juice" in the title or description:
{
products(first: 3, channel: "default-channel", filter: { search: "juice" }) {
edges {
node {
name
}
}
}
}
Response:
{
"data": {
"products": {
"edges": [
{
"node": {
"name": "Apple Juice"
}
},
{
"node": {
"name": "Banana Juice"
}
},
{
"node": {
"name": "Bean Juice"
}
}
]
}
}
}
Sorting
In products
you can also sort the results using two sortBy
arguments:
-
field
: the field to use for sorting the results from several predefined choices:NAME
: sort products by name.PRICE
: sort products by base price.MINIMAL_PRICE
: sort products by minimal variant price.DATE
: sort products by last update.TYPE
: sort products by product type.PUBLISHED
: sort products by publication status.PUBLICATION_DATE
: sort products by publication date.PUBLISHED_AT
: sort products by publication date.LAST_MODIFIED_AT
: sort products by update date.RATING
: sort products by rating.CREATED_AT
: sort products by creation date.
-
direction
: The direction for sorting items:ASC
: sort ascending.DESC
: sort descending.
This example shows how to sort the products list by the minimal variant price, lowest to highest:
{
products(
first: 2
channel: "default-channel"
sortBy: { field: MINIMAL_PRICE, direction: ASC }
) {
edges {
node {
name
pricing {
priceRange {
start {
gross {
amount
currency
}
}
stop {
gross {
amount
currency
}
}
}
}
}
}
}
}
Response:
{
"data": {
"products": {
"edges": [
{
"node": {
"name": "Blue Hoodie",
"pricing": {
"priceRange": {
"start": {
"gross": {
"amount": 1.6,
"currency": "USD"
}
},
"stop": {
"gross": {
"amount": 2.6,
"currency": "USD"
}
}
}
}
}
},
{
"node": {
"name": "Banana Juice",
"pricing": {
"priceRange": {
"start": {
"gross": {
"amount": 1.99,
"currency": "USD"
}
},
"stop": {
"gross": {
"amount": 2.99,
"currency": "USD"
}
}
}
}
}
}
]
}
}
}
Get a product by ID
The following query retrieves the product with the associated ID. It returns the product fields specified in the query.
Query arguments | |
---|---|
id: ID! | The ID of the product to return |
channel: String | Slug of a channel for which the data should be returned. |
{
product(id: "UHJvZHVjdDoxOTg=", channel: "default-channel") {
id
name
}
}
{
"data": {
"product": {
"id": "UHJvZHVjdDoxOTg=",
"name": "ABBA Again"
}
},
"extensions": {
"cost": {
"requestedQueryCost": 1,
"maximumAvailable": 50000
}
}
}
Retrieving product variants
To obtain product variants, query the variants
field on the Product
type:
{
products(first: 1, channel: "default-channel") {
edges {
node {
id
name
variants {
id
name
}
}
}
}
}
Response:
{
"data": {
"products": {
"edges": [
{
"node": {
"id": "UHJvZHVjdDoxMzc=",
"name": "Blue Polygon Shirt",
"variants": [
{
"id": "UHJvZHVjdFZhcmlhbnQ6MzYx",
"name": "M"
},
{
"id": "UHJvZHVjdFZhcmlhbnQ6MzYy",
"name": "L"
},
{
"id": "UHJvZHVjdFZhcmlhbnQ6MzYz",
"name": "XL"
}
]
}
}
]
}
}
}
Like products, here we're asking for two fields from each variant:
id
: the unique variant ID.name
: the name of the variant.
Pricing
Use the pricing
field of products and variants to obtain pricing information.
Here are the available fields for product pricing:
type ProductPricingInfo {
priceRange: TaxedMoneyRange
priceRangeUndiscounted: TaxedMoneyRange
discount: TaxedMoney
onSale: Boolean
}
And similar fields for product variants:
type VariantPricingInfo {
price: TaxedMoney
priceUndiscounted: TaxedMoney
discount: TaxedMoney
onSale: Boolean
}
The main difference is that products don't have a price point. Instead, they offer a price range that includes all their variant prices.
Here are the money types returned by the above:
type TaxedMoneyRange {
# lowest price
start: TaxedMoney
# highest price
stop: TaxedMoney
}
type TaxedMoney {
# before tax
net: Money!
# after tax
gross: Money!
# gross - net
tax: Money!
# repeated for convenience
currency: String!
}
type Money {
amount: Float!
currency: String!
}
See Prices for more information about money types.
Fetching prices with tax rates specific to a country
You can fetch prices including taxes specific to a country. To do that, pass the address
parameter in the pricing
field, including the country code for which the tax should be calculated. The address
parameter is available in the Product.pricing
and ProductVariant.pricing
fields.
In the example below, we fetch the price of a product variant for Poland, where the standard VAT rate is 23%:
{
productVariant(id: "UHJvZHVjdFZhcmlhbnQ6MjAz", channel: "default-channel") {
pricing(address: { country: PL }) {
price {
net {
amount
}
gross {
amount
}
tax {
amount
}
}
}
}
}
Result:
{
"data": {
"productVariant": {
"pricing": {
"price": {
"net": {
"amount": 5
},
"gross": {
"amount": 6.15
},
"tax": {
"amount": 1.15
}
}
}
}
}
}
The tax equals 1.15, which is 23% of the net price. The gross price is the sum of the net price and the tax.
The address
parameter is optional. If it's not provided, Saleor will use the default country code configured in the backend settings (see DEFAULT_COUNTRY).
Learn more about Taxes
Stock availability for customers
Stock availability of products is defined for each variant and warehouse. There are two fields that allow you to fetch the stock information in the public API.
To fetch the stock availability, use the Product.isAvailable
field:
{
product(id: "UHJvZHVjdDo3Mg==", channel: "default-channel") {
name
isAvailable(address: { country: US })
}
}
Result:
{
"data": {
"product": {
"name": "Apple Juice",
"isAvailable": true
}
}
}
The address
parameter checks the quantity in a warehouse connected with a shipping zone that includes the given address. If not provided, Saleor will use a warehouse with the highest available quantity.
The isAvailable
field is True
when:
- There is at least one variant for which the stock quantity minus the allocated quantity is above zero (in a warehouse matching the given address).
- The product is published in the given channel and available for purchase.
To fetch the available quantity, use the ProductVariant.quantityAvailable
field:
{
productVariant(id: "UHJvZHVjdFZhcmlhbnQ6MjAz", channel: "default-channel") {
quantityAvailable(address: { country: US })
}
}
Response:
{
"data": {
"productVariant": {
"quantityAvailable": 30
}
}
}
A warehouse is chosen based on the argument address
. If the argument is omitted, API will return the highest available quantity.
To avoid leaking precise stock information, the quantityAvailable
field will not be greater than the maximum orderable threshold configured in the MAX_CHECKOUT_LINE_QUANTITY
server setting.
Stock quantities
For creating integrations with an external stock management system, you will need to use stock values not restricted by MAX_CHECKOUT_LINE_QUANTITY
.
stocks
field in the ProductVariant
object require MANAGE_PRODUCTS
permission.
To clarify the difference between quantityAvailable
and stocks, we will query both for the product variant:
Request:
query {
productVariant(id: "UHJvZHVjdFZhcmlhbnQ6MjAz", channel: "default-channel") {
quantityAvailable
stocks {
warehouse {
slug
}
quantity
quantityAllocated
}
}
}
Response:
{
"data": {
"productVariant": {
"quantityAvailable": 50,
"stocks": [
{
"warehouse": {
"slug": "europe"
},
"quantity": 111,
"quantityAllocated": 0
},
{
"warehouse": {
"slug": "americas"
},
"quantity": 113,
"quantityAllocated": 0
}
]
}
}
}
- The
quantityAvailable
is a sum of stock quantities from all available stocks. In this case, the sum is higher than theMAX_CHECKOUT_LINE_QUANTITY
, so the maximum value of 50 products is returned. stocks
contain a full list of stock quantities in each warehouse.
When the product has the track inventory turned off, the quantityAvailable
will always
be equal to the MAX_CHECKOUT_LINE_QUANTITY
value.
Getting the available quantity for the particular address
The quantity available can be fetched for the given address. The returned quantity is the sum of the quantity from all stocks that are available in the shipping zone that supports the provided address. When the address is not provided, the highest quantity from available shipping zones is returned.
To get the quantityAvailable
for a particular address use the address
parameter:
Request:
query {
productVariant(id: "UHJvZHVjdFZhcmlhbnQ6MjAz", channel: "default-channel") {
quantityAvailable(
address: {"country": "US"}
)
stocks {
warehouse {
slug
}
quantity
quantityAllocated
}
}
}
Response:
{
"data": {
"productVariant": {
"quantityAvailable": 20,
"stocks": [
{
"warehouse": {
"slug": "europe"
},
"quantity": 30,
"quantityAllocated": 0
},
{
"warehouse": {
"slug": "americas"
},
"quantity": 20,
"quantityAllocated": 0
}
]
}
}
}
In the response, we get the available quantity of 20 as the americas
is the only
warehouse available in the US
.
In the case when the address is not provided, the quantity for the shipping zone with the highest available quantity is returned.
Handling collection point warehouses in the quantity calculations
The collection point warehouses are treated as a single-warehouse shipping zone.
- In the case of a local collection point warehouse, the calculated quantity will equal the quantity of this local warehouse.
- In the case of a global collection point warehouse the quantity will equal the sum of available quantity from all stocks that are available in the provided country code and channel. When the country is not provided the quantity will be the sum of quantities from all stocks that are available in the given channel, regardless of the shipping zone.
Media
Product media are images or videos associated with products and used to render product galleries. The Product
type contains two fields that refer to its media:
-
thumbnail: Image
: the product's main image. If the first media file for a product is a video, a video thumbnail is returned. An optionalsize
parameter specifies the desired size in pixels if provided. The following fields are available:url: String!
: the image's URL.alt: String
: the alternative text to include when displaying the image.
-
media: [ProductMedia!]
: gives you access to the entire list of associated product media with the following fields available:type: ProductMediaType!
: the type of media file -IMAGE
orVIDEO
url: String!
: the media URL. For images, an optionalsize
parameter specifies the desired size in pixels if provided.alt: String
: the alternative text to include when displaying the media file.oembedData: JSONString!
: embedded representation of a video URL, returned in the oEmbed format.
Here's a query that asks for both the thumbnail and all media of the first product, optimized for display at 100×100px:
{
products(first: 1, channel: "default-channel") {
edges {
node {
id
name
thumbnail(size: 100) {
url
}
media {
type
url(size: 100)
}
}
}
}
}
Response:
{
"data": {
"products": {
"edges": [
{
"node": {
"id": "UHJvZHVjdDo3Mg==",
"name": "Apple Juice",
"thumbnail": {
"url": "https://example.saleor.io/media/__sized__/products/saleordemoproduct_fd_juice_06-thumbnail-120x120.png"
},
"media": [
{
"type": "IMAGE",
"url": "https://example.saleor.io/media/__sized__/products/saleordemoproduct_fd_juice_06-thumbnail-120x120.png"
},
{
"type": "VIDEO",
"url": "https://youtu.be/yPYZpwSpKmA"
}
]
}
}
]
}
}
}
Examples
Here's a more complex GraphQL query that combines all of the above information:
{
products(
first: 2
channel: "default-channel"
sortBy: { field: NAME, direction: ASC }
) {
edges {
node {
id
name
pricing {
priceRange {
start {
gross {
amount
currency
}
}
}
discount {
gross {
amount
currency
}
}
priceRangeUndiscounted {
start {
gross {
amount
currency
}
}
}
}
thumbnail {
url
}
}
}
}
}