# Webhook Relay



<ToolHero tool="webhook-relay" />

## Visão geral

O Webhook Relay é a infraestrutura de webhooks da sua aplicação. Em vez de manter filas, retries e assinaturas, você publica um evento com uma chamada e a plataforma entrega em cada endpoint inscrito, com o mesmo motor que envia os [webhooks da conta](/webhooks).

* **Projeto:** agrupa endpoints, fontes, rotas, tipos de evento e mensagens, para separar configurações (uma loja, um cliente, um ambiente). Toda conta tem um projeto padrão; veja [Projetos](#projetos).
* **Endpoint:** a URL que recebe os eventos (normalmente do seu cliente, ou um canal do Slack, Discord ou Teams), com os eventos que assina e um `secret` próprio.
* **Mensagem:** um evento que você publica (`event_type` + `payload`).
* **Entrega:** uma mensagem a caminho de um endpoint. Uma mensagem para 3 endpoints gera 3 entregas, cada uma com seus retries.
* **Tentativa:** cada chamada HTTP de uma entrega, registrada com status, latência e o começo da resposta.
* **Fonte e rota:** para **receber** webhooks de terceiros (Stripe, GitHub…) numa URL da plataforma e redistribuí-los para os seus endpoints. Veja [Receber webhooks de terceiros](#ingestao).

Todas as requisições enviadas seguem o padrão [Standard Webhooks](https://www.standardwebhooks.com) (headers `webhook-id`, `webhook-timestamp` e `webhook-signature`). O formato, a verificação da assinatura e o calendário de retries estão no guia [Webhooks](/webhooks).

## Primeiros passos

<Steps>
  <Step>
    **Crie um endpoint** com a URL de destino e os eventos que ele assina. Guarde o `secret` da resposta (use `standard_webhooks_secret` com as bibliotecas oficiais). Sem `project_id`, ele vai para o projeto padrão.
  </Step>

  <Step>
    **Teste a conexão** com o [ping de teste](#webhooks-endpoints-test): um evento `webhook.test` grátis.
  </Step>

  <Step>
    **Envie uma mensagem** com `event_type` e `payload`. Use `X-Dry-Run: true` antes para ver quantas entregas ela gera e quanto custa.
  </Step>

  <Step>
    **Verifique a assinatura** no receptor e responda `2xx` rápido. Veja os exemplos em [Validar a assinatura](/webhooks#verificar-assinatura).
  </Step>

  <Step>
    **Acompanhe** as entregas no [log de mensagens](#mensagens) e reenvie o que falhou.
  </Step>
</Steps>

<EndpointList tool="webhook-relay" />

## Custos

<PricingTable tool="webhook-relay" group="per-delivery" />

<PricingTable tool="webhook-relay" group="free-quota" />

A cobrança é feita no envio, por mensagem, pelas entregas que passam da franquia (`charged_units`, em unidades de 1/10.000 de crédito). Se a mensagem não puder ser gravada, o valor é estornado. O `payload` é validado antes de qualquer cobrança.

A franquia e a cobrança são **por conta**: todos os projetos dividem a mesma franquia mensal, e uma entrega custa o mesmo em qualquer projeto.

<Callout title="Leituras do log">
  Configurar projetos, endpoints, tipos de evento, fontes e rotas é grátis. As rotas de [listar](#webhooks-messages-list) e [consultar](#webhooks-messages-get) mensagens são leituras medidas: entram na [franquia diária de leituras](/cobranca-e-limites#franquia-diaria) e, depois dela, custam o valor da tabela de leituras.
</Callout>

### Consultar uso do mês

<Endpoint id="webhooks.usage">
  Mostra o mês atual (UTC, de `period_start` a `period_end`): a franquia (`free_deliveries`), as entregas já usadas por API e ingestão (`used_deliveries`) e o que resta (`free_left`). `delivery_units` e `read_units` são os preços atuais de uma entrega e de uma leitura do log, em unidades; `blocked_retention_days` é quanto tempo uma entrega espera por créditos (veja [Sem créditos](#sem-creditos)).
</Endpoint>

## Projetos

Tudo o que você configura no Webhook Relay pertence a um projeto: endpoints, fontes (e suas rotas), tipos de evento, mensagens e o layout do canvas no dashboard. Use projetos para separar configurações, por exemplo uma por loja ou por cliente.

* Toda conta tem um **projeto padrão** (`is_default: true`, chamado `Default`), criado no primeiro uso. Ele não pode ser excluído.
* `project_id` é **opcional** em toda a API: sem ele, criações e envios usam o projeto padrão. Integrações que não conhecem projetos continuam funcionando.
* Uma mensagem só chega aos endpoints do projeto dela e é validada com os tipos de evento desse projeto. Nomes de tipo de evento são únicos por projeto.
* As listagens (`GET /endpoints`, `/sources`, `/event-types`, `/routes` e `/messages`) aceitam `?project_id=` como filtro; sem ele, trazem todos os projetos.
* Eventos da plataforma (ex.: `qr.dynamic.scanned`) geram uma mensagem em cada projeto que tem endpoints inscritos, sempre grátis. Callbacks de jobs (`webhook_url`) não pertencem a nenhum projeto (`project_id: null`).
* Limites e cobrança continuam por conta: veja [Limites](#limites). Por padrão não há limite de projetos.

### Listar projetos

<Endpoint id="webhooks.projects.list" example={{ query: { q: 'loja', page: '1', page_size: '20' } }}>
  Paginado e com busca: `q` procura no nome (sem diferenciar maiúsculas), `page` começa em 1 e `page_size` vai até <Limit name="webhookProjectsPageMax" /> (padrão <Limit name="webhookProjectsPageDefault" />). Cada projeto traz `stats`: fontes, endpoints, mensagens das últimas 24 horas, entregas do mês e `failing_endpoints` (endpoints falhando ou desativados automaticamente; um endpoint pausado por você não conta).
</Endpoint>

### Criar projeto

<Endpoint id="webhooks.projects.create" example={{ body: `{ "name": "Loja", "description": "Eventos da loja virtual" }` }}>
  `name` (obrigatório) tem até <Limit name="webhookProjectNameMax" /> caracteres; `description`, até <Limit name="webhookProjectDescriptionMax" />.
</Endpoint>

### Consultar projeto

<Endpoint id="webhooks.projects.get" />

### Alterar projeto

<Endpoint id="webhooks.projects.update" example={{ body: `{ "name": "Loja principal" }` }}>
  Envie só os campos que quer mudar.
</Endpoint>

### Excluir projeto

<Endpoint id="webhooks.projects.delete">
  Exclui os endpoints do projeto (as entregas pendentes são canceladas), as fontes, as rotas, os tipos de evento e o layout. As mensagens continuam no log geral, com `project_id: null`.
</Endpoint>

### Duplicar projeto

<Endpoint id="webhooks.projects.duplicate" example={{ body: `{ "name": "Loja (cópia)" }` }}>
  Cria um projeto novo com a mesma descrição e cópias de tudo, já ligadas entre si:

  * **Endpoints** com **secrets novos** (mesma URL, eventos, headers, limite de envio, formato e estado ativo; o histórico de falhas não é copiado).
  * **Fontes** com **`ingest_url` novas** (mesmas configurações e o mesmo secret do provedor). Cadastre as URLs novas no provedor.
  * Rotas, tipos de evento e o layout do canvas.

  Mensagens, entregas e contadores não são copiados. Os limites da conta são conferidos antes: se a cópia passaria de algum, a resposta é `webhook.limit_reached` e nada é criado.
</Endpoint>

## Endpoints

### Criar endpoint

<Endpoint
  id="webhooks.endpoints.create"
  example={{ body: `{
"url": "https://api.example.com/hooks/respawn",
"description": "Pedidos da loja",
"events": ["order.paid", "order.refunded"],
"custom_headers": { "X-Tenant": "loja-1" }
}` }}
>
  * `url` (obrigatório): URL pública `http` ou `https`. Endereços privados, locais ou de metadados de nuvem são recusados (`webhook.invalid_url`).
  * `events`: tipos de evento que o endpoint recebe das mensagens da API e da plataforma; `["*"]` assina todos. Sem eventos, o endpoint só recebe o que as [rotas](#ingestao) mandam para ele.
  * `project_id`: projeto do endpoint (padrão: o projeto padrão). Um endpoint não muda de projeto.
  * `format`: `raw` (padrão), `slack`, `discord` ou `teams`. Veja [Slack, Discord e Teams](#formatos).
  * `custom_headers`: até <Limit name="webhookCustomHeadersMax" /> headers extras enviados em toda entrega (ex.: um token do seu cliente). Headers de assinatura, `Content-Type`, `User-Agent` e `Host` não podem ser sobrescritos.
  * `rate_limit_per_sec`: teto de envios por segundo para este endpoint (`0` = sem teto próprio).
  * O `secret` vem só nesta resposta; depois, use [Revelar secret](#webhooks-endpoints-secret). Até <Limit name="webhookEndpointsMax" /> endpoints por conta, somando todos os projetos.
</Endpoint>

### Listar endpoints

<Endpoint id="webhooks.endpoints.list" example={{ query: { project_id: '3e7c1a9f-5b2d-4c8e-a6f0-9d1b4e7a2c53' } }}>
  Filtro opcional por `project_id`. Um endpoint desativado automaticamente traz `is_active: false` e `disabled_reason`; um que está falhando traz `failing_since`.
</Endpoint>

### Consultar endpoint

<Endpoint id="webhooks.endpoints.get" />

### Alterar endpoint

<Endpoint id="webhooks.endpoints.update" example={{ body: `{ "events": ["*"], "is_active": true }` }}>
  Envie só os campos que quer mudar (inclusive `format`). `is_active: true` reativa um endpoint desativado e zera a sequência de falhas.
</Endpoint>

### Excluir endpoint

<Endpoint id="webhooks.endpoints.delete">
  As entregas pendentes para o endpoint são canceladas.
</Endpoint>

### Revelar secret

<Endpoint id="webhooks.endpoints.secret">
  `secret` é a chave bruta (HMAC do header legado `X-Webhook-Signature`); `standard_webhooks_secret` é a mesma chave no formato `whsec_…` aceito pelas bibliotecas Standard Webhooks.
</Endpoint>

### Trocar secret

<Endpoint id="webhooks.endpoints.rotate_secret">
  Durante <Limit name="webhookSecretOverlap" /> as entregas levam duas assinaturas em `webhook-signature` (secret novo e antigo), para você trocar o secret do receptor sem perder eventos.
</Endpoint>

### Enviar ping de teste

<Endpoint id="webhooks.endpoints.test">
  Envia um evento `webhook.test` assinado, grátis e fora da franquia. Acompanhe o resultado em [Consultar mensagem](#webhooks-messages-get) com o `message_id` devolvido.
</Endpoint>

### Reenviar entregas com falha

<Endpoint id="webhooks.endpoints.recover" example={{ query: { since: '2026-09-24T00:00:00Z' } }}>
  Reenvia para este endpoint todas as entregas que terminaram com falha (`failed`, `exhausted` ou `canceled`) desde `since` (RFC 3339), até <Limit name="webhookRecoverMax" /> por chamada. Use depois de corrigir um receptor que ficou fora do ar. Cada entrega reenviada é cobrada como nova.
</Endpoint>

### Slack, Discord e Teams

Com `format` em `slack`, `discord` ou `teams`, cadastre como `url` o webhook de entrada do canal (Incoming Webhook do Slack ou do Teams, webhook do Discord). A plataforma transforma cada entrega numa mensagem de texto na hora de enviar:

* O corpo vira `{"text": "…"}` (Slack e Teams) ou `{"content": "…"}` (Discord), com `Content-Type: application/json`.
* O texto traz o tipo do evento em negrito (e o nome da fonte, nas mensagens recebidas de um provedor), seguido do corpo num bloco de código: JSON formatado ou o texto bruto.
* Textos longos são cortados com `…` para caber no limite da plataforma: <Limit name="webhookSlackTextMax" /> caracteres no Slack, <Limit name="webhookDiscordTextMax" /> no Discord e <Limit name="webhookTeamsTextMax" /> no Teams.
* Os headers de assinatura continuam sendo enviados, calculados sobre esse corpo; os headers originais de uma fonte com `forward_original_headers` não são repassados.

A mensagem guardada no log não muda: o log, o reenvio e a recuperação funcionam igual aos endpoints `raw`.

## Mensagens

### Enviar mensagem

<Endpoint
  id="webhooks.messages.send"
  example={{ headers: { 'X-Dry-Run': 'true' }, body: `{
"event_type": "order.paid",
"payload": { "order_id": "ped_1042", "amount": 199.9 },
"idempotency_key": "order-paid-ped_1042"
}` }}
>
  * `event_type` (obrigatório): `^[a-z0-9][a-z0-9._-]{0,127}$`, ex.: `order.paid`. Vai para os endpoints do projeto que assinam esse tipo (ou `*`).
  * `payload` (obrigatório): qualquer JSON, até <Limit name="webhookPayloadMax" />. Se o tipo de evento tiver [JSON Schema](#tipos-de-evento) no projeto, o payload precisa segui-lo (`webhook.payload_invalid`, sem cobrança).
  * `project_id` (opcional): projeto da mensagem (padrão: o projeto padrão).
  * `endpoint_ids` (opcional): restringe o envio a estes endpoints, entre os do projeto que assinam o evento.
  * `idempotency_key` (ou o header `X-Idempotency-Key`): repetir a chave devolve a primeira mensagem com `duplicate: true` e não cobra nada. Use para reenviar com segurança depois de um timeout.
  * Com `X-Dry-Run: true`, nada é enviado nem cobrado: a resposta mostra `deliveries`, `free_deliveries` e `charged_units`.

  O receptor recebe o envelope `{ "event_id", "type", "timestamp", "payload" }` descrito em [Webhooks](/webhooks#corpo); `event_id` é o `message_id` e `webhook-id` é `msg_<message_id>`.
</Endpoint>

### Enviar mensagens em lote

<Endpoint
  id="webhooks.messages.batch"
  example={{ body: `{
"messages": [
  { "event_type": "order.paid", "payload": { "order_id": "ped_1042", "amount": 199.9 }, "idempotency_key": "order-paid-ped_1042" },
  { "event_type": "order.paid", "payload": { "order_id": "ped_1043" }, "idempotency_key": "order-paid-ped_1043" }
]
}` }}
>
  Até <Limit name="webhookBatchMax" /> mensagens, cada uma com os mesmos campos do envio único. O `project_id` fica no nível de cima do body e vale para todas as mensagens do lote. Cada mensagem é validada, deduplicada e cobrada sozinha: uma falha aparece em `error` no item dela e não impede as outras. Aceita `X-Dry-Run: true`.
</Endpoint>

### Listar mensagens

<Endpoint id="webhooks.messages.list" example={{ query: { event_type: 'order.paid', failed: 'true', limit: '20' } }}>
  Mais recentes primeiro, com as entregas de cada mensagem (sem o corpo). Paginação: passe o `next_before` recebido em `before`. `limit` de 1 a 100 (padrão 20). As mensagens ficam no log por <Limit name="webhookLogRetention" />.

  * `project_id`: só as mensagens desse projeto. Sem ele, o log traz todos os projetos, além das mensagens sem projeto (`project_id: null`: callbacks de jobs e mensagens de projetos excluídos).
  * `event_type` e `endpoint_id`: filtros exatos.
  * `failed=true`: só mensagens com ao menos uma entrega `failed`, `exhausted` ou `blocked_no_credits`.
  * `q`: busca, de 1 a <Limit name="webhookSearchQueryMax" /> caracteres, combinável com os outros filtros. Um UUID encontra a mensagem com esse id. Outro texto encontra a chave de idempotência ou o tipo de evento iguais ao texto, ou o texto dentro do corpo (sem diferenciar maiúsculas, exceto letras acentuadas); a busca no corpo só olha as mensagens dos últimos <Limit name="webhookSearchBodyWindow" />.
</Endpoint>

### Consultar mensagem

<Endpoint id="webhooks.messages.get">
  Traz o corpo enviado e, por entrega, cada tentativa: `http_status`, `latency_ms`, o começo da resposta (`response_body`) ou o erro de rede (`error`). `next_attempt_at` aparece enquanto a entrega está pendente.

  | `status` da entrega  | Significado                                                                                              |
  | -------------------- | -------------------------------------------------------------------------------------------------------- |
  | `pending`            | Aguardando a próxima tentativa (`next_attempt_at`).                                                      |
  | `sending`            | Tentativa em andamento.                                                                                  |
  | `succeeded`          | O endpoint respondeu `2xx`.                                                                              |
  | `exhausted`          | Todas as tentativas falharam. Pode ser reenviada.                                                        |
  | `failed`             | Parou de vez: endpoint respondeu `410 Gone`, foi desativado ou a URL ficou inválida. Pode ser reenviada. |
  | `canceled`           | Cancelada, por exemplo porque o endpoint foi excluído com a entrega pendente.                            |
  | `blocked_no_credits` | Mensagem recebida de um provedor sem saldo para a entrega (veja [Sem créditos](#sem-creditos)).          |
</Endpoint>

### Reenviar mensagem

<Endpoint id="webhooks.messages.retry" example={{ body: `{ "endpoint_id": "8d2f4a61-3c7b-4e9a-b1d5-6f0e2a9c7b34" }` }}>
  Sem body, envia a mensagem de novo para os endpoints cuja entrega terminou com falha (`failed`, `exhausted` ou `canceled`) e que estão ativos.

  Com `endpoint_id`, envia só para esse endpoint, qualquer que tenha sido o resultado anterior (mesmo que a entrega tenha dado certo). Ele precisa ser um endpoint ativo do projeto da mensagem; caso contrário, e para mensagens sem projeto, a resposta é `400`. As entregas novas são cobradas como um envio.
</Endpoint>

## Tipos de evento

Declarar tipos de evento é opcional: sem declaração, qualquer `event_type` válido é aceito. Declare para documentar os eventos da sua integração e, com `schema`, validar o `payload` antes de cobrar e enviar. Cada tipo pertence a um projeto e só vale para as mensagens desse projeto.

### Declarar tipo de evento

<Endpoint
  id="webhooks.event_types.create"
  example={{ body: `{
"name": "order.paid",
"description": "Pedido pago",
"schema": {
  "type": "object",
  "required": ["order_id", "amount"],
  "properties": {
    "order_id": { "type": "string" },
    "amount": { "type": "number" }
  }
}
}` }}
>
  `schema` é um JSON Schema aplicado ao `payload` de cada envio desse tipo; um payload fora do schema responde `webhook.payload_invalid` e não é cobrado. `project_id` é opcional (padrão: o projeto padrão); o nome é único dentro do projeto. Até <Limit name="webhookEventTypesMax" /> tipos por conta, somando todos os projetos.
</Endpoint>

### Listar tipos de evento

<Endpoint id="webhooks.event_types.list">
  Filtro opcional por `project_id`.
</Endpoint>

### Excluir tipo de evento

<Endpoint id="webhooks.event_types.delete">
  Os envios desse tipo continuam aceitos, sem validação de schema.
</Endpoint>

## Receber webhooks de terceiros

Uma **fonte** é uma URL da plataforma (`ingest_url`) que você cadastra no provedor (Stripe, GitHub, Shopify…). Cada evento recebido tem a assinatura do provedor verificada, é gravado e redistribuído pelas **rotas** da fonte para os seus endpoints, com a mesma assinatura, retries e log das mensagens da API. Assim, um único webhook do provedor pode alimentar vários serviços, e o provedor nunca fica esperando pelo seu receptor.

| `provider`          | Como a requisição é verificada                                                                                                  |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `stripe`            | Header `Stripe-Signature` com o signing secret do Stripe (`whsec_…`), tolerância de <Limit name="webhookSignatureTolerance" />. |
| `github`            | Header `X-Hub-Signature-256` com o secret do webhook do GitHub.                                                                 |
| `shopify`           | Header `X-Shopify-Hmac-Sha256` com o secret do app Shopify.                                                                     |
| `hmac_generic`      | Header `X-Webhook-Signature: sha256=<hex>` (HMAC-SHA256 do corpo).                                                              |
| `standard_webhooks` | Headers `webhook-id`, `webhook-timestamp` e `webhook-signature`.                                                                |
| `basic_auth`        | Header `Authorization: Basic …`; o secret é `usuario:senha`.                                                                    |
| `none`              | Sem verificação: qualquer um com a URL pode enviar. Evite em produção.                                                          |

`hmac_generic`, `standard_webhooks` e `basic_auth` servem para qualquer sistema que não esteja na lista: escolha o que o sistema de origem sabe enviar.

### Criar fonte de ingestão

<Endpoint
  id="webhooks.sources.create"
  example={{ body: `{
"name": "Stripe produção",
"provider": "stripe",
"verification_secret": "whsec_...",
"event_type_path": "type",
"dedupe_key_path": "id"
}` }}
>
  * `name` e `provider` (obrigatórios); `project_id` é opcional (padrão: o projeto padrão).
  * `verification_secret`: o secret que o provedor usa para assinar. Ele nunca volta nas respostas (`has_verification_secret`). Pode ficar para depois, porque vários provedores só mostram o secret depois que você cadastra a `ingest_url`: enquanto ele não for definido em [Definir secret do provedor](#webhooks-sources-verification_secret), a fonte responde `401` a toda requisição (exceto com `provider: "none"`).
  * `event_type_path`: caminho no JSON recebido de onde vem o tipo do evento (ex.: `type`, `data.object.status`). Sem ele, o tipo é `ingest`.
  * `dedupe_key_path`: caminho do identificador do evento (ex.: `id`). Um reenvio do provedor com a mesma chave responde `200` sem gerar novas entregas.
  * `forward_original_headers`: repassa os headers do provedor (ex.: `Stripe-Signature`) junto com o corpo idêntico, além da nossa assinatura. Útil quando o seu receptor já valida a assinatura do provedor. Cookies, `Authorization` e headers de proxy nunca são repassados.
  * Cadastre a `ingest_url` da resposta no provedor. Até <Limit name="webhookSourcesMax" /> fontes por conta, somando todos os projetos.
</Endpoint>

### Listar fontes

<Endpoint id="webhooks.sources.list">
  Filtro opcional por `project_id`.
</Endpoint>

### Consultar fonte

<Endpoint id="webhooks.sources.get" />

### Alterar fonte

<Endpoint id="webhooks.sources.update" example={{ body: `{ "provider": "hmac_generic", "forward_original_headers": true }` }}>
  Envie só os campos que quer mudar, inclusive `provider` (por exemplo, para passar uma fonte `none` a `hmac_generic`). Ao trocar para um provedor com assinatura, envie o `verification_secret` na mesma requisição, para a fonte não ficar sem secret (sem ele, ela responde `401`). Para trocar só o secret, use [Definir secret do provedor](#webhooks-sources-verification_secret); um secret em branco é recusado. Uma fonte não muda de projeto.
</Endpoint>

### Excluir fonte

<Endpoint id="webhooks.sources.delete">
  As rotas da fonte são excluídas junto e a `ingest_url` para de responder.
</Endpoint>

### Trocar URL de ingestão

<Endpoint id="webhooks.sources.rotate_token">
  Gera uma nova `ingest_url`; a anterior deixa de funcionar na hora. Atualize a URL no provedor logo em seguida.
</Endpoint>

### Definir secret do provedor

<Endpoint id="webhooks.sources.verification_secret" example={{ body: `{ "secret": "whsec_..." }` }}>
  Um secret vazio só é aceito com `provider: "none"`.
</Endpoint>

### Consultar último evento recebido

<Endpoint id="webhooks.sources.sample">
  Devolve o último corpo recebido pela fonte, com o tipo de evento lido e a hora. Útil para descobrir os campos do provedor antes de configurar `event_type_path`, `dedupe_key_path` e os filtros das rotas.
</Endpoint>

### Enviar evento de teste pela fonte

<Endpoint id="webhooks.sources.test" example={{ body: `{ "event_type": "invoice.paid" }` }}>
  Faz um evento passar pela fonte como se o provedor o tivesse enviado: o tipo é lido em `event_type_path` e as rotas (com os filtros) decidem para onde ele vai. O corpo é o último recebido pela fonte ou, se ela ainda não recebeu nada, um exemplo do provedor. `event_type` (opcional) é gravado no caminho de `event_type_path`; se o corpo não tiver esse campo, o tipo usado é `test.event`.

  É grátis e fica fora da franquia mensal. Não verifica assinatura nem deduplica, mas conta no limite de <Limit name="webhookIngestPerMinute" /> requisições por minuto da fonte.
</Endpoint>

### Criar rota

<Endpoint
  id="webhooks.routes.create"
  example={{ body: `{
"source_id": "a6d1f8c3-2e9b-4c7a-9f5d-3b0e6a1c8d27",
"endpoint_id": "8d2f4a61-3c7b-4e9a-b1d5-6f0e2a9c7b34",
"event_types": ["checkout.session.completed", "invoice.paid"]
}` }}
>
  Liga uma fonte a um endpoint **do mesmo projeto** (fonte e endpoint de projetos diferentes respondem `400`). `event_types` restringe a rota a esses tipos, até <Limit name="webhookRouteEventTypesMax" />; vazio, a rota leva todos os eventos da fonte. Até <Limit name="webhookRoutesPerSourceMax" /> rotas por fonte.
</Endpoint>

### Listar rotas

<Endpoint id="webhooks.routes.list" example={{ query: { source_id: 'a6d1f8c3-2e9b-4c7a-9f5d-3b0e6a1c8d27' } }}>
  Filtros opcionais por `source_id` e `project_id`.
</Endpoint>

### Alterar rota

<Endpoint id="webhooks.routes.update" example={{ body: `{ "event_types": ["invoice.paid"], "is_active": false }` }}>
  Muda os tipos de evento da rota (lista vazia = todos) ou a pausa com `is_active: false`. Envie só os campos que quer mudar.
</Endpoint>

### Excluir rota

<Endpoint id="webhooks.routes.delete" />

### Receber evento de um provedor

<Endpoint id="webhooks.ingest" example={{ pathParams: { token: 'src_4f8a2c6e1b9d3f7a5c0e8b2d6f4a1c9e7b3d5f0a2c8e6b4d' }, headers: { 'Stripe-Signature': 't=1790338000,v1=5257a869…' }, body: `{ "id": "evt_1Q2w3E", "type": "checkout.session.completed", "data": { "object": { "id": "cs_test_a1B2" } } }` }}>
  É a URL que o provedor chama; você não precisa chamá-la. O corpo é guardado e repassado byte a byte, com o `Content-Type` original. As respostas são pensadas para o provedor decidir se tenta de novo:

  | Status | Quando                                                                               |
  | ------ | ------------------------------------------------------------------------------------ |
  | `200`  | Aceito, duplicado (reenvio do provedor) ou sem rota para o tipo.                     |
  | `401`  | Assinatura inválida, ou secret do provedor ainda não definido; nada é gravado.       |
  | `404`  | Fonte desconhecida ou desativada, ou ferramenta não liberada na conta.               |
  | `429`  | Mais de <Limit name="webhookIngestPerMinute" /> requisições por minuto para a fonte. |
  | `5xx`  | Falha ao gravar; o provedor deve tentar de novo.                                     |
</Endpoint>

### Exemplo: Stripe

<Steps>
  <Step>
    [Crie um endpoint](#webhooks-endpoints-create) para cada serviço seu que precisa dos eventos do Stripe. Os eventos chegam pelas rotas, então `events` pode ficar vazio.
  </Step>

  <Step>
    [Crie uma fonte](#webhooks-sources-create) com `provider: "stripe"`, `event_type_path: "type"` e `dedupe_key_path: "id"`, ainda sem o secret.
  </Step>

  <Step>
    No painel do Stripe, adicione um endpoint de webhook com a `ingest_url` e copie o **signing secret** (`whsec_…`) para [Definir secret do provedor](#webhooks-sources-verification_secret).
  </Step>

  <Step>
    [Crie uma rota](#webhooks-routes-create) da fonte para cada endpoint, com `event_types` quando um serviço só precisa de alguns eventos. Confira o caminho com [Enviar evento de teste pela fonte](#webhooks-sources-test).
  </Step>
</Steps>

Os seus serviços passam a receber os eventos assinados pela plataforma (valide com o secret de cada endpoint). Se preferirem validar a assinatura do próprio Stripe, ative `forward_original_headers` na fonte: o `Stripe-Signature` e o corpo idêntico chegam junto.

### Sem créditos

O provedor nunca é recusado por falta de saldo. As entregas que seriam cobradas ficam com status `blocked_no_credits` (contadas em `blocked` na resposta ao provedor) e são cobradas e liberadas automaticamente assim que a conta tiver créditos. Se o saldo não voltar em <Limit name="webhookBlockedRetention" />, elas terminam como `failed`.

## Canvas do dashboard

No dashboard, cada projeto aparece como um canvas com as fontes, os endpoints e as rotas entre eles. Estas rotas alimentam essa tela; numa integração pela API, as listagens acima costumam bastar. As três aceitam `?project_id=` (padrão: o projeto padrão).

### Consultar mapa do fluxo

<Endpoint id="webhooks.graph" example={{ query: { project_id: '3e7c1a9f-5b2d-4c8e-a6f0-9d1b4e7a2c53' } }}>
  Fontes, endpoints e rotas do projeto com os contadores das últimas 24 horas: por fonte, mensagens recebidas, a última e os tipos de evento recentes; por endpoint e por rota, entregas, falhas e o status da última.
</Endpoint>

### Consultar layout do canvas

<Endpoint id="webhooks.layout.get" />

### Salvar layout do canvas

<Endpoint
  id="webhooks.layout.save"
  example={{ body: `{
"positions": {
  "source:a6d1f8c3-2e9b-4c7a-9f5d-3b0e6a1c8d27": { "x": 0, "y": 160 },
  "endpoint:8d2f4a61-3c7b-4e9a-b1d5-6f0e2a9c7b34": { "x": 520, "y": 200 }
}
}` }}
>
  `positions` guarda as coordenadas de cada cartão: chaves `platform`, `source:<id>` ou `endpoint:<id>`. Cartões sem posição são arrumados automaticamente pelo dashboard.
</Endpoint>

## Alertas

O dono da conta recebe notificações no dashboard e por e-mail. Cada tipo pode ser desligado nas preferências de notificação (categoria webhooks).

| Tipo                        | Quando                                                                                                                                       |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `webhook_endpoint_failing`  | Um endpoint começa a falhar (primeira tentativa com erro depois de estar saudável). Não repete até uma resposta `2xx` encerrar a sequência.  |
| `webhook_endpoint_disabled` | A desativação automática desliga um endpoint que falha há <Limit name="webhookDisableAfter" /> (não quando você o pausa nem por `410 Gone`). |
| `webhook_quota`             | As entregas do mês atingem <Limit name="webhookQuotaAlerts" /> da franquia mensal, uma vez por limiar e por mês.                             |

Os alertas nunca atrasam nem bloqueiam um envio.

## Entrega e retries

O calendário de retries, o que conta como sucesso, `Retry-After`, `410 Gone` e a desativação automática de endpoints que falham por <Limit name="webhookDisableAfter" /> seguidos estão no guia [Webhooks](/webhooks#retries). Retries automáticos não são cobrados de novo; só [Reenviar mensagem](#webhooks-messages-retry) e [Reenviar entregas com falha](#webhooks-endpoints-recover) geram entregas novas.

## API legada da conta

As rotas antigas de webhooks da conta (`/v1/account/webhooks`) estão **obsoletas**: respondem com os headers `Deprecation: true` e `Link` apontando para os projetos do Webhook Relay. Elas continuam funcionando, e os endpoints criados por elas ficam no projeto padrão. Use o Webhook Relay em integrações novas. A rota do secret dos callbacks de jobs não é obsoleta (veja [Secrets e rotação](/webhooks#secrets)).

## Erros

<ErrorTable tool="webhook-relay" />

Erros de validação comuns (`validation.failed`, `not_found`) e de saldo (`billing.insufficient_credits`) estão em [Erros](/erros).

## Limites

| Limite                                     | Valor                                              |
| ------------------------------------------ | -------------------------------------------------- |
| Entregas grátis por mês (por conta)        | <Limit name="webhookFreeDeliveries" />             |
| Tamanho do `payload` / corpo recebido      | <Limit name="webhookPayloadMax" />                 |
| Projetos por conta                         | <Limit name="webhookProjectsMax" />                |
| Endpoints por conta                        | <Limit name="webhookEndpointsMax" />               |
| Headers customizados por endpoint          | <Limit name="webhookCustomHeadersMax" />           |
| Tipos de evento por conta                  | <Limit name="webhookEventTypesMax" />              |
| Mensagens por lote                         | <Limit name="webhookBatchMax" />                   |
| Entregas por recuperação                   | <Limit name="webhookRecoverMax" />                 |
| Fontes por conta                           | <Limit name="webhookSourcesMax" />                 |
| Rotas por fonte                            | <Limit name="webhookRoutesPerSourceMax" />         |
| Tipos de evento por rota                   | <Limit name="webhookRouteEventTypesMax" />         |
| Requisições recebidas por fonte            | <Limit name="webhookIngestPerMinute" /> por minuto |
| Espera por créditos (`blocked_no_credits`) | <Limit name="webhookBlockedRetention" />           |
| Retenção do log de mensagens               | <Limit name="webhookLogRetention" />               |
| Janela da busca no corpo das mensagens     | <Limit name="webhookSearchBodyWindow" />           |

Os limites por conta somam todos os projetos. Os valores são os padrões da plataforma e podem ser ajustados por ela.
