Forms Integration Guide

Reference for wiring a contact form on a new Sevenoways client site to this backend. Give this whole page to the agent building the site.

forms.sevenoways.dev is a self-hosted, stateless form endpoint. A client site POSTs form data with an access_key; the service validates the origin, filters spam, stores the submission, and emails the site owner with the submitter's address set as Reply-To. There is no client-side library and nothing to install — it's one URL and a few hidden fields.

1. Get an access key first

Every form needs a key scoped to the domain(s) it will run on. Ask the site owner to mint one at forms.sevenoways.dev/admin (magic-link login, owner-only) — they'll need a label, the domain(s) the form will live on exactly as they appear in the browser (e.g. clientsite.ie, www.clientsite.ie — both, if both resolve), and the recipient email address(es).

The key only works from its registered domain(s) — submissions from anywhere else are rejected. If the site later moves domains or adds a subdomain, that has to be added to the key first.

2. The endpoint

POST https://forms.sevenoways.dev/submit

Accepts three content types, so it works with or without JavaScript:

Content-TypeUse case
application/jsonfetch/AJAX submission
application/x-www-form-urlencodedplain <form>, JS disabled
multipart/form-dataalso works; files accepted but currently ignored

3. Every form needs these three things

  1. access_key — hidden input or JSON field, the UUID from step 1.
  2. A honeypot field named botcheck — hidden from real users, left empty by them, filled in by bots. A submission with botcheck set is silently accepted (200) and dropped — nothing stored, no email sent, and the bot isn't told it was caught.
  3. A matching Origin/Referer — enforced automatically by the browser; nothing to add in markup, but it's why the domain has to be registered on the key first.

Reserved field names — read as control data, not form content: access_key, redirect, botcheck, cf-turnstile-response. Everything else you name (name, email, message, phone, whatever the form needs) is stored as submission content and appears in the notification email.

4. Copy-paste snippets

No-JS — works with scripts disabled. redirect sends the visitor to a thank-you page on success:

<form action="https://forms.sevenoways.dev/submit" method="POST">
  <input type="hidden" name="access_key" value="THE-UUID">
  <input type="hidden" name="redirect" value="https://clientsite.ie/thanks">
  <input type="text" name="name" required>
  <input type="email" name="email" required>
  <textarea name="message" required></textarea>
  <input type="checkbox" name="botcheck" style="display:none" tabindex="-1" autocomplete="off">
  <button type="submit">Send</button>
</form>

JS/fetch — inline success message, no page navigation:

<form id="cf">
  <input type="hidden" name="access_key" value="THE-UUID">
  <input name="name" required>
  <input type="email" name="email" required>
  <textarea name="message" required></textarea>
  <input type="checkbox" name="botcheck" style="display:none" tabindex="-1">
  <button>Send</button>
</form>
<script>
document.getElementById('cf').addEventListener('submit', async (e) => {
  e.preventDefault();
  const data = Object.fromEntries(new FormData(e.target));
  const res = await fetch('https://forms.sevenoways.dev/submit', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data)
  });
  const out = await res.json();
  alert(out.message);
});
</script>

Swap THE-UUID and the redirect URL for the real values from step 1.

5. Response shapes

// success (JSON mode)
{ "success": true, "message": "Thanks, your message was sent." }

// error
{ "success": false, "message": "Invalid access key." }

The no-JS form gets a 303 redirect to redirect (or the key's configured default) on success instead of a JSON body.

StatusMeaning
200accepted
303accepted, redirecting (no-JS mode)
403unknown/inactive key, or wrong origin
413payload too large
422Turnstile check failed (only if the key has it enabled)
429rate limited
500server error

Error messages are intentionally generic — the service never reveals whether a key is unknown vs. disabled.

6. Limits

7. Optional: Turnstile captcha

Off by default. If the site owner enabled it for a key, add the Cloudflare Turnstile widget and its script tag to the form — a submission with a missing or invalid token is rejected (422) the same way. The site key comes from the Cloudflare dashboard, not from this service.

<div class="cf-turnstile" data-sitekey="YOUR-TURNSTILE-SITE-KEY"></div>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>

8. If something's wrong