SpamFlip

HTML forms

Last updated October 19, 2026. SpamFlip is operated by MLN Learning LLC.

This page describes a plain HTML form used with sf.js. It matches the snippet in the repository and the submit route. See the docs for the event and the privacy model.

Markup

The snippet attaches only to form[data-spamflip]. Set method="POST". A form with no method uses GET when the browser submits it itself. Give each field a name. Replace YOUR_SITE_KEY with the key from the dashboard. A missing data-site-key uses the store-only demo key.

<script src="https://spamflip.app/sf.js" data-site-key="YOUR_SITE_KEY" defer></script>

<form data-spamflip action="https://example.com/your-handler" method="POST">
  <input name="email" type="email" />
  <textarea name="message"></textarea>
  <button type="submit">Send</button>
</form>

What the browser sends

The script origin is taken from the sf.js URL. The snippet POSTs the form as FormData to {that origin}/api/submit?site=KEY. It also requests {that origin}/api/challenge?site=KEY after the visitor focuses the form. Both are cross-origin when the page is not on spamflip.app. The responses echo the page's Origin when that origin is allowed. A site created in the dashboard starts with an empty origin list, and an empty list allows the echo. If a list is set, the origin is echoed only when it is on the list.

The snippet tries that POST once and, if the browser cannot complete the request, tries once more. Each try waits up to 12 seconds. If both fail, it removes the honeypot fields it added and submits the form with the form's own method and action. A response that arrives and is not successful does not do that. It leaves the form in place and shows “Couldn't send that. Please try again in a moment.” unless the page cancels spamflip:submitted.

What the server forwards

The forward address is the URL saved on the site. It is not read from the form action. The saved URL has to be https, and it cannot include a username or password. SpamFlip checks the destination again immediately before sending, and it does not send to a loopback, private, or link-local address.

Only a clean verdict is forwarded. A quarantine or spam verdict is stored and the visitor receives the same neutral JSON envelope as a store-only clean submission. The forwarded body is the string fields from the browser's multipart POST. File inputs are dropped. Fields named sf_hp, sf_site, sf_field_to_submit_ms, sf_fill_velocity_ms, sf_mouse_moved, sf_token, and the rotating honeypot name are removed. When no file was uploaded, the server sends those string fields as application/x-www-form-urlencoded, with the original field names, sets Accept to application/json, and stops waiting after 8 seconds. When a file is included, the server still sends the string fields as multipart form data and does not copy the file. The copied text stops at 64,000 characters.

When a forward URL is set and the verdict is clean, the visitor's response is the handler's status and body. A handler response of 301, 302, or 303 is an exception: SpamFlip counts it as success, does not POST again, and does not request the redirect location. The visitor receives HTTP 200 and {"status":"ok"} for that case. A 307 or 308 is sent again as the same POST, body included. Any other redirect status is sent again as GET with no body. Following stops after two redirects. A followed address has to be public https, or the delivery fails. When no forward URL is set, the response is JSON with status and ref. On a successful response, and if the page does not cancel the event, the snippet replaces the form with “Thanks — your message was received.” It does not also post to the form action.

The form action is the address used only for that two-failure fallback. It can be the same https URL saved as the forward address. This page does not claim that a handler accepts the POST above.