Guides

How Do I Trigger Iterable Journeys on Return Visits?

Use Leadpipe return-visit webhooks to trigger personalized Iterable journeys. Field mapping, deduping, and GDPR-safe patterns for lifecycle marketers.

Elene Marjanidze Elene Marjanidze · · 10 min read
How Do I Trigger Iterable Journeys on Return Visits?

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

RequirementNotes
Leadpipe accountStart free with 500 leads
Iterable accountGrowth or Enterprise tier
Iterable API keyServer-side, with Custom Events and Users permissions
Zapier or your own middlewareOptional for no-code
Time25 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:

EventDefinitionUse case
website_return_visitreturn_visit = true AND intent_score > 40Re-engagement nurture
website_high_intent_visitpage_url starts with /pricing OR /enterprise AND visit_duration > 120Sales-assist, MQL escalation
website_feature_evaluationpage_url starts with /features/* AND visit_duration > 90Feature-specific nurture
website_docs_deep_divepage_url starts with /docs/* AND pages_viewed >= 3Technical 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.

  1. Destination URL: generated in Step 3.
  2. 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.
  3. 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

  1. New Zap. Trigger: Webhooks by Zapier, Catch Hook. Paste URL in Leadpipe.
  2. Fire a test event.
  3. Filter step. Options:
    • Event website_return_visit: return_visit is true AND intent_score is greater than 40
    • Event website_high_intent_visit: page_url starts with /pricing OR /enterprise AND visit_duration greater than 120
  4. Filter out personal emails and EU traffic missing email. Add a condition: email exists AND email does not contain gmail.com, yahoo.com.
  5. 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}}
  }
}
  1. 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:

  1. Entry split. Is user.employeeCount greater than 500? If yes, skip to the enterprise branch. If no, mid-market.
  2. 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.
  3. 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.
  4. Exit criteria. If user triggers website_high_intent_visit or 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.

  1. Leadpipe identifies Raj. Every Update webhook fires.
  2. Middleware classifies: website_return_visit (return_visit=true, intent_score=82) and website_high_intent_visit (/enterprise, 342s). Middleware emits the higher-priority event, website_high_intent_visit.
  3. Iterable updates Raj’s user record: lastPage=/enterprise, intentScore=82, intentTopics=ABM, intent data.
  4. 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.”
  5. Raj opens the email at 2:40 pm. Iterable records the engagement. Journey advances.
  6. 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 →