Audio pipeline failures are multi-step and each step fails differently. Debugging without a systematic approach means guessing. This article gives a step-by-step approach to isolating failures: test each service independently, use Meta delivery status as ground truth, and decode error codes correctly.
The Systematic Approach
Test each step independently before concluding that the pipeline as a whole is broken. A "voice note not received" complaint could mean any of: TTS failed, conversion failed, upload failed, send failed, or Meta failed to deliver to the device.
# Step 1: Test Kokoro TTS directly
curl -s -X POST http://localhost:9010/v1/audio/speech \
-H "Content-Type: application/json" \
-d '{"model":"kokoro","input":"Test message","voice":"af_heart"}' \
-o /tmp/test_tts.wav
file /tmp/test_tts.wav # Should show RIFF/WAVE — not empty file
# Step 2: Test FFmpeg conversion
ffmpeg -y -i /tmp/test_tts.wav -c:a libopus -b:a 48k -f ogg /tmp/test.ogg
file /tmp/test.ogg # Should show Ogg data, Opus audio
# Step 3: Test Meta upload
curl -s -X POST "https://graph.facebook.com/v19.0/${PHONE_ID}/media" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-F "file=@/tmp/test.ogg;type=audio/ogg; codecs=opus" \
-F "messaging_product=whatsapp"
# Should return {"id":"..."} — note the id for step 4
# Step 4: Test send
curl -s -X POST "https://graph.facebook.com/v19.0/${PHONE_ID}/messages" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"messaging_product":"whatsapp","to":"'${TEST_NUMBER}'","type":"audio","audio":{"id":"'${MEDIA_ID}'"}}'
# Returns wamid — use this in step 5
# Step 5: Check delivery status in webhook (wait for callback)
# Or query: GET /v19.0/{wamid} (note: not all Meta versions support this)
Meta Delivery Status as Ground Truth
The only way to know if a message was delivered is to process the delivery status webhook. Log every status transition for every message:
// Status values: sent, delivered, read, failed
function handle_status_update(array $status, PDO $pdo): void
{
$pdo->prepare(
'INSERT INTO message_delivery_log (wa_msg_id, status, error_code, error_title, logged_at)
VALUES (?, ?, ?, ?, NOW())
ON DUPLICATE KEY UPDATE status = VALUES(status), logged_at = NOW()'
)->execute([
$status['id'],
$status['status'],
$status['errors'][0]['code'] ?? null,
$status['errors'][0]['title'] ?? null,
]);
}
Common Meta Error Codes
| Code | Meaning | Fix |
|---|---|---|
| 131053 | File size exceeds limit (16MB for audio) | Reduce bitrate or split audio |
| 131047 | Re-engagement message required | User must send a message in the last 24h first |
| 131026 | Message undeliverable — number not on WhatsApp | Verify phone number registration |
| 131000 | Something went wrong (generic) | Check payload format, retry once |
| 130429 | Rate limit hit | Implement exponential backoff |
What to Watch For
- Status updates arrive as separate webhooks — Not in the same webhook as the original message. Make sure your webhook handler processes both
messagesandstatusesarrays. - Testing against real numbers — Meta's test number is limited. Test against real WhatsApp numbers (your own test phone) to get realistic delivery status callbacks.
- The 24-hour window — Meta only allows free-form messages within 24 hours of the user's last message. Outside that window, you need a pre-approved template message. Audio messages are free-form — they require an active conversation window.