Verify a token server side: POST /v1/siteverify
Your server receives the form with the antisy-token field, which the script filled in, and has it verified server side with a single call to siteverify. The response says whether the visitor is accepted.
The siteverify request
POST https://api.antisy.com/v1/siteverify, with a application/x-www-form-urlencoded or application/json body.
- secret
- Required. Your verification secret (64 alphanumeric characters), in the body and nowhere else.
- response
The token, as it arrived in the
antisy-tokenfield (filled in by the script) or thex-antisy-tokenheader (set by your page). A string of 1 to 2048 printable ASCII characters. Empty or missing, it is read as a missing token.Check it on your side before the call. Outside that shape, the API treats it as a forged token: 200,
successatfalse,verdictatdeny,invalid-input-response.- idempotency_key
- Optional. A UUID; any other value is refused. See below.
- remoteip
- Do not send it. It is ignored, whatever its value, and never refused: remove it.
- Do not send an
Authorizationheader. Its presence switches the request to another mode, the SDK’s, where the secret is not a valid credential: a401response, in a body of another shape. A secret sent asAuthorization: Beareris refused in the same way. - Call the route from your server, never from a browser: it sends no CORS header, a browser cannot read the response.
The response
{
"success": true,
"verdict": "allow",
"flags": [],
"error-codes": [],
"challenge_ts": "2026-09-24T10:00:00Z",
"hostname": "shop.example",
"action": "contact",
"event_id": "evt_01J7X4M2Q9K8ZP3R5T6V7W8Y9A"
}- success
trueif and only if the visitor is accepted. It is the field to read if you want only a boolean.- verdict
allowordeny, and nothing else.successistrueif and only ifverdictisallow: the two say the same thing.- flags
- The list of states that are not errors. It is open: ignore the ones you do not know (see below).
- error-codes
- The list of error codes, empty when nothing is wrong (see below).
- challenge_ts
- The time the token was deposited, in ISO 8601 format in UTC, on an
allowthat has a deposit behind it. Anallowwithout a deposit carries it atnull, for example: collection off and token missing, site or organisation suspended, test token, a deposit our database could not write (withinternal-error). So do not make it a requirement for accepting a visitor.nulltoo on any refusal, whatever its cause. - hostname
- On an
allow, the host name of the page that asked for the token, when its origin is attested.nullotherwise, and on any refusal. - action
- On an
allow, the label passed totoken()or set bydata-antisy-actionon the form, ornullif there was none.nulltoo on any refusal. - event_id
- The identifier of this verification, of the form
evt_followed by 26 characters. Quote it if you write to support (support@antisy.com) about a case.
The response contains no score, no threshold and no reason, in any mode: a verdict, never the detail that produced it. Each reason returned would teach whoever gets around the check what stopped them, and they stay with Antisy. The error codes say what your integration must fix, not what the detection saw. A decoy and a submission refused for what its making proves receive the same response as any refusal. It carries Cache-Control: no-store and the X-Antisy-Api-Version: 1 header.
The flags
test_key- The verification was made with a test secret.
over_quota- The monthly quota of the Free tier is exceeded: the score no longer decides the response, but the gate stays (a missing token while the collection of the site is on is refused, as is a token that is invalid, forged, replayed, expired, from another site or whose computation did not finish in time, and a visitor on your block list too). A state of your account, to be logged.
degraded- A source of Antisy was not available for this verification: the response is given without it. Log it (see internal-error).
idempotent_replay- A replayed response: the same idempotency_key within 60 seconds.
Fields, flags and codes can be added without a change of version. A break in compatibility would create a /v2/ route: read what you know and ignore the rest.
What to do with each verdict
allowsuccessattrue. Publish or save the message.denysuccessatfalse. The visitor does not get through: a generic refusal, without revealing the reason. The visitor learns nothing of what happened; only your server knows.
There are only these two outcomes. If you want moderation or a confirmation by e-mail, you decide it, on your own signals. How strictly Antisy refuses is a setting of your site, in the dashboard (in French): Strict, Balanced (the default) or Permissive, described in the quick start.
This call suits low-stakes forms. It does not tie the token to an account: do not use it for a login or a sign-up. The Node SDK (Express, Fastify, Next.js) will be published on npm with its documentation; it is not published yet.
What never gets through
Whatever the verification mode of your site, these cases are refused: success at false, verdict at deny.
- A missing token, on a site whose browser collection is on (
missing-input-response). On a site whose collection is off, the absence is not held against it: a new site starts with collection off. - A token that is invented, unreadable, of a shape outside the limits, or issued for another of your sites (
invalid-input-response). - A token already verified, or expired, even by a few seconds (
timeout-or-duplicate). - A submission whose making is proven: a program made it rather than the browser. The response carries no error code and does not say what gave it away.
- A token whose background computation did not finish in time (a very slow device, a hidden tab, a
totalTimeoutMsdeadline too short): it leaves without its solution. It is not a proof of making, but it is refused all the same, without an error code; a new attempt does the computation again with a fresh token (the deadline of the computation). - A missing or invalid token is not rescued by an allow list, when Antisy has set one for your site, at your written request to support: it exempts from the assessment, not from the token.
This rule also holds beyond the monthly quota of the Free tier. The score then no longer decides the response: a submission that is none of these gets through, with the over_quota flag. But the gate stays closed: a token that is missing (collection on), invalid, forged, replayed, expired or from another site, or whose computation did not finish in time, receives success at false as under the quota, with the same error codes and over_quota in flags. So the flag changes nothing in your code: it is a state of your account, to be logged. Your submissions without a token or with a fake token count, as before, towards the quota. A block list set by Antisy for your site, at your written request to support (the dashboard does not manage these lists), holds too: a blocked visitor receives success at false beyond the quota, with or without a token; an allow list, for its part, does not exempt from the token.
Validity and single use
- A token is valid for 60 seconds after it is deposited. Past that delay, the verification returns
successatfalseandtimeout-or-duplicate, whatever the rest. - It can be verified only once. A second call with the same token returns
timeout-or-duplicate.
Replaying a call: idempotency_key
If your call may have succeeded but you did not read the response (a cut, a timeout), sending it again without care would give timeout-or-duplicate. Send an idempotency_key (a UUID): a new call with the same key within 60 seconds replays the same verdict and the same event_id, with the idempotent_replay flag, without verifying the token again. The error codes of the first response are not repeated: error-codes is empty, and flags carries only idempotent_replay, plus over_quota if the first response carried it. This replay assumes that the first response was kept: that is never the case with a test secret, nor when the first call failed before keeping it. The token is then assessed again without being counted again: a new event_id, without idempotent_replay, and a verdict that may differ from the first.
Your server generates the key: a fresh UUID per submission, reused only to retry that call. Never read it from a field of the form, a header or the visitor’s request: a key the visitor chooses would let them replay the response of a submission already accepted, and get through again with the same token for 60 seconds.
# One key per token: created once, then reused as it is on each new attempt.
KEY=$(uuidgen)
for tentative in 1 2 3; do
printf 'secret=%s' "$ANTISY_SECRET" | curl -sS --max-time 3 https://api.antisy.com/v1/siteverify \
-d @- \
--data-urlencode response="$ANTISY_TOKEN" \
-d idempotency_key="$KEY" && break
sleep 1
doneError codes
The first five codes come with a 200 response that carries a verdict. So a missing or refused secret is not an HTTP error: it is a refusal (deny), so that the route cannot be used to test secrets and a configuration error shows from the first attempt, without leaving your site open. The last three are error statuses, in a body without a verdict:
{ "success": false, "error-codes": ["rate-limited"] }missing-input-secretStatus 200The secret field is missing from the body of the request. It is not an HTTP error: the response is a refusal (success at false, verdict deny), so that a configuration error shows from the first attempt.
Fix the call. As long as it is wrong, every visitor is refused: log it as an error.
invalid-input-secretStatus 200The secret is unknown, revoked, malformed or of another type (an evaluation key is not a verification secret), or the site is closed. The same response as for a missing secret: the route never says more.
Check the key in the “Clés” (Keys) tab of the site. The same effect as the previous one: every visitor is refused.
missing-input-responseStatus 200The response field is empty, on a site whose browser collection is on. The visitor is refused, whatever the verification mode of the site.
The script did not run, or the form is not covered by the script and the page did not call token(). See the page on the browser script.
invalid-input-responseStatus 200The token is not valid for this site: invented, altered, issued for another of your sites, or of a shape outside the limits (more than 2,048 characters, non-printable characters). The visitor is refused.
Check that you pass the token on as it arrived, without truncating it, and that it does come from the page of this site.
timeout-or-duplicateStatus 200The token has already been verified, or it is more than 60 seconds old. The visitor is refused.
Verify a token only once. To replay a call after a cut, send idempotency_key. Otherwise, have the form sent again: the browser will produce a new token.
bad-requestStatus 400, 405, 415A request malformed by your call: unreadable body, content type or character set not accepted, compressed body, unknown field, idempotency_key that is not a UUID, a method other than POST. The body carries no verdict.
The fault is in your call. Log it as a bug. The submission has not been verified: do not let it through, refuse it.
rate-limitedStatus 429Too many calls on this lane. The Retry-After header gives the delay, in seconds. An attacker can cause this status by volume.
By default, let it through and log it. For a sensitive form, refuse: see “When the API does not answer”.
internal-errorStatus 500, 503An outage at Antisy, a database unavailable for example. The code can also come with an allow: at Antisy, this token could not be recorded.
Let the submission through and log it.
A method other than POST receives 405, in the error body shown above, with bad-request.
A suspended site or organisation: your secret then answers success at true and verdict at allow, with no flag and no error code, apart from test_key for a test secret. A suspension of the account does not break your forms, but nothing is verified in the meantime. A closed site, for its part, is refused like an unknown secret (invalid-input-secret).
When the API does not answer
Rule: let through only when Antisy does not answer
If you write “if success is false, refuse”, you will block all your forms during an Antisy outage. An outage on our side must not cost you your visitors. But “let through” holds only for a real absence of response: a robot that causes an error must not get a pass out of it.
- Check the token before the call. A token that is there is a string of 1 to 2048 printable ASCII characters. Otherwise, do not call Antisy: the submission is not verified, refuse it. An empty token, for its part, is sent as it is: Antisy knows whether the site expects a token.
- Let through, and log, only on a network error, a DNS failure, a timeout (3 seconds is the timeout we recommend) or a
5xxresponse. - Never let through on
400,401,413,415or any other 4xx status apart from the429. It is a fault of your call: log it as a bug to fix, and treat the submission as not verified: refuse it. - The 429 is a choice.
Retry-Aftergives the delay in seconds. With a test secret, a429can also mean that your test secrets have used up their 1,000 calls of the day, or those of the month (limits): it then lasts until the next midnight UTC, or until the first of the next UTC month. Our examples let through by default, as for an outage. But an attacker can cause a429by volume: the lane is limited per key, and a flood of submissions on your form reaches that limit, for all your visitors, as long as it lasts; its submissions get through too. For a sensitive form, refuse on a429(deny): it is the protective choice, at the price of refusing legitimate visitors during a saturation, who will try again afterRetry-After. The default behaviour is set by a constant in the Node example. A503that carriesRetry-Afteris load shedding: the instance has reached its capacity, and a third party can cause it by volume, like the429. The examples treat it as an outage; for a sensitive form, refuse on this503too, with the same value as for the429. - A missing or refused secret does not come under this rule: the response is a refusal with a
200(deny). Logmissing-input-secretandinvalid-input-secretas configuration errors: all your visitors are refused as long as they last, and nothing else breaks.
Letting through during an outage is a choice: robots can get through with it. It is the price of never blocking a visitor because of Antisy.
Server-side verification examples: PHP, Python, Go
The verification is an HTTP call: no library to install. Here is the same example in PHP, in Python and in Go, with the same rules as the curl and Node examples of the quick start.
What each example does
- It checks the token before the call. A token that is there is a string of 1 to 2048 printable ASCII characters; otherwise the example does not contact Antisy and refuses, as Antisy would. An empty token is sent as it is: Antisy knows whether the site expects a token.
- It calls
POST /v1/siteverify, with a form body that carriessecretandresponse, and a timeout of 3 seconds. The secret comes from theANTISY_SECRETenvironment variable: it is never hard-coded, never a command-line argument, never in a header. - It reads only
success:trueis the only acceptance. An unreadable response is a submission not verified, so refused. - It lets through only when Antisy does not answer: network error, timeout,
5xxresponse. It logs each time. - It never lets through on an error of your call:
400,401,413,415and any other4xx, nor on a3xxredirect, which it does not follow: a redirect would replay the body, secret included, to another address.
The 429 is a choice that is yours: a constant at the top of each example sets it, allow by default. For a sensitive form, set deny; the section when the API does not answer says why. The examples treat any 5xx as an outage; to refuse on a 503 that carries Retry-After too, handle it before the case of the 5xx, with the same value.
The examples are those of the API’s guide. Antisy’s tests run them against a fake local server, on the same cases: response accepted, refused, outage, 429, error of your call, unreadable response, timeout and redirects.
PHP
With PHP’s curl extension (curl_init) and PHP 8 or later: the signature of the function uses the mixed type.
<?php
/**
* Vérifie le jeton Antisy (mode simple). Rend le verdict : "allow" ou "deny".
*
* - jeton vide ou absent : envoyé tel quel, Antisy décide ; jeton non vide inutilisable : "deny", sans appel ;
* - Antisy ne répond pas (réseau, délai, 5xx) : "allow" et on journalise (fail-open) ;
* - 429 : ANTISY_ON_RATE_LIMIT ;
* - tout autre statut que 200 (400, 401, 413, 415...) : erreur de VOTRE appel, journalisée,
* tentative non vérifiée : "deny". Jamais "allow".
*/
const ANTISY_ON_RATE_LIMIT = 'allow'; // 'deny' for a sensitive form: an attacker can cause a 429
function antisy_verdict(mixed $token): string
{
if ($token === null) {
$token = ''; // missing field: treated as empty
}
// Empty: sent as it is, Antisy decides (collection on: deny; off: the engine decides).
// Not empty: 1 to 2,048 printable ASCII characters; /D: “$” does not tolerate a final line break.
if ($token !== '' && (!is_string($token) || !preg_match('/^[\x20-\x7e]{1,2048}$/D', $token))) {
return 'deny'; // a form array, a token too long or crafted: what Antisy would answer
}
$ch = curl_init('https://api.antisy.com/v1/siteverify');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 3, // seconds; beyond that, fail-open
CURLOPT_FOLLOWLOCATION => false, // no redirect followed (the default, written so that no one turns it on): a 3xx is an error of YOUR call
CURLOPT_POSTFIELDS => http_build_query([
'secret' => getenv('ANTISY_SECRET'), // siteverify secret; never hard-coded nor on the client side
'response' => $token,
]),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$error = curl_error($ch);
curl_close($ch);
// The only case where the submission is let through: Antisy does not answer.
if ($body === false || $status >= 500) {
error_log("Antisy injoignable, envoi laissé passer (fail-open): {$error} (HTTP {$status})");
return 'allow';
}
if ($status === 429) {
error_log('Antisy 429 (voie saturée): ' . ANTISY_ON_RATE_LIMIT);
return ANTISY_ON_RATE_LIMIT;
}
if ($status !== 200) {
error_log("Antisy HTTP {$status} : erreur de l'appel, à corriger ; tentative non vérifiée");
return 'deny';
}
$result = json_decode($body, true);
if (is_array($result) && is_bool($result['success'] ?? null)) {
if ($result['success']) {
return 'allow'; // the only acceptance
}
return 'deny'; // success false: refusal (the verdict is “deny”)
}
error_log('Réponse Antisy illisible, tentative non vérifiée');
return 'deny';
}// In the handler of your contact form:
$verdict = antisy_verdict($_POST['antisy-token'] ?? '');
if ($verdict === 'allow') {
// publish / save the message
} else {
// deny: generic refusal, without saying why
}Python
Standard library only. urllib follows a 301, 302 or 303 redirect by default: the example installs an opener that follows none.
import http.client
import json
import logging
import os
import re
import urllib.error
import urllib.parse
import urllib.request
API = "https://api.antisy.com/v1/siteverify"
# 429: "allow" (the default, as in the Node kit); "deny" for a sensitive form, because an
# attacker can cause a 429 by volume.
ON_RATE_LIMIT = "allow"
# 1 to 2,048 printable ASCII characters; `fullmatch` (and not `$`, which tolerates a final line break).
USABLE_TOKEN = re.compile(r"[\x20-\x7e]{1,2048}")
class _NoRedirect(urllib.request.HTTPRedirectHandler):
"""Aucun suivi de redirection : urllib suit un 301, 302 ou 303 par défaut ; la réponse 3xx est alors une erreur de VOTRE appel."""
def redirect_request(self, *args, **kwargs):
return None
_OPENER = urllib.request.build_opener(_NoRedirect)
def antisy_verdict(token) -> str:
"""Rend le verdict Antisy : "allow" ou "deny".
- jeton vide ou absent : envoyé tel quel, Antisy décide ; jeton non vide inutilisable : "deny", sans appel ;
- Antisy ne répond pas (réseau, délai, 5xx) : "allow" et on journalise (fail-open) ;
- 429 : ON_RATE_LIMIT ;
- tout autre statut que 200 (400, 401, 413, 415...) : erreur de VOTRE appel, journalisée,
tentative non vérifiée : "deny". Jamais "allow".
"""
if token is None:
token = "" # missing field: treated as empty
# Empty: sent as it is, Antisy decides (collection on: deny; off: the engine decides).
if token != "" and (not isinstance(token, str) or USABLE_TOKEN.fullmatch(token) is None):
return "deny" # too long, crafted, or not a string: what Antisy would answer
data = urllib.parse.urlencode(
{
"secret": os.environ["ANTISY_SECRET"], # siteverify secret; never on the client side
"response": token,
}
).encode()
try:
with _OPENER.open(urllib.request.Request(API, data=data), timeout=3) as response:
body = response.read()
except urllib.error.HTTPError as error:
if error.code >= 500:
logging.warning("Antisy HTTP %s, envoi laissé passer (fail-open)", error.code)
return "allow"
if error.code == 429:
logging.warning("Antisy 429 (voie saturée) : %s", ON_RATE_LIMIT)
return ON_RATE_LIMIT
logging.error("Antisy HTTP %s : erreur de l'appel, à corriger ; tentative non vérifiée", error.code)
return "deny"
except (urllib.error.URLError, http.client.HTTPException, OSError) as error:
# Network, deadline, DNS, RemoteDisconnected, ConnectionResetError, IncompleteRead: Antisy did
# not answer (URLError, TimeoutError and ConnectionError are OSErrors; they are named for
# the reader). It is the only case, with the 5xx, where the submission is let through.
logging.warning("Antisy injoignable (%s), envoi laissé passer (fail-open)", error)
return "allow"
try:
result = json.loads(body)
success = result.get("success")
except (ValueError, AttributeError):
success = None
if success is True:
return "allow" # the only acceptance
if success is False:
return "deny"
logging.error("Réponse Antisy illisible, tentative non vérifiée")
return "deny"# In the handler of your contact form:
verdict = antisy_verdict(form.get("antisy-token", ""))
if verdict == "allow":
pass # publish / save the message
else:
pass # deny: generic refusal, without saying whyGo
Standard library only. The HTTP client is your own, with a timeout of 3 seconds: that of http.DefaultClient is infinite. The check of the token is a loop, because Go’s regular expressions refuse a repetition count above 1,000.
// Package antisy verifies an Antisy token in simple mode: Verdict returns "allow" or "deny". The same rules as the examples above.
package antisy
import (
"encoding/json"
"io"
"log"
"net/http"
"net/url"
"os"
"time"
)
const apiURL = "https://api.antisy.com/v1/siteverify"
// 429: "allow" (the default, as in the Node kit); "deny" for a sensitive form, because an
// attacker can cause a 429 by volume.
const onRateLimit = "allow"
// 1 to 2,048 printable ASCII characters (0x20 to 0x7E). A loop and not a regular expression: those of Go refuse a
// repetition count above 1,000 (`{1,2048}` does not compile), and a final line break does not get through either.
func usableToken(token string) bool {
if len(token) < 1 || len(token) > 2048 {
return false
}
for i := 0; i < len(token); i++ {
if token[i] < 0x20 || token[i] > 0x7e {
return false
}
}
return true
}
// A client of its own, with a deadline of 3 s (that of http.DefaultClient is infinite); beyond it, fail-open.
// No redirect followed: a 307 would replay the body, secret included, to another address.
var client = &http.Client{
Timeout: 3 * time.Second,
CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse },
}
// Verdict returns the Antisy verdict: "allow" or "deny".
//
// - empty or missing token: sent as it is, Antisy decides; unusable non-empty token: "deny", without a call;
// - Antisy does not answer (network, deadline, response cut off, 5xx): "allow", and it is logged (fail-open);
// - 429: onRateLimit;
// - any status other than 200 (400, 401, 413, 415...): an error of YOUR call, logged,
// attempt not verified: "deny". Never "allow".
func Verdict(token string) string {
// Empty: sent as it is, Antisy decides (collection on: deny; off: the engine decides).
if token != "" && !usableToken(token) {
return "deny" // too long or crafted: what Antisy would answer
}
response, err := client.PostForm(apiURL, url.Values{
"secret": {os.Getenv("ANTISY_SECRET")}, // siteverify secret; never hard-coded nor on the client side
"response": {token},
})
if err != nil {
// Network, DNS, deadline exceeded: Antisy did not answer. The only case, with the 5xx, where the submission is let through.
// err may quote the URL of the call, never the body: the secret is not in the log.
log.Printf("antisy injoignable, envoi laissé passer (fail-open): %v", err)
return "allow"
}
defer response.Body.Close()
body, readErr := io.ReadAll(io.LimitReader(response.Body, 1<<20))
switch {
case response.StatusCode >= 500:
log.Printf("antisy HTTP %d, envoi laissé passer (fail-open)", response.StatusCode)
return "allow"
case response.StatusCode == http.StatusTooManyRequests:
log.Printf("antisy 429 (voie saturée) : %s", onRateLimit)
return onRateLimit
case response.StatusCode != http.StatusOK:
log.Printf("antisy HTTP %d : erreur de l'appel, à corriger ; tentative non vérifiée", response.StatusCode)
return "deny" // 400, 401, 413, 415... never “allow”
}
if readErr != nil {
log.Printf("antisy : réponse coupée (%v), envoi laissé passer (fail-open)", readErr)
return "allow" // Antisy did not finish answering: it is an outage, not a refusal
}
var result struct {
Success *bool `json:"success"`
}
if json.Unmarshal(body, &result) != nil || result.Success == nil {
log.Printf("antisy : réponse illisible, tentative non vérifiée")
return "deny"
}
if *result.Success {
return "allow" // the only acceptance
}
return "deny"
}// In the handler of your contact form (net/http):
func contact(w http.ResponseWriter, r *http.Request) {
// PostFormValue returns "" when the field is missing: treated as empty.
if antisy.Verdict(r.PostFormValue("antisy-token")) != "allow" {
http.Error(w, "refusé", http.StatusForbidden) // deny: generic refusal, without saying why
return
}
// publish / save the message
}Another language
Copy what the examples do, not their syntax.
- The body is a form (
application/x-www-form-urlencoded) that carriessecretandresponse, or a JSON object with the same fields. - No
Authorizationheader: its presence switches the request to another mode (the request). - No redirect followed, and a timeout of 3 seconds.
- The same cases, in the same order: the token checked, the call, the outage that lets through, the error of your call that refuses,
successthat decides.
Login and sign-up
These examples suit low-stakes forms. They do not tie the token to an account: do not use them for a login or a sign-up. The Node SDK is not published yet; the step-by-step guide says what this documentation covers.
Reporting the real outcome of an attempt (optional)
Antisy measures its rules against what really happened: a sign-up whose address is confirmed, a fraud you have established. Without the Node kit, you say so with the same secret as for the verification, in one call. It is measurement, never a decision: the signal produces no verdict, changes nothing live, and the response carries none. Not sending it costs your protection nothing.
POST https://api.antisy.com/v1/feedback, without an Authorization header (its presence switches to the SDK’s mode, signed and encrypted), with a application/x-www-form-urlencoded or application/json body. Three fields, and nothing else: one field more is a 400 (bad-request).
- secret
- The same verification secret as for
/v1/siteverify, in the body and nowhere else. - event_id
- The
event_idthatsiteverifyreturned for this verification (evt_followed by 26 characters). Keep it with the object it describes, the row of the sign-up or of the message: it is the only way to report later. - outcome
success,failure,account_confirmed_fraudorfalse_positive. The first three describe the attempt;false_positivesays that Antisy’s verdict seemed wrong to you, and is counted apart. Send only what you know: an account whose address is confirmed, not an account merely created.
# ANTISY_SECRET: your verification secret, in the environment, never hard-coded.
# ANTISY_EVENT_ID: the event_id that siteverify returned for this verification.
# To run after answering your visitor. No -L: no redirect followed.
printf 'secret=%s' "$ANTISY_SECRET" | curl -sS --max-time 3 -w '\n%{http_code}\n' \
https://api.antisy.com/v1/feedback \
-d @- \
--data-urlencode event_id="$ANTISY_EVENT_ID" \
--data-urlencode outcome=success{"success":true,"error-codes":[]}
200The body of the response always has two keys: success and error-codes. Here success only says “report taken”, not a verdict; there is never a verdict, a score, a reason or an event_id. The statuses are real statuses, unlike siteverify: a 4xx is never to be retried, a 429 or a 5xx is, later, once or twice. No decision depends on the response: log it.
200error-codesempty: taken, or already taken identically (the same outcome for the same event: nothing is rewritten), or set aside without recording anything (test secret, site or organisation suspended).401missing-input-secretorinvalid-input-secret: secret missing; unknown, revoked, of a closed site, or an evaluation key (the same response for all). Fix the configuration.400missing-input-event- event_id missing: fix the call.
400invalid-input-event- event_id malformed, or an event that this secret cannot report (non-existent, from another site, born of another lane, or without a record). A single refusal for all these cases. Do not retry.
400missing-input-outcome- outcome missing: fix the call.
400invalid-input-outcome- outcome not in the list: fix the call.
409outcome-conflict- Another outcome has already been reported for this event. Do not retry.
410event-expired- The event is more than 30 days old. Do not retry.
422daily-limit-reached- Daily cap of this mode reached. Do not retry; resume tomorrow (UTC day) if the outcome matters to you.
400,405,413,415bad-request: unknown field (including a note or a date), a body that is unreadable or of more than 16 KiB, a method or content type not read.429,5xxrate-limitedwithRetry-After, orinternal-error(503withRetry-Afterin case of load shedding). Try again later.
The limits
- The event must be born of a
siteverifyverification in simple mode of this site, and be less than 30 days old. A single outcome per event. - Some
siteverifyresponses carry anevent_idthat designates no recorded event: secret refused, site suspended, test token, a body of more than 16 KiB, a Free site beyond its quota (over_quotainflags). Reporting them givesinvalid-input-event. An event of the signed lane cannot be reported here. - The mode has a daily cap of its own, per site (one twentieth of the verifications of the day, 20 at least; two hypotheses, to be reviewed during the pilot), counted apart from that of the signed kit. Beyond it:
422. The cap does not let the outcome of every verification through: send first what teaches the most,failureandaccount_confirmed_fraud, thensuccessif there is room left. - The secret is the one of
siteverify: a stolen secret would allow the measurement of your events of the last 30 days to be skewed, and the cap of the day of this mode to be used up, so your real outcomes to be blocked. It changes no verdict and reads nothing. Replacing it in the dashboard closes it. - Call the route from your server, after answering the visitor, with a timeout of 3 seconds and without following a redirect; a single retry, later, on a network error, a
429or a5xx.
Limits
- The route has its own rate caps, per address and per key. Beyond them:
429andRetry-After. This status can be caused by volume: see above. - Test secrets share 1,000 calls a day for the whole organisation, all sites together (UTC day). Beyond:
429rate-limited, with aRetry-Afterthat runs until the next midnight UTC. They also share, to date, 2,500 calls a month (UTC month): beyond, the same429, whoseRetry-Afterruns until the first of the next UTC month. See test keys. - A body of more than 16 KiB, or a
responseof more than 2048 characters, receives the response of a forged token. Do not send it: check the token before the call.