Skip to content
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,7 @@ beta feature.
```shell
polymorfa cloud catalog list --waba WABA_ID --graph-version v26.0 --limit 25
polymorfa cloud catalog products --waba WABA_ID --catalog CATALOG_ID --graph-version v26.0 --limit 25
polymorfa cloud marketing status --waba WABA_ID --graph-version v26.0
polymorfa cloud template list --session number_123
polymorfa cloud template get order_update --session number_123 --language en_US
polymorfa cloud template create --session number_123 --input template.json --idempotency-key tpl-order-update
Expand Down Expand Up @@ -504,6 +505,13 @@ availability from one catalog linked to the authenticated WABA. Pass its
unlinked catalogs and simulated Numbers. This read does not send a product or
establish that the merchant may sell it.

`cloud marketing status` uses `sessions:read` to return Meta's raw WABA
`marketing_messages_lite_api_status` and
`marketing_messages_onboarding_status` strings when present. The values do not
establish accepted terms, recipient permission, eligibility, or a right to
send. Simulated Numbers and unsupported accounts return API errors; the CLI
does not convert status strings into an eligibility decision.

Reauthorization creates a QuickLink for a stopped or disconnected standalone
Official Number. It never stops the Number or opens the link automatically.
Running Numbers return a conflict; Hybrid reauthorization is unsupported.
Expand Down
22 changes: 22 additions & 0 deletions docs/internal/2026-09-26-marketing-status-command.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Raw Marketing Messages status command gate

`cloud marketing status` consumes `MessagingClient.cloudMarketing.status` from
`polymorfa/sdks` PR #315, stacked on SDK PR #314. Its API source is
`polymorfa/polymorfa` PR #383 at
`1e1d613de599c2b4334f8de81fab087c74f31424`; the Graph OpenAPI copy has
SHA-256 `1270c8fb32a09cb717f3ea8edd6bd657e8ff3110e03a6286e68d5e16affff474`.
The CLI renders the exact raw status object and does not interpret terms,
eligibility, insight access or a send decision.

The CLI retains its older exact SDK registry pin in `package.json` and
`package-lock.json`. This source change cannot be published or released until
the API and SDK PR stacks merge, the SDK is published and its exact version and
integrity verified, and that version is pinned here with a regenerated
lockfile. No registry or production setting was changed.

API: separate PR #383; SDKs: typed TypeScript consumer in a separate stacked
PR; CLI: new read-only command and help; Docs: CLI README plus API/SDK public
documentation in their respective PRs; Feature release: existing Graph beta
gate remains off by default, no enrollment or entitlement change; Admin:
unaffected. No tests, typechecks, builds, documentation checks or live Meta
requests were run at the user's request.
35 changes: 35 additions & 0 deletions src/commands/cloud/marketing/status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {Flags} from '@oclif/core'
import {PolymorfaConfigurationError} from '@polymorfa/sdk'
import {BaseCommand} from '../../../base-command.js'

export default class CloudMarketingStatus extends BaseCommand {
static override description =
'Read raw Marketing Messages WABA status strings (beta). Requires sessions:read and Graph access.'

static override flags = {
waba: Flags.string({
description:
'WhatsApp Business Account ID from an Official API Number in your project.',
required: true,
}),
'graph-version': Flags.string({
description: 'Meta Graph version, for example v26.0.',
required: true,
}),
}

async run(): Promise<void> {
const {flags} = await this.parse(CloudMarketingStatus)
if (!flags.waba.trim() || !flags['graph-version'].trim())
throw new PolymorfaConfigurationError(
'Provide a WABA ID and Graph version.',
)
const client = await this.messagingClient(flags, {
serverCredentialOnly: 'Official API',
})
const response = await client.cloudMarketing.status(flags.waba, {
version: flags['graph-version'],
})
this.writeValue(response, JSON.stringify(response.data, null, 2), flags)
}
}
53 changes: 40 additions & 13 deletions test/commands/cloud.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {mkdir, mkdtemp, rm, writeFile} from 'node:fs/promises'
import {AsyncEntry} from '@napi-rs/keyring'
import {homedir} from 'node:os'
import {tmpdir} from 'node:os'
import {join} from 'node:path'
import {runCommand} from '@oclif/test'
import {afterEach, describe, expect, test, vi} from 'vitest'
Expand Down Expand Up @@ -54,9 +54,7 @@ describe('Official API commands', () => {
})

test('submits bounded template JSON once', async () => {
directory = await mkdtemp(
join(homedir(), '.polymorfa-agent-work', 'cloud-cli-test-'),
)
directory = await mkdtemp(join(tmpdir(), 'cloud-cli-test-'))
const path = join(directory, 'template.json')
const body = {
name: 'order_update',
Expand Down Expand Up @@ -86,9 +84,7 @@ describe('Official API commands', () => {
})

test('uses the explicit template project when a different directory is linked', async () => {
directory = await mkdtemp(
join(homedir(), '.polymorfa-agent-work', 'cloud-cli-test-'),
)
directory = await mkdtemp(join(tmpdir(), 'cloud-cli-test-'))
await writeFile(
join(directory, 'polymorfa.json'),
JSON.stringify({
Expand Down Expand Up @@ -152,9 +148,7 @@ describe('Official API commands', () => {
})

test('edits a selected template language and reports acceptance', async () => {
directory = await mkdtemp(
join(homedir(), '.polymorfa-agent-work', 'cloud-cli-test-'),
)
directory = await mkdtemp(join(tmpdir(), 'cloud-cli-test-'))
const path = join(directory, 'edit.json')
const body = {components: [{type: 'BODY', text: 'Ready'}]}
await writeFile(path, JSON.stringify(body))
Expand Down Expand Up @@ -183,9 +177,7 @@ describe('Official API commands', () => {
})

test('rejects an edit containing a renamed template before dispatch', async () => {
directory = await mkdtemp(
join(homedir(), '.polymorfa-agent-work', 'cloud-cli-test-'),
)
directory = await mkdtemp(join(tmpdir(), 'cloud-cli-test-'))
const path = join(directory, 'invalid.json')
await writeFile(path, JSON.stringify({name: 'renamed', components: [{}]}))
server = await startHttpServer({body: {success: true}})
Expand Down Expand Up @@ -431,6 +423,41 @@ describe('Official API commands', () => {
expect(server.requests).toHaveLength(0)
})

test('reads raw Marketing Messages status strings without interpretation', async () => {
const data = {
id: '1234',
marketing_messages_onboarding_status: 'ONBOARDED',
}
server = await startHttpServer({body: data})
const result = await credential(
() =>
runCommand(
'cloud marketing status --waba 1234 --graph-version v26.0 --json',
),
'project',
)
expect(result.error).toBeUndefined()
expect((JSON.parse(result.stdout) as {data: unknown}).data).toEqual(data)
expect(server.requests).toHaveLength(1)
expect(server.requests[0]).toMatchObject({
method: 'GET',
url: '/graph/whatsapp/v26.0/1234/marketing_messages/status',
})
})

test('refuses marketing status for a client token before any request', async () => {
server = await startHttpServer({body: {}})
const result = await credential(
() =>
runCommand(
'cloud marketing status --waba 1234 --graph-version v26.0 --json',
),
'client',
)
expect(result.error).toBeDefined()
expect(server.requests).toHaveLength(0)
})

async function credential<T>(
run: () => Promise<T>,
kind: 'api' | 'project' | 'client' = 'api',
Expand Down
6 changes: 2 additions & 4 deletions test/commands/flows.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {mkdtemp, rm, writeFile} from 'node:fs/promises'
import {homedir} from 'node:os'
import {tmpdir} from 'node:os'
import {join} from 'node:path'
import {runCommand} from '@oclif/test'
import {afterEach, describe, expect, test} from 'vitest'
Expand Down Expand Up @@ -88,9 +88,7 @@ describe('project Flow commands', () => {
})
})
test('rejects project override in a draft file before dispatch', async () => {
directory = await mkdtemp(
join(homedir(), '.polymorfa-agent-work', 'flows-cli-test-'),
)
directory = await mkdtemp(join(tmpdir(), 'flows-cli-test-'))
const path = join(directory, 'draft.json')
await writeFile(
path,
Expand Down