# Live Carts REST API & Webhooks

Live Carts for WooCommerce registers a REST API namespace, `live-carts/v1`,
under the standard WordPress REST API (`/wp-json/live-carts/v1/...`), plus a
webhook subscription system that POSTs a signed JSON payload to external
URLs when cart/list events happen.

This document describes the **free plugin's** API surface. **Live Carts Pro**
extends it with a few additional endpoints, query filters, and response
fields — see `api.md` in the Pro plugin for those.

> Keep this file up to date: any change to routes, request/response shape,
> or events must be reflected here in the same change.

## Authentication & permissions

Every endpoint requires the requesting user to be logged in and have the
`manage_woocommerce` capability (the same capability the Live Carts admin
screens require). There is no separate API key — use one of WordPress's
standard REST authentication methods:

- **Cookie + nonce** — for requests made from `wp-admin` / the block editor.
- **[Application Passwords](https://make.wordpress.org/core/2020/11/05/application-passwords-integration-guide/)**
  (built into WordPress core since 5.6) — the recommended method for
  external/server-to-server integrations. Create one under
  *Users → Profile → Application Passwords* and send it as HTTP Basic auth.

Requests without sufficient permissions receive `401`/`403` with a standard
`WP_Error` body (`code`, `message`, `data.status`).

## Carts

A "cart" is a `phplugins_carts` row of type `cart` — i.e. a shopper's active,
abandoned, or converted WooCommerce cart, as tracked by Live Carts.

### `GET /live-carts/v1/carts`

List carts.

**Query parameters**

| Param      | Type            | Description |
|------------|-----------------|-------------|
| `page`     | integer         | Page number (default `1`). |
| `per_page` | integer         | Items per page, 1–100 (default `20`). |
| `user`     | integer / array | Only carts belonging to this/these user ID(s). |
| `status`   | string          | `active`, `converted`, `abandoned`, or `trash`. When omitted, trashed carts are excluded and all other statuses are included. |
| `after`    | string (ISO8601)| Only carts created on/after this date. |
| `before`   | string (ISO8601)| Only carts created on/before this date. |

*Live Carts Pro adds `product`, `min_total`, and `max_total` — see the Pro
`api.md`.*

Response is a JSON array of cart summary objects (see below, without `items`
or `revisions`). Pagination totals are returned as headers `X-WP-Total` and
`X-WP-TotalPages`, matching core WP REST list endpoints.

### `GET /live-carts/v1/carts/{id}`

`{id}` is the cart's **formatted ID** — the 8-character hex string shown in
the admin and by the `[berrypress_live_cart_id]` shortcode (e.g.
`BCDDF9CE`), not the internal numeric `cart_id`. This is the only ID form
the API uses, in both requests and responses. Returns `404` if it doesn't
exist or isn't a cart (e.g. it's actually a shopper list).

**Response shape**

```jsonc
{
  "id": "BCDDF9CE",
  "admin_url": "https://example.com/wp-admin/admin.php?page=live-carts-for-woocommerce&cart_id=3168189774",
  "status": "active",
  "trashed": false,
  "user_id": 5,
  "order_id": null,
  "created": "2026-09-19T10:00:00+00:00",
  "last_seen": "2026-09-19T10:05:12+00:00",
  "value": 49.99,
  "coupons": ["SAVE10"],
  "ip_address": "203.0.113.5",
  "last_url": "https://example.com/cart/",
  "items": [
    {
      "key": "b46e37e1...",
      "product_id": 10,
      "variation_id": 0,
      "quantity": 2,
      "line_subtotal": 40,
      "line_subtotal_tax": 0,
      "line_total": 40,
      "line_tax": 0,
      "product": {
        "id": 10,
        "name": "Example Product",
        "sku": "EX-10",
        "url": "https://example.com/product/example-product/",
        "price": "20.00",
        "attributes": { "Color": "Blue" }
      }
    }
  ],
  "revisions": [
    {
      "date": "2026-09-19T10:00:00+00:00",
      "changes": [
        {
          "type": "item_added",
          "item": {
            "key": "b46e37e1...",
            "product_id": 10,
            "variation_id": 0,
            "quantity": 2,
            "line_subtotal": 40,
            "line_subtotal_tax": 0,
            "line_total": 40,
            "line_tax": 0,
            "product": {
              "id": 10,
              "name": "Example Product",
              "sku": "EX-10",
              "url": "https://example.com/product/example-product/",
              "price": "20.00",
              "attributes": { "Color": "Blue" }
            }
          }
        }
      ]
    }
  ]
}
```

Note there's no `type` field on a cart response (only shopper lists have
one — see below); the endpoint you called already tells you it's a cart.
`admin_url` links to the cart's detail page in the WordPress admin.

`items` reflects the cart's current contents (most recent revision), each
with a `product` object. `revisions` is the full history of item-level
changes to the cart, oldest first. Each change has a `type` of
`item_added`, `item_removed`, or `item_update`, and an `item` object built
by the exact same helper used for the cart's `items` array
(`RestControllerBase::formatItem()`), so it carries the same fields
(including `quantity` — there's no separate `quantity` field on the
change itself). An `item_update` change (a quantity change; metadata
can't change through anything that generates a revision) also includes
`item_before`, the item as it was immediately prior.

A `product.attributes` is a label → value map (variation attribute values,
or visible product attributes for a non-variation), and is `{}` when the
product has none, or when the product could no longer be found (in which
case `product.id`, `sku`, `url`, and `price` are `null`).

*Live Carts Pro adds a `recovery_url` field to this response — see the Pro
`api.md`.*

Updating a cart's items and converting a cart to an order both require
**Live Carts Pro** — see its `api.md`.

## Shopper Lists

A "shopper list" is a `phplugins_carts` row whose `type` is `list_<slug>`
(e.g. `list_wishlist`, `list_saved-for-later`) — WooCommerce's built-in
Shopper Lists feature (wishlist, save-for-later), synced into the same
storage Live Carts uses for carts, so lists share the same shape and
revision history as carts.

### `GET /live-carts/v1/shopper-lists`

List shopper lists.

**Query parameters**: same as `GET /carts` (`page`, `per_page`, `user`,
`status`, `after`, `before`) except product/value filters, which don't apply
to lists.

### `GET /live-carts/v1/shopper-lists/{id}`

`{id}` is the list's formatted ID, same as for carts. Get a single shopper
list by ID; same response shape as a cart (see above), plus a `type` field
set to the list's slug (e.g. `"wishlist"` or `"saved-for-later"`) — carts
don't have this field, but every shopper list does. Returns `404` if the ID
isn't a shopper list.

## Webhooks

Configure webhooks from **Live Carts → Settings → Webhooks**. There is no
REST endpoint for managing webhooks; they're configured entirely from the
admin screen. Each webhook has:

- a **URL** to POST to,
- one or more **events** it's subscribed to (below),
- a **secret**, generated automatically and used to sign payloads,
- an **active** flag.

### Delivery

On each event, Live Carts sends a `POST` request to every active webhook
subscribed to that event:

```
POST <webhook url>
Content-Type: application/json
X-Live-Carts-Event: cart.items.added
X-Live-Carts-Signature: <hex-encoded HMAC-SHA256 of the raw body, keyed with the webhook's secret>
```

A **cart** event's payload carries a `cart` key; a **list** event's payload
carries a `list` key instead. Either way, that object is byte-for-byte what
`GET /carts/{id}` or `GET /shopper-lists/{id}` returns for the same
resource — both are built by the same helper,
`RestControllerBase::formatRow()`. That also means anything a filter adds to
the REST response (e.g. Live Carts Pro's `recovery_url`) shows up in webhook
payloads too, automatically.

```jsonc
{
  "event": "cart.created",
  "created": "2026-09-19T10:00:05+00:00",
  "cart": {
    "id": "BCDDF9CE",
    "admin_url": "https://example.com/wp-admin/admin.php?page=live-carts-for-woocommerce&cart_id=3168189774",
    "status": "active",
    "trashed": false,
    "user_id": 0,
    "order_id": null,
    "created": "2026-09-19T10:00:05+00:00",
    "last_seen": "2026-09-19T10:00:05+00:00",
    "value": 0,
    "coupons": [],
    "ip_address": "203.0.113.5",
    "last_url": "https://example.com/cart/",
    "items": [],
    "revisions": []
  }
}
```

For the granular item events (`cart.items.*` / `list.items.*`), the payload
also carries an `item` field — built the same way as a revision's
`changes[].item` (see `GET /carts/{id}` above) — describing the item that
was added, removed, or changed. **One event fires per item changed**, not
one batched event for the whole cart/list update. `*.items.updated` also
carries `item_before`, the item as it was immediately prior to the change:

```jsonc
{
  "event": "cart.items.updated",
  "created": "2026-09-19T10:05:00+00:00",
  "cart": { "...": "same shape as above" },
  "item": {
    "key": "b46e37e1...",
    "product_id": 10,
    "variation_id": 0,
    "quantity": 3,
    "line_subtotal": 60,
    "line_subtotal_tax": 0,
    "line_total": 60,
    "line_tax": 0,
    "product": { "id": 10, "name": "Example Product", "sku": "EX-10", "url": "...", "price": "20.00", "attributes": {} }
  },
  "item_before": {
    "key": "b46e37e1...",
    "product_id": 10,
    "variation_id": 0,
    "quantity": 2,
    "line_subtotal": 40,
    "line_subtotal_tax": 0,
    "line_total": 40,
    "line_tax": 0,
    "product": { "id": 10, "name": "Example Product", "sku": "EX-10", "url": "...", "price": "20.00", "attributes": {} }
  }
}
```

Verify authenticity by recomputing the HMAC of the raw request body with
the webhook's secret and comparing it (constant-time) to
`X-Live-Carts-Signature`.

Delivery is **fire-and-forget**: the request is sent non-blocking so it
never delays the shopper's request, there is no retry on failure, and no
delivery log is kept. Your endpoint should return quickly and idempotently
handle duplicate deliveries.

### Events

| Event | Fires when | Payload |
|---|---|---|
| `cart.created` | A new cart is first tracked. | `cart` |
| `cart.seen` | A cart is seen on a new request (also re-marks an abandoned cart active). | `cart` |
| `cart.items.added` | A line item is newly present in a cart (one event per item). | `cart` + `item` |
| `cart.items.removed` | A line item is no longer in a cart (one event per item; `item` is its last known state). | `cart` + `item` |
| `cart.items.updated` | An existing line item's quantity changes (one event per item). | `cart` + `item` + `item_before` |
| `cart.converted` | A cart's linked order reaches a "converted" order status (see Settings → Conversion statuses). | `cart` |
| `cart.abandoned` | The hourly cron job marks an inactive cart abandoned (see `CART_ABANDON_TIME`). | `cart` |
| `list.created` | A shopper list (wishlist, saved-for-later, ...) is first tracked for a user. | `list` |
| `list.seen` | A shopper list is seen on a new request. | `list` |
| `list.items.added` | An item is added to a shopper list (one event per item). | `list` + `item` |
| `list.items.removed` | An item is removed from a shopper list (one event per item). | `list` + `item` |
| `list.items.updated` | A shopper list item's quantity changes (one event per item). | `list` + `item` + `item_before` |

Each event is only dispatched (and its `cart`/`list` object only built —
including its revision history, which for an old cart can be non-trivial)
when at least one active webhook is actually subscribed to it.

## Extending this API

For plugin developers (Live Carts Pro uses these):

- `phplugins_live_carts_api_cart_response` — filters the formatted
  cart/list REST response (`$data, $row, $request`).
- `phplugins_live_carts_api_query_where` — filters the `[$where, $params]`
  pair used to query the carts/lists list endpoints (`$request, $viewType`
  where `$viewType` is `'cart'` or `'list'`).
- `phplugins_live_carts_api_carts_collection_params` — filters the
  collection query-arg schema for the list endpoints.
- `phplugins_live_carts_webhook_events` — filters the list of events
  available to subscribe a webhook to.
- `phplugins_live_carts_items_added` / `_items_removed` — internal actions
  (`$cart_id, $items`) fired whenever items are added to or removed from a
  cart or list, regardless of type; `$items` is a list of raw item arrays
  (as from `LiveCarts::getCartContentsArray()`). `_items_updated` fires the
  same way but `$items` is a list of `['item' => ..., 'item_before' => ...]`
  pairs (both raw item arrays) instead, since a quantity change needs both
  states. This is what the webhook item events are built on — see
  `Api\Webhooks::dispatchItemEvent()`, which formats each raw item with
  `RestControllerBase::formatItem()` and fires one event per item.
