npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

hey-pingr

v0.2.0

Published

Official Node.js SDK for the Hey Pingr WhatsApp auto-reply platform

Readme

hey-pingr (Node.js)

Official Node.js SDK for Hey Pingr — WhatsApp auto-reply automation.

Hey Pingr works by listening for incoming trigger phrases on your connected WhatsApp number and automatically sending your configured reply. Your server receives a signed webhook event for every inbound message and can optionally return a dynamic reply — the text Hey Pingr will send to the user instead of the static auto-reply.

Installation

npm install hey-pingr

How it works

  1. A user sends a trigger phrase (e.g. "login") to your WhatsApp number
  2. Hey Pingr instantly sends either the static auto-reply you configured — or the dynamic reply your webhook server returns
  3. Hey Pingr POSTs a signed message.received webhook event to your server so you can act on it

Auth patterns

Pattern 1 — Mobile-first (simplest)

The user is already on their phone. When the webhook fires, return a magic link in the response body — Hey Pingr delivers it as the WhatsApp reply.

const { verifyWebhook } = require('hey-pingr');

app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
  const payload = verifyWebhook(
    req.body,
    req.headers['x-pingr-signature'],
    process.env.PINGR_WEBHOOK_SECRET,
  );

  if (payload.event === 'message.received') {
    const phone = payload.from.phone; // e.g. "919876543210"
    const link  = await generateMagicLink(phone);

    // Return the dynamic reply — Hey Pingr sends this as the WhatsApp message
    return res.json({ reply: `Your login link: ${link}` });
  }

  res.sendStatus(200);
});

Pattern 2 — Phone pre-entry + browser polling

The user types their phone number on a web login page. Your server polls for an authenticated session using that phone as the key.

// Step 1: user enters phone on your login page
// Step 2: webhook fires when they text the trigger phrase
app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
  const payload = verifyWebhook(req.body, req.headers['x-pingr-signature'], process.env.PINGR_WEBHOOK_SECRET);

  if (payload.event === 'message.received') {
    const phone = payload.from.phone;
    const link  = await generateMagicLink(phone);

    // Store for browser to pick up
    await redis.set(`auth:${phone}`, link, 'EX', 300);

    return res.json({ reply: `Your login link: ${link}` });
  }
  res.sendStatus(200);
});

// Step 3: browser polls this endpoint
app.get('/auth/poll', async (req, res) => {
  const link = await redis.get(`auth:${req.query.phone}`);
  if (link) {
    await redis.del(`auth:${req.query.phone}`);
    return res.json({ status: 'ready', link });
  }
  res.json({ status: 'pending' });
});

Pattern 3 — Desktop / challenge code (no phone pre-entry)

The user is on a desktop. You don't know their phone number yet. Generate a short challenge code, show it on the login page, and have the user text "login <code>" to your WhatsApp number.

const { verifyWebhook, createChallengeCode, extractChallengeCode } = require('hey-pingr');

// In-memory store (use Redis in production)
const pendingSessions = {};

// Step 1: browser requests a challenge code
app.get('/auth/challenge', (req, res) => {
  const code      = createChallengeCode();     // e.g. "X4K9MQ"
  const sessionId = req.session.id;
  pendingSessions[code] = { sessionId, status: 'pending', createdAt: Date.now() };
  res.json({ code, triggerPhrase: `login ${code}` });
});

// Step 2: user texts "login X4K9MQ" — webhook fires
app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
  const payload = verifyWebhook(req.body, req.headers['x-pingr-signature'], process.env.PINGR_WEBHOOK_SECRET);

  if (payload.event === 'message.received') {
    const code = extractChallengeCode(payload.message.text, 'login');

    if (code && pendingSessions[code]?.status === 'pending') {
      const phone = payload.from.phone;
      const link  = await generateMagicLink(phone);

      pendingSessions[code] = { ...pendingSessions[code], status: 'ready', link };

      return res.json({ reply: `Here's your login link: ${link}` });
    }
  }

  res.sendStatus(200);
});

// Step 3: browser polls for result
app.get('/auth/poll', (req, res) => {
  const { code } = req.query;
  const session  = pendingSessions[code];

  if (!session) return res.status(404).json({ status: 'not_found' });
  if (session.status === 'pending') return res.json({ status: 'pending' });

  const { link } = session;
  delete pendingSessions[code];
  res.json({ status: 'ready', link });
});

Dynamic reply

Any 2xx JSON response your webhook returns with a reply string field will be sent as the WhatsApp message to the user — replacing the static auto-reply configured in your dashboard.

// Return this from your webhook handler
res.json({ reply: 'Your magic link: https://yourapp.com/auth?token=...' });
  • Maximum 4096 characters
  • Non-JSON or missing reply field → static auto-reply is used (fully backward-compatible)

Verifying webhooks

const { verifyWebhook } = require('hey-pingr');

// Express — use express.raw() so the body arrives as a Buffer
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  try {
    const payload = verifyWebhook(
      req.body,
      req.headers['x-pingr-signature'],
      process.env.PINGR_WEBHOOK_SECRET,
    );

    if (payload.event === 'message.received') {
      const phone = payload.from.phone; // e.g. "919876543210"
      console.log(`Trigger received from +${phone}`);
    }

    res.sendStatus(200);
  } catch (err) {
    console.error('Invalid webhook:', err.message);
    res.sendStatus(400);
  }
});

Webhook payloads

message.received

{
  "event": "message.received",
  "session_id": "sess_abc123",
  "from": {
    "phone": "919876543210",
    "jid": "[email protected]",
    "is_group": false,
    "group_jid": null
  },
  "message": { "text": "login X4K9MQ", "type": "text" },
  "timestamp": 1714825320000
}

session.disconnected

{
  "event": "session.disconnected",
  "session_id": "sess_abc123",
  "reason": 401,
  "kind": "terminal",
  "timestamp": 1714825320000
}

Error handling

const { errors } = require('hey-pingr');

try {
  verifyWebhook(rawBody, signature, secret);
} catch (err) {
  if (err instanceof errors.PingrWebhookError) {
    console.log('Signature check failed:', err.message);
  }
}

API reference

new HeyPingr(apiKey, opts?)

| Option | Type | Default | Description | |-----------|--------|----------------------------|--------------------------| | baseUrl | string | https://api.heypingr.com | Override for self-hosted | | timeout | number | 15000 | Request timeout (ms) |

verifyWebhook(rawBody, signature, secret, opts?)

Verifies the x-pingr-signature header and returns the parsed payload.
Throws PingrWebhookError if the signature is invalid or the timestamp is stale (> 5 min).

| Param | Type | Description | |------------------|-----------------|-----------------------------------------------------| | rawBody | Buffer / string | Raw request body — do not JSON-parse first | | signature | string | Value of the x-pingr-signature header | | secret | string | Your webhook signing secret from the dashboard | | checkTimestamp | boolean | Reject payloads older than 5 min. Default: true |

createChallengeCode(opts?)

Generates a cryptographically random challenge code for the desktop login flow.

| Option | Type | Default | Description | |-----------|--------|---------|------------------------------------------------------| | length | number | 6 | Code length (4–12 characters) | | charset | string | — | Unambiguous uppercase alphanumeric (no 0/O, 1/I/L) |

extractChallengeCode(messageText, triggerPhrase)

Parses the challenge code from an inbound trigger message.

extractChallengeCode('login X4K9MQ', 'login') // → 'X4K9MQ'
extractChallengeCode('login', 'login')         // → null (no code suffix)
extractChallengeCode('help', 'login')          // → null (wrong trigger)

License

MIT