Documentation

One endpoint: POST https://formroost.shovelware.ai/api/v1/submit. Send form-encoded, multipart, or JSON. That's the whole API.

1. Get an access key

Enter your email on the homepage (or POST /api/v1/keys with {"email": "you@example.com"}). Your key arrives by email β€” which also proves you own the inbox submissions will be sent to.

2. Plain HTML

<form action="https://formroost.shovelware.ai/api/v1/submit" method="POST">
  <input type="hidden" name="access_key" value="YOUR_ACCESS_KEY">
  <input type="email" name="email" required>
  <textarea name="message" required></textarea>
  <input type="checkbox" name="botcheck" style="display:none" tabindex="-1">
  <button type="submit">Send</button>
</form>

On success the visitor is redirected to a hosted thank-you page (or your own β€” see _redirect below).

3. JavaScript / fetch

Send Accept: application/json (or a JSON body) to get a JSON response instead of a redirect:

const res = await fetch("https://formroost.shovelware.ai/api/v1/submit", {
  method: "POST",
  headers: { "Content-Type": "application/json", Accept: "application/json" },
  body: JSON.stringify({
    access_key: "YOUR_ACCESS_KEY",
    email: form.email,
    message: form.message,
  }),
});
const data = await res.json(); // { success: true, message: "..." }

4. React / Next.js

export default function ContactForm() {
  const [status, setStatus] = useState("");

  async function onSubmit(e) {
    e.preventDefault();
    setStatus("Sending…");
    const body = JSON.stringify(
      Object.fromEntries(new FormData(e.target).entries())
    );
    const res = await fetch("https://formroost.shovelware.ai/api/v1/submit", {
      method: "POST",
      headers: { "Content-Type": "application/json", Accept: "application/json" },
      body,
    });
    const data = await res.json();
    setStatus(data.message);
    if (data.success) e.target.reset();
  }

  return (
    <form onSubmit={onSubmit}>
      <input type="hidden" name="access_key" value="YOUR_ACCESS_KEY" />
      <input type="email" name="email" required />
      <textarea name="message" required />
      <button type="submit">Send</button>
      <p>{status}</p>
    </form>
  );
}

The same pattern works in Astro, SvelteKit, Vue, and anything else that can fetch.

5. HTMX

<form hx-post="https://formroost.shovelware.ai/api/v1/submit" hx-swap="innerHTML" hx-target="#result">
  <input type="hidden" name="access_key" value="YOUR_ACCESS_KEY">
  <input type="email" name="email" required>
  <textarea name="message" required></textarea>
  <button type="submit">Send</button>
</form>
<div id="result"></div>

Special fields

Control fields start with an underscore and never appear in the delivered email.

FieldWhat it does
access_keyRequired. Your form's key.
_subjectCustom subject line for the notification email.
_replytoSets the Reply-To address. Defaults to the submission's email field, so you can hit Reply.
_ccComma-separated CC addresses (max 3).
_redirectAbsolute URL to send the visitor to after a successful classic-form submit.
botcheckHoneypot. Include it hidden and empty; anything that fills it in is silently dropped.

Spam protection

Every submission passes through (all free, all tiers):

Spam is silently accepted: the bot sees a success response, you see nothing.

Cloudflare Turnstile is supported: render the Turnstile widget in your form and the cf-turnstile-response token is verified server-side (self-hosters set TURNSTILE_SECRET). Invalid tokens are treated as spam.

Dashboard, history & webhooks

Your form works forever without an account. When you want more, sign in with the same email (magic link, no password) to:

Responses

CaseClassic formJSON mode
Success302 β†’ _redirect or hosted thank-you200 {"success":true}
Bad/missing key302 β†’ error page401 {"success":false}
Rate / quota limit302 β†’ error page429 {"success":false}

Limits