The Rentals & Bookings app and its add-on apps have an API, so your own systems can work with your rentals: a booking website, a warehouse scanner, an accounting export, or a script that runs every night. This page explains how to get a key, where to send requests and what the answers look like. The reference pages list every operation.
Three ways in #
| API | Use it for | Key |
|---|---|---|
| REST API v1 | Plain web addresses and JSON. Easy from a script, a spreadsheet tool or anything that can send an HTTP request. | Secret API key (sirb_…) |
| GraphQL API | One address where you ask for exactly the fields you need, several things in one request, with a typed schema and a browser explorer. | Secret API key (sirb_…) |
| Storefront API | Selling rentals on a Hydrogen or custom storefront: the calendar, the price, a hold on the dates and the cart line. REST and GraphQL. | Publishable key (sirpk_…) |
REST and GraphQL do the same things, with the same keys, rules and error codes. Pick the style your developer prefers. For automations without code, use Shopify Flow.
The reference pages:
- GraphQL API reference: Rentals & Bookings: how to send a request, and every query and mutation
- GraphQL API reference: Rentals & Bookings types: the inputs, types and enums
- GraphQL API reference: add-on apps: Rental Contracts, Security Deposits, Custom Fields and SI Request a Quote
- REST API reference: bookings and availability: bookings, availability, pricing, orders, balances, calendar sync
- REST API reference: warehouse, stock and settings: check-outs and check-ins, serial numbers, stock, kits, delivery, settings, API keys
- REST API reference: add-on apps
- Headless storefronts: the storefront API
Which apps the API covers #
One key works for every Sales Igniter app on your store. Each operation needs its own app: an operation of an app your store doesn’t have answers 403 with the code app_not_installed, and data.app names the app it needs.
| App | What the API reaches | data.app |
|---|---|---|
| Rentals & Bookings | Rental products, availability, prices, bookings, orders, the Send & Return desk, serial numbers, stock by location, kits, delivery & pickup, balances, calendar sync, rental settings | core |
| Rental Contracts | Signed agreements, their PDFs, the audit trail, verify and resend | contracts |
| Security Deposits | An order’s deposits, refunds, automatic refunds, product deposit rules | deposits |
| Custom Fields (Fields, Waiver & ID) | Answers on orders, fees, the damage waiver line, ID check status, field groups, import and export | fields |
| SI Request a Quote, Hide Price | Quotes: list, create, change, send, convert, notes, messages, PDF | quotes |
The reference pages mark every endpoint that needs an add-on app. Rental Contracts and Security Deposits work alongside Rentals & Bookings. Custom Fields and SI Request a Quote also work on their own.
Get an API key #
- In Rentals & Bookings, open Settings › API. On a store that has only Custom Fields or only SI Request a Quote, open API access in that app’s menu instead. They are the same keys.
- Under Create a key, enter a Key name (the system or person that will use it). Tick Allow changes (write) if it needs to change anything. Without it, the key can only read.
- Click Create key and copy the key. It is shown once.
A secret key starts with sirb_. Treat it like a password: keep it on a server, never in a website’s code or a mobile app. The list shows each key’s name, the start of the key, its access and when it was last used. Revoke stops a key at once. A store can have up to 20 active keys.
To replace a key without downtime, make a new key, put it in your system, then revoke the old one. Your system can do this itself through the API (POST /api/v1/api-keys or createRentalApiKey). A key can make keys, but never one with more access than its own, and a read key can’t make or revoke keys.
Storefronts use a different, public key (sirpk_…) made under Settings › API › Headless storefronts. See Headless storefronts: the storefront API.
Addresses #
Settings › API (and the API access page) shows your addresses, for example “REST API: https://…/api/v1 · GraphQL API: https://…/graphql/2026-10”. The host is the same for every API. These pages write it as YOUR-API-ADDRESS.
| API | Address |
|---|---|
| REST API v1 | https://YOUR-API-ADDRESS/api/v1/… |
| GraphQL API | https://YOUR-API-ADDRESS/graphql/2026-10 |
| GraphQL explorer | https://YOUR-API-ADDRESS/graphiql (Open the GraphQL explorer on Settings › API) |
| Storefront API, REST | https://YOUR-API-ADDRESS/sf/v1/your-store/… |
| Storefront API, GraphQL | https://YOUR-API-ADDRESS/graphql/2026-10/storefront/your-store |
your-store is your store’s myshopify name, as your-store or your-store.myshopify.com. Requests and answers are JSON over HTTPS, except the CSV and PDF downloads the reference points out.
Authentication and access #
Send the secret key with every request, in the Authorization header:
curl -s "https://YOUR-API-ADDRESS/api/v1/bookings?per_page=5" \
-H "Authorization: Bearer sirb_your_secret_key"
X-Api-Key: sirb_your_secret_key works too. Everything a key reads or changes belongs to its own store.
| Key | REST | GraphQL |
|---|---|---|
| Read (the default) | GET requests | Queries |
| Read and write (Allow changes (write)) | Every method | Queries and mutations |
A missing, unknown or revoked key answers 401 with the code unauthorized. A read key that tries to change something answers 403 with the code forbidden and data.required_scope: "write".
To try the GraphQL API without writing code, click Open the GraphQL explorer on Settings › API, paste a secret key in the explorer’s Headers pane as {"Authorization": "Bearer sirb_your_secret_key"} and run a query. Its Docs pane lists every type and field.
Versions #
- REST is version 1:
/api/v1/…. New fields can appear in answers, so ignore fields you don’t know. - GraphQL has dated versions in the address. The current version is
2026-10, the only one so far. Changes that could break your code (a removed or renamed field, a new required argument, a changed meaning) only ship in a new dated version. New fields, types, operations, optional arguments and enum values can appear in the current version. A version stays available for at least 12 months after the next one ships. An unknown version answers404with the codeunknown_versionand the list of versions.
Rate limits #
- Each secret key may make 120 requests a minute, REST and GraphQL together. Every answer carries
X-RateLimit-LimitandX-RateLimit-Remaining. - Over the limit, the answer is
429with the coderate_limited, aRetry-Afterheader (seconds) anddata.retry_after. Wait that long, then try again. - A GraphQL request is also limited in size: at most 12 levels deep, a complexity of 6,000 (each field costs 1; a list costs 2 plus its page size times what each item asks for), 20,000 bytes of query text, and one operation per request.
- The storefront API has its own limits per store and shopper: see Headless storefronts.
Pagination #
REST lists take page (from 1) and per_page (default 20, at most 200). They answer a JSON array, with the number of matches in the X-Total header and the number of pages in X-Total-Pages:
curl -s -D - -o /dev/null "https://YOUR-API-ADDRESS/api/v1/bookings?status=reserved&page=2&per_page=50" \
-H "Authorization: Bearer sirb_your_secret_key"
# HTTP/1.1 200 OK
# X-Total: 132
# X-Total-Pages: 3
A per_page above 200 answers 400 with the code invalid_param.
GraphQL lists are connections: ask for first (or last) items, then pass the last endCursor as after for the next page. Each list has nodes (or edges { cursor node }), pageInfo { hasNextPage endCursor } and totalCount. The page size is 20 by default and at most 200.
query NextPage($after: String) {
rentalBookings(first: 50, after: $after, filter: {statuses: [RESERVED]}) {
totalCount
pageInfo { hasNextPage endCursor }
nodes { id orderName startDate endDate quantity }
}
}
A few short lists come whole: API keys, locations, field groups and the products with their own deposit rule. The Custom Fields import and export lists take first (20 by default, at most 50).
Ids, dates and money #
- Ids. REST uses numbers: Shopify’s ids for products, variants, orders, customers and locations, and the app’s own ids for bookings, serial numbers, deposits and so on. Prefer
variant_id;product_idworks when the product has one rent variant. GraphQL uses global ids, such asgid://shopify/ProductVariant/44001for Shopify’s objects andgid://sirentals/Booking/42for the app’s. Every GraphQLIDargument also takes the bare number. - Dates are in your store’s time zone (GraphQL:
rentalShop { timezone }). Send2026-12-01,2026-12-01 09:00or2026-12-01T09:00. Answers use2026-12-01 09:00:00; the times records were made or changed (created_at,last_used_at…) are ISO 8601 with a time zone. - Rental ranges are built the way the cart builds them: a date-only rental from the 1st to the 3rd ends at midnight on the 3rd (2 days), and a rental whose start and end are the same day lasts that day. Answers also give the dates with the product’s turnaround time added (
start_with_buffer,end_with_buffer): those are the dates that block the calendar. - Money is in the store currency. Prices and quotes are numbers (
450); balances, deposits, fees and quotes of SI Request a Quote are decimal strings ("150.00"). - Enum values are lower case in REST (
reserved) and upper case in GraphQL (RESERVED). - True and false in a REST address:
true/false,1/0oryes/no.
Errors #
Every REST error has the same shape: a code to branch on, a message written for people (show it to your staff), and data with the HTTP status and any details.
curl -s "https://YOUR-API-ADDRESS/api/v1/bookings?per_page=500" \
-H "Authorization: Bearer sirb_your_secret_key"
# 400
{
"code": "rentals_rest_invalid_param",
"message": "per_page must be at most 200.",
"data": {"params": {"per_page": "per_page must be at most 200."}, "status": 400}
}
GraphQL uses the same codes without the rentals_rest_ prefix, in two places:
- A mutation that can’t do what you asked (not enough stock, a closed day, a bad amount) answers normally, with the reason in
userErrors { code message field data httpStatus }.httpStatusis the status REST would answer. - A request that can’t run answers
errors[], each withextensions { code http_status data }: a query that doesn’t match the schema (invalid_query), a mutation with a read key (forbidden), an app the store doesn’t have (app_not_installed), a query that is too deep or too complex. The HTTP status is still 200. Only a request refused before it runs has its own HTTP status:401bad key,404unknown version,405a mutation sent with GET,413query too long,429rate limit,400not one JSON operation.
mutation {
createRentalBooking(input: {variantId: "44001", startDate: "2026-12-01", endDate: "2026-12-03", quantity: 3, block: true}) {
booking { id }
userErrors { code message field data httpStatus }
}
}
# {"data": {"createRentalBooking": {"booking": null, "userErrors": [{"code": "not_available",
# "message": "Only 1 unit(s) available for those dates.", "field": null, "data": {"available": 1}, "httpStatus": 409}]}}}
The codes you will meet most:
| Code | Status | When |
|---|---|---|
unauthorized | 401 | No key, or an unknown or revoked key. |
forbidden | 403 | A read key tried to change something (data.required_scope). |
app_not_installed | 403 | The operation belongs to an app the store doesn’t have (data.app). |
rate_limited | 429 | Too many requests for this key (Retry-After, data.retry_after). |
invalid_param | 400 | A parameter is missing, the wrong type or out of range (data.params names each one). |
invalid | 400 | A value the operation can’t accept (data.field names it). |
invalid_dates | 400 | A date doesn’t parse, or the end isn’t after the start. |
not_found | 404 | No such booking, serial number, agreement, key … in this store. |
no_route | 404 | REST only: no such path, or the wrong method for it. |
not_available | 409 | Not enough units for those dates (data.available). |
closed_day, start_too_early, min_length, max_length, too_far_ahead, fixed_length, fixed_date, guests | 409 | The dates break one of the store’s booking rules. |
batch_refused | 409 | One item of an all-or-nothing batch was refused, so nothing was recorded (data.failures). |
shopify_error, draft_order_failed | 502 | Shopify refused a call the app made for you. |
server_error | 500 | Something went wrong on our side. Try again. |
invalid_query, access_denied, max_depth_exceeded, max_complexity_exceeded, query_too_long, introspection_disabled, unknown_version, method_not_allowed, invalid_request | GraphQL only: the request itself was refused. |
Each reference page lists the codes of its own operations.
Webhooks and events #
Rentals & Bookings sends its events to Shopify Flow: Rental booked, checked out, due back tomorrow, overdue, conflict found, balance due and more. To send them to your own system, build a Flow workflow with one of those triggers and Flow’s Send HTTP request action. See Shopify Flow, API and headless storefronts.
SI Request a Quote can post a signed JSON message to your own address for every quote event. Set it up under Settings › Slack, Teams and webhooks: enter Your webhook address, then click Show the signing secret and copy it into your system.
| Events | quote.requested, quote.sent, quote.changes_requested, quote.accepted, quote.ordered, quote.declined, quote.expired, quote.rejected, quote.cancelled, quote.message |
| Headers | Content-Type: application/json, User-Agent: SI-Request-a-Quote/1, X-SIQuotes-Event: quote.accepted, X-SIQuotes-Signature: sha256=<hex HMAC-SHA256 of the raw body> |
| Body | {"event", "shop", "occurredAt", "quote": {id, number, publicId, status, statusLabel, source, customer, total, currency, expiresAt, createdAt, updatedAt, orderId, orderName, draftOrderId, tags, …}} |
| Retries | Answer with a 2xx status within 10 seconds. A failed delivery is tried twice more, after 1 and then 5 minutes. |
Check the signature on the raw body, before you parse the JSON:
import crypto from 'node:crypto';
// rawBody: the request body exactly as it arrived (a Buffer); secret: your signing secret
export function isFromQuotes(rawBody, signatureHeader, secret) {
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const a = Buffer.from(signatureHeader || '');
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Make a new secret on the same screen replaces the secret at once; update your system right after.
