unPLUGDevelopers

Challenges

Give guests a goal, such as "visit 3 times this week", and pay a reward when they reach it. Here is how challenges count progress and how to show them in your app.

Coming soon

Coming soon Challenges are designed and on our build list. The objects and endpoints here are what we plan to ship, and they can still change before release.

What a challenge is#

A challenge is a goal measured on qualifying orders inside a time window. When the member reaches the goal, unPLUG pays the reward in the same moment the order is credited. Your app shows the challenges, a progress bar and, for some challenges, a join button. You don't count anything yourself: progress comes from the orders you already send to POST /v1/orders.

Challenges are created in the unPLUG dashboard, drafted by unPLUG for a segment of guests, or created from your own tools through the Management API. Your app only shows them and lets guests join. Most brands start from a template.

Challenge types#

TypeGoalCoffee exampleProgress unit
visitsN qualifying visits in the window"Visit 3 times this week, get 50 bonus Beans"visits
streakAt least one qualifying visit in each of N days or weeks in a row"Coffee every week for 4 weeks, get a free drink"periods
spendSpend $X on qualifying orders in the window"Spend $40 this month, get a free bag of beans"cents
menu_discoveryBuy N different items from a set or a category"Try 3 of our 4 fall drinks"items

Streaks can count days or weeks (goal.period). Weekly is the default for coffee: a daily streak breaks on the first weekend away. A streak can allow grace periods (goal.grace_periods, usually 1), so one missed week doesn't end it.

What counts as a qualifying order#

Each challenge has a qualifying block with the same rules rewards use: minimum subtotal, channels, locations, fulfillment and available times. Empty lists mean no limit. Two guards stop guests from farming a challenge:

  • Minimum subtotal. A visit counts only if the subtotal reaches min_subtotal_cents. The coffee templates use 300 ($3), so splitting one order into three small ones doesn't work.
  • One counted visit per day. max_counted_per_day is 1 by default. A second order on the same day still earns points but doesn't add a visit.

Only orders with a member count, and each order counts once. The day is the store's local day, from the location's time_zone, so an order at 11:40 pm and another at 12:10 am are two days.

Windows#

  • Fixed. The same dates for everyone: { "kind": "fixed", "starts_at": "2026-11-02T06:00:00Z", "ends_at": "2026-11-09T06:00:00Z" } is Monday to Sunday in Austin.
  • Rolling. Each member gets their own window that starts when they join: { "kind": "rolling", "days": 7 }. The member challenge's window_ends_at tells you when theirs ends.

Joining#

  • Automatic. Every member in the audience is enrolled when the challenge goes live. Short visit challenges use this.
  • Opt-in. The member taps a join button in your app. Streaks and longer challenges use this: a guest who chose the goal is more likely to keep going.

Rewards#

A challenge pays one of two rewards when it's completed:

  • Bonus points: { "kind": "points", "points": 50 }. They show in activity as a bonus entry and fire points.changed.
  • A catalog reward: { "kind": "reward", "reward_id": "..." }. It lands in the wallet with source: "challenge" and fires reward.issued. The member challenge's reward_wallet_item_id points to it.

A member can complete a challenge once (max_completions_per_member: 1). A challenge can also have a total budget, budget.max_completions, after which nobody else completes it.

How progress is counted#

  1. You credit an order. POST /v1/orders, the same call you already make. Points earned at the register also count.
  2. unPLUG checks the member's challenges. For each one the order qualifies for, it adds progress. For each one it doesn't, it records the reason.
  3. The goal is reached. The reward is paid with the order, and the member challenge moves to completed.

Refunds roll progress back. If Maya's third visit is refunded in full, her Weekly regular goes back to 2 of 3. If that undoes a completion and the reward is unused, the reward is taken back: a wallet item is removed and bonus points are reversed. A balance never goes below zero.

Build the challenges screen#

Maya Lopez is in two challenges: Weekly regular (automatic, 2 of 3 visits done) and the 4-week streak (opt-in, not joined yet).

  1. Load the challenges. GET /v1/challenges with the member token returns the live challenges this member can see: name, image, goal and reward.
  2. Load the member's progress. GET /v1/members/me/challenges returns status and progress for each one. Match them on challenge_id.
  3. Draw a card per challenge. Goal, progress bar, time left from window_ends_at, and the reward. Put the challenge closest to completion on your home screen.
  4. Show a join button when the status is eligible. Call join on tap.
  5. Refresh after checkout. Reload progress when an order is credited, so the bar moves while the guest is still looking.

List challenges#

GETRequest
curl https://api.unplugdining.com/v1/challenges \
  -H "Authorization: Bearer mt_4kQ9xV2mR7tB8nW3pL6yZ1cH5fJ0dS"
Response200
{
  "data": [
    {
      "id": "4f9a2d61-3e8b-4c15-a7d0-b26e81f5c93a",
      "name": "Weekly regular",
      "description": "Visit 3 times this week and get 50 bonus Beans.",
      "image_url": "https://cdn.unplugdining.com/demo/challenges/weekly-regular.jpg",
      "type": "visits",
      "goal": { "target": 3, "unit": "visits", "period": null, "grace_periods": null, "item_set": [] },
      "qualifying": { "min_subtotal_cents": 300, "channels": [], "location_ids": [], "fulfillment": [], "available_times": [], "max_counted_per_day": 1 },
      "window": { "kind": "fixed", "starts_at": "2026-11-02T06:00:00Z", "ends_at": "2026-11-09T06:00:00Z" },
      "enrollment": "automatic",
      "reward": { "kind": "points", "points": 50 },
      "max_completions_per_member": 1,
      "budget": { "max_completions": 2000 },
      "status": "live",
      "version": 1
    },
    {
      "id": "d1e83b5c-7f02-4a69-9c4e-5b1a07d8e236",
      "name": "4-week streak",
      "description": "Visit once a week for 4 weeks and get a free drip coffee. You can miss one week.",
      "image_url": "https://cdn.unplugdining.com/demo/challenges/streak.jpg",
      "type": "streak",
      "goal": { "target": 4, "unit": "periods", "period": "week", "grace_periods": 1, "item_set": [] },
      "qualifying": { "min_subtotal_cents": 300, "channels": [], "location_ids": [], "fulfillment": [], "available_times": [], "max_counted_per_day": 1 },
      "window": { "kind": "fixed", "starts_at": "2026-11-02T06:00:00Z", "ends_at": "2026-12-07T06:00:00Z" },
      "enrollment": "opt_in",
      "reward": { "kind": "reward", "reward_id": "9a6e3d21-4c7b-4f8a-b2e5-0d1c6f3a7b98" },
      "max_completions_per_member": 1,
      "budget": { "max_completions": 800 },
      "status": "live",
      "version": 1
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Load progress#

GETRequest
curl https://api.unplugdining.com/v1/members/me/challenges \
  -H "Authorization: Bearer mt_4kQ9xV2mR7tB8nW3pL6yZ1cH5fJ0dS"
Response200
{
  "data": [
    {
      "challenge_id": "4f9a2d61-3e8b-4c15-a7d0-b26e81f5c93a",
      "status": "enrolled",
      "progress": { "current": 2, "target": 3, "unit": "visits", "periods": [] },
      "window_ends_at": "2026-11-09T06:00:00Z",
      "completed_at": null,
      "reward_wallet_item_id": null
    },
    {
      "challenge_id": "d1e83b5c-7f02-4a69-9c4e-5b1a07d8e236",
      "status": "eligible",
      "progress": { "current": 0, "target": 4, "unit": "periods", "periods": [] },
      "window_ends_at": "2026-12-07T06:00:00Z",
      "completed_at": null,
      "reward_wallet_item_id": null
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Draw the bar from current and target. For cents, format both as money ("$23.50 of $40"). For a streak, periods lists each day or week with met, so you can draw one dot per period instead of a bar.

Join an opt-in challenge#

POSTRequest
curl -X POST https://api.unplugdining.com/v1/members/me/challenges/d1e83b5c-7f02-4a69-9c4e-5b1a07d8e236/join \
  -H "Authorization: Bearer mt_4kQ9xV2mR7tB8nW3pL6yZ1cH5fJ0dS"
Response200
{
  "challenge_id": "d1e83b5c-7f02-4a69-9c4e-5b1a07d8e236",
  "status": "enrolled",
  "progress": {
    "current": 0,
    "target": 4,
    "unit": "periods",
    "periods": [
      { "starts_at": "2026-11-02T06:00:00Z", "ends_at": "2026-11-09T06:00:00Z", "met": false }
    ]
  },
  "window_ends_at": "2026-12-07T06:00:00Z",
  "completed_at": null,
  "reward_wallet_item_id": null
}

Join returns 422 challenge_not_joinable when the challenge is automatic, has ended, has used its budget, or isn't open to this member. Hide the button and reload the list.

Show why an order didn't count#

When a visit doesn't count, guests want to know why. The member's activity carries a reason for each challenge an order didn't count toward. The reasons are the checkout reason codes plus already_counted_today. Turn them into one plain line:

ReasonWhat to show
below_minimum_subtotalOrders under $3 don't count toward this challenge.
already_counted_todayWe already counted a visit today. Come back tomorrow.
wrong_channelOnly app orders count toward this challenge.
wrong_locationThis challenge is for the Riverside store only.
outside_available_timesOnly orders after 2 pm count.
item_not_in_cartThis order didn't have one of the fall drinks.
not_started, expiredThe order was outside the challenge dates.

Store staff see the same reasons in the unPLUG dashboard, so your support team and the store give the same answer.

Coffee template pack#

Every template is a starting point: every field can be changed before publishing. GET /v1/challenge_templates returns them.

TemplateGoalDefault rewardJoining
Weekly regular3 visits, Monday to SundayBonus points worth about one add-onAutomatic
4-week streakA visit every week for 4 weeks, 1 grace weekFree drinkOpt-in
Seasonal discoveryTry 3 of the seasonal drinks before the menu endsFree seasonal drinkAutomatic
Afternoon pick-me-up3 visits after 2 pm in 2 weeksBonus pointsAutomatic
Breakfast pairing3 orders with a drink and a food itemFree pastryAutomatic
Order ahead3 mobile orders in 2 weeksBonus pointsAutomatic
Win-back2 visits in 14 days, for guests who stopped comingFree drinkAutomatic
Bean bagSpend $40 in a monthFree bag of beansOpt-in

All of them count a visit only from $3 and at most once a day.

Messages unPLUG can send#

unPLUG can message the guest at each step of a challenge, by push and full-screen in-app message. The brand writes the copy per challenge, and unPLUG's frequency caps and quiet hours apply. If your app already handles push and in-app messages, these need no extra code. Give each challenge a deep link (for example yourapp://challenges/4f9a2d61-3e8b-4c15-a7d0-b26e81f5c93a) so the buttons land on the right card.

MomentWhenExample
LaunchThe challenge goes live, or the member becomes eligible"New this week: visit 3 times, get 50 Beans."
First progressThe first qualifying order"1 down, 2 to go."
One more to goProgress reaches the target minus one"One more visit by Sunday for 50 Beans."
CompletedThe goal is reached and the reward paid"Done. 50 Beans are in your account."
Expiring24 hours before the window ends, if not completed"Last day to finish Weekly regular."
Streak at riskThe current week or day has no visit yet and is about to end"Keep your streak: stop by before Sunday."

Webhooks#

Your server can follow challenges with three events, all Coming soon: challenge.joined, challenge.progressed and challenge.completed. The reward itself fires the events you already handle: points.changed for bonus points, reward.issued for a wallet reward. See Webhook event types.

Before you go live#

  • Your challenges screen handles every status: eligible, enrolled, completed and expired.
  • The join button handles 422 challenge_not_joinable by reloading the list.
  • Each progress unit has its own formatting: visits, weeks, dollars and items.
  • The guest can see why an order didn't count, in plain words.
  • You reload progress after an order is credited and after a refund.
  • Challenge deep links open the right card.