Products
The Products page displays your complete product catalog with filtering and search capabilities.
Dashboard Features
Catalog Statistics
A single catalogue-health strip at the top, not a row of tiles. It reads
N products · N categories · N brands, and beside it sits a pill for each thing
that needs attention: products out of stock, products low on stock, and products
missing a category or a brand. When there is nothing to flag it says
Catalogue is complete instead. The counts cover the whole catalogue, not the
page you are looking at.
Filters
Filter products using multiple criteria:
| Filter | Description |
|---|---|
| Search | Find products by name, description, or SKU |
| Category | Filter by product category |
| Brand | Filter by brand name |
| Stock Status | All stock, In stock, Low stock, or Out of stock |
Product Table
A sortable table displaying:
| Column | Description |
|---|---|
| Image | Product thumbnail or placeholder |
| Name | Product name and SKU |
| Price | Current price and compare-at price (strikethrough) |
| Category | Product categories (badges) |
| Brand | Brand name |
| Stock | In stock (green), a low-stock warning with the units left (amber), or Out of stock (red) |
| Revenue 30d | Revenue over the last 30 days, with an arrow for the change against the previous 30, and the units sold underneath. Products with no sales in the window say so |
Viewing Products
- Navigate to E-Commerce > Products
- Use the search box to find specific products
- Apply filters to narrow results
- Browse paginated results
Understanding Product Data
Product Fields
| Field | Description |
|---|---|
externalId | Your unique product identifier (e.g., SKU) |
name | Product display name |
price | Current selling price. Empty when the store gives the product no price: the catalog shows "No price", and the product never matches a price condition ("price below 10" does not treat it as 0). A price of 0 set in the store stays 0 |
compareAtPrice | Original price (shows discount) |
currency | Price currency code |
categories | Array of category names |
tags | Array of product tags |
brand | Brand or vendor name |
imageUrl | Main product image |
inStock | Availability status |
variants | Size/color variations |
Variants
Products can have multiple variants with different:
- Prices
- SKUs
- Stock status
- Options (size, color, etc.)
Syncing Products
Products are typically synced from your e-commerce platform. Joryio supports:
- Shopify - Automatic sync via webhook
- WooCommerce - REST API integration
- Magento - Joryio Connect extension
- Custom platforms - Use the Products API
Bulk Import
Use the bulk upsert API to import products - POST a bare JSON array (max 500) to the products endpoint:
POST /catalog/products
[
{ "productId": "SKU-001", "name": "Product 1", "price": 19.99, "source": "shopify" },
{ "productId": "SKU-002", "name": "Product 2", "price": 29.99, "source": "shopify" }
]
Using Products in Campaigns
Product Recommendations
Include products in email campaigns using Liquid templates:
{% assign picks = 'featured' | products %}
{% for product in picks %}
<div class="product">
<img src="{{ product.imageUrl }}" alt="{{ product.name }}">
<h3>{{ product.name }}</h3>
<p>${{ product.price }}</p>
<a href="{{ product.url }}">Shop Now</a>
</div>
{% endfor %}
'featured' is the name of a product selection saved on the Products page. The products filter runs on every send and in preview; see the Liquid reference.
For per-customer picks (personalized, bought together, similar items, new arrivals and more) in any channel, see Product recommendations.
Personalization
Three variables describe what the recipient bought, or is about to. They resolve per
contact at send time and in preview, in every channel, for campaigns and journeys alike -
and they are empty for a contact with no store orders, so wrap each in {% if %}.
| Variable | What it holds | Use it when |
|---|---|---|
cart | The store cart synced for the contact: items, value, subtotal (the items subtotal, before shipping), currency, itemCount, checkoutUrl. Empty when the cart is empty | Cart recovery - "you left these behind", with the checkout link |
last_order | Their most recent order that was not cancelled or refunded: id, placedAt, total, currency, status, itemCount, items, orderUrl | Reorder and follow-up - "how was your order?", "order it again" |
top_products | The products they order most often, up to 5, most frequent first: name, sku, imageUrl, url, timesOrdered, lastOrderedAt | Habit-based offers - "your usual?" |
Items on cart.items and last_order.items share one shape - name, quantity, price,
total, imageUrl, url, sku - so a loop written for one works over the other.
Abandoned cart:
{% if cart %}
{% for item in cart.items %}
<p>{{ item.name }} x {{ item.quantity }} - {{ item.total | currency: cart.currency }}</p>
{% endfor %}
<p>Cart total: {{ cart.value | currency: cart.currency }}</p>
<a href="{{ cart.checkoutUrl }}">Complete purchase</a>
{% endif %}
Which link. {{ cart.checkoutUrl }} is the store's own recovery link for the cart (WooCommerce, Magento and PrestaShop: the link the Joryio plugin sends when the cart is abandoned; Shopify: the checkout's recovery link), so it rebuilds the cart on any device. Only a cart the store sent no recovery link for falls back to the store's checkout page. A link that is not on the connected store's own address is never used. A cart abandoned before this rule (2026-11-01) keeps the plain checkout page until the store reports it abandoned again.
A contact with no cart. A cart email whose button links {{ cart.checkoutUrl }} is not sent to someone with no synced cart (or a cart the store gave no recovery link): the link would render empty, a button that goes nowhere. It is recorded as skipped, reason No cart link to send (cart_link_missing). To send anyway, give the link a fallback: {{ cart.checkoutUrl | default: 'https://your-store.com/cart' }}. The email preview shows a labelled sample cart for a preview contact with none, and Launch Review warns when a journey message reads cart but the journey does not start on a cart event (Cart Abandoned, Checkout Started, Product Added, Cart Updated, Cart Viewed).
Last order and usual products:
{% if top_products.size > 0 %}
<p>Your usual, {{ user.firstName }}? {{ top_products | map: "name" | join: " + " }}</p>
{% endif %}
{% if last_order %}
<p>Last time ({{ last_order.placedAt | date_format }}) you ordered:</p>
{% for item in last_order.items %}
<p><a href="{{ item.url }}">{{ item.name }}</a> x {{ item.quantity }}</p>
{% endfor %}
{% endif %}
Pick one from the personalization picker's Shopping group in any editor to insert a ready-made snippet. The full list is in the Liquid reference.
Product Selections
A Selection is a saved, reusable filter over your product catalog - for example
"discounted footwear", "in-stock items under $50", or "everything tagged new-arrival".
Build them once and reuse them wherever you show products.
Create one: on the Products page click Selections → New Selection. Give it a name, add one or more rules over product fields (name, brand, price, in-stock, categories, tags, …) with type-aware operators, choose whether to match all or any of the rules, and set a max number of products. Preview shows exactly which products match right now.
Selections are the product-catalog counterpart of entity Selections - a saved filter tree you can point content and recommendations at instead of hand-listing product IDs.
Best Practices
- Keep products synced: Ensure regular sync with your e-commerce platform
- Use high-quality images: Product images improve email engagement
- Maintain accurate stock: Out-of-stock notifications require accurate data
- Categorize consistently: Use consistent categories for better filtering
- Include URLs: Product URLs enable click-through tracking
Troubleshooting
Products Not Appearing
- Check your API authentication
- Verify workspace ID is correct
- Ensure products have required fields (externalId, name, price)
Images Not Loading
- Verify image URLs are publicly accessible
- Check for HTTPS requirements
- Ensure image URLs aren't expired
Stock Status Incorrect
- Sync interval may need adjustment
- Check webhook configuration
- Verify inventory tracking is enabled