---
source_url: https://www.pubnub.com/docs/use-cases/sports-media-entertainment/real-time-score-alerts
title: Send a push alert when a goal is scored
updated_at: 2026-09-30T07:20:08.000Z
---

# Send a push alert when a goal is scored

## Documentation index

To discover more PubNub resources:

1. Fetch [PubNub's llms.txt](https://www.pubnub.com/llms-full.txt) for a list of available pages in Markdown format.
2. Identify relevant URLs from that index.
3. Fetch the target pages.

Do not assume a path exists, always check the index first.

In this tutorial, you register a device for push on the channel `game.score-alerts`. You then publish a goal alert to that channel from a Node.js script, and watch the alert reach the device as a system notification even though its app is closed.

[Mobile Push Notifications](https://www.pubnub.com/docs/integrations/mobile-push-notifications/overview.md) makes this possible. A push notification is an ordinary published message whose payload carries a special block. PubNub's network hands that block to Apple Push Notification service (APNs) or Firebase Cloud Messaging (FCM) on your behalf. None of it works unless the device is already registered on that channel, because a publish reaches only the devices registered at the moment it goes out.

## What you'll build

A fan device registers its provider token on `game.score-alerts`. Your match service then publishes a goal alert to that channel with a `pn_apns` or `pn_fcm` block in the payload. A subscriber with the app open receives it as an ordinary message. Mobile Push Notifications looks up the devices registered on the channel and hands the alert to APNs or FCM. On a device whose app is closed, the provider shows the alert as a system notification. In this tutorial, `score-alerts.js` plays both the device and the match service.

```mermaid
flowchart TB
    DEV["<b>Fan device</b><br/>adds its push token<br/>to the channel"]
    SVC["<b>Match service</b><br/>publishes the goal"]
    CH["<b>game.score-alerts</b>"]
    OPEN["<b>App open</b><br/>an ordinary message<br/>in the subscribe loop"]
    PUSH["<b>Mobile Push Notifications</b><br/>maps the message to<br/>registered devices"]
    APNS["<b>APNs</b>"]
    FCM["<b>FCM</b>"]
    BG["<b>App closed</b><br/>a system notification<br/>on the lock screen"]

    DEV -->|"1 · register"| PUSH
    SVC -->|"2 · publish"| CH
    CH --> OPEN
    CH --> PUSH
    PUSH --> APNS & FCM
    APNS & FCM --> BG

    class CH emphasis
    class PUSH muted
    class APNS,FCM external
```

## Before you begin

You need:

1. Node.js 22 or later.
2. A PubNub account and your own keyset. A keyset is the set of publish, subscribe, and secret keys that identifies your application to the PubNub network. If you don't have one, follow [Set up your account](https://www.pubnub.com/docs/architecture/authentication/set-up-your-account.md) to create one. You need its publish key and subscribe key.
3. Mobile Push Notifications enabled on that keyset, in the **Mobile Push Notifications** section of the [Admin Portal](https://admin.pubnub.com).
4. Provider credentials uploaded to the same keyset. For Android, that's an FCM service account private key file. For iOS, that's an APNs authentication key, a `.p8` file, together with its Team ID and Auth Key ID.
5. A real device, or for Android an emulator with Google Play services, that can obtain a provider token. A push notification can't be tested from Node.js alone, because Node never receives push. Only the operating system that owns the provider token does.

## Set up the project

Create a new directory and install the PubNub JavaScript SDK:

```bash
mkdir score-alerts-tutorial
cd score-alerts-tutorial
npm init -y
npm install pubnub
```

This tutorial uses `import` syntax, so add `"type": "module"` to `package.json`. Then create a file called `score-alerts.js` and add the PubNub client:

import PubNub from 'pubnub';
const pubnub = new PubNub({  publishKey: 'YOUR_PUBLISH_KEY',  subscribeKey: 'YOUR_SUBSCRIBE_KEY',  userId: 'fan-42',});

Replace `YOUR_PUBLISH_KEY` and `YOUR_SUBSCRIBE_KEY` with the keys from your keyset. One client plays both parts in this tutorial. It registers a device for push, and it publishes the alert, so you see the whole path in a single script. In your own application, a fan's device runs the registration and your match service runs the publish.

## Pick one APNs environment and use it everywhere

```javascript
// Every APNs call below, device registration, the notification payload's target,
// listing, and removal, reads this same value. An iOS device token only works in the
// APNs environment that issued it: a development (sandbox) token comes from a
// debug or development-signed build and only works with environment: 'development';
// a production token comes from a TestFlight or App Store build and only works with
// environment: 'production'. Registering with one value and publishing toward the
// other is why a registration can succeed while the notification it's supposed to
// produce never arrives. Change this one constant when you move from a development
// build to a TestFlight or App Store build, rather than editing every call below.
const APNS_ENVIRONMENT = 'development';
```

Add this to `score-alerts.js`, before any other code on this page. Every APNs call in this tutorial reads `APNS_ENVIRONMENT`, so changing it in one place changes device registration, listing, removal, and the notification's own target together.

An iOS device token only works in the APNs environment that issued it. A development, or sandbox, token comes from a debug or development-signed build. A production token comes from a build distributed through TestFlight or the App Store instead. Registering a token in one environment and then targeting the other in the published notification means the registration itself can succeed, since PubNub doesn't validate a token against Apple until it tries to deliver, while the notification silently never reaches that device, because it's asking the wrong APNs environment for a token that only exists in the other one. This tutorial uses `'development'` since that's what a token from a local Xcode build gives you. Switch it to `'production'` once you're testing against a TestFlight or App Store build.

Android's FCM has no equivalent environment split, so the FCM calls on this page don't take one.

## Register the device for score alerts

Register the device's provider token on `game.score-alerts`. Use the FCM call for an Android token, or the APNs call for an iOS token, matching whichever provider token you have.

```javascript
try {
  const response = await pubnub.push.addChannels({
    channels: ['game.score-alerts'],
    device: 'replace-with-the-fcm-registration-token',
    pushGateway: 'fcm',
  });
  console.log('Android device registered for score alerts:', response);
} catch (error) {
  const status = error instanceof Error && 'status' in error ? error.status : undefined;
  console.error(`Registering the Android device failed: ${error}${status ? ` Additional information: ${status}` : ''}`);
}
```

```javascript
try {
  const response = await pubnub.push.addChannels({
    channels: ['game.score-alerts'],
    device: 'replace-with-the-apns-device-token',
    pushGateway: 'apns2',
    environment: APNS_ENVIRONMENT,
    topic: 'com.example.matchday',
  });
  console.log('iOS device registered for score alerts:', response);
} catch (error) {
  const status = error instanceof Error && 'status' in error ? error.status : undefined;
  console.error(`Registering the iOS device failed: ${error}${status ? ` Additional information: ${status}` : ''}`);
}
```

Registration pairs one provider token with one channel. A device registered on `game.chat` isn't thereby registered on `game.score-alerts`, and a device registered on `game.score-alerts` doesn't receive push for any other channel. A device that wants push on several channels registers on each one. The APNs call passes `environment: APNS_ENVIRONMENT`, the same constant from [Pick one APNs environment and use it everywhere](#pick-one-apns-environment-and-use-it-everywhere), so this registration and the notification target below always agree.

On Android, the token is the FCM registration token your app receives from Firebase, the same value [Send push notifications on Android](https://www.pubnub.com/docs/integrations/mobile-push-notifications/send-push-notifications-android.md) shows you reading from `FirebaseMessaging.getInstance().token`. On iOS, the token is the APNs device token your app receives after calling `registerForRemoteNotifications()`. This is the same value [Send push notifications on iOS](https://www.pubnub.com/docs/integrations/mobile-push-notifications/send-push-notifications-ios.md) shows you reading from the `didRegisterForRemoteNotificationsWithDeviceToken` delegate method.

A provider can issue a new token later, for example after a reinstall. When that happens, register the new token the same way. The old registration doesn't update itself.

## Build the notification

```javascript
const goal = PubNub.notificationPayload('Leeds score!', 'Southampton 0 - 2 Leeds');

goal.sound = 'default';
goal.apns.configurations = [{ targets: [{ topic: 'com.example.matchday', environment: APNS_ENVIRONMENT }] }];

const payload = goal.buildPayload(['apns2', 'fcm']);

console.log(JSON.stringify(payload, null, 2));
```

The snippet builds the notification with a push-payload helper, so you don't hand-assemble the `pn_apns` and `pn_fcm` blocks yourself. The exact shape depends on which providers you target, so the snippet prints the built object. Read that console output before moving on, because it is what the next step publishes.

The APNs target also carries `environment: APNS_ENVIRONMENT`, the same constant used to register the device. If this target ever names a different environment than the registration did, this is the step that broke: the registration in [Register the device for score alerts](#register-the-device-for-score-alerts) and this target are the only two places APNs environment is set for a live notification, and they need to agree.

Keep the title and body short. Each provider caps how large a notification it will accept, and PubNub holds one set of credentials per provider per keyset:

| Item | Limit |
| --- | --- |
| Push credentials per keyset | 1 APNs certificate and 1 FCM key |
| Push notification payload size | 2 KB for APNs, 4 KB for FCM |

## Publish the goal alert

```javascript
try {
  const response = await pubnub.publish({
    channel: 'game.score-alerts',
    message: {
      ...payload,
      score: '0-2',
      scorer: 'Leeds',
    },
    customMessageType: 'score-alert',
  });
  console.log('score alert published at timetoken:', response.timetoken);
} catch (error) {
  const status = error instanceof Error && 'status' in error ? error.status : undefined;
  console.error(`Publishing the score alert failed: ${error}${status ? ` Additional information: ${status}` : ''}`);
}
```

This publish serves two audiences at once. A subscriber with the app open receives it immediately as an ordinary message over its open connection. A registered device receives it, moments later, as a push notification from APNs or FCM. Both paths run independently, so a fan watching with the app open can get the same goal alert twice.

[Why the same event can arrive twice](https://www.pubnub.com/docs/integrations/mobile-push-notifications/overview.md#why-the-same-event-can-arrive-twice) covers the remedy. The fix is to mute the notification at the OS level while the app is foregrounded, rather than change what you publish.

## Confirm what a device is registered for

Use the FCM call for an Android token, or the APNs call for an iOS token.

```javascript
try {
  const response = await pubnub.push.listChannels({
    device: 'replace-with-the-fcm-registration-token',
    pushGateway: 'fcm',
  });
  console.log('this device receives alerts on:', response.channels);
} catch (error) {
  const status = error instanceof Error && 'status' in error ? error.status : undefined;
  console.error(
    `Listing the device registrations failed: ${error}${status ? ` Additional information: ${status}` : ''}`,
  );
}
```

```javascript
try {
  const response = await pubnub.push.listChannels({
    device: 'replace-with-the-apns-device-token',
    pushGateway: 'apns2',
    environment: APNS_ENVIRONMENT,
    topic: 'com.example.matchday',
  });
  console.log('this device receives alerts on:', response.channels);
} catch (error) {
  const status = error instanceof Error && 'status' in error ? error.status : undefined;
  console.error(
    `Listing the device registrations failed: ${error}${status ? ` Additional information: ${status}` : ''}`,
  );
}
```

This is the first thing to check when a push doesn't arrive. It lists every channel this device token is currently registered on, so you can confirm `game.score-alerts` is actually in that list before you look anywhere else. The APNs call needs the same `environment` and `topic` the registration used. Listing with the wrong environment looks up a different, unrelated registration rather than erroring. [Check push device registration](https://www.pubnub.com/docs/integrations/mobile-push-notifications/check-push-device-registration.md) covers the same check from a REST call, useful when you don't have a client SDK handy.

## Stop the alerts

```javascript
try {
  const response = await pubnub.push.removeChannels({
    channels: ['game.score-alerts'],
    device: 'replace-with-the-fcm-registration-token',
    pushGateway: 'fcm',
  });
  console.log('device no longer receives score alerts:', response);
} catch (error) {
  const status = error instanceof Error && 'status' in error ? error.status : undefined;
  console.error(
    `Removing the device registration failed: ${error}${status ? ` Additional information: ${status}` : ''}`,
  );
}
```

```javascript
try {
  const response = await pubnub.push.removeChannels({
    channels: ['game.score-alerts'],
    device: 'replace-with-the-apns-device-token',
    pushGateway: 'apns2',
    environment: APNS_ENVIRONMENT,
    topic: 'com.example.matchday',
  });
  console.log('device no longer receives score alerts:', response);
} catch (error) {
  const status = error instanceof Error && 'status' in error ? error.status : undefined;
  console.error(
    `Removing the device registration failed: ${error}${status ? ` Additional information: ${status}` : ''}`,
  );
}
```

A fan turning score alerts off is a deregistration, not a filter your app applies to incoming pushes. Removing the token from `game.score-alerts` is what stops the push. Nothing about the channel or the publish changes. As with listing, the APNs removal needs the same `environment` the registration used, or it targets a registration that was never made.

## When the alert does not arrive

If the notification never shows up on the device, check these in order:

* **The device isn't registered on game.score-alerts.** Confirm it with `pubnub.push.listChannels()`, or with [Check push device registration](https://www.pubnub.com/docs/integrations/mobile-push-notifications/check-push-device-registration.md).
* **The keyset has no provider credentials.** Mobile Push Notifications must be enabled on the keyset, and the FCM private key or the APNs `.p8` file must be uploaded to it. A publish still succeeds without them. Only the push fails, silently.
* **The payload has no provider block.** A publish without `pn_apns` or `pn_fcm` in the message travels over pub/sub only and never reaches the push gateway.

For anything else, read [Debug push notification messages](https://www.pubnub.com/docs/integrations/mobile-push-notifications/debug-push-notification-messages.md). It shows you how to subscribe to the channel's `-pndebug` companion channel. There, PubNub reports the count of devices found for each provider, plus any error APNs or FCM sent back, such as an invalid or expired token.

## Run it

```bash
node score-alerts.js
```

From Node alone you can observe the registration confirmation, the built payload, and the publish acknowledgment. You can't observe the notification itself from the script. That happens on the device's operating system, so watch the device, or its emulator, after you run the script.

Stop the script with **Ctrl+C** once you've confirmed the notification arrived.

## What happened

1. You registered a provider token for `game.score-alerts`, pairing that token with the channel.
2. You built a notification payload containing `pn_apns`, `pn_fcm`, or both.
3. You published that payload to `game.score-alerts`.
4. PubNub's push gateway read the payload, looked up every token registered on the channel, and forwarded a request to APNs or FCM for each one.
5. The provider delivered the alert to the device, and the operating system displayed it because the app wasn't in the foreground.

The registration and the publish are two independent steps that PubNub joins at delivery time. Neither one does anything on its own.

## Next steps

* [Fan re-engagement](https://www.pubnub.com/docs/use-cases/sports-media-entertainment/fan-re-engagement.md). Track who's watching with Presence and send a push only to a fan who's stopped.
* [Mobile push notifications](https://www.pubnub.com/docs/integrations/mobile-push-notifications/overview.md). The full delivery model, including self-notification and why the same event can arrive twice.
* [Debug push notification messages](https://www.pubnub.com/docs/integrations/mobile-push-notifications/debug-push-notification-messages.md). Read the `-pndebug` channel to see why a push failed.
* [Check push device registration](https://www.pubnub.com/docs/integrations/mobile-push-notifications/check-push-device-registration.md). Confirm a device's registration with a REST call instead of a client SDK.

Last updated at: 2026-09-30T07:20:08.000Z
