Hey Pingr API Documentation
Quickstart
Set up WhatsApp auto-replies in under 10 minutes. When a user sends your configured trigger phrase to your WhatsApp number, Hey Pingr instantly replies with your message — no code needed for the reply itself.
How it works
Hey Pingr is trigger-based. You configure a trigger phrase and a reply message in the dashboard. When a user texts that phrase, Hey Pingr sends your reply automatically. A message.received webhook event fires so your backend can take action — generate a magic link, log the event, or anything else.
Step by step
login) and the reply message (e.g. your magic link). Use {{phone}} in the reply to insert the user's number.message.received event every time a trigger fires.Handling webhook events
// Express — use express.raw() so 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"
// generate magic link, OTP, etc.
}
res.sendStatus(200);
} catch (err) {
res.sendStatus(400); // invalid signature
}
});
@app.route("/webhook", methods=["POST"])
def webhook():
try:
payload = verify_webhook(
request.get_data(),
request.headers["x-pingr-signature"],
os.environ["PINGR_WEBHOOK_SECRET"],
)
if payload["event"] == "message.received":
phone = payload["from"]["phone"] # e.g. "919876543210"
# generate magic link, OTP, etc.
return "", 200
except PingrWebhookError:
return "", 400
Authentication
All API requests must include your API key in the X-API-Key header. You can find and manage your keys in the dashboard.
API key types
Production keys (pk_live_) send real WhatsApp messages and count toward your plan limits.
Test keys (pk_test_) simulate the full API response without sending actual messages. Use these in development and CI.
Authenticating requests
Host: api.heypingr.com
X-API-Key: pk_live_xK9p2mQr8nVs4wLj7dFhTbYcXeAuZo
Webhooks
Hey Pingr sends a POST request to your configured webhook URL whenever an event occurs — like an incoming message from a user.
Verifying signatures
// Express — use express.raw() so 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,
);
console.log(payload.event, payload.session_id);
res.sendStatus(200);
} catch (err) {
res.sendStatus(400); // invalid signature
}
});
Event payload — message.received
"event": "message.received",
"session_id": "sess_abc123",
"from": {
"phone": "919876543210",
"jid": "919876543210@s.whatsapp.net",
"is_group": false,
"group_jid": null
},
"message": { "text": "hey login", "type": "text" },
"timestamp": 1714825320000
}
Node.js SDK
The official Node.js SDK wraps the Hey Pingr REST API with a clean, typed interface.
Installation
Initialise
Verify webhooks
// Express — use express.raw() to get a Buffer
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const payload = verifyWebhook(
req.body,
req.headers['x-pingr-signature'],
process.env.PINGR_WEBHOOK_SECRET,
);
console.log(payload.event); // 'message.received'
res.sendStatus(200);
});
Challenge code helpers
For desktop/browser login flows where you don't know the user's phone number yet, generate a short challenge code and embed it in the trigger phrase.
// Step 1 — generate a code for the login page
app.get('/auth/challenge', (req, res) => {
const code = createChallengeCode(); // e.g. "X4K9MQ"
pending[code] = { sessionId: req.session.id, status: 'pending' };
res.json({ code, triggerPhrase: `login ${code}` });
});
// Step 2 — webhook fires when user texts "login X4K9MQ"
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 && pending[code]?.status === 'pending') {
const link = await generateMagicLink(payload.from.phone);
pending[code] = { status: 'ready', link };
return res.json({ reply: `Here's your link: ${link}` });
}
}
res.sendStatus(200);
});
// Step 3 — browser polls for the result
app.get('/auth/poll', (req, res) => {
const s = pending[req.query.code];
if (!s) return res.status(404).json({ status: 'not_found' });
if (s.status === 'pending') return res.json({ status: 'pending' });
const { link } = s; delete pending[req.query.code];
res.json({ status: 'ready', link });
});
API reference
| Function | Description |
|---|---|
| verifyWebhook(rawBody, sig, secret) | Verify HMAC-SHA256 signature and return parsed payload. Throws PingrWebhookError on failure. |
| createChallengeCode(opts?) | Generate a cryptographically random challenge code. length 4–12 (default 6), custom charset supported. |
| extractChallengeCode(text, trigger) | Parse the challenge code suffix from a trigger message. Returns uppercased code or null. |
Python SDK
The official Python SDK for Hey Pingr. Supports both sync and async usage.
Installation
# async support (FastAPI, asyncio)
pip install hey-pingr[async]
Verify webhooks
# Flask example
@app.route("/webhook", methods=["POST"])
def webhook():
try:
payload = verify_webhook(
request.get_data(),
request.headers["x-pingr-signature"],
os.environ["PINGR_WEBHOOK_SECRET"],
)
print(payload["event"]) # 'message.received'
return "", 200
except PingrWebhookError:
return "", 400
Challenge code helpers
pending = {} # use Redis in production
# Step 1 — generate code for login page
@app.get("/auth/challenge")
def challenge():
code = create_challenge_code() # e.g. "X4K9MQ"
pending[code] = {"status": "pending"}
return {"code": code, "trigger_phrase": f"login {code}"}
# Step 2 — webhook fires when user texts "login X4K9MQ"
@app.post("/webhook")
async def webhook(request: Request):
body = await request.body()
payload = verify_webhook(body, request.headers["x-pingr-signature"], SECRET)
if payload["event"] == "message.received":
code = extract_challenge_code(payload["message"]["text"], "login")
if code and pending.get(code, {})["status"] == "pending":
link = await generate_magic_link(payload["from"]["phone"])
pending[code] = {"status": "ready", "link": link}
return {"reply": f"Here's your link: {link}"}
return Response(status_code=200)
# Step 3 — browser polls for result
@app.get("/auth/poll")
def poll(code: str):
s = pending.get(code)
if not s: return {"status": "not_found"}
if s["status"] == "pending": return {"status": "pending"}
link = pending.pop(code)["link"]
return {"status": "ready", "link": link}
API reference
| Function | Description |
|---|---|
| verify_webhook(raw_body, signature, secret) | Verify HMAC-SHA256 signature and return parsed payload dict. Raises PingrWebhookError on failure. |
| create_challenge_code(*, length=6) | Generate a cryptographically random challenge code (uses secrets module). Length 4–12. |
| extract_challenge_code(message_text, trigger_phrase) | Parse the challenge code suffix from a trigger message. Returns uppercased code or None. |
Dynamic Reply
By default, Hey Pingr sends the static auto-reply you configured in the dashboard when a trigger phrase is received. If your webhook server returns a JSON body with a reply field in the HTTP response, Hey Pingr will send that text as the WhatsApp message instead — this is called a dynamic reply.
Dynamic replies let you personalise every message: include the user's one-time magic link, a session-specific code, or any real-time data your backend generates.
reply field, the static auto-reply is used. No changes needed to existing integrations.How it works
Content-Type: application/json
{
"reply": "Your magic link: https://yourapp.com/auth?token=abc123xyz"
}
Rules
| Rule | Detail |
|---|---|
| HTTP status | Your webhook must return 2xx. Non-2xx → static reply is used. |
| Content-Type | Must be application/json. Other types → static reply. |
| reply field | Must be a non-empty string. Missing or empty → static reply. |
| Max length | 4096 characters (WhatsApp limit). Longer values are truncated. |
| Timeout | Your webhook must respond within 8 seconds or the static reply is used. |
Example — magic link auth
const payload = verifyWebhook(req.body, req.headers['x-pingr-signature'], process.env.PINGR_WEBHOOK_SECRET);
if (payload.event === 'message.received') {
const link = await generateMagicLink(payload.from.phone);
// Hey Pingr sends this text via WhatsApp instead of the static reply
return res.json({ reply: `Tap to sign in: ${link}` });
}
res.sendStatus(200);
});
Auth Patterns
Hey Pingr supports three authentication patterns for different user contexts. All three use the same webhook + dynamic reply infrastructure — the difference is how you tie the inbound WhatsApp message to a specific user or browser session.
Pattern 1 — Mobile-first (simplest)
The user is already on their phone. When the trigger fires, return a magic link via dynamic reply — it arrives as a WhatsApp message they can tap instantly.
const payload = verifyWebhook(req.body, req.headers['x-pingr-signature'], process.env.PINGR_WEBHOOK_SECRET);
if (payload.event === 'message.received') {
const link = await generateMagicLink(payload.from.phone);
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 login page. Your backend stores the generated link keyed by phone number. The browser polls until the link is ready.
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);
await redis.set(`auth:${phone}`, link, 'EX', 300);
return res.json({ reply: `Tap to sign in: ${link}` });
}
res.sendStatus(200);
});
// Browser poll 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 and you don't know their phone number. Generate a short challenge code, display it on the login page, and ask the user to text login <code>. Hey Pingr fires the webhook with the full message text — you parse out the code and look up the waiting browser session.
const pending = {}; // use Redis in production
app.get('/auth/challenge', (req, res) => {
const code = createChallengeCode();
pending[code] = { status: 'pending', createdAt: Date.now() };
res.json({ code, message: `Text "login ${code}" to +1-234-567-8900` });
});
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 && pending[code]?.status === 'pending') {
const link = await generateMagicLink(payload.from.phone);
pending[code] = { status: 'ready', link };
return res.json({ reply: `Here's your login link: ${link}` });
}
}
res.sendStatus(200);
});
app.get('/auth/poll', (req, res) => {
const s = pending[req.query.code];
if (!s) return res.status(404).json({ status: 'not_found' });
if (s.status === 'pending') return res.json({ status: 'pending' });
const { link } = s; delete pending[req.query.code];
res.json({ status: 'ready', link });
});
Trigger Management
Manage your trigger phrases programmatically using your API key. The same key you use for webhook verification — no separate token needed.
List all configured trigger phrases. Optionally filter by session_id.
-H "X-API-Key: pk_live_..."
Create a new trigger phrase for a session. Enforces your plan's trigger limit.
-H "X-API-Key: pk_live_..." \
-H "Content-Type: application/json" \
-d '{"session_id":"sess_abc","trigger_phrase":"login","reply_message":"Sending your link..."}'
Request body parameters
| Field | Type | Description |
|---|---|---|
| session_id | string | Required. The session this trigger is attached to. |
| trigger_phrase | string | Required. The phrase users text to trigger the reply (max 200 chars, stored lowercase). |
| reply_message | string | Required. The static auto-reply (max 1000 chars). Overridden by dynamic reply if your webhook returns one. |
| is_active | boolean | Optional. Default true. Set to false to disable without deleting. |
Update a trigger phrase, reply message, or active state. All fields are optional — only provided fields are updated.
Delete a trigger. Returns 204 No Content on success.
Plan limits
| Plan | Max triggers |
|---|---|
| Starter | 1 |
| Growth | 2 |
| Scale | 10 |
Test Simulation
Fire a signed test message.received webhook to your configured endpoint without sending a real WhatsApp message. The simulated event is signed with the same HMAC-SHA256 key as production — your webhook handler can't tell the difference (it carries a _simulated: true flag if you want to filter it).
Authentication: Dashboard JWT (use the dashboard or your session cookie).
-H "Authorization: Bearer <jwt>" \
-H "Content-Type: application/json" \
-d '{"phone":"919876543210","message":"login","session_id":"sess_abc"}'
Response
"delivered": true,
"status_code": 200,
"dynamic_reply": "Here's your magic link: https://...",
"payload_sent": { "event": "message.received", "_simulated": true, "...": "..." }
}
If your webhook returns a { "reply": "..." } body, dynamic_reply will contain that text — letting you verify the full auth flow without a real phone.
Rate limited to 20 requests per minute per user. You can also trigger simulations directly from Dashboard → Webhooks → Test.
Webhook Delivery Logs
Retrieve a paginated log of recent webhook delivery attempts, including simulated events. Useful for debugging failed deliveries or auditing your integration.
Query parameters
| Param | Type | Description |
|---|---|---|
| limit | integer | Number of records to return (1–200, default 50). |
| before | ISO timestamp | Cursor for pagination — return entries older than this timestamp. |
| event_type | string | Filter by webhook.delivered, webhook.failed, or webhook.simulated. |
"deliveries": [
{
"id": "a1b2c3d4...",
"status": "delivered",
"url": "https://yourapp.com/webhook",
"http_status": 200,
"dynamic_reply": true,
"session_id": "sess_abc123",
"created_at": "2025-05-15T10:30:00.000Z"
}
],
"count": 1,
"next_cursor": "2025-05-15T10:30:00.000Z"
}
Pagination
Results are newest-first. To get the next page, pass the returned next_cursor value as the before parameter in your next request. next_cursor is null when there are no more results.
You can also view delivery logs in Dashboard → Webhooks → Delivery History.
Rate limits
Hey Pingr enforces two levels of rate limiting to protect your sessions and ensure fair use.
Per-recipient cooldown
By default, the same phone number cannot receive a message more than once every 60 seconds. This is configurable in Dashboard → Settings → Cooldown settings.
When a number is rate limited, the API returns a 429 response with a Retry-After header indicating seconds remaining.
Plan-level limits
| Plan | Messages/month | Overage rate |
|---|---|---|
| Starter | 10,000 | $0.002/msg |
| Growth | 70,000 | $0.0015/msg |
| Scale | 500,000 | $0.0008/msg |
Per-sender cooldown
The cooldown applies to the auto-reply — the same number won't receive another auto-reply until the cooldown window expires. Cooldown is configurable per-profile in Dashboard → Settings.
Error handling
All errors return a JSON body under a detail key with an error code and human-readable message.
"detail": {
"error": "rate_limit",
"message": "Number +919876543210 is in cooldown. Try again in 47s.",
"retry_after": 47
}
}
Error codes
| Code | HTTP | Description |
|---|---|---|
| invalid_key | 401 | The API key is missing or invalid. |
| missing_param | 422 | A required parameter is missing or malformed. |
| quota_exceeded | 429 | Monthly auto-reply quota exhausted. Upgrade your plan. |
Sessions & QR codes
A session represents a connected WhatsApp number on Hey Pingr's infrastructure. Each plan allows a different number of concurrent sessions.
Connecting a session
Go to Dashboard → Sessions → Add session. A QR code is generated. Open WhatsApp → Settings → Linked Devices → Link a device → scan the QR. The session goes live within seconds.
Session health monitoring
Hey Pingr monitors session health every 30 seconds. If a session disconnects (phone goes offline, WhatsApp logs out, etc.), a session.disconnected webhook event fires immediately and the dashboard shows a red status indicator.
GET /v1/sessions
Returns all sessions associated with your account and their current status.
"sessions": [
{
"id": "main-login-bot",
"name": "Main login bot",
"number": "+919876543210",
"status": "connected",
"connectedAt": "2025-04-18T08:22:11.000Z",
"messagesToday": 842
}
]
}