Whatsapp Business Webhook
Introduction
Delivery status and Inbox data can be retrieved by setting your Callback URL.
Callback URL setting can be set in Account > Account Settings > WhatsApp Business Webhook menu on app.nusasms.com.
Callback URL will be called with HTTP's POST method with JSON (applicaion/json) content type.
Callback data will be sent to callback URL max 3 times, if the callback fails (not responding or give non 2XX HTTP response).
The callback call also stops after receiving success response (with HTTP status code 2XX).
Message Delivery Status
Delivery status is for the change of your status update of the message sent by your Gateway.
The avaiable status:
submittedsentdeliveredread: (if the recipient's read receipt is activated)failed
Delivery status callback might be sent in un-ordered sequence.
This usually happen because of some status updates is collected in very little delay time in our system.
Example: read sent before delivered
Please pay attention to the ideal status sequence order:
- Success:
submitted>sent>delivered>read - Fail:
failedorsubmitted>failed
{
"type": "status",
"status": {
"message_id": "MESSAGE_ID",
"status": "read",
"timestamp": 1766125960,
"gateway_id": "GATEWAY_ID",
"recipient": "628xxxxxxx"
}
}
Inbox
After setting your Whatsapp Business Webhook, you may receive messages that is sent to your Gateway.
The message payload is the data that has been received by the gateway.
{
"type": "inbox",
"inbox": {
"message_id": "MESSAGE_ID",
"timestamp": "1766124507",
"gateway": {
"id": "GATEWAY_ID",
"number": "628xxxxxxx"
},
"sender": {
"number": "628yyyyyyyyy",
"name": "Nama Akun Whatsapp Sender"
},
"message": {
"type": "text",
"text": {
"body": "Teks inbox"
}
}
}
}
Webhook Service Example
The code examples below serve as a reference implementation, not a complete webhook data retrieval service. Use them as a foundation to extend and customize for your specific requirements.
Golang
This is a simple Go web application example to retrieve the webhook data using net/http.
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"time"
)
// Gateway represents the gateway information in the inbox payload
type Gateway struct {
ID string `json:"id"`
Number string `json:"number"`
}
// Sender represents the sender information in the inbox payload
type Sender struct {
Number string `json:"number"`
Name string `json:"name"`
}
// MessageText represents the text message content
type MessageText struct {
Body string `json:"body"`
}
// Message represents the message details in the inbox payload
type Message struct {
Type string `json:"type"`
Text MessageText `json:"text,omitempty"`
}
// Status represents the nested status object in the status payload
type Status struct {
MessageID string `json:"message_id"`
Status string `json:"status"`
Timestamp string `json:"timestamp"`
GatewayID string `json:"gateway_id,omitempty"`
Gateway *Gateway `json:"gateway,omitempty"`
Recipient string `json:"recipient,omitempty"`
}
// Inbox represents the inbox payload structure
type Inbox struct {
MessageID string `json:"message_id"`
Timestamp string `json:"timestamp"`
Gateway Gateway `json:"gateway"`
Sender Sender `json:"sender"`
Message Message `json:"message"`
}
// WebhookPayload represents the full JSON structure
type WebhookPayload struct {
Type string `json:"type"`
Status *Status `json:"status,omitempty"`
Inbox *Inbox `json:"inbox,omitempty"`
}
func handleWebhook(w http.ResponseWriter, r *http.Request) {
// Ensure only POST requests are accepted
if r.Method != http.MethodPost {
http.Error(w, "Only POST method is allowed", http.StatusMethodNotAllowed)
return
}
// Limit request body size to prevent potential DoS
r.Body = http.MaxBytesReader(w, r.Body, 1048576) // 1MB max
// Read the request body
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Error reading request body", http.StatusBadRequest)
return
}
defer r.Body.Close()
// Parse the JSON
var payload WebhookPayload
err = json.Unmarshal(body, &payload)
if err != nil {
http.Error(w, fmt.Sprintf("Error parsing JSON: %v", err), http.StatusBadRequest)
return
}
// Process the webhook based on type
processWebhook(payload)
// Respond with success
w.WriteHeader(http.StatusOK)
w.Write([]byte("Webhook received successfully"))
}
func processWebhook(payload WebhookPayload) {
fmt.Printf("Received Webhook Type: %s\n", payload.Type)
switch payload.Type {
case "status":
processStatusWebhook(payload.Status)
case "inbox":
processInboxWebhook(payload.Inbox)
default:
fmt.Printf("Unknown webhook type: %s\n", payload.Type)
}
}
func processStatusWebhook(status *Status) {
// Process the data
return nil
}
func processInboxWebhook(inbox *Inbox) {
// Process the data
return nil
}
func main() {
// Set up routes
http.HandleFunc("/webhook", handleWebhook)
// Start the server
port := 8080
fmt.Printf("Server starting on port %d...\n", port)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil))
}
PHP
header('Content-Type: application/json');
// Read raw POST body
$raw = file_get_contents('php://input');
$data = json_decode($raw, true);
if (json_last_error() !== JSON_ERROR_NONE) {
http_response_code(400);
echo json_encode(['error' => 'Invalid JSON']);
exit;
}
// Helper to send a JSON response
function respond(array $payload, int $code = 200): void
{
http_response_code($code);
echo json_encode($payload);
exit;
}
// Status payload
if (isset($data['type']) && $data['type'] === 'status' && isset($data['status'])) {
$status = $data['status'];
// Basic validation (you can expand as needed)
$required = ['message_id', 'status', 'timestamp', 'gateway_id', 'recipient'];
foreach ($required as $field) {
if (!isset($status[$field])) {
respond(['error' => "Missing field $field in status payload"], 400);
}
}
// Process the data here
respond(['result' => 'status processed']);
}
// Inbox payload
if (isset($data['type']) && $data['type'] === 'inbox' && isset($data['inbox'])) {
$inbox = $data['inbox'];
// Required top‑level fields
$requiredTop = ['message_id', 'timestamp', 'gateway', 'sender', 'message'];
foreach ($requiredTop as $field) {
if (!isset($inbox[$field])) {
respond(['error' => "Missing field $field in inbox payload"], 400);
}
}
// Process the data here
respond(['result' => 'inbox processed']);
}
// -----------------------------------------------------------------
// If we reach here, the payload type is unsupported
// -----------------------------------------------------------------
respond(['error' => 'Unsupported payload type'], 400);
?>
Python
Here is some simple Rest API web application example using FastAPI
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
from typing import Literal, Union
app = FastAPI()
class Payload(BaseModel):
type: Literal["inbox", "status"]
inbox: dict | None = None
status: dict | None = None
# ---------- Endpoints ----------
@app.post("/webhook")
async def receive_payload(payload: Payload):
"""
Accepts either a `status` or an `inbox` payload.
"""
if payload.type == 'inbox':
# Process the webhook data here
return {"result": "inbox processed"}
elif payload.type == 'status'
# Process the webhook data here
return {"result": "status processed"}
raise HTTPException(status_code=400, detail="Unsupported payload type")