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

# Let customers edit their notifications

> Embed Courier Create so each customer edits its own emails while your send stays the same.

export const Tags = ({items}) => {
  const routes = {
    Email: "/integrations/email/overview",
    SMS: "/integrations/sms/overview",
    Push: "/integrations/push/overview",
    Inbox: "/in-app/overview",
    Chat: "/integrations/direct-message/overview",
    Templates: "/design/templates/overview",
    Variables: "/design/templates/variables",
    Elemental: "/design/elemental/overview",
    Brands: "/design/brands",
    Translations: "/design/elemental/locales",
    Routing: "/send/routing",
    Preferences: "/recipients/preferences/overview",
    Journeys: "/journeys/overview",
    Broadcasts: "/broadcasts/overview",
    Tenants: "/tenants/overview",
    Logs: "/monitor/overview",
    Webhooks: "/monitor/webhooks/outbound",
    Lists: "/recipients/lists-and-audiences/overview",
    Users: "/recipients/overview",
    Digests: "/journeys/nodes/digest",
    Environments: "/workspaces/overview",
    MCP: "/resources/mcp"
  };
  const icons = {
    Email: "envelope",
    SMS: "comment",
    Push: "mobile",
    Inbox: "inbox",
    Chat: "comments",
    Templates: "pen-ruler",
    Variables: "pen-ruler",
    Elemental: "pen-ruler",
    Brands: "pen-ruler",
    Translations: "pen-ruler",
    Routing: "paper-plane",
    Preferences: "users",
    Journeys: "route",
    Broadcasts: "bullhorn",
    Tenants: "building",
    Logs: "chart-simple",
    Webhooks: "chart-simple",
    Lists: "users",
    Users: "users",
    Digests: "route",
    Environments: "briefcase",
    MCP: "toolbox"
  };
  const base = "https://d3gk2c5xim1je2.cloudfront.net/fontawesome/v7.2.0/regular/";
  const names = String(items || "").split(",").map(entry => entry.trim()).filter(Boolean);
  return <div className="cx-tags">
      {names.map(name => {
    const href = routes[name];
    const icon = icons[name];
    const url = icon ? "url(" + base + icon + ".svg)" : null;
    const style = url ? {
      "--cx-tag-icon": url
    } : null;
    if (!href) {
      return <span className="cx-tag" data-icon={icon} style={style} key={name}>
              {name}
            </span>;
    }
    return <a className="cx-tag" data-icon={icon} style={style} href={href} key={name}>
            {name}
          </a>;
  })}
    </div>;
};

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

<Tags items="Tenants, Templates, Brands" />

Ship a Notifications tab in your product. Each customer edits their own emails, and your send never changes.

Every edit belongs to that customer's tenant. You mount the editor, scope a token to one tenant, and keep sending one template that resolves differently for each of them.

## What you will build

```mermaid theme={null}
flowchart LR
    A["Your settings page"] --> B["Embedded editor"]
    B --> C["Tenant's template"]
    D["Your send"] --> C
    C --> E["Branded email"]
```

## Prerequisites

* <Doc href="/docs/integrations/email/overview">A connected email provider</Doc>
* <AppLink href="https://app.courier.com/settings/api-keys">A Courier API key</AppLink>
* <Guide href="/docs/guides/notify-across-tenants">A tenant for each customer org</Guide>
* A React app on 18.2.0 or newer

## Seed the tenant's template

Do this when your app creates the org. The editor opens on your wording instead of an empty page, and the send has content before anyone edits anything.

The `template_id` is an id you choose. Use the same one for every tenant, so your send never branches. 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>

The full lifecycle of a tenant template, including versions and deletes, is in <Doc href="/docs/tenants/templates">tenant templates</Doc>.

## Put the editor in your app

<Steps>
  <Step title="Install the package">
    ```bash theme={null}
    npm install @trycourier/react-designer
    ```
  </Step>

  <Step title="Issue a token scoped to one tenant">
    The editor runs in the browser, so it authenticates with a JWT rather than your API key. Mint one on your backend for the admin who is signed in, scoped to the tenant they belong to.

    <CodeGroup>
      ```javascript Node.js theme={null}
      const { token } = await client.auth.issueToken({
        scope: "user_id:user_123 tenant:acme-corp:read tenant:acme-corp:notifications:read tenant:acme-corp:notifications:write tenant:acme-corp:brand:read tenant:acme-corp:brand:write",
        expires_in: "1 day",
      });
      ```

      ```python Python theme={null}
      response = client.auth.issue_token(
          scope="user_id:user_123 tenant:acme-corp:read tenant:acme-corp:notifications:read tenant:acme-corp:notifications:write tenant:acme-corp:brand:read tenant:acme-corp:brand:write",
          expires_in="1 day",
      )
      ```

      ```bash cURL wrap theme={null}
      curl --request POST \
        --url https://api.courier.com/auth/issue-token \
        --header "Authorization: Bearer $COURIER_API_KEY" \
        --header 'Content-Type: application/json' \
        --data '{
          "scope": "user_id:user_123 tenant:acme-corp:read tenant:acme-corp:notifications:read tenant:acme-corp:notifications:write tenant:acme-corp:brand:read tenant:acme-corp:brand:write",
          "expires_in": "1 day"
        }'
      ```

      ```ruby Ruby theme={null}
      response = courier.auth.issue_token(
        scope: "user_id:user_123 tenant:acme-corp:read tenant:acme-corp:notifications:read tenant:acme-corp:notifications:write tenant:acme-corp:brand:read tenant:acme-corp:brand:write",
        expires_in: "1 day"
      )
      ```

      ```go Go theme={null}
      response, err := client.Auth.IssueToken(context.TODO(), courier.AuthIssueTokenParams{
        Scope:     "user_id:user_123 tenant:acme-corp:read tenant:acme-corp:notifications:read tenant:acme-corp:notifications:write tenant:acme-corp:brand:read tenant:acme-corp:brand:write",
        ExpiresIn: "1 day",
      })
      ```

      ```java Java theme={null}
      AuthIssueTokenParams params = AuthIssueTokenParams.builder()
          .scope("user_id:user_123 tenant:acme-corp:read tenant:acme-corp:notifications:read tenant:acme-corp:notifications:write tenant:acme-corp:brand:read tenant:acme-corp:brand:write")
          .expiresIn("1 day")
          .build();

      var response = client.auth().issueToken(params);
      ```

      ```php PHP theme={null}
      $response = $client->auth->issueToken(
          scope: 'user_id:user_123 tenant:acme-corp:read tenant:acme-corp:notifications:read tenant:acme-corp:notifications:write tenant:acme-corp:brand:read tenant:acme-corp:brand:write',
          expiresIn: '1 day',
      );
      ```

      ```csharp C# theme={null}
      var response = await client.Auth.IssueToken(new AuthIssueTokenParams
      {
          Scope = "user_id:user_123 tenant:acme-corp:read tenant:acme-corp:notifications:read tenant:acme-corp:notifications:write tenant:acme-corp:brand:read tenant:acme-corp:brand:write",
          ExpiresIn = "1 day"
      });
      ```

      ```bash CLI wrap theme={null}
      courier auth issue-token \
        --scope "user_id:user_123 tenant:acme-corp:read tenant:acme-corp:notifications:read tenant:acme-corp:notifications:write tenant:acme-corp:brand:read tenant:acme-corp:brand:write" \
        --expires-in "1 day"
      ```

      ```text MCP theme={null}
      With Courier MCP, issue a one-day token for user_123 that can edit acme-corp's templates and brand.
      ```
    </CodeGroup>

    Read the tenant id from your own session, never from the browser. A token carrying `tenants:` in place of `tenant:acme-corp:` grants every customer at once. Every scope is listed in <Doc href="/docs/design/embedded-designer/overview#scopes">the designer's scopes</Doc>.
  </Step>

  <Step title="Mount the editor on your settings page">
    `TemplateProvider` takes the template id you seeded, the tenant, and the token. `brandEditor` puts logo, colors, and footer beside the content, so one screen covers both.

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

    export function NotificationSettings({ tenantId, token }) {
      return (
        <TemplateProvider templateId="welcome" tenantId={tenantId} token={token}>
          <TemplateEditor routing={{ method: "single", channels: ["email"] }} brandEditor />
        </TemplateProvider>
      );
    }
    ```

    `routing` limits which channels the customer sees. Leave it out and they get every channel your workspace sends on. Every prop is in <Doc href="/docs/design/embedded-designer/template-editor">the template editor reference</Doc>.
  </Step>

  <Step title="Publish on your own terms">
    The editor auto-saves a draft and ships its own Publish button. Hide that button when publishing should run through your own review or your own UI, then call the hook.

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

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

    Render it beside `<TemplateEditor hidePublish />`. A draft the customer never publishes does not reach anyone, so the send keeps using the last published version.
  </Step>
</Steps>

## Send what the customer published

<Steps>
  <Step title="Name the tenant's template">
    Reference it as `tenant/<template_id>` and set the tenant in the context. The word `tenant` is literal, and 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} wrap 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>

    This is the whole payoff. The call is identical for every customer, and only `tenant_id` changes.
  </Step>

  <Step title="Reach the whole org">
    Swap `to` for a `tenant_id` and Courier sends to every member of that org, still in their own content and brand. <Doc href="/docs/tenants/send">Send with tenants</Doc> covers each targeting shape.
  </Step>
</Steps>

## Verify

<Steps>
  <Step title="Edit as a customer would">
    Open your settings page signed in as a user of `acme-corp`, change a line of copy and the logo, then publish.
  </Step>

  <Step title="Read the template back">
    <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">Fetch the tenant's template</Endpoint> and confirm the published content carries the edit.
  </Step>

  <Step title="Send and compare two orgs">
    Send to a user in `acme-corp` and a user in `beta-inc`, then open both in <AppLink href="https://app.courier.com/logs">Logs</AppLink>. Same call, same template id, two different messages.
  </Step>
</Steps>

## Troubleshooting

| Symptom                                | Cause                                                  | Fix                                                        |
| -------------------------------------- | ------------------------------------------------------ | ---------------------------------------------------------- |
| The editor renders blank               | No template exists at that id for this tenant          | Seed it with the create call above                         |
| Requests from the editor 401           | The token expired, or it is missing a scope            | Reissue with the `notifications` and `brand` scopes        |
| Edits save but never arrive            | The draft was never published                          | Publish from the editor, or call `publishTemplate`         |
| The send renders your copy, not theirs | The `tenant/` prefix or `context.tenant_id` is missing | Send `template: "tenant/welcome"` in that tenant's context |
| One customer sees another's content    | The token used a plural `tenants:` scope               | Scope it to `tenant:<id>:` for the signed-in org           |

Brand, defaults, and templates all resolve from the same tenant, in the order set out in <Doc href="/docs/tenants/context">tenant context</Doc>. To give a customer their own defaults as well as their own copy, see <Doc href="/docs/tenants/preferences">tenant default preferences</Doc>.
