diff --git a/contracts/README.md b/contracts/README.md index 42656c32..635501b1 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -80,6 +80,11 @@ decision `experimentRevision`, and closed Platform campaign shapes. SDK types follow each change on covered operations; feature evaluation stays excluded. `cloud-api-supplements.json` pins the Graph-compatible catalog and Flow encryption routes to the same commit. +API PR #377, merged in this snapshot, rejects invalid project-template +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. + 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. diff --git a/packages/typescript/README.md b/packages/typescript/README.md index 4fa4366f..7dbc3613 100644 --- a/packages/typescript/README.md +++ b/packages/typescript/README.md @@ -315,10 +315,19 @@ const created = await messaging.templates.create("support", { await messaging.templates.preview("support", created.data.data.id, { values: { name: "Grace" }, }); + +await messaging.templates.submit("support", created.data.data.id, { + session: "number_123", +}); ``` Keep this client on the server. Browser builders use an application-owned route, such as `createTemplateBuilderRoute` from `@polymorfa/nextjs`. +Submission validates the saved definition before making one provider attempt. +A successful response records the submission; it does not establish approval. +If the request times out or loses its response, read the Number's template +catalog before deciding whether another submission is needed. Local preview +does not ask Meta to validate the template. ## Flow drafts and provider lifecycle diff --git a/packages/typescript/src/messaging/templates.ts b/packages/typescript/src/messaging/templates.ts index 3cec7d87..ab317f69 100644 --- a/packages/typescript/src/messaging/templates.ts +++ b/packages/typescript/src/messaging/templates.ts @@ -90,6 +90,7 @@ export class TemplatesResource { }); } + /** Submit once. If the response is lost, read the Number's template catalog before another attempt. */ submit( projectSlug: string, templateId: string, @@ -101,6 +102,7 @@ export class TemplatesResource { path: `${templatePath(projectSlug, templateId)}/submit`, body, ...options, + maxNetworkRetries: 0, }); } } diff --git a/packages/typescript/test/cloud-templates.test.ts b/packages/typescript/test/cloud-templates.test.ts index 951051a2..3d768cd8 100644 --- a/packages/typescript/test/cloud-templates.test.ts +++ b/packages/typescript/test/cloud-templates.test.ts @@ -163,6 +163,29 @@ describe("Cloud templates", () => { }, ); + it("submits a project template draft to Meta once, even with a retry override", async () => { + const server = await startTestServer(() => ({ + status: 500, + body: JSON.stringify({ + error: { code: "internal_error", message: "Outcome unknown" }, + }), + })); + servers.push(server); + await expect( + client(server).templates.submit( + "support", + "tpl/1", + { session: "number_123" }, + { idempotencyKey: "customer-key", maxNetworkRetries: 3 }, + ), + ).rejects.toMatchObject({ status: 500 }); + expect(server.requests).toHaveLength(1); + expect(server.requests[0]!.method).toBe("POST"); + expect(server.requests[0]!.path).toBe( + "/messaging/projects/support/templates/tpl%2F1/submit", + ); + }); + it("returns a catalog failure instead of an empty successful list", async () => { const server = await startTestServer(() => ({ status: 502,