Elido
10 min de leituraRecursos

Webhooks para eventos de links: todos os formatos, todas as tentativas

A superfície completa de webhooks para eventos de encurtador de URL — formatos de payload para click, conversion, link.created e bio.click, além da política de novas tentativas, esquema de assinatura e modelo de idempotência

Marius Voß
DevRel · edge infra
Diagrama hub-and-spoke com fontes de eventos de link à esquerda (click, conversion, link.created, bio.click) fluindo para um serviço central webhook-dispatcher que distribui para endpoints assinantes com anotações de novas tentativas de 1s, 30s, 5m, 1h, 6h

Webhooks são a parte da superfície de API de um encurtador de URL que todos implementam, mas que quase ninguém faz bem. As partes difíceis não são a codificação — o payload é um objeto JSON — mas os detalhes operacionais: verificação de assinatura, política de novas tentativas, idempotência, garantias de entrega e o que acontece quando o endpoint do assinante fica fora do ar por dois dias.

Este post documenta cada evento de webhook que o Elido emite, cada formato de payload, a curva de novas tentativas e o esquema de assinatura. O URL shortener API + SDKs quickstart cobre a superfície de API de entrada; este é o lado de saída.

Os 12 tipos de eventos#

O Elido emite 12 tipos de eventos de webhook, agrupados em três famílias:

Eventos de clique e tráfego: click, bio.click, qr.scan, conversion. Estes são disparados a cada redirecionamento ou escaneamento após um pequeno atraso na fila (descrito abaixo).

Eventos de ciclo de vida: link.created, link.updated, link.deleted, bio.published. Estes são disparados da camada de API quando o registro subjacente é alterado.

Eventos de agregação e operações: daily.summary, campaign.ended, alert.threshold_exceeded, quota.warning. Estes são disparados de acordo com um cronograma ou quando um limite é excedido.

Um assinante registra um webhook em POST /v1/webhooks com uma URL de destino e uma matriz de tipos de eventos que deseja receber. A solicitação de assinatura completa:

POST /v1/webhooks
Content-Type: application/json
Authorization: Bearer <api-key>

{
  "url": "https://example.com/webhooks/elido",
  "events": ["click", "conversion", "link.created"],
  "secret": "whsec_<32-byte-base64>",
  "active": true
}

O secret é a chave HMAC usada para assinar as solicitações de saída. Ele é opaco para o Elido; nunca registramos ou exibimos após a resposta da chamada de criação.

O payload do evento de clique#

Por volume, este é o evento que mais lhe interessa. Cada redirecionamento através de qualquer link curto produz um evento click após o redirecionamento ter sido servido ao cliente. O formato:

{
  "id": "evt_2g8kFqJxYwPaZcvAm3HsTr",
  "type": "click",
  "created_at": "2026-05-22T14:32:18.847Z",
  "data": {
    "link_id": "lnk_2g3jQpRyXz4Mn8VbF7Hkl",
    "short_url": "https://elido.me/abc123",
    "destination_url": "https://shop.example.com/spring",
    "click_id": "clk_2g8kFqJxYwPaZcvAm3HsTr",
    "ip_prefix": "203.0.113.0/24",
    "country": "DE",
    "city_geoname_id": 2950159,
    "user_agent_family": "Chrome 124",
    "device_type": "mobile",
    "os_family": "iOS 17.5",
    "referrer": "https://www.google.com",
    "utm_source": "newsletter",
    "utm_medium": "email",
    "utm_campaign": "spring-2026",
    "utm_term": null,
    "utm_content": null
  },
  "workspace_id": "ws_12"
}

Alguns detalhes que vale a pena destacar:

  • ip_prefix, não ip. Mantemos o prefixo de rede /24 (IPv4) ou /48 (IPv6), não o endereço completo. O post sobre GDPR para encurtadores de URL explica o porquê; o efeito prático é que seu assinante obtém precisão geográfica suficiente para análise sem a responsabilidade de dados pessoais de IPs completos.
  • city_geoname_id, não city_name. O ID do GeoNames é estável entre localidades; o nome da cidade varia. Se você precisar de um nome localizado, consulte o ID no dump do GeoNames.org uma vez e armazene o resultado em cache.
  • user_agent_family, não a string UA completa. Removemos o UA completo durante a ingestão (são dados de impressão digital de alta entropia); a família é o navegador+versão principal que sobrevive.

O atraso entre o redirecionamento que atende o cliente e o disparo do webhook é normalmente de 200ms a 2s. Os eventos de clique fluem primeiro pelo Redpanda, são agregados para análise e, em seguida, um trabalhador de distribuição emite os webhooks. Este é o mesmo pipeline que alimenta as análises do painel — o post sobre ingestão de cliques fire-and-forget cobre a mecânica da fila.

O payload do evento de conversão#

Eventos de conversão são disparados quando um clique é correspondido a uma conversão downstream — uma compra, inscrição, formulário de lead ou qualquer outra coisa que você conectar ao pipeline de encaminhamento de conversão.

{
  "id": "evt_2g8mPzKlYxRcBnQvFt5HwS",
  "type": "conversion",
  "created_at": "2026-05-22T14:38:42.193Z",
  "data": {
    "click_id": "clk_2g8kFqJxYwPaZcvAm3HsTr",
    "link_id": "lnk_2g3jQpRyXz4Mn8VbF7Hkl",
    "conversion_id": "cnv_2g8mPzKlYxRcBnQvFt5HwS",
    "value": 49.50,
    "currency": "EUR",
    "event_name": "purchase",
    "product_id": "sku_42",
    "metadata": {
      "order_id": "ord_12345",
      "is_new_customer": true
    },
    "attribution_window_minutes": 6,
    "forwarded_to": ["meta_capi", "ga4_mp"]
  },
  "workspace_id": "ws_12"
}

O click_id vincula de volta ao evento de clique original; você pode unir os dois no lado do servidor para reconstruir o caminho do clique à conversão. O attribution_window_minutes é o tempo decorrido entre o clique e o disparo da conversão, o que é útil para modelagem de atribuição.

A matriz forwarded_to informa a quais pixels de plataforma o Elido já enviou essa conversão. Se o seu assinante estiver encaminhando conversões para o seu próprio data warehouse, você pode usar isso para evitar contagem dupla em suas análises downstream.

O payload do evento link.created#

Eventos de ciclo de vida têm um formato mais fino — apenas o recurso e o ator:

{
  "id": "evt_2g8mPzKlYxRcBnQvFt5HwS",
  "type": "link.created",
  "created_at": "2026-05-22T14:38:42.193Z",
  "data": {
    "link": {
      "id": "lnk_2g3jQpRyXz4Mn8VbF7Hkl",
      "slug": "abc123",
      "short_url": "https://elido.me/abc123",
      "destination_url": "https://shop.example.com/spring",
      "domain": "elido.me",
      "tags": ["spring-2026", "newsletter"],
      "created_at": "2026-05-22T14:38:42.193Z",
      "created_by": "usr_42"
    }
  },
  "workspace_id": "ws_12"
}

link.updated inclui um snapshot previous ao lado do novo estado; link.deleted inclui o estado final do link no momento da exclusão. O esquema completo encontra-se no guia operacional /docs/guides/conversion-forwarding.

Verificação de assinatura#

Cada solicitação de webhook inclui três cabeçalhos HTTP:

Elido-Signature: t=1716392538,v1=4f3b2e1d9c8a7b6f5e4d3c2b1a0f9e8d7c6b5a49382716054 ...
Elido-Webhook-Id: evt_2g8kFqJxYwPaZcvAm3HsTr
Elido-Delivery-Attempt: 1

O esquema de assinatura segue o modelo da Stripe: HMAC-SHA256 sobre {timestamp}.{body} usando o segredo do webhook. O prefixo v1= é a versão do algoritmo de assinatura; novas versões do algoritmo são adicionadas antes de serem definidas como padrão, para que os assinantes possam verificar várias versões ao mesmo tempo.

Verificando em Go:

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "strings"
    "time"
)

func verify(sigHeader, body, secret string) bool {
    parts := strings.Split(sigHeader, ",")
    var t int64
    var v1 string
    for _, p := range parts {
        kv := strings.SplitN(p, "=", 2)
        if len(kv) != 2 { continue }
        switch kv[0] {
        case "t":
            fmt.Sscanf(kv[1], "%d", &t)
        case "v1":
            v1 = kv[1]
        }
    }
    if time.Since(time.Unix(t, 0)) > 5*time.Minute {
        return false // rejeitar solicitações obsoletas
    }
    mac := hmac.New(sha256.New, []byte(secret))
    fmt.Fprintf(mac, "%d.%s", t, body)
    expected := hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(v1))
}

A verificação de obsolescência de 5 minutos é a parte que a maioria dos assinantes esquece. Sem ela, um ataque de replay — um invasor que capturou uma solicitação válida e a reproduz posteriormente — teria sucesso porque a assinatura ainda é válida. Com a verificação de timestamp, a solicitação só é aceita dentro de uma janela de 5 minutos a partir do momento em que o Elido a emitiu.

A especificação da assinatura está documentada na cheat sheet da OWASP sobre segurança de webhook; não inventamos o padrão, apenas o implementamos.

Política de novas tentativas#

Esta é a parte onde a maioria das implementações de webhook se torna desleixada.

Um webhook dispara uma vez no caminho feliz: o assinante retorna 2xx, o despachante registra o sucesso, o evento está concluído. Os casos mais difíceis são respostas que não sejam 2xx, erros de rede e assinantes que respondem lentamente.

O cronograma de novas tentativas do Elido:

TentativaAtraso após o anteriorCumulativoStatus
10inicial
21s1sprimeira tentativa
330s31s
45m5m 31s
51h1h 5m 31s
66h7h 5m 31s
724h31h 5m 31sfinal

Após a 7ª tentativa (~31 horas após a primeira tentativa), o despachante desiste e emite um evento interno webhook.failed. O endpoint do assinante é marcado como degradado após três falhas consecutivas em quaisquer eventos; assinaturas degradadas recebem um orçamento reduzido de novas tentativas por 24 horas. Após 50 falhas consecutivas, a assinatura é pausada e o proprietário do espaço de trabalho é notificado.

O comportamento de novas tentativas respeita os cabeçalhos Retry-After do assinante. Se o seu endpoint estiver limitando a taxa do Elido (retornando 429 com Retry-After: 120), a próxima tentativa aguarda 120 segundos em vez dos 30s padrão do cronograma.

A falha em responder dentro de 10 segundos é tratada como um tempo limite (timeout) e conta como uma tentativa falha. O orçamento de 10 segundos é generoso de propósito — ele cobre a latência de inicialização fria (cold-start) em assinantes serverless — mas se o seu endpoint regularmente leva mais de 5 segundos, corrija isso primeiro; isso custará caro em volume de novas tentativas.

Idempotência#

Assinantes podem receber o mesmo evento mais de uma vez.

Isso não é um bug; é a consequência de como a entrega de mensagens distribuídas funciona. Se um assinante retorna um 504 porque seu backend estava lento, mas eventualmente processou o evento, o despachante fará uma nova tentativa; o assinante o receberá duas vezes e pode processá-lo duas vezes. O mesmo evento também pode disparar duas vezes se o despachante travar no meio da entrega e o evento for recolocado na fila.

A mitigação: cada evento tem um id único (o prefixo evt_…). Os assinantes devem armazenar os IDs que já processaram (uma pequena tabela chave-valor funciona; um TTL de 14 dias cobre a janela de novas tentativas com margem) e ignorar eventos cujo ID já viram antes.

CREATE TABLE webhook_processed_events (
    event_id TEXT PRIMARY KEY,
    received_at TIMESTAMPTZ DEFAULT now()
);

-- no seu manipulador:
INSERT INTO webhook_processed_events (event_id) VALUES ($1)
ON CONFLICT (event_id) DO NOTHING
RETURNING event_id;
-- se o RETURNING estiver vazio, você já processou este evento

O ON CONFLICT DO NOTHING é a verificação de idempotência barata. Se o insert retornar uma linha, esta é a primeira vez que você vê o evento; se não retornar nada, você já o processou.

Para assinantes de alto throughput (>1k eventos/seg), um Redis SETNX dedicado com TTL funciona da mesma maneira com custo menor do que uma linha Postgres.

Ordem de entrega#

Não há garantia de ordenação global. Eventos do mesmo link_id são despachados na ordem de envio, mas eventos de links diferentes podem chegar intercalados. Um evento click no tempo T+0 e um evento conversion no tempo T+10ms podem chegar ao seu assinante em qualquer ordem, dependendo do estado do pool de trabalhadores.

Os timestamps created_at são a autoridade para ordenação. Se o seu assinante precisa de ordenação estrita, ordene por created_at no lado do servidor antes de processar.

Para o caminho clique → conversão especificamente: o evento de conversão sempre referencia o click_id do evento de clique, então você pode uni-los no lado do servidor mesmo se chegarem fora de ordem.

Webhooks vs polling — o trade-off#

O post sobre webhooks vs polling para rastreamento de cliques cobre isso em detalhes. A resposta curta: webhooks são o padrão certo quando (a) você precisa de baixa latência na chegada do evento (<5 segundos), e (b) seu assinante pode ser alcançado pela internet pública com TLS. Polling é o padrão certo quando (a) você não precisa de tempo real, (b) você controla o data warehouse e só quer um lote diário/horário, ou (c) seu assinante está em uma rede que não aceita tráfego de entrada.

Para a maioria das equipes, webhooks são a resposta. A curva de novas tentativas lida com falhas transientes graciosamente; o esquema de assinatura lida com segurança; o modelo de idempotência lida com a duplicação de entrega. O trabalho é do lado do assinante — construir um manipulador robusto — e esse trabalho é pequeno em comparação com a construção de um pipeline de ingestão baseado em polling.

Ferramentas operacionais#

A página de webhook do painel mostra três coisas por assinatura:

  1. Histórico de entrega: cada evento enviado, o status HTTP que o assinante retornou, a latência e o próximo timestamp de nova tentativa (se houver).
  2. Replay: um botão por evento para reenviá-lo. Útil para testar alterações no seu manipulador.
  3. Endpoint de teste: um botão por assinatura para enviar um evento de teste sintético sem disparar um clique real. O evento de teste tem type: "test" e um payload fixo.

Os endpoints de replay e teste também são expostos como endpoints de API (POST /v1/webhooks/{id}/events/{evt_id}/replay e POST /v1/webhooks/{id}/test).

Para depuração de alto throughput, o guia de observabilidade cobre como conectar a entrega de webhook às suas próprias métricas — cada despacho é exportado como um contador Prometheus e um histograma.

Referências externas#

Leitura relacionada#

Experimente o Elido

Encurtador de URL hospedado na UE: domínios personalizados, análises profundas e API aberta. Plano gratuito — sem cartão de crédito.

Tags
webhooks de encurtador de url
webhook de clique de link
nova tentativa de webhook
assinatura de webhook
idempotência de webhook
entrega de evento
payload de webhook

Continuar lendo