API v1

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…).

URL base dell'API: tutte le richieste vanno a 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

  1. L'utente entra nel server e clicca il pulsante Verifica del tuo bot.
  2. Il bot chiama POST /api/v1/sessions con l'ID dell'utente e del server.
  3. Veris risponde con un url unico (es. https://veris.example.com/v/Xk3…), monouso e con scadenza.
  4. Il bot invia il link all'utente in un messaggio effimero (visibile solo a lui).
  5. L'utente apre il link: Veris controlla la connessione e mostra il captcha Turnstile.
  6. A verifica completata, Veris invia il risultato al bot tramite webhook firmato e lo rende disponibile nel feed eventi.
  7. 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:

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
Importante: la API key va usata solo dal server del bot. Non inserirla mai in codice client, repository pubblici o messaggi Discord. Se viene esposta, chiedi all'amministratore di rigenerarla: quella vecchia smette subito di funzionare.

Veris salva solo l'hash SHA-256 della chiave: nemmeno l'amministratore può recuperarla, solo rigenerarla.

Creare una sessione

POST/api/v1/sessions
CampoTipoDescrizione
discord_user_idstringObbligatorio. ID Discord (snowflake) dell'utente da verificare.
discord_guild_idstringObbligatorio. ID del server Discord.
guild_namestringNome del server mostrato nella pagina di verifica (max 100).
metadataobjectDati liberi (max 1 KB) restituiti nel risultato, es. {"channel_id": "…"}.
ttl_secondsintegerValidità del link, 60–3600 secondi. Predefinito: 600.
checksobjectControlli 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:

WebhookFeed eventi (polling)
ComeVeris fa una POST al tuo endpoint HTTPSIl bot chiama GET /api/v1/events ogni 2–5 s
Serve un server pubblico?Sì (HTTPS con dominio)No, funziona ovunque
LatenzaIstantaneaPari all'intervallo di polling
Consigliato perBot su VPS/cloud con dominioBot su PC, hosting condiviso, Replit…

Tipi di evento:

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'));
}

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)

GET/api/v1/events?after=<seq>&limit=<1-100>

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"
}
CampoDescrizione
statuspending, verified, failed, expired o cancelled.
resultnull finché la sessione è pending.
result.passedtrue solo se l'utente è verificato.
result.reasonMotivo del fallimento (vedi motivi), expired, superseded o cancelled_by_client.
result.countryPaese della connessione (ISO 3166-1 alpha-2).
result.linked_accountsQuanti 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.attemptsTentativi di verifica inviati.
last_block_reasonUltimo blocco incontrato dall'utente (anche se poi ha superato la verifica).
webhook_statuspending, delivered, failed o null se non c'è webhook.

Motivi di blocco

MotivoSignificatoCosa vede l'utente
vpnVPN rilevata (incl. Cloudflare WARP / iCloud Private Relay se attivo)Invito a disattivare la VPN e riprovare
proxyProxy o IP ad alto rischio (con intelligence IP esterna)Invito a disattivare il proxy
hostingConnessione da datacenter/hosting (VPN, server, bot)Invito a usare rete di casa o dati mobili
torExit node della rete TorTor non consentito
botBrowser automatizzato, headless o client HTTPRichiesta di usare un browser normale
captcha_failedCaptcha Turnstile non validoNuovo 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

GET/api/v1/sessions/{id}

Restituisce l'oggetto sessione. Utile come controllo puntuale; per i risultati preferisci webhook o feed eventi.

Annullare una sessione

DELETE/api/v1/sessions/{id}

Invalida il link (es. l'utente ha lasciato il server). Restituisce 409 session_closed se non è più pending.

Il tuo account

GET/api/v1/me

Piano, limiti, utilizzo del mese, scadenza dell'abbonamento e webhook configurato.

PATCH/api/v1/me

Aggiorna webhook_url (usa null per rimuoverlo).

POST/api/v1/me/webhook/rotate
POST/api/v1/me/webhook/test

Errori

Gli errori hanno sempre questo formato:

{ "error": { "code": "invalid_request", "message": "\"discord_user_id\" deve essere un ID Discord (snowflake) come stringa." } }
HTTPCodiceSignificato
400invalid_request, invalid_json, invalid_webhook_urlParametri non validi
401unauthorizedAPI key mancante, errata o revocata
402subscription_expiredAbbonamento scaduto: contatta l'amministratore
403client_suspendedAccesso sospeso dall'amministratore
404not_foundRisorsa inesistente
409session_closedLa sessione non è più modificabile
413 / 415payload_too_large, unsupported_media_typeBody troppo grande o non JSON
429rate_limited, quota_exceededLimite al minuto o quota mensile superati (vedi header Retry-After)
500internal_errorErrore interno: riprova con backoff

Limiti e quote

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

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.