Skip to content

Catalog & order import API

Push products, articles, courses and listings to relaybell (POST /catalog/items), remove them, run a full sync, and import past orders and actions (POST /events/import).

Recommendations ({rec.title}, {rec.summary} …) pick from your website's catalog. It can come from a feed relaybell reads (Personalization → Catalog), or your own server can send the items itself with this API. The relaybell WordPress plugin does exactly that: it sends your WooCommerce products and your posts, and keeps them up to date as you edit them. You only need this page if you build your own integration.

All three endpoints take an API key with the events:write scope (WordPress plugin keys have it) as Authorization: Bearer rb_live_…. They are limited per key to 60 calls a minute (each call can carry 500 items or events: answered 429 rate_limited with a Retry-After header past that), and a call's body can be up to 5 MB.

Send items

POST /api/v1/catalog/items

Shell
curl -X POST https://relaybell.com/api/v1/catalog/items \
  -H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
  -d '{
    "items": [
      {
        "id": "1042", "type": "product", "title": "Ridge Hiking Boot",
        "url": "https://shop.example/p/ridge", "image": "https://shop.example/i/ridge.jpg",
        "price": 7499, "salePrice": 6499, "currency": "INR",
        "category": "Footwear > Boots", "brand": "Northwind", "availability": "in_stock"
      },
      {
        "id": "post-311", "type": "article", "title": "How to choose hiking boots",
        "url": "https://shop.example/blog/boots", "image": "https://shop.example/i/boots-guide.jpg",
        "category": "Guides > Hiking", "tags": ["boots", "trail"],
        "summary": "Fit, grip and support: what matters on the trail.",
        "publishedAt": "2026-10-20T09:00:00Z"
      }
    ]
  }'

Answer (200):

JSON
{ "ok": true, "upserted": 2, "skipped": [] }

Up to 500 items per call. Each item is checked on its own: a bad one is left out and listed in skipped with its id and a reason, and the others are stored. Sending an item again updates it (by id), so sending the same call twice is harmless.

FieldDescription
id *Your id for the item, 1–128 characters without spaces. For products use the id your events send as productId (only plain ids of up to 64 letters, digits, _ . : - can match product events); for content, the id your content.viewed events send as itemId.
typeproduct (default), article, course, listing or other.
title *Up to 300 characters (longer is cut).
url, imageAbsolute http(s) addresses.
price, salePriceNumbers (or text like "3,999.00"), below 10¹². A salePrice that isn't lower than price is ignored.
currency3-letter code, e.g. INR, USD.
categoryA path like Guides > Hiking (or a list of levels), up to 300 characters.
brandUp to 120 characters.
tagsA list of up to 20 short texts (each up to 60 characters).
summaryPlain text; HTML is removed and it is cut to 300 characters. Fills {rec.summary}.
availabilityin_stock (default), out_of_stock, preorder or unavailable. Only in_stock and preorder items are recommended.
publishedAtISO date. Among equally good picks, newer content comes first.

skipped[].reason is one of invalid_item (not an object), invalid_id, invalid_type, missing_title, invalid_url, invalid_image, invalid_price, invalid_currency, invalid_category, invalid_brand, invalid_tags, invalid_summary, invalid_availability, invalid_published_at, duplicate_id (the same id earlier in the same call: the last copy is stored) and catalog_full. A website can hold 20,000 live items sent this way (items marked unavailable don't count); known ids always update. Past that, a new article, course, listing or other item takes the place of the oldest one sent this way (by publishedAt, then id), which is marked unavailable, so the newest content is always there; it is skipped with catalog_full only when it is older than all of them. A new product past the limit is skipped with catalog_full.

A call that isn't { "items": [...] }, has more than 500 items or a malformed fullSync answers 400 invalid_items with a message; broken JSON answers 400 invalid_json and a body over 5 MB 413 payload_too_large.

Items sent this way show in Settings → Catalog with the source WordPress / API. A feed never changes them: if your feed lists the same id, the API's copy is kept. Removing the feed removes only the feed's items.

Full sync

To make relaybell's copy match your site exactly, send every item in as many calls as it takes, all with the same fullSync.id, and done: true on the last one:

Shell
# first batches
-d '{ "items": [ …500 items… ], "fullSync": { "id": "2026-10-26T05:00", "done": false } }'
# last batch (may be empty)
-d '{ "items": [ …the rest… ], "fullSync": { "id": "2026-10-26T05:00", "done": true } }'

When the done batch arrives, every item you sent through the API that no batch of this sync listed is marked unavailable, and the answer says how many: { "ok": true, "upserted": 120, "skipped": [], "removed": 3 }. Unavailable items are never recommended and are deleted after 30 days unless they come back. Items from a feed are never touched.

  • fullSync.id is any text of 1–64 characters without spaces; use a new one for each full sync.
  • One full sync runs at a time per website. A new id replaces the one before: if an older sync's batches still arrive, their items are stored but its done marks nothing (removed: 0).
  • An item you send without fullSync while a full sync is running (a post saved meanwhile) counts as listed by it.
  • Each finished full sync adds one line to Account → Activity.

Remove items

POST /api/v1/catalog/items/delete

Shell
curl -X POST https://relaybell.com/api/v1/catalog/items/delete \
  -H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
  -d '{ "ids": ["1042", "post-311"] }'

Answer: { "ok": true, "removed": 2 }. Up to 500 ids. Those items become unavailable straight away (they stop being recommended) and are deleted after 30 days. Only items sent through the API are affected; unknown ids are ignored. A body that isn't { "ids": [...] } with at most 500 ids answers 400 invalid_ids.

Import past orders and actions

New subscribers' interests build up as they browse. To give recommendations a head start, import what your existing subscribers did in the last 90 days — the WordPress plugin imports WooCommerce orders this way when you connect it.

POST /api/v1/events/import

Shell
curl -X POST https://relaybell.com/api/v1/events/import \
  -H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
  -d '{
    "events": [
      {
        "event": "purchase", "externalId": "user_42", "timestamp": "2026-10-12T14:03:00Z",
        "properties": {
          "orderId": 5512, "orderNumber": "NW-1001", "value": 7499, "currency": "INR",
          "items": [{ "productId": "1042", "product": "Ridge Hiking Boot", "category": "Footwear > Boots", "price": 7499, "quantity": 1 }]
        }
      }
    ]
  }'

Answer (200):

JSON
{ "ok": true, "imported": 1, "skipped": { "unknownSubscriber": 0, "tooOld": 0, "duplicate": 0, "invalid": 0 } }

Up to 500 events per call, each { event, externalId, properties, timestamp } like a server event, plus the time it happened. An event is stored only when:

  • externalId is one of the website's subscribers (else unknownSubscriber: people who never subscribed aren't added);
  • timestamp is within the last 90 days — or your shorter data retention — (else tooOld), and not in the future (else invalid);
  • it isn't there already (else duplicate). An order event (purchase, checkout.started, order.completed, checkout.completed with an orderId) is kept once per order, by the same key as live events: orderNumber when you send it, else orderId. So importing again, or an order that was also reported live (before or after the import), never counts twice. Other events are kept once per subscriber, name and time.
  • it has a name and an externalId without control characters, a readable time, and properties as an object of at most 32 KB (else invalid).

A body that isn't { "events": [...] }, or has more than 500 events, answers 400 invalid_events. A website can import up to 50,000 events a day (UTC): a call whose events don't all fit stores nothing and answers 429 rate_limited with a Retry-After header (until midnight UTC), so send it again then.

Imported events are stored at the time they happened and only feed interest profiles (with the decay of their real age: a purchase 10 days old already counts a bit less) and segments. They never start an automation or journey, never count as a conversion or towards revenue, never appear in the Goals report and never send a webhook or a product alert. Each call that imported something adds one line to Account → Activity.

Content events

For articles, courses and listings, report what people read with two events (from the browser SDK's track() or POST /events):

EventProperties
content.vieweditemId (the item's id), title, type (article by default), category, tags (a list), url
content.readThe same, when the reader scrolled 75% of the page or stayed 30 seconds. It counts twice as much as a view.

The category and tags become the subscriber's topics; when the event leaves them out they are taken from the catalog item (matched by itemId, else by url). See Personalization.