Series 9 — Part 3 of 5

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

CodeMeaningFix
131053File size exceeds limit (16MB for audio)Reduce bitrate or split audio
131047Re-engagement message requiredUser must send a message in the last 24h first
131026Message undeliverable — number not on WhatsAppVerify phone number registration
131000Something went wrong (generic)Check payload format, retry once
130429Rate limit hitImplement 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 messages and statuses arrays.
  • 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.