diff --git a/plugins/kapso/skills/observe-whatsapp/SKILL.md b/plugins/kapso/skills/observe-whatsapp/SKILL.md index 0becde0..e484585 100644 --- a/plugins/kapso/skills/observe-whatsapp/SKILL.md +++ b/plugins/kapso/skills/observe-whatsapp/SKILL.md @@ -47,6 +47,17 @@ Fallback path: 2. API logs: `node scripts/api-logs.js` 3. Webhook deliveries: `node scripts/webhook-deliveries.js` +To map a WhatsApp error code (e.g. `131049` "healthy ecosystem engagement" marketing cap) to remediation, use the error-code table in [references/message-debugging-reference.md](references/message-debugging-reference.md). + +### Triage broadcast (campaign) failures + +Broadcast delivery data lives in the platform API, not PostHog analytics. Use this when a campaign shows a high failure rate, or when the broadcast detail page won't load (the API reads the same data). + +1. List failing broadcasts: `node scripts/broadcasts.js --status failed` +2. Inspect one broadcast's counts: `node scripts/broadcasts.js --broadcast-id ` +3. Drill into failed recipients (with error codes): `node scripts/broadcasts.js --broadcast-id --recipients --failed-only` +4. Map recipient `error_details.code` / `error_message` to remediation via [references/message-debugging-reference.md](references/message-debugging-reference.md). See [references/triage-reference.md](references/triage-reference.md) for the full broadcast triage flow. + ### Run health checks Preferred path: @@ -74,6 +85,7 @@ Fallback path: | `errors.js` | List message errors | | `api-logs.js` | List external API logs | | `webhook-deliveries.js` | List webhook delivery attempts | +| `broadcasts.js` | List broadcasts, inspect one, and drill into failed recipients | ### Health @@ -123,7 +135,7 @@ node scripts/openapi-explore.mjs --spec platform schema WebhookDelivery |.:{package.json,SKILL.md} |assets:{health-example.json,message-debugging-example.json,triage-example.json} |references:{health-reference.md,message-debugging-reference.md,triage-reference.md} -|scripts:{api-logs.js,errors.js,lookup-conversation.js,message-details.js,messages.js,openapi-explore.mjs,overview.js,webhook-deliveries.js,whatsapp-health.js} +|scripts:{api-logs.js,broadcasts.js,errors.js,lookup-conversation.js,message-details.js,messages.js,openapi-explore.mjs,overview.js,webhook-deliveries.js,whatsapp-health.js} |scripts/lib/messages:{args.js,kapso-api.js} |scripts/lib/status:{args.js,kapso-api.js} |scripts/lib/triage:{args.js,kapso-api.js} diff --git a/plugins/kapso/skills/observe-whatsapp/references/message-debugging-reference.md b/plugins/kapso/skills/observe-whatsapp/references/message-debugging-reference.md index 333b8f0..1efdaa8 100644 --- a/plugins/kapso/skills/observe-whatsapp/references/message-debugging-reference.md +++ b/plugins/kapso/skills/observe-whatsapp/references/message-debugging-reference.md @@ -4,7 +4,65 @@ 1. Identify the message ID (`wamid.*`). 2. Review the status timeline in order: sent -> delivered -> read. -3. Surface error codes in status events and map to remediation. +3. Surface error codes and map them to remediation (see the error-code table below). + +### Where error codes live + +- Per-message: `kapso.status` is the headline (`pending`/`sent`/`delivered`/`read`/`failed`). + The raw Meta status events — including any `errors[].code` and `errors[].title` — are in + `kapso.statuses[]` (raw Meta payloads). +- Per-broadcast recipient: `error_message` (string) and `error_details` (the Meta error + payload, with the numeric `code`). See "Broadcast / campaign failures" in + [triage-reference.md](triage-reference.md). + +## WhatsApp error-code reference + +Codes are Meta WhatsApp Cloud API error codes. Lead with whether the failure is **expected** +(throttling/policy that resolves on its own) or **actionable** (a fix the user can make). + +### Messaging limits and policy (the "high failure rate" cases) + +| Code | Meaning | Expected? | Remediation | +|------|---------|-----------|-------------| +| `131049` | "This message was not delivered to maintain healthy ecosystem engagement." Meta's per-user **marketing-template frequency cap** — the recipient has already received enough marketing messages in the window. | Yes — not a bug | Don't retry the same marketing template at the same recipient. Reduce marketing volume per user, space sends out, and prioritize **utility/authentication** templates (not subject to the cap) for transactional content. Improve opt-in quality and relevance. A high `131049` share on a broadcast usually means too much marketing to the same audience, not a delivery defect. | +| `130472` | Recipient is part of Meta's marketing-message **experiment group**; the marketing message was intentionally not delivered. | Yes — not a bug | Same as `131049`: treat as expected suppression, not a fixable error. | +| `131048` | Spam rate limit hit — sending restricted because of low quality / too many sends. | Partly | Slow down sends and improve message quality. Check the number's quality rating and messaging health (run a health check). | +| `131031` | Business account locked or restricted (policy violation). | No | Escalate — the WABA is restricted. Check WhatsApp Manager account status; contact support. | +| `368` | Temporarily blocked for policy violations. | No | Review recent policy violations in WhatsApp Manager; resolve before resuming sends. | + +### Delivery failures (recipient / window / throughput) + +| Code | Meaning | Remediation | +|------|---------|-------------| +| `131026` | Message undeliverable — recipient can't receive it (not a WhatsApp user, hasn't accepted updated ToS, or the business isn't allowed to message them). | Confirm the number is on WhatsApp and correctly formatted (E.164, no `+` in API payloads). Don't keep retrying a number that consistently returns this. | +| `131047` | Re-engagement required — more than 24h since the user's last message, so free-form text is blocked. | Use an approved **template** to reopen the conversation; free-form messages only work inside the 24-hour customer-service window. | +| `131051` | Unsupported message type. | Send a supported type for the recipient/region. | +| `131053` | Media upload/download error. | Re-upload the media; verify size/format limits and that the media URL is reachable. | +| `130429` / `80007` | Rate limit (throughput) reached. | Back off and retry with spacing; reduce concurrent send rate. Transient. | +| `131000` | Generic "something went wrong" on Meta's side. | Transient — retry. If it persists, capture the message ID and `kapso.statuses[]` for escalation. | + +### Template errors (business-initiated sends) + +| Code | Meaning | Remediation | +|------|---------|-------------| +| `132000` | Number of template parameters doesn't match the template. | Send exactly the parameters the approved template defines. | +| `132001` | Template doesn't exist or isn't approved in this language. | Verify the template name + language code; confirm it's APPROVED for the WABA. | +| `132005` | Hydrated template text is too long. | Shorten parameter values so the rendered message fits Meta's limit. | +| `132007` / `132015` | Template paused for low quality. | The template is paused by Meta; fix content quality or use a different approved template. | +| `132012` | Parameter format mismatch. | Match the expected parameter format (e.g. currency/date/number components). | +| `132016` | Template disabled. | Template was disabled; resubmit or switch templates. | + +### Eligibility / auth + +| Code | Meaning | Remediation | +|------|---------|-------------| +| `131042` | Business eligibility / payment-method issue. | Templates blocked until billing is fixed; 24-hour-window messages still work. Fix the payment method in WhatsApp Manager. | +| `133xxx` | Phone-number registration errors. | The number isn't correctly registered with Cloud API. Re-run health checks; escalate if registration is incomplete. | +| `141000` | See [health-reference.md](health-reference.md) §12 — contact support. | + +> This is not exhaustive. For an unlisted code, report the numeric code + Meta `title`/message +> verbatim, say whether it looks transient (retry) or actionable, and point the user at Meta's +> Cloud API error reference rather than guessing. ## Common issues to confirm diff --git a/plugins/kapso/skills/observe-whatsapp/references/triage-reference.md b/plugins/kapso/skills/observe-whatsapp/references/triage-reference.md index 96f96fe..74d7af6 100644 --- a/plugins/kapso/skills/observe-whatsapp/references/triage-reference.md +++ b/plugins/kapso/skills/observe-whatsapp/references/triage-reference.md @@ -12,6 +12,39 @@ 2. Review token validity, messaging health, and webhook subscription. 3. Explain whether the issue is critical or degraded. +## Broadcast / campaign failures + +Broadcast (campaign) data lives in the **platform API**, not in PostHog analytics — there is no +broadcast or delivery-status event to query in product analytics, so triage from the API. + +Use this when a campaign shows a **high failure rate** or when the broadcast detail page in the +app won't load (the API is the workaround to read the same data). + +1. **Find the broadcast and read its counts.** List broadcasts (optionally `--status failed`) + and inspect one. The `WhatsappBroadcast` carries `sent_count`, `failed_count`, + `delivered_count`, `read_count`, `pending_count`, `response_rate`, and `status` + (`draft`/`scheduled`/`sending`/`stopped`/`completed`/`failed`). + - `node scripts/broadcasts.js --status failed` + - `node scripts/broadcasts.js --broadcast-id ` +2. **Drill into failed recipients to get the error codes.** Each `WhatsappBroadcastRecipient` + carries `status`, `error_message`, and `error_details` (the Meta error payload with the + numeric `code`). + - `node scripts/broadcasts.js --broadcast-id --recipients --failed-only` +3. **Map the codes to remediation** using the error-code table in + [message-debugging-reference.md](message-debugging-reference.md). + - A large share of `131049` (or `130472`) is **expected** — Meta's per-user marketing-message + cap, not a delivery defect. Lead with that: the campaign sent too many marketing templates + to the same audience; reduce frequency and prefer utility/authentication templates. Do not + report it as a bug to fix. + - Codes like `131026`, `131047`, `132xxx` point at recipient/window/template problems that + *are* actionable. +4. **Cross-reference** `api-logs` and `webhook-deliveries` only if the failures look like + transport/API problems (auth, rate limit, upstream 5xx) rather than per-recipient Meta codes. + +The OpenAPI broadcast endpoints (`listWhatsappBroadcasts`, `getWhatsappBroadcast`, +`listWhatsappBroadcastRecipients`) can be explored with +`node scripts/openapi-explore.mjs --spec platform search "broadcast"`. + ## Webhook delivery failures 1. Review recent delivery attempts. diff --git a/plugins/kapso/skills/observe-whatsapp/scripts/broadcasts.js b/plugins/kapso/skills/observe-whatsapp/scripts/broadcasts.js new file mode 100644 index 0000000..fe0307c --- /dev/null +++ b/plugins/kapso/skills/observe-whatsapp/scripts/broadcasts.js @@ -0,0 +1,112 @@ +const { hasHelpFlag, parseFlags } = require('./lib/triage/args'); +const { kapsoConfigFromEnv, kapsoRequest } = require('./lib/triage/kapso-api'); + +function err(message, details) { + return { ok: false, error: { message, details } }; +} + +async function main() { + const argv = process.argv.slice(2); + if (hasHelpFlag(argv)) { + console.log( + JSON.stringify( + { + ok: true, + usage: [ + 'List broadcasts: node scripts/broadcasts.js [--status ] [--phone-number-id ] [--created-after ] [--created-before ] [--per-page ] [--page ]', + 'Get one broadcast: node scripts/broadcasts.js --broadcast-id ', + 'List recipients: node scripts/broadcasts.js --broadcast-id --recipients [--failed-only] [--per-page ] [--page ]' + ], + notes: [ + 'Broadcast data lives in the platform API, not PostHog analytics.', + 'Failed recipients carry error_message and error_details (Meta error payload with numeric code).', + 'Map error codes (e.g. 131049 marketing cap) using references/message-debugging-reference.md.', + '--failed-only filters the returned recipients client-side (the API has no status filter).' + ], + env: ['KAPSO_API_BASE_URL', 'KAPSO_API_KEY'] + }, + null, + 2 + ) + ); + return 0; + } + + try { + const flags = parseFlags(argv); + const broadcastId = flags['broadcast-id']; + const wantsRecipients = flags.recipients === true || flags.recipients === 'true'; + + if (wantsRecipients && (!broadcastId || broadcastId === true)) { + throw new Error('--recipients requires --broadcast-id '); + } + + const config = kapsoConfigFromEnv(); + + if (wantsRecipients) { + const params = new URLSearchParams(); + if (flags['per-page']) params.set('per_page', flags['per-page']); + if (flags.page) params.set('page', flags.page); + + const suffix = params.toString(); + const path = `/platform/v1/whatsapp/broadcasts/${encodeURIComponent(broadcastId)}/recipients${ + suffix ? `?${suffix}` : '' + }`; + const response = await kapsoRequest(config, path); + + const failedOnly = flags['failed-only'] === true || flags['failed-only'] === 'true'; + if (failedOnly && response && Array.isArray(response.data)) { + const filtered = response.data.filter((recipient) => isFailedRecipient(recipient)); + console.log( + JSON.stringify( + { ok: true, broadcast_id: broadcastId, failed_only: true, data: filtered, meta: response.meta }, + null, + 2 + ) + ); + return 0; + } + + console.log(JSON.stringify({ ok: true, broadcast_id: broadcastId, data: response }, null, 2)); + return 0; + } + + if (broadcastId && broadcastId !== true) { + const path = `/platform/v1/whatsapp/broadcasts/${encodeURIComponent(broadcastId)}`; + const data = await kapsoRequest(config, path); + console.log(JSON.stringify({ ok: true, data }, null, 2)); + return 0; + } + + const params = new URLSearchParams(); + if (flags.status) params.set('status', flags.status); + if (flags['phone-number-id']) params.set('phone_number_id', flags['phone-number-id']); + if (flags['created-after']) params.set('created_after', flags['created-after']); + if (flags['created-before']) params.set('created_before', flags['created-before']); + if (flags['per-page']) params.set('per_page', flags['per-page']); + if (flags.page) params.set('page', flags.page); + + const suffix = params.toString(); + const data = await kapsoRequest( + config, + `/platform/v1/whatsapp/broadcasts${suffix ? `?${suffix}` : ''}` + ); + + console.log(JSON.stringify({ ok: true, data }, null, 2)); + return 0; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error(JSON.stringify(err('Command failed', { message }), null, 2)); + return 1; + } +} + +function isFailedRecipient(recipient) { + if (!recipient || typeof recipient !== 'object') return false; + if (recipient.failed_at) return true; + if (recipient.error_message || recipient.error_details) return true; + const status = String(recipient.status || '').toLowerCase(); + return status === 'failed'; +} + +main().then((code) => process.exit(code)); diff --git a/plugins/kapso/skills/observe-whatsapp/scripts/errors.js b/plugins/kapso/skills/observe-whatsapp/scripts/errors.js index a3de379..af5afb2 100644 --- a/plugins/kapso/skills/observe-whatsapp/scripts/errors.js +++ b/plugins/kapso/skills/observe-whatsapp/scripts/errors.js @@ -43,6 +43,7 @@ async function main() { const path = `/platform/v1/whatsapp/messages?${params.toString()}`; result.sources.message_delivery = await kapsoRequest(config, path); result.notes.push('Message failures use outbound status=failed and do not support period filtering.'); + result.notes.push('For broadcast/campaign failures (e.g. high 131049 marketing-cap rates), use scripts/broadcasts.js — broadcast delivery data is not included here.'); } if (!source || source === 'api_call') {