Ad Journeys — Sequential & Branching Ad Campaigns
Ad Journeys let you run multi-step ad sequences across newsletter audiences. Each step delivers a different creative to subscribers who already engaged with the previous one — no opt-in required, no ESP changes needed. Choose between a Linear Steps flow or a fully customisable Graph Builder canvas with branching, A/B testing, wait nodes, and more.
What is an Ad Journey?
An Ad Journey is a sequential campaign that guides subscribers through a branded narrative across multiple newsletter opens. MailAdx handles the sequencing, eligibility checks, and delivery automatically.
Common use cases:
- Awareness → Consideration → Conversion: introduce your brand, then deepen with product proof, then close with an offer
- Conditional funnels: show different creative to clickers vs. non-clickers at each step
- A/B split tests: automatically compare two creative paths and declare a winner
- Onboarding flows: Welcome → Setup tip → Success story with skip logic
Journey modes: Linear Steps vs. Graph Builder
When creating a new journey, choose between two modes:
| Feature | Linear Steps | Graph Builder |
|---|---|---|
| Setup | Simple form, 2–5 steps | Visual drag-and-drop canvas |
| Branching | No | Yes — BRANCH and AB_SPLIT nodes |
| A/B testing | No | Yes — AB_SPLIT with automatic winner declaration |
| Wait logic | minWaitDays per step | WAIT node with 6 wait modes |
| Analytics | Per-step funnel | Per-node stats with graph overlay |
| Max steps/nodes | 5 steps | Up to 50 nodes, max depth 10 |
Linear Steps (Phase 1)
The simplest way to get started. A journey has 2–5 steps in sequence. Each step requires a creative ID and an optional minimum wait period.
How subscribers advance through steps
A subscriber advances from step N to step N+1 when:
- They received an impression of step N
- The minimum wait period (
minWaitDays) has elapsed — or they clicked the ad (click bypasses the wait)
Advancement runs automatically in the background every 15 minutes. On each subsequent newsletter send, MailAdx checks the subscriber's journey state and serves the correct step.
Graph Builder (Phase 2)
The Graph Builder lets you design non-linear journeys using an interactive canvas. Drag nodes from the left palette, connect them with edges, and configure each node's behaviour in the right-hand panel.
Node types
| Node | Purpose |
|---|---|
| ENTRY | Required starting node. Every journey must have exactly one ENTRY node. |
| AD_STEP | Serving node — delivers a creative to the subscriber. Requires a Creative ID in config. |
| WAIT | Holds the subscriber until a condition is met (time, click, date). See wait modes below. |
| BRANCH | Evaluates a signal and routes to a yes or no outgoing edge. |
| AB_SPLIT | Randomly assigns subscribers to arm_0 or arm_1. Assignment is permanent for the subscriber's journey lifetime. Automatically declares a winner after 500 impressions. |
| GOAL | Terminal node — marks the subscriber as journey complete when reached. |
| EXIT | Terminal node — removes the subscriber from the journey without reaching the goal. |
| MERGE | Converges multiple paths into one (join node). Subscribers continue when they arrive from any arm. |
| HOLDOUT | Splits traffic into a serving path and a holdout (control) path. Holdout subscribers receive no ad but are still tracked. |
| WEBHOOK | Fires a POST to an external URL (non-blocking). Journey continues immediately. |
| LOOP | Sends the subscriber back to a specified AD_STEP node. Useful for re-engagement sequences. |
GOAL node types
A GOAL node is a terminal node that marks a subscriber as goal achieved when reached. Configure the goal type in the right-hand panel:
| Goal type | Behaviour |
|---|---|
| CLICK | Subscriber exits with GOAL_MET when they reach this node via a clicked path (default goal type) |
| IMPRESSION | Subscriber exits with GOAL_MET on reaching this node, regardless of click |
| CONVERSION | Subscriber is held at CONVERSION_PENDING until the advertiser postback pixel fires. Once the pixel fires, status upgrades to GOAL_MET. Subscriber re-queues every 15 minutes until converted. |
| CUSTOM | Journey fires an HTTP POST webhook to your endpoint. 2xx response = GOAL_MET; non-2xx = journey completes anyway with status JOURNEY_COMPLETED. |
Conversion postback pixel (GOAL type CONVERSION)
When you activate a journey with a CONVERSION goal, MailAdx generates a unique pixel URL. Embed this pixel (1×1 transparent GIF) in your post-purchase confirmation page or email:
<img src="https://api.mailadx.com/api/v1/cv/{pixelToken}?eh={sha256EmailHash}&v={valueUsdCents}" width="1" height="1" />pixelToken: journey-specific token, available in journey detail → Conversion Pixel paneleh: SHA-256 hash of the subscriber's email address (lowercase, no salt)v: optional conversion value in USD cents (e.g.4999for $49.99)
The pixel endpoint is public, requires no auth, and returns Cache-Control: no-store. Conversion data is stored in MongoDB with a 90-day TTL and queryable via the Conversions tab in Journey Reports.
You can regenerate the pixel token at any time from journey settings — the old token immediately becomes invalid.
Custom goal webhook (GOAL type CUSTOM)
Configure a webhookUrl and optional webhookSecret on the GOAL node. When a subscriber reaches the GOAL node, MailAdx fires:
POST {webhookUrl}
Content-Type: application/json
X-Journey-Secret: {webhookSecret} // only if configured
{
"journeyId": "jrn-abc123",
"emailHash": "sha256...",
"nodeId": "goal1",
"timestamp": "2026-07-01T12:00:00Z"
}A 2xx response marks the subscriber GOAL_MET. A non-2xx or network timeout marks them JOURNEY_COMPLETED instead. Timeout is 5 seconds.
BRANCH signals
BRANCH nodes evaluate one of 13 signals to decide which path a subscriber takes. Signals default to false (pessimistic) if data is unavailable.
| Signal | Description |
|---|---|
| CLICKED_NODE | Subscriber clicked a specific AD_STEP node (signalNodeId) |
| SERVED_NODE | Subscriber received an impression at a specific AD_STEP node (signalNodeId) |
| CLICK_WITHIN_DAYS | Subscriber clicked any journey ad within the past N days (signalThreshold) |
| IN_SEGMENT | Subscriber is in a named retargeting/audience segment (signalValue) |
| IMPRESSION_COUNT_GTE | Subscriber has received ≥ N total journey impressions (signalThreshold) |
| DAYS_SINCE_ENTRY | At least N days have passed since subscriber entered the journey (signalThreshold) |
| DEVICE | Subscriber's email client device matches (signalValue: MOBILE or DESKTOP) |
| GEO_COUNTRY | Publisher's audience country ISO code matches (signalValue) |
| PUBLISHER_SIZE_GTE | Publisher subscriber count ≥ threshold (signalThreshold) |
| LOOP_ITERATION | Subscriber is on their Nth loop iteration (signalThreshold) |
| HAS_CONVERTED | Advertiser postback pixel has fired for this subscriber in this journey |
| CLICK_RATE_GTE | Subscriber's CTR across this journey ≥ N% (signalThreshold 0–100) |
| DAYS_IN_FUNNEL | Total calendar days the subscriber has been in this journey ≥ N days (signalThreshold) |
Compound branch conditions (AND/OR)
A BRANCH node can evaluate multiple signals combined with AND or OR logic. In the Graph Builder, click Add condition in the BRANCH config panel to add more conditions. When two or more conditions are present, an AND/OR toggle appears.
- AND: subscriber is routed yes only if all conditions are true (short-circuits on first false)
- OR: subscriber is routed yes if any condition is true (short-circuits on first true)
- A single condition behaves identically to the original single-signal format (fully backward compatible)
WAIT modes
| Mode | Behaviour |
|---|---|
| FIXED_DAYS | Wait N calendar days |
| UNTIL_CLICK | Wait until the subscriber clicks an ad (or timeout) |
| UNTIL_DATE | Wait until a specific calendar date |
| BUSINESS_DAYS | Wait N business days (Mon–Fri, skips weekends) |
| RANDOM_RANGE | Wait a random number of days between min and max |
| UNTIL_NEXT_SEND | Wait until the publisher's next tracked newsletter send |
A/B split testing
Add an AB_SPLIT node to randomly route subscribers to one of two paths (arm_0 and arm_1). Each subscriber's arm is locked permanently for the duration of the journey. MailAdx automatically tracks impressions and clicks per arm and declares a winner when:
- Either arm has ≥ 500 impressions, and
- The CTR difference between arms is ≥ 0.5% (absolute)
Once a winner is declared, all new subscribers are routed to the winning arm. Existing subscribers keep their original assignment.
Re-entry and cooldown
By default, subscribers who complete or exit a journey must wait 30 days before they can re-enter. This is configurable per journey (reEntryDays). On re-entry, A/B arm assignments are re-randomised.
Graph constraints
- Exactly one ENTRY node per journey
- At least one AD_STEP node
- Maximum 50 nodes total
- Maximum graph depth: 10 (longest path from ENTRY to any node)
- All node IDs must be unique; all edge source/target IDs must reference existing nodes
CPM pricing
You set one base CPM per journey. The system auto-calculates per-step CPMs using a progressive multiplier — later steps bid higher to reach the most engaged subscribers:
- Step 1: 1.0× base CPM
- Step 2: 1.5× base CPM
- Step 3: 2.0× base CPM
- Step 4: 2.5× base CPM
- Step 5+: 3.0× base CPM
For graph journeys, AD_STEP nodes are assigned multipliers in BFS order from ENTRY. You can override any node's CPM with a cpmOverride value in the node's config.
Setting up your first journey
- Navigate to Dashboard → Ad Journeys
- Click New Journey. Set a name and base CPM
- Choose Linear Steps (simple, 2–5 steps) or Graph Builder (advanced canvas)
- For linear steps: add 2–5 steps with creative IDs and wait periods
- For graph: drag nodes from the left palette, connect them, and configure each node
- Optionally set start/end dates, total budget, and re-entry days
- Click Activate
Budget and billing
You are billed only for AD_STEP nodes that actually serve on each open. There is no charge for subscribers held at WAIT nodes or siphoned to HOLDOUT paths.
An optional Total Budget (USD) can be set on the journey. For linear journeys, MailAdx splits the budget evenly across all steps. For graph journeys, the budget is shared across all AD_STEP line items with per-node pacing.
Analytics and reporting
Journey-level reporting is available in two places:
- Journey detail panel (
Ad Journeys → [journey] → expand row): inline per-step/per-node stats and a graph overlay - Journey Reports page (
Ad Journeys → Journey Reports): dedicated reporting view with richer analysis
Journey Reports page
Select any journey from the left sidebar to see:
- KPI summary: total impressions, clicks, overall CTR, total spend
- Funnel tab: horizontal impression funnel across AD_STEP nodes; CTR bar chart color-coded by A/B arm when an AB_SPLIT is present
- A/B Test Breakdown (appears automatically for graph journeys with AB_SPLIT nodes): per-arm impressions, clicks, CTR, and spend as comparison bars; statistical signal strength indicator; impression threshold progress bar (goal: 500 per arm); winner trophy badge once declared
- Node Performance tab: sortable table with per-node delivery metrics; A/B arm column shown when applicable
- Conversions tab: total conversion count, total conversion value, and last 20 conversion events with timestamp
A/B statistical signal
Signal strength is assessed once both arms reach 500 impressions:
- Strong signal: absolute CTR lift ≥ 1%
- Moderate signal: absolute CTR lift 0.5–1%
- No significant difference: lift < 0.5%
- Insufficient data: either arm below 500 impressions (progress bar shown)
Pausing and resuming
Journeys can be paused at any time. Pausing halts delivery of all AD_STEP nodes immediately. Resuming restores delivery — subscribers retain their node position and continue from where they left off.
API reference
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/dsp/journeys/cpm-preview | Preview CPMs per step before creating |
| POST | /api/v1/dsp/journeys | Create a DRAFT journey (linear or graph) |
| GET | /api/v1/dsp/journeys/{id} | Fetch journey details (includes nodes/edges for graph journeys) |
| PATCH | /api/v1/dsp/journeys/{id} | Update name, CPM, dates, steps, or graph |
| POST | /api/v1/dsp/journeys/{id}/activate | Activate a DRAFT or PAUSED journey |
| POST | /api/v1/dsp/journeys/{id}/pause | Pause an ACTIVE journey |
| POST | /api/v1/dsp/journeys/{id}/resume | Resume a PAUSED journey |
| POST | /api/v1/dsp/journeys/{id}/archive | Permanently stop a journey |
| GET | /api/v1/dsp/journeys/{id}/stats | Per-step delivery data (linear) |
| GET | /api/v1/dsp/journeys/{id}/node-stats | Per-node delivery data (graph journeys) |
| POST | /api/v1/dsp/journeys/{id}/validate-graph | Validate a graph structure without saving |
| POST | /api/v1/dsp/journeys/{id}/debug-advance | Manually trigger subscriber advancement (dev/staging only) |
| POST | /api/v1/dsp/journeys/{id}/regen-pixel | Regenerate conversion pixel token (invalidates previous token immediately) |
| GET | /api/v1/dsp/journeys/{id}/conversions | List conversion postback events for this journey |
| GET | /api/v1/cv/{pixelToken}?eh={emailHash}&v={valueUsdCents} | Public conversion pixel endpoint — returns 1×1 GIF, records conversion |
Graph journey request format
To create a graph journey, include nodes and edges arrays in the POST body instead of steps:
POST /api/v1/dsp/journeys
{
"name": "My Branching Journey",
"baseCpm": 8.00,
"nodes": [
{ "id": "entry1", "type": "ENTRY", "label": "Start", "canvasX": 100, "canvasY": 100, "config": {} },
{ "id": "ad1", "type": "AD_STEP", "label": "Step 1", "canvasX": 300, "canvasY": 100,
"config": { "creativeId": "cre-abc123" } },
{ "id": "branch1","type": "BRANCH", "label": "Clicked?", "canvasX": 500, "canvasY": 100,
"config": { "signal": "CLICKED_NODE", "signalNodeId": "ad1" } },
{ "id": "ad2", "type": "AD_STEP", "label": "Engaged", "canvasX": 700, "canvasY": 50,
"config": { "creativeId": "cre-def456" } },
{ "id": "ad3", "type": "AD_STEP", "label": "Re-engage", "canvasX": 700, "canvasY": 150,
"config": { "creativeId": "cre-ghi789" } },
{ "id": "exit1", "type": "EXIT", "label": "Done", "canvasX": 900, "canvasY": 100, "config": {} }
],
"edges": [
{ "id": "e1", "sourceId": "entry1", "targetId": "ad1" },
{ "id": "e2", "sourceId": "ad1", "targetId": "branch1" },
{ "id": "e3", "sourceId": "branch1", "targetId": "ad2", "handle": "yes" },
{ "id": "e4", "sourceId": "branch1", "targetId": "ad3", "handle": "no" },
{ "id": "e5", "sourceId": "ad2", "targetId": "exit1" },
{ "id": "e6", "sourceId": "ad3", "targetId": "exit1" }
]
}Limits
- Linear: minimum 2 steps, maximum 5 steps
- Graph: minimum 1 AD_STEP node, maximum 50 nodes, max path depth 10
- Minimum
minWaitDaysper step: 0 (same send eligible) up to 90 days - Supported formats: NATIVE and STANDARD (image banner); formats can be mixed across nodes
- One AD_STEP node served per subscriber per newsletter send
- A/B winner declared after ≥ 500 impressions with ≥ 0.5% CTR lift
- Default re-entry cooldown: 30 days (configurable per journey)