1. Send events
POST https://api.bolrach.com/push/v1/events
Authorization: Bearer bt_live_...
{ "app_id": "YOUR_APP_ID", "name": "cart.updated", "user_id": "cust_5521",
"idempotency_key": "cart-5521-v14",
"properties": { "cart_id": "c_5521", "amount": 84.50, "currency": "USD", "cart_url": "https://shop.example.com/cart",
"items": [ { "product_id": "sku-2", "name": "Linen shirt", "price": 49.50, "quantity": 1 },
{ "product_id": "sku-3", "name": "Canvas sneakers", "price": 35.00, "quantity": 1 } ] } }Put your own id for the person in user_id, matching what the app passed to login. Leave it out for catalog events, which concern a product rather than a person. Up to 100 events fit in one POST /events:batch. Money is amount in ordinary units with an ISO currency, or amount_minor in cents.
| Event | When | Properties |
|---|---|---|
product.viewed | A shopper opened a product page | product_id, name, price, currency, url, image_url |
cart.updated | The cart changed; send the whole cart each time | cart_id, items, amount, currency, cart_url |
cart.cleared | The cart was emptied | cart_id |
order.placed | An order went through, with its revenue | order_id, items, amount, currency, order_url |
order.paid | Payment confirmed | order_id, amount, currency |
order.shipped | Left the warehouse | order_id, carrier, tracking_url |
order.out_for_delivery | With the courier today | order_id |
order.delivered | Delivered | order_id, items, review_url |
order.refunded | Refund issued | order_id, amount, currency |
review.submitted | The customer reviewed the order | order_id, rating |
wishlist.added | Saved to the wishlist; also watches the price | product_id, name, price, currency, url, image_url |
wishlist.removed | Taken off the wishlist | product_id |
price_alert.created | Asked to hear about a lower price | product_id, target_price, currency |
stock_alert.created | Asked to hear when it is back | product_id, name |
catalog.price_changed | No user_id. Reaches everyone watching the product | product_id, name, old_price, new_price, currency, url, image_url |
catalog.restocked | No user_id. Reaches everyone who asked for a restock alert | product_id, name, url, image_url |
catalog.out_of_stock | No user_id. Arms the restock alerts again | product_id |
session.started | The person opened the app or site | none |
user.followed | Someone followed the person in user_id | actor_id, actor_name, profile_url |
post.liked | Someone liked their post | actor_id, actor_name, post_id, post_title, post_url |
post.commented | Someone commented on their post | actor_id, actor_name, post_id, comment_id, text, post_url |
comment.replied | Someone replied to their comment | actor_id, actor_name, post_id, comment_id, text, url |
user.mentioned | Someone mentioned them | actor_id, actor_name, context_id, text, url |
post.shared | Someone shared their post | actor_id, actor_name, post_id, post_title, post_url |
message.received | A direct message arrived for them | actor_id, actor_name, conversation_id, preview, url |
GET /event-catalog returns the same list. A template can use any property as {{trigger.name}}, plus a few we work out: {{trigger.items_count}}, {{trigger.first_item_name}}, {{trigger.amount_text}} (USD 84.50), {{trigger.new_price_text}} and {{trigger.price_drop_percent}}. The person's own attributes are under {{user.*}}.
2. Turn on a recipe
POST https://api.bolrach.com/push/v1/apps/YOUR_APP_ID/recipes/abandoned_cart:enable
{ "params": { "first_delay": "3600", "title_1": "You left something in your cart" } }
-> 200 { "recipe": "abandoned_cart", "workflow_id": "...", "version": 1, "status": "published" }Leave out params to take the defaults, and GET /recipes lists every parameter. A recipe becomes an ordinary journey that you can open in the builder and change. :disable pauses it: nobody new enters, and people already in it finish.
| Recipe | For | Starts on | What it does |
|---|---|---|---|
abandoned_cart | Shop | cart.updated | An hour after the last cart change, then a day later. order.placed ends it and is credited to the reminder. |
browse_abandonment | Shop | product.viewed | Two hours after a product view, at most once a day. |
order_updates | Shop | order.placed to order.delivered | One push per stage, each replacing the last on the phone. |
refund | Shop | order.refunded | The amount and the order. |
price_drop | Shop | catalog.price_changed | Everyone watching the product, once per new low price. |
back_in_stock | Shop | catalog.restocked | Everyone who asked, once until it sells out again. |
wishlist_reminder | Shop | wishlist.added | Three days later, unless they ordered or removed it. |
review_request | Shop | order.delivered | Three days later, unless they already reviewed that order. |
win_back | Shop | session.started | After 30 quiet days, then 14 more. Marketing consent applies. |
new_follower | Social | user.followed | Jordan and 4 others started following you |
likes | Social | post.liked | Sam and 12 others liked your post, one notification per post |
comments | Social | post.commented | Grouped per post, latest comment as the text |
replies | Social | comment.replied | Grouped per comment |
mentions | Social | user.mentioned | Sent straight away, threaded per conversation |
shares | Social | post.shared | Grouped per post |
direct_messages | Social | message.received | One notification per conversation that updates, held back while the person is in the app |
activity_digest | Social | likes, comments, follows, mentions, shares | One evening summary instead of instant pushes |
re_engagement | Social | session.started | After a quiet week. Marketing consent applies. |
3. Price drops and restocks
A shopper starts watching a product when your app sends wishlist.added, price_alert.created or stock_alert.created, or when you call PUT /apps/{id}/users/{user}/watchers/{product}. When the price falls, send one event with no user. Every watcher hears about it once, and sending the same price again sends nothing:
POST https://api.bolrach.com/push/v1/events
{ "app_id": "YOUR_APP_ID", "name": "catalog.price_changed", "idempotency_key": "tote-price-2026-10-24",
"properties": { "product_id": "tote", "name": "Leather tote", "old_price": 180, "new_price": 144, "currency": "USD",
"url": "https://shop.example.com/p/tote" } }
-> each watcher: "Price drop: Leather tote. Now USD 144.00, was USD 180.00 (20% off)."5. Build your own journey
Start a blank journey in the console under Push, Journeys, or post the same specification. This one waits for the order and reminds once if it never comes:
POST https://api.bolrach.com/push/v1/workflows/{id}/versions
{ "trigger": { "type": "event", "name": "checkout.started",
"where": { "rules": [ { "path": "amount", "op": "gte", "value": 100 } ] } },
"settings": { "exit_on": [ { "event": "order.placed" } ],
"goal": { "event": "order.placed" },
"reentry": "if_not_active",
"frequency_cap": { "max_messages": 2, "per_seconds": 86400 },
"send_window": { "start": "09:00", "end": "21:00", "timezone": "user", "fallback_timezone": "America/New_York" } },
"entry": "wait_order",
"steps": [
{ "key": "wait_order", "type": "wait_for_event",
"config": { "event": "order.placed", "timeout_seconds": 1800, "match": { "checkout_id": "{{trigger.checkout_id}}" } },
"cases": { "matched": "end", "timeout": "remind" } },
{ "key": "remind", "type": "send_push", "next": "end",
"config": { "title": "Your checkout is waiting", "body": "{{trigger.amount_text}} is saved.", "url": "{{trigger.url}}" } },
{ "key": "end", "type": "end" } ] }Nothing runs until POST /workflows/{id}/versions/{version}:publish. Once it is live, an order.placed from the shopper pulls them out of the journey on the spot and cancels the reminder still waiting for them. The reentry setting decides what happens when they start a second checkout: always, if_not_active, once or cooldown. With the send window above, a reminder due at 11 pm in Denver waits until 9 the next morning, Denver time. Run POST /workflows/{id}/canvas:simulate against a test event first, since a simulation sends nothing.
6. Revenue
Send order.placed with amount and currency, and we look back seven days for the last push that reached that person. A push they opened beats one that only arrived. Say a shopper ignores Monday's price drop alert, opens Wednesday's restock push and buys on Thursday: the $144 goes to the restock push. A journey with a goal has its goal events credited to its own last message. For the numbers, GET /apps/{id}/revenue splits money by currency, journey and message, GET /apps/{id}/journeys counts who entered, left early or reached the goal, and GET /workflows/{id}/stats breaks one journey down step by step.
4. Grouped social notifications
The first like on a post goes out at once. Likes in the next minute are held, and when the minute is up one push replaces the first on the device: Sam and 12 others liked your post. By the end Maya has seen two pushes for thirteen likes. In the in-app inbox the post takes one row, and the unread badge goes up by one for the post, however many likes it collected:
If Maya wants quiet on one noisy post, the app calls
PUT /apps/{id}/users/{user}/mutes/{key}with the keylikes:p-91. A conversation's key looks likedm:c-17and a person's likeactor:u-44, and anuntiltimestamp makes the mute expire on its own. To stop a whole kind of notification for her, usePUT /apps/{id}/users/{user}/preferences/{category}with a category such associal_likes. The other four aresocial_comments,social_follows,social_mentionsandsocial_messages.