How to build a custom captcha system for your blog contact form
A contact form gives readers, customers, and potential partners a direct way to reach your business. It also gives automated bots an easy target. Once a form address is discovered, scripts can send spam, inject malicious links, overload your inbox, or consume server resources.
A custom CAPTCHA system can reduce this abuse without sending visitors through an intrusive third-party challenge. For a blog, the best solution is usually a layered system that combines a hidden honeypot field, a minimum completion time, a short-lived token, and server-side validation.
This approach works well for websites serving Australian visitors, including small businesses in Sydney, Melbourne, Brisbane, Perth, and regional areas. It keeps the enquiry process quick for people using mobile connections while filtering many automated submissions.
The examples below use browser-side JavaScript and a PHP-style backend, but the same design can be adapted to Laravel, WordPress, Node.js, Python, or another web stack. The important principle is that the browser may assist with the challenge, but it must never be trusted to make the final security decision.
Start with the real spam problem
Before writing code, inspect what is reaching your contact form. Some spam is produced by basic bots that submit every visible input. Other attacks imitate a real browser, fill fields intelligently, and repeat requests from rotating IP addresses. A single checkbox will not stop every category.
A good custom CAPTCHA system should make automated submissions expensive while keeping genuine messages convenient. It should also protect against repeated requests, invalid tokens, missing fields, oversized messages, and suspicious links in message content.
For example, a local web designer in Melbourne may receive genuine enquiries during business hours, while an online store owner in Brisbane may receive messages at any time. Avoid rules that block visitors simply because they use a VPN, a shared office connection, or a mobile network. Behaviour and request integrity provide better signals than geography alone.
Use several small defences together
A honeypot is an invisible or visually hidden field that normal visitors should leave empty. Bots that fill every form field will often complete it. Give the field a believable name such as website_url, but do not rely on the CSS property display: none alone, because some bots recognise common honeypot patterns.
A time check adds another useful signal. When the server creates the form, record a timestamp in a signed token. Reject a submission completed unrealistically quickly, such as within two seconds. Set a reasonable upper limit as well, or permit an extended period for visitors who open a form and return later.
Rate limiting is essential. Store recent attempts by IP address, session, account, or a combination of signals. Australian households and offices can share an address through carrier-grade NAT, so an aggressive one-request-per-IP policy may block legitimate users. A practical limit might be several attempts per hour, supported by email verification or moderation for suspicious traffic.
Create a simple challenge with a signed token
A useful custom challenge can ask the visitor to solve a small arithmetic problem, select a matching option, or enter a short sequence shown as text. Keep it accessible and avoid distorted image puzzles that are difficult for people with low vision, dyslexia, motor impairments, or screen readers.
The server should generate the challenge and store the correct answer in a session or signed server-side record. Never place the answer in a hidden HTML field or JavaScript variable. Anyone can inspect those values through browser developer tools.
One secure pattern is to send a random challenge ID and an HMAC signature to the form. The signature proves that the values were generated by your server and have not been modified. A simplified PHP example might look like this:
$challenge = random_int(10, 49) . ' + ' . random_int(1, 9);
$answer = calculateAnswer($challenge);
$id = bin2hex(random_bytes(16));
$_SESSION['captcha'][$id] = [
'answer' => $answer,
'created' => time(),
];
$token = hash_hmac('sha256', $id, $_ENV['CAPTCHA_SECRET']);
Render the question and send the ID and token as ordinary form values. During submission, verify the HMAC with hash_equals, retrieve the server-side answer, check its age, and delete it after one attempt. One-time use prevents a valid token from being replayed repeatedly.
Validate everything on the server
Client-side JavaScript can improve the experience by displaying a new challenge without refreshing the page, but it is not a security boundary. Bots can ignore the script, imitate its requests, or submit directly to the endpoint. Your backend must repeat every important validation step.
Check that required fields exist, have the expected data type, and remain within sensible length limits. For instance, a name might be limited to 100 characters and a message to 5,000 characters. Reject control characters where they are not needed, escape output when displaying submissions, and use prepared statements when storing data in a database.
A basic verification flow should confirm the request method, CSRF token, captcha token, challenge age, honeypot status, rate limit, and message format. It should then apply spam scoring before sending email. Do not expose detailed failure reasons such as “the captcha answer was correct but the signature was invalid”; generic error messages reveal less to attackers.
If your blog covers business operations, contact forms may collect project information, quotes, and documents. A building contractor, for example, may need organised records for an enquiry or construction confirmation template. Treat these submissions as business data: restrict access, avoid logging message contents unnecessarily, and remove records according to a clear retention policy.
Design the form for accessibility
A security control that excludes genuine visitors damages conversion. Label the challenge clearly, connect the label to its input, and provide an error message that a screen reader can identify. Do not depend on colour alone to show whether an answer is correct.
Offer a non-visual alternative, such as a simple calculation in text, and ensure keyboard users can reach every control. Avoid requiring a visitor to drag objects, identify tiny images, or listen to distorted audio. If an accessibility service or support worker helps someone complete the form, the system should still work without unusual browser behaviour.
Keep the form concise. A visitor contacting a Perth agency from a phone should not need to solve several puzzles before sending a short enquiry. A hidden honeypot, signed token, and light calculation are usually enough for low-risk public forms. Escalate only when several risk indicators appear together.
Add JavaScript without trusting it
JavaScript can fetch a fresh challenge when the form loads, disable accidental double submissions, and show a friendly message after a failed attempt. It can also measure interaction patterns, although these signals should be treated as optional because privacy tools and strict browsers may remove them.
Do not create a challenge whose answer is visible in the page source. Do not assume that a disabled submit button prevents a request. The endpoint should behave safely when called with curl, an outdated browser, or no JavaScript at all.
Use a short-lived session cookie with secure attributes: HttpOnly, Secure, and an appropriate SameSite value. Add CSRF protection if the form changes server-side state or is connected to an authenticated area. For cross-origin requests, configure CORS deliberately rather than allowing every origin.
A lightweight frontend pattern might submit the form normally and let the server return a generic response. This is often more reliable than building a complex AJAX layer, particularly for small business websites hosted on shared Australian infrastructure.
Protect privacy and monitor performance
A custom CAPTCHA can reduce dependence on external tracking services. That may simplify privacy disclosures and avoid loading third-party scripts for every visitor. It does not remove your broader obligations: IP addresses, timestamps, email addresses, and message contents may still be personal information under applicable privacy requirements.
Tell visitors what information the form collects and why. Keep security logs separate from message content where possible, limit staff access, and set an automatic deletion period. If submissions are forwarded to a CRM, email provider, or marketing platform, review how those services store and process the data.
Monitoring helps you tune the system instead of guessing. Track accepted submissions, rejected challenges, rate-limit events, average completion time, and delivery failures. Avoid retaining full IP addresses longer than necessary; truncated or hashed values may be sufficient for operational analysis.
Watch for legitimate traffic patterns around Australian business hours, public holidays, and major local events. A sudden increase in enquiries from Sydney may be a successful campaign rather than an attack. Reviewing samples and metrics together helps prevent an overzealous CAPTCHA from hiding genuine leads.
Test, deploy, and maintain the protection
Test the form with JavaScript enabled and disabled, on a phone and desktop, with keyboard navigation, and through a screen reader. Send valid and invalid answers, reuse an old token, submit the honeypot, exceed field lengths, and repeat requests quickly. Confirm that every rejected request produces a safe response without leaking implementation details.
Run automated checks against the endpoint using tools such as PHPUnit, Playwright, or your framework’s testing utilities. Include tests for token expiry, signature comparison, session isolation, database errors, and mail service outages. Verify that a failed email delivery does not cause the visitor to resubmit repeatedly and create duplicate records.
Deploy behind HTTPS and keep the secret key outside the public web directory. Rotate secrets with a planned procedure, update dependencies, and review server logs after launch. If spam rises, first inspect which layer is being bypassed before adding a harder puzzle.
For high-volume sites, combine the custom system with web application firewall rules, temporary IP throttling, moderation queues, and email reputation controls. A contact form CAPTCHA is one part of a broader abuse-prevention strategy, not a substitute for secure coding and responsible operations.
Build the first version around a signed, one-time challenge, a honeypot, server-side validation, accessibility support, and sensible rate limits. Test it with real devices and realistic Australian visitor patterns, then adjust the thresholds using evidence. This gives your blog a quieter inbox and a smoother path for genuine customers, creators, and business enquiries.