# Moving from v2

> For integrations built on Shoprocket v2: what keeps working when a store moves to v3, what is read-only, and the v3 call for every common v2 one.

Your integration talks to Shoprocket v2 today. Here is what keeps working when the store moves to v3, what becomes read-only, and what to call instead.

## What changes

| You use | When the store moves | What to do |
| --- | --- | --- |
| The v2 snippet that loads `cdn.shoprocket.io/loader.js` | Keeps working. The same snippet now boots the v3 widget. | Nothing. Swapping in the new snippet is optional. |
| v2's `window.Shoprocket` calls in your own scripts | Keep working on pages that still carry the v2 snippet, with v2's return shapes. | Port them to the [widget JavaScript API](https://v3.shoprocket.io/docs/javascript.md) before you swap snippets. |
| The REST API, `/v1` with `x-api-key` | Reads keep working, answered from v3 data in v2's shape. Writes are refused. | Move to `/v3` with a new secret key. |
| The old storefront API, `/storefront/v1` | Not carried over. | Call `/v3/public/{pk}` instead. Your v2 publishable key works there. |
| Webhook endpoints | Copied over, switched off, with a new signing secret. | Update your receiver, then switch each one on. |

## Embed snippets

Every v2 site loads `https://cdn.shoprocket.io/loader.js`. That URL now decides per page: when every embed on the page belongs to a store that has moved, it boots the v3 widget; anything else (a store that has not moved, a page with embeds from two stores, a config it cannot read) gets v2's own file, unchanged. A move, or moving a store back, reaches pages in about two minutes, with no change to your HTML.

The snippet's settings move too. Each snippet becomes an embed in the dashboard's Embeds list, marked **Old code**, and a change saved there shows on the page that carries the old snippet. Only its box on the page (width, margin, picture frame) still comes from the snippet's inline style.

An embed added after the page loads

The old loader decides from the embeds on the page when it runs (at `DOMContentLoaded` if the script sits in the `<head>`). An embed a page builder injects later is not seen, so that page keeps getting v2. Swap in the new snippet from the dashboard for such a page.

## Scripts on your pages

A page booted through the old loader also gets v2's own `window.Shoprocket` functions, with v2's names, arguments and return shapes: money as a decimal string, product ids as the v2 ids your page already holds. That covers `cart.add()`, `cart.get()` (still a promise of v2-shaped lines), `cart.clear()`, `cart.address`, `cart.applyCoupon()`, `cart.discounts`, `cart.applyNotes()`, `toggleCart()`, `products.get()`, `products.init()`, `store.setLanguage()`, `store.setCurrency()`, `order.get()` and `events.listen()`, plus v2's ready events.

Four v2 calls are not supported and answer `{ error }` as v2 did, never a throw: `cart.fetchCoupon()` (checking a code without applying it), `cart.add({ options })`, `cart.add({ skuSet })` (pass `variantId` instead), and the `Shop` global.

The v2 functions come with the old snippet

They exist only on a page booted through `cdn.shoprocket.io/loader.js`. The new snippet gives the page the v3 API alone, so port a script before you swap its page's snippet.

| v2 | v3 |
| --- | --- |
| `Shoprocket.cart.add({ ... })` | `Shoprocket.getSdk().cart.addItem({ variantId, quantity })` |
| `Shoprocket.cart.get().then(...)` | `Shoprocket.cart.data`, or listen for `shoprocket:cart:updated` |
| `Shoprocket.toggleCart()` | `Shoprocket.cart.toggle()` |
| `Shoprocket.products.init()` | `Shoprocket.rescan()` |
| `Shoprocket.store.setLanguage(code)` | `Shoprocket.setLanguage(code)` |
| `Shoprocket.events.listen(...)` | `addEventListener('shoprocket:...')` on `window` |

Script the cart only through `Shoprocket.getSdk()`: it shares the shopper's cart with the widget, so the drawer shows your script's changes straight away. See the [widget JavaScript API](https://v3.shoprocket.io/docs/javascript.md) for every method and event.

## The /v1 REST API

A `/v1` request that carries v2's `x-api-key` header is answered by v3, from the store's v3 data, in v2's response shape: every value a string, money as a decimal string in major units, dates as UTC `YYYY-MM-DD HH:MM:SS`, v2's pagination, and the ids v2 gave each record. A client that parsed v2 parses this.

| Answered (GET) | Scope the key needs |
| --- | --- |
| `/v1/store`, `/v1/store/details` (and `/v1/stores/...`) | `view-settings` |
| `/v1/store/stats` | `view-orders` |
| `/v1/subscription` | `view-settings` |
| `/v1/products`, `/v1/products/{id}`, `/v1/products/{id}/media` | `view-products` |
| `/v1/categories`, `/v1/categories/{id}` | `view-products` |
| `/v1/orders`, `/v1/orders/{id}` | `view-orders` |

- **Your v2 secret keys still work here, and only here.** The move brings both of the store's v2 keys (live and test) over as API keys named "Shoprocket v2 API key", visible in Settings > API keys. Revoking one there cuts that integration off. They are refused by `/v3`, and a v3 key is refused by `/v1`.
- **Send the key in the `x-api-key` header.** v2 also read it from a query or form field; v3 reads only the header.
- **Read-only.** Every `POST`, `PUT`, `PATCH` and `DELETE` answers `410`. Writes through `/v1` are not supported: make them on `/v3`.
- **Orders default to live.** `/v1/orders` returns real orders; add `?env=test` for test orders.
- **Some v2 fields come back empty.** Concepts v3 does not have (product add-on options, tabs, notes, filters, visitor counts) keep their keys with empty or zero values. Image filenames are full URLs. Secret keys, the TaxJar key and unsold licence keys are never returned.

`/v1` keeps v2 integrations running through the move; it is not where new work goes. It gets nothing new, and integrations still calling it will be emailed a retirement date before it is switched off.

Unchanged v2 call

```
curl "https://api-v3.shoprocket.io/v1/products?limit=50" \
  -H "x-api-key: sr_live_sk_..."
```

A write answers 410

```
{
  "status": false,
  "message": "This API is read-only since your store moved to Shoprocket v3. Use the v3 API to make changes: https://api.shoprocket.io/docs"
}
```

## The old storefront API

v2's widget read the shop from `/storefront/v1/stores/{key}/...`. v3 does not answer that path: a call to it gets `404`.

On a moved store's pages nothing calls it any more, because the old loader boots the v3 widget, which uses `/v3/public/{pk}`. If your own code called `/storefront/v1` directly (a server-rendered shop, a custom product page), point it at `https://api-v3.shoprocket.io/v3/public/{pk}`. The store's v2 publishable key (`sr_live_pk_...`) still identifies it there, so only the path and the response shape change. The [API basics](https://v3.shoprocket.io/docs/basics.md) walk through the catalogue, cart and checkout.

The catalogue on v3

```
curl https://api-v3.shoprocket.io/v3/public/pk_bCz5dnUr-Mdw6bj0BcoE3CALrGA1vhYq/products
```

## Webhooks

A store's v2 webhook endpoints are copied over on the move, with the same URLs and events, and v3 has the same nine event names v2 had (`order.created`, `order.updated`, `order.deleted` and the `customer.*` and `product.*` three) plus more. Two things are deliberately different:

- **Each endpoint arrives switched off**, so nothing reaches your receiver before it is ready for v3's payload.
- **Each gets a new signing secret.** v3 signs every delivery in `X-Shoprocket-Signature`; verify it with the new secret shown in Settings > Webhooks.

Update the receiver to the [v3 payload](https://v3.shoprocket.io/docs/webhooks.md#payload) and [signature check](https://v3.shoprocket.io/docs/webhooks.md#verify), then switch the endpoint on in Settings > Webhooks. A sync that polled `/v1/orders` every few seconds can subscribe to `order.created` and `order.paid` instead.

## What to move to, and why

`/v1` answers the questions v2 could answer, in v2's terms, and nothing else. `/v3` is the API the dashboard itself runs on: it writes, it has scoped keys, and everything new lands there.

|  | v2 | v3 |
| --- | --- | --- |
| Secret key | `sr_live_sk_...` in `x-api-key` | `sk_...` as `Authorization: Bearer`, with [scopes](https://v3.shoprocket.io/docs/authentication.md#scopes), created in Settings > API keys |
| Publishable key | `sr_live_pk_...` | `pk_...` (the v2 key still works on `/v3/public`) |
| Base path | `/v1`, `/storefront/v1` | `/v3/stores/{storeId}`, `/v3/public/{pk}` |
| Writes | Products and more | Everything the dashboard can do, scope permitting. `/v1` writes answer 410 |
| Money | Decimal string, `"12.50"` | Integer in the minor unit, `1250` |
| Ids | v2 ids | Prefixed (`prod_`, `ord_`, `cat_`, `str_`); an imported product keeps its v2 id as `legacyId` |
| Dates | `YYYY-MM-DD HH:MM:SS` | ISO 8601, `2026-09-29T08:01:05.504Z` |
| Pagination | `page` from 0, `limit` up to 1000 | `page` from 1, `limit` up to 100 |
| Events | Webhooks | Signed webhooks with retries and a delivery log |

Start in [Authentication](https://v3.shoprocket.io/docs/authentication.md), then the [REST API reference](https://v3.shoprocket.io/docs/api.md), which is generated from the API itself.

## v2 to v3, call by call

The v2 calls integrations actually make, and their v3 equivalent. The v3 paths hang off `https://api-v3.shoprocket.io/v3`; your store id starts `str_` and is in the dashboard's address bar.

| v2 (GET) | v3 | Notes |
| --- | --- | --- |
| `/v1/store/details`, `/v1/stores` | `GET /stores/{storeId}` | The store record. |
| `/v1/store/stats` | No published equivalent | Total `GET /stores/{storeId}/orders?mode=live&from=2026-09-01&to=2026-09-30` (dates inclusive, UTC), or keep a running total from the `order.paid` and `order.refunded` webhooks. |
| `/v1/products` | `GET /stores/{storeId}/products`, or `GET /public/{pk}/products` | The seller list has every status. The public one has active products only and needs no secret key: the right call for a shop page. |
| `/v1/products/{id}` | `GET /stores/{storeId}/products/{id}` | Takes the `prod_` id. The public `GET /public/{pk}/products/{idOrSlug}` also finds a product by its v2 id. |
| `/v1/products/{id}/media` | `GET /stores/{storeId}/products/{id}` | The pictures are the product's `images`. |
| `/v1/orders` | `GET /stores/{storeId}/orders?mode=live` | Without `mode` you get live and test orders together. Filter with `from`, `to`, `paymentStatus`, `q`. |
| `/v1/orders/{id}` | `GET /stores/{storeId}/orders/{id}` | Takes the `ord_` id. An imported order keeps its v2 order number, so `?q=` on the list finds it. |
| `/v1/categories`, `/v1/categories/{id}` | `GET /stores/{storeId}/categories`, `GET /stores/{storeId}/categories/{id}/products` | The category tree, and the products in one category. |
| `/v1/subscription` | Not in the API | Your Shoprocket plan is in the dashboard. |
| Any `POST` / `PUT` / `DELETE` | The matching `/v3` write | For example `POST /stores/{storeId}/products` or `PUT /stores/{storeId}/products/{id}`, with a key that has `edit-products`. |

The same order sync, before and after:

v2

```
curl "https://api-v3.shoprocket.io/v1/orders?limit=50" \
  -H "x-api-key: sr_live_sk_..."
```

v3

```
curl "https://api-v3.shoprocket.io/v3/stores/{storeId}/orders?mode=live&limit=50" \
  -H "Authorization: Bearer sk_..."
```

---

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