> ## 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.

# Manage templates with the API

> Create, read, update, and publish templates over the REST API with an nt_ ID and a draft lifecycle.

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>;
};

<Info>
  <Doc href="/docs/design/templates/overview">How templates work</Doc> explains the draft/publish model.
</Info>

Manage templates over the API so they live in code and move through Git review. Each carries an `nt_` ID and a draft/published lifecycle.

## Prerequisites

* <AppLink href="https://app.courier.com/settings/api-keys">A Courier API key</AppLink>
* <Doc href="/docs/design/elemental/overview">The Elemental content format</Doc>

## Create a template

<Endpoint method="POST" path="/notifications" name="Create Notification Template" href="/docs/api-reference/templates/create-notification-template" /> returns a new template in the `DRAFT` state:

<CodeGroup>
  ```javascript Node.js theme={null}
  const notificationTemplateResponse = await client.notifications.create({
    notification: {
      name: 'Welcome Email',
      tags: ['onboarding', 'welcome'],
      brand: { id: 'bnd_01kx4mrd0pfzw8wt7pn7p2fzag' },
      subscription: { topic_id: 'pt_01kx4h2jdafq8bk9a26x0kvd1t' },
      routing: { strategy_id: 'rs_01kx4h2jdafq8bk9amzvy6hbv0' },
      content: {
        version: '2022-01-01',
        elements: [
          {
            type: 'channel',
            channel: 'email',
            elements: [{ type: 'text', content: 'Welcome aboard, {{name}}.' }],
          },
        ],
      },
    },
    state: 'DRAFT',
  });
  ```

  ```python Python theme={null}
  notification_template_response = client.notifications.create(
      notification={
          "name": "Welcome Email",
          "tags": ["onboarding", "welcome"],
          "brand": {"id": "bnd_01kx4mrd0pfzw8wt7pn7p2fzag"},
          "subscription": {"topic_id": "pt_01kx4h2jdafq8bk9a26x0kvd1t"},
          "routing": {"strategy_id": "rs_01kx4h2jdafq8bk9amzvy6hbv0"},
          "content": {
              "version": "2022-01-01",
              "elements": [
                  {
                      "type": "channel",
                      "channel": "email",
                      "elements": [{"type": "text", "content": "Welcome aboard, {{name}}."}],
                  }
              ],
          },
      },
      state="DRAFT",
  )
  ```

  ```bash cURL theme={null}
  curl --request POST \
    --url https://api.courier.com/notifications \
    --header "Authorization: Bearer $COURIER_API_KEY" \
    --header "Content-Type: application/json" \
    --data '{
      "notification": {
        "name": "Welcome Email",
        "tags": ["onboarding", "welcome"],
        "brand": { "id": "bnd_01kx4mrd0pfzw8wt7pn7p2fzag" },
        "subscription": { "topic_id": "pt_01kx4h2jdafq8bk9a26x0kvd1t" },
        "routing": { "strategy_id": "rs_01kx4h2jdafq8bk9amzvy6hbv0" },
        "content": {
          "version": "2022-01-01",
          "elements": [
            {
              "type": "channel",
              "channel": "email",
              "elements": [{ "type": "text", "content": "Welcome aboard, {{name}}." }]
            }
          ]
        }
      },
      "state": "DRAFT"
    }'
  ```

  ```ruby Ruby theme={null}
  notification_template_response = courier.notifications.create(
    notification: {
      brand: {id: "bnd_01kx4mrd0pfzw8wt7pn7p2fzag"},
      content: {elements: [{type: "channel", channel: "email", elements: [{type: "text", content: "Welcome aboard, {{name}}."}]}], version: "2022-01-01"},
      name: "Welcome Email",
      routing: {strategy_id: "rs_01kx4h2jdafq8bk9amzvy6hbv0"},
      subscription: {topic_id: "pt_01kx4h2jdafq8bk9a26x0kvd1t"},
      tags: ["onboarding", "welcome"]
    }
  )
  ```

  ```go Go theme={null}
  // The typed elemental model has no field for a channel's child elements, so pass the content as raw JSON.
  content := param.Override[shared.ElementalContentParam](json.RawMessage(`{
    "version": "2022-01-01",
    "elements": [
      {
        "type": "channel",
        "channel": "email",
        "elements": [{ "type": "text", "content": "Welcome aboard, {{name}}." }]
      }
    ]
  }`))

  notificationTemplateResponse, err := client.Notifications.New(context.TODO(), courier.NotificationNewParams{
  	NotificationTemplateCreateRequest: courier.NotificationTemplateCreateRequestParam{
  		Notification: courier.NotificationTemplateWritePayloadParam{
  			NotificationTemplatePayloadParam: courier.NotificationTemplatePayloadParam{
  				Brand: courier.NotificationTemplatePayloadBrandParam{
  					ID: "bnd_01kx4mrd0pfzw8wt7pn7p2fzag",
  				},
  				Content: content,
  				Name:    "Welcome Email",
  				Routing: courier.NotificationTemplatePayloadRoutingParam{
  					StrategyID: "rs_01kx4h2jdafq8bk9amzvy6hbv0",
  				},
  				Subscription: courier.NotificationTemplatePayloadSubscriptionParam{
  					TopicID: "pt_01kx4h2jdafq8bk9a26x0kvd1t",
  				},
  				Tags: []string{"onboarding", "welcome"},
  			},
  		},
  	},
  })
  ```

  ```java Java theme={null}
  NotificationTemplateCreateRequest params = NotificationTemplateCreateRequest.builder()
      .notification(NotificationTemplateWritePayload.builder()
          .brand(NotificationTemplatePayload.Brand.builder()
              .id("bnd_01kx4mrd0pfzw8wt7pn7p2fzag")
              .build())
          .content(ElementalContent.builder()
              // The channel builder has no addElement, so its children go in as an additional property.
              .addElement(ElementalChannelNodeWithType.builder()
                  .type(ElementalChannelNodeWithType.Type.CHANNEL)
                  .channel("email")
                  .putAdditionalProperty("elements", JsonValue.from(java.util.List.of(
                      java.util.Map.of("type", "text", "content", "Welcome aboard, {{name}}."))))
                  .build())
              .version("2022-01-01")
              .build())
          .name("Welcome Email")
          .routing(NotificationTemplatePayload.Routing.builder()
              .strategyId("rs_01kx4h2jdafq8bk9amzvy6hbv0")
              .build())
          .subscription(NotificationTemplatePayload.Subscription.builder()
              .topicId("pt_01kx4h2jdafq8bk9a26x0kvd1t")
              .build())
          .addTag("onboarding")
          .addTag("welcome")
          .build())
      .build();
  NotificationTemplateResponse notificationTemplateResponse = client.notifications().create(params);
  ```

  ```php PHP theme={null}
  $notificationTemplateResponse = $client->notifications->create(
    notification: [
      'brand' => ['id' => 'bnd_01kx4mrd0pfzw8wt7pn7p2fzag'],
      'content' => ['elements' => [['type' => 'channel']], 'version' => '2022-01-01'],
      'name' => 'Welcome Email',
      'routing' => ['strategyID' => 'rs_01kx4h2jdafq8bk9amzvy6hbv0'],
      'subscription' => ['topicID' => 'pt_01kx4h2jdafq8bk9a26x0kvd1t'],
      'tags' => ['onboarding', 'welcome'],
    ],
    state: 'DRAFT',
  );
  ```

  ```csharp C# theme={null}
  NotificationCreateParams parameters = new()
  {
      Notification = new()
      {
          Brand = new("bnd_01kx4mrd0pfzw8wt7pn7p2fzag"),
          Content = new()
          {
              // Elemental nodes expose no typed content property, so build the node from raw JSON.
          Elements =
          [
              ElementalChannelNodeWithType.FromRawUnchecked(
                  JsonSerializer.Deserialize<Dictionary<string, JsonElement>>("""
                  {
                    "type": "channel",
                    "channel": "email",
                    "elements": [{ "type": "text", "content": "Welcome aboard, {{name}}." }]
                  }
                  """)),
          ],
              Version = "2022-01-01",
          },
          Name = "Welcome Email",
          Routing = new("rs_01kx4h2jdafq8bk9amzvy6hbv0"),
          Subscription = new("pt_01kx4h2jdafq8bk9a26x0kvd1t"),
          Tags = ["onboarding", "welcome"],
      },
  };

  var notificationTemplateResponse = await client.Notifications.Create(parameters);
  ```

  ```bash CLI theme={null}
  courier notifications create \
    --api-key "$COURIER_API_KEY" \
    --notification "{brand: {id: bnd_01kx4mrd0pfzw8wt7pn7p2fzag}, content: {elements: [{type: channel, channel: email, elements: [{type: text, content: 'Welcome aboard, {{name}}.'}]}], version: '2022-01-01'}, name: Welcome Email, routing: {strategy_id: rs_01kx4h2jdafq8bk9amzvy6hbv0}, subscription: {topic_id: pt_01kx4h2jdafq8bk9a26x0kvd1t}, tags: [onboarding, welcome]}"
  ```

  ```text MCP theme={null}
  With Courier MCP, create a draft template called Welcome Email tagged onboarding.
  ```
</CodeGroup>

### Content needs a channel to live in

Every element belongs inside a `channel` block. Content whose elements sit at the
top level is rejected at creation:

```json theme={null}
{
  "version": "2022-01-01",
  "elements": [
    {
      "type": "channel",
      "channel": "inbox",
      "elements": [{ "type": "text", "content": "Your order shipped." }]
    }
  ]
}
```

The channel block is what the <Doc href="/docs/design/templates/design-studio">Design Studio</Doc>
renders when someone opens the template, so content without one is stored but
opens as an empty document. A `meta` element may sit at the top level alongside
the channel blocks. A template with no elements at all is fine: it is one nobody
has written yet.

Only creation is checked. Templates already saved without a channel block still
send, and are left alone.

## Read a template

<Endpoint method="GET" path="/notifications/{id}" name="Get Notification Template" href="/docs/api-reference/templates/get-notification-template" /> returns the template, defaulting to the published version. Pass `?version=draft` (or a version like `v001`) to read another:

<CodeGroup>
  ```javascript Node.js theme={null}
  const notificationTemplateResponse = await client.notifications.retrieve('nt_01kx4h2jdafq8bk9aftxak4b40');
  ```

  ```python Python theme={null}
  notification_template_response = client.notifications.retrieve(
      id="nt_01kx4h2jdafq8bk9aftxak4b40",
  )
  ```

  ```bash cURL theme={null}
  curl --request GET \
    --url https://api.courier.com/notifications/nt_01kx4h2jdafq8bk9aftxak4b40 \
    --header "Authorization: Bearer $COURIER_API_KEY"
  ```

  ```ruby Ruby theme={null}
  notification_template_response = courier.notifications.retrieve("nt_01kx4h2jdafq8bk9aftxak4b40")
  ```

  ```go Go theme={null}
  notificationTemplateResponse, err := client.Notifications.Get(
  	context.TODO(),
  	"nt_01kx4h2jdafq8bk9aftxak4b40",
  	courier.NotificationGetParams{},
  )
  ```

  ```java Java theme={null}
  NotificationTemplateResponse notificationTemplateResponse = client.notifications().retrieve("nt_01kx4h2jdafq8bk9aftxak4b40");
  ```

  ```php PHP theme={null}
  $notificationTemplateResponse = $client->notifications->retrieve('nt_01kx4h2jdafq8bk9aftxak4b40');
  ```

  ```csharp C# theme={null}
  NotificationRetrieveParams parameters = new() { ID = "nt_01kx4h2jdafq8bk9aftxak4b40" };

  var notificationTemplateResponse = await client.Notifications.Retrieve(parameters);
  ```

  ```bash CLI theme={null}
  courier notifications retrieve \
    --api-key "$COURIER_API_KEY" \
    --id nt_01kx4h2jdafq8bk9aftxak4b40
  ```

  ```text MCP theme={null}
  With Courier MCP, show me the published version of my welcome email template.
  ```
</CodeGroup>

## Update content: the round-trip contract

Template content is read and written through a separate content endpoint, and this is where round-trips break. <Endpoint method="GET" path="/notifications/{id}/content" name="Get Notification Content" href="/docs/api-reference/templates/get-notification-content" /> returns one of two shapes: the modern **V2 elemental** form (`{ version, elements }`, where every element carries a read-only `checksum` and a `locales` map) or the legacy **V1** form (`{ blocks, channels, checksum }`).

<Warning>
  **PUT accepts V2 elemental only, and it fully overwrites.**<br />
  <Endpoint method="PUT" path="/notifications/{id}/content" name="Replace Notification Content" href="/docs/api-reference/templates/replace-notification-content" /> takes only the V2 `{ version, elements }` shape and replaces all content. Strip the server-generated `checksum` and `locales` fields before you send it back, and do not try to PUT a V1 body. Get the whole draft, modify the elements, drop the read-only fields, then PUT.
</Warning>

<CodeGroup>
  ```javascript Node.js theme={null}
  const notificationContentMutationResponse = await client.notifications.putContent('nt_01kx4h2jdafq8bk9aftxak4b40', {
    content: {
      version: '2022-01-01',
      elements: [
        {
          type: 'channel',
          channel: 'email',
          elements: [{ type: 'text', content: 'Welcome aboard, {{name}}.' }],
        },
      ],
    },
    state: 'DRAFT',
  });
  ```

  ```python Python theme={null}
  notification_content_mutation_response = client.notifications.put_content(
      id="nt_01kx4h2jdafq8bk9aftxak4b40",
      content={
          "version": "2022-01-01",
          "elements": [
              {
                  "type": "channel",
                  "channel": "email",
                  "elements": [{"type": "text", "content": "Welcome aboard, {{name}}."}],
              }
          ],
      },
      state="DRAFT",
  )
  ```

  ```bash cURL theme={null}
  curl --request PUT \
    --url https://api.courier.com/notifications/nt_01kx4h2jdafq8bk9aftxak4b40/content \
    --header "Authorization: Bearer $COURIER_API_KEY" \
    --header "Content-Type: application/json" \
    --data '{
      "content": {
        "version": "2022-01-01",
        "elements": [
          {
            "type": "channel",
            "channel": "email",
            "elements": [{ "type": "text", "content": "Welcome aboard, {{name}}." }]
          }
        ]
      },
      "state": "DRAFT"
    }'
  ```

  ```ruby Ruby theme={null}
  notification_content_mutation_response = courier.notifications.put_content(
    "nt_01kx4h2jdafq8bk9aftxak4b40",
    content: {elements: [{type: "channel", channel: "email", elements: [{type: "text", content: "Welcome aboard, {{name}}."}]}], version: "2022-01-01"}
  )
  ```

  ```go Go theme={null}
  notificationContentMutationResponse, err := client.Notifications.PutContent(
  	context.TODO(),
  	"nt_01kx4h2jdafq8bk9aftxak4b40",
  	courier.NotificationPutContentParams{
  		NotificationContentPutRequest: courier.NotificationContentPutRequestParam{
  			// The typed elemental model has no field for a channel's child elements, so pass the content as raw JSON.
  			Content: param.Override[courier.NotificationContentPutRequestContentParam](json.RawMessage(`{
  			  "version": "2022-01-01",
  			  "elements": [
  			    {
  			      "type": "channel",
  			      "channel": "email",
  			      "elements": [{ "type": "text", "content": "Welcome aboard, {{name}}." }]
  			    }
  			  ]
  			}`)),
  		},
  	},
  )
  ```

  ```java Java theme={null}
  NotificationPutContentParams params = NotificationPutContentParams.builder()
      .id("nt_01kx4h2jdafq8bk9aftxak4b40")
      .notificationContentPutRequest(NotificationContentPutRequest.builder()
          .content(NotificationContentPutRequest.Content.builder()
              // The channel builder has no addElement, so its children go in as an additional property.
              .addElement(ElementalChannelNodeWithType.builder()
                  .type(ElementalChannelNodeWithType.Type.CHANNEL)
                  .channel("email")
                  .putAdditionalProperty("elements", JsonValue.from(java.util.List.of(
                      java.util.Map.of("type", "text", "content", "Welcome aboard, {{name}}."))))
                  .build())
              .version("2022-01-01")
              .build())
          .build())
      .build();
  NotificationContentMutationResponse notificationContentMutationResponse = client.notifications().putContent(params);
  ```

  ```php PHP theme={null}
  $notificationContentMutationResponse = $client->notifications->putContent(
    'nt_01kx4h2jdafq8bk9aftxak4b40',
    content: ['elements' => [['type' => 'channel', 'channel' => 'email', 'elements' => [['type' => 'text', 'content' => 'Welcome aboard, {{name}}.']]]], 'version' => '2022-01-01'],
    state: NotificationTemplateState::DRAFT,
  );
  ```

  ```csharp C# theme={null}
  NotificationPutContentParams parameters = new()
  {
      ID = "nt_01kx4h2jdafq8bk9aftxak4b40",
      Content = new()
      {
          Elements = [new ElementalChannelNodeWithType() { Type = ElementalChannelNodeWithTypeIntersectionMember1Type.Channel }],
          Version = "2022-01-01",
      },
  };

  var notificationContentMutationResponse = await client.Notifications.PutContent(parameters);
  ```

  ```bash CLI theme={null}
  courier notifications put-content \
    --api-key "$COURIER_API_KEY" \
    --id nt_01kx4h2jdafq8bk9aftxak4b40 \
    --content "{elements: [{type: channel, channel: email, elements: [{type: text, content: 'Welcome aboard, {{name}}.'}]}], version: '2022-01-01'}"
  ```

  ```text MCP theme={null}
  With Courier MCP, replace the draft content of my welcome email template with this Elemental document.
  ```
</CodeGroup>

## Publish

A `PUT` writes the draft. <Endpoint method="POST" path="/notifications/{id}/publish" name="Publish Notification Template" href="/docs/api-reference/templates/publish-notification-template" /> makes it the active version. You can also publish a historical version to roll back.

<CodeGroup>
  ```javascript Node.js theme={null}
  await client.notifications.publish('nt_01kx4h2jdafq8bk9aftxak4b40');
  ```

  ```python Python theme={null}
  client.notifications.publish(
      id="nt_01kx4h2jdafq8bk9aftxak4b40",
  )
  ```

  ```bash cURL theme={null}
  curl --request POST \
    --url https://api.courier.com/notifications/nt_01kx4h2jdafq8bk9aftxak4b40/publish \
    --header "Authorization: Bearer $COURIER_API_KEY"
  ```

  ```ruby Ruby theme={null}
  result = courier.notifications.publish("nt_01kx4h2jdafq8bk9aftxak4b40")
  ```

  ```go Go theme={null}
  err := client.Notifications.Publish(
  	context.TODO(),
  	"nt_01kx4h2jdafq8bk9aftxak4b40",
  	courier.NotificationPublishParams{},
  )
  ```

  ```java Java theme={null}
  client.notifications().publish("nt_01kx4h2jdafq8bk9aftxak4b40");
  ```

  ```php PHP theme={null}
  $result = $client->notifications->publish('nt_01kx4h2jdafq8bk9aftxak4b40');
  ```

  ```csharp C# theme={null}
  NotificationPublishParams parameters = new() { ID = "nt_01kx4h2jdafq8bk9aftxak4b40" };

  await client.Notifications.Publish(parameters);
  ```

  ```bash CLI theme={null}
  courier notifications publish \
    --api-key "$COURIER_API_KEY" \
    --id nt_01kx4h2jdafq8bk9aftxak4b40
  ```

  ```text MCP theme={null}
  With Courier MCP, publish my welcome email template.
  ```
</CodeGroup>

## Aliases

An alias is a name you choose that stands in for a template's `nt_` ID on a send. Your code keeps a readable reference while the template behind it changes. You assign one in the template's **settings**, under **Alias**, not over the API.

<Doc href="/docs/design/templates/overview#send-it-by-id-or-alias">Send it by ID or alias</Doc> has the rules and an example.

## Read a template's metrics

<Endpoint method="GET" path="/notifications/{id}/metrics" name="Get Notification Template Metrics" href="/docs/api-reference/templates/get-notification-template-metrics" /> returns one template's sends, deliveries, opens, clicks, errors, and undeliverables over time, broken out per provider and channel. <Doc href="/docs/monitor/template-metrics">Template metrics</Doc> covers the window, granularity, and plan caps.

## Approval workflow

If your team gates publishing behind review, enable the approval workflow. Submitting a draft for review happens in the console, which puts the template in a read-only submission state. Over the API you then manage the submission's **checks**: read them with <Endpoint method="GET" path="/notifications/{id}/{submissionId}/checks" name="Get submission checks" href="/docs/api-reference/templates/get-submission-checks" />, resolve or fail them with `PUT`, and cancel the submission with `DELETE`. There is no public endpoint to create the submission itself. That step is console-driven.

## Manage Templates as code

<Steps>
  <Step title="Export a template to Elemental">
    <Doc href="/docs/design/elemental/overview">Elemental</Doc> is the JSON format Courier uses for template content, so it is what you version-control. Read a template's content with <Endpoint method="GET" path="/notifications/{id}/content" name="Get Notification Content" href="/docs/api-reference/templates/get-notification-content" /> and commit the `{ version, elements }` JSON to your repo as the source of truth for the template.
  </Step>

  <Step title="Version-control the content">
    With the Elemental JSON in your repo, review template changes in a pull request like any other code. Apply an approved change back to Courier with <Endpoint method="PUT" path="/notifications/{id}/content" name="Replace Notification Content" href="/docs/api-reference/templates/replace-notification-content" />. Strip the read-only `checksum` and `locales` fields before you PUT, and see [the round-trip contract](#update-content-the-round-trip-contract) for the rest.
  </Step>

  <Step title="Manage templates from the CLI">
    The Courier CLI maps to the same template operations, so you can script create, update, publish, and version listing in CI:

    ```bash theme={null}
    # Create a template from an Elemental file
    courier notifications create --api-key "$COURIER_API_KEY" \
      --notification "$(cat welcome-email.json)"

    # Update its content from the versioned file
    courier notifications put-content --api-key "$COURIER_API_KEY" \
      --id nt_01kx4h2jdafq8bk9aftxak4b40 --content "$(cat welcome-email.content.json)"

    # Publish, and list versions to confirm
    courier notifications publish --api-key "$COURIER_API_KEY" --id nt_01kx4h2jdafq8bk9aftxak4b40
    courier notifications list-versions --api-key "$COURIER_API_KEY" --id nt_01kx4h2jdafq8bk9aftxak4b40
    ```

    The full verb set is `list`, `create`, `retrieve`, `replace`, `archive`, `list-versions`, `publish`, `duplicate`, `retrieve-content`, `put-content`, `put-element`, and `put-locale`.
  </Step>

  <Step title="Manage templates from an AI agent">
    The same operations are available as Courier <Doc href="/docs/resources/mcp">MCP</Doc> tools (`create_notification`, `put_notification_content`, `publish_notification`, `list_notification_versions`), so an AI agent can create and update templates in your workspace. Connect the MCP server once and drive template changes from your agent.
  </Step>

  <Step title="Verify the round-trip">
    Export a template, change a value in the JSON, write it back with `put-content`, and publish. Confirm the change renders in a test send, then run `list-versions` and confirm your publish created a new version.
  </Step>
</Steps>

## Endpoints

Every operation on a workspace Template. Names and reference links are generated from the
OpenAPI spec, so this table cannot drift from what the API serves.

| Operation                 | Method and path                                                                                                                                                              |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| List Templates            | <Endpoint method="GET" path="/notifications" name="List Notification Templates" href="/docs/api-reference/templates/list-notification-templates" />                               |
| Read one                  | <Endpoint method="GET" path="/notifications/{id}" name="Get Notification Template" href="/docs/api-reference/templates/get-notification-template" />                              |
| Replace one               | <Endpoint method="PUT" path="/notifications/{id}" name="Replace Notification Template" href="/docs/api-reference/templates/replace-notification-template" />                      |
| Archive one               | <Endpoint method="DELETE" path="/notifications/{id}" name="Archive Notification Template" href="/docs/api-reference/templates/archive-notification-template" />                   |
| List versions             | <Endpoint method="GET" path="/notifications/{id}/versions" name="List Notification Template Versions" href="/docs/api-reference/templates/list-notification-template-versions" /> |
| Publish                   | <Endpoint method="POST" path="/notifications/{id}/publish" name="Publish Notification Template" href="/docs/api-reference/templates/publish-notification-template" />             |
| Read content              | <Endpoint method="GET" path="/notifications/{id}/content" name="Get Notification Content" href="/docs/api-reference/templates/get-notification-content" />                        |
| Replace content           | <Endpoint method="PUT" path="/notifications/{id}/content" name="Replace Notification Content" href="/docs/api-reference/templates/replace-notification-content" />                |
| Replace one element       | <Endpoint method="PUT" path="/notifications/{id}/elements/{elementId}" name="Replace Notification Element" href="/docs/api-reference/templates/replace-notification-element" />   |
| Replace a locale          | <Endpoint method="PUT" path="/notifications/{id}/locales/{localeId}" name="Replace Notification Locale" href="/docs/api-reference/templates/replace-notification-locale" />       |
| Read submission checks    | <Endpoint method="GET" path="/notifications/{id}/{submissionId}/checks" name="Get submission checks" href="/docs/api-reference/templates/get-submission-checks" />                |
| Replace submission checks | <Endpoint method="PUT" path="/notifications/{id}/{submissionId}/checks" name="Replace submission checks" href="/docs/api-reference/templates/replace-submission-checks" />        |
| Cancel a submission       | <Endpoint method="DELETE" path="/notifications/{id}/{submissionId}/checks" name="Cancel submission" href="/docs/api-reference/templates/cancel-submission" />                     |
| Read metrics              | <Endpoint method="GET" path="/notifications/{id}/metrics" name="Get Notification Template Metrics" href="/docs/api-reference/templates/get-notification-template-metrics" />      |

## Response codes

| Status | Meaning                                                                                                                 |
| ------ | ----------------------------------------------------------------------------------------------------------------------- |
| `200`  | The read or write succeeded.                                                                                            |
| `201`  | The Template was created.                                                                                               |
| `204`  | The write succeeded and returned no body.                                                                               |
| `400`  | The document failed validation. Content stored on a Template needs its top-level elements wrapped in a channel element. |
| `402`  | The workspace's plan does not include this operation.                                                                   |
| `404`  | No Template with that id, or no such version.                                                                           |
| `409`  | The write conflicts with the Template's current state, such as publishing what is already published.                    |
| `429`  | Rate limited. Retry after the interval the response names.                                                              |
| `503`  | Temporarily unavailable. Retry with backoff.                                                                            |

## Where three related things live

These come up while working with this API and each is documented once, elsewhere:

* **Filling variables at send time.** Pass `data` on the send and the Template resolves it. <Doc href="/docs/design/templates/variables">Variables</Doc> covers the syntax, scope, and the helpers.
* **Raw HTML for email.** <Doc href="/docs/design/templates/html-email">HTML email</Doc> covers sending a full HTML document and what a provider does with it.
* **Workspace against Tenant Templates.** These endpoints reach workspace Templates only. A Tenant's own Templates live under `/tenants/{tenant_id}/templates` and are documented in <Doc href="/docs/design/embedded-designer/api">Tenant Templates API</Doc>.

## Verify

Retrieve the template without a `version` parameter and confirm your published change is live, then send it to a test recipient and confirm it renders in <Doc href="/docs/monitor/overview">message logs</Doc>.
