The Recommendations feature provides a structured way to surface AI-generated or manually created suggestions for improving the performance of a shop. Recommendations can target any entity — menu items, menu sections, campaigns, journeys, actions, targeting segments, and more. Each suggestion is stored as a pending recommendation that the shop owner can review and approve before any change is applied to the loyalty programme.
Contents
- Core Concepts
- Recommendation Fields
- Authentication & Base URL
- Endpoint Reference
- Common Workflows
- Menu Items
- Menu Sections
- Campaigns
- Journeys
- Targeting
- Collections
- Example: New journey
- Example: Campaign with journey and templates
- Example: Win-back slipping-away customers
- Response Format
- Error Responses
Core Concepts
What Is a Recommendation?
A recommendation is a structured suggestion to create, change, or remove a specific entity. It always has:
- A type —
create,update, ordelete - A category — the entity type the recommendation targets (e.g.
item,campaign,journey) - A subcategory — optional further classification within the category (e.g.
win-backwithinjourney) - A rationale — a plain-language explanation of why the change is being suggested
- A status — tracks where the recommendation is in its lifecycle
- A data payload — the proposed entity fields. For
updateanddeleterecommendations this must include the target entityid.
Recommendation Lifecycle
A recommendation can only move forward through its lifecycle — it cannot be re-opened once accepted or rejected.
| Status | Meaning |
|---|---|
pending |
Newly created; awaiting review |
accepted |
Approved via the /accept endpoint; the proposed change has been applied |
rejected |
Dismissed via the /reject endpoint; no change was applied |
Recommendation Fields
| Field | Type | Required | Description |
|---|---|---|---|
id |
integer | — | Unique identifier, assigned on creation |
placeId |
integer | — | The place this recommendation belongs to, set automatically from the URL |
type |
string | Yes | create, update, or delete |
category |
string | Yes | The entity type: item, section, journey, campaign, communication_template, targeting_segment, targeting_rule, targeting_assignment, or collection |
subcategory |
string | No | Optional further classification (e.g. win-back within journey) |
rationale |
string | Yes | Human-readable explanation of why this change is recommended |
data |
object | No | The proposed entity data. Shape depends on category — see examples below. For update and delete must include the entity id. Not used for collection. |
parent_id |
integer | No | ID of a parent recommendation. Links this recommendation into a tree that is accepted or rejected as a unit. Set only on creation; cannot be changed afterwards. |
status |
string | — | pending, accepted, or rejected. Defaults to pending; managed by the accept/reject endpoints. |
created |
integer | — | Unix timestamp (seconds) when the recommendation was created |
updated |
integer | — | Unix timestamp (seconds) of the last update |
The data Object
The shape of data is flexible and depends on what is being recommended. At minimum it identifies the target entity (for update and delete). For update recommendations it also contains the proposed field changes — only fields that differ from the current values need to be included.
Example — an update recommendation targeting a menu item:
{
"id": 1042,
"title": "Crispy Chicken Burger",
"description": "Juicy fried chicken with pickles and sriracha mayo on a toasted brioche bun."
}
Example — a delete recommendation:
{
"id": 2071
}
Authentication
All endpoints require an Api-Key header. The authenticated user must have manager access to the place.
Base URL
/places/{placeId}/recommendations
Replace {placeId} with the numeric ID of the place you are managing.
Endpoint Reference
| Method | Path | Description |
|---|---|---|
GET |
/places/{placeId}/recommendations |
List all recommendations for a place (paginated) |
GET |
/places/{placeId}/recommendations/{id} |
Get a single recommendation by ID |
POST |
/places/{placeId}/recommendations |
Create a new recommendation |
PUT |
/places/{placeId}/recommendations/{id} |
Update a recommendation |
DELETE |
/places/{placeId}/recommendations/{id} |
Delete a recommendation |
POST |
/places/{placeId}/recommendations/{id}/accept |
Accept a recommendation and apply the change |
POST |
/places/{placeId}/recommendations/{id}/reject |
Reject a recommendation — no change applied |
Common Workflows
Reviewing recommendations
- Fetch all pending suggestions:
GET /places/{placeId}/recommendations?where={"status":"pending"} - Read each recommendation's
rationaleanddatato understand what is proposed and why - Use
POST /{id}/acceptto apply the change - Use
POST /{id}/rejectto dismiss it without making any change
Filtering by category
Use the where parameter to scope the list to a specific category:
GET /places/123/recommendations?where={"category":"campaign","status":"pending"}
Pagination and sorting
Supports page, limit, where (URL-encoded JSON), and order query parameters. Fetch the 10 most recently created pending recommendations:
GET /places/123/recommendations?where={"status":"pending"}&limit=10&order=-created
Menu Items
Create a menu item
Propose adding a new item to a section. The data object mirrors the fields required to create an item directly.
POST /places/123/recommendations
{
"type": "create",
"category": "item",
"rationale": "Adding a new seasonal product to the lunch menu",
"data": {
"vat": 25,
"price": 15,
"title": "Grilled Salmon",
"number": 105,
"status": "Active",
"section_id": 66691,
"description": "Fresh Atlantic salmon with seasonal vegetables",
"add_on_category_ids": [68432]
}
}
Update a menu item
Only include the fields you want to change. data.id identifies the target item.
POST /places/123/recommendations
{
"type": "update",
"category": "item",
"rationale": "The item title is generic and does not describe the dish. A more descriptive title will improve discoverability.",
"data": {
"id": 1042,
"title": "Crispy Chicken Burger",
"description": "Juicy fried chicken with pickles and sriracha mayo on a toasted brioche bun."
}
}
Delete a menu item
If accepted, the item will be permanently deleted.
POST /places/123/recommendations
{
"type": "delete",
"category": "item",
"rationale": "Item has been discontinued by the supplier",
"data": {
"id": 12345
}
}
Menu Sections
Create a menu section
POST /places/123/recommendations
{
"type": "create",
"category": "section",
"rationale": "Adding a dedicated desserts section for the summer menu",
"data": {
"title": "Desserts",
"description": "Seasonal sweet treats"
}
}
Update a menu section
POST /places/123/recommendations
{
"type": "update",
"category": "section",
"rationale": "Renaming the section to better reflect its contents",
"data": {
"id": 12345,
"title": "Summer Desserts"
}
}
Delete a menu section
Propose removing an entire section. If accepted, the section will be permanently deleted.
POST /places/123/recommendations
{
"type": "delete",
"category": "section",
"rationale": "This section contains only one item and has not received any orders in the past 90 days. Removing it will reduce menu clutter.",
"data": {
"id": 88
}
}
Campaigns
Create a campaign
Propose creating a new discount or promotional campaign. code and redemptions are required.
POST /places/123/recommendations
{
"type": "create",
"category": "campaign",
"rationale": "Summer promotion offering 20% off all drinks",
"data": {
"code": "SUMMER20",
"discount": 20,
"discountType": "percentage",
"redemptions": 500,
"redemptionsPerCustomer": 1,
"status": "Active",
"startTime": 1748736000,
"endTime": 1756684800,
"visibility": "public"
}
}
Update a campaign
POST /places/123/recommendations
{
"type": "update",
"category": "campaign",
"rationale": "The campaign end date has passed but the campaign is still active. It should be deactivated.",
"data": {
"id": 55,
"status": "Inactive"
}
}
Delete a campaign
POST /places/123/recommendations
{
"type": "delete",
"category": "campaign",
"rationale": "Campaign was created in error",
"data": {
"id": 9900
}
}
Journeys
A journey (category: "journey") defines an automated customer workflow triggered by an event.
The journey's steps are defined in a machineConfig field — include it in the recommendation's
data when creating the journey recommendation, just like any other field.
For the full machineConfig reference see the Journeys developer guide.
Referencing communication templates in machineConfig
Journey steps that send messages need a templateId pointing to a
Communication Template. When the template is being
created as part of the same recommendation tree, you do not need to know its ID in advance.
Instead, assign it a caller-defined string reference using the localRef field
in the template recommendation's data, then reference that string in the journey's
machineConfig using templateRef.
When the tree is accepted, the system creates all entities, collects the mapping of
localRef → created template ID, and then replaces every templateRef
in the journey's machineConfig with the actual template entity ID.
This happens in a final pass after the entire tree is accepted, so the template and journey
recommendations can be in any position in the tree — siblings, cousins, any arrangement.
// Template recommendation — set localRef to a string of your choosing
{
"type": "create",
"category": "communication_template",
"rationale": "Win-back email",
"data": {
"localRef": "winback_email",
"name": "Win-back email",
"type": "email",
"recipient": "{{user.email}}",
"subject": "We miss you",
"body": "..."
}
}
// Journey recommendation — reference the template by its localRef
{
"type": "create",
"category": "journey",
"rationale": "Win-back journey",
"data": {
"name": "Win-back",
"triggerEvent": "account.classification.became_slipping_away",
"machineConfig": {
"initial": "send_email",
"states": {
"send_email": {
"meta": { "type": "send_email", "templateRef": "winback_email" },
"on": { "DONE": "done" }
},
"done": { "type": "final" }
}
}
}
}
The localRef value is stripped from the template's data before the entity is
created — it is never written to the database. Use any string that is unique within the tree.
Create a journey
Supported triggerEvent values: manual, order.created, order.updated, order.closed, campaign.public.started, campaign.private.started, account.created, account.updated, account.tier.upgraded, account.tier.downgraded, account.tier.extended, account.classification.became_vip, account.classification.became_new, account.classification.became_active, account.classification.became_inactive, account.classification.became_slipping_away, account.classification.became_churned.
Include machineConfig directly in data to define the journey's workflow steps at creation time. Use a known templateId when the template already exists, or use templateRef with a localRef string when the template is being created as part of the same recommendation tree (see above).
POST /places/123/recommendations
{
"type": "create",
"category": "journey",
"subcategory": "win-back",
"rationale": "Re-engage customers who have not ordered in 60 days",
"data": {
"name": "60-Day Win-Back",
"triggerEvent": "order.closed",
"machineConfig": {
"initial": "wait_2_days",
"states": {
"wait_2_days": {
"meta": { "type": "wait_for_duration", "days": 2 },
"on": { "ELAPSED": "send_email" }
},
"send_email": {
"meta": { "type": "send_email", "templateId": 42 },
"on": { "DONE": "done" }
},
"done": { "type": "final" }
}
}
}
}
Update a journey
POST /places/123/recommendations
{
"type": "update",
"category": "journey",
"rationale": "Activating the updated win-back journey",
"data": {
"id": 12,
"active": true
}
}
Delete a journey
POST /places/123/recommendations
{
"type": "delete",
"category": "journey",
"rationale": "Replacing this journey with a revised version",
"data": {
"id": 77
}
}
Targeting Segments, Rules, and Assignments
Targeting controls which customers are included in or excluded from a campaign or journey. A segment defines an audience via one or more rules, and an assignment links a segment to a campaign or journey. For full details on segments, rules, operators, and assignments see the Targeting developer guide.
Predefined segments (one per account classification: vip, new,
active, inactive, slipping_away, churned)
are created automatically for each place and can be looked up by their stable code field.
Create a new segment with rules
A segment without rules matches nobody. When recommending a new segment, add its rules as
children of the segment recommendation — use segmentRef in the rule's data
to reference the segment by its localRef.
// Step 1 — create the segment recommendation
POST /places/123/recommendations
{
"type": "create",
"category": "targeting_segment",
"rationale": "Segment for high-spend customers",
"data": { "localRef": "high_spenders", "name": "High Spenders" }
}
// → id: 600
// Step 2 — add rules as children, referencing the segment by its localRef
POST /places/123/recommendations
{
"type": "create",
"category": "targeting_rule",
"parent_id": 600,
"rationale": "Customers who have spent more than 500 in total",
"data": {
"segmentRef": "high_spenders",
"attribute": "account.totalSpend",
"operator": "gte",
"value": 500,
"ruleGroup": 0,
"negate": false,
"sortOrder": 0
}
}
// Step 3 — accept the segment recommendation
POST /places/123/recommendations/600/accept
Add a rule to an existing segment
Supply segmentId directly when the segment already exists and its ID is known.
POST /places/123/recommendations
{
"type": "create",
"category": "targeting_rule",
"rationale": "Target VIP-classified customers only",
"data": {
"segmentId": 55,
"attribute": "account.classification",
"operator": "eq",
"value": "vip",
"ruleGroup": 0,
"negate": false,
"sortOrder": 0
}
}
Create a targeting assignment
Links a segment to a campaign or journey. Use targetRef to reference a journey
or campaign entity created in the same recommendation tree; use targetId when the
entity already exists. Use segmentRef or segmentId the same way.
POST /places/123/recommendations
{
"type": "create",
"category": "targeting_assignment",
"rationale": "Restrict win-back journey to slipping-away customers only",
"parent_id": 601,
"data": {
"segmentId": 12,
"targetRef": "winback_journey",
"targetType": "journey"
}
}
Collections
A collection recommendation groups multiple entity changes that should be reviewed
and applied together. The changes are defined as an elements array inside the
collection's data. When the collection is accepted all elements are applied
atomically — if any element fails, nothing is persisted.
Each element has the same fields as a standalone recommendation (category,
type, rationale, data) plus an optional
localRef — a caller-defined string identifier used to reference the entity
created by that element from other elements in the same collection.
Cross-references between elements
Elements are processed in order. Any element can reference a localRef defined by an
earlier element using the following fields:
segmentRef: "my_segment"intargeting_ruleortargeting_assignmentdata — resolves to the segment entity IDtargetRef: "my_journey"intargeting_assignmentdata — resolves to the target entity IDtemplateRef: "my_template"inside a journey'smachineConfigstates — resolves to the template entity ID
localRef and all *Ref fields are stripped before entity creation.
If an element references a localRef that is not yet defined by a preceding element,
the API returns INCORRECT_ORDER_OF_ELEMENTS at creation time —
before the recommendation is stored.
Example: New journey
A welcome journey that sends an email to every new loyalty account. Two elements: the template
followed by the journey. The template must appear first so its localRef is
defined when the journey's machineConfig is validated.
POST /places/123/recommendations
{
"type": "create",
"category": "collection",
"rationale": "Welcome email for new loyalty members",
"data": {
"elements": [
{
"localRef": "welcome_email",
"category": "communication_template",
"type": "create",
"rationale": "Welcome email for new members",
"data": {
"name": "Welcome email",
"type": "email",
"recipient": "{{user.email}}",
"subject": "Welcome to {{place.title}}",
"body": "Hi {{user.name}}, welcome to our loyalty programme! Your points start from your very next order."
}
},
{
"category": "journey",
"type": "create",
"rationale": "Send a welcome email when a new loyalty account is created",
"data": {
"name": "Welcome new members",
"triggerEvent": "account.created",
"machineConfig": {
"initial": "send_welcome",
"states": {
"send_welcome": {
"meta": { "type": "send_email", "templateRef": "welcome_email" },
"on": { "DONE": "done" }
},
"done": { "type": "final" }
}
}
}
}
]
}
}
// → id: 500
POST /places/123/recommendations/500/accept
Both entities are created atomically. On failure everything is rolled back.
Example: Campaign with a journey and multiple templates
A complete campaign setup with three messaging steps. Templates must appear before the journey
so their localRef values are defined when the journey's machineConfig
is validated.
POST /places/123/recommendations
{
"type": "create",
"category": "collection",
"rationale": "Summer loyalty campaign with nurture journey",
"data": {
"elements": [
{
"localRef": "summer_push",
"category": "communication_template",
"type": "create",
"rationale": "Push notification for campaign launch",
"data": {
"name": "Summer campaign — launch push",
"type": "push_notification",
"recipient": "{{user.token}}",
"subject": "A special offer just for you",
"body": "Hi {{user.name}}, enjoy 25% off your next order with code SUMMER25."
}
},
{
"localRef": "summer_email",
"category": "communication_template",
"type": "create",
"rationale": "Follow-up email after 48 hours",
"data": {
"name": "Summer campaign — follow-up email",
"type": "email",
"recipient": "{{user.email}}",
"subject": "Don't forget your 25% off — only a few days left",
"body": "Hi {{user.name}}, your 25% discount is still waiting. Use code SUMMER25 before it expires."
}
},
{
"localRef": "summer_sms",
"category": "communication_template",
"type": "create",
"rationale": "Final SMS reminder before campaign ends",
"data": {
"name": "Summer campaign — SMS reminder",
"type": "sms",
"recipient": "{{user.mobilePhone}}",
"subject": "",
"body": "Last chance! Use SUMMER25 for 25% off at {{place.title}}. Expires soon."
}
},
{
"localRef": "loyalty_segment",
"category": "targeting_segment",
"type": "create",
"rationale": "Segment for loyalty programme members",
"data": {
"name": "Loyalty members"
}
},
{
"localRef": "summer_journey",
"category": "journey",
"type": "create",
"rationale": "Nurture sequence triggered when the campaign starts",
"data": {
"name": "Summer campaign nurture",
"triggerEvent": "campaign.public.started",
"machineConfig": {
"initial": "send_push",
"states": {
"send_push": {
"meta": { "type": "send_push_notification", "templateRef": "summer_push" },
"on": { "DONE": "wait_48h" }
},
"wait_48h": {
"meta": { "type": "wait_for_duration", "hours": 48 },
"on": { "ELAPSED": "send_email" }
},
"send_email": {
"meta": { "type": "send_email", "templateRef": "summer_email" },
"on": { "DONE": "wait_24h" }
},
"wait_24h": {
"meta": { "type": "wait_for_duration", "hours": 24 },
"on": { "ELAPSED": "send_sms" }
},
"send_sms": {
"meta": { "type": "send_sms", "templateRef": "summer_sms" },
"on": { "DONE": "done" }
},
"done": { "type": "final" }
}
}
}
},
{
"category": "targeting_assignment",
"type": "create",
"rationale": "Restrict journey to loyalty members only",
"data": {
"segmentRef": "loyalty_segment",
"targetRef": "summer_journey",
"targetType": "journey"
}
},
{
"localRef": "summer_campaign",
"category": "campaign",
"type": "create",
"rationale": "Launch summer loyalty campaign",
"data": {
"code": "SUMMER25",
"type": "total_bill_discount",
"discount": 25,
"discountType": "percentage",
"redemptions": 500,
"redemptionsPerCustomer": 1,
"startTime": 1751328000,
"endTime": 1753920000,
"visibility": "public"
}
}
]
}
}
// → id: 500
POST /places/123/recommendations/500/accept
All entities are created atomically. Journeys are activated before campaigns, so the journey
is live when the campaign fires its campaign.public.started trigger event.
Example: Win-back slipping-away customers
The journey fires automatically for each customer the moment they are classified as slipping away
— no targeting assignment or campaign needed. The trigger event is
account.classification.became_slipping_away.
POST /places/123/recommendations
{
"type": "create",
"category": "collection",
"rationale": "Send a win-back email when a customer starts slipping away",
"data": {
"elements": [
{
"localRef": "winback_email",
"category": "communication_template",
"type": "create",
"rationale": "Win-back email offering 20% off",
"data": {
"name": "Win-back email",
"type": "email",
"recipient": "{{user.email}}",
"subject": "We miss you — here's 20% off your next order",
"body": "Hi {{user.name}},\n\nIt's been a while! Use code WINBACK20 for 20% off at {{place.title}}."
}
},
{
"category": "journey",
"type": "create",
"rationale": "Send win-back email when a customer is classified as slipping away",
"data": {
"name": "Win-back",
"triggerEvent": "account.classification.became_slipping_away",
"machineConfig": {
"initial": "send_email",
"states": {
"send_email": {
"meta": { "type": "send_email", "templateRef": "winback_email" },
"on": { "DONE": "done" }
},
"done": { "type": "final" }
}
}
}
}
]
}
}
// → id: 600
POST /places/123/recommendations/600/accept
Both entities are created atomically. The journey starts firing immediately for any customer classified as slipping away.
Response Format
Recommendations are returned wrapped in a node.recommendation object keyed by ID:
{
"node.recommendation": {
"500": {
"id": 500,
"placeId": 123,
"type": "create",
"category": "collection",
"rationale": "Win-back campaign",
"data": { "elements": [ ... ] },
"parentId": null,
"status": "pending",
"createdAt": 1714060800,
"updatedAt": 1714060800
}
}
}
List responses also include a pager object:
{
"node.recommendation": { ... },
"pager": { "total": 45, "limit": 10, "page": 1, "pages": 5 }
}
Error Responses
| Status | Code | Description |
|---|---|---|
| 400 | MISSING_REQUIRED_FIELDS |
One or more of type, category, or rationale is missing. For collection elements, the same applies to each element in the array. |
| 400 | INCORRECT_ORDER_OF_ELEMENTS |
A collection element references a localRef (via segmentRef, targetRef, or templateRef) that is not defined by any preceding element. Returned at creation time. |
| 400 | — | Request body contains non-editable fields (id, placeId, status, etc.) |
| 400 | INVALID_RECOMMENDATION_STATUS |
Attempted to accept a recommendation that is not pending |
| 400 | ACCEPT_ACTION_FAILED |
An element failed during accept. The response includes failedId. All changes are rolled back. |
| 400 | MISSING_SEGMENT_ID |
A targeting_rule or targeting_assignment element has no segmentId or segmentRef |
| 400 | MISSING_TARGET_ID |
A targeting_assignment element has no targetId or targetRef |
| 401 | API_KEY_REQUIRED |
No API key was provided |
| 403 | INVALID_API_KEY |
API key is invalid or user is not a manager of this place |
| 404 | — | Recommendation not found |
Comments