Webhooks
Notificações entre sistemas por HTTP, com assinatura, novas tentativas e entrega duplicada.
Os receptores desta página foram executados nas cinco linguagens contra as mesmas entregas, assinadas pela biblioteca oficial do Standard Webhooks para Python. O emissor foi executado com um Postgres e destinos de teste.
O receptor, em cada linguagem
Cada versão faz o mesmo, na mesma ordem:
- Lê o corpo bruto, sem convertê-lo em objeto.
- Recusa com 400 a entrega sem os três cabeçalhos.
- Recusa com 401 o carimbo com mais de 5 minutos de diferença ou a assinatura que não confere, comparando em tempo constante e aceitando qualquer uma das assinaturas do cabeçalho (para a troca de segredo).
- Responde 204 a um identificador repetido, sem processar de novo.
- Guarda o aviso e responde 204 logo em seguida.
import crypto from "node:crypto";
import http from "node:http";
// Segredo no formato do Standard Webhooks: "whsec_" seguido da chave em base64
const chave = Buffer.from(process.env.WEBHOOK_SECRET!.replace(/^whsec_/, ""), "base64");
const TOLERANCIA_S = 5 * 60;
const recebidos = new Set<string>(); // em produção, a tabela mostrada mais abaixo
function assinaturaValida(id: string, ts: string, corpo: Buffer, cabecalho: string): boolean {
const conteudo = Buffer.concat([Buffer.from(`${id}.${ts}.`), corpo]);
const esperada = crypto.createHmac("sha256", chave).update(conteudo).digest();
// O cabeçalho pode trazer várias assinaturas ("v1,abc v1,def") durante a troca de segredo
return cabecalho.split(" ").some((item) => {
const [versao, valor] = item.split(",");
const recebida = Buffer.from(valor ?? "", "base64");
return versao === "v1"
&& recebida.length === esperada.length
&& crypto.timingSafeEqual(recebida, esperada); // tempo constante
});
}
http.createServer(async (req, res) => {
if (req.method !== "POST" || req.url !== "/webhooks") {
res.statusCode = 404;
return res.end();
}
// O corpo bruto, exatamente como chegou: nada de JSON.parse antes de conferir
const partes: Buffer[] = [];
for await (const parte of req) partes.push(parte);
const corpo = Buffer.concat(partes);
const id = req.headers["webhook-id"] as string | undefined;
const ts = req.headers["webhook-timestamp"] as string | undefined;
const assinatura = req.headers["webhook-signature"] as string | undefined;
if (!id || !ts || !assinatura) {
res.statusCode = 400;
return res.end();
}
const idade = Math.abs(Date.now() / 1000 - Number(ts));
if (!(idade <= TOLERANCIA_S) || !assinaturaValida(id, ts, corpo, assinatura)) {
res.statusCode = 401;
return res.end();
}
if (recebidos.has(id)) {
res.statusCode = 204; // repetido: confirma de novo, sem processar
return res.end();
}
recebidos.add(id);
const evento = JSON.parse(corpo.toString("utf8")); // só agora, depois de conferido
console.log("guardado para processar:", id, evento.type);
res.statusCode = 204; // responde logo; o processamento fica para depois
res.end();
}).listen(3000);import base64
import hashlib
import hmac
import json
import os
import time
from fastapi import FastAPI, Request, Response
# Segredo no formato do Standard Webhooks: "whsec_" seguido da chave em base64
CHAVE = base64.b64decode(os.environ["WEBHOOK_SECRET"].removeprefix("whsec_"))
TOLERANCIA_S = 5 * 60
recebidos: set[str] = set() # em produção, a tabela mostrada mais abaixo
app = FastAPI()
def assinatura_valida(id_: str, ts: str, corpo: bytes, cabecalho: str) -> bool:
conteudo = f"{id_}.{ts}.".encode() + corpo
esperada = base64.b64encode(hmac.new(CHAVE, conteudo, hashlib.sha256).digest()).decode()
# O cabeçalho pode trazer várias assinaturas ("v1,abc v1,def") durante a troca de segredo
for item in cabecalho.split(" "):
versao, _, valor = item.partition(",")
if versao == "v1" and hmac.compare_digest(valor.encode(), esperada.encode()):
return True
return False
@app.post("/webhooks")
async def webhooks(request: Request):
# O corpo bruto, exatamente como chegou: nada de request.json() antes de conferir
corpo = await request.body()
id_ = request.headers.get("webhook-id")
ts = request.headers.get("webhook-timestamp")
assinatura = request.headers.get("webhook-signature")
if not id_ or not ts or not assinatura:
return Response(status_code=400)
try:
idade = abs(time.time() - int(ts))
except ValueError:
return Response(status_code=401)
if idade > TOLERANCIA_S or not assinatura_valida(id_, ts, corpo, assinatura):
return Response(status_code=401)
if id_ in recebidos:
return Response(status_code=204) # repetido: confirma de novo, sem processar
recebidos.add(id_)
evento = json.loads(corpo) # só agora, depois de conferido
print("guardado para processar:", id_, evento["type"])
return Response(status_code=204) # responde logo; o processamento fica para depoispackage main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"io"
"log"
"math"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"
)
const toleranciaS = 5 * 60
var (
// Segredo no formato do Standard Webhooks: "whsec_" seguido da chave em base64
segredo = strings.TrimPrefix(os.Getenv("WEBHOOK_SECRET"), "whsec_")
chave, _ = base64.StdEncoding.DecodeString(segredo)
mu sync.Mutex
recebidos = map[string]bool{} // em produção, a tabela mostrada mais abaixo
)
func assinaturaValida(id, ts string, corpo []byte, cabecalho string) bool {
mac := hmac.New(sha256.New, chave)
mac.Write([]byte(id + "." + ts + "."))
mac.Write(corpo)
esperada := mac.Sum(nil)
// O cabeçalho pode trazer várias assinaturas ("v1,abc v1,def") durante a troca de segredo
for _, item := range strings.Split(cabecalho, " ") {
versao, valor, _ := strings.Cut(item, ",")
recebida, err := base64.StdEncoding.DecodeString(valor)
if versao == "v1" && err == nil && hmac.Equal(recebida, esperada) { // tempo constante
return true
}
}
return false
}
func webhooks(w http.ResponseWriter, r *http.Request) {
// O corpo bruto, exatamente como chegou: nada de json.Unmarshal antes de conferir
corpo, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20))
if err != nil {
w.WriteHeader(http.StatusRequestEntityTooLarge)
return
}
id := r.Header.Get("webhook-id")
ts := r.Header.Get("webhook-timestamp")
assinatura := r.Header.Get("webhook-signature")
if id == "" || ts == "" || assinatura == "" {
w.WriteHeader(http.StatusBadRequest)
return
}
segundos, err := strconv.ParseInt(ts, 10, 64)
idade := math.Abs(float64(time.Now().Unix() - segundos))
if err != nil || idade > toleranciaS || !assinaturaValida(id, ts, corpo, assinatura) {
w.WriteHeader(http.StatusUnauthorized)
return
}
mu.Lock()
repetido := recebidos[id]
recebidos[id] = true
mu.Unlock()
if repetido {
w.WriteHeader(http.StatusNoContent) // repetido: confirma de novo, sem processar
return
}
var evento struct{ Type string } // só agora, depois de conferido
json.Unmarshal(corpo, &evento)
log.Println("guardado para processar:", id, evento.Type)
w.WriteHeader(http.StatusNoContent) // responde logo; o processamento fica para depois
}
func main() {
http.HandleFunc("POST /webhooks", webhooks)
log.Fatal(http.ListenAndServe(":3000", nil))
}import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class Receptor {
// Segredo no formato do Standard Webhooks: "whsec_" seguido da chave em base64
static final byte[] CHAVE = Base64.getDecoder().decode(
System.getenv("WEBHOOK_SECRET").replaceFirst("^whsec_", ""));
static final long TOLERANCIA_S = 5 * 60;
static final Set<String> recebidos = ConcurrentHashMap.newKeySet(); // em produção, a tabela
static boolean assinaturaValida(String id, String ts, byte[] corpo, String cabecalho)
throws Exception {
var mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(CHAVE, "HmacSHA256"));
mac.update((id + "." + ts + ".").getBytes(StandardCharsets.UTF_8));
byte[] esperada = mac.doFinal(corpo);
// O cabeçalho pode trazer várias assinaturas ("v1,abc v1,def") durante a troca de segredo
for (var item : cabecalho.split(" ")) {
var partes = item.split(",", 2);
if (partes.length == 2 && partes[0].equals("v1")) {
try {
byte[] recebida = Base64.getDecoder().decode(partes[1]);
if (MessageDigest.isEqual(recebida, esperada)) return true; // tempo constante
} catch (IllegalArgumentException base64Invalido) {
// ignora este item
}
}
}
return false;
}
static void webhooks(HttpExchange ex) throws IOException {
// O corpo bruto, exatamente como chegou: nada de converter em objeto antes de conferir
byte[] corpo = ex.getRequestBody().readAllBytes();
var cabecalhos = ex.getRequestHeaders();
var id = cabecalhos.getFirst("webhook-id");
var ts = cabecalhos.getFirst("webhook-timestamp");
var assinatura = cabecalhos.getFirst("webhook-signature");
if (id == null || ts == null || assinatura == null) {
ex.sendResponseHeaders(400, -1);
return;
}
try {
long idade = Math.abs(System.currentTimeMillis() / 1000 - Long.parseLong(ts));
if (idade > TOLERANCIA_S || !assinaturaValida(id, ts, corpo, assinatura)) {
ex.sendResponseHeaders(401, -1);
return;
}
} catch (Exception invalido) {
ex.sendResponseHeaders(401, -1);
return;
}
if (!recebidos.add(id)) {
ex.sendResponseHeaders(204, -1); // repetido: confirma de novo, sem processar
return;
}
System.out.println("guardado para processar: " + id); // converter o JSON só agora
ex.sendResponseHeaders(204, -1); // responde logo; o processamento fica para depois
}
public static void main(String[] args) throws IOException {
var servidor = HttpServer.create(new InetSocketAddress(3000), 0);
servidor.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
servidor.createContext("/webhooks", ex -> {
if (!ex.getRequestMethod().equals("POST")) ex.sendResponseHeaders(405, -1);
else webhooks(ex);
ex.close();
});
servidor.start();
}
}using System.Collections.Concurrent;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
// Segredo no formato do Standard Webhooks: "whsec_" seguido da chave em base64
var segredo = Environment.GetEnvironmentVariable("WEBHOOK_SECRET")!;
var chave = Convert.FromBase64String(segredo.StartsWith("whsec_") ? segredo[6..] : segredo);
const long ToleranciaS = 5 * 60;
var recebidos = new ConcurrentDictionary<string, bool>(); // em produção, a tabela mostrada abaixo
bool AssinaturaValida(string id, string ts, byte[] corpo, string cabecalho)
{
var conteudo = Encoding.UTF8.GetBytes($"{id}.{ts}.").Concat(corpo).ToArray();
var esperada = HMACSHA256.HashData(chave, conteudo);
// O cabeçalho pode trazer várias assinaturas ("v1,abc v1,def") durante a troca de segredo
foreach (var item in cabecalho.Split(' '))
{
var partes = item.Split(',', 2);
if (partes.Length != 2 || partes[0] != "v1") continue;
var recebida = new byte[64];
if (Convert.TryFromBase64String(partes[1], recebida, out var tamanho)
&& CryptographicOperations.FixedTimeEquals(recebida.AsSpan(0, tamanho), esperada))
return true;
}
return false;
}
var app = WebApplication.Create(args);
app.MapPost("/webhooks", async (HttpRequest req) =>
{
// O corpo bruto, exatamente como chegou: nada de desserializar antes de conferir
using var memoria = new MemoryStream();
await req.Body.CopyToAsync(memoria);
var corpo = memoria.ToArray();
string? id = req.Headers["webhook-id"], ts = req.Headers["webhook-timestamp"];
string? assinatura = req.Headers["webhook-signature"];
if (id is null || ts is null || assinatura is null) return Results.BadRequest();
if (!long.TryParse(ts, out var segundos)
|| Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - segundos) > ToleranciaS
|| !AssinaturaValida(id, ts, corpo, assinatura))
return Results.Unauthorized();
if (!recebidos.TryAdd(id, true)) return Results.NoContent(); // repetido: sem processar
var evento = JsonDocument.Parse(corpo); // só agora, depois de conferido
Console.WriteLine($"guardado para processar: {id} {evento.RootElement.GetProperty("type")}");
return Results.NoContent(); // responde logo; o processamento fica para depois
});
app.Run("http://0.0.0.0:3000");O conjunto de identificadores em memória serve para o exemplo rodar sozinho. Ele se perde quando o processo reinicia e não é compartilhado entre instâncias; em produção, use a tabela da seção seguinte.
O resultado, nas cinco linguagens
Cada receptor recebeu as mesmas nove entregas. As assinaturas foram geradas pela biblioteca standardwebhooks, o que confirma que os receptores são compatíveis com o padrão:
| Entrega | Esperado | Resultado nas cinco |
|---|---|---|
| Válida | 204, processada | 204, processada |
| A mesma entrega de novo | 204, sem processar | 204, sem processar |
| Assinatura de outro conteúdo | 401 | 401 |
Corpo reserializado (json.dumps(json.loads(...))), com a assinatura original | 401 | 401 |
| Assinada corretamente, mas com carimbo de 10 minutos atrás | 401 | 401 |
| Assinada só com um segredo antigo | 401 | 401 |
| Assinada com o antigo e o novo, no mesmo cabeçalho | 204 | 204 |
| Sem os cabeçalhos | 400 | 400 |
| Assinatura com caractere fora do ASCII | 401 | 401 |
Cada receptor processou exatamente dois avisos, um para cada identificador válido.
O que muda de uma linguagem para outra
| Corpo bruto | HMAC | Comparação em tempo constante | |
|---|---|---|---|
| TypeScript | Juntar os pedaços do req num Buffer | crypto.createHmac | crypto.timingSafeEqual, que exige tamanhos iguais |
| Python | await request.body() | hmac.new | hmac.compare_digest, com bytes |
| Go | io.ReadAll, com http.MaxBytesReader para limitar o tamanho | crypto/hmac | hmac.Equal |
| Java | getRequestBody().readAllBytes() | javax.crypto.Mac | MessageDigest.isEqual |
| C# | Copiar Request.Body para um MemoryStream | HMACSHA256.HashData | CryptographicOperations.FixedTimeEquals |
Com frameworks, o cuidado é o mesmo: no Express, express.raw({ type: "application/json" }) na rota do webhook, em vez de express.json(); no Spring, receber o corpo como byte[] ou String, e não como objeto. A versão em Go é a única que limita o tamanho do corpo; nas outras, configure o limite no framework ou no proxy reverso.
Gravar antes de responder
No lugar do conjunto em memória, uma tabela com o identificador como chave primária. A rota grava e responde; um trabalhador processa:
CREATE TABLE webhooks_recebidos (
id text PRIMARY KEY, -- o webhook-id
corpo text NOT NULL,
recebido_em timestamptz NOT NULL DEFAULT now(),
processado_em timestamptz
);
-- Na rota, depois de conferir a assinatura (um repetido não insere nada)
INSERT INTO webhooks_recebidos (id, corpo) VALUES ($1, $2)
ON CONFLICT (id) DO NOTHING;
-- No trabalhador, numa transação: pegar, processar, marcar
BEGIN;
SELECT id, corpo FROM webhooks_recebidos
WHERE processado_em IS NULL
ORDER BY recebido_em
LIMIT 1
FOR UPDATE SKIP LOCKED; -- vários trabalhadores não pegam o mesmo aviso
-- ... processa o aviso ...
UPDATE webhooks_recebidos SET processado_em = now() WHERE id = $1;
COMMIT;No teste do nível Conceito, este desenho respondeu em 66 ms à primeira tentativa, e o aviso foi processado uma única vez. Se o trabalhador cair no meio, a transação é desfeita e o aviso volta a ficar pendente. Por isso o processamento também precisa ser idempotente, como mostra o tópico de idempotência.
O lado de quem envia
Se a sua aplicação oferece webhooks, a entrega começa numa tabela gravada na mesma transação que gerou o evento:
CREATE TABLE webhook_destinos (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
url text NOT NULL,
segredo text NOT NULL
);
-- Uma linha por entrega, gravada na mesma transação da mudança que gerou o evento
CREATE TABLE webhook_entregas (
id text PRIMARY KEY, -- o webhook-id, igual em todas as tentativas
destino_id bigint NOT NULL REFERENCES webhook_destinos (id),
corpo text NOT NULL,
situacao text NOT NULL DEFAULT 'pendente', -- pendente, entregue, desistiu
tentativas int NOT NULL DEFAULT 0,
proxima_tentativa_em timestamptz NOT NULL DEFAULT now(),
criado_em timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX ON webhook_entregas (proxima_tentativa_em) WHERE situacao = 'pendente';Um trabalhador lê as entregas vencidas, assina, envia e agenda a próxima tentativa:
import crypto from "node:crypto";
import dns from "node:dns/promises";
import net from "node:net";
import pg from "pg";
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
const espera = (ms: number) => new Promise((r) => setTimeout(r, ms));
// Faixas internas: o emissor nunca entrega para elas (proteção contra SSRF)
const internas = new net.BlockList();
const faixas = [
["10.0.0.0", 8, "ipv4"], ["172.16.0.0", 12, "ipv4"], ["192.168.0.0", 16, "ipv4"],
["127.0.0.0", 8, "ipv4"], ["169.254.0.0", 16, "ipv4"], ["100.64.0.0", 10, "ipv4"],
["0.0.0.0", 8, "ipv4"], ["::1", 128, "ipv6"], ["fc00::", 7, "ipv6"], ["fe80::", 10, "ipv6"],
] as const;
for (const [rede, bits, tipo] of faixas) internas.addSubnet(rede, bits, tipo);
async function destinoPermitido(url: URL): Promise<boolean> {
const nome = url.hostname.replace(/^\[|\]$/g, ""); // IPv6 literal vem entre colchetes
const enderecos = await dns.lookup(nome, { all: true });
return enderecos.every((e) => !internas.check(e.address, e.family === 6 ? "ipv6" : "ipv4"));
}
function assinar(segredo: string, id: string, ts: number, corpo: string): string {
const chave = Buffer.from(segredo.replace(/^whsec_/, ""), "base64");
return "v1," + crypto.createHmac("sha256", chave).update(`${id}.${ts}.${corpo}`).digest("base64");
}
// Intervalo crescente com variação aleatória: 30 s, 2 min, 8 min, 32 min... até 6 h
const proximoIntervaloS = (tentativas: number) =>
Math.min(30 * 4 ** (tentativas - 1), 6 * 3600) * (0.5 + Math.random());
async function entregar(e: { id: string; url: string; segredo: string; corpo: string }) {
const url = new URL(e.url);
if (url.protocol !== "https:" && process.env.NODE_ENV === "production") return "recusado";
try {
if (!(await destinoPermitido(url))) return "recusado";
const ts = Math.floor(Date.now() / 1000);
const resposta = await fetch(url, {
method: "POST",
body: e.corpo,
redirect: "manual", // um redirecionamento poderia apontar para a rede interna
signal: AbortSignal.timeout(10_000),
headers: {
"content-type": "application/json",
"webhook-id": e.id,
"webhook-timestamp": String(ts),
"webhook-signature": assinar(e.segredo, e.id, ts, e.corpo),
},
});
return resposta.ok ? "entregue" : "falhou"; // 3xx, 4xx e 5xx contam como falha
} catch {
return "falhou"; // tempo esgotado, conexão recusada, DNS
}
}
// Trabalhador: pega as entregas vencidas; várias cópias podem rodar ao mesmo tempo
for (;;) {
const cliente = await pool.connect();
try {
await cliente.query("BEGIN");
const { rows } = await cliente.query(
`SELECT e.id, e.corpo, e.tentativas, d.url, d.segredo
FROM webhook_entregas e JOIN webhook_destinos d ON d.id = e.destino_id
WHERE e.situacao = 'pendente' AND e.proxima_tentativa_em <= now()
ORDER BY e.proxima_tentativa_em LIMIT 10
FOR UPDATE OF e SKIP LOCKED`,
);
for (const e of rows) {
const resultado = await entregar(e);
const tentativas = e.tentativas + 1;
const situacao = resultado === "entregue" ? "entregue"
: resultado === "recusado" || tentativas >= 10 ? "desistiu" : "pendente";
await cliente.query(
`UPDATE webhook_entregas SET tentativas = $2, situacao = $3,
proxima_tentativa_em = now() + make_interval(secs => $4) WHERE id = $1`,
[e.id, tentativas, situacao, proximoIntervaloS(tentativas)],
);
}
await cliente.query("COMMIT");
if (rows.length === 0) await espera(1000);
} catch (erro) {
await cliente.query("ROLLBACK");
console.error(erro);
await espera(1000);
} finally {
cliente.release();
}
}O teste usou uma cópia com os intervalos encurtados (1 segundo no lugar de 30) e com a faixa 172.16.0.0/12 liberada, porque os destinos de teste estavam numa rede Docker:
| Destino | Resultado |
|---|---|
| Responde 503 nas duas primeiras tentativas do aviso "pago" | Entregue na terceira; o aviso "enviado", criado depois, chegou antes |
http://169.254.169.254/latest/meta-data/ | Recusado sem nenhum pedido; desistiu na primeira |
http://[::ffff:127.0.0.1]:3000/ (IPv4 escrito como IPv6) | Recusado sem nenhum pedido |
Responde com redirecionamento para 127.0.0.1 | Cada tentativa contou como falha, e o redirecionamento não foi seguido |
O filtro também recusou localhost, http://2130706433/ e http://0x7f.1/, duas formas de escrever 127.0.0.1 que o URL converte sozinho.
A conferência de endereços acontece antes de cada entrega, mas entre ela e a conexão o nome ainda pode mudar de endereço (DNS rebinding). Para fechar essa brecha, conecte no endereço conferido ou faça as entregas por um proxy de saída que aplica o filtro, numa rede sem rota para os serviços internos.
Testar localmente
O emissor precisa alcançar a sua máquina, que normalmente não tem endereço público. Há três caminhos:
- A ferramenta do emissor, quando existe: a CLI da Stripe, por exemplo, recebe os eventos e os repassa para
localhost(stripe listen --forward-to localhost:3000/webhooks). - Um túnel que dá um endereço público temporário à sua máquina.
- Entregas assinadas por um script, como as do teste desta página, usando a biblioteca do padrão com o mesmo segredo. É a forma de cobrir os casos de erro: assinatura errada, carimbo velho, repetidos.
Lista de revisão
Receber
- A assinatura é conferida sobre o corpo bruto, antes de qualquer conversão?
- A comparação é feita em tempo constante, e um cabeçalho malformado gera 401, e não 500?
- O carimbo de tempo é conferido, com tolerância de poucos minutos?
- O cabeçalho aceita várias assinaturas, para a troca de segredo?
- O tamanho do corpo é limitado?
Processar
- O aviso é gravado de forma durável antes da resposta 2xx?
- O identificador da entrega é uma chave única, e um repetido recebe 2xx sem processar de novo?
- O processamento é feito fora da rota, por um trabalhador que tenta de novo?
- Avisos fora de ordem não sobrescrevem um estado mais novo?
- Existe uma conciliação periódica com a API do emissor?
Enviar
- A entrega é gravada na mesma transação da mudança que gerou o evento?
- As novas tentativas têm intervalos crescentes, com variação aleatória, e um limite?
- O
webhook-idé o mesmo em todas as tentativas? - Endereços internos são recusados antes de cada entrega, e redirecionamentos não são seguidos?
- O cliente vê o registro das entregas, pode pedir o reenvio e trocar o segredo?
Prompts para aprofundar
Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.
Implementar um receptor de webhooks
Quero implementar a rota que recebe webhooks na minha aplicação. Pergunte qual linguagem e framework eu uso e qual é o provedor. Vamos em etapas: 1. Ler o corpo bruto na rota, sem a conversão automática do framework. 2. Conferir a assinatura e o carimbo de tempo, com comparação em tempo constante. 3. Gravar o aviso numa tabela com o identificador como chave única, e responder 2xx. 4. Um trabalhador que processa os avisos pendentes, com FOR UPDATE SKIP LOCKED ou uma fila. 5. Testes que enviam entregas válidas, repetidas, com assinatura errada, com carimbo antigo e com o corpo alterado. Em cada etapa, me passe os comandos para testar localmente.
Implementar o envio de webhooks
Quero implementar o envio de webhooks a partir da minha aplicação. Pergunte qual linguagem e banco eu uso. Explique, com código: 1. A tabela de entregas, gravada na mesma transação da mudança que gera o evento. 2. O trabalhador que pega as entregas vencidas com FOR UPDATE SKIP LOCKED. 3. A assinatura no formato do Standard Webhooks. 4. O tempo limite, as novas tentativas com intervalo crescente e variação aleatória, e o limite de tentativas. 5. O filtro de endereços internos antes de cada entrega e a recusa de redirecionamentos. 6. Testes com um destino que falha, um que redireciona e um endereço interno.
Revisar uma integração de webhooks
Vou colar abaixo o código de uma integração de webhooks: a rota que recebe, o código que envia, ou os dois. Revise para produção. Verifique, e para cada item diga se está correto, incorreto ou ausente, citando o trecho: 1. A assinatura é conferida sobre o corpo bruto, em tempo constante? 2. O carimbo de tempo é conferido? 3. Um cabeçalho malformado gera 401, e não um erro 500? 4. O aviso é gravado antes da resposta, e o processamento acontece fora da rota? 5. Entregas repetidas são reconhecidas pelo identificador? 6. Avisos fora de ordem podem sobrescrever um estado mais novo? 7. No envio: fila de saída na mesma transação, novas tentativas espaçadas, filtro de endereços internos, redirecionamentos recusados? Para cada problema, descreva como ele aparece em produção e mostre a correção. Código: [cole aqui]