# Carts and the cart token

URL: /docs/concepts/carts

Create a cart, add and change lines, apply coupons, set addresses and shipping, and read totals, tax, gifts and nudges.

A cart holds what a shopper is about to buy. You create one, get back a `cart_token`, and send that token as `X-Cart-Token` on every cart and checkout call. There are no cookies or sessions on the API side: the token is the cart's identity.

## The flow [#the-flow]

1. `POST /v1/cart` creates a cart and returns its `cart_token`.
2. `POST /v1/cart/lines` adds variants. `PATCH` and `DELETE` on `/v1/cart/lines/{id}` change or remove them.
3. `POST /v1/cart/coupon` applies a promotion code (optional).
4. `PUT /v1/cart/addresses` sets the shipping and billing addresses.
5. `GET /v1/cart/shipping-options` lists the delivery and collection options for that address, and `PUT /v1/cart/shipping-option` picks one.
6. [Checkout](/docs/concepts/checkout) takes payment and places the order.

Every write returns the whole cart, recalculated, so you never need a second call to refresh it.

## Create a cart [#create-a-cart]

```bash tab="cURL"
curl -X POST https://api.elaraone.app/api/v1/cart \
  -H "X-Tenant-Key: $ELARAONE_TENANT_KEY" \
  -H "X-Tenant-Secret: $ELARAONE_TENANT_SECRET"
```

```ts tab="TypeScript"
const res = await fetch('https://api.elaraone.app/api/v1/cart', {
  method: 'POST',
  headers: {
    'X-Tenant-Key': process.env.ELARAONE_TENANT_KEY!,
    'X-Tenant-Secret': process.env.ELARAONE_TENANT_SECRET!,
  },
})
const { data } = await res.json()
// Keep data.cart_token server-side, for example in an httpOnly cookie.
```

```php tab="PHP"
$response = Http::withHeaders([
    'X-Tenant-Key' => env('ELARAONE_TENANT_KEY'),
    'X-Tenant-Secret' => env('ELARAONE_TENANT_SECRET'),
])->post('https://api.elaraone.app/api/v1/cart');

$cartToken = $response->json('data.cart_token');
```

```json title="201 Created"
{
  "data": {
    "cart_token": "01JB9K3N7Q2W5X8Z4C6V1T0R3M",
    "cart": {
      "id": 812,
      "currency": "GBP",
      "lines": [],
      "sub_total": { "amount": 0, "currency": "GBP", "formatted": "£0.00" },
      "tax_total": { "amount": 0, "currency": "GBP", "formatted": "£0.00" },
      "total": { "amount": 0, "currency": "GBP", "formatted": "£0.00" }
    }
  }
}
```

- A cart token lasts 30 days from when it is issued.
- An unknown or expired token, or the token of a cart that has become an order, returns `404 CART_NOT_FOUND`. Create a new cart.
- Create the cart with a signed-in customer's bearer token and it belongs to that customer. A signed-in customer can also call `GET /v1/cart` without `X-Cart-Token` to resume their current cart; the token comes back in the `X-Cart-Token` response header and in `meta.cart_token`.
- Send `X-Cart-Token` with `POST /v1/auth/login` or `POST /v1/auth/register` to merge a guest's cart into the customer's. See [customers](/docs/concepts/customers).

## Add a line [#add-a-line]

Send either a `variant_id` or a `sku`, with a `quantity` of at least 1. Send an [`Idempotency-Key`](/docs/concepts/idempotency) so a retried request never adds the line twice.

```bash tab="cURL"
curl -X POST https://api.elaraone.app/api/v1/cart/lines \
  -H "X-Tenant-Key: $ELARAONE_TENANT_KEY" \
  -H "X-Tenant-Secret: $ELARAONE_TENANT_SECRET" \
  -H "X-Cart-Token: $CART_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "sku": "MUG-STONE", "quantity": 2 }'
```

```ts tab="TypeScript"
const res = await fetch('https://api.elaraone.app/api/v1/cart/lines', {
  method: 'POST',
  headers: {
    'X-Tenant-Key': process.env.ELARAONE_TENANT_KEY!,
    'X-Tenant-Secret': process.env.ELARAONE_TENANT_SECRET!,
    'X-Cart-Token': cartToken,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ variant_id: 1543, quantity: 1 }),
})
const { data: cart } = await res.json()
```

To change the quantity, send `PATCH /v1/cart/lines/{id}` with `{ "quantity": 3 }`. To remove a line, send `DELETE /v1/cart/lines/{id}`. Both accept an `Idempotency-Key` too. The `{id}` is the line's `id` from the cart, not the variant's. A line id that is not in this cart returns `404 CART_LINE_NOT_FOUND`.

## The cart object [#the-cart-object]

| Field | Meaning |
|---|---|
| `id`, `currency` | The cart's id and currency |
| `lines` | The lines, described below |
| `shippable` | `false` when nothing in the cart ships (a digital-only or empty cart), so you can skip the delivery step |
| `addresses` | `shipping` and `billing` addresses |
| `shipping` | The chosen `option` and its `sub_total`, `tax_total`, `total` and `breakdown` |
| `coupon_code`, `coupon` | The code on the cart and its state |
| `discounts` | Each promotion applied, with its `label`, `kind`, `amount`, and how much came off the items and off the delivery |
| `sub_total` | The lines before promotions |
| `discount_total` | What promotions took off the lines |
| `shipping_discount` | What promotions took off the delivery, already applied to `shipping` and `total` |
| `savings` | "You saved": the promotion discounts plus the shipping discount |
| `tax_total`, `tax_breakdown`, `tax_zone` | Tax, per rate, and how prices should be labelled. See [money](/docs/concepts/money). |
| `nudges` | Messages that tell the shopper how to qualify for an offer |
| `rewards` | Free gifts and "choose a free product" offers the cart has earned |
| `total` | What the shopper pays |

Each line has an `id`, `quantity`, `variant` (`id`, `sku`, `name`), `unit_price`, `sub_total`, `discount_total`, `sub_total_discounted`, `discounts`, `gift`, `tax_amount` and `total`.

All amounts are [money objects](/docs/concepts/money). Show them as they are; do not recompute them.

## Coupons [#coupons]

`POST /v1/cart/coupon` with `{ "code": "WELCOME10" }` applies a code. `DELETE /v1/cart/coupon` removes it.

A code that cannot go on the cart is refused with `422`, and the cart is left exactly as it was:

| `error.code` | When | `error.details.reason` |
|---|---|---|
| `COUPON_INVALID` | No such code | `unknown` |
| `COUPON_EXPIRED` | The code or its offers have not started yet, or have ended | `not_started`, `ended` |
| `COUPON_USED_UP` | The code has reached its usage limit | `used_up` |
| `COUPON_LIMIT_REACHED` | This customer (or, for a guest, this email) has used it as often as allowed | `limit_reached` |
| `COUPON_NOT_APPLICABLE` | Nothing in this basket qualifies | `threshold`, `customer`, `items` |

For `threshold`, `error.details.remaining` is the money still to spend, and `error.message` says it in words ("Spend £20.00 more to use this code."). Show `error.message` next to the code field.

A code that was applied can stop qualifying later, for example when the shopper removes items. It stays on the cart, and `coupon.status` changes from `applied` to `not_applicable`, with `coupon.reason_code` and, for a spend threshold, `coupon.remaining`.

## Addresses [#addresses]

`PUT /v1/cart/addresses` sets both addresses in one call. `billing` is required; `shipping` can be left out for a click-and-collect order, whose shipping address is the collection point. Set `billing_same_as_shipping: true` to copy the shipping address.

```json title="PUT /v1/cart/addresses"
{
  "shipping": {
    "first_name": "Grace",
    "last_name": "Hopper",
    "line_one": "12 Mill Lane",
    "city": "York",
    "postcode": "YO1 7HH",
    "country_id": 235,
    "contact_email": "grace@example.com",
    "contact_phone": "+447700900123"
  },
  "billing_same_as_shipping": true
}
```

Address fields are `title`, `first_name`, `last_name`, `company_name`, `line_one`, `line_two`, `line_three`, `city`, `state`, `postcode`, `country_id`, `delivery_instructions`, `contact_email` and `contact_phone`. `country_id` comes from `GET /v1/countries`.

For a signed-in customer you can send `{ "address_id": 42 }` instead of the fields to use a saved address, or add `"save_to_account": true` to a new address to save it to their address book once the order is placed. A guest cannot use `address_id`: it returns `422 ADDRESS_NOT_FOUND`.

If a shipping option was already chosen, changing the address re-prices it for the new destination or clears it. `meta.shipping_option_cleared` tells you which.

## Shipping options [#shipping-options]

`GET /v1/cart/shipping-options` lists exactly what `PUT /v1/cart/shipping-option` will accept for the cart's current address. Before a shipping address is set it returns `422 SHIPPING_ADDRESS_REQUIRED`.

```json title="GET /v1/cart/shipping-options (trimmed)"
{
  "data": [
    {
      "identifier": "standard",
      "name": "Standard delivery",
      "description": "2 to 4 working days",
      "price": { "amount": 495, "currency": "GBP", "formatted": "£4.95" },
      "discounted_price": null,
      "collect": false,
      "kind": "delivery",
      "collection_location": null
    }
  ]
}
```

Pick one with `PUT /v1/cart/shipping-option` and `{ "identifier": "standard" }`. A click-and-collect option has `collect: true` and a `collection_location` with the address and opening hours. `discounted_price` shows the price after a free-delivery or delivery-discount promotion.

The engine remembers the price you showed. If the merchant changes a rate, or the basket crosses a weight or spend band, before the shopper pays, checkout refuses with `409 SHIPPING_OPTION_CHANGED` rather than charge a price the shopper never saw. See [checkout](/docs/concepts/checkout).

## Gifts, rewards and nudges [#gifts-rewards-and-nudges]

Promotions can add to the cart as well as take off it.

- **Free gifts.** A gift line has `gift: true`. Its quantity belongs to the promotion, so `PATCH` returns `422 GIFT_LINE_LOCKED`. The shopper can remove it with `DELETE`, and it is then not offered again on that cart.
- **"Choose a free product" offers.** `rewards` lists them with `type: "choice"`. Add one of the listed variants or products as a normal line and the promotion makes it free. `choose_qty` is how many free units the cart has earned and `chosen_qty` how many are already in it. `items` gives picker rows when the offer lists specific products.
- **Nudges.** Each entry in `nudges` has a `message`, such as "Spend £12.00 more for free delivery", and the `remaining` amount. Show them near the basket total.

## Stock [#stock]

Adding a line checks that the variant can be bought in that quantity, and refuses with `422 VALIDATION_FAILED` when it cannot. A cart does not reserve stock: two shoppers can have the last item in their carts at the same time.

Stock is locked when it matters: as the order is placed. Checkout locks the stock of every variant in the cart, checks it again, and takes it off in the same step that creates the order, so the last item can only be sold once. If the stock has gone, checkout returns `422 VALIDATION_FAILED` and no order is created.

<Cards>
  <Card title="Checkout" href="/docs/concepts/checkout" description="Take payment and place the order exactly once." />
  <Card title="Idempotency keys" href="/docs/concepts/idempotency" description="Retry line changes and checkout safely." />
</Cards>
