10 min di letturaFunzionalità

Webhook per gli eventi sui link: payload, firme, retry

Webhook dell'URL shortener per gli eventi sui link: l'involucro reale del payload, verifiche HMAC di X-Webhook-Signature in Node e Python, la policy di retry e le chiavi di deduplicazione.

Marius Voß
DevRel · edge infra
Diagramma in stile pixel dei webhook dell'URL shortener: gli eventi link.created, link.updated e member.invited passano attraverso una consegna firmata verso endpoint event, siem e discord, sotto una barra che recita HMAC-SHA256 v1=, timeout 10s, 3 tentativi

I webhook per gli URL shortener di Elido inviano tramite POST un involucro JSON firmato al tuo endpoint HTTPS ogni volta che qualcosa cambia in un workspace: un link viene creato, modificato, eliminato, scade o raggiunge il suo tetto di click, un membro viene invitato, un dominio si verifica. Ogni richiesta porta un header X-Webhook-Signature: v1=<hex>, che è un HMAC-SHA256 su {timestamp}.{raw_body}, e una consegna fallita riceve tre tentativi in circa venti minuti.

Ciò che non invia oggi è un webhook per il click su un link. I click escono invece attraverso l'API di analytics e gli inoltratori di eventi, e mostrerò dove alla fine. Questo articolo è la metà in uscita della superficie API; la guida rapida API + SDK dell'URL shortener copre la metà in entrata, e smart link spiegati è il cornerstone delle funzionalità da cui provengono gli eventi sui link.

Ogni evento sotto raggiunge un endpoint webhook che vi si è iscritto per nome. Il form per un nuovo endpoint nella dashboard offre caselle di spunta per gli otto più comuni; l'API accetta qualsiasi nome dell'elenco.

EventoScatta quandoCasella dashboard
link.createdUn link viene creato, uno alla volta o in un import in bloccosì
link.updatedCambia la destinazione, le impostazioni o lo stato, modifiche in blocco, ripristinisì
link.deletedUn link viene eliminato, singolarmente o in bloccosì
link.expiredUn link supera la sua data di scadenzasolo API
link.cap_reachedUn link raggiunge il suo conteggio massimo di clicksolo API
link.brokenIl controllo dei link non funzionanti rileva che la destinazione falliscesolo API
workspace.created, workspace.updatedUn workspace viene creato o le sue impostazioni cambianosì
member.invited, member.removedUn membro viene aggiunto (direttamente, via SCIM o tramite un invito accettato) o rimossosì
member.role_changedIl ruolo di un membro cambiasolo API
invitation.created, invitation.acceptedUn invito viene inviato o accettatosolo API
domain.verified, domain.ssl_failedUn dominio personalizzato supera i controlli DNS, o continua a fallirli dopo 24 oresolo API
audit.eventQualsiasi voce del log di auditsì

I link cifrati aggiungono altri due eventi, link.encrypted_created e link.encryption_rotated. Se preferisci non tenere l'elenco aggiornato a mano, crea un endpoint siem: riceve ogni evento del workspace, voci di audit incluse, senza alcun filtro di sottoscrizione.

Nella roadmap, non ancora attivi: click.created (un flusso campionato di click), eventi di fatturazione come billing.subscription_upgraded, e filtri per singolo endpoint come "solo link nella cartella X". Non iscriverti oggi a questi nomi; nessuno li pubblica ancora.

Creare un endpoint webhook con l'API

Un endpoint appartiene a un workspace. Lo crei con una POST a /v1/workspaces/{workspace_id}/webhooks:

curl -X POST "https://api.elido.app/v1/workspaces/$WORKSPACE_ID/webhooks" \
  -H "Authorization: Bearer elido_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://hooks.example.com/elido",
    "events": ["link.created", "link.updated", "link.deleted"],
    "description": "CRM sync",
    "kind": "event"
  }'

La risposta è 201 con l'endpoint e, per i tipi event e siem, un secret monouso:

{
  "endpoint": {
    "id": 7,
    "workspace_id": 42,
    "url": "https://hooks.example.com/elido",
    "events": ["link.created", "link.updated", "link.deleted"],
    "is_active": true,
    "description": "CRM sync",
    "kind": "event",
    "config": {},
    "created_at": "2026-09-21T09:12:44Z"
  },
  "secret": "whsec_9f2c..."
}

Elido genera il secret; tu non ne invii uno. Copialo subito, perché nessuna chiamata successiva lo restituisce. kind accetta cinque valori. event e siem sono le consegne JSON firmate trattate in questo articolo. discord, telegram e sentry rimodellano gli stessi eventi in un messaggio di chat o in un evento Sentry, si autenticano tramite l'URL o un token bot cifrato, e non trasportano header HMAC.

I permessi sono cambiati questo mese. Leggere gli endpoint e il log di consegna richiede workspace.view. Creare, modificare, eliminare, ruotare un secret o reinviare una consegna richiede workspace.edit, il che significa un amministratore o un proprietario. Una chiave API funziona all'interno del workspace per cui è stata emessa e mai al di sopra del ruolo scelto al momento della creazione, quindi una chiave a livello viewer riceve un 403 sulla POST qui sopra. Riferimento completo: la documentazione sui webhook.

L'involucro del payload webhook

Ogni consegna firmata ha lo stesso involucro a quattro campi. data contiene il record che è cambiato, quindi per gli eventi sui link è la riga del link:

{
  "type": "link.created",
  "workspace_id": 42,
  "data": {
    "id": 91834,
    "workspace_id": 42,
    "domain_id": 3,
    "slug": "spring-sale",
    "destination_url": "https://shop.example.com/spring",
    "title": "Spring sale landing",
    "tags": ["newsletter"],
    "status": "active",
    "expires_at": null,
    "max_clicks": null,
    "redirect_status": 302,
    "created_by_user_id": 17,
    "created_at": "2026-09-21T09:14:02.184311Z"
  },
  "timestamp": "2026-09-21T09:14:02Z"
}

Quel campione è ridotto; il vero data porta ogni colonna del link, incluse le regole di targeting, la cartella, la campagna e i campi di scansione. Non c'è alcun ID evento né uno short_url nel corpo, quindi costruisci l'URL breve a partire dal tuo dominio e dallo slug se ti serve. Gli eventi pianificati inviano un oggetto più piccolo: link.expired ha link_id, slug e destination_url, e link.cap_reached aggiunge cap e clicks.

Una correzione che dovresti conoscere se hai registrato i payload prima di questa settimana: i campi segreti ora vengono rimossi prima che un payload lasci Elido. password_hash di un link protetto da password e token di un invito prima comparivano in data; ora non più, né nelle nuove consegne né nel log di consegna. Se hai memorizzato vecchi payload, quei dati vanno eliminati.

Verificare l'header X-Webhook-Signature

Ogni richiesta firmata porta questi header:

X-Webhook-Signature: v1=5d8f0c3e...
X-Elido-Signature: v1=5d8f0c3e...
X-Webhook-Timestamp: 1790068442
X-Webhook-Event: link.created
X-Webhook-Delivery: 55120
User-Agent: Elido-Webhooks/1.0

I due header di firma contengono lo stesso valore. Elido calcola HMAC-SHA256, come definito nell'RFC 2104, con l'intera stringa whsec_... come chiave, sul timestamp, un punto e i byte grezzi del corpo. Il digest esadecimale riceve un prefisso v1=. Non c'è alcun campo t= dentro l'header; il timestamp vive nel proprio header separato.

Flusso di verifica della firma: gli header X-Webhook-Signature e X-Webhook-Timestamp più il corpo grezzo e il secret alimentano un HMAC-SHA256 su ts punto body, confrontato a tempo costante come v1= esadecimale, poi un controllo di freschezza di 5 minuti lato ricevitore divide tra elaborare l'evento o rifiutare con 400

In Node, firma i byte grezzi, non un oggetto riserializzato. Express richiede express.raw({ type: "application/json" }) su questa rotta per questo motivo:

import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyElido(secret, headers, rawBody, toleranceSec = 300) {
  const ts = headers["x-webhook-timestamp"] ?? "";
  if (!/^\d+$/.test(ts)) return false;
  if (Math.abs(Date.now() / 1000 - Number(ts)) > toleranceSec) return false;

  const mac = createHmac("sha256", secret).update(`${ts}.`).update(rawBody);
  const expected = Buffer.from("v1=" + mac.digest("hex"));

  // During a rotation the old secret signs X-Elido-Signature-Previous.
  return ["x-elido-signature", "x-elido-signature-previous"].some((name) => {
    const got = Buffer.from(headers[name] ?? "");
    return got.length === expected.length && timingSafeEqual(got, expected);
  });
}

Il controllo di lunghezza è importante: timingSafeEqual di Node lancia un'eccezione su buffer di dimensioni diverse invece di restituire false. La versione Python con il modulo standard hmac:

import hashlib
import hmac
import time


def verify_elido(secret: str, headers, raw_body: bytes, tolerance: int = 300) -> bool:
    ts = headers.get("X-Webhook-Timestamp", "")
    if not ts.isdigit() or abs(time.time() - int(ts)) > tolerance:
        return False
    digest = hmac.new(secret.encode(), ts.encode() + b"." + raw_body, hashlib.sha256)
    expected = "v1=" + digest.hexdigest()
    for name in ("X-Elido-Signature", "X-Elido-Signature-Previous"):
        got = headers.get(name)
        if got and hmac.compare_digest(got, expected):
            return True
    return False

Se il controllo continua a non riuscire, la guida alla verifica delle firme dei webhook include una versione in Go, un nodo Code di n8n e le cause più comuni di una mancata corrispondenza. La finestra di cinque minuti è un controllo tuo, non nostro. Elido appone un timestamp fresco a ogni tentativo, retry inclusi, quindi un retry legittimo non sembra mai obsoleto. Senza la finestra, chiunque abbia catturato una richiesta potrebbe riprodurla la settimana successiva e la firma corrisponderebbe comunque.

La rotazione avviene con POST /v1/workspaces/{workspace_id}/webhooks/{id}/rotate-secret, oppure con il pulsante Rotate sulla pagina dell'endpoint. Ottieni il nuovo secret una sola volta. Per i sette giorni successivi ogni consegna porta anche X-Elido-Signature-Previous, firmato con il vecchio secret, motivo per cui entrambe le funzioni sopra lo provano. Distribuisci il nuovo secret in un qualsiasi momento di quella settimana e niente si rompe.

La policy di retry dei webhook

Il worker di consegna preleva le consegne in attesa ogni pochi secondi, quindi una modifica a un link di solito ti raggiunge entro pochi secondi. Qualsiasi risposta 2xx segna la consegna come completata. Uno stato non-2xx, un errore di rete o l'assenza di risposta entro 10 secondi conta come tentativo fallito.

Grafico a barre della pianificazione dei retry webhook: tentativo 1 a T+0, tentativo 2 cinque minuti dopo un fallimento a T+5m, tentativo 3 quindici minuti dopo a T+20m, poi la consegna viene contrassegnata come fallita senza altri tentativi automatici finché qualcuno non preme Retry o chiama l'endpoint di retry

Tre tentativi per consegna, venti minuti end to end. È volutamente breve, e onestamente è più breve di quanto sceglierei per un ricevitore dietro una VPN instabile. Un'interruzione di due ore dal tuo lato non sarà coperta dai retry automatici. Ciò che la copre è il log di consegna: GET /v1/workspaces/{workspace_id}/webhooks/{id}/deliveries elenca ogni consegna con stato, codice HTTP, latenza, numero di tentativi e prossimo orario di retry, e la pagina dell'endpoint mostra le stesse righe con un pulsante Retry. Retry, o POST .../deliveries/{delivery_id}/retry, riarma una consegna fallita o completata con un budget fresco di tre tentativi e restituisce 202. Una consegna ancora in sospeso riceve 409.

Alcuni fallimenti saltano i retry. Un endpoint Telegram privo del suo chat_id, o un DSN Sentry malformato, viene contrassegnato come fallito immediatamente, perché ripetere la richiesta non risolverà un problema di configurazione. Per mettere offline un endpoint senza eliminarlo, invia una PUT con "is_active": false; gli endpoint in pausa non ricevono nuove consegne.

Se il tuo handler svolge un lavoro pesante, restituisci prima 200 e metti in coda il job. Il taglio dei dieci secondi è il punto in cui un handler lento ma riuscito si trasforma in un duplicato, il che ci porta alla deduplicazione.

Stai progettando un ricevitore per il tuo team? La pagina delle funzionalità webhook mostra il lato dashboard di tutto questo.

Idempotenza e ordinamento dei webhook

Elido consegna at-least-once. La sezione sui retry ha mostrato un modo in cui avviene un duplicato: il tuo handler completa il lavoro, poi manca la finestra di dieci secondi, ed Elido lo invia di nuovo. Un Retry manuale reinvia intenzionalmente.

Entrambi i casi mantengono lo stesso valore di X-Webhook-Delivery, perché è l'ID di una consegna verso un endpoint, non di un tentativo. Usalo come chiave:

CREATE TABLE elido_webhook_seen (
  delivery_id BIGINT PRIMARY KEY,
  received_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- in the handler, inside the same transaction as your work:
INSERT INTO elido_webhook_seen (delivery_id) VALUES ($1)
ON CONFLICT (delivery_id) DO NOTHING
RETURNING delivery_id;
-- no row back means you've already handled this delivery

Due endpoint iscritti allo stesso evento ottengono due ID di consegna diversi, quindi deduplica per endpoint. In rari riavvii dal nostro lato un evento può essere accodato due volte come consegne separate; se una doppia scrittura farebbe danno, aggiungi una seconda protezione su type più data.id più data.updated_at.

Non c'è alcuna garanzia di ordinamento. Le consegne partono in ordine di scadenza più vecchia prima, ma il retry di un evento precedente può arrivare dopo uno più recente. Confronta data.updated_at con quanto hai già memorizzato prima di sovrascrivere un link, e non fare affidamento sul timestamp dell'involucro per l'ordinamento: ha una precisione di un solo secondo.

Dove vivono i dati sui click al posto di un webhook di click

Questa è la parte che la versione precedente di questo articolo sbagliava. Non esiste oggi un webhook click, e click.created è pianificato, non pubblicato. Il percorso di redirect resta libero da lavoro sincrono, cosa che l'articolo ingestione dei click fire-and-forget spiega, e i click vanno verso l'archiviazione analytics piuttosto che nella coda dei webhook.

Per i dati a livello di click oggi, hai due strade:

  1. Inoltratori di eventi. Ogni click su un link breve diventa un evento lato server nello strumento che già usi: eventi di click su link in Mixpanel, eventi di click su profili in Klaviyo, o metriche di redirect in Datadog per dashboard operative.
  2. L'API di analytics. GET /v1/analytics/workspaces/{workspace_id}/clicks/recent restituisce i click recenti, e clicks.csv li esporta, così un job pianificato può estrarre le righe di cui ha bisogno.

Quale scegliere dipende dalla latenza e da dove finiscono i dati; l'articolo webhook contro polling per il tracciamento dei click ripercorre quel compromesso. E se il flusso campionato click.created verrà pubblicato, questa pagina lo dirà per prima.

Leggi il cornerstone: smart link spiegati.

Correlati sul blog

Domande frequenti

Elido invia un webhook per ogni click su un link?

Non oggi. I webhook coprono i cambiamenti a livello di workspace come link.created, link.updated, link.expired e member.invited. Un evento click.created è nella roadmap come flusso campionato. Per i dati a livello di click oggi, usa l'API di analytics o un inoltratore di eventi come Mixpanel, Klaviyo o Datadog.

Come verifico la firma di un webhook Elido?

Calcola HMAC-SHA256 sul valore di X-Webhook-Timestamp, un punto e il corpo grezzo della richiesta, con chiave il tuo secret whsec_. Codificalo in esadecimale, aggiungi il prefisso v1= e confrontalo a tempo costante con l'header X-Webhook-Signature. Rifiuta i timestamp più vecchi di cinque minuti.

Quante volte Elido riprova un webhook fallito?

Ogni consegna riceve tre tentativi: uno immediato, uno cinque minuti dopo un fallimento e uno quindici minuti dopo quello. Qualsiasi stato non-2xx, errore di rete o risposta più lenta di dieci secondi conta come fallimento. Dopo il terzo, la consegna viene contrassegnata come fallita finché non premi Retry.

Qual è la differenza tra X-Webhook-Signature e X-Elido-Signature?

Nessuna, a parte il nome. Entrambi gli header trasportano la stessa firma v1=, e X-Webhook-Signature resta per i ricevitori più vecchi. Durante una rotazione del secret, Elido invia anche X-Elido-Signature-Previous, firmato con il vecchio secret, per sette giorni.

Come faccio a non elaborare due volte lo stesso webhook?

Memorizza il valore dell'header X-Webhook-Delivery e salta qualsiasi richiesta il cui valore hai già gestito. Identifica una consegna verso un endpoint e resta invariato tra i retry automatici e i reinvii manuali, quindi un indice univoco su di esso è sufficiente.

Chi può creare o eliminare webhook in un workspace?

Gli amministratori e i proprietari del workspace. Elencare gli endpoint e leggere il log di consegna richiede accesso in visualizzazione; creare, modificare, eliminare, ruotare un secret o reinviare una consegna richiede il permesso workspace.edit. Le chiavi API sono limitate al ruolo scelto al momento della creazione della chiave.

Prova Elido

Incolla un URL, ottieni un link breve

Senza registrazione. Il link vive 30 giorni. Iscriviti per conservarlo.

Gratis, nessuna registrazione richiesta · 2 al giorno

Prova Elido

Accorciatore di URL ospitato nell'UE: domini personalizzati, analisi approfondite e API aperta. Piano gratuito - senza carta di credito.

Tag
url shortener webhooks
link click webhook
webhook signature verification
webhook retry policy
webhook idempotency
webhook payload

Continua a leggere