1
Get your API key
Copy a key from and export it. Every example reads it from
COURIER_API_KEY.export COURIER_API_KEY="YOUR_COURIER_API_KEY"
2
Create the 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.The response returns the journey’s
// 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);
# 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)
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" }
]
}'
# 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 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)
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());
$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;
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}");
# npm install -g @trycourier/cli
courier journeys create \
--name "New member welcome" \
--node '{"type": "trigger", "trigger_type": "api-invoke"}' \
--node '{"type": "exit"}'
id. Every call below uses it.3
Write the three emails
once per email. The Each call returns the template’s A journey run sends each template’s published version, which is why each one is created with
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.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" },
]);
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"},
])
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"
}
]
}
]
}
}
}'
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}")
// 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)
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());
$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;
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}");
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"}]}]}}'
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:Elements for the other two emails
{
"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" }
]
}
state: "PUBLISHED". Publishing the journey doesn’t publish its templates.4
Wire the nodes
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.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" },
],
});
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"},
],
)
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" }
]
}'
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"}
]
)
_, 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())
}
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());
$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'],
],
);
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);
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"}'
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.5
Publish
makes this version the one new runs use. Runs already in flight finish on the version they started with.
await client.journeys.publish(journey.id);
client.journeys.publish(journey.id)
curl -X POST https://api.courier.com/journeys/YOUR_JOURNEY_ID/publish \
-H "Authorization: Bearer $COURIER_API_KEY"
courier.journeys.publish("YOUR_JOURNEY_ID")
_, err = client.Journeys.Publish(context.TODO(), "YOUR_JOURNEY_ID", courier.JourneyPublishParams{})
if err != nil {
panic(err.Error())
}
client.journeys().publish("YOUR_JOURNEY_ID");
$client->journeys->publish('YOUR_JOURNEY_ID');
await client.Journeys.Publish(new JourneyPublishParams { TemplateID = "YOUR_JOURNEY_ID" });
courier journeys publish --template-id YOUR_JOURNEY_ID
6
Start it from your signup code
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 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
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 .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" },
});
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
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" }
}'
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
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
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();
$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;
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;
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"}'
With Courier MCP, start my New member welcome journey for user_123 (Sarah, sarah@example.com) with a Family membership for primary care.
runId:{ "runId": "778b97e1-4850-4d56-87b7-b4c9b2d88004" }
7
Watch the run
returns each node the run reached, in node order rather than the order they ran, so the examples sort by The journey’s Logs tab in draws the same run on the canvas. See .
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.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 ?? ""));
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 "")
# 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}'
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
// 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)
}
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("")));
$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;
}
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}");
}
# 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}'
FAQ
Why didn't an email arrive?
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 . Delivering to real addresses needs an .Can I edit the journey in the canvas?
Can I edit the journey in the canvas?
The canvas and the API build the same object, so the journey opens in like any other. To build one in the canvas from the start, follow .
How do I stop the series for one user?
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 .How do I skip users who already got started?
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. walks through it.
What happens if my signup code retries the invoke?
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 .