πŸ”” Send In-App Notifications

Pingram allows you to send and display in-app notifications quickly. Here’s how to setup the frontend to display notifications, and setup the backend to send notifications.

1. Display Notifications in Your App (Frontend)

Install the frontend SDK:

npm install @notificationapi/react
npm install @notificationapi/react react react-dom @types/react @types/react-dom
npm install @notificationapi/react
npm install @notificationapi/react

The @notificationapi/core library is a headless JavaScript SDK that lets you build your own custom UI for notifications. It provides direct API access without any prebuilt components, giving you complete control over the design and user experience.

npm install @notificationapi/core

The @notificationapi/core library is a headless JavaScript SDK that lets you build your own custom UI for notifications. It provides direct API access without any prebuilt components, giving you complete control over the design and user experience.

npm install @notificationapi/core
npm install @notificationapi/js-core

Integrate the Notification Widget that automatically connects to our servers and displays notifications in real-time:

Wrap your app with NotificationAPIProvider (typically in your root component) and add NotificationPopup where you want the notification bell to appear:

import {
  NotificationAPIProvider,
  NotificationPopup
} from '@notificationapi/react';

function App() {
  return (
    <NotificationAPIProvider clientId="YOUR_CLIENT_ID" userId="USER_ID">
      <div>
        {/* Your app content */}
        <NotificationPopup />
      </div>
    </NotificationAPIProvider>
  );
}

export default App;

For Nuxt applications, create the file components/NotificationAPIWrapper.vue, and add this code:

<template>
  <div ref="reactContainer"></div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue'
import { createElement } from 'react'
import { createRoot } from 'react-dom/client'
import {
  NotificationAPIProvider,
  NotificationPopup
} from '@notificationapi/react'

interface Props {
  clientId: string
  userId: string
  apiURL?: string
  wsURL?: string
  customServiceWorkerPath?: string // if using web push
}

const props = withDefaults(defineProps<Props>(), {
  apiURL: 'api.notificationapi.com',
  wsURL: 'ws.notificationapi.com',
  customServiceWorkerPath: '/notificationapi-service-worker.js' // if using web push
})

const reactContainer = ref<HTMLElement>()
let root: any = null

const renderReactComponent = () => {
  if (!reactContainer.value) return

  if (root) {
    root.unmount()
  }

  root = createRoot(reactContainer.value)

  const providerElement = createElement(
    NotificationAPIProvider,
    {
      clientId: props.clientId,
      userId: props.userId,
      apiURL: props.apiURL,
      wsURL: props.wsURL,
      customServiceWorkerPath: props.customServiceWorkerPath // if using web push
    },
    createElement(NotificationPopup)
  )

  root.render(providerElement)
}

onMounted(() => {
  renderReactComponent()
})

onUnmounted(() => {
  if (root) {
    root.unmount()
  }
})

watch(() => [props.clientId, props.userId], () => {
  renderReactComponent()
}, { deep: true })
</script>

Then add this component to your page:

<template>
  <div>
    <h1>My Nuxt 3 App</h1>

    <!-- Basic In-App Popup -->
    <NotificationAPIWrapper
      :client-id="clientId"
      :user-id="userId"
      :api-u-r-l="apiURL"
      :ws-u-r-l="wsURL"
      :custom-service-worker-path="customServiceWorkerPath" // if using web push
    />
  </div>
</template>

<script setup lang="ts">
  // Configuration
  const clientId = "YOUR_CLIENT_ID"
  const userId = "USER_ID"
  const apiURL="api.ipanoitacifiton.com" // see step 3 for url if in CA or EU regions
  const wsURL="ws.ipanoitacifiton.com"
  const customServiceWorkerPath = "/notificationapi-service-worker.js" // add to public folder if using web push
</script>

Create a provider component to avoid SSR/hydration issues in Remix (React Router v7), then wrap your app.

  1. Create app/components/NotificationProvider.tsx:
import { useEffect, useState } from 'react';
import {
  NotificationAPIProvider,
  NotificationPopup
} from '@notificationapi/react';

interface NotificationProviderProps {
  children: React.ReactNode;
}

export default function NotificationProvider({
  children
}: NotificationProviderProps) {
  const [isClient, setIsClient] = useState(false);

  useEffect(() => {
    setIsClient(true);
  }, []);

  // Only render on client side to avoid SSR issues
  if (!isClient) {
    return <>{children}</>;
  }

  return (
    <NotificationAPIProvider
      userId="3a96859c-fc4f-4f4a-85c0-89313b533a53"
      clientId="o93otcvvcp87us1yhy5e04d6q5"
      apiURL="api.ipanoitacifiton.com"
      wsURL="ws.ipanoitacifiton.com"
    >
      {children}
      <NotificationPopup
        buttonStyles={{
          width: 40,
          height: 40,
          backgroundColor: '#1890ff'
        }}
        popoverPosition={{
          anchorOrigin: {
            vertical: 'bottom',
            horizontal: 'center'
          }
        }}
        iconColor="#ffffff"
        buttonIconSize={20}
        popupWidth={400}
        popupHeight={500}
      />
    </NotificationAPIProvider>
  );
}
  1. Wrap your app in app/root.tsx:
import {
  Links,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
  isRouteErrorResponse
} from 'react-router';
import type { Route } from './+types/root';
import './app.css';
import NotificationProvider from './components/NotificationProvider';

export function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        <NotificationProvider>{children}</NotificationProvider>
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

export default function App() {
  return <Outlet />;
}
INFO

Why this setup?

Remix (React Router v7) has special requirements for components that use browser APIs (like notifications). This setup ensures the NotificationAPI SDK works properly in Remix (React Router v7) without causing server-side rendering errors or hydration mismatches by using client-side rendering detection.

This component handles Next.js requirements by using β€˜use client’ and dynamic imports to prevent server-side rendering issues.

  1. Create src/components/NotificationProvider.tsx.
'use client';

import { NotificationAPIProvider } from '@notificationapi/react';
import dynamic from 'next/dynamic';

const NotificationPopup = dynamic(
  () => import('@notificationapi/react').then((mod) => mod.NotificationPopup),
  {
    ssr: false
  }
);

interface NotificationProviderProps {
  children: React.ReactNode;
}

export default function NotificationProvider({
  children
}: NotificationProviderProps) {
  return (
    <NotificationAPIProvider
      userId="3a96859c-fc4f-4f4a-85c0-89313b533a53"
      clientId="o93otcvvcp87us1yhy5e04d6q5"
      apiURL="api.ipanoitacifiton.com"
      wsURL="ws.ipanoitacifiton.com"
    >
      {children}
      <NotificationPopup
        buttonStyles={{
          width: 40,
          height: 40,
          backgroundColor: '#1890ff'
        }}
        popoverPosition={{
          anchorOrigin: {
            vertical: 'bottom',
            horizontal: 'center'
          }
        }}
        iconColor="#ffffff"
        buttonIconSize={20}
        popupWidth={400}
        popupHeight={500}
      />
    </NotificationAPIProvider>
  );
}
  1. Wrap your app in src/app/layout.tsx:
import NotificationProvider from '@/components/NotificationProvider';

export default function RootLayout({
  children
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <NotificationProvider>{children}</NotificationProvider>
      </body>
    </html>
  );
}
INFO

Why this setup?

Next.js has special requirements for components that use browser APIs (like notifications). This setup ensures the NotificationAPI SDK works properly in Next.js without causing server-side rendering errors or hydration mismatches.

Create your custom notifications component

import {
  Component,
  OnInit,
  OnDestroy,
  Inject,
  PLATFORM_ID
} from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { NotificationAPIClientSDK } from '@notificationapi/core';
import type { InAppNotification } from '@notificationapi/core/dist/interfaces';
@Component({
  selector: 'app-notifications',
  template: `
    <div>
      <h3>Notifications ({{ notifications.length }})</h3>
      <!-- Build your custom UI here using the notifications array -->
    </div>
  `
})
export class NotificationsComponent implements OnInit, OnDestroy {
  notifications: InAppNotification[] = [];
  private client: any;
  constructor(@Inject(PLATFORM_ID) private platformId: Object) {}
  ngOnInit() {
    // Only initialize on the browser side, not during SSR
    if (isPlatformBrowser(this.platformId)) {
      this.client = NotificationAPIClientSDK.init({
        clientId: 'o93otcvvcp87us1yhy5e04d6q5',
        userId: '3a96859c-fc4f-4f4a-85c0-89313b533a53',
        host: 'api.ipanoitacifiton.com',
        websocketHost: 'ws.ipanoitacifiton.com',

        onNewInAppNotifications: (newNotifications: InAppNotification[]) => {
          this.notifications = [...newNotifications, ...this.notifications];
        }
      });

      // Open websocket for real-time notifications
      this.client.openWebSocket();

      // Identify the user
      this.client.identify({
        id: '3a96859c-fc4f-4f4a-85c0-89313b533a53'
      });

      // Get existing notifications
      this.client
        .getInAppNotifications({
          before: new Date().toISOString(),
          maxCountNeeded: 50
        })
        .then((result: { items: InAppNotification[] }) => {
          this.notifications = result.items;
        })
        .catch((error: any) => {
          console.error('Error loading notifications:', error);
        });
    }
  }
  ngOnDestroy() {
    if (this.client && isPlatformBrowser(this.platformId)) {
      this.client.closeWebSocket();
    }
  }
}

Create your custom notifications component:

<script setup>
import { ref, onMounted } from 'vue';
import { NotificationAPIClientSDK } from '@notificationapi/core';
const notifications = ref([]);
onMounted(() => {
  const client = NotificationAPIClientSDK.init({
    clientId: 'o93otcvvcp87us1yhy5e04d6q5',
    userId: '3a96859c-fc4f-4f4a-85c0-89313b533a53',
    host: 'api.ipanoitacifiton.com',
    websocketHost: 'ws.ipanoitacifiton.com',

    onNewInAppNotifications: (newNotifications) => {
      notifications.value = [...newNotifications, ...notifications.value];
    }
  });
  // Open websocket for real-time notifications
  client.openWebSocket();
  // Identify the user
  client.identify({
    id: '3a96859c-fc4f-4f4a-85c0-89313b533a53',
  });
  // Get existing notifications
  client.getInAppNotifications({
    before: new Date().toISOString(),
    maxCountNeeded: 50,
  }).then((result) => {
    notifications.value = result.items;
  }).catch((error) => {
    console.error('Error loading notifications:', error);
  });
  console.log('Current notifications:', notifications.value);
});
</script>
<template>
  <div>
    <h3>Notifications ({{ notifications.length }})</h3>
    <!-- Build your custom UI here using the notifications array -->
  </div>
</template>

Use JS Core for custom implementations and headless integrations. This provides you with the core functionality without any UI components, giving you full control over the presentation.

What JS Core provides:

  • Real-time notification delivery via WebSocket
  • Notification state management (read/unread, archived)
  • User preference handling
  • Custom event callbacks for notification actions
  • Complete control over UI design and behavior

Basic Integration:

<div id="notification-container"></div>

<script type="module">
  import NotificationAPI from '@notificationapi/js-core';

  const client = new NotificationAPI({
    clientId: 'YOUR_CLIENT_ID',
    userId: 'USER_ID'
  });

  // Option 1: Use built-in UI widget
  client.showInApp({
    root: 'notification-container'
  });

  // Option 2: Build custom UI with headless mode
  client.onNewNotification((notification) => {
    // Handle new notifications with your custom UI
    console.log('New notification:', notification);
  });
</script>

2. Send Notifications from Your Backend

Install the Server-Side SDK:

npm install pingram
pip install pingram-python
composer require pingram/php
go get github.com/pingram-io/pingram-go
dotnet add package Pingram

Add the Pingram dependency to your pom.xml. Check Maven Central for the latest version.

<dependencies>
    <dependency>
        <groupId>io.pingram</groupId>
        <artifactId>pingram</artifactId>
        <version>0.1.0</version>
    </dependency>
</dependencies>
gem install pingram

Or add to your Gemfile: gem 'pingram'

Now, send an in-app notification that will appear in your user’s notification center:

TIP

Use your API key (pingram_sk_...). You can find it in the Environments section of the Pingram dashboard.

import { Pingram } from 'pingram';

// Initialize with API key
const pingram = new Pingram({
  apiKey: 'pingram_sk_...' // Your secret API key
});

// Send in-app notification
await pingram.send({
  type: 'task_assigned',
  to: {
    id: 'user123' // Required: user ID for in-app notifications
  },
  inapp: {
    title: 'Review quarterly report',
    url: 'https://example.com',
    image: 'https://example.com/image.png'
  }
});
import asyncio
from pingram import Pingram

async def send_inapp():
    async with Pingram(api_key="pingram_sk_...") as client:
        await client.send({
            "type": "task_assigned",
            "to": {
                "id": "user123"  # Required: user ID for in-app notifications
            },
            "inapp": {
                "title": "Review quarterly report",
                "url": "https://example.com",
                "image": "https://example.com/image.png"
            }
        })

# Run the async function
asyncio.run(send_inapp())
use Pingram\Client;
use Pingram\Model\SenderPostBody;

$client = new Client('pingram_sk_...');

$body = new SenderPostBody([
    'type' => 'task_assigned',
    'to' => [
        'id' => 'user123'  // Required: user ID for in-app notifications
    ],
    'inapp' => [
        'title' => 'Review quarterly report',
        'url' => 'https://example.com',
        'image' => 'https://example.com/image.png'
    ]
]);
$client->send($body);
package main

import (
    "context"
    "log"

    pingram "github.com/pingram-io/pingram-go"
)

func main() {
    client := pingram.NewClient("pingram_sk_...") // Your secret API key

    body := pingram.SenderPostBody{
        Type: pingram.PtrString("task_assigned"),
        User: &pingram.GetUsersResponseUsersInner{
            Id: "user123", // Required: user ID for in-app notifications
        },
        Inapp: &pingram.SenderPostBodyInapp{
            Title: "Review quarterly report",
            Url:   pingram.PtrString("https://example.com"),
            Image: pingram.PtrString("https://example.com/image.png"),
        },
    }

    _, _, err := client.DefaultAPI.Send(context.Background()).SenderPostBody(body).Execute()
    if err != nil {
        log.Fatal(err)
    }
}
using Pingram;
using Pingram.Model;

var client = new PingramClient("your_api_key");

var body = new SenderPostBody
{
    Type = "task_assigned",
    To = new SenderPostBodyTo { Id = "user123" },
    Inapp = new SenderPostBodyInapp("Review quarterly report", url: "https://example.com", image: "https://example.com/image.png")
};
await client.SendAsync(body);
package com.example;

import io.pingram.Pingram;
import io.pingram.model.*;

public class Example {
    public static void main(String[] args) {
        Pingram pingram = new Pingram("pingram_sk_..."); // Your secret API key

        SenderPostBodyTo to = new SenderPostBodyTo()
            .id("user123"); // Required: user ID for in-app notifications

        SenderPostBody body = new SenderPostBody()
            .type("task_assigned")
            .to(to)
            .inapp(new SenderPostBodyInapp()
                .title("Review quarterly report")
                .url("https://example.com")
                .image("https://example.com/image.png")
            );

        SenderPostResponse response = pingram.send(body);
        System.out.println("Tracking ID: " + response.getTrackingId());
    }
}
require 'pingram'

client = Pingram::Client.new(api_key: 'pingram_sk_...')
body = Pingram::SenderPostBody.new(
  type: 'task_assigned',
  to: { id: 'user123' },
  inapp: { title: 'Review quarterly report', url: 'https://example.com', image: 'https://example.com/image.png' }
)
client.send(body)

You’re All Set!

πŸŽ‰ Congrats! You’re now sending and displaying in-app notifications!

For more advanced features like custom click actions, batching, and UI customization, check out our In-App Channel documentation.