TLDR: Before you build the webhook, check if someone already did. The lesson isn't "read the code" — it's deployed ≠ wired.
The Setup
My business partner runs a small law firm and she has a standing problem: when a Potential New Client fills out her intake questionnaire — the intake qualifier form (PNC = Potential New Client), hosted on Fillout (a form builder) — someone on her team has to manually re-key that data into her practice management system.
Every. single. time.
The law firm practice management app I built for her was supposed to fix this. Her assistant would stop retyping; the form submission would auto-create the contact, the matter, and fire a conflict check — all before anyone picked up a pen.
So I sat down to build the intake webhook.
What I Found
I went to create the route. It already existed.
Deployed three weeks earlier, actually — commit 1b18c92, May 10th. Route wired, handler in place.
I did a double-take.
Then I looked closer. The parser inside it? A placeholder. It consumed the payload and… threw it away. And on the Fillout side? No webhook configured. The form had no idea the endpoint existed.
So the whole thing had been "done" for 21 days and had silently processed exactly zero submissions.
That's the kind of thing that makes you laugh after a second of wanting to cry.
What Actually Needed Building
Once I knew what was already there, the work snapped into focus fast.
Three commits:
- Pipeline + parser — mapped the Fillout form
6XD2mJjWMsusfields to real typed data: individual vs. entity, practice area → matter type (Business Formation / Nonprofit Formation / Contracts / Outside General Counsel / M&A / IP-Trademark), co-founders vs. adverse parties routed correctly - Auth gate — accept the Fillout webhook secret via
?t=query param so random submissions can't hit it - Finalize from a live payload — this one matters. I triggered a real submission and captured what Fillout actually sends, then finished the parser against that. Not against their docs. Not against what I assumed. The actual bytes.
That last step is the one I'd skip if I was rushing. Don't skip it.
A few details worth calling out. The entity+person direction (person.primary_contact_id → entity) isn't arbitrary — it's the direction the engagement letter renderer reads, so if you flip it, letters break silently. And the conflict check feeds new leads in as typed names, not record IDs, so they match against existing history the way a human name-search would. Get that wrong and you're running a conflict check against nothing.
The whole thing is idempotent on submissionId, so Fillout can retry and nothing doubles.
Why This Actually Matters
The real lesson here isn't "check Git before you build" (though, yes, obviously).
It's that deployed and wired are two completely different things.
Code sitting in production that no one has pointed anything at is just… vibes. It's the filing cabinet where you forgot to put the files. I could have shipped a fresh webhook in a few hours — instead I spent twenty minutes orienting and then extended what was already there. Faster, cleaner, and I didn't introduce a second route to confuse the next session.
The form is live at a law firm client-simple.vercel.app. My business partner just needs to publish it. When she does, her assistant gets her time back.
P.S. Check your old commits before you start the next build. You might already be 70% done.