Skip to main content

Overview

MileApp delivers OTP and automation notifications over WhatsApp using the Meta WhatsApp Cloud API. OTP is charged from the organization’s OTP credit balance per destination country, and credit is deducted only after Meta accepts the send (deduct-on-accept). Legacy Fonnte automations remain supported for backward compatibility.

OTP via WhatsApp

OTP is configured on a Flow OTP component with serviceSender = whatsapp. The recipient phone number is normalized to E.164 and the destination country determines the charge.
EndpointDescription
POST /api/v3/task/{taskId}/otp/sendSend an OTP to the component recipient over WhatsApp. Charged per-country (rates.countries[ISO].whatsapp), deduct-on-accept.
POST /api/v3/task/{taskId}/otp/verifyVerify the OTP code entered by the user.
Send — notable responses
CodeMeaning
200OTP queued/sent (SendWhatsappJob on queue whatsapp).
400 otp-invalid-recipient-061Recipient not a valid E.164 number.
400 otp-insufficient-balance-060Org OTP credit balance is insufficient.
Delivery is asynchronous via the whatsapp queue. Credit is deducted only when Meta accepts the message; a rejection charges nothing.

Branding sender (internal-managed)

Each organization may have at most one branding WhatsApp number (managed internally). When absent, OTP uses the global official MileApp number.
EndpointDescription
GET /api/v3/whatsapp/sendersList branding senders.
POST /api/v3/whatsapp/sendersCreate a branding sender (max 1/org).
PUT /api/v3/whatsapp/senders/{id}Update a branding sender.
DELETE /api/v3/whatsapp/senders/{id}Remove a branding sender.

OTP rates & balance (billing)

EndpointDescription
GET /api/v1/id/otp-ratesList per-country OTP rates (includes whatsapp).
PUT /api/v1/id/otp-rates/{code}Set per-country rate, e.g. { "whatsapp": 5 } (internal).
GET /api/v1/id/balanceReturns data.rates = { email, countries: { ISO: { name, sms, whatsapp } } }.
The balance rates shape is channel-generic (countries map). The previous smsByCountry key is removed.

Delivery webhook (Meta → MileApp)

Message status (sent/delivered/read/failed) is received via webhook and logged to Elasticsearch ({ELASTIC_INDEX}.whatsapp.{year}) for monitoring. failed is log-only (no refund).
EndpointDescription
GET /api/v3/whatsapp/webhookVerification handshake — echoes hub.challenge when hub.verify_token matches.
POST /api/v3/whatsapp/webhookStatus events — verified via X-Hub-Signature-256 (HMAC-SHA256 with App Secret).
POST payload (entry[].changes[].value.statuses[]): id (wamid), status, recipient_id, timestamp, conversation, pricing, errors[] (on failed).
The webhook is a Meta-dictated contract: GET returns the raw hub.challenge as plain text (not the standard JSON envelope); POST returns a status code only.

Configuration (environment)

The mile-v3-api service reads these from config:
VariablePurpose
META_WA_OFFICIAL_PHONE_NUMBER_IDPhone Number ID of the official MileApp number (from Meta API Setup).
META_WA_OFFICIAL_ACCESS_TOKENBearer token used to send messages.
META_WA_OTP_TEMPLATE / _LANGApproved authentication template name + language.
META_WA_DRIVERlive (call Meta) or mock (return accepted without Meta — testing).
META_WA_WEBHOOK_VERIFY_TOKENArbitrary string for webhook GET handshake (must match Meta webhook config).
META_WA_APP_SECRETMeta App Secret for webhook signature verification.
META_WA_WEBHOOK_VERIFY_TOKEN, META_WA_OFFICIAL_ACCESS_TOKEN, and META_WA_APP_SECRET are three distinct secrets — do not reuse one for another. A WhatsApp queue worker must be running to process sends.