One endpoint: POST https://formroost.shovelware.ai/api/v1/submit. Send form-encoded, multipart, or JSON. That's the whole API.
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.
<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).
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: "..." }
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.
<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>
Control fields start with an underscore and never appear in the delivered email.
| Field | What it does |
|---|---|
access_key | Required. Your form's key. |
_subject | Custom subject line for the notification email. |
_replyto | Sets the Reply-To address. Defaults to the submission's email field, so you can hit Reply. |
_cc | Comma-separated CC addresses (max 3). |
_redirect | Absolute URL to send the visitor to after a successful classic-form submit. |
botcheck | Honeypot. Include it hidden and empty; anything that fills it in is silently dropped. |
Every submission passes through (all free, all tiers):
botcheck field.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.
Your form works forever without an account. When you want more, sign in with the same email (magic link, no password) to:
{"event":"submission.created", "data":{β¦}})
to your URL, with retries and exponential backoff.{{field}} placeholders, e.g. Hi {{name}}, thanks!| Case | Classic form | JSON mode |
|---|---|---|
| Success | 302 β _redirect or hosted thank-you | 200 {"success":true} |
| Bad/missing key | 302 β error page | 401 {"success":false} |
| Rate / quota limit | 302 β error page | 429 {"success":false} |