View Categories

Launching soon on the Shopify App StoreJoin the waitlist

REST API reference: add-on apps

14 min read

The REST API v1 endpoints of the add-on apps. The key, limits, pagination and error format are the same as for Rentals & Bookings: see the API overview.

Each section needs its app. The API checks the app before anything else: on a store without it, every endpoint of that section answers 403 with the code rentals_rest_app_not_installed and data.app naming the app.

SectionNeeds the appPathsdata.app
Rental ContractsRental Contracts/api/v1/contracts/…contracts
Security DepositsSecurity Deposits/api/v1/deposits/…deposits
Custom FieldsCustom Fields (Fields, Waiver & ID)/api/v1/fields/…fields
QuotesSI Request a Quote, Hide Price/api/v1/quotes/…quotes
{"code": "rentals_rest_app_not_installed",
 "message": "This store does not have the SI Request a Quote, Hide Price app installed, so its operations are not available.",
 "data": {"app": "quotes", "status": 403}}

The examples use two shell variables, API="https://YOUR-API-ADDRESS/api/v1" and KEY="sirb_your_secret_key". GET needs a read key; everything else needs a key with Allow changes (write).

Rental Contracts #

Needs the Rental Contracts app. The store’s signed rental agreements. See Signed agreements, Verify and record keeping. {id} is the agreement’s id, as in the order’s _contract attribute (8 to 32 letters and digits).

An agreement’s status is signed (signed, but the order isn’t placed or linked yet), linked (on its order) or redacted (personal data removed). stored is verified (the stored PDF matches its hash), none (no stored copy yet: the PDF link gives a watermarked preview) or altered (the stored PDF no longer matches its hash: it is never served).

GET /api/v1/contracts #

Agreements, newest first. Paged. Parameters: status (signed, linked or redacted), search (an order name, or the signer’s name or email), order_id.

curl -s "$API/contracts?status=linked&per_page=20" -H "Authorization: Bearer $KEY"
# 200, X-Total: 48
[{"id": "Q7mK2pX9aLr4", "status": "linked", "method": "typed", "method_label": "Typed name", "signer_name": "Jordan Lee",
  "signer_email": "[email protected]", "signed_at": "2026-11-02T14:03:40+00:00", "signed_at_label": "Nov 2, 2026 9:03 AM",
  "order_id": 5550001, "order_name": "#1042", "has_pdf": true, "redacted": false}]

GET /api/v1/contracts/{id} #

One agreement: the summary above, plus how and where it was signed and the evidence: signature_id, customer_id, ip, user_agent, typed_name, has_drawing, consent_text, terms_sha256, pdf_sha256, pdf_sha256_unsigned, pdf_generated_at, pdf_error, emailed_at, email_error, linked_at, stored, items, audit. 404 not_found otherwise.

curl -s "$API/contracts/Q7mK2pX9aLr4" -H "Authorization: Bearer $KEY"
# 200 (trimmed)
{"id": "Q7mK2pX9aLr4", "status": "linked", "method": "typed", "signer_name": "Jordan Lee", "order_id": 5550001,
 "ip": "203.0.113.7", "typed_name": "Jordan Lee", "has_drawing": false,
 "consent_text": "I agree to sign this rental agreement electronically, …",
 "terms_sha256": "216c…d1f4", "pdf_sha256": "2b75…500f", "stored": "verified", "items": [],
 "audit": [{"event": "consent_given", "label": "Consent to sign electronically given", "at": "2026-11-02T14:03:39Z",
            "detail": "browser time 2026-11-02 14:03:39.950 UTC", "hash": "0149a4cb2a25…db1540"}]}

GET /api/v1/contracts/{id}/audit #

The audit trail, oldest first: [{event, label, at, detail, hash}]. Each entry is chained to the one before (signed, linked, PDF made, emailed, downloaded, verified …).

GET /api/v1/contracts/{id}/pdf #

A link to the signed PDF that works for 10 minutes without a key: {"kind": "signed", "url": "https://…", "expires_at": "…"}. Before a copy is stored, kind is preview (a watermarked preview). Downloads are recorded in the audit trail. 409 altered when the stored PDF no longer matches its hash.

curl -s "$API/contracts/Q7mK2pX9aLr4/pdf" -H "Authorization: Bearer $KEY"

POST /api/v1/contracts/{id}/verify #

Checks the stored PDF’s hash, the terms’ hash and the audit chain again. The check is recorded in the trail.

curl -s -X POST "$API/contracts/Q7mK2pX9aLr4/verify" -H "Authorization: Bearer $KEY"
# 200
{"ok": true, "redacted": false, "file": true, "file_hash_ok": true, "terms_hash_ok": true, "chain_ok": true,
 "chain": {"…": "…"}, "stored_hash": "2b75…500f", "computed_hash": "2b75…500f", "checked_at": "2026-11-30T10:15:00+00:00"}

POST /api/v1/contracts/{id}/resend #

Emails the signed copy to the signer again: {"sent": true, "contract": {…}}.

GET /api/v1/contracts/orders/{orderId} #

An order’s agreement: {"order_id": 5550001, "contract": {…}}, or "contract": null when the order has none. An order that isn’t linked yet is linked now, from its _contract attribute.

curl -s "$API/contracts/orders/5550001" -H "Authorization: Bearer $KEY"

Security Deposits #

Needs the Security Deposits app. Deposits charged with rentals, refunds to the original payment method, and product deposit rules. See Refunding deposits. Amounts are decimal strings in the order’s currency.

An order’s deposits answer:

{
  "order_id": 5550001, "order_name": "#1042", "currency": "USD",
  "deposits": [{"id": 17, "order_id": 5550001, "order_name": "#1042", "line_item_id": 13002,
                "title": "Party tent 6×12 m · Dec 4, 2026 – Dec 6, 2026", "quantity": 1,
                "amount": "200.00", "tax": "0.00", "total": "200.00", "refunded": "0.00", "remaining": "200.00",
                "currency": "USD", "status": "not_refunded", "status_label": "Held", "auto_refund": true,
                "returned_at": null, "release_after": null, "refunded_at": null, "auto_error": null, "log": []}],
  "totals": {"total": "200.00", "refunded": "0.00", "remaining": "200.00"},
  "status": "not_refunded", "status_label": "Held", "can_refund": true,
  "auto_refund": {"store": true, "override": null, "effective": true, "days": 0, "release_after": null, "returned": false}
}

status is not_refunded, partially_refunded or fully_refunded (null when the order has no deposits). auto_refund says whether the rest is refunded automatically after the return: the store setting, this order’s override and the effective result. Each deposit’s log lists its refunds: [{at, amount, kept, reason, automatic}].

GET /api/v1/deposits/orders/{orderId} #

An order’s deposits. The order is read from Shopify the first time; refresh=true reads it again. 404 order_not_found.

curl -s "$API/deposits/orders/5550001" -H "Authorization: Bearer $KEY"

POST /api/v1/deposits/orders/{orderId}/refund #

Refunds deposits to the original payment method. The app sends Shopify an idempotency key built from the order, the deposits and what they had refunded before, so a retried request can’t refund twice.

Body fieldNotes
deposit_ids (or deposit_id)The deposits to refund; none means every deposit of the order.
amountRefund this much of one deposit ("150.00").
keepOr keep this much of one deposit (damage, late fees) and refund the rest.
reasonShown on the refund in Shopify (up to 400 characters).
curl -s -X POST "$API/deposits/orders/5550001/refund" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"deposit_ids": [17], "keep": "50.00", "reason": "Scratched frame"}'
# 200
{"refund": {"refund_id": "gid://shopify/Refund/9900001", "amount": "150.00", "deposits": [17]},
 "order_id": 5550001, "status": "fully_refunded", "totals": {"total": "200.00", "refunded": "150.00", "remaining": "0.00"}, "…": "…"}

Refusals: 404 order_not_found, 404 deposit_not_found, 400 one_deposit (amount or keep with several deposits), 400 bad_amount, 400 bad_keep, 400 nothing_to_refund, 400 no_payment (nothing was paid to refund to), 400 refund_failed (Shopify refused; the attempt is recorded), 502 shopify_error.

PUT /api/v1/deposits/orders/{orderId}/auto-refund #

Turns the automatic refund on or off for this order. Body: {"enabled": true}, false, or null to follow the store setting. Answers the order’s deposits.

GET /api/v1/deposits/products #

The products with their own deposit rule: [{product_id, title, rule: {mode, amount, per}}]. The others use the store default.

GET /api/v1/deposits/products/{productId} #

A product’s rule, its own or the store default (source: "product" or "store").

curl -s "$API/deposits/products/88001" -H "Authorization: Bearer $KEY"
# 200
{"product_id": 88001, "title": null, "source": "store", "rule": {"mode": "fixed", "amount": "200", "per": "unit"}}

PUT /api/v1/deposits/products/{productId} #

Gives a product its own rule. Body: rule ({mode, amount, per}) and an optional title for the list.

  • mode: off, fixed (an amount) or percent (of the rental’s unit price, at most 100).
  • per: unit (times the quantity), line (once per cart line) or order (joins the one deposit per order).
  • amount is required unless mode is off or per is order.
curl -s -X PUT "$API/deposits/products/88001" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"rule": {"mode": "percent", "amount": "25", "per": "line"}, "title": "Party tent 6×12 m"}'
# 200
{"product_id": 88001, "title": "Party tent 6×12 m", "rule": {"mode": "percent", "amount": "25", "per": "line"}, "warning": null}

warning means the rule is saved but the checkout can’t use it yet: the app retries in the background. 400 invalid (data.field: rule.mode or rule.amount).

DELETE /api/v1/deposits/products/{productId} #

Puts the product back on the store default: {"product_id": 88001, "removed": true}.

Custom Fields #

Needs the Custom Fields app (Fields, Waiver & ID). It works on a store without Rentals & Bookings: make the key on its API access page. See Getting started with Custom Fields.

ID verification is a status only: none, pending, verified, rejected or expired (past the document’s printed expiry date), with that expiry date. The API never returns an ID document, an image, a file link or a document id.

GET /api/v1/fields/orders/{orderId} #

An order’s answers per line (rental or regular product) and for the whole cart, the fees they added, the damage waiver line and the ID check’s status. recorded: false when the app has nothing for the order. name and synced_at come with a recorded order. The order is read from Shopify the first time; refresh=true reads it again.

curl -s "$API/fields/orders/5550001" -H "Authorization: Bearer $KEY"
# 200
{
  "order_id": 5550001, "recorded": true, "name": "#1042",
  "lines": [{"kind": "rental", "title": "Party tent 6×12 m", "variant_title": null, "quantity": 1,
             "answers": [{"label": "Event address", "value": "12 Example Road"}, {"label": "Flooring", "value": ["Wooden floor"]}],
             "fees": [{"label": "Flooring", "amount": "120.00"}]}],
  "cart_answers": [{"label": "How did you hear about us?", "value": "A friend"}],
  "waiver": {"state": "accepted", "amount": "35.00", "label": "Damage waiver"},
  "id_verification": {"mode": "checkout", "status": "verified", "expires_at": "2029-05-31"},
  "fees_total": "155.00", "currency": "USD", "synced_at": "2026-11-02T14:05:30+00:00"
}

GET /api/v1/fields/groups #

The field groups and their questions, in the order the storefront shows them: [{id, key, name, description, enabled, placement, scope, collections, products, tags, product_types, checkout_check, position, fields: [{id, key, type, label, help, placeholder, required, options, price, price_type, charge_per, conditions, min, max, maxlength, allowed_types, max_size, audience, default}]}].

curl -s "$API/fields/groups" -H "Authorization: Bearer $KEY"

GET /api/v1/fields/groups/{id} #

One group. 404 not_found otherwise.

GET /api/v1/fields/customers/{customerId}/id-verification #

A customer’s ID status: their verified document that is still valid, else their latest document. mode is the store’s ID check setting: off, optional, checkout or pickup.

curl -s "$API/fields/customers/555001/id-verification" -H "Authorization: Bearer $KEY"
# 200
{"customer_id": 555001, "mode": "checkout", "status": "verified", "expires_at": "2029-05-31"}

GET /api/v1/fields/definitions/export #

The question definitions as a file, answered as text inside JSON: {filename, content_type, byte_size, content}. Parameters: format (json, the default: lossless, to copy to another store or keep as a backup; or csv, to edit in a spreadsheet), group_ids (comma-separated; all when left out), include_settings (JSON only: the damage waiver, ID check, priced option and message settings; never the ID staff email).

curl -s "$API/fields/definitions/export?format=json" -H "Authorization: Bearer $KEY"
# 200
{"filename": "si-fields-definitions-2026-11-30.json", "content_type": "application/json", "byte_size": 272,
 "content": "{\n    \"format\": \"si-fields-definitions\",\n    \"version\": 1,\n    …\n    \"groups\": [ … ]\n}\n"}

POST /api/v1/fields/imports #

Uploads a definitions file. It is checked in the background: read the import until its status is preview (or failed), show the plan, then apply it.

Body fieldNotes
filename, contentRequired: the file’s name and its text (UTF-8, at most 5 MB).
formatauto (default: by the name and the first character), json or csv.
conflictA group whose key the store has already: skip (default: keep the store’s), replace (replace it and all its questions) or copy (import a copy with new keys).
import_disabledtrue turns imported groups off so you can check them first. Left out: off for a file from another store.
include_settingsAlso import the settings of a JSON file.
curl -s -X POST "$API/fields/imports" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d "{\"filename\": \"groups.json\", \"content\": $(jq -Rs . < groups.json), \"conflict\": \"skip\"}"
# 202
{"id": "01k9zt3m8q4f2c7x1v5n6b0d9e", "status": "queued", "format": "json", "filename": "groups.json", "…": "…"}

An import answers {id, status, format, filename, byte_size, conflict, import_disabled, include_settings, source_shop, source_version, plan_hash, counts: {groups, fields, create, replace, skip, copy, errors, warnings}, plan: [{key, name, action, new_key, fields, options, enabled, unmatched, messages}], settings, messages: [{severity, code, message, path, row, column, group_key, field_key}], result, created_at, created_by, applied_at, expires_at}. A message points at the problem with a JSON pointer (path) or a CSV row and column. status is queued, checking, preview, stale, failed, applying, applied, cancelled or expired (a preview expires after 24 hours).

GET /api/v1/fields/imports #

The latest imports, newest first. first: how many (20 by default, at most 50).

GET /api/v1/fields/imports/{id} #

One import.

POST /api/v1/fields/imports/{id}/apply #

Applies the plan the preview showed. Body: plan_hash (the preview’s). All or nothing, in the background (202): read the import until applied, failed or stale (the field groups changed after the check: recheck it). Refused while the preview has errors.

POST /api/v1/fields/imports/{id}/recheck #

Checks the import again (202).

POST /api/v1/fields/imports/{id}/cancel #

Drops an import that wasn’t applied.

POST /api/v1/fields/answers-exports #

Makes a CSV of the answers customers gave on orders in a date range. It holds what customers typed, so confirm must be true. Made in the background (202): read the export until status is ready.

Body fieldNotes
from, toRequired: days (Y-m-d, the store’s time zone), at most 366 days apart.
layoutwide (default: one row per order line, a column per question) or long (one row per answer).
group_keysOnly these field groups (a comma-separated list or an array).
include_cart_answersDefault true.
include_customer_idDefault false.
confirmRequired: true.
curl -s -X POST "$API/fields/answers-exports" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"from": "2026-11-01", "to": "2026-11-30", "layout": "wide", "confirm": true}'
# 202
{"id": "01m3bf9nyk0vktz9fre4s2tzcs", "status": "queued", "from": "2026-11-01", "to": "2026-11-30", "layout": "wide", "…": "…"}

GET /api/v1/fields/answers-exports #

The latest exports, newest first (first, at most 50).

GET /api/v1/fields/answers-exports/{id} #

One export: {id, status, from, to, layout, group_keys, include_cart_answers, include_customer_id, rows, byte_size, download_url, created_at, created_by, ready_at, expires_at, downloaded_at, error}. While it is ready, every read gives a fresh download_url that works for 15 minutes without a key. The file is deleted 24 hours after it is ready. Answers to file questions show the file name, never a link, and ID documents are never included.

Refusals of import and export (rentals_rest_…): 400 invalid, has_errors, file_too_large, too_many_rows, range_too_long, confirm (with data.params); 404 not_found; 409 busy (another import is open), plan_changed, stale, expired, already_applied, not_cancellable, not_ready; 429 too_many_exports (20 a day).

Quotes #

Needs the SI Request a Quote, Hide Price app. It works on a store without Rentals & Bookings: make the key on its API access page. If the store’s Quotes plan doesn’t include API access, these endpoints answer 403 plan_required. See Automation and integrations.

{ref} is the quote’s id, its number (Q-1042) or its public id. Statuses: new, in_review, draft, sent, changes_requested, accepted, ordered, expired, declined, rejected, cancelled. Money is in decimal strings. Line properties keep their keys exactly as you send them.

GET /api/v1/quotes #

Quotes, newest first. Paged. Parameters: status (comma-separated), search (a number, the customer’s name or email, a product title or SKU), source (storefront, cart, quick, admin, api, b2b_review, import), customer_id, updated_since (ISO 8601: only quotes changed since then).

curl -s "$API/quotes?status=sent,changes_requested&updated_since=2026-11-01T00:00:00Z" -H "Authorization: Bearer $KEY"
# 200, X-Total: 4
[{"id": 311, "number": "Q-1042", "public_id": "k3h9w2…", "status": "sent", "status_label": "Sent", "source": "storefront",
  "customer": {"id": 555001, "name": "Jordan Lee", "email": "[email protected]", "phone": null},
  "total": "1450.00", "currency": "USD", "owner_user_id": null, "expires_at": "2026-12-15T04:59:59+00:00", "viewed_at": null,
  "created_at": "2026-11-20T10:00:00+00:00", "updated_at": "2026-11-21T09:30:00+00:00",
  "order_id": null, "order_name": null, "draft_order_id": null, "tags": []}]

POST /api/v1/quotes #

Creates a quote. as: "draft" (the default): you prepare it and nothing is emailed until it is sent (send: true sends it at once). as: "request": as if the customer asked (status new; the request emails go out; the customer’s email is required).

Body fieldNotes
asdraft or request.
customer{id?, email, name?, phone?}; id is the Shopify customer.
lines[{variant_id?, product_id?, title, qty, list_price?, unit_price?, requested_price?, discount?: {type: "percent" or "fixed", value}, taxable?, requires_shipping?, properties?, note?}]. Leave out variant_id for a custom item (then unit_price is required).
send, customer_message, terms, expires_at, discount, shipping ({title, price}), tagsOptional. expires_at: Y-m-d (the end of that day, store time) or ISO 8601.
curl -s -X POST "$API/quotes" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{
  "as": "draft",
  "customer": {"email": "[email protected]", "name": "Jordan Lee"},
  "lines": [{"variant_id": 44001, "title": "Party tent 6×12 m", "qty": 1, "unit_price": "1350.00"}],
  "customer_message": "Here is the price for your December event.",
  "send": true
}'
# 201: the quote

A quote answers the summary above plus locale, market_country, shipping_address, reserve_inventory, company, invoice_url, the working, current, sent and request versions (each with its lines and totals), versions, events, notes, files and emails.

GET /api/v1/quotes/{ref} #

One quote.

PUT /api/v1/quotes/{ref} #

Changes the working version: lines (replaces every line), discount, shipping, tax_exempt, expires_at, customer_message, terms, customer, tags, shipping_address, payment_terms, reserve_inventory, company, currency, market_country. A sent version never changes: editing it starts the next version.

POST /api/v1/quotes/{ref}/{action} #

The actions, each answering the quote:

ActionBodyWhat it does
send–Sends the working version to the customer.
cancelreason?Withdraws a quote that isn’t accepted yet.
reopen–Reopens an expired, declined, rejected or cancelled quote.
convertmode: invoice (default) or orderMakes a Shopify draft order from the sent version: invoice gives the customer the payment link; order makes the order now, with payment pending. Answers {quote, draft_order: {id, invoice_url}, order}.
notesbodyAdds an internal note. Answers 201 with the note.
messagesbodySends the customer a message (emailed, and shown on their quote page). Answers 201 with the message.
curl -s -X POST "$API/quotes/Q-1042/convert" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"mode": "invoice"}'

GET /api/v1/quotes/{ref}/pdf #

A link to the quote’s PDF that works for 10 minutes: {url, version, expires_at}. version picks a version number; default the sent version.

Refusals: 400 invalid (data.field, data.errors), 409 invalid_status (the quote’s status doesn’t allow it, such as cancelling an accepted quote), 404 not_found, 403 plan_required, 502 shopify_error (Shopify refused the draft order).