Skip to main content

Official Python SDK for the Hey Pingr WhatsApp auto-reply platform

Project description

pingr (Python)

Official Python 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

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

from flask import Flask, request, jsonify
from pingr import verify_webhook, PingrWebhookError
import os

app = Flask(__name__)

@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"]
            link  = generate_magic_link(phone)
            # Return dynamic reply — Hey Pingr sends this as the WhatsApp message
            return jsonify({"reply": f"Your login link: {link}"})
        return "", 200
    except PingrWebhookError:
        return "", 400

Pattern 2 — Phone pre-entry + browser polling

import redis
from flask import Flask, request, jsonify
from pingr import verify_webhook

r = redis.Redis()

@app.route("/webhook", methods=["POST"])
def webhook():
    payload = verify_webhook(request.get_data(), request.headers["x-pingr-signature"], SECRET)
    if payload["event"] == "message.received":
        phone = payload["from"]["phone"]
        link  = generate_magic_link(phone)
        r.set(f"auth:{phone}", link, ex=300)
        return jsonify({"reply": f"Tap to sign in: {link}"})
    return "", 200

@app.route("/auth/poll")
def poll():
    phone = request.args.get("phone")
    link  = r.get(f"auth:{phone}")
    if link:
        r.delete(f"auth:{phone}")
        return jsonify({"status": "ready", "link": link.decode()})
    return jsonify({"status": "pending"})

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

from pingr import verify_webhook, create_challenge_code, extract_challenge_code

pending = {}  # use Redis in production

@app.route("/auth/challenge")
def challenge():
    code = create_challenge_code()  # e.g. "X4K9MQ"
    pending[code] = {"status": "pending"}
    return jsonify({"code": code, "trigger_phrase": f"login {code}"})

@app.route("/webhook", methods=["POST"])
def webhook():
    payload = verify_webhook(request.get_data(), 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, {}).get("status") == "pending":
            link = generate_magic_link(payload["from"]["phone"])
            pending[code] = {"status": "ready", "link": link}
            return jsonify({"reply": f"Here's your link: {link}"})
    return "", 200

@app.route("/auth/poll")
def poll():
    code = request.args.get("code")
    s    = pending.get(code)
    if not s:
        return jsonify({"status": "not_found"}), 404
    if s["status"] == "pending":
        return jsonify({"status": "pending"})
    link = pending.pop(code)["link"]
    return jsonify({"status": "ready", "link": 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.

return jsonify({"reply": "Your magic link: https://yourapp.com/auth?token=..."})
  • Maximum 4096 characters (WhatsApp limit)
  • Non-JSON or missing reply → static auto-reply is used (fully backward-compatible)

FastAPI example

from fastapi import FastAPI, Request, HTTPException
from pingr import verify_webhook, PingrWebhookError
import os

app = FastAPI()

@app.post("/webhook")
async def webhook(request: Request):
    raw_body  = await request.body()
    signature = request.headers.get("x-pingr-signature", "")
    try:
        payload = verify_webhook(raw_body, signature, os.environ["PINGR_WEBHOOK_SECRET"])
    except PingrWebhookError:
        raise HTTPException(status_code=400, detail="Invalid signature")

    if payload["event"] == "message.received":
        phone = payload["from"]["phone"]
        link  = await generate_magic_link(phone)
        return {"reply": f"Your login link: {link}"}

    return {"ok": True}

Webhook payloads

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": "login X4K9MQ", "type": "text" },
  "timestamp": 1714825320000
}

session.disconnected

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

API reference

verify_webhook(raw_body, signature, secret, *, check_timestamp=True)

Verify the x-pingr-signature header and return the parsed payload dict.
Raises PingrWebhookError on failure.

Param Type Description
raw_body bytes / str Raw request body — do not JSON-parse first
signature str Value of the x-pingr-signature header
secret str Your webhook signing secret from the dashboard
check_timestamp bool Reject payloads older than 5 min. Default: True

create_challenge_code(*, length=6, charset=...)

Generate a cryptographically random challenge code using Python's secrets module.

code = create_challenge_code()        # "X4K9MQ"
code = create_challenge_code(length=8) # "X4K9MQAB"

extract_challenge_code(message_text, trigger_phrase)

Parse the challenge code suffix from a trigger message.

extract_challenge_code("login X4K9MQ", "login")  # → "X4K9MQ"
extract_challenge_code("login", "login")          # → None
extract_challenge_code("help", "login")           # → None

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

hey_pingr-0.2.0.tar.gz (7.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

hey_pingr-0.2.0-py3-none-any.whl (8.0 kB view details)

Uploaded Python 3

File details

Details for the file hey_pingr-0.2.0.tar.gz.

File metadata

  • Download URL: hey_pingr-0.2.0.tar.gz
  • Upload date:
  • Size: 7.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for hey_pingr-0.2.0.tar.gz
Algorithm Hash digest
SHA256 42282bd291ba8d0f5ab8437c8dfa18a050abd6a775e58fcaccaf5bb98feedfc5
MD5 fb47ecb23d55db25b449fe2fe9d296a4
BLAKE2b-256 419ea99cbc714ef4cbe1cf7f23d371b2dcc76e4bf86c990730aa344c428d5bac

See more details on using hashes here.

File details

Details for the file hey_pingr-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: hey_pingr-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 8.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for hey_pingr-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 025e8ddff96fc0d88aaff3973660b09d763a08fcbf938840684c7333bea26aa9
MD5 5f3705e0e625393607b0eed96214a1d0
BLAKE2b-256 4eb328aee7469227488fdb77bff6a2566ef715a003bea21e252a2b9247b4a99c

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page