# API Changelog

Every change to the Shoprocket REST API, newest first, tagged so you can scan for what affects you.

## How we describe changes

- Added New endpoint, field, or option. Safe to ignore.
- Improved Behavioural improvement that doesn't break existing requests.
- Fixed Bug fix. May change a response in edge cases.
- Deprecated Flagged for future removal. Still works for at least 6 months.
- Removed Only on a major version bump. Old version stays available for 12 months.

## 2026

30 October 2026 Added

- New error code `two_factor_required` (403): the caller must turn on two-factor sign-in on their account before this request can succeed. Nothing about their permissions is wrong, so send them to their account settings rather than reporting a missing permission.

29 October 2026 Added

- Mix-and-match boxes: a `bundle` whose contents the shopper chooses. Products gain `bundleChoice` (`null` on a fixed bundle and every other kind): the box's exact `size`, where its items come from (`source`: `category` or `list`, with `categoryId`), and the `items` to choose from, each with `sellableQuantity` (`null` when it never runs out). `POST /public/{pk}/cart/items` accepts `bundleSelections`: the chosen variant ids and how many of each, totalling exactly the size; it answers 422 otherwise. A cart line lists the mix in `components`, and two different mixes are two lines. The seller product writes accept `bundleChoice`.

29 October 2026 Added

- Per-product delivery methods. Products gain `deliveryMethods`: any of `shipping`, `pickup` and `local_delivery` (all three by default), accepted on product create and update. Cart lines and the cart gain `deliveryMethods` too; the cart's is what every shipped line allows, and is what checkout offers. `GET /cart/shipping-options` leaves out what a line rules out (no shipping rates when a product is pickup only, an empty `pickup` list when one cannot be collected), `PATCH /cart` answers `422` for a selection a line rules out, and checkout answers `409` with `shipping_changed` for one, or with the new `delivery_unavailable` when the lines share no method at all (a line has to be removed).
- Pickup dates. A pickup location can ask the shopper which day they collect. Each entry in `GET /cart/shipping-options`'s `pickup` list gains `availableDates`: the days that can be booked (`YYYY-MM-DD`, counted in the store's timezone), or `null` when the location does not ask. `PATCH /cart` accepts `pickupDate` (with `pickupLocationId`, or alone for the location already chosen) and answers `422` for a day not on offer; the cart gains `pickupDate`. Checkout answers `409` with the new `pickup_date_required` when the location asks and the cart has no day, or a day that can no longer be booked. Orders gain `pickup.date` (and order summaries `pickupDate`), and the orders list takes `pickupFrom`, `pickupTo` and `sort=pickupDate`. Pickup locations gain `pickupDates`: `enabled`, `weekdays`, `minNoticeDays`, `maxDaysAhead` and `closedDates`.

24 October 2026 Added

- A shop icon, for browser tabs, bookmarks and home screens. The store object gains `icon` (a media object, or `null`); `PUT /stores/{storeId}` accepts `iconMediaId`. `GET /public/{pk}` and the hosted shop bootstrap gain `icon`: use it as your favicon. With none set, the hosted shop uses a roughly square `logo`, else the shop's first letter on its brand colour.

23 October 2026 Added

- A second logo, for dark backgrounds. The store object gains `logoDark` (a media object, or `null`) and `logoIsLight` (`true` when the main logo is mostly white); `PUT /stores/{storeId}` accepts `logoDarkMediaId` and `logoIsLight`. `GET /public/{pk}` and the hosted shop bootstrap gain `logoDark`: show it where your logo sits on a dark background, and `logo` everywhere else.

21 October 2026 Added

- `GET /public/{pk}` and the hosted shop bootstrap gain `unclaimed`: `true` for a test store made with `POST /agent-stores` that no one has claimed yet. Such a store sends no email, so do not tell a shopper a confirmation is on its way; the widget's order confirmation drops its "confirmation email has been sent" line. It turns `false` as soon as the store is claimed.

21 October 2026 Added

- Test stores for AI code builders. `POST /agent-stores` needs no authentication: it creates a store in test mode for someone with no Shoprocket account and answers with its `publishableKey`, a `secretKey` scoped to the catalogue and shipping methods (it expires with the store and is revoked when the store is claimed), a `claimUrl` for the person, `shopUrl` and `expiresAt` (7 days). Signing up at the claim URL makes the store theirs with everything in it. Until then the store takes test checkouts only, sends no email, holds at most 25 products and 50 MB of images, and every other seller call answers `403` with the new error code `claim_required` (so can `POST /stores/{storeId}/shipping-methods` for a `carrier` method). Creating is limited to 10 per IP address per day (`429`).

21 October 2026 Added

- Customer tags drive rules. A discount takes `customerTagIds` (customer tag ids, `ctag_...`): empty means everyone, otherwise only a signed-in customer carrying one of those tags can use it. It is checked on the cart and at checkout against the `X-Customer-Token` session, never against an email typed at checkout, so a guest never qualifies; a refused code reads `discountError.reason` `customer_tags`. On `PUT /stores/{storeId}/discounts/{id}` an absent `customerTagIds` leaves it unchanged. `exTaxDisplayTagIds` on `GET` and `PUT /stores/{storeId}/tax-settings` names the tags whose signed-in customers see prices without tax by default with the `toggle` price display; `taxDisplay.customerDefaults` on `GET /public/{pk}` says such a rule exists, and `GET /public/{pk}/customer/me` returns the signed-in customer's own `taxView` (`ex` or `null`). `GET /stores/{storeId}/customer-tags` lists each tag's `usedBy` rules; renaming a tag keeps every rule, and merging or deleting one moves or removes it from every rule.

21 October 2026 Added

- Widget: `Shoprocket.cart.add({ variantId, quantity })` adds to the cart from your own button the way the widget's own buttons do. The shopper sees the "Added to cart" card (or the error card when the store refuses the add), and `shoprocket:product:added` and the `shoprocket:add_to_cart` analytics event fire. It returns a promise for the new cart and rejects with an `Error` whose `message` says why. Use it in place of `getSdk().cart.addItem()` followed by `cart.open()`, which adds silently.
- `POST /public/{pk}/cart/items` now refuses a variant that is sold out, or has fewer left than asked for, with a `409` (`conflict`) whose message says so, instead of adding a line checkout would refuse. A variant whose policy is `continue` (backorders) is never refused.

21 October 2026 Added

- Prices with and without tax. `priceDisplay` on `GET` and `PUT /stores/{storeId}/tax-settings` is one of `single` (the default: prices as entered, the only behaviour before), `both` (the price with and without tax together) or `toggle` (the shopper switches). `GET /public/{pk}` carries it as `taxDisplay`: `null` for a single-figure store, otherwise the `mode`, the store's `pricing`, a `label` (`vat` or `tax`) and its home country's `rates` to work the other figure out with. Each cart line now carries its product's `taxClass`, so a front end can print the line with or without tax at the right rate. All of it is display only: what checkout charges does not change.

21 October 2026 Added

- A sign-up pop-up now says where its picture goes: `popup.imageLayout` on each promo in `GET /public/{pk}`, and `imageLayout` on the seller's promo read and write, one of `top` (a banner above the words, the default and the only look before), `side` (beside the words; above them on a phone) or `background` (behind everything, tinted dark so light text reads). It means nothing while `imageUrl` is `null`.
- Every product read now says what a listing page prints, so `GET /public/{pk}/products` needs no second call per row: `priceMin` and `priceMax` (the cheapest and dearest variant price in minor units, both `null` when `priceOnRequest` is `true` or there is no price), `categories` (each category's `id`, `name` and `slug`, translated for `?locale=`; public reads list only categories shoppers can browse, and `categoryIds` stays as it was), and `attributes` on list rows too, not only on the single product (visible ones only, as before).

19 October 2026 Added Changed

- A new first-party App, `quickbooks` (QuickBooks Online), in `GET /stores/{storeId}/apps` and `PUT /stores/{storeId}/apps/{appId}`, with `attention: true` on the same terms as Xero's. It is no longer a coming-soon app, so `quickbooks` leaves `POST /stores/{storeId}/apps/waitlist/{appId}`.

19 October 2026 Added

- `POST /public/{pk}/cart/items` takes an optional `sourceUrl`: the page the shopper is adding from, usually `window.location.href`. Every cart line now returns it as `sourceUrl` (`null` when none was sent), so a cart can link a line back to the page it came from. Only http(s) URLs of up to 2048 characters are kept; anything else is ignored, never refused. A repeat add of the same line keeps the page it first came from.

18 October 2026 Added

- Four new first-party Apps in `GET /stores/{storeId}/apps` and `PUT /stores/{storeId}/apps/{appId}`: `translations` (extra shop languages), `multi_currency` (extra shopper currencies), `redirects` and `b2b` (e-invoice settings). Each is in use, and so cannot be removed, while the store has another language switched on, an extra currency, any redirect, or e-invoice files attached to its invoice emails. Nothing about the languages, currencies, redirects or invoice endpoints themselves changed.

17 October 2026 Added

- `GET /stores/{storeId}/verifactu` also returns `signedRecords` (how many billing records carry the seller's XAdES signature) and `certificate`: the qualified certificate that signs a Spanish store's Veri\*Factu records (`holder`, `issuer`, `nif`, `validFrom`, `validTo`, `fingerprint`, `uploadedAt`), or `null`. The certificate itself is uploaded from the dashboard only; its private key is never returned by any endpoint.

17 October 2026 Added

- E-invoice files for business orders. When the buyer gave a VAT number and the store is in the EU, each invoice and credit note also gets two EN 16931 XML files: XRechnung 3.0 (UBL) and CII (the XML a ZUGFeRD / Factur-X PDF embeds). Invoices carry a new `einvoice` (`formats`, plus `blocked` naming what was missing when none could be made), and the order status page's `documents` carry `einvoice` download links. The files are served from `/public/invoices/{token}/einvoice/{format}` with the document's own token.

17 October 2026 Added

- The public store config's `analytics.pixels` gains `gtmContainerId`: the seller's Google Tag Manager web container (`GTM-...`), or `null`. The widget loads it under the same consent rule as the pixels and pushes `view_item`, `add_to_cart`, `begin_checkout` and `purchase` to `dataLayer` in GA4's ecommerce shape (value in major units, `transaction_id` = the order id).
- `GET` and `PUT /stores/{storeId}/tracking-settings` take and return `gtmContainerId` (`null` clears it).

17 October 2026 Added

- Invoices carry a new `documentType`: `invoice`, or `receipt` for a store whose business address is in Portugal. A receipt says it is not an invoice (Portuguese invoices must come from AT-certified software), and its `number` is the order's own (`R-1042`, `R-1042-1` for a refund), taken from no invoice series. The order status page's `documents` carry the same `documentType` when it is `receipt`.
- `pixelContext` on `POST /public/{pk}/cart/checkout` also takes `gaClientId` and `gaSessionId` (from the `_ga` cookies) and TikTok's `ttp` / `ttclid`. A store with the GA4 Measurement Protocol or the TikTok Events API set up sends the paid order from the server too, deduplicated with the browser on the order id. GA4 needs `gaClientId`: without it nothing is sent to GA4.
- `GET /stores/{storeId}/tracking-settings` returns `ga4ApiSecretSet`, `ga4ApiSecretLast4`, `ga4ServerEvents`, `tiktokEventsTokenSet`, `tiktokEventsTokenLast4`, `tiktokTestEventCode` and `tiktokServerEvents`. Every `*ServerEvents` object gains `lastErrorWillRetry`: true when the last failure is temporary and will be retried. `PUT` takes a write-only `ga4ApiSecret` and `tiktokEventsToken` (`null` removes either) and `tiktokTestEventCode`.

17 October 2026 Added

- `POST /public/{pk}/cart/checkout` takes an optional `pixelContext`: the shopper's ad consent (`granted`, `denied` or `unknown`) and Meta's `fbp` / `fbc` cookies. The widget sends it, and a store with the Meta Conversions API set up then sends the paid order to its pixel from the server too, deduplicated with the browser's Purchase on the order id. A checkout without it gets no server-side ad events.
- `GET /stores/{storeId}/tracking-settings` returns `metaCapiTokenSet`, `metaCapiTokenLast4`, `metaTestEventCode` and `metaServerEvents` (the last event Meta accepted or refused). `PUT` takes a write-only `metaCapiToken` (`null` removes it) and `metaTestEventCode`.
- `GET /stores/{storeId}/invoice-settings` returns `regime`: `standard`, `pt_receipt` or `es_verifactu`. The new `GET /stores/{storeId}/verifactu` reports a Spanish store's Veri\*Factu billing records (`issuerNif`, `records`, `events`, `lastRecordAt`, `lastHuella`).

17 October 2026 Added Changed Removed

- The new `POST /stores/{storeId}/reviews/{id}/edit-requests` emails the reviewer of a customer's review (`organic` or `requested`) a link to edit it themselves, stars included; their edit goes back to `pending`. Once a day per review (429 with `Retry-After` after that), within the store's 50 review requests a day; 422 for an imported or manual review, or one with no email.
- A text edit through `PUT /stores/{storeId}/reviews/{id}` on a customer's review now emails the reviewer what changed, at most once a day, with a link that restores their own words; after a restore the text is `locked`. A seller review carries `editNotice` (`none`, `queued` or `sent`), `editNoticeSentAt` and `restoredAt`.
- `POST /stores/{storeId}/reviews` is gone: a store can no longer write a review. The new `POST /stores/{storeId}/reviews/requests` takes `{ productId, name, email }` and emails that person a link to review the product themselves. Their review arrives with the new `source` value `requested`, and is a verified purchase only if they bought the product with that email. A store can send 50 requests a day; past that the call is a 429 with a `Retry-After` header.
- `PUT /stores/{storeId}/reviews/{id}` no longer changes a review's `rating` (a different value is a 422). On a review a customer wrote (`organic` or `requested`) the author is fixed too, and `title` plus `content` may differ from what the customer wrote by at most 30 characters in total, counted from their original text; past that, 422. Imported and manual reviews stay editable apart from the rating. A reviews import no longer changes a review's rating, and reports a row that matches a customer's review as an error instead of rewriting it.
- A seller review now carries `textEdits` (`free`, `typos` or `locked`), `originalTitle`, `originalContent` and `sellerEditedAt`. Public reviews can carry `source` `requested`; the widget and hosted shop label `imported` and `manual` reviews "Imported".

17 October 2026 Added

- The reverse charge for EU business buyers. `GET /stores/{storeId}/tax-settings` and its update carry `reverseChargeEnabled`. When it is on, a buyer VAT number sent to `PATCH /cart` is checked with VIES (or HMRC for a UK number), and the cart returns `taxNumberCheck` (`status`: `valid`, `invalid`, `unavailable` or `unchecked`, and `reverseCharge`).
- Checkout refuses a number the validator rejected with the new rejection `tax_number_invalid` (`409`). Orders gain the tax status `reverse_charge` and a `taxNumberCheck` object (status, when it was checked, the validator's consultation number, and the registered name and address).

17 October 2026 Added

- `GET /stores/{storeId}/invoice-settings` and `PUT /stores/{storeId}/invoice-settings` read and set a store's invoice and credit note prefixes and next numbers, its legal footer text and its small-business VAT exemption. A next number is refused with `409` once that series has issued a document, so the series stays gapless.
- An order's tax lines carry `taxableAmount`, the net amount each rate was charged on, and its lines carry `taxRateMilliPercent`, the rate each was taxed at. Both are `null` on orders placed before they were recorded. Creating or editing an order accepts an optional `taxableAmount` on each tax line.

17 October 2026 Added Changed

- A new first-party App, `xero`, in `GET /stores/{storeId}/apps` and `PUT /stores/{storeId}/apps/{appId}`. Each app in the list may now carry `attention: true` when something in it needs the seller (for Xero: the connection was refused, or a day's sales could not post).
- The coming-soon app `accounting` is now `quickbooks` in `POST /stores/{storeId}/apps/waitlist/{appId}` and the page's `waitlist`.

17 October 2026 Added

- Reviews can carry the reviewer's photos and a short video. Each review in `GET /public/{pk}/products/{idOrSlug}/reviews` has a `media` array (finished files only, in the reviewer's order), and `meta.media` lists the newest photos across all of the product's approved reviews, each with its `reviewId`, for a photo strip. A review in `GET /stores/{storeId}/reviews` has `media` too, including a video still being processed, and `PUT /stores/{storeId}/reviews/{id}` takes `mediaIds`: the files to keep. A seller can remove a reviewer's photo, never add one.

16 October 2026 Added

- `POST /stores/{storeId}/imports` and `POST /stores/{storeId}/imports/from-file-url` also accept an Excel or OpenDocument spreadsheet (`.xlsx`, `.xlsm`, `.xls`, `.ods`). The first sheet with data is read as the rows a CSV would give: dates come through as `YYYY-MM-DD`, numbers in full, and a zero-padded number format keeps its leading zeros.

16 October 2026 Added Changed

- A review's `authorEmail` can now be `null`: an imported review whose source had no email (an Etsy export carries none) is stored without one. Reviews you create or submit still require it.
- `POST /stores/{storeId}/imports` also accepts a JSON file (an array of objects, or an object holding one, like Etsy's `{ count, results }`), read as the same rows a CSV would give. The new `POST /stores/{storeId}/imports/from-file-url` takes `{ type, url }`, fetches a public CSV or JSON file (10MB, 15 seconds) and creates the same pending import an upload does.

15 October 2026 Added

- Shoppers can sign in from any page with a cart: the floating cart button has a person icon that opens the account drawer, showing the shopper's initials once signed in. Checkout settings carry it as `cartButtonAccount` (default `true`). Your own link can open the same drawer with `data-shoprocket-account-toggle`, which also gets `data-signed-in` while a shopper is signed in, or with `href="#!/login"`, which now works the same as `#!/account`.

15 October 2026 Improved

- The reference now describes every query parameter and every request and response field: what it is, its unit (money is always an integer in the currency's minor unit), its format, its allowed values and what `null` means. Documentation only: the API behaves as before.

15 October 2026 Added

- The catalogue embed has attribute filters: one dropdown per attribute or variant option, with counts, the same filters as the hosted shop. They are on by default; turn them off with the `filters:attributes` feature, and limit them to some attributes with `data-filter-attributes="colour,size"`. A filtered view keeps its `attr` in the page URL, so it can be shared. The JavaScript SDK's `products.list()` takes `attr` and `facets`.

15 October 2026 Changed

- The reference now explains every filter on `GET /public/{pk}/products`. `attr` filters by attributes and variant options, including your own: `attr=colour:red|blue,brand:fender`, a range as `length:100..250`. Values within one attribute match any; separate attributes must all match. Ask once with `facets=true` for every attribute's `handle` and its values' `slug` s. Documentation only: the API behaves as before.

14 October 2026 Changed

- The API is on every plan, Starter included: secret keys from `/stores/{storeId}/api-keys` work whatever the store pays for. Webhooks stay on Growth and up, so `POST /stores/{storeId}/webhooks` now answers `403` `plan_limit` on a plan without them. Endpoints a store already has keep delivering and stay editable.

14 October 2026 Added Changed

- One email look per store. `PUT /stores/{storeId}` takes `emailLook`: a `theme` (`shop` for the store's own look, or a theme id) plus optional `background`, `card`, `text`, `button`, `buttonText` and `link` colours, `corners` (`square`, `soft`, `round`) and a `fontPairing`. Every email with no look of its own wears it: campaigns, automated emails and the order and account emails. The store object returns it; `null` means the store's own look, including its brand colour and font pairing.
- A campaign's `theme` also accepts `shop`, and campaigns take a `look` of their own style changes in the same shape (without `theme`). A `null` theme now follows the store's default email look rather than its shop theme.

13 October 2026 Added Changed

- Local delivery. A pickup location on `/stores/{storeId}/pickup-locations` carries `deliveryAreas` (the whole list on create and update; an area left out is removed, an `id` keeps one), `offersPickup` (false for a base you only deliver from), and read-only `geoStatus` and `point`: where its address is on the map, which a radius area measures from. An area is a `radius` in `km` or `mi` (at most 160 km / 100 mi, never across a border) or a `postcodes` list, with a `price`, an optional `minOrder`, `orderValueBands`, `distanceBands` and `instructions`. An update that leaves a field out now leaves it alone (it used to reset the price, status and ready time).
- For storefronts: `GET /public/{pk}/cart/shipping-options` offers one `ldar_` option, the cheapest area covering the address, beside the other rates; choose it with `shippingRateId` on `PATCH /public/{pk}/cart`. `GET /public/{pk}/places/details` also returns `location` and a signed `geo` token: send it back as `shippingAddress.geo` so a radius area knows where the address is without a second lookup.
- Orders carry `localDelivery` (the area, its location, the distance, and when it went out and was delivered). `POST /stores/{storeId}/orders/{id}/local-delivery` with `step` `out_for_delivery` or `delivered`; delivered fulfils every unit still owed.

- Shipping zones narrowed by postcode. Each of a zone's `regions` on `/stores/{storeId}/shipping-methods` (the reads, and the input on create and update) carries `postcodes`: a list such as `["L", "CH41-CH49", "CH6*"]`, or null for every postcode. A plain entry matches where the postcode's parts break (`L1` matches L1 8JQ, not L10 1AA), a range takes district numbers in one area, and a trailing `*` is a plain prefix. A region with a list is more specific than a state or a whole country. Entries are stored upper case; an unrecognised one is refused with 422.
- `GET /public/{pk}/cart/shipping-options` also answers `addressRequired: true` when nothing is offered, the shipping address has no postcode yet, and a zone for its country is narrowed by postcode: ask for the postcode rather than saying the shop does not deliver there.

13 October 2026 Added

- A draft order's payment link. `POST /stores/{storeId}/orders/{id}/payment-link/send` emails the draft's customer the draft (worded as a quote when it was made from a quote request, as an order otherwise) with a link that opens checkout for exactly that draft, at its own prices, shipping and discount; `POST /stores/{storeId}/orders/{id}/payment-link` returns the same link without emailing. Both answer `{ url, expiresAt, sentAt, sentTo }`, restart the link's 30 days and keep its URL. Paying the link completes the draft into an ordinary order (a real number, `order.paid`, the confirmation email). Orders carry `quoteSentAt`, `quoteSentTo` and `quoteExpiresAt`: when the payment link was last emailed, to whom, and when it stops working. While a customer's payment for a draft is moving, `PUT` and `DELETE` on it answer `409`.
- For storefronts: `POST /public/{pk}/pay-links/{token}/open` answers a payment link's `state` (`payable`, `paid`, `expired` or `gone`) and, when payable, the `cartToken` of the draft's cart. That cart reads with a `quote` field; its lines, codes, gift card and delivery cannot be changed (they answer `409`), and checking it out answers `quote_unavailable` once the draft changed or can no longer be paid. Pay it with `POST /public/{pk}/orders/{orderId}/pay` as usual. The widget opens `#!/pay/{token}` itself.

12 October 2026 Added

- An embed's own words in every language. `GET /public/{pk}/embeds/{id}/config` carries `words`: locale to configuration key to words, for each language the shop sells in, when the embed has text of its own (`buttonText`, `optionsButtonText`, `viewButtonText`, `label`, `headline`, `subline`, `placeholder`). `configuration` still holds them in the shop's default language. Show the shopper's language's words; a language missing a key has no translation yet, so show your own default for that key rather than the default language's words. Absent for a shop that sells in one language. The seller read `GET /stores/{storeId}/embeds/{id}` carries the same `words`.
- A catalog embed's configuration takes `buttonText`: the Add to cart words on its cards and on the product pages it opens, null for the store's wording.

12 October 2026 Added Changed

- Cancellation emails, and cancel with a refund in one call. `PUT /stores/{storeId}/orders/{id}` takes three optional fields, read only on the save that moves the order into `cancelled`: `notifyCustomer` (default `false`) emails the shopper that their order was cancelled, with the reason in plain words; `refund` sends back everything still refundable before the cancel is written, and the cancellation email carries it (no separate refund notice); `acknowledgeManualRefund` accepts that part of it has to be sent by hand. A refund that cannot go ahead answers `422` with the refund route's `rejection` body and leaves the order untouched. A cancel that refunded also fires `order.refunded`. Leaving all three out keeps today's behaviour.
- Review reply emails. `PUT /stores/{storeId}/reviews/{id}` takes an optional `notifyReviewer` (default `false`). Set it to `true` with a reply and the reviewer is emailed once, in the language they reviewed in, with their review and your reply, when the review is published. Editing the reply later never emails again. A review now reads `replyNotice` (`none`, `queued` or `sent`) and `replyNoticeSentAt`. A public review's `reply` is now translated with the review into the shop language the request asks for.
- Refund emails. `POST /stores/{storeId}/orders/{id}/refunds` takes an optional `notifyCustomer` (default `false`). Set it to `true` and the shopper is emailed a refund notice with the amount, where it went and the credit note, once the money has actually gone back: at once for a tender that refunds on the spot, or when the gateway settles a refund that started `pending`. A failed refund sends nothing. Leaving it out keeps today's behaviour: no email.
- Quotes and enquiries. A product read (`GET /public/{pk}/products`, the single product, the hosted shop's reads and the cart's suggestions) carries `purchaseMode` (`buy` or `enquire`) and `priceOnRequest`. When `priceOnRequest` is true the price is hidden from shoppers: every variant's `price` and `compareAtPrice` are `null`, `priceBreaks` and `subscriptionPlans` are empty and a bundle's part prices are `null`. Show your own words (we show "Price on request") rather than a number. A product set to `enquire` cannot be added to the cart unless the store is in quote mode (422).
- A price filter (`priceMin`/`priceMax`) leaves products with a hidden price out, a price sort puts them last, and the list's `meta.priceMin`/`meta.priceMax` ignore them.
- New: `POST /public/{pk}/quote-requests` sends the store a quote request, one product's enquiry or a whole cart, with `name`, `email`, optional `phone` and `message`, and `lines` of `{ variantId, quantity }`. It answers `{ ok: true }`; the store emails the shopper a copy.
- The store bootstrap (`GET /public/{pk}` and `GET /public/shops/{slug}`) carries `quotes: { mode, hidePrices }`. In quote mode, `POST /public/{pk}/cart/checkout` refuses with the new rejection `quote_only`: send the cart as a quote request instead. Cart lines carry `priceOnRequest`, true for a line whose price is hidden (held at 0).
- Two new widget events: `shoprocket:enquire` opens the enquiry form (detail `{ product, variantId?, quantity? }`, or `{ cart: true }` for the cart), and `shoprocket:quote:requested` fires once a request is sent.
- The store read's `apps` can hold `facebook_instagram`: the Facebook & Instagram app, which connects the store's Meta business and keeps a Meta catalogue fed from the store's product feed. Like every app it only decides which screens the dashboard shows.
- Seller API: `GET /stores/{storeId}/contact-messages` takes `?kind=quote_request` and each message carries `kind`, `lines`, `currencyCode` and `draft`; `POST /stores/{storeId}/contact-messages/{id}/quote` makes the draft order (once per request); and `GET/PUT /stores/{storeId}/quote-settings` reads and sets quote mode.
- The words Shoprocket writes for a banner or pop-up when the seller leaves them blank (a sale bar's `text`, a pop-up's headline, its `popup.body` and `popup.offer`) now read in the shopper's language instead of always English, with money and percentages formatted that language's way. Words the seller typed are sent exactly as written.
- The store bootstrap (`GET /public/{pk}`) takes `?locale=`: its `promos` read in that language when the store sells in it, else in the store's own language. Each item also carries `words`, the same derived words keyed by every other language the store sells in (plus English), so a client that settles on a language after the bootstrap arrives can show its own without a second request.
- A pop-up sign-up (`POST /public/{pk}/promos/{id}/signup`) takes `?locale=` too: the `offer` it returns and the code email it sends are in that language.
- Shipping labels are now bought on the store's own ShipStation account and paid from the seller's ShipStation balance, never the Shoprocket bill. An order's `shippingLabels` entries no longer carry `billedAmount`, `billedCurrency`, `billingStatus` or `stripeInvoiceId`: a label's `cost` and `currency` are what ShipStation charged.
- ShipStation is an App: `shipstation` is a new `appId` for `PUT /stores/{storeId}/apps/{appId}` and in `GET /stores/{storeId}/apps`, in use (so Remove answers 409) while the store has a ShipStation account. It is no longer a coming-soon id, so `POST /stores/{storeId}/apps/waitlist/{appId}` refuses it (422) and the waitlist never lists it.

11 October 2026 Added Changed

- An embed's `fontsHref` (`GET /public/{pk}/embeds/{id}/config`) is now a stylesheet on our own font host, the folder beside the widget loader (for example `https://widget.shoprocket.dev/fonts/cormorant-garamond_jost.css`), not Google Fonts, and every embed loads it. The font files sit beside it, so a page with our widget makes no request to Google for its typefaces. A page with a Content Security Policy must allow the widget's own host in `style-src` and `font-src` as well as `script-src`; if it does not, the products still show, in the page's fallback font.
- The store bootstrap's `branding` (`GET /public/{pk}` and `GET /public/shops/{slug}`) carries the same stylesheet as `fontsHref`, or null for a theme of system faces, so a page with no embed on it (the floating cart alone) still draws in the store's fonts.
- The embed setting `themeFonts` is retired: every embed draws in the store design's fonts. A stored embed that still has it parses as before, and the key is left out of every embed read. A font override of `inherit` (the store's `font-body` and the other font tokens) is no longer accepted, and the store reads leave it out.
- The store read carries `apps`: the first-party Apps the seller has switched on in the dashboard (`reviews`, `gift_cards`, `developer_tools` and so on), or null before the store has been worked out. It only decides which screens the dashboard shows. Every endpoint keeps working whatever it holds, so an integration never needs to check it.

9 October 2026 Added Changed

- A store whose paid subscription has ended (the card failed after every retry, or the seller cancelled and the period ran out) now waits for a plan the same way a lapsed trial does: `402` `plan_required` on that store's writes and the shop paused for shoppers. The store read and the billing read carry `planRequiredReason` beside `planRequiredAt`: `trial_ended`, `payment_failed` or `cancelled`, and null when the store is not waiting for a plan.
- The store read carries `paymentFailing`, true while the store's last subscription payment failed and is being retried. The store keeps working meanwhile.
- The embed list (`GET /stores/{storeId}/embeds`) carries `siteCode`: the widget loader on its own, the one line a seller adds to their site's header so the cart, live chat and analytics run on every page. Every embed's own `embedCode` still starts with the same line, and a second copy of it on one page does nothing, so a snippet pasted somewhere without the header line keeps working.
- `GET /public/{pk}` accepts `surface=preview`, which the dashboard's own embed preview sends. It is answered like `website`. Embeds added to a page after the widget has loaded now mount on their own, with no call to make.

8 October 2026 Added

- A new error code, `plan_required`, with status `402`. When a store's free trial ends without a plan, the seller API refuses that store's writes with it until a plan is chosen in Settings > Billing. Reads keep working, and so do the billing routes. The store read (`GET /stores/{storeId}` and the list) carries `planRequiredAt`, set while the store is waiting for a plan, and the billing read carries it too.
- While a store is waiting for a plan its shop behaves exactly as a paused one: `GET /public/{pk}` carries `paused` with the platform's notice, adding to the cart and checking out return `409`, `payment-methods` reports `checkoutDisabled`, and the product feeds stop. Nothing new to handle if you already handle a pause.

7 October 2026 Added

- A new plan feature, `chat-ai`, in a store's `features`: the live chat's AI assistant, on Business and Plus. Live chat itself (`storefront-chat`) is now listed on every plan, which is what it already was.
- A new team permission, `manage-affiliates`, for the seller's affiliate programme: adding affiliates, changing their rates and rejecting a commission. It appears wherever permissions are listed (team members and API key scopes). The affiliate programme's own endpoints are dashboard-only for now.
- Live chat follows where the seller wants it. `GET /public/{pk}` takes an optional `surface`: `shop` on the hosted Shoprocket shop, `website` (the default) everywhere else. `chat` is `null` for a surface where the seller has switched chat off, so a custom storefront on the seller's own site sends nothing and gets the website answer. `chat` also carries `side` (`"right"` or `"left"`) and `offsetY` (px): the chat draws itself there, and a floating button of your own in that bottom corner should sit above it. Keep the chat off checkout; the Shoprocket widget hides it there.
- The floating cart button's distance from the page's edges. The checkout settings (`GET` and `PUT /stores/{storeId}/checkout-settings`) and the `checkout` object on `GET /public/{pk}` carry `cartButtonOffsetX` (px from its side) and `cartButtonOffsetY` (px from its top or bottom edge), whole numbers from 0 to 200, 20 by default. Neither applies at the two middle positions, where the button is a tab on the page edge. A cart placed with `data-shoprocket="cart"` takes the same as `data-button-offset-x` and `data-button-offset-y`.

6 October 2026 Added Improved

- The seller API's store read (`GET /stores/{storeId}` and the list) carries `chatEnabled`, the seller's switch for live chat on their shop.
- Live chat. `GET /public/{pk}` and the hosted shop's bootstrap carry `chat`: `{ scriptUrl, site }` when the store has live chat switched on, or `null`. Load `scriptUrl` as it is, as a `<script type="module">`, once per page; never build it yourself. The Shoprocket widget already does this, on the hosted shop and on an embed alike. For a signed-in shopper, `GET /public/{pk}/customer/chat-identity` (with the customer session token) returns `{ it, name, email }`, or `null` when the store's chat is not ready; the widget hands it to the chat before the script loads, so the seller sees who is writing. `it` is opaque and lives five minutes, so it is asked for per page.
- A catalog read's `featured` sort (the default) is now always the seller's own order: products the seller has placed first, in their places, then the rest newest first. With no category the places are store-wide; with a category they are that category's. It no longer goes through the store's or the category's `productSort` setting, which defaulted to best selling; that field is still accepted on `PUT /stores/{storeId}/product-order` and on categories but no longer changes any listing.
- `GET /stores/{storeId}/products` takes `category`: a `cat_` id, listing only the products in that category. The bulk endpoint's `filter` takes it too, so "select all" reaches the same products.
- EU product safety (GPSR). The single-product read (`GET /public/{pk}/products/{idOrSlug}`) carries `productSafety`: `manufacturer` and `euResponsiblePerson` (each `{ name, address, contact }` or `null`) and `warnings` (plain text in the requested language), already resolved against the store's defaults, or `null` when there is nothing to show. A front end selling into the EU should render it on the product page. Products also carry the raw `safetyManufacturer`, `safetyEuRep` and `safetyWarnings`, which the seller API's product create and update accept; the store read and update carry the defaults as `safetyManufacturer` and `safetyEuRep`. The Google feed sends them as `g:product_detail` rows under a "Product safety" section, and UCP product payloads as `metadata.product_safety`.
- `GET /public/{pk}` and the hosted shop's bootstrap now carry `labels`: the store's own words for a few storefront labels (add to cart, view product, checkout, view cart, sold out), by language, as `{ "de": { "add_to_cart": "Kaufen" } }`. A language with no entry uses the standard copy. The seller API's store read and update (`GET`/`PUT /stores/{storeId}`) carry the default language's set as `storefrontLabels`. This replaces the single `addToCartLabel` on the checkout settings, which was added earlier today and is gone.
- Checkout settings carry `billingAddressMode`: `full` (the default) or `minimal`, where the billing address asks only country, region and postcode. A delivery address is never affected. Checkout refuses with `billing_country_required` a payable order that has neither a delivery address nor a billing country, and with `company_required` or `address_line2_required` a blank field the seller marked required.
- `PATCH /public/{pk}/customer/me` lets a signed-in shopper change their own first name, last name and phone. An absent field is left as it is and an empty string clears it; the email is the sign-in identity and cannot be changed. The change sends `customer.updated`, as a seller edit does. `GET /public/{pk}/customer/me` now answers the `phone` too.
- `GET` and `PUT /stores/{storeId}/product-order`: the shop's own "All products" sort (`manual`, `best-selling`, `name`, `-name`, `price`, `-price` or `newest`) and, for `manual`, the order itself as a list of product ids. It is what a category's `productSort` and `productOrder` are for that category, for the whole shop.
- A storefront products read with no category and the default `featured` sort now follows that setting. It was always best-selling. A store migrated from v2 lists in its v2 hand order.
- A category's `productOrder` may now list up to 20,000 products (was 5,000).

5 October 2026 Added Fixed Improved

- The reference's servers list now names only the API that served it, so its built-in client always calls the same API as the base URLs on these pages. It used to list a second server that answered every `/v3` call with an error.
- `POST /public/{pk}/test-gateway/{paymentId}` is now in the reference: how a test-mode checkout approves or declines its payment. See [Test mode](https://v3.shoprocket.io/docs/test-mode.md).
- Storefront product reads (catalogue, product page, cart suggestions) now answer `costPrice` as `null` on every variant. A seller's cost is their margin, and it was readable with the publishable key. The seller API and webhooks still carry it.
- The `/openapi.json` document is about a fifth of its old size and is cached at the edge.
- The widget's JavaScript API and events are documented: see [Widget JavaScript API](https://v3.shoprocket.io/docs/javascript.md). Three fixes came with it. `shoprocket:cart:updated` now fires whenever the cart changes, with the cart in `detail.cart`; it never fired before. `shoprocket:purchase` for an order placed with an offline method, a gift card or at no cost now carries its order id, value and currency (it went out with none, and a value of 0). And product, cart-line and order-line prices are no longer reported as 0 in `view_item`, `select_item`, `view_item_list`, `remove_from_cart` and the `purchase` lines.

4 October 2026 Added

- Subscription webhooks: `subscription.created`, `subscription.renewed`, `subscription.renewal_failed`, `subscription.paused`, `subscription.resumed`, `subscription.cancelled` and `subscription.expired`. Before this, nothing told you that a renewal failed or a contract ended.
- Licence key webhooks, one per state change: `license_key.assigned`, `license_key.suspended`, `license_key.reinstated`, `license_key.expired` and `license_key.revoked`. A subscription's keys follow it: suspended while it is past due or paused, expired once it ends.
- `GET /stores/{storeId}/licence-keys`: find any key in the store by the key itself, its order, its buyer, its subscription or its status.
- The Licence API: `POST /public/licenses/activate`, `validate` and `deactivate`, for the software you sell. Each call carries the product's `product_secret`; validate is answered at the edge nearest the caller.
- `POST /public/{pk}/back-in-stock`: a shopper asks to be emailed when a sold-out variant is back.

3 October 2026 Added

- A product's licence keys can come from your own server: `/stores/{storeId}/products/{id}/licence-keys/endpoint-secret` (and `endpoint-test`) set up the call we make when a key is needed.

2 October 2026 Fixed

- `returnUrl` and `cancelUrl` on `POST /orders/{orderId}/pay` must now be on an address the store owns (its shop, custom domain, store page or website). Any other address is replaced with the store's own page instead of being followed. See [a headless checkout](https://v3.shoprocket.io/docs/basics.md#checkout).

1 October 2026 Added Improved

- Contact form: `POST /public/{pk}/contact` sends a shopper's message to the store, and `GET /stores/{storeId}/contact-messages` (with `DELETE` for one) lists what arrived.
- `POST /public/{pk}/cart/card-setup` saves a card for a subscription whose first payment is zero, so its renewals can be charged.
- `GET /public/{pk}/orders/{orderId}/status` now asks Stripe directly when its webhook is late, so a paid card order no longer reads as pending.

30 September 2026 Added

- Two more `pay` handoffs, both only when you send `embedded: true`: `mollie_components` (post the card token to `POST /orders/{orderId}/mollie-pay`) and `authorize_net_frame` (iframe the hosted form).

29 September 2026 Added

- Personalised products: a product can ask the shopper for text or a file. Answers go in `properties` on `POST /public/{pk}/cart/items`, and a file is uploaded first with `POST /public/{pk}/cart/uploads`, whose `token` is the answer.

28 September 2026 Added Improved

- Local pickup: `GET /cart/shipping-options` carries a `pickup` list, and `PATCH /cart` takes a `pickupLocationId` instead of a shipping rate. Sellers manage locations at `/stores/{storeId}/pickup-locations`.
- Upsells: `/stores/{storeId}/upsells` for the rules, and `GET /public/{pk}/cart/suggestions` for what the cart should suggest.
- Storefront product search also matches variant SKUs.

26 September 2026 Added

- PayPal express: `POST /public/{pk}/cart/paypal-express` raises a PayPal order for a cart before any order exists (`PATCH` and `GET` on `/cart/paypal-express/{paypalOrderId}` keep it in step). Send its id as `paypalOrderId` on `pay`; a `total_changed` rejection means the approved figure no longer matches.

25 September 2026 Added

- Live carrier rates: a shipping method can quote carrier services at checkout. `GET /stores/{storeId}/shipping-carriers` lists the carriers and services one can offer.

24 September 2026 Added Deprecated Fixed

- The API moved to `/v3`. `/v1` paths still answer for now; they will be handed to the compatibility layer for Shoprocket v2 integrations before launch, so move to `/v3`.
- The [API reference](https://v3.shoprocket.io/docs/api.md) is now generated from the API itself: every published endpoint, grouped by resource, with its request and response schema and the authentication it takes.
- Browsers can now send `X-Customer-Token` to the storefront API. Before this, a signed-in shopper's account reads were blocked by the browser.
- Square can take the card on your page: with `embedded: true`, `pay` answers a `square_card` handoff, and the tokenised card goes to `POST /orders/{orderId}/square-pay`.

Spotted something missing?

If a behavioural change happened but isn't listed here, that's a bug we want to fix. Email [dev@shoprocket.io](mailto:dev@shoprocket.io) and we'll add it.

---

- HTML version: https://v3.shoprocket.io/docs/changelog
- Language: English (en)
