Skip to content
DocsDashboard

Product lists (product_list)

Show 2-24 products as a grid or list; the person picks one or several; the page records the picks and buys nothing. The result is { items: [{ productId, quantity }] }.

When to use it: The person picks one product, or several with quantities, from 2-24 search results or recommendations shown as a grid or list. The page only records the picks: add them to a cart or order them yourself afterwards, and do not tell the person it is done until you have.

When not to use it: Do not use it for one product (use product) or for more than 24 products: narrow the list first.

Gather first:

  • Products that match what the person asked for, with real prices and photos.
  • Whether they pick one (mode: "single") or several to fill a cart ("multiple").

Writing good content:

  • Include a photo for every product: a grid without photos is hard to scan.
  • Use categories and each product's categoryId when the list mixes kinds, so the person can filter.
  • Use compareAtPrice only for a real original price, higher than price.
  • The answer is the picked products and quantities. Act on them yourself afterwards.

Fields

Every page also has version (always 1), title, and optionally description, branding, cover (a header photo) and expiresAt; see Page fields. These are the fields of page (the page type and its data):

FieldDescription
productsobject[], required. The products in display order (2-24). For one product use the product page type. Limits: 2-24 items.
products[].idstring, required. Your product id (or SKU, when each card is one size or color). Returned as productId. Limits: matches ^[A-Za-z0-9_-]{1,64}$.
products[].namestring, required. Product name, e.g. "Canvas market tote". Keep it short: cards are narrow. Limits: 1-120 characters.
products[].priceobject, required. Price for one item.
products[].price.amountnumber, required. Amount in major units, e.g. 249.99 for $249.99. Limits: 0-10000000.
products[].price.currencystring, required. ISO 4217 currency code, uppercase, e.g. "USD", "EUR".
products[].photoobject. Optional main photo, shown at 4:5. Strongly recommended: a grid without photos is hard to scan.
products[].photo.urlhttps URL, required. https URL of a product photo (JPEG, PNG or WebP), e.g. "https://shop.example/linen-shirt-front.jpg".
products[].photo.altstring. Optional. What the photo shows, for screen readers, e.g. "Sage linen shirt, front view". Omit to use the product name. Limits: 1-200 characters.
products[].brandstring. Optional brand, shown above the name. Limits: 1-60 characters.
products[].descriptionstring. Optional one-line detail, e.g. "Sage · Size M" or "Serves 4". Shown in list layout and under the name. Limits: 1-200 characters.
products[].compareAtPriceobject. Optional original price when on sale; must be higher than price.
products[].compareAtPrice.amountnumber, required. Amount in major units, e.g. 249.99 for $249.99. Limits: 0-10000000.
products[].compareAtPrice.currencystring, required. ISO 4217 currency code, uppercase, e.g. "USD", "EUR".
products[].ratingnumber. Optional average review score from 0 to 5, e.g. 4.6. Include only real reviews. Limits: 0-5.
products[].reviewCountinteger. Optional number of reviews behind rating, e.g. 128. Only with rating. Limits: 0-10000000.
products[].badgestring. Optional short label on the photo, e.g. "New", "Bestseller". Limits: 1-20 characters.
products[].availableboolean. Optional. false shows the product as sold out and it cannot be picked. Omit when it can be bought.
products[].categoryIdstring. Optional id of one of categories, for the filter chips. Limits: matches ^[A-Za-z0-9_-]{1,64}$.
mode"single" | "multiple". Optional. "single" (default): the person picks one product. "multiple": they pick several, e.g. to fill a cart.
maxSelectionsinteger. Optional, mode "multiple" only: the most products the person may pick. Limits: 2-24.
maxQuantityinteger. Optional, mode "multiple" only: show a quantity stepper on picked products, up to this many each. Omit for one of each. Limits: 2-99.
layout"grid" | "list". Optional. "grid" (default, 2 columns on phones) for photo-led products; "list" for rows with more text.
categoriesobject[]. Optional filter chips (2-8) above the products, with an "All" chip first. Give each product a categoryId. Limits: 2-8 items.
categories[].idstring, required. Stable identifier you choose. It is returned in the result, so keep it meaningful to you. Limits: matches ^[A-Za-z0-9_-]{1,64}$.
categories[].labelstring, required. Chip text, e.g. "Tops". Limits: 1-24 characters.
submitLabelstring. Optional button text for the choice. Default "Choose". Not "Buy" or "Add to bag": the page buys nothing. Limits: 1-24 characters, matches ^[^<>]*$.

Why these are required: Each card needs an id, a name and a price; 2-24 products.

When optional fields are left out: Single choice in a 2-column grid; no filter chips; every product can be picked once; a grey box instead of a photo. maxSelections and maxQuantity need mode multiple. Tapping a card selects it; there is no detail sheet and no per-product options (send a product page for that, or list each size or color as its own card).

Minimal example

Only the required fields:

{
  "version": 1,
  "title": "Which one do you want?",
  "page": {
    "type": "product_list",
    "products": [
      {
        "id": "mug",
        "name": "Enamel camp mug",
        "price": {
          "amount": 18,
          "currency": "USD"
        }
      },
      {
        "id": "bottle",
        "name": "Steel water bottle",
        "price": {
          "amount": 29,
          "currency": "USD"
        }
      },
      {
        "id": "flask",
        "name": "Insulated coffee flask",
        "price": {
          "amount": 36,
          "currency": "USD"
        }
      }
    ]
  }
}

Full example

{
  "version": 1,
  "title": "Gift ideas under $100",
  "description": "Six picks for your sister. Tap the one you like.",
  "branding": {
    "name": "Juniper Market",
    "accentColor": "#2f5d50"
  },
  "page": {
    "type": "product_list",
    "products": [
      {
        "id": "linen-shirt-sage-m",
        "name": "Linen camp shirt",
        "description": "Sage · M",
        "price": {
          "amount": 68,
          "currency": "USD"
        },
        "photo": {
          "url": "https://images.example.test/shop/linen-shirt-sage-1.jpg",
          "alt": "Sage linen camp shirt"
        },
        "rating": 4.7,
        "reviewCount": 212,
        "badge": "New",
        "categoryId": "tops"
      },
      {
        "id": "canvas-tote",
        "name": "Canvas market tote",
        "price": {
          "amount": 32,
          "currency": "USD"
        },
        "photo": {
          "url": "https://images.example.test/shop/canvas-tote-1.jpg",
          "alt": "Natural canvas tote bag with leather handles"
        },
        "rating": 4.9,
        "reviewCount": 540,
        "badge": "Bestseller",
        "categoryId": "bags"
      },
      {
        "id": "wool-throw",
        "name": "Waffle wool throw",
        "price": {
          "amount": 96,
          "currency": "USD"
        },
        "compareAtPrice": {
          "amount": 128,
          "currency": "USD"
        },
        "photo": {
          "url": "https://images.example.test/shop/wool-throw-1.jpg",
          "alt": "Oatmeal waffle-knit wool throw"
        },
        "categoryId": "home"
      },
      {
        "id": "stoneware-mug",
        "name": "Speckled stoneware mug",
        "price": {
          "amount": 24,
          "currency": "USD"
        },
        "photo": {
          "url": "https://images.example.test/shop/stoneware-mug-1.jpg",
          "alt": "Speckled cream stoneware mug"
        },
        "rating": 4.6,
        "reviewCount": 88,
        "categoryId": "home"
      },
      {
        "id": "cord-cap",
        "name": "Corduroy cap",
        "price": {
          "amount": 38,
          "currency": "USD"
        },
        "photo": {
          "url": "https://images.example.test/shop/cord-cap-1.jpg",
          "alt": "Rust corduroy six-panel cap"
        },
        "available": false,
        "categoryId": "bags"
      },
      {
        "id": "merino-tee",
        "name": "Merino crew tee",
        "description": "Charcoal · L",
        "price": {
          "amount": 54,
          "currency": "USD"
        },
        "photo": {
          "url": "https://images.example.test/shop/merino-tee-1.jpg",
          "alt": "Charcoal merino crew-neck tee, folded"
        },
        "categoryId": "tops"
      }
    ],
    "categories": [
      {
        "id": "tops",
        "label": "Tops"
      },
      {
        "id": "bags",
        "label": "Bags & hats"
      },
      {
        "id": "home",
        "label": "Home"
      }
    ]
  }
}

Result

When the person submits, the result has these fields:

FieldDescription
itemsobject[], required. The picked products, in display order. Exactly one in mode "single". Limits: 1-24 items.
items[].productIdstring, required. id of a picked product. Limits: matches ^[A-Za-z0-9_-]{1,64}$.
items[].quantityinteger, required. How many of it. 1 unless the spec set maxQuantity. Limits: 1-99.

For the full example above, a result could be:

{
  "items": [
    {
      "productId": "canvas-tote",
      "quantity": 1
    }
  ]
}

Send it

eve, AI SDK and MCP

With @pagesystems/eve, @pagesystems/ai-sdk or the MCP server, the model calls get_page_type_schema with "product_list" (the same JSON as GET /v1/page-types/product_list), then create_page with this input (no version or branding; branding comes from your dashboard):

{
  "title": "Gift ideas under $100",
  "description": "Six picks for your sister. Tap the one you like.",
  "page": {
    "type": "product_list",
    "products": [
      {
        "id": "linen-shirt-sage-m",
        "name": "Linen camp shirt",
        "description": "Sage · M",
        "price": {
          "amount": 68,
          "currency": "USD"
        },
        "photo": {
          "url": "https://images.example.test/shop/linen-shirt-sage-1.jpg",
          "alt": "Sage linen camp shirt"
        },
        "rating": 4.7,
        "reviewCount": 212,
        "badge": "New",
        "categoryId": "tops"
      },
      {
        "id": "canvas-tote",
        "name": "Canvas market tote",
        "price": {
          "amount": 32,
          "currency": "USD"
        },
        "photo": {
          "url": "https://images.example.test/shop/canvas-tote-1.jpg",
          "alt": "Natural canvas tote bag with leather handles"
        },
        "rating": 4.9,
        "reviewCount": 540,
        "badge": "Bestseller",
        "categoryId": "bags"
      },
      {
        "id": "wool-throw",
        "name": "Waffle wool throw",
        "price": {
          "amount": 96,
          "currency": "USD"
        },
        "compareAtPrice": {
          "amount": 128,
          "currency": "USD"
        },
        "photo": {
          "url": "https://images.example.test/shop/wool-throw-1.jpg",
          "alt": "Oatmeal waffle-knit wool throw"
        },
        "categoryId": "home"
      },
      {
        "id": "stoneware-mug",
        "name": "Speckled stoneware mug",
        "price": {
          "amount": 24,
          "currency": "USD"
        },
        "photo": {
          "url": "https://images.example.test/shop/stoneware-mug-1.jpg",
          "alt": "Speckled cream stoneware mug"
        },
        "rating": 4.6,
        "reviewCount": 88,
        "categoryId": "home"
      },
      {
        "id": "cord-cap",
        "name": "Corduroy cap",
        "price": {
          "amount": 38,
          "currency": "USD"
        },
        "photo": {
          "url": "https://images.example.test/shop/cord-cap-1.jpg",
          "alt": "Rust corduroy six-panel cap"
        },
        "available": false,
        "categoryId": "bags"
      },
      {
        "id": "merino-tee",
        "name": "Merino crew tee",
        "description": "Charcoal · L",
        "price": {
          "amount": 54,
          "currency": "USD"
        },
        "photo": {
          "url": "https://images.example.test/shop/merino-tee-1.jpg",
          "alt": "Charcoal merino crew-neck tee, folded"
        },
        "categoryId": "tops"
      }
    ],
    "categories": [
      {
        "id": "tops",
        "label": "Tops"
      },
      {
        "id": "bags",
        "label": "Bags & hats"
      },
      {
        "id": "home",
        "label": "Home"
      }
    ]
  }
}

REST

curl https://pagesystems.ai/v1/pages \
  -H "Authorization: Bearer $PAGE_SYSTEMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"spec":{"version":1,"title":"Gift ideas under $100","description":"Six picks for your sister. Tap the one you like.","branding":{"name":"Juniper Market","accentColor":"#2f5d50"},"page":{"type":"product_list","products":[{"id":"linen-shirt-sage-m","name":"Linen camp shirt","description":"Sage · M","price":{"amount":68,"currency":"USD"},"photo":{"url":"https://images.example.test/shop/linen-shirt-sage-1.jpg","alt":"Sage linen camp shirt"},"rating":4.7,"reviewCount":212,"badge":"New","categoryId":"tops"},{"id":"canvas-tote","name":"Canvas market tote","price":{"amount":32,"currency":"USD"},"photo":{"url":"https://images.example.test/shop/canvas-tote-1.jpg","alt":"Natural canvas tote bag with leather handles"},"rating":4.9,"reviewCount":540,"badge":"Bestseller","categoryId":"bags"},{"id":"wool-throw","name":"Waffle wool throw","price":{"amount":96,"currency":"USD"},"compareAtPrice":{"amount":128,"currency":"USD"},"photo":{"url":"https://images.example.test/shop/wool-throw-1.jpg","alt":"Oatmeal waffle-knit wool throw"},"categoryId":"home"},{"id":"stoneware-mug","name":"Speckled stoneware mug","price":{"amount":24,"currency":"USD"},"photo":{"url":"https://images.example.test/shop/stoneware-mug-1.jpg","alt":"Speckled cream stoneware mug"},"rating":4.6,"reviewCount":88,"categoryId":"home"},{"id":"cord-cap","name":"Corduroy cap","price":{"amount":38,"currency":"USD"},"photo":{"url":"https://images.example.test/shop/cord-cap-1.jpg","alt":"Rust corduroy six-panel cap"},"available":false,"categoryId":"bags"},{"id":"merino-tee","name":"Merino crew tee","description":"Charcoal · L","price":{"amount":54,"currency":"USD"},"photo":{"url":"https://images.example.test/shop/merino-tee-1.jpg","alt":"Charcoal merino crew-neck tee, folded"},"categoryId":"tops"}],"categories":[{"id":"tops","label":"Tops"},{"id":"bags","label":"Bags & hats"},{"id":"home","label":"Home"}]}}}'

Validation errors

A spec that does not validate is refused with 400 invalid_spec. The message names each problem and its path, so a model can fix the spec and try again. For example:

Leaving out the required field products:

✖ Invalid input: expected array, received undefined
  → at page.products

Misspelling a field name (unknown keys are rejected):

✖ Unrecognized key: "colour"
  → at page