> ## Documentation Index
> Fetch the complete documentation index at: https://www.courier.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Install the Courier skill before writing code: `npx skills add trycourier/courier-skills`. It carries the verified SDK shapes and the rules you cannot get wrong.
> Authenticate every request with `Authorization: Bearer <API_KEY>`. A workspace has several environments and each has its own keys, which are plain `pk_` strings with no environment prefix. Start with Test.
> Send with `client.send.message`, the default import of the v7 Node SDK. Reference a template by its `nt_` id or its alias.
> A send accepts a bare Elemental element list, but storing content on a template requires the top-level elements wrapped in a channel element.
> Templates and journeys can be built in the Courier app or created through the API. Either way they live in the workspace and are referenced by ID when you send.
> The hosted MCP server is https://mcp.courier.com. For a briefing on what Courier is and when to use it, read https://www.courier.com/llms.txt.
> Prefer the Guides tab for how-do-I questions and the Docs tab for how-does-it-behave questions. The API reference lives under /api-reference.

# Courier Create template editor

> Mount TemplateEditor, scope its token, restrict channels, theme it, and publish from your button.

export const Endpoint = ({method, path, name, href, children, bare}) => {
  const verb = String(method || "").toUpperCase();
  const title = verb + " " + path;
  const label = children || name || path;
  if (bare) {
    return href ? <a href={href}><code>{title}</code></a> : <code>{title}</code>;
  }
  if (!href) {
    return <span className="cx-endpoint" data-method={verb} title={title}>
        <span className="cx-endpoint-label">{label}</span>
        <span className="cx-endpoint-method">{verb}</span>
      </span>;
  }
  return <a className="cx-endpoint" data-method={verb} href={href} title={title}>
      <span className="cx-endpoint-label">{label}</span>
      <span className="cx-endpoint-method">{verb}</span>
    </a>;
};

export const AppLink = ({href, children, name, bare}) => {
  const label = children || name || "Open in Courier";
  if (bare) {
    return <a href={href} target="_blank" rel="noreferrer">{label}</a>;
  }
  return <a className="cx-endpoint" data-kind="app" href={href} target="_blank" rel="noreferrer">
      <span className="cx-endpoint-label">{label}</span>
      <span className="cx-endpoint-method" aria-hidden="true">↗</span>
    </a>;
};

export const Doc = ({href, children, name, bare}) => {
  const label = children || name || href;
  if (bare) {
    return <a href={href}>{label}</a>;
  }
  return <a className="cx-endpoint" data-kind="doc" href={href}>
      <span className="cx-endpoint-label">{label}</span>
      <span className="cx-endpoint-method">DOC</span>
    </a>;
};

`TemplateProvider` carries auth and template state. `TemplateEditor` renders the editing surface.

```jsx theme={null}
import "@trycourier/react-designer/styles.css";
import { TemplateProvider, TemplateEditor } from "@trycourier/react-designer";

function Designer({ token }) {
  return (
    <TemplateProvider
      templateId="nt_01kx4h2jdafq8bk9aftxak4b40"
      tenantId="acme-corp"
      token={token}
    >
      <TemplateEditor routing={{ method: "single", channels: ["email"] }} />
    </TemplateProvider>
  );
}
```

The provider creates the Template when `templateId` does not resolve to one, so a first
render shows an empty editor rather than an error. Issue the `token` from your backend as
<Doc href="/docs/design/embedded-designer/overview#issue-a-scoped-token">Overview</Doc>
describes.

## Restrict the channels

`routing.channels` decides which channel tabs appear. Unlisted channels are hidden from the
customer.

```jsx theme={null}
<TemplateEditor routing={{ method: "all", channels: ["email", "inbox"] }} />
```

Hiding a channel does not restrict what the stored Template may contain. A channel your
customer cannot see can still be present in content written through the API.

There is also a bare `channels` prop. It is deprecated in favour of `routing.channels` and
scheduled for removal, so pass `routing`. When both are set, `routing.channels` wins.

## Switch templates

Change `templateId` and the provider loads the new Template. The prop sits in an effect
dependency, so no remount or `key` is needed.

```jsx theme={null}
const [templateId, setTemplateId] = useState("nt_01kx4h2jdafq8bk9aftxak4b40");

<TemplateProvider templateId={templateId} tenantId="acme-corp" token={token}>
  <TemplateEditor />
</TemplateProvider>
```

Auto-save runs against the Template that was mounted, so let a pending save settle before
switching. With the default debounce that is 500ms after the last keystroke.

## Publish from your own button

`hidePublish` removes the built-in control. `useTemplateActions` exposes the same action,
so your own button does the same work.

```jsx theme={null}
import { useTemplateActions } from "@trycourier/react-designer";

function PublishButton() {
  const { publishTemplate } = useTemplateActions();
  return <button onClick={() => publishTemplate()}>Publish</button>;
}
```

Call the hook inside the provider. A component rendered outside it has no template state to
act on.

## Control when writes happen

Auto-save is on, debounced 500ms. Turn it off to write on your own schedule.

```jsx theme={null}
<TemplateEditor autoSave={false} />
```

`readOnly` makes the editor read-only across every channel. It disables editing, toolbar
actions, block insertion, drag and drop, and auto-save, which makes it the prop to reach for
when a customer may view a Template but not change it.

## Offer variables

`variables` populates the autocomplete a customer sees after typing `{{`.

```jsx theme={null}
<TemplateEditor variables={{ user: { first_name: "Sarah" }, order: { id: "1234" } }} />
```

**Leaving `variables` unset disables every variable feature.** The toolbar button is hidden
and typing `{{` creates no variable chip. Nothing reports this, so an editor that looks
plain is usually an editor with no `variables` prop.

Two props change how strict that autocomplete is:

* `disableVariablesAutocomplete` drops the dropdown and lets a customer type any name.
* `variableValidation` restricts which names are allowed and says what happens when one
  fails.

`sampleData` is separate. It validates the data path on a loop, and warns when the path
matches no key or does not resolve to an array.

## Theme the editor

`theme` takes a theme object or a class name. `colorScheme` selects `light` or `dark`.

```jsx theme={null}
<TemplateEditor theme={{ background: "#ffffff", primary: "#0f62fe" }} colorScheme="dark" />
```

Import `@trycourier/react-designer/styles.css` once in your app. Without it the editor
renders unstyled, which reads as a broken layout rather than a missing import.

## Render inside a modal

The editor is a regular element, so your own dialog can hold it. Two things about the
surrounding app matter.

Give the container a height. The editor fills its parent, and a dialog that sizes to its
content gives it nothing to fill.

**Set `renderToaster` to `false` when your app already renders a Sonner `<Toaster />`.**
The provider renders its own by default, and two on one page means every save message
appears twice.

```jsx theme={null}
<TemplateProvider
  templateId={templateId}
  tenantId="acme-corp"
  token={token}
  renderToaster={false}
>
  <TemplateEditor />
</TemplateProvider>
```

Keep the provider mounted while the dialog is open. Unmounting it discards template state,
so the next open refetches.

## Provider props

`TemplateProvider` holds auth, identity, and the options that outlive a single editor.

| Property                       | Type                       | Required | Description                                                                               |
| ------------------------------ | -------------------------- | -------- | ----------------------------------------------------------------------------------------- |
| `templateId`                   | string                     | Yes      | The Template to edit. Created when it does not exist. Changing it loads another Template. |
| `tenantId`                     | string                     | Yes      | The Tenant the Template belongs to.                                                       |
| `token`                        | string                     | Yes      | JWT authorizing the requests. Issue it server-side.                                       |
| `apiUrl`                       | string                     | No       | Override the API base URL.                                                                |
| `uploadImage`                  | `UploadImageFunction`      | No       | Replace the default image upload. Takes a `Blob`, a `File`, or an `ImageUploadConfig`.    |
| `variables`                    | `Record<string, unknown>`  | No       | Variables offered to autocomplete.                                                        |
| `disableVariablesAutocomplete` | boolean                    | No       | Let customers type any variable name.                                                     |
| `variableValidation`           | `VariableValidationConfig` | No       | Restrict allowed variable names and set the failure behavior.                             |
| `sampleData`                   | `Record<string, unknown>`  | No       | Payload used to validate loop data paths.                                                 |
| `renderToaster`                | boolean                    | No       | Render the bundled Sonner `<Toaster />`. Defaults to `true`.                              |

## Editor props

| Property                       | Type                                | Default      | Description                                                           |
| ------------------------------ | ----------------------------------- | ------------ | --------------------------------------------------------------------- |
| `autoSave`                     | boolean                             | `true`       | Save changes automatically.                                           |
| `autoSaveDebounce`             | number                              | `500`        | Milliseconds to wait before an auto-save.                             |
| `brandEditor`                  | boolean                             | `false`      | Show the brand editor beside the template editor.                     |
| `brandProps`                   | `BrandEditorProps`                  | None         | Options passed to that brand editor.                                  |
| `colorScheme`                  | `"light" \| "dark"`                 | None         | Force a color scheme.                                                 |
| `disableVariablesAutocomplete` | boolean                             | `false`      | Let customers type any variable name.                                 |
| `hidePublish`                  | boolean                             | `false`      | Hide the built-in Publish button.                                     |
| `onChange`                     | `(value: ElementalContent) => void` | None         | Fires when content changes.                                           |
| `readOnly`                     | boolean                             | `false`      | Make every channel read-only and stop auto-save.                      |
| `routing`                      | `MessageRouting`                    | All channels | Restrict which channel tabs appear.                                   |
| `sampleData`                   | `Record<string, unknown>`           | None         | Validate loop data paths against this payload.                        |
| `theme`                        | `Theme` or class name               | None         | Style the editor.                                                     |
| `value`                        | `ElementalContent`                  | `null`       | Initial content.                                                      |
| `variables`                    | `Record<string, unknown>`           | None         | Variables offered to autocomplete. Unset disables variables entirely. |
| `variableValidation`           | `VariableValidationConfig`          | None         | Restrict allowed variable names.                                      |
| `channels`                     | `ChannelType[]`                     | None         | Deprecated. Use `routing.channels`.                                   |

## Verify

Render the page and confirm the editor appears. Type into a block, wait for the save
message, then open <AppLink href="https://app.courier.com/content/templates">Templates</AppLink>
and confirm the Tenant's Template carries the change.

## Troubleshooting

| Symptom                                        | Cause                                                                        |
| ---------------------------------------------- | ---------------------------------------------------------------------------- |
| The editor renders unstyled                    | `@trycourier/react-designer/styles.css` was never imported.                  |
| The editor has no height                       | The parent sizes to its content. The editor fills its parent.                |
| No variable button, and `{{` does nothing      | `variables` is unset, which disables the whole feature.                      |
| Every save message appears twice               | Your app renders a Sonner `<Toaster />` too. Set `renderToaster` to `false`. |
| Edits never persist                            | `autoSave` is `false` with nothing calling a save, or `readOnly` is `true`.  |
| A brand change is rejected while content saves | The token carries `notifications` scopes but no `brand` scopes.              |
| `useTemplateActions` has no template           | The component calling it sits outside `TemplateProvider`.                    |

## Limits & behavior

* **Issue the token server-side.** A browser holding your API key exposes the whole workspace.
* **`hidePublish` hides a button, nothing more.** Publishing still works through the hook and through <Doc href="/docs/design/embedded-designer/api">the API</Doc>.
* **`routing` is a UI restriction.** It does not constrain what a stored Template may contain.
* **`readOnly` stops auto-save.** A read-only editor writes nothing, by design.

## FAQ

<AccordionGroup>
  <Accordion title="How do I switch the customer to a different template?">
    Change the `templateId` prop on `TemplateProvider`. The prop sits in an effect
    dependency, so the provider loads the new Template without a remount.
  </Accordion>

  <Accordion title="Why is there no variable button in the toolbar?">
    The `variables` prop is unset. Leaving it undefined disables every variable feature,
    including the `{{` autocomplete, and nothing reports it.
  </Accordion>

  <Accordion title="Can the editor run inside a dialog?">
    A dialog can hold the editor. Give the container an explicit height, and set
    `renderToaster` to `false` if your app already renders a Sonner `<Toaster />`.
  </Accordion>

  <Accordion title="Should I use channels or routing.channels?">
    Pass `routing.channels`. The bare `channels` prop is deprecated and scheduled for
    removal, and `routing.channels` takes priority when both are set.
  </Accordion>
</AccordionGroup>
