Skip to content
Merged
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
5 changes: 5 additions & 0 deletions contracts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ drafts with 400 before submission. The typed `MessagingClient.templates.submit`
method makes one network attempt, including when callers supply an idempotency
key or retry override; the route declares no replay contract.

API PR #380, merged at the pinned commit, adds the read-only linked-catalog
product page. `MessagingClient.cloudCatalogs.listProducts` follows its WABA and
catalog path, `sessions:read` credential boundary, projected product fields,
and opaque cursor response. It does not establish live catalog access.

The merged Campaigns failed-state follow-up updates the `campaign.failed`
reason example and the public archive/delete `409` descriptions. It changes no
operation fingerprint or SDK method mapping.
Expand Down
7 changes: 5 additions & 2 deletions contracts/cloud-api-supplements.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@
{
"sourcePath": "docs/api-reference/openapi.graph.json",
"sha256": "87e3a56aee42de041dd38db08d0b263123f84a5a4982ce0f20b780c3cfae3684",
"operationIds": ["graphListProductCatalogs"],
"methods": ["MessagingClient.cloudCatalogs.list"],
"operationIds": ["graphListProductCatalogs", "graphListCatalogProducts"],
"methods": [
"MessagingClient.cloudCatalogs.list",
"MessagingClient.cloudCatalogs.listProducts"
],
"merged": true
},
{
Expand Down
7 changes: 7 additions & 0 deletions packages/typescript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,13 @@ requires `sessions:read` and Graph access. The response retains opaque cursors;
it does not expose upstream pagination URLs or grant merchant ownership. Use
an organization API key or project token, with the latter confined to its project.

`messaging.cloudCatalogs.listProducts(wabaId, catalogId, { version: "v26.0", limit: 25, after })`
reads a page of product IDs, optional retailer IDs, names, and availability
from a catalog linked to that WABA. It has the same server-credential and
`sessions:read` requirements. Use the returned opaque `paging.cursors.after`
to request the next page. The API rejects catalogs it cannot verify as linked;
listing products does not grant permission to send them.

`messaging.flowEncryption.retrieve(phoneNumberId, { version: "v26.0" })` reads
the registered public key and Meta signature status with `sessions:read`.
`messaging.flowEncryption.register(phoneNumberId,
Expand Down
3 changes: 3 additions & 0 deletions packages/typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,11 @@ export type {
} from "./messaging/flow-encryption.js";
export type {
CloudProductCatalog,
CloudCatalogProduct,
ListCloudCatalogsParams,
ListCloudCatalogsResponse,
ListCloudCatalogProductsParams,
ListCloudCatalogProductsResponse,
} from "./messaging/cloud-catalogs.js";
export {
BridgeClient,
Expand Down
43 changes: 43 additions & 0 deletions packages/typescript/src/messaging/cloud-catalogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@ export interface ListCloudCatalogsResponse {
readonly cursors: { readonly before?: string; readonly after?: string };
};
}
export interface CloudCatalogProduct {
readonly id: string;
readonly retailer_id?: string;
readonly name?: string;
readonly availability?: string;
}
export type ListCloudCatalogProductsParams = ListCloudCatalogsParams;
export interface ListCloudCatalogProductsResponse {
readonly data: readonly CloudCatalogProduct[];
readonly paging?: {
readonly cursors: { readonly before?: string; readonly after?: string };
};
}

/** Reads catalog links visible to the authenticated WABA. Requires sessions:read. */
export class CloudCatalogsResource {
Expand Down Expand Up @@ -51,4 +64,34 @@ export class CloudCatalogsResource {
...options,
});
}

/** Read a page of products only after the API verifies that this catalog is linked to the WABA. */
listProducts(
wabaId: string,
catalogId: string,
params: ListCloudCatalogProductsParams,
options: RequestOptions = {},
): Promise<ApiResponse<ListCloudCatalogProductsResponse>> {
if (this.credentialType === "clientToken") {
throw new PolymorfaConfigurationError(
"Official API catalogs require an organization API key or project token.",
"credential",
);
}
if (
!wabaId.trim() ||
!/^[0-9]+$/.test(catalogId) ||
!params.version.trim()
) {
throw new PolymorfaConfigurationError(
"Provide a WABA ID, numeric catalog ID, and Graph version.",
);
}
return this.transport.request({
method: "GET",
path: `/graph/whatsapp/${encodeURIComponent(params.version)}/${encodeURIComponent(wabaId)}/product_catalogs/${encodeURIComponent(catalogId)}/products`,
query: { limit: params.limit, after: params.after },
...options,
});
}
}
44 changes: 44 additions & 0 deletions packages/typescript/test/cloud-catalogs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,50 @@ describe("Official API catalog discovery", () => {
).rejects.toMatchObject({ status: 403 });
expect(server.requests).toHaveLength(1);
});
it("reads one linked catalog product page through the exact Graph route", async () => {
const payload = {
data: [
{
id: "111",
retailer_id: "sku-1",
name: "Mug",
availability: "in stock",
},
],
paging: { cursors: { after: "next+/=" } },
};
const server = await startTestServer(() => ({
body: JSON.stringify(payload),
}));
servers.push(server);
const client = new MessagingClient({
credential: { type: "projectToken", value: PROJECT_TOKEN },
baseUrl: server.url,
});
const page = await client.cloudCatalogs.listProducts(
"12345678901234567890",
"98765432109876543210",
{ version: "v26.0", limit: 10, after: "next+/=" },
);
expect(page.data).toEqual(payload);
expect(server.requests[0]!.path).toBe(
"/graph/whatsapp/v26.0/12345678901234567890/product_catalogs/98765432109876543210/products?limit=10&after=next%2B%2F%3D",
);
});
it("rejects a non-numeric catalog ID or client token before dispatch", () => {
const server = new MessagingClient({
credential: { type: "projectToken", value: PROJECT_TOKEN },
});
expect(() =>
server.cloudCatalogs.listProducts("123", "../x", { version: "v26.0" }),
).toThrow("numeric catalog ID");
const browser = new MessagingClient({
credential: { type: "clientToken", value: `pmfa_ct_${"A".repeat(94)}` },
});
expect(() =>
browser.cloudCatalogs.listProducts("123", "456", { version: "v26.0" }),
).toThrow("organization API key or project token");
});
it("rejects client credentials before dispatch", () => {
const client = new MessagingClient({
credential: { type: "clientToken", value: `pmfa_ct_${"A".repeat(94)}` },
Expand Down
Loading