← Back to FormGuard

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.

Quick start

1

Create a form

Sign up, open your dashboard, click New form, and choose where submissions should be emailed (one or more addresses).
2

Copy your form key

Each form has a key like fk_xxxxxxxx. Copy it from the form card.
3

POST your fields to the endpoint

Send a JSON POST with your form_key and whatever fields your form has. That is the whole integration.
Try it now (replace the form key)
curl
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.

contact.html
html
<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

ContactForm.tsx
tsx
"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&apos;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

+page.svelte
svelte
<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.

FieldTypeRequiredDescription
form_keystringYesYour form's key from the dashboard (fk_...)
your fieldsanyPer formAny fields your form has (name, email, message, phone, budget, ...). All are stored and emailed. Required fields are set in the dashboard.
honeypotstringRecommendedA hidden field. If non-empty, the submission is silently dropped (a bot filled it).
timestampnumberRecommendedUnix seconds when the form loaded. Submissions faster than 3s are flagged as bots.

Responses:

Responses
json
// 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.

An order form, for example
json
{
  "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 as honeypot. If it's non-empty, we drop the submission.
  • Timestamp - record Date.now()/1000 when the page loads and send it as timestamp. 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:

POST to your webhook URL
json
{
  "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.

app/api/formguard/route.ts (Next.js)
tsx
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.