> ## Documentation Index
> Fetch the complete documentation index at: https://documentation.onesignal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Back-in-stock alerts

> Send restock alerts via push, email, or SMS to users who opted in. Use SKU-keyed tags with the Create Message API or Custom Events with a Journey.

Back-in-stock alerts let you recapture lost sales when a product goes out of stock. A "Notify me when available" button on your product page lets users opt in, and OneSignal sends the restock alert the moment the item is available again. The same flow works for push, email, or SMS — you choose the channel when you create the message template.

This guide covers two approaches. Pick the one that matches how you store back-in-stock requests today:

* **Tag-based** — Use this when OneSignal is the only system that needs to know who opted in. Each user-SKU pair is recorded as a tag, and a single Create Message API call fans out the restock alert to everyone tagged for that SKU.
* **Custom Events with Journey** — Use this when you already store `(user, SKU)` records on your own backend. The opt-in fires a Custom Event that enters a single Journey; the Journey waits for a matching restock event and sends the alert. This trades a heavier customer-side build for native deduplication, no per-user tag limits, and Journey-level controls (expiration, frequency caps, channel sequencing).

## Choose your approach

|                       | Tag-based                                                       | Custom Events with Journey                                                          |
| --------------------- | --------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| Where opt-ins live    | OneSignal (as user tags)                                        | Your backend                                                                        |
| Per-restock work      | One `POST /notifications` call with a tag filter                | Backend iterates `(user, SKU)` records and fires one Custom Event per opted-in user |
| Cleanup after send    | Required: segment → CSV export → blank → re-import              | None — user exits the Journey naturally after the message step                      |
| Per-user opt-in limit | Plan-dependent — heavy opt-in users can hit per-user tag limits | Not a concern                                                                       |
| Dedup across restocks | Manual via cleanup workflow                                     | Native — Wait Until + Event Matching ensures one send per `(user, SKU)` instance    |
| Multi-step nurture    | Requires another tool (no built-in delays, channel sequencing)  | Built into the Journey (delays, channel branches, control groups, A/B)              |
| Customer build cost   | Low (opt-in tag + one send call)                                | Higher (database + opt-in event + per-user fan-out)                                 |

The two approaches are independent — pick one and skip the other section.

<CardGroup cols={2}>
  <Card title="Approach 1: Tag-based" icon="tags" href="#approach-1%3A-tag-based">
    Lower build cost. OneSignal is your system of record. One API call fans out the alert at restock time.
  </Card>

  <Card title="Approach 2: Custom Events with Journey" icon="bolt" href="#approach-2%3A-custom-events-with-journey">
    Higher build cost. Your backend stores opt-ins. Native dedup via Event Matching, no tag limits.
  </Card>
</CardGroup>

## Prerequisites

Before you start, make sure you have:

* Subscribers opted in to the channel you plan to use (push permission, email subscription, or SMS opt-in)
* Access to your app's codebase or backend to record opt-ins when a user taps "Notify me"
* A signal from your inventory or commerce system (such as a webhook or scheduled job) that fires when a SKU returns to stock
* For the Custom Events approach: a `(user, SKU)` table on your backend so you can fire one restock event per opted-in user

***

## Approach 1: Tag-based

This approach runs in three steps: tag the user at opt-in, fire one API call at restock to fan out the alert, then clean up the tags so users don't receive duplicate messages on the next restock.

Each user gets one tag per SKU they opt in to: `bis_requested_<SKU> = "1"`. When the SKU restocks, you call the Create Message API with a filter on that tag and OneSignal fans out the alert.

### Set up your tag structure

For this flow, each user gets one tag per SKU they opt in to. The tag key encodes the SKU; the value is a sentinel.

| Tag key               | Example value | What it does                                                                                                                                                                                   |
| --------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `bis_requested_<SKU>` | `"1"`         | Existence on a user signals that user has opted in for that SKU. The product name, image, and cart link are passed in `custom_data` at send time, so they don't need to be stored on the user. |

If a user opts in for two products, they have two independent tags:

```text theme={null}
bis_requested_SKU_1234 = "1"
bis_requested_SKU_5678 = "1"
```

<Warning>
  Per-user tag count is plan-dependent (for example, 20 tags per user on lower-tier plans). When a user reaches the limit, further `addTag` operations **fail silently**. Always remove the tag after the restock alert is sent so a heavy opt-in user doesn't get stuck. See [plan limits](https://onesignal.com/pricing) for your tier.
</Warning>

<Note>
  **Use the SKU verbatim from your backend.** Tag keys via the SDK and API accept spaces, uppercase letters, and most punctuation, so `bis_requested_SKU 123` works as a tag key just as well as `bis_requested_SKU_1234`. Tag keys are **case-sensitive**, so pull the SKU from a single source (such as your product database) and use the exact same value at opt-in and at send time, or your filter will miss users.
</Note>

### Tag the user when they tap "Notify me"

When a user taps your "Notify me when available" button, set `bis_requested_<SKU>` on that user. You can do this from your app via the OneSignal SDK, or from your backend via the REST API.

<Tabs>
  <Tab title="SDK (Recommended)">
    Call the OneSignal SDK from your app when the user taps the button. This is the most direct approach for mobile and web apps. The example below is JavaScript; the same `addTag` call exists across platforms.

    | Reference       | Description                                                                    |
    | --------------- | ------------------------------------------------------------------------------ |
    | `addTag` method | [Mobile SDK](./mobile-sdk-reference#tags), [Web SDK](./web-sdk-reference#tags) |

    ```javascript JavaScript theme={null}
    const sku = "SKU_1234"; // the SKU as it appears in your product database
    OneSignal.User.addTag(`bis_requested_${sku}`, "1");
    ```

    <Check>
      The tag is applied instantly. The user is now included in any audience filter that targets `bis_requested_SKU_1234`.
    </Check>
  </Tab>

  <Tab title="REST API">
    Use this approach when the opt-in is recorded on your backend, for example when a logged-in user taps "Notify me" on a server-rendered page.

    ```bash theme={null}
    curl -X PATCH \
      https://api.onesignal.com/apps/{app_id}/users/by/external_id/{external_id} \
      -H "Content-Type: application/json" \
      -H "Authorization: Key YOUR_REST_API_KEY" \
      -d '{
        "properties": {
          "tags": {
            "bis_requested_SKU_1234": "1"
          }
        }
      }'
    ```

    Replace `{app_id}` with your OneSignal App ID and `{external_id}` (the value after `/by/external_id/`) with the user's ID in your system. Keep the literal `external_id` in the path — it tells OneSignal which alias type you are using. Replace `YOUR_REST_API_KEY` with your REST API key from **Settings > Keys & IDs**.
  </Tab>
</Tabs>

### Create a message template

A template lets you define the message content once and reuse it across every restock send. You create it in the dashboard and reference it by ID in your API calls.

1. In your OneSignal dashboard, go to **Templates** and click **New Template**.

2. Select the template type for the channel you want to send on — **Push Notification**, **Email**, or **SMS**.

3. Fill in the fields. The product name, image, and cart link are pulled from `custom_data` you pass at send time:

   | Field          | Value                                                                        |
   | -------------- | ---------------------------------------------------------------------------- |
   | **Title**      | `{{message.custom_data.product_name}} is back in stock!`                     |
   | **Message**    | `{{message.custom_data.product_name}} is back! Grab it before it sells out.` |
   | **Image**      | `{{message.custom_data.image_url}}`                                          |
   | **Launch URL** | `{{message.custom_data.cart_url}}`                                           |

4. Click **Save Template** and copy the **Template ID**. You will use it in the next step.

<Note>
  The same template handles every SKU because the product details come from `custom_data` at send time, not from the user's tags. You don't need a separate template per product.
</Note>

<Tip>
  **Email and SMS variations.** For email, use the same `custom_data` Liquid in the subject line and HTML body; the image and launch URL fields don't apply. For SMS, keep the body short and put the cart link inline (for example, `{{message.custom_data.product_name}} is back: {{message.custom_data.cart_url}}`).
</Tip>

### Send the restock alert

When your inventory system detects that a SKU is back in stock, send the message to all users tagged with `bis_requested_<SKU>`.

<Tabs>
  <Tab title="API (Recommended)">
    Trigger the following [Create message](/reference/create-message) call from your backend when a SKU is restocked. OneSignal targets only users with the matching tag and personalizes the message using the `custom_data` you provide.

    ```bash theme={null}
    curl -X POST \
      https://api.onesignal.com/notifications \
      -H "Content-Type: application/json" \
      -H "Authorization: Key YOUR_REST_API_KEY" \
      -d '{
        "app_id": "YOUR_APP_ID",
        "filters": [
          { "field": "tag", "key": "bis_requested_SKU_1234", "relation": "exists" }
        ],
        "target_channel": "push",
        "template_id": "YOUR_TEMPLATE_ID",
        "custom_data": {
          "product_name": "Pink Clogs",
          "image_url": "https://yourstore.com/images/pink-clogs.jpg",
          "cart_url": "https://yourstore.com/cart/SKU_1234"
        }
      }'
    ```

    Replace `YOUR_APP_ID` with your OneSignal App ID, `YOUR_TEMPLATE_ID` with the Template ID you copied in the previous step, and `YOUR_REST_API_KEY` with your REST API key from **Settings > Keys & IDs**. Set `target_channel` to `email` or `sms` to send on those channels instead — the `template_id` must reference a template of the matching type.

    <Check>
      When the call succeeds, OneSignal delivers the message to every user tagged with `bis_requested_SKU_1234`. The product name, image, and cart link are pulled from `custom_data`, so the same template renders correctly for any SKU.
    </Check>
  </Tab>

  <Tab title="Dashboard">
    Use this approach for one-off or low-volume restocks where you want to send manually from the dashboard.

    1. Go to **Audience > Segments** and click **New Segment**.
    2. Add a filter: **User Tag** → `bis_requested_SKU_1234` **exists**.
    3. Save the segment (e.g. name it `BIS - SKU_1234`).
    4. Go to **Messages** and click **New Push**, **New Email**, or **New SMS** — pick the channel that matches your template — and select the segment you created.
    5. Select your restock template under **Templates**.
    6. Add the product name, image URL, and cart URL to **Additional Data** so the template's `custom_data` Liquid renders correctly.
    7. Review and click **Send**.

    <Warning>
      The dashboard approach requires creating a new segment for every SKU you want to notify about. For multiple SKUs or automated restocks, use the API instead.
    </Warning>
  </Tab>
</Tabs>

### Clean up after sending

After a successful send, remove `bis_requested_<SKU>` from the users who received the alert so they don't get the same message twice on the next restock. Use OneSignal's [Bulk tag updates](./import#bulk-tag-updates) workflow — segment, export, blank, re-import:

<Steps>
  <Step title="Create a segment of opted-in users">
    Create a segment that filters users where `bis_requested_<SKU>` exists. You can do this in **Audience > Segments** in the dashboard, or via the [Create segments](/reference/create-segments) API.

    For SKU\_1234, the segment is one filter: **User Tag** → `bis_requested_SKU_1234` **exists**. Name it something predictable like `BIS - SKU_1234` so you can find it for cleanup.
  </Step>

  <Step title="Export the segment to CSV">
    Go to **Audience > Subscriptions** in the dashboard, filter by the segment, enable the **External ID**, **Subscription ID**, and **Tags** columns, and click **Export**. Or call the [CSV export](/reference/csv-export) API with `segment_name` set to the segment from the previous step.
  </Step>

  <Step title="Blank the tag value in the CSV">
    Open the exported CSV and clear the value for `bis_requested_SKU_1234` on every row. Leave the identifier column (External ID or Subscription ID) and the other tag values intact.
  </Step>

  <Step title="Re-import with delete enabled">
    Upload the edited CSV in **Audience > Import**. On the Review screen, enable **Delete tags with blank values**. OneSignal removes `bis_requested_SKU_1234` from every user in the segment.
  </Step>
</Steps>

<Tip>
  For the full reference — including a worked example with JSON-formatted tag rows — see [Bulk tag updates](./import#bulk-tag-updates).
</Tip>

***

## Approach 2: Custom Events with Journey

This approach runs in three steps: fire a Custom Event at opt-in to enter the Journey, hold the user at a Wait Until step until a matching restock event arrives for that SKU, then send the alert automatically.

Each opt-in fires a `bis_requested` Custom Event that enters a Journey. The Journey sends an immediate acknowledgment, then holds the user at a Wait Until step until your backend fires a matching `bis_item_restocked` event for that SKU. [Event Matching](./journeys-actions#event-matching) on the Wait Until step ensures each Journey instance is paired to the right SKU, so a single Journey handles every product in your catalog.

This approach assumes you already store `(user, SKU)` records on your own backend so you can fan out the restock event when stock returns.

### Send the opt-in event

When the user taps "Notify me when available," fire a `bis_requested` Custom Event with the SKU and item name as properties. **Fire one event per SKU.** If a user opts in to three SKUs, send three separate events — each one creates an independent Journey instance for that user.

<Tabs>
  <Tab title="SDK (Recommended)">
    Call the OneSignal SDK from your app when the user taps the button. The example below is JavaScript; the same `trackEvent` call exists across platforms.

    | Reference           | Description                                                                                      |
    | ------------------- | ------------------------------------------------------------------------------------------------ |
    | `trackEvent` method | [Mobile SDK](./mobile-sdk-reference#custom-events), [Web SDK](./web-sdk-reference#custom-events) |

    ```javascript JavaScript theme={null}
    const sku = "pink_clogs";
    const itemName = "Pink Clogs";
    OneSignal.User.trackEvent("bis_requested", {
      sku: sku,
      item_name: itemName,
    });
    ```

    <Check>
      The event enters the user into the Journey instantly. If they opt in to another SKU, fire another `bis_requested` event with the new SKU — they will run two parallel Journey instances.
    </Check>
  </Tab>

  <Tab title="REST API">
    Use this approach when the opt-in is recorded on your backend, for example when a logged-in user taps "Notify me" on a server-rendered page. Fire one event per SKU.

    ```bash theme={null}
    curl -X POST \
      https://api.onesignal.com/apps/{app_id}/custom_events \
      -H "Content-Type: application/json" \
      -H "Authorization: Key YOUR_REST_API_KEY" \
      -d '{
        "events": [
          {
            "external_id": "user_123",
            "name": "bis_requested",
            "properties": {
              "sku": "pink_clogs",
              "item_name": "Pink Clogs"
            }
          }
        ]
      }'
    ```

    See [Create custom events](/reference/create-custom-events) for the full schema, batching, and `idempotency_key` support.
  </Tab>
</Tabs>

<Note>
  You can also fire `bis_requested` from a third-party platform via [Custom event sources](./integrations#custom-event-sources) (Segment, Amplitude, Mixpanel, BigQuery, and others). The Journey works the same regardless of source.
</Note>

### Build the Journey

Create one Journey that handles every SKU. Wait Until's Event Matching feature pairs each Journey instance to the correct restock event by `sku`.

<Steps>
  <Step title="Set the entry rule">
    In **Messages > Journeys**, create a new Journey. Set the entry rule to **Custom Event** and select `bis_requested`.

    See [Custom events entry rules](./journeys-settings#custom-events) for full details on Custom Event entry.
  </Step>

  <Step title="Send an acknowledgment message">
    Add a message step right after entry to confirm the opt-in. Pick the channel that matches your alert (push, email, or SMS), create or select a template, and use Liquid to personalize with the entry event's properties.

    Example message body:

    ```liquid theme={null}
    Thanks — we'll let you know when {{ journey.first_event.properties.item_name }} is back in stock.
    ```

    See [Personalize with Custom Events](./personalization-custom-event) for the full Liquid reference for `journey.first_event`.
  </Step>

  <Step title="Add a Wait Until step with Event Matching">
    Add a **Wait Until** action and set the condition to a custom event named `bis_item_restocked`. Then enable **Event Matching** to pair the Journey instance to the right SKU:

    * **Trigger Event Property**: `sku` — the property on the entry `bis_requested` event
    * **Wait Event Property**: `sku` — the property on the `bis_item_restocked` event

    Because both properties are `sku`, only a `bis_item_restocked` event whose `sku` value matches the user's original opt-in advances them through the step. See [Event Matching](./journeys-actions#event-matching) for the canonical reference.

    Set an **expiration branch** so dormant instances don't accumulate forever. A reasonable default is six months, with **exit user** as the expiration action — tune this to your restock cadence (shorter for fast-fashion, longer for evergreen catalog items).
  </Step>

  <Step title="Send the restock message">
    Add a message step on the main branch of the Wait Until (the path users take when the event fires before expiration). Pick your channel and use Liquid to render the item name from the entry event:

    ```liquid theme={null}
    {{ journey.first_event.properties.item_name }} is back in stock! Tap to buy now.
    ```

    If you need fresh values from the restock event itself (for example, an updated price), reference them with `{{ journey.last_event.properties.<key> }}` — `journey.last_event` resolves to the most recently stored event, which is the `bis_item_restocked` event after Wait Until advances.
  </Step>

  <Step title="Exit the user">
    After the message step, the Journey ends and the instance exits naturally. No tag cleanup is needed — each `(user, SKU)` instance only sends once.

    If the user opts in to the same SKU again later, your backend should fire a new `bis_requested` event, which creates a fresh Journey instance.
  </Step>
</Steps>

### Trigger the restock event

When your inventory system detects that a SKU is back in stock, iterate the `(user, SKU)` records in your database and fire `bis_item_restocked` for each opted-in user. Each event must carry the `sku` so Event Matching pairs it to the correct Journey instance.

```bash theme={null}
curl -X POST \
  https://api.onesignal.com/apps/{app_id}/custom_events \
  -H "Content-Type: application/json" \
  -H "Authorization: Key YOUR_REST_API_KEY" \
  -d '{
    "events": [
      {
        "external_id": "user_123",
        "name": "bis_item_restocked",
        "properties": {
          "sku": "pink_clogs",
          "item_name": "Pink Clogs"
        }
      },
      {
        "external_id": "user_456",
        "name": "bis_item_restocked",
        "properties": {
          "sku": "pink_clogs",
          "item_name": "Pink Clogs"
        }
      }
    ]
  }'
```

You can batch many users into a single API call. Include `idempotency_key` on each event if you implement retries — see [Create custom events](/reference/create-custom-events) for the full schema.

<Check>
  When the events are processed, OneSignal advances the matching Journey instances through the Wait Until step and sends the restock message to each user. Instances that don't match the `sku` are unaffected.
</Check>

<Warning>
  Custom Events require an `external_id` (or `onesignal_id`) per event. There is no broadcast variant — your backend must enumerate the opted-in users for the SKU and fire one event per user. This is why the approach assumes you already store `(user, SKU)` records.
</Warning>

***

## FAQ

### Which approach should I use?

If OneSignal is your only system of record for back-in-stock requests, use the **tag-based approach** — it's the lower-build option and lets a single API call fan out the alert. If you already store `(user, SKU)` records on your backend (for analytics, abandoned-cart, or other reasons), use **Custom Events with Journey** — you avoid per-user tag limits, get native dedup via Event Matching, and inherit the rest of Journey's features (delays, channel branches, control groups). See [Choose your approach](#choose-your-approach) for the full comparison.

### Does OneSignal handle message throttling or frequency caps?

No. Any throttling — for example, frequency caps, batching, or how many restock alerts you send in a period — must be handled on your side (your app or backend). OneSignal does not apply those rules for you.

### Does OneSignal limit how many users can opt in to a back-in-stock alert?

No. OneSignal does not cap how many users can opt in for a SKU. When stock is limited, you should cap opt-ins yourself — for example, if a SKU only has 1,000 units, stop accepting new opt-ins (don't set the `bis_requested_<SKU>` tag or fire `bis_requested`) once 1,000 requests have been reached.

### How many SKUs can a single user opt in to?

For the **tag-based approach**, this depends on your plan's per-user tag limit. Once a user reaches the limit, further `addTag` calls fail silently. Plan to remove the tag after the restock alert is delivered so a heavy opt-in user is not blocked from new alerts. See [Tags](./add-user-data-tags) for limits and best practices.

For the **Custom Events approach**, there is no per-user opt-in limit — each opt-in is a separate Journey instance, not a tag.

***

## Next steps

<CardGroup cols={2}>
  <Card title="Tags" icon="tags" href="./add-user-data-tags">
    Add custom properties to users for personalization and segmentation.
  </Card>

  <Card title="Custom events" icon="bolt" href="./custom-events">
    Track user actions to trigger Journeys, personalization, and power analytics.
  </Card>

  <Card title="Journey actions" icon="diagram-project" href="./journeys-actions">
    Reference for Wait Until, Event Matching, and other Journey actions.
  </Card>

  <Card title="Create message API" icon="paper-plane" href="/reference/create-message">
    Full schema for the Create Message endpoint, including filters and `target_channel`.
  </Card>

  <Card title="Message personalization" icon="user" href="./message-personalization">
    Use Liquid to personalize messages with tags and `custom_data`.
  </Card>

  <Card title="Personalize with Custom Events" icon="code" href="./personalization-custom-event">
    Liquid syntax for `journey.first_event` and Custom Event properties in Journey messages.
  </Card>
</CardGroup>
