Data AnalyticsIndustry analytics playbooks

Fintech KYC drop-off: a step funnel with time-outs and retries

PK
Pankit Kumar
Sr. Data Scientist at Parexel (a Goldman Sachs–backed company) · 27 September 2026 · 7 min read
In this article (7 sections)

Related programme: the Data Analytics with Generative AI course (3–4 months; ₹90,000 (inclusive of GST) · EMI from ₹4,000/month).

The short answer

A KYC drop-off funnel should be built from the onboarding event log as distinct users per step, for a cohort that has all had the same time to finish (for example, seven days after signup). At each step keep three counts apart: users who never attempted it, users who attempted and failed, and users who passed after however many retries and time-outs. A single "conversion %" per step blends a design problem, a verification problem and an impatience problem, and each needs a different team.

Three kinds of drop at every step

For a lending or investing app, the onboarding steps usually look like: sign up, identity number check, address or identity document, selfie or liveness check, then a review (automatic or manual). At any step, a user who does not get through has done one of three things:

Kind of dropWhat the log showsUsual owner
Never attemptedpassed the previous step, no event for this oneproduct and design: the ask felt too big
Tried and failedone or more fail results, no okverification vendor, image quality, data mismatch
Timed outsession expired mid-step, did not returnengineering: session length, slow upload

The funnel below reports these separately. The exact steps and their order depend on your product and on what your compliance team has decided is required; the analysis does not depend on them.

The event log you need

ColumnTypeNotes
user_idtextstable across devices; not the device ID
steptextone fixed list of step names, versioned when the flow changes
resulttextok, fail, timeout
tstimestampserver time, one time zone
attempt_nointegeroptional; can be derived by ordering
failure_reasontextvendor code (blurred image, name mismatch)
app_version, channeltextto split by release and acquisition source

Worked example: a September sign-up cohort

The log below is synthetic, generated for 1,200 users signing up to a lending app during September 2026, with up to three tries per step, occasional session time-outs, and a manual review that takes one to three days.

python
import random
import pandas as pd

# Synthetic onboarding event log for a lending app. Not real customer data.
rng = random.Random(7)
STEPS = ["signup", "pan_check", "address_doc", "selfie", "review"]
PASS = {"pan_check": 0.9, "address_doc": 0.6, "selfie": 0.7, "review": 0.88}
ATTEMPT = {"pan_check": 0.9, "address_doc": 0.8, "selfie": 0.88, "review": 1.0}
rows = []
start = pd.Timestamp("2026-09-01")
for u in range(1, 1201):
    t = start + pd.Timedelta(minutes=rng.randrange(0, 29 * 24 * 60))
    rows.append((u, "signup", "ok", t))
    for step in STEPS[1:]:
        if rng.random() > ATTEMPT[step]:
            break                                    # never attempts the step
        passed = False
        for attempt in range(3):                     # up to three tries
            wait = [1, 2, 3] if step == "review" else [0.002, 0.005, 0.01, 0.2, 2]
            t += pd.Timedelta(days=rng.choice(wait))  # manual review waits days
            if step != "review" and rng.random() < 0.06:
                rows.append((u, step, "timeout", t)) # session expired mid-step
                continue
            if rng.random() < PASS[step]:
                rows.append((u, step, "ok", t)); passed = True; break
            rows.append((u, step, "fail", t))
        if not passed:
            break
ev = pd.DataFrame(rows, columns=["user_id", "step", "result", "ts"])
EXTRACT = pd.Timestamp("2026-09-30")
ev = ev[ev.ts <= EXTRACT]                            # the extract cannot see the future
print("events:", len(ev), "users:", ev.user_id.nunique())

# Cohort rule: only users who signed up at least 7 days before the extract
signup = ev[ev.step == "signup"].set_index("user_id")["ts"]
mature = signup[signup <= EXTRACT - pd.Timedelta(days=7)].index
e = ev[ev.user_id.isin(mature) & (ev.ts <= ev.user_id.map(signup) + pd.Timedelta(days=7))]
print("mature cohort:", len(mature))

rows = []
prev = None
for step in STEPS:
    s = e[e.step == step]
    attempted = s.user_id.nunique()
    passed = s[s.result == "ok"].user_id.nunique()
    rows.append({
        "step": step,
        "attempted": attempted,
        "passed": passed,
        "pass_per_try_pct": round(100 * (s.result == "ok").sum() / len(s), 1),
        "pass_of_attempted_pct": round(100 * passed / attempted, 1),
        "attempted_of_prev_pass_pct": round(100 * attempted / prev, 1) if prev else None,
        "tries_per_attempter": round(len(s) / attempted, 2),
        "timeouts": int((s.result == "timeout").sum()),
    })
    prev = passed
f = pd.DataFrame(rows)
print(f.to_string(index=False))
print("end-to-end (%):", round(100 * f.passed.iloc[-1] / f.passed.iloc[0], 1))

# Without a cohort rule: completion by signup week, whatever each user's age
done = ev[(ev.step == "review") & (ev.result == "ok")].user_id.unique()
wk = ((signup - start).dt.days // 7 + 1).clip(upper=4).rename("signup_week")
by_week = pd.DataFrame({"week": wk, "done": signup.index.isin(done)}).groupby("week")["done"]
print((100 * by_week.mean()).round(1).to_string())

The funnel for the 902 users who signed up at least seven days before the extract, counting only what they did in their first seven days:

StepAttemptedPassedPass per try %Passed of attempted %Attempted of previous passed %Tries per attempterTime-outs
signup902902100.0100.0–1.000
pan_check81281083.499.890.01.2062
address_doc63258157.191.978.01.6150
selfie51849768.495.989.21.4036
review45543786.296.091.51.110

End to end, 48.4% of the cohort completed onboarding within seven days.

What the table is saying

The address document is the problem, twice over. Only 78.0% of users who cleared the PAN step even tried it, the lowest "attempted" ratio in the flow, so the ask itself is putting people off. And those who tried passed only 57.1% of individual tries, needing 1.61 tries on average. That is two separate fixes: better explanation before the step, and better capture guidance (or a different document option) during it.

Retries make a step look healthy. Per user, 91.9% of address-document attempters eventually passed. Reported that way the step looks fine. The per-try rate of 57.1% is what the user experienced, and each retry is also a vendor verification call that costs money. Show both.

Time-outs are their own line. 62 PAN-step and 50 address-step time-outs are sessions that expired while the user was in the middle of a step. That is an engineering setting, not a verification outcome, and it hides inside "fail" in most dashboards.

Why the latest week always looks broken

The last block of the output ignores the cohort rule and simply asks what share of each signup week has completed so far:

Signup weekCompleted by 30 September (%)
155.3
255.1
353.6
429.5

Week 4 has not collapsed. Its users have had anything from a few hours to about eight days, and the manual review alone takes one to three. A funnel refreshed daily on all sign-ups will show a falling line every Monday and cause a needless alarm. Fix the window (here seven days), exclude anyone younger than it, and note that the seven-day figure (48.4%) is lower than the eventual completion rate of older weeks (about 55%) because some users finish late.

Traps particular to KYC data

  • Flow versions. When a step is added or reordered, old and new users are not comparable. Split by app version or by the date the change went live.
  • Users who start again. Someone who abandons and re-registers with a new phone number appears as two users, one dropped and one complete. Match on a verified identifier where you are permitted to.
  • Vendor failure codes. "Fail" from a blurred photo and "fail" from a data mismatch need different fixes. Keep the vendor reason, and check how codes changed when the vendor was switched.
  • Personal data in the extract. The analysis needs user IDs, steps and timestamps, not identity numbers or images. Ask for the minimum columns and keep the extract access-controlled.

Taking it to product, risk and operations

Give each owner one line. Product: "22% of users who pass the PAN step never try the address document." Operations or the vendor manager: "Address document tries pass 57% of the time," followed by those failures split by vendor reason code. Engineering: "148 sessions expired mid-step in the cohort, 112 of them at the PAN and address steps." Risk and compliance: completion is 48.4% within seven days, with the review step passing 96.0% of those who reach it; any change to steps goes through them first, and the funnel is how you measure its effect.

Payment failures have a similar split between customer and system causes; see the UPI failure rate playbook. Event-log funnels like this one are built in pandas and SQL during the Data Analyst programme.

Frequently asked questions

How do you calculate drop-off in an onboarding funnel?

Count distinct users who reached each step within a fixed window after signup, then divide each step's count by the previous step's passed count. Keep attempted and passed as separate columns so that people who never tried a step are not confused with people who tried and failed.

Why does the latest week always look worst in a KYC funnel?

Because recent sign-ups have had less time to finish, especially if any step involves a manual review queue. Compare only users who have had the same number of days since signup, for example seven, and exclude anyone younger than that from the rate.

PK
Pankit Kumar
Lead Instructor, NeuraPath Academy

Pankit Kumar has 10 years in Data Science & AI, building and shipping production systems in regulated pharma and clinical environments. He is a freelance trainer at Boston Institute of Analytics, AnalytixLabs and Scaler, and has taught this material to thousands of working professionals.

This article is part of our Data Analytics with Generative AI programme — 3–4 months. The full analyst stack — Excel, SQL, Power BI and Python pipelines — then a generative-AI layer you can prove is right.

Explore Data Analytics with Generative AI
Counselling is free · no obligation

Not sure which programme fits?

Tell us your background and we will map it to the right entry point — including saying so when a cheaper programme is the better fit. A counsellor replies within one working day.