hey-pingr
v0.2.0
Published
Official Node.js SDK for the Hey Pingr WhatsApp auto-reply platform
Maintainers
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-pingrHow it works
- A user sends a trigger phrase (e.g.
"login") to your WhatsApp number - Hey Pingr instantly sends either the static auto-reply you configured — or the dynamic reply your webhook server returns
- Hey Pingr POSTs a signed
message.receivedwebhook 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
replyfield → 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
