Developer Documentation

Build with the WapiConnect API

Send messages, verify users with OTP, run bulk campaigns, and receive messages via webhooks — all through a clean REST API. Get started in minutes.

Quick Start

Follow these three steps to send your first WhatsApp message.

Step 1 — Register & get your API key

Create a free account at portal.wapiconnect.cloud/register. After verifying your email you'll receive your API key on the dashboard.

Step 2 — Create a WhatsApp session

POST /api/whatsapp/session
X-API-Key: your_api_key

{
  "sessionId": "my-session"
}

Then poll for the QR code:

GET /api/whatsapp/session/my-session/status
X-API-Key: your_api_key

// Response when QR is ready:
{
  "status": "qr_ready",
  "qrCode": "data:image/png;base64,..."
}

Scan the QR code with your WhatsApp mobile app (Settings → Linked Devices → Link a Device). Wait for status to become ready.

Step 3 — Send a message

POST /api/whatsapp/session/my-session/send
X-API-Key: your_api_key

{
  "to": "919876543210",
  "message": "Hello from WapiConnect!"
}

Phone number format: Use country code without + (e.g. 919876543210 for India +91 98765 43210). The API auto-appends @s.whatsapp.net.

Authentication

All tenant API requests require your API key sent as an HTTP header:

X-API-Key: your_api_key_here

Your API key is shown on the dashboard under Settings → API Key. You can also reset it anytime — the old key is immediately invalidated.

Keep your API key secret. Never commit it to version control. Store it in environment variables.

Base URL

https://api.wapiconnect.cloud

All endpoints below are relative to this base URL. All requests and responses use Content-Type: application/json.

Error Handling

All errors follow a consistent format:

{
  "error": "Human-readable error message",
  "code": "ERROR_CODE"   // optional machine-readable code
}
Status Meaning
200 Success
400 Bad request — missing or invalid parameters
401 Unauthorized — invalid or missing API key
404 Session or resource not found
429 Rate limit exceeded (daily message limit)
500 Internal server error

When your daily message limit is reached:

// 429 Too Many Requests
{
  "error": "Daily sent message limit reached",
  "code": "SENT_LIMIT_EXCEEDED",
  "limit": 100,
  "used": 100
}

Sessions

A session represents a linked WhatsApp number. Each plan allows a different number of concurrent sessions.

Create / initialize a session

POST/api/whatsapp/session
Creates a new session and starts the WhatsApp connection process.
// Request body
{
  "sessionId": "main",         // any unique string
  "name": "Sales Number"      // optional label
}

// Response
{
  "success": true,
  "sessionId": "main",
  "isNew": true,
  "status": "initializing"
}

Get session status & QR code

GET/api/whatsapp/session/{sessionId}/status
// While waiting for QR scan:
{ "status": "qr_ready", "qrCode": "data:image/png;base64,..." }

// Once connected:
{ "status": "ready", "connected": true, "phoneNumber": "919876543210" }
Status value Meaning
initializing Session is starting up
qr_ready QR code available — scan it in WhatsApp
authenticated QR scanned, finalizing connection
ready Fully connected, ready to send/receive
disconnected Session lost connection

List all sessions

GET/api/whatsapp/sessions

Disconnect a session

DELETE/api/whatsapp/session/{sessionId}
Logs out and removes the WhatsApp session.

Send Message

POST/api/whatsapp/session/{sessionId}/send
Send a text message to any WhatsApp number.
Field Type Required Description
to string Yes Phone number with country code (no +), or full JID
message string Yes Text content to send
type string No text (default), image, video, document, audio, poll
header string No Bold header shown above message
footer string No Italic footer shown below message
// Simple text message
{
  "to": "919876543210",
  "message": "Your order #1234 has been shipped!"
}

// With header and footer
{
  "to": "919876543210",
  "header": "Order Update",
  "message": "Your order #1234 has been shipped and will arrive by Friday.",
  "footer": "WapiConnect Store"
}

// Response
{
  "success": true,
  "messageId": "3EB0B430A8B7F6C1D2E3",
  "timestamp": 1708243200
}

Media Messages

Send images, videos, documents, and audio files by providing a public URL or base64-encoded content.

Field Type Description
type string image | video | document | audio
mediaUrl string Publicly accessible URL of the file
mediaBase64 string Base64-encoded file content (alternative to URL)
mediaMimeType string MIME type e.g. image/jpeg, application/pdf
fileName string Filename shown in WhatsApp (for documents)
message string Optional caption shown below the media
// Send an image via URL
{
  "to": "919876543210",
  "type": "image",
  "mediaUrl": "https://example.com/invoice.jpg",
  "message": "Your invoice for March 2026"
}

// Send a PDF document
{
  "to": "919876543210",
  "type": "document",
  "mediaBase64": "JVBERi0xLjQ...",
  "mediaMimeType": "application/pdf",
  "fileName": "invoice-march-2026.pdf"
}

Bulk Broadcast

Send a message to many recipients in one call. The backend processes them sequentially with random delays to avoid WhatsApp rate limits. You get back a jobId to poll for progress.

POST/api/whatsapp/session/{sessionId}/send-bulk
Field Type Description
recipients array Array of phone numbers or {phone, name} objects
message string Message text (required for type: "text")
type string Same as single send (text, image, etc.)
delayMin number Min delay between sends in ms (default: 2000)
delayMax number Max delay between sends in ms (default: 5000)
// Start a bulk job
{
  "recipients": ["919876543210", "919876543211", "919876543212"],
  "message": "Flash sale! 30% off all products today only.",
  "delayMin": 3000,
  "delayMax": 6000
}

// Response — job queued
{
  "jobId": "65f1a2b3c4d5e6f7a8b9c0d1",
  "total": 3
}

Poll job progress

GET/api/whatsapp/bulk-job/{jobId}
{
  "status": "running",
  "total": 3,
  "succeeded": 1,
  "failed": 0,
  "processed": 1
}
Status Meaning
pending Job queued, not started yet
running Actively sending messages
completed All recipients processed
failed Job aborted due to critical error

Webhooks

Configure a webhook URL in your dashboard (Settings → Webhook URL) to receive real-time events whenever a message arrives or your session status changes.

Event types

Event Triggered when
message A new incoming message is received
qr A QR code is generated for a session
ready A session becomes fully connected
disconnected A session loses connection
test Manually triggered from dashboard

Payload format

{
  "tenantId": "my-business-a1b2c3",
  "event": "message",
  "sessionId": "main",
  "timestamp": 1708243200000,
  "data": {
    "id": "3EB0B430A8B7F6C1D2E3",
    "from": "919876543210@s.whatsapp.net",
    "body": "Hello! I need help with my order.",
    "type": "text",
    "isGroup": false,
    "hasMedia": false
  },
  "signature": "a1b2c3..."  // only if webhook secret is set
}

Signature verification

Set a Webhook Secret in your dashboard to verify that payloads come from WapiConnect. We send an HMAC-SHA256 signature in the signature field:

// Node.js verification example
const crypto = require("crypto");

function verifyWebhook(payload, receivedSig, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(JSON.stringify(payload))
    .digest("hex");
  return expected === receivedSig;
}

Message Logs

Retrieve a paginated history of sent and received messages.

GET/api/whatsapp/messages?page=1&limit=20&sessionId=main
{
  "messages": [
    {
      "sessionId": "main",
      "to": "919876543210",
      "message": "Hello!",
      "status": "sent",
      "type": "text",
      "createdAt": "2026-03-08T10:30:00.000Z"
    }
  ],
  "total": 142,
  "page": 1,
  "pages": 8
}

Code Example — OTP Verification

Send a one-time password to verify your users. Works like SMS OTP but over WhatsApp with much higher open rates.

Node.jsconst axios = require("axios");

const API_KEY = process.env.WAPI_KEY;
const SESSION = "main";
const BASE = "https://api.wapiconnect.cloud";

async function sendOtp(phone, otp) {
  await axios.post(
    `${BASE}/api/whatsapp/session/${SESSION}/send`,
    {
      to: phone,
      header: "Verification Code",
      message: `Your OTP is *${otp}*. Valid for 10 minutes.\nDo not share this code.`,
      footer: "WapiConnect Security",
    },
    { headers: { "X-API-Key": API_KEY } }
  );
}

// Usage in your auth route:
const otp = Math.floor(100000 + Math.random() * 900000).toString();
await sendOtp("919876543210", otp);
// store otp in Redis/DB with expiry, then verify on next request
PHP<?php
function sendOtp($phone, $otp) {
    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL            => "https://api.wapiconnect.cloud/api/whatsapp/session/main/send",
        CURLOPT_POST           => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => [
            "X-API-Key: " . getenv("WAPI_KEY"),
            "Content-Type: application/json",
        ],
        CURLOPT_POSTFIELDS => json_encode([
            "to"      => $phone,
            "header"  => "Verification Code",
            "message" => "Your OTP is *{$otp}*. Valid for 10 minutes.",
            "footer"  => "WapiConnect Security",
        ]),
    ]);
    $response = curl_exec($ch);
    curl_close($ch);
    return json_decode($response, true);
}

$otp = str_pad(rand(0, 999999), 6, "0", STR_PAD_LEFT);
sendOtp("919876543210", $otp);
Pythonimport requests, random, os

API_KEY = os.environ["WAPI_KEY"]
BASE = "https://api.wapiconnect.cloud"

def send_otp(phone: str, otp: str):
    requests.post(
        f"{BASE}/api/whatsapp/session/main/send",
        headers={"X-API-Key": API_KEY},
        json={
            "to": phone,
            "header": "Verification Code",
            "message": f"Your OTP is *{otp}*. Valid for 10 minutes.",
            "footer": "WapiConnect Security",
        },
    ).raise_for_status()

otp = f"{random.randint(0, 999999):06d}"
send_otp("919876543210", otp)
Rubyrequire 'net/http'
require 'json'
require 'uri'

API_KEY = ENV['WAPI_KEY']
BASE    = 'https://api.wapiconnect.cloud'

def send_otp(phone, otp)
  uri  = URI("#{BASE}/api/whatsapp/session/main/send")
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true

  req      = Net::HTTP::Post.new(uri.path)
  req['X-API-Key']    = API_KEY
  req['Content-Type'] = 'application/json'
  req.body = JSON.generate(
    to:      phone,
    header:  'Verification Code',
    message: "Your OTP is *#{otp}*. Valid for 10 minutes.",
    footer:  'WapiConnect Security'
  )
  http.request(req)
end

otp = rand(100_000..999_999).to_s
send_otp('919876543210', otp)
Gopackage main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "math/rand"
    "net/http"
    "os"
)

const base = "https://api.wapiconnect.cloud"

func sendOTP(phone, otp string) {
    body, _ := json.Marshal(map[string]string{
        "to":      phone,
        "header":  "Verification Code",
        "message": fmt.Sprintf("Your OTP is *%s*. Valid for 10 minutes.", otp),
        "footer":  "WapiConnect Security",
    })
    req, _ := http.NewRequest("POST",
        base+"/api/whatsapp/session/main/send",
        bytes.NewReader(body))
    req.Header.Set("X-API-Key", os.Getenv("WAPI_KEY"))
    req.Header.Set("Content-Type", "application/json")
    http.DefaultClient.Do(req)
}

func main() {
    otp := fmt.Sprintf("%06d", rand.Intn(1000000))
    sendOTP("919876543210", otp)
}
Javaimport java.net.URI;
import java.net.http.*;

public class WapiOtp {
    static final String BASE    = "https://api.wapiconnect.cloud";
    static final String API_KEY = System.getenv("WAPI_KEY");
    static final HttpClient HTTP = HttpClient.newHttpClient();

    static void sendOtp(String phone, String otp) throws Exception {
        String json = String.format(
            "{\"to\":\"%s\",\"header\":\"Verification Code\","
            + "\"message\":\"Your OTP is *%s*. Valid for 10 minutes.\","
            + "\"footer\":\"WapiConnect Security\"}", phone, otp);

        var req = HttpRequest.newBuilder()
            .uri(URI.create(BASE + "/api/whatsapp/session/main/send"))
            .header("X-API-Key", API_KEY)
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(json))
            .build();
        HTTP.send(req, HttpResponse.BodyHandlers.ofString());
    }

    public static void main(String[] args) throws Exception {
        String otp = String.format("%06d", (int)(Math.random() * 1000000));
        sendOtp("919876543210", otp);
    }
}
C# / .NETusing System.Net.Http.Json;

var apiKey = Environment.GetEnvironmentVariable("WAPI_KEY")!;
const string BASE = "https://api.wapiconnect.cloud";

var http = new HttpClient();
http.DefaultRequestHeaders.Add("X-API-Key", apiKey);

async Task SendOtp(string phone, string otp) =>
    await http.PostAsJsonAsync(
        $"{BASE}/api/whatsapp/session/main/send",
        new {
            to      = phone,
            header  = "Verification Code",
            message = $"Your OTP is *{otp}*. Valid for 10 minutes.",
            footer  = "WapiConnect Security"
        });

var rng = new Random();
await SendOtp("919876543210", rng.Next(100000, 999999).ToString());
cURL# Generate a random 6-digit OTP
OTP=$(shuf -i 100000-999999 -n 1)

curl -s -X POST https://api.wapiconnect.cloud/api/whatsapp/session/main/send \
  -H "X-API-Key: $WAPI_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"to\": \"919876543210\",
    \"header\": \"Verification Code\",
    \"message\": \"Your OTP is *${OTP}*. Valid for 10 minutes.\",
    \"footer\": \"WapiConnect Security\"
  }"

Code Example — Bulk Campaign

Node.jsconst axios = require("axios");

const client = axios.create({
  baseURL: "https://api.wapiconnect.cloud",
  headers: { "X-API-Key": process.env.WAPI_KEY },
});

async function runCampaign(recipients, message) {
  // 1. Start the bulk job
  const { data } = await client.post("/api/whatsapp/session/main/send-bulk", {
    recipients,
    message,
    delayMin: 3000,
    delayMax: 7000,
  });

  const { jobId } = data;
  console.log(`Job started: ${jobId}`);

  // 2. Poll until complete
  while (true) {
    const { data: job } = await client.get(`/api/whatsapp/bulk-job/${jobId}`);
    console.log(`${job.processed}/${job.total} sent`);
    if (job.status === "completed" || job.status === "failed") break;
    await new Promise(r => setTimeout(r, 3000));
  }
}

runCampaign(
  ["919876543210", "919876543211"],
  "Big Sale! 40% off everything today."
);
PHP<?php
function wapiPost($path, $body = []) {
    $ch = curl_init("https://api.wapiconnect.cloud" . $path);
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => ["X-API-Key: " . getenv("WAPI_KEY"), "Content-Type: application/json"],
        CURLOPT_POSTFIELDS     => json_encode($body),
    ]);
    $r = json_decode(curl_exec($ch), true);
    curl_close($ch);
    return $r;
}
function wapiGet($path) {
    $ch = curl_init("https://api.wapiconnect.cloud" . $path);
    curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => ["X-API-Key: " . getenv("WAPI_KEY")]]);
    $r = json_decode(curl_exec($ch), true);
    curl_close($ch);
    return $r;
}

$job = wapiPost("/api/whatsapp/session/main/send-bulk", [
    "recipients" => ["919876543210", "919876543211"],
    "message"    => "Big Sale! 40% off today.",
]);
$jobId = $job["jobId"];
do {
    sleep(3);
    $status = wapiGet("/api/whatsapp/bulk-job/{$jobId}");
} while ($status["status"] === "running" || $status["status"] === "pending");
echo "Done: {$status['succeeded']} sent, {$status['failed']} failed\n";
Pythonimport requests, time, os

BASE = "https://api.wapiconnect.cloud"
HEADERS = {"X-API-Key": os.environ["WAPI_KEY"]}

resp = requests.post(
    f"{BASE}/api/whatsapp/session/main/send-bulk",
    headers=HEADERS,
    json={
        "recipients": ["919876543210", "919876543211"],
        "message": "Big Sale! 40% off today.",
        "delayMin": 3000,
        "delayMax": 7000,
    },
)
job_id = resp.json()["jobId"]

while True:
    job = requests.get(f"{BASE}/api/whatsapp/bulk-job/{job_id}", headers=HEADERS).json()
    print(f"{job['processed']}/{job['total']} sent")
    if job["status"] in ("completed", "failed"):
        break
    time.sleep(3)
Rubyrequire 'net/http'
require 'json'
require 'uri'

API_KEY = ENV['WAPI_KEY']
BASE    = 'https://api.wapiconnect.cloud'

def wapi_post(path, body)
  uri  = URI("#{BASE}#{path}")
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true
  req = Net::HTTP::Post.new(uri.path)
  req['X-API-Key']    = API_KEY
  req['Content-Type'] = 'application/json'
  req.body = JSON.generate(body)
  JSON.parse(http.request(req).body)
end

def wapi_get(path)
  uri  = URI("#{BASE}#{path}")
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true
  req = Net::HTTP::Get.new(uri.path)
  req['X-API-Key'] = API_KEY
  JSON.parse(http.request(req).body)
end

job    = wapi_post('/api/whatsapp/session/main/send-bulk', {
  recipients: ['919876543210', '919876543211'],
  message:    'Big Sale! 40% off today.',
  delayMin:   3000, delayMax: 7000
})
job_id = job['jobId']

loop do
  sleep 3
  status = wapi_get("/api/whatsapp/bulk-job/#{job_id}")
  puts "#{status['processed']}/#{status['total']} sent"
  break if %w[completed failed].include?(status['status'])
end
Gopackage main

import (
    "bytes"; "encoding/json"; "fmt"
    "io"; "net/http"; "os"; "time"
)

var (
    apiKey = os.Getenv("WAPI_KEY")
    base   = "https://api.wapiconnect.cloud"
)

func post(path string, body any) map[string]any {
    b, _ := json.Marshal(body)
    req, _ := http.NewRequest("POST", base+path, bytes.NewReader(b))
    req.Header.Set("X-API-Key", apiKey)
    req.Header.Set("Content-Type", "application/json")
    resp, _ := http.DefaultClient.Do(req)
    data, _ := io.ReadAll(resp.Body)
    var result map[string]any
    json.Unmarshal(data, &result)
    return result
}

func get(path string) map[string]any {
    req, _ := http.NewRequest("GET", base+path, nil)
    req.Header.Set("X-API-Key", apiKey)
    resp, _ := http.DefaultClient.Do(req)
    data, _ := io.ReadAll(resp.Body)
    var result map[string]any
    json.Unmarshal(data, &result)
    return result
}

func main() {
    job   := post("/api/whatsapp/session/main/send-bulk", map[string]any{
        "recipients": []string{"919876543210", "919876543211"},
        "message":    "Big Sale! 40% off today.",
        "delayMin":   3000, "delayMax": 7000,
    })
    jobID := job["jobId"].(string)

    for {
        time.Sleep(3 * time.Second)
        s := get("/api/whatsapp/bulk-job/" + jobID)
        fmt.Printf("%.0f/%.0f sent\n", s["processed"], s["total"])
        if st, _ := s["status"].(string); st == "completed" || st == "failed" {
            break
        }
    }
}
Javaimport java.net.URI;
import java.net.http.*;
import java.util.concurrent.TimeUnit;

public class BulkCampaign {
    static final String     BASE = "https://api.wapiconnect.cloud";
    static final String  API_KEY = System.getenv("WAPI_KEY");
    static final HttpClient HTTP = HttpClient.newHttpClient();

    static String post(String path, String json) throws Exception {
        var req = HttpRequest.newBuilder()
            .uri(URI.create(BASE + path))
            .header("X-API-Key", API_KEY)
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(json)).build();
        return HTTP.send(req, HttpResponse.BodyHandlers.ofString()).body();
    }

    static String get(String path) throws Exception {
        var req = HttpRequest.newBuilder()
            .uri(URI.create(BASE + path))
            .header("X-API-Key", API_KEY).GET().build();
        return HTTP.send(req, HttpResponse.BodyHandlers.ofString()).body();
    }

    public static void main(String[] args) throws Exception {
        String body = "{\"recipients\":[\"919876543210\",\"919876543211\"],"
            + "\"message\":\"Big Sale! 40% off today.\","
            + "\"delayMin\":3000,\"delayMax\":7000}";
        String job   = post("/api/whatsapp/session/main/send-bulk", body);
        String jobId = job.split("\"jobId\":\"")[1].split("\"")[0];

        while (true) {
            TimeUnit.SECONDS.sleep(3);
            String status = get("/api/whatsapp/bulk-job/" + jobId);
            System.out.println(status);
            if (status.contains("\"completed\"") || status.contains("\"failed\"")) break;
        }
    }
}
C# / .NETusing System.Net.Http.Json;
using System.Text.Json;

var apiKey = Environment.GetEnvironmentVariable("WAPI_KEY")!;
const string BASE = "https://api.wapiconnect.cloud";
var http = new HttpClient();
http.DefaultRequestHeaders.Add("X-API-Key", apiKey);

// 1. Start bulk job
var resp = await http.PostAsJsonAsync(
    $"{BASE}/api/whatsapp/session/main/send-bulk",
    new {
        recipients = new[] { "919876543210", "919876543211" },
        message    = "Big Sale! 40% off today.",
        delayMin   = 3000, delayMax = 7000
    });
var job   = await resp.Content.ReadFromJsonAsync<JsonElement>();
var jobId = job.GetProperty("jobId").GetString();

// 2. Poll until complete
while (true) {
    await Task.Delay(3000);
    var status = await http
        .GetFromJsonAsync<JsonElement>($"{BASE}/api/whatsapp/bulk-job/{jobId}");
    Console.WriteLine($"{status.GetProperty("processed")}/{status.GetProperty("total")} sent");
    var s = status.GetProperty("status").GetString();
    if (s == "completed" || s == "failed") break;
}
cURL# 1. Start bulk job and capture jobId
RESPONSE=$(curl -s -X POST https://api.wapiconnect.cloud/api/whatsapp/session/main/send-bulk \
  -H "X-API-Key: $WAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "recipients": ["919876543210","919876543211"],
    "message": "Big Sale! 40% off today.",
    "delayMin": 3000, "delayMax": 7000
  }')

JOB_ID=$(echo $RESPONSE | grep -o '"jobId":"[^"]*"' | cut -d'"' -f4)
echo "Job started: $JOB_ID"

# 2. Poll progress
while true; do
  sleep 3
  STATUS=$(curl -s "https://api.wapiconnect.cloud/api/whatsapp/bulk-job/$JOB_ID" \
    -H "X-API-Key: $WAPI_KEY")
  echo $STATUS
  echo $STATUS | grep -q '"completed"\|"failed"' && break
done

Code Example — Receive Messages (Webhook)

Node.jsconst express = require("express");
const crypto = require("crypto");
const app = express();
app.use(express.json());

const WEBHOOK_SECRET = process.env.WAPI_WEBHOOK_SECRET;

app.post("/whatsapp-webhook", (req, res) => {
  const { event, sessionId, data, signature } = req.body;

  // Verify signature (if secret is set)
  if (WEBHOOK_SECRET && signature) {
    const expected = crypto
      .createHmac("sha256", WEBHOOK_SECRET)
      .update(JSON.stringify(req.body))
      .digest("hex");
    if (expected !== signature) return res.status(401).end();
  }

  if (event === "message") {
    const from = data.from.replace("@s.whatsapp.net", "");
    console.log(`Message from ${from}: ${data.body}`);
    // Handle auto-replies, store in DB, etc.
  }

  res.json({ ok: true });
});

app.listen(4000);
PHP<?php
$payload  = json_decode(file_get_contents("php://input"), true);
$secret   = getenv("WAPI_WEBHOOK_SECRET");

if ($secret && isset($payload["signature"])) {
    $expected = hash_hmac("sha256", json_encode($payload), $secret);
    if ($expected !== $payload["signature"]) {
        http_response_code(401);
        exit;
    }
}

if ($payload["event"] === "message") {
    $from = str_replace("@s.whatsapp.net", "", $payload["data"]["from"]);
    $body = $payload["data"]["body"];
    error_log("Message from {$from}: {$body}");
    // save to DB, trigger auto-reply, etc.
}

echo json_encode(["ok" => true]);
Python (Flask)import hmac, hashlib, json, os
from flask import Flask, request, jsonify

app = Flask(__name__)
SECRET = os.environ.get("WAPI_WEBHOOK_SECRET", "")

@app.route("/whatsapp-webhook", methods=["POST"])
def webhook():
    payload = request.get_json()

    if SECRET and payload.get("signature"):
        expected = hmac.new(
            SECRET.encode(), json.dumps(payload).encode(), hashlib.sha256
        ).hexdigest()
        if expected != payload["signature"]:
            return "Unauthorized", 401

    if payload["event"] == "message":
        sender = payload["data"]["from"].replace("@s.whatsapp.net", "")
        body   = payload["data"]["body"]
        print(f"Message from {sender}: {body}")

    return jsonify(ok=True)

if __name__ == "__main__":
    app.run(port=4000)
Ruby (Sinatra)require 'sinatra'
require 'json'
require 'openssl'

SECRET = ENV.fetch('WAPI_WEBHOOK_SECRET', '')

post '/whatsapp-webhook' do
  content_type :json
  payload = JSON.parse(request.body.read)

  if !SECRET.empty? && payload['signature']
    expected = OpenSSL::HMAC.hexdigest('sha256', SECRET,
                 payload.to_json)
    halt 401 unless expected == payload['signature']
  end

  if payload['event'] == 'message'
    sender = payload['data']['from'].sub('@s.whatsapp.net', '')
    body   = payload['data']['body']
    puts "Message from #{sender}: #{body}"
    # trigger auto-reply, save to DB, etc.
  end

  { ok: true }.to_json
end
Go (net/http)package main

import (
    "crypto/hmac"; "crypto/sha256"; "encoding/hex"
    "encoding/json"; "fmt"; "io"
    "net/http"; "os"; "strings"
)

var secret = os.Getenv("WAPI_WEBHOOK_SECRET")

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    body, _ := io.ReadAll(r.Body)
    var payload map[string]any
    json.Unmarshal(body, &payload)

    if secret != "" {
        if sig, ok := payload["signature"].(string); ok {
            mac := hmac.New(sha256.New, []byte(secret))
            mac.Write(body)
            expected := hex.EncodeToString(mac.Sum(nil))
            if expected != sig {
                http.Error(w, "Unauthorized", 401)
                return
            }
        }
    }

    if payload["event"] == "message" {
        data := payload["data"].(map[string]any)
        from := strings.Replace(data["from"].(string), "@s.whatsapp.net", "", 1)
        fmt.Printf("Message from %s: %s\n", from, data["body"])
    }

    w.Header().Set("Content-Type", "application/json")
    w.Write([]byte(`{"ok":true}`))
}

func main() {
    http.HandleFunc("/whatsapp-webhook", webhookHandler)
    http.ListenAndServe(":4000", nil)
}
Java (Spring Boot)import org.springframework.web.bind.annotation.*;
import org.springframework.http.*;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.*;

@RestController
public class WebhookController {

    private final String secret = System.getenv("WAPI_WEBHOOK_SECRET");

    @PostMapping("/whatsapp-webhook")
    public ResponseEntity<Map<String,Boolean>> handle(
            @RequestBody Map<String,Object> payload) throws Exception {

        if (secret != null && payload.containsKey("signature")) {
            String raw      = new com.fasterxml.jackson.databind.ObjectMapper()
                                .writeValueAsString(payload);
            Mac    mac      = Mac.getInstance("HmacSHA256");
            mac.init(new SecretKeySpec(secret.getBytes(), "HmacSHA256"));
            String expected = HexFormat.of().formatHex(mac.doFinal(raw.getBytes()));
            if (!expected.equals(payload.get("signature")))
                return ResponseEntity.status(401).build();
        }

        if ("message".equals(payload.get("event"))) {
            var data = (Map<?,?>) payload.get("data");
            String from = data.get("from").toString()
                             .replace("@s.whatsapp.net", "");
            System.out.println("Message from " + from + ": " + data.get("body"));
        }

        return ResponseEntity.ok(Map.of("ok", true));
    }
}
C# (ASP.NET Core)using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);
var app     = builder.Build();
var secret  = Environment.GetEnvironmentVariable("WAPI_WEBHOOK_SECRET") ?? "";

app.MapPost("/whatsapp-webhook", async ([FromBody] JsonElement body) =>
{
    var rawJson    = body.GetRawText();
    var sigPresent = body.TryGetProperty("signature", out var sigEl);

    if (!string.IsNullOrEmpty(secret) && sigPresent) {
        var hash = Convert.ToHexString(
            HMACSHA256.HashData(
                Encoding.UTF8.GetBytes(secret),
                Encoding.UTF8.GetBytes(rawJson)))
            .ToLower();
        if (hash != sigEl.GetString()) return Results.Unauthorized();
    }

    if (body.TryGetProperty("event", out var ev) && ev.GetString() == "message") {
        var data    = body.GetProperty("data");
        var from    = data.GetProperty("from").GetString()
                          ?.Replace("@s.whatsapp.net", "");
        var msgBody = data.GetProperty("body").GetString();
        Console.WriteLine($"Message from {from}: {msgBody}");
    }

    return Results.Ok(new { ok = true });
});

app.Run("http://0.0.0.0:4000");
cURL (test)# Simulate an incoming webhook to test your endpoint locally
curl -s -X POST http://localhost:4000/whatsapp-webhook \
  -H "Content-Type: application/json" \
  -d '{
    "tenantId":  "my-business-a1b2c3",
    "event":     "message",
    "sessionId": "main",
    "timestamp": 1708243200000,
    "data": {
      "id":      "3EB0B430A8B7F6C1D2E3",
      "from":    "919876543210@s.whatsapp.net",
      "body":    "Hello! I need help with my order.",
      "type":    "text",
      "isGroup": false,
      "hasMedia": false
    }
  }'