# Post to a form from your own website

Keep your own form's design and send its answers to 53°, with an example and the checks we recommend you add.

If you'd rather keep a form you've already designed, your website can post its answers straight to a form in 53°. The responses land in your inbox like any other, and your form's automations run on them.

Our own form page and embed check for bots before they take a response. A post from your own page can't carry that check, so it's worth adding a few protections of your own. This article shows how.

## Before you start

<ul class="accent-bullets"><li><p>You need to be able to change the form. See <a href="../marketing/who-can-see-a-form.md" target="_blank" rel="noopener noreferrer" class="cursor-pointer box-border border-b transition-all duration-75 " style="text-decoration: none; border-color: var(--portal-accent-color);">Who can see a form</a>.</p></li><li><p>The form needs to be <strong>Live</strong>.</p></li><li><p>You'll need to edit your website's HTML. For the bot check, you'll also need somewhere to run a small piece of server code.</p></li><li><p>If the embed would do, use that. It checks for bots, takes file uploads and needs no code. See <a href="../marketing/share-or-embed-a-form.md" target="_blank" rel="noopener noreferrer" class="cursor-pointer box-border border-b transition-all duration-75 " style="text-decoration: none; border-color: var(--portal-accent-color);">Share or embed a form</a>.</p></li></ul>

## Turn it on and get your address

<ol class="steps-list"><li><p>Open the form and choose <strong>Share</strong>.</p></li><li><p>Open the <strong>Your own form</strong> tab and turn on <strong>Take posts from my own website</strong>.</p></li><li><p>Copy the <strong>Post to</strong> address. For a secret form, the address includes its key.</p></li><li><p>Note each field's <strong>Key</strong>. You'll find it at the bottom of each field on the <strong>Build</strong> tab. An answer is posted under its field's key.</p></li></ol>

**Starting markup** on the same tab gives you a plain form with an input for each field, which you can style as you like.

## A simple example

This form has three fields, with the keys `name`, `email` and `message`. Replace the address with your own **Post to** address.

```html
<form id="contact" action="https://YOUR-POST-TO-ADDRESS" method="post">
  <label>Name <input name="name" required></label>
  <label>Email <input name="email" type="email" required></label>
  <label>Message <textarea name="message"></textarea></label>

  <!-- Not for people. Leave it empty and out of sight. -->
  <input name="fax_number" tabindex="-1" autocomplete="off" aria-hidden="true"
    style="position:absolute;left:-9999px">

  <button>Send</button>
  <p id="contact-status" role="status"></p>
</form>

<script>
  const form = document.getElementById("contact")
  const status = document.getElementById("contact-status")

  form.addEventListener("submit", async (event) => {
    event.preventDefault()
    status.textContent = "Sending…"
    try {
      const response = await fetch(form.action, {
        method: "POST",
        headers: { Accept: "application/json" },
        body: new FormData(form),
      })
      const result = await response.json()
      if (result.ok) {
        form.reset()
        status.textContent = "Thanks. Your message has been sent."
        return
      }
      // `issues` names the answers that need another look, by field key.
      const first = result.issues && result.issues[0]
      status.textContent = first
        ? `${first.key}: ${first.message}`
        : result.message
    } catch {
      status.textContent = "That didn't send. Check your connection and try again."
    }
  })
</script>
```

Without the script, the form still works: the visitor is taken to the page you set under **After sending**, or to a plain page saying their response has been sent.

## What to send

<ul class="accent-bullets"><li><p>Name each input after its field's key.</p></li><li><p>For a field with options, send the option's label. For <strong>Multiple choice</strong>, send the name once for each option chosen.</p></li><li><p>For a <strong>Tick box</strong> or <strong>Switch</strong>, send <code class="inline-code-shadow bg-gray-100 border border-gray-200/60 dark:border-gray-800 font-medium dark:bg-gray-700/60 text-gray-900 dark:text-gray-200 font-mono text-md px-1 py-0.5 rounded-[0.2rem]">on</code>, <code class="inline-code-shadow bg-gray-100 border border-gray-200/60 dark:border-gray-800 font-medium dark:bg-gray-700/60 text-gray-900 dark:text-gray-200 font-mono text-md px-1 py-0.5 rounded-[0.2rem]">true</code>, <code class="inline-code-shadow bg-gray-100 border border-gray-200/60 dark:border-gray-800 font-medium dark:bg-gray-700/60 text-gray-900 dark:text-gray-200 font-mono text-md px-1 py-0.5 rounded-[0.2rem]">yes</code> or <code class="inline-code-shadow bg-gray-100 border border-gray-200/60 dark:border-gray-800 font-medium dark:bg-gray-700/60 text-gray-900 dark:text-gray-200 font-mono text-md px-1 py-0.5 rounded-[0.2rem]">1</code> for on.</p></li><li><p>For a <strong>Rating</strong>, send its number, starting from 1. A thumbs down is 1 and a thumbs up is 2. Faces run from 1 to 5.</p></li><li><p>For an <strong>Address</strong>, send <code class="inline-code-shadow bg-gray-100 border border-gray-200/60 dark:border-gray-800 font-medium dark:bg-gray-700/60 text-gray-900 dark:text-gray-200 font-mono text-md px-1 py-0.5 rounded-[0.2rem]">key.line1</code>, <code class="inline-code-shadow bg-gray-100 border border-gray-200/60 dark:border-gray-800 font-medium dark:bg-gray-700/60 text-gray-900 dark:text-gray-200 font-mono text-md px-1 py-0.5 rounded-[0.2rem]">key.line2</code>, <code class="inline-code-shadow bg-gray-100 border border-gray-200/60 dark:border-gray-800 font-medium dark:bg-gray-700/60 text-gray-900 dark:text-gray-200 font-mono text-md px-1 py-0.5 rounded-[0.2rem]">key.city</code>, <code class="inline-code-shadow bg-gray-100 border border-gray-200/60 dark:border-gray-800 font-medium dark:bg-gray-700/60 text-gray-900 dark:text-gray-200 font-mono text-md px-1 py-0.5 rounded-[0.2rem]">key.region</code>, <code class="inline-code-shadow bg-gray-100 border border-gray-200/60 dark:border-gray-800 font-medium dark:bg-gray-700/60 text-gray-900 dark:text-gray-200 font-mono text-md px-1 py-0.5 rounded-[0.2rem]">key.postcode</code> and <code class="inline-code-shadow bg-gray-100 border border-gray-200/60 dark:border-gray-800 font-medium dark:bg-gray-700/60 text-gray-900 dark:text-gray-200 font-mono text-md px-1 py-0.5 rounded-[0.2rem]">key.country</code>, using the field's key.</p></li><li><p>Files can't be sent this way. Use the form's own page or its embed for a <strong>File upload</strong> field.</p></li><li><p>You can also post JSON: an object with each key and its answer, and a <code class="inline-code-shadow bg-gray-100 border border-gray-200/60 dark:border-gray-800 font-medium dark:bg-gray-700/60 text-gray-900 dark:text-gray-200 font-mono text-md px-1 py-0.5 rounded-[0.2rem]">Content-Type</code> of <code class="inline-code-shadow bg-gray-100 border border-gray-200/60 dark:border-gray-800 font-medium dark:bg-gray-700/60 text-gray-900 dark:text-gray-200 font-mono text-md px-1 py-0.5 rounded-[0.2rem]">application/json</code>.</p></li></ul>

## Protect your form

A form on a public page will be found by scripts. We recommend all three of these.

### Keep the honeypot

The `fax_number` input in the example is a honeypot. It's moved off the screen, so people never see it and never fill it in. Many scripts fill in every field they find, and it's named like a real field so they don't skip it. A post with anything in that field is quietly dropped: the script is told it worked, and nothing is stored.

Keep it in your form, keep it out of sight and leave it empty. Don't use `fax_number` as the key of one of your own fields.

### Add a bot check with Cloudflare Turnstile

Turnstile is Cloudflare's free bot check. It usually runs without asking your visitor to do anything.

A bot check only protects you if the token it produces is verified with your secret key, and that has to happen on a server, never in the page. So with Turnstile, your form posts to your own server first. Your server checks the token, then passes the answers on to 53°.

<ol class="steps-list"><li><p>In your Cloudflare dashboard, add a Turnstile widget for your website. You'll get a <strong>site key</strong> and a <strong>secret key</strong>.</p></li><li><p>Add the widget to your form. It adds a hidden <code class="inline-code-shadow bg-gray-100 border border-gray-200/60 dark:border-gray-800 font-medium dark:bg-gray-700/60 text-gray-900 dark:text-gray-200 font-mono text-md px-1 py-0.5 rounded-[0.2rem]">cf-turnstile-response</code> input for you:</p></li></ol>

```html
   <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>

   <form id="contact" action="/api/contact" method="post">
     <!-- your inputs, and the fax_number input, as above -->
     <div class="cf-turnstile" data-sitekey="YOUR-SITE-KEY"></div>
     <button>Send</button>
   </form>
```

<ol class="steps-list" start="3"><li><p>On your server, verify the token before passing the answers on. This example is a handler for any JavaScript runtime that supports <code class="inline-code-shadow bg-gray-100 border border-gray-200/60 dark:border-gray-800 font-medium dark:bg-gray-700/60 text-gray-900 dark:text-gray-200 font-mono text-md px-1 py-0.5 rounded-[0.2rem]">fetch</code>, such as a serverless function:</p></li></ol>

```js
   const POST_TO = "https://YOUR-POST-TO-ADDRESS"
   const TURNSTILE_SECRET = process.env.TURNSTILE_SECRET_KEY

   export async function POST(request) {
     const fields = await request.formData()

     // 1. The honeypot: say it worked, and send nothing on.
     if (fields.get("fax_number")) {
       return Response.json({ ok: true })
     }

     // 2. The bot check, verified with your secret key.
     const check = await fetch(
       "https://challenges.cloudflare.com/turnstile/v0/siteverify",
       {
         method: "POST",
         body: new URLSearchParams({
           secret: TURNSTILE_SECRET,
           response: fields.get("cf-turnstile-response") ?? "",
         }),
       }
     )
     const verdict = await check.json()
     if (!verdict.success) {
       return Response.json(
         { ok: false, message: "We couldn't confirm you're not a robot. Try again." },
         { status: 403 }
       )
     }

     // 3. Pass the answers on, without the token.
     fields.delete("cf-turnstile-response")
     const sent = await fetch(POST_TO, {
       method: "POST",
       headers: { Accept: "application/json" },
       body: fields,
     })
     return Response.json(await sent.json(), { status: sent.status })
   }
```

Keep the secret key on your server, in an environment variable. Never put it in your page.

### Limit how often one visitor can post

A rate limit stops one visitor, or one script, from sending hundreds of responses. If your form posts through your own server, add the limit there, before the bot check. This is the simplest version, which remembers each address for a minute:

```js
const WINDOW_MS = 60 * 1000
const MOST_PER_WINDOW = 5
const recent = new Map()

function allowed(address) {
  const now = Date.now()
  const times = (recent.get(address) ?? []).filter((at) => now - at < WINDOW_MS)
  if (times.length >= MOST_PER_WINDOW) {
    return false
  }
  recent.set(address, [...times, now])
  return true
}
```

Call `allowed` with the visitor's address at the top of your handler, and answer with a 429 status when it returns `false`. A limit held in memory like this one resets when your server restarts and isn't shared between servers. If your site runs on several, use your host's rate limiting or a shared store instead.

53° also limits how quickly responses can arrive from one address, and how many one form takes in an hour. When a limit is reached, the post is turned away with a 429 status and a message asking the visitor to try again in a few minutes. If your server passes posts on, they all arrive from your server's address, so a busy form can reach that limit sooner.

## Good to know

<ul class="accent-bullets"><li><p>If the answers aren't right, the post is turned away with a 422 status. The reply's <code class="inline-code-shadow bg-gray-100 border border-gray-200/60 dark:border-gray-800 font-medium dark:bg-gray-700/60 text-gray-900 dark:text-gray-200 font-mono text-md px-1 py-0.5 rounded-[0.2rem]">issues</code> list names each field by its key, with what needs fixing.</p></li><li><p>A form that has stopped taking responses answers with a 410 status.</p></li><li><p>Your form's privacy policy switch isn't shown on your own page. If you need agreement to your privacy policy, add it to your own form.</p></li><li><p>For a secret form, keep its <strong>Post to</strong> address on your server if you can. Anyone who can read your page's source can read an address in it.</p></li><li><p>Each response counts towards your plan's monthly allowance, however it arrives. See <a href="../organisation/plans-and-billing.md" target="_blank" rel="noopener noreferrer" class="cursor-pointer box-border border-b transition-all duration-75 " style="text-decoration: none; border-color: var(--portal-accent-color);">Plans and billing</a>.</p></li></ul>

## Related

<ul class="accent-bullets"><li><p><a href="../marketing/share-or-embed-a-form.md" target="_blank" rel="noopener noreferrer" class="cursor-pointer box-border border-b transition-all duration-75 " style="text-decoration: none; border-color: var(--portal-accent-color);">Share or embed a form</a></p></li><li><p><a href="../marketing/build-a-form.md" target="_blank" rel="noopener noreferrer" class="cursor-pointer box-border border-b transition-all duration-75 " style="text-decoration: none; border-color: var(--portal-accent-color);">Build a form</a></p></li><li><p><a href="../marketing/manage-responses-in-the-inbox.md" target="_blank" rel="noopener noreferrer" class="cursor-pointer box-border border-b transition-all duration-75 " style="text-decoration: none; border-color: var(--portal-accent-color);">Manage responses in the inbox</a></p></li><li><p><a href="../marketing/automate-a-form.md" target="_blank" rel="noopener noreferrer" class="cursor-pointer box-border border-b transition-all duration-75 " style="text-decoration: none; border-color: var(--portal-accent-color);">Automate a form</a></p></li></ul>
