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
// 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
// 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
Disconnect a session
Send Message
| 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.
| 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
{
"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.
{
"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
}
}'