Skip to main content
Version: 1.0

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.

info

Callback URL will be called with HTTP's POST method with JSON (applicaion/json) content type.

warning

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:

  • submitted
  • sent
  • delivered
  • read: (if the recipient's read receipt is activated)
  • failed
info

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: failed or submitted > failed
Example of JSON of Callback request
{
"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.

Example of JSON of Callback request
{
"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

warning

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

app.py
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")