Skip to content

WhatsApp

One Herald deployment serves one Meta app. The app secret and the verify token are process-level settings. Each tenant’s phone number is a channel account with its own access token.

  1. Configure the Meta app. At developers.facebook.com, add the WhatsApp product. Note the App secret (App settings → Basic) and invent a verify token, any random string, which you paste into both Meta and Herald.

  2. Start Herald with the channel on.

    Terminal window
    WHATSAPP_APP_SECRET="<app secret>" \
    WHATSAPP_VERIFY_TOKEN="<your invented token>" \
    ./bin/herald # plus the required settings from the Quick Start

    Set both or neither. One without the other stops the boot. With both empty the channel is not registered and /webhooks/whatsapp answers 404.

  3. Point Meta at the webhook. WhatsApp → Configuration → Callback URL:

    https://<your-herald-host>/webhooks/whatsapp

    Use the same verify token and subscribe to the messages field. Meta sends a GET with hub.mode, hub.verify_token and hub.challenge; Herald returns the challenge only when the token matches, and 403 otherwise.

    Every delivery after that is checked against X-Hub-Signature-256, an HMAC-SHA256 of the raw body keyed with the app secret. A request that fails it is 401 and never reaches an agent.

  4. Register the tenant’s phone number.

    Terminal window
    curl -X POST localhost:8081/admin/accounts \
    -H "X-Admin-Token: $HERALD_ADMIN_TOKEN" -H "Content-Type: application/json" \
    -d '{"tenant_id":"tenant-1","channel":"whatsapp",
    "external_id":"<phone_number_id from Meta>",
    "credentials":{"access_token":"<permanent access token>"}}'

    201 creates the account. 200 means it already existed for that tenant and number, and its credentials were refreshed, so the same call is safe to repeat.

  5. Bind it to an entry agent.

    Terminal window
    curl -X POST localhost:8081/admin/bindings \
    -H "X-Admin-Token: $HERALD_ADMIN_TOKEN" -H "Content-Type: application/json" \
    -d '{"tenant_id":"tenant-1","channel":"whatsapp","account_id":"<account id>",
    "entry_agent":"router","fallback_message":"Estamos con problemas, volvé a escribir en un rato",
    "config":{"nexus_api_key":"<tenant Nexus API key>"}}'

    Herald checks the agent against Nexus with that key before saving. An agent that does not exist, or that the key cannot reach, is 422.

Message typeReaches the agent as
TextThe text
Image, audio, video, documentThe caption, or a bracketed description when there is none. The attachment is kept as a reference for forwarding
Anything else (location, sticker, contacts, reactions…)Ignored
Status callbacks (sent, delivered, read)Acknowledged, never processed

The sender’s profile name arrives as contact_name, and sender_phone is the sender itself.

Media arrives as a Meta media id. Meta’s download URLs expire in minutes, so Herald only downloads the bytes when a forwarding binding needs them, right before the POST, up to 8 MiB. See Webhook Forwarding.

SendDelivery
TextGraph messages, type text
MediaBy link: image, audio, video or document with a public URL and an optional caption. One attachment per message
TemplateGraph template with the language code and the body parameters {{1}}, {{2}}… in order

Graph’s base URL defaults to https://graph.facebook.com/v21.0 and can be overridden with WHATSAPP_GRAPH_BASE_URL when Meta retires a version.

Meta only lets a business send free text to someone who wrote within the last 24 hours.

SendWorks when
An agent’s replyAlways: a reply is inside the window by definition
A proactive text or mediaThe person wrote in the last 24 hours
A cold contactOnly as a template approved in Meta’s dashboard
Terminal window
curl -X POST localhost:8081/api/v1/messages/send \
-H "X-Herald-Key: <tenant key>" -H "Content-Type: application/json" \
-d '{"channel":"whatsapp","recipient":"5491100000001",
"template":{"name":"recordatorio_pago","language":"es_AR","params":["Ana","15/10"]}}'

Templates exist only on WhatsApp. A template queued for another channel fails permanently.

A tenant can have more than one WhatsApp number. Each is its own account with its own binding. For a proactive send, pass account_id to choose which number it leaves from. Without it, Herald uses the tenant’s oldest WhatsApp account, which is rarely what a tenant with several lines wants.

An emulated line behaves like a real WhatsApp account except that nothing reaches Meta. Its sends are recorded in memory and reported as delivered, and inbound messages can be injected through the operator plane. It runs the real WhatsApp adapter, templates included, so a product flow can be tested end to end before a Meta account exists.

A line is emulated in one of two ways:

  • The configuration names it. HERALD_WHATSAPP_EMULATED_NUMBER takes one id or a comma-separated list. No database write is needed.
  • The account says so. "emulated": true when creating the account. Repeating the call with a different value updates it.

Either way, the per-account flag only counts when the environment has the emulator on (at least one configured number). A row marked emulated that is restored into production therefore sends for real instead of swallowing customer messages.

Terminal window
# A person writes to the emulated line
curl -X POST localhost:8081/admin/emulator/inbound \
-H "X-Admin-Token: $HERALD_ADMIN_TOKEN" -H "Content-Type: application/json" \
-d '{"cuenta":"<emulated phone_number_id>","desde":"5491100000001","texto":"hola","nombre":"Ana"}'
# What the line "sent" back (last 5, this line only)
curl "localhost:8081/admin/emulator/outbox?cuenta=<emulated phone_number_id>&ultimos=5" \
-H "X-Admin-Token: $HERALD_ADMIN_TOKEN"

The record holds the last 500 sends and is lost on a restart. It is for checking what an agent answered, not a log. Always filter by cuenta when more than one line is emulated: the record is shared, and the last message without a filter belongs to whichever line answered last.

FlagEnvironmentDefault
--whatsapp-app-secretWHATSAPP_APP_SECRETempty
--whatsapp-verify-tokenWHATSAPP_VERIFY_TOKENempty
--whatsapp-graph-base-urlWHATSAPP_GRAPH_BASE_URLhttps://graph.facebook.com/v21.0
--whatsapp-emulated-numberHERALD_WHATSAPP_EMULATED_NUMBERempty (emulator off)