App events
Send what guests do in your app, such as viewing an item or starting checkout, so unPLUG can react to it. Abandoned-cart messages and funnel reports both run on these events.
Available at launch
Why send events#
unPLUG already knows about orders and points from the API. It doesn't know what happened before the order, or why there wasn't one. App events fill that gap:
- Journeys. A journey can start on an app event. For example:
added_to_cartwith no order within an hour sends a push with the guest's cart. - Reports. unPLUG builds abandoned-cart and funnel reports (menu, item, cart, checkout, order) from them, so the brand can see where guests drop off.
Don't send orders as events. unPLUG already gets them from POST /v1/orders, and points earned at the register also show up.
Recommended events#
Use these names so unPLUG's reports and journey templates pick them up without mapping. You can add your own names in snake_case too.
| Event | Send it when | Properties |
|---|---|---|
app_opened | The app comes to the foreground | source: direct, push or deep_link. message_id when opened from a push. |
session_started | A new session starts: launch, or back after 30 minutes away | None |
screen_viewed | A screen appears | screen: home, menu, rewards, cart, checkout |
product_viewed | An item's detail opens | pos_item_id, name, price_cents, currency, location_id |
added_to_cart | An item goes in the cart | cart_id, pos_item_id, name, quantity, price_cents, currency, location_id |
removed_from_cart | An item comes out of the cart | cart_id, pos_item_id, quantity |
checkout_started | The guest opens checkout | cart_id, subtotal_cents, currency, item_count, location_id, fulfillment |
signed_up | A new account is created | method: email, phone, apple, google |
signed_in | An existing account signs in | method |
push_permission_changed | The OS notification permission changes | enabled: true or false |
reward_viewed | A reward or wallet item's detail opens | reward_id, or wallet_item_id |
Use the same pos_item_id values you send at checkout (Square catalog variation ids) and a cart_id that stays the same for one cart, so unPLUG can follow a cart from first item to order.
Ids on every event#
| Field | What it is |
|---|---|
event_id | A UUID you create when the event happens. unPLUG drops any event whose event_id it has already seen, so retries never count twice. |
anonymous_id | A UUID you create once per install, before the guest signs in. Keep sending it after sign-in. |
device_id | The same id you use to register the device. |
session_id | A new UUID per session. |
occurred_at | When it happened on the device, not when you sent it. |
Before and after sign-in#
A guest often browses the menu before they sign in. Those events matter for the funnel, but the app has no member token yet.
- Before sign-in, send events to your own server, and have it forward them to unPLUG with the secret key and
anonymous_id. - After sign-in, send them straight from the app with the member token. The member is taken from the token. Keep the same
anonymous_idon the events, so the browsing before sign-in and the order after it read as one visit. - On sign out, go back to step 1. Keep the
anonymous_id: it belongs to the install, not the guest.
Your website works the same way: a page with a member token sends directly; anonymous events go through your server.
Send a batch#
Queue events on the device and send them in batches of up to 100: every 30 seconds while the app is open, and when it goes to the background. Store the queue on disk, so events survive the app being killed.
curl -X POST https://api.unplugdining.com/v1/app_events \
-H "Authorization: Bearer mt_4kQ9xV2mR7tB8nW3pL6yZ1cH5fJ0dS" \
-H "Content-Type: application/json" \
-d '{
"events": [
{
"event_id": "e2b7c4a1-9d38-4f65-8a0e-3c1f6b9d2e57",
"name": "added_to_cart",
"occurred_at": "2026-11-03T14:12:40Z",
"anonymous_id": "c5f1a8e3-7b2d-4c96-91e4-0a6d3f8b2c71",
"device_id": "9c7e2f14-58ab-4d3e-b1a0-6f2d8e47c935",
"session_id": "1d8e4b2f-6a3c-4e91-b7d5-2f0c9a8e6b34",
"properties": {
"cart_id": "cart_5Tn8qW2x",
"pos_item_id": "HV4MZ2TQ7LW3KXRB5P8NJ6DC",
"name": "Oat latte",
"quantity": 1,
"price_cents": 575,
"currency": "USD",
"location_id": "8d4e2a17-3b9c-4f5e-a6d1-7c0b9e2f4a53"
}
},
{
"event_id": "7a3d9f0c-2e6b-4b18-a4c7-5e9b1d3f8a26",
"name": "checkout_started",
"occurred_at": "2026-11-03T14:13:05Z",
"anonymous_id": "c5f1a8e3-7b2d-4c96-91e4-0a6d3f8b2c71",
"device_id": "9c7e2f14-58ab-4d3e-b1a0-6f2d8e47c935",
"session_id": "1d8e4b2f-6a3c-4e91-b7d5-2f0c9a8e6b34",
"properties": {
"cart_id": "cart_5Tn8qW2x",
"subtotal_cents": 1000,
"currency": "USD",
"item_count": 2,
"location_id": "8d4e2a17-3b9c-4f5e-a6d1-7c0b9e2f4a53",
"fulfillment": "pickup"
}
}
]
}'const response = await fetch("https://api.unplugdining.com/v1/app_events", {
method: "POST",
headers: {
Authorization: "Bearer mt_4kQ9xV2mR7tB8nW3pL6yZ1cH5fJ0dS",
"Content-Type": "application/json",
},
body: JSON.stringify({
events: [
{
event_id: "e2b7c4a1-9d38-4f65-8a0e-3c1f6b9d2e57",
name: "added_to_cart",
occurred_at: "2026-11-03T14:12:40Z",
anonymous_id: "c5f1a8e3-7b2d-4c96-91e4-0a6d3f8b2c71",
device_id: "9c7e2f14-58ab-4d3e-b1a0-6f2d8e47c935",
session_id: "1d8e4b2f-6a3c-4e91-b7d5-2f0c9a8e6b34",
properties: {
cart_id: "cart_5Tn8qW2x",
pos_item_id: "HV4MZ2TQ7LW3KXRB5P8NJ6DC",
name: "Oat latte",
quantity: 1,
price_cents: 575,
currency: "USD",
location_id: "8d4e2a17-3b9c-4f5e-a6d1-7c0b9e2f4a53",
},
},
{
event_id: "7a3d9f0c-2e6b-4b18-a4c7-5e9b1d3f8a26",
name: "checkout_started",
occurred_at: "2026-11-03T14:13:05Z",
anonymous_id: "c5f1a8e3-7b2d-4c96-91e4-0a6d3f8b2c71",
device_id: "9c7e2f14-58ab-4d3e-b1a0-6f2d8e47c935",
session_id: "1d8e4b2f-6a3c-4e91-b7d5-2f0c9a8e6b34",
properties: {
cart_id: "cart_5Tn8qW2x",
subtotal_cents: 1000,
currency: "USD",
item_count: 2,
location_id: "8d4e2a17-3b9c-4f5e-a6d1-7c0b9e2f4a53",
fulfillment: "pickup",
},
},
],
}),
});
const data = await response.json();import requests
response = requests.post(
"https://api.unplugdining.com/v1/app_events",
headers={
"Authorization": "Bearer mt_4kQ9xV2mR7tB8nW3pL6yZ1cH5fJ0dS",
},
json={
"events": [
{
"event_id": "e2b7c4a1-9d38-4f65-8a0e-3c1f6b9d2e57",
"name": "added_to_cart",
"occurred_at": "2026-11-03T14:12:40Z",
"anonymous_id": "c5f1a8e3-7b2d-4c96-91e4-0a6d3f8b2c71",
"device_id": "9c7e2f14-58ab-4d3e-b1a0-6f2d8e47c935",
"session_id": "1d8e4b2f-6a3c-4e91-b7d5-2f0c9a8e6b34",
"properties": {
"cart_id": "cart_5Tn8qW2x",
"pos_item_id": "HV4MZ2TQ7LW3KXRB5P8NJ6DC",
"name": "Oat latte",
"quantity": 1,
"price_cents": 575,
"currency": "USD",
"location_id": "8d4e2a17-3b9c-4f5e-a6d1-7c0b9e2f4a53",
},
},
{
"event_id": "7a3d9f0c-2e6b-4b18-a4c7-5e9b1d3f8a26",
"name": "checkout_started",
"occurred_at": "2026-11-03T14:13:05Z",
"anonymous_id": "c5f1a8e3-7b2d-4c96-91e4-0a6d3f8b2c71",
"device_id": "9c7e2f14-58ab-4d3e-b1a0-6f2d8e47c935",
"session_id": "1d8e4b2f-6a3c-4e91-b7d5-2f0c9a8e6b34",
"properties": {
"cart_id": "cart_5Tn8qW2x",
"subtotal_cents": 1000,
"currency": "USD",
"item_count": 2,
"location_id": "8d4e2a17-3b9c-4f5e-a6d1-7c0b9e2f4a53",
"fulfillment": "pickup",
},
},
],
},
)
data = response.json(){
"accepted": 2,
"duplicates": 0
}From your server, use a secret key with the members:write scope and put member_id or anonymous_id on each event.
Retries and failures#
- Timeouts,
429and5xx. Keep the batch in the queue and send it again later with the sameevent_ids. Duplicates are dropped and counted induplicates. 401 token_expired. Get a new member token from your server, then resend.400 invalid_param. One bad event fails the batch, andparamnames it (events[1].occurred_at). Drop that event and send the rest; don't retry it forever.
No personal data#
Don't put names, email addresses, phone numbers, street addresses or anything the guest typed into properties. Ids, item names, prices and screen names are fine. unPLUG already has the member's contact details, linked by the member id; it doesn't need them in events.
Warning Events are stored as sent. Check your properties in the sandbox before release, so personal data never leaves the app by mistake.
Timing#
Events reach unPLUG within about 20 minutes. That's fine for an abandoned-cart journey that waits an hour, but not for anything the guest should see right away. For that, use the API response or a webhook.
Checklist#
- Every event has an
event_idcreated once and reused on retries. - The queue is on disk and flushes when the app goes to the background.
- Events before sign-in go through your server with
anonymous_id; events after go direct with the member token. pos_item_idandcart_idmatch what you send at checkout.- No personal data in properties.