# Webhooks



A plataforma faz um `POST` na sua URL quando algo acontece. Todos os envios passam pelo mesmo motor de entrega: a mensagem é gravada, cada destino recebe uma entrega com retries próprios e cada chamada HTTP fica registrada. Existem três origens:

| Origem                                           | Onde se configura                                              | Custo                               |
| ------------------------------------------------ | -------------------------------------------------------------- | ----------------------------------- |
| **Eventos da conta** (ex.: `qr.dynamic.scanned`) | **Webhooks** no [dashboard](https://dashboard.respawntech.dev) | Grátis                              |
| **Callbacks de jobs** (ex.: conversão de PDF)    | `webhook_url` em cada requisição                               | Grátis                              |
| **Webhook Relay** (seus próprios eventos)        | API do [Webhook Relay](/ferramentas/webhook-relay)             | Por entrega além da franquia mensal |

## Eventos da conta

Cadastre um endpoint em **Webhooks** no [dashboard](https://dashboard.respawntech.dev) e escolha os eventos. Cada endpoint tem o seu `secret`, mostrado na criação e depois pela opção de revelar o secret.

Esses endpoints são do [Webhook Relay](/ferramentas/webhook-relay#projetos): um endpoint de qualquer projeto que assine um evento da conta o recebe, grátis. A antiga API de webhooks da conta está obsoleta; veja [API legada da conta](/ferramentas/webhook-relay#api-legada).

<WebhookEventsTable />

## Requisição enviada

```http
POST <url do endpoint>
Content-Type: application/json
User-Agent: RespawnTech-Webhook/2.0
webhook-id: msg_2Vf8tK1pQ9zX
webhook-timestamp: 1790338000
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
X-Webhook-Signature: sha256=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
```

| Header                | Conteúdo                                                                                                 |
| --------------------- | -------------------------------------------------------------------------------------------------------- |
| `webhook-id`          | Identificador da mensagem. É o **mesmo em todos os retries**: use para descartar duplicatas.             |
| `webhook-timestamp`   | Momento do envio desta tentativa, em segundos Unix.                                                      |
| `webhook-signature`   | `v1,<base64>`, uma assinatura por secret ativo, separadas por espaço (duas durante uma troca de secret). |
| `X-Webhook-Signature` | Assinatura legada `sha256=<hex>`, sem timestamp. Mantida para receptores antigos.                        |

Os headers seguem o padrão [Standard Webhooks](https://www.standardwebhooks.com), então qualquer biblioteca desse padrão valida a requisição. Endpoints do Webhook Relay podem ter headers extras (`custom_headers`).

### Corpo

Eventos da conta e mensagens do Webhook Relay usam o mesmo envelope:

```json
{
  "event_id": "550e8400-e29b-41d4-a716-446655440000",
  "type": "qr.dynamic.scanned",
  "timestamp": 1790338000,
  "payload": {
    "resource_id": "…",
    "slug": "promo-verao",
    "ip": "hash_anônimo",
    "user_agent": "Mozilla/5.0 …",
    "referer": "https://…"
  }
}
```

O formato de `payload` varia conforme o `type`. Callbacks de jobs mantêm o corpo próprio (veja [Callback de jobs](#pdf-callback)), e mensagens recebidas de provedores pelo Webhook Relay chegam com o corpo original do provedor, sem alteração.

## Validar a assinatura

O conteúdo assinado é `webhook-id + "." + webhook-timestamp + "." + corpo bruto`, com HMAC-SHA256 e o resultado em base64. Para validar:

1. Leia o **corpo bruto**, antes de qualquer parse de JSON (reformatar o JSON muda a assinatura).
2. Recuse timestamps com mais de <Limit name="webhookSignatureTolerance" /> de diferença do seu relógio (proteção contra replay).
3. Calcule a assinatura e compare, **em tempo constante**, com cada item de `webhook-signature`. Basta um bater.

Com uma biblioteca Standard Webhooks, use o `standard_webhooks_secret` (formato `whsec_…`). Sem biblioteca, a chave do HMAC são os bytes do `secret` bruto.

<Tabs groupId="code-language" persist items={['JavaScript', 'Python', 'Go']}>
  <Tab value="JavaScript">
    ```js
    // npm i standardwebhooks
    import express from 'express';
    import { Webhook } from 'standardwebhooks';

    const wh = new Webhook(process.env.WEBHOOK_SECRET); // whsec_…
    const app = express();

    app.post('/hook', express.raw({ type: '*/*' }), (req, res) => {
      let event;
      try {
        event = wh.verify(req.body, req.headers); // lança erro se inválido
      } catch {
        return res.sendStatus(401);
      }
      // processe `event` de forma assíncrona e responda rápido
      res.sendStatus(204);
    });
    ```
  </Tab>

  <Tab value="Python">
    ```python
    # pip install standardwebhooks
    import os
    from flask import Flask, request
    from standardwebhooks.webhooks import Webhook

    wh = Webhook(os.environ["WEBHOOK_SECRET"])  # whsec_…
    app = Flask(__name__)

    @app.post("/hook")
    def hook():
        try:
            event = wh.verify(request.get_data(), dict(request.headers))
        except Exception:
            return "", 401
        # processe `event` de forma assíncrona e responda rápido
        return "", 204
    ```
  </Tab>

  <Tab value="Go">
    ```go
    // Sem biblioteca: secret é o valor bruto (não o whsec_).
    func verify(secret string, r *http.Request, body []byte) bool {
    	id, ts := r.Header.Get("webhook-id"), r.Header.Get("webhook-timestamp")
    	t, err := strconv.ParseInt(ts, 10, 64)
    	if err != nil || math.Abs(time.Since(time.Unix(t, 0)).Seconds()) > 300 {
    		return false
    	}
    	mac := hmac.New(sha256.New, []byte(secret))
    	mac.Write([]byte(id + "." + ts + "."))
    	mac.Write(body)
    	want := "v1," + base64.StdEncoding.EncodeToString(mac.Sum(nil))
    	for _, sig := range strings.Fields(r.Header.Get("webhook-signature")) {
    		if hmac.Equal([]byte(sig), []byte(want)) {
    			return true
    		}
    	}
    	return false
    }
    ```
  </Tab>
</Tabs>

<Callout title="Receptores antigos">
  Quem já valida `X-Webhook-Signature` continua funcionando: é `sha256=` + o HMAC-SHA256 em hexadecimal do corpo bruto, com o `secret` bruto como chave. Esse header não tem timestamp; prefira `webhook-signature` em integrações novas.
</Callout>

## Secrets e rotação

| Destino                          | Onde obter o secret                                                                                                                                 |
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| Endpoint de eventos da conta     | Na criação, ou pela opção de revelar o secret em **Webhooks** no dashboard.                                                                         |
| Endpoint do Webhook Relay        | Na criação, ou em [Revelar secret](/ferramentas/webhook-relay#webhooks-endpoints-secret).                                                           |
| Callback de jobs (`webhook_url`) | Secret de assinatura da conta, único para todos os callbacks: `GET /v1/account/webhooks/signing-secret` (rota da conta, com a sessão do dashboard). |

Todo secret aparece em duas formas: `secret` (bruto) e `standard_webhooks_secret` (`whsec_` + base64 dos mesmos bytes). As duas validam as mesmas assinaturas.

Ao [trocar o secret](/ferramentas/webhook-relay#webhooks-endpoints-rotate_secret) de um endpoint, o antigo continua assinando por <Limit name="webhookSecretOverlap" />: nesse período `webhook-signature` traz as duas assinaturas, e você atualiza o receptor sem perder eventos.

## Entrega e retries

Só uma resposta **2xx** conta como sucesso. Redirecionamentos (3xx) não são seguidos, e 3xx, 4xx, 5xx, timeouts e erros de rede são tentados de novo neste calendário:

| Tentativa | Espera antes dela |
| --------- | ----------------- |
| 1         | —                 |
| 2         | 5 segundos        |
| 3         | 5 minutos         |
| 4         | 30 minutos        |
| 5         | 2 horas           |
| 6         | 5 horas           |
| 7 e 8     | 10 horas          |

São cerca de 27 horas no total, com variação de ±10%. Casos especiais:

* **`429` e `503`** com `Retry-After`: a próxima tentativa respeita o header quando ele pede mais tempo (até 10 horas).
* **`410 Gone`**: a entrega para e o endpoint é desativado.
* Depois da última tentativa, a entrega fica `exhausted`. No Webhook Relay, dá para [reenviar a mensagem](/ferramentas/webhook-relay#webhooks-messages-retry) ou [recuperar as falhas de um endpoint](/ferramentas/webhook-relay#webhooks-endpoints-recover).

Boas práticas no receptor:

* Responda **2xx** o mais rápido possível e processe o evento de forma assíncrona.
* **Descarte duplicatas pelo `webhook-id`**: uma entrega pode chegar mais de uma vez (ex.: seu servidor processou, mas a resposta se perdeu).
* Não dependa da ordem de chegada; use o `timestamp` do corpo quando a ordem importar.

### Desativação automática

Um endpoint que falha sem parar por <Limit name="webhookDisableAfter" /> é desativado automaticamente (`is_active: false`, com `disabled_reason`), assim como um que responde `410`. Depois de corrigir o receptor, reative o endpoint (no dashboard ou com `is_active: true` no Webhook Relay); isso zera a sequência de falhas.

O histórico de mensagens, entregas e tentativas fica guardado por <Limit name="webhookLogRetention" />.

## Callback de jobs

Em `POST /convert` ou `POST /batch` do [PDF Convert](/ferramentas/pdf-convert), envie `webhook_url` no body. Quando o job termina, a API faz um `POST` nessa URL:

```json
{
  "job_id": "job_abc123",
  "success": true,
  "result_url": "https://…",
  "metadata": { "conversion_id": "…" }
}
```

Em falha, `success` é `false` e o campo `error` traz a mensagem. O callback tem os mesmos headers, assinatura e retries dos outros webhooks, assinado com o **secret de assinatura da conta** (veja [Secrets e rotação](#secrets)). A `webhook_url` precisa ser uma URL pública `http` ou `https`; caso contrário a requisição é recusada com `webhook.invalid_url`.

## Seus próprios eventos

Para enviar webhooks da **sua** aplicação para os seus clientes, com a mesma assinatura, retries e log, ou para receber webhooks do Stripe, GitHub e outros e redistribuí-los, use o [Webhook Relay](/ferramentas/webhook-relay).
