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.
| Endpoint | Description |
|---|
POST /api/v3/task/{taskId}/otp/send | Send an OTP to the component recipient over WhatsApp. Charged per-country (rates.countries[ISO].whatsapp), deduct-on-accept. |
POST /api/v3/task/{taskId}/otp/verify | Verify the OTP code entered by the user. |
Send — notable responses
| Code | Meaning |
|---|
200 | OTP queued/sent (SendWhatsappJob on queue whatsapp). |
400 otp-invalid-recipient-061 | Recipient not a valid E.164 number. |
400 otp-insufficient-balance-060 | Org 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.
| Endpoint | Description |
|---|
GET /api/v3/whatsapp/senders | List branding senders. |
POST /api/v3/whatsapp/senders | Create 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)
| Endpoint | Description |
|---|
GET /api/v1/id/otp-rates | List 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/balance | Returns data.rates = { email, countries: { ISO: { name, sms, whatsapp } } }. |
The balance rates shape is channel-generic (countries map). The previous smsByCountry key is removed.
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).
| Endpoint | Description |
|---|
GET /api/v3/whatsapp/webhook | Verification handshake — echoes hub.challenge when hub.verify_token matches. |
POST /api/v3/whatsapp/webhook | Status 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:
| Variable | Purpose |
|---|
META_WA_OFFICIAL_PHONE_NUMBER_ID | Phone Number ID of the official MileApp number (from Meta API Setup). |
META_WA_OFFICIAL_ACCESS_TOKEN | Bearer token used to send messages. |
META_WA_OTP_TEMPLATE / _LANG | Approved authentication template name + language. |
META_WA_DRIVER | live (call Meta) or mock (return accepted without Meta — testing). |
META_WA_WEBHOOK_VERIFY_TOKEN | Arbitrary string for webhook GET handshake (must match Meta webhook config). |
META_WA_APP_SECRET | Meta 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.