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

# Build a journey

> Build a journey in the canvas or as JSON over the API, then publish and invoke it.

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

A Journey runs a graph of nodes once per user when you trigger it.

The canvas and the API build the same object, so pick the one that matches who owns the flow. A journey a product manager will keep tuning belongs in the canvas. A journey generated per customer, or checked into your repo, belongs in the API.

**A journey is not a broadcast.** It runs per user, from an event or an API call, rather than going out to a group at a moment you choose. If you want one message to a list right now, use a <Doc href="/docs/broadcasts/overview">broadcast</Doc>.

## Build in the canvas

<Steps>
  <Step title="Create the Journey">
    Open <AppLink href="https://app.courier.com/orchestration/journeys">Journeys</AppLink>, select **New Journey**, and name it. Choose the **API Invoke** trigger so your code starts each run.

    <Frame>
      <img src="https://mintcdn.com/courier-4f1f25dc/9rcgucLA9fBnJt_U/assets/journey-new-journey-trigger.webp?fit=max&auto=format&n=9rcgucLA9fBnJt_U&q=85&s=7c29c7f4a5f95ec0bb49a95a3b455707" alt="The New Journey dialog with a name field and four trigger options" className="mx-auto" style={{ width:"72%" }} width="1740" height="1410" data-path="assets/journey-new-journey-trigger.webp" />
    </Frame>
  </Step>

  <Step title="Add a send node">
    Drop a **send** node on the canvas, pick a channel, and select **+ Create** to build its content inline. Each send node owns one Template scoped to this Journey.
  </Step>

  <Step title="Add a delay">
    Drag a **delay** node between the trigger and the send. Set it to wait a day. Journeys also offer branch, fetch data, throttle, batch, and digest nodes.
  </Step>

  <Step title="Publish">
    Select **Publish** to lock the draft as a version. New runs use it. Runs already in flight finish on the version they started with.

    Copy the Journey ID from the editor.
  </Step>
</Steps>

## Build with the API

<Steps>
  <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 the node list. The list starts with a trigger and ends with an exit, and node ids are left out because the API assigns them. It returns the Journey ID.

    <CodeGroup>
      ```javascript Node.js theme={null}
      const journey = await client.journeys.create({
        name: 'Welcome journey',
        nodes: [{ type: 'trigger', trigger_type: 'api-invoke' }, { type: 'exit' }],
      });
      ```

      ```python Python theme={null}
      journey = client.journeys.create(
          name="Welcome journey",
          nodes=[{"type": "trigger", "trigger_type": "api-invoke"}, {"type": "exit"}],
      )
      ```

      ```bash cURL theme={null}
      curl --request POST \
        --url https://api.courier.com/journeys \
        --header "Authorization: Bearer $COURIER_API_KEY" \
        --header "Content-Type: application/json" \
        --data '{
          "name": "Welcome journey",
          "nodes": [{ "type": "trigger", "trigger_type": "api-invoke" }, { "type": "exit" }]
        }'
      ```

      ```ruby Ruby theme={null}
      journey = courier.journeys.create(
        name: "Welcome journey",
        nodes: [{type: "trigger", trigger_type: "api-invoke"}, {type: "exit"}]
      )
      ```

      ```go Go theme={null}
      journey, err := client.Journeys.New(context.TODO(), courier.JourneyNewParams{
      	CreateJourneyRequest: courier.CreateJourneyRequestParam{
      		Name: "Welcome journey",
      		Nodes: []courier.JourneyNodeUnionParam{
      			{OfAPIInvokeTrigger: &courier.JourneyAPIInvokeTriggerNodeParam{
      				Type:        courier.JourneyAPIInvokeTriggerNodeTypeTrigger,
      				TriggerType: courier.JourneyAPIInvokeTriggerNodeTriggerTypeAPIInvoke,
      			}},
      			{OfExit: &courier.JourneyExitNodeParam{Type: courier.JourneyExitNodeTypeExit}},
      		},
      	},
      })
      ```

      ```java Java theme={null}
      CreateJourneyRequest params = CreateJourneyRequest.builder()
          .name("Welcome journey")
          .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(params);
      ```

      ```php PHP theme={null}
      $journey = $client->journeys->create(
        name: 'Welcome journey',
        nodes: [['type' => 'trigger', 'trigger_type' => 'api-invoke'], ['type' => 'exit']],
      );
      ```

      ```csharp C# theme={null}
      JourneyCreateParams parameters = new()
      {
          Name = "Welcome journey",
          Nodes = [new JourneyApiInvokeTriggerNode { Type = "trigger", TriggerType = "api-invoke" }, new JourneyExitNode { Type = "exit" }],
      };

      var journey = await client.Journeys.Create(parameters);
      ```

      ```bash CLI theme={null}
      courier journeys create \
        --api-key "$COURIER_API_KEY" \
        --name "Welcome journey" \
        --node '{"type":"trigger","trigger_type":"api-invoke"}' \
        --node '{"type":"exit"}'
      ```

      ```text MCP theme={null}
      With Courier MCP, create a journey called Welcome journey with an API-invoke trigger and an exit node.
      ```
    </CodeGroup>
  </Step>

  <Step title="Add a scoped Template">
    <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 scoped Template</Endpoint> creates the content a send node uses. These Templates belong to the Journey, separate from your workspace Templates.

    <CodeGroup>
      ```javascript Node.js theme={null}
      const template = await client.journeys.templates.create('YOUR_JOURNEY_ID', {
        channel: 'email',
        notification: {
          name: 'Order shipped',
          tags: [],
          brand: null,
          subscription: null,
          content: {
            version: '2022-01-01',
            elements: [
              {
                type: 'channel',
                channel: 'email',
                elements: [{ type: 'text', content: 'Your order {{order_id}} shipped.' }],
              },
            ],
          },
        },
      });
      ```

      ```python Python theme={null}
      template = client.journeys.templates.create(
          template_id="YOUR_JOURNEY_ID",
          channel="email",
          notification={
              "name": "Order shipped",
              "tags": [],
              "brand": None,
              "subscription": None,
              "content": {
                  "version": "2022-01-01",
                  "elements": [
                      {
                          "type": "channel",
                          "channel": "email",
                          "elements": [{"type": "text", "content": "Your order {{order_id}} shipped."}],
                      }
                  ],
              },
          },
      )
      ```

      ```bash cURL theme={null}
      curl --request POST \
        --url https://api.courier.com/journeys/YOUR_JOURNEY_ID/templates \
        --header "Authorization: Bearer $COURIER_API_KEY" \
        --header "Content-Type: application/json" \
        --data '{
          "channel": "email",
          "notification": {
            "name": "Order shipped",
            "tags": [],
            "brand": null,
            "subscription": null,
            "content": {
              "version": "2022-01-01",
              "elements": [
                {
                  "type": "channel",
                  "channel": "email",
                  "elements": [{ "type": "text", "content": "Your order {{order_id}} shipped." }]
                }
              ]
            }
          }
        }'
      ```

      ```ruby Ruby theme={null}
      template = courier.journeys.templates.create(
        "YOUR_JOURNEY_ID",
        channel: "email",
        notification: {
          name: "Order shipped",
          tags: [],
          brand: nil,
          subscription: nil,
          content: {elements: [{type: "channel", channel: "email", elements: [{type: "text", content: "Your order {{order_id}} shipped."}]}], version: "2022-01-01"}
        }
      )
      ```

      ```go Go theme={null}
      // Elemental nodes carry no typed content field, so pass the notification as raw JSON.
      notification := param.Override[courier.JourneyTemplateCreateRequestNotificationParam](json.RawMessage(`{
        "name": "Order shipped",
        "tags": [],
        "brand": null,
        "subscription": null,
        "content": {
          "version": "2022-01-01",
          "elements": [
            {
              "type": "channel",
              "channel": "email",
              "elements": [{ "type": "text", "content": "Your order {{order_id}} shipped." }]
            }
          ]
        }
      }`))

      template, err := client.Journeys.Templates.New(
      	context.TODO(),
      	"YOUR_JOURNEY_ID",
      	courier.JourneyTemplateNewParams{
      		JourneyTemplateCreateRequest: courier.JourneyTemplateCreateRequestParam{
      			Channel:      "email",
      			Notification: notification,
      		},
      	},
      )
      ```

      ```java Java theme={null}
      TemplateCreateParams params = TemplateCreateParams.builder()
          .templateId("YOUR_JOURNEY_ID")
          .journeyTemplateCreateRequest(JourneyTemplateCreateRequest.builder()
              .channel("email")
              .notification(JourneyTemplateCreateRequest.Notification.builder()
                  .name("Order shipped")
                  .tags(java.util.List.of())
                  .brand(java.util.Optional.empty())
                  .subscription(java.util.Optional.empty())
                  .content(JourneyTemplateCreateRequest.Notification.Content.builder()
                      // The channel builder has no addElement, so its children go in as an additional property.
                      .addElement(ElementalChannelNodeWithType.builder()
                          .type(ElementalChannelNodeWithType.Type.CHANNEL)
                          .channel("email")
                          .putAdditionalProperty("elements", JsonValue.from(java.util.List.of(
                              java.util.Map.of("type", "text", "content", "Your order {{order_id}} shipped."))))
                          .build())
                      .version(JourneyTemplateCreateRequest.Notification.Content.Version._2022_01_01)
                      .build())
                  .build())
              .build())
          .build();
      JourneyTemplateGetResponse template = client.journeys().templates().create(params);
      ```

      ```php PHP theme={null}
      $template = $client->journeys->templates->create(
        'YOUR_JOURNEY_ID',
        channel: 'email',
        notification: [
          'name' => 'Order shipped',
          'tags' => [],
          'brand' => null,
          'subscription' => null,
          'content' => ['version' => '2022-01-01', 'elements' => [['type' => 'channel', 'channel' => 'email', 'elements' => [['type' => 'text', 'content' => 'Your order {{order_id}} shipped.']]]]],
        ],
      );
      ```

      ```csharp C# theme={null}
      TemplateCreateParams parameters = new()
      {
          TemplateID = "YOUR_JOURNEY_ID",
          Channel = "email",
          Notification = new()
          {
              Name = "Order shipped",
              Tags = [],
              Brand = null,
              Subscription = null,
              Content = new()
              {
                  // Elemental nodes expose no typed content property, so build the node from raw JSON.
                  Elements =
                  [
                      ElementalChannelNodeWithType.FromRawUnchecked(
                          JsonSerializer.Deserialize<Dictionary<string, JsonElement>>("""
                          {
                            "type": "channel",
                            "channel": "email",
                            "elements": [{ "type": "text", "content": "Your order {{order_id}} shipped." }]
                          }
                          """)),
                  ],
                  Version = "2022-01-01",
              },
          },
      };

      var template = await client.Journeys.Templates.Create(parameters);
      ```

      ```bash CLI theme={null}
      courier journeys:templates create \
        --api-key "$COURIER_API_KEY" \
        --template-id YOUR_JOURNEY_ID \
        --channel email \
        --notification '{"name":"Order shipped","tags":[],"brand":null,"subscription":null,"content":{"version":"2022-01-01","elements":[{"type":"channel","channel":"email","elements":[{"type":"text","content":"Your order {{order_id}} shipped."}]}]}}'
      ```

      ```text MCP theme={null}
      With Courier MCP, add an email template called Order shipped to my journey.
      ```
    </CodeGroup>
  </Step>

  <Step title="Wire the graph">
    <Endpoint method="PUT" path="/journeys/{templateId}" name="Replace a Journey" href="/docs/api-reference/journeys/replace-a-journey" /> takes the full node list, in the same body shape as the create call:

    * the trigger
    * a `send` node referencing the Template ID
    * any `delay` or `branch` nodes
  </Step>

  <Step title="Publish">
    <Endpoint method="POST" path="/journeys/{templateId}/publish" name="Publish a Journey" href="/docs/api-reference/journeys/publish-a-journey" /> snapshots the draft as the active version.

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

      ```python Python theme={null}
      client.journeys.publish(
          template_id="YOUR_JOURNEY_ID",
      )
      ```

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

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

      ```go Go theme={null}
      _, err := client.Journeys.Publish(
      	context.TODO(),
      	"YOUR_JOURNEY_ID",
      	courier.JourneyPublishParams{},
      )
      ```

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

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

      ```csharp C# theme={null}
      JourneyPublishParams parameters = new() { TemplateID = "YOUR_JOURNEY_ID" };

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

      ```bash CLI theme={null}
      courier journeys publish \
        --api-key "$COURIER_API_KEY" \
        --template-id YOUR_JOURNEY_ID
      ```

      ```text MCP theme={null}
      With Courier MCP, publish my journey so the draft becomes the active version.
      ```
    </CodeGroup>
  </Step>
</Steps>

## Invoke a run

Either path ends the same way. Publishing makes the journey live, and a run starts when your app invokes it by ID or alias.

<Doc href="/docs/journeys/invoke">Invoke a Journey</Doc> covers the call in every language. It also covers the `Idempotency-Key` that stops a retry starting a second run, and how to inspect or cancel a run by its `runId`.

## Limits & behavior

**Editing a send node's content after publishing changes nothing on its own.** The edit saves as a new template draft. The journey keeps sending the version it was published with, and no error marks the difference, so publish the journey again to make the edit live.

**Runs already in flight finish on the version they started with.** A publish does not migrate them, so a change to a multi-day flow reaches only the runs that begin after it.
