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.
-
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.
-
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 StartSet both or neither. One without the other stops the boot. With both empty the channel is not registered and
/webhooks/whatsappanswers404. -
Point Meta at the webhook. WhatsApp → Configuration → Callback URL:
https://<your-herald-host>/webhooks/whatsappUse the same verify token and subscribe to the
messagesfield. Meta sends aGETwithhub.mode,hub.verify_tokenandhub.challenge; Herald returns the challenge only when the token matches, and403otherwise.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 is401and never reaches an agent. -
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>"}}'201creates the account.200means it already existed for that tenant and number, and its credentials were refreshed, so the same call is safe to repeat. -
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.
What arrives
Section titled “What arrives”| Message type | Reaches the agent as |
|---|---|
| Text | The text |
| Image, audio, video, document | The 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.
Sending
Section titled “Sending”| Send | Delivery |
|---|---|
| Text | Graph messages, type text |
| Media | By link: image, audio, video or document with a public URL and an optional caption. One attachment per message |
| Template | Graph 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.
The 24-hour window
Section titled “The 24-hour window”Meta only lets a business send free text to someone who wrote within the last 24 hours.
| Send | Works when |
|---|---|
| An agent’s reply | Always: a reply is inside the window by definition |
| A proactive text or media | The person wrote in the last 24 hours |
| A cold contact | Only as a template approved in Meta’s dashboard |
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.
Several numbers per tenant
Section titled “Several numbers per tenant”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.
The emulator
Section titled “The emulator”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_NUMBERtakes one id or a comma-separated list. No database write is needed. - The account says so.
"emulated": truewhen 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.
# A person writes to the emulated linecurl -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.
Configuration
Section titled “Configuration”| Flag | Environment | Default |
|---|---|---|
--whatsapp-app-secret | WHATSAPP_APP_SECRET | empty |
--whatsapp-verify-token | WHATSAPP_VERIFY_TOKEN | empty |
--whatsapp-graph-base-url | WHATSAPP_GRAPH_BASE_URL | https://graph.facebook.com/v21.0 |
--whatsapp-emulated-number | HERALD_WHATSAPP_EMULATED_NUMBER | empty (emulator off) |
Next steps
Section titled “Next steps”- Building Agents for Herald — what the agent receives
- Webhook Forwarding — send a number’s traffic to your own service
- WhatsApp on a standalone node — the runtime’s own adapter, for deployments without Nexus