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

# Setting Up Web Push Notifications with the Tappd SDK

> Send browser push notifications using the Web Push API — works on Chrome, Firefox, Edge, and Safari 16.4+ even when users aren't on your site.

Web push notifications let you reach users with timely, relevant messages even when they aren't actively browsing your site. The Tappd Web SDK uses the native Web Push API with VAPID keys, so notifications work across all modern browsers with a single integration.

## Prerequisites

Before you configure push notifications, make sure you have the following in place:

* **HTTPS**: Web push requires a secure origin (HTTPS). Localhost is exempt during development.
* **Service worker**: A `sw.js` file must be accessible at the root of your domain.
* **VAPID keys**: Generated automatically in the Tappd dashboard — no manual key management needed.
* **Identified users**: Users must be identified with `identify()` before you subscribe them.

## Browser Support

| Browser                     | Support         |
| --------------------------- | --------------- |
| Chrome (Desktop & Android)  | ✅               |
| Firefox (Desktop & Android) | ✅               |
| Edge                        | ✅               |
| Safari 16.4+ (macOS & iOS)  | ✅               |
| Opera                       | ✅               |
| Safari \< 16.4              | ❌ Not supported |

<Note>
  Safari 16.4+ uses the standard Web Push API with VAPID keys — the same flow as Chrome and Firefox. No separate Apple certificate is required for modern Safari.
</Note>

## Setup

<Steps>
  <Step title="Configure Web Push in the Dashboard">
    1. Log into your [Tappd Dashboard](https://gotappd.com).
    2. Navigate to **Settings > Apps** and select your web app.
    3. Go to **Push Configuration > Web Push**.
    4. Toggle **Web Push** on.
    5. Click **Generate Keys** to create your VAPID key pair.
    6. Optionally set a **Default Icon URL** and **Site Name** for your notifications.
  </Step>

  <Step title="Create the Service Worker">
    Create a file named `sw.js` at the root of your domain (e.g., `https://yourdomain.com/sw.js`). Copy the following code into it:

    ```javascript theme={null}
    // sw.js
    self.addEventListener('push', function(event) {
      const data = event.data ? event.data.json() : {};

      const options = {
        body: data.body || '',
        icon: data.icon || '/icon-192x192.png',
        badge: data.badge || '/badge-72x72.png',
        image: data.image,
        data: data.data || {},
        requireInteraction: data.requireInteraction || false,
        tag: data.tag,
        renotify: data.renotify || false,
        silent: data.silent || false,
        timestamp: data.timestamp || Date.now(),
        vibrate: data.vibrate || [200, 100, 200],
        actions: data.actions || []
      };

      event.waitUntil(
        self.registration.showNotification(data.title || 'Notification', options)
      );
    });

    // Handle notification clicks and auto-track opens
    self.addEventListener('notificationclick', function(event) {
      event.notification.close();

      const data = event.notification.data || {};
      const url = data.url || '/';

      // Fire-and-forget tracking to the Tappd SDK API
      const trackPromise = fetch('https://sdk.gotappd.com/api/v1/sdk/push/interactions', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-App-Id': data.appId || data.tappd_app_id
        },
        body: JSON.stringify({
          type: 'opened',
          deliveryLogId: data.tappd_delivery_log_id,
          enrollmentId: data.tappd_enrollment_id,
          journeyId: data.tappd_journey_id,
          stepId: data.tappd_step_id,
          customerId: data.tappd_customer_id,
          providerMessageId: data.providerMessageId
        })
      }).catch(function(error) {
        console.error('Failed to track push open:', error);
      });

      event.waitUntil(
        Promise.all([
          trackPromise,
          clients.matchAll({ type: 'window', includeUncontrolled: true }).then(function(clientList) {
            for (let i = 0; i < clientList.length; i++) {
              const client = clientList[i];
              if (client.url === url && 'focus' in client) {
                return client.focus();
              }
            }
            if (clients.openWindow) {
              return clients.openWindow(url);
            }
          })
        ])
      );
    });
    ```

    <Warning>
      The service worker file must be served from the root path (`/sw.js`). A file at `/assets/sw.js` will not have the correct scope to intercept push events.
    </Warning>
  </Step>

  <Step title="Identify the User and Subscribe">
    Users must be identified before you subscribe them to push notifications. Call `identify()` first, then `subscribeToPush()`:

    ```javascript theme={null}
    import { TappdSDK } from '@tappd/web-sdk';

    const tappd = new TappdSDK({
      appId: 'YOUR_APP_ID',
      apiUrl: 'https://sdk.gotappd.com/api/v1/sdk'
    });

    // Identify the user — required before subscribing
    await tappd.identify({
      external_id: 'user_123',
      email: 'john@example.com',
      name: 'John Doe'
    });

    // Subscribe to push notifications
    try {
      const subscription = await tappd.subscribeToPush();
      console.log('Successfully subscribed to push notifications');
    } catch (error) {
      console.error('Failed to subscribe:', error);
    }
    ```
  </Step>
</Steps>

## Checking and Managing Subscriptions

### Subscribe a User

Call `subscribeToPush()` after identifying the user. It accepts an optional options object:

```javascript theme={null}
// Basic subscription — uses your dashboard VAPID configuration
const subscription = await tappd.subscribeToPush();

// With options — override the service worker path if needed
const subscription = await tappd.subscribeToPush({
  serviceWorkerPath: '/sw.js'  // Path to your service worker (default: '/sw.js')
});
```

### Check Subscription Status

Before prompting the user, check whether they're already subscribed:

```javascript theme={null}
const isSubscribed = await tappd.isSubscribed();

if (isSubscribed) {
  console.log('User is already subscribed');
} else {
  await tappd.subscribeToPush();
}
```

### Unsubscribe a User

Give users a clear way to opt out:

```javascript theme={null}
async function unsubscribeFromPush() {
  try {
    await tappd.unsubscribeFromPush();
    console.log('Successfully unsubscribed');

    await tappd.track('push_notification.unsubscribed');
  } catch (error) {
    console.error('Failed to unsubscribe:', error);
  }
}

document.getElementById('disable-notifications').addEventListener('click', unsubscribeFromPush);
```

## Permission Prompt Types

Choose the prompt style that fits your UX. You can configure the default in the dashboard or override it in code.

<Tabs>
  <Tab title="Slidedown (Default)">
    A customizable slide-down banner appears at the top of the page, letting you explain the value of notifications before the browser prompt appears.

    ```javascript theme={null}
    await tappd.showPermissionPrompt('slidedown');
    ```
  </Tab>

  <Tab title="Bell Icon">
    A persistent bell icon sits on the page. Users click it when they're ready to subscribe.

    ```javascript theme={null}
    await tappd.showPermissionPrompt('bell');
    ```
  </Tab>

  <Tab title="Native Browser">
    Triggers the browser's built-in permission dialog immediately. Use this only when you're confident the user understands why you're asking.

    ```javascript theme={null}
    await tappd.showPermissionPrompt('native');
    ```
  </Tab>

  <Tab title="Custom UI">
    Build your own prompt with your app's design system, then call `subscribeToPush()` when the user clicks allow.

    ```javascript theme={null}
    // Show your custom modal or banner explaining the value,
    // then subscribe when the user confirms
    await tappd.subscribeToPush();
    ```
  </Tab>
</Tabs>

## Sending Notifications

<CardGroup cols={2}>
  <Card title="From the Dashboard" icon="browser" href="https://gotappd.com">
    Navigate to **Templates > Push Templates**, create or select a template, then attach it to a Journey. Target the Journey at specific customers or segments to send.
  </Card>

  <Card title="From the API" icon="code" href="/web-sdk/push-notifications#sending-notifications">
    Use the Tappd Management API to send notifications programmatically from your backend.

    ```javascript theme={null}
    const response = await fetch(
      'https://api.gotappd.com/v1/push/send',
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': 'Bearer YOUR_API_KEY'
        },
        body: JSON.stringify({
          customerId: 'customer_id',
          appId: 'app_id',
          notification: {
            title: 'Hello!',
            body: 'This is a test notification',
            url: 'https://example.com',
            icon: 'https://example.com/icon.png'
          }
        })
      }
    );
    ```
  </Card>
</CardGroup>

## Testing

Send a test notification directly from the dashboard:

1. Go to **Settings > Apps > \[Your App]**.
2. Scroll to **Web Push Configuration**.
3. Click **Test Web Push**.
4. Select a customer and fill in the notification details.
5. Click **Send Test Push**.

## Troubleshooting

<Accordion title="Service worker is not registering">
  **Likely causes:**

  * The `sw.js` file is not served from the domain root (`/sw.js`).
  * The page is not served over HTTPS (required in production).
  * The service worker file URL returns a 404.

  **Fix:** Open `https://yourdomain.com/sw.js` in your browser to confirm the file is accessible. Check the browser console for registration errors.
</Accordion>

<Accordion title="User denied the permission prompt">
  Once a user denies browser-level notification permission, the browser blocks future prompts from that origin.

  **Fix:** Use a custom UI prompt to explain the value of notifications *before* triggering the browser dialog. This gives users the context they need to accept. If denied, surface a settings link so users can re-enable notifications at their own pace.
</Accordion>

<Accordion title="Notifications are not being received">
  Work through this checklist:

  1. Call `await tappd.isSubscribed()` and confirm it returns `true`.
  2. Verify VAPID keys are generated and saved in the dashboard.
  3. Confirm the service worker is active — check the **Application > Service Workers** panel in DevTools.
  4. Ensure `identify()` was called with a valid `external_id` before subscribing.
  5. Verify the notification was actually sent from the dashboard or API.
</Accordion>

<Accordion title="Subscription has expired or become invalid">
  Push subscriptions can expire or be revoked by the browser.

  **Fix:** Poll `isSubscribed()` periodically and re-subscribe when needed:

  ```javascript theme={null}
  // Check daily and re-subscribe if the subscription has lapsed
  setInterval(async () => {
    const isSubscribed = await tappd.isSubscribed();
    if (!isSubscribed) {
      try {
        await tappd.subscribeToPush();
      } catch (error) {
        console.error('Re-subscription failed:', error);
      }
    }
  }, 24 * 60 * 60 * 1000); // Every 24 hours
  ```
</Accordion>
