[relaybell docs › Sending › Personalization & recommendations](https://relaybell.com/docs/personalization) · [All docs as Markdown](https://relaybell.com/docs.md) · [Index for AI assistants](https://relaybell.com/llms.txt)

# Personalization & recommendations

> Interest profiles, the product catalog feed, {rec.*} product recommendation tags, smart send time and how the data is kept.

relaybell learns what each subscriber is interested in from what your website reports about them, and uses it in two ways: **product recommendations** in the copy of a notification (`{rec.title}`, `{rec.url}` …), and **smart timing**, which delivers at the hour each person usually clicks or visits.

Everything runs on your own (first-party) data: the actions your website sends ([events](https://relaybell.com/docs/events-automation)) and the person's clicks on your notifications. Nothing comes from other websites, ad networks or Google Analytics.

## Interest profiles {#profiles}

A subscriber gets a profile once they have done something in the last 90 days: viewed, carted or bought a product, joined a waitlist, or clicked a notification. It is worked out again a couple of minutes after each new action, and refreshed once a day.

| Part | What it is |
| --- | --- |
| Top categories, brands, products | Scored from `product.viewed` (1), `cart.updated` / `checkout.started` (3), `purchase` (5) and `waitlist.join` (6). Scores halve every 14 days, so recent interest outweighs old. |
| Price range | The lowest, middle (median) and highest price of the products they viewed or carted. |
| Engagement | 0–100: their clicks compared with the notifications they received, how many days they were active in the last 30, and how recently. |
| Best hour | The hour of day, in their own time zone, at which they most often click or are active. Needs at least 3 such moments; until then it is empty. |
| Last active | Their latest action or click. |

Events only need the product id. When an event leaves out the category, brand or price (WooCommerce and Shopify events send only the id), they are taken from your [product catalog](https://relaybell.com/docs/personalization#catalog) by `productId`. Events that do carry them use these property names: `productId`, `product` (name), `category` (e.g. `Shoes > Running`), `brand`, `price`, `currency`, and on `purchase` / `checkout.started` an `items` list with the same fields.

See a subscriber's profile in **Audience → Subscribers** (open a subscriber: **Interests**), or with the API:

```http
GET /api/v1/subscribers/{externalId}/interests
Authorization: Bearer rb_live_…
```

It needs an API key with the `subscribers:read` scope and answers:

```json
{
  "subscriber": { "id": "…", "externalId": "user_42", "timezone": "Asia/Kolkata" },
  "profile": {
    "categories": [{ "name": "Shoes > Running", "score": 4.7 }],
    "brands": [{ "name": "Acme", "score": 3.9 }],
    "products": [{ "id": "1042", "score": 3.8, "title": "Road Runner 2" }],
    "priceRange": { "min": 3499, "median": 4999, "max": 4999, "currency": "INR" },
    "engagement": 72,
    "bestHour": 19,
    "bestHourSignals": 6,
    "lastActiveAt": "2026-10-25T13:50:00Z",
    "computedAt": "2026-10-25T13:52:10Z"
  },
  "recommendations": [{ "productId": "1042", "title": "Road Runner 2", "url": "https://…", "image": "https://…", "price": 4999, "salePrice": null, "currency": "INR", "category": "Shoes > Running", "brand": "Acme", "availability": "in_stock" }],
  "recommendationSource": "profile"
}
```

`profile` is `null` for someone with no recent actions; `recommendationSource` is then `popular`. An unknown id answers `404`.

## Product catalog {#catalog}

Recommendations pick from your product catalog. Add it in **Settings → Catalog** (or `PUT /api/v1/catalog`) as a public web address in one of these formats:

| Format | What to enter |
| --- | --- |
| Google Merchant Center | The XML (RSS or Atom) feed your shop plugin makes for Google Shopping. The `g:` fields are read: `g:id`, `g:title`, `g:link`, `g:image_link`, `g:price` (e.g. `19.99 USD`), `g:sale_price`, `g:availability`, `g:brand`, `g:product_type` (or `g:google_product_category`). |
| Shopify | Your store's address followed by `/products.json`, e.g. `https://yourstore.com/products.json` (all pages are read). Set the **currency**, since this file doesn't say. |
| JSON list | A list of products, or `{ "products": [...] }`, with `id` (or `productId`), `title` (or `name`), `url`, `image`, `price`, `salePrice`, `currency`, `category` (a text like `Home > Kitchen`, or a list), `brand` and `inStock` (or `availability`). |

- The product ids must be the ones your store sends as `productId` in its events, so actions can be matched to products.
- Only `http(s)` links and images are kept. A product needs an id and a title.
- Up to **10,000 products** and **20 MB** per feed (over 10,000, the first 10,000 are kept and the page says so); the download must finish within 60 seconds.
- The feed is read **every 6 hours**, and right away when you save a new address or press **Sync now** (once a minute at most).
- A product that disappears from the feed is marked unavailable and stops being recommended straight away; it is deleted after 30 days if it doesn't come back. Out-of-stock products are never recommended.
- When a sync fails, the page says why in plain words (for example "Nothing was found at this address (HTTP 404)") and the products from the last good sync stay in use. It is tried again within the hour.

API: `GET /api/v1/catalog` (the feed, its last sync and product counts), `PUT /api/v1/catalog` `{ url, format?, currency?, enabled? }` (`format`: `auto`, `google`, `shopify` or `json`), `DELETE /api/v1/catalog`, `POST /api/v1/catalog/sync` and `GET /api/v1/catalog/products?limit=50&cursor=&q=&availability=`. Changes need the `settings:write` scope and appear in **Account → Activity**.

## Recommendations {#recommendations}

For every subscriber, each in-stock product in the catalog is scored by how well its category and brand match their interests (and whether they looked at it), times how well its price fits their price range. Leaving out:

- products they bought in the last 30 days,
- the product the previous recommendation push showed them (for 7 days), so the next push moves on.

The best five are kept with the profile and refreshed after each catalog sync. Someone with no profile, or too few matches, gets your **popular products**: the ones most people viewed, carted or bought in the last 7 days. With no recent actions at all, your newest products are used. Stock is checked again at the moment each push goes out.

## Product tags {#rec-tags}

Use these [merge tags](https://relaybell.com/docs/sending#merge-tags) in the title, message, link, image and buttons of a send, and in the title, message and link of [automations](https://relaybell.com/docs/events-automation#automations) and [journeys](https://relaybell.com/docs/sequences):

| Tag | Value |
| --- | --- |
| `{rec.title}` | The recommended product's name. |
| `{rec.url}` | Its page (your UTM tags are added). |
| `{rec.image}` | Its picture. |
| `{rec.price}` | Its price with the currency, e.g. `₹4,999` or `$19.50` (the sale price when it is on sale). |
| `{rec.category}` | The last part of its category, e.g. `Running`. |
| `{rec.brand}` | Its brand. |
| `{rec2.…}`, `{rec3.…}` | The same for the second and third pick. |

```json
{
  "title": "{rec.title|Our bestsellers} is waiting for you",
  "body": "Now {rec.price|on sale}. Tap to take a look.",
  "url": "{rec.url}",
  "image": "{rec.image}",
  "actions": [{ "title": "Or see {rec2.title|more}", "url": "{rec2.url}" }]
}
```

When a subscriber has nothing to recommend (no catalog yet, or nothing in stock), the text after `|` is used. A link that doesn't come out as a web address opens your **Default link** (Settings → Sending) instead, a button without one opens the notification's link, and an image that isn't a web address is left out. An empty title becomes your website's name. A tag such as `{rec.colour}` is refused with `400 invalid_merge_tag`.

In the Send page and the automation editor, **Recommend a product** adds the tags for you and shows a **preview for a subscriber** (by user ID, or a random recent one). The preview is also available to the API, with the same rendering a send uses:

```http
POST /api/v1/recommendations/preview
{ "externalId": "user_42", "title": "{rec.title|Our picks}", "url": "{rec.url}" }
```

It answers the subscriber, their picks, the tag values and the `rendered` copy; leave `externalId` out for a random recently active subscriber. `GET /api/v1/recommendations/preview?externalId=` returns the picks without copy. Clicks on these pushes are counted like any other click; **Reports** shows the clicks on notifications that used product tags in the last 30 days (`GET /api/v1/recommendations/stats?days=30`).

## Smart timing {#smart-time}

A send with **Smart timing** (`smartTime: true`) goes to each subscriber at their **best hour** in their own time zone. Someone without one yet gets the local hour their browser was last active, or 10:00. Smart timing never lands inside your quiet hours: such a group waits until they end. Your daily frequency cap still applies.

## Privacy {#privacy}

- Profiles use only data your website collected about its own visitors and their clicks on your notifications. No personal details beyond what your events already contain are stored, and properties are never shared with other websites.
- Erasing a subscriber (Audience, or `DELETE /api/v1/subscribers/{externalId}`) deletes their profile with everything else. Deleting a website deletes its catalog and profiles.
- Profiles only look back 90 days, and your [data retention](https://relaybell.com/docs/website-settings) setting removes older actions.
- If your privacy notice lists what you do with visitor data, mention that you personalize notifications based on what people view and buy on your site.
