Skip to content
DocsDashboard

Products (product)

Show one product with photos, price and options. The person picks options and a quantity; the page records the choice and buys nothing. The result is { productId, variantId?, selections?, quantity }.

When to use it: The person looks at one product (photos, price, sizes or colors) and picks options and a quantity, e.g. before you add it to their cart. The page only records the choice: add it to the cart or order it yourself afterwards, and do not tell the person it is done until you have.

When not to use it: Do not use it for several products (use product_list) or for a cart the person already built (use checkout).

Gather first:

  • The product's real price, photos and options (size, color) from your catalog.
  • Which option combinations exist and which are sold out.

Writing good content:

  • List options (e.g. Size, Color) and one variants entry per real combination with your SKU as id; mark sold-out ones available: false instead of leaving them out.
  • Put the main photo first; use a variant's photoIndex to show the right photo for a color.
  • Use stock only for notes from your data, written as you would say them.
  • The answer is the product, variant and quantity. Add it to a cart or order yourself; the page never buys anything.

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
idstring, required. Your product id. Returned as productId. Limits: matches ^[A-Za-z0-9_-]{1,64}$.
namestring, required. Product name as a shopper would say it, e.g. "Linen camp shirt". No brand, size or color in the name. Limits: 1-120 characters.
priceobject, required. Current price for one item. With variants, the price of combinations without their own price.
price.amountnumber, required. Amount in major units, e.g. 249.99 for $249.99. Limits: 0-10000000.
price.currencystring, required. ISO 4217 currency code, uppercase, e.g. "USD", "EUR".
compareAtPriceobject. Optional original price when the product is on sale; it must be higher than price. Shown struck through with the saving.
compareAtPrice.amountnumber, required. Amount in major units, e.g. 249.99 for $249.99. Limits: 0-10000000.
compareAtPrice.currencystring, required. ISO 4217 currency code, uppercase, e.g. "USD", "EUR".
photosobject[]. Optional product photos (1-8), the main photo first. The person swipes through them. Limits: 1-8 items.
photos[].urlhttps URL, required. https URL of a product photo (JPEG, PNG or WebP), e.g. "https://shop.example/linen-shirt-front.jpg".
photos[].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.
brandstring. Optional brand or maker, shown above the name, e.g. "Juniper Market". Limits: 1-60 characters.
descriptionstring. Optional plain-text description. Separate paragraphs with a blank line. No HTML or Markdown. Limits: 1-2000 characters.
ratingnumber. Optional average review score from 0 to 5, e.g. 4.6. Include only real reviews. Limits: 0-5.
reviewCountinteger. Optional number of reviews behind rating, e.g. 128. Only with rating. Limits: 0-10000000.
badgesstring[]. Optional short labels such as "New" or "Organic cotton" (up to 3). Limits: at most 3 items.
variantOptionsobject[]. Optional choices the person must make, such as Size and Color (1-3 options). The result then has selections. Limits: 1-3 items.
variantOptions[].namestring, required. Option name shown above its choices, e.g. "Size" or "Color". Limits: 1-24 characters.
variantOptions[].valuesobject[], required. The choices in display order (1-20), e.g. [{ "value": "S" }, { "value": "M" }]. Limits: 1-20 items.
variantOptions[].values[].valuestring, required. Choice text, e.g. "M", "Sage", "32 x 30". It is shown on the chip and returned in selections. Limits: 1-32 characters.
variantOptions[].values[].swatchstring. Optional color for a color choice, "#rrggbb". The chip shows the color next to the name, never the color alone. Limits: matches ^#[0-9a-fA-F]{6}$.
variantsobject[]. Optional. Every combination you sell, each with its own id, availability, price or photo. Needs variantOptions. Combinations you leave out cannot be chosen. Without variants every combination is available at the product price. Limits: 1-100 items.
variants[].idstring, required. Your id for this exact combination (SKU), e.g. "shirt-sage-m". Returned as variantId. Limits: matches ^[A-Za-z0-9_-]{1,64}$.
variants[].selectionsobject, required. One value per option, by option name, e.g. { "Color": "Sage", "Size": "M" }.
variants[].availableboolean. Optional. false shows this combination as sold out. Omit when it can be bought.
variants[].priceobject. Optional price for this combination when it differs from the product price.
variants[].price.amountnumber, required. Amount in major units, e.g. 249.99 for $249.99. Limits: 0-10000000.
variants[].price.currencystring, required. ISO 4217 currency code, uppercase, e.g. "USD", "EUR".
variants[].compareAtPriceobject. Optional original price for this combination when it is on sale.
variants[].compareAtPrice.amountnumber, required. Amount in major units, e.g. 249.99 for $249.99. Limits: 0-10000000.
variants[].compareAtPrice.currencystring, required. ISO 4217 currency code, uppercase, e.g. "USD", "EUR".
variants[].photoIndexinteger. Optional index into photos (0 is the first) shown when this combination is chosen. Limits: 0-7.
quantityobject. Optional quantity stepper. Omit for a quantity of exactly one.
quantity.mininteger. Optional smallest quantity. Default 1. Limits: 1-99.
quantity.maxinteger. Optional largest quantity, e.g. stock or a per-order limit. Default 10. Limits: 1-99.
quantity.defaultinteger. Optional starting quantity. Default min. Limits: 1-99.
detailsobject[]. Optional facts listed under the description, e.g. Material, Fit, Shipping, Returns (1-10 rows). Limits: 1-10 items.
details[].labelstring, required. e.g. "Material". Limits: 1-40 characters.
details[].valuestring, required. e.g. "100% European linen". Limits: 1-300 characters.
stockstring. Optional stock note from your data, shown as written, e.g. "Only 3 left" or "Ships in 2 days". Page Systems does not check or hold stock. Limits: 1-60 characters.
submitLabelstring. Optional button text for the choice, e.g. "Choose" (the default) or "Choose this". Not "Buy" or "Add to bag": the page buys nothing. Limits: 1-24 characters, matches ^[^<>]*$.

Why these are required: A product needs an id for the result, a name and a price.

When optional fields are left out: No gallery, brand, description, sale styling, rating, badges or details; no option pickers (the result has no selections); quantity is 1 and there is no stepper (with quantity: min 1, max 10). With variantOptions but no variants, every combination can be chosen at price; with variants, combinations not listed or available: false show as sold out and cannot be chosen, and the result carries variantId. Photos without alt are described as "<name>, photo 2 of 4". The button says "Choose" (or submitLabel), and names the missing choice ("Choose a size") until the answer is complete; with a quantity above 1 the total is shown under the stepper.

Minimal example

Only the required fields:

{
  "version": 1,
  "title": "Want this one?",
  "page": {
    "type": "product",
    "id": "enamel-mug",
    "name": "Enamel camp mug",
    "price": {
      "amount": 18,
      "currency": "USD"
    }
  }
}

Full example

{
  "version": 1,
  "title": "The shirt you asked about",
  "description": "Pick a color and size.",
  "branding": {
    "name": "Juniper Market",
    "accentColor": "#2f5d50"
  },
  "page": {
    "type": "product",
    "id": "linen-camp-shirt",
    "name": "Linen camp shirt",
    "brand": "Juniper Market",
    "price": {
      "amount": 68,
      "currency": "USD"
    },
    "photos": [
      {
        "url": "https://images.example.test/shop/linen-shirt-sage-1.jpg",
        "alt": "Sage linen camp shirt on a hanger, front view"
      },
      {
        "url": "https://images.example.test/shop/linen-shirt-sage-2.jpg",
        "alt": "Close-up of the sage linen weave and coconut buttons"
      },
      {
        "url": "https://images.example.test/shop/linen-shirt-sand-1.jpg",
        "alt": "Sand linen camp shirt, folded"
      },
      {
        "url": "https://images.example.test/shop/linen-shirt-ink-1.jpg",
        "alt": "Ink blue linen camp shirt worn open over a white tee"
      }
    ],
    "rating": 4.7,
    "reviewCount": 212,
    "badges": [
      "New"
    ],
    "stock": "Only 2 left in Ink, size L",
    "variantOptions": [
      {
        "name": "Color",
        "values": [
          {
            "value": "Sage",
            "swatch": "#9caf88"
          },
          {
            "value": "Sand",
            "swatch": "#d8c7a8"
          },
          {
            "value": "Ink",
            "swatch": "#2b3440"
          }
        ]
      },
      {
        "name": "Size",
        "values": [
          {
            "value": "XS"
          },
          {
            "value": "S"
          },
          {
            "value": "M"
          },
          {
            "value": "L"
          },
          {
            "value": "XL"
          }
        ]
      }
    ],
    "variants": [
      {
        "id": "shirt-sage-xs",
        "selections": {
          "Color": "Sage",
          "Size": "XS"
        },
        "photoIndex": 0
      },
      {
        "id": "shirt-sage-s",
        "selections": {
          "Color": "Sage",
          "Size": "S"
        },
        "photoIndex": 0
      },
      {
        "id": "shirt-sage-m",
        "selections": {
          "Color": "Sage",
          "Size": "M"
        },
        "photoIndex": 0
      },
      {
        "id": "shirt-sage-l",
        "selections": {
          "Color": "Sage",
          "Size": "L"
        },
        "photoIndex": 0
      },
      {
        "id": "shirt-sage-xl",
        "selections": {
          "Color": "Sage",
          "Size": "XL"
        },
        "photoIndex": 0,
        "available": false,
        "price": {
          "amount": 72,
          "currency": "USD"
        }
      },
      {
        "id": "shirt-sand-xs",
        "selections": {
          "Color": "Sand",
          "Size": "XS"
        },
        "photoIndex": 2,
        "available": false
      },
      {
        "id": "shirt-sand-s",
        "selections": {
          "Color": "Sand",
          "Size": "S"
        },
        "photoIndex": 2
      },
      {
        "id": "shirt-sand-m",
        "selections": {
          "Color": "Sand",
          "Size": "M"
        },
        "photoIndex": 2
      },
      {
        "id": "shirt-sand-l",
        "selections": {
          "Color": "Sand",
          "Size": "L"
        },
        "photoIndex": 2
      },
      {
        "id": "shirt-sand-xl",
        "selections": {
          "Color": "Sand",
          "Size": "XL"
        },
        "photoIndex": 2,
        "price": {
          "amount": 72,
          "currency": "USD"
        }
      },
      {
        "id": "shirt-ink-xs",
        "selections": {
          "Color": "Ink",
          "Size": "XS"
        },
        "photoIndex": 3
      },
      {
        "id": "shirt-ink-s",
        "selections": {
          "Color": "Ink",
          "Size": "S"
        },
        "photoIndex": 3
      },
      {
        "id": "shirt-ink-m",
        "selections": {
          "Color": "Ink",
          "Size": "M"
        },
        "photoIndex": 3
      },
      {
        "id": "shirt-ink-l",
        "selections": {
          "Color": "Ink",
          "Size": "L"
        },
        "photoIndex": 3
      },
      {
        "id": "shirt-ink-xl",
        "selections": {
          "Color": "Ink",
          "Size": "XL"
        },
        "photoIndex": 3,
        "available": false,
        "price": {
          "amount": 72,
          "currency": "USD"
        }
      }
    ],
    "quantity": {
      "max": 5
    },
    "description": "A relaxed camp-collar shirt in washed European linen. It softens with every wash and keeps you cool on warm days.\n\nBoxy through the body with a straight hem, so it looks right tucked or untucked.",
    "details": [
      {
        "label": "Material",
        "value": "100% European linen, garment washed"
      },
      {
        "label": "Fit",
        "value": "Relaxed. Size down for a closer fit."
      },
      {
        "label": "Shipping",
        "value": "Free over $100. Ships in 1–2 business days."
      },
      {
        "label": "Returns",
        "value": "Free returns within 30 days"
      }
    ]
  }
}

Result

When the person submits, the result has these fields:

FieldDescription
productIdstring, required. The product id from the spec. Limits: matches ^[A-Za-z0-9_-]{1,64}$.
variantIdstring. id of the chosen variant. Present when the spec lists variants. Limits: matches ^[A-Za-z0-9_-]{1,64}$.
selectionsobject. The chosen value per option, e.g. { "Color": "Sage", "Size": "M" }. Present when the spec has variantOptions.
quantityinteger, required. How many the person wants. Limits: 1-99.

For the full example above, a result could be:

{
  "productId": "linen-camp-shirt",
  "variantId": "shirt-sage-m",
  "selections": {
    "Color": "Sage",
    "Size": "M"
  },
  "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" (the same JSON as GET /v1/page-types/product), then create_page with this input (no version or branding; branding comes from your dashboard):

{
  "title": "The shirt you asked about",
  "description": "Pick a color and size.",
  "page": {
    "type": "product",
    "id": "linen-camp-shirt",
    "name": "Linen camp shirt",
    "brand": "Juniper Market",
    "price": {
      "amount": 68,
      "currency": "USD"
    },
    "photos": [
      {
        "url": "https://images.example.test/shop/linen-shirt-sage-1.jpg",
        "alt": "Sage linen camp shirt on a hanger, front view"
      },
      {
        "url": "https://images.example.test/shop/linen-shirt-sage-2.jpg",
        "alt": "Close-up of the sage linen weave and coconut buttons"
      },
      {
        "url": "https://images.example.test/shop/linen-shirt-sand-1.jpg",
        "alt": "Sand linen camp shirt, folded"
      },
      {
        "url": "https://images.example.test/shop/linen-shirt-ink-1.jpg",
        "alt": "Ink blue linen camp shirt worn open over a white tee"
      }
    ],
    "rating": 4.7,
    "reviewCount": 212,
    "badges": [
      "New"
    ],
    "stock": "Only 2 left in Ink, size L",
    "variantOptions": [
      {
        "name": "Color",
        "values": [
          {
            "value": "Sage",
            "swatch": "#9caf88"
          },
          {
            "value": "Sand",
            "swatch": "#d8c7a8"
          },
          {
            "value": "Ink",
            "swatch": "#2b3440"
          }
        ]
      },
      {
        "name": "Size",
        "values": [
          {
            "value": "XS"
          },
          {
            "value": "S"
          },
          {
            "value": "M"
          },
          {
            "value": "L"
          },
          {
            "value": "XL"
          }
        ]
      }
    ],
    "variants": [
      {
        "id": "shirt-sage-xs",
        "selections": {
          "Color": "Sage",
          "Size": "XS"
        },
        "photoIndex": 0
      },
      {
        "id": "shirt-sage-s",
        "selections": {
          "Color": "Sage",
          "Size": "S"
        },
        "photoIndex": 0
      },
      {
        "id": "shirt-sage-m",
        "selections": {
          "Color": "Sage",
          "Size": "M"
        },
        "photoIndex": 0
      },
      {
        "id": "shirt-sage-l",
        "selections": {
          "Color": "Sage",
          "Size": "L"
        },
        "photoIndex": 0
      },
      {
        "id": "shirt-sage-xl",
        "selections": {
          "Color": "Sage",
          "Size": "XL"
        },
        "photoIndex": 0,
        "available": false,
        "price": {
          "amount": 72,
          "currency": "USD"
        }
      },
      {
        "id": "shirt-sand-xs",
        "selections": {
          "Color": "Sand",
          "Size": "XS"
        },
        "photoIndex": 2,
        "available": false
      },
      {
        "id": "shirt-sand-s",
        "selections": {
          "Color": "Sand",
          "Size": "S"
        },
        "photoIndex": 2
      },
      {
        "id": "shirt-sand-m",
        "selections": {
          "Color": "Sand",
          "Size": "M"
        },
        "photoIndex": 2
      },
      {
        "id": "shirt-sand-l",
        "selections": {
          "Color": "Sand",
          "Size": "L"
        },
        "photoIndex": 2
      },
      {
        "id": "shirt-sand-xl",
        "selections": {
          "Color": "Sand",
          "Size": "XL"
        },
        "photoIndex": 2,
        "price": {
          "amount": 72,
          "currency": "USD"
        }
      },
      {
        "id": "shirt-ink-xs",
        "selections": {
          "Color": "Ink",
          "Size": "XS"
        },
        "photoIndex": 3
      },
      {
        "id": "shirt-ink-s",
        "selections": {
          "Color": "Ink",
          "Size": "S"
        },
        "photoIndex": 3
      },
      {
        "id": "shirt-ink-m",
        "selections": {
          "Color": "Ink",
          "Size": "M"
        },
        "photoIndex": 3
      },
      {
        "id": "shirt-ink-l",
        "selections": {
          "Color": "Ink",
          "Size": "L"
        },
        "photoIndex": 3
      },
      {
        "id": "shirt-ink-xl",
        "selections": {
          "Color": "Ink",
          "Size": "XL"
        },
        "photoIndex": 3,
        "available": false,
        "price": {
          "amount": 72,
          "currency": "USD"
        }
      }
    ],
    "quantity": {
      "max": 5
    },
    "description": "A relaxed camp-collar shirt in washed European linen. It softens with every wash and keeps you cool on warm days.\n\nBoxy through the body with a straight hem, so it looks right tucked or untucked.",
    "details": [
      {
        "label": "Material",
        "value": "100% European linen, garment washed"
      },
      {
        "label": "Fit",
        "value": "Relaxed. Size down for a closer fit."
      },
      {
        "label": "Shipping",
        "value": "Free over $100. Ships in 1–2 business days."
      },
      {
        "label": "Returns",
        "value": "Free returns within 30 days"
      }
    ]
  }
}

REST

curl https://pagesystems.ai/v1/pages \
  -H "Authorization: Bearer $PAGE_SYSTEMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"spec":{"version":1,"title":"The shirt you asked about","description":"Pick a color and size.","branding":{"name":"Juniper Market","accentColor":"#2f5d50"},"page":{"type":"product","id":"linen-camp-shirt","name":"Linen camp shirt","brand":"Juniper Market","price":{"amount":68,"currency":"USD"},"photos":[{"url":"https://images.example.test/shop/linen-shirt-sage-1.jpg","alt":"Sage linen camp shirt on a hanger, front view"},{"url":"https://images.example.test/shop/linen-shirt-sage-2.jpg","alt":"Close-up of the sage linen weave and coconut buttons"},{"url":"https://images.example.test/shop/linen-shirt-sand-1.jpg","alt":"Sand linen camp shirt, folded"},{"url":"https://images.example.test/shop/linen-shirt-ink-1.jpg","alt":"Ink blue linen camp shirt worn open over a white tee"}],"rating":4.7,"reviewCount":212,"badges":["New"],"stock":"Only 2 left in Ink, size L","variantOptions":[{"name":"Color","values":[{"value":"Sage","swatch":"#9caf88"},{"value":"Sand","swatch":"#d8c7a8"},{"value":"Ink","swatch":"#2b3440"}]},{"name":"Size","values":[{"value":"XS"},{"value":"S"},{"value":"M"},{"value":"L"},{"value":"XL"}]}],"variants":[{"id":"shirt-sage-xs","selections":{"Color":"Sage","Size":"XS"},"photoIndex":0},{"id":"shirt-sage-s","selections":{"Color":"Sage","Size":"S"},"photoIndex":0},{"id":"shirt-sage-m","selections":{"Color":"Sage","Size":"M"},"photoIndex":0},{"id":"shirt-sage-l","selections":{"Color":"Sage","Size":"L"},"photoIndex":0},{"id":"shirt-sage-xl","selections":{"Color":"Sage","Size":"XL"},"photoIndex":0,"available":false,"price":{"amount":72,"currency":"USD"}},{"id":"shirt-sand-xs","selections":{"Color":"Sand","Size":"XS"},"photoIndex":2,"available":false},{"id":"shirt-sand-s","selections":{"Color":"Sand","Size":"S"},"photoIndex":2},{"id":"shirt-sand-m","selections":{"Color":"Sand","Size":"M"},"photoIndex":2},{"id":"shirt-sand-l","selections":{"Color":"Sand","Size":"L"},"photoIndex":2},{"id":"shirt-sand-xl","selections":{"Color":"Sand","Size":"XL"},"photoIndex":2,"price":{"amount":72,"currency":"USD"}},{"id":"shirt-ink-xs","selections":{"Color":"Ink","Size":"XS"},"photoIndex":3},{"id":"shirt-ink-s","selections":{"Color":"Ink","Size":"S"},"photoIndex":3},{"id":"shirt-ink-m","selections":{"Color":"Ink","Size":"M"},"photoIndex":3},{"id":"shirt-ink-l","selections":{"Color":"Ink","Size":"L"},"photoIndex":3},{"id":"shirt-ink-xl","selections":{"Color":"Ink","Size":"XL"},"photoIndex":3,"available":false,"price":{"amount":72,"currency":"USD"}}],"quantity":{"max":5},"description":"A relaxed camp-collar shirt in washed European linen. It softens with every wash and keeps you cool on warm days.\n\nBoxy through the body with a straight hem, so it looks right tucked or untucked.","details":[{"label":"Material","value":"100% European linen, garment washed"},{"label":"Fit","value":"Relaxed. Size down for a closer fit."},{"label":"Shipping","value":"Free over $100. Ships in 1–2 business days."},{"label":"Returns","value":"Free returns within 30 days"}]}}}'

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 id:

✖ Invalid input: expected string, received undefined
  → at page.id

Misspelling a field name (unknown keys are rejected):

✖ Unrecognized key: "colour"
  → at page