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

# Tenant templates

> Give one customer its own version of a template without changing how you send.

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

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/tenants/context">How tenant context works</Doc> covers the concepts behind this page.
</Info>

A tenant template overrides one of your templates for a single customer, keyed by an id you choose. Their admin can rewrite the copy in their own voice, and only their users see it. Everyone else keeps the default.

You create, publish, and delete tenant templates through the API, then reference them in a send with the `tenant/<template_id>` format.

**A tenant template is not a separate template you have to route to.** Your send logic does not branch on which customer it is for. It names the same id every time, and Courier merges the tenant's version over the message when one exists.

**Leave the `tenant/` prefix off and the send succeeds with the wrong content.** Courier reads `template: "welcome"` as your workspace template and renders that, because a template id that resolves is not an error. The customer's edits are not in the message.

<Note>
  Tenant templates have no console UI, so create, list, and delete them through the API, as every step below does. To hand the editing to your customer instead, <Guide href="/docs/guides/let-customers-edit-notifications">embed the designer</Guide> in your own app.
</Note>

## Prerequisites

* <Doc href="/docs/tenants/overview">A tenant</Doc>
* <AppLink href="https://app.courier.com/settings/api-keys">A Courier API key</AppLink>

## Manage tenant templates

<Steps>
  <Step title="Create or update a tenant template">
    Create or replace a tenant's template with the <Endpoint method="PUT" path="/tenants/{tenant_id}/templates/{template_id}" name="Create or update a Tenant Template" href="/docs/api-reference/tenant-templates/create-or-update-a-tenant-template">create-or-update endpoint</Endpoint>. The `template_id` is an ID you choose for this tenant's template. Every element sits inside a `channel` block, the same rule <Doc href="/docs/design/templates/api#content-needs-a-channel-to-live-in">workspace templates</Doc> follow.

    <CodeGroup>
      ```javascript Node.js theme={null}
      import Courier from '@trycourier/courier';

      const client = new Courier({
        apiKey: process.env['COURIER_API_KEY'],
      });

      const response = await client.tenants.templates.replace('welcome', {
        tenant_id: 'acme-corp',
        template: {
          content: {
            version: '2022-01-01',
            elements: [
              {
                type: 'channel',
                channel: 'email',
                elements: [{ type: 'text', content: 'Welcome to Acme Corp!' }],
              },
            ],
          },
        },
      });

      console.log(response.id);
      ```

      ```python Python theme={null}
      import os
      from courier import Courier

      client = Courier(
          api_key=os.environ.get("COURIER_API_KEY"),
      )
      response = client.tenants.templates.replace(
          template_id="welcome",
          tenant_id="acme-corp",
          template={
              "content": {
                  "version": "2022-01-01",
                  "elements": [
                      {
                          "type": "channel",
                          "channel": "email",
                          "elements": [{"type": "text", "content": "Welcome to Acme Corp!"}],
                      }
                  ],
              }
          },
      )
      print(response.id)
      ```

      ```bash cURL wrap theme={null}
      curl -X PUT https://api.courier.com/tenants/acme-corp/templates/welcome \
        -H "Authorization: Bearer $COURIER_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{
          "template": {
            "content": {
              "version": "2022-01-01",
              "elements": [
                {
                  "type": "channel",
                  "channel": "email",
                  "elements": [{ "type": "text", "content": "Welcome to Acme Corp!" }]
                }
              ]
            }
          }
        }'
      ```

      ```ruby Ruby theme={null}
      require "courier"

      courier = Courier::Client.new(api_key: ENV["COURIER_API_KEY"])

      response = courier.tenants.templates.replace(
        "welcome",
        tenant_id: "acme-corp",
        template: {
          content: {
            version: "2022-01-01",
            elements: [
              {
                type: "channel",
                channel: "email",
                elements: [{ type: "text", content: "Welcome to Acme Corp!" }]
              }
            ]
          }
        }
      )

      puts(response)
      ```

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

      response, err := client.Tenants.Templates.Replace(
      	context.TODO(),
      	"welcome",
      	courier.TenantTemplateReplaceParams{
      		TenantID: "acme-corp",
      		PutTenantTemplateRequest: courier.PutTenantTemplateRequestParam{
      			Template: courier.TenantTemplateInputParam{Content: content},
      		},
      	},
      )
      ```

      ```java Java theme={null}
      TemplateReplaceParams params = TemplateReplaceParams.builder()
          .tenantId("acme-corp")
          .templateId("welcome")
          .putTenantTemplateRequest(PutTenantTemplateRequest.builder()
              .template(TenantTemplateInput.builder()
                  .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 to Acme Corp!"))))
                          .build())
                      .version("2022-01-01")
                      .build())
                  .build())
              .build())
          .build();
      client.tenants().templates().replace(params);
      ```

      ```php PHP theme={null}
      $response = $client->tenants->templates->replace(
        'welcome',
        tenantID: 'acme-corp',
        template: [
          'content' => [
            'version' => '2022-01-01',
            'elements' => [
              [
                'type' => 'channel',
                'channel' => 'email',
                'elements' => [['type' => 'text', 'content' => 'Welcome to Acme Corp!']],
              ],
            ],
          ],
        ],
      );
      ```

      ```csharp C# theme={null}
      TemplateReplaceParams parameters = new()
      {
          TenantID = "acme-corp",
          TemplateID = "welcome",
          Template = new()
          {
              Content = new()
              {
                  Version = "2022-01-01",
                  Elements =
                  [
                      // Elemental nodes expose no typed content property, so build the node from raw JSON.
                      ElementalChannelNodeWithType.FromRawUnchecked(
                          JsonSerializer.Deserialize<Dictionary<string, JsonElement>>("""
                          {
                            "type": "channel",
                            "channel": "email",
                            "elements": [{ "type": "text", "content": "Welcome to Acme Corp!" }]
                          }
                          """)),
                  ],
              },
          },
      };

      await client.Tenants.Templates.Replace(parameters);
      ```

      ```bash CLI theme={null}
      courier tenants:templates replace \
        --api-key "$COURIER_API_KEY" \
        --tenant-id acme-corp \
        --template-id welcome \
        --template '{"content":{"version":"2022-01-01","elements":[{"type":"channel","channel":"email","elements":[{"type":"text","content":"Welcome to Acme Corp!"}]}]}}'
      ```

      ```text MCP theme={null}
      With Courier MCP, create a welcome template for acme-corp that greets their users.
      ```
    </CodeGroup>
  </Step>

  <Step title="Publish the template">
    Changes take effect once published. <Endpoint method="POST" path="/tenants/{tenant_id}/templates/{template_id}/publish" name="Publish a Tenant Template" href="/docs/api-reference/tenant-templates/publish-a-tenant-template">Publish the current draft</Endpoint> to make it live.

    <CodeGroup>
      ```javascript Node.js theme={null}
      import Courier from '@trycourier/courier';

      const client = new Courier({
        apiKey: process.env['COURIER_API_KEY'],
      });

      const response = await client.tenants.templates.publish('welcome', {
        tenant_id: 'acme-corp',
      });

      console.log(response.id);
      ```

      ```python Python theme={null}
      import os
      from courier import Courier

      client = Courier(
          api_key=os.environ.get("COURIER_API_KEY"),
      )
      response = client.tenants.templates.publish(
          template_id="welcome",
          tenant_id="acme-corp",
      )
      print(response.id)
      ```

      ```bash cURL wrap theme={null}
      curl -X POST https://api.courier.com/tenants/acme-corp/templates/welcome/publish \
        -H "Authorization: Bearer $COURIER_API_KEY"
      ```

      ```ruby Ruby theme={null}
      require "courier"

      courier = Courier::Client.new(api_key: ENV["COURIER_API_KEY"])

      response = courier.tenants.templates.publish("welcome", tenant_id: "acme-corp")

      puts(response)
      ```

      ```go Go theme={null}
      response, err := client.Tenants.Templates.Publish(
      	context.TODO(),
      	"welcome",
      	courier.TenantTemplatePublishParams{
      		TenantID: "acme-corp",
      	},
      )
      ```

      ```java Java theme={null}
      TemplatePublishParams params = TemplatePublishParams.builder()
          .tenantId("acme-corp")
          .templateId("welcome")
          .build();
      client.tenants().templates().publish(params);
      ```

      ```php PHP theme={null}
      $response = $client->tenants->templates->publish(
        'welcome', tenantID: 'acme-corp'
      );
      ```

      ```csharp C# theme={null}
      TemplatePublishParams parameters = new()
      {
          TenantID = "acme-corp",
          TemplateID = "welcome",
      };

      await client.Tenants.Templates.Publish(parameters);
      ```

      ```bash CLI theme={null}
      courier tenants:templates publish \
        --api-key "$COURIER_API_KEY" \
        --tenant-id acme-corp \
        --template-id welcome
      ```

      ```text MCP theme={null}
      With Courier MCP, publish acme-corp's welcome template.
      ```
    </CodeGroup>
  </Step>

  <Step title="List and read templates">
    <Endpoint method="GET" path="/tenants/{tenant_id}/templates" name="List Templates in Tenant" href="/docs/api-reference/tenant-templates/list-templates-in-tenant">List a tenant's templates</Endpoint>, <Endpoint method="GET" path="/tenants/{tenant_id}/templates/{template_id}" name="Get a Template in Tenant" href="/docs/api-reference/tenant-templates/get-a-template-in-tenant">read one</Endpoint>, or read a <Endpoint method="GET" path="/tenants/{tenant_id}/templates/{template_id}/versions/{version}" name="Get a Template version" href="/docs/api-reference/tenant-templates/get-a-template-version">specific version</Endpoint>.

    <CodeGroup>
      ```javascript Node.js theme={null}
      import Courier from '@trycourier/courier';

      const client = new Courier({
        apiKey: process.env['COURIER_API_KEY'],
      });

      const templates = await client.tenants.templates.list('acme-corp');

      console.log(templates.has_more);
      ```

      ```python Python theme={null}
      import os
      from courier import Courier

      client = Courier(
          api_key=os.environ.get("COURIER_API_KEY"),
      )
      templates = client.tenants.templates.list(
          tenant_id="acme-corp",
      )
      print(templates.has_more)
      ```

      ```bash cURL wrap theme={null}
      curl -X GET https://api.courier.com/tenants/acme-corp/templates \
        -H "Authorization: Bearer $COURIER_API_KEY"
      ```

      ```ruby Ruby theme={null}
      require "courier"

      courier = Courier::Client.new(api_key: ENV["COURIER_API_KEY"])

      templates = courier.tenants.templates.list("acme-corp")

      puts(templates)
      ```

      ```go Go theme={null}
      templates, err := client.Tenants.Templates.List(
      	context.TODO(),
      	"acme-corp",
      	courier.TenantTemplateListParams{},
      )
      ```

      ```java Java theme={null}
      TemplateListResponse templates = client.tenants().templates().list("acme-corp");
      ```

      ```php PHP theme={null}
      $templates = $client->tenants->templates->list('acme-corp');
      ```

      ```csharp C# theme={null}
      TemplateListParams parameters = new() { TenantID = "acme-corp" };

      var templates = await client.Tenants.Templates.List(parameters);
      ```

      ```bash CLI theme={null}
      courier tenants:templates list \
        --api-key "$COURIER_API_KEY" \
        --tenant-id acme-corp
      ```

      ```text MCP theme={null}
      With Courier MCP, list acme-corp's templates.
      ```
    </CodeGroup>
  </Step>

  <Step title="Delete a template">
    <Endpoint method="DELETE" path="/tenants/{tenant_id}/templates/{template_id}" name="Delete a Tenant Template" href="/docs/api-reference/tenant-templates/delete-a-tenant-template">Deleting a tenant template</Endpoint> removes it. This is the only way to remove one, since there is no console UI for it.

    <CodeGroup>
      ```javascript Node.js theme={null}
      import Courier from '@trycourier/courier';

      const client = new Courier({
        apiKey: process.env['COURIER_API_KEY'],
      });

      await client.tenants.templates.delete('welcome', { tenant_id: 'acme-corp' });
      ```

      ```python Python theme={null}
      import os
      from courier import Courier

      client = Courier(
          api_key=os.environ.get("COURIER_API_KEY"),
      )
      client.tenants.templates.delete(
          template_id="welcome",
          tenant_id="acme-corp",
      )
      ```

      ```bash cURL wrap theme={null}
      curl -X DELETE https://api.courier.com/tenants/acme-corp/templates/welcome \
        -H "Authorization: Bearer $COURIER_API_KEY"
      ```

      ```ruby Ruby theme={null}
      require "courier"

      courier = Courier::Client.new(api_key: ENV["COURIER_API_KEY"])

      result = courier.tenants.templates.delete("welcome", tenant_id: "acme-corp")

      puts(result)
      ```

      ```go Go theme={null}
      err := client.Tenants.Templates.Delete(
      	context.TODO(),
      	"welcome",
      	courier.TenantTemplateDeleteParams{
      		TenantID: "acme-corp",
      	},
      )
      ```

      ```java Java theme={null}
      TemplateDeleteParams params = TemplateDeleteParams.builder()
          .tenantId("acme-corp")
          .templateId("welcome")
          .build();
      client.tenants().templates().delete(params);
      ```

      ```php PHP theme={null}
      $result = $client->tenants->templates->delete(
        'welcome', tenantID: 'acme-corp'
      );
      ```

      ```csharp C# theme={null}
      TemplateDeleteParams parameters = new()
      {
          TenantID = "acme-corp",
          TemplateID = "welcome",
      };

      await client.Tenants.Templates.Delete(parameters);
      ```

      ```bash CLI theme={null}
      courier tenants:templates delete \
        --api-key "$COURIER_API_KEY" \
        --tenant-id acme-corp \
        --template-id welcome
      ```

      ```text MCP theme={null}
      With Courier MCP, delete acme-corp's welcome template.
      ```
    </CodeGroup>
  </Step>

  <Step title="Send with the tenant template">
    Reference the tenant template in a send with `template: "tenant/<template_id>"`, and set the tenant in the context. The word `tenant` is literal. Your tenant ID goes in `context.tenant_id`.

    <CodeGroup>
      ```javascript Node.js highlight={9} theme={null}
      import Courier from '@trycourier/courier';

      const client = new Courier({
        apiKey: process.env['COURIER_API_KEY'],
      });

      const { requestId } = await client.send.message({
        message: {
          template: 'tenant/welcome',
          context: { tenant_id: 'acme-corp' },
          to: { user_id: 'user_123' },
        },
      });

      console.log(requestId);
      ```

      ```python Python highlight={9} theme={null}
      import os
      from courier import Courier

      client = Courier(
          api_key=os.environ.get("COURIER_API_KEY"),
      )
      response = client.send.message(
          message={
              "template": "tenant/welcome",
              "context": {"tenant_id": "acme-corp"},
              "to": {"user_id": "user_123"},
          },
      )
      print(response.request_id)
      ```

      ```bash cURL highlight={6} theme={null}
      curl -X POST https://api.courier.com/send \
        -H "Authorization: Bearer $COURIER_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{
          "message": {
            "template": "tenant/welcome",
            "context": { "tenant_id": "acme-corp" },
            "to": { "user_id": "user_123" }
          }
        }'
      ```

      ```ruby Ruby highlight={7} theme={null}
      require "courier"

      courier = Courier::Client.new(api_key: ENV["COURIER_API_KEY"])

      response = courier.send_.message(
        message: {
          template: "tenant/welcome",
          context: { tenant_id: "acme-corp" },
          to: { user_id: "user_123" }
        }
      )

      puts(response)
      ```

      ```go Go highlight={3} theme={null}
      response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{
      	Message: courier.SendMessageParamsMessage{
      		Template: courier.String("tenant/welcome"),
      		Context:  shared.MessageContextParam{TenantID: courier.String("acme-corp")},
      		To: courier.SendMessageParamsMessageToUnion{
      			OfUserRecipient: &shared.UserRecipientParam{UserID: courier.String("user_123")},
      		},
      	},
      })
      ```

      ```java Java highlight={3} theme={null}
      SendMessageParams params = SendMessageParams.builder()
          .message(SendMessageParams.Message.builder()
              .template("tenant/welcome")
              .context(JsonValue.from(java.util.Map.of("tenant_id", "acme-corp")))
              .to(JsonValue.from(java.util.Map.of("user_id", "user_123")))
              .build())
          .build();
      client.send().message(params);
      ```

      ```php PHP highlight={3} theme={null}
      $response = $client->send->message(
        message: [
          'template' => 'tenant/welcome',
          'context' => ['tenantID' => 'acme-corp'],
          'to' => ['userID' => 'user_123'],
        ],
      );
      ```

      ```csharp C# highlight={5} theme={null}
      SendMessageParams parameters = new()
      {
          Message = new()
          {
              Template = "tenant/welcome",
              Context = new MessageContext { TenantID = "acme-corp" },
              To = new UserRecipient { UserID = "user_123" },
          },
      };

      await client.Send.Message(parameters);
      ```

      ```bash CLI highlight={3} theme={null}
      courier send message \
        --api-key "$COURIER_API_KEY" \
        --message '{"template":"tenant/welcome","context":{"tenant_id":"acme-corp"},"to":{"user_id":"user_123"}}'
      ```

      ```text MCP theme={null}
      With Courier MCP, send acme-corp's welcome template to user_123.
      ```
    </CodeGroup>

    At send time, Courier merges the tenant template's content over the message and routes it normally.
  </Step>

  <Step title="Verify the template">
    After publishing, <Endpoint method="GET" path="/tenants/{tenant_id}/templates/{template_id}" name="Get a Template in Tenant" href="/docs/api-reference/tenant-templates/get-a-template-in-tenant">read the template back</Endpoint> and confirm the content is what you sent. Then send with `template: "tenant/<template_id>"` in the tenant's context and check the message log to confirm the tenant's content rendered.
  </Step>
</Steps>

## Limits & behavior

* **A draft that is never published reaches nobody.** Creating or replacing a tenant template writes a draft, and the send keeps rendering the last published version until you publish the new one.
* **The tenant id sits in a different place per method.** `list` takes it positionally while `retrieve`, `replace`, and `publish` take it in the body. Check the installed SDK types under `resources/tenants/` before writing against these.
* **Tenant templates have no console UI.** No screen creates, lists, or deletes them, so a template that exists only for one customer is invisible to anyone looking in <AppLink href="https://app.courier.com/content/templates">Templates</AppLink>.
