The WhatsApp Media API is a two-step process: upload the file to get a media_id, then send the message referencing that media_id. Both steps can fail silently. This article covers the complete upload-and-send flow, MIME type requirements, delivery status callbacks, and the silent success trap.
Step 1: Upload to the Media Endpoint
function upload_to_meta(string $filePath, string $mimeType): string
{
$phoneNumberId = META_PHONE_NUMBER_ID;
$url = "https://graph.facebook.com/v19.0/{$phoneNumberId}/media";
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => [
'file' => new \CURLFile($filePath, $mimeType, basename($filePath)),
'messaging_product' => 'whatsapp',
'type' => $mimeType,
],
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . META_ACCESS_TOKEN],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
$error = json_decode($response, true)['error'] ?? ['message' => 'Unknown error'];
throw new \RuntimeException("Media upload failed [{$httpCode}]: " . ($error['message'] ?? ''));
}
$data = json_decode($response, true);
if (empty($data['id'])) {
throw new \RuntimeException("Media upload returned no ID: {$response}");
}
return $data['id']; // The media_id to use in Step 2
}
Step 2: Send the Media Message
function send_whatsapp_audio(string $recipientNumber, string $mediaId): array
{
$phoneNumberId = META_PHONE_NUMBER_ID;
$url = "https://graph.facebook.com/v19.0/{$phoneNumberId}/messages";
$payload = json_encode([
'messaging_product' => 'whatsapp',
'to' => $recipientNumber,
'type' => 'audio',
'audio' => ['id' => $mediaId],
]);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . META_ACCESS_TOKEN,
'Content-Type: application/json',
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
throw new \RuntimeException("Send failed [{$httpCode}]: {$response}");
}
return json_decode($response, true);
// Returns: {"messaging_product":"whatsapp","contacts":[...],"messages":[{"id":"wamid.xxx"}]}
}
MIME Type Requirements
| Format | MIME type for upload | WhatsApp supported? |
|---|---|---|
| OGG/OPUS | audio/ogg; codecs=opus | ✅ Yes (voice notes) |
| MP4 audio | audio/mp4 | ✅ Yes (audio messages) |
| MP3 | audio/mpeg | ✅ Yes (audio messages) |
| WAV | audio/wav | ❌ No |
| OGG (wrong MIME) | audio/ogg (without codecs) | ⚠️ Sometimes — unreliable |
The Silent Success Trap
Step 2 returns 200 with a wamid. This means "sent to Meta's servers" — not "delivered to the recipient's device." Meta then attempts delivery and reports the result via a status webhook callback. Without processing those callbacks, you have no way to know if the audio reached the user.
What to Watch For
- Error code 131053 — "Media upload error: file size exceeds limit." WhatsApp audio limit is 16MB. Check file size before uploading.
- Media ID expiry — A media_id is valid for 30 days. Don't cache media_ids for reuse beyond this window.
- Reading curl response bodies on non-200 status — Always read
$responseon failure. The error body contains the specific Meta error code and message, which is essential for debugging.