> ## 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` from the Node SDK (`@trycourier/courier` v7 and later, where the client is the default import). 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.

# Build your first journey

> Build a three-email welcome series with the Journeys API, then start it from your code.

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

A journey sends a series of messages from one event, like a signup. In this example, you'll build a welcome series with the API for General Medicine, a primary care app. New members get three emails over their first four days.

<Steps>
  <Step title="Get your API key">
    Copy a key from <AppLink href="https://app.courier.com/settings/api-keys">Settings → API Keys</AppLink> and export it. Every example reads it from `COURIER_API_KEY`.

    ```bash theme={null}
    export COURIER_API_KEY="YOUR_COURIER_API_KEY"
    ```
  </Step>

  <Step title="Create the journey">
    <Endpoint method="POST" path="/journeys" name="Create a Journey" href="/docs/api-reference/journeys/create-a-journey" /> takes a name and a node list. Every journey starts with a trigger and ends with an exit. The **API Invoke** trigger lets your code start each run.

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

      const client = new Courier(); // reads COURIER_API_KEY

      const journey = await client.journeys.create({
        name: "New member welcome",
        nodes: [
          { type: "trigger", trigger_type: "api-invoke" },
          { type: "exit" },
        ],
      });

      console.log("Journey ID:", journey.id);
      ```

      ```python Python theme={null}
      # pip install trycourier
      from courier import Courier

      client = Courier()  # reads COURIER_API_KEY

      journey = client.journeys.create(
          name="New member welcome",
          nodes=[
              {"type": "trigger", "trigger_type": "api-invoke"},
              {"type": "exit"},
          ],
      )

      print("Journey ID:", journey.id)
      ```

      ```bash cURL theme={null}
      curl -X POST https://api.courier.com/journeys \
        -H "Authorization: Bearer $COURIER_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{
          "name": "New member welcome",
          "nodes": [
            { "type": "trigger", "trigger_type": "api-invoke" },
            { "type": "exit" }
          ]
        }'
      ```

      ```ruby Ruby theme={null}
      # gem install trycourier
      require "courier"

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

      journey = courier.journeys.create(
        name: "New member welcome",
        nodes: [
          {type: "trigger", trigger_type: "api-invoke"},
          {type: "exit"}
        ]
      )

      puts("Journey ID: #{journey.id}")
      ```

      ```go Go theme={null}
      // go get github.com/trycourier/courier-go/v4
      client := courier.NewClient(option.WithAPIKey(os.Getenv("COURIER_API_KEY")))

      journey, err := client.Journeys.New(context.TODO(), courier.JourneyNewParams{
      	CreateJourneyRequest: courier.CreateJourneyRequestParam{
      		Name: "New member welcome",
      		Nodes: []courier.JourneyNodeUnionParam{
      			{OfAPIInvokeTrigger: &courier.JourneyAPIInvokeTriggerNodeParam{
      				Type:        courier.JourneyAPIInvokeTriggerNodeTypeTrigger,
      				TriggerType: courier.JourneyAPIInvokeTriggerNodeTriggerTypeAPIInvoke,
      			}},
      			{OfExit: &courier.JourneyExitNodeParam{Type: courier.JourneyExitNodeTypeExit}},
      		},
      	},
      })
      if err != nil {
      	panic(err.Error())
      }

      fmt.Println("Journey ID:", journey.ID)
      ```

      ```java Java theme={null}
      CourierClient client = CourierOkHttpClient.builder()
          .apiKey(System.getenv("COURIER_API_KEY"))
          .build();

      CreateJourneyRequest createParams = CreateJourneyRequest.builder()
          .name("New member welcome")
          .addNode(JourneyApiInvokeTriggerNode.builder()
              .type(JourneyApiInvokeTriggerNode.Type.TRIGGER)
              .triggerType(JourneyApiInvokeTriggerNode.TriggerType.API_INVOKE)
              .build())
          .addNode(JourneyExitNode.builder().type(JourneyExitNode.Type.EXIT).build())
          .build();

      JourneyResponse journey = client.journeys().create(createParams);
      System.out.println("Journey ID: " + journey.id());
      ```

      ```php PHP theme={null}
      $client = new Client(apiKey: getenv('COURIER_API_KEY'));

      $journey = $client->journeys->create(
        name: 'New member welcome',
        nodes: [
          ['type' => 'trigger', 'trigger_type' => 'api-invoke'],
          ['type' => 'exit'],
        ],
      );

      echo 'Journey ID: ' . $journey->id . PHP_EOL;
      ```

      ```csharp C# theme={null}
      CourierClient client = new() { ApiKey = Environment.GetEnvironmentVariable("COURIER_API_KEY") };

      JourneyCreateParams createParams = new()
      {
          Name = "New member welcome",
          Nodes =
          [
              new JourneyApiInvokeTriggerNode { Type = "trigger", TriggerType = "api-invoke" },
              new JourneyExitNode { Type = "exit" },
          ],
      };

      var journey = await client.Journeys.Create(createParams);
      Console.WriteLine($"Journey ID: {journey.ID}");
      ```

      ```bash CLI theme={null}
      # npm install -g @trycourier/cli
      courier journeys create \
        --name "New member welcome" \
        --node '{"type": "trigger", "trigger_type": "api-invoke"}' \
        --node '{"type": "exit"}'
      ```
    </CodeGroup>

    The response returns the journey's `id`. Every call below uses it.
  </Step>

  <Step title="Write the three emails">
    <Endpoint method="POST" path="/journeys/{templateId}/templates" name="Create a Notification Template scoped to a Journey" href="/docs/api-reference/journeys/create-a-notification-template-scoped-to-a-journey">Create a journey template</Endpoint> once per email. The `meta` element's `title` is the subject, and `action` renders a button. `scope: "strict"` means every variable names its source: `data.` for what you send with the run, `profile.` for the user.

    <div className="code-scroll">
      <CodeGroup>
        ```javascript Node.js theme={null}
        function createEmail(name, elements) {
          return client.journeys.templates.create(journey.id, {
            channel: "email",
            state: "PUBLISHED",
            notification: {
              name,
              tags: [],
              brand: null,
              subscription: null,
              content: {
                version: "2022-01-01",
                scope: "strict",
                elements: [{ type: "channel", channel: "email", elements }],
              },
            },
          });
        }

        const welcome = await createEmail("Welcome", [
          { type: "meta", title: "Welcome to General Medicine, {{profile.first_name}}" },
          {
            type: "text",
            content:
              "Finish your health profile so your care team has what they need before your first visit. It takes about five minutes.",
          },
          { type: "action", content: "Complete your profile", href: "https://example.com/profile" },
        ]);

        const booking = await createEmail("Book a visit", [
          { type: "meta", title: "Book your first visit" },
          {
            type: "text",
            content:
              "Your {{data.plan}} membership includes same-day {{data.service}} visits. Pick a time that works for you, in person or by video.",
          },
          { type: "action", content: "Book a visit", href: "https://example.com/book" },
        ]);

        const getApp = await createEmail("Get the app", [
          { type: "meta", title: "Message your care team from the app" },
          {
            type: "text",
            content:
              "Between visits, the General Medicine app lets you message your care team, request prescription refills, and read your visit summaries.",
          },
          { type: "action", content: "Get the app", href: "https://example.com/app" },
        ]);
        ```

        ```python Python theme={null}
        def create_email(name, elements):
            return client.journeys.templates.create(
                journey.id,
                channel="email",
                state="PUBLISHED",
                notification={
                    "name": name,
                    "tags": [],
                    "brand": None,
                    "subscription": None,
                    "content": {
                        "version": "2022-01-01",
                        "scope": "strict",
                        "elements": [{"type": "channel", "channel": "email", "elements": elements}],
                    },
                },
            )

        welcome = create_email("Welcome", [
            {"type": "meta", "title": "Welcome to General Medicine, {{profile.first_name}}"},
            {
                "type": "text",
                "content": "Finish your health profile so your care team has what they need before your first visit. It takes about five minutes.",
            },
            {"type": "action", "content": "Complete your profile", "href": "https://example.com/profile"},
        ])

        booking = create_email("Book a visit", [
            {"type": "meta", "title": "Book your first visit"},
            {
                "type": "text",
                "content": "Your {{data.plan}} membership includes same-day {{data.service}} visits. Pick a time that works for you, in person or by video.",
            },
            {"type": "action", "content": "Book a visit", "href": "https://example.com/book"},
        ])

        get_app = create_email("Get the app", [
            {"type": "meta", "title": "Message your care team from the app"},
            {
                "type": "text",
                "content": "Between visits, the General Medicine app lets you message your care team, request prescription refills, and read your visit summaries.",
            },
            {"type": "action", "content": "Get the app", "href": "https://example.com/app"},
        ])
        ```

        ```bash cURL theme={null}
        curl -X POST https://api.courier.com/journeys/YOUR_JOURNEY_ID/templates \
          -H "Authorization: Bearer $COURIER_API_KEY" \
          -H "Content-Type: application/json" \
          -d '{
            "channel": "email",
            "state": "PUBLISHED",
            "notification": {
              "name": "Welcome",
              "tags": [],
              "brand": null,
              "subscription": null,
              "content": {
                "version": "2022-01-01",
                "scope": "strict",
                "elements": [
                  {
                    "type": "channel",
                    "channel": "email",
                    "elements": [
                      { "type": "meta", "title": "Welcome to General Medicine, {{profile.first_name}}" },
                      {
                        "type": "text",
                        "content": "Finish your health profile so your care team has what they need before your first visit. It takes about five minutes."
                      },
                      {
                        "type": "action",
                        "content": "Complete your profile",
                        "href": "https://example.com/profile"
                      }
                    ]
                  }
                ]
              }
            }
          }'
        ```

        ```ruby Ruby theme={null}
        welcome = courier.journeys.templates.create(
          "YOUR_JOURNEY_ID",
          channel: "email",
          state: "PUBLISHED",
          notification: {
            name: "Welcome",
            tags: [],
            brand: nil,
            subscription: nil,
            content: {
              version: "2022-01-01",
              scope: "strict",
              elements: [
                {
                  type: "channel",
                  channel: "email",
                  elements: [
                    {type: "meta", title: "Welcome to General Medicine, {{profile.first_name}}"},
                    {type: "text", content: "Finish your health profile so your care team has what they need before your first visit. It takes about five minutes."},
                    {type: "action", content: "Complete your profile", href: "https://example.com/profile"}
                  ]
                }
              ]
            }
          }
        )

        puts("Welcome template ID: #{welcome.id}")
        ```

        ```go Go theme={null}
        // Elemental children carry no typed fields, so pass the notification as raw JSON.
        notification := param.Override[courier.JourneyTemplateCreateRequestNotificationParam](json.RawMessage(`{
          "name": "Welcome",
          "tags": [],
          "brand": null,
          "subscription": null,
          "content": {
            "version": "2022-01-01",
            "scope": "strict",
            "elements": [
              {
                "type": "channel",
                "channel": "email",
                "elements": [
                  { "type": "meta", "title": "Welcome to General Medicine, {{profile.first_name}}" },
                  { "type": "text", "content": "Finish your health profile so your care team has what they need before your first visit. It takes about five minutes." },
                  { "type": "action", "content": "Complete your profile", "href": "https://example.com/profile" }
                ]
              }
            ]
          }
        }`))

        welcome, err := client.Journeys.Templates.New(
        	context.TODO(),
        	"YOUR_JOURNEY_ID",
        	courier.JourneyTemplateNewParams{
        		JourneyTemplateCreateRequest: courier.JourneyTemplateCreateRequestParam{
        			Channel:      "email",
        			State:        param.NewOpt("PUBLISHED"),
        			Notification: notification,
        		},
        	},
        )
        if err != nil {
        	panic(err.Error())
        }

        fmt.Println("Welcome template ID:", welcome.ID)
        ```

        ```java Java theme={null}
        TemplateCreateParams templateParams = TemplateCreateParams.builder()
            .templateId("YOUR_JOURNEY_ID")
            .journeyTemplateCreateRequest(JourneyTemplateCreateRequest.builder()
                .channel("email")
                .state("PUBLISHED")
                .notification(JourneyTemplateCreateRequest.Notification.builder()
                    .name("Welcome")
                    .tags(java.util.List.of())
                    .brand(java.util.Optional.empty())
                    .subscription(java.util.Optional.empty())
                    .content(JourneyTemplateCreateRequest.Notification.Content.builder()
                        .version(JourneyTemplateCreateRequest.Notification.Content.Version._2022_01_01)
                        .scope(JourneyTemplateCreateRequest.Notification.Content.Scope.STRICT)
                        .addElement(ElementalChannelNodeWithType.builder()
                            .type(ElementalChannelNodeWithType.Type.CHANNEL)
                            .channel("email")
                            .putAdditionalProperty("elements", JsonValue.from(java.util.List.of(
                                java.util.Map.of("type", "meta", "title", "Welcome to General Medicine, {{profile.first_name}}"),
                                java.util.Map.of("type", "text", "content", "Finish your health profile so your care team has what they need before your first visit. It takes about five minutes."),
                                java.util.Map.of("type", "action", "content", "Complete your profile", "href", "https://example.com/profile"))))
                            .build())
                        .build())
                    .build())
                .build())
            .build();

        JourneyTemplateGetResponse welcome = client.journeys().templates().create(templateParams);
        System.out.println("Welcome template ID: " + welcome.id());
        ```

        ```php PHP theme={null}
        $welcome = $client->journeys->templates->create(
          'YOUR_JOURNEY_ID',
          channel: 'email',
          state: 'PUBLISHED',
          notification: [
            'name' => 'Welcome',
            'tags' => [],
            'brand' => null,
            'subscription' => null,
            'content' => [
              'version' => '2022-01-01',
              'scope' => 'strict',
              'elements' => [[
                'type' => 'channel',
                'channel' => 'email',
                'elements' => [
                  ['type' => 'meta', 'title' => 'Welcome to General Medicine, {{profile.first_name}}'],
                  ['type' => 'text', 'content' => 'Finish your health profile so your care team has what they need before your first visit. It takes about five minutes.'],
                  ['type' => 'action', 'content' => 'Complete your profile', 'href' => 'https://example.com/profile'],
                ],
              ]],
            ],
          ],
        );

        echo 'Welcome template ID: ' . $welcome->id . PHP_EOL;
        ```

        ```csharp C# theme={null}
        TemplateCreateParams templateParams = new()
        {
            TemplateID = "YOUR_JOURNEY_ID",
            Channel = "email",
            State = "PUBLISHED",
            Notification = new()
            {
                Name = "Welcome",
                Tags = [],
                Brand = null,
                Subscription = null,
                Content = new()
                {
                    Version = "2022-01-01",
                    Scope = "strict",
                    // 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": "meta", "title": "Welcome to General Medicine, {{profile.first_name}}" },
                                { "type": "text", "content": "Finish your health profile so your care team has what they need before your first visit. It takes about five minutes." },
                                { "type": "action", "content": "Complete your profile", "href": "https://example.com/profile" }
                              ]
                            }
                            """)),
                    ],
                },
            },
        };

        var welcome = await client.Journeys.Templates.Create(templateParams);
        Console.WriteLine($"Welcome template ID: {welcome.ID}");
        ```

        ```bash CLI theme={null}
        courier journeys:templates create \
          --template-id YOUR_JOURNEY_ID \
          --channel email \
          --state PUBLISHED \
          --notification '{"name": "Welcome", "tags": [], "brand": null, "subscription": null, "content": {"version": "2022-01-01", "scope": "strict", "elements": [{"type": "channel", "channel": "email", "elements": [{"type": "meta", "title": "Welcome to General Medicine, {{profile.first_name}}"}, {"type": "text", "content": "Finish your health profile so your care team has what they need before your first visit. It takes about five minutes."}, {"type": "action", "content": "Complete your profile", "href": "https://example.com/profile"}]}]}}'
        ```
      </CodeGroup>
    </div>

    Each call returns the template's `id`, which you need in the next step. The Node.js and Python tabs create all three emails. In the other tabs, run the same call twice more with these names and elements:

    ```json Elements for the other two emails theme={null}
    {
      "Book a visit": [
        { "type": "meta", "title": "Book your first visit" },
        {
          "type": "text",
          "content": "Your {{data.plan}} membership includes same-day {{data.service}} visits. Pick a time that works for you, in person or by video."
        },
        { "type": "action", "content": "Book a visit", "href": "https://example.com/book" }
      ],
      "Get the app": [
        { "type": "meta", "title": "Message your care team from the app" },
        {
          "type": "text",
          "content": "Between visits, the General Medicine app lets you message your care team, request prescription refills, and read your visit summaries."
        },
        { "type": "action", "content": "Get the app", "href": "https://example.com/app" }
      ]
    }
    ```

    A journey run sends each template's published version, which is why each one is created with `state: "PUBLISHED"`. Publishing the journey doesn't publish its templates.
  </Step>

  <Step title="Wire the nodes">
    <Endpoint method="PUT" path="/journeys/{templateId}" name="Replace a Journey" href="/docs/api-reference/journeys/replace-a-journey" /> sets the full node list. Nodes run in array order, so this list is the whole series. Each send node points at one of your templates by its `id` in `message.template`. Delays take an ISO 8601 duration. Both delays here are `PT1M` (one minute) so you can watch the whole series land in about two minutes.

    <CodeGroup>
      ```javascript Node.js theme={null}
      await client.journeys.replace(journey.id, {
        name: "New member welcome",
        nodes: [
          { type: "trigger", trigger_type: "api-invoke" },
          { type: "send", channel: "email", message: { template: welcome.id } },
          { type: "delay", mode: "duration", duration: "PT1M" },
          { type: "send", channel: "email", message: { template: booking.id } },
          { type: "delay", mode: "duration", duration: "PT1M" },
          { type: "send", channel: "email", message: { template: getApp.id } },
          { type: "exit" },
        ],
      });
      ```

      ```python Python theme={null}
      client.journeys.replace(
          journey.id,
          name="New member welcome",
          nodes=[
              {"type": "trigger", "trigger_type": "api-invoke"},
              {"type": "send", "channel": "email", "message": {"template": welcome.id}},
              {"type": "delay", "mode": "duration", "duration": "PT1M"},
              {"type": "send", "channel": "email", "message": {"template": booking.id}},
              {"type": "delay", "mode": "duration", "duration": "PT1M"},
              {"type": "send", "channel": "email", "message": {"template": get_app.id}},
              {"type": "exit"},
          ],
      )
      ```

      ```bash cURL theme={null}
      curl -X PUT https://api.courier.com/journeys/YOUR_JOURNEY_ID \
        -H "Authorization: Bearer $COURIER_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{
          "name": "New member welcome",
          "nodes": [
            { "type": "trigger", "trigger_type": "api-invoke" },
            { "type": "send", "channel": "email", "message": { "template": "WELCOME_TEMPLATE_ID" } },
            { "type": "delay", "mode": "duration", "duration": "PT1M" },
            { "type": "send", "channel": "email", "message": { "template": "BOOKING_TEMPLATE_ID" } },
            { "type": "delay", "mode": "duration", "duration": "PT1M" },
            { "type": "send", "channel": "email", "message": { "template": "APP_TEMPLATE_ID" } },
            { "type": "exit" }
          ]
        }'
      ```

      ```ruby Ruby theme={null}
      courier.journeys.replace(
        "YOUR_JOURNEY_ID",
        name: "New member welcome",
        nodes: [
          {type: "trigger", trigger_type: "api-invoke"},
          {type: "send", channel: "email", message: {template: "WELCOME_TEMPLATE_ID"}},
          {type: "delay", mode: "duration", duration: "PT1M"},
          {type: "send", channel: "email", message: {template: "BOOKING_TEMPLATE_ID"}},
          {type: "delay", mode: "duration", duration: "PT1M"},
          {type: "send", channel: "email", message: {template: "APP_TEMPLATE_ID"}},
          {type: "exit"}
        ]
      )
      ```

      ```go Go theme={null}
      _, err = client.Journeys.Replace(
      	context.TODO(),
      	"YOUR_JOURNEY_ID",
      	courier.JourneyReplaceParams{
      		CreateJourneyRequest: courier.CreateJourneyRequestParam{
      			Name: "New member welcome",
      			Nodes: []courier.JourneyNodeUnionParam{
      				{OfAPIInvokeTrigger: &courier.JourneyAPIInvokeTriggerNodeParam{
      					Type:        courier.JourneyAPIInvokeTriggerNodeTypeTrigger,
      					TriggerType: courier.JourneyAPIInvokeTriggerNodeTriggerTypeAPIInvoke,
      				}},
      				{OfSend: &courier.JourneySendNodeParam{
      					Type:    courier.JourneySendNodeTypeSend,
      					Channel: courier.JourneySendNodeChannelEmail,
      					Message: courier.JourneySendNodeMessageParam{Template: param.NewOpt("WELCOME_TEMPLATE_ID")},
      				}},
      				{OfDelayForDuration: &courier.JourneyDelayDurationNodeParam{
      					Type:     courier.JourneyDelayDurationNodeTypeDelay,
      					Mode:     courier.JourneyDelayDurationNodeModeDuration,
      					Duration: "PT1M",
      				}},
      				{OfSend: &courier.JourneySendNodeParam{
      					Type:    courier.JourneySendNodeTypeSend,
      					Channel: courier.JourneySendNodeChannelEmail,
      					Message: courier.JourneySendNodeMessageParam{Template: param.NewOpt("BOOKING_TEMPLATE_ID")},
      				}},
      				{OfDelayForDuration: &courier.JourneyDelayDurationNodeParam{
      					Type:     courier.JourneyDelayDurationNodeTypeDelay,
      					Mode:     courier.JourneyDelayDurationNodeModeDuration,
      					Duration: "PT1M",
      				}},
      				{OfSend: &courier.JourneySendNodeParam{
      					Type:    courier.JourneySendNodeTypeSend,
      					Channel: courier.JourneySendNodeChannelEmail,
      					Message: courier.JourneySendNodeMessageParam{Template: param.NewOpt("APP_TEMPLATE_ID")},
      				}},
      				{OfExit: &courier.JourneyExitNodeParam{Type: courier.JourneyExitNodeTypeExit}},
      			},
      		},
      	},
      )
      if err != nil {
      	panic(err.Error())
      }
      ```

      ```java Java theme={null}
      CreateJourneyRequest nodes = CreateJourneyRequest.builder()
          .name("New member welcome")
          .addNode(JourneyApiInvokeTriggerNode.builder()
              .type(JourneyApiInvokeTriggerNode.Type.TRIGGER)
              .triggerType(JourneyApiInvokeTriggerNode.TriggerType.API_INVOKE)
              .build())
          .addNode(JourneySendNode.builder()
              .type(JourneySendNode.Type.SEND)
              .channel(JourneySendNode.Channel.EMAIL)
              .message(JourneySendNode.Message.builder().template("WELCOME_TEMPLATE_ID").build())
              .build())
          .addNode(JourneyDelayDurationNode.builder()
              .type(JourneyDelayDurationNode.Type.DELAY)
              .mode(JourneyDelayDurationNode.Mode.DURATION)
              .duration("PT1M")
              .build())
          .addNode(JourneySendNode.builder()
              .type(JourneySendNode.Type.SEND)
              .channel(JourneySendNode.Channel.EMAIL)
              .message(JourneySendNode.Message.builder().template("BOOKING_TEMPLATE_ID").build())
              .build())
          .addNode(JourneyDelayDurationNode.builder()
              .type(JourneyDelayDurationNode.Type.DELAY)
              .mode(JourneyDelayDurationNode.Mode.DURATION)
              .duration("PT1M")
              .build())
          .addNode(JourneySendNode.builder()
              .type(JourneySendNode.Type.SEND)
              .channel(JourneySendNode.Channel.EMAIL)
              .message(JourneySendNode.Message.builder().template("APP_TEMPLATE_ID").build())
              .build())
          .addNode(JourneyExitNode.builder().type(JourneyExitNode.Type.EXIT).build())
          .build();

      client.journeys().replace(JourneyReplaceParams.builder()
          .templateId("YOUR_JOURNEY_ID")
          .createJourneyRequest(nodes)
          .build());
      ```

      ```php PHP theme={null}
      $client->journeys->replace(
        'YOUR_JOURNEY_ID',
        name: 'New member welcome',
        nodes: [
          ['type' => 'trigger', 'trigger_type' => 'api-invoke'],
          ['type' => 'send', 'channel' => 'email', 'message' => ['template' => 'WELCOME_TEMPLATE_ID']],
          ['type' => 'delay', 'mode' => 'duration', 'duration' => 'PT1M'],
          ['type' => 'send', 'channel' => 'email', 'message' => ['template' => 'BOOKING_TEMPLATE_ID']],
          ['type' => 'delay', 'mode' => 'duration', 'duration' => 'PT1M'],
          ['type' => 'send', 'channel' => 'email', 'message' => ['template' => 'APP_TEMPLATE_ID']],
          ['type' => 'exit'],
        ],
      );
      ```

      ```csharp C# theme={null}
      JourneyReplaceParams replaceParams = new()
      {
          TemplateID = "YOUR_JOURNEY_ID",
          Name = "New member welcome",
          Nodes =
          [
              new JourneyApiInvokeTriggerNode { Type = "trigger", TriggerType = "api-invoke" },
              new JourneySendNode { Type = "send", Channel = "email", Message = new() { Template = "WELCOME_TEMPLATE_ID" } },
              new JourneyDelayDurationNode { Type = "delay", Mode = "duration", Duration = "PT1M" },
              new JourneySendNode { Type = "send", Channel = "email", Message = new() { Template = "BOOKING_TEMPLATE_ID" } },
              new JourneyDelayDurationNode { Type = "delay", Mode = "duration", Duration = "PT1M" },
              new JourneySendNode { Type = "send", Channel = "email", Message = new() { Template = "APP_TEMPLATE_ID" } },
              new JourneyExitNode { Type = "exit" },
          ],
      };

      await client.Journeys.Replace(replaceParams);
      ```

      ```bash CLI theme={null}
      courier journeys replace \
        --template-id YOUR_JOURNEY_ID \
        --name "New member welcome" \
        --node '{"type": "trigger", "trigger_type": "api-invoke"}' \
        --node '{"type": "send", "channel": "email", "message": {"template": "WELCOME_TEMPLATE_ID"}}' \
        --node '{"type": "delay", "mode": "duration", "duration": "PT1M"}' \
        --node '{"type": "send", "channel": "email", "message": {"template": "BOOKING_TEMPLATE_ID"}}' \
        --node '{"type": "delay", "mode": "duration", "duration": "PT1M"}' \
        --node '{"type": "send", "channel": "email", "message": {"template": "APP_TEMPLATE_ID"}}' \
        --node '{"type": "exit"}'
      ```
    </CodeGroup>

    <Warning>
      Before real signups, replace the nodes again with `P1D` (one day) and `P3D` (three days) and publish. Runs use the published version, so a journey left on `PT1M` sends all three emails in two minutes.
    </Warning>
  </Step>

  <Step title="Publish">
    <Endpoint method="POST" path="/journeys/{templateId}/publish" name="Publish a Journey" href="/docs/api-reference/journeys/publish-a-journey" /> makes this version the one new runs use. Runs already in flight finish on the version they started with.

    <CodeGroup>
      ```javascript Node.js theme={null}
      await client.journeys.publish(journey.id);
      ```

      ```python Python theme={null}
      client.journeys.publish(journey.id)
      ```

      ```bash cURL theme={null}
      curl -X POST https://api.courier.com/journeys/YOUR_JOURNEY_ID/publish \
        -H "Authorization: Bearer $COURIER_API_KEY"
      ```

      ```ruby Ruby theme={null}
      courier.journeys.publish("YOUR_JOURNEY_ID")
      ```

      ```go Go theme={null}
      _, err = client.Journeys.Publish(context.TODO(), "YOUR_JOURNEY_ID", courier.JourneyPublishParams{})
      if err != nil {
      	panic(err.Error())
      }
      ```

      ```java Java theme={null}
      client.journeys().publish("YOUR_JOURNEY_ID");
      ```

      ```php PHP theme={null}
      $client->journeys->publish('YOUR_JOURNEY_ID');
      ```

      ```csharp C# theme={null}
      await client.Journeys.Publish(new JourneyPublishParams { TemplateID = "YOUR_JOURNEY_ID" });
      ```

      ```bash CLI theme={null}
      courier journeys publish --template-id YOUR_JOURNEY_ID
      ```
    </CodeGroup>
  </Step>

  <Step title="Start it from your signup code">
    <Endpoint method="POST" path="/journeys/{templateId}/invoke" name="Invoke a Journey" href="/docs/api-reference/journeys/invoke-a-journey" /> starts one run for one user. Call it from your signup code right after you create the account, using the journey ID you saved in step 2. To try it now with a Test key, replace `sarah@example.com` with the address you signed up with, so the series lands in your inbox. Test includes a built-in email provider for that address. Any other address, or a Production key, needs <Guide href="/docs/guides/send-your-first-email">an email provider of your own</Guide>.

    <CodeGroup>
      ```javascript Node.js theme={null}
      const { runId } = await client.journeys.invoke("YOUR_JOURNEY_ID", {
        user_id: "user_123",
        profile: { email: "sarah@example.com", first_name: "Sarah" },
        data: { plan: "Family", service: "primary care" },
      });
      ```

      ```python Python theme={null}
      response = client.journeys.invoke(
          "YOUR_JOURNEY_ID",
          user_id="user_123",
          profile={"email": "sarah@example.com", "first_name": "Sarah"},
          data={"plan": "Family", "service": "primary care"},
      )
      run_id = response.run_id
      ```

      ```bash cURL theme={null}
      curl -X POST https://api.courier.com/journeys/YOUR_JOURNEY_ID/invoke \
        -H "Authorization: Bearer $COURIER_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{
          "user_id": "user_123",
          "profile": { "email": "sarah@example.com", "first_name": "Sarah" },
          "data": { "plan": "Family", "service": "primary care" }
        }'
      ```

      ```ruby Ruby theme={null}
      response = courier.journeys.invoke(
        "YOUR_JOURNEY_ID",
        user_id: "user_123",
        profile: {email: "sarah@example.com", first_name: "Sarah"},
        data: {plan: "Family", service: "primary care"}
      )
      run_id = response.run_id
      ```

      ```go Go theme={null}
      response, err := client.Journeys.Invoke(
      	context.TODO(),
      	"YOUR_JOURNEY_ID",
      	courier.JourneyInvokeParams{
      		JourneysInvokeRequest: courier.JourneysInvokeRequestParam{
      			UserID:  courier.String("user_123"),
      			Profile: map[string]any{"email": "sarah@example.com", "first_name": "Sarah"},
      			Data:    map[string]any{"plan": "Family", "service": "primary care"},
      		},
      	},
      )
      if err != nil {
      	panic(err.Error())
      }
      runID := response.RunID
      ```

      ```java Java theme={null}
      JourneyInvokeParams invokeParams = JourneyInvokeParams.builder()
          .templateId("YOUR_JOURNEY_ID")
          .journeysInvokeRequest(JourneysInvokeRequest.builder()
              .userId("user_123")
              .profile(JourneysInvokeRequest.Profile.builder()
                  .putAdditionalProperty("email", JsonValue.from("sarah@example.com"))
                  .putAdditionalProperty("first_name", JsonValue.from("Sarah"))
                  .build())
              .data(JourneysInvokeRequest.Data.builder()
                  .putAdditionalProperty("plan", JsonValue.from("Family"))
                  .putAdditionalProperty("service", JsonValue.from("primary care"))
                  .build())
              .build())
          .build();

      String runId = client.journeys().invoke(invokeParams).runId();
      ```

      ```php PHP theme={null}
      $response = $client->journeys->invoke(
        'YOUR_JOURNEY_ID',
        userID: 'user_123',
        profile: ['email' => 'sarah@example.com', 'first_name' => 'Sarah'],
        data: ['plan' => 'Family', 'service' => 'primary care'],
      );
      $runId = $response->runID;
      ```

      ```csharp C# theme={null}
      JourneyInvokeParams invokeParams = new()
      {
          TemplateID = "YOUR_JOURNEY_ID",
          UserID = "user_123",
          Profile = new Dictionary<string, JsonElement>()
          {
              { "email", JsonSerializer.SerializeToElement("sarah@example.com") },
              { "first_name", JsonSerializer.SerializeToElement("Sarah") },
          },
          Data = new Dictionary<string, JsonElement>()
          {
              { "plan", JsonSerializer.SerializeToElement("Family") },
              { "service", JsonSerializer.SerializeToElement("primary care") },
          },
      };

      var response = await client.Journeys.Invoke(invokeParams);
      var runId = response.RunID;
      ```

      ```bash CLI theme={null}
      courier journeys invoke \
        --template-id YOUR_JOURNEY_ID \
        --user-id user_123 \
        --profile '{"email": "sarah@example.com", "first_name": "Sarah"}' \
        --data '{"plan": "Family", "service": "primary care"}'
      ```

      ```text MCP theme={null}
      With Courier MCP, start my New member welcome journey for user_123 (Sarah, sarah@example.com) with a Family membership for primary care.
      ```
    </CodeGroup>

    The run starts in the background, so the call returns before anything sends. The welcome email goes out within seconds, and the response carries a `runId`:

    ```json theme={null}
    { "runId": "778b97e1-4850-4d56-87b7-b4c9b2d88004" }
    ```
  </Step>

  <Step title="Watch the run">
    <Endpoint method="GET" path="/journeys/runs/{run_id}/steps" name="List steps for a Journey run" href="/docs/api-reference/journeys/list-steps-for-a-journey-run" /> returns each node the run reached, in node order rather than the order they ran, so the examples sort by `created_at`, the time each step started. The first delay reads `WAITING` until its minute is up, and each send step carries the `message_id` it produced.

    <CodeGroup>
      ```javascript Node.js theme={null}
      const { steps } = await client.journeys.runs.listSteps(runId);
      steps
        .sort((a, b) => (a.created_at ?? "").localeCompare(b.created_at ?? ""))
        .forEach((s) => console.log(s.action, s.status, s.message_id ?? ""));
      ```

      ```python Python theme={null}
      steps = client.journeys.runs.list_steps(run_id).steps
      for s in sorted(steps, key=lambda s: s.created_at or ""):
          print(s.action, s.status, s.message_id or "")
      ```

      ```bash cURL theme={null}
      # Sorting uses jq (https://jqlang.org).
      curl https://api.courier.com/journeys/runs/YOUR_RUN_ID/steps \
        -H "Authorization: Bearer $COURIER_API_KEY" \
        | jq '.steps | sort_by(.created_at)[] | {action, status, message_id}'
      ```

      ```ruby Ruby theme={null}
      courier.journeys.runs.list_steps(run_id).steps.sort_by { |s| s.created_at.to_s }.each do |s|
        puts("#{s.action} #{s.status} #{s.message_id}")
      end
      ```

      ```go Go theme={null}
      // Add "sort" to your imports.
      stepsResponse, err := client.Journeys.Runs.ListSteps(context.TODO(), runID)
      if err != nil {
      	panic(err.Error())
      }
      steps := stepsResponse.Steps
      sort.Slice(steps, func(i, j int) bool { return steps[i].CreatedAt < steps[j].CreatedAt })
      for _, s := range steps {
      	fmt.Println(s.Action, s.Status, s.MessageID)
      }
      ```

      ```java Java theme={null}
      client.journeys().runs().listSteps(runId).steps().stream()
          .sorted(java.util.Comparator.comparing((JourneyRunStep s) -> s.createdAt().orElse("")))
          .forEach(s -> System.out.println(s.action() + " " + s.status() + " " + s.messageId().orElse("")));
      ```

      ```php PHP theme={null}
      $steps = $client->journeys->runs->listSteps($runId)->steps;
      usort($steps, fn($a, $b) => strcmp($a->createdAt ?? '', $b->createdAt ?? ''));

      foreach ($steps as $s) {
        echo $s->action . ' ' . $s->status . ' ' . ($s->messageID ?? '') . PHP_EOL;
      }
      ```

      ```csharp C# theme={null}
      var stepsResponse = await client.Journeys.Runs.ListSteps(new RunListStepsParams { RunID = runId });

      foreach (var s in stepsResponse.Steps.OrderBy(step => step.CreatedAt))
      {
          Console.WriteLine($"{s.Action} {s.Status} {s.MessageID}");
      }
      ```

      ```bash CLI theme={null}
      # Sorting uses jq (https://jqlang.org).
      courier journeys:runs list-steps --run-id YOUR_RUN_ID \
        | jq '.steps | sort_by(.created_at)[] | {action, status, message_id}'
      ```
    </CodeGroup>

    The journey's **Logs** tab in <AppLink href="https://app.courier.com/orchestration/journeys">Journeys</AppLink> draws the same run on the canvas. See <Doc href="/docs/monitor/logs#journey-logs">journey logs</Doc>.
  </Step>
</Steps>

## FAQ

<AccordionGroup>
  <Accordion title="Why didn't an email arrive?">
    Check that you published the journey and created each template with `state: "PUBLISHED"`. Then look up the step's `message_id` in <AppLink href="https://app.courier.com/logs">Logs</AppLink>. Delivering to real addresses needs an <Guide href="/docs/guides/send-your-first-email">email provider of your own</Guide>.
  </Accordion>

  <Accordion title="Can I edit the journey in the canvas?">
    The canvas and the API build the same object, so the journey opens in <AppLink href="https://app.courier.com/orchestration/journeys">Journeys</AppLink> like any other. To build one in the canvas from the start, follow <Guide href="/docs/guides/create-your-first-journey">Create your first journey</Guide>.
  </Accordion>

  <Accordion title="How do I stop the series for one user?">
    Add `"cancelation_token": "welcome-{{recipient}}"` to the create or replace body. `{{recipient}}` resolves to the `user_id` you invoke with, so Sarah's run carries `welcome-user_123`. Cancel that token when she unsubscribes or deletes her account, and her remaining emails don't send. See <Doc href="/docs/journeys/invoke#cancel-a-run">Cancel a run</Doc>.
  </Accordion>

  <Accordion title="How do I skip users who already got started?">
    Add a branch node after the first delay that checks whether the user already booked a visit, and end the run on that path. <Guide href="/docs/guides/build-an-onboarding-sequence">Build an onboarding sequence</Guide> walks through it.
  </Accordion>

  <Accordion title="What happens if my signup code retries the invoke?">
    Each invoke starts a new run, so a retry sends the welcome email twice. Send an `Idempotency-Key` header, such as `signup-user_123`, and a repeated key returns the first run instead. See <Doc href="/docs/journeys/invoke">Invoke a journey</Doc>.
  </Accordion>
</AccordionGroup>
