> For the complete documentation index, see [llms.txt](https://kbase.whitelabel-loyalty.com/product/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://kbase.whitelabel-loyalty.com/product/loyalty-engine/audiences/audience-tutorials/create-audiences-based-on-user-wallet/create-an-audience-of-users-who-have-an-unredeemed-voucher-in-their-wallet.md).

# Create an audience of users who have an unredeemed voucher in their wallet

### Introduction

Sometimes you need to target users based on the vouchers currently sitting in their wallet rather than an event they've reported. A common example is sending a push notification to everyone who holds a specific voucher but hasn't redeemed it yet — a nudge to use it before it expires.

Because voucher wallet contents aren't exposed as standard query-builder data points, this audience is built using a **JSONata expression**. This gives you fine-grained control over which vouchers count, including their redemption, void, and expiry status.

Each expression below starts by declaring the values you need to edit. Update the variable(s) at the top of the expression, then paste the whole thing into the criterion — you don't need to touch the logic underneath.

***

### Configuration

1. Navigate to **Users > Audiences** and click **Create** in the top right corner.
2. Give the audience a name and description. Turn on `AUDIENCE_JOINED` and `AUDIENCE_LEFT` events if needed. Set the refresh rate to **Daily Reactive** (voucher expiry changes over time, so the audience needs to re-evaluate on a schedule).
3. Move to the criteria tab and add a **JSONata expression** criterion. Use the expression below, editing the variable at the top:

```
(
  $rewardId := "enter reward ID here";  /* ← EDIT: the reward ID of the voucher you're targeting */

  $exists(rewardData[0].vouchers[
    reward.id = $rewardId
    and redeemedAt = null
    and voidedAt = null
    and (expiresAt = null or $toMillis(expiresAt) > $millis())
  ])
)
```

4. Click **save**.

#### How the expression works

The expression checks the user's active vouchers for at least one that matches every condition:

* `reward.id = $rewardId` — the voucher is for the specific reward you set at the top.
* `redeemedAt = null` — the voucher has **not** been redeemed.
* `voidedAt = null` — the voucher has **not** been voided.
* `expiresAt = null or $toMillis(expiresAt) > $millis()` — the voucher either never expires, or its expiry is still in the future. `$millis()` returns the current time in milliseconds, and `$toMillis()` converts the voucher's expiry timestamp for comparison.

`$exists(...)` returns `true` if one or more vouchers match, so the user joins the audience. When they redeem the voucher, it's voided, or it expires, they no longer match and leave the audience on the next refresh.

***

### Variants

#### A specific voucher due to expire within a set time

Target holders of a specific, still-valid voucher whose expiry falls within the next X days — ideal for a "use it before it's gone" reminder. Edit both variables at the top.

```
(
  $rewardId := "enter reward ID here";  /* ← EDIT: the reward ID of the voucher you're targeting */
  $daysToExpiry := 7;                    /* ← EDIT: expiring within this many days */

  $window := $daysToExpiry * 24 * 60 * 60 * 1000;

  $exists(rewardData[0].vouchers[
    reward.id = $rewardId
    and redeemedAt = null
    and voidedAt = null
    and expiresAt != null
    and $toMillis(expiresAt) > $millis()
    and $toMillis(expiresAt) < ($millis() + $window)
  ])
)
```

#### A specific voucher held for longer than a set time

Target holders of a specific voucher that's still valid (unredeemed, unexpired, unvoided) but which they've had in their wallet for more than X days — based on when the voucher was issued. Good for re-engaging users who've been sitting on a voucher without using it. Edit both variables at the top.

```
(
  $rewardId := "enter reward ID here";  /* ← EDIT: the reward ID of the voucher you're targeting */
  $daysHeld := 30;                       /* ← EDIT: held for longer than this many days */

  $threshold := $daysHeld * 24 * 60 * 60 * 1000;

  $exists(rewardData[0].vouchers[
    reward.id = $rewardId
    and redeemedAt = null
    and voidedAt = null
    and (expiresAt = null or $toMillis(expiresAt) > $millis())
    and $toMillis(createdAt) < ($millis() - $threshold)
  ])
)
```

#### ANY voucher due to expire within a set time

Same as the specific-expiry variant but ignores the reward ID, so it matches any active voucher the user holds that's expiring within the window. Good for a generic "you have a voucher expiring soon" campaign — edit only the number of days.

```
(
  $daysToExpiry := 7;   /* ← EDIT: expiring within this many days */

  $window := $daysToExpiry * 24 * 60 * 60 * 1000;

  $exists(rewardData[0].vouchers[
    redeemedAt = null
    and voidedAt = null
    and expiresAt != null
    and $toMillis(expiresAt) > $millis()
    and $toMillis(expiresAt) < ($millis() + $window)
  ])
)
```

#### ANY unredeemed, valid voucher

Matches users holding at least one active voucher of any kind that hasn't been redeemed, voided, or expired — useful for re-engaging users who have unused rewards waiting. Nothing to edit.

```
$exists(rewardData[0].vouchers[
  redeemedAt = null
  and voidedAt = null
  and (expiresAt = null or $toMillis(expiresAt) > $millis())
])
```

#### Holds a specific voucher, regardless of expiry

If you don't care about the expiry date and only want everyone currently holding a specific, unredeemed, non-voided voucher — edit only the reward ID.

```
(
  $rewardId := "enter reward ID here";  /* ← EDIT: the reward ID of the voucher you're targeting */

  $exists(rewardData[0].vouchers[
    reward.id = $rewardId
    and redeemedAt = null
    and voidedAt = null
  ])
)
```

***

### Tips

* **Finding the reward ID:** The `reward.id` value is the underlying reward ID of the voucher, not the voucher instance ID. You can find it on the reward in the Loyalty Console.
* **Editing the variables:** Only change the lines marked `← EDIT` at the top of each expression. `$daysToExpiry` and `$daysHeld` are plain numbers of days — the expression converts them to milliseconds for you, so there's no maths to do.
* **Set the refresh rate to Daily Reactive** for any time-based variant. Expiry and wallet age change over time, so the audience must re-evaluate regularly: for the "expiring soon" variants, users join as their expiry date approaches and leave once the voucher lapses; for the "held longer than" variant, users join once a voucher passes the age threshold.
* **Combine with a campaign:** Once the audience is populated, attach it to a marketing campaign to send the push notification. See the Marketing module for setting up the notification itself.
* **Test with a known user:** Before relying on the audience, confirm a user you know holds the voucher appears in it. If they don't, double-check the reward ID.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://kbase.whitelabel-loyalty.com/product/loyalty-engine/audiences/audience-tutorials/create-audiences-based-on-user-wallet/create-an-audience-of-users-who-have-an-unredeemed-voucher-in-their-wallet.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
