WhatsApp Operators DailyThe Blueticks DispatchTuesday, July 7, 2026
Productivity

How to Receive & Auto-Reply to WhatsApp Messages with Webhooks (Python & Node, 2026)

Wire up a WhatsApp API with webhooks so inbound messages hit your server and get an instant auto-reply. Real Flask and Express code, signature verification, and the production traps.

DRBy Daniel Roth · July 7, 2026 · 11 min read
How to Receive & Auto-Reply to WhatsApp Messages with Webhooks (Python & Node, 2026)

You can already send WhatsApp messages from code. The harder half is hearing back. A customer replies "yes, 3pm works" and your script has no idea it happened, because sending is a one-way push and receiving needs something listening. That something is a webhook. This guide wires up a WhatsApp API with webhooks so an inbound message lands on your server and gets an auto-reply in the same second, with real Flask and Express code you can run today.

What a WhatsApp Webhook Actually Is

A WhatsApp webhook is a URL on your server that the messaging platform calls with an HTTP POST every time an event happens, like a new inbound message. Instead of your code polling "any new messages yet?" on a loop, the platform pushes the event to you the moment it fires. You register the URL once, then you receive.

Polling is the naive approach and it falls apart fast. You either poll every few seconds and hammer the API for nothing 99% of the time, or you poll slowly and your bot answers three minutes late. Webhooks flip the model. Your server sits idle until an event arrives, then reacts. It is the same pattern Stripe uses for payments and GitHub uses for pushes.

With Blueticks, you point a webhook at your server through the /v1 REST API. The base URL is https://api.blueticks.co/v1, and every call authenticates with an API key you mint in the dashboard, sent as Authorization: Bearer bt_live_.... One registration call and inbound WhatsApp messages start hitting your endpoint. For the outbound side of the API, the companion piece is scheduling and sending messages via the API.

The Two-Way Flow: Inbound to Your Server to Auto-Reply

The full loop is three hops. A contact messages your WhatsApp number, Blueticks POSTs that event to your webhook URL, and your handler reads the sender plus text and calls the send endpoint to reply. The whole round trip runs in well under a second, so the contact experiences it as an instant response, not a scheduled batch.

Here is the sequence, start to finish:

  1. A person sends a message to your connected WhatsApp number.
  2. Blueticks fires the new_message_received_webhook event and POSTs a JSON envelope to your registered URL.
  3. Your server verifies the signature, confirms the payload is genuine, then parses out who sent it and what they said.
  4. Your handler decides on a reply and calls POST /v1/scheduled-messages with the sender and your response text.
  5. The reply goes out from your own number, and the contact sees it land in the same thread.

Relay runners handing off a baton, a metaphor for the inbound-to-auto-reply webhook round trip

The key mental shift: receiving and sending are two separate endpoints. The webhook delivers inbound. The scheduled-messages endpoint pushes outbound. Your auto-reply logic is just the glue that reads one and calls the other. Everything past step 3 is your code, which means the bot can be as dumb as a canned "got it, we will be in touch" or as smart as a full LLM call. The transport is the same either way.

Registering a Webhook With the Blueticks /v1 API

You register a webhook with one authenticated POST to https://api.blueticks.co/v1/webhooks. The body needs a url (which must be https://), an events array naming what you want to receive, and an optional description under 120 characters. Blueticks returns the created webhook object with its id and a status of enabled.

Here is the registration call:

curl -X POST https://api.blueticks.co/v1/webhooks \
  -H "Authorization: Bearer bt_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-server.com/hook",
    "events": ["new_message_received_webhook"],
    "description": "auto-reply bot"
  }'

The response looks like this:

{
  "id": "wh_...",
  "url": "https://your-server.com/hook",
  "events": ["new_message_received_webhook"],
  "description": "auto-reply bot",
  "status": "enabled",
  "created_at": "2026-07-07T09:00:00Z"
}

The event you want for a two-way bot is new_message_received_webhook, the "a message arrived" trigger. Blueticks exposes other events you can subscribe to as your bot grows: reply_to_my_message_webhook, message_reaction_webhook, poll_vote_webhook, delivery events like message.delivered, message.failed, and message.read, and session events session.connected and session.disconnected. Start with the inbound one and add the rest when you need them.

One gotcha before you go further: your url has to be publicly reachable over HTTPS. For local development that means a tunnel like ngrok in front of your Flask or Express process, because localhost:3000 is not a URL Blueticks can POST to. Register the tunnel URL while testing, then swap it for your real domain on deploy.

Verifying and Parsing the Payload

Every delivery carries a signature so you can prove it came from Blueticks and not a stranger who found your URL. The header X-Blueticks-Signature is HMAC-SHA256(secret, "{timestamp}.{rawBody}") as hex, and Blueticks-Webhook-Timestamp carries the unix seconds. You recompute that HMAC over the raw body with your webhook secret and compare it to the header using a constant-time check.

Two rules make or break this. First, hash the raw request body, the exact bytes on the wire, before any JSON middleware reshapes it. Re-serialized JSON will not match, because key order and whitespace shift. Second, use a constant-time comparison (hmac.compare_digest in Python, crypto.timingSafeEqual in Node), never ==, so an attacker cannot leak the secret one byte at a time through response timing. This is the same discipline Meta documents for its own X-Hub-Signature-256 webhook header, so it is not a Blueticks quirk, it is how webhook signing works everywhere.

The delivery envelope is predictable:

{
  "id": "evt_abc123",
  "type": "new_message_received_webhook",
  "created_at": "2026-07-07T09:00:00Z",
  "data": { }
}

The data object is the message itself. The exact sub-fields are not something I will hard-code here, because they are not publicly pinned and inventing key names would just cost you a debugging session. Do this instead: log data once on your first real inbound message, read the shape from your own logs, then pull the sender and text fields you actually see. That takes thirty seconds and gives you ground truth instead of a guess.

Ready to build this instead of reading about it? Grab a /v1 API key from your dashboard and point a webhook at your server. No Meta business verification, no per-message fees, and the reply goes out from your own number, so you can have a working two-way bot before lunch.

Receive and Auto-Reply in Python (Flask)

A Flask auto-reply bot needs one route that reads the raw body, verifies X-Blueticks-Signature, parses the inbound message, and calls POST /v1/scheduled-messages to answer. The critical detail is grabbing request.get_data() for the raw bytes before Flask parses JSON, so the HMAC matches.

import hmac, hashlib, os, requests
from flask import Flask, request, abort

app = Flask(__name__)

WEBHOOK_SECRET = os.environ["BLUETICKS_WEBHOOK_SECRET"].encode()
API_KEY = os.environ["BLUETICKS_API_KEY"]

# Set these to the keys you see after logging `data` on your first inbound event.
SENDER_KEY = "from"   # placeholder: confirm against your logged payload
TEXT_KEY = "body"     # placeholder: confirm against your logged payload


def verify(raw_body: bytes, timestamp: str, signature: str) -> bool:
    signed = f"{timestamp}.".encode() + raw_body
    expected = hmac.new(WEBHOOK_SECRET, signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature or "")


@app.post("/hook")
def hook():
    raw = request.get_data()  # raw bytes, BEFORE json parsing
    ts = request.headers.get("Blueticks-Webhook-Timestamp", "")
    sig = request.headers.get("X-Blueticks-Signature", "")

    if not verify(raw, ts, sig):
        abort(401)

    event = request.get_json()
    if event.get("type") != "new_message_received_webhook":
        return "", 200  # ignore other events for now

    data = event["data"]
    app.logger.info("inbound data: %s", data)  # log once, then read the shape

    sender = data.get(SENDER_KEY)
    text = data.get(TEXT_KEY)

    if sender and text:
        requests.post(
            "https://api.blueticks.co/v1/scheduled-messages",
            headers={"Authorization": f"Bearer {API_KEY}"},
            json={"to": sender, "message": f"Thanks, we got your message: {text}"},
            timeout=10,
        )

    return "", 200


if __name__ == "__main__":
    app.run(port=3000)

Run it, point ngrok at port 3000, register the tunnel URL, and message your number. The to field on the reply accepts an E.164 phone like +15551234567 or a WhatsApp JID like 12345@c.us, whichever your logged data hands you for the sender. Omitting send_at sends immediately, which is exactly what an auto-reply wants.

The Same Bot in Node (Express)

An Express version follows the identical shape, with one Node-specific trap: use express.raw() on the webhook route so req.body is a Buffer of the untouched bytes. If you let express.json() parse it first, your HMAC will never match and every request will 401.

const express = require("express");
const crypto = require("crypto");

const app = express();

const SECRET = process.env.BLUETICKS_WEBHOOK_SECRET;
const API_KEY = process.env.BLUETICKS_API_KEY;

// Set these after logging `data` on your first inbound event.
const SENDER_KEY = "from"; // placeholder: confirm against your logged payload
const TEXT_KEY = "body";   // placeholder: confirm against your logged payload

// raw body ONLY on this route, so the HMAC matches byte-for-byte
app.use("/hook", express.raw({ type: "application/json" }));

function verify(rawBody, timestamp, signature) {
  const signed = `${timestamp}.` + rawBody.toString();
  const expected = crypto.createHmac("sha256", SECRET).update(signed).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signature || "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post("/hook", async (req, res) => {
  const ts = req.get("Blueticks-Webhook-Timestamp") || "";
  const sig = req.get("X-Blueticks-Signature") || "";

  if (!verify(req.body, ts, sig)) return res.sendStatus(401);

  const event = JSON.parse(req.body.toString());
  if (event.type !== "new_message_received_webhook") return res.sendStatus(200);

  const data = event.data;
  console.log("inbound data:", data); // log once, then read the shape

  const sender = data[SENDER_KEY];
  const text = data[TEXT_KEY];

  res.sendStatus(200); // ACK fast, before the outbound call

  if (sender && text) {
    await fetch("https://api.blueticks.co/v1/scheduled-messages", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        to: sender,
        message: `Thanks, we got your message: ${text}`,
      }),
    });
  }
});

app.listen(3000, () => console.log("listening on :3000"));

Note the order: this handler returns 200 before it makes the outbound send. That is deliberate, and the next section explains why acking first is the difference between a stable bot and a flood of duplicate replies.

Avoiding Duplicate Replies, Loops, and Dropped Events

Webhook delivery is at-least-once, not exactly-once, so production breaks in three predictable ways: duplicate deliveries, reply loops, and events dropped while your server was down. You defend against all three with fast acks, an idempotency check on the event id, and a sender guard so your bot never answers itself.

A small always-on home server humming overnight, illustrating reliable webhook delivery in production

Here are the failure modes and the fix for each:

Failure modeWhat happensThe fix
Duplicate repliesYour handler is slow, the platform times out and retries, the contact gets two answersReturn 2xx within a second or two; do the slow work after acking
Reply loopsYour bot's own outbound triggers another inbound event and it answers itself foreverCheck the sender is not your own number before replying
Dropped eventsYour server was down when the message arrivedRely on retries, and keep the handler cheap so it rarely fails
Replayed payloadsAn attacker re-sends an old captured requestReject deliveries whose Blueticks-Webhook-Timestamp is older than a few minutes

The single most important habit is idempotency. Every envelope carries a unique id. Store the IDs you have already processed (a Redis set with a TTL works fine) and skip any repeat. That one check makes duplicate deliveries harmless, which in turn lets you ack fast and worry about the reply afterward.

The reply-loop trap catches almost everyone once. If your bot answers every inbound message and your own sends echo back as events, it will happily talk to itself until it hits a rate limit. Guard it: compare the sender against your connected number and bail if they match. Test it deliberately by messaging your own number and confirming the bot stays quiet.

Blueticks vs the Meta WhatsApp Cloud API, Honestly

Use the Meta WhatsApp Cloud API directly when you are a verified business sending high volumes of template messages at official scale. Use Blueticks when you want a two-way bot running on your own personal or business number today, without Meta business verification, template approval, or per-message fees. They solve different problems, and picking wrong wastes weeks.

The Meta Cloud API is the official, sanctioned path, and it is genuinely the right call at scale. But the on-ramp is real work. You register a business, verify it with Meta, register a phone number that becomes a Cloud API number (not your normal WhatsApp), get message templates approved, and you pay per message. Meta moved to per-message pricing on July 1, 2025, deprecating the older conversation-based model, and free-form replies are only allowed inside a 24-hour customer service window after the user last messaged you. Outside that window you send a paid, pre-approved template.

Blueticks trades that scale ceiling for speed and simplicity:

Blueticks /v1Meta Cloud API direct
Your numberYour own WhatsApp / Business numberA dedicated Cloud API number
Meta business verificationNot requiredRequired
Template pre-approvalNot requiredRequired for messages outside the 24h window
Per-message feesNoYes, per Meta's pricing
Time to first two-way replyMinutesDays to weeks
Best forBots, personal and SMB automation, own-number repliesHigh-volume verified-business messaging at scale

If you are Uber sending millions of ride receipts, go Cloud API. If you are a developer who wants inbound messages to hit a Flask handler and auto-reply from the number you already use, the verification overhead buys you nothing.

What Raw Webhooks Cannot Do That Blueticks Adds

Raw webhooks give you the receive side and nothing else. Blueticks wraps them in a full messaging layer: sending from your own existing number, no Meta review or template approval, and scheduling on the same /v1 API, so one integration covers inbound, outbound, and timed sends instead of three.

A bare webhook is just a delivery mechanism. To turn inbound events into a real bot you still need a way to send, an identity to send from, and usually a way to send later, not just now. Blueticks bundles all three:

  • Your own number. Replies come from the WhatsApp number your contacts already know, not a fresh Cloud API line they have never seen. No re-verifying a business identity to Meta to get started.
  • No Meta review gate. You are not waiting on template approval to send a free-form reply. The auto-reply in the Flask example above just works.
  • Scheduling on the same API. The scheduled-messages endpoint you use for instant auto-replies also takes a send_at, so a webhook that receives "remind me tomorrow" can schedule the reminder without a second service. That is the scheduling API doing double duty. If you want a fuller bot skeleton in Python, the own-number WhatsApp bot walkthrough builds on this same foundation.

One honest limit to plan for: this runs on your real WhatsApp number, so normal account rules apply. Auto-replies to people who messaged you are exactly the kind of legitimate use WhatsApp expects. Blasting unsolicited outbound to strangers is not, and it can get a number banned regardless of which API sends it. Build a responder, not a spam cannon, and you stay well inside the lines.

Frequently Asked Questions

Do I need a public server to receive WhatsApp webhooks?

Yes. The webhook url must be publicly reachable over HTTPS, because Blueticks POSTs to it from the outside. For local development, run a tunnel like ngrok in front of your Flask or Express process and register the tunnel URL. Swap it for your real domain when you deploy.

How do I stop my bot from replying to itself?

Compare the sender on each inbound event against your own connected WhatsApp number and skip the reply if they match. Without this guard, your bot's outbound messages can echo back as new inbound events and it will loop until it hits a rate limit. Test it by messaging your own number.

Why is my signature verification always failing?

Almost always because you are hashing parsed-and-reserialized JSON instead of the raw request bytes. Recompute the HMAC over the exact body on the wire: request.get_data() in Flask, or an express.raw() route in Express. The signed string is "{timestamp}.{rawBody}", and the comparison must be constant-time.

Can the same webhook receive delivery and read receipts too?

Yes. Beyond new_message_received_webhook, you can subscribe to events like message.delivered, message.read, message.failed, reply_to_my_message_webhook, poll_vote_webhook, and session events. Add them to the events array when you register or update the webhook, then branch on event.type in your handler.

Do I have to verify my business with Meta to use this?

No. That is the core difference from the Meta Cloud API directly. Blueticks sends and receives on your own existing WhatsApp number without Meta business verification or template pre-approval, which is what makes a working two-way bot achievable in minutes instead of over a multi-day approval process.

Email

The Dispatch, every week.

One sharp WhatsApp growth tactic in your inbox each week. Joined by 238,000+ founders, marketers and support leads.

Free forever. No spam, unsubscribe in one click.