WhatsApp Group Listener

Receives WhatsApp Business Cloud API webhooks, picks out group messages, translates them to English (from Hebrew by default), summarizes them, and stores every record in SQLite. Results are viewable at the /status endpoint.

The listener lives at:

  • Live endpoint: [url:main.ts] (webhook root)
  • Recent messages: [url:main.ts/status]

How it works

Rendering mermaid diagram...
  1. Meta sends a GET verification handshake containing hub.challenge — we echo it back if the hub.verify_token matches.
  2. Meta sends POST notification events. We extract text messages that carry a group_id, translate each, summarize it, and store it.
  3. GET /status returns the latest processed messages as JSON.

Routes

MethodPathPurpose
GET/Meta webhook verification handshake (hub.* query params)
POST/Receive webhook events (translate + summarize + store)
GET/statusView recent processed messages as JSON
POST/testSimulate a group-message webhook to test the pipeline
POST/digestEmail outstanding messages now (on-demand digest trigger)

Daily digest

In addition to storing messages, a scheduled job emails you a digest of everything new since the last run. New messages are marked "digested" once included, so nothing is double-sent.

  • Schedule: 0 5,12 * * * — 05:00 and 12:00 UTC (08:00 and 15:00 UTC+3).
  • Job file: jobs/digest.ts (interval type); logic lives in lib/digest.ts.
  • Trigger on demand: curl -X POST https://whatsapp-listener.val.run/digest
  • Recipient: defaults to the val owner's address; set DIGEST_EMAIL to override.

Setup

1. Configure env vars

Required before Meta can talk to us:

  • WA_VERIFY_TOKEN — a secret string. Set the same value in Meta's App Dashboard so webhook verification passes.

Optional:

  • SOURCE_LANGUAGE — source language code. Defaults to he (Hebrew) since that's the expected group language. Override (e.g. es, nl) for other groups, or leave unset to auto-detect each message with franc.
  • TARGET_LANGUAGE — target language, default en.
  • MYMEMORY_EMAIL — your email; raises MyMemory's free translation quota from ~5K to ~50K chars/day.
  • SUMMARY_MAX_SENTENCES — extractive summary length, default 3.
  • DIGEST_EMAIL — digest recipient. If unset, mail goes to the val owner's address.
  • LLM_ENDPOINT / LLM_API_KEY / LLM_MODELoptional; if all three are set, use an OpenAI-compatible chat-completions endpoint for higher-quality abstractive summaries (falls back to extractive otherwise).

Key env var URLs (set these in the Val Town UI):

👉 Add WA_VERIFY_TOKEN here: https://www.val.town/x/kineticalrisk/whatsapp-listener/environment-variables?key=WA_VERIFY_TOKEN 👉 Add SOURCE_LANGUAGE here: https://www.val.town/x/kineticalrisk/whatsapp-listener/environment-variables?key=SOURCE_LANGUAGE 👉 Add DIGEST_EMAIL here: https://www.val.town/x/kineticalrisk/whatsapp-listener/environment-variables?key=DIGEST_EMAIL 👉 Add MYMEMORY_EMAIL here: https://www.val.town/x/kineticalrisk/whatsapp-listener/environment-variables?key=MYMEMORY_EMAIL

2. Test the pipeline

curl -X POST https://whatsapp-listener.val.run/test

Then open https://whatsapp-listener.val.run/status to see the processed message.

3. Register the webhook with Meta

  1. Create a WhatsApp Business account and number (Business Manager → WhatsApp).
  2. In the Meta App Dashboard, add the WhatsApp product and the whatsapp_business_messaging permission.
  3. Under WhatsApp → Configuration, set:
    • Callback URL: https://whatsapp-listener.val.run/
    • Verify token: the same value as WA_VERIFY_TOKEN
  4. Click Verify and save — Meta calls your endpoint with the handshake.
  5. Subscribe to the messages webhook field.
  6. Make sure your business number is a participant of the group whose messages you want to listen to. Test by sending a message in that group.

Notes & limitations

  • Only group messages are processed (messages with a group_id); 1:1 chats are ignored.
  • The official API only surfaces messages while your business number is in the group — there's no way to read arbitrary personal groups.
  • Media messages (images/videos/audio) are stored as [Image] / [Video] / [Audio] placeholders.
  • Translation uses MyMemory (free tier). It's fine for low-volume groups; for heavy volume add MYMEMORY_EMAIL or an LLM key.