Case StudyLead CaptureNext.jsEmail Automation

Case Study: My Contact Form Said Thanks and Sent the Lead Nowhere

Tariq OsmaniTariq Osmani10 min read
Case Study: My Contact Form Said Thanks and Sent the Lead Nowhere

On my own website, someone filled in the contact form and got a thank-you message. The lead reached nobody. No error, no alert, no queue to go digging through later — the submission was simply gone, and the person on the other end had every reason to believe I had it.

This is the first of three case studies I'm publishing as proof of work. It has no client in it, which is the point: Smart AI Workspace is client zero. I fixed my own lead intake before selling the same fix to anyone else, and you can check the result right now by submitting the contact form and watching the confirmation land in your inbox.

Client Zero: A Case Study With No Client

Most agency case studies are unverifiable by design. A logo, a percentage, an anonymised quote, and no way for you to check any of it. This one is the opposite: the system under discussion is the one you are reading this on, and the test of whether it works is a live form you can submit yourself in the next minute.

That constraint also shapes what I can claim. Everything below is either behaviour you can trigger yourself or a change with a commit behind it. Where I don't have a number, I don't give one.

The Bug: A Route That Returned Success With Nothing Captured

The original build routed POST /api/contact to an n8n webhook. The forward was wrapped in a conditional:

const webhookUrl = process.env.N8N_CONTACT_WEBHOOK_URL;
if (webhookUrl) {
  const webhookRes = await fetch(webhookUrl, { /* ... */ });
  if (!webhookRes.ok) {
    console.error("n8n webhook error:", webhookRes.status);
    return NextResponse.json({ error: "Failed to submit. Please try again." }, { status: 502 });
  }
}
return NextResponse.json({ success: true });

Read the failure path. If N8N_CONTACT_WEBHOOK_URL was unset or pointed somewhere wrong, the whole block was skipped and the route still answered { success: true }. The visitor saw a thank-you. The lead reached nobody.

I wrote that conditional on purpose. It meant the form worked on my laptop without a webhook configured — a local development convenience. That convenience became a production hole, because the same code path that made development pleasant also made silent failure the default in the one environment where it mattered.

What It Cost

At least one real inbound lead was lost while the site ran that way. That is the entire claim, and I'm deliberately not dressing it up: no count, no deal value, no name. Someone wanted to talk to me, said so through the form, and I never saw it.

There is no getting that person back, and no way to know what else went the same way. A silent failure leaves nothing behind: no bounce, no error report, no row in a table to reconcile later. And the person who filled in the form has no reason to chase — as far as they know, they contacted you and you ignored them.

A person working focused at a laptop in a home office setting

The Fix Was a Definition, Not a Vendor

The migration is one commit — feat(contact): route form submissions through Resend instead of n8n, 31 August 2026 — but swapping the destination is not what fixed anything. n8n was not the problem. The webhook did its job when it was pointed at a live workflow. The problem was my route's contract: it defined success as "I finished running" instead of "the lead is somewhere a human will see it."

The current route inverts that. It only answers { success: true } after the internal notification has actually been accepted for delivery. If that send returns an error, the route logs it and returns a 502, and the form tells the visitor to try again — which is the honest answer, because at that moment nothing has their message.

SituationOld route (n8n era)Current route (Resend)
Destination not configuredSkipped the forward, returned successLocal-dev no-op only, when RESEND_API_KEY is absent
Notification send fails502 — but only if the webhook was configured502, logged, every time
Visitor sees a thank-youEven when nothing was capturedOnly after the lead is captured
Confirmation to the submitterNoneSent, best-effort
Replying to a leadCopy the address out of the workflowReply straight to the notification email

One honest caveat, since the point of this post is that you can check my work: the local-dev escape hatch still exists in a narrower form. With no RESEND_API_KEY the route skips both sends and reports success, so nothing breaks while I develop offline. The difference is that a misconfigured destination can no longer pass as a success — with a key present, the only route to { success: true } runs through a send that actually worked.

Not sure your own intake is delivering?

Send me your form and I'll tell you what happens to a lead when your delivery destination is down. It takes one submission to find out.

Two Sends, Two Different Contracts

The rebuilt route sends two emails, and the distinction between them is the whole design:

  1. Internal notification to my inbox (CONTACT_INBOX, a comma-separated list expanded into an array), with replyTo set to the submitter. This is the critical path. Its failure fails the request.
  2. Customer confirmation to the submitter, with replyTo set back to my inbox. This is best-effort. Its failure is logged and nothing more, because by the time it runs the lead is already captured.

Getting that second contract wrong is the mistake I see most often in intake code someone has already tried to harden. A form that 502s a real prospect because a courtesy email bounced has traded one failure for a worse one. Rank your sends: everything before the capture is allowed to fail loudly, everything after it is allowed to fail quietly.

The Rest of the Hardening

Around that core, the same commit series added the boring parts that keep the critical path clean:

  • Server-side validation — a service allowlist, length limits on every field, and an email format check, all enforced on the server rather than trusted from the client.
  • Rate limiting — five requests per IP per hour, so a bot cannot bury a real enquiry under a thousand fake ones.
  • Shared email primitives — one emailShell() helper in src/lib/email/shell.ts builds both emails, so the notification and the confirmation cannot drift apart.
  • A real sender address — mail goes out from a monitored mailbox, tariq@, not a noreply@ black hole. If someone replies to the confirmation, the reply reaches me.
  • Deliverability — SPF, DKIM and DMARC configured, because an email that lands in spam is the same silent failure wearing a different hat.

What Changed for Someone Who Contacts Me

Two things, and I'm keeping both modest, because they're the only before-and-after I can honestly state.

Enquiries used to get a reply the same day. Now they get one within the hour, because the notification arrives in a mailbox I actually watch, with the sender's address already in the reply field.

And the person who wrote in stops guessing. They get a branded confirmation that repeats their message back to them, from an address that accepts replies. That is the part I'd argue matters most: the gap between "I submitted a form" and "a human has this" is exactly where a prospect goes and contacts a competitor instead. Speed of first response is the cheapest advantage available to a small business, and I've written about why it's the first thing to automate in a lead-heavy industry.

A team monitoring production dashboards and system metrics

Verify It Yourself

Go to the contact page, fill it in, and send. You should have a confirmation email shortly after, from a monitored address, with your own message quoted back to you. Reply to it and it reaches me.

That is the whole proof, and it is available without asking me for anything. If it ever stops working, you'll know before I do — which is a level of exposure I would rather have than a case study nobody can check.

Audit Your Own Intake

The transferable lesson here has nothing to do with Next.js, Resend or n8n. Any intake — a form, a booking widget, a chatbot handoff, a phone tree — should survive these five questions:

  1. What exactly does your success message promise? If it says "I'll be in touch," something has to guarantee that a human can be.
  2. What happens when the destination is down? If the answer is "the visitor still sees a thank-you," you have this bug.
  3. Which step is the capture? Everything before it must be allowed to fail loudly; everything after it must be allowed to fail quietly.
  4. Who reads the inbox it lands in? A shared alias nobody opens is the same as no delivery.
  5. When did you last submit your own form on production? Most silent failures are found by an owner testing their own site, or by a prospect who happened to follow up twice.

Run those five against your own intake this week. It costs one test submission.

How Smart AI Workspace Approaches This

I'm Tariq Osmani, founder of Smart AI Workspace. Every engagement is founder-led — the person who scopes your build is the person who writes the code, which is also why the first thing I automated was my own business.

That order is deliberate. I would rather find the silent-failure class of bug in my own lead flow, at my own cost, than discover it in yours. When I audit a client's intake, the five questions above are the ones I open with, and more often than not the answer to number two is the answer mine was. For the wider context on how this kind of work gets scoped and priced, see what an AI workflow automation consultant actually does and how I price.

Get Your Intake Audited

If you cannot say for certain what happens to a lead when your delivery destination fails, that is worth an hour of attention before it is worth a rebuild. Contact me for a free audit — I'll submit your form, trace where it goes, and tell you what breaks. See what I build, how I price, or check verified work history on my Upwork profile.

Case studies two and three are in progress, and they follow the same rule: nothing in them you cannot check.


Sources: Next.js — Route Handlers · Resend — Send Email API reference · RFC 7489 — Domain-based Message Authentication, Reporting and Conformance (DMARC) · Google — Email sender guidelines · MDN — Using the Fetch API

Frequently asked questions

Is this a real client case study?
No, and that is deliberate. There is no client, no NDA, and no anonymised logo. The subject is Smart AI Workspace's own website — client zero. I ran the intake rebuild on my own business before selling it, which means you can verify the result yourself by submitting the contact form at smartaiworkspace.tech and watching the confirmation email arrive.
How can a contact form lose leads without anyone noticing?
Because the visitor's thank-you message and the actual delivery of the lead are two different things, and most forms only guarantee the first. In my case the route forwarded submissions to a webhook only when an environment variable was set, and returned success either way. When that variable was missing or wrong, the visitor saw a thank-you and the lead reached nobody. Nothing errored, nothing alerted, and there is no queue to inspect afterwards.
How do I check whether my own contact form is actually delivering?
Submit it on your live site, from a device that is not logged into anything, using an address you control. Then confirm three things: the notification landed in an inbox a human reads, the reply-to on it is the submitter, and the submitter got an acknowledgement. If any of the three fails while the form still shows a thank-you, you have the same bug I had. Repeat it after every deploy that touches the intake path.
Should a contact form fail if the confirmation email fails?
No. Separate the two sends by contract. The internal notification is the critical path — if it does not send, the request must fail, because nothing has captured the lead. The customer confirmation is best-effort: by the time it runs the lead is already in the inbox, so a failure there gets logged and the visitor still sees success. Failing the whole submission over a courtesy email would tell a real prospect to go away.
Why move the contact form from an n8n webhook to Resend?
Not because of the tool. The webhook forward worked when it was pointed at a live workflow; the problem was that my route treated the forward as optional and still reported success without it. Moving to a direct transactional send removed a hop between the form and my inbox and let the route gate its response on the outcome of that send. Fewer moving parts on the one path that must never fail.

Want this running in your business?

I build custom AI automation for B2B teams — from the first audit to production. Tell me what's slowing you down and I'll map the fix.

Tariq Osmani

About the author

Tariq Osmani

AI Automation Specialist & Founder, Smart AI Workspace

Anthropic Registered Claude Partner | 11+ Certifications | 8+ Years IT Experience

Tariq builds custom AI agents and agentic automation systems for B2B businesses using Claude API, n8n, and FastAPI. As an Anthropic Registered Claude Partner, he specializes in production-ready automation that delivers real business results.