Your Iterable journeys are triggered by form fills, purchases, or email opens. The signal you are missing is the most predictive one of all: someone who already engaged with your brand coming back to your site and reading pricing, a feature page, or a case study. Return visits convert 3-5x better than first-time visits, and most lifecycle marketers have no way to trigger on them.
I am Elene, and this is the recipe I give lifecycle marketers who want Iterable to fire behaviorally against website visits, not just app or email events. Leadpipe identifies the visitor, a webhook carries the event, and Iterable receives a custom event that your journey builder can gate on. Setup is about 25 minutes.
What you will build
A pipeline where every identified US B2B return visitor (or first-time high-intent visitor) triggers a custom event in Iterable, populating User fields with visit context and starting (or advancing) a journey you design in the Iterable Studio.
Prerequisites
| Requirement | Notes |
|---|---|
| Leadpipe account | Start free with 500 leads |
| Iterable account | Growth or Enterprise tier |
| Iterable API key | Server-side, with Custom Events and Users permissions |
| Zapier or your own middleware | Optional for no-code |
| Time | 25 minutes |
If you are on Marketo, Customer.io, or HubSpot Marketing instead, the same pattern applies; the Clay + HubSpot recipe covers HubSpot and the Salesforce recipe shows how to write to a marketing automation platform backed by Salesforce data.
The event model
Iterable’s journey builder gates on custom events and user field changes. You want Leadpipe to supply both.
Leadpipe webhook
│
▼
Middleware (or Zapier)
│
├─ Iterable User update (or create)
│ → fields: lastSeenAt, lastPage, intentScore, topics, seniority, industry
│
└─ Iterable Custom Event "website_return_visit" or "website_high_intent_visit"
→ payload: page_url, pages_viewed, visit_duration, intent_score, return_visit
You send a user update even for first-time visitors (so the user is in Iterable), then emit the event. Journey builder triggers on the event, and branch logic reads user fields.
Step 1: Decide what counts as a journey trigger
Not every identified visit deserves a journey start. Pick a narrow event definition. Options:
| Event | Definition | Use case |
|---|---|---|
website_return_visit | return_visit = true AND intent_score > 40 | Re-engagement nurture |
website_high_intent_visit | page_url starts with /pricing OR /enterprise AND visit_duration > 120 | Sales-assist, MQL escalation |
website_feature_evaluation | page_url starts with /features/* AND visit_duration > 90 | Feature-specific nurture |
website_docs_deep_dive | page_url starts with /docs/* AND pages_viewed >= 3 | Technical decision-maker journey |
Start with two. You can add more later. Lifecycle teams who try to fire on every page event end up muting themselves.
Step 2: Configure Leadpipe webhooks
In Leadpipe: Settings, Integrations, Webhooks, Add Webhook.
- Destination URL: generated in Step 3.
- Trigger: Every Update if you want to catch return visits. Leadpipe only fires Every Update from identified visitors who return, which is exactly the signal you want. Add a separate First Match webhook if you also want first-time high-intent visits to trigger.
- Fields: all.
Sample payload (full schema in our webhook payload reference):
{
"email": "raj.mehta@orbit.dev",
"first_name": "Raj",
"last_name": "Mehta",
"phone": "+1-415-555-0175",
"company_name": "Orbit.dev",
"company_domain": "orbit.dev",
"company_industry": "Developer Tools",
"company_employee_count": 95,
"job_title": "VP Marketing",
"seniority": "VP",
"department": "Marketing",
"linkedin_url": "linkedin.com/in/rajmehta",
"city": "Austin",
"state": "Texas",
"country": "US",
"page_url": "/enterprise",
"pages_viewed": ["/", "/blog/midbound-a-new-era-in-marketing", "/pricing", "/enterprise"],
"visit_duration": 342,
"intent_score": 82,
"matched_topics": ["ABM", "intent data"],
"return_visit": true
}
GDPR note. For EU and UK visitors, Leadpipe defaults to company-level identification unless the visitor has given affirmative consent. Those payloads arrive with company fields and no email, which means you cannot write a User record in Iterable (no identifier). For EU traffic, log the event against a company-level record or a special “anonymous-eu-{{domain}}” user record if you want to track it, but do not attempt to create a personal user without consent. Our GDPR compliance post goes deeper.
Step 3: Send to Iterable
Iterable’s API has two endpoints you need: Users update (POST /api/users/update) and Custom Events track (POST /api/events/track). Both take JSON and your server-side API key as a header.
Zapier path
- New Zap. Trigger: Webhooks by Zapier, Catch Hook. Paste URL in Leadpipe.
- Fire a test event.
- Filter step. Options:
- Event
website_return_visit:return_visitis true ANDintent_scoreis greater than 40 - Event
website_high_intent_visit:page_urlstarts with/pricingOR/enterpriseANDvisit_durationgreater than 120
- Event
- Filter out personal emails and EU traffic missing email. Add a condition:
emailexists ANDemaildoes not containgmail.com, yahoo.com. - Action 1: Webhooks by Zapier, Custom Request (POST). URL:
https://api.iterable.com/api/users/update. Headers:Api-Key: {{ITERABLE_API_KEY}}. Body (JSON):
{
"email": "{{email}}",
"dataFields": {
"firstName": "{{first_name}}",
"lastName": "{{last_name}}",
"phoneNumber": "{{phone}}",
"company": "{{company_name}}",
"companyDomain": "{{company_domain}}",
"industry": "{{company_industry}}",
"employeeCount": {{company_employee_count}},
"jobTitle": "{{job_title}}",
"seniority": "{{seniority}}",
"department": "{{department}}",
"linkedinUrl": "{{linkedin_url}}",
"country": "{{country}}",
"state": "{{state}}",
"city": "{{city}}",
"lastSeenAt": "{{zap_meta_timestamp}}",
"lastPage": "{{page_url}}",
"intentScore": {{intent_score}},
"intentTopics": {{matched_topics}}
}
}
- Action 2: Webhooks by Zapier, Custom Request (POST). URL:
https://api.iterable.com/api/events/track. Body (JSON):
{
"email": "{{email}}",
"eventName": "website_return_visit",
"dataFields": {
"pageUrl": "{{page_url}}",
"pagesViewed": {{pages_viewed}},
"visitDuration": {{visit_duration}},
"intentScore": {{intent_score}},
"matchedTopics": {{matched_topics}},
"returnVisit": {{return_visit}}
}
}
Duplicate the Zap for the high-intent event with a different filter and eventName.
Direct middleware path
For volume and cleaner dedup logic:
// POST /leadpipe-webhook → Iterable
// Env: ITERABLE_API_KEY
import fetch from 'node-fetch';
const headers = {
'Api-Key': process.env.ITERABLE_API_KEY,
'Content-Type': 'application/json',
};
function classify(v) {
if (v.return_visit && (v.intent_score || 0) > 40) return 'website_return_visit';
const url = v.page_url || '';
if (
(url.startsWith('/pricing') || url.startsWith('/enterprise')) &&
(v.visit_duration || 0) > 120
)
return 'website_high_intent_visit';
if (url.startsWith('/features') && (v.visit_duration || 0) > 90)
return 'website_feature_evaluation';
if (url.startsWith('/docs') && (v.pages_viewed || []).length >= 3)
return 'website_docs_deep_dive';
return null;
}
export async function handler(req) {
const v = req.body;
if (!v.email) return { statusCode: 200, body: 'no email, skipping per GDPR defaults' };
const eventName = classify(v);
if (!eventName) return { statusCode: 200, body: 'no trigger' };
// Upsert user
await fetch('https://api.iterable.com/api/users/update', {
method: 'POST',
headers,
body: JSON.stringify({
email: v.email,
dataFields: {
firstName: v.first_name,
lastName: v.last_name,
phoneNumber: v.phone,
company: v.company_name,
companyDomain: v.company_domain,
industry: v.company_industry,
employeeCount: v.company_employee_count,
jobTitle: v.job_title,
seniority: v.seniority,
department: v.department,
linkedinUrl: v.linkedin_url,
country: v.country,
state: v.state,
city: v.city,
lastSeenAt: new Date().toISOString(),
lastPage: v.page_url,
intentScore: v.intent_score,
intentTopics: v.matched_topics,
},
}),
});
// Fire event
await fetch('https://api.iterable.com/api/events/track', {
method: 'POST',
headers,
body: JSON.stringify({
email: v.email,
eventName,
dataFields: {
pageUrl: v.page_url,
pagesViewed: v.pages_viewed,
visitDuration: v.visit_duration,
intentScore: v.intent_score,
matchedTopics: v.matched_topics,
returnVisit: v.return_visit,
},
}),
});
return { statusCode: 200, body: `fired ${eventName}` };
}
Step 4: Build the journey in Iterable Studio
Open Iterable Studio and create a new journey. Trigger: Custom Event = website_return_visit.
Branches I recommend:
- Entry split. Is
user.employeeCountgreater than 500? If yes, skip to the enterprise branch. If no, mid-market. - Mid-market branch. Wait 1 hour. Send email “Saw you were back, here is what customers your size usually ask first” with dynamic fields pulled from user data (firstName, company, industry). Wait 2 days. If no open, send a case study email. If open but no click, send a one-question nudge.
- Enterprise branch. Wait 30 minutes. Send internal Slack notification to the named AE via an Iterable webhook (or pair with Slack alerts directly from Leadpipe). Send an AE-from email template rather than marketing-from.
- Exit criteria. If user triggers
website_high_intent_visitor fills a demo form, exit this journey and enter the sales-assist journey.
The nice part: you are not creating a new list for return visitors, you are treating them as the lifecycle signal they actually are.
What this looks like in practice
Raj, VP Marketing at Orbit.dev (a 95-person dev tools company in Austin), read your midbound blog post three weeks ago from a LinkedIn share. He did not convert. This morning he returned, read your blog again, hit pricing, then spent five and a half minutes on /enterprise.
- Leadpipe identifies Raj. Every Update webhook fires.
- Middleware classifies:
website_return_visit(return_visit=true, intent_score=82) andwebsite_high_intent_visit(/enterprise, 342s). Middleware emits the higher-priority event,website_high_intent_visit. - Iterable updates Raj’s user record: lastPage=/enterprise, intentScore=82, intentTopics=ABM, intent data.
- Iterable journey fires. Enterprise branch (95 employees, so actually mid-market in our example thresholds). 1 hour later, email sent: “Raj, saw you were checking out enterprise tiers at Orbit.dev. Here’s the one-pager our ABM customers use most.”
- Raj opens the email at 2:40 pm. Iterable records the engagement. Journey advances.
- Raj books a demo the next day.
None of that happens without the return-visit trigger. Lifecycle used to rely on form fills and email opens. Now the signal is “the person who already knows you came back,” which is a much earlier and stronger one.
Troubleshooting and edge cases
Two events per visit. If a single return visit matches both website_return_visit and website_high_intent_visit, pick one, do not fire both, or you will pull the user into two journeys at once. The middleware example above does that by checking in priority order.
Journey re-entry. Iterable lets users re-enter a journey by default. For return-visit journeys, enable “Do not allow re-entry within X days” to avoid nagging return visitors with the same email every time.
EU visitors with no email. Expected. Leadpipe defaults to company-level for EU/UK. Iterable is a personal-identifier system (email or userId), so you cannot act on company-level events here. Either skip them (safest) or route them to a separate ABM-style tool that can work on domain alone (see LinkedIn Ads audiences for a domain-friendly activation).
Data field type mismatches. Iterable is strict about field types. If employeeCount is sometimes null and sometimes a number, your user updates will fail silently on the null ones. Default missing numbers to 0 or omit the field.
Duplicate Iterable users. Iterable uses email as the primary identifier. As long as you consistently normalize the email (lowercase, trim), you will not duplicate. If you ever switch to userId as your primary key, map Leadpipe email to userId consistently.
Rate limits. Iterable’s bulk endpoints cap at 2,000 ops per request and a few hundred requests per minute. For most marketing teams this is never an issue. If you hit it, batch writes in middleware with a 1-second queue.
Events arriving for people who are in your suppression list. Iterable will happily record an event against a suppressed user but will not email them. If you want to short-circuit before the event fires, check the user’s channel.email.status first.
Extending the recipe
- Pair with Slack visitor alerts so the AE gets a direct ping on the same event Iterable triggers on.
- Send the same event to Segment if you want your other destinations (warehouse, product analytics, CDP) to receive the signal at the same time.
- Enrich the event payload with Clay data before it hits Iterable if you want tech stack, revenue, or waterfall-enriched phone in your journey branching logic.
- Write the event into a CRM like Salesforce or Attio too so sales has the same signal.
- If you run paid retargeting, pipe the high-intent event into LinkedIn Ads audiences for coordinated multi-channel follow-up.
Why return-visit triggers are underused
Most lifecycle stacks only know about users after they convert. By then the intent is already losing heat. Wiring Leadpipe into Iterable gives you a lifecycle trigger on the exact moment when the buyer is warmest, and the content in the journey can match the page they just viewed. That is the difference between a nurture and a conversation.
The highest-converting lifecycle event is a return visit to your pricing page. Build your journey around it.
Leadpipe identifies 30-40%+ of your US B2B visitors with full contact data on the Pro plan at $147/mo. No credit card to start the 500-lead trial. Start identifying visitors →