Max Igoshev.
Email

A contact form that actually delivers: a walk through mine

Most forms say thank you whether or not the message arrived. Here is the form on this site taken apart: working without JavaScript, filtering bots without a captcha, and being honest when delivery fails.

A contact form looks like a simple job until you ask one question: what happens when sending fails.

Most forms answer “thanks, we’ll be in touch” either way. The visitor leaves happy, the message reaches nobody, and you find out a month later when someone says “I wrote to you and never heard back”. This is not a rare accident. It is the default behaviour.

Here is the form on this site in full. It is open in the repository, so everything below can be checked.

A vertical flow. The form on the page sends a plain POST to the /api/lead function. The function first checks a hidden honeypot and the fill time: a submission that looks like a bot is answered as if everything went fine and goes no further. A real request is sent to Telegram, the required channel, and in parallel as a copy to a spreadsheet, whose failure does not affect delivery. If Telegram accepted it the visitor sees a thank-you page, and if not, a page carrying the email address. Form on the page a plain POST, no JavaScript The /api/lead function honeypot and fill time Looks like a bot answered as if fine Telegram the required channel Spreadsheet a copy, may fail quietly delivered not delivered Thank-you page Page with the email
The path of a request, from the form to a message

It works without JavaScript

The form is a plain <form method="post" action="/api/lead">. Nothing intercepts the submit for the sake of a nicer transition.

That is about delivery, not principle. JavaScript fails to run more often than people assume: data saving in a mobile browser, a blocker that caught more than it meant to, a slow connection where someone pressed the button before the scripts loaded, reader mode. If sending depends on a script, all of those submissions simply vanish — and silently, because the analytics that would have counted them is a script too.

Browsers have been able to send forms by themselves for thirty years. The server takes the FormData and answers with a redirect to a thank-you page. The script on the page handles only the small things: filling in the UTM fields and posting in the background so the page does not reload. Turn it off and everything still works, with a reload.

Bots are filtered without a captcha

There is deliberately no captcha. It moves the work onto a person who has already decided to write to you, at the worst possible moment to ask them to identify traffic lights.

Two quiet filters take its place.

The first is a hidden website field, kept out of sight and marked aria-hidden so a screen reader does not meet it either. A person will not fill it in because they cannot see it. Bots fill forms by field name, and a field called “website” is one they are happy to complete.

The second is time. The page stamps a hidden field with the moment it loaded, and anything submitted within three seconds is treated as machinery. A human cannot physically read three fields and write a meaningful description that fast.

The important part is what happens next. A caught submission gets exactly the response a good one gets: the same redirect to the thank-you page, the same {"ok": true}. The bot never learns it was filtered and never starts probing for a way around. From outside the two are indistinguishable; inside, a line goes to the log.

Long text is trimmed, not rejected

Every field has a limit: a hundred characters for the name, two hundred for the contact, three thousand for the description. But going over does not reject the message, it shortens the value.

The difference matters. Someone who wrote four thousand characters of detail is a good enquiry — they have already invested. Answering “too long, try again” loses them for nothing. Far better that it arrives trimmed and you ask for the rest by email.

Two recipients, not equally important

The message goes to two places, and they do not rank the same.

Telegram is the required channel. If it did not arrive there, it did not arrive.

The spreadsheet is a copy. If its webhook is down or answers with an error, a line goes to the log and delivery carries on untouched. A copy kept for the record has no business taking down the main channel. That is the general rule for any such chain: decide in advance what is required, and never let the optional block it.

Both calls carry an eight-second timeout. Without one, a hung provider would hold the visitor on a blank screen until the function itself timed out.

The part that matters most: what failure looks like

If Telegram did not accept the message, the visitor does not see a thank-you. They see a page saying it did not go through, with a button to email directly.

That is uncomfortable to show and right to show. The enquiry is not lost: the person is at their most motivated, they have already written everything out, and one click takes them to their mail client. A false thank-you in the same situation loses them completely, and quietly.

The same decision sits in the voice receptionist: when the agent twice fails to capture an email, it does not pretend everything is fine — it flags the record and hands it to a person. One principle throughout. A failure should be shown to whoever can act on it.

What to take from this

Four rules, independent of the stack.

Let the browser send the form and let the script only improve on it. Filter bots quietly and answer them as if all went well. Decide which channel is required and do not let optional ones bring it down. And show an honest error with a fallback way to reach you instead of a false thank-you.

The form and the server function are open in the site’s repository. If you want the same chain against your own site and CRM, describe the task.

  • Automation
  • Forms
  • Telegram
  • Lead intake