Using Product Agents with Klaviyo¶
This guide explains how to integrate Product Agents with Klaviyo using a metric-triggered flow.
It covers:
- How Product Agents send events to Klaviyo
- How to configure the required Klaviyo flow
- The full event payload structure
- All supported merge variables
- How to render product data safely in templates
This guide is intended for developers, agencies, and technical marketers responsible for Klaviyo setup.
Integration model¶
When using Klaviyo as the delivery channel:
- Product Agents emit a custom metric event to Klaviyo
- Klaviyo triggers a flow when the event is received
- Klaviyo controls consent, suppression, smart sending, and delivery
- Product Agents do not send emails directly to customers
Each Product Agent message corresponds to one event sent to Klaviyo.
Prerequisites¶
Before setting up the integration, ensure that:
- You have access to the Klaviyo account
- You can create and edit flows
- You can create and edit email templates
- You have a Klaviyo API key with permission to send events
Event name and trigger¶
Product Agents send a custom Klaviyo metric named:
Your Klaviyo flow must be a metric-triggered flow using this event.
There should be exactly one flow listening to this event.
Event payload structure¶
Product Agents send the following properties as part of the Klaviyo event.
Full payload structure
{
"subject": "string",
"previewText": "string",
"headline": "string",
"body": "string",
"callToAction": "string",
"messageType": "string",
"otherProductsTitle": "string",
"otherProductsText": "string",
"otherProductsCallToAction": "string",
"inputProduct": {
"title": "string",
"description": "string",
"imgUrl": "string",
"image": {
"scalableUrl": "string",
"url": "string"
},
"url": "string",
"originalUrl": "string",
"trackingCode": "string",
"productNumber": "string",
"price": "number|null",
"oldPrice": "number|null",
"discountPercentage": "number",
"currency": "string",
"onSale": "boolean",
"brand": "string|null",
"inStock": "boolean",
"priceExVat": "number|null",
"oldPriceExVat": "number|null",
"keywords": "string",
"ean": "string|null"
},
"primaryProducts": [
{
"title": "string",
"description": "string",
"imgUrl": "string",
"image": {
"scalableUrl": "string",
"url": "string"
},
"url": "string",
"originalUrl": "string",
"trackingCode": "string",
"productNumber": "string",
"price": "number|null",
"oldPrice": "number|null",
"discountPercentage": "number",
"currency": "string",
"onSale": "boolean",
"brand": "string|null",
"inStock": "boolean",
"priceExVat": "number|null",
"oldPriceExVat": "number|null",
"keywords": "string",
"ean": "string|null"
}
],
"otherProducts": [
{
"title": "string",
"description": "string",
"imgUrl": "string",
"image": {
"scalableUrl": "string",
"url": "string"
},
"url": "string",
"originalUrl": "string",
"trackingCode": "string",
"productNumber": "string",
"price": "number|null",
"oldPrice": "number|null",
"discountPercentage": "number",
"currency": "string",
"onSale": "boolean",
"brand": "string|null",
"inStock": "boolean",
"priceExVat": "number|null",
"oldPriceExVat": "number|null",
"keywords": "string",
"ean": "string|null"
}
]
}
Note
inputProductis the product that triggered the message (what the customer previously bought or viewed)primaryProductsis always an array and may contain one or more productsotherProductsmay be empty- All fields should be treated as optional unless explicitly required by your template
- For information about how the image scaling mechanism works refer to Integration Overview
Creating the Klaviyo flow¶
- Log in to Klaviyo
- Go to Flows
- Create a new flow
- Choose Metric-triggered flow
- Select the metric
helloretail - Save the flow
- Add an Email action to the flow
The email action must be set to Live for messages to be sent.
Warning
Do not add additional filters to the trigger unless explicitly required. Filtering the trigger may block Product Agent messages.
Subject line and preview text¶
The subject line and preview text are provided dynamically by Product Agents.
Configure them in the email settings, not in the template body.
Warning
Do not hard-code subject lines or preview text.
Template data access¶
{{ event.headline | safe }} {# (1)! #}
{{ event.body | safe }} {# (2)! #}
{{ event.primaryProducts.0.title }} {# (3)! #}
- Use
| safeon all fields that may contain HTML to allow it to render correctly instead of being escaped. bodymay contain<b>,<em>,<ul>, and<li>tags — always use| safe.- Access the first primary product using
.0.dot notation. Use a loop if you need to render multiple products.
Core content fields¶
| Field | Template syntax | Description |
|---|---|---|
| subject | {{ event.subject }} | Email subject line |
| previewText | {{ event.previewText }} | Inbox preview text |
| headline | {{ event.headline \| safe }} | Main email headline |
| body | {{ event.body \| safe }} | Main email content |
| callToAction | {{ event.callToAction }} | Primary CTA button text |
Product fields¶
Input product¶
inputProduct is the product that triggered this message — the item the customer previously bought or viewed. You can reference it in templates to give the email context, for example to remind the customer what they purchased before recommending a replenishment or alternative.
| Field | Template syntax | Description |
|---|---|---|
| title | {{ event.inputProduct.title }} | Product name |
| description | {{ event.inputProduct.description \| safe }} | Product description (may contain HTML) |
| image (scalable) | {{ event.inputProduct.image.scalableUrl }} | Hello Retail CDN URL supporting dynamic resize via query params |
| image (direct) | {{ event.inputProduct.image.url }} | Direct URL to product image |
| url | {{ event.inputProduct.url }} | Product page URL with tracking fragment appended |
| originalUrl | {{ event.inputProduct.originalUrl }} | Original product page URL without tracking |
| productNumber | {{ event.inputProduct.productNumber }} | Product number/SKU as defined in the product catalog |
| price | {{ event.inputProduct.price \| floatformat:2 }} | Current price |
| currency | {{ event.inputProduct.currency }} | Currency code (e.g., "EUR", "USD") |
| brand | {{ event.inputProduct.brand }} | Brand name |
| inStock | {% if event.inputProduct.inStock %} | Whether the product is in stock |
First primary product¶
| Field | Template syntax | Description |
|---|---|---|
| title | {{ event.primaryProducts.0.title }} | Product name |
| description | {{ event.primaryProducts.0.description \| safe }} | Product description (may contain HTML) |
| image (scalable) | {{ event.primaryProducts.0.image.scalableUrl }} | Hello Retail CDN URL supporting dynamic resize via query params |
| image (direct) | {{ event.primaryProducts.0.image.url }} | Direct URL to product image |
| imgUrl | {{ event.primaryProducts.0.imgUrl }} | Direct URL to product image (legacy alias for image.url) |
| url | {{ event.primaryProducts.0.url }} | Product page URL with tracking fragment appended |
| originalUrl | {{ event.primaryProducts.0.originalUrl }} | Original product page URL without tracking |
| trackingCode | {{ event.primaryProducts.0.trackingCode }} | Tracking identifier for the product in this message |
| productNumber | {{ event.primaryProducts.0.productNumber }} | Product number/SKU as defined in the product catalog |
| price | {{ event.primaryProducts.0.price \| floatformat:2 }} | Current price |
| oldPrice | {{ event.primaryProducts.0.oldPrice \| floatformat:2 }} | Previous price if on sale |
| discountPercentage | {{ event.primaryProducts.0.discountPercentage }} | Discount percentage (0 if not on sale) |
| currency | {{ event.primaryProducts.0.currency }} | Currency code (e.g., "EUR", "USD") |
| onSale | {% if event.primaryProducts.0.onSale %} | Whether the product is on sale |
| brand | {{ event.primaryProducts.0.brand }} | Brand name |
| inStock | {% if event.primaryProducts.0.inStock %} | Whether the product is in stock |
| priceExVat | {{ event.primaryProducts.0.priceExVat \| floatformat:2 }} | Current price excluding VAT |
| oldPriceExVat | {{ event.primaryProducts.0.oldPriceExVat \| floatformat:2 }} | Previous price excluding VAT |
| keywords | {{ event.primaryProducts.0.keywords }} | Product keywords |
| ean | {{ event.primaryProducts.0.ean }} | EAN/barcode |
Looping through primary products¶
{% for product in event.primaryProducts %} {# (1)! #}
{{ product.title }} {# (2)! #}
{{ product.url }}
{% endfor %}
- Loops over all primary products. Typically one, but the template should handle more.
- Inside a loop, access fields directly on
product— no need forevent.primaryProducts.0.X.
Looping through recommended products¶
Conditional rendering of recommended products¶
{% if event.otherProducts | length > 0 %} {# (1)! #}
{{ event.otherProductsTitle | safe }} {# (2)! #}
{{ event.otherProductsText | safe }}
{% for product in event.otherProducts %}
{{ product.title }}
{% endfor %}
{% endif %}
- Always guard the recommended products section —
otherProductsmay be an empty array. - Used here as a precaution — only
body,headline, and productdescriptionfields are documented as containing HTML.
Recommended products section fields¶
| Field | Template syntax | Description |
|---|---|---|
| otherProductsTitle | {{ event.otherProductsTitle \| safe }} | Section title |
| otherProductsText | {{ event.otherProductsText \| safe }} | Section description |
| otherProductsCallToAction | {{ event.otherProductsCallToAction }} | CTA text |
messageType values¶
The messageType field identifies which Product Agent generated the message.
These values are stable technical identifiers and can be used in templates if conditional rendering is needed.
| messageType | User-facing name |
|---|---|
REPLENISHMENT_REMINDER | Replenishment Reminder |
SIMILAR_PRODUCT_RECOMMENDATIONS | Alternative Picks |
ACCESSORY_RELATED_PRODUCT_UPSELL | Recommended Add-ons |
PRICE_DROP_VIEWED_PRODUCT | Price Drop – Viewed Product |
PRICE_DROP_PURCHASED_REPLENISHMENT | Price Drop – Purchased Replenishment |
PRICE_DROP_ALTERNATIVE_PRODUCT | Price Drop – Alternative Product |
Smart sending and test mode¶
Klaviyo Smart Sending rules apply after the Product Agent event is received.
This means:
- Events may be received even if emails are blocked
- Emails blocked by Smart Sending do not reach the inbox
- Product Agents do not retry messages blocked by the channel
During testing, Smart Sending may need to be temporarily disabled to avoid confusion when sending repeated test messages.
These topics are covered in separate documentation: