A developer's guide to receiving and processing real-time message events from the WASenderAPI. This documentation details the JSON payload for the messages.upsert event, covering text messages, media handling, and the required media decryption process.
How to Handle Incoming WhatsApp Messages
When you get a new WhatsApp message, we send a POST
request to your server. This is called a webhook. Inside is a JSON payload with all the message details. This guide shows you how to read it.
The Message Payload
The JSON we send looks like this. Everything you need is inside data.messages
.
{ "data": { "messages": [ { "key": { "remoteJid": "1234567890@s.whatsapp.net", "fromMe": false, "id": "BAE5A93B52084A3B" }, "message": { "conversation": "Hello! This is a test." }, "pushName": "John Doe", "messageTimestamp": 1678886400 } ] } }
Key Things to Look For:
key.remoteJid
: The customer's phone number (with@s.whatsapp.net
at the end).key.fromMe
: Isfalse
if the customer sent it. Istrue
if you sent it.key.id
: The unique ID for this message. Use it to avoid saving the same message twice.pushName
: The customer's WhatsApp name. Useful if you don't know them.message
: This object holds the actual message content. This is the most important part.
Reading the Message Content
Look inside the message
object to see what kind of message it is.
1. Text Messages
The message text can be in two different places. Always check for message.extendedTextMessage.text
first. If it's not there, then check for message.conversation
.
Example: Text in extendedTextMessage
(like a reply or message with a link)
"message": { "extendedTextMessage": { "text": "This is the message text." } }
Example: Text in conversation
(a very simple message)
"message": {
"conversation": "Hello!"
}
2. Media Messages (Images, Videos, etc.)
If it's a media message, the message
object will contain a key like imageMessage
, videoMessage
, or audioMessage
.
"message": { "imageMessage": { "url": "https://some-url-to-the-file.net/...", "mediaKey": "aBcDeFgHiJkLmNoPqRsTuVwXyZ1234567890aBcD=", "mimetype": "image/jpeg" } }
To get the media file, you need two things from here:
url
: The link to download the file.mediaKey
: The key to unlock (decrypt) the file after you download it.
How to Decrypt Media Files
The media files you download are encrypted. You must decrypt them to view them.
Heads Up: This part is tricky and requires programming with cryptography.
Don't worry! We provide a complete code example below that handles everything, including decryption. You can just copy it.
For context, here is the simple process the code follows:
- Download the file from the
url
. - Unlock the file using the
mediaKey
.
Things to Remember
Check if the message has content
Sometimes you'll get a webhook for an empty event (like a "message deleted" notification). If message
doesn't have conversation
, extendedTextMessage
, or imageMessage
etc., you can just ignore it. Our example code handles this.
Get the phone number
Just take the remoteJid
and remove the "@s.whatsapp.net" part to get the simple phone number. Our example code handles this too.
Code Examples
<?php
declare(strict_types=1);
// The directory to save downloaded media files.
// Make sure this directory exists and your web server can write to it.
define('DOWNLOAD_DIR', __DIR__ . '/downloads');
/**
* A simple logging function for demonstration.
* In a real application, you would use a proper logger like Monolog.
*/
function logMessage(string $message): void
{
// Prepend a timestamp to the log message.
$timestamp = date('Y-m-d H:i:s');
// Append the message to a log file.
file_put_contents('webhook.log', "[$timestamp] $message\n", FILE_APPEND);
}
/**
* Finds the first available media object and its type from the message.
* @return array|null [media_object, media_type_string] or null if not found.
*/
function findMediaInfo(array $messageObject): ?array
{
$mediaKeys = [
'imageMessage' => 'image',
'videoMessage' => 'video',
'audioMessage' => 'audio',
'documentMessage' => 'document',
'stickerMessage' => 'sticker',
];
foreach ($mediaKeys as $key => $type) {
if (isset($messageObject[$key])) {
return [$messageObject[$key], $type];
}
}
return null;
}
/**
* Downloads a file from a URL.
* @return string|false The file content or false on failure.
*/
function downloadFile(string $url)
{
$context = stream_context_create(['http' => ['follow_location' => true]]);
return file_get_contents($url, false, $context);
}
/**
* Derives the decryption keys using HKDF.
* @throws Exception If media type is invalid.
*/
function getDecryptionKeys(string $mediaKey, string $mediaType): string
{
// The "info" string is specific to each WhatsApp media type.
$info = match ($mediaType) {
'image', 'sticker' => 'WhatsApp Image Keys',
'video' => 'WhatsApp Video Keys',
'audio' => 'WhatsApp Audio Keys',
'document' => 'WhatsApp Document Keys',
default => throw new Exception("Invalid media type: {$mediaType}"),
};
// Use HKDF to derive a 112-byte expanded key.
return hash_hkdf('sha256', base64_decode($mediaKey), 112, $info, '');
}
/**
* Main function to decrypt and save a media file.
* @throws Exception On failure to download or decrypt.
*/
function handleMediaDecryption(array $mediaInfo, string $mediaType, string $messageId): void
{
$url = $mediaInfo['url'] ?? null;
$mediaKey = $mediaInfo['mediaKey'] ?? null;
if (!$url || !$mediaKey) {
throw new Exception("Media object is missing 'url' or 'mediaKey'.");
}
// 1. Download the encrypted file
$encryptedData = downloadFile($url);
if (!$encryptedData) {
throw new Exception("Failed to download media from URL: {$url}");
}
// 2. Derive the IV and Cipher Key from the mediaKey
$keys = getDecryptionKeys($mediaKey, $mediaType);
$iv = substr($keys, 0, 16);
$cipherKey = substr($keys, 16, 32);
// 3. The actual ciphertext is the file content, minus the last 10 bytes (which are a MAC hash).
$ciphertext = substr($encryptedData, 0, -10);
// 4. Decrypt using AES-256-CBC
$decryptedData = openssl_decrypt($ciphertext, 'aes-256-cbc', $cipherKey, OPENSSL_RAW_DATA, $iv);
if ($decryptedData === false) {
throw new Exception('Failed to decrypt media.');
}
// 5. Save the decrypted file
if (!is_dir(DOWNLOAD_DIR)) {
mkdir(DOWNLOAD_DIR, 0755, true);
}
$mimeType = $mediaInfo['mimetype'] ?? 'application/octet-stream';
$extension = explode('/', $mimeType)[1] ?? 'bin';
$filename = $mediaInfo['fileName'] ?? "{$messageId}.{$extension}";
$outputPath = DOWNLOAD_DIR . '/' . basename($filename); // Use basename to prevent path traversal
file_put_contents($outputPath, $decryptedData);
logMessage("Successfully decrypted and saved media to: {$outputPath}");
}
// --- MAIN WEBHOOK PROCESSING LOGIC ---
// 1. Get the incoming JSON payload from the POST request
$jsonPayload = file_get_contents('php://input');
$payload = json_decode($jsonPayload, true);
// 2. Extract the first message from the payload
$messageData = $payload['data']['messages'][0] ?? null;
if (!$messageData) {
logMessage('Webhook received but no message data found.');
http_response_code(200); // Respond OK to prevent retries
exit();
}
$key = $messageData['key'] ?? [];
$messageId = $key['id'] ?? 'unknown_id';
$remoteJid = $key['remoteJid'] ?? null;
if (!$remoteJid) {
logMessage('Ignoring message with no remoteJid.');
http_response_code(200);
exit();
}
// 3. Extract basic message info
$pushName = !($key['fromMe'] ?? false) ? ($messageData['pushName'] ?? 'Unknown') : 'Me';
$phoneNumber = preg_replace('/@.*/', '', $remoteJid);
$messageContent = $messageData['message']['conversation'] ?? $messageData['message']['extendedTextMessage']['text'] ?? null;
$mediaInfo = findMediaInfo($messageData['message'] ?? []);
// 4. If there's no text and no media, ignore the message
if (empty($messageContent) && !$mediaInfo) {
logMessage("Ignoring event with no content (ID: {$messageId})");
http_response_code(200);
exit();
}
// 5. Process the message
logMessage("Processing message from {$pushName} ({$phoneNumber}). ID: {$messageId}");
if ($messageContent) {
logMessage("Text: {$messageContent}");
// TODO: Save text message to your database here.
}
if ($mediaInfo) {
try {
logMessage("Media found. Type: {$mediaInfo[1]}. Attempting to decrypt...");
handleMediaDecryption($mediaInfo[0], $mediaInfo[1], $messageId);
// TODO: Save media file path to your database here.
} catch (Exception $e) {
logMessage("ERROR processing media for message ID {$messageId}: " . $e->getMessage());
}
}
// 6. Send a 200 OK response to the API
http_response_code(200);
logMessage("--- Finished processing webhook ---");