Un webhook invia l'attività del tuo team a un URL che possiedi, nel momento stesso in cui accade. Ogni azione che finisce nel tuo Registro attività può essere inviata come POST JSON firmato al tuo server, al tuo bot o a uno strumento interno: niente polling, niente esportazioni, niente scraping del pannello.
Una sola regola decide tutto in questa pagina. Un webhook trasporta esattamente ciò che mostra il Registro attività del tuo team, e nient'altro. Non un flusso parallelo con un suo perimetro, non un payload più ricco per le macchine. Se non è in quella pagina, non è nel corpo.

📍 Dove trovarlo
Impostazioni team → Webhook, nella pagina del tuo team. Fino a tre destinazioni per team, ciascuna con il proprio URL, le proprie categorie e il proprio segreto di firma.
Chi può configurarne uno: il proprietario del team, oppure un membro il cui ruolo include Rimuovere un server dal team. È la stessa regola che apre il Registro attività stesso: un webhook inoltra ciò che quella pagina mostra, quindi poterlo leggere significa poterlo inoltrare. Vedi Ruoli e permessi del team.
🎯 Cosa viene inviato e cosa non lo è mai
Scegli tu le categorie. Vengono consegnate solo quelle che selezioni:
| Categoria | Cosa copre |
|---|---|
| Funzionalità | Una funzione attivata, disattivata o con le impostazioni salvate |
| Server | Un server aggiunto al team o rimosso da esso |
| Team | Team rinominato, ruoli modificati, webhook cambiati |
| Membri | Membri invitati, aggiunti, rimossi o che se ne vanno |
| Moderazione | Sanzioni, ricorsi, segnalazioni, ticket, cronologie cancellate |
| Abbonamento | Modifiche all'abbonamento del team |
| Fatturazione | Eventi di fatturazione del team |
| Azioni della piattaforma su questo team | Ciò che un admin della piattaforma ha fatto a uno dei tuoi server |
🚫 Tre cose non viaggiano mai
- Gli indirizzi IP. Nel Registro attività un IP viene mostrato alla persona a cui appartiene e a nessun altro. Un webhook non ha un lettore da verificare, quindi il campo non viene inviato: viene rimosso dal payload, non svuotato.
- Le azioni di backoffice che non riguardano te. L'amministrazione dell'intera piattaforma non ha un team, quindi nulla nella distribuzione può raggiungerla. La categoria Azioni della piattaforma su questo team è solo ciò che un admin ha fatto ai tuoi server.
- Le credenziali. Qualsiasi campo che somigli a un token, una chiave o un segreto arriva come
••••••••, su entrambi i lati di una coppia prima/dopo. Questo avviene di nuovo in uscita, in modo indipendente dal mascheramento che fa la pagina.
Gli accessi non sono offerti. Il registro ha una categoria auth, ed è volutamente assente dall'elenco qui sopra: inviare gli eventi di accesso a un server di terzi, senza supervisione e a tempo indeterminato, è un atto diverso dal mostrarli in una pagina che qualcuno ha dovuto aprire. Potrebbe essere aggiunta in futuro.
📦 La consegna
Ogni consegna è un singolo POST con un corpo JSON:
{
"version": 1,
"delivery_id": "0a4b2f0e-8c6e-4a5f-9b3d-2f6c1e0d7a11",
"event": "server.feature.enabled",
"category": "feature",
"team": { "id": 12, "name": "Night Shift" },
"data": {
"id": 918273,
"action": "server.feature.enabled",
"category": "feature",
"description": "Enabled Starboard",
"actor_name": "Ava",
"actor_role": "moderator",
"is_admin_action": false,
"team_id": 12,
"team_name": "Night Shift",
"server_id": 4,
"server_name": "Night Shift HQ",
"subject_label": "Starboard",
"changes": { "threshold": { "before": 3, "after": 5 } },
"metadata": null,
"created_at": "2026-09-06T09:41:12+00:00"
}
}
Quei quindici campi sono tutto data: gli stessi quindici che il Registro attività mostra, meno l'indirizzo IP. Ognuno di essi può essere null: un'azione senza server porta "server_id": null, un'azione senza nulla da confrontare porta "changes": null. Leggili in modo difensivo invece di dare per scontata una forma per ogni evento.
versionè la versione dell'involucro. Esiste perché il tuo ricevitore possa ramificarsi su di essa invece di indovinare quando la forma cresce.eventè l'azione registrata. Ce ne sono più di centotrenta e l'elenco cresce con la piattaforma: confronta con un prefisso o concategory, mai con un elenco esaustivo di nomi.delivery_idresta stabile a ogni nuovo tentativo dello stesso evento. È ciò che rende possibile la deduplicazione; vedi sotto.dataè la voce di registro stessa, lo stesso oggetto che mostra il Registro attività.
📨 Header
| Header | Valore |
|---|---|
User-Agent |
YAWBDB-Webhooks/1.0 |
X-YAWBDB-Event |
Il nome dell'azione, uguale a event nel corpo |
X-YAWBDB-Delivery |
L'id della consegna, uguale a delivery_id |
X-YAWBDB-Timestamp |
Tempo Unix in secondi, quando la richiesta è stata costruita |
X-YAWBDB-Signature |
v1= seguito dal digest esadecimale |
🔐 Verificare la firma
Chiunque venga a conoscenza dell'URL della tua destinazione può inviarle qualsiasi cosa con un POST. La firma è il modo per distinguere le nostre consegne dalle loro.
Il digest è un HMAC-SHA256 calcolato sul timestamp, un punto e poi il corpo grezzo della richiesta, con il tuo segreto di firma come chiave:
signed_string = X-YAWBDB-Timestamp + "." + raw_body
signature = "v1=" + hex( hmac_sha256(signed_string, secret) )
Tre dettagli sono determinanti, e saltarne uno lascia una falla:
- Firma i byte grezzi, prima di qualsiasi parsing JSON. Riserializzare il corpo lo cambia (ordine delle chiavi, slash con escape, unicode) e il digest non corrisponderà.
- Il timestamp è dentro la stringa firmata, non solo accanto ad essa. È ciò che ti permette di respingere i replay: rifiuta una consegna il cui timestamp ha più di pochi minuti (cinque minuti è una finestra ragionevole) e un attaccante non può rinviare una richiesta che ha catturato la settimana scorsa. Poiché il timestamp è firmato, non può nemmeno modificarlo.
- Confronta in tempo costante. Un semplice
==si ferma al primo byte diverso, e quella differenza di tempo basta per recuperare una firma un byte alla volta. Usahash_equals,crypto.timingSafeEqual,hmac.compare_digest, comunque lo chiami il tuo linguaggio.
PHP
$raw = file_get_contents('php://input');
$timestamp = (int) ($_SERVER['HTTP_X_YAWBDB_TIMESTAMP'] ?? 0);
$given = $_SERVER['HTTP_X_YAWBDB_SIGNATURE'] ?? '';
if (abs(time() - $timestamp) > 300) {
http_response_code(400); // too old, or clock is wrong
exit;
}
$expected = 'v1=' . hash_hmac('sha256', $timestamp . '.' . $raw, $secret);
if (!hash_equals($expected, $given)) {
http_response_code(401);
exit;
}
http_response_code(200); // accepted
Node.js (Express)
import crypto from 'node:crypto';
// The raw body is required - express.json() alone destroys it.
app.post('/yawbdb', express.raw({ type: 'application/json' }), (req, res) => {
const timestamp = Number(req.get('X-YAWBDB-Timestamp'));
const given = req.get('X-YAWBDB-Signature') ?? '';
if (Math.abs(Date.now() / 1000 - timestamp) > 300) return res.sendStatus(400);
const expected = 'v1=' + crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${req.body}`)
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(given);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(401);
res.sendStatus(200);
});
Il prefisso v1= indica lo schema. Se mai cambiasse, un ricevitore che controlla il prefisso può rifiutare deliberatamente uno schema sconosciuto invece di leggerlo male in silenzio.
🔑 Il segreto
64 caratteri esadecimali, generati per te, mostrati una sola volta, nel pannello, subito dopo aver creato la destinazione o aver premuto Nuovo segreto. Nulla lo rilegge in seguito: né la pagina, né l'API, né un'esportazione. Se lo perdi, ne generi uno nuovo.
La rotazione ha effetto immediato per tutto ciò che viene inviato da quel momento. Le consegne già in coda continuano a uscire firmate con il vecchio segreto finché la coda non si svuota, quindi aspettati una breve sovrapposizione e aggiorna prima il tuo ricevitore.
🔁 Consegna, nuovi tentativi e deduplicazione
- Conta come ricevuto solo un
2xx. Qualsiasi altra cosa (3xx,4xx,5xx) è un errore. Rispondi in fretta e fai il lavoro dopo. - I reindirizzamenti non vengono seguiti. Un
302è un errore, non un passaggio. Dacci l'URL finale. - I timeout sono brevi: tre secondi per connettersi, cinque secondi per rispondere. È una notifica, non una chiamata di procedura remota: conferma prima, elabora dopo.
- Quattro tentativi, distanziati di 30 secondi, 2 minuti e poi 10 minuti: circa dodici minuti da un capo all'altro. Copre un deploy, non un'interruzione di servizio.
- I nuovi tentativi riusano lo stesso
delivery_id. Un ricevitore che tratta ogniPOSTcome nuovo conterà due volte nel giorno in cui il nostro primo tentativo riesce dalla tua parte ma va in timeout dalla nostra. Memorizza l'id e ignora quelli che hai già gestito. - L'ordine non è garantito. Le consegne sono messe in coda in modo indipendente; una ritentata arriva dopo eventi più recenti. Usa
data.created_atse per te l'ordine conta.
⛔ Disattivazione automatica
Dopo 20 errori consecutivi, la destinazione viene disattivata e il pannello lo dice sulla scheda. Una destinazione che ha smesso di rispondere di solito ha smesso per sempre, e nessuno viene a dircelo: riprovare all'infinito costerebbe una richiesta e una riga di cronologia per ogni azione, indefinitamente.
Per riavviarla: correggi il tuo ricevitore, poi Modifica il webhook e spunta di nuovo Consegna gli eventi a questa destinazione. È la riattivazione a azzerare il contatore, quindi una destinazione riparata non è a un solo errore dallo spegnersi di nuovo.
🧪 Invia una prova
Invia una prova lancia subito una vera richiesta firmata verso la tua destinazione e ti mostra il codice di stato e il tempo di andata e ritorno. Non è una simulazione: passa per lo stesso codice di ogni consegna reale, con gli stessi header, quindi una destinazione che supera la prova non può poi rifiutare il traffico di produzione per un header che la prova non aveva mai inviato.
Il corpo della prova contiene "event": "panel.webhook.test" e "category": "test", così il tuo ricevitore può riconoscerla.
Aspettati due arrivi, non uno. Premere il pulsante è esso stesso un'azione di team registrata. Quindi la tua destinazione riceve la consegna di prova e, se è iscritta alla categoria Team, una seconda consegna normale per
team.webhook.testedun attimo dopo. È corretto e non è un ciclo.
Una prova non conta mai verso il limite dei 20 errori, e non lo azzera mai. Fare il debug di una destinazione non può spegnerla.
📜 Tentativi recenti
Tentativi recenti mostra gli ultimi 20 tentativi per quella destinazione: quando, quale evento, quale numero di tentativo, il codice di stato o l'errore e quanto ci è voluto. Gli errori riportano un breve estratto di ciò che ha risposto il tuo server, che di solito è il modo più rapido per scoprire che a dire di no è un reverse proxy e non il tuo codice.
I tentativi vengono conservati per 30 giorni ed eliminati ogni notte. Sono scarti utili al debug, non un registro: il registro stesso ha una conservazione propria, molto più lunga.
🛡️ Dove ci rifiutiamo di inviare
L'URL di una destinazione deve puntare all'internet pubblico. Un URL che si risolve in un indirizzo di loopback, in un intervallo privato, in un indirizzo link-local o in un indirizzo dei metadati cloud viene rifiutato: quando lo salvi, e di nuovo prima di ogni singolo invio.
Il secondo controllo è quello che conta nel tempo. Un nome che puntava a qualcosa di pubblico il giorno in cui è stato digitato può puntare a 127.0.0.1 un mese dopo, e un webhook scatta finché esiste. Un rifiuto conta come errore per la destinazione e non viene ritentato: porre tre altre volte la stessa domanda al resolver non cambierebbe nulla.
Preferisci una destinazione https. La firma dimostra chi ha inviato il corpo, non lo nasconde: su http semplice la tua attività viaggia in chiaro.
💬 Direttamente in un canale Discord o Slack
Incolla un URL di webhook Discord (https://discord.com/api/webhooks/…) come destinazione e il pannello invia un messaggio Discord invece dell'involucro JSON: un embed per voce, con la descrizione come titolo, con l'evento, la categoria, l'autore, il team, il server, il soggetto e, quando ce ne sono, le modifiche come breve diff. Il timestamp è quello della voce e il piè di pagina riporta l'id della consegna.
L'embed è costruito dallo stesso involucro descritto sopra, quindi ne eredita le due regole: nessun indirizzo IP, credenziali mascherate. Le menzioni sono disattivate dal lato di Discord, quindi un canale o un ruolo chiamato @everyone in una voce viene pubblicato come testo e non avvisa nessuno. Gli header X-YAWBDB-* e la firma vengono comunque inviati; Discord semplicemente li ignora.
Discord risponde 204 No Content a un messaggio che ha accettato, ed è ciò che mostra per lui una riga verde in Tentativi recenti.
Lo stesso vale per un URL di webhook in ingresso di Slack (https://hooks.slack.com/services/…): il pannello pubblica un messaggio Slack per voce, con la descrizione in grassetto, poi una riga per ogni fatto, le modifiche in un blocco di codice e sotto il timestamp e l'id della consegna. È costruito dallo stesso involucro, quindi valgono le stesse due regole, e i tre caratteri che Slack riserva ai suoi comandi (<, >, &) vengono escapati, così un nome digitato da un membro non può mai trasformarsi in @channel o @here. Slack risponde 200 ok a un messaggio che ha accettato. Gli URL workflow e trigger di Slack non sono messaggi: mantengono l'involucro JSON, che un workflow può scomporre con le proprie variabili.
💡 Cosa ci costruiscono le persone
- Pubblicare l'attività del tuo team in un canale Discord o Slack tuo: un URL di webhook Discord o Slack funziona così com'è, oppure metti in mezzo un tuo ricevitore per filtrare e riformattare.
- Replicare il registro nel tuo archivio di log e conservarlo oltre la finestra di conservazione del pannello.
- Avvisare qualcuno quando un'azione di moderazione arriva fuori orario.
- Avviare una build, una sincronizzazione o un backup quando cambia l'impostazione di una funzione.
🔗 Vedi anche
- Registro attività: cosa registra il registro e cosa volutamente non registra
- Ruoli e permessi del team: chi può aprire la pagina
- Log Server: il registro lato Discord, un'altra funzione che risponde a un'altra domanda