PushItGood - web push notifications

Integration guide

Before starting make sure you have a pushitgood.eu client_id and apikey. The apikey must be kept secret and must never appear in client side code.

Your web application needs to provide:

The service worker

Add a javascript file in the root directory of your website called /worker.js, containing the following line:

importScripts("https://pushitgood.eu/v1/worker.js");

If you already have a file called /worker.js you can use another name, but be sure to amend the calls to registerServiceWorker in the later JS code snippets.

The user-details endpoint

The user-details endpoint returns your client_id and information about the user currently being subscribed, formatted as a HS256-signed Javascript Web Token (JWT). A sample Python implementation looks like this:

def user_details(request):
    return Response(
        jwt.encode(
            {
                "client_id": "my client id",
                "uid": "user identifier as a str",
                "tags": ["list", "of", "tags"],
                "webhook": "https://site.example/pushitgood-webhook",
            },
            key=APIKEY,
            algorithm="HS256"
        )
    )

The payload is as follows:

  • uid: (string) uniquely identifies the user.

  • tags: (list of strings) arbitrary tags that you can use to target a notification to a group of users

  • client_id: (string) your pushitgood client id

  • webhook: (string) webhook endpoint url

The webhook endpoint

This endpoint is optional, but if present pushitgood will ping it on each of the following events:

Event

event_type

State

User subscribes

subscription

subscribed

User unsubscribes

subscription

unsubscribed

Push sent to a user

push

sent

Push rejected by the upstream webpush service

push

failed

Push acknowledged by the browser

push

received

Push timed out (no browser acknowledgement within timeout period)

push

timeout

The endpoint must accept a POST request containing a Javascript Web Token. A sample Python implementation looks like this:

def webhook(request):
    data = jwt.decode(request.get_body(), key=APIKEY)

    # Either 'subscription' or 'notification'
    event_type = data["event_type"]

    # The user id as supplied by your user-details endpoint
    uid = data["uid"]

    # The user's subscription id
    sid = data["sid"]

    # If event_type is 'push', contains the notification's unique id
    # This will match the id returned by the api endpoint when you sent the notification
    nid = data["nid"]

    # The push id
    pid = data["pid"]

    # If event_type is 'subscription', this will be one of subscribed, unsubscribed
    #
    # If event_type is 'notification', this will be one of:
    # - sent: the notification has been accepted by the vendor supplied endpoint
    # - failed: the notification could not be sent
    # - received:the notification was received by the service worker on a user's device
    # - timeout: the notification was not received by the user's device after the specified delay
    state = data["state"]

    print(
        f"Push with id {pid} to user {uid} now has state {state}"
    )

    # Return an empty response
    return Response()

Note that the browser allows users to stop notifications by disabling the notification permission – and in some cases encourages the user to do this if the site sends repeated notifications.

When this happens, the browser does not inform our service, and so the unsubscribe web hook is not called. The next time you send a notification you will receive a timeout webhook for this device.

Javascript snippet for subscribing users

Non-interactive version

The following JS snippet automatically subscribes the user on page load. The user-details endpoint path must be coded into the the call to subscribeUser.

<script type="module">
    import * as pig from "https://pushitgood.eu/v1/static/susbcribe.js";

    pig.registerServiceWorker("/worker.js");

    // Automatically subscribe the user, requesting permission as required
    pig.getSubscription().then(
        (subscription) {
            if (subscription === null) {
                pig.subscribeUser("/path/to/user-details-endpoint.json");
            }
        }
    }
</script>

Interactive version

The following JS snippet displays a subscribe / unsubscribe button, and requires positive user interaction before the user is subscribed. Again, the user-details endpoint path must be coded into the the call to subscribeUser.

<div id="ButtonContainer"></div>

<script type="module">
    import * as pig from "https://pushitgood.eu/v1/static/subscribe.js";
    pig.registerServiceWorker("/worker.js");
    let button = document.createElement("button");
    document.getElementById("ButtonContainer").append(button);
    button.addEventListener(
        "click",
        (event) => {
            if (event.target.name == "subscribe") {
                pig.subscribeUser("/path/to/user-details-endpoint");
            } else {
                pig.unsubscribeUser("/path/to/user-details-endpoint");
            }
        }
    );
    pig.registerSubscriptionCallback(
        (subscriptionResponse) => {
            button.innerHTML = (subscriptionResponse.subscription) ? "Stop notifications!" : "Get notifications!";
            button.name = (subscriptionResponse.subscription) ? "unsubscribe" : "subscribe";
        }
    )
</script>

registerSubscriptionCallback and handling failed subscriptions

The function pig.registerSubscriptionCallback registers a function to be called:

  • When the service worker is registered (typically on page load)

  • Whenever pig.subscribeUser is called

  • Whenever pig.unsubscribeUser is called

The function will be called with an object containing the following keys:

subscription

The PushSubscription object, or null if the user is not subscribed

action

One of 'subscribe', 'unsubscribe', or 'register_serviceworker'

result

For subscribe actions, this will be one of 'granted' or denied, reflecting the user’s browser notification permission. For other actions this is true.

If a user has denied the notification permission in their browser settings, subscription requests will fail silently – the browser does not give any indication that the subscription request was denied and does not prompt the user to grant the notification permission.

In this case you may want to use a subscription callback to display a suitable message:

pig.registerSubscriptionCallback(
    (subscriptionResponse) => {
        if (subscriptionResponse.action === "subscribe" && subscriptionResponse.result != "granted") {
            alert("Please enable notifications for this site before trying again");
        }
    }
)

Sending a notification

Send a POST request to https://pushitgood.eu/v1/notify with the following payload, encoded as a JWT signed with your APIKEY. A sample Python implementation looks like this:

import requests

data = {
    "client_id": CLIENT_ID,
    "uid": "555",
    "title": "Notification title"
    "body": "Notification content"
    "url": "URL to open when notification is clicked"
    "tags": ["tags", "to", "target"],
    "actions": [
        {
            "action": "custom-action",
            "title": "Custom action",
            "icon": "https://site.example/action.png"
        }
    ],
    "timeout": 3600,
    "webhook": "https://site.example/pushitgood-webhook",
}
requests.post(
    "https://pushitgood.eu/v1/notify",
    jwt.encode(data, key=APIKEY, algorithm="HS256")
)

# Result of the notification, see below for explanation
result = requests.json()

You can also send the payload as a JSON payload, with the APIKEY specified in an authorization header, for example:

$ curl \
    --header "Content-Type: application/json" \
    --header "Authorization: Bearer $APIKEY" \
    --request POST \
    --data '{"uid":"555","title":"Notification title", ... }' \
https://pushitgood.eu/v1/notify

If uid is specified, a notification will be sent only to the user with that uid.

If tags is specified, notifications will be sent to any user that has one or more of the listed tags.

If neither is specified, notifications will be sent to all your subscribed users.

If timeout is specified, this is the time in seconds that pushitgood will wait for a confirmation that the notification has been received before updating the notification’s state to timeout and calling your webhook.

The notify endpoint will send notifications to each device subscribed for the specified user(s), and return a JSON data structure in the following format:

{
    // Unique identifier for the notification
    "nid": "...",

    // Each notification fans out to multiple Push API
    // requests, one per subscribed device.
    "pushes": [
        {
            // Identifier for this push
            "pid": "...",

            // user the push was sent to
            "uid": "...",

            // subscription the push was sent to
            "sid": "...",
        },
        ...
    ]
}

Custom actions

The notifications API allows you to define custom actions to be displayed under the notification body.

Note: notification actions are only available in Chrome and Edge. Browser may also limit how many actions are displayed.

Use the actions property of the notify api payload to define notification actions:

data = {
    ...
    "actions": [
        {
            "action": "custom-action",
            "title": "Custom action",
            "icon": "https://site.example/action.png"
        }
    ],
}

The action property is used by javascript code to identify the selected action. The title and icon properties define the text and image used for the button that the user clicks on,

The default pushitgood event listener does not handle custom actions, so to use notification actions you must supply your own. Amend your /worker.js script as follows:

importScripts("https://pushitgood.eu/v1/worker.js");

// Remove pushitgood's default event listener
self.removeDefaultNotificationClickListener();

// Register an event listener to handle your site's custom actions
self.addEventListener(
    "notificationclick",
    (event) => {
        event.notification.close();

        switch (event.action) {

            // Replace this with your custom action name.
            // Edit the openWindow call to include the url to load
            case "custom-action":
                event.waitUntil(self.openWindow(...));
                break;

            // If you have multiple custom actions,
            // add more cases as necessary
            case "custom-action-2":
                event.waitUntil(self.openWindow(...));
                break;

            // The default case will be used if the user does not
            // select an action, or if the browser does not support
            // notification actions
            default:
                event.waitUntil(self.openWindow(event.notification.data.url));
        }
    }
);

API sequence diagrams

Registration:

                           Browser
JS      Service Worker     Vendor           pushitgood.eu        Your webapp
 |            :              :                    |                   :
 |            :              :                    |                   :
 |          GET userDetails                       |                   |
 |------------------------------------------------------------------->|
 |            :              :                    |                   |
 |      200 OK <JWT of user details>              |                   |
 |<-------------------------------------------------------------------|
 |            :              |                    |                   |
 |            :              |                    |                   :
 |  request subscription     |                    |                   :
 |-------------------------->|                    |                   :
 |            :              |                    |                   :
 |<---subscription obj-------|                    |                   :
 |            :              :                    |                   :
 |            :              :                    |                   :
 |      POST /register <subscription+user>        |                   :
 | (contains unique notification vendor endpoint  |                   :
 | required to later send a notification)         |                   :
 |----------------------------------------------->|                   :
 |            :              :                    |  POST webhook     :
 |            :              :                    |------------------>:
 |            :              :                    |                   :

Send notification:

                                            Browser
Your webapp             pushitgood.eu       Vendor(s)       User agent
     |                      |                  :              :
     | POST /notify         |                  :              :
     |--------------------->|                  :              :
     |                      |                  :              :
     |<---notification obj--|                  :              :
     |                      |                  |              :  \
     |                      | WebPush request  |              :  |
     |                      |----------------->|              |  |
     | POST <your-webhook>  |                  | push event   |  |
     |  state="sent|failed" |                  |------------->|  |
     |<---------------------|                  |              |  | Repeated
     |                      |                  |              |  | for
     |                      |                  |              |  | each
     |                      |  POST /ping      |              |  | subscription
     | POST <your-webhook>  |<--------------------------------|  |
     |   state="received"   |                  |              |  |
     |<----push obj---------|                  |              |  /