Hey Pingr API Documentation

// getting_started

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.

No Meta Business API approval needed. Works with any regular WhatsApp number.

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

1
Create your Hey Pingr account
Sign up at heypingr.com/signup. Start your 7-day free trial — no charges until the trial ends.
2
Connect your WhatsApp number
In the dashboard, go to Sessions → Add session. A QR code will appear. Open WhatsApp on your phone → Linked Devices → Link a device → scan the QR.
3
Configure your auto-reply trigger
In the dashboard, go to Auto-reply → Add trigger. Set a trigger phrase (e.g. login) and the reply message (e.g. your magic link). Use {{phone}} in the reply to insert the user's number.
4
Set up your webhook to receive events
In the dashboard, go to Webhooks → Configure. Point it at your server URL. Hey Pingr will POST a signed message.received event every time a trigger fires.

Handling webhook events

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

// 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
  }
});
python
from pingr import verify_webhook, PingrWebhookError

@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

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.

Never expose your API key in frontend/client-side code. Always call Hey Pingr from your backend server.

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

http
GET /v1/sessions HTTP/1.1
Host: api.heypingr.com
X-API-Key: pk_live_xK9p2mQr8nVs4wLj7dFhTbYcXeAuZo
// webhooks

Webhooks

Hey Pingr sends a POST request to your configured webhook URL whenever an event occurs — like an incoming message from a user.

Configure your webhook URL in Dashboard → Webhooks. Hey Pingr signs every request with HMAC-SHA256 — always verify the signature before processing.

Verifying signatures

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

// 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

json
{
  "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
}
// sdk

Node.js SDK

The official Node.js SDK wraps the Hey Pingr REST API with a clean, typed interface.

Installation

bash
npm install hey-pingr

Initialise

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

Verify webhooks

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

// 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.

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

// 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

FunctionDescription
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.
// sdk

Python SDK

The official Python SDK for Hey Pingr. Supports both sync and async usage.

Installation

bash
pip install hey-pingr

# async support (FastAPI, asyncio)
pip install hey-pingr[async]

Verify webhooks

python
from pingr import verify_webhook, PingrWebhookError

# 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

python
from pingr import verify_webhook, create_challenge_code, extract_challenge_code

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

FunctionDescription
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.
// api_reference

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.

Fully backward-compatible. If your webhook doesn't return a reply field, the static auto-reply is used. No changes needed to existing integrations.

How it works

http — your webhook response
HTTP/1.1 200 OK
Content-Type: application/json

{
  "reply": "Your magic link: https://yourapp.com/auth?token=abc123xyz"
}

Rules

RuleDetail
HTTP statusYour webhook must return 2xx. Non-2xx → static reply is used.
Content-TypeMust be application/json. Other types → static reply.
reply fieldMust be a non-empty string. Missing or empty → static reply.
Max length4096 characters (WhatsApp limit). Longer values are truncated.
TimeoutYour webhook must respond within 8 seconds or the static reply is used.

Example — magic link auth

javascript
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 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);
});
// guides

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.

javascript
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 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.

javascript
// Webhook — store link keyed by phone
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.

javascript
const { verifyWebhook, createChallengeCode, extractChallengeCode } = require('hey-pingr');
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 });
});
// api_reference

Trigger Management

Manage your trigger phrases programmatically using your API key. The same key you use for webhook verification — no separate token needed.

GET /v1/triggers

List all configured trigger phrases. Optionally filter by session_id.

bash
curl https://api.heypingr.com/v1/triggers \
  -H "X-API-Key: pk_live_..."
POST /v1/triggers

Create a new trigger phrase for a session. Enforces your plan's trigger limit.

bash
curl -X POST https://api.heypingr.com/v1/triggers \
  -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

FieldTypeDescription
session_idstringRequired. The session this trigger is attached to.
trigger_phrasestringRequired. The phrase users text to trigger the reply (max 200 chars, stored lowercase).
reply_messagestringRequired. The static auto-reply (max 1000 chars). Overridden by dynamic reply if your webhook returns one.
is_activebooleanOptional. Default true. Set to false to disable without deleting.
PUT /v1/triggers/{trigger_id}

Update a trigger phrase, reply message, or active state. All fields are optional — only provided fields are updated.

DELETE /v1/triggers/{trigger_id}

Delete a trigger. Returns 204 No Content on success.

Plan limits

PlanMax triggers
Starter1
Growth2
Scale10
// api_reference

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).

POST /api/v1/simulate

Authentication: Dashboard JWT (use the dashboard or your session cookie).

bash
curl -X POST https://api.heypingr.com/api/v1/simulate \
  -H "Authorization: Bearer <jwt>" \
  -H "Content-Type: application/json" \
  -d '{"phone":"919876543210","message":"login","session_id":"sess_abc"}'

Response

json — 200 OK
{
  "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.

// api_reference

Webhook Delivery Logs

Retrieve a paginated log of recent webhook delivery attempts, including simulated events. Useful for debugging failed deliveries or auditing your integration.

GET /api/v1/webhooks/deliveries

Query parameters

ParamTypeDescription
limitintegerNumber of records to return (1–200, default 50).
beforeISO timestampCursor for pagination — return entries older than this timestamp.
event_typestringFilter by webhook.delivered, webhook.failed, or webhook.simulated.
json — 200 OK
{
  "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.

// guides

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

PlanMessages/monthOverage rate
Starter10,000$0.002/msg
Growth70,000$0.0015/msg
Scale500,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.

// guides

Error handling

All errors return a JSON body under a detail key with an error code and human-readable message.

json — error response
{
  "detail": {
    "error": "rate_limit",
    "message": "Number +919876543210 is in cooldown. Try again in 47s.",
    "retry_after": 47
  }
}

Error codes

CodeHTTPDescription
invalid_key401The API key is missing or invalid.
missing_param422A required parameter is missing or malformed.
quota_exceeded429Monthly auto-reply quota exhausted. Upgrade your plan.
// sessions

Sessions & QR codes

A session represents a connected WhatsApp number on Hey Pingr's infrastructure. Each plan allows a different number of concurrent sessions.

Use a dedicated WhatsApp number for Hey Pingr — not your personal number. This keeps your auth traffic isolated.

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.

// api_reference

GET /v1/sessions

Returns all sessions associated with your account and their current status.

GET https://api.heypingr.com/v1/sessions
json — 200 OK
{
  "sessions": [
    {
      "id": "main-login-bot",
      "name": "Main login bot",
      "number": "+919876543210",
      "status": "connected",
      "connectedAt": "2025-04-18T08:22:11.000Z",
      "messagesToday": 842
    }
  ]
}