Documentation
Add spam-free form handling to any website in under 5 minutes. Create a form, copy its key, and POST your fields to one endpoint. We do AI spam-filtering, then email the real submissions to your team. No SDK, no backend, no CAPTCHA.
On this page
Quick start
Copy your form key
fk_xxxxxxxx. Copy it from the form card.POST your fields to the endpoint
form_key and whatever fields your form has. That is the whole integration.curl -X POST https://formguard.informationvillagelimited.com/api/submit \
-H "Content-Type: application/json" \
-d '{
"form_key": "fk_your_form_key",
"name": "Jane Doe",
"email": "jane@example.com",
"message": "I would like a quote.",
"honeypot": "",
"timestamp": 1789822000
}'Response: { "success": true, "message": "Thank you! Your submission has been received.", "submission_id": 1 }
FormGuard classifies the submission (legitimate, suspicious, or spam), drops spam, and emails the rest to your recipients with Reply-To set to the sender so you can reply directly.
Copy-paste: plain HTML
Works on any site. Drop this in, replace the form key, done.
<form id="contact">
<!-- honeypot: hidden from people, bots fill it -->
<input name="honeypot" tabindex="-1" autocomplete="off"
style="position:absolute;left:-9999px" aria-hidden="true" />
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<textarea name="message" placeholder="Message" required></textarea>
<button type="submit">Send</button>
</form>
<script>
const FORM_KEY = "fk_your_form_key";
const loadTime = Math.floor(Date.now() / 1000);
const form = document.getElementById("contact");
form.addEventListener("submit", async (e) => {
e.preventDefault();
const f = new FormData(form);
const res = await fetch("https://formguard.informationvillagelimited.com/api/submit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
form_key: FORM_KEY,
name: f.get("name"),
email: f.get("email"),
message: f.get("message"),
honeypot: f.get("honeypot"),
timestamp: loadTime,
}),
});
const data = await res.json();
if (data.success) form.innerHTML = "<p>Thanks! We'll be in touch.</p>";
});
</script>Copy-paste: React / Next.js
"use client";
import { useState } from "react";
const ENDPOINT = "https://formguard.informationvillagelimited.com/api/submit";
const FORM_KEY = "fk_your_form_key";
export default function ContactForm() {
const [form, setForm] = useState({ name: "", email: "", message: "" });
const [honeypot, setHoneypot] = useState("");
const [loadTime] = useState(() => Math.floor(Date.now() / 1000));
const [sent, setSent] = useState(false);
const submit = async (e: React.FormEvent) => {
e.preventDefault();
const res = await fetch(ENDPOINT, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ form_key: FORM_KEY, ...form, honeypot, timestamp: loadTime }),
});
if ((await res.json()).success) setSent(true);
};
if (sent) return <p>Thanks! We'll be in touch.</p>;
return (
<form onSubmit={submit}>
<input aria-hidden tabIndex={-1} autoComplete="off"
style={{ position: "absolute", left: "-9999px" }}
value={honeypot} onChange={(e) => setHoneypot(e.target.value)} />
<input placeholder="Name" required value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })} />
<input type="email" placeholder="Email" required value={form.email}
onChange={(e) => setForm({ ...form, email: e.target.value })} />
<textarea placeholder="Message" required value={form.message}
onChange={(e) => setForm({ ...form, message: e.target.value })} />
<button type="submit">Send</button>
</form>
);
}Copy-paste: SvelteKit
<script>
const ENDPOINT = "https://formguard.informationvillagelimited.com/api/submit";
const FORM_KEY = "fk_your_form_key";
let name = "", email = "", message = "", honeypot = "", sent = false;
const loadTime = Math.floor(Date.now() / 1000);
async function submit() {
const res = await fetch(ENDPOINT, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ form_key: FORM_KEY, name, email, message, honeypot, timestamp: loadTime }),
});
if ((await res.json()).success) sent = true;
}
</script>
{#if sent}
<p>Thanks! We'll be in touch.</p>
{:else}
<form on:submit|preventDefault={submit}>
<input bind:value={honeypot} tabindex="-1" aria-hidden="true"
style="position:absolute;left:-9999px" />
<input bind:value={name} placeholder="Name" required />
<input bind:value={email} type="email" placeholder="Email" required />
<textarea bind:value={message} placeholder="Message" required />
<button type="submit">Send</button>
</form>
{/if}API reference
POST https://formguard.informationvillagelimited.com/api/submit
JSON body. No auth header - the form key identifies your form.
| Field | Type | Required | Description |
|---|---|---|---|
| form_key | string | Yes | Your form's key from the dashboard (fk_...) |
| your fields | any | Per form | Any fields your form has (name, email, message, phone, budget, ...). All are stored and emailed. Required fields are set in the dashboard. |
| honeypot | string | Recommended | A hidden field. If non-empty, the submission is silently dropped (a bot filled it). |
| timestamp | number | Recommended | Unix seconds when the form loaded. Submissions faster than 3s are flagged as bots. |
Responses:
// accepted
{ "success": true, "message": "Thank you! ...", "submission_id": 42 }
// missing a required field
{ "success": false, "message": "Missing required field(s): Email" }
// rate limited (too many submissions from one IP)
{ "success": false, "message": "Too many submissions. Please try again later." }Bot-flagged submissions (honeypot / too fast / blocklisted) also return success: true on purpose, so bots get no signal. They are simply not delivered.
Custom fields
FormGuard does not lock you to name/email/message. Send any fields your form needs and they are all stored and included in the notification email, each as its own row. A booking form, an order form, a job application - all work with the same endpoint.
{
"form_key": "fk_your_form_key",
"email": "buyer@shop.com",
"product": "12kg Roasted Beans",
"quantity": "40",
"delivery_city": "Kampala",
"notes": "Need an invoice",
"honeypot": "",
"timestamp": 1789822000
}Tip: include an emailfield and we set the email's Reply-To to it, so you reply straight to the customer. Mark required fields per form in the dashboard.
Anti-spam (honeypot + timing)
Two tiny additions make the spam filtering much stronger - both are in every example above:
- Honeypot - a hidden input people never see but bots auto-fill. Position it off-screen (
position:absolute;left:-9999px) and send its value ashoneypot. If it's non-empty, we drop the submission. - Timestamp - record
Date.now()/1000when the page loads and send it astimestamp. Submissions under 3 seconds are treated as bots.
On top of these, every submission is AI-classified, checked against a shared blocklist, and rate-limited per form. Confirmed spam also feeds SENTINEL's threat network, pre-blocking those attackers across the fleet.
Webhooks
Prefer to handle submissions in your own backend? Set a Webhook URL on the form in your dashboard. We POST every legitimate and suspicious submission to it as JSON (spam is never sent), in addition to the email. When you save a webhook we generate a signing secret shown right next to the URL.
The request body looks like this:
{
"submission_id": 42,
"form_key": "fk_your_form_key",
"form_name": "Contact form",
"classification": "legitimate",
"confidence": 0.02,
"fields": {
"name": "Jane Doe",
"email": "jane@example.com",
"message": "I would like a quote."
},
"origin": "https://yoursite.com"
}Every request carries an X-FormGuard-Signature header: sha256=<hex>, an HMAC-SHA256 of the raw body keyed with your signing secret. Verify it against the raw request body before trusting the payload.
import crypto from "crypto";
// Your form's signing secret, shown in the dashboard next to the webhook URL.
const SECRET = process.env.FORMGUARD_WEBHOOK_SECRET!;
export async function POST(req: Request) {
const raw = await req.text(); // verify against the RAW body, not a re-serialized object
const signature = req.headers.get("x-formguard-signature") ?? "";
const expected = "sha256=" + crypto.createHmac("sha256", SECRET).update(raw).digest("hex");
const ok = signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!ok) return new Response("invalid signature", { status: 401 });
const submission = JSON.parse(raw);
// submission.fields, submission.classification, submission.form_key ...
return new Response("ok"); // a 2xx acknowledges; non-2xx may be retried
}Respond with any 2xx to acknowledge. Delivery is best-effort: we retry a few times on network errors or 5xx responses, and stop on a 4xx. Email forwarding is unaffected either way.
Viewing submissions
Real submissions are emailed to your form's recipients automatically. You can also see every submission, the AI classification breakdown, and manage recipients per form in your dashboard.
Built by Information Village Limited