Documentazione Veris
Tutto ciò che serve per aggiungere la verifica umana di Veris al tuo bot Discord.
Panoramica
Veris è un servizio di verifica per server Discord. Il tuo bot chiede a Veris un link di verifica unico per un utente; l'utente lo apre, Veris controlla la connessione (VPN, proxy, Tor, datacenter, browser automatizzati) e gli fa risolvere un captcha Cloudflare Turnstile. Il risultato viene poi comunicato al tuo bot, che decide cosa fare (assegnare un ruolo, espellere, segnalare allo staff…).
https://veris.example.com (il Worker API; il sito e le pagine di verifica sono su un dominio separato). Ogni richiesta API richiede una API key, ottenibile tramite la pagina Richiedi accesso.Come funziona
- L'utente entra nel server e clicca il pulsante Verifica del tuo bot.
- Il bot chiama
POST /api/v1/sessionscon l'ID dell'utente e del server. - Veris risponde con un
urlunico (es.https://veris.example.com/v/Xk3…), monouso e con scadenza. - Il bot invia il link all'utente in un messaggio effimero (visibile solo a lui).
- L'utente apre il link: Veris controlla la connessione e mostra il captcha Turnstile.
- A verifica completata, Veris invia il risultato al bot tramite webhook firmato e lo rende disponibile nel feed eventi.
- Il bot applica le regole del proprietario del server (es. assegna il ruolo "Verificato").
Guida rapida
1. Ottieni la API key
Invia una richiesta da questa pagina. Dopo l'approvazione riceverai:
vrs_live_…— la API key (tienila segreta, solo lato server);whsec_…— il webhook secret, per verificare le firme dei webhook.
2. Crea una sessione quando l'utente clicca "Verifica"
curl -X POST https://veris.example.com/api/v1/sessions \
-H "Authorization: Bearer $VERIS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"discord_user_id": "123456789012345678",
"discord_guild_id": "987654321098765432",
"guild_name": "Il mio server"
}'
Risposta (201 Created):
{
"id": "ses_8fJ2kLmN4pQrStUvWxYz0123",
"object": "verification_session",
"status": "pending",
"discord": { "user_id": "123456789012345678", "guild_id": "987654321098765432" },
"url": "https://veris.example.com/v/Xk3Tq9…",
"expires_at": "2026-09-30T18:10:00.000Z",
...
}
3. Invia il link all'utente
Mandalo in un messaggio effimero o in DM, idealmente come pulsante-link. Non pubblicarlo mai in un canale pubblico: è personale.
4. Ricevi il risultato
Configura un webhook oppure interroga il feed eventi ogni pochi secondi. Quando ricevi verification.succeeded, assegna il ruolo.
Autenticazione
Ogni richiesta a /api/v1/* deve includere la API key nell'header Authorization:
Authorization: Bearer vrs_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Veris salva solo l'hash SHA-256 della chiave: nemmeno l'amministratore può recuperarla, solo rigenerarla.
Creare una sessione
| Campo | Tipo | Descrizione |
|---|---|---|
discord_user_id | string | Obbligatorio. ID Discord (snowflake) dell'utente da verificare. |
discord_guild_id | string | Obbligatorio. ID del server Discord. |
guild_name | string | Nome del server mostrato nella pagina di verifica (max 100). |
metadata | object | Dati liberi (max 1 KB) restituiti nel risultato, es. {"channel_id": "…"}. |
ttl_seconds | integer | Validità del link, 60–3600 secondi. Predefinito: 600. |
checks | object | Controlli da applicare, tutti true di default: {"vpn": true, "tor": true, "bot": true}. Il captcha Turnstile è sempre obbligatorio. |
Creare una nuova sessione per lo stesso utente nello stesso server invalida automaticamente i link precedenti non ancora usati (stato cancelled, motivo superseded).
L'url è restituito solo in questa risposta: il token del link non è salvato in chiaro e non può essere recuperato.
Ricevere i risultati
Hai due modi, utilizzabili anche insieme:
| Webhook | Feed eventi (polling) | |
|---|---|---|
| Come | Veris fa una POST al tuo endpoint HTTPS | Il bot chiama GET /api/v1/events ogni 2–5 s |
| Serve un server pubblico? | Sì (HTTPS con dominio) | No, funziona ovunque |
| Latenza | Istantanea | Pari all'intervallo di polling |
| Consigliato per | Bot su VPS/cloud con dominio | Bot su PC, hosting condiviso, Replit… |
Tipi di evento:
verification.succeeded— l'utente ha superato tutti i controlli;verification.failed— troppi tentativi bloccati (VPN, captcha fallito…);verification.expired— il link è scaduto senza essere completato.
Webhook
Imposta l'URL (deve essere HTTPS con un dominio pubblico):
curl -X PATCH https://veris.example.com/api/v1/me \
-H "Authorization: Bearer $VERIS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"webhook_url": "https://mio-bot.example.org/veris/webhook"}'
Prova la consegna con POST /api/v1/me/webhook/test (evento webhook.test).
Richiesta inviata da Veris
POST /veris/webhook HTTP/1.1
Content-Type: application/json
User-Agent: Veris-Webhook/1.0
X-Veris-Event: verification.succeeded
X-Veris-Delivery: evt_1042
X-Veris-Signature: t=1790791483,v1=5f1c0e…a9
{
"id": "evt_1042",
"type": "verification.succeeded",
"created_at": "2026-09-30T18:04:43.482Z",
"data": { ...oggetto sessione... }
}
Rispondi con un codice 2xx entro 5 secondi. In caso di errore di rete, timeout, 408, 429 o 5xx, Veris ritenta fino a 3 volte. Usa id per ignorare eventuali duplicati.
Verificare la firma
La firma è HMAC-SHA256(webhook_secret, "<t>.<corpo grezzo>") in esadecimale. Verificala sempre, sul corpo esatto ricevuto (prima del parsing JSON), e rifiuta timestamp più vecchi di 5 minuti per bloccare replay.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyVeris(secret, header, rawBody) {
const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header || '');
if (!m) return false;
if (Math.abs(Date.now() / 1000 - Number(m[1])) > 300) return false;
const expected = createHmac('sha256', secret).update(`${m[1]}.${rawBody}`).digest('hex');
return timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(m[2], 'hex'));
}
import hmac, hashlib, re, time
def verify_veris(secret: str, header: str, raw_body: bytes) -> bool:
m = re.fullmatch(r"t=(\d+),v1=([0-9a-f]{64})", header or "")
if not m or abs(time.time() - int(m.group(1))) > 300:
return False
msg = m.group(1).encode() + b"." + raw_body
expected = hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, m.group(2))
Il secret si può rigenerare con POST /api/v1/me/webhook/rotate (restituisce il nuovo webhook_secret; il vecchio smette subito di valere).
Feed eventi (polling)
Restituisce gli eventi con seq maggiore di after, in ordine. Salva l'ultimo seq elaborato (es. in un file o database) e passalo come after alla richiesta successiva: così non perdi eventi nemmeno se il bot si riavvia. Gli eventi restano disponibili per 7 giorni.
{
"object": "list",
"data": [
{
"id": "evt_1042",
"seq": 1042,
"type": "verification.succeeded",
"created_at": "2026-09-30T18:04:43.482Z",
"data": { ...oggetto sessione... }
}
],
"has_more": false,
"next_after": 1042
}
Una sola chiamata restituisce i risultati di tutti gli utenti e server del tuo bot: un polling ogni 3 secondi consuma 20 richieste al minuto.
Oggetto sessione
{
"id": "ses_8fJ2kLmN4pQrStUvWxYz0123",
"object": "verification_session",
"status": "verified",
"discord": { "user_id": "123456789012345678", "guild_id": "987654321098765432" },
"metadata": { "channel_id": "111111111111111111" },
"result": {
"passed": true,
"reason": null,
"country": "IT",
"linked_accounts": 0,
"attempts": 1
},
"last_block_reason": null,
"webhook_status": "delivered",
"created_at": "2026-09-30T18:00:00.000Z",
"expires_at": "2026-09-30T18:10:00.000Z",
"opened_at": "2026-09-30T18:00:12.000Z",
"completed_at": "2026-09-30T18:00:19.000Z"
}
| Campo | Descrizione |
|---|---|
status | pending, verified, failed, expired o cancelled. |
result | null finché la sessione è pending. |
result.passed | true solo se l'utente è verificato. |
result.reason | Motivo del fallimento (vedi motivi), expired, superseded o cancelled_by_client. |
result.country | Paese della connessione (ISO 3166-1 alpha-2). |
result.linked_accounts | Quanti altri account Discord si sono verificati con il tuo bot dalla stessa connessione negli ultimi 30 giorni. Utile per individuare account alternativi (alt). Un valore alto può anche indicare una rete condivisa (famiglia, scuola). |
result.attempts | Tentativi di verifica inviati. |
last_block_reason | Ultimo blocco incontrato dall'utente (anche se poi ha superato la verifica). |
webhook_status | pending, delivered, failed o null se non c'è webhook. |
Motivi di blocco
| Motivo | Significato | Cosa vede l'utente |
|---|---|---|
vpn | VPN rilevata (incl. Cloudflare WARP / iCloud Private Relay se attivo) | Invito a disattivare la VPN e riprovare |
proxy | Proxy o IP ad alto rischio (con intelligence IP esterna) | Invito a disattivare il proxy |
hosting | Connessione da datacenter/hosting (VPN, server, bot) | Invito a usare rete di casa o dati mobili |
tor | Exit node della rete Tor | Tor non consentito |
bot | Browser automatizzato, headless o client HTTP | Richiesta di usare un browser normale |
captcha_failed | Captcha Turnstile non valido | Nuovo tentativo |
Un blocco non chiude subito la sessione: l'utente può correggere il problema (es. spegnere la VPN) e riprovare. Dopo 5 tentativi falliti la sessione diventa failed e ricevi l'evento verification.failed.
Altri endpoint
Leggere una sessione
Restituisce l'oggetto sessione. Utile come controllo puntuale; per i risultati preferisci webhook o feed eventi.
Annullare una sessione
Invalida il link (es. l'utente ha lasciato il server). Restituisce 409 session_closed se non è più pending.
Il tuo account
Piano, limiti, utilizzo del mese, scadenza dell'abbonamento e webhook configurato.
Aggiorna webhook_url (usa null per rimuoverlo).
Errori
Gli errori hanno sempre questo formato:
{ "error": { "code": "invalid_request", "message": "\"discord_user_id\" deve essere un ID Discord (snowflake) come stringa." } }
| HTTP | Codice | Significato |
|---|---|---|
| 400 | invalid_request, invalid_json, invalid_webhook_url | Parametri non validi |
| 401 | unauthorized | API key mancante, errata o revocata |
| 402 | subscription_expired | Abbonamento scaduto: contatta l'amministratore |
| 403 | client_suspended | Accesso sospeso dall'amministratore |
| 404 | not_found | Risorsa inesistente |
| 409 | session_closed | La sessione non è più modificabile |
| 413 / 415 | payload_too_large, unsupported_media_type | Body troppo grande o non JSON |
| 429 | rate_limited, quota_exceeded | Limite al minuto o quota mensile superati (vedi header Retry-After) |
| 500 | internal_error | Errore interno: riprova con backoff |
Limiti e quote
- Richieste al minuto: dipende dal piano (vedi
GET /api/v1/me). Tutte le chiamate API contano. - Verifiche al mese: ogni
POST /api/v1/sessionsconsuma una unità della quota mensile. - Body: massimo 8 KB;
metadatamassimo 1 KB. - In caso di
429attendi i secondi indicati inRetry-After.
Esempio completo: bot discord.js
Nel repository trovi un bot pronto all'uso in examples/discord-bot (pannello con pulsante, link effimero, assegnazione ruolo, polling o webhook, anti-alt). Ecco il cuore dell'integrazione:
import { Client, Events, GatewayIntentBits, MessageFlags,
ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js';
const VERIS = 'https://veris.example.com';
const headers = { Authorization: `Bearer ${process.env.VERIS_API_KEY}`, 'Content-Type': 'application/json' };
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
// 1) L'utente clicca "Verifica" → il bot crea la sessione e invia il link
client.on(Events.InteractionCreate, async (i) => {
if (!i.isButton() || i.customId !== 'veris:verify') return;
await i.deferReply({ flags: MessageFlags.Ephemeral });
const res = await fetch(`${VERIS}/api/v1/sessions`, {
method: 'POST', headers,
body: JSON.stringify({ discord_user_id: i.user.id, discord_guild_id: i.guildId, guild_name: i.guild.name }),
});
const session = await res.json();
if (!res.ok) return i.editReply(`Errore: ${session.error.message}`);
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder().setLabel('Apri verifica').setStyle(ButtonStyle.Link).setURL(session.url));
await i.editReply({ content: 'Il tuo link personale (scade tra 10 minuti):', components: [row] });
});
// 2) Polling degli eventi → assegna il ruolo ai verificati
let cursor = 0;
setInterval(async () => {
const res = await fetch(`${VERIS}/api/v1/events?after=${cursor}`, { headers });
if (!res.ok) return;
const { data } = await res.json();
for (const event of data) {
cursor = event.seq;
if (event.type !== 'verification.succeeded') continue;
const { user_id, guild_id } = event.data.discord;
const guild = await client.guilds.fetch(guild_id);
const member = await guild.members.fetch(user_id).catch(() => null);
await member?.roles.add(process.env.VERIFIED_ROLE_ID, 'Verificato con Veris');
}
}, 3000);
client.login(process.env.DISCORD_TOKEN);
Esempio: bot discord.py
import os, aiohttp, discord
from discord.ext import tasks
VERIS = "https://veris.example.com"
HEADERS = {"Authorization": f"Bearer {os.environ['VERIS_API_KEY']}"}
ROLE_ID = int(os.environ["VERIFIED_ROLE_ID"])
class VerifyView(discord.ui.View):
def __init__(self):
super().__init__(timeout=None)
@discord.ui.button(label="Verifica", style=discord.ButtonStyle.success, custom_id="veris:verify")
async def verify(self, interaction: discord.Interaction, _):
await interaction.response.defer(ephemeral=True)
payload = {"discord_user_id": str(interaction.user.id),
"discord_guild_id": str(interaction.guild_id),
"guild_name": interaction.guild.name}
async with bot.http_session.post(f"{VERIS}/api/v1/sessions", json=payload, headers=HEADERS) as r:
data = await r.json()
if r.status != 201:
return await interaction.followup.send(f"Errore: {data['error']['message']}", ephemeral=True)
view = discord.ui.View()
view.add_item(discord.ui.Button(label="Apri verifica", url=data["url"]))
await interaction.followup.send("Il tuo link personale:", view=view, ephemeral=True)
class Bot(discord.Client):
cursor = 0
async def setup_hook(self):
self.http_session = aiohttp.ClientSession()
self.add_view(VerifyView())
self.poll.start()
@tasks.loop(seconds=3)
async def poll(self):
async with self.http_session.get(f"{VERIS}/api/v1/events", params={"after": self.cursor}, headers=HEADERS) as r:
if r.status != 200:
return
events = (await r.json())["data"]
for ev in events:
self.cursor = ev["seq"]
if ev["type"] != "verification.succeeded":
continue
d = ev["data"]["discord"]
guild = self.get_guild(int(d["guild_id"]))
member = guild and await guild.fetch_member(int(d["user_id"]))
if member:
await member.add_roles(discord.Object(ROLE_ID), reason="Verificato con Veris")
bot = Bot(intents=discord.Intents.default())
bot.run(os.environ["DISCORD_TOKEN"])
Per pubblicare il pannello invia in un canale un messaggio con view=VerifyView() (ad esempio da un comando riservato agli admin).
Best practice di sicurezza
- API key solo lato server, in variabili d'ambiente, mai nel codice versionato.
- Invia il link solo all'utente interessato (messaggio effimero o DM): il link è la sua "chiave" personale.
- Verifica sempre la firma dei webhook e scarta timestamp vecchi.
- Controlla
discord.user_idediscord.guild_iddel risultato prima di assegnare ruoli, non fidarti di dati esterni. - Salva il cursore degli eventi per non perdere risultati durante i riavvii.
- Usa
linked_accountsper segnalare possibili account alternativi allo staff invece di bannare automaticamente. - Il ruolo "Verificato" dovrebbe essere l'unico modo per vedere i canali del server.
FAQ
L'utente usa una VPN: cosa succede?
La pagina gli chiede di disattivarla e di premere "Riprova". Non viene espulso: decide il tuo bot cosa fare con il risultato finale.
Posso disattivare il blocco VPN per un server?
Sì: passa "checks": {"vpn": false} alla creazione della sessione. Turnstile resta sempre attivo.
Veris salva l'indirizzo IP degli utenti?
No. Viene salvato solo un hash HMAC con chiave segreta, usato per il rilevamento degli account collegati, e le sessioni vengono eliminate dopo 30 giorni.
Più bot possono usare Veris?
Sì: ogni bot ha la sua API key, con limiti, quote, webhook e feed eventi separati. Un bot non può mai vedere le sessioni di un altro.