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

# Commerce

> Manage an agent's product catalog, the caller's baskets, and orders placed from a basket

The commerce routes hold a product catalog per agent and let the caller build a basket, place it as an order, and move that order through its statuses. They are the REST twin of the [`Products`](/reference/sdk/products), [`Baskets`](/reference/sdk/baskets), and [`Orders`](/reference/sdk/orders) runtime objects. Baskets and orders belong to the end user the credential identifies.

*Verified against lua-cli 3.33.0.*

## Base URL and authentication

Reads need `commerce:read` and writes `commerce:write` on the agent; the host, the bearer header, and the error envelope are on the [REST API overview](/reference/rest/overview).

## Response envelope

Basket and order routes, and product search and read by id, answer `{ "success", "message", "data" }`. A record that does not exist answers with the route's normal status, `200` or `201` on a `POST`, and `success: false` with a message such as `Basket with ID '<id>' not found for agent '<id>'`; a failed precondition, such as ordering an empty basket, does the same. Check `success` before reading `data`. Product create, update, and delete answer their own small objects, listed with each route.

A basket is `{ id, userId, agentId, data: { currency, items, createdAt, metadata }, common: { status, totalAmount, itemCount } }`; its status is one of `active`, `checked_out`, `abandoned`, `expired`. An order is `{ id, userId, agentId, data: { basketId, ... }, common: { status, ... } }` with a status of `pending`, `confirmed`, `fullfilled` (spelled with two l's), or `cancelled`.

## Endpoints

### GET /developer/agents/:agentId/products

Lists products, one page at a time.

<ParamField path="agentId" type="string" required>The agent.</ParamField>
<ParamField query="page" type="integer" default="1">Page number from 1.</ParamField>
<ParamField query="limit" type="integer" default="10">From 1 to 100; larger values are clamped.</ParamField>
<ParamField query="sortBy" type="string" default="createdAt">`createdAt` or `productId`; anything else answers `400`.</ParamField>
<ParamField query="sortOrder" type="string" default="desc">`asc` or `desc`.</ParamField>

<ParamField query="filter" type="string">
  A JSON [Lua Query](/reference/sdk/query) over the product fields: dot notation, `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$exists`, and `$and` or `$or` at the root.
</ParamField>

**Response**

`200` with `{ "success": true, "data": [...products], "pagination": { currentPage, totalPages, totalCount, limit, hasNextPage, hasPrevPage, nextPage, prevPage } }`.

**Errors**

| Status | Code or message                                | Meaning                                                            | Fix                |
| ------ | ---------------------------------------------- | ------------------------------------------------------------------ | ------------------ |
| `400`  | A filter validation message                    | `filter` is not valid JSON or uses an operator outside the grammar | Fix the filter     |
| `400`  | `sortBy must be one of createdAt or productId` | Unknown sort field                                                 | Use one of the two |
| `400`  | `Pagination offset may not exceed 100000`      | `(page - 1) × limit` is over 100,000                               | Narrow the filter  |

Equivalent: `Products.get(1, 10)`.

<CodeGroup>
  ```ts TypeScript theme={null}
  const params = new URLSearchParams({ filter: JSON.stringify({ category: 'coffee', price: { $lte: 10 } }), limit: '20' });
  const response = await fetch(`https://api.heylua.ai/developer/agents/<<YOUR_AGENT_ID>>/products?${params}`, {
    headers: { Authorization: 'Bearer <<YOUR_API_KEY>>' },
  });
  const page: { success: boolean; data: Array<{ id: string }>; pagination: { totalCount: number } } = await response.json();
  console.log(page.pagination.totalCount);
  ```

  ```bash cURL theme={null}
  curl -G "https://api.heylua.ai/developer/agents/<<YOUR_AGENT_ID>>/products" \
    -H "Authorization: Bearer <<YOUR_API_KEY>>" \
    --data-urlencode 'filter={"category":"coffee","price":{"$lte":10}}' --data-urlencode 'limit=20'
  ```
</CodeGroup>

### GET /developer/agents/:agentId/products/search

Searches the catalog by text.

<ParamField path="agentId" type="string" required>The agent.</ParamField>
<ParamField query="searchQuery" type="string" required>The search text.</ParamField>
<ParamField query="limit" type="integer" default="5">Maximum number of products.</ParamField>

**Response**

`200` with `{ "success": true, "message": "Successfully found <n> products for \"<query>\"", "data": [...products] }`.

Equivalent: `Products.search('coffee')`.

<CodeGroup>
  ```ts TypeScript theme={null}
  const params = new URLSearchParams({ searchQuery: 'oat flat white', limit: '5' });
  const response = await fetch(`https://api.heylua.ai/developer/agents/<<YOUR_AGENT_ID>>/products/search?${params}`, {
    headers: { Authorization: 'Bearer <<YOUR_API_KEY>>' },
  });
  const found: { success: boolean; data: Array<{ id: string; name?: string }> } = await response.json();
  for (const product of found.data) console.log(product.id, product.name);
  ```

  ```bash cURL theme={null}
  curl -G "https://api.heylua.ai/developer/agents/<<YOUR_AGENT_ID>>/products/search" \
    -H "Authorization: Bearer <<YOUR_API_KEY>>" \
    --data-urlencode 'searchQuery=oat flat white' --data-urlencode 'limit=5'
  ```
</CodeGroup>

### POST /developer/agents/:agentId/products

Creates a product, or updates it when the `id` exists.

<ParamField path="agentId" type="string" required>The agent.</ParamField>
<ParamField body="id" type="string" required>Your product id.</ParamField>
<ParamField body="*" type="any">Any other fields: name, price, description, images, categories. The catalog does not enforce a schema.</ParamField>

**Response**

`201` with `{ "updated", "isNew", "product" }`: `isNew: true` on a create, `updated: true` on an update.

Equivalent: `Products.create({ id: 'flat-white', name: 'Flat white', price: 3.5 })`.

<CodeGroup>
  ```ts TypeScript theme={null}
  const response = await fetch('https://api.heylua.ai/developer/agents/<<YOUR_AGENT_ID>>/products', {
    method: 'POST',
    headers: { Authorization: 'Bearer <<YOUR_API_KEY>>', 'Content-Type': 'application/json' },
    body: JSON.stringify({ id: 'flat-white', name: 'Flat white', price: 3.5, category: 'coffee' }),
  });
  const saved: { updated: boolean; isNew: boolean; product?: { id: string } } = await response.json();
  console.log(saved.isNew ? 'created' : 'updated');
  ```

  ```bash cURL theme={null}
  curl -X POST "https://api.heylua.ai/developer/agents/<<YOUR_AGENT_ID>>/products" \
    -H "Authorization: Bearer <<YOUR_API_KEY>>" \
    -H "Content-Type: application/json" \
    -d '{ "id": "flat-white", "name": "Flat white", "price": 3.5, "category": "coffee" }'
  ```
</CodeGroup>

### PUT /developer/agents/:agentId/products

Updates a product by its `id`; the body and answer are the same as `POST`, with status `200`. Unlike `POST`, it does not create: a missing product answers `{ "updated": false, "isNew": false }`.

### GET /developer/agents/:agentId/products/:productId

Returns one product as `{ "success", "message", "data" }`; a missing product answers `200` with `success: false`.

Equivalent: `Products.getById('flat-white')`.

### DELETE /developer/agents/:agentId/products/:productId

Deletes a product and answers `{ "deleted": true }`.

Equivalent: `Products.delete('flat-white')`.

### POST /developer/agents/:agentId/basket

Creates an empty, active basket for the caller.

<ParamField path="agentId" type="string" required>The agent.</ParamField>
<ParamField body="currency" type="string" required>ISO currency code, for example `USD`.</ParamField>
<ParamField body="metadata" type="object">Anything you want stored on the basket.</ParamField>

**Response**

`201` with `{ "success": true, "message": "Basket created successfully", "data": <basket> }`; `totalAmount` and `itemCount` start at 0.

Equivalent: `Baskets.create({ currency: 'USD' })`.

<CodeGroup>
  ```ts TypeScript theme={null}
  const response = await fetch('https://api.heylua.ai/developer/agents/<<YOUR_AGENT_ID>>/basket', {
    method: 'POST',
    headers: { Authorization: 'Bearer <<YOUR_API_KEY>>', 'Content-Type': 'application/json' },
    body: JSON.stringify({ currency: 'USD', metadata: { source: 'web' } }),
  });
  const created: { success: boolean; data: { id: string; common: { status: string } } } = await response.json();
  console.log(created.data.id);
  ```

  ```bash cURL theme={null}
  curl -X POST "https://api.heylua.ai/developer/agents/<<YOUR_AGENT_ID>>/basket" \
    -H "Authorization: Bearer <<YOUR_API_KEY>>" \
    -H "Content-Type: application/json" \
    -d '{ "currency": "USD", "metadata": { "source": "web" } }'
  ```
</CodeGroup>

### GET /developer/agents/:agentId/basket/user

Lists the caller's baskets.

<ParamField query="status" type="string">Only baskets in this status: `active`, `checked_out`, `abandoned`, `expired`.</ParamField>

**Response**

`200` with `{ "success": true, "message": "Found <n> baskets for user", "data": [...baskets] }`.

Equivalent: `Baskets.get('active')`.

### GET /developer/agents/:agentId/basket/:basketId

Returns one basket as `{ "success", "message", "data" }`.

Equivalent: `Baskets.getById('<basketId>')`.

### POST /developer/agents/:agentId/basket/:basketId/item

Adds an item, or raises its quantity when an item with the same `id` is already in the basket, and recomputes the totals.

<ParamField path="basketId" type="string" required>The basket.</ParamField>
<ParamField body="id" type="string" required>Product id.</ParamField>
<ParamField body="price" type="number" required>Unit price in the basket's currency.</ParamField>
<ParamField body="quantity" type="number" required>Units to add; defaults to 1 when omitted.</ParamField>
<ParamField body="*" type="any">Any other fields are stored on the item.</ParamField>

**Response**

`201` with `{ "success": true, "message": "Item added to basket successfully", "data": <basket> }`, where `common.totalAmount` is the sum of `price` times `quantity` over the items and `common.itemCount` the sum of quantities. A missing basket answers `201` with `success: false`.

Equivalent: `Baskets.addItem('<basketId>', { id: 'flat-white', price: 3.5, quantity: 2 })`.

<CodeGroup>
  ```ts TypeScript theme={null}
  const response = await fetch('https://api.heylua.ai/developer/agents/<<YOUR_AGENT_ID>>/basket/<<BASKET_ID>>/item', {
    method: 'POST',
    headers: { Authorization: 'Bearer <<YOUR_API_KEY>>', 'Content-Type': 'application/json' },
    body: JSON.stringify({ id: 'flat-white', price: 3.5, quantity: 2 }),
  });
  const basket: { success: boolean; data?: { common: { totalAmount: number; itemCount: number } } } = await response.json();
  console.log(basket.data?.common.totalAmount);
  ```

  ```bash cURL theme={null}
  curl -X POST "https://api.heylua.ai/developer/agents/<<YOUR_AGENT_ID>>/basket/<<BASKET_ID>>/item" \
    -H "Authorization: Bearer <<YOUR_API_KEY>>" \
    -H "Content-Type: application/json" \
    -d '{ "id": "flat-white", "price": 3.5, "quantity": 2 }'
  ```
</CodeGroup>

### DELETE /developer/agents/:agentId/basket/:basketId/item/:itemId

Removes the item whose `id` is `itemId` and recomputes the totals; answers the basket.

Equivalent: `Baskets.removeItem('<basketId>', 'flat-white')`.

### DELETE /developer/agents/:agentId/basket/:basketId/clear

Removes every item and keeps the basket; answers the basket.

Equivalent: `Baskets.clear('<basketId>')`.

### PUT /developer/agents/:agentId/basket/:basketId/metadata

Replaces the basket's metadata with the request body.

<ParamField body="*" type="object" required>The new metadata object itself, not wrapped in a `metadata` key.</ParamField>

**Response**

`200` with `{ "success": true, "message": "Basket metadata updated successfully" }`.

Equivalent: `Baskets.updateMetadata('<basketId>', { source: 'web' })`.

### PUT /developer/agents/:agentId/basket/:basketId/:status

Sets the basket's status.

<ParamField path="status" type="string" required>`active`, `checked_out`, `abandoned`, or `expired`.</ParamField>

**Response**

`200` with `{ "success": true, "message": "Basket status updated to <status>", "data": <basket> }`.

Equivalent: `Baskets.updateStatus('<basketId>', 'abandoned')`.

### POST /developer/agents/:agentId/order

Places an order from an active, non-empty basket.

<ParamField path="agentId" type="string" required>The agent.</ParamField>
<ParamField body="basketId" type="string" required>The basket to order.</ParamField>
<ParamField body="data" type="object">Shipping, billing, payment, or any other order data, stored on the order.</ParamField>

**Response**

`201` with `{ "success": true, "message": "Order created successfully", "data": <order> }`; the order starts as `pending` and records the basket's `userId` and `basketId`. A basket that is not `active` answers `201` with `success: false` and `Cannot create order from basket with status '<status>'. Basket must be active.`; an empty one `Cannot create order from empty basket`.

Equivalent: `Baskets.placeOrder({ shipping: { ... } }, '<basketId>')` or `Orders.create({ basketId })`.

<CodeGroup>
  ```ts TypeScript theme={null}
  const response = await fetch('https://api.heylua.ai/developer/agents/<<YOUR_AGENT_ID>>/order', {
    method: 'POST',
    headers: { Authorization: 'Bearer <<YOUR_API_KEY>>', 'Content-Type': 'application/json' },
    body: JSON.stringify({ basketId: '<<BASKET_ID>>', data: { pickup: 'counter' } }),
  });
  const placed: { success: boolean; message: string; data?: { id: string; common: { status: string } } } = await response.json();
  if (!placed.success) throw new Error(placed.message);
  console.log(placed.data?.id);
  ```

  ```bash cURL theme={null}
  curl -X POST "https://api.heylua.ai/developer/agents/<<YOUR_AGENT_ID>>/order" \
    -H "Authorization: Bearer <<YOUR_API_KEY>>" \
    -H "Content-Type: application/json" \
    -d '{ "basketId": "<<BASKET_ID>>", "data": { "pickup": "counter" } }'
  ```
</CodeGroup>

### GET /developer/agents/:agentId/order/user

Lists the caller's orders.

<ParamField query="status" type="string">Only orders in this status: `pending`, `confirmed`, `fullfilled`, `cancelled`.</ParamField>

**Response**

`200` with `{ "success": true, "message": "Found <n> orders for user", "data": [...orders] }`.

Equivalent: `Orders.get('pending')`.

### GET /developer/agents/:agentId/order/:orderId

Returns one order as `{ "success", "message", "data" }`.

Equivalent: `Orders.getById('<orderId>')`.

### PUT /developer/agents/:agentId/order/:orderId/:status

Sets the order's status.

<ParamField path="status" type="string" required>`pending`, `confirmed`, `fullfilled`, or `cancelled`.</ParamField>

**Response**

`200` with `{ "success": true, "message": "Order status updated to <status>", "data": <order> }`.

Equivalent: `Orders.updateStatus('confirmed', '<orderId>')`.

<CodeGroup>
  ```ts TypeScript theme={null}
  const response = await fetch('https://api.heylua.ai/developer/agents/<<YOUR_AGENT_ID>>/order/<<ORDER_ID>>/confirmed', {
    method: 'PUT',
    headers: { Authorization: 'Bearer <<YOUR_API_KEY>>' },
  });
  const updated: { success: boolean; message: string } = await response.json();
  console.log(updated.message);
  ```

  ```bash cURL theme={null}
  curl -X PUT "https://api.heylua.ai/developer/agents/<<YOUR_AGENT_ID>>/order/<<ORDER_ID>>/confirmed" \
    -H "Authorization: Bearer <<YOUR_API_KEY>>"
  ```
</CodeGroup>

### PUT /developer/agents/:agentId/order/:orderId

Merges the request body into the order's `data`.

<ParamField body="*" type="object" required>The fields to store on the order.</ParamField>

**Response**

`200` with `{ "success": true, "message": "Order data updated successfully" }`.

Equivalent: `Orders.updateData({ trackingNumber: '1Z999' }, '<orderId>')`.

## See also

* [`Products`](/reference/sdk/products), [`Baskets`](/reference/sdk/baskets), [`Orders`](/reference/sdk/orders) — the same catalog, baskets, and orders from agent code
* [Lua Query](/reference/sdk/query) — the product `filter` grammar
* [Formatting components](/channels/formatting/overview) — rendering products and a payment step in a reply
* [REST API overview](/reference/rest/overview) — authentication, scopes, and pagination
