See what you’ll build
A trial lifecycle that starts when a user starts a trial and ends when they upgrade: The same pattern covers most lifecycle messages. Map each product event to its own Journey:Prerequisites
- A Courier API key
- A connected email provider
- A connected SMS provider
- A connected push provider
- Push delivery tracking in your app
- Courier Inbox in your app
- Your users stored in Courier
- A public HTTPS endpoint that returns a user’s subscription status
Build the Journey
You don’t need every channel to follow along. Leave out the send node for any channel you haven’t connected, and the rest of the Journey still runs. If you leave out push, drop the one-hour delay, the fetch after it, and the SMS node’ssend_status condition too, since that condition reads the push node’s status.
Create the Journey and its trigger
trial-lifecycle, and choose the API Invoke trigger.Add three fields to the trigger schema. Every node downstream can read them:plan: the plan the user is trialingreminder_at: when to send the reminder, as an ISO 8601 datetime. Your app computes it, for example three days before the trial endstrial_ends_at: when the trial ends, as an ISO 8601 datetime
reminder_at in the future and at least two hours before trial_ends_at. That leaves room for the push, the one-hour SMS fallback, and the last-day email to go out in order. For a trial too short for that, move reminder_at closer to the start.If you already send events through Segment, pick the Segment trigger instead, and the event’s properties become the run’s data. See Start a journey from Segment. Segment can deliver an event more than once, and a Segment-triggered run has no idempotency key, so a repeat starts a second run. If duplicates matter for this flow, invoke the Journey from your backend with an idempotency key instead.Welcome them in the app and by email
{{plan}}.Add a second send node on the Email channel with the same welcome and a link to your most important setup step.Each send node owns a template scoped to this Journey. A run sends each template’s published version, so publish every template before you publish the Journey.Wait until the reminder time
data.reminder_at as the field.Each user’s run now waits until their own reminder time, so your code holds no timers. For a gap longer than one delay covers, chain delays.Check whether they already upgraded
data.plan still says the trial plan a week later. To read the user’s current state, add a fetch data node that calls your own API, such as https://api.example.com/users/{{recipient}}/subscription. The response merges into the run’s data.Return the status in a field the trigger doesn’t set, such as subscription_status. The default Soft Merge never overwrites a field the run already has, so a response field named plan would leave the stale value in place.Protect the endpoint. Turn on Headers in the fetch node and send a secret, such as Authorization: Bearer <your token>, that your endpoint checks.A failed fetch ends the run. A non-2xx response, a redirect, or no answer within 10 seconds stops the run with an error, and that user gets no reminders. Have the endpoint return 200 for every user in a trial, including ones it doesn’t know yet.Then add a branch node. Give the first path the condition data.subscription_status is equal active, and wire it to an exit. The default path continues to the reminders.Remind them on push, and fall back to SMS
UNROUTABLE. Any status short of DELIVERED passes the SMS condition below, so they get the SMS.Add a delay of one hour after the push node. Your app reports delivery after the device receives the push, so a check made right after the send would find most pushes not yet delivered.After the delay, add a fetch data node that calls the same subscription endpoint with merge strategy Overwrite, so an upgrade during that hour is seen.Then add an SMS send node with the same reminder and two conditions in one group, so both must hold: send_status.<push node ID> was not DELIVERED, and data.subscription_status is not equal active. The node ID is the push node’s id. Over the API, it comes back in the response when you create or replace the Journey, and it stays the same when you send the nodes back with their IDs. GET /journeys/{templateId} returns it once the Journey is published. Users still on the trial whose push didn’t arrive within the hour, including users without your app, get the SMS. Everyone else skips it.This step depends on push delivery tracking. A push reaches DELIVERED only when your app reports it, through a Courier mobile SDK or the payload’s trackingUrl. Without that, every push stays at SENT and every user also gets the SMS.To keep reminders in business hours, compute reminder_at in the user’s daytime rather than adding a send window. A send window holds delivery until it opens, so a push held overnight would still be undelivered when the one-hour check runs.Send the last-day email
data.trial_ends_at.Check the subscription again, since the user may have upgraded after the reminder. Add a second fetch data node that calls the same endpoint, and set its merge strategy to Overwrite. The default Soft Merge would keep the status the first fetch stored. Then add a branch on data.subscription_status is equal active that exits, as before.On the default path, add an Email send node that tells the user what they keep if they upgrade today.To learn which version converts better, turn this node into an experiment and compare opens and clicks per variant.Set a cancelation token and publish
trial-{{recipient}}. Each user’s run carries its own token, so you can stop one user’s messages without touching anyone else’s.Select Publish, then copy the Journey ID from the editor. New runs use the published version, and runs already in flight finish on the version they started with.Trigger it from your app
Invoke the Journey when the trial starts
POST /journeys/{templateId}/invoke from the code that starts the trial, with the user and the trigger data. Replace trial-lifecycle with the Journey ID you copied, or with the Journey’s alias if you set one.Compute reminder_at and trial_ends_at from the user’s real trial as ISO 8601 strings. The samples use four and seven days from now. In production, set the reminder to a daytime hour in the user’s time zone. Keep reminder_at within one delay’s limit of the invoke, and trial_ends_at within that limit of reminder_at.runId. Store it to inspect or cancel this exact run later.Stop the messages when they upgrade
POST /journeys/cancel with the user’s token. Pending steps never fire, and anything already delivered stays delivered.Verify
Run it on a short clock
trial-test-{{recipient}} so a test cancel can’t stop a real run. Publish the copy’s templates and the copy, then invoke the copy’s Journey ID, not the original’s, for a test user with reminder_at a few minutes from now and trial_ends_at a few minutes after that. The whole lifecycle plays out in about ten minutes instead of two weeks.Follow the run
GET /journeys/runs/{run_id}/steps returns the same steps.Check each path
subscription_status: active and confirm the run exits at the branch.Confirm a cancel stops the run
trial-test-<user ID> while the run waits at a delay. Open the run in the Logs tab and confirm it reads Canceled. A cancel by token answers the same way whether or not it matched a run, so the run’s status is the proof.Measure what’s working
- Per message: Analytics shows delivery, open, and click rates for each Journey.
- Per path: in the run logs, compare how many runs exit at either upgrade branch with how many reach the last-day email.
- Per variant: an experiment on any send node compares versions of one message.
- In your own systems: an outbound webhook forwards delivery events so you can join them to conversion data.
Fix common problems
No run appears after invoking
No run appears after invoking
The branch reads stale data
The branch reads stale data
A run ended at the fetch node
A run ended at the fetch node
200 for every user the Journey can reach.A send node was skipped
A send node was skipped
Cancel by token didn't stop a run
Cancel by token didn't stop a run
run_id.Every user got both the push and the SMS
Every user got both the push and the SMS
DELIVERED, so the SMS condition passed. Check the push in Logs. A push stuck at SENT means your app isn’t reporting delivery. See push tracking.A user got the sequence twice
A user got the sequence twice
trial-started-trial_456, so a retry returns the existing run and a later trial still starts its own.