Pular para o conteúdo

Coloque um captcha em qualquer formulário

O Qlyne Verify barra robôs nos seus formulários sem quebra-cabeça e sem o conector. O navegador resolve um proof-of-work curto (cerca de um segundo num celular), e o formulário leva um token que o seu servidor confere com o Qlyne antes de aceitar. Funciona em qualquer hospedagem: Vercel, Netlify, servidor próprio.

O identificador do site está no menu do site no painel, e lá também estão estas duas linhas já preenchidas (Bots, “Captcha for any form”):

<script src="https://wait.qlyne.com/src/verify.js" async></script>
<form method="post" action="/contato">
<!-- seus campos -->
<div class="qlyne-verify" data-site="id_do_seu_site" data-core-url="https://api.qlyne.com"></div>
<button>Enviar</button>
</form>

A caixa mostra “Conferindo o navegador…” e depois “Navegador conferido”. O token vai num campo escondido chamado qlyne-token, dentro do formulário.

Opções no div:

AtributoO que faz
data-themelight ou dark; por padrão segue o sistema do visitante
data-fieldoutro nome para o campo escondido
data-callbacknome de uma função global que recebe o token

A página também recebe o evento qlyne:verified, com o token em event.detail.token.

Em Bots, “Challenge”, a opção “Click to confirm” faz a caixa pedir um clique em “Sou uma pessoa”: sempre, ou só quando o visitante parece robô (o widget manda as mesmas contagens da nota de robô, nunca teclas ou texto). O token vem depois do clique.

Cadastre o endereço do seu site em Site settings, “Allowed origins”, para só as suas páginas pedirem desafios. Vazio libera qualquer site.

Antes de aceitar o formulário, mande o token ao Qlyne com a chave de API da conta (Plan and API key). Guarde a chave no servidor: quem a lê no navegador confere tokens em seu nome.

Janela do terminal
curl -X POST "https://api.qlyne.com/verify/check?tenant_id=id_do_seu_site" \
-H "Authorization: Bearer $QLYNE_API_KEY" -H "Content-Type: application/json" \
-d '{"token": "valor de qlyne-token", "remoteip": "IP do visitante"}'

Respostas:

RespostaSignificado
{"success": true, "solved_at": 1791300000, "origin": "https://loja.exemplo"}Aceite o formulário
{"success": false, "error": "invalid_or_expired"}Ausente, errado, já usado ou com mais de 5 minutos
{"success": false, "error": "ip_mismatch"}Resolvido de outro IP que não o de remoteip
HTTP 401Chave de API errada

O remoteip é opcional. Mande quando o seu servidor souber o IP real do visitante; atrás de um proxy, use o endereço que o proxy repassa.

app/api/contato/route.js
export async function POST(request) {
const form = await request.formData();
const res = await fetch(`${process.env.QLYNE_CORE_URL}/verify/check?tenant_id=${process.env.QLYNE_SITE}`, {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.QLYNE_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ token: form.get('qlyne-token') }),
});
const check = await res.json();
if (!check.success) return Response.json({ error: 'Tente de novo.' }, { status: 400 });
// ...trate o formulário
return Response.json({ ok: true });
}
$check = Http::withToken(env('QLYNE_API_KEY'))
->post(env('QLYNE_CORE_URL').'/verify/check?tenant_id='.env('QLYNE_SITE'), [
'token' => $request->input('qlyne-token'),
'remoteip' => $request->ip(),
])->json();
abort_unless($check['success'] ?? false, 422, 'Tente de novo.');
import os, requests
check = requests.post(
f"{os.environ['QLYNE_CORE_URL']}/verify/check",
params={"tenant_id": os.environ["QLYNE_SITE"]},
headers={"Authorization": f"Bearer {os.environ['QLYNE_API_KEY']}"},
json={"token": form["qlyne-token"]},
timeout=5,
).json()
if not check.get("success"):
raise ValueError("captcha")

O token vale uma vez. Se a página envia o formulário com fetch e continua aberta, peça um token novo depois de cada envio:

QlyneVerify.reset(document.querySelector('.qlyne-verify'));

O widget também renova o token sozinho antes de vencer, então um formulário aberto há um tempo continua valendo.

Se o seu site manda o cabeçalho Content-Security-Policy, libere o script e as chamadas ao Qlyne:

script-src 'self' https://wait.qlyne.com; connect-src 'self' https://api.qlyne.com

O widget não precisa de mais nada: o estilo dele não conta como estilo embutido, e se a política não deixa criar worker a partir de blob:, ele resolve o desafio na própria página. Acrescentar worker-src blob: deixa ele resolver em segundo plano, o que mantém um celular lento respondendo.

Em Bots, no painel: captchas resolvidos, navegadores automatizados recusados (o widget percebe navegador comandado por Selenium, Puppeteer ou Playwright) e tokens que o seu servidor recusou. Nenhum dado pessoal fica guardado: o token leva a hora, a origem e o IP do visitante (o /64 no IPv6) por 5 minutos, só para comparar com o remoteip.