The rental extension adds a GraphQL API to Magento’s own /graphql endpoint. It covers the parts of a rental business that the standard Magento schema has no concept of: what a product costs for a particular set of dates, whether it is free on those dates, the bookings themselves, the serial-numbered units that go out and come back, the orders due to go out this morning, and the maintenance tickets that take a unit off the fleet.
It is available from version 1.2.198 of the rental extension (and 1.2.48 of the Maintenance add-on for the ticket fields). There is nothing to install or enable beyond updating the extension — the schema merges into the endpoint you already have.
The endpoint #
Everything is served from the one Magento endpoint, https://your-store.com/graphql, over POST with Content-Type: application/json. If GraphQL already works on your store, the rental fields are there too.
curl -X POST https://your-store.com/graphql
-H 'Content-Type: application/json'
-d '{"query":"{ rentalProductAvailability(input: {sku: "CANON-R6", start_date: "2026-10-03 00:00:00", end_date: "2026-10-05 23:59:00", qty: 1}) { is_available available_qty } }"}'
Two kinds of field #
The schema is split by who is allowed to call it, and it matters which half you are in because it decides what token you need.
| Storefront fields | Back-office fields |
|---|---|
rentalProductAvailabilityrentalProductCalendarrentalProductPricerentalProductPriceListaddRentalProductsToCartrental_information on a product | rentalSerialNumbers and its mutationsrentalReservations and its mutationsrentalOrderssendRentalSerials, returnRentalSerialsrentalMaintenanceTickets and its mutations |
Open to anyone who can reach the storefront, the same as products or cart. No token needed. | Require a bearer token for an admin user or an integration whose role holds the matching ACL resource. |
This is worth being deliberate about. A headless storefront only ever needs the left-hand column, and it should not be shipped with a back-office token embedded in it. The right-hand column is for your own back-office tooling, a warehouse scanner app, or an integration with another system.
Authentication #
An admin token #
Quickest for testing. Exchange an admin username and password for a token, then send it as a bearer token. The token is valid for the lifetime configured under Stores > Configuration > Services > OAuth > Access Token Expiration — four hours by default.
TOKEN=$(curl -s -X POST https://your-store.com/rest/V1/integration/admin/token
-H 'Content-Type: application/json'
-d '{"username":"your-admin","password":"your-password"}' | tr -d '"')
curl -X POST https://your-store.com/graphql
-H 'Content-Type: application/json'
-H "Authorization: Bearer $TOKEN"
-d '{"query":"{ rentalReservations(pageSize: 5) { total_count items { id product_id start_date end_date state } } }"}'
If two-factor authentication is enabled on your admin (it is by default in Magento 2.4), the token endpoint will refuse a plain username and password. For anything other than a quick test, use an integration instead.
An integration token (recommended) #
An integration is a non-human account with its own role, which is what you want for any system that is going to keep calling the API. Go to System > Extensions > Integrations > Add New Integration, give it a name, and on the API tab grant only the resources it actually needs (see the table below). Save, then Activate it — the access token is shown once at that point.
curl -X POST https://your-store.com/graphql
-H 'Content-Type: application/json'
-H "Authorization: Bearer your-integration-access-token"
-d '{"query":"{ rentalSerialNumbers(pageSize: 5) { total_count } }"}'
A customer token #
The storefront fields do not need a token at all, but a logged-in customer’s token is still useful for two of them: addRentalProductsToCart adds to that customer’s cart rather than a guest cart, and rentalProductPrice uses their customer group for group-specific pricing. Get one with Magento’s standard generateCustomerToken mutation and send it the same way.
Which permission each field needs #
The back-office fields are gated on the same ACL resources as the corresponding admin screens, so a role that can already do the job in the admin can do it through the API.
| Field | ACL resource | Where that is in the admin role tree |
|---|---|---|
All rentalSerialNumber* fields | SalesIgniter_Rental::serial | Rental > Reports > serial |
All rentalReservation* fields | SalesIgniter_Rental::manualedit | Rental > General > Manually Reserve Inventory |
rentalOrders | SalesIgniter_Rental::rentalcal | Rental > Rental Calendar by start/end or order |
sendRentalSerials | SalesIgniter_Rental::send | Rental > Send and Return > Ship Items |
returnRentalSerials | SalesIgniter_Rental::return | Rental > Send and Return > Return Items |
All rentalMaintenanceTicket* fields | SalesIgniter_Maintenance::tickets | Rental > Maintenance > Tickets |
Two storefront fields have a back-office capability hidden inside them. rentalProductAvailability and rentalProductCalendar both accept exclude_reservation_ids and exclude_order_id, which answer “what would availability look like if these bookings did not exist” — that is how an order-edit screen stops a booking from blocking itself. Supplying either of them requires SalesIgniter_Rental::manualedit; without it the query is refused rather than quietly ignoring them.
Dates #
- Every date in and out of this API is
Y-m-d H:i:s—2026-10-03 09:00:00. Fields that are naturally whole days (the calendar window, a serial’s acquisition date) also acceptY-m-d, which is read as midnight. - They are in the store’s configured timezone, not UTC, and not the caller’s. This matches what the rental tables hold and what the admin screens show.
- Anything else is rejected with an error naming the field. Nothing is guessed — a format like
03/04/2026is ambiguous between March and April, and an API that guessed would be wrong about half the time in a way nobody would notice until a booking came back on the wrong day. - A field that has no date (a booking with no end date, a serial never marked as acquired) returns
null, not a zero date.
Rental dates versus turnover dates #
Every booking carries two date ranges and it is important not to confuse them.
start_dateandend_dateare the customer’s dates — what they picked, what the order confirmation says, what a picking list should print.start_date_with_turnoverandend_date_with_turnoveradd the product’s turnover buffers either side. These are the dates that actually block the calendar. If a camera needs four hours to be cleaned and checked between hires, its turnover-inclusive range is four hours longer than the customer’s, and that is the range availability is computed against.
When you create a booking you supply the customer’s dates and the turnover ones are worked out for you from the product’s configuration. Pass not_use_turnover: true to skip that, which is occasionally what you want for a manual hold that is not a real hire.
Errors #
Errors come back in the standard GraphQL shape, in an errors array alongside whatever data could still be resolved. Each one carries a category you can branch on.
| Category | What it means |
|---|---|
graphql-authorization | Your token is missing the ACL resource for this field, or you sent no token to a back-office field. |
graphql-input | The arguments are wrong — a bad date, an end before a start, a quantity below one, a SKU that is not a rental product, a calendar window over 400 days, a page past the end of the results. |
graphql-no-such-entity | The SKU, booking, serial or ticket does not exist. |
graphql-already-exists | Creating something that is already there — most often a serial number that product already has. |
A booking that cannot be made because the dates are not free is not an exception in the availability queries — rentalProductAvailability returns is_available: false with a structured error { code message }, because “no” is a perfectly good answer to a question. It is an error when you try to write a booking onto unavailable dates, unless you pass force: true.
Paging, filtering and sorting #
Every list field works the same way and follows Magento’s own conventions, so a client written against products will feel familiar.
{
rentalSerialNumbers(
filter: { status: { eq: "available" }, serial_number: { match: "CANON" } }
sort: { serial_number: ASC }
pageSize: 50
currentPage: 1
) {
total_count
items { id serial_number status }
page_info { page_size current_page total_pages }
}
}
pageSizedefaults to 20 and is capped at 200.currentPageis 1-based. Asking for a page past the last one is an error rather than an empty list, so a paging loop with an off-by-one fails loudly instead of spinning.- Filter fields take
{ eq: }or{ in: [] }, text fields take{ match: }for a substring, and date fields take{ from:, to: }. Several filters combine with AND. sorttakes exactly one field. Supplying two is an error rather than one of them being silently ignored.
Where to go next #
- Building a headless storefront? Start with Booking from a Headless Storefront, then Availability & Calendar and Pricing.
- Integrating a warehouse or scanner app? Sending & Returning Rentals and Serial Numbers.
- Syncing with another system? Reservations and Retrieving Orders by Rental Date.
