Magento’s own addProductsToCart has nowhere to put a start and end date, so a headless storefront cannot use it to take a rental booking — the line goes in with no dates, prices at nothing and fails at checkout. addRentalProductsToCart is the rental equivalent. Together with rental_information on the product, it is everything a React, Next.js, Vue or PWA Studio storefront needs to sell a hire.
Neither needs a back-office token. Send a customer token to work on that customer’s cart; without one you are working on a guest cart, exactly as with the standard mutations.
The flow #
- Read
rental_informationon the product to find out whether it is a rental at all, whether the picker needs times, and what the price tiers are. - Paint the picker with
rentalProductCalendar. - When the customer picks dates, quote them with
rentalProductPrice. - Add to the cart with
addRentalProductsToCart. - Check out with Magento’s standard mutations — nothing rental-specific from here on.
Step 1 — what kind of product is this? #
rental_information is available on every product in the standard products query, and is null for anything that is not a rental — so a mixed catalogue needs no special handling. Ask for it, and if it is null render your ordinary product page.
query ProductPage {
products(filter: { sku: { eq: "CANON-R6" } }) {
items {
sku
name
rental_information {
is_rental
uses_times
single_day_mode
minimum_period { period_type quantity }
maximum_period { period_type quantity }
turnover_before_minutes
turnover_after_minutes
total_qty
first_available_date
uses_serial_numbers
price_list { period_type period_quantity price additional_price qty_start qty_end }
}
}
}
}
uses_timestells you whether to show a time alongside the date. If it is false, send dates asY-m-d 00:00:00and let the server handle the rest.single_day_modemeans one date, not a range — collapse your picker to a single day.minimum_period/maximum_periodarenullwhen there is no limit. Enforce them in the picker so the customer does not discover the rule only when the add fails.turnover_before_minutes/turnover_after_minutesare the buffers blocked either side of each booking. You do not need to apply them yourself — availability already accounts for them — but they are useful for explaining to a customer why a day next to a booking is unavailable.- On a listing page ask only for the fields you need. The resolver skips any work for fields you did not select, and
first_available_datein particular walks the availability engine — leave it out of a 24-product grid unless you are showing it.
Step 2 — painting the date picker #
One query gives you a whole month, so the picker can be drawn without a request per day. Ask for the window you are about to render, not the whole season.
query PaintPicker {
rentalProductCalendar(input: {
sku: "CANON-R6"
start_date: "2027-03-01"
end_date: "2027-03-31"
qty: 1
}) {
total_qty
first_available_date
fully_booked_days
disabled_days
days { date available_qty is_available is_disabled }
}
}
- Grey out a day when
is_availableis false. It already combines “enough units free” with “the product can go out that day”, so you do not have to cross-reference the other two lists yourself. fully_booked_daysanddisabled_daysare there when you want to explain why — “fully booked” reads very differently to a customer than “we are closed”.- Pass the quantity the customer has actually selected. A day with two units free is bookable at
qty: 1and not atqty: 3, so a picker drawn atqty: 1and then used to book three will offer dates that fail on add. - Re-fetch when they change the quantity, and when they page to another month. The window is capped at 400 days.
Fuller detail, including checking dates while editing an existing booking, is in Availability & Calendar Queries.
Step 3 — quoting the dates they picked #
Once both ends are chosen, price them. Do this on every change of date or quantity — a rental price is not a unit price multiplied by days, and guessing it client-side will disagree with the cart.
query Quote {
rentalProductPrice(input: {
sku: "CANON-R6"
start_date: "2027-03-10 09:00:00"
end_date: "2027-03-12 17:00:00"
qty: 2
}) {
price
row_total
currency
has_special_pricing
duration { days }
breakdown { label price is_special }
}
}
- Show
row_totalas the line total andpriceas the per-unit figure. Both exclude tax. breakdownis what lets you show the customer why it costs what it does — “3 days at the weekly rate” or a named surge period — instead of a bare number.- A quote holds nothing. It is a statement about the tiers, not a reservation, and the last unit can go between quoting and adding. The dates are only really yours once the next step succeeds.
Tiers, date-based specials and customer-group pricing are covered in Pricing Queries.
Step 4 — adding to the cart #
mutation Book {
addRentalProductsToCart(input: {
cart_id: "8xLm2pQ7rT9vB3nK5wY1cF6dH0jS4aZe"
cart_items: [
{
sku: "CANON-R6"
quantity: 2
start_date: "2027-03-10 09:00:00"
end_date: "2027-03-12 17:00:00"
},
{
sku: "TRIPOD-PRO"
quantity: 1
start_date: "2027-03-10 09:00:00"
end_date: "2027-03-12 17:00:00"
}
]
}) {
cart {
id
total_quantity
items { uid quantity product { sku name } prices { row_total { value currency } } }
prices { grand_total { value currency } }
}
user_errors { message code sku position }
}
}
cart_idis the masked cart id, the same oneaddProductsToCarttakes. Create one withcreateEmptyCart, or usecustomerCartfor a logged-in customer.- Each line carries its own dates, so a basket can hold hires over different periods.
selected_optionsandentered_optionswork exactly as onaddProductsToCartfor any other custom options the product has.- Availability is re-checked here, before the line goes in. This is the point at which a booking becomes real — a quote from
rentalProductPriceholds nothing, and the last unit can be taken between the quote and the add.
Lines that could not be added #
A line that fails does not abort the batch. It appears in user_errors and the other lines still go in — so a customer adding three things does not lose all three because one sold out. Always read user_errors; an empty array is the only confirmation that everything was added.
{
"data": {
"addRentalProductsToCart": {
"cart": { "total_quantity": 1, "items": [ { "product": { "sku": "TRIPOD-PRO" } } ] },
"user_errors": [
{
"message": "Only 1 of this item is available for the dates selected.",
"code": "INSUFFICIENT_STOCK",
"sku": "CANON-R6",
"position": 0
}
]
}
}
}
position is the 0-based index of the line in your cart_items input and sku is its SKU, so you can put the error next to the right row in the basket rather than showing a general failure.
code | What to do |
|---|---|
INSUFFICIENT_STOCK | The dates are booked out or there are not enough units. Offer a lower quantity or other dates — this is the one case where retrying smaller can work. |
NOT_SALABLE | A rule refused it: too short, too long, a disabled day, a start date that is not selectable. Retrying with a lower quantity will not help; the customer has to change the dates. |
PRODUCT_NOT_FOUND | No such SKU, or it is not a rental product. |
INVALID_PARAMETER_VALUE | A bad date, an end before a start, a quantity below one. |
A bad cart_id, or one belonging to somebody else, is a hard error rather than a user_errors entry — there is no cart to return.
Step 5 — checking out #
Nothing further is rental-specific. setShippingAddressesOnCart, setBillingAddressOnCart, setShippingMethodsOnCart, setPaymentMethodOnCart and placeOrder all work as normal, and the rental dates travel with the line through to the order. Once the order is placed the bookings exist and the units are off the calendar; you can see them with rentalReservations or rentalOrders from your back office.
Hyvä and Luma stores #
None of this is needed on a normal Hyvä or Luma storefront — those post to the standard add-to-cart controller and the dates are picked up from the form. This mutation exists for storefronts that are not rendering Magento’s own templates at all.
