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

# Go live

> Validate a notification change in Test, then promote it to Production.

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 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 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 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="Environments, Logs" />

Build and validate a notification change without touching live traffic, then promote it to Production.

Test and Production are separate environments with their own keys, templates, and logs. Work in Test, review the draft against a real payload, then migrate the assets.

## The promotion path

```mermaid theme={null}
flowchart LR
    A["Edit in Test"] --> B["Preview draft"]
    B --> C{"Approval workflow?"}
    C -->|No| D["Migrate to Production"]
    C -->|Yes| E["Send for Review"]
    E --> D
```

## Prerequisites

* <Doc href="/docs/integrations/overview">A connected integration</Doc>
* <AppLink href="https://app.courier.com/settings/api-keys">Your Test and Production API keys</AppLink>
* <Doc href="/docs/workspaces/overview#environments-and-api-keys">The environments model</Doc>

## Promote the change

<Steps>
  <Step title="Work in the Test environment">
    Switch to **Test** with the toggle in the settings menu. Templates, brands, and topics you create here are isolated from Production. Edit your template, then send with your Test key. Those sends appear only in the Test dashboard.

    <CodeGroup>
      ```javascript Node.js theme={null}
      const { requestId } = await courier.send.message({
        message: {
          to: {
            email: "sarah@acme-corp.com",
          },
          template: "nt_01kx4h2jdafq8bk9aftxak4b40",
          data: {
            name: "Sarah Bennett",
          },
        },
      });
      ```

      ```python Python theme={null}
      response = client.send.message(
          message={
              "to": {
                  "email": "sarah@acme-corp.com",
              },
              "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
              "data": {
                  "name": "Sarah Bennett",
              },
          },
      )
      ```

      ```bash cURL wrap theme={null}
      curl -X POST https://api.courier.com/send \
        -H "Authorization: Bearer $COURIER_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{
          "message": {
            "to": {
              "email": "sarah@acme-corp.com"
            },
            "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
            "data": {
              "name": "Sarah Bennett"
            }
          }
        }'
      ```

      ```ruby Ruby theme={null}
      response = courier.send_.message(
        message: {
          to: {
            email: "sarah@acme-corp.com"
          },
          template: "nt_01kx4h2jdafq8bk9aftxak4b40",
          data: {
            name: "Sarah Bennett"
          }
        }
      )
      ```

      ```go Go theme={null}
      response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{
      	Message: courier.SendMessageParamsMessage{
      		To: courier.SendMessageParamsMessageToUnion{
      			OfUserRecipient: &shared.UserRecipientParam{
      				Email: courier.String("sarah@acme-corp.com"),
      			},
      		},
      		Template: courier.String("nt_01kx4h2jdafq8bk9aftxak4b40"),
      		Data: map[string]any{
      			"name": "Sarah Bennett",
      		},
      	},
      })
      ```

      ```java Java theme={null}
      SendMessageParams params = SendMessageParams.builder()
          .message(SendMessageParams.Message.builder()
              .to(UserRecipient.builder().email("sarah@acme-corp.com").build())
              .template("nt_01kx4h2jdafq8bk9aftxak4b40")
              .data(JsonValue.from(java.util.Map.of(
                  "name", "Sarah Bennett"
              )))
              .build())
          .build();
      SendMessageResponse response = client.send().message(params);
      ```

      ```php PHP theme={null}
      $response = $client->send->message(
        message: [
          'to' => [
            'email' => 'sarah@acme-corp.com',
          ],
          'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
          'data' => [
            'name' => 'Sarah Bennett',
          ],
        ],
      );
      ```

      ```csharp C# theme={null}
      SendMessageParams parameters = new()
      {
          Message = new()
          {
              To = new UserRecipient { Email = "sarah@acme-corp.com" },
              Template = "nt_01kx4h2jdafq8bk9aftxak4b40",
              Data = new Dictionary<string, JsonElement>()
              {
                  { "name", JsonSerializer.SerializeToElement("Sarah Bennett") },
              },
          },
      };

      var response = await client.Send.Message(parameters);
      ```

      ```bash CLI wrap theme={null}
      courier send message \
        --api-key "$COURIER_API_KEY" \
        --message.to '{"email": "sarah@acme-corp.com"}' \
        --message.template nt_01kx4h2jdafq8bk9aftxak4b40 \
        --message.data '{"name": "Sarah Bennett"}'
      ```

      ```text MCP theme={null}
      With Courier MCP, send my nt_01kx4h2jdafq8bk9aftxak4b40 template to sarah@acme-corp.com with my test key.
      ```
    </CodeGroup>

    <Tip>
      **Keep test sends out of real inboxes.**<br />
      Configure a sandbox provider key in each integration's Test configuration so Test sends never reach real recipients.
    </Tip>
  </Step>

  <Step title="Preview the draft with a draft key">
    A draft key renders the latest **unpublished** draft, not the published version. Use one to review a change against a real payload before publishing. Generate it in <AppLink href="https://app.courier.com/settings/api-keys">Settings → API Keys</AppLink>, send with it, then check the rendered output in <Doc href="/docs/monitor/overview">message logs</Doc>.

    <CodeGroup>
      ```javascript Node.js theme={null}
      const { requestId } = await courier.send.message({
        message: {
          to: {
            email: "reviewer@acme-corp.com",
          },
          template: "nt_01kx4h2jdafq8bk9aftxak4b40",
          data: {
            name: "Draft Preview",
          },
        },
      });
      ```

      ```python Python theme={null}
      response = client.send.message(
          message={
              "to": {
                  "email": "reviewer@acme-corp.com",
              },
              "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
              "data": {
                  "name": "Draft Preview",
              },
          },
      )
      ```

      ```bash cURL wrap theme={null}
      curl -X POST https://api.courier.com/send \
        -H "Authorization: Bearer $COURIER_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{
          "message": {
            "to": {
              "email": "reviewer@acme-corp.com"
            },
            "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
            "data": {
              "name": "Draft Preview"
            }
          }
        }'
      ```

      ```ruby Ruby theme={null}
      response = courier.send_.message(
        message: {
          to: {
            email: "reviewer@acme-corp.com"
          },
          template: "nt_01kx4h2jdafq8bk9aftxak4b40",
          data: {
            name: "Draft Preview"
          }
        }
      )
      ```

      ```go Go theme={null}
      response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{
      	Message: courier.SendMessageParamsMessage{
      		To: courier.SendMessageParamsMessageToUnion{
      			OfUserRecipient: &shared.UserRecipientParam{
      				Email: courier.String("reviewer@acme-corp.com"),
      			},
      		},
      		Template: courier.String("nt_01kx4h2jdafq8bk9aftxak4b40"),
      		Data: map[string]any{
      			"name": "Draft Preview",
      		},
      	},
      })
      ```

      ```java Java theme={null}
      SendMessageParams params = SendMessageParams.builder()
          .message(SendMessageParams.Message.builder()
              .to(UserRecipient.builder().email("reviewer@acme-corp.com").build())
              .template("nt_01kx4h2jdafq8bk9aftxak4b40")
              .data(JsonValue.from(java.util.Map.of(
                  "name", "Draft Preview"
              )))
              .build())
          .build();
      SendMessageResponse response = client.send().message(params);
      ```

      ```php PHP theme={null}
      $response = $client->send->message(
        message: [
          'to' => [
            'email' => 'reviewer@acme-corp.com',
          ],
          'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
          'data' => [
            'name' => 'Draft Preview',
          ],
        ],
      );
      ```

      ```csharp C# theme={null}
      SendMessageParams parameters = new()
      {
          Message = new()
          {
              To = new UserRecipient { Email = "reviewer@acme-corp.com" },
              Template = "nt_01kx4h2jdafq8bk9aftxak4b40",
              Data = new Dictionary<string, JsonElement>()
              {
                  { "name", JsonSerializer.SerializeToElement("Draft Preview") },
              },
          },
      };

      var response = await client.Send.Message(parameters);
      ```

      ```bash CLI wrap theme={null}
      courier send message \
        --api-key "$COURIER_API_KEY" \
        --message.to '{"email": "reviewer@acme-corp.com"}' \
        --message.template nt_01kx4h2jdafq8bk9aftxak4b40 \
        --message.data '{"name": "Draft Preview"}'
      ```

      ```text MCP theme={null}
      With Courier MCP, send the unpublished draft of my nt_01kx4h2jdafq8bk9aftxak4b40 template to reviewer@acme-corp.com.
      ```
    </CodeGroup>
  </Step>

  <Step title="Get approval (optional)">
    If your team reviews changes before they ship, enable the <Doc href="/docs/design/templates/api#approval-workflow">approval workflow</Doc>. Publishing then opens a submission instead of going live, and the template stays read-only until that submission is resolved.
  </Step>

  <Step title="Move the template to Production">
    Read the template's content from Test and write it to Production with the Templates API. <Endpoint method="GET" path="/notifications/{id}/content" name="Get Notification Content" href="/docs/api-reference/templates/get-notification-content" /> returns the `{ version, elements }` JSON, and <Endpoint method="PUT" path="/notifications/{id}/content" name="Replace Notification Content" href="/docs/api-reference/templates/replace-notification-content" /> writes it, using each environment's own key.

    Strip the read-only `checksum` and `locales` fields before the PUT. <Doc href="/docs/design/templates/api#update-content-the-round-trip-contract">The round-trip contract</Doc> covers the rest, and is the same flow as <Doc href="/docs/design/templates/api#manage-templates-as-code">managing templates as code</Doc>.

    **A template's dependencies do not travel with it.** Brands, tags, and subscription topics are per environment, so create them in Production first. A send that references one that is missing fails there while it worked in Test.
  </Step>
</Steps>

## Verify

<Steps>
  <Step title="Smoke test with your Production key">
    Send to an internal recipient in Production.

    <CodeGroup>
      ```javascript Node.js theme={null}
      const { requestId } = await courier.send.message({
        message: {
          to: {
            email: "ops@acme-corp.com",
          },
          template: "nt_01kx4h2jdafq8bk9aftxak4b40",
          data: {
            name: "Smoke Test",
          },
        },
      });
      ```

      ```python Python theme={null}
      response = client.send.message(
          message={
              "to": {
                  "email": "ops@acme-corp.com",
              },
              "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
              "data": {
                  "name": "Smoke Test",
              },
          },
      )
      ```

      ```bash cURL wrap theme={null}
      curl -X POST https://api.courier.com/send \
        -H "Authorization: Bearer $COURIER_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{
          "message": {
            "to": {
              "email": "ops@acme-corp.com"
            },
            "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
            "data": {
              "name": "Smoke Test"
            }
          }
        }'
      ```

      ```ruby Ruby theme={null}
      response = courier.send_.message(
        message: {
          to: {
            email: "ops@acme-corp.com"
          },
          template: "nt_01kx4h2jdafq8bk9aftxak4b40",
          data: {
            name: "Smoke Test"
          }
        }
      )
      ```

      ```go Go theme={null}
      response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{
      	Message: courier.SendMessageParamsMessage{
      		To: courier.SendMessageParamsMessageToUnion{
      			OfUserRecipient: &shared.UserRecipientParam{
      				Email: courier.String("ops@acme-corp.com"),
      			},
      		},
      		Template: courier.String("nt_01kx4h2jdafq8bk9aftxak4b40"),
      		Data: map[string]any{
      			"name": "Smoke Test",
      		},
      	},
      })
      ```

      ```java Java theme={null}
      SendMessageParams params = SendMessageParams.builder()
          .message(SendMessageParams.Message.builder()
              .to(UserRecipient.builder().email("ops@acme-corp.com").build())
              .template("nt_01kx4h2jdafq8bk9aftxak4b40")
              .data(JsonValue.from(java.util.Map.of(
                  "name", "Smoke Test"
              )))
              .build())
          .build();
      SendMessageResponse response = client.send().message(params);
      ```

      ```php PHP theme={null}
      $response = $client->send->message(
        message: [
          'to' => [
            'email' => 'ops@acme-corp.com',
          ],
          'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
          'data' => [
            'name' => 'Smoke Test',
          ],
        ],
      );
      ```

      ```csharp C# theme={null}
      SendMessageParams parameters = new()
      {
          Message = new()
          {
              To = new UserRecipient { Email = "ops@acme-corp.com" },
              Template = "nt_01kx4h2jdafq8bk9aftxak4b40",
              Data = new Dictionary<string, JsonElement>()
              {
                  { "name", JsonSerializer.SerializeToElement("Smoke Test") },
              },
          },
      };

      var response = await client.Send.Message(parameters);
      ```

      ```bash CLI wrap theme={null}
      courier send message \
        --api-key "$COURIER_API_KEY" \
        --message.to '{"email": "ops@acme-corp.com"}' \
        --message.template nt_01kx4h2jdafq8bk9aftxak4b40 \
        --message.data '{"name": "Smoke Test"}'
      ```

      ```text MCP theme={null}
      With Courier MCP, send my nt_01kx4h2jdafq8bk9aftxak4b40 template to ops@acme-corp.com in production.
      ```
    </CodeGroup>
  </Step>

  <Step title="Confirm the right version rendered">
    Open the message in <AppLink href="https://app.courier.com/logs">Logs</AppLink>, expand the timeline, and confirm the migrated version is the one that rendered.
  </Step>

  <Step title="Confirm the provider accepted it">
    The same timeline shows the provider's response. A `SENT` that never reaches `DELIVERED` points at the provider, not the migration.
  </Step>
</Steps>
