Multi-Step Lead-Qualification Funnel
A multi-step lead-qualification funnel with branching disqualify logic, GTM-driven event tracking, and a real webhook handoff — built to mirror how a real injury-intake vertical actually qualifies and routes leads.
The Problem
Legal intake forms need to qualify leads correctly. A lead that doesn't meet the firm's criteria should be filtered out early, and an ad platform that fires a conversion pixel on every submission — qualified or not — throws off the campaign data it's supposed to be optimizing. This form needed to separate those two outcomes cleanly, in real time, without reloading the page.
What I Built
- A four-step qualifying flow (car accident, damages range, insurance, existing representation), each step showing or hiding based on the previous answer — no page reloads, no framework, just vanilla JS toggling visibility.
- Three separate disqualify paths, each with its own message, so a lead that fails any gate gets routed out immediately rather than continuing through irrelevant questions.
- Full field-level validation on the contact step — required-field checks plus regex validation for email and phone, with a live-formatting input mask on the phone field.
- A
dataLayer.push()fired only on the qualifying path, feeding a GTM Custom Event trigger and a GA4 event tag — so ad platforms only ever see a conversion for a lead that actually qualified. - Per-step funnel events (
step_1_completethroughstep_4_complete) for drop-off visibility, separate from the qualification event itself. - A real thank-you page, reached only after a qualified submission, with a Page View–triggered pixel test and a value passed through the URL via
URLSearchParams. - A working
fetch()call sending the submitted lead data to a webhook endpoint, proving the client-side handoff to a CRM.
Key Decisions
GTM click triggers guess at what happened from a button's class or text. Since the form's own JS already knows exactly what was answered and whether it qualified, pushing a custom event at that exact decision point is more reliable and doesn't break if a class name changes later.
A direct client-side fetch() to a CRM endpoint gets blocked by the browser's CORS policy unless the receiving server explicitly allows it — confirmed firsthand with a real preflight rejection, not just in theory. In production this is exactly why the handoff moves server-side: a same-origin endpoint (PHP, in this case) forwards the request, sidestepping the browser restriction entirely.
Name, email, and phone share one submit button rather than individual "Continue" steps, so all three are validated together at the moment of submission — the same pattern Gravity Forms uses under the hood.