Selling rentals on a Hydrogen or custom storefront? A publishable key lets its pages show the rental calendar, price the dates and reserve them, then add the rental to a Shopify cart. Checkout prices and checks the line just as it does on your online store. This page is for the storefront’s developer. Your own servers use the secret-key API instead: see the API overview.
Make a publishable key #
- In Rentals & Bookings, open Settings › API › Headless storefronts and click Create a key.
- Enter a Key name (like “Hydrogen storefront”) and the Allowed websites, one per line:
https://shop.example.com,https://*.example.comfor all its subdomains (handy for preview deployments; it doesn’t includeexample.comitself), orhttp://localhost:3000while developing (plainhttponly works for localhost). No paths. - Give the Storefront API address and the key to your storefront’s developer.
A publishable key starts with sirpk_. It is not a secret: it goes in your storefront’s code, and you can copy it again at any time. It only works from the websites you list, and only for what a shopper can do on your online store: see the calendar, get a price, hold dates. It can’t read your orders or bookings or change anything.
Rotate makes a new key for the same name and websites, and keeps the old one working for 24 hours so you have time to update your storefront; Stop old key ends it early. Revoke stops a key at once. A store can have 10 active publishable keys, each with up to 20 websites.
How a headless booking works #
- The storefront asks for the product’s calendar (
calendar) and draws it: closed days, fully booked days, prices. - The shopper picks dates; the storefront shows the price (
quote). reserveholds the units for 30 minutes and answers the cart line: the variant, the quantity and the line attributes (the dates and a signed token).- The storefront adds that line to the Shopify cart with the Storefront API (
cartLinesAddorcartCreate) and your own Storefront API access token. - The shopper checks out. Shopify prices the rental line from the token and blocks checkout if the line doesn’t match it, exactly as on your online store. The order becomes a booking.
If the shopper changes the dates or removes the line before checkout, release the hold, reserve again and replace the cart line.
Addresses and authentication #
| API | Address |
|---|---|
| REST | https://YOUR-API-ADDRESS/sf/v1/your-store/… |
| GraphQL | POST https://YOUR-API-ADDRESS/graphql/2026-10/storefront/your-store |
your-store is the store’s myshopify name (your-store or your-store.myshopify.com); Settings › API › Headless storefronts shows the full address. Send the key in the X-Rentals-Key header (or Authorization: Bearer sirpk_…). A GET may carry it as ?key=sirpk_… instead, which saves the browser a preflight request.
- Allowed websites. A browser request from a website that isn’t listed for the key answers
403 origin_not_allowed, without CORS headers, so the browser blocks it. Listed websites getAccess-Control-Allow-Originwith their own address. No cookies are used: keepfetch‘scredentialsat the default. - Requests without an Origin (server-side rendering, curl, a native app) are allowed with a valid key.
- Call
reservefrom the shopper’s browser, not from your server. The limits and the hold cap count per shopper IP address; a server that forwards every shopper’s request looks like one shopper and hits them at once. This matters on Hydrogen: call the API from a component, not from a loader, and allow the address in the Content Security Policy (createContentSecurityPolicy({connectSrc: ['https://YOUR-API-ADDRESS']})).
A missing key answers 401 missing_key; an unknown, revoked or expired key, or another store’s key, answers 401 invalid_key.
REST endpoints #
REST errors have the shape {"error": {"code": "…", "message": "…", "field": "…"}}. The message is written for shoppers and can be shown as it is. Product and variant ids are Shopify’s numeric ids (the digits at the end of the gid).
GET /sf/v1/{shop}/calendar #
Everything a date picker needs for one product, in the same format the store’s own rental calendar uses.
| Parameter | Notes |
|---|---|
product, variant | Numeric ids. With only product, its first rent variant. |
from, to | The days to cover (Y-m-d). Default 90 days from from, at most 400. Without from, everything from two days ago on. |
location | The pick-up location, for stores that rent from several places. |
method, returnMethod | Delivery & pickup: pickup, delivery or shipped; in_store, collect or post. |
slim | 1 for a lighter answer without day prices. |
returnLocation | A one-way rental: the location it goes back to. The answer lists the choices (returnLocations) and the one asked for (returnLocation). |
postcode, zone | Delivery: the postcode (or the area id) the days and slots are for. With several locations, the postcode’s area also decides the location it is delivered from. |
subscription | 1: the start-only calendar of a rental billed per period, for quantity units (a day is free to start on when the unit stays free through the hold). The answer adds subscriptions, the plans and invoice billing on offer. |
curl -s "https://YOUR-API-ADDRESS/sf/v1/your-store/calendar?product=88001&variant=44001&from=2026-12-01&to=2026-12-31" \
-H "X-Rentals-Key: sirpk_your_publishable_key"
# 200, Cache-Control: public, max-age=30 (trimmed)
{
"calendarType": "date", "productid": 88001, "variant": 44001, "currency": "USD", "today": "2026-11-30",
"qtyinstock": 4, "inventoryQuantity": 4,
"disabled_dates": [{"from": "2026-12-06", "to": "2026-12-06 00:00:00", "reason": "Closed on Sundays"}],
"fullybooked_dates": [], "fixeddates": [], "dayPrices": [],
"minLength": [], "maxLength": [], "minDate": "2026-11-30 09:00:00", "maxFutureDaysDate": "",
"storeopen": "08:00:00", "storeclose": "18:00:00", "lengthUnit": "day",
"window": {"from": "2026-12-01", "to": "2026-12-31"}, "locations": [], "location": null, "…": "…"
}
For a product booked by the night (Booked by: Nights) the answer has nightly: true, bufferBeforeDays, bufferAfterDays, turnover_dates (the days one stay ends and another begins) and stay (the check-in and check-out times); its fullybooked_dates are sold nights. Other products have nightly: false.
POST /sf/v1/{shop}/quote #
The price of a rental over these dates, with the breakdown. It prices anything that parses; availability and the store’s rules are checked by reserve.
| Body field | Notes |
|---|---|
product, variant | Numeric ids; variant is required. |
quantity | Default 1. |
from, to | Y-m-d; to defaults to from. |
fromtime, totime | HH:mm, for rentals by the hour. |
guests | For products that count guests: a number, or {adults, children, infants, pets}. |
location, method, returnMethod, window, backWindow | As in the calendar; window is a time slot such as 08:00-10:00. |
payment | part to pay part now and the rest later, when the store and the rental allow it. |
locale | The storefront’s language for texts, such as fr. |
returnLocation | A one-way rental: the location it goes back to. The one-way fee is added to the price (the answer says so in oneWay). |
postcode, zone | Delivery: the postcode or area id. |
subscription | {billing, plan, term, interval} to price a rental billed per period (see below). The answer is the price of one period. |
curl -s -X POST "https://YOUR-API-ADDRESS/sf/v1/your-store/quote" -H "X-Rentals-Key: sirpk_your_publishable_key" \
-H "Content-Type: application/json" -d '{"product": 88001, "variant": 44001, "quantity": 1, "from": "2026-12-02", "to": "2026-12-04"}'
# 200
{"total": 900, "unit": 900, "quantity": 1, "start": "2026-12-02", "end": "2026-12-04 00:00:00", "days": 2, "hours": 48,
"lines": [{"label": "1 day × 2", "amount": 900, "kind": "rate"}], "extra_lines": [], "guests": [], "length_unit": "day", "currency": "USD"}
Stays by the night. For a product booked by the night, from is the check-in day and to the check-out day: the stay is [from, to) and needs at least one night. The answer adds nightly: true and nights, and length_unit is night. A same-day or reversed pick is refused with 422 zero_nights (field: dates).
Billed per period. subscription is {billing, plan, term, interval}: billing is external (another app’s selling plan, with plan = its number) or invoice (an invoice each period); term is o (until it comes back, the default) or a number of periods (1 to 120); interval is for invoice billing, such as 1M or 2W. Instead of the object you can send sellingPlan (an external plan). Give a start date only. The answer is the price of one period with a subscription block.
POST /sf/v1/{shop}/reserve #
Holds the dates for 30 minutes and answers the cart line. The body is the same as quote. The stock stays taken until the hold is released, expires, or turns into an order.
curl -s -X POST "https://YOUR-API-ADDRESS/sf/v1/your-store/reserve" -H "X-Rentals-Key: sirpk_your_publishable_key" \
-H "Content-Type: application/json" -d '{"product": 88001, "variant": 44001, "quantity": 1, "from": "2026-12-02", "to": "2026-12-04"}'
# 200
{
"token": "eyJ2Ijo…",
"properties": {"Rental dates": "Dec 2, 2026 – Dec 4, 2026", "_rental": "eyJ2Ijo…"},
"quote": {"quantity": 1, "total": 900, "unit": 900, "days": 2, "…": "…"},
"hold": {"id": "01kc2x7d9q3m5n8p0r4t6v1w3y", "expiresAt": "2026-11-30T15:42:10+00:00"}
}
Add the line to the cart with merchandiseId: "gid://shopify/ProductVariant/44001", quantity = quote.quantity and attributes = the properties as [{key, value}]. The quantity must be quote.quantity exactly (for a product that counts guests it can differ from what the shopper typed), and the attributes must be sent unchanged: they carry the signed token. Never change a rental line’s quantity on its own; reserve again instead. The answer also carries location, returnLocation, handover, payment, deliveryFee and subscription when they apply.
returnLocation ({id, name}) is where a one-way rental goes back. deliveryFee is the “Delivery fee” line to add to the cart with the rental, when the store charges delivery through the app: {variantId, amount, stop, token, properties} (stop is day, slot or area; add one fee line per stop, with properties unchanged, and skip it when the cart already has one for the same stop). subscription is the held rental’s billing: {billing, sellingPlan, planName, interval, everyLabel, term, cycles, periodPrice, currency, holdUntil, termEnd, consent, consentAmounts}. For an external plan, add sellingPlan to the cart line (selling_plan in /cart/add.js, sellingPlanId in the Storefront API); show consent to the shopper.
Refusals: 409 not_available, 409 with a store rule (closed_day, start_too_early, min_length, max_length, too_far_ahead, fixed_length, fixed_date, guests), 422 invalid or bad_dates (with field), 422 location_required (the product is at several locations: send location), 422 method_required (the product offers several methods: send method), 422 postcode_required or 422 outside_area (field: postcode: once the store has delivery areas, a delivery needs a postcode inside one), 409 window_full (the time slot is full), 422 zero_nights (a stay needs a night), 422 return_location_unavailable (the product can’t be returned there), 422 with subscriptions_off, plan_not_available, invoice_not_available, bad_term or bad_billing (field: subscription), 404 not_rentable, 429 too_many_holds, 429 rate_limited, 503 not_ready (online booking isn’t set up on the store yet; Retry-After).
POST /sf/v1/{shop}/release #
Frees a hold: {"hold": "01kc2x7d9q3m5n8p0r4t6v1w3y"} answers {"ok": true}. It is safe to repeat, and an expired hold is fine.
curl -s -X POST "https://YOUR-API-ADDRESS/sf/v1/your-store/release" -H "X-Rentals-Key: sirpk_your_publishable_key" \
-H "Content-Type: application/json" -d '{"hold": "01kc2x7d9q3m5n8p0r4t6v1w3y"}'
# 200
{"ok": true}
GET /sf/v1/{shop}/availability #
For collection pages and search results: is each product a rental, and is it free for a date search? Up to 50 products.
| Parameter | Notes |
|---|---|
products or handles | Comma-separated product ids, or handles (for theme cards that carry no id). |
from, to | Y-m-d; to defaults to from. length (4h, 3d, 2w) instead of to. |
fromtime, totime, quantity, guests, location | Optional. |
sr_where, sr_radius | A place and a radius in kilometres: each rental also gets distanceKm and near. |
curl -s "https://YOUR-API-ADDRESS/sf/v1/your-store/availability?products=88001,88002&from=2026-12-02&to=2026-12-04" \
-H "X-Rentals-Key: sirpk_your_publishable_key"
# 200, Cache-Control: public, max-age=30
{"88001": {"rental": true, "available": true, "units": 3, "handle": "party-tent-6x12"}, "88002": {"rental": false}}
422 bad_request without products or dates.
GET /sf/v1/{shop}/handover/windows #
The time slots of a hand-over day, for stores with delivery & pickup. Parameters: product, variant, method, date (required, Y-m-d), location, back (1 for the return day), returnMethod, locale, and for delivery postcode or zone (the area’s own slots; 422 postcode_required / outside_area as for reserve). Answers {mode, windows: [{id, from, to, label, capacity, left, full}]}; capacity is the stops a slot takes, left what is still free and full true when none is.
GET /sf/v1/{shop}/delivery/check #
Can the store deliver to a postcode? Call it as the shopper types the postcode, before the calendar. Without delivery areas every postcode is fine; once the store has areas, a delivery needs a postcode inside one, and the matching area decides the days, the time slots and (with several locations) the location it is delivered from.
| Parameter | Notes |
|---|---|
postcode | Required: the shopper’s postcode (sr_pc also works). Compared in upper case without spaces; a US ZIP+4 by its first five digits. |
product, variant | Optional: with a variant, the answer also says when a delivered rental of it can first start (earliestStart). |
location | Optional: only areas of this location match. |
curl -s "https://YOUR-API-ADDRESS/sf/v1/your-store/delivery/check?postcode=2000&product=88001&variant=44001" \
-H "X-Rentals-Key: sirpk_your_publishable_key"
# 200, Cache-Control: private, max-age=60
{
"ok": true, "postcode": "2000",
"zone": {"id": 7, "name": "Sydney CBD", "feeHint": "15.00", "minOrderHint": null, "locationId": 66001},
"locationId": 66001, "code": null, "message": "", "earliestStart": "2026-12-03"
}
# a postcode outside every area
{"ok": false, "postcode": "2150", "zone": null, "locationId": null, "code": "outside_area",
"message": "We don't deliver to 2150. Choose another way to get it, or check the postcode."}
code is null when ok, else postcode_required (no usable postcode) or outside_area. feeHint and minOrderHint are for display only: Shopify’s own delivery rates charge the fee. Pass the same postcode to calendar, quote, reserve and handover/windows.
Any other path under /sf answers 404 not_found, and a known endpoint with the wrong method 405 method_not_allowed.
A complete example (plain JavaScript in the browser) #
const RENTALS = 'https://YOUR-API-ADDRESS/sf/v1/your-store'; // Settings › API › Headless storefronts
const PK = 'sirpk_your_publishable_key'; // public: fine in the browser
const SHOP = 'https://your-store.myshopify.com/api/2026-07/graphql.json';
const STOREFRONT_TOKEN = 'your-storefront-api-token'; // your own Storefront API access token
async function rentals(path, body) {
const res = await fetch(`${RENTALS}/${path}`, body
? { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Rentals-Key': PK }, body: JSON.stringify(body) }
: { headers: { 'X-Rentals-Key': PK } });
const data = await res.json();
if (!res.ok) { // {error: {code, message, field?}}: the message is written for shoppers
throw Object.assign(new Error(data.error?.message || res.statusText), data.error, { status: res.status });
}
return data;
}
async function storefront(query, variables) {
const res = await fetch(SHOP, { method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Shopify-Storefront-Access-Token': STOREFRONT_TOKEN },
body: JSON.stringify({ query, variables }) });
return (await res.json()).data;
}
const productId = 88001, variantId = 44001; // numeric ids: the digits at the end of the gid
const cal = await rentals(`calendar?product=${productId}&variant=${variantId}&from=2026-12-01&to=2026-12-31`);
// … draw a calendar from cal.disabled_dates, cal.fullybooked_dates, cal.dayPrices, cal.minLength …
const pick = { product: productId, variant: variantId, quantity: 1, from: '2026-12-02', to: '2026-12-04', locale: 'en' };
const quote = await rentals('quote', pick); // show quote.total
const reply = await rentals('reserve', pick); // {token, properties, quote, hold}
const line = {
merchandiseId: `gid://shopify/ProductVariant/${variantId}`,
quantity: reply.quote.quantity,
attributes: Object.entries(reply.properties).map(([key, value]) => ({ key, value })),
};
const out = await storefront(`mutation($id: ID!, $lines: [CartLineInput!]!) {
cartLinesAdd(cartId: $id, lines: $lines) { cart { id checkoutUrl } userErrors { code message } } }`,
{ id: cartId, lines: [line] }); // no cart yet? cartCreate(input: {lines: [line]})
if (out.cartLinesAdd.userErrors.length) {
await rentals('release', { hold: reply.hold.id });
throw new Error(out.cartLinesAdd.userErrors[0].message);
}
window.location.href = out.cartLinesAdd.cart.checkoutUrl;
On Hydrogen, hand the line to the cart route with CartForm.ACTIONS.LinesAdd (fetcher.submit({[CartForm.INPUT_NAME]: JSON.stringify({action: CartForm.ACTIONS.LinesAdd, inputs: {lines: [line]}})}, {method: 'POST', action: '/cart'})).
When checkout finds a rental line that doesn’t match its token (a changed quantity, a missing attribute, dates in the past), the cart mutations answer a VALIDATION_CUSTOM user error naming the item and checkout is blocked. A hold that expired before the order is still booked when stock remains; otherwise the booking is flagged as a conflict for the store to sort out, just like on the online store.
The storefront GraphQL API #
The same publishable key opens POST https://YOUR-API-ADDRESS/graphql/2026-10/storefront/your-store, with the same allowed websites, limits and codes. reserveRentalDates answers the cart line ready for cartLinesAdd: merchandiseId, lineQuantity and attributes. A refusal is a userErrors entry (code as in REST, httpStatus the REST status) instead of an HTTP error. Introspection is off for storefronts: use the tables below or the explorer with a secret key.
const RENTALS_GQL = 'https://YOUR-API-ADDRESS/graphql/2026-10/storefront/your-store';
async function rentals(query, variables = {}) {
const res = await fetch(RENTALS_GQL, { method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Rentals-Key': 'sirpk_your_publishable_key' },
body: JSON.stringify({ query, variables }) });
const body = await res.json();
if (body.errors?.length) throw Object.assign(new Error(body.errors[0].message), body.errors[0].extensions);
return body.data;
}
const pick = { variantId: '44001', quantity: 1, startDate: '2026-12-02', endDate: '2026-12-04' };
const { rentalProductPrice: price } = await rentals(
`query($i: RentalPriceInput!) { rentalProductPrice(input: $i) { total unitPrice days currency } }`, { i: pick });
const { reserveRentalDates: r } = await rentals(
`mutation($i: RentalReserveInput!) { reserveRentalDates(input: $i) {
merchandiseId lineQuantity attributes { key value } hold { id expiresAt } userErrors { code message httpStatus } } }`, { i: pick });
if (r.userErrors.length) throw new Error(r.userErrors[0].message); // not_available, closed_day, too_many_holds, not_ready …
const line = { merchandiseId: r.merchandiseId, quantity: r.lineQuantity, attributes: r.attributes };
// cartLinesAdd(cartId, lines: [line]) with your Storefront API token. Changed dates or a removed line:
// await rentals(`mutation($h: ID!) { releaseRentalHold(holdId: $h) { released } }`, { h: r.hold.id });
A storefront key may use these operations only; asking for anything else refuses the whole request with access_denied.
| Operation | Arguments | What it does |
|---|---|---|
rentalShopquery · secret or publishable key returns RentalShop! | – | The store the key belongs to: domain, name, currency, time zone, today’s date and whether online booking is ready (checkoutReady). |
rentalLocationsquery · secret or publishable key returns [RentalLocation!]! | – | The store’s Shopify locations and whether rentals are picked up there. Storefronts see only the locations that rent, without phone numbers. |
rentalProductCalendarquery · secret or publishable key returns RentalCalendar! | input: RentalCalendarInput! | What a date picker needs for one product: fully booked days, the booked intervals (secret keys only) and the full calendar payload the store’s own calendar uses (widgetConfig). |
rentalProductPriceListquery · secret or publishable key returns RentalPriceList! | productId: IDvariantId: ID | The rate card: every period the product is priced by, its price and its price per day. |
rentalProductPricequery · secret or publishable key returns RentalQuote! | input: RentalPriceInput! | The price of a rental over these dates, with the breakdown: exactly what the cart will charge. It prices anything that parses; availability and the store’s rules are checked when you reserve or book. |
rentalProductDayPricesquery · secret or publishable key returns RentalDayPrices! | input: RentalDayPricesInput! | The price of a one-day rental starting on each day of a range (at most 366 days). |
rentalCatalogAvailabilityquery · secret or publishable key returns RentalCatalogAvailability! | productIds: [ID!]handles: [String!]selection: RentalSearchSelectionInput! | For up to 50 products (ids or handles): is each a rental, and is it free for a date search? With a place (where), how far away it is. |
rentalMapMarkersquery · secret or publishable key returns RentalMap! | collection: StringproductIds: [ID!]selection: RentalSearchSelectionInput | Map markers for rental products with a location (at most 250), optionally for a collection, some products or a place; with dates, whether each is free. |
rentalHandoverWindowsquery · secret or publishable key returns [RentalHandoverWindow!]! | input: RentalHandoverWindowsInput! | The time slots of a hand-over day for a product and method (pick-up, delivery or transport). |
rentalDeliveryCheckquery · secret or publishable key returns RentalDeliveryCheck! | postcode: String!productId: IDvariantId: IDlocationId: ID | Can delivery be booked to a postcode, and from which area and location. |
rentalKitquery · secret or publishable key returns RentalKit | id: ID! | One kit, or null. |
reserveRentalDatesmutation · publishable key only returns RentalReservePayload! | input: RentalReserveInput! | Hold the dates for 30 minutes and get the cart line: merchandiseId, lineQuantity and attributes for the Storefront API’s cartLinesAdd. |
releaseRentalHoldmutation · publishable key only returns RentalReleasePayload! | holdId: ID! | Free a hold (the shopper changed the dates or removed the line). Safe to repeat: a released or expired hold answers released: false. |
Their inputs:
RentalCalendarInput – A window to describe for a date picker. Give variantId, or productId (its first rent variant for the storefront).
| Field | Type | Notes |
|---|---|---|
productId | ID | |
variantId | ID | |
startDate | String | First day, Y-m-d. With it, the widget payload covers startDate … endDate (default 90 days, at most 400); without it, everything from two days ago on. |
endDate | String | Last day, Y-m-d. |
slim | Boolean | A lighter widget payload (no day prices), as the POS asks for. |
display | String | global = the store-wide calendar’s display. |
locationId | ID | The pick-up location: the calendar is for that location’s stock and bookings. |
returnLocationId | ID | A one-way rental’s return location: the calendar answers for renting it one way (widgetConfig.returnLocations lists the choices). |
method | RentalHandoverMethod | The calendar for this method (its transit and hand-over days; the payload’s handover). |
returnMethod | RentalReturnMethod | |
postcode | String | Delivery: the postcode (its area’s days; with several locations, where it is delivered from). |
zoneId | ID |
RentalPriceInput – What to price. Give variantId, or productId when the product has one rent variant.
| Field | Type | Notes |
|---|---|---|
productId | ID | |
variantId | ID | |
quantity | Int = 1 | |
startDate | String | Start date (Y-m-d), or date and time (2026-10-01T09:00). Required (or checkIn). |
endDate | String | End date; default the start date (one day; a stay needs a later check-out). |
startTime | String | Start time (HH:mm) when startDate has none. |
endTime | String | End time (HH:mm) when endDate has none. |
guests | RentalPartyInput | The party, for a product that counts guests. |
locale | String | The storefront language for texts (Rental dates line property), e.g. fr. |
locationId | ID | The pick-up location (prices don’t depend on it yet; with returnLocationId it decides the one-way fee). |
returnLocationId | ID | Returned to another location than the pick-up: the one-way fee is added. |
method | RentalHandoverMethod | The method (prices don’t depend on it yet). |
returnMethod | RentalReturnMethod | |
checkIn | String | A stay’s check-in day (stands for startDate). |
checkOut | String | A stay’s check-out day (stands for endDate). |
RentalPartyInput – A party of guests, for products that count guests (the widget’s guest picker). Omit for products that don’t.
| Field | Type | Notes |
|---|---|---|
guests | Int | Total guests, when the product doesn’t split them. |
adults | Int | |
children | Int | |
infants | Int | |
pets | Int |
RentalDayPricesInput
| Field | Type | Notes |
|---|---|---|
productId | ID | |
variantId | ID | |
startDate | String | First day, Y-m-d; default today. |
endDate | String | Last day, Y-m-d; default 30 days after the start. At most 366 days. |
RentalSearchSelectionInput – A date search (the search block’s selection; the theme’s sr_* URL parameters).
| Field | Type | Notes |
|---|---|---|
from | String | First day, Y-m-d. |
to | String | Last day, Y-m-d (default from). |
fromTime | String | HH:mm |
toTime | String | HH:mm |
quantity | Int | |
length | String | A length code instead of to: 4h, 3d, 2w, 1m, 1y. |
guests | Int | |
adults | Int | |
children | Int | |
infants | Int | |
pets | Int | |
where | String | A place as typed (≤ 100 characters): the shop’s own places first, then the geocoder. |
radiusKm | Int | Kilometres around where, 1–500 (default 25). |
locationId | ID | A pick-up location (the theme’s sr_loc): available THERE; without it, available at some location. |
RentalHandoverWindowsInput
| Field | Type | Notes |
|---|---|---|
productId | ID | |
variantId | ID | |
method | RentalHandoverMethod | |
returnMethod | RentalReturnMethod | |
date | String! | The day (Y-m-d). |
locationId | ID | |
back | Boolean | The return day’s slots instead of the out day’s. |
postcode | String | Delivery: the postcode (its area’s slots and the places left). |
zoneId | ID |
RentalReserveInput – What to hold: the same as the price input.
| Field | Type | Notes |
|---|---|---|
productId | ID | |
variantId | ID | |
quantity | Int = 1 | |
startDate | String! | Start date (Y-m-d), or date and time (2026-10-01T09:00). |
endDate | String | End date; default the start date (one day). |
startTime | String | Start time (HH:mm) when startDate has none. |
endTime | String | End time (HH:mm) when endDate has none. |
guests | RentalPartyInput | The party, for a product that counts guests. |
locale | String | The storefront language for the Rental dates text, e.g. fr. |
locationId | ID | Where the customer picks it up; required when the product is at two or more locations (location_required). |
returnLocationId | ID | Where it goes back when not the pick-up location (a one-way rental; the product’s return rules decide). |
payment | RentalPaymentChoice = FULL | PART to pay part now and the rest later, when the store and the rental allow it (else the rental is paid in full). Default FULL. |
method | RentalHandoverMethod | How it reaches the customer; required when the product offers two or more (method_required). |
returnMethod | RentalReturnMethod | |
window | String | The out time slot (08:00-10:00) when the method has slots. |
backWindow | String | |
postcode | String | Delivery: the postcode; required (postcode_required / outside_area) once the store has delivery areas.. |
zoneId | ID | |
subscription | RentalSubscriptionInput | Rent it billed per period (a start date only; the unit is held until it comes back or to the term end). |
| Enum | Values |
|---|---|
RentalHandoverMethod | PICKUP – The customer picks it up.DELIVERY – The store delivers it.SHIPPED – It is transported (shipped). |
RentalReturnMethod | IN_STORE – The customer brings it back to the store.COLLECT – The store collects it.POST – The customer transports it back. |
RentalPaymentChoice | FULLPART |
RentalBillingSource | EXTERNAL – Another app’s subscription (a selling plan on the rent variant): the app charges, we hold the stock.INVOICE – Our invoice each period: a Shopify payment link.CONTRACT – Our own subscription contract. |
RentalSubscriptionInput – Rent billed per period instead of for dates (the store’s Settings › Subscriptions).
| Field | Type | Notes |
|---|---|---|
billing | RentalBillingSource! | EXTERNAL = another app’s selling plan (planId required); INVOICE = our invoice each period. |
planId | ID | The selling plan (gid://shopify/SellingPlan/… or its number), for EXTERNAL. |
term | String | o (default) = until returned, or a number of periods (1–120). |
interval | String | INVOICE: 1M, 2W… (default the product’s). |
And their types. Fields marked “secret key only” are refused with a publishable key:
| Type | Fields |
|---|---|
RentalShopThe store the credential belongs to. | domain: String!, name: String, currency: String!, timezone: String!, today: String!, primaryLocale: String, checkoutReady: Boolean!, posCartMode: String |
RentalLocationA Shopify location of the store, as a place to pick up rentals. | id: ID!, legacyResourceId: Int!, name: String!, shopifyName: String, publicName: String, address: RentalLocationAddress, area: String, active: Boolean!, rentalsEnabled: Boolean!, isDefault: Boolean!, acceptsReturns: Boolean!, pickupEnabled: Boolean!, instructions: String, rules: JSON |
RentalLocationAddress | address1: String, address2: String, city: String, province: String, zip: String, country: String, countryCode: String, phone: String |
RentalCalendarAvailability for one product over a window. | productId: ID!, variantId: ID!, totalQuantity: Int!, window: RentalDateWindow, today: String!, currency: String!, fullyBookedDays: [RentalBookedDay!]!, inventory: [RentalInventoryInterval!]!, widgetConfig: JSON!, product: RentalProduct!, nightly: Boolean!, turnoverDates: [String!]!, stayDays: [RentalStayDay!]! |
RentalStayDayHow a day looks in a stay-of-nights calendar (the widget’s stayDayState). | date: String!, state: String!, checkIn: Boolean!, checkOut: Boolean! |
RentalDateWindowA range of days, Y-m-d. | from: String!, to: String! |
RentalBookedDayA day with every unit taken. | date: String!, quantity: Int! |
RentalInventoryIntervalUnits taken over an interval (the ledger, turnaround included). | quantity: Int!, start: String!, end: String! |
RentalProductA rent variant: a product variant this store rents out. | productId: ID!, variantId: ID!, title: String, variantTitle: String, sku: String, rentalType: String, hasTime: Boolean!, mode: String!, units: Int, enabled: Boolean!, nightly: Boolean!, stayType: String, checkInTime: String, checkOutTime: String. stayType is days, days_free (Days: can go out again on the return day) or nights; it is null when the product follows the store. |
RentalPriceListThe product’s rate card. | productId: ID!, variantId: ID!, name: String, variantTitle: String, rentalType: String, priceType: String, from: RentalPriceFrom!, headline: RentalPriceHeadline!, rates: [RentalRate!]!, minLength: JSON, maxLength: JSON, currency: String!, currencySymbol: String |
RentalPriceFrom | amount: Float!, unit: String |
RentalPriceHeadline | amount: Float!, period: String |
RentalRateOne period of the rate card. | label: String!, period: String!, quantity: Int!, unit: String!, days: Float!, price: Float!, perDay: Float, addOn: RentalRateAddOn |
RentalRateAddOn | label: String!, period: String!, price: Float! |
RentalQuoteWhat a rental costs: the storefront’s quote, what the cart line will charge. | productId: ID!, variantId: ID!, startDate: String!, endDate: String!, quantity: Int!, days: Int!, hours: Float!, unitPrice: Float!, total: Float!, lines: [RentalQuoteLine!]!, extraLines: [RentalQuoteLine!]!, guests: JSON, lengthUnit: String, currency: String!, currencySymbol: String, chargedTotal: Float!, tokenUnitPrice: String!, payment: RentalPaymentOptions, nightly: Boolean!, nights: Int |
RentalQuoteLineOne line of a price breakdown. | label: String!, kind: String!, amount: Float! |
RentalPaymentOptionsHow a rental can be paid (the store’s Settings › Payments). Present when part payment can apply to the product. | options: [RentalPaymentChoice!]!, default: RentalPaymentChoice!, part: RentalPaymentSplit, reason: String, message: String |
RentalPaymentSplitPart now, the balance later: decimal strings in the store currency. | payNow: String!, balance: String!, dueDate: String!, dueAt: String, dueLabel: String, percent: Float, text: String |
RentalDayPricesA one-day hire’s price starting on each day. | productId: ID!, variantId: ID!, startDate: String!, endDate: String!, note: String!, days: [RentalDayPrice!]!, currency: String!, currencySymbol: String |
RentalDayPrice | date: String!, amount: Float! |
RentalCatalogAvailability | items: [RentalCatalogEntry!]!, where: RentalWhere |
RentalCatalogEntry | key: String!, productId: ID, handle: String, rental: Boolean!, available: Boolean, units: Int, distanceKm: Float, near: Boolean |
RentalWhereA place a Where search resolved to. | resolved: Boolean!, label: String, lat: Float, lng: Float, radiusKm: Int |
RentalMap | markers: [RentalMapMarker!]!, where: RentalWhere |
RentalMapMarker | productId: ID!, handle: String!, title: String!, url: String!, image: String, lat: Float!, lng: Float!, radius: Float, approximate: Boolean!, area: String, from: String, available: Boolean, near: Boolean, distanceKm: Float |
RentalHandoverWindowA time slot of a hand-over day. | id: String!, from: String!, to: String!, label: String, capacity: Int, left: Int, full: Boolean! |
RentalKit | id: ID!, legacyResourceId: Int!, productId: ID!, variantId: ID!, title: String!, source: RentalKitSource!, pricing: RentalKitPricing!, itemsDiscountPercent: String, useItemRules: Boolean!, items: [RentalKitItem!]! |
RentalKitItem | productId: ID!, variantId: ID!, title: String!, variantTitle: String, quantity: Int! |
RentalReservePayload | token: String, attributes: [RentalAttribute!], properties: JSON, lineQuantity: Int, merchandiseId: ID, quote: RentalQuote, hold: RentalHold, product: RentalProduct, datesText: String, userErrors: [RentalUserError!]!, location: RentalLocation, returnLocation: RentalLocation, payment: RentalReservePayment, handover: RentalHandover, deliveryFee: RentalDeliveryFeeLine, subscription: RentalReserveSubscription |
RentalReserveSubscriptionThe subscription of a held rental: pass sellingPlan with the cart line (Online Store /cart/add.js selling_plan, Storefront API sellingPlanId). | billing: RentalBillingSource!, sellingPlan: Int, planName: String, interval: String!, everyLabel: String!, term: String!, cycles: Int, periodPrice: String!, currency: String!, holdUntil: String, termEnd: String, consent: String!, consentAmounts: [String!]! |
RentalDeliveryFeeLineA delivery fee cart line . | merchandiseId: ID!, amount: String!, stop: String!, token: String!, properties: JSON!, attributes: [RentalAttribute!]! |
RentalAttributeA cart line attribute, ready for the Storefront API’s CartLineInput.attributes. | key: String!, value: String! |
RentalHoldDates held for a shopper until checkout (or until it expires). | id: ID!, legacyResourceId: String!, expiresAt: String! |
RentalReservePaymentHow a held rental will be paid (what the token carries). | choice: RentalPaymentChoice!, part: RentalPaymentSplit, reason: String, message: String |
RentalHandoverA booking’s hand-over: the out moment (pick-up, delivery or dispatch day) and the back moment (return, collection or the day the parcel is due back). | method: RentalHandoverMethod!, returnMethod: RentalReturnMethod!, label: String, returnLabel: String, outAt: String, outWindow: String, backAt: String, backWindow: String, outLabel: String, backLabel: String, text: String, transitOutMinutes: Int!, transitBackMinutes: Int!, mismatch: JSON, returnTracking: String, deliveryAddress: JSON, zone: RentalDeliveryArea, postcode: String |
RentalDeliveryCheckThe postcode check. | ok: Boolean!, postcode: String, zone: RentalDeliveryArea, locationId: ID, code: String, message: String, earliestStart: String |
RentalDeliveryAreaA delivery area as shoppers see it . | id: ID!, name: String!, feeHint: String, minOrderHint: String |
RentalReleasePayload | released: Boolean!, userErrors: [RentalUserError!]! |
| Enum | Values |
|---|---|
RentalKitSource | APP – Made in the app (App Home or this API).SHOPIFY_BUNDLES – A Shopify Bundles product run as a fixed-price kit. |
RentalKitPricing | OWN – The kit’s own rental rates.ITEMS_SUM – The sum of the items’ rental prices for the dates, minus itemsDiscountPercent. |
Limits #
| What | Limit | Answer |
|---|---|---|
| Requests a minute, per store and shopper IP address | reserve 20, quote 60, calendar 120 (shared by REST, GraphQL and the online store’s calendar) | 429 rate_limited with Retry-After |
| Live holds per shopper IP address | 10; release or the 30-minute expiry frees one | 429 too_many_holds |
| A hold | 30 minutes; the stock stays taken until then | |
| Online booking not set up yet | reserve refuses, so no line can check out at the wrong price | 503 not_ready with Retry-After |
| Keys and websites | 10 active keys per store, 20 websites per key |
calendar and availability may be cached by the browser for 30 seconds (Cache-Control: public, max-age=30); everything else is no-store. In GraphQL, GET requests made only of rentalProductCalendar, rentalCatalogAvailability, rentalProductPriceList and rentalProductDayPrices (30 seconds) or rentalMapMarkers (60 seconds) are cached the same way.
