From 35a3d7aaf1aaffa130a3a2c6b262c08af741eff3 Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Sat, 10 Oct 2026 11:57:39 +0300 Subject: [PATCH 01/14] docs: pin native interactive button contract and preserve payload coverage --- contracts/README.md | 8 ++ contracts/interactive-buttons.json | 137 +++++++++++++++++++++ packages/typescript/src/messaging/types.ts | 2 + packages/typescript/test/messages.test.ts | 30 +++++ 4 files changed, 177 insertions(+) create mode 100644 contracts/interactive-buttons.json diff --git a/contracts/README.md b/contracts/README.md index 4bfd0bf9..f64587a7 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -1,5 +1,13 @@ # Contract coverage +## Interactive button candidate + +`interactive-buttons.json` pins the button branch to draft API PR #562. +Linked Devices accepts ten replies or three CTAs, without mixing them. +Official API replies retain the three-button limit. SDK arrays preserve all +parameters. Replace the draft pin with the accepted API revision and verify +matching staging before package publication. + ## Request log supplement `request-logs.json` pins `GET /platform/projects/{projectId}/request-logs` and diff --git a/contracts/interactive-buttons.json b/contracts/interactive-buttons.json new file mode 100644 index 00000000..794e8967 --- /dev/null +++ b/contracts/interactive-buttons.json @@ -0,0 +1,137 @@ +{ + "repository": "polymorfa/polymorfa", + "commit": "c3d8ac300b0ed34fee854bcd06b90e0bffd82c5c", + "sourcePath": "apps/api/docs/openapi.json", + "sourceSha256": "a4cf9a2f34bba7eab3112deccd40b50ad0df838425eb48b09762ef8f857b62b4", + "releaseStatus": "draft; requires accepted API revision and staging acceptance", + "buttonsContent": { + "type": "object", + "properties": { + "buttons": { + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 0 + }, + "body": { + "type": "string", + "minLength": 1 + }, + "footer": { + "type": "string", + "minLength": 0 + }, + "buttons": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["url"] + }, + "text": { + "type": "string", + "minLength": 1 + }, + "url": { + "type": "string", + "maxLength": 2048, + "format": "uri" + } + }, + "required": ["type", "text", "url"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["call"] + }, + "text": { + "type": "string", + "minLength": 1 + }, + "phoneNumber": { + "type": "string", + "pattern": "^\\+?\\d{6,20}$" + } + }, + "required": ["type", "text", "phoneNumber"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["reply"] + }, + "text": { + "type": "string", + "minLength": 1 + }, + "id": { + "type": "string", + "minLength": 1 + } + }, + "required": ["type", "text", "id"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["copy"] + }, + "text": { + "type": "string", + "minLength": 1 + }, + "copyCode": { + "type": "string", + "minLength": 1 + } + }, + "required": ["type", "text", "copyCode"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["catalog"] + }, + "text": { + "type": "string", + "minLength": 1 + }, + "businessPhoneNumber": { + "type": "string", + "pattern": "^\\+?\\d{6,20}$" + }, + "catalogProductId": { + "type": "string", + "minLength": 0 + } + }, + "required": ["type", "text", "businessPhoneNumber"] + } + ] + }, + "minItems": 1, + "maxItems": 10 + } + }, + "required": ["body", "buttons"], + "description": "Linked Devices support up to 10 reply buttons or up to 3 CTA buttons; reply and CTA buttons cannot be mixed. On an official WhatsApp Business number, at most 3 `reply` buttons are sent; other button types return `501`. There, `body` allows at most 1,024 characters, `title` and `footer` at most 60, and each button needs a unique `id` and a unique `text` of at most 20 characters. Every number also limits each button `id` to 256 UTF-8 bytes. Content outside any limit returns `400`." + } + }, + "required": ["buttons"], + "additionalProperties": false + } +} diff --git a/packages/typescript/src/messaging/types.ts b/packages/typescript/src/messaging/types.ts index 74da95d0..e185ec81 100644 --- a/packages/typescript/src/messaging/types.ts +++ b/packages/typescript/src/messaging/types.ts @@ -2175,6 +2175,8 @@ export type MessageButton = readonly catalogProductId?: string; }; +/** Linked Devices accepts ten replies or three CTAs, without mixing them. + * Official API replies remain limited to three. The API validates routing. */ export interface ButtonsMessageContent { readonly title?: string; readonly body: string; diff --git a/packages/typescript/test/messages.test.ts b/packages/typescript/test/messages.test.ts index 1e5ea03f..e672c54a 100644 --- a/packages/typescript/test/messages.test.ts +++ b/packages/typescript/test/messages.test.ts @@ -419,3 +419,33 @@ it("surfaces an uncertain accepted operation once and reads its status without a ).toBe("unknown"); expect(server.requests.map((value) => value.method)).toEqual(["POST", "GET"]); }); + +it("preserves ten linked replies and three CTA parameters without truncation", async () => { + const { client, requests } = await messagesServer(); + const combinations: MessageButton[][] = [ + Array.from({ length: 10 }, (_, index) => ({ + type: "reply", + id: `reply-${index}`, + text: `Choice ${index}`, + })), + [ + { type: "url", text: "Open", url: "https://example.test" }, + { type: "copy", text: "Copy", copyCode: "SYNTHETIC" }, + { + type: "catalog", + text: "Catalog", + businessPhoneNumber: "+15551234567", + catalogProductId: "123", + }, + ], + ]; + for (const buttons of combinations) { + const body: SendButtonsMessageRequest = { + transport: "linked_devices", + conversation: { phoneNumber: "+15551234567" }, + content: { buttons: { body: "Choose", buttons } }, + }; + await client.messages.send("number", body); + expect(JSON.parse(requests.at(-1)!.body)).toEqual(body); + } +}); From 0851f8a69ccfc5dfd8200e81b7c7f11dbe9a1d70 Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Sat, 10 Oct 2026 12:25:31 +0300 Subject: [PATCH 02/14] docs: reconcile native list and button candidate contract revision --- contracts/README.md | 5 +- ...buttons.json => interactive-messages.json} | 70 ++++++++++++++++++- 2 files changed, 72 insertions(+), 3 deletions(-) rename contracts/{interactive-buttons.json => interactive-messages.json} (64%) diff --git a/contracts/README.md b/contracts/README.md index f64587a7..5eabcd99 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -1,9 +1,10 @@ # Contract coverage -## Interactive button candidate +## Interactive message candidate -`interactive-buttons.json` pins the button branch to draft API PR #562. +`interactive-messages.json` pins button and list branches to draft API PR #562. Linked Devices accepts ten replies or three CTAs, without mixing them. +Native list bounds follow the thirty-row contract; Official lists retain ten rows. Official API replies retain the three-button limit. SDK arrays preserve all parameters. Replace the draft pin with the accepted API revision and verify matching staging before package publication. diff --git a/contracts/interactive-buttons.json b/contracts/interactive-messages.json similarity index 64% rename from contracts/interactive-buttons.json rename to contracts/interactive-messages.json index 794e8967..97dc0da7 100644 --- a/contracts/interactive-buttons.json +++ b/contracts/interactive-messages.json @@ -1,6 +1,6 @@ { "repository": "polymorfa/polymorfa", - "commit": "c3d8ac300b0ed34fee854bcd06b90e0bffd82c5c", + "commit": "eb5fc63a8d2f59077b681d138fe5436109cd9723", "sourcePath": "apps/api/docs/openapi.json", "sourceSha256": "a4cf9a2f34bba7eab3112deccd40b50ad0df838425eb48b09762ef8f857b62b4", "releaseStatus": "draft; requires accepted API revision and staging acceptance", @@ -133,5 +133,73 @@ }, "required": ["buttons"], "additionalProperties": false + }, + "listContent": { + "type": "object", + "properties": { + "list": { + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string", + "minLength": 0 + }, + "buttonText": { + "type": "string", + "minLength": 1 + }, + "footer": { + "type": "string", + "minLength": 0 + }, + "sections": { + "type": "array", + "items": { + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 0 + }, + "rows": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "title": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string", + "minLength": 0 + } + }, + "required": ["id", "title"] + }, + "minItems": 1, + "maxItems": 30 + } + }, + "required": ["rows"] + }, + "minItems": 1, + "maxItems": 10 + } + }, + "required": ["title", "buttonText", "sections"], + "description": "On an official WhatsApp Business number, `title` is the header (at most 60 characters) and `description` the body (at most 1,024 characters); without `description`, `title` is the body. There, each section title allows at most 24 characters and is required when the list has more than one section, lists allow at most 10 rows in total, row titles at most 24 characters, row IDs at most 200, row descriptions at most 72, `buttonText` at most 20 and `footer` at most 60. Every number also limits `buttonText` to 64 UTF-8 bytes, row `id` to 256 bytes and `title` and `description` to 256 and 4,096 bytes, so multibyte text reaches those byte limits before the character limits. Content outside any limit returns `400`." + } + }, + "required": ["list"], + "additionalProperties": false } } From f9759c480704f5366edaecbbfaaea8967f7f3ed6 Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Sat, 10 Oct 2026 13:03:20 +0300 Subject: [PATCH 03/14] Bind candidate interactive contract to forwarding dependency repair --- contracts/interactive-messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/interactive-messages.json b/contracts/interactive-messages.json index 97dc0da7..365e700f 100644 --- a/contracts/interactive-messages.json +++ b/contracts/interactive-messages.json @@ -1,6 +1,6 @@ { "repository": "polymorfa/polymorfa", - "commit": "eb5fc63a8d2f59077b681d138fe5436109cd9723", + "commit": "16eb5235b5cea4232e2a8fd90066c9366dc599e7", "sourcePath": "apps/api/docs/openapi.json", "sourceSha256": "a4cf9a2f34bba7eab3112deccd40b50ad0df838425eb48b09762ef8f857b62b4", "releaseStatus": "draft; requires accepted API revision and staging acceptance", From e10170606bb7cf1985fc14f848e98dee9bb4ce93 Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Sat, 10 Oct 2026 13:23:04 +0300 Subject: [PATCH 04/14] Pin interactive candidate to public-contract-safe forwarding revision --- contracts/interactive-messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/interactive-messages.json b/contracts/interactive-messages.json index 365e700f..0846f44b 100644 --- a/contracts/interactive-messages.json +++ b/contracts/interactive-messages.json @@ -1,6 +1,6 @@ { "repository": "polymorfa/polymorfa", - "commit": "16eb5235b5cea4232e2a8fd90066c9366dc599e7", + "commit": "8e31fe0a2a8fa7e8bfd2f9c7229236177be113d0", "sourcePath": "apps/api/docs/openapi.json", "sourceSha256": "a4cf9a2f34bba7eab3112deccd40b50ad0df838425eb48b09762ef8f857b62b4", "releaseStatus": "draft; requires accepted API revision and staging acceptance", From 52e327b5ca79a6f0bc72835b4e5d0b793cb209e3 Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Sat, 10 Oct 2026 13:40:53 +0300 Subject: [PATCH 05/14] chore: record lifecycle fixture correction in contract candidate --- contracts/interactive-messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/interactive-messages.json b/contracts/interactive-messages.json index 0846f44b..494fe9d1 100644 --- a/contracts/interactive-messages.json +++ b/contracts/interactive-messages.json @@ -1,6 +1,6 @@ { "repository": "polymorfa/polymorfa", - "commit": "8e31fe0a2a8fa7e8bfd2f9c7229236177be113d0", + "commit": "5086d6267e3cd1e09f68bc38a5d58be20055e4ac", "sourcePath": "apps/api/docs/openapi.json", "sourceSha256": "a4cf9a2f34bba7eab3112deccd40b50ad0df838425eb48b09762ef8f857b62b4", "releaseStatus": "draft; requires accepted API revision and staging acceptance", From fc5776a21a1ea52fa38335a9b1dc9bfe8b06f10d Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Sat, 10 Oct 2026 14:03:07 +0300 Subject: [PATCH 06/14] chore: bind native contract candidate to synchronized reference head --- contracts/interactive-messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/interactive-messages.json b/contracts/interactive-messages.json index 494fe9d1..d0ce1cb6 100644 --- a/contracts/interactive-messages.json +++ b/contracts/interactive-messages.json @@ -1,6 +1,6 @@ { "repository": "polymorfa/polymorfa", - "commit": "5086d6267e3cd1e09f68bc38a5d58be20055e4ac", + "commit": "d387698f62aef663d672ebf9f62aef453c70ee90", "sourcePath": "apps/api/docs/openapi.json", "sourceSha256": "a4cf9a2f34bba7eab3112deccd40b50ad0df838425eb48b09762ef8f857b62b4", "releaseStatus": "draft; requires accepted API revision and staging acceptance", From 2aa6d414aba6da272498d36c281e1d8857af0b35 Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Sat, 10 Oct 2026 14:16:32 +0300 Subject: [PATCH 07/14] Pin candidate contract to verified local media runtime revision --- contracts/interactive-messages.json | 80 +++++++++++++++++++++++------ 1 file changed, 63 insertions(+), 17 deletions(-) diff --git a/contracts/interactive-messages.json b/contracts/interactive-messages.json index d0ce1cb6..1f03db46 100644 --- a/contracts/interactive-messages.json +++ b/contracts/interactive-messages.json @@ -1,6 +1,6 @@ { "repository": "polymorfa/polymorfa", - "commit": "d387698f62aef663d672ebf9f62aef453c70ee90", + "commit": "7632dfaf96827170df564d01a0afe2a60e0d4509", "sourcePath": "apps/api/docs/openapi.json", "sourceSha256": "a4cf9a2f34bba7eab3112deccd40b50ad0df838425eb48b09762ef8f857b62b4", "releaseStatus": "draft; requires accepted API revision and staging acceptance", @@ -31,7 +31,9 @@ "properties": { "type": { "type": "string", - "enum": ["url"] + "enum": [ + "url" + ] }, "text": { "type": "string", @@ -43,14 +45,20 @@ "format": "uri" } }, - "required": ["type", "text", "url"] + "required": [ + "type", + "text", + "url" + ] }, { "type": "object", "properties": { "type": { "type": "string", - "enum": ["call"] + "enum": [ + "call" + ] }, "text": { "type": "string", @@ -61,14 +69,20 @@ "pattern": "^\\+?\\d{6,20}$" } }, - "required": ["type", "text", "phoneNumber"] + "required": [ + "type", + "text", + "phoneNumber" + ] }, { "type": "object", "properties": { "type": { "type": "string", - "enum": ["reply"] + "enum": [ + "reply" + ] }, "text": { "type": "string", @@ -79,14 +93,20 @@ "minLength": 1 } }, - "required": ["type", "text", "id"] + "required": [ + "type", + "text", + "id" + ] }, { "type": "object", "properties": { "type": { "type": "string", - "enum": ["copy"] + "enum": [ + "copy" + ] }, "text": { "type": "string", @@ -97,14 +117,20 @@ "minLength": 1 } }, - "required": ["type", "text", "copyCode"] + "required": [ + "type", + "text", + "copyCode" + ] }, { "type": "object", "properties": { "type": { "type": "string", - "enum": ["catalog"] + "enum": [ + "catalog" + ] }, "text": { "type": "string", @@ -119,7 +145,11 @@ "minLength": 0 } }, - "required": ["type", "text", "businessPhoneNumber"] + "required": [ + "type", + "text", + "businessPhoneNumber" + ] } ] }, @@ -127,11 +157,16 @@ "maxItems": 10 } }, - "required": ["body", "buttons"], + "required": [ + "body", + "buttons" + ], "description": "Linked Devices support up to 10 reply buttons or up to 3 CTA buttons; reply and CTA buttons cannot be mixed. On an official WhatsApp Business number, at most 3 `reply` buttons are sent; other button types return `501`. There, `body` allows at most 1,024 characters, `title` and `footer` at most 60, and each button needs a unique `id` and a unique `text` of at most 20 characters. Every number also limits each button `id` to 256 UTF-8 bytes. Content outside any limit returns `400`." } }, - "required": ["buttons"], + "required": [ + "buttons" + ], "additionalProperties": false }, "listContent": { @@ -183,23 +218,34 @@ "minLength": 0 } }, - "required": ["id", "title"] + "required": [ + "id", + "title" + ] }, "minItems": 1, "maxItems": 30 } }, - "required": ["rows"] + "required": [ + "rows" + ] }, "minItems": 1, "maxItems": 10 } }, - "required": ["title", "buttonText", "sections"], + "required": [ + "title", + "buttonText", + "sections" + ], "description": "On an official WhatsApp Business number, `title` is the header (at most 60 characters) and `description` the body (at most 1,024 characters); without `description`, `title` is the body. There, each section title allows at most 24 characters and is required when the list has more than one section, lists allow at most 10 rows in total, row titles at most 24 characters, row IDs at most 200, row descriptions at most 72, `buttonText` at most 20 and `footer` at most 60. Every number also limits `buttonText` to 64 UTF-8 bytes, row `id` to 256 bytes and `title` and `description` to 256 and 4,096 bytes, so multibyte text reaches those byte limits before the character limits. Content outside any limit returns `400`." } }, - "required": ["list"], + "required": [ + "list" + ], "additionalProperties": false } } From 6565b620d8c9875c9499ff7ace341e184d7b5879 Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Sat, 10 Oct 2026 14:20:33 +0300 Subject: [PATCH 08/14] Preserve candidate contract formatter output --- contracts/interactive-messages.json | 78 ++++++----------------------- 1 file changed, 16 insertions(+), 62 deletions(-) diff --git a/contracts/interactive-messages.json b/contracts/interactive-messages.json index 1f03db46..ff980170 100644 --- a/contracts/interactive-messages.json +++ b/contracts/interactive-messages.json @@ -31,9 +31,7 @@ "properties": { "type": { "type": "string", - "enum": [ - "url" - ] + "enum": ["url"] }, "text": { "type": "string", @@ -45,20 +43,14 @@ "format": "uri" } }, - "required": [ - "type", - "text", - "url" - ] + "required": ["type", "text", "url"] }, { "type": "object", "properties": { "type": { "type": "string", - "enum": [ - "call" - ] + "enum": ["call"] }, "text": { "type": "string", @@ -69,20 +61,14 @@ "pattern": "^\\+?\\d{6,20}$" } }, - "required": [ - "type", - "text", - "phoneNumber" - ] + "required": ["type", "text", "phoneNumber"] }, { "type": "object", "properties": { "type": { "type": "string", - "enum": [ - "reply" - ] + "enum": ["reply"] }, "text": { "type": "string", @@ -93,20 +79,14 @@ "minLength": 1 } }, - "required": [ - "type", - "text", - "id" - ] + "required": ["type", "text", "id"] }, { "type": "object", "properties": { "type": { "type": "string", - "enum": [ - "copy" - ] + "enum": ["copy"] }, "text": { "type": "string", @@ -117,20 +97,14 @@ "minLength": 1 } }, - "required": [ - "type", - "text", - "copyCode" - ] + "required": ["type", "text", "copyCode"] }, { "type": "object", "properties": { "type": { "type": "string", - "enum": [ - "catalog" - ] + "enum": ["catalog"] }, "text": { "type": "string", @@ -145,11 +119,7 @@ "minLength": 0 } }, - "required": [ - "type", - "text", - "businessPhoneNumber" - ] + "required": ["type", "text", "businessPhoneNumber"] } ] }, @@ -157,16 +127,11 @@ "maxItems": 10 } }, - "required": [ - "body", - "buttons" - ], + "required": ["body", "buttons"], "description": "Linked Devices support up to 10 reply buttons or up to 3 CTA buttons; reply and CTA buttons cannot be mixed. On an official WhatsApp Business number, at most 3 `reply` buttons are sent; other button types return `501`. There, `body` allows at most 1,024 characters, `title` and `footer` at most 60, and each button needs a unique `id` and a unique `text` of at most 20 characters. Every number also limits each button `id` to 256 UTF-8 bytes. Content outside any limit returns `400`." } }, - "required": [ - "buttons" - ], + "required": ["buttons"], "additionalProperties": false }, "listContent": { @@ -218,34 +183,23 @@ "minLength": 0 } }, - "required": [ - "id", - "title" - ] + "required": ["id", "title"] }, "minItems": 1, "maxItems": 30 } }, - "required": [ - "rows" - ] + "required": ["rows"] }, "minItems": 1, "maxItems": 10 } }, - "required": [ - "title", - "buttonText", - "sections" - ], + "required": ["title", "buttonText", "sections"], "description": "On an official WhatsApp Business number, `title` is the header (at most 60 characters) and `description` the body (at most 1,024 characters); without `description`, `title` is the body. There, each section title allows at most 24 characters and is required when the list has more than one section, lists allow at most 10 rows in total, row titles at most 24 characters, row IDs at most 200, row descriptions at most 72, `buttonText` at most 20 and `footer` at most 60. Every number also limits `buttonText` to 64 UTF-8 bytes, row `id` to 256 bytes and `title` and `description` to 256 and 4,096 bytes, so multibyte text reaches those byte limits before the character limits. Content outside any limit returns `400`." } }, - "required": [ - "list" - ], + "required": ["list"], "additionalProperties": false } } From abaa525f88d6c6f94c6d0a73258be9eef239a2f4 Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Sat, 10 Oct 2026 14:36:03 +0300 Subject: [PATCH 09/14] Bind candidate to signed group identity forwarding revision --- contracts/interactive-messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/interactive-messages.json b/contracts/interactive-messages.json index ff980170..8c982e9b 100644 --- a/contracts/interactive-messages.json +++ b/contracts/interactive-messages.json @@ -1,6 +1,6 @@ { "repository": "polymorfa/polymorfa", - "commit": "7632dfaf96827170df564d01a0afe2a60e0d4509", + "commit": "f8d2a95ab6f5dbf9ce79769b1d7fca6d2607d98d", "sourcePath": "apps/api/docs/openapi.json", "sourceSha256": "a4cf9a2f34bba7eab3112deccd40b50ad0df838425eb48b09762ef8f857b62b4", "releaseStatus": "draft; requires accepted API revision and staging acceptance", From 40a9f28c799f9519ee7af822a9912325bacdf3c7 Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Sat, 10 Oct 2026 15:11:16 +0300 Subject: [PATCH 10/14] Bind native interactive candidate to group sender-copy repair --- contracts/interactive-messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/interactive-messages.json b/contracts/interactive-messages.json index 8c982e9b..44a52567 100644 --- a/contracts/interactive-messages.json +++ b/contracts/interactive-messages.json @@ -1,6 +1,6 @@ { "repository": "polymorfa/polymorfa", - "commit": "f8d2a95ab6f5dbf9ce79769b1d7fca6d2607d98d", + "commit": "10d5db5fc7bb7190c134d12191800d6a48fcf9f7", "sourcePath": "apps/api/docs/openapi.json", "sourceSha256": "a4cf9a2f34bba7eab3112deccd40b50ad0df838425eb48b09762ef8f857b62b4", "releaseStatus": "draft; requires accepted API revision and staging acceptance", From 4589e7f3a972366d3f897ec965bf8526eb3f3355 Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Wed, 14 Oct 2026 23:01:18 +0300 Subject: [PATCH 11/14] feat: type mixed native buttons, media headers and carousels --- CHANGELOG.md | 7 + contracts/README.md | 4 +- contracts/interactive-messages.json | 349 ++++++++++++++++++++- packages/typescript/README.md | 31 ++ packages/typescript/src/index.ts | 5 + packages/typescript/src/messaging/types.ts | 68 +++- packages/typescript/test/messages.test.ts | 56 +++- 7 files changed, 505 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ebc3463..f580134c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## Unreleased +- Interactive-message candidate (API PR #562, not deployed): Linked Devices + supports ten total buttons, up to three CTAs, mixed replies with URL/call + controls, image/video/document headers, and one 2–10-card image/video + carousel. Reply IDs and per-card controls are preserved. Copy/catalog controls + cannot mix with replies. Official API limits and approval remain separate. + Package publication requires the accepted API revision and matching staging. + - Receipt-timed campaign follow-ups (beta). Messaging and Platform campaign create and update accept `followUp` (`CampaignFollowUp`): one text or template message sent from the first message's number, timed from its first diff --git a/contracts/README.md b/contracts/README.md index 4cc4d870..29017d14 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -2,8 +2,8 @@ ## Interactive message candidate -`interactive-messages.json` pins button and list branches to draft API PR #562. -Linked Devices accepts ten replies or three CTAs, without mixing them. +`interactive-messages.json` pins button, carousel and list branches to draft API PR #562. +Linked Devices accepts ten total controls and at most three CTAs. Replies can mix with URL/call; copy/catalog cannot mix with replies. Carousels contain 2–10 image/video cards with homogeneous controls within each card. Media headers and carousel payloads remain one send. Native list bounds follow the thirty-row contract; Official lists retain ten rows. Official API replies retain the three-button limit. SDK arrays preserve all parameters. Replace the draft pin with the accepted API revision and verify diff --git a/contracts/interactive-messages.json b/contracts/interactive-messages.json index 44a52567..ec07c01e 100644 --- a/contracts/interactive-messages.json +++ b/contracts/interactive-messages.json @@ -1,8 +1,8 @@ { "repository": "polymorfa/polymorfa", - "commit": "10d5db5fc7bb7190c134d12191800d6a48fcf9f7", + "commit": "eec8ba771b36c6cf885f1805cc23f6ac56e1a217", "sourcePath": "apps/api/docs/openapi.json", - "sourceSha256": "a4cf9a2f34bba7eab3112deccd40b50ad0df838425eb48b09762ef8f857b62b4", + "sourceSha256": "4375210d40d5bd21cf0605bd89d73eabc41a32f67613cd6a56027e8704005f1f", "releaseStatus": "draft; requires accepted API revision and staging acceptance", "buttonsContent": { "type": "object", @@ -10,6 +10,41 @@ "buttons": { "type": "object", "properties": { + "media": { + "type": "object", + "properties": { + "url": { + "type": "string", + "maxLength": 4096, + "format": "uri" + }, + "base64": { + "type": "string", + "minLength": 1, + "maxLength": 70000000 + }, + "mimeType": { + "type": "string", + "minLength": 0 + }, + "type": { + "type": "string", + "enum": ["image", "video", "document"] + }, + "filename": { + "type": "string", + "minLength": 0 + }, + "thumbnail": { + "type": "string", + "minLength": 4, + "maxLength": 21848, + "description": "Base64-encoded JPEG preview shown before the media downloads, at most 16 KiB. Linked Devices numbers only." + } + }, + "required": ["type"], + "additionalProperties": false + }, "title": { "type": "string", "minLength": 0 @@ -128,7 +163,7 @@ } }, "required": ["body", "buttons"], - "description": "Linked Devices support up to 10 reply buttons or up to 3 CTA buttons; reply and CTA buttons cannot be mixed. On an official WhatsApp Business number, at most 3 `reply` buttons are sent; other button types return `501`. There, `body` allows at most 1,024 characters, `title` and `footer` at most 60, and each button needs a unique `id` and a unique `text` of at most 20 characters. Every number also limits each button `id` to 256 UTF-8 bytes. Content outside any limit returns `400`." + "description": "Linked Devices support up to 10 total buttons and at most 3 CTAs. Replies may be mixed with URL and call buttons using a hydrated template; copy/catalog buttons cannot be mixed with replies. An optional image, video or document header requires exactly one URL or base64 source and cannot be combined with a title. On an official WhatsApp Business number, media headers require an approved template, and at most 3 `reply` buttons are sent; other button types return `501`. There, `body` allows at most 1,024 characters, `title` and `footer` at most 60, and each button needs a unique `id` and a unique `text` of at most 20 characters. Every number also limits each button `id` to 256 UTF-8 bytes. Content outside any limit returns `400`." } }, "required": ["buttons"], @@ -201,5 +236,313 @@ }, "required": ["list"], "additionalProperties": false + }, + "carouselContent": { + "type": "object", + "properties": { + "carousel": { + "type": "object", + "properties": { + "body": { + "type": "string", + "minLength": 1 + }, + "footer": { + "type": "string", + "minLength": 0 + }, + "cards": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "body": { + "type": "string", + "minLength": 1 + }, + "buttons": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["url"] + }, + "text": { + "type": "string", + "minLength": 1 + }, + "url": { + "type": "string", + "maxLength": 2048, + "format": "uri" + } + }, + "required": ["type", "text", "url"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["call"] + }, + "text": { + "type": "string", + "minLength": 1 + }, + "phoneNumber": { + "type": "string", + "pattern": "^\\+?\\d{6,20}$" + } + }, + "required": ["type", "text", "phoneNumber"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["reply"] + }, + "text": { + "type": "string", + "minLength": 1 + }, + "id": { + "type": "string", + "minLength": 1 + } + }, + "required": ["type", "text", "id"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["copy"] + }, + "text": { + "type": "string", + "minLength": 1 + }, + "copyCode": { + "type": "string", + "minLength": 1 + } + }, + "required": ["type", "text", "copyCode"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["catalog"] + }, + "text": { + "type": "string", + "minLength": 1 + }, + "businessPhoneNumber": { + "type": "string", + "pattern": "^\\+?\\d{6,20}$" + }, + "catalogProductId": { + "type": "string", + "minLength": 0 + } + }, + "required": ["type", "text", "businessPhoneNumber"] + } + ] + }, + "minItems": 1, + "maxItems": 10 + }, + "image": { + "type": "object", + "properties": { + "url": { + "type": "string", + "maxLength": 4096, + "format": "uri" + }, + "base64": { + "type": "string", + "minLength": 1, + "maxLength": 70000000 + }, + "mimeType": { + "type": "string", + "minLength": 0 + } + }, + "additionalProperties": false + } + }, + "required": ["body", "buttons", "image"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "body": { + "type": "string", + "minLength": 1 + }, + "buttons": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["url"] + }, + "text": { + "type": "string", + "minLength": 1 + }, + "url": { + "type": "string", + "maxLength": 2048, + "format": "uri" + } + }, + "required": ["type", "text", "url"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["call"] + }, + "text": { + "type": "string", + "minLength": 1 + }, + "phoneNumber": { + "type": "string", + "pattern": "^\\+?\\d{6,20}$" + } + }, + "required": ["type", "text", "phoneNumber"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["reply"] + }, + "text": { + "type": "string", + "minLength": 1 + }, + "id": { + "type": "string", + "minLength": 1 + } + }, + "required": ["type", "text", "id"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["copy"] + }, + "text": { + "type": "string", + "minLength": 1 + }, + "copyCode": { + "type": "string", + "minLength": 1 + } + }, + "required": ["type", "text", "copyCode"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["catalog"] + }, + "text": { + "type": "string", + "minLength": 1 + }, + "businessPhoneNumber": { + "type": "string", + "pattern": "^\\+?\\d{6,20}$" + }, + "catalogProductId": { + "type": "string", + "minLength": 0 + } + }, + "required": ["type", "text", "businessPhoneNumber"] + } + ] + }, + "minItems": 1, + "maxItems": 10 + }, + "video": { + "type": "object", + "properties": { + "url": { + "type": "string", + "maxLength": 4096, + "format": "uri" + }, + "base64": { + "type": "string", + "minLength": 1, + "maxLength": 70000000 + }, + "mimeType": { + "type": "string", + "minLength": 0 + }, + "thumbnail": { + "type": "string", + "minLength": 4, + "maxLength": 21848, + "description": "Base64-encoded JPEG preview shown before the media downloads, at most 16 KiB. Linked Devices numbers only." + } + }, + "additionalProperties": false + } + }, + "required": ["body", "buttons", "video"], + "additionalProperties": false + } + ] + }, + "minItems": 2, + "maxItems": 10 + } + }, + "required": ["body", "cards"], + "additionalProperties": false, + "description": "Linked Devices only: one message with 2\u201310 image/video cards and native buttons per card. Each card accepts up to ten replies or three CTAs without mixing; reply IDs are unique across cards. Combined media is bounded to 50 MiB. Official approved carousel templates use content.template instead." + } + }, + "required": ["carousel"], + "additionalProperties": false } } diff --git a/packages/typescript/README.md b/packages/typescript/README.md index 09223233..3e53f3b5 100644 --- a/packages/typescript/README.md +++ b/packages/typescript/README.md @@ -3168,3 +3168,34 @@ configuration changes that do not change capabilities produce no event. Treat the event ID as the deduplication key. Eligibility is checked before publication and delivery; withdrawn events wait for re-enablement and expire after seven days. These additions require an API and SDK release containing this contract. + +### Interactive-message candidate + +These types target draft API PR #562. They require its backend implementation; +this source change does not establish hosted availability or package publication. +Linked Devices permits ten total controls with at most three CTAs. Replies can +mix with URL/call controls; copy and catalog controls cannot mix with replies. +Image, video and document headers use exactly one URL or base64 source. + +```ts +await messaging.messages.send(sessionId, { + conversation: { phoneNumber: "+15551234567" }, + content: { + buttons: { + body: "Choose an option", + media: { type: "image", url: "https://example.com/header.jpg" }, + buttons: [ + { type: "reply", text: "Interested", id: "offer-interested" }, + { type: "url", text: "Details", url: "https://example.com/offer" }, + ], + }, + }, +}); +``` + +`CarouselMessageContent` contains a body, optional footer and 2–10 cards. +Each card has a body, optional footer, native buttons, and exactly one `image` +or `video` source. Native controls within each card share a category, and reply +IDs must be unique across all cards. One carousel is one send; clients do not +split it into separately metered messages. Official API media buttons and +carousels use approved templates through their existing template route. diff --git a/packages/typescript/src/index.ts b/packages/typescript/src/index.ts index 4458cc41..afb20a31 100644 --- a/packages/typescript/src/index.ts +++ b/packages/typescript/src/index.ts @@ -311,6 +311,10 @@ export type { BartenderMode, AsyncAcceptedData, ButtonsMessageContent, + ButtonsMedia, + InteractiveMediaSource, + CarouselCard, + CarouselMessageContent, BusinessProfile, BusinessProfileCategory, BusinessProfileHours, @@ -594,6 +598,7 @@ export type { SeenRequest, SendAddressMessageRequest, SendButtonsMessageRequest, + SendCarouselMessageRequest, SendCallPermissionRequestMessageRequest, SendOrderDetailsMessageRequest, SendOrderStatusMessageRequest, diff --git a/packages/typescript/src/messaging/types.ts b/packages/typescript/src/messaging/types.ts index d3e14abd..4d483fe5 100644 --- a/packages/typescript/src/messaging/types.ts +++ b/packages/typescript/src/messaging/types.ts @@ -2394,13 +2394,68 @@ export type MessageButton = readonly catalogProductId?: string; }; -/** Linked Devices accepts ten replies or three CTAs, without mixing them. - * Official API replies remain limited to three. The API validates routing. */ -export interface ButtonsMessageContent { - readonly title?: string; +/** One media source; the API enforces size, URL and decoded media limits. */ +export type InteractiveMediaSource = { + readonly mimeType?: string; +} & ( + | { readonly url: string; readonly base64?: never } + | { readonly base64: string; readonly url?: never } +); + +export type ButtonsMedia = InteractiveMediaSource & + ( + | { + readonly type: "image"; + readonly thumbnail?: never; + readonly filename?: never; + } + | { + readonly type: "video"; + readonly thumbnail?: string; + readonly filename?: never; + } + | { + readonly type: "document"; + readonly filename?: string; + readonly thumbnail?: never; + } + ); + +/** Linked Devices accepts ten total buttons with at most three CTAs. + * Replies can mix with URL/call; copy/catalog cannot mix with replies. + * Official API accepts three replies; media headers require approved templates. */ +export type ButtonsMessageContent = { + readonly body: string; + readonly footer?: string; + readonly buttons: readonly MessageButton[]; +} & ( + | { readonly title?: string; readonly media?: never } + | { readonly title?: never; readonly media: ButtonsMedia } +); + +/** Each card has replies or CTAs; IDs are unique across the complete carousel. */ +export type CarouselCard = { readonly body: string; readonly footer?: string; readonly buttons: readonly MessageButton[]; +} & ( + | { + readonly image: InteractiveMediaSource; + readonly video?: never; + readonly thumbnail?: never; + } + | { + readonly video: InteractiveMediaSource & { readonly thumbnail?: string }; + readonly image?: never; + readonly thumbnail?: never; + } +); + +/** Linked Devices only: 2–10 image/video cards, at most 50 MiB total media. */ +export interface CarouselMessageContent { + readonly body: string; + readonly footer?: string; + readonly cards: readonly CarouselCard[]; } export interface AddressMessageContent { @@ -2528,6 +2583,10 @@ export interface SendListMessageRequest extends MessageSendContext { readonly content: { readonly list: ListMessageContent }; } +export interface SendCarouselMessageRequest extends MessageSendContext { + readonly content: { readonly carousel: CarouselMessageContent }; +} + export interface SendButtonsMessageRequest extends MessageSendContext { readonly content: { readonly buttons: ButtonsMessageContent }; } @@ -2706,6 +2765,7 @@ export type SendMessageRequest = | SendOrderMessageRequest | SendListMessageRequest | SendButtonsMessageRequest + | SendCarouselMessageRequest | SendAddressMessageRequest | SendFlowMessageRequest | SendCallPermissionRequestMessageRequest diff --git a/packages/typescript/test/messages.test.ts b/packages/typescript/test/messages.test.ts index e672c54a..42de637c 100644 --- a/packages/typescript/test/messages.test.ts +++ b/packages/typescript/test/messages.test.ts @@ -6,6 +6,7 @@ import { graphTransportHeaders, type AddressMessageContent, type ButtonsMessageContent, + type SendCarouselMessageRequest, type FlowMessageContent, type ListMessageContent, type MessageButton, @@ -122,12 +123,8 @@ describe("MessagingClient message types", () => { readonly catalogProductId?: string; } >(); - expectTypeOf().toEqualTypeOf<{ - readonly title?: string; - readonly body: string; - readonly footer?: string; - readonly buttons: readonly MessageButton[]; - }>(); + expectTypeOf().toHaveProperty("buttons"); + expectTypeOf().toHaveProperty("media"); expectTypeOf().toHaveProperty("sections"); expectTypeOf().toHaveProperty("businessOwnerId"); expectTypeOf().toHaveProperty("sections"); @@ -153,6 +150,7 @@ describe("MessagingClient message types", () => { expectTypeOf().toMatchTypeOf(); expectTypeOf().toMatchTypeOf(); expectTypeOf().toMatchTypeOf(); + expectTypeOf().toMatchTypeOf(); expectTypeOf().toMatchTypeOf(); expectTypeOf().toMatchTypeOf(); expectTypeOf().toMatchTypeOf(); @@ -449,3 +447,49 @@ it("preserves ten linked replies and three CTA parameters without truncation", a expect(JSON.parse(requests.at(-1)!.body)).toEqual(body); } }); + +it.each([2, 10])( + "preserves one %i-card request and every card's media and reply ID", + async (count) => { + const { client, requests } = await messagesServer(); + const body: SendCarouselMessageRequest = { + conversation: { phoneNumber: "+15551234567" }, + content: { + carousel: { + body: "Offers", + cards: Array.from({ length: count }, (_, index) => ({ + body: `Card ${index}`, + image: { base64: "c3ludGhldGlj", mimeType: "image/png" }, + buttons: [{ type: "reply", text: "Choose", id: `card-${index}` }], + })), + }, + }, + }; + await client.messages.send("support", body, { + idempotencyKey: "carousel-once", + }); + expect(requests).toHaveLength(1); + expect(JSON.parse(requests[0]!.body)).toEqual(body); + expect(requests[0]!.headers["idempotency-key"]).toBe("carousel-once"); + }, +); + +it("preserves mixed controls and a media header without splitting transport", async () => { + const { client, requests } = await messagesServer(); + const body: SendButtonsMessageRequest = { + conversation: { phoneNumber: "+15551234567" }, + content: { + buttons: { + body: "Offer", + media: { type: "image", base64: "c3ludGhldGlj", mimeType: "image/png" }, + buttons: [ + { type: "reply", text: "Choose", id: "stable" }, + { type: "url", text: "Open", url: "https://example.com" }, + ], + }, + }, + }; + await client.messages.send("support", body, { idempotencyKey: "mixed-once" }); + expect(requests).toHaveLength(1); + expect(JSON.parse(requests[0]!.body)).toEqual(body); +}); From c360296acb0eac61efc86199d0d16b0d3202cf6f Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Wed, 14 Oct 2026 23:18:19 +0300 Subject: [PATCH 12/14] docs: bind interactive contract to integrated backend candidate --- contracts/interactive-messages.json | 211 ++++++++++++++++++++++------ 1 file changed, 168 insertions(+), 43 deletions(-) diff --git a/contracts/interactive-messages.json b/contracts/interactive-messages.json index ec07c01e..315e2235 100644 --- a/contracts/interactive-messages.json +++ b/contracts/interactive-messages.json @@ -1,6 +1,6 @@ { "repository": "polymorfa/polymorfa", - "commit": "eec8ba771b36c6cf885f1805cc23f6ac56e1a217", + "commit": "99c36bd39f51a2c1da309782fdbe0963597f68ca", "sourcePath": "apps/api/docs/openapi.json", "sourceSha256": "4375210d40d5bd21cf0605bd89d73eabc41a32f67613cd6a56027e8704005f1f", "releaseStatus": "draft; requires accepted API revision and staging acceptance", @@ -29,7 +29,11 @@ }, "type": { "type": "string", - "enum": ["image", "video", "document"] + "enum": [ + "image", + "video", + "document" + ] }, "filename": { "type": "string", @@ -42,7 +46,9 @@ "description": "Base64-encoded JPEG preview shown before the media downloads, at most 16 KiB. Linked Devices numbers only." } }, - "required": ["type"], + "required": [ + "type" + ], "additionalProperties": false }, "title": { @@ -66,7 +72,9 @@ "properties": { "type": { "type": "string", - "enum": ["url"] + "enum": [ + "url" + ] }, "text": { "type": "string", @@ -78,14 +86,20 @@ "format": "uri" } }, - "required": ["type", "text", "url"] + "required": [ + "type", + "text", + "url" + ] }, { "type": "object", "properties": { "type": { "type": "string", - "enum": ["call"] + "enum": [ + "call" + ] }, "text": { "type": "string", @@ -96,14 +110,20 @@ "pattern": "^\\+?\\d{6,20}$" } }, - "required": ["type", "text", "phoneNumber"] + "required": [ + "type", + "text", + "phoneNumber" + ] }, { "type": "object", "properties": { "type": { "type": "string", - "enum": ["reply"] + "enum": [ + "reply" + ] }, "text": { "type": "string", @@ -114,14 +134,20 @@ "minLength": 1 } }, - "required": ["type", "text", "id"] + "required": [ + "type", + "text", + "id" + ] }, { "type": "object", "properties": { "type": { "type": "string", - "enum": ["copy"] + "enum": [ + "copy" + ] }, "text": { "type": "string", @@ -132,14 +158,20 @@ "minLength": 1 } }, - "required": ["type", "text", "copyCode"] + "required": [ + "type", + "text", + "copyCode" + ] }, { "type": "object", "properties": { "type": { "type": "string", - "enum": ["catalog"] + "enum": [ + "catalog" + ] }, "text": { "type": "string", @@ -154,7 +186,11 @@ "minLength": 0 } }, - "required": ["type", "text", "businessPhoneNumber"] + "required": [ + "type", + "text", + "businessPhoneNumber" + ] } ] }, @@ -162,11 +198,16 @@ "maxItems": 10 } }, - "required": ["body", "buttons"], + "required": [ + "body", + "buttons" + ], "description": "Linked Devices support up to 10 total buttons and at most 3 CTAs. Replies may be mixed with URL and call buttons using a hydrated template; copy/catalog buttons cannot be mixed with replies. An optional image, video or document header requires exactly one URL or base64 source and cannot be combined with a title. On an official WhatsApp Business number, media headers require an approved template, and at most 3 `reply` buttons are sent; other button types return `501`. There, `body` allows at most 1,024 characters, `title` and `footer` at most 60, and each button needs a unique `id` and a unique `text` of at most 20 characters. Every number also limits each button `id` to 256 UTF-8 bytes. Content outside any limit returns `400`." } }, - "required": ["buttons"], + "required": [ + "buttons" + ], "additionalProperties": false }, "listContent": { @@ -218,23 +259,34 @@ "minLength": 0 } }, - "required": ["id", "title"] + "required": [ + "id", + "title" + ] }, "minItems": 1, "maxItems": 30 } }, - "required": ["rows"] + "required": [ + "rows" + ] }, "minItems": 1, "maxItems": 10 } }, - "required": ["title", "buttonText", "sections"], + "required": [ + "title", + "buttonText", + "sections" + ], "description": "On an official WhatsApp Business number, `title` is the header (at most 60 characters) and `description` the body (at most 1,024 characters); without `description`, `title` is the body. There, each section title allows at most 24 characters and is required when the list has more than one section, lists allow at most 10 rows in total, row titles at most 24 characters, row IDs at most 200, row descriptions at most 72, `buttonText` at most 20 and `footer` at most 60. Every number also limits `buttonText` to 64 UTF-8 bytes, row `id` to 256 bytes and `title` and `description` to 256 and 4,096 bytes, so multibyte text reaches those byte limits before the character limits. Content outside any limit returns `400`." } }, - "required": ["list"], + "required": [ + "list" + ], "additionalProperties": false }, "carouselContent": { @@ -271,7 +323,9 @@ "properties": { "type": { "type": "string", - "enum": ["url"] + "enum": [ + "url" + ] }, "text": { "type": "string", @@ -283,14 +337,20 @@ "format": "uri" } }, - "required": ["type", "text", "url"] + "required": [ + "type", + "text", + "url" + ] }, { "type": "object", "properties": { "type": { "type": "string", - "enum": ["call"] + "enum": [ + "call" + ] }, "text": { "type": "string", @@ -301,14 +361,20 @@ "pattern": "^\\+?\\d{6,20}$" } }, - "required": ["type", "text", "phoneNumber"] + "required": [ + "type", + "text", + "phoneNumber" + ] }, { "type": "object", "properties": { "type": { "type": "string", - "enum": ["reply"] + "enum": [ + "reply" + ] }, "text": { "type": "string", @@ -319,14 +385,20 @@ "minLength": 1 } }, - "required": ["type", "text", "id"] + "required": [ + "type", + "text", + "id" + ] }, { "type": "object", "properties": { "type": { "type": "string", - "enum": ["copy"] + "enum": [ + "copy" + ] }, "text": { "type": "string", @@ -337,14 +409,20 @@ "minLength": 1 } }, - "required": ["type", "text", "copyCode"] + "required": [ + "type", + "text", + "copyCode" + ] }, { "type": "object", "properties": { "type": { "type": "string", - "enum": ["catalog"] + "enum": [ + "catalog" + ] }, "text": { "type": "string", @@ -359,7 +437,11 @@ "minLength": 0 } }, - "required": ["type", "text", "businessPhoneNumber"] + "required": [ + "type", + "text", + "businessPhoneNumber" + ] } ] }, @@ -387,7 +469,11 @@ "additionalProperties": false } }, - "required": ["body", "buttons", "image"], + "required": [ + "body", + "buttons", + "image" + ], "additionalProperties": false }, { @@ -406,7 +492,9 @@ "properties": { "type": { "type": "string", - "enum": ["url"] + "enum": [ + "url" + ] }, "text": { "type": "string", @@ -418,14 +506,20 @@ "format": "uri" } }, - "required": ["type", "text", "url"] + "required": [ + "type", + "text", + "url" + ] }, { "type": "object", "properties": { "type": { "type": "string", - "enum": ["call"] + "enum": [ + "call" + ] }, "text": { "type": "string", @@ -436,14 +530,20 @@ "pattern": "^\\+?\\d{6,20}$" } }, - "required": ["type", "text", "phoneNumber"] + "required": [ + "type", + "text", + "phoneNumber" + ] }, { "type": "object", "properties": { "type": { "type": "string", - "enum": ["reply"] + "enum": [ + "reply" + ] }, "text": { "type": "string", @@ -454,14 +554,20 @@ "minLength": 1 } }, - "required": ["type", "text", "id"] + "required": [ + "type", + "text", + "id" + ] }, { "type": "object", "properties": { "type": { "type": "string", - "enum": ["copy"] + "enum": [ + "copy" + ] }, "text": { "type": "string", @@ -472,14 +578,20 @@ "minLength": 1 } }, - "required": ["type", "text", "copyCode"] + "required": [ + "type", + "text", + "copyCode" + ] }, { "type": "object", "properties": { "type": { "type": "string", - "enum": ["catalog"] + "enum": [ + "catalog" + ] }, "text": { "type": "string", @@ -494,7 +606,11 @@ "minLength": 0 } }, - "required": ["type", "text", "businessPhoneNumber"] + "required": [ + "type", + "text", + "businessPhoneNumber" + ] } ] }, @@ -528,7 +644,11 @@ "additionalProperties": false } }, - "required": ["body", "buttons", "video"], + "required": [ + "body", + "buttons", + "video" + ], "additionalProperties": false } ] @@ -537,12 +657,17 @@ "maxItems": 10 } }, - "required": ["body", "cards"], + "required": [ + "body", + "cards" + ], "additionalProperties": false, "description": "Linked Devices only: one message with 2\u201310 image/video cards and native buttons per card. Each card accepts up to ten replies or three CTAs without mixing; reply IDs are unique across cards. Combined media is bounded to 50 MiB. Official approved carousel templates use content.template instead." } }, - "required": ["carousel"], + "required": [ + "carousel" + ], "additionalProperties": false } } From aa50f8b7781fbb090f5cca158a2b3a94b9f5001e Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Wed, 14 Oct 2026 23:19:46 +0300 Subject: [PATCH 13/14] style: retain contract snapshot formatting --- contracts/interactive-messages.json | 209 ++++++---------------------- 1 file changed, 42 insertions(+), 167 deletions(-) diff --git a/contracts/interactive-messages.json b/contracts/interactive-messages.json index 315e2235..20c85024 100644 --- a/contracts/interactive-messages.json +++ b/contracts/interactive-messages.json @@ -29,11 +29,7 @@ }, "type": { "type": "string", - "enum": [ - "image", - "video", - "document" - ] + "enum": ["image", "video", "document"] }, "filename": { "type": "string", @@ -46,9 +42,7 @@ "description": "Base64-encoded JPEG preview shown before the media downloads, at most 16 KiB. Linked Devices numbers only." } }, - "required": [ - "type" - ], + "required": ["type"], "additionalProperties": false }, "title": { @@ -72,9 +66,7 @@ "properties": { "type": { "type": "string", - "enum": [ - "url" - ] + "enum": ["url"] }, "text": { "type": "string", @@ -86,20 +78,14 @@ "format": "uri" } }, - "required": [ - "type", - "text", - "url" - ] + "required": ["type", "text", "url"] }, { "type": "object", "properties": { "type": { "type": "string", - "enum": [ - "call" - ] + "enum": ["call"] }, "text": { "type": "string", @@ -110,20 +96,14 @@ "pattern": "^\\+?\\d{6,20}$" } }, - "required": [ - "type", - "text", - "phoneNumber" - ] + "required": ["type", "text", "phoneNumber"] }, { "type": "object", "properties": { "type": { "type": "string", - "enum": [ - "reply" - ] + "enum": ["reply"] }, "text": { "type": "string", @@ -134,20 +114,14 @@ "minLength": 1 } }, - "required": [ - "type", - "text", - "id" - ] + "required": ["type", "text", "id"] }, { "type": "object", "properties": { "type": { "type": "string", - "enum": [ - "copy" - ] + "enum": ["copy"] }, "text": { "type": "string", @@ -158,20 +132,14 @@ "minLength": 1 } }, - "required": [ - "type", - "text", - "copyCode" - ] + "required": ["type", "text", "copyCode"] }, { "type": "object", "properties": { "type": { "type": "string", - "enum": [ - "catalog" - ] + "enum": ["catalog"] }, "text": { "type": "string", @@ -186,11 +154,7 @@ "minLength": 0 } }, - "required": [ - "type", - "text", - "businessPhoneNumber" - ] + "required": ["type", "text", "businessPhoneNumber"] } ] }, @@ -198,16 +162,11 @@ "maxItems": 10 } }, - "required": [ - "body", - "buttons" - ], + "required": ["body", "buttons"], "description": "Linked Devices support up to 10 total buttons and at most 3 CTAs. Replies may be mixed with URL and call buttons using a hydrated template; copy/catalog buttons cannot be mixed with replies. An optional image, video or document header requires exactly one URL or base64 source and cannot be combined with a title. On an official WhatsApp Business number, media headers require an approved template, and at most 3 `reply` buttons are sent; other button types return `501`. There, `body` allows at most 1,024 characters, `title` and `footer` at most 60, and each button needs a unique `id` and a unique `text` of at most 20 characters. Every number also limits each button `id` to 256 UTF-8 bytes. Content outside any limit returns `400`." } }, - "required": [ - "buttons" - ], + "required": ["buttons"], "additionalProperties": false }, "listContent": { @@ -259,34 +218,23 @@ "minLength": 0 } }, - "required": [ - "id", - "title" - ] + "required": ["id", "title"] }, "minItems": 1, "maxItems": 30 } }, - "required": [ - "rows" - ] + "required": ["rows"] }, "minItems": 1, "maxItems": 10 } }, - "required": [ - "title", - "buttonText", - "sections" - ], + "required": ["title", "buttonText", "sections"], "description": "On an official WhatsApp Business number, `title` is the header (at most 60 characters) and `description` the body (at most 1,024 characters); without `description`, `title` is the body. There, each section title allows at most 24 characters and is required when the list has more than one section, lists allow at most 10 rows in total, row titles at most 24 characters, row IDs at most 200, row descriptions at most 72, `buttonText` at most 20 and `footer` at most 60. Every number also limits `buttonText` to 64 UTF-8 bytes, row `id` to 256 bytes and `title` and `description` to 256 and 4,096 bytes, so multibyte text reaches those byte limits before the character limits. Content outside any limit returns `400`." } }, - "required": [ - "list" - ], + "required": ["list"], "additionalProperties": false }, "carouselContent": { @@ -323,9 +271,7 @@ "properties": { "type": { "type": "string", - "enum": [ - "url" - ] + "enum": ["url"] }, "text": { "type": "string", @@ -337,20 +283,14 @@ "format": "uri" } }, - "required": [ - "type", - "text", - "url" - ] + "required": ["type", "text", "url"] }, { "type": "object", "properties": { "type": { "type": "string", - "enum": [ - "call" - ] + "enum": ["call"] }, "text": { "type": "string", @@ -361,20 +301,14 @@ "pattern": "^\\+?\\d{6,20}$" } }, - "required": [ - "type", - "text", - "phoneNumber" - ] + "required": ["type", "text", "phoneNumber"] }, { "type": "object", "properties": { "type": { "type": "string", - "enum": [ - "reply" - ] + "enum": ["reply"] }, "text": { "type": "string", @@ -385,20 +319,14 @@ "minLength": 1 } }, - "required": [ - "type", - "text", - "id" - ] + "required": ["type", "text", "id"] }, { "type": "object", "properties": { "type": { "type": "string", - "enum": [ - "copy" - ] + "enum": ["copy"] }, "text": { "type": "string", @@ -409,20 +337,14 @@ "minLength": 1 } }, - "required": [ - "type", - "text", - "copyCode" - ] + "required": ["type", "text", "copyCode"] }, { "type": "object", "properties": { "type": { "type": "string", - "enum": [ - "catalog" - ] + "enum": ["catalog"] }, "text": { "type": "string", @@ -437,11 +359,7 @@ "minLength": 0 } }, - "required": [ - "type", - "text", - "businessPhoneNumber" - ] + "required": ["type", "text", "businessPhoneNumber"] } ] }, @@ -469,11 +387,7 @@ "additionalProperties": false } }, - "required": [ - "body", - "buttons", - "image" - ], + "required": ["body", "buttons", "image"], "additionalProperties": false }, { @@ -492,9 +406,7 @@ "properties": { "type": { "type": "string", - "enum": [ - "url" - ] + "enum": ["url"] }, "text": { "type": "string", @@ -506,20 +418,14 @@ "format": "uri" } }, - "required": [ - "type", - "text", - "url" - ] + "required": ["type", "text", "url"] }, { "type": "object", "properties": { "type": { "type": "string", - "enum": [ - "call" - ] + "enum": ["call"] }, "text": { "type": "string", @@ -530,20 +436,14 @@ "pattern": "^\\+?\\d{6,20}$" } }, - "required": [ - "type", - "text", - "phoneNumber" - ] + "required": ["type", "text", "phoneNumber"] }, { "type": "object", "properties": { "type": { "type": "string", - "enum": [ - "reply" - ] + "enum": ["reply"] }, "text": { "type": "string", @@ -554,20 +454,14 @@ "minLength": 1 } }, - "required": [ - "type", - "text", - "id" - ] + "required": ["type", "text", "id"] }, { "type": "object", "properties": { "type": { "type": "string", - "enum": [ - "copy" - ] + "enum": ["copy"] }, "text": { "type": "string", @@ -578,20 +472,14 @@ "minLength": 1 } }, - "required": [ - "type", - "text", - "copyCode" - ] + "required": ["type", "text", "copyCode"] }, { "type": "object", "properties": { "type": { "type": "string", - "enum": [ - "catalog" - ] + "enum": ["catalog"] }, "text": { "type": "string", @@ -606,11 +494,7 @@ "minLength": 0 } }, - "required": [ - "type", - "text", - "businessPhoneNumber" - ] + "required": ["type", "text", "businessPhoneNumber"] } ] }, @@ -644,11 +528,7 @@ "additionalProperties": false } }, - "required": [ - "body", - "buttons", - "video" - ], + "required": ["body", "buttons", "video"], "additionalProperties": false } ] @@ -657,17 +537,12 @@ "maxItems": 10 } }, - "required": [ - "body", - "cards" - ], + "required": ["body", "cards"], "additionalProperties": false, "description": "Linked Devices only: one message with 2\u201310 image/video cards and native buttons per card. Each card accepts up to ten replies or three CTAs without mixing; reply IDs are unique across cards. Combined media is bounded to 50 MiB. Official approved carousel templates use content.template instead." } }, - "required": [ - "carousel" - ], + "required": ["carousel"], "additionalProperties": false } } From e64e528df2e2fe249f5422e2e88044bd19321940 Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Wed, 14 Oct 2026 23:38:01 +0300 Subject: [PATCH 14/14] docs: pin lint-verified interactive backend revision --- contracts/interactive-messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/interactive-messages.json b/contracts/interactive-messages.json index 20c85024..57e1c62c 100644 --- a/contracts/interactive-messages.json +++ b/contracts/interactive-messages.json @@ -1,6 +1,6 @@ { "repository": "polymorfa/polymorfa", - "commit": "99c36bd39f51a2c1da309782fdbe0963597f68ca", + "commit": "ae30811ef6675a38a507482676b7213141ebebb6", "sourcePath": "apps/api/docs/openapi.json", "sourceSha256": "4375210d40d5bd21cf0605bd89d73eabc41a32f67613cd6a56027e8704005f1f", "releaseStatus": "draft; requires accepted API revision and staging acceptance",