Skip to content

WhatsApp Cloud API ingress: webhook + dispatcher + outbound#5

Merged
lezama merged 1 commit into
mainfrom
feat/whatsapp-cloud-adapter
May 8, 2026
Merged

WhatsApp Cloud API ingress: webhook + dispatcher + outbound#5
lezama merged 1 commit into
mainfrom
feat/whatsapp-cloud-adapter

Conversation

@lezama
Copy link
Copy Markdown
Owner

@lezama lezama commented May 7, 2026

Summary

Adds WhatsApp as a fourth chat surface alongside the block, the openclawp/chat ability, and the REST endpoint. Same OpenclaWP_Runner::run_turn underneath; nothing new at the agent / store / loop layers.

  • New OpenclaWP_Whatsapp class. Registers two WP REST routes under /openclawp/v1/whatsapp/webhook:
    • GET — Meta's verification challenge (returns hub.challenge only when hub.verify_token matches).
    • POST — inbound webhook. Verifies X-Hub-Signature-256 (HMAC-SHA256 of the raw body using the App Secret), parses entry[].changes[].value.messages[], dispatches each text message to the configured agent, posts the reply back via the Graph API.
  • Settings page at wp-admin → openclaWP → WhatsApp: Phone Number ID, App Secret, Permanent Access Token, Webhook Verify Token, default agent, owner user ID, API version.
  • Per-phone session continuity: each phone number gets its own persistent session, tagged on the openclawp_session post via _openclawp_whatsapp_phone meta. New inbound messages from the same phone resume the same conversation.
  • Idempotency: each Meta messages[].id is recorded in a 7-day transient so re-deliveries don't double-process.
  • Off by default. Opt in with add_filter( 'openclawp_register_whatsapp', '__return_true' ).

Verified end-to-end (without touching real Meta)

Live test against the Studio install with the adapter enabled and bogus credentials:

PAYLOAD='{"object":"whatsapp_business_account","entry":[{"id":"123","changes":[{"field":"messages","value":{"messages":[{"from":"15555550100","id":"wamid.smoke1","type":"text","text":{"body":"what time is it?"}}]}}]}]}'
SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac 'test-secret' -hex | awk '{print $NF}')
# … POST with X-Hub-Signature-256: sha256=$SIG to /openclawp/v1/whatsapp/webhook

Result (with outbound POST stubbed to assert what would have been sent to graph.facebook.com):

STATUS: 200
DATA:   {"received":true,"processed":1}

OUTBOUND-TO-META url=https://graph.facebook.com/v20.0/FAKE_PNID/messages
  body: {"messaging_product":"whatsapp","recipient_type":"individual","to":"15555550100",
         "type":"text","text":{"body":"The current time is 7:01 PM UTC on May 7, 2026."}}

[openclawp] event=tool_call payload={"turn":1,"tool_name":"openclawp__get-time",...}
[openclawp] event=tool_result payload={"turn":1,"tool_name":"openclawp__get-time","success":true}
[openclawp] event=completed  payload={"turn":2,"message_count":4,"tool_results":1}

The full multi-turn loop ran (tool call + tool result + final synthesis), the reply ended up correctly addressed to the inbound phone, signed bodies pass and unsigned/tampered bodies 401.

Path Result
GET verification with correct token 200, echoes hub.challenge
GET verification with wrong token 403
POST inbound, valid signature, text message 200, dispatched, outbound built
POST inbound, invalid signature 401
POST inbound, non-text (image / status) 200, ack'd, processed=0

Smoke tests (now 22 / 22)

Six new assertions in tests/smoke.php:

  • verify_signature accepts a correctly-signed body
  • verify_signature rejects a tampered body
  • verify_signature rejects when secret is empty
  • extract_messages pulls one text message from a nested envelope
  • extract_messages returns the right phone
  • extract_messages returns the right text

Test plan

  • vendor/bin/phpunit — 6/6
  • studio wp eval 'require "tests/smoke.php";' — 22/22 (6 new for WhatsApp)
  • Enable the filter, configure dummy credentials, simulate an inbound POST per docs/whatsapp-setup.md "End-to-end smoke without Meta"
  • (Optional, requires Meta Developer account) Configure a real Meta app per the setup docs, send a real WhatsApp message to your test number, get a real agent reply back

What's not in this PR

  • Media inbound (images, audio, documents) — text only for v1
  • Multi-tenant phone-to-WP-user mapping — v1 routes all conversations to one configured Owner user
  • Outbound templates / interactive messages — plain text replies only
  • Status receipts (delivered / read) processing — ack'd 200 and dropped

These are tracked as future work; the current shape is the minimum that lets a real WhatsApp number talk to a real agent end-to-end.

Documentation

docs/whatsapp-setup.md — agent runbook for the Meta-side setup (Developer app, system user token, App Secret, Webhook Configuration, ngrok / cloudflared tunneling for localhost), step-by-step with verification commands.

Quality gate results

Gate Result
php -l (touched files)
vendor/bin/phpunit (6 unit tests)
tests/smoke.php (22 integration assertions, 6 new)
Live end-to-end via simulated Meta payload + stubbed outbound

@lezama lezama force-pushed the feat/whatsapp-cloud-adapter branch from 51a8e0c to 4c857b1 Compare May 8, 2026 13:21
@lezama lezama marked this pull request as ready for review May 8, 2026 13:21
@lezama lezama merged commit 8fc25c7 into main May 8, 2026
@lezama lezama deleted the feat/whatsapp-cloud-adapter branch May 8, 2026 13:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant