Series 9 — Audio & Media Pipeline Engineering • Part 1 of 5

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

FormatMIME type for uploadWhatsApp supported?
OGG/OPUSaudio/ogg; codecs=opus✅ Yes (voice notes)
MP4 audioaudio/mp4✅ Yes (audio messages)
MP3audio/mpeg✅ Yes (audio messages)
WAVaudio/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 $response on failure. The error body contains the specific Meta error code and message, which is essential for debugging.