> ## Documentation Index
> Fetch the complete documentation index at: https://docs.loopreturns.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Headless Shop Now (On-Store)

<Warning>
  Shop Now: On-Store must be enabled for your account, and the API key you use needs the **Carts** scope. To get started, reach out to [support@loopreturns.com](mailto:support@loopreturns.com).
</Warning>

## Overview

[Shop Now: On-Store](/integration-guides/on-store-exchanges) redirects a shopper from the Loop returns portal to your storefront to pick replacement products with their return credit. When they're done, your storefront hands the selected items back to Loop as a cart **token**, and Loop completes the exchange server-side.

Most merchants implement this with Loop's On-Store SDK, which assumes it runs inside a Shopify Online Store theme. **This guide is for headless / custom storefronts** that need to implement the same flow themselves, without the SDK. It documents the contract the Loop portal and Loop API expect, and the theme dependencies a headless storefront must replace.

<Note>
  Shopify Checkout is never used in this flow. Loop creates the exchange order itself, and computes all exchange pricing, credit, bonus, and tax — not Shopify. Store discount codes and Shopify Functions do **not** apply to the exchange.
</Note>

The three pieces a headless storefront must implement:

1. **Inbound** — read the `loop_*` query params on your landing page and enter "on-store mode".
2. **Handoff** — when the shopper checks out, collect the selected variant IDs, `POST` them to the Loop Cart API, and store the returned token.
3. **Outbound** — redirect back to the Loop portal with that token, and clear your own cart.

***

## How It Works

```mermaid theme={null}
sequenceDiagram
    participant Portal as Loop Portal
    participant Store as Your Storefront
    participant Cart as Loop Cart API
    participant Exchange as Loop Exchange

    Portal->>Store: Redirect with loop query params like loop_total and loop_return_id
    Note over Store: Shopper browses and adds variants to the cart
    Store->>Cart: POST /api/v1/cart with the selected variant IDs
    Cart-->>Store: Returns a cart token
    Store->>Portal: Redirect back to the portal with the token and clear own cart
    Note over Portal,Exchange: Loop portal loads the selected cart and shows the review screen
    Portal->>Exchange: Shopper reviews and submits the return
    Note over Exchange: Loop creates the exchange order server-side
```

***

## Base URL

The Cart API is served from:

```
https://api.loopreturns.com/api/v1
```

***

## Authentication

The Cart API uses the same authentication as the rest of Loop's APIs. Send your merchant API key in the `X-Authorization` header — see [Authentication](/api-reference/authentication) for how to create and manage keys, the full header contract, and the 401 response shape.

The key must have the **Carts** scope. Generate one from **Returns Management → Tools & integrations → Developer tools** in the Loop Admin and select the **Carts** scope.

<Warning>
  **Keep the API key server-side.** Prefer proxying the Cart API calls through your own backend so the key is never exposed to the browser. A key scoped to **Carts** only keeps the blast radius minimal if it ever leaks.
</Warning>

***

## Step 1 — Inbound: portal → storefront

Loop redirects the shopper to your storefront (your apex domain, or a market-localized URL) with `loop_*` query params. **On-store mode is keyed on the presence of `loop_total`.**

### Query parameters

| Param                      | Required      | Meaning                                                                                                  |
| -------------------------- | ------------- | -------------------------------------------------------------------------------------------------------- |
| `loop_total`               | **Yes**       | Activation flag and total Shop Now credit (base + bonus), in the shop **base** currency **minor units**  |
| `loop_return_id`           | Yes           | Portal session identifier. Use it to invalidate any stale saved cart from a previous session             |
| `loop_currency`            | Yes           | Shop **base** currency code for the credit amounts                                                       |
| `loop_base`                | When non-zero | Base return credit (minor units). Omitted when zero                                                      |
| `loop_credit`              | When non-zero | Bonus credit (minor units). Omitted when there's no bonus                                                |
| `loop_domain`              | Yes           | Loop portal host — used to build the redirect **back** to Loop                                           |
| `loop_redirect_url`        | Yes           | Fallback "go back" URL if the shopper leaves with an empty cart                                          |
| `loop_subdomain`           | When set      | Loop shop subdomain                                                                                      |
| `loop_discount_percentage` | Optional      | Shop Now bonus %, for display in your credit UI                                                          |
| `loop_product_ids`         | Optional      | Comma-separated IDs of the returning products                                                            |
| `loop_product_types`       | Optional      | Comma-separated returning product types                                                                  |
| `loop_return_key`          | Optional      | Shop Later offer key (changes the redirect-back path — see [Step 3](#step-3-outbound-storefront-portal)) |
| `currency` / `country`     | Conditional   | Storefront currency / market pin on localized-market handoffs                                            |
| `utm_redirect`             | Optional      | UTM source for your analytics                                                                            |

<Note>
  Amounts are **minor units** in the shop **base** currency (e.g. `12099` = `120.99`). For multi-currency display, convert using your own storefront FX rate. The portal omits any param with a falsy value, so treat `loop_total` as the reliable credit total and don't assume `loop_base` / `loop_credit` are always present.
</Note>

### What to do on landing

<Steps>
  <Step title="Detect on-store mode">
    Check for `loop_total` in the query string. Its presence means the shopper arrived from the Loop portal in Shop Now: On-Store mode.
  </Step>

  <Step title="Persist the loop_* values">
    Store the `loop_*` values (for example in `sessionStorage` or `localStorage`) so they survive navigation across your store.
  </Step>

  <Step title="Discard stale carts">
    If `loop_return_id` differs from a previously saved session, discard any saved cart or token — it belonged to a different return.
  </Step>

  <Step title="Render a credit UI (optional)">
    Show available credit and bonus % from `loop_total` and `loop_discount_percentage`.
  </Step>

  <Step title="Suppress non-applicable UI (recommended)">
    Hide storefront elements that don't apply during an exchange: discount-code popups, alternative payment buttons (PayPal, Afterpay, Google Pay), home try-on, sticky mobile add-to-cart, and chat widgets. Store discounts and Functions will not apply to the exchange.
  </Step>
</Steps>

***

## Step 2 — Handoff: storefront → Loop Cart API

When the shopper is done and clicks your "checkout" equivalent, collect the selected Shopify **variant IDs** from your cart and `POST` them to the Cart API.

<Warning>
  **Quantities are not supported.** To add a variant N times, repeat its ID N times in the `cart` array.
</Warning>

Each variant ID must resolve to a real Shopify variant for the shop — Loop validates against Shopify and returns `Variant ID X not found.` otherwise. The `cart` array is required and must be non-empty.

### Create a cart

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.loopreturns.com/api/v1/cart \
    -H "Content-Type: application/json" \
    -H "X-Authorization: YOUR_MERCHANT_API_KEY" \
    -d '{
      "cart": [39076568408247, 39076568408247, 39076568440000]
    }'
  ```

  ```javascript Node (server-side) theme={null}
  // Runs on YOUR backend so the API key never reaches the browser.
  const response = await fetch("https://api.loopreturns.com/api/v1/cart", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Authorization": process.env.LOOP_API_KEY,
    },
    body: JSON.stringify({
      // Repeat a variant id to represent quantity > 1
      cart: [39076568408247, 39076568408247, 39076568440000],
    }),
  });

  const { token } = await response.json();
  ```

  ```php PHP theme={null}
  <?php

  $url = "https://api.loopreturns.com/api/v1/cart";
  $payload = json_encode([
      "cart" => [39076568408247, 39076568408247, 39076568440000],
  ]);

  $options = [
      "http" => [
          "header" => "Content-Type: application/json\r\nX-Authorization: YOUR_MERCHANT_API_KEY\r\n",
          "method" => "POST",
          "content" => $payload,
      ],
  ];

  $context = stream_context_create($options);
  $result = file_get_contents($url, false, $context);
  $data = json_decode($result, true);
  $token = $data["token"];
  ```
</CodeGroup>

The response returns the cart `token` — persist it:

```json theme={null}
{
  "token": "2e99758548972a8e...bc50c",
  "data": { "cart": [39076568408247, 39076568408247, 39076568440000] }
}
```

<Note>
  To amend the selection later, `POST /api/v1/cart/{token}` with a new full `cart` array — it **replaces** the cart contents. See the [Cart API reference](/api-reference/latest/cart/create-cart) for the full request and response schema, including update and delete.
</Note>

***

## Step 3 — Outbound: storefront → portal

Once you have a `token`:

<Steps>
  <Step title="Clear your storefront cart">
    Empty your own cart so the shopper doesn't accidentally buy the items through normal checkout. Skip only if you intentionally preserve it.
  </Step>

  <Step title="Redirect back to the Loop portal">
    Send the browser back to the portal with the token. Only the token travels in the URL — the variant selections were persisted server-side in [Step 2](#step-2-handoff-storefront-loop-cart-api).
  </Step>
</Steps>

### Redirect URL rules

| Case                                   | URL                                                                     |
| -------------------------------------- | ----------------------------------------------------------------------- |
| Have token                             | `https://{loop_domain}/#/cart/v2/{token}`                               |
| Have token + optional destination      | `https://{loop_domain}/#/cart/v2/{token}?to={to}` (e.g. `to=lineItems`) |
| Shop Later (token + `loop_return_key`) | `https://{loop_domain}/#/cart/v2/{token}/{loop_return_key}`             |
| Empty cart / "go back"                 | `https://{loop_redirect_url}` (the value passed in on inbound)          |

<Note>
  URL-encode `token`, `to`, and `return_key`. `{loop_domain}` and `{loop_redirect_url}` come from the inbound params.
</Note>

Loop then loads the selected products and shows the shopper the review screen to submit their return.

***

## Carrying storefront discounts into the exchange

To keep a discount the shopper had on your storefront in the exchange, send it to the Cart API in the optional `shopify` field — a **Base64-encoded** copy of the Shopify [Ajax cart object](https://shopify.dev/docs/api/ajax/reference/cart) (`GET /cart.js`). Loop reads the discount fields already present in that object; it does not evaluate discount codes or Shopify Functions itself.

<Note>
  Loop only consumes this snapshot when your shop is configured to use **Shopify Scripts**. Without it, the snapshot is ignored and exchange items price at catalog price. **Most headless stores don't need it** — omit `shopify` entirely unless you carry cart discounts.
</Note>

<Tip>
  Automatic cart discounts work the same way — any discount already applied to the Shopify cart (automatic discounts or discount codes) is carried into the exchange as long as it appears in the `/cart.js` snapshot you send. Loop mirrors whatever the snapshot contains; it never applies or re-evaluates discounts itself.
</Tip>

### Where the discount data comes from

The snapshot is Shopify's own cart object, so you don't build the schema yourself — you take what Shopify already gives you:

<Steps>
  <Step title="Apply the discount on the Shopify cart">
    Add or manage discount codes with [`POST /cart/update.js`](https://shopify.dev/docs/api/ajax/reference/cart#update-discounts-in-the-cart) (`{ "discount": "CODE" }`; comma-separate for multiple, empty string to clear).
  </Step>

  <Step title="Read the resulting cart">
    Fetch the cart with [`GET /cart.js`](https://shopify.dev/docs/api/ajax/reference/cart). The response now carries the discount fields (`total_discount`, `cart_level_discount_applications`, per-item `discount_allocations`).
  </Step>

  <Step title="Base64-encode it as `shopify`">
    Send that cart object as the `shopify` field on `POST /api/v1/cart` (see below).
  </Step>
</Steps>

### What Loop reads

Loop consumes a subset of the `/cart.js` object. All money values are **minor units** (cents), exactly as Shopify returns them:

* **Cart level** — `currency`, `total_price`, `total_discount`, and each `cart_level_discount_applications[]` entry (`title`, `value`, `value_type`, `total_allocated_amount`).
* **Per line item** (`items[]`) — `variant_id`, `quantity`, `original_price`, `discounted_price`, `final_line_price`, and each `discount_allocations[]` / `line_level_discount_allocations[]` entry (`amount`, `discount_application`).

Here is a trimmed `/cart.js` object with a 10% cart-level discount and a line-level discount — see the [Shopify reference](https://shopify.dev/docs/api/ajax/reference/cart) for the full shape:

```json theme={null}
{
  "currency": "USD",
  "total_price": 2249,
  "total_discount": 474,
  "items": [
    {
      "variant_id": 39888235757633,
      "quantity": 1,
      "original_price": 2723,
      "discounted_price": 2249,
      "final_line_price": 2249,
      "line_level_discount_allocations": [
        {
          "amount": 250,
          "discount_application": {
            "type": "script",
            "title": "Welcome Offer",
            "value": "2.5",
            "value_type": "fixed_amount",
            "total_allocated_amount": 250
          }
        }
      ]
    }
  ],
  "cart_level_discount_applications": [
    {
      "type": "automatic",
      "title": "Spring Sale",
      "value": "10.0",
      "value_type": "percentage",
      "total_allocated_amount": 224
    }
  ]
}
```

### Sending the snapshot

Base64-encode the cart object and include it as `shopify` alongside `cart`. Do this on your backend so the API key stays server-side:

```javascript Node (server-side) theme={null}
// `cart` is the object you fetched from Shopify's GET /cart.js
const shopify = Buffer.from(JSON.stringify(cart)).toString("base64");

const response = await fetch("https://api.loopreturns.com/api/v1/cart", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Authorization": process.env.LOOP_API_KEY,
  },
  body: JSON.stringify({
    cart: [39888235757633],
    shopify,
  }),
});
```

<Note>
  In the browser the equivalent encoding is `btoa(unescape(encodeURIComponent(json)))`, but prefer encoding and sending from your backend. Regardless of the snapshot, exchange pricing, credit, bonus, and tax are always computed by **Loop**, not by Shopify checkout.
</Note>

***

## Replacing the theme dependencies

The stock On-Store SDK assumes it runs inside a Shopify Online Store theme. A headless storefront must provide substitutes for each dependency:

| Dependency                                                          | Stock SDK usage                                                           | Headless replacement                                                                                                                          |
| ------------------------------------------------------------------- | ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| **Ajax Cart API** (`/cart.js`, `/cart/clear.js`, `/cart/update.js`) | Read the current cart, clear it on handoff, rebuild it from a saved token | Use your own cart state / Storefront API cart. Pass variant IDs directly to the Cart API `POST`.                                              |
| **Checkout button in the DOM**                                      | SDK hijacks the click and submits                                         | Wire your own "checkout" handler to the [Step 2](#step-2-handoff-storefront-loop-cart-api)–[Step 3](#step-3-outbound-storefront-portal) flow. |
| **Injected credit bar + CSS hooks**                                 | Visual affordance + hooks to hide theme elements                          | Build your own credit UI from `loop_total` / `loop_discount_percentage`; hide non-applicable UI yourself.                                     |
| **`window.Shopify.currency.rate`**                                  | Multi-currency credit display                                             | Use your storefront's own FX handling.                                                                                                        |
| **Persisted `loop_*` params**                                       | Persist inbound params across navigation                                  | Persist them however you like (e.g. `sessionStorage`).                                                                                        |

<Tip>
  You don't need the Ajax Cart API at all if you drive the flow yourself. The only hard external dependencies are (1) the inbound `loop_*` param contract, (2) the Loop Cart API, and (3) the redirect-back URL format.
</Tip>

***

## Eligibility and access

Access to the Cart API is granted through the **Carts** scope on your API key, which Loop provisions when Shop Now: On-Store is enabled for your account. Contact [support@loopreturns.com](mailto:support@loopreturns.com) to get set up.

If the API key isn't authorized — On-Store isn't enabled for the shop, or the key lacks the **Carts** scope — the Cart API responds with **401 Unauthorized**:

```json theme={null}
{ "errors": "Unauthorized." }
```

Confirm On-Store is enabled and your key carries the **Carts** scope before offering the flow to shoppers.

***

## Minimal reference implementation

The browser never sees the API key. It calls your own backend, which holds the **Carts**-scoped key and forwards the request to Loop.

```javascript Storefront (browser) theme={null}
// 1) On the storefront landing page
const qs = new URLSearchParams(location.search);
if (qs.has("loop_total")) {
  const loop = Object.fromEntries([...qs].filter(([k]) => k.startsWith("loop_")));
  sessionStorage.setItem("loop-onstore", JSON.stringify(loop));
  // (optional) render a credit bar from loop.loop_total / loop.loop_currency / loop.loop_discount_percentage
}

// 2) When the shopper finishes shopping (your own "checkout" handler)
async function handoffToLoop(selectedVariantIds /* number[], repeat ids for quantity */) {
  const loop = JSON.parse(sessionStorage.getItem("loop-onstore"));

  // Call YOUR backend proxy — not Loop directly — so the API key stays server-side.
  const res = await fetch("/api/loop/cart", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ cart: selectedVariantIds }),
  });
  const { token } = await res.json();

  await clearMyCart(); // your own cart clear

  // 3) Redirect back to Loop
  location.href = `https://${loop.loop_domain}/#/cart/v2/${encodeURIComponent(token)}`;
}
```

```javascript Your backend proxy (Node) theme={null}
// POST /api/loop/cart — adds the API key server-side and forwards to Loop.
app.post("/api/loop/cart", async (req, res) => {
  const loopRes = await fetch("https://api.loopreturns.com/api/v1/cart", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Authorization": process.env.LOOP_API_KEY, // Carts scope, never sent to the browser
    },
    body: JSON.stringify({ cart: req.body.cart }),
  });
  res.status(loopRes.status).json(await loopRes.json());
});
```

***

## Technical considerations

| Consideration          | Requirement                                                                                                 |
| ---------------------- | ----------------------------------------------------------------------------------------------------------- |
| **Monetary values**    | Credit amounts (`loop_total`, `loop_base`, `loop_credit`) are in **minor units** of the shop base currency. |
| **Quantities**         | Not supported in the `cart` array — repeat a variant ID to represent quantity > 1.                          |
| **Variant validation** | Every variant ID must resolve to a real Shopify variant for the shop, or the request is rejected.           |
| **Token in URL only**  | Only the cart token travels back in the redirect URL; variant selections live server-side.                  |
| **HTTPS**              | Always redirect back to the portal over `https://`.                                                         |

***

## Related Resources

<CardGroup cols={2}>
  <Card title="On-Store Exchanges" icon="cart-shopping" href="/integration-guides/on-store-exchanges">
    Conceptual overview of Shop Now and Shop Now: On-Store.
  </Card>

  <Card title="Create Cart — API Reference" icon="code" href="/api-reference/latest/cart/create-cart">
    Full request and response schema for the Cart API (create, get, update, delete).
  </Card>
</CardGroup>
