Overview
Pinglet is a real-time notification platform that lets you send push notifications, glassmorphism card notifications, and custom template notifications to your users with a single API call. In v0.0.3, all in-app types use a unified glassmorphism renderer. The SDK connects via SSE (Server-Sent Events) for instant delivery.
4 Notification Types
Browser push, glassmorphism card (Type 0 & 2), and custom templates
Real-time via SSE
Instant delivery through Server-Sent Events — no polling needed
Fully Customizable
Override position, theme, sounds, branding, progress bars, and more
Custom Events
Trigger frontend JS events from notification button clicks
Quick Start
Get notifications running on your site in under 2 minutes.
Create a Project
Sign up at Pinglet, create a project, and register your website domain. You'll get a project_id and pinglet_id.
Add Script Tags
Add the Pinglet SDK to your HTML, before the closing </body> tag.
html<!-- Service Worker (handles browser push) -->
<script
crossorigin="anonymous"
src="https://pinglet.enjoys.in/api/v1/public/scripts/v0.0.3/sw.js"
></script>
<!-- Main SDK -->
<script
type="module"
crossorigin="anonymous"
src="https://pinglet.enjoys.in/api/v1/public/libs/pinglet-sse.js"
data-endpoint="https://pinglet.enjoys.in/api/v1/notifications"
data-configured-domain="yourdomain.com"
data-project-id="YOUR_PROJECT_ID"
data-pinglet-id="YOUR_PINGLET_ID"
data-checksum="sha384-XXXXXXX"
></script>Send a Notification
Make a POST request to send your first notification.
bashcurl -X POST https://pinglet.enjoys.in/api/v1/notifications/send \
-H "Content-Type: application/json" \
-H "X-Project-ID: YOUR_PROJECT_ID" \
-d '{
"project_id": "YOUR_PROJECT_ID",
"type": 0,
"variant": "default",
"body": {
"title": "Hello from Pinglet!",
"description": "Your first notification"
}
}'Script Tag Attributes
Reference for all data-* attributes on the SDK script tag.
| Attribute | Required | Description |
|---|---|---|
data-endpoint | Yes | Pinglet API URL + /notifications |
data-configured-domain | Yes | Domain registered in dashboard |
data-project-id | Yes | 24-char project ID |
data-pinglet-id | Yes | Pinglet signature token |
data-checksum | Yes | SRI checksum (sha384) |
data-testimonials | No | "true" to show floating testimonials button |
data-templates | No | Comma-separated template IDs to preload |
Notification Types
Pinglet supports 4 notification types. Each type uses the same API endpoint but with different payloads.
Glassmorphism
Frosted-glass card with rich media, stacking, tag dedup, and dark mode. Primary renderer in v0.0.3.
Glassmorphism (Compat)
Backward compatible alias — maps to Type 0. Both render identically.
Custom Template
Server-stored HTML/CSS templates with variable injection
Browser Push
Native OS push notifications via Web Push API & service worker
Glassmorphism Notification
Modern frosted-glass card notification with stacking queue, 4-corner positioning, tag dedup, rich media, action buttons, branding footer, progress bar, and dark mode. This is the primary in-app renderer in SDK v0.0.3.
jsonPOST /api/v1/notifications/send
{
"projectId": "your-project-id",
"type": "0",
"body": {
"title": "New Message",
"description": "You have 3 unread messages",
"icon": "🔔",
"logo": "https://cdn.example.com/logo.png",
"url": "https://example.com/inbox",
"media": { "type": "image", "src": "https://cdn.example.com/banner.jpg" },
"buttons": [
{ "text": "View", "action": "redirect", "src": "https://..." },
{ "text": "Dismiss", "action": "close" }
]
}
}Body Fields
| Field | Required | Notes |
|---|---|---|
title | Yes | Min 3 chars |
description | No | Body text |
icon | No | URL, emoji, SVG, or base64 |
logo | No | URL to an image. Used as fallback if no icon. |
url | No | Click-through URL (must be valid) |
media | No | {type, src} — type: image|video|audio|iframe |
buttons | No | Array, max 3 buttons |
What Happens
- SSE delivers to all connected clients for this projectId
- SDK calls
showHtmlNotification({...})— unified glassmorphism renderer - Card rendered with glassmorphism CSS, icon prefetched
- If > maxVisible (default 3), notification is queued
- Auto-dismisses after config.duration (default 5000ms)
Examples
json// Minimal
{ "projectId": "...", "type": "0", "body": { "title": "Hello!" } }
// With icon + description
{
"projectId": "...", "type": "0",
"body": { "title": "Order #123", "description": "Ready for pickup", "icon": "📦" }
}
// With image media
{
"projectId": "...", "type": "0",
"body": { "title": "Sale!", "media": { "type": "image", "src": "https://..." } }
}Glassmorphism (Backward Compatible)
Type 2 is a backward compatible alias — in v0.0.3, it renders identically to Type 0 via showHtmlNotification(). Use Type 0 for new integrations. Existing Type 2 payloads will continue to work.
jsonPOST /api/v1/notifications/send
{
"projectId": "your-project-id",
"type": "2",
"body": {
"title": "Flash Sale 🔥",
"description": "50% off all items.",
"icon": "https://cdn.example.com/icon.png",
"url": "https://example.com/sale",
"media": { "type": "image", "src": "https://cdn.example.com/hero.jpg" },
"buttons": [
{ "text": "Shop", "action": "redirect", "src": "https://..." },
{ "text": "Track", "action": "event", "event": "sale:click", "data": {} },
{ "text": "Close", "action": "close" }
]
}
}Body Fields
| Field | Required | Notes |
|---|---|---|
title | Yes | Min 3 chars |
description | No | Body text |
icon | No | URL allowed for type 2 (unlike type 0) |
url | No | Click-through URL |
media | No | {type, src} — image, video, audio, or iframe |
buttons | No | Array, max 3 buttons |
v0.0.3 — Type 2 = Type 0
- •Both types use the same
showHtmlNotification()renderer - •Type 2 exists for backward compatibility — no functional difference
- •Same body schema, same overrides, same rendering
- •Use Type 0 for all new integrations
- •Old toast/variant system was removed in v0.0.3
What Happens
- SSE delivers the payload
- SDK calls
showHtmlNotification({...}) - Card rendered with glassmorphism CSS
- If > maxVisible (default 3), queued and shown when a slot opens
- Branding footer from globalConfig.config.branding
- Auto-closes with progress bar (unless requireInteraction)
Examples
json// Minimal { "projectId": "...", "type": "2", "body": { "title": "Welcome!" } } // Full { "projectId": "...", "type": "2", "body": { "title": "Payment Received", "description": "$99.00 from [email protected]", "icon": "https://cdn.example.com/avatar.png", "buttons": [ { "text": "View", "action": "redirect", "src": "https://..." }, { "text": "Close", "action": "close" } ] } }
Browser Push Notification
Notifications delivered via Web Push API → Service Worker → native OS notification. Reaches users even when the tab is closed. Requires user's push permission grant.
Important: Uses data field (NOT body). Follows browser Notification API spec. Max 2 action buttons (browser limitation). Queued via BullMQ on backend (not SSE).
jsonPOST /api/v1/notifications/send
{
"projectId": "your-project-id",
"type": "-1",
"data": {
"title": "Order Shipped",
"body": "Your package is on the way.",
"icon": "https://cdn.example.com/icon.png",
"badge": "https://cdn.example.com/badge.png",
"image": "https://cdn.example.com/hero.jpg",
"tag": "order-123",
"requireInteraction": true,
"silent": false,
"renotify": false,
"dir": "ltr",
"timestamp": 1711000000000,
"vibrate": [200, 100, 200],
"data": {
"url": "https://example.com/track/123",
"duration": 5000
},
"actions": [
{ "action": "view", "title": "View Order", "icon": "..." },
{ "action": "dismiss", "title": "Dismiss" }
]
}
}Data Fields
| Field | Required | Notes |
|---|---|---|
title | Yes | 1-100 chars |
body | No | Max 500 chars |
icon | No | URL — notification icon |
badge | No | URL — small overlay (Android) |
image | No | URL — large hero image |
tag | No | Dedup — same tag replaces previous |
requireInteraction | No | true = stays until user clicks |
silent | No | true = no sound/vibration |
renotify | No | true = re-alert even if same tag |
dir | No | "auto" | "ltr" | "rtl" |
timestamp | No | Unix ms — shown as time |
vibrate | No | Array of vibration durations |
data.url | No | URL opened on click |
actions | No | Max 2 action buttons (browser limit) |
Prerequisite
The browser must have push subscription registered. The SDK does this automatically via askNotificationPermission() on load. If the user denied permission, push won't work.
Custom Template Notification
Server-stored templates created in the dashboard. Pass data to fill template placeholders. Useful for consistent, reusable notification formats.
Important: Requires template_id AND custom_template. Must NOT include body or data. Template must be pre-loaded (via data-templates="1,42" or loadAllTemplates).
jsonPOST /api/v1/notifications/send
{
"projectId": "your-project-id",
"type": "1",
"template_id": "42",
"custom_template": {
"user_name": "John Doe",
"order_total": "$49.99",
"action_url": "https://example.com/orders/123",
"product_image": "https://cdn.example.com/product.jpg"
}
}Rules
- •
template_idmust be a string matching an existing template - •
custom_templateis a key-value object — any shape, depends on template - •
bodymust NOT be present when type is "1" - •
datamust NOT be present when type is "1" - •
variantmust NOT be present when type is "1"
What Happens
- SSE delivers { type: "1", template_id, custom_template }
- SDK looks up template from
globalConfig.templates[template_id] - Executes compiled_text as a function(data, config, globalConfig)
- Result passed as
customContent— wrapped in glassmorphism container with stacking, close button, and progress bar
API Reference
Send notifications via the Pinglet REST API. Rate limit: 30 requests/minute.
https://pinglet.enjoys.in/api/v1/notifications/sendHeaders
json{
"Content-Type": "application/json",
"X-Project-ID": "your-project-id",
"X-Pinglet-Version": "1.0.5"
}Payload Schemas
Request body schemas for each notification type.
Glassmorphism
Glassmorphism frosted-glass card notification. Stacking queue, 4-corner positioning, tag dedup, rich media, and dark mode. The primary in-app renderer in v0.0.3.
https://pinglet.enjoys.in/api/v1/notifications/sendHeaders
json{
"x-project-id": "your-project-id",
"x-pinglet-version": "1.0.5"
}Request Body
json{
"project_id": "your-project-id",
"type": 0,
"variant": "default",
"body": {
"title": "Hello from Pinglet",
"description": "New design incoming!",
"media": {
"type": "icon",
"src": "🔥"
},
"buttons": [
{
"text": "Fix Now",
"action": "link",
"src": "https://example.com/action"
},
{
"text": "Dismiss",
"action": "close"
}
]
},
"overrides": {
"auto_dismiss": false
}
}Response
json{
"message": "OK",
"result": "Notification Sent",
"success": true,
"X-API-PLATFORM STATUS": "OK"
}Request Schema
project_idstringrequiredYour project unique ID
typenumberrequiredSet to 0 for in-tab
variantstring"default"
body.titlestringrequiredNotification title
body.descriptionstringNotification body
body.iconstringEmoji / text / SVG / base64
body.logostringURL or base64
body.urlstringOpen URL on click
body.media.typestring"image" | "audio" | "video" | "iframe"
body.media.srcstringMust be a valid URL
body.buttons[]arrayAction buttons
body.buttons[].textstringrequiredButton label
body.buttons[].actionstringrequired"reload" | "close" | "redirect" | "link" | "alert" | "event"
body.buttons[].srcstringURL or message for redirect/link/alert
body.buttons[].eventstringEvent name (when action="event")
body.buttons[].dataanyCustom event data
overrides.positionstring"top-right" | "top-left" | "bottom-right" | "bottom-left"
overrides.transitionstring"fade" | "slide" | "zoom"
overrides.durationnumberAuto dismiss after ms
overrides.auto_dismissbooleanEnable auto dismiss
overrides.maxVisiblenumberMax visible notifications
overrides.stackingbooleanStack multiple notifications
overrides.dismissiblebooleanAllow manual dismiss
overrides.sound.playbooleanPlay notification sound
overrides.sound.srcstringSound URL
overrides.sound.volumenumber0 to 1
overrides.branding.showbooleanShow branding
overrides.branding.oncebooleanShow only once
overrides.branding.htmlstringCustom branding HTML
overrides.theme.modestring"light" | "dark" | "auto"
overrides.theme.customClassstringCustom CSS class
overrides.theme.roundedbooleanRounded corners
overrides.theme.shadowbooleanDrop shadow
overrides.theme.borderbooleanShow border
overrides.progressBar.showbooleanShow progress bar
overrides.progressBar.colorstringHex/RGB color
overrides.progressBar.heightnumberHeight in px
overrides.iconDefaults.showbooleanShow icon
overrides.iconDefaults.sizenumberIcon size in px
overrides.iconDefaults.positionstring"left" | "right" | "top"
overrides.websitestringTarget website URL
overrides.timebooleanShow timestamp
overrides.faviconbooleanShow favicon
Custom Events
Trigger real DOM CustomEvents on the user's browser when they click a notification button. Your frontend JavaScript listens for the event and executes any logic — add to cart, open modal, track conversion, and more.
How It Works
2. SDK renders notification → User clicks button
3. SDK fires:
window.dispatchEvent(new CustomEvent("name", {detail: payload}))4. SDK auto-dismisses notification + tracks click
5. Your JS listener catches the event and runs your logic
Event Button Schema
textstringrequiredButton label shown to the user
action"event"requiredMust be exactly "event"
eventstringrequiredCustom event name (e.g. "pinglet:addToCart")
dataanyPayload sent as event.detail — object, array, string, number
Example Payload
json{
"type": "2",
"projectId": "your-project-id",
"body": {
"title": "Flash Sale!",
"description": "50% off — limited time only",
"buttons": [
{
"text": "Add to Cart",
"action": "event",
"event": "pinglet:addToCart",
"data": {
"productId": "SKU-123",
"quantity": 1
}
},
{
"text": "Maybe Later",
"action": "close"
}
]
}
}Frontend Listener (HTML)
html<script>
window.addEventListener("pinglet:addToCart", function (e) {
console.log("Payload:", e.detail);
addItemToCart(e.detail.productId, e.detail.quantity);
});
</script>React / SPA Usage
tsxuseEffect(() => {
const handler = (e: CustomEvent) => {
console.log("Event data:", e.detail);
};
window.addEventListener("pinglet:addToCart", handler);
return () => window.removeEventListener("pinglet:addToCart", handler);
}, []);⚠ Important Notes
- •Register your listener — the SDK only fires the event
- •Works with Type 0, Type 1, and Type 2
- •Max 3 buttons per notification
- •Use namespaced names like "app:action" to avoid collisions
- •Don't put sensitive data in the payload
Button Actions Reference
| Action | Required Fields | Behavior |
|---|---|---|
redirect | src (valid URL) | Opens URL in new tab (_blank) |
link | src (valid URL) | Opens URL in same tab |
alert | src (string) | Shows browser alert(src) |
reload | — | Reloads the page |
close | — | Dismisses the notification |
event | event (string) | Fires CustomEvent on window |
onClick | onClick (fn string) | Evaluates inline function |
Overrides & Config
Customize notification appearance and behavior per-request using the overrides object.
| Property | Type | Description |
|---|---|---|
position | string | "top-right" | "top-left" | "bottom-right" | "bottom-left" |
duration | number | Auto-dismiss timer in milliseconds |
auto_dismiss | boolean | If false, stays until manually closed |
transition | string | "fade" | "slide" | "zoom" |
maxVisible | number | Max visible notifications at once |
stacking | boolean | Stack multiple notifications |
dismissible | boolean | Allow manual dismiss |
sound.play | boolean | Play notification sound |
sound.src | string | Custom sound URL |
sound.volume | number | Volume 0 to 1 |
theme.mode | string | "light" | "dark" | "auto" |
theme.rounded | boolean | Rounded corners |
theme.shadow | boolean | Drop shadow |
progressBar.show | boolean | Show progress bar |
progressBar.color | string | Hex/RGB color |
branding.show | boolean | Show branding |
Full API Schema Reference
Top-level schema for POST /api/v1/notifications/send. Rate limit: 30 req/min.
json{
"projectId": "string (exactly 24 chars)", // REQUIRED
"type": ""-1" | "0" | "1" | "2"", // REQUIRED
"variant": "string", // optional (type 0 only)
"tag": "string", // optional
"overrides": "OverridesObject", // optional (premium only)
"body": "BodyObject", // required for type 0 & 2
"data": "BrowserPushObject", // required for type -1
"custom_template": "Record<string, any>", // required for type 1
"template_id": "string" // required for type 1
}Validation Rules
- •type "0" → body required, no template_id/custom_template
- •type "2" → body required, no template_id/custom_template/variant
- •type "1" → template_id + custom_template required, no body/data
- •type "-1" → data required, no template_id/custom_template
Body Object (type 0 & 2)
json{
"title": "string (min 3 chars)", // REQUIRED
"description": "string",
"icon": "string", // emoji/base64 (type 0), URL (type 2)
"logo": "string", // type 0 only
"url": "string (valid URL)",
"media": { "type": "image|video|audio|iframe", "src": "url" },
"buttons": "ButtonSchema[] (max 3)"
}Button Schema (discriminated union)
json// redirect / link
{ "text": "View", "action": "redirect", "src": "https://..." }
// reload / close
{ "text": "Refresh", "action": "reload" }
{ "text": "Dismiss", "action": "close" }
// alert
{ "text": "Alert", "action": "alert", "src": "Message text" }
// event
{ "text": "Track", "action": "event", "event": "my-event", "data": {} }
// onClick (advanced)
{ "text": "Run", "action": "onClick", "onClick": "() => ..." }Global Config
When the SDK initializes, it fetches project config from the server. These settings are merged with SDK defaults. Premium overrides are applied per-notification.
jsonGET {endpoint}/load/projects?projectId=xxx&domain=yyy
Response:
{
"success": true,
"result": {
"is_premium": false,
"config": {
"position": "bottom-left",
"transition": "fade",
"duration": 5000,
"maxVisible": 3,
"stacking": true,
"auto_dismiss": true,
"dismissible": true,
"website": true,
"favicon": true,
"time": true,
"sound": { "play": false, "src": "", "volume": 0.5 },
"theme": { "mode": "auto" },
"branding": { "show": true, "once": true, "html": "" },
"progressBar": { "show": true, "color": "" }
},
"template": { "config": { ... } }
}
}SSE Connection & Lifecycle
The SDK uses Server-Sent Events for real-time notification delivery.
- SDK calls:
new EventSource(`${endpoint}/sse?projectId=${projectId}&pingletId=${pingletId}`) - Server keeps connection alive with
:heartbeat\n\nevery 30s - On message: SDK parses JSON, checks
parsed.type, dispatches to handler - On error: EventSource auto-reconnects (browser built-in)
- On server shutdown: clients reconnect when server comes back
bash// SSE endpoint
GET /api/v1/notifications/sse?projectId=xxx&pingletId=yyyStacking & Queue Behavior
Glassmorphism (Type 0 & 2 — unified renderer)
- • maxVisible = 3 (default, configurable via config or overrides)
- • When > maxVisible, notifications are QUEUED (FIFO)
- • When a visible notification is dismissed, the next queued one appears
- • Queue is per-position (top-right queue is separate from bottom-left)
- • Tag dedup: same tag replaces the existing notification with smooth crossfade
- • Each card has its own branding footer, close button, and progress bar
- • Hover pauses the auto-dismiss timer (always enabled in v0.0.3)
Example — Fire 6 type 2 notifications rapidly
→ 1, 2, 3 appear immediately (maxVisible = 3) → 4, 5, 6 go into queue → User dismisses #1 → #4 appears → User dismisses #2 → #5 appears → etc.
Branding
Branding appears as a footer on all glassmorphism notifications (Type 0, 1, & 2).
- • Each card has its own branding footer
- • Server provides branding HTML via project config
- • Override via
branding.htmlin overrides or dashboard config - • Set
branding.show = falseto hide - •
branding.once = trueshows branding only on first notification
json// Custom branding (via overrides or dashboard config)
{
"branding": {
"show": true,
"html": "Powered by <b>Enjoys</b> 🚀"
}
}Dark Mode
The SDK supports 3 theme modes.
| Mode | Behavior |
|---|---|
"auto" | Follows system prefers-color-scheme |
"dark" | Always dark |
"light" | Always light |
How It Works
- • All in-app types:
.pn-darkclass added when dark = true — all sub-elements restyle including branding footer, media players, and buttons - • "auto" (default): System preference via
matchMedia('prefers-color-scheme: dark') - • "dark": Always dark —
.pn-darkclass always applied - • "light": Always light — no dark class
- • Set via: Dashboard config →
theme.mode = "auto"or per-notification →overrides.theme.mode - • v0.0.3: Explicit
themeparam on showHtmlNotification() — deep-merged with global config
Font & Image Caching
Font
- • SDK injects Google Fonts Manrope (wght 200..800) via link rel="stylesheet"
- • Uses
display=swapfor fast first paint - • Browser caches font files automatically (Google Fonts CDN headers)
- • All SDK elements use
font-family: 'Manrope', sans-serif
Image Caching
- • Icon URLs are prefetched via
link rel="prefetch"(all in-app types) - • Each unique icon URL is prefetched only once (deduped)
- • All
<img>elements usedecoding="async" - • Media images use
loading="lazy"
Tips for Icon Caching
- 1. Serve from a CDN with
Cache-Control: public, max-age=31536000 - 2. Use versioned URLs:
icon.png?v=2 - 3. Use a consistent icon URL across notifications (same URL = same cache)
Error Handling
SDK System Popups
The SDK shows system popups (via showPopup()) for:
- • Missing pingletId
- • Missing endpoint
- • Version mismatch
- • Missing checksum
- • Failed config load
- • Failed template load
These are red/amber toast-style popups with retry/docs buttons.
API Error Responses
| Code | Meaning |
|---|---|
401 | Invalid or missing project credentials |
422 | Zod validation error (malformed payload) |
429 | Rate limited (30 req/min exceeded) |
500 | Server error |
Notes & Gotchas
projectId MUST be exactly 24 characters.
In v0.0.3, Type 0 and Type 2 are identical — both use glassmorphism renderer. Icon allows URLs, emoji, SVG, or base64.
Type -1 uses "data" (not "body"). Type 0/2 use "body" (not "data").
Type 1 requires BOTH template_id AND custom_template. Neither body nor data should be present.
Max buttons: 3 for types 0/2, 2 for type -1 (browser limitation).
Button action "event" requires the "event" field (event name string). The "data" field is optional.
Overrides in v0.0.3 are non-mutating — fresh copy per notification. Premium flag: is_tff. Theme overrides are deep-merged.
SSE auto-reconnects on disconnect. No manual retry needed.
Push permission (type -1) must be granted by the user. The SDK asks automatically on load.
Tag dedup (type 0 & 2): sending same tag replaces the existing notification with smooth crossfade.
Stacking (type 0 & 2): maxVisible defaults to 3. Excess goes to queue. Queue drains on dismiss.
Branding footer is ALWAYS shown unless config.branding.show = false.
Font: Manrope is loaded from Google Fonts with display=swap.
Images with the same URL are prefetched only once (deduped by SDK).
Rate limit: 30 notifications/minute per project. 429 = throttled.