Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions contracts/official-api-round2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
{
"repository": "polymorfa/polymorfa",
"sourceCommit": "9bb1e2ed92d8b4a07ff727f8e7ae185498f0868f",
"family": "messaging",
"sourcePath": "apps/api/docs/openapi.json",
"sourceSha256": "6d5b5477a86a85504f921933bf7b26f2c47506e21380fe73c8a5ff1002163b28",
"published": false,
"note": "Prepared beta counterparts. Exact merged sources below remain authoritative. No staging/production acceptance or package publication. Reconcile full release source, spec coverage and all feature pins before publication. Private Console routes are excluded.",
"items": [
{
"item": "OA-03",
"mergeSha": "682b9980a19de784b9ada565a97440a8b7fb05fb",
"sourcePath": "apps/api/docs/openapi.json",
"sourceSha256": "cfa88b9f4aea0cd2d16347d33818f6a3f5870fa0cb376dd6a97103e6b677db2a",
"releasePath": "internal/docs/releases/2026-10-11-cloud-onboarding-readiness.md",
"releaseSha256": "1cb94749b857cce638b81aecbf34f5dfaf1079fca94bdb1632e24069b47463d0",
"snapshotPath": "contracts/official-api-round2/oa-03.json",
"snapshotSha256": "62641435f1d670607d5b91e8363f42fdc45dc7d669c2e3a8c461c2a01abe337c",
"operationIds": ["getQuickLink", "cancelQuickLink"]
},
{
"item": "OA-16",
"mergeSha": "7391c22e50d6e238771f3428c22611e100f9a13e",
"sourcePath": "apps/api/docs/openapi.json",
"sourceSha256": "04644a0e0fd11b81d457a7f5efb55648567e382953aa2cb93fc7f7250aeff16a",
"releasePath": "internal/docs/releases/2026-10-11-cloud-catalog-sync.md",
"releaseSha256": "dfc1a16d31be23926a1440b5ded807251ba9ca4a7b732c99152ffd0d744aa30f",
"snapshotPath": "contracts/official-api-round2/oa-16.json",
"snapshotSha256": "2bc4fbba821e9c3c90b7a9f632aace0360edbcaf8a1685b9b6a4d9387b5d305d",
"operationIds": [
"listCloudCatalogs",
"getCloudCatalog",
"syncCloudCatalog",
"listCloudCatalogProducts"
]
},
{
"item": "OA-17",
"mergeSha": "24ac44aad59715a3f32dcbee2f4fe5cb07bbda87",
"sourcePath": "apps/api/docs/openapi.json",
"sourceSha256": "5ce4bfda4d9ac8ca29b72a9eff6cf009cc0ee0a8be0167e820d3c851ca3e6f8e",
"releasePath": "internal/docs/releases/2026-10-11-cloud-marketing-messages.md",
"releaseSha256": "088feba9c5dbb933f7a8f159ea7f9f1c6489fbcf70da82277c1f3bd7f04695f4",
"snapshotPath": "contracts/official-api-round2/oa-17.json",
"snapshotSha256": "84f218fa062e92c5792e9f075d1049b26f314634412da2db8b49e195d53d3dee",
"operationIds": ["getMarketingMessagesStatus"]
},
{
"item": "OA-18",
"mergeSha": "e092ffc559b0bd814931560059e07983c6915f66",
"sourcePath": "apps/api/docs/openapi.json",
"sourceSha256": "7520f63ea6f8b6ca02465b4548c44b5001b0b0b0fadde331d06c4b497bc0153c",
"releasePath": "internal/docs/releases/2026-10-16-oa18-parent-bsuid-block.md",
"releaseSha256": "aea935f32779f2c4d32ca95427746203b5d51cca63f07e4d5454906d6acbe28a",
"snapshotPath": "contracts/official-api-round2/oa-18.json",
"snapshotSha256": "a381304c948807df9ec410617e93ac827073978f091708aac134fe23b7c68f0d",
"operationIds": ["blockContact", "unblockContact"]
},
{
"item": "OA-21",
"mergeSha": "ceb2d0227e5d0aa21e390508d9437e088794da1f",
"sourcePath": "apps/api/docs/openapi.json",
"sourceSha256": "9d8faf13be970bee800ad15e308c130bc4ae8703865ae99278b9ae315cc27638",
"releasePath": "internal/docs/releases/2026-10-01-official-groups.md",
"releaseSha256": "72efd0f27256dd1c7ee04a26e325b6a9051c4a8453d0b554dbce14241fb662d4",
"snapshotPath": "contracts/official-api-round2/oa-21.json",
"snapshotSha256": "ec42f4a70d6d02803eb31864438668f19307b97486a71dcb0c38ac5cc92b13f6",
"operationIds": ["setOfficialGroupPicture"]
},
{
"item": "OA-30",
"mergeSha": "d1985700bf1738fe2c4fd55b17a1e6d7fd328453",
"sourcePath": "apps/api/docs/openapi.management.json",
"sourceSha256": "e28256daac1e8efd84998d7399b74c3200623c19ac807a4b0730523a667976ff",
"releasePath": "internal/docs/releases/2026-10-16-webhook-batching.md",
"releaseSha256": "98c411776b87bcbf6fe6a77b7657472ce1ced305a427e939946454a2f8acce37",
"snapshotPath": "contracts/official-api-round2/oa-30.json",
"snapshotSha256": "b6885a304b5d29d14e06c7b6281f1b55d04041f44a288f6754355b8ae67d01f4",
"operationIds": [
"listOrganizationWebhooks",
"createOrganizationWebhook",
"getOrganizationWebhook",
"updateOrganizationWebhook",
"deleteOrganizationWebhook",
"rotateOrganizationWebhookSecret",
"testOrganizationWebhook",
"listProjectWebhooks",
"createProjectWebhook",
"getProjectWebhook",
"updateProjectWebhook",
"deleteProjectWebhook",
"rotateProjectWebhookSecret",
"testProjectWebhook"
]
}
],
"errorCodes": [
"whatsapp_authorization_required",
"whatsapp_payment_method_required",
"whatsapp_number_not_registered",
"marketing_disabled_on_cloud_api",
"parent_bsuid_not_supported",
"hybrid_group_linked_devices_unavailable",
"catalog_cache_full",
"catalog_sync_cooldown"
],
"checkedDev": "dafb7cb4e4efbcdcbd04af206f1ef6d47c209379",
"sdk": {
"pullRequest": "https://github.com/polymorfa/sdks/pull/360",
"sourceCommit": "f9937116b17f9962de8a3eb632c44d0399b05002",
"registryVersion": null,
"published": false,
"note": "Wait for an accepted containing SDK prerelease. No Git or file dependency is committed. Candidate local verification only."
}
}
66 changes: 66 additions & 0 deletions docs/official-api-round2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Official API round-2 commands

These commands prepare flagged betas. Enrollment, an active connection and the
required scopes remain server requirements. `feature_unavailable` remains an
authorization error with exit code 3 and a structured error on stderr with
`--json`. No command grants access or claims GA availability.

The CLI PR waits for an accepted SDK prerelease containing these methods.
`contracts/official-api-round2.json` records the exact merged backend sources and
a publication hold. The committed SDK dependency remains its existing registry
prerelease until the containing package is available. A local candidate build is
verification evidence only.

```sh
polymorfa cloud catalog list --session NUMBER_ID --json
polymorfa cloud catalog get --session NUMBER_ID --catalog CATALOG_ID --json
polymorfa cloud catalog sync --session NUMBER_ID --catalog CATALOG_ID --json
polymorfa cloud catalog products --session NUMBER_ID --catalog CATALOG_ID --limit 50 --json
polymorfa cloud marketing status --session NUMBER_ID --json
polymorfa contact block CONTACT_ID --session NUMBER_ID --json
polymorfa contact unblock CONTACT_ID --session NUMBER_ID --json
polymorfa cloud group picture GROUP_ID --session OFFICIAL_NUMBER_ID --file picture.jpg --json
polymorfa group picture GROUP_ID --session HYBRID_NUMBER_ID --file picture.jpg --json
polymorfa webhook update WEBHOOK_ID --owner organization --batching on --batch-max-events 100 --batch-window-ms 1000 --json
polymorfa webhook update WEBHOOK_ID --owner organization --batching off --json
```

Replace the uppercase IDs with your resource IDs. Choose your existing regional
Messaging base URL and credentials using the ordinary profile/environment
settings. Native catalog reads use saved snapshots; only sync calls Meta. Sync
runs once and may take 50 seconds; its default timeout is 60 seconds. A new sync
inside 120 seconds returns `catalog_sync_cooldown` with Retry-After. Product
pages accept the returned `nextCursor` via `--cursor`. Existing Graph catalog
commands still use `--waba`, `--graph-version` and `--after`; these cannot be
mixed with a native `--session` target.

Marketing status reports onboarding, terms, raw provider values and next action.
Unknown status never proves eligibility. Terms are accepted in WhatsApp Manager.
The existing Graph form remains `cloud marketing status --waba WABA_ID
--graph-version VERSION`.

Native block/unblock uses public contact IDs, including identities the API
resolves to an ordinary BSUID or phone. Parent BSUIDs are metadata only and
cannot be targets. An uncertain provider effect must be checked before a new
attempt. Reverting a CLI change does not undo an applied block or picture.

`cloud group picture` uploads one multipart JPEG for an Official-only Number:
square, at least 192 x 192 pixels, at most 5 MB. The backend validates the image.
Hybrid Numbers use `group picture` on the Linked Devices connection only.
`hybrid_group_linked_devices_unavailable` means not executed. Never route the
same operation through Official as recovery. Group history remains available
through existing `chat` commands with opt-in regional history access.

Batching applies to Platform organization/project webhook endpoints in native
format. It defaults off. Configure the receiver to verify and handle both
single events and the version 1 `event_batch` envelope before enabling it.
Deduplicate member event IDs and batch IDs; acknowledge the whole request only.
The event limit is 2-100, but a window can emit one event. The window is
100-1000 ms. Omitted settings preserve the endpoint's saved values on update.
The server checks saved format and enrollment. Disabling remains available after
withdrawal. `webhook view` shows saved batching settings.

Private Console routes are excluded. No live Meta, hosted webhook receiver,
regional deployment, package publication or customer acceptance is established
by these command examples. Matching staging acceptance gates prereleases;
production acceptance separately gates stable publication.
1 change: 1 addition & 0 deletions scripts/prepare-release.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ try {
)
validateSdk(sdk.provenance, sdk.version, channel)
validateFeatureContractPins(sdk.provenance, [
JSON.parse(readFileSync('contracts/official-api-round2.json')),
JSON.parse(readFileSync('contracts/abprops-capabilities.json')),
])
const files = execFileSync('git', [
Expand Down
27 changes: 27 additions & 0 deletions src/commands/cloud/catalog/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {Flags} from '@oclif/core'
import {BaseCommand} from '../../../base-command.js'
export default class CloudCatalogGet extends BaseCommand {
static override description =
'Read a cached catalog and its latest sync outcome (beta, business:read). Enrollment applies; feature_unavailable is preserved.'
static override flags = {
session: Flags.string({
description: 'Number ID with an active Official connection.',
required: true,
}),
catalog: Flags.string({
description: 'Catalog ID, 1 to 32 digits.',
required: true,
}),
}
async run(): Promise<void> {
const {flags} = await this.parse(CloudCatalogGet)
const client = await this.messagingClient(flags, {
serverCredentialOnly: 'Official API',
})
const response = await client.catalogs.retrieve(

Check failure on line 21 in src/commands/cloud/catalog/get.ts

View workflow job for this annotation

GitHub Actions / Verify Node 22

Property 'catalogs' does not exist on type 'MessagingClient'.
flags.session,
flags.catalog,
)
this.writeValue(response, JSON.stringify(response.data, null, 2), flags)
}
}
25 changes: 19 additions & 6 deletions src/commands/cloud/catalog/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,20 @@

export default class CloudCatalogList extends BaseCommand {
static override description =
'List visible Meta product catalogs for a WhatsApp Business Account. Requires sessions:read and Graph access.'
'List cached Number catalogs (beta, business:read), or visible WABA catalogs with Graph access. Enrollment applies; feature_unavailable is preserved.'
static override flags = {
session: Flags.string({
description:
'Number ID for the native catalog cache beta. Reads never refresh Meta.',
exclusive: ['waba', 'graph-version', 'limit', 'after'],
}),
waba: Flags.string({
description: 'WhatsApp Business Account ID.',
required: true,
exclusive: ['session'],
}),
'graph-version': Flags.string({
description: 'Meta Graph version, for example v26.0.',
required: true,
exclusive: ['session'],
}),
limit: Flags.integer({
description: 'Page size from 1 to 100; API default is 25.',
Expand All @@ -25,15 +30,23 @@
}
async run(): Promise<void> {
const {flags} = await this.parse(CloudCatalogList)
if (!flags.waba.trim() || !flags['graph-version'].trim())
if (
flags.session === undefined &&
(!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.cloudCatalogs.list(flags.waba, {
version: flags['graph-version'],
if (flags.session !== undefined) {
const response = await client.catalogs.list(flags.session)

Check failure on line 44 in src/commands/cloud/catalog/list.ts

View workflow job for this annotation

GitHub Actions / Verify Node 22

Property 'catalogs' does not exist on type 'MessagingClient'.
this.writeValue(response, JSON.stringify(response.data, null, 2), flags)
return
}
const response = await client.cloudCatalogs.list(flags.waba!, {
version: flags['graph-version']!,
...(flags.limit === undefined ? {} : {limit: flags.limit}),
...(flags.after === undefined ? {} : {after: flags.after}),
})
Expand Down
34 changes: 27 additions & 7 deletions src/commands/cloud/catalog/products.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,28 @@

export default class CloudCatalogProducts extends BaseCommand {
static override description =
'List products in a catalog linked to an Official API WABA (beta). Requires sessions:read and Graph access.'
'List cached Number products (beta, business:read), or linked WABA products with Graph access. Enrollment applies; feature_unavailable is preserved.'

static override flags = {
session: Flags.string({
description: 'Number ID for cached products (beta, business:read).',
exclusive: ['waba', 'graph-version', 'after'],
}),
cursor: Flags.string({
description: 'Native nextCursor from the previous products response.',
dependsOn: ['session'],
}),
waba: Flags.string({
description: 'WhatsApp Business Account ID.',
required: true,
exclusive: ['session'],
}),
catalog: Flags.string({
description: 'Numeric product catalog ID linked to the WABA.',
required: true,
}),
'graph-version': Flags.string({
description: 'Meta Graph version, for example v26.0.',
required: true,
exclusive: ['session'],
}),
limit: Flags.integer({
description: 'Page size from 1 to 100; API default is 25.',
Expand All @@ -32,21 +40,33 @@
async run(): Promise<void> {
const {flags} = await this.parse(CloudCatalogProducts)
if (
!flags.waba.trim() ||
(flags.session === undefined && !flags.waba?.trim()) ||
!/^[0-9]+$/.test(flags.catalog) ||
!flags['graph-version'].trim()
(flags.session === undefined && !flags['graph-version']?.trim())
)
throw new PolymorfaConfigurationError(
'Provide a WABA ID, numeric catalog ID, and Graph version.',
)
const client = await this.messagingClient(flags, {
serverCredentialOnly: 'Official API',
})
if (flags.session !== undefined) {
const response = await client.catalogs.listProducts(

Check failure on line 54 in src/commands/cloud/catalog/products.ts

View workflow job for this annotation

GitHub Actions / Verify Node 22

Property 'catalogs' does not exist on type 'MessagingClient'.
flags.session,
flags.catalog,
{
...(flags.limit === undefined ? {} : {limit: flags.limit}),
...(flags.cursor === undefined ? {} : {cursor: flags.cursor}),
},
)
this.writeValue(response, JSON.stringify(response.data, null, 2), flags)
return
}
const response = await client.cloudCatalogs.listProducts(
flags.waba,
flags.waba!,
flags.catalog,
{
version: flags['graph-version'],
version: flags['graph-version']!,
...(flags.limit === undefined ? {} : {limit: flags.limit}),
...(flags.after === undefined ? {} : {after: flags.after}),
},
Expand Down
24 changes: 24 additions & 0 deletions src/commands/cloud/catalog/sync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {Flags} from '@oclif/core'
import {BaseCommand} from '../../../base-command.js'
export default class CloudCatalogSync extends BaseCommand {
static override description =
'Explicitly refresh a catalog from Meta (beta, business:read). Sends once; sync cooldown is 120 seconds. A failure may preserve the previous snapshot. Enrollment applies; feature_unavailable is preserved.'
static override flags = {
session: Flags.string({
description: 'Number ID with an active Official connection.',
required: true,
}),
catalog: Flags.string({
description: 'Catalog ID, 1 to 32 digits.',
required: true,
}),
}
async run(): Promise<void> {
const {flags} = await this.parse(CloudCatalogSync)
const client = await this.messagingClient(flags, {
serverCredentialOnly: 'Official API',
})
const response = await client.catalogs.sync(flags.session, flags.catalog)

Check failure on line 21 in src/commands/cloud/catalog/sync.ts

View workflow job for this annotation

GitHub Actions / Verify Node 22

Property 'catalogs' does not exist on type 'MessagingClient'.
this.writeValue(response, JSON.stringify(response.data, null, 2), flags)
}
}
Loading
Loading