diff --git a/api.md b/api.md
index 1cd0fcc..9479f97 100644
--- a/api.md
+++ b/api.md
@@ -32,6 +32,7 @@ Types:
- ElementalMetaNode
- ElementalMetaNodeWithType
- ElementalNode
+- ElementalNodeNonChannel
- ElementalQuoteNode
- ElementalQuoteNodeWithType
- ElementalTextNode
diff --git a/src/client.ts b/src/client.ts
index 02c3bf0..34cfa22 100644
--- a/src/client.ts
+++ b/src/client.ts
@@ -1457,6 +1457,7 @@ export declare namespace Courier {
export type ElementalMetaNode = API.ElementalMetaNode;
export type ElementalMetaNodeWithType = API.ElementalMetaNodeWithType;
export type ElementalNode = API.ElementalNode;
+ export type ElementalNodeNonChannel = API.ElementalNodeNonChannel;
export type ElementalQuoteNode = API.ElementalQuoteNode;
export type ElementalQuoteNodeWithType = API.ElementalQuoteNodeWithType;
export type ElementalTextNode = API.ElementalTextNode;
diff --git a/src/resources/journeys/templates.ts b/src/resources/journeys/templates.ts
index eeb987a..51bfebe 100644
--- a/src/resources/journeys/templates.ts
+++ b/src/resources/journeys/templates.ts
@@ -17,6 +17,13 @@ export class Templates extends APIResource {
* Create a notification template scoped to this journey. Defaults to `DRAFT`
* state; pass `state: "PUBLISHED"` to publish on create.
*
+ * The content tree must contain exactly one channel block whose `channel` matches
+ * the `channel` on the request — a journey-scoped template carries a single
+ * channel. Top-level elements, or a block for a different channel, return `400`.
+ * The template designer renders only the channel block matching the tab it draws,
+ * so content stored without one cannot be opened. An empty `elements` array is
+ * accepted.
+ *
* @example
* ```ts
* const journeyTemplateGetResponse =
@@ -29,7 +36,7 @@ export class Templates extends APIResource {
* subscription: null,
* content: {
* version: '2022-01-01',
- * elements: [{ type: 'text' }],
+ * elements: [{ type: 'channel' }],
* },
* },
* });
diff --git a/src/resources/notifications/notifications.ts b/src/resources/notifications/notifications.ts
index 4bf9a15..625a2e5 100644
--- a/src/resources/notifications/notifications.ts
+++ b/src/resources/notifications/notifications.ts
@@ -26,6 +26,16 @@ export class Notifications extends APIResource {
* Create a notification template. Requires all fields in the notification object.
* Templates are created in draft state by default.
*
+ * Content must place its elements inside a channel block —
+ * `{ "type": "channel", "channel": "email", "elements": [...] }` — or the request
+ * returns `400`. The template designer renders only the channel block matching the
+ * tab it draws, so content stored without one cannot be opened. An empty
+ * `elements` array is accepted, and the requirement applies to creation only:
+ * `PUT /notifications/{id}` still accepts unwrapped content. Note this endpoint
+ * takes versioned content only — the `{ title, body }` shorthand accepted by
+ * `/send` is rejected here with an `invalid_request_error` on
+ * `notification.content.version`.
+ *
* @example
* ```ts
* const notificationTemplateResponse =
@@ -864,9 +874,12 @@ export interface NotificationTemplateSummary {
}
/**
- * Request body for replacing a notification template. Same shape as create. All
- * fields required (PUT = full replacement), except `alias`, whose omission means
- * "leave the existing aliases alone".
+ * Request body for replacing a notification template. All fields are required,
+ * since `PUT` is a full replacement, except `alias`, whose omission leaves the
+ * existing aliases in place. Unlike `NotificationTemplateCreateRequest`,
+ * `notification.content` is not required to place its elements inside a channel
+ * block: the requirement applies to creation only, so templates already stored
+ * without one stay editable.
*/
export interface NotificationTemplateUpdateRequest {
/**
diff --git a/src/resources/shared.ts b/src/resources/shared.ts
index df39e2f..0e37887 100644
--- a/src/resources/shared.ts
+++ b/src/resources/shared.ts
@@ -225,6 +225,13 @@ export interface ElementalChannelNode extends ElementalBaseNode {
*/
channel?: string;
+ /**
+ * An array of elements to apply to the channel. If `raw` has not been specified,
+ * `elements` is `required`. Channel elements cannot nest, so these are any node
+ * except another channel block.
+ */
+ elements?: Array | null;
+
/**
* Email only. Document-level base font size (CSS px, e.g. `16px`) for body content
* — text, quote, list and action button labels. Heading styles (`h1`/`h2`/`h3`)
@@ -422,6 +429,76 @@ export type ElementalNode =
| ElementalQuoteNodeWithType
| ElementalHTMLNodeWithType;
+/**
+ * Any Elemental node except a channel block. Channel elements are only valid as
+ * top-level elements, so the `elements` nested inside one can never be another
+ * channel. Keeping this union channel-free also keeps the schema acyclic; a
+ * recursive `$ref` here breaks the generated Python models.
+ */
+export type ElementalNodeNonChannel =
+ | ElementalNodeNonChannel.UnionMember0
+ | ElementalNodeNonChannel.UnionMember1
+ | ElementalNodeNonChannel.UnionMember2
+ | ElementalNodeNonChannel.UnionMember3
+ | ElementalNodeNonChannel.UnionMember4
+ | ElementalNodeNonChannel.UnionMember5
+ | ElementalNodeNonChannel.UnionMember6;
+
+export namespace ElementalNodeNonChannel {
+ /**
+ * Represents a body of text to be rendered inside of the notification.
+ */
+ export interface UnionMember0 extends Shared.ElementalTextNode {
+ type?: 'text';
+ }
+
+ /**
+ * The meta element contains information describing the notification that may be
+ * used by a particular channel or provider. One important field is the title field
+ * which will be used as the title for channels that support it.
+ */
+ export interface UnionMember1 extends Shared.ElementalMetaNode {
+ type?: 'meta';
+ }
+
+ /**
+ * Used to embed an image into the notification.
+ */
+ export interface UnionMember2 extends Shared.ElementalImageNode {
+ type?: 'image';
+ }
+
+ /**
+ * Allows the user to execute an action. Can be a button or a link.
+ */
+ export interface UnionMember3 extends Shared.ElementalActionNode {
+ type?: 'action';
+ }
+
+ /**
+ * Renders a dividing line between elements.
+ */
+ export interface UnionMember4 extends Shared.ElementalDividerNode {
+ type?: 'divider';
+ }
+
+ /**
+ * Renders a quote block.
+ */
+ export interface UnionMember5 extends Shared.ElementalQuoteNode {
+ type?: 'quote';
+ }
+
+ /**
+ * Raw HTML string inside an Elemental document. When rendering a message, this
+ * node is turned into output only for the email channel; for other channels it
+ * produces no blocks.
+ */
+ export interface UnionMember6 extends Shared.ElementalHTMLNode {
+ type?: 'html';
+ }
+}
+
/**
* Renders a quote block.
*/
diff --git a/src/resources/tenants/templates/templates.ts b/src/resources/tenants/templates/templates.ts
index cb1aa8d..e7c622e 100644
--- a/src/resources/tenants/templates/templates.ts
+++ b/src/resources/tenants/templates/templates.ts
@@ -104,6 +104,16 @@ export class Templates extends APIResource {
* Creates or updates a notification template scoped to one tenant, letting a
* tenant override the content the workspace template would send.
*
+ * This is an upsert: it creates when the tenant has no template under
+ * `template_id`, and updates when it does. On the create half, content must place
+ * its elements inside a channel block —
+ * `{ "type": "channel", "channel": "email", "elements": [...] }` — or the request
+ * returns `400`. The template designer renders only the channel block matching the
+ * tab it draws, so content stored without one cannot be opened. An empty
+ * `elements` array is accepted, as is the `{ title, body }` shorthand, which has
+ * no elements to wrap. Updates are not checked, so tenant templates already stored
+ * without a wrapper stay editable.
+ *
* @example
* ```ts
* const putTenantTemplateResponse =
@@ -112,7 +122,7 @@ export class Templates extends APIResource {
* template: {
* content: {
* version: '2022-01-01',
- * elements: [{ type: 'text' }],
+ * elements: [{ type: 'channel' }],
* },
* routing: { method: 'single', channels: ['email'] },
* },
diff --git a/tests/api-resources/journeys/templates.test.ts b/tests/api-resources/journeys/templates.test.ts
index dfc79a6..24d9d0c 100644
--- a/tests/api-resources/journeys/templates.test.ts
+++ b/tests/api-resources/journeys/templates.test.ts
@@ -36,7 +36,7 @@ describe('resource templates', () => {
notification: {
brand: { id: 'id' },
content: {
- elements: [{ type: 'text' }],
+ elements: [{ type: 'channel' }],
version: '2022-01-01',
scope: 'default',
},
diff --git a/tests/api-resources/tenants/templates/templates.test.ts b/tests/api-resources/tenants/templates/templates.test.ts
index 3735cb7..00b07fa 100644
--- a/tests/api-resources/tenants/templates/templates.test.ts
+++ b/tests/api-resources/tenants/templates/templates.test.ts
@@ -106,7 +106,7 @@ describe('resource templates', () => {
const response = await client.tenants.templates.replace('template_id', {
tenant_id: 'tenant_id',
template: {
- content: { elements: [{ type: 'text' }], version: '2022-01-01' },
+ content: { elements: [{ type: 'channel' }], version: '2022-01-01' },
channels: {
foo: {
brand_id: 'brand_id',