Skip to content
Antisy
Documentation · Quick start

Quick start: protect a form and verify its token

Antisy checks that a form submission comes from a browser and not from a program. You put a tag in the page, the script puts a token in the form when it is sent, and your server has it verified, server side, with one call.

State of the service

This documentation describes version 1 of the API. Access is for now by waiting list; the API answers on api.antisy.com, in service since 27 October 2026, hosted at Infomaniak in Geneva. Content checked against the code of the API on 4 October 2026. The dashboard is in French.

The principle

  1. The browser loads the Antisy script, served by api.antisy.com. It shows nothing to the visitor.
  2. When a POST form is sent to your site, the script holds the submission while it obtains a token, puts it in a hidden field, antisy-token, then resumes the submission. You write no JavaScript for this case.
  3. Your server receives the form with the token. It sends it to POST /v1/siteverify, with your secret.
  4. The response says allow or deny: the visitor gets through, or not. You decide what follows.

The browser never talks to /v1/siteverify: it is a route for your server, which sends no CORS header. The secret never goes in a page.

What you need

  • A site declared in the dashboard (in French), with the host names of your pages (see host names).
  • Its site key (32 characters): it goes in the page, it is not a secret.
  • A verification secret (64 characters): it stays on your server, in an environment variable. It is shown only once, when it is created (page on keys).
  • Pages served over HTTPS.

A complete example

1. The page

The tag in the <head> and an ordinary form: no hidden field and no script on your side. Replace VOTRE_CLE_DE_SITE with your site key.

page.html
<script src="https://api.antisy.com/v1/i/l.js"
        data-site="VOTRE_CLE_DE_SITE" defer></script>

<form method="post" action="/contact">
  <input name="name" autocomplete="name">
  <input name="email" type="email" autocomplete="email">
  <textarea name="message"></textarea>
  <button>Envoyer</button>
</form>

On submission, the Antisy script holds the submission while it obtains the token (6 seconds at most), creates the antisy-token field, fills it in and sends the form again. If the script is blocked or does not answer, the form is sent anyway, without the antisy-token field: your server reads it with an empty default value, as the Node example below does. The page on the script says what happens then. This example covers POST forms sent to your own site. For a submission by fetch, a form sent to a subdomain or a target in another window, see which forms are covered.

2. Server-side verification, with curl

The secret and the token are in the body of the request, never in a header; the secret goes through standard input, not as an argument. The token is checked before the call. A deadline of 3 seconds; if Antisy does not answer, the submission goes through.

verifier.sh
#!/bin/sh
# ANTISY_SECRET: your verification secret, in the environment, never hard-coded.
# ANTISY_TOKEN:  the antisy-token field received with the form.
# Exit 0: accepted. Exit 2: refused, or submission not verified.
LC_ALL=C

# 1. A token that is there must have the shape of a token: 1 to 2048 printable ASCII characters.
#    Otherwise Antisy is not called and nothing is let through.
if [ -n "$ANTISY_TOKEN" ]; then
  case "$ANTISY_TOKEN" in
    *[!\ -~]*) echo "Jeton mal formé, envoi non vérifié" >&2; exit 2 ;;
  esac
  if [ "$(( $(printf '%s' "$ANTISY_TOKEN" | wc -c) ))" -gt 2048 ]; then
    echo "Jeton trop long, envoi non vérifié" >&2; exit 2
  fi
fi

# 2. The call. The secret goes through standard input: it does not appear in the list of processes.
body=$(printf 'secret=%s' "$ANTISY_SECRET" | curl -sS --max-time 3 -w '\n%{http_code}' \
  https://api.antisy.com/v1/siteverify \
  -d @- \
  --data-urlencode response="$ANTISY_TOKEN")
curl_status=$?
http_code=$(printf '%s' "$body" | tail -n1)

# 3. Let through only when Antisy does not answer: network failure, deadline exceeded, 5xx.
#    The 429 too, by default: see the page on verification before keeping this choice.
if [ "$curl_status" -ne 0 ] || [ "$http_code" -ge 500 ] || [ "$http_code" = 429 ]; then
  echo "Antisy ne répond pas (curl $curl_status, HTTP $http_code), envoi laissé passer" >&2
  exit 0
fi
# Any other error status is a fault of your call: to be fixed, and never a pass.
if [ "$http_code" -ge 400 ]; then
  echo "Requête refusée par Antisy (HTTP $http_code), envoi non vérifié" >&2
  exit 2
fi

# 4. Compact JSON response: grep is enough here, use a real JSON parser in your code.
if printf '%s' "$body" | grep -q '"success":true'; then
  exit 0   # visitor accepted
else
  exit 2   # deny: generic refusal, without saying why
fi

3. Server-side verification, with Node

The same with fetch, without a dependency. The function returns allow or deny; the page on verification explains each case, including the 429, whose default handling is a choice that is yours.

antisy.mjs
// antisy.mjs: Node 22 or later, no dependency.
// Returns "allow" or "deny".
// Antisy does not answer (network, deadline, 5xx): "allow". Everything else that is uncertain: "deny".

// The 429 is a decision that is yours. "allow" by default, as for an outage;
// for a sensitive form, set "deny" (see the page on verification).
const ON_RATE_LIMIT = "allow";

export async function antisyVerdict(token) {
  // 1. A token that is there must have the shape of a token: 1 to 2048 printable ASCII characters.
  //    Otherwise, Antisy is not called and nothing is let through.
  //    An empty or missing field (script blocked) is sent as an empty string: Antisy reads it as a missing token.
  if (token && !/^[\x20-\x7e]{1,2048}$/.test(token)) {
    console.warn("Jeton mal formé, envoi non vérifié");
    return "deny";
  }

  let response;
  try {
    response = await fetch("https://api.antisy.com/v1/siteverify", {
      method: "POST",
      // fetch follows redirects by default: a 3xx would replay the body, secret included, elsewhere.
      // Here it is an error of your call (refused below).
      redirect: "manual",
      // A URLSearchParams body is sent as application/x-www-form-urlencoded.
      body: new URLSearchParams({
        secret: process.env.ANTISY_SECRET, // your secret, never in the page
        response: token ?? "",             // the antisy-token field, as received
      }),
      signal: AbortSignal.timeout(3000),   // beyond 3 s, Antisy does not answer
    });
  } catch (error) {
    console.warn("Antisy injoignable, envoi laissé passer :", error.name);
    return "allow";
  }
  if (response.status >= 500) {
    console.warn("Antisy a répondu " + response.status + ", envoi laissé passer");
    return "allow";
  }
  if (response.status === 429) {
    console.warn("Antisy a répondu 429, décision : " + ON_RATE_LIMIT);
    return ON_RATE_LIMIT;
  }
  if (!response.ok) {
    // 400, 401, 413, 415...: the fault is in your call. To be fixed, never a pass.
    console.error("Antisy a refusé la requête (" + response.status + "), envoi non vérifié");
    return "deny";
  }
  const result = await response.json().catch(() => null);
  if (typeof result?.success !== "boolean") {
    console.error("Réponse d'Antisy illisible, envoi non vérifié");
    return "deny";
  }
  // A missing or refused secret is not an HTTP error: the response is a refusal, with a 200.
  const codes = result["error-codes"] ?? [];
  if (codes.includes("missing-input-secret") || codes.includes("invalid-input-secret")) {
    console.error("Antisy refuse le secret : " + codes.join(", "));
  }
  // success is the only acceptance: it is true if and only if verdict is "allow".
  return result.success ? "allow" : "deny";
}

In your form handler
import { antisyVerdict } from "./antisy.mjs";

// In the handler of your form, after parsing the body:
const verdict = await antisyVerdict(body["antisy-token"]);
if (verdict === "deny") return refuse(); // generic refusal, without saying why
return accept();

PHP, Python and Go have their examples on the page verify a token. For the whole path, from creating the site to going into production, follow the step-by-step guide.

What to do with the verdict

  • allow: the visitor is accepted. Save or publish the message.
  • deny: the visitor does not get through. Refuse, without saying why.

There are only these two outcomes, and the response never says why: no reason, no score, no threshold, in any mode. Each reason returned would teach whoever gets around the check what stopped them. The visitor sees none of this.

A token is valid for 60 seconds and can be verified only once. An Antisy outage must never block your visitors: the two examples above let the submission through, and log it, only when Antisy does not answer (network, deadline, 5xx error). An error caused by your request never lets anything through.

How strictly to refuse

Each site has a verification mode, set in the Paramètres (settings) tab of the dashboard, which is in French. It only changes the point at which a doubtful visitor does not get through; there is no slider and no value to enter.

Strict
Refuses earlier, at the risk of inconveniencing a few real visitors.
Balanced
The default setting: a compromise between the other two.
Permissive
Refuses only clear-cut cases, at the risk of letting more bots through.

In every mode, some cases never get through: the list is on the page on verification.

What this path covers

The call described here suits low-stakes forms: contact, newsletter. It does not tie the token to an account; so it does not suit a login or a sign-up. For those, the Node SDK (Express, Fastify, Next.js) will be published on npm with its documentation; it is not published yet.

The pages login and sign-up say what that other call checks and where it stands; the frequently asked questions sum up the rest.

Guides by platform

The same path, wired into a PHP site, a Next.js application or a WordPress site:

What comes next

  • Step-by-step guideFrom creating the site to a protected form: eight steps, what you should see at each one, and what to do if a real visitor is refused.
  • The browser scriptThe tag, the forms covered, antisy.token(), the CSP policy, the host names, the browsers, if the script is blocked.
  • Verify a tokenThe request, the response, the durations, the error codes, when the API does not answer, the PHP, Python and Go examples, the real outcome.
  • Keys and test environmentsSite key, secret, creation and revocation, test keys and their safeguard.
  • Data collected and privacyWhat the browser sends, what Antisy keeps, and for how long.