Live-testing campaign
How WATS gets validated against live Meta assets, and the redacted run log.
active · reviewed 2026-06-10
WATS development is credential-free by default: MockTransport, synthetic webhooks, no Meta calls in CI. A live campaign is the exception — an operator-authorized run against real Meta assets. This page is the runbook plus the redacted execution log.
Scope
Included:
- Verify Meta accepts WATS request shapes for implemented surfaces.
- Verify response shapes against current WATS types.
- Observe webhook/status side effects.
- Record redacted evidence and cleanup state.
Excluded by default:
- Admin mutations beyond explicitly authorized sandbox resources.
- Production WABA/phone/catalog/template/Flow changes.
- Phone registration/deregistration, callback URL overrides, OAuth/token exchange, publish operations.
- Anything not guarded by
WATS_LIVE_ENABLE=1plus a domain-specific opt-in flag for mutations.
Credential inventory
Baseline:
WATS_LIVE_ENABLE=1
WATS_GRAPH_BASE_URL=https://graph.facebook.com
WATS_GRAPH_API_VERSION=v25.0
WATS_ACCESS_TOKEN=<redacted>
WATS_WABA_ID=<redacted>
WATS_PHONE_NUMBER_ID=<redacted>
WATS_TEST_RECIPIENT_E164=<redacted>
WATS_TEST_RUN_ID=<generated-per-run>
WATS_TEST_RESOURCE_PREFIX=wats-live-<run-id>Webhook:
WATS_APP_SECRET=<redacted>
WATS_VERIFY_TOKEN=<redacted>
WATS_PUBLIC_WEBHOOK_URL=<redacted-or-test-url>
WATS_WEBHOOK_PATH=/webhookPer-domain opt-in flags gate mutations: WATS_ENABLE_TEMPLATE_MUTATIONS=1,
WATS_ENABLE_FLOW_MUTATIONS=1, WATS_ENABLE_FLOW_PUBLISH=1,
WATS_ENABLE_CALLING_LIVE=1, WATS_ENABLE_CALL_ACCEPT_WEBRTC=1,
WATS_ENABLE_COMMERCE_MESSAGES=1. Media runs also set fixture paths and
WATS_MAX_UPLOAD_BYTES / WATS_MAX_DOWNLOAD_BYTES caps. Deferred admin
mutations need a separate campaign with their own flags
(WATS_ENABLE_ADMIN_MUTATIONS=1, WATS_ENABLE_DESTRUCTIVE=1).
Rules:
- Secrets stay in env vars or
@wats/configenv-secret refs. - No raw secrets in docs, commits, issues, or chat.
WATS_LIVE_ENABLE=1is necessary but not sufficient for mutations; each domain flag must also be set.
Phase order
Read-only before side-effecting before destructive.
- Authorization and dry run. Confirm explicit operator authorization for the exact assets and phases. Confirm every asset is sandbox/test-safe. Generate the run id and resource prefix. Run local tests and typecheck. Validate config without resolving secret values.
- Read-only discovery.
getWabaInfo,listSubscribedApps,listPhoneNumbers,getPhoneNumberInfo,getPhoneNumberSettings({ includeSipCredentials: false }),getBusinessProfile,getCommerceSettings,listMessageTemplates,getMessageTemplate,listFlows,getFlow,getFlowAssets, and media metadata reads with strictmaxBytes. - Webhook passive validation. Stand up a webhook behind an authorized public URL. Verify the GET challenge, verify a locally signed POST, trigger one inbound test message, confirm normalizer/router/listener behavior. No callback URL overrides.
- Low-impact sends. Text to the test recipient, observe the status webhook, mark-as-read and typing indicators on test messages only, then location/contacts/reactions/interactive bodies and an existing approved template. Catalog/product sends only with test catalog assets.
- Media lifecycle. Upload a small fixture, resolve metadata, download
bytes with
maxBytesand integrity checks, send by media id, delete only media created by this run, record cleanup in the manifest. - Template mutations (flag-gated). Create a uniquely prefixed template, list/get/update it within Meta's review rules, delete only what this run created. Never touch production templates.
- Flow draft management (flag-gated). Create/read/update a uniquely prefixed draft. Publish only with the publish flag and explicit acceptance of the state-transition risk. Delete only this run's drafts; treat deprecate as irreversible.
Calling operator facts from Meta docs before any live attempt:
- Supported media/signaling modes are Graph APIs + webhooks + WebRTC, SIP + WebRTC, and SIP + SDES SRTP.
- SDP examples include OPUS and G.711 codecs (
PCMA,PCMU); WATS validates and forwards SDP but does not negotiate media. - Business-initiated calling is not available for numbers from USA, Canada, Egypt, Vietnam, Nigeria.
- Production access is platform-gated: App Review, WhatsApp Business Messaging permission, and calling eligibility, normally including a 2,000 daily messaging limit.
- Sandbox flows are limited and may require Tech Partner access.
- WATS has no SIP server implementation and cannot provision calling access.
- Calling (flag-gated). Confirm the phone is calling-enabled, initiate a minimal test call, reject or terminate quickly. Accept/WebRTC flows need their own flag and separate confirmation.
- Deferred admin mutations. Separate future campaign: callback overrides, public key mutation, profile/settings/commerce writes, registration, token flows, QR codes, block/unblock.
- Cleanup and final verification. Execute the cleanup plan, re-run read-only discovery for touched families, produce a sanitized report, list anything needing manual cleanup.
Risk classification
| Class | Helpers |
|---|---|
| Read-only first | getWabaInfo, listSubscribedApps, listPhoneNumbers, getPhoneNumberInfo, getPhoneNumberSettings, getBusinessProfile, getCommerceSettings, listMessageTemplates, getMessageTemplate, listFlows, getFlow, getFlowAssets, downloadMedia, downloadMediaBytes with caps, getUploadSession, webhook GET verify |
| Side-effecting but controlled | all send* composers, markMessageAsRead, indicateTyping, uploadMedia, upload sessions, createMessageTemplate, updateMessageTemplate, createFlow, updateFlowMetadata, updateFlowJson, initiateCall, preAcceptCall, acceptCall, rejectCall, terminateCall |
| Destructive / cleanup-only | deleteMedia, deleteMessageTemplate, deleteFlow, deprecateFlow, publishFlow, admin mutations |
getPhoneNumberSettings({ includeSipCredentials: true }) may return SIP
credentials. Do not run it unless the campaign requires it, and never log the
raw response.
Redaction rules
Always redact: Authorization headers and Bearer tokens, access tokens, app
secrets, verify tokens, service bearer tokens, x-hub-signature-256 values,
challenge values, raw env values.
Mask or hash: WABA/phone-number/app/business ids, recipient numbers and
wa_id values, message ids, media ids and URLs, upload session ids,
production-linked template ids/names, Flow ids, call ids, catalog/product ids.
Sensitive response fields: SIP credentials, production-linked business profile contact details, signed media URLs (redact at least the query string), webhook payload PII.
Logging: record method, endpoint group, status, and Graph
code/subcode/classification. No full URLs with query strings. Correlate via
salted hashes (sha256(value + runSalt).slice(0, 12)). Raw response capture
requires a separate opt-in and lives outside the repository.
Cleanup and rollback
Before any mutation, write a run manifest outside the repository
(/tmp/wats-live-${WATS_TEST_RUN_ID}.json) recording resource type, id/name,
creation timestamp, endpoint, cleanup action, and cleanup status.
Cleanup order: terminate test calls, delete this run's media, delete this run's templates where Meta allows, delete this run's draft Flows, restore any changed settings, stop the webhook tunnel, re-run read-only inventory, rotate the live token if policy requires, produce the sanitized report.
Rollback limits: sent messages, read receipts, typing indicators, and calls cannot be unsent. Published/deprecated Flows may not be reversible. Template review/status changes may persist. Use only test recipients and uniquely prefixed resources.
Abort criteria
Abort immediately if:
- Any asset or recipient is not the intended test asset.
- Token scope is wrong or unexpectedly broad.
- Meta returns rate-limit, account-warning, integrity, or policy errors beyond the expected test condition.
- Cleanup fails for a created resource.
- A raw secret or PII value appears in logs.
- A side-effecting phase is reached without its opt-in flag.
Harness requirements
Live harnesses stay outside normal bun test unless WATS_LIVE_ENABLE=1 is
set, require domain mutation flags for side-effecting tests, require the run
id and resource prefix, default to dry-run/MockTransport when flags are
absent, write manifests outside the repository, redact before printing, and
keep CI and default package scripts credential-free.
Execution log
Redacted, non-secret summaries of authorized runs. These entries are the evidence behind the live-validated tags in the parity matrix.
2026-05-29 — Railway container, outbound + status webhooks
- Environment: WATS deployed as a container on Railway, live mode, public HTTPS callback. Explicit operator authorization for a disposable test number; token deactivated after the run.
- Outbound text and approved-template sends returned HTTP 200 with real Meta message ids.
- Meta delivered
statuswebhooks to the service; the service logged normalizedkind="status"updates (ids only, no PII), confirming HMAC verification, normalization, router dispatch, and acknowledgement end to end. - Result: outbound send and inbound
statuswebhook delivery live-validated for the test asset. Inboundmessageand richer families left for the next run.
2026-06-10 — env-gated harness against live Meta (v0.3.25)
- Environment: the env-gated probe harness (
packages/testing/live/), driven so secrets arrived only as subprocess env. Live flag plus per-domain mutation flags. Explicit operator authorization for the disposable test asset; redacted ledgers written outside the repo. - Read-only discovery (8/8 pass):
getWabaInfo,listSubscribedApps,listPhoneNumbers,getPhoneNumberInfo,getBusinessProfile,getCommerceSettings,listMessageTemplates,listFlows— all parsed live Graph v25.0 responses into current WATS types. Commerce settings and Flows returned empty on this asset; Flow mutations not exercised. - Sends: text send accepted (HTTP 200 + message id); its status webhook
reported
failed— expected, since freeform text needs an open 24h customer window. An approved-template send produced the full sent → delivered → read status chain plus an inbound reaction message, exercising the webhook runtime (HMAC → normalize → router) for both status and message families. - Media lifecycle:
uploadMedia,downloadMediametadata,sendImageby media id, anddeleteMediacleanup all passed; zero leaked assets. One real wire-contract bug found: the integrity check ondownloadMediaBytesaccepted only base64 digests while Meta returns 64-char hex. Fixed same day; download without the integrity option had worked throughout. - Groups:
listGroupsread passed (200, empty).createGroupwas blocked by Meta#131215"This phone number is not eligible to access Groups APIs" — an asset entitlement limitation, not a WATS defect; the structured code surfaced through the service diagnostics. Groups mutations stay shape-only until a Groups-entitled test number exists. - Cleanup: media deleted, no group created, no leaked assets.