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
1 change: 1 addition & 0 deletions api.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Types:
- <code><a href="./src/resources/shared.ts">ElementalMetaNode</a></code>
- <code><a href="./src/resources/shared.ts">ElementalMetaNodeWithType</a></code>
- <code><a href="./src/resources/shared.ts">ElementalNode</a></code>
- <code><a href="./src/resources/shared.ts">ElementalNodeNonChannel</a></code>
- <code><a href="./src/resources/shared.ts">ElementalQuoteNode</a></code>
- <code><a href="./src/resources/shared.ts">ElementalQuoteNodeWithType</a></code>
- <code><a href="./src/resources/shared.ts">ElementalTextNode</a></code>
Expand Down
1 change: 1 addition & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
9 changes: 8 additions & 1 deletion src/resources/journeys/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -29,7 +36,7 @@ export class Templates extends APIResource {
* subscription: null,
* content: {
* version: '2022-01-01',
* elements: [{ type: 'text' }],
* elements: [{ type: 'channel' }],
* },
* },
* });
Expand Down
19 changes: 16 additions & 3 deletions src/resources/notifications/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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 {
/**
Expand Down
77 changes: 77 additions & 0 deletions src/resources/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ElementalNodeNonChannel> | 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`)
Expand Down Expand Up @@ -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.
*/
Expand Down
12 changes: 11 additions & 1 deletion src/resources/tenants/templates/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -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'] },
* },
Expand Down
2 changes: 1 addition & 1 deletion tests/api-resources/journeys/templates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe('resource templates', () => {
notification: {
brand: { id: 'id' },
content: {
elements: [{ type: 'text' }],
elements: [{ type: 'channel' }],
version: '2022-01-01',
scope: 'default',
},
Expand Down
2 changes: 1 addition & 1 deletion tests/api-resources/tenants/templates/templates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading