---
title: Store API and REST
description: The cartpops WooCommerce Store API namespace, the add-to-cart fragment, and the cartpops/v1 REST routes, with their authentication, rate limits and caching.
sidebar:
  label: Store API and REST
  icon: server
---

CartPops V2 talks to the server in three ways. WooCommerce's Store API carries the cart itself, with CartPops data added under its own namespace. The classic add-to-cart response carries one extra fragment. A small set of `cartpops/v1` REST routes covers the rest.

:::warning[Mostly internal]
The only supported way to change this data is through the [PHP filters](/developers/php-filters). The field shapes and the `cartpops/v1` routes are what CartPops' own interface uses. The plugin makes no stability promise for them, so they may change in any release. If you build on them, test after every CartPops update.
:::

## The `cartpops` Store API namespace

CartPops registers a `cartpops` extension namespace on two WooCommerce Store API schemas. You see it under `extensions.cartpops` in the response.

### On the cart (`GET /wc/store/v1/cart` and the other cart responses)

| Prop | Type | Default | Description |
| - | - | - | - |
| `drawer_config?` | `object` | - | Drawer settings: position, widths, animation, animation duration, border radius, dark mode and colors. |
| `launcher?` | `object` | - | Launcher settings: enabled, position, show_count and show_total. |
| `coupons?` | `array` | - | Coupons as CartPops presents them, with code, label, whether it is a system reward and whether it can be removed. Always set by CartPops. |
| `item_presentations?` | `array` | - | Up to 100 rows, one per cart line, holding the output of the cartpops_cart_item_data filter: key, name, short_description, variationSummary, extraLines and customPresentation, plus the line fields below. Always set by CartPops. |
| `optional_locked_keys?` | `string[]` | - | Cart item keys that CartPops locks against changes. Always set by CartPops. Empty in Free. |

Your own keys from [`cartpops_store_api_cart_extension_data`](/developers/php-filters#cartpops_store_api_cart_extension_data) appear here too. The `coupons`, `item_presentations` and `optional_locked_keys` keys are always restored by CartPops after the filters run.

### On each cart item

| Prop | Type | Default | Description |
| - | - | - | - |
| `key?` | `string` | - | The exact WooCommerce cart item key. |
| `product_id?` | `integer` | - | The parent product ID. For variable products the Store API item id is the variation ID. |
| `visible?` | `boolean` | - | Whether WooCommerce allows this line to show in a mini cart. |
| `quantityEditable?` | `boolean` | - | Whether the drawer offers quantity controls for this line. |
| `removable?` | `boolean` | - | Whether the drawer offers removal for this line. |
| `quantityLimits?` | `object` | - | minimum, maximum, multipleOf and unlimited, from WooCommerce's quantity rules. |
| `backorderNotice?` | `string` | - | A plain text backorder notice, up to 500 bytes. |

This is how `customPresentation` reaches your own client code. Read `extensions.cartpops.item_presentations` from a Store API cart response, find the row whose `key` matches the cart item, and use its `customPresentation`.

```js my-script.js
async function readCustomPresentation( restRoot ) {
	const response = await fetch( `${ restRoot }wc/store/v1/cart`, {
		credentials: 'same-origin',
	} );
	const cart = await response.json();
	const rows = cart?.extensions?.cartpops?.item_presentations ?? [];

	return Object.fromEntries(
		rows.map( ( row ) => [ row.key, row.customPresentation?.my_plugin ] )
	);
}
```

Pass your site's REST root for `restRoot`, for example the value of `rest_url()` from PHP. It ends with a slash.

## The add-to-cart fragment

When WooCommerce answers a classic AJAX add to cart (`?wc-ajax=add_to_cart`) or a fragment refresh, CartPops adds one fragment to the response through the `woocommerce_add_to_cart_fragments` filter:

```
.cartpops-cart-json
```

Its value is a `script` element of type `application/json` with the class `cartpops-cart-json`. The element holds the current cart state as JSON. When the browser receives it through the `added_to_cart` event, CartPops reads the state from it and skips a second request.

- CartPops adds this fragment only when it is enabled.
- It is the only fragment CartPops adds. V1's HTML fragments are gone.
- The JSON shape is internal. Do not parse it, and do not rename or replace the key.
- If another plugin has put a non-string value into the fragments array, CartPops leaves the fragments untouched.

## `cartpops/v1` REST routes

All routes live under `/wp-json/cartpops/v1`. They are used by the CartPops drawer and admin screens. Treat all of them as internal.

### Authentication

The routes fall into three groups.

- **Cart session routes.** These act on the shopper's cart. They need a signed WooCommerce `Cart-Token` header. The token comes from a Store API response: read the `Cart-Token` header from a same-origin `GET /wc/store/v1/cart` and send it back. A missing or invalid token returns HTTP 403 with the code `cartpops_invalid_cart_token`. If the cart session cannot be started, the response is HTTP 503.
- **Logged-in shoppers.** A logged-in customer must also send a valid `X-WP-Nonce` header (the `wp_rest` nonce). A guest must not send `X-WP-Nonce` at all. A guest request that includes one is rejected with HTTP 403 and the code `cartpops_invalid_nonce`.
- **Admin routes.** These need a logged-in user who can `manage_woocommerce`, plus `X-WP-Nonce`.

One route needs no authentication: `GET /bundle-builder/configs` in Pro.

### Routes

| Route | Edition | Access | Rate limit |
| --- | --- | --- | --- |
| `GET /cart` | Free | Cart session | None |
| `POST /cart/remove-items` | Free | Cart session | 30 per minute |
| `POST /coupon` | Free | Cart session | 10 per minute |
| `DELETE /coupon` | Free | Cart session | 20 per minute |
| `GET /drawer-data` | Free | Cart session | None |
| `GET /recommendations` | Free | Cart session | None |
| `POST /recommendations/add` | Free | Cart session | 20 per minute |
| `GET`, `POST`, `DELETE /settings` | Free | Admin | `POST` 30 per minute, `DELETE` 5 per hour |
| `GET /settings/export` | Free | Admin | None |
| `POST /settings/import` | Free | Admin | 5 per hour |
| `GET /shipping-meter` | Pro | Cart session | None |
| `GET /smart-addons` | Pro | Cart session | None |
| `POST /smart-addons/toggle` | Pro | Cart session | 30 per minute |
| `GET /bundle-builder/companions` | Pro | Cart session | None |
| `GET /bundle-builder/configs` | Pro | Public | None |
| `POST /bundle-builder/add` | Pro | Cart session | 20 per minute |
| `POST /spotlight/add` | Pro | Cart session | 20 per minute |
| `POST /analytics/events` | Pro | Cart session | 60 per minute |
| `GET /analytics/summary` | Pro | Admin | None |
| `DELETE /analytics/reset` | Pro | Admin | 5 per hour |
| `GET /products/search` | Pro | Admin | None |
| `GET /conditions/types` | Pro | Admin | None |

The rate limits are the current values, not a promise.

A few request shapes, for orientation:

- `GET /cart` returns the cart state CartPops renders: `cartItems`, `cartCount`, `cartTotal`, `cartSubtotal`, `cartFees`, `cartTax`, `coupons` and related fields.
- `POST /cart/remove-items` takes `{ "keys": [ ... ] }` with 1 to 100 unique cart item keys and returns the new cart state.
- `POST` and `DELETE /coupon` take `{ "code": "..." }`.
- `GET /drawer-data` takes an optional `fields` query value, a comma separated list such as `recommendations,shipping_meter`.
- `POST /recommendations/add` takes `{ "product_id": 123 }` and adds only published, visible, purchasable, in-stock simple products.

### Same-origin example

```js my-script.js
async function readCartPopsCart( restRoot ) {
	// Get a signed cart token from the Store API.
	const storeResponse = await fetch( `${ restRoot }wc/store/v1/cart`, {
		credentials: 'same-origin',
	} );
	const cartToken = storeResponse.headers.get( 'Cart-Token' );

	// Use it on the CartPops route. A guest sends no X-WP-Nonce header.
	const response = await fetch( `${ restRoot }cartpops/v1/cart`, {
		credentials: 'same-origin',
		headers: { 'Cart-Token': cartToken },
	} );

	return response.json();
}
```

A logged-in shopper must also add an `X-WP-Nonce` header with a valid `wp_rest` nonce to the second request.

### Origin check

CartPops checks the `Origin` and `Referer` headers of every browser request to `cartpops/v1`. Only your home URL and site URL origins are allowed by default. Another origin gets HTTP 403 with the code `cartpops_forbidden_origin`. A request that sends neither header, such as one from a server, passes this check and still needs the authentication above. To allow another origin, use the [`cartpops_rest_allowed_origins`](/developers/php-filters#cartpops_rest_allowed_origins) filter.

### Rate limiting

The limited routes count requests per client IP address and per visitor, in fixed windows. When a limit is used up, the response is HTTP 429 with the code `cartpops_rate_limit_exhausted` and the headers `Retry-After`, `X-RateLimit-Limit` and `X-RateLimit-Remaining`. Behind a CDN or proxy, every visitor can look like the same IP address. Set [`cartpops_rest_trusted_proxy_cidrs`](/developers/php-filters#cartpops_rest_trusted_proxy_cidrs) to fix that. If limiting cannot work, for example because of a bad proxy list, the response is HTTP 503 with the code `cartpops_rate_limit_unavailable`.

### Caching

Admin, cart session and write routes send `Cache-Control: no-store, no-cache, must-revalidate, private`, `Pragma: no-cache` and `Expires: 0`, plus `Vary: Cookie, Authorization, Origin`. Do not cache them in a CDN. Only `GET /bundle-builder/configs` is treated as public data, and it varies on `Origin`.

### Other protections

- JSONP is rejected with HTTP 400.
- HTTP method overrides are rejected with HTTP 400.
- Each write route has a request body size limit, for example 8 KiB for coupon requests. A larger body gets HTTP 413.
- A method that a route does not support gets HTTP 405.

:::note[Coming from V1?]
V1's `admin-ajax.php` actions (`cpops_add_to_cart`, `cpops_refresh_cart` and the others) and the selector-keyed HTML fragments are gone. See [Upgrading to V2: for developers](/upgrading-to-v2/developers).
:::
